feat(linkedin-studio): N15 — do-next-kontrakt (recordDoNext + skrivere/lesere + forrige-utgave-kalibrering) [skip-docs]

Måling som ikke endrer neste utgave er teater: report/analyze/ab-test/48h-monitor
endte alle i chat (grep do.next = 0 i hele repoet). N15 gjør kanalen til en kontrakt.

- recordDoNext i hooks/scripts/state-updater.mjs (TDD, 17 nye tester): seksjonen
  Do-Next Directives, rader "[dato] (kilde) direktiv — evidence: peker".
  Levetid = erstatt-per-kilde + 60-dagers aldersgulv, maks 3 per skriv, additiv
  scalar last_donext_date, $-sikre replacement-funksjoner. CLI: --record-do-next.
- Skrivere: report Step 7b, analyze Step 6b, ab-test 2c.6 (kun Adopt),
  post-feedback-monitor (kun 48h-sjekken); analytics-interpreter emitterer nå
  anbefalinger på do-next-form (imperativ + evidens).
- Lesere: post/quick/batch/create Step 0 + newsletter Step 1, som i tillegg
  kalibrerer på forrige utgaves MÅLTE tall via queue-id-match (queue.json →
  analytics/posts) med eksplisitt honest-miss — aldri estimert.
- Guards: test-runner Section 16v (15 ubetingede sjekker, floor 198 → 213),
  $-safety-batteriet dekker recordDoNext (coverage-gate fanget den nye eksporten).

Suiter: test-runner 232/0, hooks 191/0, trends 300/0, brain 134/0, editions 72/0,
specifics-bank 45/0, contract-gate 33/0, tests 35/0, render 60/0.

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 15:23:35 +02:00
commit b45fdad911
16 changed files with 713 additions and 6 deletions

View file

