diff --git a/tools/README.md b/tools/README.md index 51db07d..26887d5 100644 --- a/tools/README.md +++ b/tools/README.md @@ -212,7 +212,7 @@ of them, which is why it is stated here rather than in a docstring. | `exists-anywhere.py` | does this name exist at ANY ref, or only on the one checked out? | 0 on the ref · 1 exists unmerged · 2 absent everywhere · **3 established nothing** | | `memory-index-check.py` | does the memory index cover the memory files, and can it be loaded whole? | 0 covered · 1 orphans/dangling/oversize · **2 established nothing** | | `marker-reachability.py` | can any CI invocation actually collect this test? | 0 all reachable · 1 unreachable found · **2 established nothing** | -| `close-condition-scan.py` | which open issues carry no close condition — and which hide one in a comment? | 0 every open issue has one **in its body** · 1 `NONE` or `BURIED` found · **2 established nothing (failed query, empty board, or a truncated reading)** · **3 known-positive failed** · `--self-test` `--states` | +| `close-condition-scan.py` | which open issues carry no close condition — and which hide one in a comment? | 0 every open issue has one **in its body** · 1 `NONE` or `BURIED` found · **2 established nothing (failed query, empty board, or a truncated reading)** · **3 known-positive failed** · `--self-test` `--states` (DECLARES the space) `--by-state` (REPORTS subjects) | | `runnable-condition.py` | can this close condition be RUN, or only agreed with? | 0 every condition RUNNABLE · 1 at least one ASSERTED · 2 established nothing · 3 control failed | ⚙ GENERATED-FROM: --states | | `states-index-check.py` | does a tool's README row agree with the exit codes the tool ITSELF emits? | 0 rows agree · 1 a row disagrees · **2 established nothing** · 3 control failed | | `truncation-guard.py` | can we show this reading was not truncated by a page bound? | 0 **SAFE** (bound known AND count strictly below it) · 1 **TRUNCATED** (count == bound) · **2 UNKNOWN — no bound determinable; ⛔ never read as SAFE** · **3 known-positive failed** · `--self-test` `--quiet` | diff --git a/tools/close-condition-scan.py b/tools/close-condition-scan.py index c4e4873..b92c3d4 100644 --- a/tools/close-condition-scan.py +++ b/tools/close-condition-scan.py @@ -199,8 +199,18 @@ def main(): ap.add_argument("--label", default=None, help="restrict to one label") ap.add_argument("--limit", type=int, default=400) ap.add_argument("--self-test", action="store_true") + # ⛔ RENAMED from --states. `--states` collided across three tools with TWO + # RELATIONS under one name: doctrine-version.py and runnable-condition.py DECLARE + # a state space; this printed SUBJECTS GROUPED BY STATE. ⇒ A format convention + # cannot fix a name that means two things (ARCHITECT, #498). Priority is theirs by + # 62 minutes (e8e1cff 19:07 vs 2fcd8e1 20:09), the count is theirs 2-to-1, and the + # NAME FITS THEIR RELATION — `--states` reads as "tell me the states". So this one + # moved. ⚠ No caller invoked it; only the index row named it. + ap.add_argument("--by-state", action="store_true", dest="by_state", + help="print only ` ` lines, for piping") ap.add_argument("--states", action="store_true", - help="print only ` ` lines, for piping") + help="DECLARE this tool's state space and exit codes (TAB-separated), " + "so an index row can be GENERATED rather than hand-written") a = ap.parse_args() if a.self_test: @@ -252,6 +262,29 @@ def main(): buckets[classify(it)].append(it) if a.states: + # ⇒ The DECLARE relation, conforming to tools/states-index-check.py's contract: + # VERDICT\t\t the state space + # EXIT\t\t what a caller reads + # ⛔ Emitted BEFORE any network call, so declaring the space never depends on + # reaching the forge — a tool that cannot say what it CAN report is worse than + # one that cannot report. + for name, why in ( + ("BODY", "a close condition is in the issue BODY, where a closer reads it"), + ("BURIED", "a condition exists ONLY in a comment — a body-reader sees none"), + ("NONE", "no close condition anywhere — cannot be closed, only declared"), + ): + print(f"VERDICT\t{name}\t{why}") + for code, why in ( + (0, "every open issue carries a clause in its body"), + (1, "NONE or BURIED found — a finding, established"), + (2, "established nothing (failed query, empty board, or a truncated reading)"), + (3, "the known-positive control failed"), + ): + print(f"EXIT\t{code}\t{why}") + result("STATES-DECLARED") + return 0 + + if a.by_state: for state in ("NONE", "BURIED", "BODY"): for it in buckets[state]: print(f"{state} {it['number']}") diff --git a/tools/test_close_condition_scan.py b/tools/test_close_condition_scan.py index 60b5188..bb081f0 100644 --- a/tools/test_close_condition_scan.py +++ b/tools/test_close_condition_scan.py @@ -93,6 +93,39 @@ def test_the_BURIED_remedy_warns_about_superseded_dispositions(self): self.assertIn("LAST comment", out) self.assertIn("WITHDRAWN", out) + def test_states_DECLARES_the_space_and_conforms_to_the_index_contract(self): + """⛔ `--states` is the DECLARE relation, matching doctrine-version.py and + runnable-condition.py. It must emit TAB-separated VERDICT and EXIT lines so + tools/states-index-check.py can GENERATE a row instead of returning VOID.""" + code, out, _ = run([], 2, [issue(1, "x")] ) + # --states short-circuits before any query; drive it directly + import sys as _s + real = _s.argv + _s.argv = ["close-condition-scan.py", "--states"] + import io as _io + from contextlib import redirect_stdout as _rs + buf = _io.StringIO() + try: + with _rs(buf): + rc = ccs.main() + finally: + _s.argv = real + o = buf.getvalue() + self.assertEqual(rc, 0) + self.assertEqual(len([l for l in o.splitlines() if l.startswith("EXIT\t")]), 4) + self.assertEqual(len([l for l in o.splitlines() if l.startswith("VERDICT\t")]), 3) + for l in o.splitlines(): + self.assertEqual(len(l.split("\t")), 3, l) + + def test_by_state_REPORTS_subjects_and_is_a_different_relation(self): + """⚠ The two must not be the same flag. `--by-state` names subjects; `--states` + names the space. One flag answering both is the collision #498 recorded.""" + code, out, _ = run(["--by-state"], 2, [issue(1, "## Done when\nx"), + issue(2, "nothing")]) + self.assertEqual(code, 1) + self.assertIn("NONE 2", out) + self.assertNotIn("VERDICT", out) + def test_empty_board_is_void_not_clean(self): """⚠ Zero open issues is what a MISTYPED LABEL returns. `gh issue list --label ` exits 0 with zero bytes on stdout and stderr, which is