Imported from aDNA-Network/Canvas.aDNA (
how/gates/AGENTS.md). Install upstream withnpx skills add aDNA-Network/Canvas.aDNA --skill gates. Copyright stays with the author.
how/gates/ — The gate set, executable
⚠ Two different things live here, and only one of them is "the gate set"
Added 2026-09-16, when Canvas adopted ISS and the collision became real rather than hypothetical.
| What | Files | |
|---|---|---|
| Test gates | gate_manifest.py is the gate set — eleven gates (ten until 2026-10-13), fail-on-omission. Everything else in this document is about these. |
gate_manifest.py · registry_census.py · this file |
| Operator decision gates | ISS surfaces where a human rules — ADR ratification, phase exit, an open queue. Generated, never hand-authored. | <gate_id>.data.json · .html · .pending · .output.json |
⛔ There is no functional conflict, and that was checked rather than assumed. gate_manifest.py's
discovery walks only what/production/ and what/code/ for test-bearing directories, so a .html
here is invisible to it and cannot affect a gate line. The collision is one of vocabulary only.
Why they share a directory anyway: how/gates/<gate_id>.html is the fleet-wide ISS convention —
the skill's own output_destination spec assumes it, and every one of the other wrappers uses it. A
Canvas-only path would buy a tidier directory at the cost of being the one vault where the convention
does not hold. Named here instead, per this file's own §"What discovery cannot see" precedent:
state the blind spot rather than coding around it.
Wrapper + render discipline: how/federation/iss/CLAUDE.md.
Purpose
gate_manifest.py is the gate set. Not a description of it — the thing itself.
Run it before any phase close, campaign close, or push batch:
python3 how/gates/gate_manifest.py --markdown # run everything, emit the STATE.md line
python3 how/gates/gate_manifest.py --discover-only # omission check only; runs no tests (~1s)
python3 how/gates/gate_manifest.py --check-doors # every pending ISS door → the live receiver? (~1s)
The standing rule
A close pastes this script's generated output. It never retypes the previous close's sentence.
That rule is the whole point, and it is worth knowing why it exists.
Why this directory exists (F-P5-3)
The gate set used to be a sentence in STATE.md that a human retyped each close. canvas_context
was in that sentence at Armature, silently fell out afterwards, and then sat red for two days
across three consecutive phase closes — P2b, P3 and P4 — each of which published an all-green gate
line. None of them was lying. No commit removed the suite; each close simply copied the previous
close's list.
A skipped test prints
s. A suite nobody invoked prints nothing at all, and the gate line beside it reads exactly as green.
This is the F-P2-11 family (a check that cannot run is not a check that passes) with a nastier variant: a check that is not run leaves no trace in any output at all.
The three registries
| Registry | Job |
|---|---|
GATES |
Every suite by name, with the counts it must produce. Producers are enumerated by name, never globbed. |
EXCLUSIONS |
Test-bearing directories that are deliberately not gates. Every entry must carry a non-empty reason, and the path must exist. |
| discovery | Walks the vault for test-bearing directories and fails if any is in neither registry. |
Discovery is the load-bearing one. Without it this file would be a prose list written in Python,
carrying the exact failure mode it replaces. EXCLUSIONS exists because a check that goes red for
good reasons gets disabled — the escape hatch has to exist, and it has to justify itself in the
same file as the rule.
Exit codes are distinct on purpose
0 green · 1 a suite failed · 2 counts disagree · 3 an undeclared surface exists ·
4 a runner-environment precondition failed.
Disagreement and omission are different bugs and must never share an exit code. A precondition
fault (the adna-canvas-std editable install drifting out of the runner env, which happened once at
Halftone) presents as a mass test failure unless it is checked first and reported separately.
⚠ The rule that matters most
A disagreement is a finding to investigate, not a number to edit into the manifest.
Editing the expectation to match the observation converts this file back into the thing it replaced, and it is precisely the defect family Operation Blueprint found nine times — a stated fact nobody re-derived. If a count moves, find out why it moved before you touch this file.
Traps encoded here, each hit live at the Blueprint P5 close
- Absolute paths throughout; the script never
chdir()s. Acdinto a package persists across shell invocations, so a latergit diff --stat -- what/code/canvas_std/from the wrong directory returns empty — indistinguishable fromfirewall diff 0. Regression-tested from a deep subdirectory. - The seventh producer package is
brief_consumer, notbrief_generator. A*_generatorglob silently returns 257 and looks right. - Preconditions are checked before anything runs, because a wrong environment makes every number below it meaningless.
Added 2026-09-11 (Plumbline P3) — the shim trap, contributed from outside
-
Discovery uses
find -Psemantics: it does not follow symlinks, and it reports every one it skips. Rosetta (aDNA.aDNA) measured the cost of the other choice: a census run asls */what/lattices/examples/*.canvasreads 254 files across 62 vaults where the physical truth is 282 across 69 — 14 root-level back-compat shims, followed by the glob, so live vaults count twice under two names and archived vaults reappear inside the live set.A shim is a second true name for one object, and a glob cannot tell a name from a thing.
⚠ This vault already instantiates it at its own root.
./gitand./iiiare symlinks intohow/federation/, sols */CLAUDE.mdreports two federation wrappers where there are three — naming both of the two by their second name, and missingcomfyui/, which has no shim at all.The omission verdict was already safe (both sides
.resolve()). What was not safe was the population line, which counted names rather than things — in the file whose entire thesis is enumerate the territory, don't re-read the map. Verified by derivation, not by reading the message: with a shim planted underwhat/production/, the old code printed15 test-bearing surfaces … (13 gated)with a correct verdict and exit 0; the new code prints14 … (physical) … 1 symlink shim(s) skippedand names it. -
No declared gate may resolve outside the vault (exit 4, precondition — not exit 3).
run_gate()setscwd=to the gate path, so a gate pointed through a shim into a peer vault would run their suite and write.pytest_cache/into their tree, against this vault's read-only conduct with peers. The realistic path to that is an agent adding a shim toGATESto clear an exit-3 omission — by accident, not malice.
⛔ What discovery cannot see — its membership rule, stated so the blind spot is named
Discovery's rule is: a directory named tests, at exactly depth 1 under what/production or
what/code, containing at least one test_*.py (plus a bare tests/ directly under either
parent).
Three things are therefore invisible by construction:
- tests living anywhere outside those two parents (e.g. under
how/); - a directory spelled
test/rather thantests/; test_*.pysitting at a package root rather than inside atests/directory.
Measured 2026-09-11: zero such files exist. The exposure is real and currently costs nothing.
⇒ A registry defines its own blind spot in its membership rule — the P3 federation_index
finding, reappearing inside the file written to fix registries. It is named here rather than coded
around: widening the walk costs runtime at every close and buys nothing measurable today, and what
made federation_index wrong for five weeks was not a narrow rule but an unstated one.
What it found on its first run (2026-09-10)
what/production/canvas_presentation/ — 57 passed / 2 skipped, real library code, never present in
any published STATE.md gate line. Unlike canvas_context it had not fallen out; it had never
been enumerated. It was green, so nothing was broken — but it had been ungated for its whole life,
and no amount of careful re-reading would have surfaced it. It is now gate #8.
What it found on its second outing (2026-09-11, Plumbline P1) — gate #9
dual_channel_freshness. Both of this vault's dual-channel canvases — the pair
pattern_diagrammatic_context cites by name as its motivating example — were stale, and had
been since 2026-09-07 when the P2c re-gate changed the layout engine. They stayed stale through
P2c, P3, P4, P5 and the Blueprint close: five all-green gate lines. Rebuilding from their own
unmodified sources differed by 36 and 32 lines.
The pattern's central law is "drift between channels is a defect, not a chore." Nothing enforced it: the visual gate checks a canvas as it stands, and no check compared a generated artifact to a regeneration of it.
⇒ A generated artifact that nobody regenerates is a claim nobody re-derived.
Sibling of F-P5-3 (a suite nobody invoked) and F-GM-1 (a suite never enrolled) — a third
variant of the same family, one layer out from tests. The gate rebuilds every *.diagram.yaml into a
temp dir and compares parsed documents (key order and whitespace are not the claim). It never
writes into the vault.
Gate #10 — registry_census, added 2026-09-13 (Operation Datum P2)
The first gate here that checks a source tree's own vocabulary rather than a suite or an artifact.
registry_census.py enumerates every hand-maintained vocabulary constant in canvas_std by walking
the package AST, traverses the JSON Schema for every enum, parses spec §7.2, and pairs Python
constants to schema enums by content — never by name. A name map would itself be a
hand-maintained registry with no consumer, i.e. the defect it measures.
Why it exists (F-GL-1). RESERVED_KEYS — the tuple naming the _reserved namespace — was read
by nothing. So when Standard v2.4.0 appended two names to it per the ratified LIP-0010, the append
was correct and inert; and the proof that an inert list rots was already inside it: interaction,
shipped and validated since v2.2.0, was missing from all three hand-maintained copies of that
namespace for three months. No canvas was ever wrong — the namespace description was.
⇒ A specification with no consumer is indistinguishable from no specification.
What it found, in order
-
P1 — three defects inside the tool, before any in its subject. The dispatch detector matched
ast.Inbut notast.NotIn, so it read half ofif "authority" in reserved and "production" not in reserved(F-DT-1); content-pairing reported coincidence as drift three times out of three, because natural vocabularies reuse words likenoneandtext(F-DT-2); and two constants hold an identical vocabulary with nothing linking them (F-DT-3). ⛩ A second blind spot cannot be closed:_validate_axessubscriptsreserved[key]with a variable, so those sites are counted and published asdynamic— the key list is a floor, not a census, and says so on its face. -
F-DT-5 — a gate pin that was a claim about the operator's desktop.
canvas_corewas pinned(1040, 3), which holds only while Obsidian is open on this vault. Closed, the same unchanged tree measures(1039, 4). ⛔ Not fixed by re-pinning to the other desktop state:Gate.env_skipsnow declares how many skips may vary with the environment, while the total must still match exactly. Defaults to0, so every other gate stays exactly as strict — a test silently becoming a skip is still a regression. -
F-DT-6 — this gate named the wrong fault class within the hour of being added. It reported "the
_reservednamespace copies disagree" unconditionally, so a drifted schema twin — the namespace untouched and agreeing 11/11 — sent the reader to the wrong three files beside a green-looking11/11. The causes are now derived and named. At P3 the derivation moved into the census and this gate consumes it: a second derivation of a cause is a second thing that can disagree with the first. ⇒ the report is part of the check. -
F-DT-7 — the finding the whole campaign was hiding behind. Content-pairing is coverage that evaporates exactly when it is needed: when a vocabulary and its schema twin diverge past the similarity floor the pair does not report drift, it dissolves, and the constant reclassifies to
VALIDATOR-ONLY— an accepted state. Gutting the schema'sfromSide/toSideenums from four values to one left all ten gates green, whilejsonschemacorrectly rejected an ordinary canvas. Worse than silence: the census printed "unrelated vocabularies reusing a generic token" about the Standard's own edge-side enum.⇒ A registry that LOOKS watched is better hidden than one that visibly is not.
Closed at P3 by making every constant declare
SCHEMA-TWINorVALIDATOR-ONLYon the line, as a falsifiable claim checked against the derivation — not a label, and not a second list.
⚠ This gate's own membership rule, stated so the blind spot is named. A "vocabulary constant" is
module-level, UPPER_SNAKE, and holds a literal collection of scalars. A vocabulary built at runtime,
or held in a dict, is invisible to it — by construction, as federation_index was.
Gate #11 — inbox_receipt, and the --check-doors flag (2026-10-13, Operation Vernier P6)
F-V-1. who/coordination/inbox/README.md says the recipient's commit is the read-receipt, so an
untracked file there is an unread memo by the drop-box's own rule. At the 2026-10-12 cold start,
seventeen sat untracked, the oldest three weeks old and six of them ack_required, while ten gates
printed green. Nothing read the drop-box, so nothing could see it fill. ⇒ an inbox nobody commits
from is a registry with no consumer. The gate fails when any untracked inbox file is older than
7 days. Age is counted from delivery: the memo's delivered_on where it parses, otherwise its
mtime, because a cp -p copy keeps the sender's mtime and would read a memo delivered this morning
as weeks overdue. Verified both ways with planted probes: no frontmatter and an old mtime → FAIL;
an old delivered_on and a fresh mtime → FAIL; a fresh delivered_on and a six-week mtime → OK.
The full run exits 1, and the pasted line reads ⛔ FAIL.
⚠ Self-caught, recorded here rather than quietly fixed. The first reporter printed the raw untracked count as "< 7 d", so one overdue memo rendered as "1 overdue (1 untracked < 7 d)". That counted the same file on both sides of the threshold, in the line written to end the F-GL-7 family. The planted run caught it.
⛔ Its blind spot (F-V-7). A memo addressed to us that was never copied here is invisible to it. Vulcan's 10-08 endpoint memo reached City's inbox and not ours. The gate measures the drop-box, not the fleet's outboxes.
--check-doors (F-V-5) is a flag rather than a gate. A door is rendered against a port, and a
port is a lease: when the receiver restarted on 8767, four pending doors still carried 8768. For
every .html with a .pending sentinel, the flag compares the <body data-receiver-url> port to
.gate_receiver.port, and checks that /health on that port names this gates root. Exits:
0 match · 1 a door points elsewhere · 4 no sidecar, a dead receiver, or one that isn't ours.
It is not a gate because whether a daemon is running is desktop state (the F-DT-5 lesson). Run it
after any receiver restart and before asking the operator to rule.
⚠ This file is written by exception, not as a registry — do not add a completeness check
Measured 2026-09-15: 4 of the registered gate names have never appeared in this file
(certification, canvas_core, comic_render, and — until this section — registry_census). That
is not drift. This document's membership rule is gates with a story: the ones whose existence
teaches something about how the gate set fails. A gate that has simply been green since Keystone
earns a row in GATES and no paragraph here.
⛔ So there is deliberately no check asserting every Gate(...) name appears below. A completeness
rule over a knowingly partial document would manufacture exactly the shape F-DT-7 describes — a check
that looks like coverage of something it was never meant to cover. Per the campaign's own rule, a
missing twin is a fact to state with its reason, not a defect to remedy.
⚠ What was genuinely wrong, and is recorded rather than quietly fixed (F-DT-11's sibling): gate
#10 was added 2026-09-13 and this file still read updated: 2026-09-10 on 2026-09-15 — it carried
dedicated sections for gates #8 and #9 and nothing for the gate an entire campaign was built around.
Nothing automated could have caught it; prose currency is a human obligation, which is the argument
for keeping this file short enough that the obligation is cheap.
Upstream candidate
The generic form — a phase-gate list should be executable, and should fail on omission rather than
on disagreement alone — is not Canvas-specific. Every vault that publishes a gate line in STATE.md
carries this exposure. See how/backlog/idea_runnable_gate_manifest.md §Upstream candidate.
Operation Datum added a second, broader one: every hand-maintained registry needs a consumer or a
discovery pass, and which one it is must be written on the line — as a per-object falsifiable
claim, never as a second list. See
how/backlog/idea_upstream_registry_derivability.md
and the doctrine at
what/context/context_registry_derivability.md.