@ -2392,6 +2392,151 @@ fi
echo ""
# --- Section 16v: Do-Next Contract (N15 / A2-F10,F12,F13,F14) ---
echo "--- Do-Next Contract (N15) ---"
# Measurement that does not change the next piece is theatre. Before N15 every
# analytics surface ENDED in chat: report/analyze printed recommendations, ab-test
# printed a verdict, the 48h monitor printed a learning - and the next drafting
# session, a fresh context, never saw any of it (grep do.next across the repo = 0).
# N15 makes the channel a contract, so what is worth linting is BOTH ends of it plus
# the lifetime rule that keeps it honest:
# (A2-F10 writer) every measurement surface persists via recordDoNext with a source
# tag and an evidence pointer - a compound predicate (mutator named +
# source declared + evidence carried), so a non-vacuity self-test
# guards it (mirrors Sections 16p-16u).
# (A2-F12/F13) the A/B Adopt verdict and the 48h learning route in the SAME
# channel - a second private channel would re-fragment the loop.
# (reader) all four create surfaces read the section at Step 0, else the
# directives are written into a file nobody opens.
# (A2-F14) newsletter Step 1 calibrates on the PREVIOUS edition's measured
# numbers via the queue-id match, with an explicit honest-miss branch
# (an unmeasured edition must never be estimated).
# (lifetime) replace-by-source + age floor, in code - a section that only grows
# stops being a steering signal and becomes a backlog.
SU_N15="hooks/scripts/state-updater.mjs"
RPT_N15="commands/report.md"
ANL_N15="commands/analyze.md"
ABT_N15="commands/ab-test.md"
PFM_N15="agents/post-feedback-monitor.md"
AIN_N15="agents/analytics-interpreter.md"
NL_N15="commands/newsletter.md"
STATE_TPL_N15="config/state-file.template.md"
donext_write_wired() { # $1 = text; wired iff it NAMES the mutator, DECLARES a source, and CARRIES evidence
echo "$1" | grep -qF "recordDoNext" \
&& echo "$1" | grep -qF "source:" \
&& echo "$1" | grep -qF "evidence"
}
DN_SELFTEST_OK=1
if ! donext_write_wired "call recordDoNext with source: 'report' and directives carrying an evidence pointer"; then
DN_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired do-next write probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if donext_write_wired "$probe"; then
DN_SELFTEST_OK=0; echo " false-positive FAIL: under-wired do-next write probe accepted -> $probe"
fi
done <<'NEGATIVE16V'
call recordDoNext with source: 'report' and two directives, no pointer to what was measured
persist the directives with an evidence pointer under source: 'report', mutator left unnamed
call recordDoNext with an evidence pointer for each directive, source never declared
NEGATIVE16V
if [ "$DN_SELFTEST_OK" -eq 1 ]; then
pass "do-next write self-test: predicate needs recordDoNext + source + evidence (1 accepted, 3 under-wired rejected)"
else
fail "do-next write self-test failed - the N15 wiring lint is vacuous or over-eager"
fi
# (A2-F10 writers) the two analytics front-doors persist instead of printing-and-forgetting
if donext_write_wired "$(cat "$RPT_N15" 2>/dev/null)"; then
pass "/linkedin:report persists its recommendations as do-next directives (A2-F10)"
else
fail "report.md still ends its recommendations in chat - nothing reaches the next draft (A2-F10)"
fi
if donext_write_wired "$(cat "$ANL_N15" 2>/dev/null)"; then
pass "/linkedin:analyze persists its recovery plan as do-next directives (A2-F10)"
else
fail "analyze.md diagnoses without persisting the corrections - re-diagnosed next session (A2-F10)"
fi
# (A2-F12) an Adopt verdict is a decision the next post inherits, not a filed test result
if donext_write_wired "$(cat "$ABT_N15" 2>/dev/null)" && grep -qF "Adopt" "$ABT_N15" 2>/dev/null; then
pass "/linkedin:ab-test routes an Adopt verdict into the same do-next channel (A2-F12)"
else
fail "ab-test.md leaves an Adopt verdict in the test file only (A2-F12)"
fi
# (A2-F13) the 48h learning is the one measurement taken closest to the next post
if donext_write_wired "$(cat "$PFM_N15" 2>/dev/null)" && grep -qF "48h-monitor" "$PFM_N15" 2>/dev/null; then
pass "post-feedback-monitor routes its 48h learning into the same do-next channel (A2-F13)"
else
fail "post-feedback-monitor.md keeps the 48h learning in chat (A2-F13)"
fi
# (A2-F10 shape) the interpreter must EMIT directives the caller can persist verbatim
if grep -qF "do-next" "$AIN_N15" 2>/dev/null && grep -qF "evidence" "$AIN_N15" 2>/dev/null; then
pass "analytics-interpreter emits recommendations in do-next form (imperative + evidence)"
else
fail "analytics-interpreter.md emits observations, not persistable directives (A2-F10)"
fi
# (reader) all four create surfaces read the section - a write nobody reads is still theatre
for READER_N15 in commands/post.md commands/quick.md commands/batch.md commands/create.md; do
if grep -qF "Do-Next Directives" "$READER_N15" 2>/dev/null; then
pass "$(basename "$READER_N15" .md) reads the do-next directives at Step 0"
else
fail "$READER_N15 drafts without reading the do-next directives - the loop is open"
fi
done
# (A2-F14) long-form calibrates on the previous edition's MEASURED numbers, and says so
# out loud when there are none rather than estimating them
STEP1_N15=$(awk '/^## Step 1: Brief/{f=1} /^## Step 1.5:/{f=0} f' "$NL_N15")
if echo "$STEP1_N15" | grep -qF "Do-Next Directives" \
&& echo "$STEP1_N15" | grep -qF "queue.json" \
&& echo "$STEP1_N15" | grep -qF "analytics/posts" \
&& echo "$STEP1_N15" | grep -qF "Never estimate"; then
pass "newsletter Step 1 calibrates on the previous edition via queue-id match, with an honest-miss branch (A2-F14)"
else
fail "newsletter Step 1 does not read the previous edition's measured numbers (or may estimate them) (A2-F14)"
fi
# (contract) the mutator exists and is reachable from a command line
if grep -qF "export function recordDoNext" "$SU_N15" 2>/dev/null; then
pass "state-updater exports recordDoNext (deterministic, testable, no AI in the write)"
else
fail "$SU_N15 has no recordDoNext export - the channel has no writer"
fi
if grep -qF -- "--record-do-next" "$SU_N15" 2>/dev/null; then
pass "state-updater exposes the --record-do-next standalone verb"
else
fail "$SU_N15 has no --record-do-next CLI verb"
fi
# (lifetime) both halves in code: a source replaces its OWN rows, and every row expires.
# Only one half and the section either grows forever or evicts other sources' directives.
if grep -qF "superseded by this write" "$SU_N15" 2>/dev/null \
&& grep -qF "past the age floor" "$SU_N15" 2>/dev/null; then
pass "do-next lifetime is replace-by-source AND age-bounded (the section cannot become a backlog)"
else
fail "$SU_N15 do-next lifetime rule is incomplete - a growing or self-evicting section"
fi
# (contract, shipped) a fresh install carries the section and the scalar, so the very
# first report writes into a documented shape instead of inventing one
if grep -qF "## Do-Next Directives" "$STATE_TPL_N15" 2>/dev/null \
&& grep -qF "last_donext_date" "$STATE_TPL_N15" 2>/dev/null; then
pass "state-file template ships the Do-Next Directives section + last_donext_date scalar"
else
fail "$STATE_TPL_N15 does not ship the do-next section/scalar (fresh installs start off-contract)"
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
@ -2450,12 +2595,17 @@ echo ""
# doc+slot grep + resumption retro-routing grep + headless background-mode grep + headless
# no-copy-paste grep + Step-6.5 background-routing grep + language-reviewer rules-file grep +
# Step-4 rules-file grep + Step-11 rules-file append grep + language-rules template opt-in
# check + fold-in-loop four-stage check + fold-in-loop adopter/silos grep) = 198.
# check + fold-in-loop four-stage check + fold-in-loop adopter/silos grep) = 198; +15 for
# N15's fifteen UNCONDITIONAL Section-16v checks (do-next write self-test + report writer +
# analyze writer + ab-test Adopt writer + 48h-monitor writer + analytics-interpreter
# directive-shape grep + four create-surface reader greps + newsletter Step-1 queue-id/
# honest-miss compound grep + recordDoNext export grep + --record-do-next CLI verb grep +
# lifetime replace-by-source/age-floor grep + state-template section/scalar grep) = 213.
# 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=198
ASSERT_BASELINE_FLOOR=213
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"