From e9991dc7dceaa87747def035d2ba6d72b4fdab3b Mon Sep 17 00:00:00 2001 From: volt Date: Fri, 30 Oct 2026 20:45:00 +0000 Subject: [PATCH] volt: Q130 -- re-land ten blobs a spark sweep reverted, pin the Q126 cells, run the guard as gate leg ten --- WORK-QUEUE.md | 2 + docs/queue-index.md | 75 ++++++-- tools/verify.py | 55 +++++- wizard-work/spark/closed_artifact_guard.py | 151 ++++++++++++++- wizard-work/spark/python-env-check.py | 163 ++++++++++++---- wizard-work/spark/snippet.py | 175 +++++++++++++++--- wizard-work/spark/test_array_intersection.py | 4 +- wizard-work/spark/test_counter_util.py | 4 +- wizard-work/spark/test_email_validator.py | 4 +- wizard-work/spark/test_hermes.py | 110 +++++++++-- wizard-work/spark/test_prime_factorizer.py | 4 +- wizard-work/spark/test_url_title_extractor.py | 6 +- wizard-work/spark/test_validator.py | 10 +- wizard-work/spark/verify_hermes_setup.sh | 167 +++++++++++++---- 14 files changed, 763 insertions(+), 167 deletions(-) diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index ccdd089..cbe8813 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -13671,3 +13671,5 @@ CHECK: bash wizard-work/volt/CHECK-q129 **Q129 — DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever written and this line is the only statement the claim census can anchor to the id.** **Q129 addendum — one more file than the record above lists: `tools/path_claim_baseline.tsv` gains one `RENAMED` row, and the reason is the record's own prose.** Measured on the landing tree before the row existed: `path_claim_check.py --repo . --rev WORKTREE` read `9637 distinct paths, 261 missing, 1 new against the baseline`, and the one row was `4x checkins/2026-10-30-timmy-tm-formidable.md first WORK-QUEUE.md:13655 [NEW -- not in the baseline]` — the record naming the name it renamed away, which the scanner is right to grade as a claim on a path that is not in the tree. The fix is the ledger's stated mechanism and its own precedent, not a reworded record: `checkins/2026-10-19-zephyrus-sovereignty.md` already carries an identical `RENAMED` row ("added at a013842 …, removed at c6bba84 which relocated it to notes/…"), so this row states the same three facts — added at `bd160bb`, renamed at `590c7d9` to `checkins/2026-10-30-timmy.md`, blob `1f3e51c1f9dfd26b7afaae2045f5c604f3c46172` on both sides. After it: `261 entries, 261 matched, 0 new, 0 cleared, 0 dead row(s)`. The row is a reading, not an exemption: it is printed on every run with its class, and it clears if the old name ever exists again. + +**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 back — three Q109/Q120 artifacts and all seven Q126 test cells — in a commit that advertises one new script, and the guard built to catch exactly that was run by nothing.** Measured at HEAD `afb231d` ("spark: add disk usage analyzer script and report", authored `spark`, 2026-10-30T20:17:47Z, 24 files, `568+/743−`) before any edit in this tick: the seven files Q126 rebound are byte-identical to the blobs they replaced — `git rev-parse afb231d:

` equals `git rev-parse b14d73a^:

` for all seven (`test_array_intersection.py` `11063512`, `test_counter_util.py` `fb5d3113`, `test_email_validator.py` `236c91d8`, `test_hermes.py` `546ee1c9`, `test_prime_factorizer.py` `22d90a1a`, `test_url_title_extractor.py` `d1f7e631`, `test_validator.py` `4bd8fe3c`) — and three artifacts are back to the pre-repair blobs Q109/Q120 closed: `python-env-check.py` sha256 `51e7a52334a2` (the `c166bf1` blob), `snippet.py` `dc4dbeeee3a2` and `verify_hermes_setup.sh` `0af7884e8192` (the `3857b60` blobs). `wizard-work/spark/CHECK` also lost the six lines `1cb001a` added to invoke the guard, so the guard Q127 built to catch this exact revert (`wizard-work/spark/closed_artifact_guard.py`, 8 cells) read **7 legs, 2 passed, 5 faults, rc 1** at HEAD and nothing ran it: `tools/verify.py` had no leg for it, and the one leg that did see part of the damage was `import-resolve`, at **7 faults in 7 files** (`OUTSIDE_REPO`, all spark), which is where this tick's opening reading came from (`verify.py --fast`: 9 legs, 7 pass, 1 fault, 1 skipped, rc 1). **The repair, in four parts.** (1) The ten blobs are restored from the revs that certified them — the seven tests from `b14d73a` (Q126), `python-env-check.py`, `snippet.py` and `verify_hermes_setup.sh` from `1cb001a` (Q127) — and each restored file's sha256 matches the pin the guard already carried (`747dcc3afdcc`, `3eb85613fce7`, `b58861a11d38`), so the restore is a digest reading, not a `git checkout` that looked right. (2) The guard gains seven `Q126` pins (`repaired` the digest restored, `pre_repair` the `afb231d` digest, so a fourth sweep is named as a revert) and a `run:` leg that executes each of the seven cells from `wizard-work/spark` — a pin alone would stay green if `test_schema.json`, `valid_test_data.json` or `invalid_test_data.json` vanished, since none of the three is pinned individually. (3) `tools/verify.py` runs the guard as a tenth leg, `closed-artifacts`, through a new `path` key in `LEGS` and a matching branch in `leg_argv` (`--root` the graded tree); its selftest fixtures stub a slash-keyed tool outside `tools/`, and cells 1, 1b, 7 and 12b move 9→10. (4) The guard's fault lines became `FAIL: -- `: measured on the mutant below, the gate's row read FAULT beside a **PASS** contract line, because `verify.py`'s STRONG hint is `FAIL:` and the guard printed `FAIL pin:…`, while the passing contract line carries the word "errors" — the detail-contradicts-verdict class `verify.py` cell 13 exists for. **Evidence, all measured this tick:** guard `rc 0`, `q127: 21 leg(s), 21 passed, 0 fault(s)`; guard `--selftest` `8 cells, 8 passed` (control, four original mutants, two new Q126 mutants — one dropped cell, one drifted byte — and the six-case classifier); `tools/verify.py` `10 legs, 10 pass, 0 fault, 0 ungradable, 0 skipped`, `gate: OK`, rc 0; `tools/verify.py --selftest` `59 cells, 59 passed`; and the gate-level mutant — `git show afb231d:wizard-work/spark/snippet.py` written back over the restored file — reads `closed-artifacts FAULT rc=1` with the row `FAIL: pin:snippet.py -- sha256=dc4dbeeee3a2 is the 3857b60 pre-repair blob -- Q109's repair at ebba640 was overwritten`, `gate: FAULT`, **rc 1**, and reads 10/10 green again after `git checkout 1cb001a --`. **Not claimed:** that the sweep was deliberate (a stale working copy produces identical bytes and intent is Testimony only); that spark's other host-pinned files are repaired — `wizard-work/spark/CHECK` still grades absolute `/root` paths and was deliberately left alone, that is Q109's open class and this record neither closes nor extends it; that the seven cells' data files are pinned (the `run:` leg is what notices their absence, and a third sweep could rotate them without a pin reddening); that the guard's pins may not be re-taken — an intended change to a pinned artifact re-pins by editing `PINS` with the new digest and the commit that took it, which is a deliberate act with a record, not an override flag; that Q126 or Q127 needed reopening — both repairs are re-landed at their certified digests and both records stand. Files edited this tick: the ten restored blobs, `wizard-work/spark/closed_artifact_guard.py`, `tools/verify.py`, `WORK-QUEUE.md` (this record, appended), and `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. diff --git a/docs/queue-index.md b/docs/queue-index.md index d57e0ae..ee552d3 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; 40 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 105 record blocks in all, 65 of them a later closure or correction for an id already opened. The same **40 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; 41 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 106 record blocks in all, 65 of them a later closure or correction for an id already opened. The same **41 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:` | |---|---|---|---|---|---|---|---| @@ -209,7 +209,8 @@ | 199 | Q116 | record | 13617-13634 | 6412 | cont: DONE | **Q116 — DONE (volt, 2026-10-30, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The grep half of the amended done-means now has an instrument that can fail, it … | -- | | 200 | Q128 | record | 13635-13652 | 6007 | record: BOLD-LEAD | **Q128 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; beam lane by authorship, unclaimed and taken). `beam/CHECK` graded zero of the lane's 570 cells on … | -- | | 201 | Q129 | record | 13653-13670 | 6104 | record: BOLD-LEAD | **Q129 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at `590c7d9` and the commit this record lands in; timmy lane by authorship, unclaimed and taken). The newest check-in in the tree … | -- | -| 202 | Q129 | record | 13671-13673 | 1514 | cont: DONE | **Q129 — DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever … | -- | +| 202 | Q129 | record | 13671-13674 | 1515 | cont: DONE | **Q129 — DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever … | -- | +| 203 | Q130 | record | 13675-13675 | 5142 | 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 … | -- | ## Ids with no `## ` heading @@ -237,7 +238,7 @@ Opened as records inside another section. `line` is the record's own opening lin | Q106 | 13171 | 13171 | 14 | Q105 | BULLET+BOLD | BOLD-LEAD | | Q107 | 13189 | 13189 | 18 | Q106 | BULLET+BOLD | BOLD-LEAD | | Q108 | 13267 | 13263 | 3 | Q106 | BULLET+BOLD | BOLD-LEAD | -| Q109 | 13270 | 13270 | 23 | Q104 | BULLET+BOLD | BOLD-LEAD | +| Q109 | 13270 | 13270 | 27 | Q104 | BULLET+BOLD | BOLD-LEAD | | Q110 | 13272 | 13272 | 6 | Q109 | BULLET+BOLD | BOLD-LEAD | | Q111 | 13274 | 13274 | 8 | Q110 | BULLET+BOLD | BOLD-LEAD | | Q112 | 13276 | 13276 | 6 | Q111 | BULLET+BOLD | BOLD-LEAD | @@ -248,15 +249,16 @@ Opened as records inside another section. `line` is the record's own opening lin | Q117 | 13290 | 13288 | 6 | Q96 | BULLET+BOLD | BOLD-LEAD | | Q118 | 13300 | 13300 | 2 | Q111 | BULLET+BOLD | BOLD-LEAD | | Q119 | 13305 | 13305 | 5 | Q118 | BULLET+BOLD | BOLD-LEAD | -| Q120 | 13308 | 13308 | 19 | Q119 | BULLET+BOLD | BOLD-LEAD | +| Q120 | 13308 | 13308 | 21 | Q119 | BULLET+BOLD | BOLD-LEAD | | Q121 | 13342 | 13342 | 4 | Q107 | BULLET+BOLD | BOLD-LEAD | | Q122 | 13344 | 13344 | 6 | Q121 | BULLET+BOLD | BOLD-LEAD | | Q123 | 13346 | 13346 | 4 | Q122 | BULLET+BOLD | BOLD-LEAD | | Q124 | 13350 | 13350 | 4 | Q95 | BULLET+BOLD | BOLD-LEAD | -| Q126 | 13545 | 13545 | 2 | Q113 | BULLET+BOLD | BOLD-LEAD | -| Q127 | 13577 | 13577 | 1 | Q110 | 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 | | Q129 | 13653 | 13653 | 3 | Q128 | BULLET+BOLD | BOLD-LEAD | +| Q130 | 13675 | 13675 | 1 | Q129 | BULLET+BOLD | BOLD-LEAD | **Not in this document.** Ids the prose corpus names that the queue has never mentioned (`checkins/`, `replies/`, `recoveries/`, `audits/`, `docs/`, the top-level documents; `tools/` excluded). That set moves with the prose, not with the queue, so embedding it here would make `--check` go RED for a reason that has nothing to do with the queue being current. It is reported on demand: `python3 tools/queue_index.py --outside`. @@ -1621,17 +1623,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q129 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at `590c7d9` and the commit this record lands in; timmy lane by authorship, unclaimed and taken). The newest check-in in the tree \u2026" }, { - "bytes": 1514, - "end_line": 13673, + "bytes": 1515, + "end_line": 13674, "id": "Q129", "kind": "record", "line": 13671, - "lines": 3, + "lines": 4, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q129 \u2014 DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever \u2026" + }, + { + "bytes": 5142, + "end_line": 13675, + "id": "Q130", + "kind": "record", + "line": 13675, + "lines": 1, + "role": "record", + "role_detail": "BOLD-LEAD", + "status": null, + "status_line": null, + "title": "**Q130 \u2014 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 \u2026" } ], "ids_multi_section": { @@ -4383,17 +4398,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q129 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at `590c7d9` and the commit this record lands in; timmy lane by authorship, unclaimed and taken). The newest check-in in the tree \u2026" }, { - "bytes": 1514, - "end_line": 13673, + "bytes": 1515, + "end_line": 13674, "id": "Q129", "kind": "record", "line": 13671, - "lines": 3, + "lines": 4, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q129 \u2014 DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever \u2026" + }, + { + "bytes": 5142, + "end_line": 13675, + "id": "Q130", + "kind": "record", + "line": 13675, + "lines": 1, + "role": "record", + "role_detail": "BOLD-LEAD", + "status": null, + "status_line": null, + "title": "**Q130 \u2014 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 \u2026" } ], "nested": [ @@ -4582,7 +4610,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q109", "line": 13270, - "refs": 23, + "refs": 27, "section": "Q104", "shape": "BULLET+BOLD" }, @@ -4681,7 +4709,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q120", "line": 13308, - "refs": 19, + "refs": 21, "section": "Q119", "shape": "BULLET+BOLD" }, @@ -4726,7 +4754,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q126", "line": 13545, - "refs": 2, + "refs": 8, "section": "Q113", "shape": "BULLET+BOLD" }, @@ -4735,7 +4763,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q127", "line": 13577, - "refs": 1, + "refs": 4, "section": "Q110", "shape": "BULLET+BOLD" }, @@ -4756,6 +4784,15 @@ The block below is the parse this document renders, byte for byte. `--check` com "refs": 3, "section": "Q128", "shape": "BULLET+BOLD" + }, + { + "first_mention": 13675, + "form": "BOLD-LEAD", + "id": "Q130", + "line": 13675, + "refs": 1, + "section": "Q129", + "shape": "BULLET+BOLD" } ], "preamble": { @@ -4763,9 +4800,9 @@ The block below is the parse this document renders, byte for byte. `--check` com "end_line": 8, "line": 1 }, - "queue_bytes": 2194789, - "queue_lines": 13673, - "queue_sha256": "95d4c744bdbe7b56b388568999e95f09b6660c3defae8647cc2a810314255797", + "queue_bytes": 2199932, + "queue_lines": 13675, + "queue_sha256": "3472c4f6d9b70859dfae17b0be0ea3cf7720662e70dc85f75de3f4c7cce431f3", "sections": [ { "bytes": 3139, diff --git a/tools/verify.py b/tools/verify.py index a26f34e..824d890 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""verify.py -- the HOME the index gate did not have: one command, nine legs, +"""verify.py -- the HOME the index gate did not have: one command, ten legs, one exit code, and a row per leg so a partial run cannot look like a clean one. WHY THIS EXISTS @@ -83,6 +83,19 @@ WHAT IT RUNS -- one row per leg, cheapest first, never in parallel flag other than `--json` (`--write`, `--save-vocab`) on the same rule. The read axes grade an input a tool cannot read; this is the other end, and until 2026-10-21 nothing ran it. + import-resolve tools/import_resolve_check.py --rev WORKTREE + every committed test file binds the module it imports to the + REPO, not to a host path the writer happened to have -- the + Q61(b)/Q100 class, graded by `ast` rather than `grep` so a + docstring that records the fix is not a fault + closed-artifacts wizard-work/spark/closed_artifact_guard.py --root . + every repair a CLOSED item landed is still the bytes that + landed: sha256 pins for Q109/Q120's three artifacts and + Q126's seven rebound cells, each cell run from its own + directory. The instrument lives in the lane that owns those + files; the leg is here because a guard nothing runs is not a + guard -- `afb231d` reverted all ten blobs and no gate leg saw + the three artifacts. The legs are ordered by cost, and a leg is never skipped because an earlier one failed: a stale index and a broken selftest are different defects and the @@ -205,6 +218,11 @@ LEGS = ( "what": "every committed test file binds the module it imports to the " "repo, not to a host path (static sys.path scan plus " "--import-probe)"}, + {"key": "closed-artifacts", "tool": "closed_artifact_guard.py", "slow": False, + "path": os.path.join("wizard-work", "spark", "closed_artifact_guard.py"), + "what": "every repair a closed item landed is still the bytes that landed " + "(sha256 pins for Q109/Q120 and the seven Q126 cells) and its " + "cell still runs green"}, ) # A detail line is a courtesy, not the verdict -- but a verdict with no line @@ -410,6 +428,13 @@ def leg_argv(key, repo, queue, index, checkin_day=None): return [sys.executable, os.path.join(repo, "tools", "import_resolve_check.py"), "--repo", repo, "--rev", "WORKTREE", "--quiet"] + if key == "closed-artifacts": + # --root, so the tree graded is the tree named on the command line and + # never the working directory this leg happened to inherit. + return [sys.executable, + os.path.join(repo, "wizard-work", "spark", + "closed_artifact_guard.py"), + "--root", repo] raise KeyError(key) @@ -425,7 +450,10 @@ def _elapsed_ms(started): 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"]) + # A leg names its tool inside `tools/` by default; `path` lets a leg grade an + # instrument a lane owns in its own directory (the smoke rule is that the + # gate runs it, not that it lives here). + tool = os.path.join(repo, leg.get("path") or os.path.join("tools", leg["tool"])) started = time.time() row = {"key": leg["key"], "tool": leg["tool"], "verdict": None, "rc": None, "detail": "", "what": leg["what"]} @@ -644,7 +672,14 @@ def _fixture(base, rcs, outs=None, sleep=0.0, markers=None, argv_markers=None, for tool, rc in rcs.items(): if rc is None: # tool absent on purpose continue - _stub(os.path.join(repo, "tools", tool), rc, + # A key with a separator is a leg whose instrument lives outside tools/; + # it is stubbed where the leg will look for it. + if os.sep in tool: + dest = os.path.join(repo, tool) + os.makedirs(os.path.dirname(dest), exist_ok=True) + else: + dest = os.path.join(repo, "tools", tool) + _stub(dest, rc, out=(outs or {}).get(tool, ""), sleep=sleep, marker=(markers or {}).get(tool), argv_marker=(argv_markers or {}).get(tool)) @@ -663,7 +698,9 @@ GREEN_ALL = {"checkin_check.py": 0, "path_claim_check.py": 0, "reply_check.py": 0, "queue_index.py": 0, "queue_append_audit.py": 0, "doc_command_check.py": 0, "selftest_sweep.py": 0, "write_probe.py": 0, - "import_resolve_check.py": 0} + "import_resolve_check.py": 0, + os.path.join("wizard-work", "spark", + "closed_artifact_guard.py"): 0} def selftest(): @@ -680,11 +717,11 @@ def selftest(): failures.append("%s\n got: %r\n want: %r" % (label, got, want)) with tempfile.TemporaryDirectory(prefix="verify-selftest-") as td: - # 1. eight green stubs -> rc 0 + # 1. ten green stubs -> rc 0 repo = _fixture(os.path.join(td, "c1"), GREEN_ALL) rc, out = _verify("--repo", repo) - check("1 all nine legs green -> rc 0", rc, 0) - check("1b summary counts the nine legs", "verify: 9 legs, 9 pass, 0 fault, " + check("1 all ten legs green -> rc 0", rc, 0) + check("1b summary counts the ten legs", "verify: 10 legs, 10 pass, 0 fault, " "0 ungradable, 0 skipped" in out, True) # 2. a stale index is a FAULT, and its own line reaches the row @@ -741,7 +778,7 @@ def selftest(): check("7 --json writes the rows", [r["key"] for r in obj.get("rows", [])], ["checkin", "path-claims", "reply-headings", "index-current", "append-audit", "doc-commands", "selftest-sweep", "write-axis", - "import-resolve"]) + "import-resolve", "closed-artifacts"]) check("7b --json carries the same rc the process returned", obj.get("rc"), rc) @@ -785,7 +822,7 @@ def selftest(): check("12 skipped leg is its own count in --json", obj.get("counts", {}).get("SKIP"), 1) check("12b and the skipped leg is not counted as a pass", - obj.get("counts", {}).get(PASS), 8) + obj.get("counts", {}).get(PASS), 9) # 13. the detail line must not contradict the verdict: a row graded # FAULT beside "0 fatal" is the SILENT failure this gate exists to catch. diff --git a/wizard-work/spark/closed_artifact_guard.py b/wizard-work/spark/closed_artifact_guard.py index 8fdb7dd..59da830 100644 --- a/wizard-work/spark/closed_artifact_guard.py +++ b/wizard-work/spark/closed_artifact_guard.py @@ -24,9 +24,21 @@ diagnosis can say "reverted" instead of "changed". Legs (each prints PASS/FAIL/INFO and names its subject): pin: sha256 equals the blob the repair commit landed cell: the item's own cell still runs green + run: a rebound Q126 test file runs rc 0 from its own directory contract: the repaired artifact's documented behaviour, by subprocess env: INFO only -- host packages the Q120 cell is coupled to - selftest six fixtures: one control, five one-edit mutants + selftest eight cells: one control, six one-edit mutants, one + classifier cell + +2026-10-30, second sweep: `afb231d` ("spark: add disk usage analyzer script and +report") put all three of the pinned artifact blobs back AND reverted the seven +spark test files Q126 had rebound, byte for byte, in one commit that advertises +one new script. Nothing ran this guard -- `1cb001a` wired it into +`wizard-work/spark/CHECK` and that commit put the six-line wiring back too -- so +the reverts stood until the repo gate's `import-resolve` leg named the test +files. The Q126 files are pinned and run here now, and `tools/verify.py` runs +this guard as the `closed-artifacts` leg, so the next sweep reds the primary +gate instead of waiting for a reader to notice. Exit codes follow the repo's tools/ contract: 0 = every pinned blob and every cell is as the closed records left it @@ -73,8 +85,76 @@ PINS = { "landed": "ebba640", "pre_rev": "3857b60", }, + # Q126 rebound nine host-bound test files to the module beside them. The + # next spark sweep (`afb231d`, 2026-10-30T20:17:47Z) put all seven of the + # spark ones back to the pre-repair bytes, byte for byte. They are pinned + # here for the same reason as the artifacts above: a repair that can be + # overwritten without a name is not a repair, it is a coincidence. + "test_array_intersection.py": { + "repaired": "d31a5ec9bc9b00a41239f6bac930a46156b234c21e3dfac68e9888a8f252cf18", + "pre_repair": "6f29649d52b50b166ce16fafc3a6021d1068301c8093d3a95cbc939afd9ca673", + "item": "Q126", + "landed": "b14d73a", + "pre_rev": "afb231d", + }, + "test_counter_util.py": { + "repaired": "8648a28152a557b2a7b2466d8232955f5f3fdfcc7445894285fd1c52882898a8", + "pre_repair": "d837f183440640416cdd8c3020ee6035288d099a704e441d2726810408645411", + "item": "Q126", + "landed": "b14d73a", + "pre_rev": "afb231d", + }, + "test_email_validator.py": { + "repaired": "b698838fc4055c1bc3a30f054855b2cf19b79023b27636b53a7dffc5b9278c37", + "pre_repair": "95d8d77c1ee8e75f1438d3915ffc2e8f07d397a8b3d67f38d5e25e81cb84540f", + "item": "Q126", + "landed": "b14d73a", + "pre_rev": "afb231d", + }, + "test_hermes.py": { + "repaired": "b8dad112c1f3e170e3b82e8cfb074e1f30e2648f9c62b38e30aaf837dc83feb0", + "pre_repair": "87f552a14a9945ce9cfdaf29bc959a394fcc779eda519b824ece80d27e99ab91", + "item": "Q126", + "landed": "b14d73a", + "pre_rev": "afb231d", + }, + "test_prime_factorizer.py": { + "repaired": "3d409c79cbeb4d068d73f0cb8dd7e539f837e5afdc9c8770584d1e7db263a9e1", + "pre_repair": "4be81385dfd01a00363977ef6ff787741d1a0ecc9892f030aaefd3b0386509e1", + "item": "Q126", + "landed": "b14d73a", + "pre_rev": "afb231d", + }, + "test_url_title_extractor.py": { + "repaired": "411d781a8d380497def3d4811900f313247d951aee7cef965675dccbdc9b311a", + "pre_repair": "b6ea8fc299a7caa24cacd2ad7aaaed302533ecc7b82f0866265612f92c1696ca", + "item": "Q126", + "landed": "b14d73a", + "pre_rev": "afb231d", + }, + "test_validator.py": { + "repaired": "0dd63e37b805090b9e290f5a1707daae45e4f6275a4ffbca1d9c3416f6a84165", + "pre_repair": "665e21645a6cd0d49c55b269e546617917741ac3f592473251c29f59515edc03", + "item": "Q126", + "landed": "b14d73a", + "pre_rev": "afb231d", + }, } +# A pin proves the bytes; this proves those bytes still measure something. The +# seven Q126 files read the module beside them AND data files that are not +# pinned one by one (test_schema.json and the two data files), so a deleted +# fixture would leave every pin green and the suite dead. +RUNS = [ + "test_array_intersection.py", + "test_counter_util.py", + "test_email_validator.py", + "test_hermes.py", + "test_prime_factorizer.py", + "test_url_title_extractor.py", + "test_validator.py", +] + # The cell that grades each artifact must still be here for the cell leg to run. CELL_FILES = ["test_env_verifiers.py", "test_python_env_check.py"] @@ -123,7 +203,14 @@ class Report: def bad(self, leg, detail): self.failed.append(leg) - print("FAIL %s: %s" % (leg, detail)) + # `FAIL:` -- with the colon -- is the token `tools/verify.py` treats as a + # STRONG hint, so the gate's row carries THIS line rather than the first + # line that happens to contain the word "error" (a passing contract + # line prints "two usage errors rc 2"). Measured on the 2026-10-30 + # mutant: the row read FAULT beside a PASS line until the colon was + # added, which is the detail-contradicts-verdict class verify.py cell 13 + # exists to catch. + print("FAIL: %s -- %s" % (leg, detail)) def note(self, leg, detail): self.info.append(leg) @@ -273,6 +360,31 @@ def leg_env(rep): "environment, not for the code") +def leg_runs(root, rep): + """Q126: the rebound test files run green from their own directory. + + Their sys.path is this directory, so rc 0 here means the run graded the + committed module -- and that the data files the file reads are still there. + """ + spark = os.path.join(root, SPARK) + for name in RUNS: + if not os.path.exists(os.path.join(spark, name)): + rep.bad("run:%s" % name, "absent from %s (Q126's cell is gone)" % SPARK) + continue + try: + proc = run([sys.executable, name], cwd=spark, timeout=180) + except subprocess.TimeoutExpired: + rep.bad("run:%s" % name, "timeout after 180s") + continue + if proc.returncode == 0: + rep.ok("run:%s" % name, + "rc 0 from %s -- the run graded the module beside it" % SPARK) + else: + lines = (proc.stdout + proc.stderr).strip().splitlines() or [""] + rep.bad("run:%s" % name, "rc %d -- %s" + % (proc.returncode, lines[-1][:160])) + + def guard(root, run_cells=True): rep = Report() if not os.path.isdir(os.path.join(root, SPARK)): @@ -282,6 +394,7 @@ def guard(root, run_cells=True): leg_cell_files(root, rep) if run_cells: leg_env_verifiers(root, rep) + leg_runs(root, rep) leg_pyenv_contract(root, rep) leg_env(rep) @@ -300,12 +413,25 @@ def guard(root, run_cells=True): # ---------------------------------------------------------------- selftest -- +# The modules and data files the pinned Q126 cells import. Copied into every +# selftest tree so the control is a tree in which a correct repair reads green: +# without them the cells are right and the fixture is empty, which is a red +# control and looks exactly like a broken repair. +RUN_DEPS = [ + "array_intersection.py", "counter_util.py", "email_validator.py", + "json_schema_cli.py", "prime_factorizer.py", "url_title_extractor.py", + "test_schema.json", "valid_test_data.json", "invalid_test_data.json", +] + + def build_fixture(dest, repo_root, repairs): """Copy the guarded files into `dest`, then apply `repairs` edits.""" spark = os.path.join(dest, SPARK) os.makedirs(spark) - for rel in list(PINS) + CELL_FILES + ["CHECK-env-verifiers"]: - shutil.copy2(os.path.join(repo_root, SPARK, rel), os.path.join(spark, rel)) + for rel in list(PINS) + CELL_FILES + ["CHECK-env-verifiers"] + RUN_DEPS: + src = os.path.join(repo_root, SPARK, rel) + if os.path.exists(src): + shutil.copy2(src, os.path.join(spark, rel)) repairs(dest) return dest @@ -328,6 +454,12 @@ def _drift(dest): fh.write("\n# a sweep appended this line\n") +def _drift_test_file(dest): + """One byte appended to a Q126 cell: the pin must name it.""" + with open(os.path.join(dest, SPARK, "test_counter_util.py"), "a") as fh: + fh.write("\n# a sweep appended this line\n") + + def _drop(path_rel): def _do(dest): os.remove(os.path.join(dest, SPARK, path_rel)) @@ -458,14 +590,21 @@ def selftest(repo_root): cell("mutant-revert-pyenv", _revert_pyenv, 1, "is the c166bf1 pre-repair blob") cell("mutant-drifted-bytes", _drift, 1, "neither the pinned repair") cell("mutant-artifact-absent", _drop("verify_hermes_setup.sh"), 1, "absent") + cell("mutant-test-file-absent", _drop("test_counter_util.py"), 1, + "Q126's cell is gone") + cell("mutant-test-file-drifted", _drift_test_file, 1, "neither the pinned repair") - # The sixth cell is not a tree: it grades the classifier on the digests the - # real reverts produced, so the "reverted" diagnosis cannot rot into prose. + # The classifier cells grade the diagnosis the real reverts produced, so the + # "reverted" naming cannot rot into prose. `afb231d` reproduced every one of + # these pre_repair digests in the tree, which is what the digests record. cases = [ ("snippet.py", PINS["snippet.py"]["pre_repair"], "reverted"), ("python-env-check.py", PINS["python-env-check.py"]["pre_repair"], "reverted"), ("snippet.py", PINS["snippet.py"]["repaired"], "repaired"), ("verify_hermes_setup.sh", "0" * 64, "drifted"), + ("test_prime_factorizer.py", + PINS["test_prime_factorizer.py"]["pre_repair"], "reverted"), + ("test_hermes.py", PINS["test_hermes.py"]["repaired"], "repaired"), ] bad = [c for c in cases if classify(c[0], c[1]) != c[2]] cells.append(("classify-revert-digests", 0 if not bad else 1, not bad)) diff --git a/wizard-work/spark/python-env-check.py b/wizard-work/spark/python-env-check.py index 3d00d9c..ea72723 100644 --- a/wizard-work/spark/python-env-check.py +++ b/wizard-work/spark/python-env-check.py @@ -1,53 +1,140 @@ #!/usr/bin/env python3 -"""Python environment health check - verifies key packages are available.""" +"""Python environment health check for the spark lane. -import sys +The dependency list is the lane's own third-party imports, measured from +`wizard-work/spark/*.py` (`grep -rhoE '^(import|from) +[A-Za-z0-9_.]+'`): + + PyYAML -> yaml debug_yaml.py:2 + numpy -> numpy qc_analyze.py:7 + Pillow -> PIL ascii_tool.py:12, qc_analyze.py:6 + jsonschema -> jsonschema validate_json_schema.py:14 + +`email_validator` is deliberately NOT declared: the lane's +`wizard-work/spark/email_validator.py` is a local, stdlib-only module, so +declaring the PyPI distribution would test a package the lane never uses. + +The check can fail. A dependency that will not import is reported as +`"status": "missing"` and the process exits 1; a package that imports but +raises something other than ImportError is reported as `"status": "broken"`. +`--require` appends a dependency for one run, which is how the committed +cell in `test_python_env_check.py` proves the red path with a package that +is genuinely absent on the host. + +Exit codes (tools/ contract): + 0 every declared dependency imports and reports + 1 at least one declared or --require'd dependency is missing/broken + 2 usage error (argparse) +""" + +import argparse +import importlib +import importlib.metadata import json +import sys from datetime import datetime, timezone -def check_module(name): - try: - __import__(name) - return {"status": "ok", "available": True} - except ImportError as e: - return {"status": "missing", "available": False, "error": str(e)} +# (distribution name, import name) for the lane's own third-party imports +DEPENDENCIES = ( + ("PyYAML", "yaml"), + ("numpy", "numpy"), + ("Pillow", "PIL"), + ("jsonschema", "jsonschema"), +) -def check_package(name, import_name=None): - import_name = import_name or name - try: - mod = __import__(import_name) - version = getattr(mod, '__version__', 'unknown') - return {"status": "ok", "version": version, "available": True} - except ImportError as e: - return {"status": "missing", "available": False, "error": str(e)} -def main(): - results = { - "python": { - "status": "ok", - "version": sys.version, - "executable": sys.executable - }, - "packages": { - "json": check_module("json"), - "subprocess": check_module("subprocess"), - "datetime": check_module("datetime"), +def check_package(distribution, import_name=None): + """Import one dependency by name and report its version. + + The version is read from the distribution metadata first; `__version__` + is the fallback for packages that ship no metadata record. + """ + import_name = import_name or distribution + try: + mod = importlib.import_module(import_name) + except ImportError as exc: + return { + "status": "missing", + "available": False, + "import_name": import_name, + "error": "%s: %s" % (type(exc).__name__, exc), } + except Exception as exc: # a package that raises on import is not healthy + return { + "status": "broken", + "available": False, + "import_name": import_name, + "error": "%s: %s" % (type(exc).__name__, exc), + } + try: + version = importlib.metadata.version(distribution) + except importlib.metadata.PackageNotFoundError: + version = getattr(mod, "__version__", "unknown") + return { + "status": "ok", + "available": True, + "import_name": import_name, + "version": version, } - - all_ok = all( - p.get("status") == "ok" or p.get("available", False) - for p in results["packages"].values() + + +def parse_requirement(text): + """`NAME` or `DISTRIBUTION:IMPORT` -> (distribution, import name).""" + if ":" in text: + distribution, import_name = text.split(":", 1) + if not distribution or not import_name: + raise argparse.ArgumentTypeError( + "--require needs NAME or DISTRIBUTION:IMPORT, got %r" % text + ) + return distribution, import_name + return text, text + + +def build_parser(): + parser = argparse.ArgumentParser( + prog="python-env-check.py", + description="Verify the spark lane's third-party dependencies import, " + "and report their versions.", + ) + parser.add_argument( + "--require", + metavar="NAME[:IMPORT]", + action="append", + default=[], + type=parse_requirement, + help="also require this package for this run (repeatable); " + "useful for proving the red path", + ) + return parser + + +def main(argv=None): + args = build_parser().parse_args(argv) + declared = list(DEPENDENCIES) + list(args.require) + packages = {} + for distribution, import_name in declared: + result = check_package(distribution, import_name) + result["distribution"] = distribution + packages[import_name] = result + missing = sorted( + name for name, result in packages.items() if result["status"] != "ok" ) - output = { - "all_ok": all_ok, + "all_ok": not missing, "timestamp": datetime.now(timezone.utc).isoformat(), - "components": results + "dependencies_declared": len(declared), + "missing": missing, + "components": { + "python": { + "status": "ok", + "version": sys.version, + "executable": sys.executable, + }, + "packages": packages, + }, } - - print(json.dumps(output, indent=2)) - sys.exit(0 if all_ok else 1) + print(json.dumps(output, indent=2, sort_keys=True)) + return 0 if not missing else 1 + if __name__ == "__main__": - main() \ No newline at end of file + sys.exit(main()) diff --git a/wizard-work/spark/snippet.py b/wizard-work/spark/snippet.py index 489591e..b3e4158 100755 --- a/wizard-work/spark/snippet.py +++ b/wizard-work/spark/snippet.py @@ -1,37 +1,156 @@ #!/usr/bin/env python3 +"""Verify the Hermes Agent environment this tree ships into. + +Exit 0: every required directory and every required tool is present. +Exit 1: at least one of them is genuinely absent; the line naming it is printed. +Exit 2: usage error. + +What "available" means. A tool is available only if `hermes_tools` imports and +exposes a *callable* of that name. Nothing else counts: a name in a list is not +a measurement, which is what this file used to print (`available[tool] = True` +in a loop over a hard-coded list, so run under a bare python3 with no Hermes +session it reported four tools and "verified successfully", rc 0 -- Q109). + +Resolution. The profile home is --home, else $HERMES_HOME (which in this +install names the active profile's home, e.g. ~/.hermes/profiles/), +else ~/.hermes/profiles/$HERMES_PROFILE (default profile: spark). The Hermes +root is --root, else the parent of the profile home's `profiles` directory, +else the profile home's own parent. Every path graded is derived from those two, +so the check can be pointed at a synthetic tree and be seen to fail. The +env-first pattern is the lane's own: hermes_agent_verify.py:27. + +Graded by test_env_verifiers.py (black box -- runs this file as a subprocess and +imports nothing from it) and by CHECK-env-verifiers. + +Run: python3 snippet.py [--home DIR] [--root DIR] [--profile NAME] [--require TOOL] """ -Snippet: Verify Hermes Agent environment and available tools. -""" -import sys + +import argparse +import importlib import os +import sys +from pathlib import Path -def check_environment(): - """Verify critical Hermes directories exist.""" - dirs = [ - "/root/.hermes", - "/root/.hermes/profiles", - "/root/.hermes/skills", - "/root/.hermes/plugins", +TOOLS = ["web_search", "web_extract", "read_file", "write_file"] + + +def profile_name(override=None): + """The profile named in the reading: --profile, else $HERMES_PROFILE, else spark.""" + if override: + return override + return os.environ.get("HERMES_PROFILE") or "spark" + + +def profile_home(override=None, profile="spark"): + """The profile home this check grades.""" + if override: + return Path(override).expanduser() + env = os.environ.get("HERMES_HOME", "") + if env: + return Path(env).expanduser() + return Path.home() / ".hermes" / "profiles" / profile + + +def hermes_root(profile_home_dir, override=None): + """The Hermes root: --root, else the parent of `profiles`, else the parent.""" + if override: + return Path(override).expanduser() + profile_home_dir = Path(profile_home_dir) + if profile_home_dir.parent.name == "profiles": + return profile_home_dir.parent.parent + return profile_home_dir.parent + + +def required_dirs(profile_home_dir, root): + """Directories whose absence makes the reading fail.""" + profile_home_dir = Path(profile_home_dir) + return [ + Path(root), + profile_home_dir, + profile_home_dir / "cache" / "scratch", ] - missing = [d for d in dirs if not os.path.isdir(d)] - return missing -def check_tools(): - """Verify core tools are available.""" - tools = ["web_search", "web_extract", "read_file", "write_file"] + +def optional_dirs(root): + """Directories this lane does not depend on; a miss is noted, not failed.""" + root = Path(root) + return [root / "skills", root / "plugins"] + + +def check_environment(profile_home_dir, root): + """Return (missing_required, present_optional, missing_optional) as strings.""" + missing = [str(p) for p in required_dirs(profile_home_dir, root) if not p.is_dir()] + present_opt = [str(p) for p in optional_dirs(root) if p.is_dir()] + missing_opt = [str(p) for p in optional_dirs(root) if not p.is_dir()] + return missing, present_opt, missing_opt + + +def check_tools(names=None): + """Import hermes_tools and ask it for each named tool. + + Returns (available: dict[str, bool], error: str). `error` is the import + failure when the package cannot be imported at all, "" otherwise. A name + that resolves to a non-callable is unavailable, not available-but-odd. + """ + names = list(names or TOOLS) + try: + module = importlib.import_module("hermes_tools") + except Exception as exc: # ImportError, or a module that raises on import + return {name: False for name in names}, "%s: %s" % (type(exc).__name__, exc) available = {} - for tool in tools: - # These are Hermes tools available via hermes_tools - available[tool] = True - return available + for name in names: + try: + candidate = getattr(module, name, None) + except Exception: + candidate = None + available[name] = callable(candidate) + return available, "" + + +def build_parser(): + parser = argparse.ArgumentParser( + description="Verify Hermes directories and tools; 0 pass, 1 fail, 2 usage.", + ) + parser.add_argument("--home", default=None, + help="profile home to grade (default: $HERMES_HOME or ~/.hermes/profiles/)") + parser.add_argument("--root", default=None, + help="Hermes root to grade (default: derived from the profile home)") + parser.add_argument("--profile", default=None, + help="profile name (default: $HERMES_PROFILE or spark)") + parser.add_argument("--require", action="append", default=[], metavar="TOOL", + help="also require this tool (repeatable); the negative-control hook") + return parser + + +def main(argv=None): + args = build_parser().parse_args(argv) # exits 2 on a usage error + profile = profile_name(args.profile) + home = profile_home(args.home, profile) + root = hermes_root(home, args.root) + names = list(TOOLS) + [n for n in args.require if n not in TOOLS] + + missing_dirs, present_opt, missing_opt = check_environment(home, root) + available, error = check_tools(names) + unavailable = [n for n in names if not available.get(n)] + + print("HERMES_ROOT: %s" % root) + print("HERMES_HOME: %s" % home) + print("HERMES_PROFILE: %s" % profile) + for path in [str(p) for p in required_dirs(home, root)]: + print("DIR %s: %s" % ("MISSING" if path in missing_dirs else "OK", path)) + for path in present_opt: + print("DIR OPTIONAL-OK: %s" % path) + for path in missing_opt: + print("DIR OPTIONAL-MISSING: %s" % path) + if error: + print("TOOLS_IMPORT_FAILED: %s" % error) + print("TOOLS_AVAILABLE: %s" % sorted(n for n in names if available.get(n))) + print("TOOLS_MISSING: %s" % unavailable) + + ok = not missing_dirs and not unavailable + print("VERDICT: %s" % ("PASS" if ok else "FAIL")) + return 0 if ok else 1 + if __name__ == "__main__": - missing_dirs = check_environment() - if missing_dirs: - print(f"MISSING_DIRS: {missing_dirs}") - sys.exit(1) - - tools = check_tools() - print(f"TOOLS_AVAILABLE: {list(tools.keys())}") - print("Hermes environment verified successfully") - sys.exit(0) \ No newline at end of file + sys.exit(main()) diff --git a/wizard-work/spark/test_array_intersection.py b/wizard-work/spark/test_array_intersection.py index 1106351..7fe8ccd 100644 --- a/wizard-work/spark/test_array_intersection.py +++ b/wizard-work/spark/test_array_intersection.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 """Test file for array_intersection.py""" import sys -sys.path.insert(0, '/root/.hermes/wizard-outputs/spark') +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) from array_intersection import array_intersection # Test cases diff --git a/wizard-work/spark/test_counter_util.py b/wizard-work/spark/test_counter_util.py index fb5d311..6a988dd 100644 --- a/wizard-work/spark/test_counter_util.py +++ b/wizard-work/spark/test_counter_util.py @@ -2,7 +2,9 @@ """Verification tests for counter_util.py""" import sys -sys.path.insert(0, '/root/.hermes/wizard-outputs/spark') +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) from counter_util import count_words, count_items, most_common, counter_to_dict, extract_counter_description from collections import Counter diff --git a/wizard-work/spark/test_email_validator.py b/wizard-work/spark/test_email_validator.py index 236c91d..aa85fac 100644 --- a/wizard-work/spark/test_email_validator.py +++ b/wizard-work/spark/test_email_validator.py @@ -2,7 +2,9 @@ """Comprehensive test suite for email_validator.py""" import sys -sys.path.insert(0, '/root/.hermes/wizard-outputs/spark') +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) from email_validator import is_valid_email, validate_email # Test cases: (email, expected_valid) diff --git a/wizard-work/spark/test_hermes.py b/wizard-work/spark/test_hermes.py index 546ee1c..44e2b1a 100644 --- a/wizard-work/spark/test_hermes.py +++ b/wizard-work/spark/test_hermes.py @@ -1,27 +1,97 @@ #!/usr/bin/env python3 -"""Verify hermes-agent skill is available and functional.""" +"""Verify the hermes-agent skill is present and readable on this host. + +This is a HOST probe, not a repo test: its subject -- the live Hermes skill +catalog -- lives outside this tree, so it binds no module from it. The skills +root is taken from HERMES_SKILLS_DIR, else HERMES_HOME, else ~/.hermes/skills; +it is never a pinned /root path, so a copy of this file grades the host it is +run on. + +Exit codes, because "not run" must never read as "verified": + 0 the skill's SKILL.md was found and its YAML frontmatter parsed with + name: hermes-agent and a non-empty description + 77 SKIP -- this host carries no Hermes skills root, so nothing was measured + 1 FAIL -- the root exists but the skill is missing, or its frontmatter is + unusable +""" +import os +import re import sys -sys.path.insert(0, '/root/.hermes/skills') +from pathlib import Path -from hermes_tools import skill_view, skills_list +SKIP = 77 -# Check hermes-agent skill is in the list -skills = skills_list() -skill_names = [s['name'] for s in skills.get('skills', [])] -if 'hermes-agent' in skill_names: - print("FOUND: hermes-agent skill in catalog") -else: - print("ERROR: hermes-agent skill not found") - print("Available skills:", skill_names[:10]) - sys.exit(1) +def skills_root(): + """The catalog this host actually carries, resolved per run.""" + explicit = os.environ.get("HERMES_SKILLS_DIR") + if explicit: + return Path(explicit).expanduser() + home = os.environ.get("HERMES_HOME") + return (Path(home).expanduser() if home else Path.home() / ".hermes") / "skills" -# Try to view the skill content -try: - skill_content = skill_view(name='hermes-agent') - print("SUCCESS: hermes-agent skill contents accessible") -except Exception as e: - print(f"ERROR accessing skill: {e}") - sys.exit(1) -print("\nAll checks passed - hermes-agent skill is working correctly") \ No newline at end of file +def find_skill_md(root, name): + """Every SKILL.md under a directory called `name`, nearest the root first.""" + direct = root / name / "SKILL.md" + hits = [direct] if direct.is_file() else [] + hits.extend(sorted(root.glob("**/%s/SKILL.md" % name))) + return [h for h in hits if h.is_file()] + + +def frontmatter(text): + """The fenced `---` block's `key: value` pairs, or None when there is none.""" + m = re.match(r"---\r?\n(.*?)\r?\n---\r?\n", text, re.S) + if not m: + return None + out = {} + for line in m.group(1).splitlines(): + if ":" not in line or line.lstrip().startswith("#"): + continue + key, val = line.split(":", 1) + out[key.strip()] = val.strip().strip('"').strip("'") + return out + + +def main(): + root = skills_root() + print("skills root: %s" % root) + if not root.is_dir(): + print("SKIP: no Hermes skills root at %s -- nothing was measured" % root) + return SKIP + + hits = find_skill_md(root, "hermes-agent") + if not hits: + print("FAIL: the root exists but carries no hermes-agent/SKILL.md") + many = sorted(p.name for p in root.iterdir() if p.is_dir()) + print("directories under the root: %s" % (many[:10] or "none")) + return 1 + + for path in hits: + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as exc: + print("FAIL: cannot read %s: %s" % (path, exc)) + return 1 + fm = frontmatter(text) + if fm is None: + print("FAIL: %s has no fenced frontmatter block" % path) + return 1 + if fm.get("name") != "hermes-agent": + print("FAIL: %s declares name %r, want 'hermes-agent'" + % (path, fm.get("name"))) + return 1 + if not fm.get("description"): + print("FAIL: %s declares no description" % path) + return 1 + print("PASS: %s" % path) + print(" name=%s version=%s" % (fm.get("name"), fm.get("version", "?"))) + print(" description=%s" % fm["description"][:70]) + + print("\nAll checks passed - hermes-agent skill is present and readable (%d file(s))" + % len(hits)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wizard-work/spark/test_prime_factorizer.py b/wizard-work/spark/test_prime_factorizer.py index 22d90a1..eb5e5c5 100644 --- a/wizard-work/spark/test_prime_factorizer.py +++ b/wizard-work/spark/test_prime_factorizer.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 """Verification test for prime_factorizer.py""" import sys -sys.path.insert(0, '/root/.hermes/wizard-outputs/spark') +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) from prime_factorizer import prime_factors # Test cases with expected results diff --git a/wizard-work/spark/test_url_title_extractor.py b/wizard-work/spark/test_url_title_extractor.py index d1f7e63..fd04503 100644 --- a/wizard-work/spark/test_url_title_extractor.py +++ b/wizard-work/spark/test_url_title_extractor.py @@ -2,7 +2,9 @@ """Test the url_title_extractor module.""" import sys -sys.path.insert(0, "/root/.hermes/wizard-outputs/spark") +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) from url_title_extractor import extract_title, TitleParser import json @@ -22,7 +24,7 @@ def test_extract_title(): def test_cache_file_creation(): """Test that cache file location is writable.""" - cache_dir = Path("/root/.hermes/wizard-outputs/spark") + cache_dir = Path(__file__).resolve().parent cache_dir.mkdir(parents=True, exist_ok=True) return cache_dir.exists() and cache_dir.is_dir() diff --git a/wizard-work/spark/test_validator.py b/wizard-work/spark/test_validator.py index 4bd8fe3..1737768 100644 --- a/wizard-work/spark/test_validator.py +++ b/wizard-work/spark/test_validator.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 """Test runner for json_schema_cli.py""" import sys -sys.path.insert(0, '/root/.hermes/wizard-outputs/spark') +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) from json_schema_cli import validate_data, load_json print("=" * 50) @@ -9,11 +11,11 @@ print("JSON Schema Validator Tests") print("=" * 50) # Load test schema -schema = load_json('/root/.hermes/wizard-outputs/spark/test_schema.json') +schema = load_json(str(Path(__file__).resolve().parent / 'test_schema.json')) # Test 1: Valid data print("\nTest 1: Valid data") -valid_data = load_json('/root/.hermes/wizard-outputs/spark/valid_test_data.json') +valid_data = load_json(str(Path(__file__).resolve().parent / 'valid_test_data.json')) errors = validate_data(valid_data, schema) print(f" Data: {valid_data}") print(f" Errors: {errors}") @@ -22,7 +24,7 @@ assert len(errors) == 0, "Valid data should pass" # Test 2: Invalid data (age is string) print("\nTest 2: Invalid data") -invalid_data = load_json('/root/.hermes/wizard-outputs/spark/invalid_test_data.json') +invalid_data = load_json(str(Path(__file__).resolve().parent / 'invalid_test_data.json')) errors = validate_data(invalid_data, schema) print(f" Data: {invalid_data}") print(f" Errors: {errors}") diff --git a/wizard-work/spark/verify_hermes_setup.sh b/wizard-work/spark/verify_hermes_setup.sh index ad4e478..4ca79c2 100755 --- a/wizard-work/spark/verify_hermes_setup.sh +++ b/wizard-work/spark/verify_hermes_setup.sh @@ -1,48 +1,141 @@ #!/bin/bash -# Verification script for Hermes Agent profile setup -set -e +# Verify the directories a Hermes Agent profile ships into -- on the tree the +# script is run from, not on the host it was written on. +# +# Q109. This script used to test three absolute paths under the root user's +# home, so it printed its three ticks and "All critical paths verified" rc 0 on +# any tree, and it read nothing relative to itself. It now resolves +# - its own directory from $0 (so the sibling snippet.py it names is the one +# that shipped beside it, and a lone copy has no opinion to report), and +# - the profile home from --home, else $HERMES_HOME, else +# ~/.hermes/profiles/$HERMES_PROFILE (default profile: spark), with the +# Hermes root from --root, else the parent of the profile home's `profiles` +# directory, else the profile home's own parent, +# so the same invocation fails on a tree that is missing the paths it names. +# +# Exit codes follow the repo's tools/ contract: +# 0 = every required path exists +# 1 = a required path is missing (each is printed on its own MISSING: line) +# 2 = usage error, or the script's own directory could not be resolved +set -u + +HERE=$(cd -- "$(dirname -- "$(readlink -f -- "$0")")" && pwd) || { + echo "could not resolve the script's own directory" >&2 + exit 2 +} + +PROFILE=${HERMES_PROFILE:-spark} +HOME_ARG="" +ROOT_ARG="" + +usage() { + cat <<'USAGE' +usage: verify_hermes_setup.sh [--home DIR] [--root DIR] [--profile NAME] + +Grades one Hermes installation: + ROOT the Hermes root + HOME the profile home (ROOT/profiles/NAME, usually) + HOME/cache/scratch the profile's scratch directory +