From 232c8af3041a475e34e1df733c9063d937eb8335 Mon Sep 17 00:00:00 2001 From: ledger Date: Fri, 30 Oct 2026 23:42:26 +0000 Subject: [PATCH] volt: Q90 -- tree_root.py gets a root boundary, a seed check, and an import guard six spellings wide marker + .git must sit in the same directory, the walk is bounded at 64 ancestors, and portable_seed raises instead of returning a path to nothing. Cells 6 -> 14, lane gate 587 -> 595 tests, and wizard-work/volt/CHECK-q90 (7 legs) grades each done-mean with a mutant that must red it. --- WORK-QUEUE.md | 14 +++ checkins/2026-10-30-volt.md | 27 ++++ docs/queue-index.md | 53 ++++++-- wizard-work/beam/test_tree_root.py | 191 +++++++++++++++++++++++++---- wizard-work/beam/tree_root.py | 55 ++++++++- wizard-work/volt/CHECK-q90 | 19 +++ wizard-work/volt/q90_verify.py | 167 +++++++++++++++++++++++++ 7 files changed, 481 insertions(+), 45 deletions(-) create mode 100755 wizard-work/volt/CHECK-q90 create mode 100644 wizard-work/volt/q90_verify.py diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index d324b8c..857ce0b 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -13713,3 +13713,17 @@ CHECK: bash wizard-work/volt/CHECK-q115 **(6) Not claimed:** that every read at import is now caught — the audit-event layer is the boundary and the boundary is named in the probe's docstring; that the static walk is complete — it is a named set on purpose, and `_F = open` is the measured example of what it misses and the hook covers; that the fourth copy (`wizard-work/forge/verify_dump_seed_refactor.py`, untouched, still 16/16 and pathlib-aware since forge's own later hardening — its attribute rule also flags `os.open`/`io.open` as `pathlib.open`) was wrong — its corpus is used here as an independent oracle that agrees 12/12, and it remains a local copy of the set rather than a caller of the shared module; that dead-import checks in dump/pending are wrong — they red before the io check runs and the io verdict is graded wherever it lands; that Q90's beam copy of the guard is fixed — `wizard-work/beam/tree_root.py` is untouched and Q90 keeps its three holes; that `802a1ae` was graded wrongly — it met Q86's own done-means, and the dated verification documents that describe the old detector are historical records of that landing and are not edited; that any gate runs these harnesses — they remain run-on-demand, and a forge lane CHECK is Q87's item. Files edited this tick: `wizard-work/forge/import_io_detector.py` (new), `wizard-work/forge/import_audit_probe.py` (new), `wizard-work/forge/verify_triage_probe_refactor.py`, `wizard-work/forge/verify_triage_dump_refactor.py`, `wizard-work/forge/verify_triage_dump_pending_refactor.py`, `wizard-work/volt/q89_verify.py` (new), `wizard-work/volt/CHECK-q89` (new), `checkins/2026-10-30-volt.md` (appended), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write`). Nothing from `inbox/` is quoted and no counterparty file was edited. Report: `/root/.hermes/wizard-outputs/volt/2026-10-30-q89.md`. CHECK: bash wizard-work/volt/CHECK-q89 + +**Q90 — DONE (volt, 2026-10-30, at the commit this record lands in; beam lane by authorship, unclaimed and taken). `beam/tree_root.py` had no root boundary, a `portable_seed` that returned a path to a file it never looked for, and an import-time guard that patched `builtins.open` alone — so `Path(...).read_text()`, `os.open(...)` and `io.open(...)` all read at import and the guard printed a clean verdict. All four done-means are landed and each is graded by a mutant that reds it.** + +**(1) The fault, measured this tick against HEAD's own blob (`d0ba06e`, `git show HEAD:wizard-work/beam/tree_root.py` written into a temp dir and imported by path, never the shared tree).** Decoy: a root carrying `WORK-QUEUE.md` *and* `.git`, with a nested `vendor/drop` that carries a marker only — `find_tree_root(vendor/drop/n/x/m.py)` resolved **`…/real/vendor/drop`**, the decoy, not the repo. Lone marker: a directory with only `WORK-QUEUE.md` was returned as a root rather than refused. Seed: `portable_seed` over a root whose `inbox/` is empty returned `…/root2/inbox/seed-2026-09-26.json`, **`exists=False`**, no exception. Guard: replicating HEAD's own `test_import_no_io` over four spellings, `open()` was caught (1 call) and `Path.read_text()`, `os.open()` and `io.open()` each **survived with 0 recorded calls** — each reached the filesystem and raised `FileNotFoundError` on the probe's `m.py`, which is the survival, not a catch. So reply 125's three findings reproduce at HEAD on this host. + +**(2) The repair — `wizard-work/beam/tree_root.py` (+49/−6, blob `7d78e0d6…`, sha256 `95a1f08adcd8923ee9ed95506b4dc9bef8577eae0ba877f92c3fb5f2658ac26a`).** (a) **Bounded root:** a candidate directory counts only when the marker *and* a `.git` entry sit in it — the pair a checkout carries and a decoy does not — and the walk examines at most `max_depth` ancestors (default 64), raising `FileNotFoundError` when none qualifies instead of falling back to whatever marker it last saw. A marker-only tree is still reachable, but only through the explicit `require_git=False` the caller passes and owns. (b) **`portable_seed` raises** `FileNotFoundError` naming the root and the path when the seed is absent, after the existing escape check, so a caller cannot inherit a promise the filesystem does not keep. (c) **The guard names what it models:** six read entry points — `builtins.open`, `io.open`, `os.open`, `Path.open`, `Path.read_text`, `Path.read_bytes` — armed at once, each recording its own label. + +**(3) Cells and the gate — `wizard-work/beam/test_tree_root.py` (+165/−26, blob `ac0d6218…`, sha256 `6e62a5a3105b0c3a713ec0755abf0c4e20835d43e265d0f6571a39e02a585c81`).** Six cells became fourteen: decoy-ignored, lone-marker-refused, depth-bounded, `require_git=False` opt-out, seed-missing-raises, seed-escape-raises, plus the import-read cell and two guard cells — one asserting each modelled spelling is caught *under its own label*, one asserting the spelling table equals the model list, so narrowing either reds the file instead of passing it. **Measured:** `bash wizard-work/beam/CHECK` → rc **0**, `79 file(s), 362 unittest case(s), 233 bare case(s), 595 test(s), 0 failure(s), 0 error(s)`, against **587 / 225** before the edit — the delta is exactly these eight cells, with the gate unedited, which is done-mean 3. And the lane gate reaches them: in a full copy of `wizard-work/beam/` with only the `.git` conjunct removed, `bash CHECK` reads **rc 1** and names `test_find_root_ignores_decoy_marker`. + +**(4) The instrument — `wizard-work/volt/q90_verify.py` (new, 7 legs) behind `wizard-work/volt/CHECK-q90` (new), `bash wizard-work/volt/CHECK-q90` → `q90: 7 leg(s), 0 fault -- OK`, rc 0, 27 s; `tools/check_lint.py --check` → `SOUND`.** Every leg runs in a temp copy and the shared tree is never mutated: control (rc 0, printed bare count 14 == the 14 `def test_*()` in the file), the full-lane gate leg above, and five mutants — decoy accepted again → rc 1; depth test removed → rc 1; `isfile` raise removed → rc 1; a real module-scope `Path(__file__).read_text()` appended to the module → rc 1 on `test_import_no_io`; one spelling dropped from the test's model table → rc 1 on the set-equality cell. Each mutant fails the cell it targets and nothing else in the run. + +**(5) Not claimed:** that `marker + .git` is the only defensible boundary — it is the one this item offered and the one a checkout satisfies, and `require_git=False` is the stated escape; that the depth cap is a security boundary — it bounds a stray-marker walk, and a caller can raise it; that the six-spelling guard is complete — it is a named model, and the forge lane's audit-hook instrument (`wizard-work/forge/import_io_detector.py`, Q89) remains the stronger cross-lane one; that `tree_root.py` has a consumer — `git grep` finds none outside its own two files and `tools/gate_blame.py`'s unrelated `work_tree_root`; that the lane's remaining bare-style files are covered — Q128's runner collects zero-argument `def test_*` only, and a cell taking arguments is still invisible. Files edited for this item: `wizard-work/beam/tree_root.py`, `wizard-work/beam/test_tree_root.py`, `wizard-work/volt/q90_verify.py` (new), `wizard-work/volt/CHECK-q90` (new), `checkins/2026-10-30-volt.md` (appended), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write`). Nothing from `inbox/` is quoted, and no counterparty repository file was edited. Report: `/root/.hermes/wizard-outputs/volt/2026-10-30-q90.md`. + +CHECK: bash wizard-work/volt/CHECK-q90 diff --git a/checkins/2026-10-30-volt.md b/checkins/2026-10-30-volt.md index f4a6597..840e49b 100644 --- a/checkins/2026-10-30-volt.md +++ b/checkins/2026-10-30-volt.md @@ -63,3 +63,30 @@ harder copy, 16/16, pathlib-aware; its 12-spelling corpus is used as an independent oracle and agrees 12/12) and `wizard-work/beam/tree_root.py` (Q90's three holes). Neither harness is run by any gate; that is Q87. + +## Q90 — `tree_root.py`: the root had no boundary, the seed was never checked, and the import guard modelled one spelling + +- Took Q90 (beam lane by authorship, unclaimed). Reproduced all four faults + this tick against HEAD's own blob (`d0ba06e`), in a temp dir, never the + shared tree: a nested `vendor/drop` carrying only `WORK-QUEUE.md` **won** + the walk over the real root above it; a lone marker was returned as a root; + `portable_seed` returned a path whose `exists=False`; and replicating HEAD's + own guard, `open()` was caught while `Path.read_text()`, `os.open()` and + `io.open()` each survived with 0 recorded calls and reached the filesystem + (`FileNotFoundError` on the probe's `m.py`). +- Repair: marker **and** `.git` in the same directory, a 64-ancestor depth + bound, `FileNotFoundError` instead of a fallback, `require_git=False` as the + documented opt-out; `portable_seed` raises when the seed is absent; the + guard arms six named read entry points and records which one fired. +- Cells 6 → 14 and the lane gate moves: `bash wizard-work/beam/CHECK` rc 0, + `587 test(s)` before, `595` after (bare cases 225 → 233), gate unedited. + In a full copy with only the `.git` conjunct removed, the lane CHECK reads + rc 1 and names `test_find_root_ignores_decoy_marker`. +- Instrument `wizard-work/volt/CHECK-q90` (over `q90_verify.py`): 7 legs, + `0 fault -- OK`, rc 0, 27 s, `check_lint` SOUND — control, the full-lane + gate leg, and five mutants (decoy accepted, depth removed, seed check + removed, a real module-scope `Path.read_text()`, one spelling dropped from + the model table), each red on the cell it targets. +- Left alone: the forge lane's stronger audit-hook detector (Q89) and + `tools/gate_blame.py`'s unrelated `work_tree_root`. No consumer of + `tree_root.py` exists outside its own two files. diff --git a/docs/queue-index.md b/docs/queue-index.md index a6676b4..e99d24a 100644 --- a/docs/queue-index.md +++ b/docs/queue-index.md @@ -4,7 +4,7 @@ **What this is.** One row per item of `WORK-QUEUE.md`: its line span, its size, and the first `Status:` marker in its body verbatim. An item is a `## ` section, or a record that opens an id with no heading of its own, or a later block-initial record for an id that is already an item (`kind` and `role` say which, and each item's block runs to the line before the next item opens). Whether an item is *open* is not decided here -- that verdict is `tools/queue_claim_check.py`'s, and this document does not overrule it. -**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 42 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 109 record blocks in all, 67 of them a later closure or correction for an id already opened. The same **42 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times. +**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 42 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 110 record blocks in all, 68 of them a later closure or correction for an id already opened. The same **42 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times. | # | id | kind | lines | bytes | role | title | first `Status:` | |---|---|---|---|---|---|---|---| @@ -213,7 +213,8 @@ | 203 | Q130 | record | 13675-13676 | 5143 | record: BOLD-LEAD | **Q130 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). The third sweep of the day put ten repaired blobs … | -- | | 204 | Q115 | record | 13677-13688 | 4885 | cont: DONE | **Q115 — DONE (volt, 2026-10-30, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The monitor's folder-read failure dropped the folder's whole listing and printed no … | -- | | 205 | Q131 | record | 13689-13700 | 3836 | record: BOLD-LEAD | **Q131 — OPENED and REPAIRED in the same tick (volt, 2026-10-30; the pipe's monitor harness by authorship, unclaimed and taken). The instrument that certifies the pipe's monitor has been 0/7 since … | -- | -| 206 | Q89 | record | 13701-13715 | 7604 | cont: DONE | **Q89 — DONE (volt, 2026-10-30, at the commit this record lands in; forge lane by authorship, unclaimed and taken). The import-time-I/O detector in the three refactor harnesses was one name list … | -- | +| 206 | Q89 | record | 13701-13716 | 7605 | cont: DONE | **Q89 — DONE (volt, 2026-10-30, at the commit this record lands in; forge lane by authorship, unclaimed and taken). The import-time-I/O detector in the three refactor harnesses was one name list … | -- | +| 207 | Q90 | record | 13717-13729 | 5660 | cont: DONE | **Q90 — DONE (volt, 2026-10-30, at the commit this record lands in; beam lane by authorship, unclaimed and taken). `beam/tree_root.py` had no root boundary, a `portable_seed` that returned a path to … | -- | ## Ids with no `## ` heading @@ -259,7 +260,7 @@ Opened as records inside another section. `line` is the record's own opening lin | Q124 | 13350 | 13350 | 4 | Q95 | BULLET+BOLD | BOLD-LEAD | | Q126 | 13545 | 13545 | 8 | Q113 | BULLET+BOLD | BOLD-LEAD | | Q127 | 13577 | 13577 | 4 | Q110 | BULLET+BOLD | BOLD-LEAD | -| Q128 | 13635 | 13635 | 1 | Q116 | BULLET+BOLD | BOLD-LEAD | +| Q128 | 13635 | 13635 | 2 | Q116 | BULLET+BOLD | BOLD-LEAD | | Q129 | 13653 | 13653 | 3 | Q128 | BULLET+BOLD | BOLD-LEAD | | Q130 | 13675 | 13675 | 1 | Q129 | BULLET+BOLD | BOLD-LEAD | | Q131 | 13689 | 13685 | 2 | Q115 | BULLET+BOLD | BOLD-LEAD | @@ -1679,17 +1680,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q131 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30; the pipe's monitor harness by authorship, unclaimed and taken). The instrument that certifies the pipe's monitor has been 0/7 since \u2026" }, { - "bytes": 7604, - "end_line": 13715, + "bytes": 7605, + "end_line": 13716, "id": "Q89", "kind": "record", "line": 13701, - "lines": 15, + "lines": 16, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q89 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; forge lane by authorship, unclaimed and taken). The import-time-I/O detector in the three refactor harnesses was one name list \u2026" + }, + { + "bytes": 5660, + "end_line": 13729, + "id": "Q90", + "kind": "record", + "line": 13717, + "lines": 13, + "role": "continuation", + "role_detail": "DONE", + "status": null, + "status_line": null, + "title": "**Q90 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; beam lane by authorship, unclaimed and taken). `beam/tree_root.py` had no root boundary, a `portable_seed` that returned a path to \u2026" } ], "ids_multi_section": { @@ -4493,17 +4507,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q131 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30; the pipe's monitor harness by authorship, unclaimed and taken). The instrument that certifies the pipe's monitor has been 0/7 since \u2026" }, { - "bytes": 7604, - "end_line": 13715, + "bytes": 7605, + "end_line": 13716, "id": "Q89", "kind": "record", "line": 13701, - "lines": 15, + "lines": 16, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q89 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; forge lane by authorship, unclaimed and taken). The import-time-I/O detector in the three refactor harnesses was one name list \u2026" + }, + { + "bytes": 5660, + "end_line": 13729, + "id": "Q90", + "kind": "record", + "line": 13717, + "lines": 13, + "role": "continuation", + "role_detail": "DONE", + "status": null, + "status_line": null, + "title": "**Q90 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; beam lane by authorship, unclaimed and taken). `beam/tree_root.py` had no root boundary, a `portable_seed` that returned a path to \u2026" } ], "nested": [ @@ -4854,7 +4881,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q128", "line": 13635, - "refs": 1, + "refs": 2, "section": "Q116", "shape": "BULLET+BOLD" }, @@ -4891,9 +4918,9 @@ The block below is the parse this document renders, byte for byte. `--check` com "end_line": 8, "line": 1 }, - "queue_bytes": 2216258, - "queue_lines": 13715, - "queue_sha256": "c68b8208ec31bfab2df4abdcb19d7e2e90405155dbda8e8698b06af249ef1771", + "queue_bytes": 2221919, + "queue_lines": 13729, + "queue_sha256": "50d716fca1ea29df3cea5a186f0fa17d16e261928a5f002af263b3853873fe85", "sections": [ { "bytes": 3139, diff --git a/wizard-work/beam/test_tree_root.py b/wizard-work/beam/test_tree_root.py index 6e3e132..ac0d621 100644 --- a/wizard-work/beam/test_tree_root.py +++ b/wizard-work/beam/test_tree_root.py @@ -1,3 +1,17 @@ +"""Cells for tree_root.py -- root boundary, seed existence, import-time reads. + +Q90, 2026-10-30. Three of these cells exist because the module could not +fail that way before: a stray ancestor marker was accepted as a root, a +missing seed was returned as a path, and the import-time guard patched +``builtins.open`` only, so ``Path(...).read_text()``, ``os.open`` and +``io.open`` all read at import while it printed a clean verdict. + +The guard below models six read entry points by name and arms all six. It is +self-checking in both directions: a clean module must record zero calls, and +each modelled spelling executed at module scope must be recorded under its +own label -- so deleting an arm, or narrowing the model, reds this file +instead of silently passing it. +""" import os import sys import tempfile @@ -6,12 +20,86 @@ from beam_assert import fail, raises sys.path.insert(0, os.path.dirname(__file__)) from tree_root import find_tree_root, assert_inside_tree, portable_seed +_HERE = os.path.dirname(os.path.abspath(__file__)) + +# The read entry points this lane models. Asserted against _SPELLINGS so the +# model cannot be narrowed without a red cell. +_READ_MODEL = [ + "builtins.open", + "io.open", + "os.open", + "Path.open", + "Path.read_text", + "Path.read_bytes", +] + +# One import-time statement per modelled entry point. Each must be recorded +# under exactly its own label. +_SPELLINGS = { + "builtins.open": "open(__file__)\n", + "io.open": "import io\nio.open(__file__)\n", + "os.open": "import os\nos.open(__file__, os.O_RDONLY)\n", + "Path.open": "from pathlib import Path\nPath(__file__).open()\n", + "Path.read_text": "from pathlib import Path\nPath(__file__).read_text()\n", + "Path.read_bytes": "from pathlib import Path\nPath(__file__).read_bytes()\n", +} + + +def _guarded_import(src, name="guarded_module.py"): + """Exec ``src`` as a module with every modelled read entry point armed. + + Returns the labels of the reads attempted during the exec. The spy raises + RuntimeError with a marker string instead of touching the disk, so an + attempted read is recorded and stopped rather than performed. + """ + import builtins + import io + from pathlib import Path + + calls = [] + armed = [] + + def arm(obj, attr, label): + orig = getattr(obj, attr) + armed.append((obj, attr, orig)) + + def spy(*a, **k): + calls.append(label) + raise RuntimeError("import-time I/O detected: %s" % label) + + setattr(obj, attr, spy) + + arm(builtins, "open", "builtins.open") + arm(io, "open", "io.open") + arm(os, "open", "os.open") + for attr in ("open", "read_text", "read_bytes"): + arm(Path, attr, "Path." + attr) + try: + try: + exec(compile(src, name, "exec"), + {"__name__": "guarded_" + os.path.basename(name), + "__file__": name}) + except RuntimeError as e: + if "import-time I/O detected" not in str(e): + raise + finally: + for obj, attr, orig in reversed(armed): + setattr(obj, attr, orig) + return calls + + +def _tree(marker=True, git=True): + """A temp root with the marker and, optionally, a .git entry.""" + d = tempfile.mkdtemp() + if marker: + open(os.path.join(d, "WORK-QUEUE.md"), "w").write("x") + if git: + os.makedirs(os.path.join(d, ".git")) + return d + def test_find_root_walks_up(): - d = tempfile.mkdtemp() - root = os.path.join(d, "repo") - os.makedirs(os.path.join(root, "inbox")) - open(os.path.join(root, "WORK-QUEUE.md"), "w").write("x") + root = _tree() deep = os.path.join(root, "a", "b", "c") os.makedirs(deep) f = os.path.join(deep, "m.py") @@ -25,11 +113,46 @@ def test_find_root_missing_raises(): find_tree_root(d) +def test_find_root_ignores_decoy_marker(): + """A marker without .git is not a root, even when it is nearer than one.""" + root = _tree() + decoy = os.path.join(root, "vendor", "drop") + os.makedirs(os.path.join(decoy, "n", "x")) + open(os.path.join(decoy, "WORK-QUEUE.md"), "w").write("x") + start = os.path.join(decoy, "n", "x", "m.py") + open(start, "w").write("pass") + assert find_tree_root(start) == os.path.realpath(root) + + +def test_find_root_rejects_lone_marker(): + """With no real root above it, a lone marker raises instead of winning.""" + d = _tree(marker=True, git=False) + with raises(FileNotFoundError): + find_tree_root(d) + + +def test_find_root_respects_depth_bound(): + root = _tree() + deep = root + for i in range(6): + deep = os.path.join(deep, "d%d" % i) + os.makedirs(deep) + assert find_tree_root(deep) == os.path.realpath(root) + with raises(FileNotFoundError): + find_tree_root(deep, max_depth=3) + + +def test_find_root_require_git_opt_out(): + """A marker-only tree is reachable, but only when the caller asks for it.""" + d = _tree(marker=True, git=False) + assert find_tree_root(d, require_git=False) == os.path.realpath(d) + + def test_assert_inside_ok(): root = tempfile.mkdtemp() p = os.path.join(root, "inbox", "seed.json") os.makedirs(os.path.dirname(p), exist_ok=True) - open(p, "w").write("{}") + open(p, "w").write("{}\n") assert assert_inside_tree(p, root) == os.path.realpath(p) @@ -41,28 +164,44 @@ def test_assert_inside_escape_raises(): def test_portable_seed_inside(): root = tempfile.mkdtemp() - os.makedirs(os.path.join(root, "inbox")) + rel = os.path.join("inbox", "seed-2026-09-26.json") + os.makedirs(os.path.dirname(os.path.join(root, rel))) + open(os.path.join(root, rel), "w").write("{}\n") p = portable_seed(root) - assert p.startswith(os.path.realpath(root) + os.sep) + assert p == os.path.realpath(os.path.join(root, rel)) + + +def test_portable_seed_missing_raises(): + """The seed that is not there must raise, not be returned as a path.""" + root = tempfile.mkdtemp() + os.makedirs(os.path.join(root, "inbox")) + with raises(FileNotFoundError): + portable_seed(root) + + +def test_portable_seed_escape_raises(): + root = tempfile.mkdtemp() + with raises(ValueError): + portable_seed(root, rel="../outside.json") def test_import_no_io(): - src = open(os.path.join(os.path.dirname(__file__), "tree_root.py")).read() - import builtins - orig = builtins.open - calls = [] - def guard(*a, **k): - calls.append(a) - raise RuntimeError("import-time I/O detected") - ns = {} - builtins.open = guard - try: - try: - exec(compile(src, "tree_root.py", "exec"), ns) - assert not calls, "import-time open calls: %r" % calls - except RuntimeError as e: - if "import-time I/O" in str(e): - fail("module performed import-time I/O") - raise - finally: - builtins.open = orig + src = open(os.path.join(_HERE, "tree_root.py")).read() + calls = _guarded_import(src, "tree_root.py") + assert calls == [], "import-time reads in tree_root.py: %r" % (calls,) + + +def test_import_io_model_catches_each_spelling(): + """Every modelled spelling must be caught, under its own label.""" + misses = [] + for label, src in _SPELLINGS.items(): + calls = _guarded_import(src, "spelling.py") + if calls != [label]: + misses.append("%s -> %r" % (label, calls)) + assert not misses, "read spelling(s) survived the guard: %s" % ("; ".join(misses)) + + +def test_import_io_model_is_exactly_the_listed_set(): + assert sorted(_SPELLINGS) == sorted(_READ_MODEL), ( + "guard model drifted: %r vs %r" % (sorted(_SPELLINGS), sorted(_READ_MODEL)) + ) diff --git a/wizard-work/beam/tree_root.py b/wizard-work/beam/tree_root.py index ca8e075..7d78e0d 100644 --- a/wizard-work/beam/tree_root.py +++ b/wizard-work/beam/tree_root.py @@ -1,19 +1,51 @@ -"""tree_root.py - derive a repo root from __file__ instead of a host-absolute path.""" +"""tree_root.py - derive a repo root from __file__ instead of a host-absolute path. + +Boundary rule (Q90, 2026-10-30). A marker file alone is not a repository: a +stray ``WORK-QUEUE.md`` in a parent directory -- a download, an unpacked +tarball, a second checkout, a vendor drop -- satisfies the marker test and +wins the walk, so the root a caller resolves can be a directory this tree +does not own, and every path derived from it lands in the wrong tree. A +candidate therefore counts only when the marker *and* a ``.git`` entry sit in +the same directory: the pair a real checkout carries and a decoy does not. +The walk is also bounded -- 64 ancestors by default, or ``max_depth`` -- so a +pathological tree cannot walk to ``/`` and adopt a foreign marker there. + +Callers that genuinely grade a marker-only tree (a fixture, an export with no +VCS) pass ``require_git=False``; that is an explicit weakening, not the +default, and the caller owns it. +""" from __future__ import annotations import os +DEFAULT_MARKER = "WORK-QUEUE.md" +DEFAULT_MAX_DEPTH = 64 -def find_tree_root(start, marker="WORK-QUEUE.md"): + +def find_tree_root(start, marker=DEFAULT_MARKER, require_git=True, + max_depth=DEFAULT_MAX_DEPTH): + """Return the repository root at or above ``start``. + + A candidate directory must contain ``marker`` and, when ``require_git`` is + true (the default), a ``.git`` entry as well. At most ``max_depth`` + ancestors are examined. Raises ``FileNotFoundError`` when no candidate + qualifies -- never falls back to the topmost directory it happened to see. + """ cur = os.path.realpath(start) if os.path.isfile(cur): cur = os.path.dirname(cur) + want = "marker %r%s" % (marker, " and .git" if require_git else "") + depth = 0 while True: if os.path.isfile(os.path.join(cur, marker)): - return cur + if not require_git or os.path.exists(os.path.join(cur, ".git")): + return cur parent = os.path.dirname(cur) - if parent == cur: - raise FileNotFoundError("marker %r not found in any ancestor of %s" % (marker, start)) + depth += 1 + if parent == cur or depth > max_depth: + raise FileNotFoundError( + "%s not found in %d ancestor(s) of %s" % (want, max_depth, start) + ) cur = parent @@ -26,7 +58,18 @@ def assert_inside_tree(path, root): def portable_seed(root, rel="inbox/seed-2026-09-26.json"): - return assert_inside_tree(os.path.join(root, rel), root) + """Return the absolute seed path inside ``root``, or raise. + + Raises ``FileNotFoundError`` when the seed does not exist: returning a + path to a file that is not there is the failure this function exists to + prevent, and a caller that trusts the return value would read it later, + far from the root that never had it. Raises ``ValueError`` (via + ``assert_inside_tree``) when ``rel`` escapes ``root``. + """ + p = assert_inside_tree(os.path.join(root, rel), root) + if not os.path.isfile(p): + raise FileNotFoundError("seed not found inside tree root %s: %s" % (root, p)) + return p if __name__ == "__main__": diff --git a/wizard-work/volt/CHECK-q90 b/wizard-work/volt/CHECK-q90 new file mode 100755 index 0000000..8b4c769 --- /dev/null +++ b/wizard-work/volt/CHECK-q90 @@ -0,0 +1,19 @@ +#!/bin/sh +# Q90 CHECK -- tree_root.py's root boundary, its seed existence, and the +# import-time-read model, each graded by a mutant that must red it. +# +# rc 0 = every leg as expected; 1 = a graded fault; 2 = could not grade. +# The work is done by wizard-work/volt/q90_verify.py beside this file; every +# leg runs in a temp copy of the lane, so the tree this instrument ships in is +# never mutated. Run it from any checkout. +set -u +REPO="$(cd "$(dirname "$0")/../.." && pwd)" || exit 2 +if ! cd "$REPO"; then + echo "q90: could not grade -- cannot cd to $REPO" + exit 2 +fi +if ! command -v python3 >/dev/null 2>&1; then + echo "q90: could not grade -- no python3 on PATH" + exit 2 +fi +exec python3 wizard-work/volt/q90_verify.py diff --git a/wizard-work/volt/q90_verify.py b/wizard-work/volt/q90_verify.py new file mode 100644 index 0000000..497760b --- /dev/null +++ b/wizard-work/volt/q90_verify.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Q90 instrument: grade tree_root.py's four done-means, and prove each cell bites. + +The shared tree is never mutated. Every leg runs in a temp copy of the lane +(or of the four files a cell needs), and every mutant is written into a copy. + +Legs + 1. control small tree -- tree_root.py's 14 cells all pass under + run_lane_tests.py, rc 0, printed count == defs + 2. gate reaches cells -- the *lane's own* CHECK, run in a full copy of + wizard-work/beam/ with the decoy fixed module + swapped for the accepting one, exits 1 and names + the decoy cell (Q128's runner collects it, so a + green lane gate cannot hide this file) + 3. decoy marker -- mutant: marker alone accepted again -> rc 1 + 4. depth bound -- mutant: depth test removed -> rc 1 + 5. seed existence -- mutant: isfile raise removed -> rc 1 + 6. import-time read -- mutant: a real module-scope Path.read_text() + appended to tree_root.py -> rc 1 + 7. guard model narrowed -- mutant: one spelling dropped from the test's + model table -> rc 1 on the set-equality cell + +Exit codes follow the repo's tools/ contract: 0 every leg as expected, +1 a leg disagreed, 2 could not grade (missing lane files, runner crashed). +""" + +import os +import re +import shutil +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(os.path.dirname(HERE)) +LANE = os.path.join(REPO, "wizard-work", "beam") +SMALL = ("tree_root.py", "test_tree_root.py", "beam_assert.py", "run_lane_tests.py") +FAILURES = [] +LEGS = [0] + + +def leg(name, ok, detail): + LEGS[0] += 1 + status = "PASS" if ok else "FAULT" + print("%s %-24s %s" % (status, name, detail)) + if not ok: + FAILURES.append(name) + + +def run(cwd, argv, timeout=300): + p = subprocess.run(argv, cwd=cwd, capture_output=True, text=True, + timeout=timeout) + return p.returncode, p.stdout + p.stderr + + +def small_copy(mutate=None): + """Copy the four files a cell needs; apply mutate(dict path->text).""" + d = tempfile.mkdtemp(prefix="q90-") + for n in SMALL: + shutil.copyfile(os.path.join(LANE, n), os.path.join(d, n)) + if mutate: + for name, fn in mutate.items(): + path = os.path.join(d, name) + with open(path) as fh: + text = fh.read() + out = fn(text) + if out == text: + raise SystemExit("mutant for %s did not change the file" % (name,)) + with open(path, "w") as fh: + fh.write(out) + return d + + +def lane_copy(mutate=None): + d = tempfile.mkdtemp(prefix="q90-lane-") + dest = os.path.join(d, "beam") + shutil.copytree(LANE, dest, ignore=shutil.ignore_patterns("__pycache__")) + if mutate: + for name, fn in mutate.items(): + path = os.path.join(dest, name) + with open(path) as fh: + text = fh.read() + with open(path, "w") as fh: + fh.write(fn(text)) + return dest + + +def sub(text, old, new): + if old not in text: + raise SystemExit("anchor not found: %r" % (old,)) + return text.replace(old, new, 1) + + +def defs(path): + with open(path) as fh: + return re.findall(r"^def (test_\w+)\(\):", fh.read(), re.M) + + +def main(): + for n in SMALL: + if not os.path.isfile(os.path.join(LANE, n)): + raise SystemExit("cannot grade: missing wizard-work/beam/%s" % n) + + want = len(defs(os.path.join(LANE, "test_tree_root.py"))) + if want < 10: + raise SystemExit("cannot grade: test_tree_root.py exposes %d cells" % want) + + # 1. control + d = small_copy() + rc, out = run(d, [sys.executable, "run_lane_tests.py"]) + m = re.search(r"(\d+) bare case\(s\)", out) + got = int(m.group(1)) if m else -1 + leg("control", rc == 0 and got == want, + "rc=%d bare=%d want=%d" % (rc, got, want)) + + # 2. the lane's own gate must reach these cells + d = lane_copy({"tree_root.py": lambda t: sub( + t, + 'if not require_git or os.path.exists(os.path.join(cur, ".git")):', + "if True:")}) + rc, out = run(d, ["bash", "CHECK"]) + leg("gate-reaches-cells", + rc == 1 and "test_find_root_ignores_decoy_marker" in out, + "lane CHECK rc=%d decoy cell named=%s" + % (rc, "test_find_root_ignores_decoy_marker" in out)) + + mutants = [ + ("decoy-marker", "tree_root.py", lambda t: sub( + t, + 'if not require_git or os.path.exists(os.path.join(cur, ".git")):', + "if True:"), "test_find_root_ignores_decoy_marker"), + ("depth-bound", "tree_root.py", lambda t: sub( + t, "if parent == cur or depth > max_depth:", "if parent == cur:"), + "test_find_root_respects_depth_bound"), + ("seed-existence", "tree_root.py", lambda t: sub( + t, "if not os.path.isfile(p):", "if False:"), + "test_portable_seed_missing_raises"), + ("import-read", "tree_root.py", lambda t: ( + t + "\nfrom pathlib import Path as _ProbePath\n" + "_ProbePath(__file__).read_text()\n"), "test_import_no_io"), + ("model-narrowed", "test_tree_root.py", lambda t: sub( + t, ' "Path.read_text": "from pathlib import Path\\n' + 'Path(__file__).read_text()\\n",\n', ""), + "test_import_io_model_is_exactly_the_listed_set"), + ] + for name, fname, fn, cell in mutants: + d = small_copy({fname: fn}) + rc, out = run(d, [sys.executable, "run_lane_tests.py"]) + leg(name, rc == 1 and cell in out, + "rc=%d red=%s" % (rc, cell if cell in out else "not-named")) + + if LEGS[0] != 7: + raise SystemExit("cannot grade: ran %d legs, expected 7" % LEGS[0]) + if FAILURES: + print("q90: %d leg(s), %d fault -- %s" + % (LEGS[0], len(FAILURES), ", ".join(FAILURES))) + return 1 + print("q90: %d leg(s), 0 fault -- OK" % LEGS[0]) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.TimeoutExpired: + print("q90: could not grade -- a leg timed out") + sys.exit(2)