feat(linkedin-studio): N9 — randsone-gates (krok/carousel/tittel) + deterministisk fakta-recheck + WebFetch [skip-docs]

C-4/C-9: Step 9 (post-lock) gets a scoped fact-reviewer (F1/F3) + language-reviewer
(L1/L2) pass over the four feed-facing surfaces — delingstekst hook, samle post,
carousel slide text, edition title — plus a title-strength gate, before the hook
conversion binary. Placed at Step 9 (first phase where all four exist; operates on
distribution copy only, never the locked body — body/canonical-title defect = pivot).

C-6: deterministic post-factcheck fact-invariant after Step 5 — any later fold-in
whose diff touches a sentence with numbers, names, or quotes fires a mandatory scoped
fact-reviewer mini-recheck (not a judgment call), closing the pivot-heuristic blind
spot; enforced at the 6.5 + 7 fold-in sites.

C-7: WebFetch added to fact-checker + fact-reviewer (verbatim source text for quote
precision / exact-figure attribution).

TDD RED→GREEN: test-runner Section 16p (8 unconditional greps + non-vacuity
self-test), anti-erosion floor 129 → 137. Suite 146 → 154/0. Domain-general.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014bE7VbkmR3cqHFEeGfzgwb
This commit is contained in:
Kjell Tore Guttormsen 2026-07-24 01:45:45 +02:00
commit 34c6c854c0
5 changed files with 239 additions and 41 deletions

View file

