linkedin-studio/docs/fix2/slice2-binding.md
Kjell Tore Guttormsen 33f18c35e6 feat(linkedin-studio): per-edition lived-specifics binding + kilder artifact (fix #2 slice 2)
Slice 2 of fix #2 (kilde-så-draft): the per-edition bridge between the global
specifics-bank (slice 1) and a draft. In scripts/specifics-bank:

- binding.ts — livedSpecifics slot-map model + validateBinding, the «vaghet
  avvises» gate (G4). BLOCK on unresolved / dangling specific-id / unjustified
  abstrakt; WARN on an unverified-number-backed slot (regel 6/7); PASS otherwise.
- kilder.ts — renderKilder, the read-only NN-kilder.md sources ledger, rendered
  deterministically from the slot-map + bank (coverage via validateBinding, so
  the artifact and the skeleton-gate cannot disagree).
- cli.ts — validate-binding (exit 1 = BLOCK) + render-kilder subcommands.

edition-state schema gains articles.NN.livedSpecifics ({ slots, status }) + _doc.
Design record + the Step 2 research re-scope spec: docs/fix2/slice2-binding.md.

Wiring into newsletter.md (Step 1.5 elicitation + Step 2.5 gate + pipeline 17->18)
is slice 3 — not in this commit.

Tests 28/28 (specifics-bank; +9 binding +4 kilder), tsc clean, plugin gate 83/0/0
unaffected, gitleaks clean on all 8 files.

[skip-docs] internal plumbing — no command/agent/pipeline surface change until
slice 3 wires it; documenting it in the user-facing root CLAUDE.md/README now would
overclaim unreachable functionality. Docs ARE updated where they belong: the design
record docs/fix2/slice2-binding.md, scripts/specifics-bank/README.md, and the
edition-state _doc. Root-doc update lands with the slice-3 wiring feat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:33:44 +02:00

118 lines
6.3 KiB
Markdown

# Fix #2 slice 2 — per-edition lived-specifics binding
> **Kilde-så-draft.** Slice 1 built the global *store* (`scripts/specifics-bank`).
> Slice 2 builds the per-EDITION *binding*: the bridge that ties one edition's
> load-bearing claims to real material before any prose exists. Slice 3 wires it
> into `/linkedin:newsletter` (Step 1.5 + the skeleton-gate); slice 4 touches the
> maskinrommet contract. This doc is the design record slice 3 implements against.
## Why (retning §3, drømme-spec «Lived-Specifics Extraction» `[CORE]`)
Sourcing, not styling, is where authenticity is won. A draft that starts from the
operator's real number / named case / what-actually-broke is forwardable; a draft
that invents plausible filler is hollow, and every downstream voice gate then
polishes hollow content. So **before prose**, each load-bearing claim of an edition
must be bound to real inventory — or the operator must *consciously* decide it does
not need a concrete instance. The binding is that ledger; vagueness is rejected.
## The slot-map (`articles.NN.livedSpecifics` in `edition-state.json`)
A per-article field, resolved at Step 1.5 (between brief and research). Each slot
is one load-bearing key-point (`kind: "key-point"`) or, after the skeleton exists,
a section (`kind: "section"` — the same shape serves both, no migration):
```jsonc
"livedSpecifics": {
"slots": [
{ "slotId": "kp1", "kind": "key-point", "label": "<claim>",
"binding": { "type": "specific", "specificId": "<bank id>" } },
{ "slotId": "kp2", "kind": "key-point", "label": "<claim>",
"binding": { "type": "abstrakt", "rationale": "<why abstract here>" } },
{ "slotId": "kp3", "kind": "key-point", "label": "<claim>",
"binding": { "type": "ekstern", "source": "<research ref, optional>" } },
{ "slotId": "kp4", "kind": "key-point", "label": "<claim>",
"binding": { "type": "unresolved" } }
],
"status": "pending" // pending → bound (set by the command when the gate passes)
}
```
### The gate — `validateBinding(livedSpecifics, bank)` (drømme-spec G4, «vaghet avvises»)
Deterministic, pure (`scripts/specifics-bank/src/binding.ts`). **BLOCK** iff any slot:
| reason | when |
|--------|------|
| `unresolved` | binding type is `unresolved` — the vagueness the gate exists to reject |
| `dangling-specific` | a `specific` binding points at a `specificId` absent from the bank |
| `unjustified-abstrakt` | an `abstrakt` escape with a blank rationale (= vagueness wearing an escape hatch) |
Otherwise **PASS**. Escape hatches are legitimate: `abstrakt` (with a rationale)
and `ekstern` (research-backed, not lived; `source` optional — Step 2 fills it).
**WARN (does not block)** — regel 6/7: a slot backed by an **unverified `number`**
specific surfaces as `unverified-number`. Numbers stay guilty-until-checked; Step 2
research must verify them before the draft asserts them as fact.
`coverage` counts by binding TYPE (`total / specific / abstrakt / ekstern /
unresolved`); a dangling specific still counts under `specific` (the binding type
is `specific`, the *problem* is separately reported). Empty slot-map → PASS, total
0 — whether an edition *needs* ≥1 slot is the command layer's call (slice 3).
## The `NN-kilder.md` artifact
Read-only, deterministically **rendered** from `livedSpecifics` + the bank
(`scripts/specifics-bank/src/kilder.ts`, `renderKilder`) — regenerated like
POST.html, never hand-edited. Lives at `<serie>/linkedin/NN-kilder.md`. Per
load-bearing slot: binding type, the material (KTG's words, verbatim), provenance
+ capture date, topic tags, and the number-verification marker (`✓` / `⚠`); then a
coverage block + a «Tall som må verifiseres (regel 6/7)» line. The coverage block
and the number list are computed by `validateBinding`, so the artifact and the
skeleton-gate can never disagree. (Golden-tested; pure + dateless on purpose.)
## CLI surface (the seam slice 3 calls)
Run from the package dir (`tsx` lives there); pass absolute paths:
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank"
node --import tsx src/cli.ts validate-binding --edition "<abs>/linkedin/edition-state.json" # exit 1 = BLOCK
node --import tsx src/cli.ts render-kilder --edition "<abs>/linkedin/edition-state.json" \
--out "<abs>/linkedin/NN-kilder.md"
```
`--article <id>` overrides `currentArticle`; `--bank <path>` overrides the default
data-dir bank; `--json` emits machine output for `validate-binding`. A missing
`livedSpecifics` field is treated as an empty slot-map (back-compat with older
editions / the template default).
## Step 2 (research) re-scope spec — for slice 3 to wire
Today Step 2 decomposes the edition's key points into scoped research sub-questions
and triangulates verified notes. Once Step 1.5 + the slot-map exist, Step 2 is
**scoped by the binding** rather than by the raw key-points:
1. **Fill the gaps, don't re-research the grounded.** Research targets slots bound
`ekstern` (and any still `unresolved` at entry) — the claims that explicitly
need an external source. Slots bound to a `specific` are already grounded in
lived material and do **not** need their substance re-sourced (research may
still corroborate, but it is not the gap).
2. **Verify KTG's own numbers (regel 6/7).** Every `unverified-number` warning from
`validateBinding` is a research target: confirm the operator's own figure
against a primary source where possible, then flip the bank specific
`unverified → verified` (`cli.ts add`/a verify path). A number that cannot be
verified stays `unverified` and is carried to the Step 5 fact-check sweep as 🟡
— never silently asserted.
3. **Re-render after research.** When research resolves an `ekstern` source or a
number flips to verified, regenerate `NN-kilder.md` so the ledger stays current.
Boundary: slice 2 ships the data model, the gate, the artifact renderer, and the
CLI. Slice 3 adds Step 1.5 (the elicitation interview + `add` to the bank +
populating the slot-map), wires `validate-binding` into the Step 2.5 skeleton-gate,
applies this Step 2 re-scope, and bumps the pipeline 17 → 18 phases.
## Tests & verification
`cd scripts/specifics-bank && npm test` (binding + kilder suites) and `npm run
build` (tsc) stay clean. The gate's three BLOCK reasons + the number WARN + the
golden `NN-kilder.md` render are all covered.