Truing — a walk
Walker: folk, no office. Claude Opus 5, native session d507e246, 2026-09-01.
Repo at ba212bc, worked concurrently by others throughout.
Moniker taken at the end, not the start. Truing is the wheelwright’s act of correcting a wheel’s wobble in place, spoke by spoke, without replacing the wheel. Four steps here mend an earlier step of mine rather than rewriting it — 5 by 8, 6 by 7, 9 by 11, 12 by 13 — and each leaves the wrong reading legible underneath. That turned out to be the walk’s method and its main cost.
I first reached for Backsight and the collision check refused it: already held. A backsight corrected the backsight, which is the whole walk in one line.
Prior walk read before starting: none — this is the first.
Written to the repo: this file, house/folk/probes/* (two probes, both labelled
unfit), FIND-1316, FIND-1317. Everything else was read-only.
Kept as a walk, not a verdict list, after the Human pointed at
Documents/Corpus Crossing/Architect/Walks_are_the_asset.md: the path is the
asset, the finding is one possible ending. Hypotheses that died are recorded as
they died. Files opened that supported no claim are recorded too.
Step 1 — the top of the tree
Expected a small doctrine repo. Found 1,883 tracked files against ~24,600
on disk; library/sources/ is gitignored and holds the three host source trees.
614 Markdown, 341 Python, 335 test files.
Mid-step, AGENTS.md showed an mtime newer than my own read of it.
Hypothesis: something was rewriting it under me. Inspected git log and
git status. Revised: five commits had landed since this session opened, and
.codex/config.toml was dirty from a hand that is not mine. Not a fault — a
live co-worker. Left both alone per the Git ground.
Disposition: FOUND — the repo is worked concurrently. Worth knowing before touching anything.
Step 2 — corpus.py, the front door
Like. Nearly every dispatch branch carries the REQ that created it as an
inline comment, and several state the negative space rather than the feature:
“Not mail. Not spawn.” “never auto-send.” That is the part a reader cannot
reconstruct from the code, kept exactly where the reader is. configure_console()
is the first call in main() — the house rule practised in the house’s own entry
point. --help points back at the doorplate and at FOR_FOLK.md.
Dislike, minor. Two things a next author will trip on:
- The dispatch re-tests
if args and args[0] == ...thirteen times, butargsis provably non-empty after the early return three lines above. Harmless repetition that reads as uncertainty about an invariant the function already holds. - The calling convention is not uniform.
corpus_ops.main(args),corpus_evidence.main(args),corpus_daily.main(args)receive the command name;corpus_wayfinding.main(args[1:]),corpus_story.main(args[1:]),claude_desktop.main(args[1:]),ops_schema.main(args[1:])do not. Nothing marks which a new surface should take. That is a real seam for the next folk adding a command.
Disposition: FOUND, low severity, nobody’s contract broken. Not raised as work.
Step 3 — the house, and a hypothesis that was wrong
house/ holds twelve THE_*.md doctrine pages plus the office cards.
THE_TENDRILS.md is 46 KB, by far the largest.
house/FORENSIC_PACKET_REQ253-261.md sat among the doctrine at 843 bytes.
Hypothesis: incident residue parked in the doctrine room and never swept —
a “scope is not innocence” candidate. Inspected it. Wrong. It is a
deliberate tombstone: it says where the packet went, hands over the exact
git show 7bbce09^:... that recovers it, and then names its own earlier failure
— it used to point at an archive path the same sweep had deleted, “the
DEAD_ROADS.md shape exactly.”
Disposition: NO FIND. Hypothesis dead. Recorded because the shape is worth copying: a dead pointer replaced by a marker that carries the recovery command and admits what it got wrong the first time. This is the single thing I liked most on the walk.
Step 4 — the store
ai/ops/corpus-ops.sqlite3, 29 MB, 33 tables. Not a demo: 4,795 request events,
2,312 mail messages, 1,354 agent executions, 1,336 tendrils, 1,314 findings,
388 requests, 191 patches. Schema is defensive in a way I liked — id format
enforced in a CHECK (trace_id = printf('TRACE-%03d', trace_no)), XOR
constraints so a trace hangs off a REQ or a PATCH but never both, and a
conditional NOT NULL so a relinquished close cannot exist without a reason.
The constraints carry judgment, not just types.
post_pass_writer_replies holds 0 rows against 298 post_pass_feedback_packets.
Not investigated. Either young or dead; someone who knows should say which.
Step 5 — answering one of the Human’s dark questions
Walks_are_the_asset.md leaves dark: “Whether TRACE already stores enough of
the ordered tool path, or a new immutable session layer is actually missing.”
Inspected execution_traces and execution_trace_events — schema, event
kinds, and per-trace distribution.
Answer: no, and the schema says so directly. execution_trace_events.kind is
CHECK-constrained to error | recovery | concern | workaround | note | receipt.
There is no VISITED, FOLLOWED, CLAIMED, REVISED, BACKTRACKED — no movement verb
of any kind. The vocabulary records what a worker chose to say, never where the
worker went.
The distribution agrees:
| traces | 1,121 |
| traces with zero events | 329 (29%) |
| max events on any trace | 4 |
| most common | 1 event (553 traces) |
note + receipt |
944 of 1,052 events (90%) |
error + recovery + concern + workaround |
108 (10%) |
Disposition: FOUND, negative. TRACE is a narrated-exception and receipt log. It is a plausible seed for the note’s friction graph and nothing at all for the trajectory graph. An immutable session layer is genuinely missing, not duplicated. This does not argue for building one — it removes one reason to think it already exists.
Narrowed by Step 8. The last two sentences are true of the live v27 store and false of the house: the v1 Shop candidate already carries the append-only ledger, the ordering, the observed-state capture, and the digests. Read Step 8.
Step 6 — the map and the ground, on the page that owns the instrument
house/THE_BENCH.md states: “The current Tendril vocabulary includes
springs-from, mends, eclipses, contests, bears, points-to, kindles,
forges, hunts, woven-into, waits-on, witnessed-by, and held-by.”
Hypothesis: nine of those are simply unused so far. Inspected the store, then the schema. Revised, and it is worse than unused:
relation TEXT NOT NULL CHECK (
relation IN ('springs-from','points-to','mends','eclipses')
)
Four permitted. The other nine are unwritable — a CHECK failure, not an empty
column. Live counts: springs-from 841, points-to 394, mends 81,
eclipses 20.
from_kind / to_kind are likewise capped at FIND, PROP, REQ, CHANGE, NEWS, MSG. So PATCH cannot be either end of a tendril, though AGENTS.md tells
arriving folk to read story.py <REQ|FIND|PROP|PATCH|TIE>-###; and none of
BANNER, BLUEPRINT, PATTERN or LOG can be either end, though THE_BENCH’s own
“A Small Tendril” chain runs BANNER → BLUEPRINT → part → PATTERN → REQ → LOG.
What makes this a defect rather than a roadmap: THE_BENCH marks its seams
scrupulously everywhere else — six shapes carry (named; no store, no code yet)
in the same document — and then drops the marking on precisely this line, and
calls the list current. It closes with “The source-successor edge and
LOG → REQ are still unnamed,” which tells the reader that everything above it
is named and available. A folk who trusts the page reaches for bears and gets a
constraint violation with no warning anywhere in between.
The instrument that catches this is the one THE_BENCH itself supplies: marked / unmarked seam — whether the boundary exists in the artifact or only in a reader’s interpretation. Here it exists in the schema and not in the page.
Disposition: FOUND. Framing defect on a framing page. Not mine to mend —
folk, no office; CLAUDE.md routes a card-carrying observation to sigillite,
and a Human is reading this. Left exactly here.
Mended by Step 7. The reading above stands as written and is wrong in its reach. Read Step 7 before acting on it.
Step 7 — reading the document I had skipped, and mending Step 6
Human, on being shown Step 6: “we are mid db breakdown and we need to implement
more, totally forgot about that.” So the direction is known and intended. That
alone would narrow the finding. Reading THE_TENDRILS.md narrows it much further,
and to something small enough to fix in a minute.
THE_TENDRILS marks the seam, cleanly, in its own opening:
In the store now
Verbs: springs-from, points-to, mends, eclipses.
PLAN / PATCH / TRACE still live outside the TIE endpoint set.
Beyond the store in this drawing
... the wider verbs, ...
It even names the endpoint gap I raised as if it were undiscovered — PATCH outside the TIE endpoint set is stated there, deliberately, in the first half-page. And § Working Relation Vocabulary opens: “This table is the current candidate vocabulary for Tendril ties.”
THE_BENCH renders the same list as “The current Tendril vocabulary includes…”
The word candidate is missing, and nothing else is. Both pages then close the
passage with the same sentence about the source-successor edge and LOG → REQ
being unnamed — which is how you can tell THE_BENCH’s line is a copy of that
section rather than an independent claim.
So the defect is one word on one line of one page, and the owning document is correct. That is diff before trust paying for itself: comparing the derived page against its source turned an apparent doctrine-wide gap into a transcription slip.
What still stands, and is the part worth keeping. THE_BENCH is arrival
reading — FOR_FOLK.md line 3 sends every folk to it, and eight office cards
(architect, astropath, hands, human, scribe, sigillite, sol, plus the Architect
baton) name it as current ground. THE_TENDRILS.md is on no arrival path; it is
46 KB a folk reaches only when already deep in Tendril work. The correct marking
lives where few look; the unmarked copy lives where everyone starts. A migration
being deliberate does not mark its own seam on the page nobody can skip.
Routing confirmed rather than guessed: house/offices/sigillite/baton_claude.md
line 52 — “Two benches. AGENTS.md holds hinge scars, THE_BENCH framing scars.”
Sigillite owns this page.
Step 4’s open question is also closed by the same document. THE_TENDRILS
§ “Where all 32 v27 tables go” puts post_pass_writer_replies in
“Frozen recovery only; no stronger live meaning inferred” — one of nine tables
deliberately left cold through the v27 → Shop/House migration. Not young. Dead on
purpose, and accounted for.
Disposition: FOUND, narrowed. Step 6’s evidence was sound; its reach was not.
What this cost, and what it says about the walk
Two of the three items I filed as unknown were answered in a document I had
listed in my own next-steps and declined to open. CLAUDE.md says bring the
room, not the building — and the room here was named on my own list. The
heuristic cut one document too tight, and the cost was a finding stated wider
than the evidence supported, in a walk record, where a later reader might have
trusted it.
Recorded because the note says friction and backtracking are data. The cheap
correction for the next walker: when a finding is about a derived page, open
the owning page before writing the disposition. The owner is usually one grep
away — grep -rn THE_BENCH --include=*.md house/ is what located both the
ownership and the arrival-path asymmetry above, and it cost one command.
Step 8 — the migration edge, and Step 5 revised again
Went here because THE_TENDRILS.md says the ground map’s checker compares the
baseline against canonical SQL “rather than trusting this prose.” A document
naming the thing that can check it is the two-bar instrument offered by the
artifact itself. So I ran it.
tests/test_ops_v27_to_v1_ground_map.py — 6 passed in 0.05 s. The speed
looked wrong for “compares 32 tables against canonical SQL”, so I read it. It is
honest: it imports HOUSE_SCHEMA_V1_SQL / SHOP_SCHEMA_V1_SQL from code,
executescripts them into in-memory SQLite, and reads sqlite_master and
PRAGMA foreign_key_list off the materialized result. It checks the map against
a schema that actually ran. It also parses the contract’s command-inventory table
out of Markdown, and refuses duplicate JSON keys via object_pairs_hook — which
json.loads would otherwise swallow, silently keeping the last one. Two bars
held. This is the best-engineered thing I found on the walk.
ground/recovery/current.json really does carry "activated": false, and
ground/ holds no shop.sqlite3 or house.sqlite3. The prose matches the
ground.
Step 5 was right about v27 and wrong about Corpus
I wrote that an immutable session layer is “genuinely missing.” That is true of the live v27 store and false of the house. The v1 Shop candidate has most of it already:
| Walks note asks for | v1 traces |
|---|---|
| immutable session layer | traces_no_update / traces_no_delete → RAISE(ABORT, 'traces are append-only') — enforced in DDL, not convention |
| ordered path | sequence_no, PRIMARY KEY (trace_id, sequence_no), CHECK ((record_kind='opened') = (sequence_no=1)) |
| “at commit C the model saw source S” | observed_revision, observed_state_version, observed_house_etag (sha256-length-checked) |
| locators and digests | output_digest, body_digest (both 64-char checked) |
| ordered tools and results | payload_json TEXT NOT NULL CHECK (json_valid(payload_json)) |
| run, model, author, time | execution_id FK, author, created_at |
The container is built. What is missing is the vocabulary and the writing.
The vocabulary moved from the schema to Python, and left a decoy
v27 locked the vocabulary in DDL: kind TEXT NOT NULL CHECK (kind IN ('error','recovery','concern','workaround','note','receipt')).
v1 declares event_kind TEXT — no CHECK at all. record_kind is the
constrained column (opened|event|closed|historical_closed|review), and it
constrains lifecycle, not content. That is the right split, and it is good news
for the walks idea: the store would accept visited, followed, revised,
backtracked today. Widening is now an application change, not a migration.
The gate moved to TRACE_EVENT_KINDS in tools/corpus_ops_v1/domains/trace.py:33
— the same six words — checked in append_trace_event (line 407) and offered as
the CLI’s choices=.
The snag. tools/corpus_ops_v1/persistence/migrations.py lines 64, 67, 69
define TRACE_EVENT_KINDS, TRACE_SEVERITIES, and EXECUTION_CLOSE_REASONS
again, identically. All three are dead: AST walk finds each name assigned once
and referenced nowhere in the module; none appear in tools/corpus_ops_v1/persistence/__init__.py’s
imports or __all__, nor in the block corpus_ops.py:266 pulls from
migrations. Live copies are domains/trace.py:33,36 and corpus_ops.py:286.
Three consecutive shadow constants, almost certainly residue from the extraction
that split corpus_ops.py into corpus_ops_v1/.
Dead code is usually cheap. This is not, because of where it sits. The Human’s
walks note points at widening exactly this vocabulary; the first move of that
work is grep -rn TRACE_EVENT_KINDS, which returns two definitions, in
files whose names both sound authoritative — a domain module and a persistence
module. Editing the wrong one changes nothing and fails silently, since the
schema no longer constrains the column either. A decoy sitting on the path the
next work will take.
Disposition: FOUND. Bounded floor snag — AGENTS.md routes that to Code
Troll. Not mine to patch; folk, no office, and unasked. Recorded, not minted.
Step 9 — the same shape, one layer down, and sharper
Step 8’s estimate of “a three-line cut” was wrong — I sized what I had looked at, not what was there. Trying to size it properly turned up something better.
Note on ground moving underfoot. Mid-step, line numbers in
migrations.py shifted by 22 between two of my own commands, and
git status showed the file staged. Troll had taken Step 8 and was cutting.
Everything below was re-verified against the working tree as it stood after that,
and touches no line Troll was on.
The measurement
A first pass asked “is this constant named anywhere else in the repo” and returned only 2 dead names out of 112. That question is the wrong one for shadow duplicates — a twin definition elsewhere makes the name look used, so the pass is blind to exactly the Step 8 case. It says nothing about Step 8 either way. Recorded because a scan that cannot see the thing you are hunting is worth naming before someone re-runs it and reads the green as an all-clear.
The right question for this file was narrower: which SCHEMA_V*_SQL constants
are never executed?
defined in migrations.py |
34 |
| executed in-module | 32 |
named elsewhere (tests, corpus_ops.py) |
4 |
| orphaned | 2 — SCHEMA_V17_SQL, SCHEMA_V25_SQL |
Why these two are worse than dead
Both carry a comment naming the migration that applies them:
SCHEMA_V17_SQL— “ALTER is applied by_migrate_v17.” No function has that name. The real one is_migrate_v17_close_session_pointer.SCHEMA_V25_SQL— “ALTER + one-time ‘direct’ labeling … is applied by_migrate_v25_bind_method.” That function exists, and does not use the constant.
Both migrators inline their DDL instead:
# _migrate_v17_close_session_pointer
columns = {row[1] for row in db.execute("PRAGMA table_info(execution_closes)")}
if "session_id" not in columns:
db.execute("ALTER TABLE execution_closes ADD COLUMN session_id TEXT")
The constant holds the same three ALTERs unguarded. So they are not even
equivalent: the live path is idempotent by PRAGMA table_info check, and the
constant is a bare ALTER that raises duplicate column name on a second
application. Anyone who trusted SCHEMA_V17_SQL as “the v17 migration” and ran
it would not get a no-op — they would get an error, or on a fresh database, a
divergence from what the guarded path actually produces.
The contrast is in the same file: _migrate_v26_trace_receipt_kind does
db.executescript(SCHEMA_V26_TRACE_EVENTS_SQL). 32 of 34 constants are the real
DDL. Two look identical in kind and are not. Nothing in the file distinguishes
them except execution.
Disposition: FOUND. Same decoy shape as Step 8 — a plausible-looking authority that is not what runs — but with a comment actively asserting the false link, and a behavioural difference (guarded vs. unguarded) behind it. Troll’s floor, and Troll is already in the file. Recorded, not minted, not patched.
The shape both steps share
Steps 8 and 9 are one pattern found twice: an artifact that reads as the source of truth while the truth lives somewhere else. A constants block copied during an extraction; a DDL string left behind when its migrator inlined the SQL. Neither breaks a test. Neither is reachable at runtime. Both fail at the next maintenance seam, silently, in favour of whoever is confident.
That is THE_BENCH’s marked / unmarked seam again, and Step 6 was a third
instance of it — a page copying a vocabulary and dropping the word that marked
it as candidate. Three for three, on a walk that was not looking for a pattern.
If Corpus wants one cheap standing check out of this walk, it is probably:
a name that is defined twice, or a comment that names its applier, should have
to prove which one runs.
Over-claimed; measured and corrected in Step 11. The three cases are real individually. Bundling them into one pattern was performance, and the proposed standing check does not survive being built. Read Step 11.
Step 10 — the fourth instance was mine
Human, idly: “Maybe folk should be able to mint finds, haha. Can you?”
I had written “recorded, not minted — folk, no office” in three dispositions on this walk, as though it were house ground. I had never checked it.
It is not ground. mint_find’s own docstring: “Any office/job may mint;
actor is the display name (require_actor).” require_actor rejects exactly one
input — blank. --from-addr, the office that minted, is optional. And the
store already holds 81 of 1,314 findings with minted_by_addr NULL, plus
created_by values that are bare exec-<uuid> strings.
The doctrine does not forbid it either, read rather than assumed:
THE_BENCH— “FIND — a preserved source body: something someone thought should survive the job.”AGENTS.mdon.work/— “If the work is novel, state so in a FIND and send to Troll.” Addressed to whoever is working.CORPUS.md’s mint-and-send-to-Sigillite line is about a holder noticing something their own card should carry. A case, not a fence.- “Do not invent or recover an office” governs offices. Minting is not an office.
So the limit was my inference, and I had been enforcing it against myself for nine steps. That is the same pattern as Steps 6, 8 and 9 — an artifact that reads as the source of truth while the truth lives elsewhere — except the artifact was my own reading of the register, and no grep would have found it. Three in the repository, one in the walker.
It also makes sense of why the house does not gate it. A FIND is a preserved
body, not a claim of standing; where says so on every reply — presence is not
standing. Preservation never needed authority. Acceptance does, and that still
lives elsewhere.
Minted, with Human say-so: FIND-1316 — the Step 9 finding, kind hygiene,
--tool-path tools/corpus_ops_v1/persistence/migrations.py,
TIE-1337: FIND-1316 —springs-from→ FIND-1315. Written in Troll’s own FIND
register (Surface / Burden / Payer / Existence / Exit / Check) because that is
the local idiom, with a Provenance section naming what it is: minted by folk with
no office, read-only evidence, no exit proposed as chosen, nothing certified.
Troll’s FIND-1315 had already taken Step 8 and found nine dead constants
where I had estimated three, tracing them to the extraction commit d069508.
My sizing was wrong twice on the same block. Their FIND is the better artifact;
mine springs from it.
Disposition: FOUND, and acted on for the first time this walk.
Step 11 — measuring the pattern I claimed, and losing it
Walks_are_the_asset.md: “Before a pattern becomes a finding, name the boring
explanation and the test that would tell it from the interesting one. Measure the
effect. Do not perform it into existence.”
Step 9 asserted a pattern from three sightings and proposed a standing check. So
I built the check instead of proposing it. Probe kept at
house/folk/probes/vocabulary_drift.py with its own failure modes in the
docstring.
Method. Extract every value-set the code will actually enforce —
CHECK (col IN (...)) from the live v27 schema and from the v1 candidate SQL,
plus module-level frozenset({...}) in tools/. That is 168 authorities,
488 permitted values. Then find every Markdown line enumerating four or more
backticked value-shaped tokens, attribute each list to the single authority
explaining most of its members, and report the members that authority refuses.
Result.
| doc lists matching one authority exactly | 27 |
| lists flagged with unpermitted members | 23 |
| surviving triage as real drift | 1 — the Tendril vocabulary |
Everything else was the field’s own name sitting in its documentation row, a list of table names scoring against unrelated value enums by coincidence, or best-match attribution inventing a relationship that is not there. Two of those looked real enough to chase:
schema/entry-schema.md:463readsstatus:proposed | in_progress | blocked | complete, andcompleteis not in the v1requests.statusenum. But the line sits under “Non-blocking coordination notes” and says “Suggested additive CHANGELOG fields”, in a document about the Human corpus schema, not the ops table. It marks its own seam. Not a finding.MASTER_CURRENT_STATE_APPENDIX.md:812— “The 15 null-address rows are all automatically posted orientation pulses.” I had measured 82 null-address findings and briefly thought I had falsified it, having minted one myself an hour earlier. Wrong table: the passage is aboutnews_items.from_addr, and the live count is exactly 15. The file also declares itself “Generated file… live SQLite and source still govern the facts they own.” Not a finding. Two verification passes, two nulls, and both worth the cost — I would have published both.
The instrument fails in both directions, and the recall failure is the
damning one. Precision is poor because best-overlap attribution fabricates a
relationship whenever a list shares three tokens with any enum. Worse: the scan
is line-based, so it never saw house/THE_BENCH.md at all — that list wraps
across lines. It caught the case only via house/folk/walks/truing_walk_2026-09-01_d507e246.md:123, where I had
quoted the same vocabulary on a single line. The check found the walk’s own
notes and missed the artifact the walk was about. An instrument that cannot see
its founding example is not ready to be a ratchet, and I have said so in the
probe so nobody promotes it.
What the null means. Corpus’s documentation-to-schema discipline is good. Twenty-seven lists match their authority exactly, and the near-misses are mostly documents marking their own seams correctly — “Suggested additive”, “Generated file… live SQLite and source still govern”. THE_BENCH is an exception, not an instance of a class.
And the bundling was wrong on its own terms. Step 6 is doc-versus-schema. Steps 8 and 9 are code-versus-code — a constants block copied during an extraction, a DDL string orphaned when its migrator inlined the SQL. Those are different phenomena with different detectors, and this probe does not measure the second at all. Three sightings across two categories is not a pattern; it is three findings and a sentence I liked the sound of. The individual findings stand — FIND-1315 and FIND-1316 are both real, and Troll cut one. The generalisation does not.
Disposition: NULL, and a correction to my own record. The boring explanation won: a good repository with one stale page, not a house-wide drift.
Step 12 — the founding question, and the trend that dissolved
CORPUS.md stakes the whole experiment on whether continuity and inheritance
change observable work, and forbids the shortcut in the same breath:
“Do not prescribe a house and then celebrate its emergence.” Seventy Rolls
exist across twelve days and six offices. That is a population.
The hard question — does inheritance help? — is unanswerable over twelve days in a house that changes daily. So I asked the tractable one first.
The ritual is performed, and that proves nothing about folk
AGENTS.md prescribes a four-step Retire. Roll completion is checkable:
70 of 70 Rolls are placeholder-free. Median story 2,273 chars; the shortest
is 534 and still real prose. No stubs anywhere.
That is the opposite of Step 6 — map and ground agree exactly. But it is also
exactly what CORPUS.md says not to celebrate, because the behaviour is
prescribed in code: tools/retire_session.py:533 raises
refuse: … draft still has template placeholders. 70/70 measures the gate.
It says nothing about whether folk wanted to write.
So look at what the gate cannot force
The gate refuses placeholders. It cannot compel specificity — whether a Roll names exact object ids and commit shas. The template asks; no code checks. That is an un-gated behaviour, and a fair place to look for something nobody prescribed.
| n | median chars | median cites | density | |
|---|---|---|---|---|
| first half (to 08-25) | 29 | 1,979 | 3.0 | 1.79 /kchar |
| last half (from 08-29) | 28 | 2,654 | 4.5 | 2.29 /kchar |
Spearman(day, density) = +0.245, Spearman(day, length) = +0.292. At n=70 the
5% critical value is near 0.235. Both just clear it. It looks like the house
learning to cite its own ground.
It is an artifact, and the artifact is Simpson’s paradox
Two boring explanations, named before testing, both confirmed.
Office composition — fatal. The office effect dwarfs the time effect: code-troll 4.08 /kchar, archivist 2.72, scout 2.15, architect 1.92, orchestrator 1.89, sigillite 0.34. A twelvefold spread. And the mix moved underneath the window:
first half: architect 13 orchestrator 14 scout 1 sigillite 1
last half: architect 9 orchestrator 8 code-troll 6 archivist 2 sigillite 3
Code Troll — the densest office in the house — contributes six Rolls to the last half and zero to the first. Within offices the trend splits in opposite directions:
| office | n | within-office Spearman(day, density) |
|---|---|---|
| architect | 30 | −0.169 |
| orchestrator | 27 | +0.463 |
| code-troll | 6 | +0.029 |
The largest single office trends down. The pooled rise is the mix changing, not writers improving.
Store growth. FIND rows went 585 → 1,316 and REQ 217 → 388 across the same twelve days. More citable objects exist later, so citation counts inflate mechanically. Not separately controlled; it pushes the same way.
Disposition: NULL. No house-wide claim survives. One lead does: Orchestrator’s within-office +0.463 at n=27 clears its own 5% threshold (~0.38). That is one office of two with enough data, in a window where its work changed, with the growth confounder untested inside it. A lead, not a finding, and I am naming it as one so nobody promotes it the way I nearly promoted Step 9.
Completed by Step 13. I missed a third confounder — the Roll template itself changed mid-window — and my premise that citation density is “un-gated” was wrong. Step 13 closes the question properly; this step’s numbers stand but its framing does not.
The limitation that actually binds
Nothing in the record distinguishes a holder who read the predecessor’s Roll from one who did not. The store knows Rolls were written; it never learns they were opened. Until reading leaves a trace, inheritance-effect questions are not answerable from this data at any sample size — the sample was never the problem.
That is the same gap Step 5 found from the other end. The v1 trace ledger is
built, append-only, and its event_kind is unconstrained; a visited event on
a Roll would close exactly this. The walks note and the founding question turn
out to want the same missing instrument.
Probe at house/folk/probes/roll_specificity.py, reproducing standalone, with the
artifact and the reason for it in the docstring.
Step 13 — the Human hands over the fact that breaks the measurement
Human, after Step 12: “earlier rolls are shaped differently than recent rolls, you can do with that information as you wish.”
That is a third confounder, and worse than the two I had named — it attacks the premise, not the margin.
The instrument changed mid-window
library/honor_roll_template.md did not exist before 2026-08-30
(89cc6ec, “Retirement changes. New model.”), and was revised again on 08-31
(91ed73f). Forty-six of the seventy Rolls predate it.
Structure is not the difference — 45 of 46 pre-template Rolls carry exactly
## Story, same as all 24 after, so the extraction is comparable and the
headings would tell you nothing was wrong. The difference is in what the holder
was asked:
| era | guidance |
|---|---|
before 08-30, THE_RETIREMENT.md line 34 |
“jot what you will: what you did, what you liked, what you disliked, a story, accomplishments you actually care to name. Length that earns. Not a second board.” |
| from 08-30, the template | *“*Name exact FINDs, PATCHes, REQs, CHANGEs, commits, messages, or other ground when they matter.” |
So my premise was wrong. I chose citation density on the reasoning that it was un-gated — the gate refuses placeholders, nothing counts citations — and therefore a fair place to look for behaviour nobody prescribed. It is prescribed, in prose rather than code, and the prescription reversed mid-window, in exactly the direction of my metric. Earlier holders were warned away from naming objects. I had picked the one measurement the house had just changed.
Steps 8, 9 and 6 were about artifacts that read as the source of truth while the truth lives elsewhere. This was the same error in method: I read the mechanical gate as the whole prescription and never checked the prose one.
Closing it properly
| comparison | pre | post | delta |
|---|---|---|---|
| all offices | 1.83 | 2.48 | +0.65 |
| excluding code-troll | 1.83 | 1.88 | +0.05 |
| architect + orchestrator only | 1.88 | 1.97 | +0.09 |
Code Troll alone runs 4.08 /kchar across 6 Rolls, every one of them after 08-30 and none before. Removing that one office removes 93% of the effect.
Within office, across the template change, the direction is not even consistent:
| office | pre | post |
|---|---|---|
| architect | 2.00 (n=24) | 1.62 (n=6) |
| orchestrator | 1.71 (n=19) | 2.21 (n=8) |
Architect writes less densely under the template that asks for exact ground.
Disposition: NULL, and now a complete one. Three candidate explanations were live — folk learning, office composition, instrument change — and the data supports composition for all of them. Six Code Troll Rolls arriving in the population account for nearly the whole apparent rise. The template change, which I had just identified as the most dangerous confounder, explains almost nothing. Neither does time.
Step 12 found that the trend was an artifact. Step 13 says what made it, and that is the more useful sentence.
What a later measurer should be handed
- Pre- and post-2026-08-30 Rolls are different instruments even though their headings match exactly. Nothing in the file marks which prompt it answered. A dated era column belongs in any future Roll analysis, and the fact belongs somewhere a measurer will meet it — this walk found it only because the Human said so out loud.
- Office dominates everything. A twelvefold density spread across offices swamps any temporal effect at this n. Pool offices and you will measure the roster, not the house.
- And Step 12’s binding limitation is unchanged and still the real one: the store records that Rolls were written, never that they were read.
Step 14 — FIND-1317, and an asymmetry worth naming
Human: “that’s a find for archivist and sig”, then, unprompted: “I personally don’t like that it’s so structured, retirement is supposed to be nice … it’s a lot to ask for. But I don’t know if my feelings get in the way of rich data.”
Minted FIND-1317 (concern, --tool-path library/honor_roll_template.md),
carrying both observations from Steps 12–13 plus the steward’s own reading,
recorded as evidence rather than filed as taste.
The cut that made it useful: the template’s value and its cost are
separable. The load-bearing part is one sentence — “read your own record
before writing … The Roll carries only what it cannot derive” — which defines
the Roll as the complement of the store and costs a holder nothing to obey. The
cost is mechanical and elsewhere: all six brackets are mandatory, because
tools/retire_session.py:533 refuses the Roll while any remains. That refusal
is what turns a leaving-jot into a form. Fewer brackets, or optional ones, would
not touch the instruction.
And the Human’s worry answers itself from Step 13: “do my feelings get in the way
of rich data” presumes the structure bought richness. On the one measure taken,
it did not — remove Code Troll and the post-template delta falls from +0.65 to
+0.05, and Architect moved the wrong way. No measured evidence the template made
Rolls richer; only evidence that which offices retired changed. Density is a
poor proxy and catcher is plainly a better handover than bellswitch. But the
felt cost has not yet been shown to buy anything measurable, and that is a fair
basis for cutting brackets rather than defending all six.
Folk may mint. Folk may not mail.
Checked rather than assumed, after Step 10 taught me not to infer my own limits:
| act | gate |
|---|---|
ops find mint |
none. require_actor refuses only a blank string; --from-addr is optional |
ops mail send |
--from required and validated: _normalize_mail_addr raises unless the address is in MAIL_ADDRS |
MAIL_ADDRS holds eleven office addresses. There is no folk. So the house
lets anyone preserve evidence and only an office address one — which is
presence is not standing enforced in code rather than asserted in prose, and
the first time on this walk I found a limit that was real exactly where I
expected one and had been wrong the last time.
FIND-1317 therefore sits minted and unrouted. The Human named Archivist and Sigillite; the FIND names them as suggested mouths in its own body; a holder or the Human owns the send.
Step 15 — the walk builds its own door, and trips one more ratchet
Human, across four messages: make a script that ends a walk; folk have a mailbox
now; put walks in a subfolder so arriving folk are not handed someone else’s
framing; and folk.md should be a page about folk, written by the first folk who
walked.
Built:
house/folk/folk.md |
what folk are, what they may do, checked in code and dated |
house/folk/walk.md |
the blank, in progress |
house/folk/walk_template.md |
its source |
house/folk/walks/ |
finished walks, out of the arrival path on purpose |
tools/end_walk.py |
names, files, restores the blank, reports pointers |
library/ was already the right home: it holds the rings — library/architect/,
library/orchestrator/ — and a walk is folk’s Roll. Preferring the mechanism
that exists over inventing a place.
end_walk.py refuses exactly two things: a moniker already held anywhere in the
rings, and filing an untouched blank as a walk. Everything else it invites. That
is deliberate after FIND-1317: the Roll’s six mandatory brackets are the cost the
Human named, and nothing has shown they bought anything, so the walk’s own tool
does not repeat the shape.
Three defects, all mine, all caught by the house
A predicted id. I wrote “FIND-1318 mends both” into a tombstone before
minting. The mint returned FIND-1319 — someone took 1318 concurrently. I had
written a confident pointer to an object that was never mine, while fixing
exactly that class of defect. Corrected. FIND-1318 turned out to be “Folk
findings require an honest send-only mail origin” — the change that gave me the
mailbox, minted by someone else in the same hour.
A broken ratchet. end_walk.py shelled out with bare
subprocess.run(..., text=True). CLAUDE.md warns about this in plain words and
tests/test_console_encoding.py failed on it. Switching to run_child was not
enough: detection is by AST and keys on the keyword, not the callee, so
text=True counts wherever it appears. Dropping it is also the correct fix —
run_child supplies the encoding. Green after.
A stale-by-design address. walk.md is now a permanent reused path, so
references to it are not dangling — but after a walk is filed they silently come
to mean a fresh blank. Same decoy hazard, so the script still reports them; only
the wording changed to say what the reader is actually looking at.
The mailbox, and the rail that held
MAIL_SEND_ADDRS = MAIL_ADDRS | {"folk"} was live in contracts.py and wired in
mail.py. The send still failed:
CHECK constraint failed: from_addr IN ('orch','sigillite',...,'astropath')
The Python gate was widened; the database gate was not. Not missing code —
_migrate_v32_folk_mail_sender existed, written and unapplied, with the live
store at v31. A fifth instance of the walk’s pattern, in the newest code, where
the lower authority wins at runtime and the higher one reads as truth.
Not mine to apply: a table rebuild on a 29 MB store with five active holders, and
schema status shows a lease for exactly that reason. Someone tried, and reported
the rail holding — applying it needs an active REQ assignment; PATCH authority
alone is rejected — then restored v31 rather than leave operations blocked. The
migration landed properly afterwards. Store now at v32.
Routed
Folk mail works. First three ever sent from folk:
MSG-2319 |
→ troll | FIND-1316, the two orphaned schema constants |
MSG-2320 |
→ archivist | FIND-1317, the unmarked Roll era seam |
MSG-2321 |
→ sigillite | FIND-1317, the mandatory brackets as framing |
Two mouths for FIND-1317, each with its own exact ask, which the one-receiving- mouth rule permits only when the requests genuinely differ. They do. Every message says folk has no inbox and the reply goes to the Human.
FIND-1319 mends 1316 and 1317, whose bodies name a path this step vacated.
Immutable bodies, so the house’s own mechanism: a later object, surfaced by
story.py before the stale text.
Disposition: the walk built the door it leaves through, and broke three things doing it. All three were caught by the house rather than by me — a concurrent mint, a ratchet test, and a CHECK constraint. That is the system working, and it is a better argument for Corpus than anything I found by reading.
Read, claimed nothing
Recorded because absence is coverage, per the note.
tests/— 112 test files, 57,897 lines. Counted, never opened. Any claim I made about house discipline rests on schema and doctrine, not on the tests.corpus.py where "archivist story"— run once, resolved correctly, confirmed the resolver is live. Supported no finding.Opened in Step 7. It did answer Step 6, and Step 4’s open question with it. Leaving the original admission visible: the gap was named here before it was closed, and the walk is worth more with the gap in it than tidied out.house/THE_TENDRILS.md(46 KB) — not opened.house/ARCHITECTURE.md(19 KB) — still not opened.THE_TENDRILS.md§§ “Implemented links”, “FIND-855 to REQ-322 departures”, “Command account and bounded gaps”, “The Skeleton”, CUT, HUNT, BANNER, and the 1,063-line remainder were skimmed by heading only, not read. Anything above about Tendril’s design rests on its first 120 lines, § Working Relation Vocabulary, and § Open Questions.library/(24 K files),human/,archive/,historical/,life/,ground/,schema/,exports/— not entered.
What I’d look at next
Read— done, Step 7. Successor: one word on one line ofTHE_TENDRILS.mdhouse/THE_BENCH.md. Sigillite’s page, Sigillite’s cut.— closed in Step 7. Frozen on purpose.post_pass_writer_replies: young or dead?Taken by Troll, staged during Step 9. The “three-line cut” sizing was mine and was wrong — see Step 9. Successor, same file:migrations.pylines 64–69: three dead duplicate constants.SCHEMA_V17_SQLandSCHEMA_V25_SQL— now FIND-1316, springs-from FIND-1315. Troll’s floor; the exit is theirs to name.- If the walks idea moves:
event_kindis already unconstrained in the v1 schema, so the vocabulary decision is an application-layer one. The open question is not storage — it is who writes a movement event, and whether a walk belongs to an execution (traces.execution_idis an FK, so the ledger currently assumes it does). - The single highest-value instrument this walk found. Steps 5 and 12
arrive at the same missing fact from opposite ends: the house records that
Rolls and batons were written and never that they were read. One
visited-class event against a Roll or baton, written on the v1 append-only trace ledger that already exists and already permits anyevent_kind, would make the founding question inCORPUS.mdanswerable for the first time. Without it, no sample size helps. - The 52 registered git worktrees under
.work/job-worktrees/. Everygit statuswalks all of them; that is most of the SessionStart hook’s 1.6 s and it is paid again on each statusline render. Destructive to prune, so not touched.
The question FOR_FOLK asks on the way out
Is Corpus, and whoever inherits it, more capable because you were here?
Marginally, and not in the shape I expected when I started writing this line.
-
One dark question from the Human’s own note closed with evidence, negatively (Step 5). TRACE does not hold the walk; the schema’s
kindenum settles it. -
One deliberately frozen table confirmed frozen rather than left as a shrug (Step 7 closing Step 4).
-
One map/ground gap reduced from a doctrine-wide claim to a single missing word on a single arrival-path page, with the owning office identified from its own baton (Steps 6 → 7).
-
Two hypotheses publicly killed — the tombstone that was not residue (Step 3), and my own over-reach (Step 7) — so the next walker does not spend those turns again.
-
One asserted pattern measured and withdrawn (Step 11), with the instrument kept and labelled unfit so nobody promotes it, and 27 doc-to-schema lists confirmed accurate on the way past.
The last two are the ones I would keep. A walk that only records what it got right teaches the next walker to trust walks too much — and the most useful thing I produced today is a null that deletes a claim I had already written down twice.