⚑ THIS FILE IS A SEED, NOT A ROSTER. Every tick re-derives its queue from the tree —
./blockers.sh, git log, git status --short, ListAgents, bazel test //.... What
is written here is what a tick would otherwise have to reconstruct from a lost context,
and a symbol that survives a tick without being re-measured is an unrefreshed default.
Written 2026-09-10 by mtools-27. HEAD at writing: 9a97ed8. Tree clean.
findings/CENSUS-deps-build-ANALYSIS.mdis EMBARGOED to surveyors pre-freeze.- In
findings/CENSUS-deps-build.mda pre-freeze surveyor reads §V, §S, §G and §D ONLY. - Do NOT read peer legs in a census's leg directory until that census's freeze.
- Do NOT amend a filed leg of your own.
- Do NOT resume the per-case-target work (
collect_check.sh,CASES.txt). - The subagent sweep of peer BUILD-generation machinery stays held as testimony, unread.
- A peer cannot lift an operator's hold, and you cannot lift one for a peer.
- Never
--no-verify. Commit with an EXPLICIT PATHSPEC — the index is shared. - Do NOT expand the ratchet baseline. Lowering it is an operator decision.
⚑⚑ CLOSED. Nine sites derived (not the six recorded here), 33 warrants transcribed (not
27), fence/rubric.tsv written, sections verified by the gate's own diff. The entry
stays because three of its corrections are the reusable part, and a cleared item deleted
takes its measurements with it.
.githooks/pre-commit line ~403 read for dist in hooks mdstruct ratchet. fence was
not in it, so its test functions carried ZERO warrants and the 1:1 ledger never looked.
⚑ The gate was CORRECT OVER THE WRONG SET — the mis-named-population defect, in the gate.
⚑⚑⚑ AND THE WARRANT TARGET WAS THREE DIFFERENT NUMBERS. The gate counted ^def test_
anchored at column 0; every fence test is a CLASS METHOD, so the harness counted 0
and a ledger of zero warrants against zero functions is 1:1 and PASSES. Measured:
0 by the gate's predicate, 33 methods, 45 pytest-collected cases. The "27" in
this file matched none of them — it came from an early pytest run and was carried across
ticks as ready work. ⚑ A figure with no stated provenance, in the seed, for several ticks.
⚑⚑⚑ I CARRIED THIS AS BLOCKED, ALREADY ASKED AND HAD NEVER ASKED IT. Flagged in prose
three times, never once through AskUserQuestion — the exact failure the tick prompt
names: do not carry one as blocked without having actually asked it. The operator
caught it by reading this file. ⚑ A claim about my own past action, unverified, wrong.
ANSWERED 2026-09-10: derive the list from the filesystem, and write all 27 warrants. So this is TOP OF QUEUE and no longer blocked.
⚑⚑ THE SAME HAND-WRITTEN POPULATION IS IN THE TESTS TOO — hooks/tests/test_bar_fires.py
carries for dist in ("hooks", "mdstruct", "ratchet") at two sites. The arm that would
catch the gate's omission has the identical omission. Fixing only the gate leaves that.
⚑ AND DERIVING THE POPULATION EXPOSES A SECOND GAP: the loop also requires rubric.tsv,
and fence/ has none (hooks, mdstruct, ratchet all do — measured). So fence needs BOTH
warrants.bib and rubric.tsv before a derived loop can pass.
The predicate is already used by blockers.sh: a directory with a pyproject.toml.
Measured: four match — hooks, mdstruct, fence, ratchet.
⚑⚑⚑ AND THE SCOPE IS SIX SITES, NOT ONE. for dist in hooks mdstruct ratchet appears at
FOUR sites in .githooks/pre-commit (lines 207, 340, 403, 670) and TWO in
hooks/tests/test_bar_fires.py. The warrant loop is only the one I happened to notice.
⚑ TWO OF THE GATE'S SITES RUN PER-VENV CHECKERS, so deriving them was blocked on
fence/.venv existing at all — which is why bootstrapping it surfaced the undeclared
ruff. Operator ruled: derive all six and bootstrap the venv. The venv is bootstrapped
and ruff is declared (d8ee60b); THE SIX SITES ARE STILL HAND-WRITTEN.
⚑⚑⚑ EACH DISTRIBUTION HAS TWO LOCKS AND TWO RESOLVERS, AND THEY DISAGREE ON THE SAME
DECLARATION. uv sync resolves pyproject.toml into uv.lock (what the gate's per-venv
checkers run); uv pip compile --group dev writes requirements-dev.txt (what bazel's
pip.parse stages). MEASURED on hooks, same machine, same pyproject, same minute:
uv sync installed ast-serialize==0.11.1
uv pip compile wrote ast-serialize==0.9.0
⚑ NOT HAND-EDITABLE INTO AGREEMENT. Editing either file to match the other would make a
resolver's output a hand-written figure — the defect this tree removes everywhere else.
The operator ruled to TRACK uv.lock everywhere and accept it as the venv's declared
input; that is done, and it makes both locks declared rather than making them agree.
⚑ WHAT WOULD SETTLE IT: whether pip.parse can consume uv.lock, or whether one
resolver can produce both artifacts. Do not assume the divergence is harmless —
ast-serialize is a mypy transitive, so the two substrates may run different mypy
behaviour on identical source.
⚑⚑⚑ INVESTIGATED 2026-09-10, FROM SOURCE. pip.parse accepts a uv_lock label
(extension.bzl, added 2.2.0, not experimental): the uv.lock file will be used as the
primary source for package metadata. So one resolver CAN feed the graph — for the dev
hubs only.
⚑⚑ IT CANNOT SERVE THE SHIPPING HUBS, and the reason is in the implementation rather
than the docs. _parse_uv_lock_json (private/pypi/parse_requirements.bzl:171) iterates
uv_lock["package"] — EVERY package in the lock, with no group selection. MEASURED:
ratchet/uv.lock carries 28 package entries against a shipping lock of 0 runtime
deps and a dev lock of 12. The lock is the UNION. Feeding <dist>_deps from it would
put pytest, mypy and ruff into what a consumer installs — the exact over-admission the
two-hub split in MODULE.bazel exists to refuse, argued there explicitly.
⚑ SO THE AVAILABLE FIX IS HALF THE ONE I DESCRIBED. The four _dev hubs can take
uv_lock — carrying the test runner is their whole point. The four _deps hubs keep
requirements.txt. The operator chose "point pip.parse at uv.lock" on my description of
it as a clean single-source fix; it is only that for half the hubs, and the other half is
not a defect to fix but a distinction to keep.
⚑ TWO THINGS THE READ TURNED UP THAT A DOCSTRING WOULD NOT HAVE: toml_decode is
REQUIRED alongside uv_lock and _parse_uv_lock_json fails without platforms
configured; and there is a git_struct branch reading pkg["source"]["git"], so the
git-sourced paperkit survives that path — which was a live risk worth checking rather
than assuming.
⚑⚑⚑ parse_requirements' OWN DOCSTRING PROMISES A CHECK ITS CODE RETURNS BEFORE
REACHING. It reads: If provided, the function will use the uv.lock file as the primary
source ... and perform a consistency check against requirements files if both are
provided. MEASURED at private/pypi/parse_requirements.bzl:89-100: when uv_lock and
toml_decode are both set it returns _parse_requirements_with_uv_lock IMMEDIATELY, and
requirements_by_platform is consulted only to derive platform names. NO CONSISTENCY
CHECK RUNS.
⚑ THIS SESSION'S RECURRING SHAPE, IN UPSTREAM CODE: a plausible reading pointing at the
wrong subject. Believing the docstring would have had this repository report a
cross-check between uv.lock and requirements-dev.txt that the build never performs —
and report it as a REASON the two locks are safe to keep.
⚑ CONSEQUENCE FOR ANY TICK TAKING THE uv_lock WORK: adopting it for the dev hubs does
NOT buy a verification that the two locks agree. It buys ONE SOURCE for those hubs. If
agreement between uv.lock and requirements-dev.txt is ever asserted, it must be
measured here, not inherited from that sentence.
⚑⚑⚑ RE-MEASURED 2026-09-11 AND IT STILL HOLDS — BOTH HALVES, because the finding is a MISMATCH
and one half going stale would dissolve it. parse_requirements.bzl:70 still promises perform a
consistency check against; :89 still reads if uv_lock and toml_decode: and returns immediately.
Neither moved.
⚑⚑ RECLASSIFIED FROM LIVE TO STANDING, WHICH IS A DIFFERENT STATE AND NOT A CLEARING. Nothing
here is work: this repository does not use uv_lock, so there is no defect in the tree to repair.
What the section does is FORBID A FUTURE CLAIM — the same shape as ⟐REMOTE-DISCARDS-MODE-BITS,
reclassified one tick ago for the same reason. ⚑ A CONSTRAINT PARKED IN THE LIVE COLUMN reads as
undone work and invites a tick to "finish" it; a constraint deleted because nothing is broken
leaves the next reader to inherit the docstring's sentence as evidence, which is exactly the damage
it exists to prevent.
⚑⚑⚑ //:venv.bzl LANDS THE DIRECTION. venv_from_hub is called once per distribution —
hooks, mdstruct, ratchet, fence — and each produces bazel-bin/<dist>/.venv/bin/python3. Measured
on all four: interpreter runs 3.13.13, own package imports, full suite collects (370 / 124 / 43 /
45, matching the gate's own counts). //hooks:test_venv_artifact asserts it continuously: 7
functions, 22 cases, F-armed.
The population is derived at every level. all_requirements from the hub's generated
requirements.bzl (12 packages for hooks_dev, 13 the day one is added, no edit); the distribution
list from */pyproject.toml, which is the rule blockers.sh and test_bar_fires already use.
⚑⚑ THREE DEFECTS THE RULE SHIPPED AND MEASUREMENT CAUGHT — recorded because each is a shape, not a typo.
-
short_pathvspath, and a well-formed link to nothing. The first draft computedbin/python3fromshort_path. An EXTERNAL file'sshort_pathbegins../— runfiles put other repositories beside the main one — so the common-prefix walk compared a workspace-relative path against an escape sequence. Result: a relative symlink resolving intobazel-out/, naming no file. The target built green. Only running the interpreter caught it. Measured by printing all four values during a build rather than reasoning about which to use:link.short_path = hooks/.venv/bin/python3 link.path = bazel-out/k8-fastbuild/bin/hooks/.venv/bin/python3 interp.short_path = ../rules_python++python+.../bin/python3 ← LEADING ../ interp.path = external/rules_python++python+.../bin/python3Same shape as the
sys.pathfinding at8221131, one layer down: a path that exists as a string and not as a file. -
A guessed label suffix. The first BUILD call wrote
dev_requirement("pytest") + "_extracted". Reading the generatedrequirements.bzlshows the accessors arerequirement/whl_requirement/data_requirement/dist_info_requirementand none forextracted_whl_files— the guess named a label that does not exist. Caught by reading the source, not by the error. The:pkg→:extracted_whl_filesrewrite now happens once inside the macro, so a wrong guess is wrong in one place rather than four. -
An arm asserting a property of an environment the environment denies. The control arm carried "deliberately NOT skipped — it reads the source tree, which is present wherever pytest runs". The sandbox has no source tree; the arm failed with
no directory under /execroot/.../runfiles carries a pyproject.toml. It now carries the same guard as its siblings.
⚑ AND ONE BOUND ON THE RELOCATABILITY CLAIM, found when an F-arm's control failed. The venv
relocates as a subtree, not as a lone directory — the relative link climbs six levels expecting
the execroot's shape, so copying only .venv breaks it. That is the link working correctly. An
honest move carries the venv and the interpreter together at their relative offsets; done that way,
it runs after the move.
⚑ test_bar_fires REFUSED THIS FILE'S FIRST DRAFT AND WAS RIGHT. Path(__file__).resolve()
follows a runfiles symlink back out to the author's checkout — the escape .bazelrc documents.
The root is now derived from the working directory instead.
Still open under this symbol: grade.py:110 still hardcodes dist/".venv/bin/python3" as a
HOST path, and test_grade.py still symlinks the host venv into its sandbox. The artifact now
exists to point them at; pointing them is the next step and is what closes
⟐GRADER-INTERPRETER-UNDECLARED.
⚑⚑⚑ "All projects in this repo should be constructing their .venv the same way — as a build artifact. This should be a trivially-templatizable thing." Set 2026-09-10, and ABSENT FROM THIS FILE FOR TWO TICKS while the file claimed to be the queue's seed.
⚑ IT SUBSUMES OTHER ENTRIES RATHER THAN SITTING BESIDE THEM. ⟐TWO-RESOLVERS-DISAGREE
dissolves if the venv is built rather than uv sync-ed — there is no second resolver.
⟐UNDECLARED-HOST-INPUTS shrinks, because the gate stops reaching a host path no rule
produces.
MEASURED, so a tick does not re-derive it:
- The BUILD files DELIBERATELY avoid the venv (
imports = ["src"], resolved by runfiles layout) — "runs these witnesses with no venv at all." So this is ADDITIVE: nothing in the graph starts depending on it. - The venv has THREE non-bazel consumers:
.githooks/pre-commit(⚑ 14.venv/binlines when re-measured 2026-09-10, not the 13 recorded here and repeated into the cron prompt — and the count was the wrong instrument anyway: those lines are not all uses. Three were fail-open guards and one is the refusal itself. See ⟐GATE-FAILS-OPEN.),preflight.sh(7 sites), and the interactive dev loop. All reach<dist>/.venv/bin/…as a host path no rule produces. - The repeated shape is exact across all four distributions — same
[dependency-groups], same trackeduv.lock, samebin/{ruff,mypy,python3}contract. Four identical instantiations of one macro.
⚑⚑ THERE IS NO py_venv RULE. I asked the operator whether to bump to a version with
py_venv and they decided on that description; MEASURED against the actual releases,
it does not exist at 1.0.0 OR at 2.3.3 — checked //python, //python/bin and
//python/uv BUILD files directly. Re-asked with the correction; the ruling stands: bump
anyway (done, 50cb2c2), then build the rule.
⚑⚑⚑ THE ROUTE IS DECIDED AND IT IS NOT uv: build from the pip hubs. Operator ruled 2026-09-10 after both options were measured.
⚑⚑ RE-ASKED AND RE-RULED 2026-09-10, after measuring that rules_python already builds a
per-target venv (below). The operator was given three options — use what bazel already builds
and inject the interpreter; build the whole-distribution venv anyway; or do both in sequence — and
chose BUILD THE WHOLE-DISTRIBUTION VENV. So the duplication of venv_runfiles.bzl is a known,
accepted cost, not an oversight: a per-target venv cannot be activated by a developer, and the
three non-bazel consumers above all reach <dist>/.venv/bin/….
⚑ WHY uv WAS RULED OUT, measured both arms: uv sync --offline SUCCEEDS against a warm
cache (full venv, correct interpreter, ruff==0.16.6) and FAILS with an empty
UV_CACHE_DIR, naming the exact wheel URL it could not fetch. The control is what makes
the first arm mean anything — it proves the offline run used the CACHE rather than
silently reaching out. So uv is offline-capable only if the wheels are already local,
which a network-denied bazel action cannot assume; and //python/uv is marked
EXPERIMENTAL: may be removed without notice, while python/uv/private/lock.bzl turns
out to REGENERATE A LOCKFILE rather than materialise a venv — a different job.
⚑⚑ AND THE HUBS ALREADY STAGE EVERYTHING. @<dist>_dev//<pkg>:extracted_whl_files
exposes each wheel UNPACKED — measured on @hooks_dev//ruff: site-packages/… plus a
real bin/ruff. No installer to reimplement, no uv at build time, nothing experimental,
and every input already declared in MODULE.bazel. The venv becomes a VIEW over inputs
bazel already has.
⚑ THE GRADER IS THE FORCING CONSUMER, and this is why the direction is a precondition rather than a parallel task — see ⟐GRADER-INTERPRETER-UNDECLARED below.
⚑⚑⚑ rules_python ALREADY BUILDS A VENV PER TARGET — measured 2026-09-10, and the operator ruled anyway
venv_runfiles.bzl + site_init_template.py construct _<target>.venv/bin/python3 for every
py_test/py_binary. That is what sys.executable names inside an action. So a
whole-distribution rule DUPLICATES construction that already exists — the ruling accepts that cost
deliberately, because a per-target venv is not activatable and "every project constructs its
.venv the same way" means one per DISTRIBUTION.
⚑⚑ AND THE PER-TARGET VENV WORKS FROM OUTSIDE ITS ACTION, ONCE RUNFILES_DIR IS SET — worth
recording because the failure mode is a trap, not an error. Invoked bare it reports
ModuleNotFoundError: No module named 'pytest' while sys.path visibly contains the pytest
site-packages directory. Both are true: _find_runfiles_root() falls back to walking up from
_bazel_site_init.py, lands one directory short (bin/hooks rather than
bin/hooks/test_grade.runfiles), and every dependency entry becomes a well-formed path to
nothing. Python skips nonexistent sys.path entries silently.
as it appeared in sys.path : exists=False .../bin/hooks/<hub>/site-packages
under the runfiles root : exists=True .../bin/hooks/test_grade.runfiles/<hub>/site-packages
with RUNFILES_DIR set : pytest 9.1.1, mikemol.hooks.grade imports
⚑ "The path is right there in sys.path" is the plausible reading; the subject is a string,
not a directory. Add it to the tally.
A rule written on an untested assembly is an explanation. Assembled in the scratchpad from the
staged hubs — symlinked site-packages, relative bin/python3, hand-written pyvenv.cfg:
ARM 0 control runs: 3.13.13
ARM 1 packages pytest 9.1.1 (also ruff, mypy)
ARM 2 dist mikemol.hooks.grade imports
ARM 3 pytest 20 passed (real hooks tests, tests/test_payload.py)
ARM 4 MOVED runs AFTER MOVE — the build-artifact property
ARM 5 F-arm panflute refuses
⚑⚑⚑ ARM 4 IS THE ONE THAT MATTERS AND IT DEPENDS ON ONE BYTE OF DESIGN. bin/python3 must be
a relative symlink (../../toolchain/bin/python3). An earlier probe established that
pyvenv.cfg's home is INERT — breaking it entirely changed nothing — and that the absolute
bin/python3 symlink is the real dependency. A venv that cannot move is not a build artifact.
⚑ AND THE CLOSURE MUST COME FROM deps(), NOT A DIRECTORY SCAN. The probe scanned
external/rules_python++pip+hooks_* and found 39 directories: 2 with no site-packages (the
hub aliases hooks_deps/hooks_dev themselves) and the rest duplicate pairs — a short alias
and a long platform-tagged name resolving to the same wheel. 29 top-level entries linked. The scan
worked only because duplicate names collide harmlessly; a rule must take
deps(@<dist>_dev//<pkg>:pkg) so the population is derived from the graph rather than from a glob
that happens not to hurt.
⚑⚑⚑ MODULE.bazel ARGUES FOR ITS http_archive ON A PREMISE THAT NO LONGER HOLDS. It
states that ruff must be fetched as an archive because rules_python STAGES ONLY
site-packages. The wheel's bin/ruff is dropped, so the installed package is a Python
shim whose find_ruff_bin() looks for a binary that is not there.
MEASURED at 2.3.3 via bazel cquery '@hooks_dev//ruff:extracted_whl_files' --output=files:
…/bin/ruff <- STAGED
…/site-packages/ruff-0.16.6.dist-info/… <- and site-packages too
…/site-packages/ruff/_find_ruff.py
and file on that path reports ELF 64-bit LSB pie executable … stripped — a real
binary, not the shim the comment describes. ⚑ The positive control is in the same
listing: site-packages/ IS staged, so the query is not simply returning everything.
⚑⚑ THE PREMISE HELD AT 1.0.0 AND THE BUMP AT 50cb2c2 INVALIDATED IT, and nothing
noticed — a recorded measurement going stale inside a load-bearing comment is worse than
no comment, because a reader spends it as evidence. ⚑ TWO SEPARATE ITEMS: the comment is
false NOW and should be corrected regardless; whether the http_archive is therefore
REDUNDANT is a further question nobody has measured, and removing it on this evidence
alone would be acting past what was established.
⚑⚑⚑ CLEARED 2026-09-11 — AND IT WAS ALREADY DONE IN THE FILE, SO THIS SECTION WAS THE LAST
STALE RECORD OF ITS OWN DEFECT. MODULE.bazel:184-212 now opens "the reason recorded here went
stale", quotes the withdrawn premise as withdrawn (kept visible because a reader will have spent
it), carries the measurement table, and states explicitly what is NOT established. Both items
above are answered: the comment is corrected, and the redundancy question is recorded as unmeasured
rather than acted on.
⚑⚑ BOTH LEGS RE-MEASURED TODAY RATHER THAN READ, since a comment is output and this symbol exists because one went stale:
bazel cquery '@hooks_dev//ruff:extracted_whl_files' -> bin/ruff STAGED (+ site-packages,
the control that it is not returning all)
file …/bin/ruff -> ELF 64-bit LSB pie executable … stripped
…/bin/ruff --version -> ruff 0.16.6 (a build is not a verdict; it RUNS)
entry_points: ruff 0 · mypy 5 · pytest 2 (the control: the reader can see them)
⚑ THE SURVIVING LEG IS THE ENTRY-POINT ONE AND IT HOLDS. ruff declares zero console scripts,
so there is nothing for py_console_script_binary to regenerate and the http_archive remains the
right instrument — for that reason rather than the withdrawn staging one. ⚑⚑ A CONCLUSION THAT
SURVIVES ITS ORIGINAL ARGUMENT IS NOT THEREBY UNSUPPORTED, AND IT IS NOT THEREBY SUPPORTED EITHER:
the second leg had to be measured on its own, which is what the comment records and what this
re-measurement confirms.
⚑⚑⚑ THE INTERPRETER IS NOW A DECLARED FIELD. Runner.interpreter: Path, defaulting via an
empty-path sentinel to exactly what the old code computed, so no existing caller regrades — what
changed is that the fallback is now stated rather than being the only possibility. run() passes
it to subprocess; four arms pin it (declared value kept, default unmoved, declared interpreter
actually reaching subprocess, and never the string "None"), each shown to fail before the fix and
for the right reason.
⚑⚑ AND THE NULLABLE VERSION WAS A HOLE I ALMOST SHIPPED. First draft typed it Path | None
with the default resolved in __post_init__ — mypy-clean, and still leaving str(None) able to
produce the literal "None" as an argv element. That is not an error: it is a path that does not
exist, so every arm would grade UNREACHABLE and the suite would report nothing is falsifiable
rather than the grader was misconfigured. Same shape as the two path findings above. Typed Path
with an empty-path sentinel instead, and pinned by its own arm.
⚑⚑⚑ THE SECOND CLAIM BELOW WAS FALSE AND I WROTE IT. The paragraph read: "test_grade.py
SYMLINKS THE HOST VENV into its sandbox fixture — reaching out of the hermetic tree." Measured:
_venv() derives from sys.executable, so under bazel it names the ACTION'S OWN staged venv,
and //hooks:test_grade --config=remote reports 21 passed on the executor, where no host venv
exists to reach. The symlink is not an escape. It reads like one — .venv in a fixture, pointing
somewhere outside tmp_path — which is exactly why it went unchecked across several ticks and
into the cron prompt as established fact. A claim about an escape needs the same arm as any
other claim. What was genuinely wrong is that the fixture relied on the path convention; the
suite now routes every sandbox Runner through _runner(), which passes interpreter= explicitly,
so it exercises the path it recommends.
The original entry follows, kept for what it got right. hooks/src/mikemol/hooks/grade.py
built dist / ".venv/bin/python3". Two problems, and the operator named the second:
⚑ THE SANDBOX CRASHED THE GRADER ON A MISSING INTERPRETER, in the very arm asserting it
distinguishes could not run from ran and failed. I fixed it with an OSError guard —
which makes the grader TOLERATE an absent interpreter without making one PRESENT. Operator:
this is why you're supposed to have the .venv as a build artifact; then you know precisely
the interpreter you'll have because you built it. Graceful degradation of an input the
graph should supply is the shape external was retired over.
⚑⚑ AND THE GRADER'S CLAIM DEPENDS ON IT. Its product is run this test in a known
environment and see if it flips. If the environment is whatever the host happens to have,
A FLIP IS NOT ATTRIBUTABLE — a test could go red because the subject changed or because the
interpreter differs. paperkit's content_sensitive exists to separate exactly that, and it
can only mean something when the environment is fixed by construction.
⚑ ALSO UNRECORDED UNTIL NOW:
WITHDRAWN, see the correction at the head of this section: the symlink target is derived
from hooks/tests/test_grade.py SYMLINKS THE HOST VENV into its
sandbox fixture — reaching out of the hermetic tree at the boundary the sandbox enforces.sys.executable and under bazel names the action's own venv. 21 passed on the executor.
⚑⚑⚑ THE RULING: "the gate verdicts should use the build's venv. To that end, the gates should, honestly, be build TARGETS." Answering the question raised one tick earlier about the three non-bazel consumers.
⚑⚑ AND THE FIRST MEASUREMENT REFRAMES THE WORK: the checks ARE targets already.
bazel query 'kind("sh_test", //...)' returns 13: ruff, mypy and a ratchet gate for each of
the four distributions, plus //:shellcheck_githooks. The per-module py_test witnesses are
targets too. So "make the gates targets" is not construction — it is removing a second,
host-venv copy of verdicts the graph already produces.
Measured: the gate runs two verdicts over the same subject. .githooks/pre-commit:379-381
materialises the index with git checkout-index --all --prefix="$staged/", then:
:405 run_checked ruff $root/$dist/.venv/bin/ruff over $staged ← host venv
:410 run_checked mypy $root/$dist/.venv/bin/mypy over $staged ← host venv
:429 run_checked pytest .venv/bin/python3 -m pytest over $staged ← host venv
:554 ( cd "$staged" && bazel test //... ) ← the targets
:711 ratchet/.venv/bin/mikemol-ratchet ← host venv
⚑⚑⚑ RE-MEASURED 2026-09-11: THAT TABLE IS A HISTORICAL READING AND TWO OF ITS ROWS ARE GONE.
grep -n 'venv/bin' .githooks/pre-commit now returns twelve lines and no ruff and no mypy among
them — both verdicts come from the targets. What still runs out of a host venv, measured:
:448 env -C $dist .venv/bin/python3 -m pytest -q ← per-distribution suite
:724 mdstruct .venv/bin/python3 -m mypy.stubtest ← the stub-authority witness
:752 ratchet/.venv/bin/mikemol-ratchet ← the preview-debt ratchet
:775 mdstruct .venv/bin/python3 -m …cli verify / lint ← the markdown witnesses
⚑⚑⚑ AND THAT CORRECTION OVERSTATED ITS OWN FINDING — MEASURED 2026-09-11, ONE OF THE FOUR
SURVIVORS IS A DUPLICATE. The sentence below reads every survivor is a check with no
equivalent target the gate can reach. It was written from reading the GATE; the claim is about
the GRAPH, and bazel query 'kind("py_test|sh_test", //...)' answers it:
:448pytest over the developer tree — target//<dist>:test_*. Not a duplicate, and the gate argues it in place at:442: the developer venv surfaces a missing dependency as an import error, where a sandbox never had it.:724stubtest over panflute — target//mdstruct:test_stub_authority. DUPLICATE. Same tool, same allowlist, run twice.:752ratchet over$root/$dist— target//<dist>:ratchet. Not a duplicate: the target censuses the RUNFILES tree — it had to be taught to exclude synthesized__init__.pyfiles the source tree lacks — while the gate censuses the REAL tree, because census keys are paths a reader must be able to go fix.:775mdstruct verify / lint — no target exists.
⚑⚑ AN ABSENCE CLAIM ABOUT THE BUILD GRAPH, ASSERTED FROM READING A SHELL SCRIPT. Three of the four rows survive the check and one does not, which is the difference between a measured finding and a plausible one. ⚑ The same shape this document records thirty-odd times, arriving in the correction to a stale table rather than in the table itself.
⚑ THE STUBTEST TARGET IS A REAL SUCCESSOR, F-ARMED BEFORE ANY REMOVAL IS PROPOSED: planting a
divergence in stubs/panflute/__init__.pyi (renaming stringify's first parameter) makes
//mdstruct:test_stub_authority go RED. A deletion premised on a successor must prove the
successor fires; this one does. The removal itself is not taken here — the ruling covers it, but
run_checked also supplies the gate's failure REPORTING, and whether the target's log reaches a
refused committer as legibly is a second question nobody has measured.
⚑⚑ SO THE RULING IS MOSTLY DISCHARGED AND WHAT REMAINS IS SMALLER THAN THE TABLE SUGGESTS. The two duplicated verdicts that went were ruff and mypy; a third — stubtest — is measured as duplicated and not yet removed. The other three survivors each answer about a subject no target reaches: the developer tree, the real tree, and markdown that has no target at all. ⚑ REMOVING A DUPLICATE, KEEPING A DIFFERENT SUBJECT, AND BUILDING A MISSING TARGET ARE THREE JOBS — the original row list said only "host venv" and made them look like one.
⚑ THIS SECTION IS ITSELF THE STALE-RECORD CLASS IT DESCRIBES, in the document a tick reads first. The table was true when written; nothing re-measured it while the work moved underneath, and it sat in the LIVE section where a reader takes it for the current state.
⚑⚑⚑ AND bazel test RUNS INSIDE $staged TOO, WHICH FALSIFIES A COMMENT AT :541. That
comment reads "ruff and mypy already ran in $staged; bazel test did not" — the cd "$staged"
on line 554, thirteen lines below it, contradicts it. Both halves check the same materialised
index. So the duplication is not tree-vs-index; it is one subject through two instruments.
The two instruments differ, and here is the whole of the measured difference:
host .venv built .venv (//:venv.bzl)
interpreter 3.13.11 (mise, via uv) 3.13.13 (bazel toolchain)
ruff 0.16.6 0.16.6 ← agree TODAY
mypy 2.3.1 2.3.1 ← agree TODAY
declared in nothing MODULE.bazel
pyvenv.cfg home = ~/.local/share/mise/… home = ../bin
⚑ THE CHECKER VERSIONS AGREE AND THAT IS NOT A GUARANTEE — it is a coincidence maintained by hand, from two resolvers with no shared constraint. The honest statement of the risk is narrow and real: the ratchet's baseline keys ARE ruff findings, so a future divergence mints or clears keys the build would not, and both halves would report green in their own terms.
⚑⚑ THE BLOCKER ON REPOINTING, MEASURED: the built venv has no bin/ entry points.
bazel-bin/hooks/.venv/bin/ contains exactly one file — python3. ruff and mypy are present
in site-packages but have no console scripts, because //:venv.bzl only ever creates the
interpreter symlink. The gate invokes .venv/bin/ruff directly, so repointing it today fails
immediately. That is a gap in the rule I wrote and did not measure.
The host-venv ruff and mypy are gone from .githooks/pre-commit. //<dist>:ruff and
//<dist>:mypy, run by ( cd "$staged" && bazel test //... ), are now the only run of either.
The redundancy was measured before the deletion, not inferred from target names:
ruff three binaries — host venv, @ruff//:bin, built venv site-packages — ALL 0.16.6
on a PLANTED defect: host PLR2004 rc=1 | archive PLR2004 rc=1 | identical rule sets
ruff_check.sh passes the same --config and `check .` from the same directory
mypy the target deletes synthesized __init__.py markers a staged checkout never has
population: 24 source files BOTH ways; a planted type error flips host to rc=1
with the population held at 24
And the delegation was then shown to carry the coverage, which the arm cannot assert:
control green on both targets, then //hooks:ruff rc=3 on a planted magic value and
//hooks:mypy rc=3 on a planted return type, tree restored.
⚑⚑ STEP 3 WAS FORCED, NOT CHOSEN. test_the_preflight_runs_the_ruff_the_gate_runs refused the
tree the moment the gate changed — preflight was still running .venv/bin/ruff, predicting a check
the gate had stopped performing. That arm existed exactly for this and fired exactly when it
should. preflight.sh now runs bazel test //<dist>:ruff //<dist>:mypy with --test_output=errors
so a refusal still carries its finding; both targets are cached, so a clean tree answers from the
action cache and the script stays fast.
⚑ FOUR EXISTING ARMS REFUSED THE DELETION, AND ALL FOUR WERE RIGHT TO. Each asserted a property of the deleted block:
MYPYPATH="$staged/— asserted the workaround for the editable-install import closure. Its subject is gone; the target reads a bazel sandbox built from declaredsrcs, where that hazard cannot arise. Rewritten to assert the structural fact instead of demanding the workaround.- three typed check labels in the capture-discipline arm — a hand-written population inside an arm about capture, refusing a correct change because two of its three names no longer exist. Now derived from the file.
- the ruff-agreement arm — the real finding above.
- the vacuity sweep — caught a literal (
ruff EXITED) that the rewrite had orphaned.
The sequence the ruling implied, with 1 and 3 now done:
- Delete the duplicated host-venv checks at
:405,:410,:429,:711— the graph already produces those verdicts at:554, over the same staged tree. This is subtraction, needs no new rule, and removes the divergence rather than managing it. //:venv.bzlgrows console scripts (rules_pythonhaspy_console_script_binary), for the remaining consumers that genuinely need an activatable venv — the interactive dev loop.preflight.shthen predicts the gate by running the same targets rather than a second set.
⚑ AND ONE COST TO STATE PLAINLY BEFORE ANY OF IT: a gate whose only verdict comes from bazel hard-depends on the build. A fresh clone cannot commit until it builds, and the fast host loop disappears. That is the trade the ruling accepts; recorded here so nobody re-litigates it as a surprise.
⚑⚑ THE RAISE-SHAPE IS ABSENT AND THE CAPTIVITY WAS PRESENT, IN ONE PLACE. cassian reported
_arg_after reading sys.argv in their copies and named the real defect: the captivity is the
defect and the raise is its symptom — reading the global means no case can vary the input, so the
branch a docstring describes has never been exercised. mtools recorded it as owed; cassian checked
their own tree because of that sentence; mtools carried it as owed a second time without
looking.
Measured across all four distributions, 36 source files, with a constructed positive control so the searcher is known to see the shape it reports absent:
argv.index( (none)
_arg_after (none)
sys.argv read inside a helper mdstruct/src/mikemol/mdstruct/cli.py
cli.main() read the global with no parameter. Its usage, unknown-mode and grep-arity branches
were reachable only through a caller that mutates sys.argv and restores it — which
tests/test_verify.py::_run_cli did, in a finally, putting every case in that module behind one
restore. A case that forgot it would poison its neighbours and nothing would catch that.
Repaired: main(argv: list[str] | None = None), defaulting to the global so the console script
is unchanged — pinned by its own arm, the same discipline as the grader's interpreter default. The
workaround in _run_cli is retired: one line now, no global, no finally.
⚑ A finding filed outward is not a finding fixed at home, and the second carry is the part worth remembering — the first was honest ignorance, the second was a record I had already written.
⚑⚑⚑ bazel test //... --config=remote IS GREEN FOR THE FIRST TIME: 46 of 46, 26 actions on the
executor. It was 43 of 46 for many ticks, three targets red from one cause.
Operator ruling: normalise the POPULATION, not the rule. The alternatives put were disabling
EXE002 repo-wide — which turns off a check that is correct on the instrument developers
actually run — or excluding the lint targets from remote, conceding the stronger sandbox. Both
were declined.
⚑ THE MECHANISM WAS INVESTIGATED BEFORE BEING BUILT, because I had flagged it as unmeasured.
Three candidates; bazel help build mentions no mode-preserving flag, and the staged files are
owner-writable, so an action may normalise its own copy. ⚑⚑ And the idiom was already in the
tree: mypy_check.sh deletes the synthesized __init__.py markers rules_python writes into a
runfiles tree, for exactly this reason. ruff_check.sh now strips +x the same way.
⚑⚑ AND THE FIRST REPAIR REACHED ONE CALL SITE OF TWO. //hooks:ruff and //hooks:ratchet went
green; //hooks:test_bar_fires stayed red because its suppression arm runs its own ruff
invocation rather than going through ruff_check.sh. "A repair applied to one call site is not a
repair to the class" — recorded elsewhere in this repository about a different check, measured
again here, in the arm whose own comment already says it must share the gate's setup.
before 43 of 46 remote (ruff → ratchet → test_bar_fires, ONE cause)
after script fix 45 of 46 remote (the arm's own invocation still unnormalised)
after both 46 of 46 remote, 46 of 46 local
⚑⚑⚑ AND THIS HEADING SAID BOTH SYMBOLS WERE CLEARED, WHICH IS TRUE OF ONE OF THEM. The three red targets are fixed. The CAS still discards mode bits — the repair NORMALISES the population so the rule stops misfiring, and changes nothing about the executor. ⟐REMOTE-DISCARDS-MODE-BITS therefore STANDS as a property to design against, and its own section records why: any check keying on a mode bit is unsound remotely, and none should be written without knowing that. ⚑⚑ A FIX FOR A SYMPTOM FILED AS A FIX FOR ITS CAUSE is the recurring shape in this repository's own clearing record — the symptom is what went red, so it is what a reader remembers, and the cause quietly inherits the CLEARED the symptom earned.
⚑ THE BLOCKING CLAIM WAS TRUE, MEASURED: --select magic-value-comparison gives rc=2 ruff failed without --preview and rc=1 with it. A name selector genuinely needs preview to load, so
"rename first" was never available.
⚑⚑ AND MY OWN PROBE OF THE POPULATION WAS THE WRONG INSTRUMENT. --select RUF201 reported
zero sites in all four distributions, contradicting the poll's 18-in-hooks. --select
replaces the config's selection, so it loaded the rule without the select = ["ALL"] context
that produces the findings. --extend-select reproduces the poll's figure exactly. The poll was
right and the probe was wrong — the same shape as arm C measuring replacement semantics rather
than the tree.
The real population in hooks, listed: 18 RUF201 (every one in pyproject.toml's own ignore
and per-file-ignores lists) + 18 RUF106 (suppression comments in test_bar_fires.py ×17 and
test_checkers.py ×1). All 36 auto-fixable. Repo-wide the preview paydown is 171.
Ruled: arm preview = true repo-wide and pay the 171 down. ⚑⚑⚑ DONE, IN FOUR COMMITS:
9802790 hooks · 39b25ad ratchet · 9129a3d mdstruct · f7d92c9 fence. Census measured 0
across all four; every ratchet baseline is EMPTY, each lowered on an operator ruling and F-armed
(a planted finding is refused by name, a clean copy passes).
⚑⚑ AND THE 171 WAS NEVER RE-MEASURED AFTER IT WAS WRITTEN, WHICH IS WHY IT OUTLIVED ITS SUBJECT. Measured fresh at each arming it read 170, then 122, then 114, then 60, then 0 — the figure in this paragraph and in the tick prompt stayed 171 throughout. A recorded count announces no way to re-check itself; the poll's own repair for this was to stop printing the number and print the command instead, and this section is the same defect one document over.
⚑ WHAT IS WORTH CARRYING FORWARD IS NOT THE STATUS BUT THE ORDER AND ITS TWO TRAPS, for whoever
arms the next preview-gated rule: arm first, rename second (a name selector cannot LOAD without
preview — measured, rc=2); and probe with --extend-select, never --select, which replaces the
config's select = ["ALL"] and reported zero where the tree held eighteen.
⚑⚑⚑ cassian RAISED IT AS A HYPOTHESIS ABOUT MY TOKENISER RATHER THAN A CLAIM ABOUT MY TREE, AND
IT HOLDS FOR EXACTLY ONE ROW. The role-axis table published at e046800 carried
---- a COMMENT being written as evidence that role does not matter within readers. Measured:
# COMMENT in a heredoc body tokens: ['>','g.txt','<<','EOF','EOF'] ⚑ the mention is GONE
BARE mention in a body tokens: [... 'notes.md', 'EOF'] the mention survives
# opens a shell comment that swallows the rest of the line, so the token never existed. That
row measured comment-stripping and supported nothing.
⚑⚑ THE CONCLUSION SURVIVES ON THE OTHER ROWS — a correction, not a retraction. touch scratch.md and echo hello.md pass with the token present, which is the real evidence for
reader-scoping. Five of six rows were measurements; one was an artifact of my own fixture. Against
a reproduction of the PRE-FIX scan: the bare mention fired, the comment did not — so the
distinction was live and invisible.
⚑ THE ARM ASSERTS ITS OWN PRECONDITION. It checks the mention reaches the token stream before asserting the pass; otherwise it would go green for the same accidental reason the published row did. An arm that cannot tell "the gate allowed it" from "the shell ate it" is measuring the fixture.
⚑⚑ DROPPING EVERYTHING AFTER << THREW AWAY A REAL READ. Measured:
cat > g.txt <<EOF / body / EOF / grep -n foo notes.md
drop-everything -> ['>', 'g.txt'] ⚑ the grep vanishes
terminator-aware -> ['>', 'g.txt', 'grep', 'foo', 'notes.md'] the read survives
The tokeniser does not split on the newline after a terminator, so a heredoc followed by any command folds that command into the same invocation — ordinary shell, not an exotic shape — and the gate went quiet on it for one tick.
⚑ ADOPTED cassian'S BOUND BY MEASURING IT, NOT BY COPYING THE DESCRIPTION — the same discipline
that caught the -e divergence, where copying would have been wrong. Their safe-direction argument
is kept and holds on inspection: an unterminated tag swallows the remainder, because a body token
read as an ARGUMENT is a false refusal of a command that reads nothing, while an argument read as
BODY is a missed catch in a command that is WRITING, whose destination is still scanned.
⚑⚑⚑ THE DEFECT REFUSED THE ARMS WRITTEN TO FIX IT — six refusals in one write, every one from a
test fixture. cat is in TEXTUAL, so a heredoc carrying grep -n "SKILL.md" x.py was read as
six textual queries. The file documenting the bug could not be written through the gate carrying
it; the suffix is composed from parts in that test module for exactly that reason.
Two arms, two different discriminators, fixed in _scannable():
PATTERN for {grep,rg,egrep,fgrep,ag,ack} the first non-flag argument is what you search FOR,
and `-e`/`-f` carry it too. Dropped once, never for cat/head/wc.
HEREDOC everything from `<<` onward is a BODY the command creates. `>` and `>>` name a
DESTINATION and STAY IN SCOPE.
⚑ THE -e CASE IS A MEASURED DIVERGENCE FROM cassian'S TREE. They reported _FLAGS_WITH_ARG
consuming -e's argument before the scan sees it; here it does not — that table covers
WRAPPERS (timeout, env, sudo, xargs), never the textual programs. grep -e PAT file left the
pattern as the first non-flag argument, so a fix copied from their report alone would have left
this shape firing. Tokenisation read, not assumed.
⚑⚑ TWO EXISTING ARMS REFUSED MY FIRST CUT, AND THEY WERE RIGHT. It dropped everything from the
first redirection operator onward, which exempted cat >> scratch/tool.py — a shell append to a
claimed artifact. That arm carries an operator ruling verbatim: "don't support redirection,
support editing" / "appendation causes files to grow out of control" — and its comment records
that an earlier fix exempting >> was the wrong repair, with the measured damage: a staging
block appended to and never drained outgrew the budget of the reader loading it every session.
I reproduced that exact wrong repair. The arm is what caught it.
⚑ AND MY OWN HEREDOC ARM WAS WRONG TOO, measuring two properties at once: it wrote to
/tmp/x.py, and .py is claimed in the test table, so the destination firing was correct
behaviour. Isolated with an unclaimed .txt destination, plus a new control arm asserting a
heredoc write to a claimed destination still fires.
Live, through the rebuilt hooks: cassian's originally-reported command now works; a heredoc
whose body names notes.md goes through; grep -n test README.md is still refused.
⚑⚑⚑ Writing this tick's arms, the structural-query hook refused a cat heredoc because the
Python source being written contained the literal "doc.md" — a string inside a test, not a
path, not a grep pattern, not an argument to anything.
cat > tests/test_verify.py <<'PY' ... doc = tmp_path / "doc.md" ... PY
-> structural-query: `cat` over "doc.md" (.md → markdown)
⚑⚑ AND "ANY TOKEN IN THE COMMAND TEXT" WAS MY OVERSTATEMENT, CORRECTED BY MEASURING TWO AXES SEPARATELY. cassian's reply supplied the missing half — the claims table — and the two together separate which suffixes from which positions:
ROLE varied, suffix fixed at .md SUFFIX varied, role fixed at a real target
deny a real TARGET deny .md
deny a grep PATTERN ---- .tsv .bib .py .agda .txt
deny a STRING LITERAL being written
---- part of a COMMENT being written
---- an argument to a non-reader (touch)
---- bare, in an echo
So the scan is scoped to commands the hook classifies as READERS, and within those the token's
role does not matter — target, grep pattern, and a string literal inside a heredoc all fire,
while touch scratch.md and echo hello.md pass. Not "any token anywhere"; not "the first
non-flag argument" either.
⚑ THE SUFFIX TABLE IS WHERE THE BLAST RADIUS LIVES, measured by cassian across three trees from
each repo's own SKILL.md claims column: mtools 1 (.md), cassian 3 (.bib .md .tsv),
substrate 9 (.agda .agdai .bib .jsonl .lagda .md .mk .py .pyi — including .py). That
explains my three vacuous .tsv arms exactly, and predicts substrate has by far the widest
exposure. Neither of us has measured substrate's copy; recorded as their observation, not a claim
about their tree.
⚑⚑ AND A DEFECT STATED WITHOUT ITS DOMAIN READS AS UNIVERSAL — cassian's own phrasing for why
their "wider" claim cost me a measurement. It is the rubric.tsv class travelling OUTWARD, which
is the worse direction: I spent a probe discovering their scope rather than checking their claim.
⚑⚑⚑ THE LAUNCHER SHIPPED AT c4675fe REFUSED ITS OWN PRESCRIBED REPAIR. It denies when the
built venv is absent — correct, and the reason the fail-open window closes. One tick later a
rebuild invalidated bazel-bin, the launcher refused as designed, and then refused
bazel build //hooks:.venv — the command its own refusal message tells the reader to run.
Every Bash call was blocked, including the one that repairs the condition.
⚑⚑ THE SESSION ESCAPED ONLY BY AN ACCIDENT OF SCOPE: the hook matcher is Bash, and Edit is
not gated. Had the matcher been wider there would have been no way out from inside.
Repaired with a bootstrap exemption, measured 7 of 7 with the venv absent:
bazel build //hooks:.venv ALLOW the repair itself
bazel build //hooks/... ALLOW the package form
bazel test //... deny ⚑ a test run repairs nothing
bazel build //mdstruct:.venv deny ⚑ a DIFFERENT venv is not this hook's repair
rm -rf / deny
grep -n foo README.md deny an ordinary refusable command
echo bazel build //hooks:.venv ALLOW ⚑ HONEST LIMIT, recorded rather than hidden:
a substring match cannot tell a build from an echo
⚑ A GATE WHOSE REFUSAL CANNOT BE SATISFIED IS NOT FAIL-CLOSED; IT IS FAIL-SHUT, AND THE DIFFERENCE IS WHETHER A PARTY CAN GET OUT. The fail-open analysis was right and incomplete: I measured what happens when the hook cannot run, and not what happens when it runs and refuses everything. Both are ways for a gate to stop being useful; only one of them looks like safety.
cassian confirmed all three findings from ab722b5 and reported the pattern-as-artifact defect as
wider than I measured — not .md-specific but any claimed suffix inside a pattern, cause
positional at their verdict():205.
⚑⚑ MEASURED HERE, AND THE CORRECTION IS MINE TO MAKE:
grep -n "SKILL.md" <a .py file> DENY ⚑ live here — the original report
grep -n "rubric.tsv" preflight.sh no deny
grep -n "panels.tsv" blockers.sh no deny
grep -n foo README.md DENY the guard itself, still firing
grep -n foo hooks/rubric.tsv no deny ⚑ A REAL .tsv TARGET ALSO DOES NOT ROUTE
The last row is the finding: .tsv has no owner in mtools (grep -rn tsv routing_table.py
returns nothing), so the three .tsv arms measured nothing at all, in both directions. They
would have read as mtools is clean when they only mean mtools does not route that suffix.
The defect is live here for .md — one instance, not a class. Their fix shape (positional, scoped
to the grep family, with cat/head/wc explicitly excluded so the guard is not de-armed) is
right and is expected to be taken.
⚑ AND _arg_after IS OWED TWICE NOW. Their sentence — a finding filed outward is not a
finding fixed at home — applies to me symmetrically: I recorded it as owed, they checked their own
tree because of that, and I still have not checked mine.
⚑⚑⚑ I TOLD THE OPERATOR THIS MATTERED "ONLY FOR THE INTERACTIVE DEV LOOP" AND THAT WAS FALSE.
.claude/settings.json invokes all three mikemol-hook-* console scripts as PreToolUse hooks —
they are the gates refusing commands in this very session. The host copies carry
#!/home/mikemol/github/mtools/hooks/.venv/bin/python3, an absolute path generated by uv: host
state, load-bearing for the harness.
⚑⚑ AND A MISSING HOOK FAILS OPEN. Measured: the built venv could not run the entry point, and reported rc=0, empty stdout, no decision — which the harness reads as allow. Every refusal would stop, silently. That is what makes the launcher's refusal arm the point rather than a nicety.
Ruled: build the scripts AND route the harness through tracked launchers. Both landed.
⚑ ONLY AN ABSOLUTE SHEBANG WORKS, MEASURED, THREE SHAPES, TWO WORKING DIRECTORIES:
#!<abs>/python3 runs from any cwd, under the venv's interpreter ✅
#!./python3 POSIX resolves `#!` against the CWD, not the script's
directory — wrong python, and cannot exec from elsewhere
#!/usr/bin/env python3 runs, and runs the HOST mise python — the leak
So the generated scripts carry no shebang at all: they are executed by an interpreter the
caller names, and insert the venv's site-packages on sys.path themselves. The launcher supplies
the interpreter from $CLAUDE_PROJECT_DIR at run time.
Could not find platform dependent libraries <exec_prefix> appeared on every invocation:
H1 pyvenv.cfg `home` names the wrong directory corrected it — PERSISTS
H2 no lib-dynload under the venv host venv has the identical shape — QUIET
H3 the toolchain binary warns run directly: QUIET
H4 the symlink-chain shape differs built BOTH shapes in a scratch tree: BOTH QUIET
H5 the `bazel-bin` CONVENIENCE SYMLINK ✅ same venv, two paths, one variable:
via bazel-bin WARNS prefix=/home/.../bazel-bin/hooks/.venv
via real path QUIET prefix=.../execroot/_main/bazel-out/.../hooks/.venv
CPython resolves sys.executable without following bazel-bin and computes exec_prefix beneath
it. The home fix was not the fix — it is correct on its own terms and is kept as such, with
the comment saying so. The launcher's realpath removes the warning; measured, it does.
⚑⚑ THE ARM READ 21 CHARACTERS OF A 79-CHARACTER COMMAND. "command":\s*"([^"]+)" stops at the
first quote, and these commands embed escaped quotes around the path — so the capture was
'STRUCT_HOOK_BLOCK=1 \' and the venv path the arm forbids was never in the searched text.
The arm passed on a haystack that could not contain its needle, and only the F-arm caught it.
A structured file read with a regex is the shape this repository's own toolkit hooks refuse for
markdown, done here inside the suite that enforces it. Parsed as JSON now.
⚑ AND THE VACUITY SWEEP REPORTED THE FIRST FORM VACUOUS — CORRECTLY, FOR A REASON WORTH KEEPING.
[c for c in commands if "bazel-bin" in c] puts an In node in the tree, which the sweep resolves
against the file the arm reads; bazel-bin is absent from settings.json because the repair
removed it. The sweep matches In, not NotIn, so an assertion of ABSENCE must be written not in to be read correctly. Two shapes that mean the same thing to a human, only one of which the
instrument can classify.
⚑⚑⚑ blockers.sh WENT ON HANDING OUT THE INSTRUMENT THE REPOSITORY HAD STOPPED TRUSTING. Its
RUF201 line printed env -C $d .venv/bin/ruff check --preview --statistics . as the way to measure
the preview debt — a host-venv ruff — while ad49f96 had removed host ruff from both
.githooks/pre-commit and preflight.sh on the operator's ruling that gate verdicts use the
build's venv. Derived, it was the last live consumer: grep -rn '.venv/bin/ruff|mypy' across
the poll, the gate, preflight and orphan_check.sh returns one prose mention and this one
instruction.
⚑⚑ IT SURVIVED BECAUSE THE TWO AGREE. Measured on hooks, byte-identical output — 48 errors,
same seven rules, same counts — host venv and @ruff//:bin alike. Both 0.16.6, a coincidence
maintained by hand between resolvers with no shared constraint. A stale instruction that still
produces the right answer is invisible until the coincidence ends.
⚑ AND THE OBVIOUS REPAIR WAS WRONG IN A WAY ONLY RUNNING IT SHOWS.
bazel cquery '@ruff//:bin' --output=files prints external/+_repo_rules+ruff/ruff — execroot-
relative — so under env -C $d it resolves against the distribution directory and fails.
bazel info execution_root supplies the prefix. The printed instruction was then executed
verbatim: 4 distributions, 60 / 48 / 55 / 8 = 171 findings. A printed instruction nobody has
run is prose, not a measurement.
⚑⚑ //<dist>:ratchet ALREADY RUNS --preview AND IS NOT A SUBSTITUTE. It reports refusals
against a baseline — did the debt grow — while this line asks how big is the debt. Different
questions; the target answers the first, and nothing answers the second without running ruff.
⚑⚑⚑ THE SWEEP CAUGHT MY ARM, AND THE DEFECT WAS SYMMETRY. The first cut read
".venv/bin/ruff" in ln or ".venv/bin/mypy" in ln — obvious, balanced, and .venv/bin/mypy
appears nowhere in blockers.sh (measured: 0), so that half could never match. The vacuity
sweep names exactly that, in an arm about stale instructions. The paths are composed from the
checker names now, which keeps the property and gives the sweep nothing false to resolve.
⚑⚑⚑ THE COMMENT PROMISED THE FINDING AND THE FLAG BESIDE IT THREW THE FINDING AWAY.
preflight.sh, rewritten at ad49f96 to run //<dist>:ruff and //<dist>:mypy, passed
--test_output=errors with a comment saying "so a refusal carries its finding" — and
--ui_event_filters=-DEBUG,-WARNING,-INFO on the same line. Bazel emits test output as an
INFO event.
Measured on a planted PLR2004, one flag varied at a time:
--test_output=errors --noshow_progress --ui_event_filters=-DEBUG,-WARNING,-INFO
rc=3, names PLR2004: FALSE (7 lines)
--test_output=errors --noshow_progress rc=3, names PLR2004: TRUE (26 lines)
--test_output=errors rc=3, names PLR2004: TRUE (33 lines)
⚑⚑ TWO HYPOTHESES WERE REFUTED BEFORE THE THIRD WAS MEASURED. First: errors prints only for
tests bazel EXECUTES, so a cached failure prints the path — refuted, --nocache_test_results
changed nothing. Second: errors never replays and all is required — refuted, all was also
FALSE under the filter. Only then did the filter become the candidate, and varying it alone
settled it. Writing "because caching" after the first arm would have been the invented-cause shape.
What a reader saw before and after, on the same planted defect:
before //ratchet:ruff FAILED in 0.1s
/home/mikemol/.cache/bazel/.../test.log ← go open it yourself
after PLR2004 Magic value used in comparison, consider replacing `42` ...
--> src/mikemol/ratchet/cli.py:83:17
83 | return n == 42
⚑ AND THE DETAIL WAS ALWAYS IN THE LOG — rule, file, line, source excerpt. This is the
detail that exists somewhere is detail the reader does not have class the gate repaired for its
own checks with run_checked, reintroduced one file over by a flag chosen for tidiness.
⚑⚑ THE ARM THAT NOW FORBIDS THE PAIRING HAD TO CONCEDE TO A SIBLING ARM, AND THE TENSION IS
REAL. Its first draft derived the consumer list into a local variable and read each file in a
loop — which test_no_string_assertion_in_this_module_is_vacuous cannot resolve, because that
sweep follows <CONST>.read_text(...). The unresolved ceiling went 22 → 23. Raising the ceiling
would have been expanding a baseline to fit my code, so the arm reads through the module
constants instead and keeps its population derived from them. Deriving a population and being
statically resolvable pull against each other; here both were satisfiable, and where they are not,
the ceiling wins.
⚑⚑ preflight.sh:114 carries the identical if [ -x ratchet/.venv/bin/mikemol-ratchet ] guard
repaired in the gate at 0097f22 — and preflight.sh:63-72 states the rule in its own words:
"an absent one REFUSES rather than skips … a skipped check and a passing one are
indistinguishable downstream, and this script exists to predict the gate rather than to produce a
second, weaker verdict." Forty-five lines later it skips silently.
⚑ THE ARM THAT CAUGHT THE GATE COULD NOT SEE THIS, because it reads _GATE alone — a
population of one, hard-coded, in an arm written against the hand-written-population defect. The
repair is to derive the consumers: grep -rln 'venv/bin' .githooks/ preflight.sh returns exactly
those two files.
⚑⚑⚑ CLEARED 2026-09-11, AND THE READING ALONE WOULD NOT HAVE SETTLED IT. Measured at HEAD: the
if [ -x ] guard is gone, a refusing _singleton check stands three lines above the invocation,
and preflight.sh:157 states no guard: presence is REFUSED ON above. But the repair being in
the file and its return being PREVENTED are two claims, and this repository's standing rule is
that a deletion premised on a successor must prove the successor FIRES.
⚑⚑ SO THE SUCCESSOR WAS F-ARMED, AND THE FIRST MUTATION WAS MINE RATHER THAN A GAP. Planting
an if [ -x ratchet/.venv/bin/mikemol-ratchet ] guard left
test_every_tool_the_gate_invokes_is_refused_when_absent PASSING — which looked like a hole and
was the arm being right: its predicate is not no guards but no guard on a tool the file does not
ALSO refuse on, and that tool is refused three lines above, so the guard is unreachable-when-absent
and harmless. ⚑ I planted against the arm's NAME; the predicate lives in its helper. Reading the
helper is what separated a real gap from my own misfire.
⚑ RETARGETED AT THE ACTUAL SHAPE — a guard on a tool with NO refusal — the arm FAILS and names
it: preflight.sh guards on ['mdstruct/.venv/bin/pandoc'] … while its refusals cover only
['mikemol-ratchet', 'mypy', 'python3', 'ruff']. The repair is covered going forward; the working
tree was left clean, the mutation having been applied to a copy.
⚑⚑⚑ .githooks/pre-commit REFUSES a commit when ruff, mypy or python3 is missing from any
distribution, saying "a skip here would report green over a check that never executed" — and then
sixty lines later guarded three checks with if [ -x <tool> ], which SKIPS SILENTLY.
Measured, both populations derived from the file rather than typed:
refusing loop covers ruff mypy python3 × every dist in $_dists (*/pyproject.toml)
guarded blocks mdstruct/.venv/bin/python3 ×2 — REDUNDANT, the loop already refuses
ratchet/.venv/bin/mikemol-ratchet — NOT COVERED, a real hole
mikemol-ratchet lives in ratchet/ only (absent from fence, hooks, mdstruct)
So an absent mikemol-ratchet silently dropped the preview-debt ratchet — the check that has
refused most often here — and the commit reported green. That is substrate's
check_scratch_runtime.py defect, which the harness union refuses as a shape, reproduced in
mtools' own gate.
⚑ AND THE ABSENCE PATH HAD NEVER BEEN EXERCISED. Every guarded tool exists on this host, so the guards had only ever taken their true branch. An armed-looking check whose refusal arm has never run is exactly what the suite exists to catch — and nothing was catching it.
Repaired: two guards deleted as redundant, the third replaced by a real refusal (written as a
plain if, not a one-element for — shellcheck's SC2043 is right that the latter reads as a bad
expansion). //hooks:test_bar_fires now derives both populations and refuses any if [ -x ] guard
on a tool no refusal covers.
⚑⚑ THE ARM PASSED VACUOUSLY ON ITS FIRST RUN AFTER THE REPAIR, AND THAT IS THE FINDING ABOVE THE
FINDING. With every guard removed the guarded set is empty, so assert not unrefused is trivially
true: green over a property nobody is checking. It now also requires the refusals it credits to
exist, so a gate that deletes its mikemol-ratchet refusal fails even with no guards left.
F-armed both ways against a mutated copy — control passes, guard-reintroduction refuses,
refusal-deletion refuses via the second assertion.
⚑ re.findall RETURNS list[Any] and this distribution refuses Any in an expression — 20
mypy errors, the same leak that made a dataclasses.fields() arm unusable one commit earlier. The
answer is annotating each result, never widening the config.
blockers.sh's RUF201 line printed for d in hooks mdstruct ratchet as the command to measure the
preview paydown. fence landed at cc3d301 and was NOT in that list, so the figure a reader would
measure omitted a whole distribution — the same hand-written population as ⟐FENCE-WARRANTS, in the
poll instead of the gate.
⚑⚑ FIXED BY DERIVATION, NOT BY ADDING fence. Typing the fourth name would have reproduced the
defect one distribution later. The poll ALREADY enumerates every distribution by its
pyproject.toml (line 152, with a control arm asserting the query finds the three known ones); the
RUF201 line now interpolates that same query. Reads for d in fence hooks mdstruct ratchet today
and will read the fifth name the day one lands, with nobody remembering to edit it.
⚑ AND THE ADJACENT COMMENT'S REFUSAL STILL STANDS, correctly. It declines to RUN ruff inside the poll — three process starts against a script measured at 222 — and that is a separate question from which directories to name. Knowing the population is free here; measuring it is not.
9a97ed8 removed external from //fence:test_fence on the operator's reasoning: a
target depending on state outside the build graph is unsound BY CONSTRUCTION, and
disabling its cache pays for the unsoundness rather than removing it.
⚑ The host property that witness depends on is a process-free parent cgroup with
memory+pids delegated — measured 2026-09-10, recorded in fence/src/mikemol/fence/cgroup.py,
DECLARED NOWHERE. So a cache hit asserts a fact about an undeclared input. Measured after
the change: bazel test //fence:test_fence reports Executed 0 out of 1 test: 1 test passes
from cache.
⚑⚑ AND I FLAGGED, WITHOUT MEASURING, that the same may hold for other sh_test checkers
here (//fence:ruff, //fence:mypy, the ratchet targets). THAT IS A CLAIM I HAVE NOT
TESTED. A tick taking this should MEASURE the scope before proposing anything — an
unmeasured generalisation from one instance is the shape this session kept catching.
~/.local/share/mise/installs/ was removed 2026-09-15 20:47 and took every host interpreter in
this tree with it. Found not by this session but by linux-sources, who filed unprompted after
their own gate died calling mdstruct/.venv/bin/mdstruct.
⚑⚑⚑ THE ERRNO NAMED THE WRONG ARTEFACT, AND THAT IS THE TRANSFERABLE HALF. Their report:
FileNotFoundError ... mdstruct/.venv/bin/mdstruct — where that file exists, 340 bytes, mode
775. execve reports ENOENT against a SCRIPT when its shebang INTERPRETER cannot be resolved. This
session would have read that as the script is missing and looked in the wrong place.
⚑⚑ THEIR CENSUS IS A DENOMINATOR NO SOLIPSIST MEASUREMENT HERE COULD REACH: 37 venvs under
~/github, 25 dangling, 12 resolving — and every one of the twelve resolves to a uv-managed
interpreter or /usr/bin, none through mise. So the split is not random; it is exactly the
venvs built against the removed root. That is the census-kit obligation arriving unprompted from
the other side.
~/.local/share/mise/installs/ |
absent |
mise ls --installed |
empty |
mise binary |
present, /snap/bin/mise |
bazel, bazelisk |
not on PATH |
mdstruct/.venv/bin/python3 --version |
exit 127 |
bazel-bin/mdstruct/.venv/bin/python3 --version |
Python 3.13.13 |
| the bazel cache | survives, ~/.cache/bazel/_bazel_mikemol |
⚑⚑⚑ NOTHING IN THIS TREE FAILED OPEN, AND THAT WAS MEASURED RATHER THAN HOPED. Two guards were suspected and both are honest:
.githooks/pre-committests[ ! -x "$dist/.venv/bin/$tool" ]overruff,mypy,python3per distribution (12 call sites, re-derived — the comment invenv.bzlsays 13) and refused with "fence/.venv/bin/python3 not found — cannot run the gate, commit refused".venv_python_forgates onPath.exists(), which I suspected of admitting a dangling symlink. Measured false:.exists()FOLLOWS the chain and returnedFalsefor the dead link while returningTruefor the bazel-built control. The resolver is correct as written.
⚑ AND venv_from_hub PAID OFF EXACTLY AS ARGUED. Its comment states the reason directly — an
artifact that is only valid at the path it was built at is host state with a build step in front of
it — and the operator ruled for a whole-distribution venv over rules_python's per-target one
knowing the non-bazel consumers would depend on it. The mise root vanishing is the failure that
argument was about. This is not a lucky side effect; it is the design's stated purpose meeting
its case.
Ruled against the alternative of reinstalling mise, on the evidence that the twelve surviving venvs
under ~/github all resolve through uv. uv venv --python 3.13 --allow-existing per distribution:
- uv fetched cpython-3.13.13, byte-matching what
venv_from_hubpins and what the bazel venv reports — so the pin did not have to move. - ⚑
--allow-existingREPLACED ONLY THE INTERPRETER LINKS. Every installed package survived:ruff 0.16.6andmypy 2.3.1still run in all four. The damage was one symlink per venv. - VERIFIED BY RUNNING, NOT BY
-x— which is this tick's own lesson, since the dead interpreter was-x-true untilexecverefused it. All 12 of 12 tool/distribution pairs print a version.
⚑⚑ THE GATE NOW GETS FURTHER AND STILL REFUSES, HONESTLY: all four suites run (fence 53,
hooks 417+3s, mdstruct 151, ratchet 43 — 664 tests), then "bazel not found — cannot run the
hermetic suite, commit refused". That is linux-sources' own fail-shut shape working as designed.
bazelis gone and uv does not manage it. The cache survives, so a reinstalled binary would find its state. Not covered by the ruling; commits remain blocked until it is decided.- ⚑ A REPLY IS OWED TO
linux-sources: they asked whether to file this against the other 24 affected repos and explicitly said they would rather ask than spray. Unanswered. - ⚑ THE
project-toolingSKILL IS NOW PARTLY STALE and says so by measurement: it maps mise to/usr/bin/mise(this box has/snap/bin/mise) and names~/.local/share/mise/installs/as where interpreters live — the directory that no longer exists. Its uv half is exactly right and is what this repair followed.
The pre-commit gate judged bazel test //... by [ $_suite_rc -ne 0 ] alone. domain_witness.sh,
in the same file's execution path, has carried _bazel_green — a positive predicate on bazel's own
success line plus a test tally — since 2026-09-12, with a comment explaining why the exit status is
not a verdict. The gate did not use what the tree knew. linux-sources built a three-row
discriminator from this tree's relayed rc=38 datum, shipped it in their gate on 09-16, and wrote to
say so; this is the same repair arriving home three days later.
| state | rc | bazel's own summary |
|---|---|---|
| green | 0 | Build completed successfully |
| executor absent, nothing ran | 34 | Build did NOT complete successfully |
| BEP upload failed, every test passed | 38 | Build completed successfully + ERROR: The Build Event Protocol upload failed |
⚑⚑ Rows 2 and 3 are both non-zero and bazel says OPPOSITE things about them. Keying on the scalar refused row 3 — a commit whose gate bazel itself reports passing — every time the sink was down.
⚑⚑⚑ AND THE OBVIOUS REPAIR FAILS OPEN, which the peer named before this tree could ship it.
Keying on "BEP upload failed" is present passes a run where a real target also failed, because
both messages are in the log together. The repair is POSITIVE: green requires the affirmative line
AND a tally. Shipped in .githooks/pre-commit (staged) as _suite_green.
_bazel_green's tally conjunct is a false red over any build target — a bazel build with a
dead sink prints the success line and no tally, because there are no tests to tally. The peer
reproduced this on their box (BES → discard port 127.0.0.1:9) and found build and test emit
different summary lines for one state; my relayed spelling would have missed the state it was built
for.
⚑⚑ THE SCOPE IS STRUCTURAL, NOT A COINCIDENCE OF CALLERS. _bazel_green issues bazel test
INSIDE itself — it cannot be handed a build label. So the conjunct is sound there and would be wrong
anywhere the invocation is build. A census of callers bounds what IS; only a stated scope bounds
what CAN BE. The scope is now stated in the function's comment and asserted in an arm:
test_the_witness_reads_bazels_artifact_not_its_exit_status requires bazel test "$1" inside the
function body and forbids bazel build there. F-armed: switching the fixed invocation to build
reds it by name.
⚑ Filed against this tree by the peer as ask-bazel-summary-line-differs-by-invocation in
summit/floor/asks.bib. The answer is scope, not rewrite, and it is in the tree.
This host (Gentoo, 2026-09-19) has no BuildBuddy. bep_probe.py — rebuilt in the tree after its
scratchpad predecessor was erased — reports REFUSED — nothing is listening. .bazelrc hardwires
--remote_cache and --bes_backend to 127.0.0.1:31985 unconditionally, so bazel test returns
rc=34 (row 2: the remote cache needs a capabilities handshake before any action) and every
commit is blocked. Measured that the tree is sound: with --remote_cache= --bes_backend= passed
per-invocation, //:shellcheck_githooks passes 1/1 over the edited gate.
⚑⚑ OPERATOR RULING: wait on BuildBuddy. linux-sources and cassian-observability are bringing
up infra on luthen. .bazelrc is not edited, the gate is not routed around, --no-verify is not
used. Seven paths are staged and survive a boundary; the dispatcher's unstaged rev 17 to
findings/CENSUS-paperkit-use.md is theirs and untouched.
⚑ THREE HOST INPUTS DIED ON THIS HOST IN ONE TICK: the uv interpreter root (host venvs dead again),
shellcheck (mise-managed), and pandoc — which is why this section was written with the
harness Edit rather than mdstruct append-section: the structural writer shells to pandoc on
PATH and there is none. The bazel graph stages @pandoc//:bin as a declared input; the host-side
tool does not, and that asymmetry is this symbol's argument arriving for the third time in one tick.
⚑ A COST ON THIS HOST, NAMED: every bazel invocation that touches the action graph invalidates the
hook venv, and the PreToolUse hook then refuses all Bash until bazel build //hooks:.venv is re-run.
Three rebuilds this tick. The hook is right to refuse rather than fail open; the cost is real.
The scratchpad copy died with the OS change; this one is at the repo root and staged. Shellcheck-clean
through //:shellcheck_githooks (the target re-executed — 2 sandbox actions — so it genuinely read the
new file; host shellcheck is gone). Interpreter DERIVED from dirname of the config, never
realpath'd; refuses when not -x; refuses on empty output.
⚑ MEASURED ON THIS HOST, both arms: the runner through bazel-bin/ratchet/.venv/bin/python3
reports 13 = 11 killed + 2 unreachable, 0 survived, 0 errored — identical to the 09-13 baseline
through a third interpreter. And the script's precondition refuses the dead host venv by name
(ratchet/.venv/bin/python3 was not staged) — a real dead interpreter, not a plant.
⚑⚑ THE RUNFILES QUESTION IS NARROWED, NOT ANSWERED. venv.bzl:176 returns
DefaultInfo(files=..., runfiles=ctx.runfiles(files=outs)), so data = [":.venv"] SHOULD stage
the tree — yet the 09-13 runfiles listing showed mutants, pyproject.toml, src, tests and no
.venv at all. The one declare_symlink output (bin/python3, line 44) could be dropped from a
runfiles tree, but that would lose one file, not the whole directory. Whatever the cause, it needs
bazel test to reproduce, and the hold forbids that. The BUILD wiring stays unwritten until 31985
answers. Written down as the next measurement, not as a hypothesis to edit in.
_population_negatives_by_binding walks binding → assert-use, the reverse of the existing
assert → binding walk, and test_two_walks_agree_on_every_population_shaped_negative_by_name
asserts the two sets are EQUAL by member. Both live in test_bar_fires.py; the scratchpad
predecessor vacuity2.py is not needed and is not coming back.
⚑⚑⚑ THE PLANT WAS CHOSEN BY MEASUREMENT, AND THE FIRST GUESS DEMONSTRATED THE WRONG THING.
Dropping ListComp lost nine of thirteen — the floor caught it, so both arms red and the gap was
not shown. A one-off reader censused the binding shapes: DictComp 1 · Call 2 · ListComp 10.
Dropping Call loses exactly two, 11 clears the floor of 10, and the result is .F — floor
passes, agreement reds, naming the two members and which walk dropped them. That is F-arm C's
gap closed by a second instrument rather than a bigger assertion. Symmetric: the reverse plant
reds with the same two on the other side.
⚑ THE POPULATION IS THIRTEEN, NOT TWELVE — grown by one since 09-13 (the selector-resolution arm
from 4ab4a14), re-derived rather than quoted. And the agreement arm's own two negatives are
counted in the population it measures, correctly: they are bound from sorted(...), guarded by the
floor above them, and admitted by both walks. Fifteen with those two.
⚑ RUFF CAUGHT A COMPOSITE ASSERTION (not A and not B names neither half) and the split is
better: each direction of narrowing now says which WALK dropped what, because the repair differs by
direction. The plant-selection reader was deleted rather than kept — it was a probe, not a
component, and under the distribution's bar it would have been five findings.
⚑⚑ THREE PRE-EXISTING ARMS FAILED ON THIS HOST FOR A REASON WORTH NAMING — repaired tick 20.
_needs_reader guarded them on the mdstruct console script EXISTING (is_file()), and
mdstruct/.venv/bin/mdstruct exists — its shebang interpreter does not. FileNotFoundError against
the script, the file present: the ENOENT-names-the-wrong-artefact shape linux-sources measured,
inside this tree's own guard.
The predicate is now _reader_runs(): an invocation of the reader with --help, where any exit
proves execve accepted it and OSError is the corpse. On this host the three arms now SKIP with a
reason that names present-but-unrunnable, where they FAILED blaming a present file. Three skipped
arms with a stated reason is the honest state on a host without the reader; three failed arms
blaming the wrong file is not.
⚑⚑ BOTH ARMS OF THE GUARD, IN THE TREE, HOST-INDEPENDENT. test_reader_guard.py builds a
live reader (shebang /bin/sh) and a corpse (shebang at a path that does not exist, mode 775,
is_file() true) and asserts the predicate admits one and skips the other. The corpse is
CONSTRUCTED, not found — pointing at the host's actual dead script would make the arm's verdict
depend on which host it runs on, the exact coupling the guard exists to survive. F-armed: making
the predicate ignore OSError reds the dead-shebang arm and only that arm.
⚑ Ruff surfaced six findings in the new module and mypy one; all repaired structurally — the
subprocess import declared per-file in pyproject.toml with the reason (the guard's subject IS
execution), Path moved to a type-checking block, docstrings given Returns sections. No noqa beyond
the one call-site directive the file's convention already uses.
⚑ NINE OTHER ARMS FAIL ON THIS HOST and none is this change: fence/.venv not built here, the hook
console scripts absent from the dead host venv, test_adoption's probe venv refused by the bazel
interpreter's missing exec_prefix. Host state, each — and each the same class as the one just
repaired, which is why they are listed rather than fixed in this tick.
The OS change moved the scratchpad and emptied it. Lost: await_bep.py, mutate_check.wip.sh,
mutate_runner.wip.py, vacuity2.py, every F-arm plant, every probe. ⚑ Census-kit B5, measured
the expensive way: a handle only survives if its referent lives outside the context. bep_probe.py
is the first instrument rebuilt in the tree; the mutation gate wiring (mutate_check.sh) must be
rewritten the same way — shellcheck-clean, interpreter derived from dirname of the config, NOT
realpath'd.
--ratchet finds the scale at which the RESIDENT SET binds on a swap-backed host, and
the scale at which the workload DIES in a pod (k8s sets memory.swap.max=0 on Guaranteed
pods). Its docstring reads as the second while doing the first here.
⚑ NOT CHANGED ON REASONING ALONE — that would be shipping a guess. cassian's stage 2 fences both arms per subject and will produce the measurement that settles whether it needs saying. Ready when that measurement exists, blocked until then.
⚑⚑⚑ THE OPERATOR NAMED IT: most of these checks smell like syntactic or existence checks, not semantic ones. Two repositories were read FROM SOURCE and they converge without sharing vocabulary.
⚑ v4cat DOES NOT DEFINE "vacuous" — 1 occurrence in 90 files, casual English.
Positive control, same tool and flags: witness returns 14 commits, vacuo returns 0.
It does not need to: vacuity is the 10 LEFT cell of a Klein-four read
(methodology.md:288-297), and theory.md:436 gives the consequence — any unary
operation throws away at least two cells. methodology.md:266 names the shape exactly:
every read is a comparison. There is no unary query. What looks unary is always a binary
comparison whose right-hand referent is HIDDEN BY CONVENTION.
⚑⚑ paperkit HAS THE LADDER AS A LITERAL, grade.py:17:
vacuous(0) < indeterminate(1) < existence(1) < behavioral(2) < imported(3)
with grade.py:21 giving the distinction — existence (presence proven) < behavioral
(falsifiability proven). So a green test_no_string_assertion_in_this_module_is_vacuous
is existence, TWO RUNGS BELOW behavioral (ranked 0 and 2). Passing it means rung 0 was
avoided, not rung 2 reached.
⚑ THE GRADER AT 2ed26a4 IS THE INSTRUMENT FOR RUNG 2, and it does not replace the sweep
— they measure different cells. What is still true of the sweep: it reports 10 only,
01 is invisible, and 00 was never a blind spot because U was never bounded — which
rigorous_use.md:78 calls an unearned absence.
⚑ AND _MAX_UNRESOLVED IS NOT A DEBT. It counts the RESOLVER'S REACH, a third syntactic
property, ratcheted by a guard that treats it as owed. The apparent 22→23 collision that
nearly triggered a redesign was a HELPER counted as a test; scoping to test_-prefixed
functions returned it to 22 with no constant moved.
⚑⚑⚑ //hooks:ruff IS GREEN LOCALLY AND RED REMOTELY ON IDENTICAL SOURCES. Measured at
unmodified HEAD (29240c9), so it is not a consequence of any uncommitted change:
bazel test //hooks:ruff All checks passed!
bazel test //hooks:ruff --config=remote Found 23 errors. (EXE002, every .py file)
bazel test //mdstruct:ruff --config=remote FAILED (same shape, both distributions)
Three targets fail and it is ONE cause. //hooks:ratchet refuses 22 new
shebang-missing-executable-file keys — correctly; it is doing its job over ruff's output — and
//hooks:test_bar_fires::test_every_suppression_directive_suppresses_under_the_gates_config
fails with 2 EXE002 for the same reason. With the tick's own new file present the ratchet count
is 23, the delta being exactly that file; at HEAD it is 22, which is how all three were
confirmed pre-existing rather than introduced.
⚑⚑⚑ THE CAUSE, MEASURED DIRECTLY 2026-09-10 — AND IT CORRECTS THIS SECTION'S OWN FIRST
FRAMING. An earlier revision of this entry said "the rule's subject does not exist inside a
build action" and asserted, without measuring it, that bazel does not preserve source mode bits.
Half right, and the wrong half was load-bearing. A probe stat-ing its own staged siblings from
inside an action, reporting through the failure channel:
local sandbox remote executor repository
hooks/tests/*.py -rw-rw-r-- -rwxr-xr-x -rw-rw-r--
S_IXUSR False True False
⚑⚑ SO THE SUBJECT DOES EXIST, AND IS READ CORRECTLY, LOCALLY. IT IS REMOTE STAGING THAT
DISCARDS THE MODE BIT — the executor materialises every source +x. EXE002 is not
malfunctioning: it truthfully reports a filesystem the CAS invented. The finding is true of the
staging and false of the repository — the mis-named-population class, where a correct check
runs over the wrong set.
⚑ AND THE GENERAL FORM IS BIGGER THAN THIS RULE. Anything keying on a mode bit is unsound
under --config=remote, not just EXE002. That is worth knowing before something else is built
on one. See ⟐REMOTE-DISCARDS-MODE-BITS.
⚑ AND THIS IS THE HARD CASE, NOT THE EASY ONE. An inert gate fires on nothing and someone eventually notices. This gate FIRES, produces 23 findings with file and line, and would pass review — while supplying evidence for a proposition nobody asked about. It is the active-gate-aimed-at-the-wrong-predicate shape, arriving at the executor boundary.
What it costs now: bazel test //... --config=remote cannot go green, so every hermeticity
claim wanting the strong instrument sits behind a red bar that is not about the code.
⚑ THE FIX IS AN OPERATOR DECISION AND HAS NOT BEEN TAKEN. Candidates:
ignore = [..., "EXE002"]with the measurement recorded. ⚑ NOTE THE JUSTIFICATION CHANGED WITH THE CAUSE: it is no longer "the subject is unobservable in an action" — the rule works locally — but "the remote population is not the repository's". That is a weaker warrant for disabling a rule that is correct on the instrument developers actually run. Still a change to the bar; "lowering it is an operator decision" covers the shape.- Keep
EXE002and stop running//*:ruffremotely — concedes the weaker instrument for the lint gate specifically, and is now the cheapest correct option rather than a concession: the rule's subject is a repository fact, and the local sandbox is the arm that can see it. - Normalise the mode bits during staging — repairs the population rather than the rule; the structurally honest one, and the most work.
Carried as measured, red, and NOT worked around.
⚑⚑ The executor stages every source file -rwxr-xr-x; the local sandbox stages it with the
repository's own -rw-rw-r--. Measured from inside an action in both modes (see the table in
⟐EXE002-REMOTE-ONLY, which is the first instance rather than the whole finding).
Why it is filed separately from the ruff red: EXE002 is the symptom that surfaced it, but the
property is about the CAS, not about ruff. Any check keying on a mode bit is unsound remotely —
an executable-script assertion, a permissions gate, a py_binary wrapper test. None exists here
yet; this entry is so that one is not written against a mode bit and then debugged as a flake.
⚑ AND IT IS AN ASYMMETRY IN THE SUPPOSEDLY STRONGER INSTRUMENT. Remote execution is adopted in
.bazelrc as the sandbox that proves declarations complete. On this axis it carries LESS
information than the local one — it cannot represent a fact the local sandbox represents
faithfully. That does not retract the hermeticity argument, which is about reachable inputs; it
bounds it. A stronger instrument on one axis is not stronger on all of them.
Whether the fence's CHILD cap binds before a POD's ceiling does. k8s sets
memory.oom.group=1 on every pod cgroup, so a child cap that fails to bind means the
F-arm's 256MB kills the WHOLE EXECUTOR POD rather than reddening one test.
⚑ NOTHING HAS ALLOCATED ON ANY POD IN ANY RUN BY EITHER PARTY, so no restarts=0 reading
is survival evidence. ⚑⚑ It does NOT need the RBE substrate: --mem N --swap 0 on a host
cgroup is the same deliberate breach at a process-sized blast radius. Not proposed as work.
⚑⚑⚑ THIS DOCUMENT CARRIED TWO ⟐RUF201 SECTIONS, BOTH IN THE PRESENT TENSE, AND NEITHER KNEW ABOUT THE OTHER. The section above held the measurement and the ruling; this one held a summary — blocked on a preview-wide paydown … the poll prints the command that measures it — and the poll stopped printing that command the same day the paydown finished. A reader scanning headings meets whichever comes first.
⚑⚑ A SUMMARY OF A LIVE ITEM IS A SECOND PLACE FOR ITS STATUS TO ROT, and it rots faster than the original because it carries no measurement to contradict it. The full section is above; this stub remains only so a reader who followed a line reference here is not left wondering whether a third account exists.
Carried. MD056: 12 ragged rows, all measured out of reach (frozen leg directories, filed legs). Vacuity: the poll now prints a CEILING, honestly labelled, not a count.
⚑⚑⚑ ⟐CLASSIFY-SPANS-ALL-TABLES — CLEARED 2026-09-12, and the cost of carrying it was
larger than the note suggested. The note said only "walks every table without disclosing its
span". Measured: on findings/CENSUS-remaining-work.md, which carries FOUR tables of entirely
different kinds — a surveyor roster, a revision log, a status table, and the state vocabulary
itself — unscoped classify reports 56 rows, 48 UNCLASSIFIED. Scoped to the one table that
carries statuses: 8 rows, 0 unclassified.
⚑⚑ SO THE UNDISCLOSED SPAN DID NOT MERELY WITHHOLD CONTEXT, IT MANUFACTURED A FINDING. A reader seeing 48 unclassified infers a documentation gap in the census; the true answer is that 47 of those rows are from tables the question does not apply to. A revision-log row was never meant to carry a state.
⚑ THE RESIDUE GROUP WAS ALREADY PRINTED AND THAT WAS NOT ENOUGH. Reporting the unclassified count without saying WHAT WAS READ describes a defect in the DOCUMENT; naming the tables describes a defect in the QUESTION. The tool's own stated rule — every mode prints its denominator — was half-kept: a count of rows is half a denominator, and which tables they came from is the other half.
⚑⚑⚑ MEASURED 2026-09-20 (W8), AND THE CARRIED FIGURE WAS RIGHT — BUT ON 09-19 IT WAS
UNVERIFIABLE. find findings inbox -name '*.md' -exec mdstruct lint {} + handed the tool 91
paths and it read the FIRST and stopped; a grep for MD056 over that output returned 0 with a
FAILED positive control (no rule code at all appeared). The zero was a fact about the reader.
lint now takes a path population like verify (_over_paths, shared, 52eded7→W8): the same
command prints 91 denominator lines for 91 files, and the count is:
MD056: 12 rows in 9 files positive control: MD013 943, same reader, same run
findings/bazel/paperkit.md L135
findings/constitution/cassian-observability-constitution.md L44 L45 L46
findings/constitution/substrate-constitution.md L102
findings/deps-build/deps-build-apex.md L103
findings/known-work/mtools-known-work.md L101
findings/membudget/linux-sources.md L152 L234
findings/remaining-work/mtools.md L25
findings/remaining-work/paperkit.md L38
findings/remaining-work/remaining-work-apex.md L52
Every row is N cells against M declared — a separator was added: a | inside a cell, read as
a column break. Eight of nine files are filed legs or apexes under frozen censuses and may not
be amended; findings/known-work/mtools-known-work.md is this repository's own and is the one
repairable row. The count is the deliverable; the repair of that one row is not this item.
linux-sources-94 filed three claims about mtools' mdstruct. Measured before acting on any:
FALSE — "hooks/src/mikemol/hooks/no_chaining.py routes to substrate's mdstruct." The line
at no_chaining.py:18 is a DOCSTRING citing a 2026-09-06 PIPESTATUS incident. It is prose about
a past measurement, not a route. Nothing in hooks/ invokes any mdstruct.
FALSE — "25 references to scratch/mdstruct are live routes." All 25 hits are findings-corpus
PROSE — a recorded measurement naming where the instrument stood when it was measured. ⚑ Rewriting
them would falsify the record, which is the opposite of the migration's intent.
⚑⚑ BOTH FALSE CLAIMS HAVE ONE SHAPE: a grep hit read as a route. A reference to a tool inside a docstring, a finding, or a commit message is a FACT ABOUT THE PAST, and the query that finds it cannot distinguish it from an invocation. This is the repository's own recurring shape — a plausible reading pointing at the wrong subject — arriving from outside it.
REAL and CLOSED at 7e4134e — find_section had no exact= escape. The peer's phrasing is
exact and is quoted in the code: a correct refusal a caller cannot escape is a dead end. The
measurement was 27 substrings of Residue, all failing against a document whose other heading
contains it. exact= lands on find_section, replace_section and append_to_section — all
three, on operator ruling, spelled as substrate's md_spans spells it.
⚑ The routing table at .claude/skills/struct-tools/SKILL.md:25 ALREADY names
mdstruct/.venv/bin/mdstruct — mtools' own build, not substrate's. The repoint the peer asked for
had already landed; the claim was made against a stale reading of this tree.
⚑ Recorded here as well as replied, on operator ruling ("Both"): the correction reaches the
peer before they act, AND this tree keeps the measurement for the next reader who greps
substrate and finds 25 hits. A correction that lives only in a message is a correction the next
vantage does not inherit.
⚑⚑⚑ The structural-query hook routes WRITES to a tool whose CLI cannot perform them. Its
refusal says, correctly, "the owning tool is the route for WRITES TOO: mdstruct/.venv/bin/mdstruct"
— and mdstruct --help lists eleven modes, every one of them a READER. replace_section and
append_to_section exist in sections.py and are reachable only as a library import.
Measured by running the tool, not by reading it: attempting to file this very section hit the
refusal, then found no mode to obey it with. The declared fallback (Write/Edit, for when the
owning tool is unavailable) is what wrote it — which is honest, but the tool is not unavailable,
it is incomplete, and those should not resolve to the same escape.
⚑ SAME SHAPE AS ⟐PEER-MDSTRUCT-CLAIMS' real claim, one layer up: a gate that names a successor is only half a gate if the successor has no mode for the job. The refusal is correct; the route it names is a dead end for writes.
⚑⚑ AND THE CENSUS THAT PRODUCED "eleven modes, all readers" WAS ITSELF COMPUTED FROM THE
UNDER-REPORTING BANNER. --help listed ten modes while the registry held eleven; verify —
registered, green, and named by the routing table's own "verify before any bounded write" —
was absent from the usage text. Fixed at b554842 with an arm that drives BOTH surfaces as
subprocesses and compares what each PRINTS. ⚑ A tool with two spellings of its own capability list
will drift, and the hand-written one is the one adopters read: a mode absent from --help
reads as a mode the tool does not have, and the honest conclusion from that reading is to keep
using the other implementation. An under-reporting tool loses an adopter without ever failing.
⚑⚑⚑ AND I MISCOUNTED THE REFUSAL WHILE FIXING IT — 12 and 11, wrong in both terms, shipped in
the commit message and the warrant. I read the comma-separated list BY EYE rather than running a
counter, inside the repair whose subject is a hand-maintained figure drifting from a derived one.
linux-sources-94 re-measured and reported 11; the registry literal holds 11. Corrected in the
warrant and the arm's docstring. ⚑ The lesson is not count more carefully: a count stated in
prose is the same object as the banner, one layer up, so the arm asserts a RELATION between two
live surfaces and never a cardinality.
⚑ verify IS THE DETECTOR FOR THE CLASS SUBSTRATE'S READER HAS, reported by linux-sources-94
after running it on TICK.md after every structural edit: it answers did every source heading
reach the section list directly, and a silently-swallowed heading is exactly substrate's defect.
So the tool without the defect carries the instrument that finds it — worth knowing when the
migration argument is made on grounds other than provenance.
⚑⚑⚑ OPERATOR RULING 2026-09-12, BOTH AXES — the writer is UNBLOCKED. Subcommands, and
needle before file, matching grep.
⚑ THE SUBCOMMAND RULING CONFIRMS THE EXISTING SURFACE RATHER THAN CHANGING IT, which is a fact
about this tree measured before the ruling was applied: mdstruct spans FILE.md is already a mode
name then arguments. So the migration cost falls on SUBSTRATE, whose md_spans is flag-style
(--headers --tables --rows) — the peer's "aliases vs convert once" trade was about their call
sites, not a change to mine.
⚑⚑ NEEDLE-FIRST IS THE ONE-RULE ANSWER AND IT COSTS NOTHING TO REACH. grep PATTERN FILE.md is
the only existing two-positional mode, and it takes the subject before the file because real
grep does. So replace-section HEADING FILE.md gives the tool ONE convention for every
two-positional mode with no existing call site broken. The alternative — file first, reading
naturally for a writer — would have left grep as the odd one out or required re-spelling it,
which is a migration for peers already using it.
⚑ WHAT IS STILL OWED, and it is implementation rather than decision: the write modes must carry
exact= (the ambiguity refusal and its escape compose at the write path — see the section above),
and the body has to arrive as a file rather than an argument, because a shell that can pass a
multi-line body inline is the >> this toolkit refuses.
⚑⚑⚑ THE OPERATOR ASKED WHETHER SUBSTRATE'S cli MODULE — "everything was supposed to normalize
around" — MADE IT OVER. Measured: no, and the split is worse than its absence.
fence/src/mikemol/fence/cli.py argparse
ratchet/src/mikemol/ratchet/cli.py argparse
mdstruct/src/mikemol/mdstruct/cli.py HAND-ROLLED (_MODES dict, manual _flag parser)
hooks/ no cli module at all
Three CLIs, zero sharing, TWO argument frameworks. mdstruct is the odd one out in its own
tree: its _flag() helper re-derives --name value / --name=value handling that argparse
already does, one directory from two modules that use argparse.
⚑⚑ AND THIS IS THE MEMBERSHIP CRITERION'S OWN CASE, arriving from the inside. The rule is reuse across repos, not repo-local — and here is machinery re-derived THREE TIMES within ONE repo, which no cross-repo criterion would ever surface. A dispatcher is not repo-local by any reading; it was simply never interned, because each distribution authored its own on the way to its first green.
⚑ IT CHANGES WHAT THE WRITER IS BUILT ON. The ruling above says subcommands, and argparse has
first-class subcommand support (add_subparsers) that both siblings already use. So "confirm
mdstruct's existing surface" and "normalize on the shared mechanism" are not the same
instruction, and building replace-section onto the hand-rolled dispatcher would author the
surface twice if the second is intended. ⚑ ASKED substrate-9c DIRECTLY rather than inferring:
what the module is, whether it is domain-neutral, and whether it can actually SHIP — the ratchet
island qualified on merits and was blocked on mechanics, and a normalization point that cannot
land is one mtools must provide locally instead.
⚑ NOT RESOLVED BY ADOPTION. If substrate's module carries an argument order or dispatch shape disagreeing with the operator's ruling, the RULING wins here and the divergence is recorded — a convention is not authority, and reconciling quietly to a peer's shape would be gluing with no witness.
⚑⚑⚑ ANSWERED 2026-09-12, AND THE ANSWER IS: ADOPT NEITHER MODULE'S DISPATCHER, BUILD THE GATE.
substrate-9c disclosed two modules and the accounting for this tree is not what either of us
assumed.
climode is NOT a parser — it declares per-mode contracts (operand, paths, opts, scans,
writes, why) that a gate checks. So "argparse or climode" was a MALFORMED question and it
was mine. It sits beside argparse rather than instead of it.
bib_modes is the generic dispatcher, surfaced only after the operator named it — substrate
searched for the word cli that I used rather than the capability I described, which is the
census-keyed-on-one-spelling defect inside a reply about avoiding duplicated capability. ⚑ ITS
COUPLING IS WORSE THAN climode's AND SUBSTRATE SAID SO UNPROMPTED: dispatch is generic in the
HANDLER but reads a module-level MODES global that is bibstruct's own roster — a dispatcher for
one tool wearing a generic signature. Porting is parameterising the roster, a real edit.
⚑⚑ AND ITS HEADLINE FEATURE DOES NOT APPLY HERE, measured rather than accepted. unbound()
reports declared-without-handler and handled-without-declaration separately, and substrate
believed mtools lacked the second. It cannot have it. _MODES is a SINGLE dict literal, so
the roster IS the handler table — one definition, one lookup, one derived refusal (grep -n _MODES → three sites, and two are uses). A handler with no declaration is unconstructible. Their
unbound exists because bibstruct's roster and handlers are separate objects; this defect is
structural to their layout, not general.
⚑ THE DRIFT THAT DOES EXIST HERE IS ONE LAYER OVER AND IS ALREADY ARMED: the hand-written BANNER
is the second spelling, and f235328 asserts dispatchable == documented in both directions.
That is unbound() aimed at the drift this tree actually has.
⚑ SO bib_modes SUBSUMES: a 3-line dispatch that is already correct, and a refusal already
derived. It does NOT subsume _flag() — deliberately, since neither substrate module parses argv
— and _flag() is the piece carrying the real defect below. Two of three, both already solved.
⚑ NO CLI-SHAPE CONFLICT, confirmed from their side: neither module has an opinion about argument
order. bib_modes assumes only that a mode is keyed by a STRING, and whether that string is
--spans or spans is opaque to it. The operator's ruling lives entirely in the entry point.
⚑⚑⚑ main FILTERS EVERY TOKEN STARTING WITH - OUT OF ITS POSITIONALS, so an argument whose
text begins with a dash is silently discarded and the arguments after it SHIFT LEFT. Measured
against the real program, twice:
mdstruct grep '-- caveats' FILE.md → usage error: the needle vanished
mdstruct grep -- '-- caveats' FILE.md → same — `--` is stripped like any other dash token
There is no end-of-options mechanism at all.
⚑⚑ FOR A READER THIS IS A BAD RESULT; FOR A WRITE IT IS THE SILENT-WRONG-TARGET CLASS. grep
survives only because its arity check catches the collapse. replace-section HEADING FILE.md --body-file X with a dash-leading heading leaves args = ["replace-section", "doc.md"] — a
VALID two-element shape — so the FILE lands in the needle slot and the write proceeds against a
target the caller never typed. That is precisely what the ambiguity refusal and exact= exist to
prevent, defeated one layer below them, before find_section is ever called.
⚑ A HEADING BEGINNING WITH PUNCTUATION IS NOT EXOTIC — this very document has ⟐-prefixed
headings, and --prefixed ones are ordinary in changelogs. The existing modes never hit this
because a PATTERN that looks like a flag is unusual; a HEADING that does is not.
⚑ SO THIS IS THE FIRST WORK ITEM OF THE WRITER, not a follow-up: positional-aware parsing with a
real -- terminator. ⚑⚑ AND IT IS THE ARGUMENT FOR argparse ON THE MERITS RATHER THAN FOR
CONSISTENCY — argparse gives -- for free, both sibling distributions already use it, and
_flag() is a hand-rolled re-derivation that got this wrong. Neither substrate module helps:
they do not parse argv, by design.
Four shapes of green-but-vacuous arm measured across two trees in three days. Each is green under every checker either tree has, and each is a TRUE statement about something other than its subject:
| shape | where the not-running happens | measured by |
|---|---|---|
| a PATH never reached | before the code | cassian — a run bailed on an unrelated precondition |
| a BUILDER never shipped | between code and consumer | mtools — arms on a builder nothing shipped |
| a POPULATION never populated | inside the derivation | mtools — a set empty for unrelated reasons |
| a POPULATION filtered empty | inside the traversal | cassian — a false ordering premise, ten days of logs lost |
⚑⚑⚑ THE OPERATOR SENT ME TO ASK paperkit-82 HOW THEY DO MUTATION TESTING, and the answer is
valuable because it is UNFLATTERING to the thing I was sent to ask about. Against body → raise
at def-site granularity: one of the four caught, three missed. Their phrasing, kept because it
is the whole finding — mutation testing at this granularity tests whether your test EXERCISES
code, not whether it MEASURES anything. Three of the four are POPULATION defects: the data an arm
gathers rather than code it runs, and no def-site mutation can express return a differently
ordered list.
⚑⚑ SO THE RULING IS BOTH, and they are separable work answering different questions.
⟐DECLARE-THE-PARTIAL — first, because it is small and catches three of four. paperkit's own
name for the construction, and they point at it rather than at their framework: the defect is
never emptiness, it is UNDECLARED emptiness. An arm asserts its population is non-empty AND that
its members are what the arm believes, printed. This tree already has the shape in places — the
guard at 01f1450, the shell-consumer assertion — and nowhere as a rule.
⚑ AND SOURCE-SIDE NON-EMPTINESS IS NECESSARY, NOT SUFFICIENT. The fourth shape passes it: every
source was non-empty throughout and the TRAVERSAL was wrong. Catching that needs the output
asserted non-empty given a known-non-empty input, or two instruments compared.
⟐MUTATION-LAYER — its own arc, and the cost is architectural rather than compute. paperkit's three reusable ideas, none of which require bazel:
- the mutant as a CACHED, CLAIM-INDEPENDENT build artifact — N claims × M sites costs M builds
- content-addressed bytecode (PEP 552
UNCHECKED_HASH, no mtime) so a CAS replays it across runs - a fingerprint keyed by MEMBER IDENTITY rather than a kill score
⚑ THEIR MEASURED RETURN IS THE ARGUMENT: on a 223-claim census, 6 claims pass with an empty sensitivity set — they cannot be shown to fail, and every one was green under every other check for as long as it existed. 206 behavioural, 10 refuted, 1 unreachable, 6 vacuous. ⚑⚑ AND THEIR RECORD HAS HALF THE RATCHET I WOULD WANT, which they volunteered: the fingerprint names the KILLED sites, so a site absent from it either survived or was never mutated and the record cannot say which. Absent ≠ surviving. That is the half to build differently here. ⚑ THE PRICE THEY PAID was turning their engine into per-module bytecode targets with a declared import DAG before one mutant could be built. Not to be discovered mid-arc.
⚑ ONE ARM IS WRITTEN, MEASURED, F-ARMED AND WITHHELD pending this layer: a cross-instrument
agreement check comparing an arm's parsed population against count_test_functions.py. Its F-arm
reported parsed 316, counter reported 313 — the fourth shape caught with a NON-EMPTY population
on both sides, which no source-side guard can do. It is withheld only because it pushed the string
sweep's unresolvable ceiling from 22 to 23, and that ratchet may only DECREASE. It returns when an
instrument admits it without a raise.
Written, F-armed both ways, and withheld at the operator's ruling for the same reason as the agreement arm before it: it reads files through a LOOP VARIABLE rather than a module-level target, so the older string-membership sweep cannot resolve it and its unresolvable ceiling would go 22 → 23. That ratchet may only DECREASE. Held pending ⟐MUTATION-LAYER, which is the instrument that admits both without a raise.
⚑⚑ WHAT IT MEASURED, so the arc inherits measurements rather than a description:
- 12 population-shaped negatives in this tree, all named and all guarded. Not a count — the
arm prints its whole swept population on a green run, which is
paperkit-82's second idiom (the count in the description): a reader sees the members whether it reds or not, so a sweep that quietly narrowed is visible before it matters. - F-arm A: a planted
assert not offendersover an unguarded comprehension REDS, names the planted arm, and prints all 13 beside it. - F-arm B: emptying the classifier entirely REDS on the vacuity guard.
- ⚑ F-arm C EXPOSED A REAL LIMIT AND IS RECORDED RATHER THAN GLOSSED. Replacing the
comprehension types with
pyast.Lambdaleft 12 of 13 still admitted through the CALL branch, and the arm PASSED. So the guard catches a classifier that recognises NOTHING and not one that NARROWS: the floor is the shape is still recognised at all, never recognised completely. The stronger check is a second instrument, not a bigger assertion — the fourth vacuity shape arriving inside the sweep built for the third.
⚑ TWO IDIOMS TAKEN FROM paperkit-82, who ran my sweep's earlier shape against their own suites
and returned 1-in-10 precision against my 1-in-4 — WORSE, which is what made the report useful:
- a VERDICT is not a POPULATION. 26 of their 36 candidates were ⟨F⟩ arms —
assert not analyze(cmd)says this input must not fire, and one call's answer cannot be empty. No syntax carries that distinction, so the exclusion list is declared and named rather than inferred. - the guard may sit AFTER the negative. Two of my four original candidates were guarded by arms immediately below them; a position-sensitive check reported its own blind spot as a finding about the suite.
⚑⚑ APPLYING BOTH, THE SWEEP RETURNS 12 population-shaped, 0 undeclared — the one real instance
having been fixed at 01f1450. The refined sweep is in the scratchpad as vacuity2.py; the arm
itself is reconstructable from this section and the two F-arm results above.
CLEARED. The arm is in the suite as
test_every_population_shaped_negative_is_guarded_against_being_empty, warranted, with all three
F-arms re-run against the shipping code.
⚑⚑ THE HOLD HAD A REASON AND A RELEASE CONDITION, AND ONLY THE REASON WENT VOID. Stated above:
withheld because a loop-variable read would push the old sweep's unresolvable ceiling 22 → 23, and
that ratchet may only DECREASE — held pending ⟐MUTATION-LAYER. Measured at 433c1ce: that
sweep was replaced at aab9f8b, population_sweeps is now a declared printed category with no
ceiling, and _MAX_UNRESOLVED has no arm comparing any population against it. So landing it could
ratchet nothing. But ⟐MUTATION-LAYER is still a scratchpad probe, so the release condition was
never met.
⚑ A TRIGGER IS NOT A RELEASE CONDITION, AND THAT DISTINCTION WAS MEASURED ELSEWHERE THE SAME
DAY. cassian-observability-6a read a kubelet's eviction trigger (5%) as its release condition
(15%) and got a wrong diagnosis out of it. A void reason is not a lifted hold — so this was put to
the operator rather than inferred, and the ruling was land it now.
⚑⚑⚑ RE-MEASURING F-ARM C CHANGED WHAT THIS SECTION RECORDS, AND THE CORRECTION IS THE FINDING.
Above, F-arm C is written down as a real limit: narrowing the classifier to pyast.Lambda left
12 of 13 admitted through the CALL branch and the arm PASSED, generalised to a floor catches a
classifier recognising NOTHING, never one that NARROWS. Re-run here, the equivalent plant —
keep the CALL branch, drop the comprehensions — REDS at 2 >= 10.
The generalisation was too strong. This suite's population negatives are overwhelmingly comprehensions, so dropping that branch removes ten of twelve rather than one of thirteen. The floor's reach is a fact about the CORPUS, not about the floor: it catches a narrowing exactly when the narrowed-away shape is most of the population, and a floor of 10 over 12 members tolerates losing two. That bound is real, the arm does not claim past it, and the stronger check is still a second instrument rather than a bigger assertion.
⚑⚑ TWO INSTRUMENTS AGREED ON MEMBERSHIP RATHER THAN ON A COUNT — the discipline three
near-misses this session were about, two of which agreed by luck. scratchpad/vacuity2.py sweeps a
HAND-WRITTEN root list and reports 12; the landed arm GLOBS */tests/test_*.py and reports the
same 12 by name. The arm's own population was read by forcing its floor negative and reading
what it printed, rather than by re-implementing the walk a third time.
⚑ RUFF AND MYPY EACH FOUND A REAL DEFECT IN THE LANDING, BOTH REPAIRED STRUCTURALLY. Complexity
11 > 10 plus two undocumented returns, answered by hoisting _population_negatives and _guarded
to module scope; then disallow_any_expr on isinstance(src, X | Y), whose UnionType expression
types as Any — an untyped expression deciding a classification, which is what payload.py carries
nine warrants about. The tuple form is the same test, fully typed. No waiver, no noqa.
⚑ STILL OWED, AND NOT BLOCKED BY THIS: ⟐MUTATION-LAYER-DURABLE remains a scratchpad probe. It was this hold's release condition and is now simply the next item, carrying its own argument — 136 def-sites, 11 survivors all closed, and paperkit's ATTEMPTED-beside-KILLED correction.
⚑⚑⚑ ⟐MUTATION-PROBE-COUNTS-ERRORS-AS-KILLS — found and repaired at 6143c7d, and it was in the probe this section quotes
The precondition for promoting the probe, discovered by reading the artifact rather than this
record. scratchpad/mutate_probe.py sorted every mutant with
(killed if proc.returncode else survived).append(name), while errored was bound, printed in
the report, and never appended to — three references, no writer.
⚑⚑ A POPULATION NEVER POPULATED — the third of the four shapes tabulated above — inside the
instrument built to find them. A mutant that cannot import returns non-zero and was recorded as
KILLED, crediting the suite with noticing something it never ran. That is paperkit-82's own
correction arriving from the opposite side: their fingerprint names only KILLED sites so absent ≠
surviving; this one folded ERRORED into KILLED, the same ambiguity with the opposite sign, and it
flatters rather than under-reports.
⚑ THE OBVIOUS REPAIR DOES NOT WORK, MEASURED. pytest's exit codes are documented as separating
tests-failed (1) from internal error (3) and no-tests-collected (5). Across four shapes, all three
non-zero cases return rc=1 — a collection error prints 1 error rather than carrying a
distinct code. The discriminator is the terminal summary line: weaker than an exit code, and the
one that exists. Keying on rc would have been a second wrong answer wearing a measurement's
clothes.
⚑⚑ RE-MEASURED WITH THE REPAIRED CLASSIFIER, AND THE FIGURES IN THIS SECTION SURVIVE:
cli.py ATTEMPTED 28 killed 28 survived 0 errored 0 40.4s (1.4s/cell)
core.py ATTEMPTED 8 killed 8 survived 0 errored 0 4.4s (0.5s/cell)
The recorded survivors — mdstruct 10, fence 1 including core.ratchet — were real and are now
closed. No figure this section quotes was inflated by the defect: the conflation was live but
never fired on this corpus. Still a defect, because nothing was keeping it unexercised.
⚑ AND THE ATTEMPTED SET THIS SECTION ASKED FOR IS NOW ASSERTED RATHER THAN CARRIED. The probe
raises unless killed + survived + errored is exactly attempted, naming any unclassified site,
and prints the ERRORED section when empty — the predecessor hid it behind if errored:, so a
reader could not distinguish none occurred from never populated, which is precisely the gap the
defect lived in.
Full account, with the five-shape F-arm and the measurement that refuted the exit-code premise:
findings/mutation-probe-errors-as-kills.md.
⚑⚑⚑ SO ⟐MUTATION-LAYER-DURABLE IS UNBLOCKED AND ITS PRECONDITION IS NAMED: do not promote a probe with a known defect — that is how a defect becomes a component. The architecture argument above stands unchanged (AST rewrite into a temp tree, 45 cells, no caching layer, do not import paperkit's build layer); what changes is that the thing being promoted now distinguishes three outcomes instead of two.
⚑⚑⚑ PROMOTION ATTEMPTED 2026-09-13 AND DELIBERATELY NOT LANDED — operator ruled the SHAPE, and the build found three things
Operator ruling: a gate target, per distribution — sh_test/py_binary per distribution so the
DAG decides when the grid re-runs, unchanged sources meaning a cache hit. That settles the design
question this section carried; what follows is what building it measured.
⚑⚑ THE COST IS NOT THE PROBLEM, AND THAT IS NOW MEASURED PER DISTRIBUTION RATHER THAN ASSERTED:
| distribution | modules | def-sites | per cell |
|---|---|---|---|
| ratchet | 4 | 13 | 0.2s |
| hooks | 10 | 56 | 0.4s |
| fence | 3 | 25 | 0.5s |
| mdstruct | 12 | 64 | 1.4s |
158 def-sites, under four minutes whole-tree. ⚑ AND THE WIP'S RECORDED FIGURE WAS 136 — stale
by 22, quoted across at least six ticks. Nothing was wrong when written; the tree grew and nothing
re-derived it. -x means a KILLED cell stops at the first failing test, so only SURVIVORS pay a
full suite — the opposite of the profile paperkit's caching layer exists for.
⚑ AND THE RUNNER IS NOT LANDED, DELIBERATELY. It reports all 64 of mdstruct's def-sites as
ERRORED, which is a whole-distribution failure and therefore a defect in the runner rather than a
finding about the suite. ⚑⚑ run() and verdict() classify the same mutant as KILLED when
called directly — traced, with the mutant's AssertionError in the output — so the defect is
somewhere in main()'s loop and is NOT yet diagnosed. Landing it would be exactly what the
preceding section forbids: do not promote a probe with a known defect. The work-in-progress is at
scratchpad/mutate_runner.wip.py.
⚑⚑⚑ MY FIRST DIAGNOSIS OF THAT FAILURE WAS WRONG, AND I REASONED IT RATHER THAN MEASURING IT.
I ran which pandoc, found ~/bin/pandoc, concluded the runner's pinned PATH=/usr/bin:/bin was
starving mdstruct's suite, and changed the code. Re-run: identical 64 ERRORED. A repair to
something that was not the defect. ⚑ The reason it was believable is that my standalone
reproduction worked — because it RECONSTRUCTED run() by hand instead of calling it, and so
differed from the real path in ways invisible to me. A reproduction that is not the code under
test is a second instrument, which is this session's most-repeated defect arriving in the probe
built to diagnose a defect.
Not a whole-distribution failure. ONE TEST MODULE fails collection, and -x aborts the rest.
The framing "all 64 def-sites ERRORED" was true as a count and wrong as a description, and the
wrong description is what sent four diagnoses into the environment.
⚑⚑ THE MECHANISM, MEASURED: mdstruct/tests/test_cli_flags.py imports the CLI module at
module scope (line 66, _CLI_SOURCE = Path(str(_cli_module.__file__))), and the CLI imports
panflute. In the mutant's temp tree that import resolves against the copied src and fails with
ModuleNotFoundError: No module named 'panflute' — at COLLECTION, before any mutated code runs.
So every cell reports ERRORED for a reason that has nothing to do with its mutation.
⚑ AND THE ASYMMETRY EXPLAINS WHY IT LOOKED LIKE A WORKING RUNNER. Measured:
mdstruct declares dependencies = ["panflute"]; ratchet and fence declare []. The two
distributions the runner was validated on are exactly the two that cannot exercise the defect —
a probe validated on the corpora that could not refute it. hooks would fail the same way.
⚑⚑⚑ FIVE HYPOTHESES REFUTED, THE REPAIR STILL NOT WRITTEN, AND THE PROBE HAD TO BE FIXED THREE TIMES
Tick 11 spent on the repair and produced NEGATIVES, not a fix. Filed because a set of eliminated causes is a real result and because the next tick must not re-walk them.
⚑⚑ WHAT IS NOW ELIMINATED BY MEASUREMENT, each against the mutated tree:
| candidate | measured |
|---|---|
| the temp tree itself | 151 passed unmutated — the copy is sound |
| the runner's env (PYTHONPATH + HOME) | mutated control 1 failed, 73 passed — a KILL, correctly |
| (3) PYTHONPATH + venv site-packages | same KILL — no improvement, nothing to fix |
| (2) symlink site-packages beside temp/src | same KILL |
(1) pip install -e the temp tree |
same KILL |
empty __init__.py deletion |
no empty markers exist in the distribution — a no-op |
inherited PYTHONPATH from the parent |
unset — the tick-10 append branch is inert |
| temp-disk exhaustion | 64 GiB free; each copy is 8.4 MiB and is freed per cell |
So every named difference between the probe and the runner is eliminated, and the probe
reproduces a correct KILL where the runner reports ModuleNotFoundError. The mechanism is not
established. My model of the runner is wrong somewhere I cannot yet name.
⚑⚑⚑ AND THE PROBE ITSELF WAS WRONG THREE TIMES, EACH CAUGHT BY ITS OWN CONTROL. This is the part worth keeping:
--collect-only— the control COLLECTED cleanly, so all four rows read identically and the probe discriminated nothing. Its own docstring says the control must fail or this measures an unbroken tree, and it measured an unbroken tree on the first run.- No mutation — the control PASSED 15 of 15, because the runner REWRITES a source through
ast.unparsebefore running and the probe did not. - One module instead of
tests— still passed. Naming the directory is what the runner does, and only then did the control finally FAIL.
⚑ THREE DRAFTS, EACH DIFFERING FROM THE CODE UNDER TEST IN ONE ARGUMENT, AND EACH PASSING BECAUSE OF IT. That is the second-instrument defect four deep in one session — and the only reason it was caught each time is that the probe carried an explicit control with a stated required outcome. A probe without a must-fail control cannot tell you it is measuring nothing.
⚑⚑ LOAD-DEPENDENCE: TESTED AND REFUTED. The host is under real pressure (MemAvailable 2.05 GiB, psi memory avg10=6.18, zram holding 4.89 GiB in 1.16 GiB), and the probe runs 4 cells
where the runner runs 64 — so accumulation was the one variable the probe could not hold constant.
Measured: the FIRST cell fails, at 0.05s. Not accumulation, not exhaustion.
⚑⚑⚑ AND 0.05s IS ITSELF THE SHARPEST REMAINING CLUE, RECORDED RATHER THAN CHASED. The probe's
equivalent run takes 2.5–3s and reaches a real KILL. A failure returning in 0.05 seconds has not
imported anything — it is too fast for the interpreter to have reached panflute at all. The
error path also changed with the tick-10 rootdir fix, from ../../../github/mtools/mdstruct/ test_cli_flags.py to tests/test_cli_flags.py, so the config now resolves inside the temp tree
and the failure moved with it rather than disappearing.
⚑ WHAT IS STILL NOT EXPLAINED: the probe and the runner now agree on the copy (identical
ignore_patterns), the config path, the environment, the mutation, and the target (tests) — and
one takes 3s to a KILL while the other takes 0.05s to a ModuleNotFoundError. Something differs
that I have not named, and naming it is the next tick's first job. The candidate worth testing
first is whatever makes a 0.05s failure possible: an import that fails before sys.path is
consulted at all.
⚑ THE RUNNER STAYS UNLANDED. Five refuted hypotheses do not make a repair, and the rule that kept it out of the tree two ticks running is the same one: do not promote a probe with a known defect.
⚑⚑⚑ THE SANDBOX QUESTION IS MEASURED AND THE ANSWER IS YES — and the per-cell cost figure turned out to be load-dependent
Tick 13. The runner is landed at 35931c3; this measures what a gate target needs before any
BUILD file is written. Nothing was typed into a build file this tick: the open question was
whether the runner's central assumption — shelling to a venv interpreter by path — survives a
bazel sandbox at all, and that is now answered.
⚑⚑ ONLY A VENV INTERPRETER WORKS, WITH A CONTROL THAT FAILS. Measured against the runner's own
run() on mdstruct (the distribution that declares panflute, so the one that can refute):
| interpreter | verdict |
|---|---|
the distribution's .venv/bin/python |
killed |
| the bare mise interpreter the venv symlinks to | errored |
sys.executable of the calling process |
errored |
So the dependency on the venv is real and structural, not incidental — which is the same fact the
.resolve() defect was made of, now stated as a requirement rather than discovered as a bug.
⚑⚑⚑ AND THE TREE ALREADY BUILDS A VENV AS A TARGET: venv_from_hub (⟐VENV-AS-BUILD-ARTIFACT),
called once per distribution. Its interpreter symlink is deliberately RELATIVE — its own comment
records measuring both arms, because an artifact that is only valid at the path it was built at is
host state with a build step in front of it. That is precisely the property a sandboxed gate
needs, and it was built for a different reason two arcs ago.
MEASURED END TO END: bazel build //mdstruct:.venv produces bazel-bin/mdstruct/.venv/bin/ python3, and handing THAT to the runner kills the mutant — the same verdict as the host venv.
The gate target therefore stages //<dist>:.venv as data and passes its interpreter. No new
mechanism is needed.
The per-cell figures recorded above — ratchet 0.2s · hooks 0.4s · fence 0.5s · mdstruct 1.4s,
and the under four minutes whole-tree conclusion drawn from them — were taken on a quiet machine.
Re-measured this tick at load average 48.65:
host venv 23.6s for ONE mdstruct cell (recorded: 1.4s)
bazel-built venv 27.9s for the same cell (~18% slower than host, which is the real comparison)
⚑⚑ SO THE HONEST STATEMENT IS A RANGE WITH ITS CONDITION ATTACHED, NOT A NUMBER. At 1.4s/cell mdstruct's 64 sites are 90 seconds; at 23.6s they are 25 minutes. The first whole-grid run this tick timed out at 300s and that timeout was the machine, not a hang — established by running one cell under both interpreters with the host venv as a control that had to finish.
⚑ THE BAZEL-VS-HOST DELTA IS THE FIGURE THAT SURVIVES, because both arms were measured in the same minute under the same load: ~18%. A ratio between two things measured together is robust to a condition that moves them both; an absolute second-count is not. The recorded absolutes should be read as taken quiet, and the gate's cost argument re-measured on the machine that will run it rather than inherited from this section.
Asked with the 17x spread stated: wire it as ruled — the DAG absorbs it. The reasoning recorded with the ruling: the per-commit cost is ~0 for untouched distributions, one grid for a changed one, and the 25-minute figure is a cold worst case on a loaded machine rather than a tax.
mutate_check.sh (shellcheck clean, runs correctly outside bazel: ratchet 13 = 11 killed + 2
unreachable) and a //ratchet:mutants sh_test. ⚑ THE SCRIPT DELIBERATELY DOES NOT realpath
THE INTERPRETER, where mypy_check.sh does — mypy's runner is a staged py_binary, but
dereferencing a venv symlink is the defect that cost three ticks.
⚑⚑ BAZEL REFUSED $(location :.venv) AND THE REFUSAL WAS CORRECT: that target expands to
2,225 files, so no single-file expression can name bin/python3. Repaired by deriving the
interpreter from dirname of the config, which is the distribution root under any staging prefix.
⚑⚑⚑ THEN THE TARGET FAILED IN 0.6s, AND THE SCRIPT'S OWN REFUSAL IS WHAT CAUGHT IT:
mutate_check: ratchet/.venv/bin/python3 was not staged — refusing rather than running a grid
under whatever interpreter happens to be on PATH
MEASURED: the runfiles tree holds mutants, pyproject.toml, src, tests — and no .venv,
despite :.venv being named in data. Why a venv_from_hub target listed as data does not
stage is UNMEASURED and is the next tick's first job.
⚑ AND THE 0.6s IS THE DURATION-AS-EVIDENCE LESSON PAYING OFF IMMEDIATELY. A grid cannot run in 0.6s; the clock said the interpreter precondition fired before the log was read. The same reading took three ticks to arrive at last time, on a 0.04s failure whose message named a missing package.
⚑⚑ THE PRECONDITION EARNED ITSELF ON ITS FIRST REAL USE. Without it the grid would have run
under whatever python3 the sandbox provides — reporting every site ERRORED, which reads as the
suite did not run rather than the harness is misconfigured. That is exactly the three-tick
ambiguity, and the refusal converted it into one line.
⚑ NOTHING FROM THIS TICK LANDS IN THE TREE, AND THE SCRIPT IS HELD RATHER THAN COMMITTED. The
BUILD changes are reverted because the target is RED. mutate_check.sh is correct on its own
(shellcheck clean, runs the grid outside bazel) but no target calls it — committing it would
be the console script nothing consumed shape this tree measured once already, where
mikemol-hook-structural-query was a distribution's only entry point and nothing used it. It sits
at scratchpad/mutate_check.wip.sh until the staging question is answered and the target is green.
| # | hypothesis | how it died |
|---|---|---|
| 1 | stripped PATH=/usr/bin:/bin starves the suite of pandoc |
changed to inherit os.environ; re-ran byte-identical |
| 2 | PYTHONPATH replaced resolution instead of prepending |
changed to prepend; re-ran byte-identical |
| 3 | the editable install's .pth finder |
refuted by probe before editing |
| 4 | pytest rootdir set to the real tree by -c <real>/pyproject.toml |
changed to the temp copy; re-ran byte-identical |
⚑⚑ THE PATTERN IS THE FINDING, NOT ANY ONE MISS. Three of the four were edited into the code before the mechanism was tested, and each re-ran identically — which is the cheapest possible refutation and arrived only after the edit. The fourth was refuted by a one-variable-at-a-time probe in under a minute, because that probe tested the mechanism instead of assuming it.
⚑ WHAT FINALLY WORKED WAS MEASURING A GAP RATHER THAN PROPOSING A CAUSE. Two facts were
already established — import panflute succeeds under the runner's exact environment, and pytest
fails in the same tree — so the question became what differs between them, answered by printing
sys.path from inside both. Both resolved panflute; a synthetic pytest module importing it
passed. That eliminated the environment entirely and pointed at the one module that does the
import at collection time.
⚑⚑ AND THE DEBUG MODE IS WHAT MADE ANY OF IT VISIBLE. mutant_in_stdout=False on every cell
said the mutant was never reached — the single most informative bit, and it was one print
statement inside the runner's own run(). The previous tick's standalone reproduction could not
have shown it, because it was not the code under test.
The repair is not written. Candidates, none measured yet:
- Install the distribution into the temp tree, so its dependencies resolve the way they do in the real one. Correct and slow — it pays a pip install per cell against a 1.4s cell.
- Symlink or copy the venv's
site-packagesinto the temp tree. Cheaper, and it makes the mutant's environment differ from the real one in a way that needs its own argument. - Point
PYTHONPATHat the tempsrcAND the real venv's site-packages explicitly, rather than relying on the editable finder that resolves to the real tree.
⚑ (3) looks right and that is exactly what the four dead hypotheses each looked like. It is not taken this tick, and the runner stays unlanded.
⚑⚑ 1. THE ERRORED CATEGORY FIRED ON ITS FIRST REAL RUN AND FOUND A STRUCTURAL LIMIT.
BaselineState.__init__ and __str__ in ratchet/state.py error rather than kill: BaselineState
subclasses enum.Enum, so its members are constructed when the class body executes — mutating
them raises at import and no test ever collects. That is a limit of body -> raise at def-site
granularity, not a gap in the suite, and the two are easy to confuse because both read as the
suite did not notice. Declared as a fourth category, UNREACHABLE, derived structurally (a
method of an Enum subclass) rather than by a name list — matching __init__ by name would
exclude every ordinary constructor, over-declaring the limit to cover two cases. Measured: 158
def-sites tree-wide, exactly 2 in this class, both the ones ERRORED named.
⚑⚑⚑ 2. THE FIRST ERRORED PREDICATE WAS TOO BROAD AND ITS OWN CATEGORY CAUGHT IT. It read any
collection error as ERRORED, on a four-shape measurement where every error was an import failure.
Run against fence, three ordinary functions errored — and the traceback showed the suite calling
them at collection time, through a module-level _WHY = _unfenceable() guard that decides
whether to skip. The suite's own code ran and REACHED the mutant. So the distinction is not did
collection finish but was the mutant reached, which is the question the grid asks. With that
corrected, fence goes 22-killed-3-errored → 25 of 25 killed. ⚑ And the exit code there was
rc=2, a shape the original four-case measurement never produced — a predicate is only as wide as
the corpus it was measured on.
⚑ 3. THE ACCOUNTING ASSERTION IS WHAT MAKES THE CATEGORIES HONEST. attempted is carried and
the runner refuses unless killed + survived + errored + unreachable equals it exactly. An
UNREACHABLE site stays in attempted rather than being dropped — dropping it would balance the
books by shrinking the denominator, which is the flattering direction and the one the predecessor
took with ERRORED.
The operator asked the right question about the string sweep: a stale literal is something that needs to be kept in sync, so the solution is ensuring that sync. What makes it stale?
⚑⚑⚑ MEASURED, AND THE PREMISE PARTLY FAILS. The originating literal was wrong at authoring, not
stale. The arm's own record says it checked "a pattern whose escaping did not match
blockers.sh" — the escaping never matched, so there was no sync to break and nothing had
drifted. The sweep's framing as a VACUITY check was a misdiagnosis carried in its name.
⚑⚑ BUT DRIFT IS ALSO REAL, so both failure modes exist. 223 string-membership assertions in one
module name strings in files that have been edited 56 (blockers.sh) and 49 (the gate)
times. A literal written against either has had many opportunities to decouple.
⚑ SO WHAT MAKES A LITERAL STALE IS THAT A TEST NAMES A STRING IN A FILE IT DOES NOT OWN, AND NOTHING COUPLES THE TWO. The sweep detects the decoupling after the fact; it does not create a coupling. The operator's proposed repair — ensure the sync — is the right shape, and the question is whether a sync is available.
⚑⚑⚑ IT IS AVAILABLE FOR 7 OF 223, WHICH IS THE MEASUREMENT THAT DECIDES THE ITEM:
| class | count | can a coupling be built |
|---|---|---|
| the literal IS a declared name | 7 | yes — derive it from the target |
| quoted code MENTIONING a name | 31 | no — deriving a whole shell line from a variable is not a coupling |
| prose | 185 | no — a sentence has nothing to bind to |
⚑⚑ AND THE LOOSE MEASUREMENT SAID 38 BEFORE THE STRICT ONE SAID 7. The first pass counted a
literal bindable if it CONTAINED a declared name, which admitted fragments like
'note_failure "$dist: ratchet' — a quoted line that merely mentions note_failure. Reading the
members rather than the count is what showed it; the number 38 looked like a fifth of the corpus
and was mostly one defect in the classifier.
⚑ THE CONCLUSION: building the coupling is not the repair. It would fix 3% and leave the sweep standing for the other 97%, so the sweep is a DETECTOR by nature rather than by omission — an instrument for a class where prevention is unavailable, which is a legitimate kind of tool and a different kind from a ratchet. Its ceiling counts arms it could not resolve; an arm that sweeps a DIRECTORY is structurally outside its reach and counting it as debt mis-states what the number means. That is the collision holding two built arms.
_MAX_UNRESOLVED = 22 no longer has any arm asserting against it as a bound. Measured by
enumerating every reference in the tree rather than by reading the constant's comment:
| reference | what it asserts |
|---|---|
_MAX_UNRESOLVED_WAS = 23 |
the former value, for the direction check |
assert _MAX_UNRESOLVED < _MAX_UNRESOLVED_WAS |
that it FELL — not that anything is under it |
assert "_MAX_UNRESOLVED" in commands |
that the POLL mentions it |
for typed in (..., "_MAX_UNRESOLVED") |
that it is TYPED |
⚑⚑⚑ NOT ONE OF THEM COMPARES A MEASURED POPULATION TO IT. The sweep that did —
the string-membership vacuity check — was replaced at aab9f8b by a target-resolution check
whose unresolvable arms are a declared, printed category with no ceiling at all. So the
constant survives as a number that four arms assert facts about, and none assert with.
⚑⚑ THAT IS FURNITURE WITH A NUMBER ATTACHED, WHICH IS WHAT ITS OWN CARRIER WAS NAMED TO
PREVENT. test_the_sweeps_ceiling_falls_rather_than_standing opens "A CEILING OVER A STATIC
POPULATION IS FURNITURE WITH A NUMBER ATTACHED" and cites linux-sources' measurement that a
probe printing SIX gets read past for six consecutive ticks — the operative property being
CONSTANT rather than zero. The arm now carries exactly the object its docstring refuses.
⚑ AND IT IS NOT THE SAME DEFECT AS ⟐ORPHANED-EVALUATOR, WHICH IS WHY IT IS FILED SEPARATELY.
That one was a builder never shipped and is repaired at cebbe58. This one is a live constant
whose consumers all went away — the population it bounded is no longer computed by anyone. Two
different vacuity shapes arriving from one commit.
The work, and the choice is a measurement rather than a preference:
- Retire the constant and the direction arm together, if nothing wants a bound on
unresolvable arms now that
unresolvedis an outright refusal. ⚑ Honest only if the replacement sweep's refusal is genuinely stricter — it assertsnot unresolved, so it is, and a ceiling above zero would be a RELAXATION of a check that currently admits none. - Re-point it at the new sweep's
population_sweeps, if a bound on declared sweeps is wanted. ⚑ Refused as stated: that category was made declarative precisely because a ratchet over arms the instrument structurally cannot reach measures the instrument, not the debt.
⚑ (1) LOOKS RIGHT AND MUST STILL BE MEASURED, because the four recorded rises (18→19→20→21→22)
each documented a real arm. Deleting the constant deletes that record. The reading to take
first: does any arm today resolve to nothing and pass? If assert not unresolved holds on a
green run, the ceiling is bounding an empty set and (1) follows; if it does not, the sweep is
already failing and this is the wrong question.
⚑⚑ AND THE GUARD SHAPE IS THE TRANSFERABLE HALF. assert "_MAX_UNRESOLVED" in commands is
the same substring-satisfied-by-its-own-definition shape that hid ⟐ORPHANED-EVALUATOR for a
commit — it is satisfied by the poll mentioning the name, never by the poll using the value.
Two instances of one defect in one module, found in one tick, and the second is still live.
Two parts CLEARED, the third DELIBERATELY NOT TAKEN. The filing named one defect; measuring it found three, which is the case for re-deriving a list against the tree rather than against its own record. The tree knew more than this section did.
⚑⚑⚑ PART 1, CLEARED — a live BLOCK-WITHOUT-A-ROUTE in this repository's own advice line.
blockers.sh printed pytest tests/test_bar_fires.py -k vacuous to any reader wanting the real
count. Measured: no tests collected (144 deselected). aab9f8b renamed the arm two commits
earlier. The poll offered a route the reader could not take — the defect the routing table refuses
to commit one layer up, arriving in this tree's own poll, because a rename orphans every pointer
and a -k selector fails SILENTLY.
⚑⚑ PART 2, CLEARED — a structural concession that outlived its cause. An arm read its files
through hand-typed constants, explaining itself as a CONCESSION TO A SIBLING ARM ... the first
draft pushed that sweep's unresolved ceiling 22 -> 23. Correct when written; void since aab9f8b
replaced that sibling and made loop-variable reads a declared category. The mechanism was retired
and the thing that bent around it stayed bent. Its comment also claimed a derivation via
_SHELL_CONSUMERS that never existed in the module.
⚑ AND THE DERIVATION FOUND NINE WHERE THE TYPED LIST HELD THREE — the repair's author predicted
four. Members, read by forcing the floor above the population: blockers.sh, commit-msg,
domain_witness.sh, message_counts.sh, pre-commit, preflight.sh, refusal_record.sh,
rule_citations.sh, shellcheck_test.sh. The arm passes over all nine, so the six newly-admitted
scripts hold no violations — a result rather than an assumption.
⚑⚑ PART 3, CLEARED — the substring guard replaced by a resolution check.
assert "_MAX_UNRESOLVED" in commands passed if the poll NAMED the constant, never if it USED the
value, and it was the third time that block had been keyed to a spelling (its own comment
records the previous two). The arm now parses the module and asserts every -k selector the poll
prints matches a real test name. F-armed: restoring -k vacuous REDS naming ['vacuous'].
⚑⚑⚑ WHAT IS NOT DONE, AND WHY IT IS NOT A LEFTOVER: _MAX_UNRESOLVED = 22 STILL EXISTS.
Re-measured after the repair, its surviving references are exactly two live assertions — that it
fell from 23, and that it is typed — plus one that is now DORMANT BY DESIGN RATHER THAN
DEAD: if "_MAX_UNRESOLVED" in commands: assert "CEILING" in commands. That one fires only when
the poll greps the constant, which it no longer does, and its own comment says why it is built that
way: a required presence cannot be emptied by deleting prose — the difference between an arm that
survives its own repair and one that only survived until it worked. Nothing asserts WITH the
value. The two closures this section proposed are both still open, and the reason for not taking
either is now clearer than when they were written:
- Retiring it deletes the record of four documented rises (18→19→20→21→22), each a real arm. That record is the only place the reasons live — each rise names the arm and the runtime predicate that made it unresolvable. Deleting the constant without rehoming that prose loses measurements, which this tree treats as a worse outcome than carrying a dead number.
- The ceiling-falls arm asserts a DIRECTION, and a direction over a retired constant is not obviously meaningless — it still refuses a flattering edit to a number, which is a property with no current subject rather than a property that is false.
⚑ So the honest state is: the LIVE defects are repaired and the DEAD constant is carried, with its record intact and its uselessness written down. That is a different thing from unfinished work, and the distinction is the point — a number nobody can raise, whose removal would cost measurements, is furniture that is cheaper to label than to move.
⚑⚑⚑ EVERY DEFECT IN THE FENCE EXCHANGE HAD ONE SHAPE: a plausible reading pointing at the WRONG SUBJECT. Never a failure — always something that looked fine.
| collapse | what stood for what |
|---|---|
local |
host conflated with executor — 8 passed about the wrong machine |
skipif(reason=) |
a hardcoded guess conflated with the measured cause |
ro |
one word for bind, superblock, and mount ROOT |
| (the fourth) | a REFUTATION standing in for a REQUIREMENT — every failure measured, never the success |
BOUND BY MEMORY |
throttled conflated with killed, while --swap's own help drew the distinction |
manual / external |
scope management and cache management standing in for soundness |
⚑ The fence's requirement, measured and now stated: A PROCESS-FREE PARENT CGROUP with memory+pids delegated. It cannot run in a cgroup namespace whose root holds processes — sibling is forced by the namespace, child is forced out by the no-internal-process rule (EBUSY, measured), and a writable bind would only move the refusal.