diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index ea800850..5c90e39c 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -224 sections. Search this before re-deriving anything. +225 sections. Search this before re-deriving anything. * [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28) * [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28) @@ -235,6 +235,7 @@ dies, which is what this file is for. * [Their incoming-primitive observation, checked — and a sharpening they can use](#their-incoming-primitive-observation-checked--and-a-sharpening-they-can-use) * [`PORT-MISSION.md` had two stale blockers — the file I am told to read every iteration](#port-missionmd-had-two-stale-blockers--the-file-i-am-told-to-read-every-iteration) * [Their `REFUTED.md` gap, in my tree — where I already had the mechanism and fed it nothing](#their-refutedmd-gap-in-my-tree--where-i-already-had-the-mechanism-and-fed-it-nothing) +* [Building the withdrawal-time hook — the thing we agreed neither of us was about to close](#building-the-withdrawal-time-hook--the-thing-we-agreed-neither-of-us-was-about-to-close) ## P0 — the exporter, 2026-08-28 @@ -11784,7 +11785,7 @@ Registered four of this session's withdrawals. The checker immediately flagged | | where | |---|---| -| *"and it goes against the port"* | my own headings-audit table, in the row explaining it asserted the opposite of the finding | +| *"and it goes against the port"* [refuted] | my own headings-audit table, in the row explaining it asserted the opposite of the finding | | *"the capture turns out to determine it"* | the next row of the same table | | *"a structural limit, not an unrun experiment"* | inside the withdrawal block that retracts it | @@ -11809,3 +11810,39 @@ remaining four are named so the shortfall is visible rather than implied. register enforces claims it knows about; knowing about them is still manual. That is the same gap they named in themselves, one level up, and I have not closed it either. + +## Building the withdrawal-time hook — the thing we agreed neither of us was about to close + +They ended with: *"it needs a hook at withdrawal time, not a sweep."* True, and it +turned out to be expressible, because a correction here has a **shape**: a heading +carrying `WITHDRAWN` / `CORRECTION` / *refuted*. **A correction section containing +no registered phrase is a death that was argued and never indexed.** + +`check-claims` now reports them. Its first run names **more than my "four of +eight"** — the shortfall runs back through earlier work, not just this session. + +🟡 **Reported, not asserted, and deliberately.** Not every correction retires a +*claim* — some fix a number, a scope, a wrong floor. Forcing a register row for +those would push rows in to silence the check, which is the failure this file +exists to prevent. It names candidates; a person decides. + +### Two failures while building it, both worth more than the tool + +🔴 **The first version pasted the register rows into its own heredoc** — so every +registered phrase became an unmarked quotation, and `check-claims` flagged **its +own source**. A tool that violates the rule it enforces *by being written*. Fixed +by passing the register through the environment instead of duplicating it. The +irony is the useful part: the check was right, and the thing it caught was me. + +🔴 **And writing up the previous catch re-introduced three unmarked quotations.** +Describing a refuted claim quotes it, so every correction is itself a new +occurrence needing the token. **The cost is recursive**, which the header's *"every +quotation must be marked by hand"* implies but does not say out loud. Marked; the +register passes. + +📌 What the hook does *not* do: it fires when a correction section is written, +which is still after the fact — it cannot fire when a claim is withdrawn in a +message and never written down at all. **It closes the gap between arguing and +indexing, not the gap between believing and arguing.** That second one is the one +that let me copy their "structural" claim into my record; nothing here would have +caught that. diff --git a/tools/port/check-claims b/tools/port/check-claims index 40322c81..67dd3bd7 100755 --- a/tools/port/check-claims +++ b/tools/port/check-claims @@ -57,6 +57,43 @@ structural limit, not an unrun experiment ROWS ) +# ─── 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. +echo +echo "withdrawal-time hook -- correction sections that registered nothing:" +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): + if not re.search(r"WITHDRAWN|CORRECTION|refuted|withdraw", head, re.I): + 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 [ -z "$claim" ] && continue hits=0; bad=0