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

6.3 KiB

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):

"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:

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.