feat(linkedin-studio): N11 — serie-destillat + skjelett-sjekk + specifics-bank bruks-logging [skip-docs]

Every long-form gate agent sees ONE edition, so «the reader has heard this
before» was structurally invisible — and the specifics-bank dedupe actively
encouraged re-surfacing the same material. At series cadence that is the
fastest-growing defect class. N11 makes it visible BEFORE the skeleton is
approved, at two grains:

- Series grain — new scripts/editions package: each locked edition's spent
  anecdotes/arguments/hooks are written to <serie>/linkedin/series-distillate.json
  at Step 8 lock (distil-append), and the next skeleton is checked against it at
  Step 2.5 (distil-check) with the finding folded into the annotation gate the
  operator already reads. Advisory, never blocking: a deliberate callback is a
  legitimate move, an unnoticed retread is not.
- Material grain — specifics-bank usedIn log: record-usage stamps «used in
  edition NN» on the specifics an edition actually consumed (read from the bound
  slot-map, so abstrakt/ekstern stamp nothing). At lock, so it means published;
  idempotent under a pivot re-lock. Additive-optional, schema stays v1.

Placement deviates from the plan text (${DATA}) after premise-verification:
per-series state belongs in the series root beside edition-state.json, where
Step 0 already resolves the path and no slug→path map is needed. Operator
approved. The distillate module also lands in scripts/editions now rather than
at N12, since the AC required a testable roundtrip and N12 planned that package
anyway — N12 extends it instead of creating it.

Similarity is character-trigram Jaccard, not word overlap: the plugin is
language-general and inflection (migrere/migreringen) breaks word tokens.
Calibrated on real paraphrase pairs — retellings 0.44-0.55, unrelated 0.04-0.06,
same-topic-different-story 0.24 — so the default threshold sits in the gap at
0.40. Word-Jaccard scored a shortened hook paraphrase at 0.11.

TDD (Iron Law): tests written first and verified red before implementation, for
both the new package and the bank logging.

Suites: editions 27/0 (new suite line + guard, floor 27) · specifics-bank
28 -> 45 · test-runner 163 -> 173 (Section 16r: 9 unconditional greps +
non-vacuity self-test; anti-erosion floor 146 -> 155) · trends 300/0 ·
brain 134/0 · hooks 140/0 · tests 35/0 · render 60/0. tsc --noEmit clean.

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 06:29:01 +02:00
commit b54e450c3e
20 changed files with 2152 additions and 19 deletions

View file

