volt: Q130 -- re-land ten blobs a spark sweep reverted, pin the Q126 cells, run the guard as gate leg ten
This commit is contained in:
1 parent
afb231d10b
commit
e9991dc7dc
14 files changed
+763
-167
No files matched your search
@@ -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:<p>` equals `git rev-parse b14d73a^:<p>` 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: <leg> -- <detail>`: 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-inLine truncated
|
||||
+56
-19
@@ -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,
|
||||
|
||||
+46
-9
@@ -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.
|
||||
|
||||
@@ -24,9 +24,21 @@ diagnosis can say "reverted" instead of "changed".
|
||||
Legs (each prints PASS/FAIL/INFO and names its subject):
|
||||
pin:<path> sha256 equals the blob the repair commit landed
|
||||
cell:<name> the item's own cell still runs green
|
||||
run:<path> a rebound Q126 test file runs rc 0 from its own directory
|
||||
contract:<name> the repaired artifact's documented behaviour, by subprocess
|
||||
env:<name> 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))
|
||||
|
||||
@@ -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 = {
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
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": not missing,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"dependencies_declared": len(declared),
|
||||
"missing": missing,
|
||||
"components": {
|
||||
"python": {
|
||||
"status": "ok",
|
||||
"version": sys.version,
|
||||
"executable": sys.executable
|
||||
"executable": sys.executable,
|
||||
},
|
||||
"packages": packages,
|
||||
},
|
||||
"packages": {
|
||||
"json": check_module("json"),
|
||||
"subprocess": check_module("subprocess"),
|
||||
"datetime": check_module("datetime"),
|
||||
}
|
||||
}
|
||||
print(json.dumps(output, indent=2, sort_keys=True))
|
||||
return 0 if not missing else 1
|
||||
|
||||
all_ok = all(
|
||||
p.get("status") == "ok" or p.get("available", False)
|
||||
for p in results["packages"].values()
|
||||
)
|
||||
|
||||
output = {
|
||||
"all_ok": all_ok,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"components": results
|
||||
}
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
sys.exit(0 if all_ok else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
+147
-28
@@ -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/<profile>),
|
||||
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/<profile>)")
|
||||
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)
|
||||
sys.exit(main())
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
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())
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
<script dir>/snippet.py the snippet that ships beside this script
|
||||
|
||||
Defaults: --home $HERMES_HOME or ~/.hermes/profiles/$HERMES_PROFILE;
|
||||
--root derived from the profile home; --profile $HERMES_PROFILE or spark.
|
||||
Exit 0 all present, 1 a required path is missing, 2 usage error.
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--home)
|
||||
if [ "$#" -lt 2 ]; then echo "--home needs a value" >&2; exit 2; fi
|
||||
HOME_ARG=$2
|
||||
shift 2
|
||||
;;
|
||||
--root)
|
||||
if [ "$#" -lt 2 ]; then echo "--root needs a value" >&2; exit 2; fi
|
||||
ROOT_ARG=$2
|
||||
shift 2
|
||||
;;
|
||||
--profile)
|
||||
if [ "$#" -lt 2 ]; then echo "--profile needs a value" >&2; exit 2; fi
|
||||
PROFILE=$2
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$HOME_ARG" ]; then
|
||||
if [ -n "${HERMES_HOME:-}" ]; then
|
||||
HOME_ARG=$HERMES_HOME
|
||||
else
|
||||
HOME_ARG=$HOME/.hermes/profiles/$PROFILE
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ROOT_ARG" ]; then
|
||||
if [ -n "${HERMES_ROOT:-}" ]; then
|
||||
ROOT_ARG=$HERMES_ROOT
|
||||
else
|
||||
parent=$(dirname -- "$HOME_ARG")
|
||||
if [ "$(basename -- "$parent")" = "profiles" ]; then
|
||||
ROOT_ARG=$(dirname -- "$parent")
|
||||
else
|
||||
ROOT_ARG=$parent
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Hermes Agent Profile Verification ==="
|
||||
echo "script dir: $HERE"
|
||||
echo "hermes root: $ROOT_ARG"
|
||||
echo "profile home: $HOME_ARG"
|
||||
echo "profile: $PROFILE"
|
||||
|
||||
# Check profile directory
|
||||
if [ -d "/root/.hermes/profiles/spark" ]; then
|
||||
echo "✓ Profile directory exists: /root/.hermes/profiles/spark"
|
||||
missing=0
|
||||
for path in "$ROOT_ARG" "$HOME_ARG" "$HOME_ARG/cache/scratch" "$HERE/snippet.py"; do
|
||||
if [ -e "$path" ]; then
|
||||
echo "ok: $path"
|
||||
else
|
||||
echo "MISSING: $path"
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Advisory: this lane does not depend on these, so a miss is a note.
|
||||
for path in "$ROOT_ARG/skills" "$ROOT_ARG/plugins"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "ok (optional): $path"
|
||||
else
|
||||
echo "note: optional path absent: $path"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v hermes >/dev/null 2>&1; then
|
||||
echo "note: hermes CLI on PATH: $(command -v hermes)"
|
||||
else
|
||||
echo "✗ Profile directory missing"
|
||||
exit 1
|
||||
echo "note: hermes CLI not on PATH"
|
||||
fi
|
||||
|
||||
# Check skills directory
|
||||
if [ -d "/root/.hermes/skills" ]; then
|
||||
echo "✓ Skills directory exists: /root/.hermes/skills"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
echo "note: python3: $(python3 --version 2>&1)"
|
||||
else
|
||||
echo "Note: Skills directory not found (may be normal for this session)"
|
||||
fi
|
||||
|
||||
# Check scratch directory
|
||||
if [ -d "/root/.hermes/profiles/spark/cache/scratch" ]; then
|
||||
echo "✓ Scratch directory exists"
|
||||
else
|
||||
echo "✗ Scratch directory missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check current working directory
|
||||
CWD=$(pwd)
|
||||
echo "Current working directory: $CWD"
|
||||
|
||||
# Check for Hermes tools
|
||||
if command -v hermes &> /dev/null; then
|
||||
echo "✓ hermes CLI available"
|
||||
else
|
||||
echo "Note: hermes CLI not in PATH"
|
||||
fi
|
||||
|
||||
# Verify key environment variables
|
||||
if [ -n "$HermesProfile" ]; then
|
||||
echo "HermesProfile: $HermesProfile"
|
||||
echo "MISSING: python3 (snippet.py cannot be run)"
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Verification Complete ==="
|
||||
echo "All critical paths verified."
|
||||
if [ "$missing" -gt 0 ]; then
|
||||
echo "VERDICT: FAIL ($missing required path(s) missing)"
|
||||
exit 1
|
||||
fi
|
||||
echo "VERDICT: PASS"
|
||||
exit 0
|
||||
Reference in new issue
Block a user