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]",