diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 55f2028..2c38504 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -27,7 +27,7 @@ module docstring, flags from the tool's own `--help`, probe rows from | `queue_claim_check.py` | the claim check for `WORK-QUEUE.md`, shipped with its tests. | `--queue` `--json` `--git` `--validate` `--selftest` `--fail-on-stale` `--explain` `--census` | 1 / 1 / 1 | - | | `queue_index.py` | the work queue's table of contents, and the gate that it is current. | `--repo` `--queue` `--index` `--json` `--write` `--check` `--outside` `--selftest` | 1 / 1 / 1 | `index-current` | | `reply_check.py` | does every reply file's own header agree with its own name? | `--repo` `--dir` `--roster` `--head-lines` `--json` `--baseline` `--emit-baseline` | 1 / 0 / 1 | `reply-headings` | -| `selftest_sweep.py` | every instrument proves itself, in one command, and the number it advertises is the number it prints. | `--repo` `--tools-dir` `--timeout` `--json` `--quiet` `--verbose` `--selftest` | 1 / 0 / 1 | `selftest-sweep` | +| `selftest_sweep.py` | every instrument proves itself, in one command, and the number it advertises is the number it prints. | `--repo` `--tools-dir` `--timeout` `--json` `--baseline` `--no-baseline` `--write-baseline` `--quiet` `--verbose` `--selftest` | 1 / 0 / 1 | `selftest-sweep` | | `served_drift_check.py` | does the host actually serve the tree at this revision? | `--root` `--rev` `--base-url` `--prefix` `--exclude` `--max-bytes` `--timeout` `--json` `--quiet` `--selftest` | 1 / 0 / 1 | - | | `tool_map.py` | the instruments get a map, and the map is generated from the tree. | `--repo` `--rev` `--write` `--check` `--json` `--selftest` | 1 / 0 / 1 | - | | `verify.py` | the HOME the index gate did not have: one command, six legs, one exit code, and a row per leg so a partial run cannot look like a clean one. | `--repo` `--queue` `--index` `--checkin-day` `--fast` `--timeout` `--json` `--quiet` `--selftest` | 1 / 0 / 1 | - | diff --git a/tools/tool_map.py b/tools/tool_map.py index 99f2a308..a38156c 100644 --- a/tools/tool_map.py +++ b/tools/tool_map.py @@ -58,10 +58,26 @@ THE PRE-COMMIT BLIND SPOT, AND THE MODE THAT CLOSES IT The gate did not catch it either. `selftest_sweep.py --repo .` at `4602bd5` reads **21 tools, 21 OK, 0 fatal**, because the sweep runs each tool as `[python, path, "--selftest"]` with no `--repo` (`selftest_sweep.py:449`) and - this tool's `DEFAULT_REPO` is an absolute path, so its selftest graded - /root/troupe-standup -- a different, clean tree. That is a second defect, and - it is NOT fixed here: the sweep's selftest axis can grade the wrong tree. It - is recorded because it is why the pre-commit runs stayed green. + this tool's `DEFAULT_REPO` was an absolute path, so its selftest graded + /root/troupe-standup -- a different, clean tree. That was a second defect: + the sweep's selftest axis could grade the wrong tree, and it did. + + FIXED 2026-10-22 (volt). `DEFAULT_REPO` is now derived from this file's own + location -- `dirname(dirname(abspath(__file__)))` -- so the default target is + the checkout the tool lives in: the same path in place, a different path in a + copy. Measured on a clone at `c3058d5`, whose `docs/TOOLS.md` is drifted + (`--check` reads **rc 1, DRIFT at line 30**): + + - before the fix, `--selftest` with no `--repo` -- the sweep's own + invocation, `cwd` = the clone -- read **20/20, rc 0**: it graded + /root/troupe-standup, the clean tree, and called the clone's drift clean; + - the same clone under `--selftest --repo .` read **19/20, rc 1**, the one + failure being the currency cell below; + - after the fix, the repo-less run reads **19/20, rc 1** -- the same + verdict as `--repo .`, because the default now names the copy. + + `selftest_sweep.py` needed no edit: `cwd` was already the tree under sweep, + so the fix alone is enough for the sweep's axis to grade what it walks. `--rev WORKTREE` derives the map from the files on disk, discovered through `git ls-files -- tools/` -- the index, which is what a commit of tracked @@ -103,7 +119,7 @@ import subprocess import sys import tempfile -DEFAULT_REPO = "/root/troupe-standup" +DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEFAULT_DOC = "docs/TOOLS.md" HELP_TIMEOUT = 30 diff --git a/tools/verify.py b/tools/verify.py index 644b75c..0e99a52 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -361,6 +361,16 @@ def leg_argv(key, repo, queue, index, checkin_day=None): raise KeyError(key) +def _elapsed_ms(started): + """Milliseconds elapsed since `started`; the integer clock every row carries. + + Extracted so run_leg() does not recompute `int((time.time() - started) * 1000)` + at each of its exit paths -- the DRY collapse that previously ran the same + expression three times (timeout, OSError, normal verdict). + """ + return int((time.time() - started) * 1000) + + def run_leg(leg, repo, queue, index, timeout, checkin_day=None): """One leg -> one row. rc 0/1/2 are graded; anything else is ungradable.""" tool = os.path.join(repo, "tools", leg["tool"]) @@ -384,30 +394,31 @@ def run_leg(leg, repo, queue, index, timeout, checkin_day=None): proc = subprocess.run(leg_argv(leg["key"], repo, queue, index, day), cwd=repo, capture_output=True, text=True, timeout=timeout) - except subprocess.TimeoutExpired: - row.update(verdict=UNGRADABLE, detail="TIMEOUT after %ss" % timeout) - row["ms"] = int((time.time() - started) * 1000) - return row - except OSError as exc: - row.update(verdict=UNGRADABLE, detail="could not run: %s" % exc) - row["ms"] = int((time.time() - started) * 1000) - return row - - out = (proc.stdout or "") + (proc.stderr or "") - row["rc"] = proc.returncode - row["detail"] = pick_detail(out, summary=(proc.returncode == 0)) - if proc.returncode == 0: - row["verdict"] = PASS - elif proc.returncode == 1: - row["verdict"] = FAULT - elif proc.returncode == 2: - row["verdict"] = UNGRADABLE + except (subprocess.TimeoutExpired, OSError) as exc: + # Both transport failures collapse to the same ungradable shape; only the + # detail differs -- timeout names the deadline, OSError names the error. + if isinstance(exc, subprocess.TimeoutExpired): + detail = "TIMEOUT after %ss" % timeout + else: + detail = "could not run: %s" % exc + row.update(verdict=UNGRADABLE, detail=detail) else: - # rc 3 is retired in this repo (one meaning, two codes); any other code - # is a tool that left the contract, and the run cannot grade through it. - row["verdict"] = UNGRADABLE - row["detail"] = "unexpected rc %d: %s" % (proc.returncode, row["detail"]) - row["ms"] = int((time.time() - started) * 1000) + out = (proc.stdout or "") + (proc.stderr or "") + row["rc"] = proc.returncode + row["detail"] = pick_detail(out, summary=(proc.returncode == 0)) + if proc.returncode == 0: + row["verdict"] = PASS + elif proc.returncode == 1: + row["verdict"] = FAULT + elif proc.returncode == 2: + row["verdict"] = UNGRADABLE + else: + # rc 3 is retired in this repo (one meaning, two codes); any other + # code is a tool that left the contract, and the run cannot grade + # through it. + row["verdict"] = UNGRADABLE + row["detail"] = "unexpected rc %d: %s" % (proc.returncode, row["detail"]) + row["ms"] = _elapsed_ms(started) return row diff --git a/tools/write_probe.py b/tools/write_probe.py index d5289cc..c749183 100644 --- a/tools/write_probe.py +++ b/tools/write_probe.py @@ -191,6 +191,9 @@ FLAG_PROBES = { "queue_index.py": ("--write", ["--repo", "{tinyrepo}", "--queue", "{queue}", "--write", "{flagtarget}"]), + "selftest_sweep.py": ("--write-baseline", + ["--repo", "{tinyrepo}", "--write-baseline", + "{flagtarget}"]), "tool_map.py": ("--write", ["--repo", "{tinyrepo}", "--write", "{flagtarget}"]), } @@ -489,25 +492,29 @@ def selftest(repo): "covered=%s/%s uncovered=%s/%s outside-class=%s/%s" % (probe_ok, faults_ok, probe_un, faults_un, probe_no, faults_no)) - # c13 is the integration cell: this repo's three caller-path write flags are - # probed by the same code path main() uses, over the same fixtures. + # c13 is the integration cell: every caller-path write flag this repo's + # table names is probed by the same code path main() uses, over the same + # fixtures. The roster is READ FROM FLAG_PROBES, not listed here: a cell + # with its own hardcoded three would keep passing while the table grew, + # which is the same blind spot the UNAXED rule closes one level down. real_rows = [] with tempfile.TemporaryDirectory(prefix="write-selftest-flag-") as tdf: fxf = build_fixtures(tdf, repo) - for nm in ("doc_census_check.py", "queue_index.py", "tool_map.py"): + for nm in sorted(FLAG_PROBES): pf = os.path.join(repo, "tools", nm) try: with open(pf, encoding="utf-8") as fh: s = fh.read() except OSError as exc: - cell("c13 this repo's three caller-path write flags probe OK", False, - "unreadable %s: %s" % (pf, exc)) + cell("c13 every caller-path write flag in the table probes OK", + False, "unreadable %s: %s" % (pf, exc)) real_rows = None break real_rows.extend(probe_flags(nm, s, pf, fxf, 300, fxf["tinyrepo"])) if real_rows is not None: - cell("c13 this repo's three caller-path write flags probe OK", - len(real_rows) == 3 and all(r["verdict"] == "OK" for r in real_rows), + cell("c13 every caller-path write flag in the table probes OK", + len(real_rows) == len(FLAG_PROBES) + and all(r["verdict"] == "OK" for r in real_rows), str([(r["tool"], r["flag"], r["verdict"]) for r in real_rows])) # c14/c15 are the blind spot the axis exists for, measured on a fixture tool