A1-10 + D-5. The queue answers "what is scheduled"; nothing answered "is the week covered, and what is still being written". N13 makes both readable at session start. - Publishing slots are operator config: profile/publishing-slots.json in the per-user data dir, schema + OPT-IN template in config/publishing-slots.template.json. The plugin ships no publishing times and writes the grid for nobody; absent config means every slot surface stays silent (same never-nag discipline as the trend/brain nudges). Data dir rather than the state file, which is machine-written and whose frontmatter reader does not parse lists. - hooks/scripts/slots.mjs (new): one implementation behind four surfaces. Zero-dep (a SessionStart hook must not spawn tsx), pure core (dates in as strings, no clock), imported by the commands exactly as queue-manager.mjs already is — so session-start's vacancy warning and Step 10's slot default cannot drift apart. - Coverage counts the short-form queue (scheduled/published; cancelled frees the slot) AND editions-register rows claiming that date, counted per date, so long-form and short-form cannot be double-booked and a two-slot day needs two posts. - Session start gains "## Editions in Flight" (series, phase, next action, claimed slot, days in flight) and "## Publishing Slots" (next open slot, open count in 14 days, and how to fill it when the gap is <=3 days). The existing discovery nudge is untouched. - /linkedin router + /linkedin:calendar surface the same roll-up; calendar documents the opt-in copy. /linkedin:newsletter Step 10 defaults to the next open slot (one confirmation, not an interview) and writes --slot onto the register row. - D-5: references/scheduling-strategy.md marked "confidence: low / directional" per the algorithm reference's own standard, deferring to the operator's grid and their own analytics. Neutral example time replaces the operator-specific one in the N12 docs. TDD: red proven first (module absent; 5 session-start behaviours failing). hooks 140 -> 174 · test-runner 184 -> 197 (Section 16t: 13 unconditional greps incl. a de-niche check that no publishing time is hardcoded in the slot path and no operator grid is committed, + non-vacuity self-test; anti-erosion floor 166 -> 179). Suites green: trends 300/0 · brain 134/0 · editions 72/0 · specifics-bank 45/0 · tests 35/0 · render 60/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2278 lines
118 KiB
Bash
Executable file
2278 lines
118 KiB
Bash
Executable file
#!/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 8–18).
|
||
#
|
||
# 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=28
|
||
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)
|
||
)
|
||
|
||
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 14–15 (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; 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 16 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 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.
|
||
# 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=179
|
||
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
|