linkedin-studio/scripts/test-runner.sh
Kjell Tore Guttormsen 63506f7d5c feat(linkedin-studio): N16 — out-of-network-andel + patterns-oppdatering + boundary-map [skip-docs]
Reach-splitten (in/out-of-network) er native i LinkedIns post-analytics siden juni
2026, men vises som PROSENT og finnes ikke i CSV-eksporten. Planen antok to
manuelle antall; verifiseringen viste prosent, så modellen er ett felt —
outOfNetworkPct — og in-network er komplementet.

- parseOptionalPercent: egen parser, ikke parseOptionalCount. Komma er desimal
  (36,5 -> 36.5, aldri 365), og verdi >100 avvises: i én kolonne kan ikke et
  absolutt antall skilles fra en andel, så svaret er unknown, ikke en gjetning.
  Blank/ikke-numerisk/negativ -> unknown; ekte 0 beholdes.
- Ett lagret halvpart, kryssjekket: In-network godtas og lagres som komplement;
  et transkribert par som ikke summerer til ~100 (±1 avrunding) forkastes som
  unknown i stedet for å bli halvveis trodd.
- weightedOutOfNetworkPct: impressions-vektet roll-up (avgOutOfNetworkPct, uke +
  måned). Flatt snitt lar en 50-visnings-post slå en på 10 000; poster uten
  avlesning ekskluderes, og null vekt gir undefined — aldri 0, aldri NaN.
- Reach inngår ALDRI i engagementRate (distribusjon != engasjement). Rapporten
  leser den som akvisisjon (ut) vs resonans (inn), og sier «ikke ført for denne
  perioden» framfor å estimere. En reach-innsikt går inn i N15s do-next-kanal.
- Step 7c (A2-F11): rapporten tilbyr diff mot brukerens engagement-patterns.md
  med eksplisitt go — aldri stille skriving, aldri inn i den shippede malen.
