From 9d152f1d48fc6102937269cbf33a1fc13c190e92 Mon Sep 17 00:00:00 2001 From: David F Glidden Date: Sat, 5 Sep 2026 11:10:43 +0200 Subject: [PATCH] [FIX] The closure rule reads three header forms the register writes, and declines a fourth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ruled_pendings() intended to detect "the PENDING ids that rulings actually DISPOSE OF" and matched exactly one header form. The register writes four. Censused over 132 placed rulings: 80 em-dash single, 3 parenthetical, 1 plus-joined, 1 slash-joined, 47 naming no PENDING at all. Eight items were counted open while ruled — four of them by the three most recent rulings — which is how the queue reported 64 when 56 was defensible. [FIX] against existing specification: the code failed its own stated intent, and no ruling widens it. Also read: closure declared on an item's own **Awaiting:** line. Two items were counted open with their own bodies saying otherwise (PENDING-82 "CLOSED 2026-08-08", the STATE-CLAIM marker "DISCHARGED 2026-08-26"), because the rule read headers only. THE SLASH-JOINED FORM IS DECLINED, AND THAT IS THE LOAD-BEARING PART. Its sole instance is REVIEWED-116 — PENDING-131/132/133/134, a design gate on a package rather than a disposition of four items, and PENDING-133's body still reads "Awaiting: Steward authorization". Reading it would have falsely closed a live item. Closure detection HIDES items, so an unrecognised form never yields the permissive answer (REVIEWED-132 condition 1). unreadable_ruling_headers() reports the decline so the gap is visible rather than looking like a clean pass. The same reasoning set the body-closure anchor. A bare \bCLOSED\b search of item bodies matches 20 items, including PENDING-95, -108 and -109 — all live, all merely discussing closure. Anchored to the Awaiting line it matches exactly the two closed. Controls are paired throughout: every form that is read has a negative twin holding the form that must not be. 99 controls pass. Verified end-to-end against the live register, not by selftest alone: 64 -> 56, and PENDING-133 still visible. PENDING.md is NOT touched. Widening the instrument to the record is authorized; normalising the record to fit the instrument is not (REVIEWED-132 §6). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3L79vgAnt1x2kxvf23Qt7 --- scripts/wake-digest.py | 96 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/scripts/wake-digest.py b/scripts/wake-digest.py index ce06dfd..493e0a1 100755 --- a/scripts/wake-digest.py +++ b/scripts/wake-digest.py @@ -74,12 +74,37 @@ def item_spans(text): for k, (h, s) in enumerate(heads)] +# An item is also closed by its OWN body saying so, on the `**Awaiting:**` line. +# ANCHORED deliberately, and the anchor is the whole point: closure detection HIDES +# items, so an unrecognised form must never yield the permissive answer (REVIEWED-132 +# condition 1). Measured 2026-09-04 against the live register: this anchored form finds +# exactly the two items closed in their own body (PENDING-82 "CLOSED 2026-08-08", the +# STATE-CLAIM marker "DISCHARGED 2026-08-26") and nothing else, while a bare \bCLOSED\b +# search of item bodies matches TWENTY — including PENDING-95, -108 and -109, all live, +# all of which merely discuss closure. The negative control below holds that line. +AWAITING_CLOSED_RE = re.compile( + r"^\*\*Awaiting:\*\*.*?\b(?:CLOSED|DISCHARGED|WITHDRAWN)\b", re.M) + + def open_items(text): """-> [(header, line_no)] for every item lacking closure evidence. Closure - evidence = a REVIEWED-N (resolved by the caller) or CLOSED/COMPLETED in the - header.""" - return [(h, s) for h, s, _e in item_spans(text) - if "CLOSED" not in h and not h.startswith("COMPLETED")] + evidence = a REVIEWED-N (resolved by the caller), CLOSED/COMPLETED in the + header, or an `**Awaiting:**` line declaring the item closed. + + The third form was added 2026-09-04. Two items were being counted open with + their own bodies saying otherwise, because this read the header only.""" + lines = text.split("\n") + out = [] + for h, s, e in item_spans(text): + if "CLOSED" in h or h.startswith("COMPLETED"): + continue + # header sits at 0-based s-1; body ends before the NEXT header at 0-based e-1, + # and that exclusion matters — otherwise a following header carrying CLOSED + # would close the item above it. + if AWAITING_CLOSED_RE.search("\n".join(lines[s:e - 1])): + continue + out.append((h, s)) + return out def tag_of(text, header): @@ -445,8 +470,38 @@ def ruled_pendings(reviewed_text): neither until it matches on the SUBJECT — the title or the decision — rather than on the presence of an id token in a header. Remedy is PENDING-142, unbuilt; this note exists so the code does not keep asserting the opposite while it waits. + + ⚠ WIDENED 2026-09-04, and the widening is deliberately PARTIAL. The single + em-dash form below was the only one recognised, while the register writes four. + Censused over 132 placed rulings: 80 em-dash single, 3 parenthetical, 1 + plus-joined, 1 slash-joined, 47 naming no PENDING at all. The parenthetical and + plus-joined forms are now read — they cost eight items their closure, four of them + ruled by the three most recent rulings, which is how a queue reported 64 while 56 + was defensible. + + THE SLASH-JOINED FORM IS NOT READ, AND THAT IS A DECISION RATHER THAN AN OMISSION. + Its sole instance is `REVIEWED-116 — PENDING-131/132/133/134 —`, a DESIGN-GATE on a + package, and PENDING-133's own body still reads `Awaiting: Steward authorization`. + Reading it as disposition would falsely close a live item, which is the direction + this function must never guess in. It is reported instead — see + `unreadable_ruling_headers`, which exists so the gap is visible rather than silent. """ - return set(re.findall(r"^## REVIEWED-\S+\s*—\s*PENDING-(\S+?)\s*—", reviewed_text, re.M)) + ids = set(re.findall(r"^## REVIEWED-\S+\s*—\s*PENDING-(\S+?)\s*—", reviewed_text, re.M)) + # REVIEWED-N (PENDING-M) — : the form of the three most recent rulings. + ids |= set(re.findall(r"^## REVIEWED-\S+\s*\(PENDING-([^)\s]+)\)\s*—", reviewed_text, re.M)) + # REVIEWED-N — PENDING-A + PENDING-B — : an explicitly joint ruling disposes of both. + for joint in re.findall(r"^## REVIEWED-\S+\s*—\s*((?:PENDING-\d+\s*\+\s*)+PENDING-\d+)\s*—", + reviewed_text, re.M): + ids |= set(re.findall(r"PENDING-(\d+)", joint)) + return ids + + +def unreadable_ruling_headers(reviewed_text): + """-> [header] for rulings that name PENDING ids in a form `ruled_pendings` will + not treat as closure. Honest degradation: the closure rule declines to guess, and + this is how the declining becomes visible instead of looking like a clean pass.""" + return [h for h in re.findall(r"^## (REVIEWED-.*)$", reviewed_text, re.M) + if re.search(r"PENDING-\d+(?:/\d+)+", h)] def sec_pending(): @@ -1028,6 +1083,37 @@ def selftest(): len(open_items("## PENDING-S2 — x\n## PENDING — ICP-19")) == 2) chk("open_items excludes CLOSED / COMPLETED", open_items("## PENDING-1 — x (CLOSED)\n## COMPLETED — y") == []) + # --- closure-rule widening, 2026-09-04. Every control below is paired: the form is + # read, AND the form that must NOT be read stays open. Closure hides items, so the + # negative halves are the load-bearing ones. + _AWT_CLOSED = "## PENDING-2 — x\n**Awaiting:** ~~steward~~ → **INSTALLED. CLOSED 2026-08-08.**" + chk("open_items honours closure declared on the Awaiting line", + open_items(_AWT_CLOSED) == []) + chk("open_items honours DISCHARGED on the Awaiting line", + open_items("## PENDING-2 — x\n**Awaiting:** nothing — **DISCHARGED 2026-08-26**") == []) + chk("CLOSED elsewhere in the body does NOT close an item " + "[negative control — bare word-match would close 20, three of them live]", + [h for h, _ in open_items("## PENDING-2 — x\nthis waits until PENDING-9 is CLOSED\n" + "**Awaiting:** steward authorization")] == ["PENDING-2 — x"]) + chk("a following header's CLOSED does not close the item above it [span boundary]", + [h for h, _ in open_items("## PENDING-2 — x\nbody\n## PENDING-3 — y (CLOSED)")] + == ["PENDING-2 — x"]) + chk("ruled_pendings reads the em-dash form", + "12" in ruled_pendings("## REVIEWED-9 — PENDING-12 — t")) + chk("ruled_pendings reads the PARENTHETICAL form [3 rulings; cost 4 items closure]", + "137" in ruled_pendings("## REVIEWED-133 (PENDING-137) — t")) + chk("ruled_pendings reads a PLUS-JOINED joint ruling, BOTH ids", + {"157", "158"} <= ruled_pendings("## REVIEWED-127 — PENDING-157 + PENDING-158 — t")) + chk("ruled_pendings does NOT read the slash-joined design-gate form " + "[negative control — PENDING-133 is live and must stay visible]", + not ({"131", "132", "133", "134"} & ruled_pendings( + "## REVIEWED-116 — PENDING-131/132/133/134 — design gate"))) + chk("a ruling header naming no PENDING suppresses nothing [FALSE-OPEN class, kept]", + ruled_pendings("## REVIEWED-128 — The rejection log against §9") == set()) + chk("unreadable_ruling_headers surfaces the form the closure rule declines", + len(unreadable_ruling_headers("## REVIEWED-116 — PENDING-131/132/133/134 — x")) == 1) + chk("unreadable_ruling_headers is silent on forms that ARE read [negative control]", + unreadable_ruling_headers("## REVIEWED-133 (PENDING-137) — t") == []) chk("open_items returns empty on empty input", open_items("") == []) FENCED = "## PENDING-9 — real\nbody\n```markdown\n## REVIEWED-9 — a draft\n```\ntail" chk("open_items ignores a header inside a fenced block [drafting-habit trigger]",