@ -24,6 +24,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Auto-gull — the gold corpus grows on its own (A2-F2)** — Step 8 lock now appends the locked edition to `ingest/published/` tagged `provenance=published`, so the corpus the chronicle profile is distilled from grows one edition at a time (the author's own locked editions, never the engine's drafts). No manual curation.
- Domain-general throughout: languages/formats come from the user's own corpus, never hardcoded. test-runner 140 → 146 (Section 16o: 6 unconditional voice-chain greps + self-test; anti-erosion floor 123 → 129).
### Added — randsone gates: gate the feed-facing surfaces (N9 / C-4, C-6, C-7, C-9)
- **Randsone fact + language gate (C-4/C-9)**`/linkedin:newsletter` Step 9 (post-lock) now runs a **scoped** `fact-reviewer` (F1 verifiable claims + F3 number attribution) + `language-reviewer` (L1 repetition + L2 anglicisms/register) pass over the four feed-facing surfaces the reader actually meets — the **delingstekst hook**, the **samle post**, the **carousel slide text**, and the **edition title** — before the hook-conversion binary. Until now only the hook's click-worth was judged; its truth and language, the samle post, the carousel slides and the title reached the feed ungated. Placed at Step 9 because that is the first phase where all four surfaces exist (carousel is authored in Step 7.5; delingstekst/samle from Step 8), and Step 9 already operates on post-lock distribution copy — so the gate revises distribution surfaces + re-renders, never the locked body; a defect tracing into the locked body/canonical title is a pivot.
- **Edition title-strength gate (C-9)** — the edition's own title now faces a strength gate at Step 9 (binding on the distribution title, advisory on the locked canonical title), not only the hook's two lines.
- **Deterministic post-factcheck fact-recheck (C-6)** — a new invariant after Step 5: any later fold-in (5.5/6/6.5/7) whose diff touches a sentence with **numbers, names, or quotes** fires a mandatory **scoped `fact-reviewer`** mini-recheck of exactly those sentences — a deterministic trigger, **not a judgment call**, replacing the old "re-run if it felt substantive" discretion and closing the pivot-heuristic blind spot (a swapped statistic under the +20 % threshold never trips `/linkedin:pivot`, but does trip this).
- **WebFetch on both fact agents (C-7)**`fact-checker` and `fact-reviewer` gain `WebFetch`: quote precision (F2) and exact-figure attribution (F3) require the verbatim source text in context, not a search snippet.
- test-runner 146 → 154 (Section 16p: 8 unconditional randsone-gate greps + self-test; anti-erosion floor 129 → 137). Domain-general; no KTG-specific values hardcoded.
### Built feedback (awaiting consumer-side proof)
- **MR-F9** (demand-sweep «innenfra og ut») — built; the (a)/(b)/(c) evidence gate is a runtime demonstration, proven consumer-side (plugin agents don't resolve in the dev repo).

View file

@ -16,7 +16,7 @@ description: |
"is this accurate", "verify numbers", "back this up", "factual correctness".
model: opus
color: brown
tools: ["Read", "WebSearch"]
tools: ["Read", "WebSearch", "WebFetch"]
---
# Fact Checker Agent
@ -89,6 +89,14 @@ Always run the contradiction sweep (5) — a claim that survives a deliberate
search for counter-evidence is far stronger than one that merely matched a
confirming page.
**Fetch the verbatim source for quotes and figures (WebFetch).** A search
*snippet* is enough to locate a source but not to confirm a **quotation** or an
**exact figure** — for those, `WebFetch` the located page and read the original
wording in context. A quote is verified only against the source's verbatim text
(«Vi» vs «Vi i Nav» is a precision failure even when the gist matches); a figure
is verified only against the number as the source actually states it. Never grade
quote precision or number attribution off a snippet alone.
### Step 3: Assess Source Credibility
For each source, evaluate: is it primary or secondary, who published it, how

View file

@ -22,7 +22,7 @@ description: |
"number attribution", "headless review", "fact-reviewer".
model: opus
color: gold
tools: ["Read", "WebSearch"]
tools: ["Read", "WebSearch", "WebFetch"]
---
# Fact Reviewer Agent
@ -211,6 +211,12 @@ source's exact wording and attribution and compare verbatim (F2). For figures,
trace to a named source and confirm the source's precision matches the draft's
(F3/F4 — "around a third" does not verify "37 %").
**Fetch the verbatim source, do not grade off a snippet (WebFetch).** F2 (quote
precision) and F3 (number attribution) cannot be settled from a search *snippet*
`WebFetch` the located source and read the original wording/number in context, then
compare verbatim. A quote or figure marked 🟢 must have been checked against the
source's actual text, never a preview line.
### Step 3 — Score on the five dimensions
Score each claim 0100 across the five dimensions. A contradicted claim is 🔴

View file

@ -1169,6 +1169,21 @@ Gate: [PASS — zero unresolved 🔴] (else REWORK/BLOCK with the open claims)
Next: Step 5.5 — Editorial review (craft gate, BEFORE the persona sweep).
```
> **Post-factcheck fact-invariant (Endring N9 / C-6) — deterministic recheck.**
> Once this sweep clears, the article's facts are gated — but every later edit can
> silently reintroduce an unchecked fact: an editorial fold-in (5.5), a persona
> tightening (6), a headless fold-in (6.5), an annotation pass (7). So the
> re-verification trigger is **deterministic — not a judgment call:** if a fold-in's
> diff touches any sentence containing **numbers, names, or quotes**, it fires a
> mandatory **scoped fact-reviewer** mini-recheck of *exactly those changed
> sentences* (`subagent_type: linkedin-studio:fact-reviewer`, inputs limited to the
> touched sentences + the sources they cite) before the phase advances. This
> replaces the old "re-run only if it felt substantive" discretion and closes the
> pivot-heuristic blind spot: a swapped statistic that adds no section and stays
> under the +20 % word-count threshold never trips `/linkedin:pivot`, but it *does*
> trip this trigger. A verified fact stays verified only if every number, name, or
> quote that changes after the gate is re-checked. `[GATE]`
## Step 5.5: Editorial review — craft gate, BEFORE the persona sweep
The fact-checked draft now faces the **editor**: a single `editorial-reviewer`
@ -1441,6 +1456,10 @@ command runs — see `commands/headless-review.md` for the full cold contract):
The editor (this session) holds the pen; never paste a reviewer's direction as
copy. If the fold-in was substantive, re-run the affected archetype on v(n+1).
All of this happens **before** lock, so the body is never reopened post-lock.
**Post-factcheck fact-invariant applies (C-6):** independently of the substantive
judgment, if any folded edit's diff touches a sentence with **numbers, names, or
quotes**, fire the deterministic **scoped fact-reviewer** mini-recheck on those
sentences (see the invariant after Step 5) before advancing.
7. **Persist + checkpoint state.** Record the run in `edition-state.json`
`articles.NN.headlessReview` (`frozenDraft`, per-reviewer `{reportPath,
@ -1497,6 +1516,10 @@ editor is satisfied with the in-session draft. It does not gate lock.
editor's manual annotation pass. Fold any resulting edits back **by
tightening** (the Step 4 rule still holds) — then, if the edit was
substantive, re-run the affected sweep (Step 5 or 6) before proceeding.
**Post-factcheck fact-invariant applies (C-6):** any annotation edit whose diff
touches a sentence with **numbers, names, or quotes** fires the deterministic
**scoped fact-reviewer** mini-recheck on those sentences (see the invariant after
Step 5), regardless of whether the edit felt substantive.
4. **Persist.** Set `currentPhase: "annotation"` in `edition-state.json` and
write an "annotation rendered (optional) → next: visual assets" line to
@ -1778,51 +1801,104 @@ Next: Step 9 — Hook / conversion gate (post-lock).
## Step 9: Hook / conversion gate — "would YOU click?" (AFTER lock)
The locked edition still has to earn the click. The **distribution text** — the
delingstekst hook that fronts the post in the feed — now faces a final binary
gate: would the primær persona, scrolling, actually stop and open this? This is
the post-lock conversion sweep, distinct from the pre-lock resonance sweep
(Step 6): it judges the **hook only**, binary, never the body.
The locked edition still has to earn the feed. Before it ships, the
**feed-facing surfaces** the reader actually meets — the **delingstekst hook**,
the **samle post**, the **carousel slide text**, and the **edition title**
face two final gates: a **randsone fact + language gate** (so nothing false or
clumsy reaches the feed in the most-read, most-requoted copy — C-4/C-9) and the
**hook conversion binary** (would the primær persona, scrolling, actually stop
and open this?). Both operate on **distribution copy only** — the post-lock
sweep, distinct from the pre-lock resonance sweep (Step 6), never the locked
body. Until N9 only the hook's click-worth was judged; the hook's *truth and
language*, the samle post, the carousel slides and the title reached the feed
ungated.
> **Order assertion (enforced).** This hook-gate (Step 9) runs AFTER lock
> (Step 8) — it operates on the distribution text of an already-locked edition
> and must never reopen the locked body. If the hook fails, you revise the
> *delingstekst* (and re-render via Step 8's `build-linkedin.mjs`), not the
> edition text. This ordering is the inverse of the resonance sweep, which runs
> BEFORE lock — keeping the two apart is what fixed the Seres process. `[GATE]`
> **Order assertion (enforced).** This gate (Step 9) runs AFTER lock (Step 8) — it
> operates on the **distribution copy** of an already-locked edition and must never
> reopen the locked body. Both the randsone fact/language gate and the hook binary
> revise only distribution surfaces (delingstekst, samle, carousel text,
> distribution title) and re-render via Step 8's `build-linkedin.mjs` /
> `build-carousel.mjs` — never the edition text. A defect that traces into the
> **locked article body or its canonical title** is a pivot (STOP →
> `/linkedin:pivot`), not an in-place edit. This ordering is the inverse of the
> resonance sweep, which runs BEFORE lock — keeping the two apart is what fixed the
> Seres process. `[GATE]`
**Why Step 9 (placement, documented per plan N9).** The randsone gate lives here,
not earlier, because Step 9 is the **first phase where all four surfaces exist**:
the carousel slide text is authored in Step 7.5 (after the last content gate 6.5),
and the delingstekst + samle post exist only from Step 8. Step 9 already operates on
post-lock distribution copy, so gating and revising these surfaces here respects the
lock invariant without breaking phase order.
**Procedure:**
1. **Take the distribution hook** — the first two lines of the edition's entry
in `linkedin/edition-delingstekst.md` (and the `samle` hook, if shipping the
collected post). This is what the reader sees before "…see more".
1. **Collect the four distribution surfaces.** These are what the reader meets in
the feed — and until N9 only the first was judged, and only for click-worth:
- the **delingstekst hook** — the first two lines of the edition's entry in
`linkedin/edition-delingstekst.md` (what the reader sees before "…see more");
- the **samle post** — the `## Samle` section, if shipping the collected post;
- the **carousel slide text**`linkedin/NN/carousel.md`, for a carousel-format
edition (authored in Step 7.5, after the last content gate — so it has never
been fact- or language-checked);
- the **edition title** — the edition's own headline (its distribution / feed
title), never strength-gated before now.
2. **Run `persona-reviewer` in conversion mode** (`subagent_type:
linkedin-studio:persona-reviewer`) for the **primær** persona
only, from THIS command layer in the foreground. Pass
**`mode: konverter`** (the after-lock, hook-only mode — NOT resonans). The
agent returns a single binary verdict, **JA / NEI**, on «would YOU click?» —
no axis scoring, no flags, no rewritten copy (principle: the jury judges,
the editor writes).
2. **Randsone fact + language gate (scoped — C-4/C-9).** Run two scoped reviewers
over *only* these surfaces (never the locked body), issued from THIS command
layer in the foreground, `subagent_type` namespaced:
- `linkedin-studio:fact-reviewer`**F1** (verifiable claims) + **F3** (number
attribution) on any figure, named case, or quotation appearing in the four
surfaces. A hook or slide that asserts a number the body never sourced is a 🔴.
(WebFetch the source for any quoted or exact figure — C-7.)
- `linkedin-studio:language-reviewer`**L1** (repetition) + **L2** (anglicisms
/ stiff register), graded against `edition-state.language`. The hook is the
most-requoted line the edition has; a clumsy or anglicised hook is an L-BLOCK.
Pass each reviewer only the four surfaces + the sources they cite + `language`.
This is a **scoped mini-pass**, not a re-review of the article. `[GATE]`
3. **Gate on the binary.**
3. **Title-strength gate (C-9).** The edition's own title was never strength-gated —
only the hook's two lines were. Judge the **title-strength** of the **edition
title** here: does it earn the open on its own, or is it a generic label? The
strongest candidate becomes the distribution title. The verdict is **binding on
the distribution title** (sharpen a weak one in place) and **advisory on the
canonical (locked) title** — changing that is a body edit → STOP, run
`/linkedin:pivot`; never edit the locked title in place.
4. **Resolve the randsone gate.**
- **Fact 🔴 / L-BLOCK on a distribution surface** → revise that surface **by
tightening** (hook, samle, carousel text, or distribution title — all
distribution copy), **re-render** the affected artifact (POST.html via Step 8's
`build-linkedin.mjs`; carousel via `build-carousel.mjs`), and re-run the scoped
pass on what changed. The locked body stays frozen. Loop until clean.
- **A defect that traces into the locked body or canonical title** → it is a
pivot: STOP, run `/linkedin:pivot`. The randsone gate never reopens the locked
body in place — the same invariant as the conversion loop. `[GATE]`
5. **Run the hook conversion binary** (`subagent_type:
linkedin-studio:persona-reviewer`, **`mode: konverter`** — the after-lock,
hook-only mode, NOT resonans) for the **primær** persona only. The agent returns
a single binary verdict, **JA / NEI**, on «would YOU click?» — no axis scoring,
no flags, no rewritten copy (the jury judges, the editor writes).
- **JA** → the hook converts. Proceed to Step 10.
- **NEI** → the hook does not earn the click. Revise the **delingstekst hook
only** (sharpen the krok by tightening — close the gap, hold the body
frozen), **re-render POST.html** via Step 8's `build-linkedin.mjs` (the
body stays locked; only the distribution copy changed), and re-run the
- **NEI** → revise the **delingstekst hook only** (sharpen the krok by tightening,
body frozen), **re-render POST.html** via `build-linkedin.mjs`, and re-run the
conversion check. Loop until JA. `[GATE]`
4. **Persist.** Record the conversion verdict in
`edition-state.json``articles.NN.personaSweep.conversion`,
and set `currentPhase: "hook-conversion-gate"`.
6. **Persist.** Record the randsone-gate result in `edition-state.json`
`articles.NN.randsoneGate` (per-surface fact/language verdicts + title-strength +
what was revised) and the conversion verdict in
`articles.NN.personaSweep.conversion`; set `currentPhase: "hook-conversion-gate"`.
```
Hook / conversion gate (post-lock).
- Primær persona: <name> mode: konverter (hook only)
- Verdict: JA — hook converts (else: NEI — revise delingstekst hook, re-render, re-check)
Randsone + conversion gate (post-lock, distribution copy only).
- Surfaces gated: delingstekst hook · samle post · carousel slide text · edition title
- Fact (F1/F3): <N> 🔴 → resolved (or pivot if it traces to the body)
- Language (L1/L2): <N> BLOCK → tightened
- Title-strength: <verdict> (distribution title sharpened; canonical-title weakness → pivot)
- Conversion (primær, konverter): JA (else: NEI — revise hook, re-render, re-check)
- Body: untouched (locked in Step 8)
Gate: [PASS — JA] (else loop on the distribution hook)
Gate: [PASS — randsone clean + JA] (else loop on distribution copy, or pivot)
Next: Step 10 — Scheduling.
```

View file

@ -64,7 +64,10 @@
# 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 assertion-count anti-erosion floor (SC6) in Section 18. All
# 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 assertion-count anti-erosion floor (SC6) in Section 18. All
# are live below (Sections 818).
#
# Usage: bash scripts/test-runner.sh
@ -1594,6 +1597,100 @@ 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 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
@ -1625,12 +1722,15 @@ echo ""
# '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.
# 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.
# 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=129
ASSERT_BASELINE_FLOOR=137
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"