feat(linkedin-studio): N14 — foldIns-fangst + Step 11 retro + background-headless + språkregel-akkumulering [skip-docs]

Lukker sløyfen pluginen manglet: en rettelse operatøren gjør i utgave N
håndheves i N+1 i stedet for å bli gjenoppdaget. Maskineriet fantes
(foldIns-skjema + promote→ratify siden fix #1); det som manglet var wiring —
ingenting skrev til køen og ingen fase tømte den.

- Fangst (A2-F7): Steps 2.5/3a/5.5/6.5 appender rettelsen ordrett med trigger,
  decision "pending". Ubetinget og bevisst dum — å avgjøre ved fangst om noe
  "fortjener" en regel er nettopp slik køen holder seg tom og sløyfen dør.
- Step 11 retro (A2-F8), ≤5 min ETTER scheduling: promoter køen med eksplisitt
  JA/NEI (mekanisk → atomisk contract-gate-promotering, teller kun med grønn
  --ratify; dømmekraft → operatørens språkregelfil; NEI → rejected, beholdes),
  effort-oppsummering fra MÅLT phaseLog (aldri re-estimert), og ÉN
  friksjons-spørring som besvares tilbake til operatøren. Faser 18 → 19;
  resumption-tabellen ruter scheduling → Step 11, retro → complete.
- articles.NN.retro: additiv-valgfri (default null), schemaVersion forblir 1.
- Background-headless (A2-F6): --background kjører pakken i en bakgrunnsagent
  som skriver rapporten til disk; drafting-sesjonen leser fila. Samme isolasjon
  som fersk sesjon, uten copy-paste-sømmen. Inline fan-out = eksplisitt fallback.
- Språkregler (C-10): ${DATA}/language-rules/<lang>.md (opt-in, template).
  Leses TO ganger — av language-reviewer (fanger) og av Step 4 (forebygger).
  Shippet banliste forblir baseline; brukerfila utvider.
- references/fold-in-loop.md (A2-F9): loopen dokumentert domene-generelt in-tree,
  så en adopter uten ekstern skrivekontrakt har hele sløyfen. Refs 28 → 29.

Suiter (alle grønne): test-runner 197 → 217 (Section 16u: 18 ubetingede greps +
non-vacuity self-test; fase-sveip 16 → 17 faser; floor 179 → 198) · hooks 174 ·
trends 300 · brain 134 · editions 72 · specifics-bank 45 · contract-gate 33 ·
tests 35 · render 60.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 12:39:16 +02:00
commit f0532dce3f
9 changed files with 711 additions and 30 deletions

View file

@ -13,9 +13,10 @@ Full-spectrum LinkedIn content engine — short-form feed posts, carousels, vide
- **Analytics:** CLI `scripts/analytics/` (TypeScript, needs `tsx` + `npm install`); data `assets/analytics/` (gitignored)
- **Specifics-bank:** `scripts/specifics-bank/` (TypeScript, needs `tsx` + `npm install`) — deterministic, topic-tagged store of the operator's lived specifics (real numbers, named cases, held opinions) in the per-user data dir; elicited/bound by `/linkedin:newsletter` Step 1.5 so drafts draw from real inventory, never invented filler. Consumption is logged back at Step 8 lock (`record-usage``usedIn`), so cross-edition re-use of the same material is visible rather than silently encouraged by the dedupe
- **Series distillate:** `scripts/editions/` (TypeScript, needs `tsx` + `npm install`) — series-level memory: each locked edition's spent narrative units (anecdotes/arguments/hooks) in `<serie>/linkedin/series-distillate.json`; written at `/linkedin:newsletter` Step 8 (`distil-append`), checked against the next skeleton at Step 2.5 (`distil-check`, advisory). Deterministic character-trigram similarity — AI extracts, code compares
- **Editions register:** `scripts/editions/` (same package) — one row per edition in production in the per-user data dir (`editions/register.json`): series, edition, path, current phase, next action, slot, `startedAt`/`completedAt`. Each of the 16 canonical `/linkedin:newsletter` phase transitions runs `register-upsert --edition-state <path>`, which appends `articles.NN.phaseLog` (lead-time telemetry, additive — schemaVersion stays 1) **and** mirrors the row in one call; Step 10 runs `register-complete`. `register-list` is the work-in-progress view. The register is a **mirror** — deterministic resumption reads `edition-state.json` only, and a lost register is rebuilt by the next transition
- **Editions register:** `scripts/editions/` (same package) — one row per edition in production in the per-user data dir (`editions/register.json`): series, edition, path, current phase, next action, slot, `startedAt`/`completedAt`. Each of the 17 canonical `/linkedin:newsletter` phase transitions runs `register-upsert --edition-state <path>`, which appends `articles.NN.phaseLog` (lead-time telemetry, additive — schemaVersion stays 1) **and** mirrors the row in one call; Step 10 runs `register-complete`. `register-list` is the work-in-progress view. The register is a **mirror** — deterministic resumption reads `edition-state.json` only, and a lost register is rebuilt by the next transition
- **Publishing slots:** `hooks/scripts/slots.mjs` — the weekly production grid. Slots are **operator config** in the per-user data dir (`profile/publishing-slots.json`, schema + opt-in template in `config/publishing-slots.template.json`); absent config ⇒ every slot surface stays silent, and the plugin hardcodes no publishing times. Coverage of a slot counts the short-form queue (`scheduled`/`published`) **and** editions-register rows claiming that date, so long-form and short-form cannot be double-booked. Zero-dep (SessionStart may not spawn tsx) and imported identically by the hook and by the commands (`queue-manager.mjs` pattern), so session-start's vacancy warning and `/linkedin:newsletter` Step 10's slot default are the same computation
- **Contract-gate:** `scripts/contract-gate/` (TypeScript, needs `tsx` + `npm install`) — deterministic §B/§C1 rule-gate on the full draft (`/linkedin:newsletter` Step 4.5, before the AI sweeps); ratifies `rules.ts` against the edition's §E-manifest, then gates with BLOCK/WARN
- **Fold-in loop:** capture → classify → promote → enforce, documented domain-generally in `references/fold-in-loop.md`. The four `/linkedin:newsletter` steps where operator judgment enters the text (2.5, 3a, 5.5, 6.5) append corrections to `articles.NN.foldIns[]` (`decision: "pending"`); **Step 11 (retro)** empties the queue with an explicit operator JA/NEI — mechanical → an atomic contract-gate promotion (only counts when `--ratify` is green), judgment → the operator's opt-in language-rules file (`${DATA}/language-rules/<lang>.md`, template `config/language-rules.template.md`), which Step 4 reads while drafting (prevent) and `language-reviewer` reads at Step 6.5 (catch). Rejected rows are kept, never deleted. Step 11 also reports effort from the **measured** `phaseLog` and asks one friction question whose answer goes back to the operator — the plugin never writes their register
- **Analytics metrics (S16):** parsed CSV columns + an optional, manually-entered `saves` count (count-only in native LinkedIn analytics since ~Sept 2025, no CSV export; the Marketing API exposes `POST_SAVE` on `/memberCreatorPostAnalytics` from v202604, but access is partner-gated — so manual entry remains the right UX). `parseOptionalCount()`: blank / non-numeric / negative → `undefined` (`unknown`, never 0), a genuine `0` is kept; saves surfaced per-post + as `totalSaves`, but **not** folded into `engagementRate`. `dwell` stays **explicitly unmeasurable** (internal to LinkedIn, no export/API). All analytics I/O routes through the `getAnalyticsRoot()` seam (M0 per-user data-dir).
## Hooks
@ -55,8 +56,8 @@ All content commands auto-copy output to clipboard via `clipboard-helper.mjs`; i
| `/linkedin:quick` | 5-minute quick post (3-line formula) + 8 post-type templates |
| `/linkedin:pipeline` | Full end-to-end content pipeline |
| `/linkedin:trends` | Trend discovery pass — delegates to `trend-spotter` (default long-form mode), persists to trend store + dated morning brief, triage per id (act/skip) |
| `/linkedin:newsletter` | Long-form orchestrator (newsletter / essay / series article) — multi-session 18-phase pipeline with gates before prose (skeleton + spine), fact-check, editorial-review (Step 5.5), persona sweep, headless adversarial review (Step 6.5), and visual-assets — all before lock |
| `/linkedin:headless-review` | Cold adversarial review package — 3 headless archetypes (`content-reviewer`, `language-reviewer`, `fact-reviewer`) + `persona-reviewer` on a FROZEN draft, no drafting-session context; operator-gated report (Step 6.5 standalone surface) |
| `/linkedin:newsletter` | Long-form orchestrator (newsletter / essay / series article) — multi-session 19-phase pipeline with gates before prose (skeleton + spine), fact-check, editorial-review (Step 5.5), persona sweep, headless adversarial review (Step 6.5), and visual-assets — all before lock; closes with the Step 11 retro that promotes this edition's corrections so the next one starts with them enforced |
| `/linkedin:headless-review` | Cold adversarial review package — 3 headless archetypes (`content-reviewer`, `language-reviewer`, `fact-reviewer`) + `persona-reviewer` on a FROZEN draft, no drafting-session context; operator-gated report (Step 6.5 standalone surface). `--background` runs the package in a background agent that writes the report to disk — fresh-session isolation without the paste-back seam |
| `/linkedin:pivot` | Re-open a long-form edition after a late substantive change so cleared gates re-run before lock (pivot heuristic: >20% word-count or >2 new sections) |
| `/linkedin:batch` | Create a full week of content |
| `/linkedin:calendar` | View/manage post scheduling queue + publish action |