#!/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