- Boundary-map (E#9): dwell eksplisitt umålbar, saves partner-gated, reach
  native men CSV-eksport uverifisert.
- Reach-frie importer er byte-identiske med før, på skjerm og på disk.

TDD: rødt bevist først (10 feilende), analytics 119 -> 144 tester, tsc ren.
test-runner 232 -> 247 (Section 16w, gulv 213 -> 228). Alle suiter grønne.
CHANGELOG: N15-oppføringen manglet og er backfilt sammen med N16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2026-07-25 15:56:04 +02:00

2812 lines
145 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# LinkedIn Studio Plugin — Structure Validator
# Validates the REAL v3.1 layout: registration counts (derived dynamically),
# frontmatter shape, hook drift, and plugin.json fields. Counts are asserted
# against the declared contract below, which is kept in sync with the
# CLAUDE.md "## Agents (N)" / "## Commands (N)" headers (cross-checked here)
# and the STATE.md "Telling" block. Adding or removing an agent, command,
# reference, or skill breaks the count-equality and fails the lint — this is
# the registration guard that gates the remediation plan's later steps.
#
# The stat-consistency grep (one magnitude per algorithm effect across the
# tree) was added in remediation Step 3; the version-consistency grep in
# Step 21; the agent model-consistency guard (each agents/<name>.md frontmatter
# model: must match every surface declaration, and canonical rosters must list
# every agent) in S11; the render-chain propagation guard (no honesty pattern a
# command was cleaned of survives in the reference it renders from) in S12; the
# `$`-safety guard (no untrusted value reaches a String.replace replacement STRING
# in state-updater.mjs — proven behaviorally, coverage-complete, self-testing) in
# S13. The external data-dir convention guard (M0: no command/agent/skill/hook prose
# references a migrated user-data path in-plugin — bare or ${CLAUDE_PLUGIN_ROOT}-pinned;
# + no ANALYTICS_ROOT in-plugin pin; + an SC2 dry-run that the tree holds no in-plugin
# user data) in Section 13, each with a non-vacuity self-test; the contract-gate
# binding guard (Slice 3: the §B/§C1 gate's own suite stays green and rules.ts
# ratifies 1:1 against the maskinrommet §E-manifest — KTG-only, skipped for an adopter
# shipping no deps/contract) in Section 14; the specifics-bank binding guard (Fix #2
# slice 3: the lived-specifics store/binding suite stays green and its case count
# never erodes — KTG-only, skipped for an adopter shipping no deps) in Section 15;
# the trends-store binding guard (research-engine slice 2b: the trend store's suite
# stays green and its case count never erodes, now that trend-spotter persists its
# findings through it — KTG-only, skipped for an adopter shipping no deps) in Section
# 16; the trend-spotter de-niche guard (B-S1: agents/trend-spotter.md names no
# hardcoded vendor/sector beat — the domain comes from the user's pillars at runtime,
# never baked into the agent — with a non-vacuity self-test) in Section 17; the brain
# profile-reader guard (SB-S3a: strategy-advisor names brain/profile.md AND carries the
# anti-sycophancy literal 'evidence to TEST', with a non-vacuity self-test) in Section
# 16d; the brain operations-reader guard (SB-S3d: strategy-advisor names
# brain/operations.md AND carries the frozen-past-self literal 'deprecates older
# inferences', with a non-vacuity self-test) in Section 16e; the brain reconcile-wiring
# guard (SB-S3e: scripts/brain/src/cli.ts dispatches `reconcile` AND calls the core
# reconcileRecentPosts by literal name, with a non-vacuity self-test) in Section 16f;
# the trends-scorer wiring guard (RE-R1: scripts/trends/src/score.ts encodes both mode
# weight-sets AND agents/trend-spotter.md references the scorer CLI 'src/cli.ts score',
# with a non-vacuity self-test) in Section 16g; the trends-capture wiring guard (RE-R2a:
# scripts/trends/src/cli.ts dispatches `capture` AND agents/trend-spotter.md references the
# capture CLI 'src/cli.ts capture', with a non-vacuity self-test) in Section 16h; the
# trends-brief wiring guard (RE-R2b: scripts/trends/src/cli.ts dispatches `brief`,
# agents/trend-spotter.md references the brief CLI 'src/cli.ts brief', AND
# hooks/scripts/session-start.mjs surfaces it via 'latestMorningBrief', with a non-vacuity
# self-test) in Section 16i; the trends-score wiring guard (RE-R3a: scripts/trends/src/score.ts
# exports the 'export interface TrendScore' persist envelope, scripts/trends/src/types.ts carries
# 'score?: TrendScore' on the record, agents/trend-spotter.md carries the judgment via '"dimensions"',
# AND scripts/trends/src/brief.ts ranks on 'score?.composite', with a non-vacuity self-test) in
# Section 16j; the trends-lifecycle wiring guard (RE-R3b: scripts/trends/src/types.ts declares
# 'export type TrendStatus' AND carries 'surfacedCount', scripts/trends/src/store.ts owns
# 'export function markSurfaced', scripts/trends/src/brief.ts excludes handled via 'effectiveStatus',
# AND scripts/trends/src/cli.ts exposes 'command === "act"', with a non-vacuity self-test) in
# Section 16k; the trends-scheduler/headless wiring guard (RE-R3c: scripts/trends/src/schedule.ts
# emits 'export function launchdPlist' AND 'export function crontabLine', scripts/trends/src/cli.ts
# exposes 'command === "schedule"', scripts/trends/run-daily.sh runs 'cli.ts" brief' AND uses
# 'LINKEDIN_STUDIO_DATA:-', with a non-vacuity self-test) in Section 16l; the trends-temporal-overlay
# wiring guard (RE-R3d: scripts/trends/src/brief.ts has 'export function temporalSignal' AND the cmp
# key 'b.temporal.rank' AND the '"first-mover"' tier, scripts/trends/src/cli.ts exposes the
# 'first-mover-days' AND 'saturation-at' brief flags, with a non-vacuity self-test) in Section 16m;
# the trends-brief-history/diff wiring guard (RE-R3e: scripts/trends/src/brief.ts has
# 'export function diffSurfaced' AND 'parseSurfacedFrontmatter' AND the 'Nytt siden sist' section AND
# the 'surfaced: ' frontmatter emit, scripts/trends/src/cli.ts wires 'selectPriorBriefFile', with a
# non-vacuity self-test) in Section 16n; the randsone-gates guard (N9: newsletter.md Step 9 runs a scoped
# fact-reviewer + language-reviewer pass over the carousel slide text AND edition title, gates title-strength,
# declares the deterministic 'numbers, names, or quotes' recheck trigger, and both fact agents carry WebFetch,
# with a non-vacuity self-test) in Section 16p; the configurable target-level span guard (N10: the hardcoded
# 'non-technical line manager' is gone from references/ + config/, references/longform-quality-rules.md names
# the 'target-level span', config/personas.template.md requires a mandatory secondary at the opposite end +
# an explicit ceiling justification + seeds a technical-end persona, config/edition-state.template.json carries
# the additive articles.NN.targetLevel slot, commands/newsletter.md persists it, agents/persona-reviewer.md
# consumes it onto the span, with a non-vacuity self-test) in Section 16q; the assertion-count anti-erosion
# floor (SC6) in Section 18. All are live below (Sections 818).
#
# Usage: bash scripts/test-runner.sh
# bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`.
set -e
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PLUGIN_ROOT"
PASS=0
FAIL=0
WARN=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
pass() { echo -e "${GREEN}${NC} $1"; PASS=$((PASS + 1)); }
fail() { echo -e "${RED}${NC} $1"; FAIL=$((FAIL + 1)); }
warn() { echo -e "${YELLOW}${NC} $1"; WARN=$((WARN + 1)); }
# --- Declared registration contract (the "Telling" block) ---
# Source of truth: CLAUDE.md headers + STATE.md Telling. Bump these together
# with the files when adding/removing an agent, command, reference, or skill.
EXPECT_AGENTS=20
EXPECT_COMMANDS=30
EXPECT_REFS=29
EXPECT_SKILLS=6
# Pre-M0 references/ baseline was 25. Every ref doc added since is NAMED below, so the
# count bump always maps to an intended, named addition — never an incidental doc masked
# by the bump (m3/m11). M0 added data-path-convention.md; each later slice appends its
# doc to POSTM0_REFS. The assert below proves EXPECT_REFS == 25 + 1 (M0) + |POSTM0_REFS|
# AND that every named doc actually exists. bash 3.2-safe: plain indexed array.
REFS_BASELINE_PRE_M0=25
M0_REF="references/data-path-convention.md"
POSTM0_REFS=(
"references/trend-scoring-modes.md" # research-engine slice 2a (scoring SSOT)
"references/figure-design-guidelines.md" # MR-F4 N3 (coded-figure design rules + token convention)
"references/fold-in-loop.md" # N14 A2-F9 (capture->classify->promote->enforce, domain-general)
)
echo "================================================"
echo "LinkedIn Studio Plugin — Structure Validator"
echo "Plugin root: $PLUGIN_ROOT"
echo "================================================"
echo ""
# --- Section 1: Core Files ---
echo "--- Core Files ---"
for f in ".claude-plugin/plugin.json" "CLAUDE.md" "CHANGELOG.md" "README.md" "config/REMEMBER.template.md"; do
if [ -f "$f" ]; then
pass "$f exists"
else
fail "$f MISSING"
fi
done
echo ""
# --- Section 2: Registration Counts (dynamic) ---
echo "--- Registration Counts ---"
AGENTS=$(ls agents/*.md 2>/dev/null | wc -l | tr -d ' ')
COMMANDS=$(ls commands/*.md 2>/dev/null | wc -l | tr -d ' ')
REFS=$(ls references/*.md 2>/dev/null | wc -l | tr -d ' ')
SKILLS=$(ls skills/*/SKILL.md 2>/dev/null | wc -l | tr -d ' ')
assert_count() {
# $1 label, $2 actual, $3 expected
if [ "$2" -eq "$3" ]; then
pass "$1: $2 (expected $3)"
else
fail "$1: $2 (expected $3) — registration drift"
fi
}
assert_count "agents/*.md" "$AGENTS" "$EXPECT_AGENTS"
assert_count "commands/*.md" "$COMMANDS" "$EXPECT_COMMANDS"
assert_count "references/*.md" "$REFS" "$EXPECT_REFS"
assert_count "skills/*/SKILL.md" "$SKILLS" "$EXPECT_SKILLS"
# references/ count must map 1:1 to the NAMED additions (M0 + every later slice's doc),
# and each named doc must exist. Guards against the bump silently absorbing an incidental
# extra ref doc (m3/m11). To add a ref doc: append it to POSTM0_REFS and bump EXPECT_REFS.
NAMED_REFS_OK=1
[ -f "$M0_REF" ] || NAMED_REFS_OK=0
for r in "${POSTM0_REFS[@]}"; do
[ -f "$r" ] || NAMED_REFS_OK=0
done
EXPECT_NAMED=$((REFS_BASELINE_PRE_M0 + 1 + ${#POSTM0_REFS[@]}))
if [ "$EXPECT_REFS" -eq "$EXPECT_NAMED" ] && [ "$NAMED_REFS_OK" -eq 1 ]; then
pass "references/ count maps to named additions (${REFS_BASELINE_PRE_M0} baseline +1 M0 +${#POSTM0_REFS[@]} post-M0 = ${EXPECT_REFS}; all named docs exist)"
else
fail "references/ count != named additions — bump EXPECT_REFS and name the doc in POSTM0_REFS (expected ${EXPECT_NAMED}, have ${EXPECT_REFS}; named-docs-exist=${NAMED_REFS_OK})"
fi
# Cross-check the CLAUDE.md declared headers against the contract (doc-drift guard)
DOC_AGENTS=$(grep -oE '^## Agents \([0-9]+\)' CLAUDE.md | grep -oE '[0-9]+' | head -1)
DOC_COMMANDS=$(grep -oE '^## Commands \([0-9]+\)' CLAUDE.md | grep -oE '[0-9]+' | head -1)
if [ "$DOC_AGENTS" = "$EXPECT_AGENTS" ]; then
pass "CLAUDE.md '## Agents ($DOC_AGENTS)' matches contract"
else
fail "CLAUDE.md agents header ($DOC_AGENTS) != contract ($EXPECT_AGENTS)"
fi
if [ "$DOC_COMMANDS" = "$EXPECT_COMMANDS" ]; then
pass "CLAUDE.md '## Commands ($DOC_COMMANDS)' matches contract"
else
fail "CLAUDE.md commands header ($DOC_COMMANDS) != contract ($EXPECT_COMMANDS)"
fi
# README shields commands-count badge must match the contract too. Added after an
# S14 /trekreview found the badge stale at commands-27 while the surface shipped 29:
# the version-consistency grep (Section 9) checks only the version badge, and the
# count guards above check the CLAUDE.md header, so the README count badge slipped
# both. This closes that gap (the count-badge analogue of the version-badge check).
if grep -q "badge/commands-${EXPECT_COMMANDS}-" README.md; then
pass "README commands badge declares ${EXPECT_COMMANDS}"
else
fail "README commands badge != ${EXPECT_COMMANDS} (expected shields badge/commands-${EXPECT_COMMANDS}-)"
fi
echo ""
# --- Section 3: Agent Frontmatter ---
echo "--- Agent Frontmatter ---"
for f in agents/*.md; do
if head -1 "$f" | grep -q "^---"; then
if grep -q "^name:" "$f" && grep -q "^description:" "$f"; then
pass "$f (frontmatter OK)"
else
fail "$f (missing name:/description:)"
fi
else
fail "$f (no YAML frontmatter)"
fi
done
echo ""
# --- Section 4: Command Frontmatter ---
echo "--- Command Frontmatter ---"
for f in commands/*.md; do
if head -1 "$f" | grep -q "^---"; then
if grep -q "^name:" "$f" && grep -q "^description:" "$f"; then
pass "$f (frontmatter OK)"
else
fail "$f (missing name:/description:)"
fi
else
fail "$f (no YAML frontmatter)"
fi
done
echo ""
# --- Section 5: Hook Configuration (drift) ---
echo "--- Hook Configuration ---"
if [ -f "hooks/hooks.json" ]; then
pass "hooks/hooks.json exists"
if python3 hooks/scripts/compile-hooks.py --check >/dev/null 2>&1; then
pass "hooks.json matches compiled template (no drift)"
else
fail "hooks.json DRIFT — run: python3 hooks/scripts/compile-hooks.py"
fi
else
fail "hooks/hooks.json MISSING"
fi
echo ""
# --- Section 6: Plugin.json Validation ---
echo "--- Plugin.json Validation ---"
if python3 -c "
import json, sys
with open('.claude-plugin/plugin.json') as f:
data = json.load(f)
required = ['name', 'version', 'description']
missing = [field for field in required if field not in data]
if missing:
print('Missing fields:', missing)
sys.exit(1)
print('Version:', data['version'])
" 2>/dev/null; then
pass "plugin.json structure valid (name/version/description)"
else
fail "plugin.json structure invalid"
fi
echo ""
# --- Section 7: Analytics Source ---
echo "--- Analytics Source ---"
if [ -f "scripts/analytics/src/cli.ts" ]; then
pass "scripts/analytics/src/cli.ts exists"
else
fail "scripts/analytics/src/cli.ts MISSING"
fi
echo ""
# --- Section 8: Algorithm-Stat Consistency ---
echo "--- Algorithm-Stat Consistency ---"
# The single source of truth for algorithm magnitudes is
# references/algorithm-signals-reference.md. After the Phase-0 reconciliation,
# stale/competing magnitudes — the retired engagement-coefficient folklore, the
# unpublishable model params/brand, and the deployment date — must not reappear
# anywhere else (cite the reference, do not restate). This enforces "one magnitude
# per algorithm effect" by forbidding EVERY retired-class value from returning, so
# the same grep that defines the Phase-0 Success Criterion fails on any survivor.
#
# S9 rebuild: the S8 list forbade only the two S7-named strings and went green
# over six more survivors (the coefficient system in analytics-interpreter/
# content-optimizer/pipeline/glossary, the playbook 15x/5x, the 150B model). This
# list is rebuilt to the FULL criterion. Forbidden classes (each maps to a
# canonical statement in the reference):
# - Carousel-rate folklore: 6.6% / 6.60% / 1.92% → reference: "~7% top format"
# - Link-penalty folklore: 40-50% / 25-40% / -40-60% → reference: one ~38% correlational band
# - Comment-multiplier folklore: "15x more reach/algorithmic", "5x more effective/
# less valuable/reach than" → reference: order only, comment ≈ 2x a like
# - Video-multiplier folklore: "5x more conversations" → reference: video declining, no multiplier
# - Engagement-coefficient system: 7-9x, 2.5x, 0.2x, (10x), (8x), "10x weight"
# → reference: "never hard coefficients to optimize against"
# - Model params/brand/date: the PATTERN CLASS [0-9]+[ -]?(B|billion)?[ -]?param
# (covers 150-parameter / 150B param / 150 billion param) / 360Brew / January 2026
# → reference: "Not publishable as fact"
#
# S10: the model-precision token is now the pattern CLASS, not a literal-token
# list. S9 forbade only "150 ?B param|150 billion param"; a hyphenated
# "150-parameter" (no "B") slipped both the discovery grep and the lint, surviving
# in glossary.md:10. The criterion is "no asserted model precision in ANY surface
# form", so the lint now enforces the shape (a number adjacent to "param"), not an
# enumeration. An adjacent digit is REQUIRED, so legitimate "param" uses with no
# leading number — "Language parameter", "parameterized", "different parameters",
# "«parametere»", "175-milliarders parametermodell" — do not match.
# Bare "10x"/"15x"/"5x" are deliberately NOT forbidden — they carry legitimate
# uses (collaboration "10x your reach" hyperbole, "5x5x5", posting cadence, pixel
# dims like 1080x1350), so each token targets the retired *phrasing*, not the bare
# number.
#
# Scope covers every dir the criterion's grep covers, including assets/checklists/
# (the 360Brew survivor lived there, outside the S8 scan), assets/templates/, and
# CHANGELOG.md (S10: the 360Brew/January-2026 survivor lived there, outside the S9
# scope). assets/{templates,checklists}/ — not all of assets/ — keeps the scan off
# gitignored runtime data (assets/analytics/, assets/drafts/, voice-samples/).
STALE_STATS='40-50%|25-40%|6\.6%|6\.60%|1\.92%|15x more reach|15x more algorithmic|5x more effective|5x less valuable|5x more reach than|5x more conversations|7-9x|2\.5x|0\.2x|\(10x\)|\(8x\)|10x weight|-40-60%|[0-9]+[ -]?(B|billion)?[ -]?param|360Brew|January 2026'
# Non-vacuity self-test (S10). A grep criterion is only meaningful if it actually
# MATCHES the forbidden forms and does NOT match legitimate ones. S7→S9 each
# shipped a lint that passed green while a survivor slipped, because the proof was
# run once by hand and never committed — so a hyphenated "150-parameter" form was
# never re-checked. This makes the proof PERMANENT: it runs on every invocation
# BEFORE the real scan, so narrowing STALE_STATS back to a literal-token list fails
# the suite instead of silently certifying an unenforced criterion. The positive
# set covers all three model-precision surface forms (incl. the exact S10
# "150-parameter" survivor); the negative set covers the legitimate "param"/x uses
# that live in the tree today.
SELFTEST_OK=1
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if ! echo "$probe" | grep -qE "$STALE_STATS"; then
SELFTEST_OK=0; echo " non-vacuity FAIL: forbidden form not caught -> $probe"
fi
done <<'POSITIVE'
40-50% link penalty
6.6% carousel rate
1.92% reach
15x more reach
5x more conversations
7-9x weight
(10x) coefficient
10x weight
150-parameter foundation model
150B parameter foundation model
150 billion parameter model
360Brew
January 2026 algorithm update
POSITIVE
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if echo "$probe" | grep -qE "$STALE_STATS"; then
SELFTEST_OK=0; echo " false-positive FAIL: legitimate form caught -> $probe"
fi
done <<'NEGATIVE'
5x5x5 pre-posting method
post 3x per week
1080x1350 pixels
10x your reach
Language parameter (configurable)
parameterized content-gatekeeper
Start over with different parameters
175-milliarders parametermodell
NEGATIVE
if [ "$SELFTEST_OK" -eq 1 ]; then
pass "STALE_STATS self-test: 13 forbidden forms caught, 8 legitimate forms ignored"
else
fail "STALE_STATS self-test failed — the lint no longer enforces the full criterion"
fi
STAT_HITS=$(grep -rnE "$STALE_STATS" references/ commands/ skills/ hooks/prompts/ agents/ assets/templates/ assets/checklists/ CLAUDE.md README.md CHANGELOG.md .claude-plugin/plugin.json 2>/dev/null | grep -v 'algorithm-signals-reference' || true)
if [ -z "$STAT_HITS" ]; then
pass "no stale algorithm magnitudes / model brand outside the canonical reference"
else
fail "stale algorithm stat(s) reintroduced — cite algorithm-signals-reference.md instead:"
echo "$STAT_HITS"
fi
echo ""
# --- Section 9: Version Consistency ---
echo "--- Version Consistency ---"
# Single source of truth for the plugin version: .claude-plugin/plugin.json.
# Its value must be declared identically in the README badge, the CLAUDE.md
# header, and the CHANGELOG top entry. Historical references to older versions
# (CHANGELOG history, the README version-history table, "vX added Y" prose) are
# NOT checked here — only the current-version DECLARATIONS must agree.
VERSION=$(python3 -c "import json; print(json.load(open('.claude-plugin/plugin.json'))['version'])" 2>/dev/null)
if [ -z "$VERSION" ]; then
fail "could not read version from plugin.json"
else
pass "plugin.json version: $VERSION"
if grep -q "version-${VERSION}-blue" README.md; then
pass "README badge declares v$VERSION"
else
fail "README badge does not declare v$VERSION (expected version-${VERSION}-blue)"
fi
if grep -q "LinkedIn Studio Plugin (v${VERSION})" CLAUDE.md; then
pass "CLAUDE.md header declares v$VERSION"
else
fail "CLAUDE.md header does not declare (v$VERSION)"
fi
if grep -q "^## \[${VERSION}\]" CHANGELOG.md; then
pass "CHANGELOG has a [$VERSION] entry"
else
fail "CHANGELOG missing a [$VERSION] entry"
fi
fi
echo ""
# --- Section 10: Agent Model-Consistency ---
echo "--- Agent Model-Consistency ---"
# Each agents/<name>.md frontmatter `model:` is the source of truth; every
# surface that DECLARES an agent's model (README, CLAUDE.md, the capability
# matrix, the SKILL rosters) must match it, and the canonical rosters must list
# every agent. Added in S11 after a cold full-brief review found
# post-feedback-monitor published as Haiku across four surfaces while the agent
# runs Opus — declaration drift the version/count/stat guards could not see. The
# checker self-tests its own non-vacuity on every run (see the .mjs header):
# a deliberately-mismatched probe must be caught and a correct one ignored, else
# the suite fails instead of certifying an unenforced criterion.
if node scripts/check-model-consistency.mjs; then
pass "agent model-consistency: all surface declarations match frontmatter + canonical rosters complete"
else
fail "agent model-consistency drift — see check-model-consistency.mjs output above"
fi
echo ""
# --- Section 11: Render-Chain Propagation ---
echo "--- Render-Chain Propagation ---"
# Commands render from the references they inline via
# ${CLAUDE_PLUGIN_ROOT}/references/…. An honesty pattern removed from a command
# surface must NOT survive in the reference that command renders from — otherwise
# the user still hits it. Added in S12 after a cold full-brief review found the
# banned A/B significance-verdict column (`Significant? (>20%)` with Yes/No cells)
# still shipping in references/ab-testing-framework.md while commands/ab-test.md had
# already been cleaned to the honest "Directional?" framing. The command-level fix
# never propagated to its render-source, and Section 8's STALE_STATS grep targets
# magnitudes, not this construct, so the survivor passed green. This generalizes the
# fix from "clean the command" to "the banned construct is forbidden across the
# WHOLE render chain (commands AND references)". Future propagation-class patterns
# get appended to PROP_FORBIDDEN, mirroring how Section 8's STALE_STATS grew to the
# full criterion rather than the single named token.
#
# Forbidden: the significance-VERDICT column — `Significant?` adjacent to a `(` (the
# `(>20%)` verdict parenthetical) or a table pipe (`| Significant?`). The defect is a
# column steering users to record a statistical-significance call that organic
# personal-post volume never reaches; "directional" is the honest frame. Legitimate
# descriptive prose ("Significantly higher", "Significant capability", "statistical
# significance", a bare sentence-final "significant?") carries no `(`/`|`-adjacency
# and is left alone.
PROP_FORBIDDEN='Significant\?[[:space:]]*\(|\|[[:space:]]*Significant\?'
# Non-vacuity self-test (mirrors Section 8): the criterion is only meaningful if it
# MATCHES the verdict-column forms and IGNORES legitimate prose. Runs on every
# invocation BEFORE the real scan, so weakening the pattern fails the suite instead
# of silently certifying an unenforced guard. The positive set covers the exact S12
# survivor + its bare-column variant; the negative set covers the honest
# "Directional?" fix and every legitimate "Significant"/"significance" string the
# tree carries today.
PROP_SELFTEST_OK=1
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if ! echo "$probe" | grep -qE "$PROP_FORBIDDEN"; then
PROP_SELFTEST_OK=0; echo " non-vacuity FAIL: forbidden form not caught -> $probe"
fi
done <<'PROP_POSITIVE'
| Difference | Significant? (>20%) |
Significant? (>20%)
| Significant? |
PROP_POSITIVE
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if echo "$probe" | grep -qE "$PROP_FORBIDDEN"; then
PROP_SELFTEST_OK=0; echo " false-positive FAIL: legitimate form caught -> $probe"
fi
done <<'PROP_NEGATIVE'
| Difference | Directional? (>20% gap) |
Significantly higher weight than generic responses
Significant capability breakthroughs
Significantly Behind (<50%)
LinkedIn analytics does not support statistical significance tests
Is the difference significant? Probably not.
PROP_NEGATIVE
if [ "$PROP_SELFTEST_OK" -eq 1 ]; then
pass "render-chain propagation self-test: 3 verdict-column forms caught, 6 legitimate forms ignored"
else
fail "render-chain propagation self-test failed — the guard no longer enforces the criterion"
fi
# Real scan across the whole user-facing render chain (commands + every reference
# they inline) plus the adjacent surfaces a copy could migrate the table into.
PROP_HITS=$(grep -rnE "$PROP_FORBIDDEN" references/ commands/ skills/ hooks/prompts/ agents/ assets/templates/ assets/checklists/ 2>/dev/null || true)
if [ -z "$PROP_HITS" ]; then
pass "no significance-verdict column survives in any command or its render-source reference"
else
fail "significance-verdict column reintroduced — use the honest 'Directional?' framing (see commands/ab-test.md):"
echo "$PROP_HITS"
fi
echo ""
# --- Section 12: `$`-Safety (String.replace replacement) ---
echo "--- \$-Safety (String.replace replacement) ---"
# state-updater.mjs mutates the state file from untrusted user content (post
# topics, hooks, targets, partners, …). In a JS replacement *string*, `$&`/`` $` ``/
# `$'`/`$$`/`$n` are special, so a `$`-bearing value rewrites the field; a
# replacement *function* inserts its return verbatim. Added in S13 after a cold
# full-brief review found the LAST member of this class: S12 converted the 5
# section-append sites to functions but left `replaceField` (the scalar writer) on a
# replacement string, and the S12 `$`-test asserted only the section entry — never
# the `last_post_topic` scalar — so the corruption shipped green. This is the S9→S12
# "close the class, not the line" lesson on the `$`-axis: rather than grep a
# syntactic proxy (which cannot tell a replacement-position template literal from a
# RegExp-pattern one across multi-line calls), check-replace-safety.mjs drives EVERY
# exported mutator with an adversarial payload of every special token in every
# free-text + date field and asserts verbatim survival. Two structural backstops run
# inside it on every invocation: COVERAGE-COMPLETENESS (a new export without
# `$`-coverage fails) and a NON-VACUITY SELF-TEST (a naive string-replace MUST
# corrupt the payload, a function MUST preserve it — else a PASS is meaningless),
# mirroring Section 8/10/11.
if node scripts/check-replace-safety.mjs; then
pass "\$-safety: no untrusted value reaches a String.replace replacement string (behavioral, coverage-complete, self-testing)"
else
fail "\$-safety guard failed — a state-updater String.replace replacement is \$-unsafe; see check-replace-safety.mjs output above"
fi
echo ""
# --- Section 13: External Data-Dir Convention (R1 — M0) ---
echo "--- External Data-Dir Convention ---"
# After the M0 migration (user data lives under ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/
# linkedin-studio}/), no command/agent/skill/hook prose may reference a MIGRATED
# user-data path in-plugin — neither bare (assets/<x>/) nor ${CLAUDE_PLUGIN_ROOT}-
# prefixed — or a command writes the external dir while Claude follows stale in-plugin
# prose (R1, brief §12.6). Shipped read-only assets keep ${CLAUDE_PLUGIN_ROOT} and are
# exempt: the analytics README, every *-template.md seed, assets/checklists/,
# assets/quick-post-resources.md, the shipped assets/templates/*, config/*.template.*.
# config/personas.local.md is a deliberate in-plugin fallback library (un-migrated, out
# of M0 scope) and is NOT in the class. Scope = the actionable prose dirs (commands/
# agents/skills/hooks-prompts); references/ is documentation (the convention doc
# legitimately names scaffold paths) and is excluded.
BARE_DATA='(\$\{CLAUDE_PLUGIN_ROOT\}/)?assets/(voice-samples|analytics|drafts|plans|audience-insights|examples|frameworks|case-studies|network)/|(\$\{CLAUDE_PLUGIN_ROOT\}/)?assets/repurposing-tracker\.md|(\$\{CLAUDE_PLUGIN_ROOT\}/)?assets/templates/my-post-templates\.md|(\$\{CLAUDE_PLUGIN_ROOT\}/)?config/user-profile\.local\.md'
BARE_EXEMPT='README\.md|-template\.md'
# Non-vacuity self-test (mirrors Section 8): the full criterion is regex-match AND
# not-exempt. Positives MUST be violations; negatives (external token + every shipped
# exemption + the out-of-scope personas lib) MUST NOT be.
B13_OK=1
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if echo "$probe" | grep -qE "$BARE_DATA" && ! echo "$probe" | grep -qE "$BARE_EXEMPT"; then :; else
B13_OK=0; echo " non-vacuity FAIL: in-plugin data path not flagged -> $probe"
fi
done <<'POSITIVE13'
assets/voice-samples/authentic-voice-samples.md
${CLAUDE_PLUGIN_ROOT}/assets/analytics/posts/
assets/drafts/queue.json
assets/templates/my-post-templates.md
assets/frameworks/ai-maturity-model.md
assets/repurposing-tracker.md
config/user-profile.local.md
POSITIVE13
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if echo "$probe" | grep -qE "$BARE_DATA" && ! echo "$probe" | grep -qE "$BARE_EXEMPT"; then
B13_OK=0; echo " false-positive FAIL: legitimate path flagged -> $probe"
fi
done <<'NEGATIVE13'
${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/
assets/analytics/README.md
assets/frameworks/framework-template.md
assets/case-studies/case-study-template.md
assets/checklists/quality-scorecard.md
assets/quick-post-resources.md
config/user-profile.template.md
config/personas.local.md
NEGATIVE13
if [ "$B13_OK" -eq 1 ]; then
pass "no-bare-path self-test: 7 in-plugin data paths caught, 8 legitimate/exempt forms ignored"
else
fail "no-bare-path self-test failed — the R1 criterion no longer enforces in-plugin data-path detection"
fi
BARE_HITS=$(grep -rnE "$BARE_DATA" commands/ agents/ skills/ hooks/prompts/ 2>/dev/null | grep -vE "$BARE_EXEMPT" || true)
if [ -z "$BARE_HITS" ]; then
pass "no command/agent/skill/hook prose references a migrated user-data path in-plugin (R1)"
else
fail "in-plugin user-data path(s) in prose — route via \${LINKEDIN_STUDIO_DATA:-\$HOME/.claude/linkedin-studio}/ (see references/data-path-convention.md):"
echo "$BARE_HITS"
fi
# Sibling no-pin guard (M3): the no-bare-path grep cannot catch an ANALYTICS_ROOT alias
# pinned to the in-plugin analytics dir — Step 11 dropped these; forbid their return.
PIN='ANALYTICS_ROOT=.{0,2}\$\{CLAUDE_PLUGIN_ROOT\}/assets/analytics'
PIN_OK=1
echo 'ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics"' | grep -qE "$PIN" || { PIN_OK=0; echo " non-vacuity FAIL: in-plugin pin not caught"; }
if echo 'ANALYTICS_ROOT="${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics"' | grep -qE "$PIN"; then PIN_OK=0; echo " false-positive FAIL: external alias caught"; fi
if [ "$PIN_OK" -eq 1 ]; then
pass "no-pin self-test: in-plugin ANALYTICS_ROOT pin caught, external alias ignored"
else
fail "no-pin self-test failed — the M3 pin criterion is not enforced"
fi
PIN_HITS=$(grep -rnE "$PIN" commands/ agents/ skills/ hooks/prompts/ 2>/dev/null || true)
if [ -z "$PIN_HITS" ]; then
pass "no prose pins ANALYTICS_ROOT to the in-plugin analytics dir (M3)"
else
fail "ANALYTICS_ROOT pinned to in-plugin analytics — drop the pin (external default applies):"
echo "$PIN_HITS"
fi
# SC2 dry-run: the migrated in-plugin user-data classes (.gitignore) must hold NO data
# files — after M0 they live external. --ignored surfaces gitignored stragglers a plain
# porcelain would hide; filtered to the data classes, it must be empty. Catches a flow
# (or a stray file) that wrote user data back into the plugin tree (SC2). personas is
# excluded (un-migrated, out of M0 scope — consistent with Section 13's class).
SC2_CLASSES='assets/analytics/(exports|posts|weekly-reports|monthly-reports)/|assets/drafts/queue\.json|assets/drafts/week-|assets/voice-samples/[^ ]*\.local\.md|config/user-profile\.local\.md'
SC2_DIRT=$(git status --porcelain --ignored 2>/dev/null | grep -E "$SC2_CLASSES" || true)
if [ -z "$SC2_DIRT" ]; then
pass "SC2 dry-run: no in-plugin user-data files (analytics/drafts/voice/profile) in the tree"
else
fail "SC2 violation — migrated user data sits in the plugin tree (should be external):"
echo "$SC2_DIRT"
fi
echo ""
# --- Section 14: Contract-Gate Binding (Slice 3) ---
echo "--- Contract-Gate Binding ---"
# The deterministic §B/§C1 contract-gate (scripts/contract-gate) is wired into
# /linkedin:newsletter as Step 4.5. Two invariants belong in CI, not per-edition:
# (14a) the gate's own test suite stays green and its case count never erodes; and
# (14b) rules.ts stays bound 1:1 to the maskinrommet skrivekontrakt §E-manifest
# (ratify). Both are KTG-internal: skipped (warn, never fail) for an adopter that
# ships no contract-gate deps or no contract — so the lint stays green everywhere.
# bash 3.2-safe; `set +e` inside the test command-substitution keeps a red npm test
# from aborting the whole runner (set -e is active), and ratify exit 1 is captured by
# the `if (subshell)` form (condition context is exempt from set -e).
CG_DIR="scripts/contract-gate"
if [ -x "$CG_DIR/node_modules/.bin/tsx" ]; then
# 14a: the gate suite is green (npm test exit 0) and holds its case-count floor.
CG_OUT=$( set +e; (cd "$CG_DIR" && npm test) 2>&1; echo "CG_EXIT:$?" )
CG_EXIT=$(echo "$CG_OUT" | grep -oE 'CG_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
CG_TESTS=$(echo "$CG_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
CONTRACT_GATE_TESTS_FLOOR=33
if [ "$CG_EXIT" = "0" ] && [ -n "$CG_TESTS" ] && [ "$CG_TESTS" -ge "$CONTRACT_GATE_TESTS_FLOOR" ]; then
pass "contract-gate suite green: $CG_TESTS tests pass (floor $CONTRACT_GATE_TESTS_FLOOR)"
else
fail "contract-gate suite NOT green (exit=${CG_EXIT:-?}, tests=${CG_TESTS:-?}, floor $CONTRACT_GATE_TESTS_FLOOR) — run: (cd $CG_DIR && npm test)"
fi
# 14b: ratify — rules.ts <-> §E-manifest 1:1. Skipped when the contract is absent.
CONTRACT="${MASKINROMMET_CONTRACT:-$PLUGIN_ROOT/../../maskinrommet/docs/skrivekontrakt.md}"
if [ -f "$CONTRACT" ]; then
if (cd "$CG_DIR" && node --import tsx src/cli.ts --ratify) >/dev/null 2>&1; then
pass "contract-gate ratify: rules.ts bound 1:1 to the §E-manifest (in sync)"
else
fail "contract-gate ratify DRIFT — rules.ts != §E-manifest; run: (cd $CG_DIR && npm run ratify)"
fi
else
warn "contract-gate ratify skipped — contract absent ($CONTRACT); KTG-only invariant"
fi
else
warn "contract-gate skipped — deps absent ($CG_DIR/node_modules); run: (cd $CG_DIR && npm install)"
fi
echo ""
# --- Section 15: Specifics-Bank Binding (Fix #2 slice 3) ---
echo "--- Specifics-Bank Binding ---"
# The lived-specifics store + per-edition binding (scripts/specifics-bank) is wired
# into /linkedin:newsletter as Step 1.5 (elicitation + slot-map) and its
# validate-binding gate into the Step 2.5 skeleton-gate. The seam is now load-bearing,
# so its suite belongs in CI: the bank/binding/kilder tests stay green and the case
# count never erodes. KTG-internal: skipped (warn, never fail) for an adopter that
# ships no specifics-bank deps — so the lint stays green everywhere. Same set +e /
# subshell discipline as Section 14 (bash 3.2-safe; keeps a red npm test from
# aborting the runner under set -e).
SB_DIR="scripts/specifics-bank"
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=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
fail "specifics-bank suite NOT green (exit=${SB_EXIT:-?}, tests=${SB_TESTS:-?}, floor $SPECIFICS_BANK_TESTS_FLOOR) — run: (cd $SB_DIR && npm test)"
fi
else
warn "specifics-bank skipped — deps absent ($SB_DIR/node_modules); run: (cd $SB_DIR && npm install)"
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=72
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 ---"
# The persistent trend store (scripts/trends) is wired into the trend-spotter agent
# (slice 2b): the agent queries prior history and persists each kept trend through the
# store's deterministic add (dedup/union), so the store is now load-bearing for the
# research engine's de-amnesia — not a standalone inventory. Its suite therefore belongs
# in CI: the store/dedup/query tests stay green and the case count never erodes. Mirrors
# the specifics-bank binding guard (Section 15) — the trend-side twin. KTG-internal:
# skipped (warn, never fail) for an adopter that ships no trends deps. Same set +e /
# subshell discipline as Sections 1415 (bash 3.2-safe; keeps a red npm test from
# aborting the runner under set -e).
TR_DIR="scripts/trends"
if [ -x "$TR_DIR/node_modules/.bin/tsx" ]; then
TR_OUT=$( set +e; (cd "$TR_DIR" && npm test) 2>&1; echo "TR_EXIT:$?" )
TR_EXIT=$(echo "$TR_OUT" | grep -oE 'TR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
TR_TESTS=$(echo "$TR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
TRENDS_TESTS_FLOOR=245 # store 24 + RE-R1: item 18 + score 16 + cli 4 + RE-R2a: store +9 + item +4 + cli +4 (capture bridge + publishedAt) + RE-R2b: brief +21 + cli +4 (morning-brief) + RE-R3a: score +6, item +12, store +6, brief +16, cli +2 (relevance score persist + rank) + RE-R3b: store +11, brief +8, cli +6 (lifecycle: re-score + status + seen-log) + RE-R3c: schedule +9, cli +8, run-daily +4 (scheduler + headless wrapper) + RE-R3d: brief +21, cli +3 (temporal overlay: first-mover + saturation) + RE-R3e: brief +27, cli +2 (brief history + diff)
if [ "$TR_EXIT" = "0" ] && [ -n "$TR_TESTS" ] && [ "$TR_TESTS" -ge "$TRENDS_TESTS_FLOOR" ]; then
pass "trends-store suite green: $TR_TESTS tests pass (floor $TRENDS_TESTS_FLOOR)"
else
fail "trends-store suite NOT green (exit=${TR_EXIT:-?}, tests=${TR_TESTS:-?}, floor $TRENDS_TESTS_FLOOR) — run: (cd $TR_DIR && npm test)"
fi
else
warn "trends-store skipped — deps absent ($TR_DIR/node_modules); run: (cd $TR_DIR && npm install)"
fi
echo ""
# --- Section 16b: Brain Foundation (SB-S0) ---
echo "--- Brain Foundation ---"
# The second-brain foundation (scripts/brain, SB-S0) ships the brain/ scaffold
# initialiser, the two-layer profile.md line-grammar, the lossless+idempotent
# user-profile fold, and the canonical entity-id + provenance module. This is the
# spine later slices hang on (the id threads through tributaries in SB-S3, the
# evolution loop consumes the profile in SB-S2), so the suite stays green and its
# case count never erodes. KTG-internal: skipped (warn, never fail) for an adopter
# that ships no brain deps. Same set +e / subshell discipline as Sections 14-16
# (bash 3.2-safe; keeps a red npm test from aborting the runner under set -e).
BR_DIR="scripts/brain"
if [ -x "$BR_DIR/node_modules/.bin/tsx" ]; then
BR_OUT=$( set +e; (cd "$BR_DIR" && npm test) 2>&1; echo "BR_EXIT:$?" )
BR_EXIT=$(echo "$BR_OUT" | grep -oE 'BR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
BR_TESTS=$(echo "$BR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
BRAIN_TESTS_FLOOR=127 # SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)] + SB-S2 19 [consolidate(12)+consolidate-cli(7)] + SB-S3b 12 [consolidate(10)+consolidate-cli(2)] + SB-S3c 19 [ingest(4)+publish(3)+assemble(8)+cli(4)] + SB-S3d 1 [scaffold dated-anchor seed] + SB-S3e 13 [reconcile: parse(5)+tiers(7)+io(1)]
if [ "$BR_EXIT" = "0" ] && [ -n "$BR_TESTS" ] && [ "$BR_TESTS" -ge "$BRAIN_TESTS_FLOOR" ]; then
pass "brain suite green: $BR_TESTS tests pass (floor $BRAIN_TESTS_FLOOR)"
else
fail "brain suite NOT green (exit=${BR_EXIT:-?}, tests=${BR_TESTS:-?}, floor $BRAIN_TESTS_FLOOR) — run: (cd $BR_DIR && npm test)"
fi
else
warn "brain skipped — deps absent ($BR_DIR/node_modules); run: (cd $BR_DIR && npm install)"
fi
echo ""
# --- Section 16c: Brain Published-Only Invariant (SB-S1) ---
echo "--- Brain Published-Only Invariant ---"
# SB-S1 wires voice-trainer to learn from the published gold signal ONLY: the agent
# must (a) name ingest/published/ as a source AND (b) carry the explicit exclusion of
# provenance=ai-draft (the model-collapse guard — never train voice on the engine's own
# drafts). Both literals are required. voice-trainer.md ALREADY contains "AI-generated"
# (a drift-cause description, line ~175), so a loose ai.?draft pattern would pass
# vacuously — we grep the EXACT literals with grep -F. The contract doc must also state
# the rule. Non-vacuity self-test mirrors Sections 8/13/17: a probe counts as "wired"
# iff it carries BOTH literals; probes missing either (incl. one with "AI-generated" but
# not "ai-draft") must NOT count as wired. The wiring is gate-enforced; the agent's
# runtime OBEDIENCE is not provable here (stated honestly in the contract doc).
PUB_LIT='ingest/published'
DRAFT_LIT='provenance=ai-draft'
pub_wired() { # $1 = text; wired iff BOTH literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$PUB_LIT" && echo "$1" | grep -qF "$DRAFT_LIT"
}
PUB_SELFTEST_OK=1
if ! pub_wired "reads from ingest/published/ and excludes provenance=ai-draft"; then
PUB_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if pub_wired "$probe"; then
PUB_SELFTEST_OK=0; echo " false-positive FAIL: under-wired probe accepted -> $probe"
fi
done <<'NEGATIVE16C'
reads from ingest/published/ but never states the draft exclusion
excludes provenance=ai-draft but never names the published source
flags AI-generated drift but carries neither required literal
NEGATIVE16C
if [ "$PUB_SELFTEST_OK" -eq 1 ]; then
pass "published-only self-test: full wiring detected; 3 under-wired forms rejected (incl. 'AI-generated'-only)"
else
fail "published-only self-test failed — the invariant lint is vacuous or over-eager"
fi
VT="agents/voice-trainer.md"
if grep -qF "$PUB_LIT" "$VT" && grep -qF "$DRAFT_LIT" "$VT"; then
pass "voice-trainer.md wired to published-only (names '$PUB_LIT', excludes '$DRAFT_LIT')"
else
fail "voice-trainer.md missing published-only wiring — needs both '$PUB_LIT' and '$DRAFT_LIT'"
fi
PUB_DOC="docs/second-brain/ingest-manual-import.md"
if grep -qF "$DRAFT_LIT" "$PUB_DOC"; then
pass "ingest-manual-import.md documents the published-only rule ('$DRAFT_LIT')"
else
fail "ingest-manual-import.md missing the published-only rule literal '$DRAFT_LIT'"
fi
echo ""
# --- Section 16d: Brain Profile Reader (SB-S3a) ---
echo "--- Brain Profile Reader ---"
# SB-S3a wires the FIRST reader of the evolving second-brain profile: strategy-advisor
# must (a) name brain/profile.md as a context source AND (b) carry the anti-sycophancy
# literal 'evidence to TEST' (architecture.md:58 — the profile informs, it never dictates
# or flatters; every fact is counter-pressured against analytics/state). Both literals
# are required, grepped EXACT with grep -F: a loose 'profile' pattern would match the
# legacy flat profile/user-profile.md vacuously, and a loose lowercase 'evidence' would
# match generic prose. Non-vacuity self-test mirrors Sections 8/13/16c/17: a probe counts
# as "wired" iff it carries BOTH exact literals; probes missing either (incl. a decoy
# naming the legacy path + lowercase "evidence") must NOT count as wired. The wiring is
# gate-enforced; the agent's runtime OBEDIENCE is verified manually (SC4/SC5 at land —
# agent-prompt behaviour is not unit-testable; the brief states this honestly).
READER_LIT='brain/profile.md'
ANTISYC_LIT='evidence to TEST'
reader_wired() { # $1 = text; wired iff BOTH literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$READER_LIT" && echo "$1" | grep -qF "$ANTISYC_LIT"
}
READER_SELFTEST_OK=1
if ! reader_wired "reads brain/profile.md as evidence to TEST"; then
READER_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if reader_wired "$probe"; then
READER_SELFTEST_OK=0; echo " false-positive FAIL: under-wired probe accepted -> $probe"
fi
done <<'NEGATIVE16D'
reads brain/profile.md but never frames how to weigh it
treats facts as evidence to TEST but names no source file
reads profile/user-profile.md and tests the evidence
NEGATIVE16D
if [ "$READER_SELFTEST_OK" -eq 1 ]; then
pass "profile-reader self-test: full wiring detected; 3 under-wired forms rejected (incl. legacy-path + lowercase 'evidence' decoy)"
else
fail "profile-reader self-test failed — the reader-wiring lint is vacuous or over-eager"
fi
SA="agents/strategy-advisor.md"
if grep -qF "$READER_LIT" "$SA" && grep -qF "$ANTISYC_LIT" "$SA"; then
pass "strategy-advisor.md wired to brain profile reader (names '$READER_LIT', frames '$ANTISYC_LIT')"
else
fail "strategy-advisor.md missing brain profile reader wiring — needs both '$READER_LIT' and '$ANTISYC_LIT'"
fi
echo ""
# --- Section 16e: Brain Operations Reader (SB-S3d) ---
echo "--- Brain Operations Reader ---"
# SB-S3d wires strategy-advisor to read the second-brain OPERATIONS centre: it must
# (a) name brain/operations.md as a context source AND (b) carry the frozen-past-self
# literal 'deprecates older inferences' (architecture.md:59 — the dated "who I am now"
# anchor outranks/deprecates older profile inferences; advisory/reader-side). Both
# literals are required, grepped EXACT with grep -F: a loose 'operations' pattern would
# match generic prose, and naming the sibling brain/profile.md must NOT satisfy the
# operations literal. Non-vacuity self-test mirrors Sections 8/13/16c/16d: a probe counts
# as "wired" iff it carries BOTH exact literals; probes missing either (incl. a sibling-
# file decoy naming brain/profile.md) must NOT count as wired. The wiring is gate-enforced;
# the agent's runtime OBEDIENCE is verified manually (SC6/SC7 at land — agent-prompt
# behaviour is not unit-testable; the brief states this honestly).
OPS_LIT='brain/operations.md'
ANCHOR_LIT='deprecates older inferences'
ops_reader_wired() { # $1 = text; wired iff BOTH literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$OPS_LIT" && echo "$1" | grep -qF "$ANCHOR_LIT"
}
OPS_SELFTEST_OK=1
if ! ops_reader_wired "reads brain/operations.md; the anchor deprecates older inferences"; then
OPS_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if ops_reader_wired "$probe"; then
OPS_SELFTEST_OK=0; echo " false-positive FAIL: under-wired probe accepted -> $probe"
fi
done <<'NEGATIVE16E'
reads brain/operations.md but never says the anchor outranks anything
the anchor deprecates older inferences but names no source file
reads brain/profile.md and tests the evidence
NEGATIVE16E
if [ "$OPS_SELFTEST_OK" -eq 1 ]; then
pass "ops-reader self-test: full wiring detected; 3 under-wired forms rejected (incl. sibling-file brain/profile.md decoy)"
else
fail "ops-reader self-test failed — the ops-reader-wiring lint is vacuous or over-eager"
fi
SA_OPS="agents/strategy-advisor.md"
if grep -qF "$OPS_LIT" "$SA_OPS" && grep -qF "$ANCHOR_LIT" "$SA_OPS"; then
pass "strategy-advisor.md wired to brain operations reader (names '$OPS_LIT', frames '$ANCHOR_LIT')"
else
fail "strategy-advisor.md missing brain operations reader wiring — needs both '$OPS_LIT' and '$ANCHOR_LIT'"
fi
echo ""
# --- Section 16f: Brain Reconcile Wiring (SB-S3e) ---
echo "--- Brain Reconcile Wiring ---"
# SB-S3e wires `brain reconcile` (read-side triple-post reconciliation: silo 1
# `## Recent Posts` ↔ the silo 2↔3 graph). The CLI must (a) dispatch the reconcile
# command AND (b) call the pure core by its literal name. Both literals live in
# scripts/brain/src/cli.ts (the dispatch `if` + runReconcile's literal-name call),
# grepped EXACT with grep -F. Non-vacuity self-test mirrors Section 16e: a probe is
# "wired" iff it carries BOTH literals; D1/D2 miss EXACTLY ONE literal (the load-
# bearing discriminators), D3 is a fully-wired SIBLING command (assemble) carrying
# neither reconcile literal (specificity — the predicate is reconcile-specific, not
# "any command + any core"). Wiring is gate-enforced; the reconcile CORE correctness
# is covered by the brain suite (reconcile.test.ts) and the end-to-end run is verified
# manually (SC4/SC6 at land — CLI behaviour is not unit-tested here; brief says so).
RECON_CLI_LIT='command === "reconcile"'
RECON_CORE_LIT='reconcileRecentPosts'
reconcile_wired() { # $1 = text; wired iff BOTH literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$RECON_CLI_LIT" && echo "$1" | grep -qF "$RECON_CORE_LIT"
}
RECON_SELFTEST_OK=1
if ! reconcile_wired 'if (command === "reconcile") return runReconcile via reconcileRecentPosts'; then
RECON_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if reconcile_wired "$probe"; then
RECON_SELFTEST_OK=0; echo " false-positive FAIL: under-wired probe accepted -> $probe"
fi
done <<'NEGATIVE16F'
if (command === "reconcile") return runReconcile(flags) but never calls the core
const nodes = reconcileRecentPosts({ recentPosts, records, graph }) with no command dispatch
if (command === "assemble") return runAssemble(flags) using assemblePostGraph
NEGATIVE16F
if [ "$RECON_SELFTEST_OK" -eq 1 ]; then
pass "reconcile self-test: full wiring detected; D1/D2 single-literal forms + D3 assemble specificity decoy rejected"
else
fail "reconcile self-test failed — the reconcile-wiring lint is vacuous or over-eager"
fi
CLI_RECON="scripts/brain/src/cli.ts"
if grep -qF "$RECON_CLI_LIT" "$CLI_RECON" && grep -qF "$RECON_CORE_LIT" "$CLI_RECON"; then
pass "brain CLI wired to reconcile (dispatch + core call by literal name)"
else
fail "brain CLI missing reconcile wiring — needs both '$RECON_CLI_LIT' and '$RECON_CORE_LIT' in $CLI_RECON"
fi
echo ""
# --- Section 17: De-Niche Guard (B-S1 + B-S2) ---
echo "--- De-Niche Guard ---"
# The de-niche sweep removed the hardcoded KTG beat (Microsoft / public-sector) from
# three surfaces so the domain comes from the user's profile/pillars at runtime, never
# baked in (plugin-is-domain-general):
# - agents/trend-spotter.md (B-S1: pillar-driven scanning; the agent's own
# contract says "the niche lives in the source
# list and the user's pillars, never in this agent")
# - agents/content-planner.md (B-S2a: planner calendar recast off the beat)
# - references/content-framework.md (B-S2a: recast + renamed from ai-content-framework.md)
# Each must name NO specific vendor or sector beat; this guard forbids those KTG-beat
# proper nouns from returning to any of them. Scoped per file BY DESIGN: the check runs
# against this explicit allowlist, NOT the whole tree, because references/content-angles.md
# legitimately lists "Public Sector" as one of six example industry tables (a KEEP surface,
# generic illustration — not a hardcoded beat), and "AI" is the plugin's own subject, not a
# niche token. Non-vacuity self-test mirrors Sections 8/13: the criterion must catch the beat
# tokens and ignore generic prose (incl. the content-framework.md reference filename — B-S2a
# recast + renamed it from ai-content-framework.md — and "AI"-flavoured planner prose, locking
# in that "AI" is never treated as a beat). Case-insensitive: "Public sector" and "public
# sector" name the same beat, and a future reintroduction could use either case — the
# positive set locks that in.
NICHE_TOKENS='Microsoft|Azure|Copilot|public sector|offentlig sektor'
TS_SELFTEST_OK=1
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if ! echo "$probe" | grep -qiE "$NICHE_TOKENS"; then
TS_SELFTEST_OK=0; echo " non-vacuity FAIL: beat token not caught -> $probe"
fi
done <<'POSITIVE17'
Microsoft platform changes
Azure updates
Copilot rollout
Public sector leaders
trender i offentlig sektor
POSITIVE17
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if echo "$probe" | grep -qiE "$NICHE_TOKENS"; then
TS_SELFTEST_OK=0; echo " false-positive FAIL: generic prose caught -> $probe"
fi
done <<'NEGATIVE17'
platform changes in the user's stack
sector milestones in the user's domain
the user's content pillars and expertise areas
references/content-framework.md
major product/model releases
AI-driven content planning across the user's pillars
NEGATIVE17
if [ "$TS_SELFTEST_OK" -eq 1 ]; then
pass "de-niche self-test: 5 beat tokens caught (case-insensitive), 6 generic forms ignored (incl. 'AI' kept as the plugin's own subject)"
else
fail "de-niche self-test failed — the guard no longer enforces the no-beat criterion"
fi
for guarded in agents/trend-spotter.md agents/content-planner.md references/content-framework.md; do
NICHE_HITS=$(grep -niE "$NICHE_TOKENS" "$guarded" 2>/dev/null || true)
if [ -z "$NICHE_HITS" ]; then
pass "$guarded names no hardcoded vendor/sector beat (domain comes from pillars)"
else
fail "$guarded hardcodes a vendor/sector beat — generalize to pillar-driven prose:"
echo "$NICHE_HITS"
fi
done
echo ""
# --- Section 16g: Trends Scorer Wiring (research-engine RE-R1 / B2) ---
echo "--- Trends Scorer Wiring ---"
# RE-R1 lifts the composite/band/threshold arithmetic out of trend-spotter.md prose into
# tested code (scripts/trends/src/score.ts) behind a CLI seam. Two literals must hold,
# grepped EXACT (grep -F), deps-absent-safe (pure grep, no tsx):
# (1) score.ts encodes BOTH mode weight-sets (the 'kortform' + 'long-form' literals), so a
# silent collapse to one mode fails here (the per-mode arithmetic itself is unit-tested
# in score.test.ts, behind the deps guard / trends-suite floor);
# (2) agents/trend-spotter.md references the scorer CLI by the literal 'src/cli.ts score' —
# the lift is real and grep-able, not merely documented.
# Non-vacuity self-test mirrors Sections 16c-17: the weight-set predicate (AND of both mode
# literals) must accept a both-modes probe and reject single-mode probes; the wiring predicate
# must accept a probe carrying the scorer-pointer literal and reject one without it. Labelled
# 16g but placed after Section 17 / before Section 18 (anti-erosion must run last so it sees
# every prior check). UNCONDITIONAL (no tsx) -> counts toward ASSERT_BASELINE_FLOOR.
WEIGHT_KORT_LIT='kortform'
WEIGHT_LONG_LIT='long-form'
SCORER_WIRE_LIT='src/cli.ts score'
weights_both_modes() { # $1 = text; true iff BOTH mode literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$WEIGHT_KORT_LIT" && echo "$1" | grep -qF "$WEIGHT_LONG_LIT"
}
G16_SELFTEST_OK=1
if ! weights_both_modes 'the kortform weight-set and the long-form weight-set are both encoded'; then
G16_SELFTEST_OK=0; echo " non-vacuity FAIL: a both-modes weight probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if weights_both_modes "$probe"; then
G16_SELFTEST_OK=0; echo " false-positive FAIL: single-mode weight probe accepted -> $probe"
fi
done <<'NEGATIVE16G'
only the kortform weight-set is present here
only the long-form weight-set is present here
NEGATIVE16G
if ! echo 'pipe the scores to src/cli.ts score for the composite' | grep -qF "$SCORER_WIRE_LIT"; then
G16_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired scorer-pointer probe was not detected"
fi
if echo 'the agent computes the composite itself' | grep -qF "$SCORER_WIRE_LIT"; then
G16_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the scorer pointer"
fi
if [ "$G16_SELFTEST_OK" -eq 1 ]; then
pass "trends-scorer self-test: both-modes weight predicate + scorer-pointer predicate detect wiring, reject the under-wired forms"
else
fail "trends-scorer self-test failed — the scorer-wiring lint is vacuous or over-eager"
fi
SCORE_TS="scripts/trends/src/score.ts"
if grep -qF "$WEIGHT_KORT_LIT" "$SCORE_TS" 2>/dev/null && grep -qF "$WEIGHT_LONG_LIT" "$SCORE_TS" 2>/dev/null; then
pass "score.ts encodes both mode weight-sets ('$WEIGHT_KORT_LIT' + '$WEIGHT_LONG_LIT')"
else
fail "score.ts missing a mode weight-set — needs both '$WEIGHT_KORT_LIT' and '$WEIGHT_LONG_LIT' in $SCORE_TS"
fi
if grep -qF "$SCORER_WIRE_LIT" agents/trend-spotter.md; then
pass "trend-spotter.md references the scorer CLI ('$SCORER_WIRE_LIT') as the deterministic-step owner"
else
fail "trend-spotter.md does not reference the scorer CLI — add a '$SCORER_WIRE_LIT' pointer (RE-R1 lift)"
fi
echo ""
# --- Section 16h: Trends Capture Wiring (research-engine RE-R2a) ---
echo "--- Trends Capture Wiring ---"
# RE-R2a closes the capture loop: the item->store bridge (itemToInput) + a `capture` CLI that
# normalizes a raw batch from stdin and folds it into the store (persisting publishedAt). Two
# literals must hold, grepped EXACT (grep -F), deps-absent-safe (pure grep, no tsx):
# (1) cli.ts dispatches the `capture` subcommand (the handler exists), by the literal
# 'command === "capture"' (the capture path is real, not merely documented);
# (2) agents/trend-spotter.md re-points Step 4.5 to the capture CLI by the literal
# 'src/cli.ts capture' (one normalizing batch call, replacing the N x `add` block).
# Non-vacuity self-test mirrors Sections 16c-16g: the wiring predicate must accept a probe
# carrying the capture-pointer literal and reject one without it. Labelled 16h but placed after
# Section 17 / before Section 18 (anti-erosion must run last so it sees every prior check).
# UNCONDITIONAL (no tsx) -> counts toward ASSERT_BASELINE_FLOOR.
CAPTURE_HANDLER_LIT='command === "capture"'
CAPTURE_WIRE_LIT='src/cli.ts capture'
H16_SELFTEST_OK=1
if ! echo 'pipe the raw batch to src/cli.ts capture for the store fold' | grep -qF "$CAPTURE_WIRE_LIT"; then
H16_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired capture-pointer probe was not detected"
fi
if echo 'the agent folds each trend into the store itself' | grep -qF "$CAPTURE_WIRE_LIT"; then
H16_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the capture pointer"
fi
if [ "$H16_SELFTEST_OK" -eq 1 ]; then
pass "trends-capture self-test: capture-pointer predicate detects wiring, rejects the under-wired form"
else
fail "trends-capture self-test failed — the capture-wiring lint is vacuous or over-eager"
fi
if grep -qF "$CAPTURE_HANDLER_LIT" scripts/trends/src/cli.ts; then
pass "cli.ts dispatches the capture subcommand ('$CAPTURE_HANDLER_LIT')"
else
fail "cli.ts has no capture handler — add a '$CAPTURE_HANDLER_LIT' branch (RE-R2a capture loop)"
fi
if grep -qF "$CAPTURE_WIRE_LIT" agents/trend-spotter.md; then
pass "trend-spotter.md references the capture CLI ('$CAPTURE_WIRE_LIT') as the Step 4.5 store-fold owner"
else
fail "trend-spotter.md does not reference the capture CLI — re-point Step 4.5 to a '$CAPTURE_WIRE_LIT' batch call (RE-R2a wiring)"
fi
echo ""
# --- Section 16i: Trends Brief Wiring (research-engine RE-R2b) ---
echo "--- Trends Brief Wiring ---"
# RE-R2b makes the store VISIBLE: a `brief` CLI ranks the store by pillar-overlap + recency and
# writes a dated Markdown file the SessionStart hook surfaces. Three literals must hold, grepped
# EXACT (grep -F), deps-absent-safe (pure grep, no tsx):
# (1) cli.ts dispatches the `brief` subcommand, by the literal 'command === "brief"';
# (2) agents/trend-spotter.md generates the brief after capture, by the literal 'src/cli.ts brief';
# (3) hooks/scripts/session-start.mjs surfaces it, by the literal 'latestMorningBrief' (the
# surfacing is wired, not merely documented).
# Non-vacuity self-test mirrors Section 16h: the wiring predicate must accept a probe carrying the
# brief-pointer literal and reject one without it. Placed after Section 16h / before Section 18
# (anti-erosion must run last so it sees every prior check). UNCONDITIONAL (no tsx) -> counts
# toward ASSERT_BASELINE_FLOOR.
BRIEF_HANDLER_LIT='command === "brief"'
BRIEF_WIRE_LIT='src/cli.ts brief'
BRIEF_SURFACE_LIT='latestMorningBrief'
I16_SELFTEST_OK=1
if ! echo 'after capture, run src/cli.ts brief --pillars to write the dated brief' | grep -qF "$BRIEF_WIRE_LIT"; then
I16_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired brief-pointer probe was not detected"
fi
if echo 'the agent renders the morning brief itself' | grep -qF "$BRIEF_WIRE_LIT"; then
I16_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the brief pointer"
fi
if [ "$I16_SELFTEST_OK" -eq 1 ]; then
pass "trends-brief self-test: brief-pointer predicate detects wiring, rejects the under-wired form"
else
fail "trends-brief self-test failed — the brief-wiring lint is vacuous or over-eager"
fi
if grep -qF "$BRIEF_HANDLER_LIT" scripts/trends/src/cli.ts; then
pass "cli.ts dispatches the brief subcommand ('$BRIEF_HANDLER_LIT')"
else
fail "cli.ts has no brief handler — add a '$BRIEF_HANDLER_LIT' branch (RE-R2b morning brief)"
fi
if grep -qF "$BRIEF_WIRE_LIT" agents/trend-spotter.md; then
pass "trend-spotter.md references the brief CLI ('$BRIEF_WIRE_LIT') as the post-capture surfacing step"
else
fail "trend-spotter.md does not reference the brief CLI — wire Step 4.6 to a '$BRIEF_WIRE_LIT' call (RE-R2b)"
fi
if grep -qF "$BRIEF_SURFACE_LIT" hooks/scripts/session-start.mjs; then
pass "session-start.mjs surfaces the morning brief ('$BRIEF_SURFACE_LIT')"
else
fail "session-start.mjs does not surface the brief — add the '$BRIEF_SURFACE_LIT' reader (RE-R2b hull 4)"
fi
echo ""
# --- Section 16j: Trends Score Wiring (research-engine RE-R3a) ---
echo "--- Trends Score Wiring ---"
# RE-R3a persists the relevance score the trend-spotter agent already computes and ranks the
# morning brief on its composite. Four literals must hold, grepped EXACT (grep -F),
# deps-absent-safe (pure grep, no tsx):
# (1) score.ts exports the persist envelope, by the literal 'export interface TrendScore';
# (2) types.ts carries it on the record, by the literal 'score?: TrendScore';
# (3) agents/trend-spotter.md carries the judgment in the capture batch, by 'dimensions'
# (verified absent pre-R3a -> the grep is non-vacuous);
# (4) brief.ts ranks on the composite, by the literal 'score?.composite' (the payoff is wired,
# not merely doc'd).
# Non-vacuity self-test mirrors Section 16i: the rank predicate must accept a probe carrying the
# composite-rank literal and reject one without it. Placed after Section 16i / before Section 18
# (anti-erosion must run last so it sees every prior check). UNCONDITIONAL (no tsx) -> counts
# toward ASSERT_BASELINE_FLOOR.
SCORE_IFACE_LIT='export interface TrendScore'
SCORE_TYPE_LIT='score?: TrendScore'
SCORE_DIMS_LIT='"dimensions"'
SCORE_RANK_LIT='score?.composite'
I16J_SELFTEST_OK=1
if ! echo 'rankForBrief sorts on (b.trend.score?.composite ?? -1) first' | grep -qF "$SCORE_RANK_LIT"; then
I16J_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired composite-rank probe was not detected"
fi
if echo 'the brief ranks on pillar overlap only' | grep -qF "$SCORE_RANK_LIT"; then
I16J_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the composite-rank pointer"
fi
if [ "$I16J_SELFTEST_OK" -eq 1 ]; then
pass "trends-score self-test: composite-rank predicate detects wiring, rejects the under-wired form"
else
fail "trends-score self-test failed — the score-wiring lint is vacuous or over-eager"
fi
if grep -qF "$SCORE_IFACE_LIT" scripts/trends/src/score.ts; then
pass "score.ts exports the persist envelope ('$SCORE_IFACE_LIT')"
else
fail "score.ts has no TrendScore envelope — add '$SCORE_IFACE_LIT' (RE-R3a persist)"
fi
if grep -qF "$SCORE_TYPE_LIT" scripts/trends/src/types.ts; then
pass "types.ts carries the score on the record ('$SCORE_TYPE_LIT')"
else
fail "types.ts does not carry the score — add '$SCORE_TYPE_LIT' to TrendRecord (RE-R3a schema v3)"
fi
if grep -qF "$SCORE_DIMS_LIT" agents/trend-spotter.md; then
pass "trend-spotter.md carries the judgment in the capture batch ($SCORE_DIMS_LIT)"
else
fail "trend-spotter.md does not carry the judgment — add the per-item $SCORE_DIMS_LIT to Step 4.5 (RE-R3a wiring)"
fi
if grep -qF "$SCORE_RANK_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts ranks on the composite ('$SCORE_RANK_LIT')"
else
fail "brief.ts does not rank on the composite — add the '$SCORE_RANK_LIT' comparator term (RE-R3a payoff)"
fi
echo ""
# --- Section 16k: Trends Lifecycle Wiring (research-engine RE-R3b) ---
echo "--- Trends Lifecycle Wiring ---"
# RE-R3b adds the trend lifecycle: re-score on re-capture, a status (new/acted/skipped) the brief
# EXCLUDES, and a seen-log (surfacedCount/lastSurfacedAt) the brief records. Five literals must hold,
# grepped EXACT (grep -F), deps-absent-safe (pure grep, no tsx):
# (1) types.ts declares the status type, by 'export type TrendStatus';
# (2) types.ts carries the seen-log field, by 'surfacedCount';
# (3) store.ts owns the seen-log writer, by 'export function markSurfaced';
# (4) brief.ts excludes handled trends, by 'effectiveStatus' (the filter is wired, not doc'd);
# (5) cli.ts exposes the lifecycle verb, by 'command === "act"'.
# Non-vacuity self-test mirrors Section 16j: the brief-filter predicate must accept a probe carrying
# the effectiveStatus pointer and reject one without it. Placed after Section 16j / before Section 18
# (anti-erosion must run last so it sees every prior check). UNCONDITIONAL (no tsx) -> counts toward
# ASSERT_BASELINE_FLOOR.
LIFECYCLE_STATUS_LIT='export type TrendStatus'
LIFECYCLE_SEEN_LIT='surfacedCount'
LIFECYCLE_MARK_LIT='export function markSurfaced'
LIFECYCLE_FILTER_LIT='effectiveStatus'
LIFECYCLE_VERB_LIT='command === "act"'
I16K_SELFTEST_OK=1
if ! echo 'rankForBrief drops a trend when effectiveStatus(trend) !== "new"' | grep -qF "$LIFECYCLE_FILTER_LIT"; then
I16K_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired status-filter probe was not detected"
fi
if echo 'the brief shows every record regardless of status' | grep -qF "$LIFECYCLE_FILTER_LIT"; then
I16K_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the status-filter pointer"
fi
if [ "$I16K_SELFTEST_OK" -eq 1 ]; then
pass "trends-lifecycle self-test: status-filter predicate detects wiring, rejects the unfiltered form"
else
fail "trends-lifecycle self-test failed — the lifecycle-wiring lint is vacuous or over-eager"
fi
if grep -qF "$LIFECYCLE_STATUS_LIT" scripts/trends/src/types.ts; then
pass "types.ts declares the lifecycle status type ('$LIFECYCLE_STATUS_LIT')"
else
fail "types.ts has no TrendStatus — add '$LIFECYCLE_STATUS_LIT' (RE-R3b lifecycle)"
fi
if grep -qF "$LIFECYCLE_SEEN_LIT" scripts/trends/src/types.ts; then
pass "types.ts carries the seen-log field ('$LIFECYCLE_SEEN_LIT')"
else
fail "types.ts does not carry the seen-log — add '$LIFECYCLE_SEEN_LIT' to TrendRecord (RE-R3b schema v4)"
fi
if grep -qF "$LIFECYCLE_MARK_LIT" scripts/trends/src/store.ts; then
pass "store.ts owns the seen-log writer ('$LIFECYCLE_MARK_LIT')"
else
fail "store.ts has no markSurfaced — add '$LIFECYCLE_MARK_LIT' (RE-R3b seen-log)"
fi
if grep -qF "$LIFECYCLE_FILTER_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts excludes handled trends ('$LIFECYCLE_FILTER_LIT')"
else
fail "brief.ts does not exclude handled trends — wire '$LIFECYCLE_FILTER_LIT' into rankForBrief (RE-R3b)"
fi
if grep -qF "$LIFECYCLE_VERB_LIT" scripts/trends/src/cli.ts; then
pass "cli.ts exposes the lifecycle verb ('$LIFECYCLE_VERB_LIT')"
else
fail "cli.ts has no act/skip/reset — add '$LIFECYCLE_VERB_LIT' (RE-R3b lifecycle verbs)"
fi
echo ""
# --- Section 16l: Trends Scheduler / Headless Wiring (research-engine RE-R3c) ---
echo "--- Trends Scheduler / Headless Wiring ---"
# RE-R3c adds the autonomous trigger + headless entry: a `schedule` CLI verb emitting a launchd plist /
# cron line (print-first), and a bash wrapper that runs the DETERMINISTIC brief headless. Five literals
# must hold, grepped EXACT (grep -F), deps-absent-safe (pure grep, no tsx):
# (1) schedule.ts emits the launchd plist, by 'export function launchdPlist';
# (2) schedule.ts emits the cron line, by 'export function crontabLine';
# (3) cli.ts exposes the schedule verb, by 'command === "schedule"';
# (4) run-daily.sh runs the deterministic brief, by 'cli.ts" brief' (the wrapper owns the subcommand);
# (5) run-daily.sh uses the data-path twin seam, by 'LINKEDIN_STUDIO_DATA:-'.
# Non-vacuity self-test mirrors Section 16k: a probe carrying the launchdPlist pointer is accepted and
# one without it rejected. Placed after Section 16k / before Section 18 (anti-erosion must run last so it
# sees every prior check). UNCONDITIONAL (no tsx) -> counts toward ASSERT_BASELINE_FLOOR.
SCHED_PLIST_LIT='export function launchdPlist'
SCHED_CRON_LIT='export function crontabLine'
SCHED_VERB_LIT='command === "schedule"'
SCHED_BRIEF_LIT='cli.ts" brief'
SCHED_DATA_LIT='LINKEDIN_STUDIO_DATA:-'
I16L_SELFTEST_OK=1
if ! echo 'a wired scheduler declares: export function launchdPlist(spec)' | grep -qF "$SCHED_PLIST_LIT"; then
I16L_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired scheduler-emit probe was not detected"
fi
if echo 'an unwired module emits no artifact at all' | grep -qF "$SCHED_PLIST_LIT"; then
I16L_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the scheduler-emit pointer"
fi
if [ "$I16L_SELFTEST_OK" -eq 1 ]; then
pass "trends-scheduler self-test: the emit pointer is detected, the no-emit form rejected"
else
fail "trends-scheduler self-test failed — the scheduler-wiring lint is vacuous or over-eager"
fi
if grep -qF "$SCHED_PLIST_LIT" scripts/trends/src/schedule.ts; then
pass "schedule.ts emits the launchd plist ('$SCHED_PLIST_LIT')"
else
fail "schedule.ts has no launchd emitter — add '$SCHED_PLIST_LIT' (RE-R3c scheduler)"
fi
if grep -qF "$SCHED_CRON_LIT" scripts/trends/src/schedule.ts; then
pass "schedule.ts emits the cron line ('$SCHED_CRON_LIT')"
else
fail "schedule.ts has no cron-line emitter — add '$SCHED_CRON_LIT' (RE-R3c scheduler)"
fi
if grep -qF "$SCHED_VERB_LIT" scripts/trends/src/cli.ts; then
pass "cli.ts exposes the schedule verb ('$SCHED_VERB_LIT')"
else
fail "cli.ts has no schedule verb — add '$SCHED_VERB_LIT' (RE-R3c trigger)"
fi
if grep -qF "$SCHED_BRIEF_LIT" scripts/trends/run-daily.sh; then
pass "run-daily.sh runs the deterministic brief ('$SCHED_BRIEF_LIT')"
else
fail "run-daily.sh does not run the brief — wire '$SCHED_BRIEF_LIT' (RE-R3c headless entry)"
fi
if grep -qF "$SCHED_DATA_LIT" scripts/trends/run-daily.sh; then
pass "run-daily.sh uses the data-path twin seam ('$SCHED_DATA_LIT')"
else
fail "run-daily.sh has no data-path seam — add '$SCHED_DATA_LIT' (RE-R3c fourth twin)"
fi
echo ""
# --- Section 16m: Trends Temporal Overlay (research-engine RE-R3d) ---
echo "--- Trends Temporal Overlay ---"
# RE-R3d adds the DERIVED temporal overlay (first-mover + saturation) to the morning-brief ranking:
# a pure temporalSignal in brief.ts, a new cmp key, and two tunable CLI flags. Five literals must
# hold, grepped EXACT (grep -F), deps-absent-safe (pure grep, no tsx); ASCII-only (bash 3.2 set -u):
# (1) brief.ts exports the signal, by 'export function temporalSignal';
# (2) brief.ts ranks on it, by 'b.temporal.rank' (the cmp key);
# (3) brief.ts declares the first-mover tier, by '"first-mover"';
# (4) cli.ts exposes the first-mover threshold flag, by 'first-mover-days';
# (5) cli.ts exposes the saturation threshold flag, by 'saturation-at'.
# Non-vacuity self-test mirrors Section 16l. Placed after Section 16l / before Section 18 (anti-erosion
# must run last so it sees every prior check). UNCONDITIONAL (no tsx) -> counts toward ASSERT_BASELINE_FLOOR.
TEMP_SIGNAL_LIT='export function temporalSignal'
TEMP_RANK_LIT='b.temporal.rank'
TEMP_TIER_LIT='"first-mover"'
TEMP_FMDAYS_LIT='first-mover-days'
TEMP_SATAT_LIT='saturation-at'
I16M_SELFTEST_OK=1
if ! echo 'a wired overlay declares: export function temporalSignal(ageDays)' | grep -qF "$TEMP_SIGNAL_LIT"; then
I16M_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired temporal-overlay probe was not detected"
fi
if echo 'an unwired module derives no temporal signal at all' | grep -qF "$TEMP_SIGNAL_LIT"; then
I16M_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the temporal-overlay pointer"
fi
if [ "$I16M_SELFTEST_OK" -eq 1 ]; then
pass "trends-temporal self-test: the signal pointer is detected, the no-signal form rejected"
else
fail "trends-temporal self-test failed — the temporal-overlay lint is vacuous or over-eager"
fi
if grep -qF "$TEMP_SIGNAL_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts derives the temporal signal ('$TEMP_SIGNAL_LIT')"
else
fail "brief.ts has no temporal signal — add '$TEMP_SIGNAL_LIT' (RE-R3d overlay)"
fi
if grep -qF "$TEMP_RANK_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts ranks on the temporal overlay ('$TEMP_RANK_LIT')"
else
fail "brief.ts cmp does not use the temporal rank — add '$TEMP_RANK_LIT' (RE-R3d ranking)"
fi
if grep -qF "$TEMP_TIER_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts declares the first-mover tier ('$TEMP_TIER_LIT')"
else
fail "brief.ts has no first-mover tier — add '$TEMP_TIER_LIT' (RE-R3d tiers)"
fi
if grep -qF "$TEMP_FMDAYS_LIT" scripts/trends/src/cli.ts; then
pass "cli.ts exposes the first-mover-days flag ('$TEMP_FMDAYS_LIT')"
else
fail "cli.ts has no first-mover-days flag — add '$TEMP_FMDAYS_LIT' (RE-R3d brief flag)"
fi
if grep -qF "$TEMP_SATAT_LIT" scripts/trends/src/cli.ts; then
pass "cli.ts exposes the saturation-at flag ('$TEMP_SATAT_LIT')"
else
fail "cli.ts has no saturation-at flag — add '$TEMP_SATAT_LIT' (RE-R3d brief flag)"
fi
echo ""
# --- Section 16n: Trends Brief History / Diff (research-engine RE-R3e) ---
echo "--- Trends Brief History / Diff ---"
# RE-R3e adds the day-over-day brief diff: a 'surfaced:' frontmatter record + three pure helpers
# (diffSurfaced / parseSurfacedFrontmatter / selectPriorBriefFile) in brief.ts, wired at the cli.ts
# edge, rendering a 'Nytt siden sist' section. Five literals must hold, grepped EXACT (grep -F),
# deps-absent-safe (pure grep, no tsx); ASCII-only (bash 3.2 set -u — the section's emoji is NEVER
# grepped, only its ASCII tail 'Nytt siden sist'):
# (1) brief.ts exports the diff, by 'export function diffSurfaced';
# (2) brief.ts parses the prior record, by 'parseSurfacedFrontmatter';
# (3) brief.ts renders the section, by 'Nytt siden sist' (the ASCII header tail);
# (4) cli.ts wires prior-discovery, by 'selectPriorBriefFile';
# (5) brief.ts emits the membership record, by 'surfaced: ' (the frontmatter line).
# Non-vacuity self-test mirrors Section 16m. Placed after Section 16m / before Section 18 (anti-erosion
# must run last so it sees every prior check). UNCONDITIONAL (no tsx) -> counts toward ASSERT_BASELINE_FLOOR.
DIFF_FN_LIT='export function diffSurfaced'
DIFF_PARSE_LIT='parseSurfacedFrontmatter'
DIFF_SECTION_LIT='Nytt siden sist'
DIFF_SELECT_LIT='selectPriorBriefFile'
DIFF_SURFACED_LIT='surfaced: '
I16N_SELFTEST_OK=1
if ! echo 'a wired diff declares: export function diffSurfaced(currentIds, priorIds)' | grep -qF "$DIFF_FN_LIT"; then
I16N_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired brief-diff probe was not detected"
fi
if echo 'an unwired module computes no day-over-day diff at all' | grep -qF "$DIFF_FN_LIT"; then
I16N_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the brief-diff pointer"
fi
if [ "$I16N_SELFTEST_OK" -eq 1 ]; then
pass "trends-brief-diff self-test: the diff pointer is detected, the no-diff form rejected"
else
fail "trends-brief-diff self-test failed — the brief-diff lint is vacuous or over-eager"
fi
if grep -qF "$DIFF_FN_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts exports the day-over-day diff ('$DIFF_FN_LIT')"
else
fail "brief.ts has no diff — add '$DIFF_FN_LIT' (RE-R3e brief history)"
fi
if grep -qF "$DIFF_PARSE_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts parses the prior surfaced record ('$DIFF_PARSE_LIT')"
else
fail "brief.ts cannot read a prior brief — add '$DIFF_PARSE_LIT' (RE-R3e)"
fi
if grep -qF "$DIFF_SECTION_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts renders the diff section ('$DIFF_SECTION_LIT')"
else
fail "brief.ts has no diff section — add '$DIFF_SECTION_LIT' (RE-R3e section header)"
fi
if grep -qF "$DIFF_SELECT_LIT" scripts/trends/src/cli.ts; then
pass "cli.ts wires prior-brief discovery ('$DIFF_SELECT_LIT')"
else
fail "cli.ts does not discover the prior brief — add '$DIFF_SELECT_LIT' (RE-R3e wiring)"
fi
if grep -qF "$DIFF_SURFACED_LIT" scripts/trends/src/brief.ts; then
pass "brief.ts emits the surfaced: membership record ('$DIFF_SURFACED_LIT')"
else
fail "brief.ts records no membership — add the 'surfaced:' frontmatter line (RE-R3e)"
fi
echo ""
# --- Section 16o: Voice Chain — chronicle-voice-profile binding (N8 / C-1,C-2,C-3,A2-F1,A2-F2) ---
echo "--- Voice Chain (N8) ---"
# N8 binds the long-form VOICE before prose — the same discipline the line already applies to
# SOURCES (validate-binding), its strongest link. Five invariants must hold in the .md/.json surfaces:
# (C-1/A2-F1) voice-trainer.md distils a chronicle-voice-profile from the PUBLISHED gold corpus, and
# newsletter.md binds drafting (Step 0/3a/3b) to THAT profile — not to the English short-form
# authentic-voice-samples corpus the engine itself calls degrading for chronicle voice.
# (C-2) Step 3a READS the chronicle-voice-drift-log as a Pass-0 negative list (today write-only).
# (C-3) Step 1 RESOLVES + persists the review `language`, making the edition-state doc-string's
# "Resolved at Step 1" claim true, so the Norwegian axis fires when content IS Norwegian.
# (A2-F2) Step 8 lock appends the locked edition to the gold corpus (ingest/published,
# provenance=published) so the corpus the profile distils from grows automatically.
# All are UNCONDITIONAL .md/.json greps (no TS deps → they raise the deps-absent floor). Auto-gull needs
# BOTH gold-corpus literals; a non-vacuity self-test guards that compound predicate (mirrors Section 16c).
CVP_LIT='chronicle-voice-profile'
DRIFTLOG_LIT='chronicle-voice-drift-log'
LANG_RESOLVE_LIT='Resolve the review language'
GOLD_PATH_LIT='ingest/published'
GOLD_PROV_LIT='provenance=published'
autogull_wired() { # $1 = text; wired iff BOTH gold-corpus literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$GOLD_PATH_LIT" && echo "$1" | grep -qF "$GOLD_PROV_LIT"
}
VC_SELFTEST_OK=1
if ! autogull_wired "lock appends the edition to ingest/published with provenance=published"; then
VC_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired auto-gull probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if autogull_wired "$probe"; then
VC_SELFTEST_OK=0; echo " false-positive FAIL: under-wired auto-gull probe accepted -> $probe"
fi
done <<'NEGATIVE16O'
lock appends the edition to ingest/published/ but never tags provenance
marks provenance=published but never names the gold-corpus path
NEGATIVE16O
if [ "$VC_SELFTEST_OK" -eq 1 ]; then
pass "voice-chain self-test: auto-gull predicate needs BOTH gold-corpus literals (1 accepted, 2 under-wired rejected)"
else
fail "voice-chain self-test failed — the auto-gull lint is vacuous or over-eager"
fi
NL="commands/newsletter.md"
VT_N8="agents/voice-trainer.md"
# (C-1/A2-F1) chronicle-voice-profile artifact generated + bound
if grep -qF "$CVP_LIT" "$VT_N8"; then
pass "voice-trainer.md distils the $CVP_LIT (per-language gold-corpus voice artifact)"
else
fail "voice-trainer.md missing '$CVP_LIT' — the chronicle voice artifact is never generated (C-1/A2-F1)"
fi
if grep -qF "$CVP_LIT" "$NL"; then
pass "newsletter.md binds long-form drafting to the $CVP_LIT (C-1)"
else
fail "newsletter.md missing '$CVP_LIT' — drafting still binds only to the short-form corpus (C-1)"
fi
# (C-2) drift-log READ (Pass 0 negative list), not just written
if grep -qF "$DRIFTLOG_LIT" "$NL"; then
pass "newsletter.md reads the $DRIFTLOG_LIT as a Pass-0 negative list (C-2)"
else
fail "newsletter.md missing '$DRIFTLOG_LIT' — the drift log is still write-only (C-2)"
fi
# (C-3) language resolved + persisted at Step 1
if grep -qF "$LANG_RESOLVE_LIT" "$NL"; then
pass "newsletter.md Step 1 resolves + persists the review language ('$LANG_RESOLVE_LIT', C-3)"
else
fail "newsletter.md missing the Step 1 language-resolution instruction ('$LANG_RESOLVE_LIT', C-3)"
fi
# (A2-F2) auto-gull at Step 8 lock — the gold corpus grows on its own
if autogull_wired "$(cat "$NL")"; then
pass "newsletter.md Step 8 auto-gull: locked edition appended to the gold corpus ('$GOLD_PATH_LIT' + '$GOLD_PROV_LIT', A2-F2)"
else
fail "newsletter.md missing auto-gull — lock does not grow the gold corpus (needs '$GOLD_PATH_LIT' + '$GOLD_PROV_LIT', A2-F2)"
fi
echo ""
# --- Section 16p: Randsone-Gates - feed-facing surfaces + deterministic fact-recheck + WebFetch (N9 / C-4,C-6,C-7,C-9) ---
echo "--- Randsone Gates (N9) ---"
# N9 closes the border-zone gap: the hook (delingstekst), the samle post, the carousel slide text and the
# edition title reach the feed today with NO fact/language gate (Step 5/6.5 run on the article BODY;
# carousel is authored in Step 7.5 AFTER the last content gate; Step 9 judged only the hook's click-worth).
# Four invariants must hold in the .md surfaces:
# (C-4/C-9) Step 9's post-lock gate is extended with a scoped fact-reviewer + language-reviewer pass over
# all four distribution surfaces (delingstekst hook, samle post, carousel slide text, edition
# title candidates). Step 9 is the first phase where all four exist, and it already operates on
# post-lock distribution copy the gate may revise without reopening the locked body.
# (C-6) any post-factcheck fold-in whose diff touches a sentence with numbers, names or quotes fires a
# mandatory scoped fact-reviewer mini-recheck - a DETERMINISTIC trigger ("not a judgment call"),
# which also covers the pivot-heuristic blind spot (a swapped number no >20% rule would catch).
# (C-7) both fact agents (fact-checker, fact-reviewer) carry WebFetch - quote precision needs the
# verbatim source text, not just a search snippet.
# The randsone predicate is compound (both reviewers AND both new surfaces), so a non-vacuity self-test
# guards it (mirrors Section 16o). All checks are UNCONDITIONAL .md greps -> they raise the deps-absent floor.
NL_N9="commands/newsletter.md"
# Slice the Step 9 block (## Step 9: up to ## Step 10:) so the gate literals are scoped to that phase.
STEP9_BLOCK=$(awk '/^## Step 9:/{f=1} /^## Step 10:/{f=0} f' "$NL_N9")
randsone_gated() { # $1 = text; wired iff BOTH reviewers AND BOTH new surfaces are named
echo "$1" | grep -qF "fact-reviewer" \
&& echo "$1" | grep -qF "language-reviewer" \
&& echo "$1" | grep -qF "carousel slide text" \
&& echo "$1" | grep -qF "edition title"
}
RZ_SELFTEST_OK=1
if ! randsone_gated "scoped fact-reviewer + language-reviewer over the carousel slide text and edition title candidates"; then
RZ_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired randsone-gate probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if randsone_gated "$probe"; then
RZ_SELFTEST_OK=0; echo " false-positive FAIL: under-wired randsone-gate probe accepted -> $probe"
fi
done <<'NEGATIVE16P'
language-reviewer over the carousel slide text and edition title but no fact reviewer here
fact-reviewer + language-reviewer over the edition title only, no carousel surface
fact-reviewer + language-reviewer over the carousel slide text only, no title surface
NEGATIVE16P
if [ "$RZ_SELFTEST_OK" -eq 1 ]; then
pass "randsone-gate self-test: predicate needs BOTH fact+language reviewers AND both new surfaces (1 accepted, 3 under-wired rejected)"
else
fail "randsone-gate self-test failed - the randsone lint is vacuous or over-eager"
fi
# (C-4/C-9) Step 9 gates all four distribution surfaces through fact + language reviewers
if randsone_gated "$STEP9_BLOCK"; then
pass "newsletter.md Step 9 runs the scoped fact-reviewer + language-reviewer randsone gate over carousel + title (C-4/C-9)"
else
fail "newsletter.md Step 9 missing the randsone fact+language gate - hook/samle/carousel/title still reach the feed ungated (C-4/C-9)"
fi
# (C-9) the edition's own title now faces a strength gate at Step 9 (today only the hook's two lines were judged)
if echo "$STEP9_BLOCK" | grep -qF "title-strength"; then
pass "newsletter.md Step 9 gates the edition title-strength, not only the hook (C-9)"
else
fail "newsletter.md Step 9 missing the title-strength gate - the edition title still ships unjudged (C-9)"
fi
# (C-6) deterministic post-factcheck fact-recheck trigger (nouns + mechanism + determinism marker)
if grep -qF "numbers, names, or quotes" "$NL_N9"; then
pass "newsletter.md declares the deterministic recheck trigger nouns ('numbers, names, or quotes', C-6)"
else
fail "newsletter.md missing the recheck trigger nouns 'numbers, names, or quotes' (C-6)"
fi
if grep -qF "scoped fact-reviewer" "$NL_N9"; then
pass "newsletter.md routes the post-factcheck recheck to a scoped fact-reviewer (C-6)"
else
fail "newsletter.md missing 'scoped fact-reviewer' - the recheck has no mechanism (C-6)"
fi
if grep -qF "not a judgment call" "$NL_N9"; then
pass "newsletter.md marks the recheck trigger deterministic ('not a judgment call', C-6)"
else
fail "newsletter.md missing the determinism marker 'not a judgment call' - the trigger is still discretionary (C-6)"
fi
# (C-7) both fact agents carry WebFetch (verbatim source text for quote precision)
if grep -qF "WebFetch" agents/fact-checker.md; then
pass "fact-checker.md carries WebFetch (verbatim source text for quote precision, C-7)"
else
fail "fact-checker.md missing WebFetch in tools - quote precision has no verbatim-source fetch (C-7)"
fi
if grep -qF "WebFetch" agents/fact-reviewer.md; then
pass "fact-reviewer.md carries WebFetch (verbatim source text for quote precision, C-7)"
else
fail "fact-reviewer.md missing WebFetch in tools - cold re-verification has no verbatim-source fetch (C-7)"
fi
echo ""
# --- Section 16q: Configurable Target-Level Span (N10 / C-8) ---
echo "--- Configurable Target-Level Span (N10) ---"
# N10 turns "practically usable at all levels" into a CONFIGURABLE gate axis and removes the
# hardcoded primary persona. Six .md/.json surfaces must hold the invariants (all UNCONDITIONAL
# greps -> they raise the deps-absent floor):
# (C-8 absence) the hardcoded primary "non-technical line manager" is GONE from references/ + config/
# — the quality-rule now points at the edition's CONFIGURED target-level span instead.
# (C-8 span-ref) references/longform-quality-rules.md names the "target-level span" (domain-general),
# not a single baked-in reader role.
# (C-8 obligatorisk-sekundær) config/personas.template.md's «primær trumfer» rule now REQUIRES a
# mandatory secondary at the opposite end of the span; a secondary NO may be waived
# as «signal, not failure» ONLY with an explicit ceiling justification.
# (C-8 tekniker-seed) the seed library gains a technical-end persona (today all three seeds are leaders).
# (C-8 edition-state) config/edition-state.template.json carries an additive per-article targetLevel slot.
# (C-8 persist) commands/newsletter.md persists the resolved targetLevel to articles.NN.targetLevel.
# (C-8 consume) agents/persona-reviewer.md READS articles.NN.targetLevel and places personas on the span
# — a compound predicate (targetLevel field + edition-state read + span placement), so a
# non-vacuity self-test guards it (mirrors Section 16p).
PR_N10="agents/persona-reviewer.md"
targetlevel_consumed() { # $1 = text; wired iff it reads targetLevel FROM edition-state AND places on the span
echo "$1" | grep -qF "targetLevel" \
&& echo "$1" | grep -qF "articles." \
&& echo "$1" | grep -qF "target-level span"
}
TL_SELFTEST_OK=1
if ! targetlevel_consumed "read articles.NN.targetLevel and place the personas on the target-level span"; then
TL_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired targetLevel-consumption probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if targetlevel_consumed "$probe"; then
TL_SELFTEST_OK=0; echo " false-positive FAIL: under-wired targetLevel probe accepted -> $probe"
fi
done <<'NEGATIVE16Q'
place the personas on the target-level span but never read the field
read articles.NN.targetLevel but do not mention the span placement here
the targetLevel matters for the target-level span without an edition-state read
NEGATIVE16Q
if [ "$TL_SELFTEST_OK" -eq 1 ]; then
pass "targetLevel-consumption self-test: predicate needs targetLevel + edition-state read (articles.) + span placement (1 accepted, 3 under-wired rejected)"
else
fail "targetLevel-consumption self-test failed - the N10 consumption lint is vacuous or over-eager"
fi
# (C-8 absence) the hardcoded primary persona is gone from the shippable surfaces
if grep -rn "non-technical line manager" references/ config/ >/dev/null 2>&1; then
fail "N10: hardcoded 'non-technical line manager' still present in references/ or config/ — the primary persona is not de-hardcoded (C-8)"
else
pass "N10: no hardcoded 'non-technical line manager' in references/ or config/ (C-8 absence)"
fi
# (C-8 span-ref) the quality rule points at the configured span, not a baked-in role
if grep -qF "target-level span" references/longform-quality-rules.md; then
pass "references/longform-quality-rules.md names the configured 'target-level span' (C-8 span-ref)"
else
fail "references/longform-quality-rules.md missing 'target-level span' — the served-reader rule is still role-hardcoded (C-8)"
fi
# (C-8 obligatorisk-sekundær) the «primær trumfer» rule requires a mandatory secondary at the opposite end
if grep -qF "opposite end of the target-level span" config/personas.template.md; then
pass "config/personas.template.md requires a mandatory secondary at the opposite end of the span (C-8 obligatorisk-sekundær)"
else
fail "config/personas.template.md missing the 'opposite end of the target-level span' requirement — one reader can still self-certify all levels (C-8)"
fi
# (C-8 ceiling-begrunnelse) a secondary NO may only be waived with an explicit ceiling justification
if grep -qF "explicit ceiling justification" config/personas.template.md; then
pass "config/personas.template.md gates the secondary-NO waiver behind an explicit ceiling justification (C-8 ceiling-begrunnelse)"
else
fail "config/personas.template.md missing 'explicit ceiling justification' — a secondary NO can still be waved away as 'signal, not failure' unproven (C-8)"
fi
# (C-8 tekniker-seed) the seed library gains a technical-end persona (today all three seeds are leaders)
if grep -qF "Technical end of the target-level span" config/personas.template.md; then
pass "config/personas.template.md seeds a technical-end persona opposite the leader personas (C-8 tekniker-seed)"
else
fail "config/personas.template.md missing a 'Technical end of the target-level span' seed — the library is still leader-only (C-8)"
fi
# (C-8 edition-state) the per-article targetLevel SLOT exists (additive, no schemaVersion bump).
# Grep the slot's default literal `"targetLevel": null`, NOT the bare word — the latter already occurs
# inside the sourceTrendId doc-string ("angle / targetLevel / key-points"), which would match vacuously.
if grep -qF '"targetLevel": null' config/edition-state.template.json; then
pass "config/edition-state.template.json carries the additive articles.NN.targetLevel slot (default null, C-8 edition-state)"
else
fail "config/edition-state.template.json missing the '\"targetLevel\": null' slot — persona-reviewer would consume a phantom field (C-8)"
fi
# (C-8 persist) newsletter.md persists the resolved targetLevel to the per-article slot
if grep -qF "articles.NN.targetLevel" commands/newsletter.md; then
pass "commands/newsletter.md persists the resolved targetLevel to articles.NN.targetLevel (C-8 persist)"
else
fail "commands/newsletter.md never persists articles.NN.targetLevel — the slot stays null and consumption is vacuous (C-8)"
fi
# (C-8 consume) persona-reviewer reads targetLevel from edition-state and places personas on the span
PR_CONTENT=$(cat "$PR_N10")
if targetlevel_consumed "$PR_CONTENT"; then
pass "agents/persona-reviewer.md reads articles.NN.targetLevel and places personas on the target-level span (C-8 consume)"
else
fail "agents/persona-reviewer.md does not consume articles.NN.targetLevel onto the span — targetLevel is set but never read (C-8)"
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 16s: Editions Register + phaseLog Telemetry (N12 / A1-11, A1-12) ---
echo "--- Editions Register (N12) ---"
# N12 makes two structurally invisible things visible: WHICH editions are in flight (until
# now readable only by opening every series folder's edition-state.json) and HOW LONG an
# edition actually takes (until now not recorded at all). The register is a MIRROR - only
# edition-state.json drives resumption - so the binding worth linting is that EVERY phase
# transition writes both the phaseLog entry and the register row, from ONE call:
# (A1-12 protocol) the transition protocol is defined once and is fully wired - a compound
# predicate (verb + logged field + mirrored store), so a non-vacuity
# self-test guards it (mirrors Sections 16p/16q/16r).
# (A1-12 coverage) every canonical phase calls it - a phase with no call is a silent hole
# in the lead-time log, which is exactly how telemetry dies.
# (A1-11 store) the register module + its data-dir placement exist.
# (A1-12 schema) edition-state carries the phaseLog slot the CLI appends to.
NL_N12="commands/newsletter.md"
REG_SRC="scripts/editions/src/register.ts"
EST_SRC="scripts/editions/src/editionState.ts"
EST_TPL="config/edition-state.template.json"
BT='`'
transition_protocol() { # $1 = text; wired iff it CALLS the verb, names the field it logs AND the store it mirrors to
echo "$1" | grep -qF "register-upsert" \
&& echo "$1" | grep -qF "phaseLog" \
&& echo "$1" | grep -qF "editions/register.json"
}
TP_SELFTEST_OK=1
if ! transition_protocol "run register-upsert --edition-state <path>; it appends articles.NN.phaseLog and mirrors the row into <data>/editions/register.json"; then
TP_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired transition-protocol probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if transition_protocol "$probe"; then
TP_SELFTEST_OK=0; echo " false-positive FAIL: under-wired transition probe accepted -> $probe"
fi
done <<'NEGATIVE16S'
run register-upsert at every transition and let phaseLog grow, store left unspecified
append articles.NN.phaseLog and mirror into editions/register.json, but nothing calls the verb
run register-upsert and write editions/register.json without logging the phase
NEGATIVE16S
if [ "$TP_SELFTEST_OK" -eq 1 ]; then
pass "transition-protocol self-test: predicate needs register-upsert + phaseLog + editions/register.json (1 accepted, 3 under-wired rejected)"
else
fail "transition-protocol self-test failed - the N12 wiring lint is vacuous or over-eager"
fi
# (A1-12 protocol) the protocol is defined once, in the resumption area, fully wired
PROTO_BLOCK=$(awk '/^### Phase-transition protocol/{f=1} /^## Step 1:/{f=0} f' "$NL_N12")
if transition_protocol "$PROTO_BLOCK"; then
pass "newsletter defines a wired phase-transition protocol (register-upsert appends phaseLog + mirrors the register) (A1-11/A1-12)"
else
fail "newsletter has no wired phase-transition protocol - phase telemetry falls back to the model remembering (A1-12)"
fi
# (A1-11 invariant) the mirror rule is stated where the protocol is defined - a register read
# as source-of-truth would break deterministic resumption, the one thing edition-state owns
if echo "$PROTO_BLOCK" | grep -qF "MIRROR, never a source of truth"; then
pass "newsletter states the mirror invariant - resumption reads edition-state only (A1-11)"
else
fail "newsletter does not state that the register is a mirror - it can drift into a second source of truth (A1-11)"
fi
# (A1-12 coverage) every canonical phase transition calls the protocol
NL_FLAT=$(tr '\n' ' ' < "$NL_N12" | tr -s ' ')
MISSING_PHASES=""
for phase in lived-specifics research skeleton-pitch spine-prose draft consistency-quality \
contract-gate factcheck-sweep editorial-review persona-sweep-prelock \
headless-review annotation visual-assets lock-delivery hook-conversion-gate scheduling \
retro; do
if ! printf '%s' "$NL_FLAT" | grep -qF "run the protocol (Step 0) for ${BT}${phase}${BT}"; then
MISSING_PHASES="$MISSING_PHASES $phase"
fi
done
if [ -z "$MISSING_PHASES" ]; then
pass "all 17 canonical phases carry a transition call - the lead-time log has no holes (A1-12)"
else
fail "newsletter phases with no transition call:$MISSING_PHASES - lead time silently undercounts them (A1-12)"
fi
# (A1-12 close) Step 10 closes the row - without it every edition stays 'in flight' forever
STEP10_BLOCK=$(awk '/^## Step 10:/{f=1} /^## Distribution channel/{f=0} f' "$NL_N12")
if echo "$STEP10_BLOCK" | grep -qF "register-complete"; then
pass "newsletter Step 10 closes the register row (register-complete) - lead time becomes a measured number (A1-12)"
else
fail "newsletter Step 10 never calls register-complete - editions never leave the in-flight view (A1-12)"
fi
# (A1-11 store) the register module exists with its mutations
if grep -qF "export function upsertEdition" "$REG_SRC"; then
pass "scripts/editions exports upsertEdition - the deterministic register write exists (A1-11)"
else
fail "$REG_SRC does not export upsertEdition (A1-11)"
fi
if grep -qF "export function completeEdition" "$REG_SRC" && grep -qF "export function listEditions" "$REG_SRC"; then
pass "scripts/editions exports completeEdition + listEditions - the WIP view and its close verb exist (A1-11)"
else
fail "$REG_SRC does not export both completeEdition and listEditions (A1-11)"
fi
# (A1-11 placement) the register lives in the per-user data dir (M0 convention), not the plugin
if grep -qF "LINKEDIN_STUDIO_DATA" "$REG_SRC" && grep -qF '"editions"' "$REG_SRC" && grep -qF '"register.json"' "$REG_SRC"; then
pass "editions register resolves under the per-user data dir - it survives plugin reinstalls (M0/A1-11)"
else
fail "$REG_SRC does not resolve the register under the data dir - WIP state placed wrong (M0/A1-11)"
fi
# (A1-12 telemetry) the phase log is appended by code, not by hand
if grep -qF "export function appendPhase" "$EST_SRC"; then
pass "scripts/editions exports appendPhase - phaseLog entries are written deterministically (A1-12)"
else
fail "$EST_SRC does not export appendPhase (A1-12)"
fi
# (A1-12 schema) edition-state documents AND carries the phaseLog slot
if grep -qF '"phaseLog": "Per-article phase-transition log' "$EST_TPL"; then
pass "edition-state template documents phaseLog (A1-12 schema)"
else
fail "$EST_TPL does not document phaseLog - the telemetry field is undefined (A1-12)"
fi
if grep -qF '"phaseLog": []' "$EST_TPL"; then
pass "edition-state template carries the per-article phaseLog slot (A1-12 schema)"
else
fail "$EST_TPL has no per-article phaseLog slot - new editions start unmeasurable (A1-12)"
fi
echo ""
# --- Section 16t: Publishing Slots + WIP Surfacing (N13 / A1-10) ---
echo "--- Publishing Slots (N13) ---"
# N13 answers the question a queue cannot: is the WEEK covered? The grid of publishing
# slots is operator config, coverage is queue + editions register, and the surfaces that
# report it (session start, router, calendar) plus the one that consumes it (newsletter
# Step 10 slot default) must all read the SAME module - two implementations of "next free
# slot" would disagree within a week, and the operator would learn to trust neither.
# (A1-10 wiring) a WIP surface loads slots.mjs and asks it for BOTH vacancy and
# in-flight editions - a compound predicate, so a non-vacuity
# self-test guards it (mirrors Sections 16p/16q/16r/16s).
# (A1-10 consume) Step 10 defaults to the next free slot AND claims it on the row.
# (A1-10 placement) the grid + register resolve under the per-user data dir (M0).
# (D-5) the shipped scheduling times are marked low/directional and defer
# to the operator's own grid.
# (de-niche) the plugin ships NO publishing times in the slot code path and no
# operator grid - the times are user config, always.
SLOTS_SRC="hooks/scripts/slots.mjs"
SS_SRC="hooks/scripts/session-start.mjs"
SLOTS_TPL="config/publishing-slots.template.json"
NL_N13="commands/newsletter.md"
SCHED_REF="references/scheduling-strategy.md"
slots_surface_wired() { # $1 = text; wired iff it LOADS the shared module and asks for BOTH vacancy and in-flight
echo "$1" | grep -qF "slots.mjs" \
&& echo "$1" | grep -qE "slotVacancies|nextFreeSlot" \
&& echo "$1" | grep -qF "activeEditions"
}
SL_SELFTEST_OK=1
if ! slots_surface_wired "import { slotVacancies, activeEditions } from hooks/scripts/slots.mjs and print both"; then
SL_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired slot-surface probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if slots_surface_wired "$probe"; then
SL_SELFTEST_OK=0; echo " false-positive FAIL: under-wired slot-surface probe accepted -> $probe"
fi
done <<'NEGATIVE16T'
import activeEditions from slots.mjs and list what is in production
call slotVacancies and activeEditions from somewhere, no module named
import slotVacancies from slots.mjs and report open slots only
NEGATIVE16T
if [ "$SL_SELFTEST_OK" -eq 1 ]; then
pass "slot-surface self-test: predicate needs slots.mjs + vacancy + activeEditions (1 accepted, 3 under-wired rejected)"
else
fail "slot-surface self-test failed - the N13 wiring lint is vacuous or over-eager"
fi
# (A1-10 wiring) the two WIP surfaces report capacity, not just a queue
if slots_surface_wired "$(cat commands/linkedin.md)"; then
pass "router surfaces work-in-progress + open slots from the shared module (A1-10)"
else
fail "commands/linkedin.md does not surface in-flight editions and open slots - the week stays invisible (A1-10)"
fi
if slots_surface_wired "$(cat commands/calendar.md)"; then
pass "calendar surfaces the slot grid + editions in flight from the shared module (A1-10)"
else
fail "commands/calendar.md does not read slots.mjs for vacancy + in-flight editions (A1-10)"
fi
# (A1-10 opt-in) the grid is offered, never written for the user
if grep -qF "publishing-slots.template.json" commands/calendar.md; then
pass "calendar documents the opt-in grid copy - the plugin imposes no schedule (A1-10/de-niche)"
else
fail "commands/calendar.md never points at the slot-grid template - the feature is undiscoverable (A1-10)"
fi
# (A1-10 session start) both blocks render at session start, from the same module
if grep -qF "from './slots.mjs'" "$SS_SRC" \
&& grep -qF "## Editions in Flight" "$SS_SRC" \
&& grep -qF "## Publishing Slots" "$SS_SRC"; then
pass "session start renders in-flight editions + publishing slots from slots.mjs (A1-10)"
else
fail "$SS_SRC does not render the week picture from slots.mjs - the operator must open files again (A1-10)"
fi
# (A1-10 coverage) vacancy counts BOTH short-form queue and long-form register
if grep -qF "export function coverageByDate" "$SLOTS_SRC" && grep -qF "readRegisterRows" "$SLOTS_SRC"; then
pass "slot coverage counts the queue AND the editions register - long-form cannot be double-booked (A1-10)"
else
fail "$SLOTS_SRC does not derive coverage from both the queue and the editions register (A1-10)"
fi
# (A1-10 placement) grid + register resolve under the per-user data dir (M0 convention)
if grep -qF "getDataRoot('profile')" "$SLOTS_SRC" && grep -qF "getDataRoot('editions')" "$SLOTS_SRC"; then
pass "slot grid + register resolve under the per-user data dir - they survive reinstalls (M0/A1-10)"
else
fail "$SLOTS_SRC does not resolve the grid and register under the data dir (M0/A1-10)"
fi
# (A1-10 consume) Step 10 defaults to the next free slot instead of interviewing
STEP10_N13=$(awk '/^## Step 10:/{f=1} /^## Distribution channel/{f=0} f' "$NL_N13")
if echo "$STEP10_N13" | grep -qF "nextFreeSlot"; then
pass "newsletter Step 10 defaults to the next free slot - scheduling is one confirmation (A1-10)"
else
fail "newsletter Step 10 still asks for the slot from scratch - the grid buys nothing (A1-10)"
fi
if echo "$STEP10_N13" | grep -qF -- "--slot"; then
pass "newsletter Step 10 claims the slot on the register row - the next session stops offering it (A1-10)"
else
fail "newsletter Step 10 never writes --slot - a scheduled edition still reads as an open slot (A1-10)"
fi
# (D-5) the shipped times are marked as what they are, and defer to the operator's grid
if grep -qF "low / directional" "$SCHED_REF"; then
pass "scheduling reference carries the low/directional epistemic marking (D-5)"
else
fail "$SCHED_REF presents practitioner lore as fact - mark it low / directional (D-5)"
fi
if grep -qF "publishing-slots.json" "$SCHED_REF"; then
pass "scheduling reference defers to the operator's own grid (D-5)"
else
fail "$SCHED_REF does not point at the operator grid - the tables read as the authority (D-5)"
fi
# (de-niche) the template exists and states that nothing is written for the user
if [ -f "$SLOTS_TPL" ] && grep -qF '"schemaVersion": 1' "$SLOTS_TPL" && grep -qF "OPT-IN" "$SLOTS_TPL"; then
pass "slot-grid template ships with a schema and an explicit opt-in (A1-10/de-niche)"
else
fail "$SLOTS_TPL missing, unversioned, or does not state the opt-in (A1-10/de-niche)"
fi
# (de-niche) no publishing times hardcoded in the slot code path, no operator grid committed
SLOT_TIME_LITERALS=$(grep -cE "['\"][0-2][0-9]:[0-5][0-9]['\"]" "$SLOTS_SRC" "$SS_SRC" 2>/dev/null | awk -F: '{s+=$2} END {print s+0}')
COMMITTED_GRIDS=$(find . -name "publishing-slots.json" -not -path "*/node_modules/*" 2>/dev/null | wc -l | tr -d ' ')
if [ "$SLOT_TIME_LITERALS" -eq 0 ] && [ "$COMMITTED_GRIDS" -eq 0 ]; then
pass "no publishing times hardcoded in the slot path and no operator grid committed (de-niche)"
else
fail "hardcoded slot times in the code path ($SLOT_TIME_LITERALS) or a committed operator grid ($COMMITTED_GRIDS) - times are user config (de-niche)"
fi
echo ""
# --- Section 16u: Fold-In Loop + Retro Step (N14 / A2-F6,F7,F8,F9, C-10) ---
echo "--- Fold-In Loop + Retro (N14) ---"
# N14 closes the loop the whole pipeline was missing: a correction the operator makes in
# edition N is ENFORCED in edition N+1 instead of being re-discovered. The machinery already
# existed (foldIns schema + promote->ratify); what was absent was the wiring - nothing wrote
# to the queue and no step ever emptied it. So the bindings worth linting are:
# (A2-F7 capture) every step where the operator corrects prose appends to the queue - a
# compound predicate (queue named + left open + routed), so a non-vacuity
# self-test guards it (mirrors Sections 16p/16q/16r/16s/16t).
# (A2-F8 retro) Step 11 exists and does all three jobs: promote the queue, summarize the
# measured effort (phaseLog, N12), ask ONE friction question.
# (A2-F6 headless) the cold package runs as a background agent writing to a file - the
# maximum-independence path with no manual copy-paste seam.
# (C-10 language) confirmed language corrections accumulate in a USER file the reviewer
# and the drafting step both read - the shipped ban-list stays the baseline.
# (A2-F9 adopter) the fang->klassifiser->promoter->handhev loop is documented in-tree, so
# an adopter without the author's contract still has the whole loop.
NL_N14="commands/newsletter.md"
HR_N14="commands/headless-review.md"
LR_N14="agents/language-reviewer.md"
EST_N14="config/edition-state.template.json"
LANG_TPL="config/language-rules.template.md"
FOLDIN_REF="references/fold-in-loop.md"
foldin_capture_wired() { # $1 = text; wired iff it NAMES the queue, leaves it OPEN, and ROUTES it
echo "$1" | grep -qF "foldIns" \
&& echo "$1" | grep -qF "pending" \
&& echo "$1" | grep -qF "Step 11"
}
FI_SELFTEST_OK=1
if ! foldin_capture_wired "append the correction to articles.NN.foldIns with decision pending; Step 11 routes it"; then
FI_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired fold-in-capture probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if foldin_capture_wired "$probe"; then
FI_SELFTEST_OK=0; echo " false-positive FAIL: under-wired fold-in-capture probe accepted -> $probe"
fi
done <<'NEGATIVE16U'
append the correction to articles.NN.foldIns with decision pending, routed by nobody
capture the correction as pending and let Step 11 route it, queue left unnamed
append it to articles.NN.foldIns and Step 11 promotes it, never marked open
NEGATIVE16U
if [ "$FI_SELFTEST_OK" -eq 1 ]; then
pass "fold-in-capture self-test: predicate needs foldIns + pending + Step 11 (1 accepted, 3 under-wired rejected)"
else
fail "fold-in-capture self-test failed - the N14 wiring lint is vacuous or over-eager"
fi
# (A2-F7 capture) each of the four fold-in steps feeds the queue - a step that folds a
# correction in without capturing it is exactly how a rule gets re-discovered next edition
STEP25_N14=$(awk '/^## Step 2.5:/{f=1} /^## Step 3a:/{f=0} f' "$NL_N14")
STEP3A_N14=$(awk '/^## Step 3a:/{f=1} /^## Step 3b:/{f=0} f' "$NL_N14")
STEP55_N14=$(awk '/^## Step 5.5:/{f=1} /^## Step 6:/{f=0} f' "$NL_N14")
STEP65_N14=$(awk '/^## Step 6.5:/{f=1} /^## Step 7:/{f=0} f' "$NL_N14")
STEP11_N14=$(awk '/^## Step 11:/{f=1} /^## Distribution channel/{f=0} f' "$NL_N14")
if foldin_capture_wired "$STEP25_N14"; then
pass "Step 2.5 captures operator corrections into the fold-in queue (A2-F7)"
else
fail "newsletter Step 2.5 folds annotations in without capturing them - the rule is lost (A2-F7)"
fi
if foldin_capture_wired "$STEP3A_N14"; then
pass "Step 3a captures operator corrections into the fold-in queue (A2-F7)"
else
fail "newsletter Step 3a folds annotations in without capturing them - the rule is lost (A2-F7)"
fi
if foldin_capture_wired "$STEP55_N14"; then
pass "Step 5.5 captures approved editorial flags into the fold-in queue (A2-F7)"
else
fail "newsletter Step 5.5 folds flags in without capturing them - the rule is lost (A2-F7)"
fi
if foldin_capture_wired "$STEP65_N14"; then
pass "Step 6.5 captures approved cold-review flags into the fold-in queue (A2-F7)"
else
fail "newsletter Step 6.5 folds flags in without capturing them - the rule is lost (A2-F7)"
fi
# (A2-F8 retro) the step exists and empties the queue with an explicit operator verdict
if echo "$STEP11_N14" | grep -qF "foldIns" && echo "$STEP11_N14" | grep -qF "promoted"; then
pass "Step 11 promotes the fold-in queue with an operator verdict (A2-F8)"
else
fail "newsletter has no Step 11 fold-in promotion - the queue fills up and is never emptied (A2-F8)"
fi
# (A2-F8 effort) the effort summary reads the MEASURED log (N12), not the model's memory
if echo "$STEP11_N14" | grep -qF "phaseLog"; then
pass "Step 11 summarizes effort from the measured phaseLog - no new measuring apparatus (A2-F8/A1-12)"
else
fail "newsletter Step 11 does not read phaseLog - effort is re-estimated from memory (A2-F8)"
fi
# (A2-F8 friction) exactly one friction question, and the operator's own register stays theirs
if echo "$STEP11_N14" | grep -qF "process friction" && echo "$STEP11_N14" | grep -qF "never write"; then
pass "Step 11 asks one friction question and never writes the operator's register (A2-F8)"
else
fail "newsletter Step 11 lacks the single friction question or claims write access to the operator's register (A2-F8)"
fi
# (A2-F8 transition) retro is a real phase - logged like every other, and it re-closes the row
if echo "$STEP11_N14" | grep -qF "register-complete" && echo "$STEP11_N14" | grep -qF "${BT}retro${BT}"; then
pass "Step 11 runs the retro transition and re-closes the register row (A1-11/A2-F8)"
else
fail "newsletter Step 11 leaves the edition re-opened in the register or logs no retro phase (A1-11/A2-F8)"
fi
# (A2-F8 schema) edition-state documents AND carries the retro slot
if grep -qF '"retro": "Per-article retro record' "$EST_N14" && grep -qF '"retro": null' "$EST_N14"; then
pass "edition-state documents and carries the per-article retro slot (A2-F8 schema)"
else
fail "$EST_N14 has no documented retro slot - the retro leaves no durable record (A2-F8)"
fi
# (A2-F8 resumption) an edition aborted after scheduling resumes INTO the retro, not past it
RESUME_N14=$(awk '/^### Deterministic resumption/{f=1} /^### Phase-transition protocol/{f=0} f' "$NL_N14")
if echo "$RESUME_N14" | grep -qF "Step 11" && echo "$RESUME_N14" | grep -qF "${BT}retro${BT}"; then
pass "resumption table routes scheduling -> Step 11 and retro -> complete (A2-F8)"
else
fail "newsletter resumption table skips the retro - an aborted session drops the fold-in queue (A2-F8)"
fi
# (A2-F6 headless) the cold package can run as a background agent writing its report to a file
if grep -qF "run_in_background" "$HR_N14" && grep -qF -- "--background" "$HR_N14"; then
pass "headless-review offers a background run - cold isolation without a fresh session (A2-F6)"
else
fail "$HR_N14 has no background mode - maximum independence still costs a manual session switch (A2-F6)"
fi
if grep -qF "no copy-paste" "$HR_N14"; then
pass "headless-review states the seam it removes - the report is read from disk, not pasted (A2-F6)"
else
fail "$HR_N14 still relies on pasting the report back into the drafting session (A2-F6)"
fi
# (A2-F6 wiring) Step 6.5 actually offers that path instead of the paste-back one
if echo "$STEP65_N14" | grep -qF -- "--background"; then
pass "newsletter Step 6.5 routes maximum-independence through the background run (A2-F6)"
else
fail "newsletter Step 6.5 still sends the operator to a fresh session with a paste-back (A2-F6)"
fi
# (C-10 language) the accumulating user file is READ by the reviewer and by the drafting step
if grep -qF "language-rules/" "$LR_N14"; then
pass "language-reviewer reads the accumulated user language rules (C-10)"
else
fail "$LR_N14 grades only against the shipped list - confirmed corrections never bind (C-10)"
fi
STEP4_N14=$(awk '/^## Step 4: /{f=1} /^## Step 4.5:/{f=0} f' "$NL_N14")
if echo "$STEP4_N14" | grep -qF "language-rules/"; then
pass "newsletter Step 4 reads the accumulated user language rules while drafting (C-10)"
else
fail "newsletter Step 4 ignores the accumulated language rules - they only catch, never prevent (C-10)"
fi
if echo "$STEP11_N14" | grep -qF "language-rules/"; then
pass "Step 11 appends confirmed language corrections to the user rules file (C-10)"
else
fail "newsletter Step 11 never writes the language-rules file - judgment corrections evaporate (C-10)"
fi
# (C-10 opt-in) the file is offered as a template, never written for the user (slots pattern)
if [ -f "$LANG_TPL" ] && grep -qF "OPT-IN" "$LANG_TPL"; then
pass "language-rules template ships with an explicit opt-in - nothing is imposed (C-10/de-niche)"
else
fail "$LANG_TPL missing or does not state the opt-in (C-10/de-niche)"
fi
# (A2-F9 adopter) the whole loop is documented in-tree, all four stages named
FOLDIN_LOOP_OK=1
for stage in "Capture" "Classify" "Promote" "Enforce"; do
grep -qF "$stage" "$FOLDIN_REF" 2>/dev/null || FOLDIN_LOOP_OK=0
done
if [ -f "$FOLDIN_REF" ] && [ "$FOLDIN_LOOP_OK" -eq 1 ]; then
pass "fold-in loop documented in-tree with all four stages (A2-F9)"
else
fail "$FOLDIN_REF missing or does not name capture/classify/promote/enforce (A2-F9)"
fi
# (A2-F9 self-contained) an adopter without the author's private contract still has the loop,
# and the four accumulation silos are named as one collection point
if grep -qF "not a gap" "$FOLDIN_REF" 2>/dev/null \
&& grep -qF "specifics-bank" "$FOLDIN_REF" 2>/dev/null \
&& grep -qF "ab-test" "$FOLDIN_REF" 2>/dev/null; then
pass "fold-in loop is adopter-self-contained and names the accumulation silos (A2-F9)"
else
fail "$FOLDIN_REF reads as requiring the author's private contract, or omits the silos (A2-F9)"
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 16w: Measure-Truth - Reach + Boundary Map (N16 / D-3, A2-F11, E#9) ---
echo "--- Measure-Truth: Reach + Boundary Map (N16) ---"
# LinkedIn split a post's impressions into in-network and out-of-network in June
# 2026 - the first native number that says whether a post ACQUIRED audience or
# only resonated with the one already there. It is shown as a PERCENTAGE split in
# the Discovery panel and is absent from the CSV export, so the ingest is a manual
# percent column, exactly like saves. What is worth linting is the honesty of that
# ingest, because every failure mode here is silent:
# (D-3 contract) percent parsing refuses what it cannot know: unknown is never 0,
# a value above 100 is not a share (a count and a share are
# indistinguishable in one column), and a comma is a DECIMAL mark
# here - parseOptionalCount's US-thousands rule would read "36,5"
# as 365.
# (D-3 shape) one stored half, not two. The halves describe one split, so
# keeping both would let a record contradict itself; a transcribed
# pair is cross-checked and DISCARDED when it does not sum to ~100.
# (D-3 roll-up) the aggregate is impressions-WEIGHTED. A flat mean lets a
# 50-impression post at 90% outvote a 10,000-impression post.
# (D-3 boundary) reach never touches engagementRate - it is distribution, not
# engagement, and folding it in would break comparability with
# every historical import.
# (A2-F11) the report OFFERS a baseline diff and writes engagement-patterns.md
# only on an explicit operator go, to the per-user data dir - never
# silently, never into the plugin's shipped template.
# (E#9) the boundary map states what is measurable and what is not:
# dwell explicitly unmeasurable, saves partner-gated, reach native
# but CSV-export status UNVERIFIED. An unstated boundary gets
# quietly filled with an estimate.
CSVP_N16="scripts/analytics/src/parsers/csv-parser.ts"
TYPES_N16="scripts/analytics/src/models/types.ts"
STATS_N16="scripts/analytics/src/utils/stats.ts"
RPT_N16="commands/report.md"
IMP_N16="commands/import.md"
README_N16="README.md"
ADATA_N16="assets/analytics/README.md"
patterns_update_gated() { # $1 = text; gated iff it NAMES the baseline file, REQUIRES a go, and FORBIDS silent writing
echo "$1" | grep -qF "audience-insights/engagement-patterns.md" \
&& echo "$1" | grep -qF "AskUserQuestion" \
&& echo "$1" | grep -qF "never write it silently"
}
PU_SELFTEST_OK=1
if ! patterns_update_gated "read audience-insights/engagement-patterns.md, offer a diff and never write it silently - ask with AskUserQuestion first"; then
PU_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-gated patterns-update probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if patterns_update_gated "$probe"; then
PU_SELFTEST_OK=0; echo " false-positive FAIL: under-gated patterns-update probe accepted -> $probe"
fi
done <<'NEGATIVE16W'
update audience-insights/engagement-patterns.md from the report, asking with AskUserQuestion when unsure
read audience-insights/engagement-patterns.md and never write it silently, applying the diff directly
offer the diff with AskUserQuestion and never write it silently, file left unnamed
NEGATIVE16W
if [ "$PU_SELFTEST_OK" -eq 1 ]; then
pass "patterns-update self-test: predicate needs baseline file + explicit go + no-silent-write (1 accepted, 3 under-gated rejected)"
else
fail "patterns-update self-test failed - the N16 A2-F11 lint is vacuous or over-eager"
fi
# (D-3 contract) the percent ingest is its OWN parser, not saves' count parser
if grep -qF "function parseOptionalPercent" "$CSVP_N16" 2>/dev/null; then
pass "csv-parser has a dedicated percent parser for the reach share (D-3)"
else
fail "$CSVP_N16 has no parseOptionalPercent - a share is being parsed as a count (D-3)"
fi
# (D-3 contract) a share above 100 is refused, and a comma is a decimal mark
if grep -qF "parsed > 100" "$CSVP_N16" 2>/dev/null \
&& grep -qF 'cleaned.replace(/,/g, ".")' "$CSVP_N16" 2>/dev/null; then
pass "reach parsing refuses >100 and reads a comma as a decimal mark (never 36,5 -> 365)"
else
fail "$CSVP_N16 reach parsing accepts a non-share value or mis-reads a decimal comma (D-3)"
fi
# (D-3 shape) both halves reduce to ONE stored value, and a contradictory pair is dropped
if grep -qF "resolveOutOfNetworkPct" "$CSVP_N16" 2>/dev/null \
&& grep -qF "REACH_SPLIT_TOLERANCE" "$CSVP_N16" 2>/dev/null; then
pass "the reach split reduces to one stored half, with a bounded rounding tolerance (D-3)"
else
fail "$CSVP_N16 stores the reach split without a cross-check - a record can contradict itself (D-3)"
fi
# (D-3 shape) the field is optional in the type, and in-network is documented as derived
if grep -qF "outOfNetworkPct?: number" "$TYPES_N16" 2>/dev/null \
&& grep -qF "complement by" "$TYPES_N16" 2>/dev/null; then
pass "PostMetrics carries the reach share as optional, with in-network documented as its complement"
else
fail "$TYPES_N16 reach field is not optional, or stores both halves (D-3)"
fi
# (D-3 boundary) reach is kept out of the engagement rate
if grep -qF "NOT folded into engagementRate" "$TYPES_N16" 2>/dev/null; then
pass "reach is explicitly kept out of engagementRate (comparability with historical imports)"
else
fail "$TYPES_N16 does not state that reach stays out of engagementRate (D-3)"
fi
# (D-3 roll-up) the aggregate is weighted, and zero total weight yields unknown - not 0, not NaN
if grep -qF "export function weightedOutOfNetworkPct" "$STATS_N16" 2>/dev/null \
&& grep -qF "totalWeight <= 0" "$STATS_N16" 2>/dev/null; then
pass "the reach roll-up is impressions-weighted and returns unknown on zero weight (never 0/NaN)"
else
fail "$STATS_N16 has no weighted reach roll-up, or can emit 0/NaN for an unweighted set (D-3)"
fi
# (rendering) the import surface documents the manual column AND its partial coverage
if grep -qF "Reach entered:" "$IMP_N16" 2>/dev/null \
&& grep -qF "Out-of-network" "$IMP_N16" 2>/dev/null; then
pass "/linkedin:import documents the reach column and the coverage-aware output line"
else
fail "$IMP_N16 does not document the manual reach column (the field can never be filled)"
fi
# (rendering) the report shows the split, reads it as acquisition-vs-resonance, and refuses
# to invent it when it was not entered
if grep -qF "Reach Split" "$RPT_N16" 2>/dev/null \
&& grep -qF "not entered for this period" "$RPT_N16" 2>/dev/null \
&& grep -qF "never estimate it" "$RPT_N16" 2>/dev/null; then
pass "/linkedin:report renders the reach split with an explicit unknown branch (never estimated)"
else
fail "$RPT_N16 renders no reach split, or may estimate an unentered one (D-3)"
fi
# (do-next) a reach reading steers the next piece instead of being admired in the report
if grep -qF "Never persist a reach directive" "$RPT_N16" 2>/dev/null; then
pass "a reach insight routes into the do-next channel, and only when it was measured (N15 contract)"
else
fail "$RPT_N16 leaves the reach reading in the report - it never reaches the next draft"
fi
# (A2-F11) the baseline update is real, gated, and aimed at the user's own copy
if patterns_update_gated "$(cat "$RPT_N16" 2>/dev/null)"; then
pass "/linkedin:report offers an operator-gated engagement-patterns update (A2-F11)"
else
fail "report.md updates the patterns baseline silently, or not at all (A2-F11)"
fi
if grep -qF "never to the plugin's shipped" "$RPT_N16" 2>/dev/null; then
pass "the patterns update targets the per-user data dir, never the shipped template (A2-F11)"
else
fail "$RPT_N16 does not forbid writing the plugin's shipped patterns template (A2-F11)"
fi
# (E#9) the boundary map is current on all three metrics: reach, saves, dwell
if grep -qF "In-network vs out-of-network reach" "$README_N16" 2>/dev/null \
&& grep -qF "unverified" "$README_N16" 2>/dev/null; then
pass "boundary map states the reach split as native-but-not-exported, export status unverified (E#9)"
else
fail "$README_N16 boundary map does not cover the reach split honestly (E#9)"
fi
if grep -qF "Explicitly unmeasurable" "$README_N16" 2>/dev/null \
&& grep -qF "partner-gated" "$README_N16" 2>/dev/null; then
pass "boundary map keeps dwell explicitly unmeasurable and saves partner-gated (E#9)"
else
fail "$README_N16 boundary map lost the dwell/saves boundaries (E#9)"
fi
# (entry rules) the operator-facing rules cover either-half entry and the cross-check
if grep -qF "Either half works" "$ADATA_N16" 2>/dev/null \
&& grep -qF "Both halves are cross-checked" "$ADATA_N16" 2>/dev/null; then
pass "the data README documents either-half entry and the split cross-check"
else
fail "$ADATA_N16 does not document the reach entry rules (the column will be filled wrong)"
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
# may only grow (brief-reviewer assumption 3). History: 74 pre-M0; +1 for the SB-S0
# brain-suite floor (Section 16b) = 75; +3 for SB-S1's three UNCONDITIONAL Section-16c
# checks (published-only self-test + voice-trainer grep + contract-doc grep) = 78;
# +2 for SB-S3a's two UNCONDITIONAL Section-16d checks (profile-reader self-test +
# strategy-advisor wiring grep) = 80; +2 for SB-S3d's two UNCONDITIONAL Section-16e
# checks (ops-reader self-test + strategy-advisor ops-wiring grep) = 82; +2 for SB-S3e's
# two UNCONDITIONAL Section-16f checks (reconcile self-test + brain-CLI reconcile grep) = 84;
# +3 for RE-R1's three UNCONDITIONAL Section-16g checks (trends-scorer self-test + score.ts
# both-modes weight-set grep + trend-spotter scorer-pointer grep) = 87; +3 for RE-R2a's three
# UNCONDITIONAL Section-16h checks (trends-capture self-test + cli.ts capture-handler grep +
# trend-spotter capture-pointer grep) = 90; +4 for RE-R2b's four UNCONDITIONAL Section-16i checks
# (trends-brief self-test + cli.ts brief-handler grep + trend-spotter brief-pointer grep +
# session-start surfacing grep) = 94; +5 for RE-R3a's five UNCONDITIONAL Section-16j checks
# (trends-score self-test + score.ts TrendScore-iface grep + types.ts score-field grep +
# trend-spotter dimensions grep + brief.ts composite-rank grep) = 99; +6 for RE-R3b's six
# UNCONDITIONAL Section-16k checks (lifecycle self-test + types.ts TrendStatus grep + types.ts
# surfacedCount grep + store.ts markSurfaced grep + brief.ts effectiveStatus grep + cli.ts
# act-verb grep) = 105; +6 for RE-R3c's six UNCONDITIONAL Section-16l checks (scheduler self-test
# + schedule.ts launchdPlist grep + schedule.ts crontabLine grep + cli.ts schedule-verb grep +
# run-daily.sh brief-invocation grep + run-daily.sh data-twin grep) = 111; +6 for RE-R3d's six
# UNCONDITIONAL Section-16m checks (temporal self-test + brief.ts temporalSignal grep + brief.ts
# b.temporal.rank cmp grep + brief.ts "first-mover" tier grep + cli.ts first-mover-days flag grep +
# cli.ts saturation-at flag grep) = 117; +6 for RE-R3e's six UNCONDITIONAL Section-16n checks
# (brief-diff self-test + brief.ts diffSurfaced grep + brief.ts parseSurfacedFrontmatter grep +
# brief.ts 'Nytt siden sist' section grep + cli.ts selectPriorBriefFile wiring grep + brief.ts
# 'surfaced: ' frontmatter grep) = 123; +6 for N8's six UNCONDITIONAL Section-16o checks (voice-chain
# self-test + voice-trainer chronicle-voice-profile grep + newsletter chronicle-voice-profile grep +
# newsletter chronicle-voice-drift-log read grep + newsletter Step-1 language-resolve grep + newsletter
# Step-8 auto-gull compound grep) = 129; +8 for N9's eight UNCONDITIONAL Section-16p checks (randsone-gate
# self-test + Step-9 randsone_gated compound grep + Step-9 title-strength grep + newsletter C-6 trigger-nouns
# grep + newsletter C-6 scoped-fact-reviewer grep + newsletter C-6 determinism-marker grep + fact-checker
# WebFetch grep + fact-reviewer WebFetch grep) = 137; +9 for N10's nine UNCONDITIONAL Section-16q checks
# (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; +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; +11 for N12's eleven UNCONDITIONAL Section-16s checks (transition-protocol
# self-test + protocol-block compound grep + mirror-invariant grep + 16-phase coverage sweep +
# Step-10 register-complete grep + register.ts upsertEdition grep + register.ts completeEdition/
# listEditions grep + register.ts data-dir placement grep + editionState.ts appendPhase grep +
# template phaseLog-doc grep + template phaseLog-slot grep) = 166; +13 for N13's thirteen
# UNCONDITIONAL Section-16t checks (slot-surface self-test + router wiring grep + calendar
# wiring grep + calendar opt-in grep + session-start two-block compound grep + coverage
# queue+register grep + data-dir placement grep + Step-10 nextFreeSlot grep + Step-10 --slot
# grep + scheduling-ref directional grep + scheduling-ref operator-grid grep + slot-template
# schema/opt-in grep + de-niche no-hardcoded-times/no-committed-grid check) = 179; +19 for
# N14's nineteen UNCONDITIONAL Section-16u checks (fold-in-capture self-test + four capture
# greps for Steps 2.5/3a/5.5/6.5 + Step-11 promotion grep + Step-11 phaseLog grep + Step-11
# friction/never-write grep + Step-11 retro-transition/re-close grep + edition-state retro
# 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; +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;
# +15 for N16's fifteen UNCONDITIONAL Section-16w checks (patterns-update self-test +
# parseOptionalPercent grep + >100/decimal-comma refusal grep + resolve/tolerance
# cross-check grep + optional-field/complement grep + engagementRate-exclusion grep +
# weighted roll-up/zero-weight grep + import reach-column grep + report reach-split/
# never-estimate compound grep + report reach do-next grep + A2-F11 gated-update
# compound grep + shipped-template write-ban grep + boundary-map reach/unverified grep +
# boundary-map dwell/saves grep + data-README entry-rules grep) = 228.
# 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=228
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"
else
fail "assertion count $TOTAL_CHECKS < baseline floor $ASSERT_BASELINE_FLOOR — a check was silently removed"
fi
echo ""
# --- Summary ---
echo "================================================"
echo "RESULTS"
echo "================================================"
echo -e "${GREEN}Passed: $PASS${NC}"
echo -e "${RED}Failed: $FAIL${NC}"
echo -e "${YELLOW}Warnings: $WARN${NC}"
echo ""
if [ $FAIL -eq 0 ]; then
echo -e "${GREEN}All structural checks passed!${NC}"
exit 0
else
echo -e "${RED}$FAIL check(s) failed. Review above.${NC}"
exit 1
fi