Files
Sylpheed/tools/port/check-claims
MechaCat02 c3758e3850 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00

414 lines
22 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Every refuted claim must appear only inside its own correction.
#
# tools/port/check-claims
#
# 🔴 WHY THIS IS A CHECK AND NOT AN AUDIT. The Decoder's rule -- *grep the corpus
# for the claim, not for the file you were working in* -- found a refuted sentence
# still shipping in this port's `manifest.json`, and a withdrawn one still
# standing in `DECISIONS.md`. Running that by hand finds the instances present on
# the day it is run. It does not stop the next one.
#
# So: a REGISTER. Each row is a claim this corpus has refuted, plus a marker that
# must appear near every occurrence. A hit without its marker fails the run.
#
# ⚠️ Two things learned building it, both from the other agent:
#
# * a "kept for the record" block STILL ASSERTS. Marking the heading superseded
# does not mark the sentence a reader lands on, so the marker must sit near
# the CLAIM, not at the top of the section.
# * naming a refuted claim keeps it greppable, so this check returns its own
# corrections as hits -- which is the point. The marker is what distinguishes
# "quoted while being refuted" from "still asserted".
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
WINDOW=400 # characters either side of a hit in which the marker must appear
fail=0; total_marked=0; scanned=0; peer_hits=0; _peer_probe_done=0
# 🔴 THE MARKER IS AN EXPLICIT SENTINEL, NOT A KEYWORD.
#
# The first version matched a per-claim keyword -- "refuted", "WITHDRAWN" -- near
# the hit. Every one of its four failures was a quotation sitting INSIDE a
# correction whose wording happened not to contain the keyword: a table cell
# reading "standing, unmarked", a sentence reading "the real count was ten".
#
# Widening the window or adding synonyms until those passed would have been
# tuning a threshold until the answer came out right, which is the failure this
# corpus has spent a fortnight cataloguing. So the marker is a TOKEN THE AUTHOR
# PLACES: `[refuted]` near any quotation of a registered claim. It cannot be
# satisfied by phrasing, and its absence means exactly one thing.
#
# ⚠️ The cost is honest: every quotation must be marked by hand, and a new
# refuted claim means a new row plus marking its existing quotations. That work
# is the check.
MARKER='[refuted]'
REGISTER=$(cat <<'ROWS'
TAIL of the kept stream :: the leading chunk of a voice region duplicates the end of the kept stream, so it can be dropped
known too fast :: the boot plays both splashes faster than the game does
only thing making the plate :: the plate reappears because of one authored cause
no loop-point field has been identified :: nothing anywhere on the disc or in the runtime states where a bank loops
AUDIBLY WRONG AT THE SEAM :: replaying the menu bed from sample 0 puts audible fade-out and silence at the loop seam
1 of 3 streams :: the exporter ships one of a voice region's three streams
six expected DIFFERS :: six screens are expected to differ from the reference renderer
goes against the port :: the JP title capture adjudicates title_jp against this port's rendering
the capture turns out to determine it :: the leaf's phase is fixed by the capture rather than being an arbitrary choice
COMPOSITED rather than standalone :: the four screens without an opaque-black primitive are drawn composited over another screen
structural limit, not an unrun experiment :: EXTRAS cannot be strengthened past n=1 because this archive holds no second destination
HANDOFF Q10 says nothing on the disc :: nothing on the disc names which track the menu plays, so the port must choose one
28 % of `S00A`'s frames :: 28 % of S00A's frames and 47 % of ADV's reached the screen, measured
by three routes :: DIFFICULTY is identified by three independent routes
HANDOFF has not moved in four milestones :: the contract itself is static, rather than static only on the branch this checkout reads
ROWS
)
[ -n "${CLAIMS_REGISTER+x}" ] && REGISTER="$CLAIMS_REGISTER"
# 🔴 A REGISTER THAT PARSES NOTHING REPORTED CLEAN, FOREVER. The scan loop runs
# once per row; with no rows it runs zero times, `fail` stays 0, and the script
# printed "every refuted claim appears only inside its correction" and exited 0.
# That is the stub defect -- prints a result, asserts nothing -- sitting in the
# checker whose clean runs both agents lean on. The Decoder found it in their
# equivalent the same day; it was here too.
_rows=$(printf '%s\n' "$REGISTER" | grep -c '[^[:space:]]' || true)
if [ "$_rows" -eq 0 ]; then
echo "🔴 the refuted register is EMPTY -- this check would pass everything." >&2
echo " Exit 2: the harness is broken, not the corpus." >&2
exit 2
fi
# --------------------------------------------------------------------------
# `--control`: the known negatives, EXECUTED.
#
# 🔴 Until now this check had NO control machinery at all. Every "planted a
# revival, it failed, removed it, it passed" in `DECISIONS.md` was done BY HAND,
# once, and never again -- in a repository where two of my own tools carry the
# line *"a control that does not execute is not a control"*. It was written
# about somebody else's tool.
#
# Four cases, each driving THIS script as a subprocess and reading its real exit
# code rather than reasoning about what it would do:
#
# clean tree -> 0
# unmarked revival planted -> 1 (the check must catch it)
# revival planted MARKED -> 0 (and must not false-positive on it)
# register emptied -> 2 (the harness is broken, not the corpus)
#
# The plant lands in a real scanned directory, because a control that runs
# somewhere the tool does not look proves nothing about the tool.
if [ "${1:-}" = "--control" ]; then
probe="docs/port/.claims-control-probe.md"
trap 'rm -f "$probe"' EXIT INT TERM
# The PHRASE only: rows now read `phrase :: proposition`, and the case strings
# below are colon-delimited, so passing a whole row made the harness parse the
# proposition as a field and report its own cases broken. A data-shape change
# breaking the harness that guards the data is this iteration's small version
# of my rows making the Decoder's parser fail silently.
claim=$(printf '%s\n' "$REGISTER" | grep -m1 '[^[:space:]]')
claim="${claim%% :: *}"
ok=0
run() { CLAIMS_CONTROL=1 "$0" >/dev/null 2>&1; echo $?; }
rm -f "$probe"
for case in "clean::0" "unmarked:$claim:1" "marked:$claim [refuted]:0"; do
IFS=: read -r name body want <<<"$case"
if [ -n "$body" ]; then printf '%s\n' "$body" > "$probe"; else rm -f "$probe"; fi
got=$(run)
if [ "$got" = "$want" ]; then
printf ' %-26s exit %s ✅\n' "$name" "$got"
else
printf ' %-26s exit %s, wanted %s 🔴\n' "$name" "$got" "$want"; ok=1
fi
done
rm -f "$probe"
# 🔴 FIFTH CASE: the same text OUTSIDE the scanned root must give 0.
#
# Without it, "the plant is inside a scanned directory" is a property I
# verified BY HAND, once -- which is the exact pattern I had just finished
# criticising in this tool one iteration earlier. The pair is what asserts the
# boundary is real: identical text, exit 1 inside and 0 outside. Either half
# alone is consistent with the tool scanning everything, or nothing.
#
# The Decoder added this to theirs after I raised the boundary; the reason it
# was worth adding is that their property held *because they had reasoned it*,
# not because anything asserted it. Mine was in the same state.
outside="${TMPDIR:-/tmp}/claims-control-outside.md"
printf '%s\n' "$claim" > "$outside"
got=$(run)
rm -f "$outside"
if [ "$got" = "0" ]; then printf ' %-26s exit 0 ✅\n' "same text outside root"
else printf ' %-26s exit %s, wanted 0 🔴\n' "same text outside root" "$got"; ok=1; fi
got=$(CLAIMS_REGISTER="" "$0" >/dev/null 2>&1; echo $?)
if [ "$got" = "2" ]; then printf ' %-26s exit 2 ✅\n' "empty register"
else printf ' %-26s exit %s, wanted 2 🔴\n' "empty register" "$got"; ok=1; fi
# Sixth case: a tree with nothing to scan. It used to die in the withdrawal
# hook and exit 1 -- "a refuted claim is still being asserted" -- for a wrong
# directory. Liveness and diagnosis are both asserted here.
_empty="${TMPDIR:-/tmp}/claims-liveness-root"; mkdir -p "$_empty"
got=$(cd "$_empty" && PROJECT_DIR="$_empty" "$OLDPWD/$0" >/dev/null 2>&1; echo $?)
if [ "$got" = "2" ]; then printf ' %-26s exit 2 ✅\n' "nothing to scan"
else printf ' %-26s exit %s, wanted 2 🔴\n' "nothing to scan" "$got"; ok=1; fi
echo
[ $ok -eq 0 ] && echo "the register check fails when it must, and says so distinctly" \
|| echo "🔴 the control machinery itself is broken"
exit $ok
fi
# ─── THE WITHDRAWAL-TIME HOOK ────────────────────────────────────────────────
# The register enforces claims it KNOWS ABOUT; knowing about them was manual, and
# that is how ~8 claims were withdrawn this session and 0 registered. A sweep
# cannot fix it -- by the time you sweep, the withdrawal is already unpublished.
# The hook fires where the withdrawal is WRITTEN.
#
# A correction in DECISIONS.md has a shape: a heading carrying WITHDRAWN /
# CORRECTION / "refuted". A section like that containing no registered phrase is
# a death argued and never indexed.
#
# ⚠️ The register is passed in the ENVIRONMENT, not inlined. The first version
# pasted the rows into this file's own heredoc -- which made every phrase an
# unmarked quotation, and the checker flagged its own source. A tool that
# violates the rule it enforces by being written is worth a comment.
#
# 🟡 REPORTED, NOT ASSERTED: not every correction retires a CLAIM -- some fix a
# number, a scope, a wrong floor -- and forcing a row for those would push rows
# in to silence the check, the failure this file exists to prevent.
#
# ⚠️ AND IT WILL ALWAYS OVER-REPORT ON WELL-WRITTEN CORRECTIONS. The detection is
# "does this section contain a registered phrase", which requires the correction
# to QUOTE the dead claim. A good correction paraphrases it away: the JP heading
# now reads "does NOT go against the port", which does not contain the registered
# "goes against the port" [refuted] and is flagged despite being registered.
#
# The Decoder's resolution is the right one and costs the correction nothing:
# **the register entry is the verbatim home of the dead phrase; prose paraphrases
# freely.** They are different documents, so the phrase always has one exact
# place to live without any correction having to carry it. What follows for this
# hook is that its candidate list mixes "never registered" with "registered and
# paraphrased", and it cannot separate them -- so the list is a prompt to check,
# never a defect count.
echo
echo "withdrawal-time hook -- correction sections that registered nothing:"
# 🔴 PREFLIGHT. Run from the wrong directory this used to die inside the
# withdrawal hook with a FileNotFoundError and exit **1** -- which in this
# script's own vocabulary means "a refuted claim is still being asserted". A real
# failure with a fabricated diagnosis, the same shape as my control anchoring at
# the wrong document. The roots it needs are named here and their absence is a
# HARNESS fault with its own code.
for _root in docs docs/port authored tools/port; do
[ -d "$_root" ] || {
echo "🔴 \`$_root\` is not here -- this check cannot scan anything." >&2
echo " Exit 2: wrong directory or a bad checkout, not a dirty corpus." >&2
exit 2
}
done
[ -f docs/port/DECISIONS.md ] || {
echo "🔴 docs/port/DECISIONS.md is missing -- the withdrawal hook has nothing" >&2
echo " to read. Exit 2: the harness is broken, not the corpus." >&2
exit 2
}
REG="$REGISTER" python3 - <<'HOOK'
import os, re
reg = [r.strip() for r in os.environ["REG"].split("\n") if r.strip()]
doc = open("docs/port/DECISIONS.md").read()
heads = [(m.start(), m.group(0)) for m in re.finditer(r"(?m)^##+ .*$", doc)]
flagged = 0
for i, (pos, head) in enumerate(heads):
# 🔴 THE FIRST REGEX MATCHED HEADINGS *ABOUT* CORRECTIONS, NOT HEADINGS
# MAKING THEM -- "withdraw" caught "rather than withdrawing", "refuted"
# caught a section discussing the register itself. 33 candidates was a
# measurement of the regex. Narrowed to headings that RETIRE something:
# a leading WITHDRAWN/CORRECTION/Refuted, or an explicit "is withdrawn".
if not re.search(r"^#+\s*(?:[^A-Za-z]*\s*)?(WITHDRAWN|CORRECTION|Refuted)\b"
r"|\bis withdrawn\b|\bnow refuted\b", head):
continue
end = heads[i + 1][0] if i + 1 < len(heads) else len(doc)
if not any(c in doc[pos:end] for c in reg):
flagged += 1
print(" candidate: %s" % head[:92].lstrip("# "))
print(" none -- every correction section names a registered claim" if not flagged
else " %d correction section(s) argue a withdrawal the register does not carry" % flagged)
HOOK
while IFS= read -r claim; do
# 🔴 ROWS CARRY A PROPOSITION NOW, `phrase :: what it asserted`.
#
# They were bare phrases, and that had two costs. A phrase is not a claim:
# `1 of 3 streams` [refuted] is dead here and a LIVE warning in the Decoder's
# corpus, and the bare row cannot say which proposition it killed -- so a peer
# hit was unadjudicable even in principle. And the bareness made THEIR parser
# fail silently: a reader looking for a quoted string in each row found none,
# built an empty claim list, and reported a clean table. My data shape made
# their instrument lie.
#
# The phrase is still the search key; the proposition is for whoever has to
# judge a hit, here or in another corpus.
proposition="${claim#* :: }"
claim="${claim%% :: *}"
[ -z "$claim" ] && continue
hits=0; bad=0; marked=0
while IFS= read -r loc; do
[ -z "$loc" ] && continue
f=${loc%%:*}
hits=$((hits+1))
out=$(python3 - "$f" "$claim" "$MARKER" "$WINDOW" <<'PY'
import sys
f, claim, marker, w = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
s = open(f, encoding="utf-8", errors="ignore").read()
# 🔴 THE REGISTER BLOCK IS ITS OWN VERBATIM HOME, and is excised before
# scanning rather than relying on marker proximity. The rows used to be bare
# phrases that happened to sit within the marker window of the file header;
# adding a proposition to each pushed them out of it, and the check began
# reporting its own register as twelve unmarked assertions. Widening the window
# would have been tuning a constant to make a failure go away. Excising exactly
# the heredoc -- and nothing else in this file -- keeps every other occurrence
# in `check-claims` under the same rule as any other file, which matters because
# the comments here quote dead phrases constantly.
if f.endswith("check-claims"):
a = s.find("REGISTER=$(cat <<'ROWS'")
b = s.find("\nROWS", a) if a >= 0 else -1
if a >= 0 and b > a:
s = s[:a] + (" " * (b - a)) + s[b:]
i = n = 0
low, claim_low = s.lower(), claim.lower()
while True:
i = low.find(claim_low, i)
if i < 0:
break
if marker.lower() not in low[max(0, i-w):i+w+len(claim)]:
print(" unmarked in %s at char %d" % (f, i))
sys.exit(1)
n += 1
i += len(claim)
# Every suppression, counted. A checker that can discard an occurrence in silence
# reports the same clean run whether or not a live assertion is hiding among the
# marked ones, and its zero is unfalsifiable. Reached from the loud end here and
# from the quiet end by the Decoder on the same day: their marker language was
# vouching for 8 of 8 mentions, so their 0 was going to be 0 either way.
print(n)
sys.exit(0)
PY
) && marked=$((marked + out)) || { printf '%s\n' "$out"; bad=$((bad+1)); }
# 🔴 CASE-INSENSITIVE since 2026-08-30, and the reason is a live miss. The
# register held "no loop-point field has been identified" [refuted]; `BLOCKED.md`
# it capitalised at the start of a sentence, and the check reported clean while
# a refuted claim stood unmarked in the file whose whole job is to say what is
# still open. The Decoder found the same class the same day from the other end
# -- their register missed a revival that kept the claim and changed the second
# clause. A register matching EXACT wording does not protect the documents that
# rewrite most, and a capital letter is the cheapest rewrite there is.
done < <(grep -ril -- "$claim" docs/port/ crates/ port/ tools/ authored/ 2>/dev/null || true)
# 🔴 PEER-OWNED ROOTS ARE SCANNED FROM THE REF, NOT THE TREE.
#
# `docs/re/`, `docs/game/` and `docs/agents/` are written by the Decoder. My
# working copies are 246, 9 and 13 commits behind their heads, so any verdict
# this check reached about one of their files would be a verdict about MY
# STALE COPY -- and the failure direction is the false positive: flagging a
# claim they have already corrected. That is exactly what they did to me by
# hand, reading my `BLOCKED.md` 234 commits behind.
#
# Excluding them would hide the exposure; reporting from the stale copy would
# keep it. So the scan reads the newest blob on any ref. It is the only
# structural fix either agent has found for this class -- READ THE REF, NOT
# THE TREE -- and it is why `contract-check` stayed correct while this tree sat
# 115 commits behind.
#
# ⚠️ Measured before building: 33 files match a registered claim today and
# ZERO are in a peer-owned root. The exposure is latent, not active. Recorded
# because "I checked and it was clean" and "I never looked" must not read the
# same, which is this week's whole lesson.
_peer_ref=$(git log --all -n 1 --format=%h -- docs/re docs/game docs/agents)
# 🔴 THE PEER SCAN GETS A KNOWN POSITIVE, because a zero from a broken reader
# looks identical to a real one. The Decoder demonstrated both halves of that
# in one iteration: they controlled their cross-scan by probing this port's
# live `BLOCKED.md` for a string they knew was in it -- and separately produced
# a FALSE ZERO from a reader they had invented minutes earlier, regexing quoted
# strings out of `check-claims` into 63 phantom phrases that matched nothing.
#
# This scan found six hits today, so it is demonstrably live NOW. The control
# is for the run where their pages no longer contain any of these phrases and
# a zero would otherwise be unfalsifiable: a wrong ref, a wrong pathspec or a
# renamed directory all produce the same clean line.
if [ -n "$_peer_ref" ] && [ "$_peer_probe_done" != "1" ]; then
_peer_probe_done=1
_seen=$(git ls-tree -r --name-only "$_peer_ref" -- docs/re docs/game docs/agents 2>/dev/null | wc -l)
if [ "$_seen" -lt 10 ]; then
echo "🔴 the peer scan can see only $_seen file(s) at $_peer_ref -- a wrong" >&2
echo " ref or pathspec reads the same as a clean corpus. Exit 2." >&2
exit 2
fi
printf ' peer scan reads %s file(s) at %s -- the reader is live\n' "$_seen" "$_peer_ref"
fi
if [ -n "$_peer_ref" ]; then
while IFS= read -r loc; do
[ -z "$loc" ] && continue
# 🔴 REPORTED, NOT COUNTED AS A FAILURE -- corrected before shipping.
#
# The first version put these in `bad`, which failed the run. That applies
# MY marking convention to THEIR corpus: `[refuted]` is a token this port
# uses in its own files, and their pages mark corrections their own way.
# Of the six hits, three are in their `METHOD.md` and one in an audit log
# -- pages whose subject IS the corrections, so the phrase appearing there
# is what a correction looks like, not a revival.
#
# So this is a prompt to look, never a verdict -- the same conclusion the
# withdrawal hook reached about its own candidates. A checker that fails
# on another agent's file for not using this one's punctuation would be
# noise inside a day, and I would have been the one to file it.
printf ' a peer-owned file at their head contains it: %s (%s)\n' \
"${loc#*:}" "$_peer_ref"
peer_hits=$((peer_hits+1))
done < <(git grep -ril -- "$claim" "$_peer_ref" -- docs/re docs/game docs/agents 2>/dev/null || true)
fi
scanned=$((scanned + hits))
if [ "$bad" -eq 0 ]; then
printf ' %-42s %d file(s), %d occurrence(s) suppressed\n' "$claim" "$hits" "$marked"
[ -n "$proposition" ] && [ "$proposition" != "$claim" ] \
&& printf ' it asserted: %s\n' "$proposition"
total_marked=$((total_marked + marked))
else
printf ' %-42s 🔴 %d file(s) assert it unmarked\n' "$claim" "$bad"; fail=1
fi
done <<< "$REGISTER"
# 🔴 LIVENESS. A register full of claims and a tree with nothing in it reports
# clean: the grep matches no files, every row scores 0, and the run passes having
# READ NOTHING. Wrong directory, renamed docs, a bad checkout -- all produce a
# green line. The Decoder's rule for the family: a control that only compares two
# things cannot tell you the comparison is happening.
if [ "$scanned" -eq 0 ]; then
echo "🔴 no file anywhere contains any registered claim -- this check READ" >&2
echo " NOTHING. Exit 2: the harness is broken, not the corpus." >&2
exit 2
fi
echo
if [ "$peer_hits" -gt 0 ]; then
printf ' %d occurrence(s) sit in PEER-OWNED files, read at their branch head\n' "$peer_hits"
echo " rather than from this stale tree. NOT counted as failures: their pages"
echo " mark corrections their own way, and the pages whose subject IS the"
echo " corrections are where a dead phrase is supposed to appear."
echo
echo " 🔴 AND A PEER HIT IS UNADJUDICABLE FROM THE PHRASE ALONE. This register"
echo " indexes PHRASES, not PROPOSITIONS. Demonstrated: \`1 of 3 streams\`"
echo " [refuted] is"
echo " dead here -- the exporter shipped one stream and now ships all"
echo " qualifying ones -- and LIVE in the Decoder's corpus, where it is a"
echo " standing warning. Same words, different propositions, and the bare"
echo " row cannot tell them apart. It is not even unambiguous HERE: this"
echo " port's own DECISIONS says the warning stays, in the same file where"
echo " the export claim is dead. The marker separates them locally because"
echo " the context is mine. Nothing separates them across corpora."
echo
fi
printf ' %d occurrence(s) were SUPPRESSED by a neighbouring `%s`.\n' "$total_marked" "$MARKER"
echo " That number is the size of what this check chose not to look at. A"
echo " detector that can discard a candidate without saying how many has an"
echo " unfalsifiable clean run -- its zero reads the same whether or not a live"
echo " assertion is hiding among the marked ones."
echo
[ $fail -eq 0 ] && echo "every refuted claim appears only inside its correction" \
|| echo "🔴 a refuted claim is still being asserted"
exit $fail