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>
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 byvalidateBinding, 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:
- Fill the gaps, don't re-research the grounded. Research targets slots bound
ekstern(and any stillunresolvedat entry) — the claims that explicitly need an external source. Slots bound to aspecificare already grounded in lived material and do not need their substance re-sourced (research may still corroborate, but it is not the gap). - Verify KTG's own numbers (regel 6/7). Every
unverified-numberwarning fromvalidateBindingis a research target: confirm the operator's own figure against a primary source where possible, then flip the bank specificunverified → verified(cli.ts add/a verify path). A number that cannot be verified staysunverifiedand is carried to the Step 5 fact-check sweep as 🟡 — never silently asserted. - Re-render after research. When research resolves an
eksternsource or a number flips to verified, regenerateNN-kilder.mdso 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.