@ -698,7 +698,7 @@ if [ -x "$SB_DIR/node_modules/.bin/tsx" ]; then
SB_OUT=$( set +e; (cd "$SB_DIR" && npm test) 2>&1; echo "SB_EXIT:$?" )
SB_EXIT=$(echo "$SB_OUT" | grep -oE 'SB_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
SB_TESTS=$(echo "$SB_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
SPECIFICS_BANK_TESTS_FLOOR=28
SPECIFICS_BANK_TESTS_FLOOR=45
if [ "$SB_EXIT" = "0" ] && [ -n "$SB_TESTS" ] && [ "$SB_TESTS" -ge "$SPECIFICS_BANK_TESTS_FLOOR" ]; then
pass "specifics-bank suite green: $SB_TESTS tests pass (floor $SPECIFICS_BANK_TESTS_FLOOR)"
else
@ -710,6 +710,30 @@ fi
echo ""
# --- Section 15b: Series-Distillate Suite (N11 / C-5) ---
echo "--- Series Distillate ---"
# The series distillate (scripts/editions) is wired into /linkedin:newsletter as the Step 2.5
# destillat-sjekk and the Step 8 lock write. Same discipline as Section 15: the suite belongs in
# CI (store + similarity + skeleton-check stay green, case count never erodes), and an adopter
# without deps warn-skips rather than failing. Same set +e / subshell discipline (bash 3.2-safe).
ED_DIR="scripts/editions"
if [ -x "$ED_DIR/node_modules/.bin/tsx" ]; then
ED_OUT=$( set +e; (cd "$ED_DIR" && npm test) 2>&1; echo "ED_EXIT:$?" )
ED_EXIT=$(echo "$ED_OUT" | grep -oE 'ED_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
ED_TESTS=$(echo "$ED_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
EDITIONS_TESTS_FLOOR=27
if [ "$ED_EXIT" = "0" ] && [ -n "$ED_TESTS" ] && [ "$ED_TESTS" -ge "$EDITIONS_TESTS_FLOOR" ]; then
pass "editions suite green: $ED_TESTS tests pass (floor $EDITIONS_TESTS_FLOOR)"
else
fail "editions suite NOT green (exit=${ED_EXIT:-?}, tests=${ED_TESTS:-?}, floor $EDITIONS_TESTS_FLOOR) — run: (cd $ED_DIR && npm test)"
fi
else
warn "editions skipped — deps absent ($ED_DIR/node_modules); run: (cd $ED_DIR && npm install)"
fi
echo ""
# --- Section 16: Trends-Store Binding (research-engine slice 2b) ---
echo "--- Trends-Store Binding ---"
@ -1804,6 +1828,106 @@ fi
echo ""
# --- Section 16r: Series-Level Protection - distillate + usage log (N11 / C-5) ---
echo "--- Series-Level Protection (N11) ---"
# N11 makes "the reader has heard this before" visible BEFORE the skeleton is approved.
# Every other gate sees ONE edition; at series cadence the fastest-growing defect is a
# REPEATED edition, and the specifics-bank dedupe actively encourages re-surfacing material.
# Two stores, two grains, both UNCONDITIONAL greps (they raise the deps-absent floor):
# (C-5 series grain) scripts/editions - what THIS SERIES already published (anecdote/argument/hook)
# (C-5 material grain) specifics-bank usedIn - which editions consumed a given lived specific
# (C-5 Step 2.5) the destillat-sjekk feeds the annotation gate BEFORE prose - a compound predicate
# (distil-check call + the series-scoped store + the advisory REUSE verdict), so a
# non-vacuity self-test guards it (mirrors Sections 16p/16q).
# (C-5 Step 8) lock writes both stores - published, never merely considered.
NL_N11="commands/newsletter.md"
DIST_SRC="scripts/editions/src/distillate.ts"
distillate_gated() { # $1 = text; wired iff it CALLS the check, names the series-scoped store AND carries the advisory verdict
echo "$1" | grep -qF "distil-check" \
&& echo "$1" | grep -qF "series-distillate.json" \
&& echo "$1" | grep -qF "REUSE"
}
DG_SELFTEST_OK=1
if ! distillate_gated "run distil-check --distillate <abs>/linkedin/series-distillate.json; on REUSE fold the finding into the annotation page"; then
DG_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired destillat-sjekk probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if distillate_gated "$probe"; then
DG_SELFTEST_OK=0; echo " false-positive FAIL: under-wired destillat probe accepted -> $probe"
fi
done <<'NEGATIVE16R'
run distil-check against the store and act on REUSE without naming the file
the series-distillate.json store exists and REUSE is possible, but nothing calls the check
call distil-check on series-distillate.json but never say what a hit means
NEGATIVE16R
if [ "$DG_SELFTEST_OK" -eq 1 ]; then
pass "destillat-gate self-test: predicate needs distil-check + series-distillate.json + REUSE verdict (1 accepted, 3 under-wired rejected)"
else
fail "destillat-gate self-test failed - the N11 wiring lint is vacuous or over-eager"
fi
# (C-5 Step 2.5) the destillat-sjekk runs in the skeleton gate, BEFORE prose
STEP25_BLOCK=$(awk '/^## Step 2.5:/{f=1} /^## Step 3a:/{f=0} f' "$NL_N11")
if distillate_gated "$STEP25_BLOCK"; then
pass "newsletter Step 2.5 runs the destillat-sjekk against the series store with an advisory REUSE verdict (C-5 Step 2.5)"
else
fail "newsletter Step 2.5 does not run the destillat-sjekk - series-level re-use stays invisible before prose (C-5)"
fi
# (C-5 Step 8 distillate) lock folds the spent units into the series distillate
STEP8_BLOCK=$(awk '/^## Step 8:/{f=1} /^## Step 9:/{f=0} f' "$NL_N11")
if echo "$STEP8_BLOCK" | grep -qF "distil-append"; then
pass "newsletter Step 8 lock writes the series distillate (distil-append) (C-5 Step 8)"
else
fail "newsletter Step 8 never calls distil-append - nothing feeds the next edition's Step 2.5 check (C-5)"
fi
# (C-5 Step 8 usage) lock stamps the consumed lived material
if echo "$STEP8_BLOCK" | grep -qF "record-usage"; then
pass "newsletter Step 8 lock stamps consumed specifics (record-usage) (C-5 Step 8)"
else
fail "newsletter Step 8 never calls record-usage - bank re-use across editions stays invisible (C-5)"
fi
# (C-5 material grain) the bank record carries the usage log
if grep -qF "usedIn" scripts/specifics-bank/src/types.ts; then
pass "specifics-bank Specific declares usedIn - the cross-edition usage log exists (C-5 material grain)"
else
fail "specifics-bank/src/types.ts has no usedIn field - «used in edition NN» is unrecordable (C-5)"
fi
if grep -qF "export function recordUsage" scripts/specifics-bank/src/bank.ts; then
pass "specifics-bank exports recordUsage (C-5 material grain)"
else
fail "specifics-bank/src/bank.ts does not export recordUsage (C-5)"
fi
if grep -qF "export function boundSpecificIds" scripts/specifics-bank/src/binding.ts; then
pass "specifics-bank exports boundSpecificIds - only specific-bound slots count as consumption (C-5)"
else
fail "specifics-bank/src/binding.ts does not export boundSpecificIds (C-5)"
fi
# (C-5 series grain) the distillate module exists with its deterministic check
if grep -qF "export function checkSkeleton" "$DIST_SRC"; then
pass "scripts/editions exports checkSkeleton - the deterministic series check exists (C-5 series grain)"
else
fail "$DIST_SRC does not export checkSkeleton (C-5)"
fi
# (C-5 placement) the distillate lives in the SERIES root beside edition-state, not the data dir
if grep -qF "series-distillate.json" "$DIST_SRC" && grep -qF '"linkedin"' "$DIST_SRC"; then
pass "series distillate resolves under <serie>/linkedin/ beside edition-state (C-5 placement)"
else
fail "$DIST_SRC does not resolve the distillate under the series root - series-scoped state placed wrong (C-5)"
fi
echo ""
# --- Section 18: Assertion-Count Anti-Erosion (SC6) ---
# The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the total pass()+fail() invocations as a monotonic floor; the count
@ -1842,12 +1966,16 @@ echo ""
# (targetLevel-consumption self-test + no-hardcoded-line-manager absence grep + references target-level-span
# grep + personas opposite-end obligatorisk-sekundær grep + personas explicit-ceiling-justification grep +
# personas technical-end seed grep + edition-state targetLevel-slot grep + newsletter articles.NN.targetLevel
# persist grep + persona-reviewer targetLevel-consumption compound grep) = 146.
# persist grep + persona-reviewer targetLevel-consumption compound grep) = 146; +9 for N11's nine
# UNCONDITIONAL Section-16r checks (destillat-gate self-test + Step-2.5 destillat_gated compound grep +
# Step-8 distil-append grep + Step-8 record-usage grep + types.ts usedIn grep + bank.ts recordUsage grep +
# binding.ts boundSpecificIds grep + distillate.ts checkSkeleton grep + distillate.ts series-root
# placement grep) = 155.
# NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop
# the count), so it is bumped only by UNCONDITIONAL new checks — NOT pinned to the
# deps-present TOTAL_CHECKS (that would zero the warn-skip margin and false-fail a fresh
# clone). Runs last so TOTAL_CHECKS sees every prior check.
ASSERT_BASELINE_FLOOR=146
ASSERT_BASELINE_FLOOR=155
TOTAL_CHECKS=$((PASS + FAIL))
if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then
pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR"