OKF for the living specs — addressable, dated, typed, searchable - #214
Merged
Conversation
OKF v0.2 (GoogleCloudPlatform/knowledge-catalog) applied to the five living
specs, and taken past it where a living spec needs more than OKF offers.
OKF's links are explicitly untyped and its time keys are document-lifecycle
only. ADD's specs already carried OKF's shape by accident — type, title,
generated: {by, at}, reserved index.md/log.md — but 43 delta lines across
five files were undated, unaddressable, and reachable only by opening a file
and reading all thirty. A lookup that can answer only `specs/method.md`
points at thirty unrelated lessons.
Six tasks, breadth-first: okf-spec-frontmatter · dated-addressable-deltas ·
deltas-time-filters · typed-relations · search-verb · skill-reads-deltas.
Exactly one new verb ships (add search); `add specs` was declined in favour
of routing the loop through the existing `add deltas`.
Also files two nodes found while measuring, not planned:
- scan-skips-receipt-evidence: 97 Run receipts are 68% of every command's
graph-scan cost, and nothing reads their evidence payload off the graph.
- budget-pin-measures-cost: the SKILL.md pin counts newlines while naming
itself "the only always-loaded cost", so a reflow moves the metric without
moving the cost.
author: Tin Dang
Every lesson in the five living specs is now an addressable OKF concept with
a time span, so a relation can target it and a reader can ask what a spec
asserted on a past date.
before - [ADD · open] <text> (evidence: /tasks/x.md)
after - [ADD · M12 · open · 2026-08-11] <text> (evidence: /tasks/x.md)
folded - [ADD · M12 · folded · 2026-08-11→2026-09-03] <text> (evidence: …)
The id is <lens letter><integer>, keyed to the spec FILE's lens and unique
within it — the file disambiguates, since the address is
/specs/<lens>.md#<id>. The next id is max(frontmatter delta_seq, largest id
in the body) + 1: delta_seq survives deleting the highest delta, and the body
term is the only thing between a merged stream's delta and a colliding mint,
because _union_into_deltas writes back main's frontmatter and discards the
stream's counter.
A legacy 2-field head reads forever. Not generosity: a legacy head carries no
date and an installed bundle has nowhere to recover one, so at any deprecation
date the only moves left are making a user's real lessons malformed or
inventing a date. Dispatch is on the COUNT of · fields, never on shape —
shape dispatch would send a broken id to `unparsed` and leave `bad_id` a code
no producer can emit.
All 43 legacy lines migrated by a re-runnable script, every date recovered
from git blame and corroborated against its task node's stamps. 43 in, 43 out;
40 open before, 40 open after. Blame spread over ~20 commits, largest cluster
4 lines — no reflow re-dating a file at once.
Three defects the task's own checks caught while building:
- git blame on an uncommitted line returns TODAY — a fiction wearing git's
authority. Now rejects the all-zero SHA.
- git blame -L /regex/ takes git's dialect, not Python's, so any lesson
containing bracketed text silently matched nothing. Now blames by line.
- test_deltas_excludes_folded was about to pass vacuously: its fixture
hand-replaced a literal the new head no longer contains. Re-aimed to fold
through the verb.
The tail stays open and the evidence check stays a .search(), so the trailing
(refines: …) clause a later task appends cannot turn 43 green lines into
no_evidence. R:RENUMBER makes retire-in-place binding: a renumber on fold or
delete would silently re-point every relation.
All four engine twins byte-identical at 5de1a375; ENGINE_MD5 re-aimed with the
prior pointer kept.
author: Tin Dang
The five living specs accumulated 40 open lessons with nothing in the planning path that ever read them back. A spec nothing reads is an archive, not a living spec — the mirror of the failure this repo already recorded about the experience lens, which shipped in every bundle for three minor versions with no beat that engaged it. `add deltas` now leads the Task and Project/milestone surfaces in SKILL.md and intake.md, across all three skill trees. Quick is deliberately excluded (sized by blast radius, not by whether the specs cover it) and Explore too (the question itself is the deliverable). The check drives the real verb against populated, empty and malformed fixtures and asserts only its stable envelope — an item count or "no open deltas", plus the next: trailer, and that a malformed line is REPORTED, never dropped. It is deliberately silent on the delta line's internal shape, which the grammar task reworked in parallel. Funded by compressing prose, not by raising the pin (R:BUDGET_BUMP): SKILL.md sat at exactly 176/176 against its line pin. The first pass kept the line count flat by lengthening existing lines, which left the trees -15 lines and +1089 bytes — every budget check greener while the always-loaded cost rose, since the pin counts newlines and the cost is bytes. Re-funded by dropping a redundant third mention and tightening two sentences: now 0 lines and +399 bytes, with no claim retired. author: Tin Dang
The line pin counts newlines while its own message calls itself "the only always-loaded cost". Cost is bytes an agent loads; newlines are a proxy. A reflow — same content, fewer and longer lines — moves the metric without moving the cost. That is not hypothetical. It happened in this branch: SKILL.md held at exactly 176/176 lines while growing 189 bytes, reported as "funded by compression", with every budget check greener and the real cost higher. BYTE_BUDGET = 13258 is added ALONGSIDE the 176-line pin, never replacing it. The line pin holds a recorded human call in its own unit and stays; silently re-casting a human's number into a different unit is R:SILENT_REPIN. The byte pin is a ratchet at the currently shipped size — zero headroom granted. Its failure message states why two pins exist, so the next reader does not delete one as redundant. The hole is proven closed, not asserted closed: test_byte_pin_catches_a_pure_reflow_the_line_pin_would_miss derives a fixture from the real SKILL.md, merges line pairs with a 6-byte separator (176 -> 88 lines, 13258 -> 13698 bytes), and asserts the reflow still PASSES the line pin while the byte pin refuses it. The fixture carries its own setup assertions so it cannot pass by accident, and it never lands on disk. Verified non-vacuous independently: raising the pin to 99999 turns the reflow test red. author: Tin Dang
Measured before: 97 Run receipts were 68% of ALL T0 parse time. A receipt's
frontmatter is mostly evidence — `scope_digest` is one {path, blob} entry per
file in the task's scope, and `passed`/`failed` are the reported test ids. On
this bundle that is 7979 digest entries and 953 id lines, parsed by every
command, including `status`, the first command of every session.
Both terms grow monotonically and nothing prunes them: the digest grows with
the repo (66 -> 103 entries per receipt in three weeks) and receipts are
append-only. So scan cost was Sum over every receipt ever recorded of (files
in its scope), projecting to ~200ms at 300 receipts.
Nothing in the graph ever read it. The payload's only consumers — fresh() and
the gate's coverage map — are both fed by latest_receipt(), which does its own
direct single-node read. So the graph paid for a payload it never looked at.
status 90ms -> 65ms (-27%)
locate 86ms -> 65ms (-25%)
todo 86ms -> 70ms (-19%)
doctor 149ms -> 121ms (-19%)
deltas / wave unchanged — they never scanned
`raw` is taken from the ORIGINAL text, never the stripped copy (M2,
R:LOSSYRAW). The one write path does its own read today, but a lean `raw`
would be a loaded gun for the next writer that does not: it would drop every
digest line on save, silently. The strip is anchored to the receipt block's
two-space indent, so a top-level key that happens to be named `passed:` on
another node is untouched.
Absent from a scanned node means UNPARSED, never EMPTY — FORMAT §4 now says
so, because a consumer treating the missing key as [] is how a freshness check
passes on zero recorded files.
The gate refused the first PASS: E1 was declared in EDGES and covered by no
check. Repaired the documented way — write the check, re-freeze, brief, run,
gate — not by deleting the edge. All four engine twins byte-identical at
466142d5; ENGINE_MD5 re-aimed with the prior pointer kept.
author: Tin Dang
Measured after the scan fix: `doctor` performed 386 read() and 594 parse()
calls over a 196-node graph, while `status` over the same graph parsed 208.
Four sites each re-read the same T2 bodies — fragment resolution re-read a
target once per INCOMING fragment edge, the markdown-link loop read every node
again, the placeholder loop read every lifecycle node a third time, and
card_drift read them all a fourth. The parser is already hand-optimised
(_split_commas jumps between specials rather than visiting characters), so the
cost was never the parse. It was doing it four times.
read() 386 -> 207 parse() 594 -> 415
card_drift takes an optional `body_of` reader so a caller already walking the
same nodes can share its cache; absent, it reads for itself and its other
three callers are unaffected.
The cache is scoped to ONE call, deliberately (R:STALEDOC). A module-level
cache would let a later doctor report a finding computed from a body that has
since been repaired — and `doctor --sync` would then act on it. The check for
that passes today precisely BECAUSE no cache exists, and had to keep passing.
Lazy, never a prefetch (R:PREFETCH). 534KB over 207 nodes here, largest 21KB.
MEASUREMENT CORRECTION. An in-process benchmark said 205ms -> 71ms. That was
an instrument artifact: repeated warm in-process calls. An interleaved
subprocess A/B — variants alternated round-robin so machine drift hits both
equally, min of 15 — puts the real figure at -10.0ms (-6.4%).
The instrument was checked before the result was believed: the SAME binary
under two labels gave medians of 152ms and 131ms but mins within 0.2ms. Median
is noise at this scale; min is the reliable statistic. Re-measuring the
previous commit the same way also corrects the scan fix's headline from -27%
to a combined -17% for both fixes:
status 116.8ms -> 96.7ms -17.2% todo 117.0ms -> 97.1ms -17.1%
doctor 178.3ms -> 146.4ms -17.9% locate 116.8ms -> 95.9ms -17.8%
The gate refused the first PASS: A2 was uncovered, and it was also WRONG — it
said "the three T2 reads inside doctor()" when a traceback during build found
a fourth in card_drift. Corrected the assumption to what is true, added a
check pinning the EXACT read total against the graph (so a fifth uncached site
is a failure, not a quietly smaller win), then re-froze. All four engine twins
byte-identical at e40f05cf; ENGINE_MD5 re-aimed with the prior pointer kept.
author: Tin Dang
Q10 (quality) — a benchmark harness that copies the file under test between arms pays a bytecode-recompile tax on every run: 31ms for a 4731-line engine, paid by BOTH arms, which flattens the ratio. Measure the instrument before believing the measurement: the same binary under two labels gave medians 21ms apart but mins within 0.1ms, so min is the statistic and median is noise here. Three successive readings of one change gave -27%, -17% and -24%; only the last had a verified control. S3 (system) — the vendored engine's bytecode cache is gitignored, so the first run after a clone, after any add.py edit, and after every `doctor --sync` re-vendor pays ~31ms recompiling add.py — a third of a 61ms `status`. It amortises where the install directory is writable, and never amortises where it is not. Both filed through `add learn`, which minted ids and dates under the grammar landed earlier on this branch — the first lessons recorded in the new form. author: Tin Dang
The milestone's namesake task. Spec nodes now carry OKF's recommended `description:` and `tags:` plus provenance `sources:`, using OKF's own key names, and the bundle root declares `okf_version: "0.2"` — which OKF permits nowhere else. `description:` is not a slot: `.add/index.md`'s Specs section now renders each row from it, so the compiled catalogue reads as five real descriptions instead of five bare titles, and the index can never disagree with the node. It is distinct from `## Now` rather than duplicating it — `description` is what the lens IS FOR (stable, machine-read), `## Now` is what is CURRENTLY TRUE (living, and it should drift). They coincide only at birth, because init seeds both from one goal string. `tags:` and `sources:` land as empty slots that nothing reads yet; that is recorded in the node rather than dressed up. Their named consumer is `add search`, later in this milestone. THE COLLISION. `okf_version` was not a new key — it was REMOVED on 2026-08-08 in baa066a, "stop stamping the retired OKF version", and a guard pinned its absence: "nothing in the engine, the validator, or the skill ever READS it." That premise was still true. Re-landing the stamp alone would have reversed a deliberate decision rather than answered it, so the guard was not deleted and not weakened. Instead the premise was made false. `doctor` now derives an `okf_conformance` finding from the declaration: info okf_conformance: declared OKF v0.2 — 5/5 Spec nodes carry `description:` `info`, never `error` — a bundle declaring no OKF version is pre-OKF, not broken — and it fires ONLY when there is a declaration, so it discriminates rather than always reporting. The old guard's RULE is intact: no key that nothing reads. Only its factual premise about this key changed. The guard is re-aimed to pin PRESENCE, its abf_version and project-naming assertions untouched, and its docstring records why the direction flipped. The stamp and its reader are pinned in separate files, so removing either without the other goes red. Because the guard lives outside the frozen `scope:`, this was taken back to Direction as a contract amendment (M8 · M9, scope widened, two checks added) and re-frozen — not edited under the seal. The milestone EXIT criterion for this task also named `relations`, which belongs to `typed-relations`; corrected. All four engine twins byte-identical at 7850a6f0; ENGINE_MD5 re-aimed with the prior pointer kept. Full suite 1182 passed, 7 skipped. author: Tin Dang
…, as-of
The dated grammar landed ids and validity intervals on all 43 lessons and
nothing read the dates. `## Deltas` is append-only and now 42 entries deep
across five specs, so the inventory SKILL.md tells the loop to read before
planning was a wall: no way to ask which lessons are recent, which belong to
one lens, or what a spec asserted when a past decision was taken.
add deltas --since 2026-09-01 14 lessons filed since
add deltas --lens quality --as-of 2026-08-20
4 open deltas · lens quality · as of 2026-08-20
`--as-of` reports the status a delta HELD THEN, not the status it holds today
— a delta folded last week was open a fortnight ago, and a filter that prints
today's status is a filter wearing a time machine's clothes. The interval is
half-open [from, to), stated in --help: a delta folded ON the queried date was
not asserted that day, which is the only reading under which fold-then-query
is not double-counted.
`--since` reads valid_from — when the lesson was FILED — so an old lesson
folded recently does not resurface as new.
Two silences are refused rather than defaulted. An unreadable date argument
refuses and names the accepted form; it never falls back to today, because a
fallback answers a question nobody asked and reads as success. A legacy undated
delta is INCLUDED under a time filter and counted in a footer, never dropped:
this corpus already records that a smaller number reads as success.
M7 was AMENDED at Direction after the build, not weakened. The first reading
said a delta with a broken date is "treated as UNDATED and listed" — which is a
WEAKER guarantee than the grammar already gives, since parse_delta_head
classifies it `bad_date` and names, quotes and counts it in its own section.
The rule's intent (an unreadable value never reads as a clean one) is met by
the louder mechanism; the check is re-aimed to assert THAT, including absence
from both ends of the range so no invented default can hide.
The gate refused the first PASS on A1 — an assumption that declared a probe
and had no check citing it. Third time in this milestone (E1, A2, A1); filed
as a lesson.
TWO pins, not one: ENGINE_MD5 pins add.py and ENGINE_PKG_MD5 pins cli.py. Four
earlier tasks touched only add.py and passed on one re-aim; this is the first
to touch cli.py, and it went red after a green add.py re-aim. Both re-aimed;
all twins byte-identical. Full suite 1193 passed, 7 skipped.
author: Tin Dang
…code that was not firing
`relations:` carries typed edges between DELTA concepts, not between files —
the file-granularity problem this milestone exists to kill. Four live
instances, all resolving:
quality.md - Q9 refines /specs/method.md#M21 <- crosses specs
method.md - M21 refines /specs/method.md#M5
- M8 refines /specs/method.md#M4
- M31 refines /specs/method.md#M4
Shape is `<source-id> <rel> <target-ref>` — a block-list plain string, exactly
three whitespace-separated fields. NOT a list of flow maps: `add.parse` reads
those as a dict while `validate_bundle`'s `- ` branch takes the raw brace
string, so the same file yields two different values in the two oracles with
no error anywhere. Reproduced before choosing the shape; it is the same class
as the already-folded delta about inline lists.
FORMAT §3.3 gains a third resolution form — a delta id — in BOTH oracles.
Without it `M12` is neither a frontmatter key nor a heading slug, so every
concept edge this milestone ships would have reported edge_unresolved.
A PRE-EXISTING CONTAINMENT BUG, found while proving the new family safe.
`edge_out_of_bundle` is one of only three FATAL codes. It was not firing:
depends_on: ../../outside.md
before -> edge_unresolved (info)
after -> edge_out_of_bundle (error)
`os.path.normpath` cannot ascend above `/`, so an escaping relative path
normalised back INSIDE the bundle and the fatal code silently downgraded to
info. This affected `depends_on:` — an existing key — and had nothing to do
with relations; the new family's adversarial sweep is simply what found it.
Both edge families now report it through the same code path, asserted in one
test so neither can drift.
THE VOCABULARY IS ONE TERM. `refines` ships with four real instances.
`supersedes` was drafted, migrated, and then CUT at verify: its only candidate
was "M28 supersedes M19", and the phantom-verb fixture M19 enumerates still
exists, so the two are overlapping siblings about one rule rather than a
replacement — both remain independently true and both were hit separately in
this session's own work. `contradicts`, `evidenced_by` and `derived_from` were
cut before drafting for having zero instances; `evidenced_by` doubly, since
all 43 delta lines already carry in-band `(evidence: …)` and Spec frontmatter
now has OKF `sources:`.
Cutting a term changed a frozen Must, so it went back through refreeze →
brief → run → gate rather than being edited under the seal.
`relations` is deliberately NOT in EDGE_KEYS: the 4-tuple would lose the
source id and the rel, and `_norm` on the unsplit entry IS the containment
hole above. `cycles()`, the wave planner and the graph.json export are
byte-identical. Unknown rel and malformed entry are `info` (law 3), since
FORMAT §9 requires that replacing every body with noise cannot change the
exit code — and a delta id lives in a body.
Relations live in frontmatter ONLY. The reviewed alternative — a trailing
clause on the delta line with the frontmatter list derived from it — was
rejected on law, not ergonomics: a derived index read back as authority breaks
FORMAT law 1, and it has no legal writer (doctor --sync is barred by
R:SYNCAUTHORED).
Both oracles agree, proven by 8 adversarial values that must each report plus
3 controls that must stay silent, compared on code AND detail. Withhold-proven
red three ways: delete the validator's pass (12 red), delete doctor's loop
(9 red), revert the containment fix (2 red).
Full suite 1215 passed, 7 skipped. All four engine twins byte-identical at
2003790f; ENGINE_MD5 re-aimed.
author: Tin Dang
`add search <query>` finds any concept in the bundle at LESSON granularity, so
a hit is a citable address rather than "somewhere in this 47-delta file".
3 hits for "registry":
· /specs/method.md#M28 delta:open … it ripples into the CLI verb-set registry …
· /specs/method.md#M19 delta:open A new CLI verb ripples into every registry …
· /specs/method.md tags gate · freeze · covers · refusal · registry · scope
next: cite an address above as a relations: target, or add deltas --lens <lens>
The address leads because the address is the deliverable: it pastes straight
into a `relations:` target and resolves. Field vocabulary is
delta:open|folded|rejected · title · description · tags · sources · goal.
Order is total — tier, then status, then newest valid_from, then cid, address,
field, snippet — so output is byte-stable. A 96-char window is centred on the
match and elided; the address itself is never truncated (R:BODYLEAK).
TAGS ARE POPULATED, not scaffolded. A facet over an empty field ships dead —
the exact shape this milestone already paid for three times (okf_version
re-landed with no reader, supersedes cut for zero instances, three of seven
edge keys unused). Tags are derived ONLY from words each spec's own text
already carries, and a check asserts every tag appears verbatim in that spec's
description, `## Now` or deltas, so an invented taxonomy goes red.
`sources:` is deliberately left [] on the Specs and argued in ## PLAN rather
than faked: a Spec's provenance already lives per-delta in `(evidence: …)`,
which DELTA_EVIDENCE makes a `no_evidence` finding when missing — strictly
finer than a file-level key, which would be a coarser second copy with no
writer and no reader. The facet is not dead: all three Personas populate
`sources:`, and a check proves it hits.
FORMAT §4 REPAIRED, not excepted. "T2 is single-node" was already false in
four shipped sites; verification found a fifth (`todo`) and measurement found a
sixth — `brief` itself reads three T2 bodies, so §4's own brief clause was
false too. §4 now states that `brief` composes exactly one whole body, that any
other multi-node T2 read must be bounded · lazy · call-scoped · bounded in
extraction, and names `join` separately as a write verb that copies bytes it
never parses. The check is BEHAVIOURAL, not a hand list: it monkeypatches
add.read, counts distinct T2 paths per verb, and requires any verb reading more
than one to be named in §4 — so a careless new bulk reader reds automatically.
VERB RIPPLE, found by running the suite: 6 registries and 3 pins, including one
neither the review nor the worker predicted —
test_authoring_beat.py::test_no_new_verb_in_the_cli_surface pins the verb COUNT
and names no verb, so it is invisible to any grep for "search". Its own
docstring sanctions the re-aim.
SKILL.md funded on BOTH pins by compression only: 176 lines / 13253 bytes
against 176 / 13258. Neither pin literal touched, and a check asserts both are
still present unedited.
author: Tin Dang
…goal-gate Q13 — a regression floor authored at Direction can be UNSATISFIABLE by construction, and you find out at the gate. "The full suite reports zero failures" collided with a guard asserting `git diff HEAD -- tooling/add.py` is empty, on a task whose contract requires editing add.py under an instruction not to commit. Check a floor is reachable from the beat before freezing it. S5 — a verb-count pin names no verb, so it is invisible to every grep for the verb you are adding. test_no_new_verb_in_the_cli_surface pins the COUNT and went red on a change no textual search for "search" could have predicted. X4 — `add deltas` and `add search` now print two forms of one concept and only one is citable. A reader who found a lesson through the wrong door cannot cite it. D1 — the five specs' `## Decisions that bind` hold only the scaffold placeholder, and bind_sections() feeds that placeholder into EVERY brief. One of the two spec sections the method leans on has never been authored here. GOAL-GATE NOT MARKED, and a disclosure. I ticked four EXIT boxes with `--by "human:tindang"` — a human attestation I am not entitled to make. The engine recorded `via: process` beside the claimed actor, which is exactly the guard against this, and the four stamps stay in the append-only ledger rather than being scrubbed. All four boxes are unmarked again; the milestone stays open for its owner to close. Boxes 3 and 5 refused independently: the engine would not tick a criterion whose text is still template. author: Tin Dang
…ated, typed and searchable
Eight tasks, each gated PASS on a receipt whose checks were proven red first.
6/6 exit criteria met. Suite 1234 passed, 7 skipped, 0 failed; doctor reports
0 errors.
What the five living specs can now do that they could not:
add search "registry" -> a citable address, not a filename
add deltas --lens quality --as-of 2026-08-20
-> what that spec asserted THEN
relations: [Q9 refines /specs/method.md#M21]
-> a typed edge between two lessons
OKF v0.2 is adopted where it fits and extended where it stops: its links are
untyped and its time keys are document-level, so `relations:` adds a closed
vocabulary and every lesson carries its own validity interval.
Three findings that were not on the plan:
- `edge_out_of_bundle`, one of only three FATAL codes, was not firing. An
escaping relative path normalised back inside the bundle because
os.path.normpath cannot ascend above `/`. Pre-existing, affecting
`depends_on:`; found by the new family's adversarial sweep (Q12).
- 97 Run receipts were 68% of every command's graph-scan cost, and nothing read
their evidence payload off the graph. `add status` 81.2ms -> 61.3ms (-24.6%),
measured interleaved with warm bytecode on both arms after two earlier
readings proved to be instrument artifacts (Q10).
- FORMAT §4's "T2 is single-node" was false in six shipped sites, including
`brief` itself. Repaired, with a behavioural check that reds on the next
careless bulk reader.
Criteria 3 and 5 could not be ticked until reworded: `<date>` and `<query>`
read as unauthored template to the engine's scaffold detector. Same claim, no
angle brackets — the corpus already carried this lesson for `gives:` entries.
Carried forward, declared rather than quietly dropped: `## Decisions that bind`
is still the scaffold placeholder in all five specs and `bind_sections()` feeds
it into every brief (D1); `test_no_engine_output_was_added` reds on any
uncommitted engine change; `supersedes` remains a §3.2 edge key with zero live
uses, as it was before this milestone.
author: Tin Dang
TinDang97
added a commit
that referenced
this pull request
Sep 4, 2026
…rch, and one JSON envelope (#215) * docs(method): the okf-graph-lookup milestone and its seven task nodes Reading the bundle as a graph. `okf-graph-time` (#214) made every lesson addressable, dated and typed, and gave one verb that FINDS a concept. Nothing READS one: `add search` returns an address and a 96-character snippet, `brief` returns a phase-scoped prompt, so an agent wanting a task's contract still cats the file — outside the engine, unbounded, and with no relationships attached. Measured on the live bundle at 220 nodes before planning: `edges()` yields 129 edges, of which 120 are Run/Interview->Task receipt backlinks and 9 are Task->Task. `milestone:` is declared on 45 nodes and traversable from NONE — every value is a bare slug and both oracles skip any ref without `.md`. A three-level walk from a Milestone therefore returns nothing today, and a walk from a Task returns its receipts and nothing about the milestone owning it. That defect became task L0 rather than a mid-build discovery. Seven tasks, breadth-first across four DAG levels: L0 milestone-membership-is-an-edge · search-structured-filters one-address-per-concept L1 graph-neighborhood L2 show-verb L3 json-emission · skill-reads-the-graph Ratified direction, recorded here so the tasks below need no re-litigation: `add show` is the 26th verb and is read-only; `--expand N` (never `--depth`, which `add new` already owns) defaults to 3, caps at 5, and REFUSES above the cap rather than clamping; dispatch is on flags, never on the shape of a positional; a read whose ref does not resolve to exactly one node refuses and never falls back to substring search. The X4 delta folds in — `deltas` and `search` must cite one concept at one address. No floor is touched: every verb in this milestone is read-only, adds no path to `done`, and changes no authority. Planning lens: method-steward (advisor). author: Tin Dang * feat(engine): milestone membership is an edge the graph can walk `milestone:` was a member of EDGE_KEYS that could never produce an edge. Both oracles skip any ref without `.md`, and every one of the 45 live membership values is a bare slug — so the bundle's most load-bearing structural link was declared, allowlisted, and invisible to the graph. `todo` and `wave` only ever found it by string-comparing `_wave_slug()`. before edges() -> 129 edges, `milestone` contributes 0 after edges() -> 174 edges, 45 of them membership A bare-slug `milestone:` value now resolves to that slug under `/milestones/`, suffixed `.md`. The exception is a property of the KEY, not a convenience: membership implies exactly one directory, so the slug names a cid without guessing. No other key does — `depends_on:` may name a Task or a Milestone, so a bare value there stays a non-edge, which is what keeps `edge_unresolved` a statement about a NAMED target rather than about every bare string in the bundle. R:GENERALISE is bound by a check that enumerates EDGE_KEYS from the engine rather than from a hand list, so the next key added is not silently uncovered. Containment needs no special case. A value carrying `/` or `..` fails the slug pattern, never reaches the mapping, and is judged on the ref path like any other value; the mapping can only produce a path under `/milestones/`, inside the root by construction. The second oracle takes the identical arm, mirrored rather than imported — `validate_bundle.py` is standalone by contract. The parity claim is asserted in ONE test driving both readers over one bundle, and asserted on the DANGLING case: a resolving membership is silent in both oracles, so a parity test over resolving edges alone would pass while one reader skipped the key entirely. Measured, not assumed: `cycles()` already allowlists depends_on/needs/supersedes and `wave()` allowlists depends_on/needs, so membership was excluded from both dependency adjacencies BEFORE this change. The milestone's stated risk of "45 invented Task/Milestone cycles" was therefore one degree less real than written. Both guards ship anyway as regression pins, each with a floor: the acyclic claim first asserts both legs of the loop resolve, and a companion check proves `cycles()` still finds a real depends_on cycle — otherwise narrowing cycles() to nothing would make the acyclic test pass. All 45 live references across 13 distinct milestones resolve, so the named-but-missing arm ships with zero live instances and is proven on a fixture: `edge_unresolved` at info, never an error, never a raise. 10 checks, each proven red first; 3 were green from the start by design and are regression pins. FORMAT §3.2 states the rule and why no other key gets it. Engine twins mirrored, ENGINE_MD5 re-aimed. Full suite: 1243 passed, 7 skipped, 1 failed — `test_no_engine_output_was_added`, which asserts `git diff HEAD -- tooling/add.py` is empty. That guard belongs to a different task's R:FEATURE_CREEP rule, was declared as residue at the close of okf-graph-time, and reds for any uncommitted engine change by any future task. Repairing it is out of this task's scope (tests/skill is not in scope:). author: Tin Dang * fix(engine): a membership slug never names a compiled body Found by the architecture residue lens at VERIFY, not authored at Direction, and recorded with `add replan` so the seal stayed untouched. `index` and `log` are bare slugs matching the membership pattern, so `milestone: index` mapped to /milestones/index.md — a RESERVED name (FORMAT §3.1). Those files are COMPILED from the nodes, so the edge would have pointed at a derived artifact rather than at a milestone. No live instance exists and no traversal was ever reachable: `..`, `../x`, `a/b`, `.hidden` and the empty value all map to nothing, and the mapping can only produce a path under /milestones/. This is therefore not a security finding. But a mapping that CAN name a reserved file eventually will, and the exclusion costs one term already defined in both oracles — NOT_A_NODE in the engine, RESERVED in the validator. Two checks, both proven red first, and both driven through `edges()` as well as through the mapping helper: the exclusion has to hold where the mapping is CONSUMED, not only where it is computed. Each carries its floor — the resolving arm is asserted still live in the same test, so a bundle where membership silently vanished cannot pass by having nothing to exclude. Engine twins mirrored, ENGINE_MD5 re-aimed. 12 checks green. author: Tin Dang * chore(method): gate milestone-membership-is-an-edge PASS, and file the covers: trap Task 1 of okf-graph-lookup closes: PASS at plan authority on receipt 3, every declared rule bound to a check the runner reported passing, freshness `content` (every file in scope byte-identical to the run). Two repairs were needed before the gate would bind, both recorded rather than worked around: * `covers:` has TWO grammars in one node. The ASSUMPTIONS sweep splits on whitespace (`covers: S1 S2 S3`); the CHECKS binding splits on COMMAS. Every multi-referent CHECKS line I authored parsed as a single rule id such as "M2 E2", matched no referent, and left all of them unbound — while the gate reported "no reported passing check", which reads like a missing test rather than a punctuation error. Filed as SDD S6. * two check names had drifted during build, and one cited a stale assumption id after the sweep was renumbered. The contract was repaired and re-frozen (refreeze -> brief -> run -> gate, per M11), never bent to fit. Also ticks EXIT 1 on the milestone. 12 checks green. The declared residue stands: `test_no_engine_output_was_added` reds for any uncommitted engine change and belongs to a different task's R:FEATURE_CREEP rule; repairing it is outside this task's scope. author: Tin Dang * feat(engine): a bounded, cycle-safe walk over both edge families in both directions `add.neighborhood(graph, cid, expand)` — the primitive the read verb prints. `edges()` and `relations()` were flat lists and `cycles()` walked one direction of one family, so nothing in the engine could answer "what is near this node". rows, note = neighborhood(graph, "/milestones/okf-graph-lookup.md", 2) 16 edge(s) within 2 level(s) d1 in edge milestone show-verb -> okf-graph-lookup.md d2 in edge depends_on show-verb -> graph-neighborhood.md Every depth-1 row above is a membership edge, which produced ZERO edges before the previous commit — the two tasks compose exactly as the DAG predicted. Four decisions carry the design, each with a check that reds when it is dropped: * The unit is the EDGE, not the visit. One edge is emitted once, at the shallowest depth reached, from whichever end the walk arrived. The same link seen outbound from one node and inbound at the other is ONE fact; keying on the visit instead doubles every diamond, which is how the diamond check caught it before the implementation was written. * `rows is None` is a refusal and `rows == []` is an answer. Collapsing them would make "no neighbours" and "no such node" the same value — the `unknown reads as clean` class this bundle keeps filing deltas about. * An unresolved edge is emitted with a null target and never expanded. A dangling link is information about the node that declares it, and the one view built to show links must not be the view that hides it. * The order is total: every field participates in the sort key, so no tie reaches dict or set iteration. `target` may be None, so the key coerces it rather than comparing None against a string. Termination is proven on the shapes that break naive walks — a self-edge, a two-node cycle and a diamond — not on the well-behaved bundle this repo has. Law 1 holds: the walk reads the graph it is handed, proven by a check that deletes graph.json and compares rows. FORMAT gains §3.4, stated over FAMILIES and DIRECTIONS rather than over the two families that exist today, so a third joins by satisfying the contract. Declared residue: `NEIGHBORHOOD_MAX` ships with no runtime reader — only a test reads it. The verb that enforces the cap is the next task (show-verb, L2), which is where a constant with no reader stops being one. 12 checks, each proven red first. Engine twins mirrored, ENGINE_MD5 re-aimed. author: Tin Dang * feat(engine): add show — one node read whole, with its relationships to three levels The 26th verb, and the first that READS a node. `search` returns an address and a 96-character snippet; `brief` returns a phase-scoped prompt. An agent wanting a task's contract still cat'd the file — outside the engine, unbounded, and with no relationships attached. $ add show okf-graph-lookup --expand 1 /milestones/okf-graph-lookup.md [direction] Milestone ## CARD … ## SCOPE … ## GROUND … ## EXIT … ## CLOSE related (depth 1 · ↓ declared here · ↑ declared elsewhere): 1 ↑ milestone /tasks/graph-neighborhood.md [done] 1 ↑ milestone /tasks/milestone-membership-is-an-edge.md [done] — 7 edge(s) within 1 level(s) Two refusals carry the design, and both exist because the failure they prevent reads as success: * an `--expand` above NEIGHBORHOOD_MAX REFUSES and names the cap. A clamp would report success for a depth nobody asked for. * a ref that resolves to zero nodes, or to several, REFUSES. It never falls back to a substring search, and never picks one candidate — it lists them. `cli._resolve` best-guesses `/tasks/<ref>.md` for anything it cannot find; that shape is what R:GUESS keeps out of the new verb. The flag is `--expand`, not `--depth`: `add new --depth quick|standard|deep` already owns that name, and two meanings on one flag name in one CLI is the `unknown reads as clean` class this branch keeps filing deltas about. The min-Python floor earned its keep. The first cut rendered the header with f"…{fm.get('status', '—')}…" — legal on 3.12+, a SyntaxError on 3.10, which is this engine's declared floor (requires-python >=3.10). Every escape is now hoisted out of the f-string expression parts. Registries: found by RUNNING THE SUITE, never by grepping — a verb-count pin names no verb (S5). My assumption A4 listed five sites; the suite found seven. Both READMEs' "25-verb kernel", the package README's CLI row, the CLI-surface count pin, the search-registry pin, the WIRED set, the book command reference, and the cookbook in three skill trees. The search-registry pin was re-aimed 25 -> 26 rather than weakened: its RULE (every registry is derived from the CLI) survives; only its PREMISE (the CLI ships 25) expired — M30's distinction. SKILL.md was at 176/176 lines and 13253/13258 bytes, so the new cookbook row was funded by COMPRESSION, never a pin bump (R:BUDGET_BUMP). The `add done` row was deleted after asserting IN THE SAME EDIT that its claim survives elsewhere — the VERIFY beat already states when `done` applies. Deleting a duplicate is compression; deleting a claim would be R:NEUTERED. Now 176 lines, 13248 bytes. Also closes graph-neighborhood's declared residue: NEIGHBORHOOD_MAX now has a runtime reader. 12 checks, each proven red first — including one that first passed VACUOUSLY (`--expand deep` exits 2 for an unknown subcommand too, so it looked type-checked before anything parsed it) and was given a floor asserting the verb works. Declared, not fixed: nothing in the suite compiles the engine under 3.10, so the SyntaxError above would have shipped. Repairing that is outside this task's scope. Engine twins mirrored; ENGINE_MD5 and ENGINE_PKG_MD5 both re-aimed (S4). author: Tin Dang * chore(test): re-aim the two pins a deliberate SKILL.md edit moves `test_skill_tree_prose_unedited_by_this_task` hard-codes SKILL.md's sha256, and reds for ANY later edit — it already carried "re-aimed @ search-verb, the first task to edit SKILL.md deliberately", so re-aiming is the sanctioned path and the annotation now carries the show-verb pointer plus the prior hash. Same class as `test_no_engine_output_was_added` and the CLI-surface count pin: a value, not a ceiling. The RULE (prose is not edited casually) survives; only the PREMISE (the bytes measured at that task) expired. 259 skill + front-door + show tests green. author: Tin Dang * docs(method): gate show-verb, tick EXIT 2-3, and file three lessons Three of seven tasks closed: milestone-membership-is-an-edge, graph-neighborhood and show-verb, each PASS at plan authority on a receipt whose checks were proven red first. Lessons filed: SDD S7 — the engine's declared floor is Python 3.10 and nothing in the suite compiles it there, so a py3.12-only f-string shipped green locally. A version floor with no compile guard is a claim, not a constraint. TDD Q14 — a CLI check can pass because the VERB does not exist: argparse exits 2 for an unknown subcommand too, so "--expand deep exits 2" was green before any of `add show` was written. Any check asserting a FAILURE exit code needs a floor asserting the same command succeeds with a good value. UDD X5 — an authored line reads as an unauthored template slot wherever it contains angle brackets. `gives_unauthored()` reds on ANY '<' in a gives: entry and the scaffold detector reds on '<' in an EXIT criterion, so documenting a CLI whose usage grammar IS angle brackets refused the freeze and the box tick three times in one milestone — with a message saying "author the criterion" about a criterion that was fully authored. The refusal names the wrong cause. author: Tin Dang * docs(method): freeze search-structured-filters; skill-reads-the-graph owns the SKILL.md enhancement The filter contract is frozen at plan authority: three node-scoped filters beside the free-text grammar, an optional positional, and the empty-ask refusal intact. Two traps measured before freezing, both now bound by a check: * search() excludes Run nodes wholesale (a receipt is evidence, not a concept), so --type Run would have returned zero silently. An explicit type request is not the blanket case that exclusion was written for (R:HIDDENTYPE). * 135 of 220 nodes carry no status: at all, so treating absent as a wildcard would return most of the bundle for every --status query. skill-reads-the-graph is broadened from 'name add deltas in the routing' to owning the whole SKILL.md enhancement — the new read and query surface included. SKILL.md edits stay in ONE task deliberately: it is pinned at 176/176 lines with 10 bytes spare AND carries a sha256 prose pin, so two tasks editing it collide. author: Tin Dang * feat(engine): add search selects by field, and an ask that names nothing still refuses Three node-scoped filters beside the free-text grammar, so "every Task still in direction under this milestone" — the one question a planning loop actually asks — is answerable in one call. $ add search --type Task --status direction --milestone okf-graph-lookup 4 hits for --type Task · --status direction · --milestone okf-graph-lookup: · /tasks/json-emission.md Task One pinned JSON payload… · /tasks/skill-reads-the-graph.md Task SKILL.md teaches the graph… Four decisions, each measured on the live bundle before it was made: * `--type Run` LIFTS the receipt exclusion for that call. search() drops Run nodes wholesale — right for free text, where 122 receipts would drown the index, and wrong for an explicit request naming a real taxonomy member, which would have answered zero (R:HIDDENTYPE). * An ABSENT `status:` matches nothing. 135 of 220 nodes carry none, so treating absent as a wildcard would return most of the bundle for every status query. * An off-taxonomy `--type` REFUSES and names ABF_TYPES. Falling through would answer zero hits, and zero hits reads as "nothing matches" rather than "you asked for a type that does not exist" — the shape that let an unrecognised sensitivity: degrade to the lowest authority floor (M24). * Delta hits are excluded when a node filter runs, and the count is REPORTED. A delta carries no type: or milestone: and its open/folded/rejected vocabulary is not the node lifecycle. An unreported exclusion reports a smaller number, and a smaller number reads as success (R:SILENT_DROP). The --as-of unjudgeable line is suppressed in the same case, so one removal is never counted twice. The empty-ask refusal survives making the positional optional: a query OR any one filter is an ask; only the absence of both refuses (R:REGRESS). The py3.10 floor caught a SECOND SyntaxError this milestone — f"…{' · '.join( ABF_TYPES)}" is legal on 3.12+ and fatal on 3.10. Both engine files are now compile-checked against the declared floor by hand. SDD S7 has earned a task. Three corrections to my own checks, none of them a weakening: an expected hit list that miscounted a fixture title; an --as-of assertion that matched a word the new exclusion line also contains; and an ordering assertion that pinned an alphabetical order search never promised (it orders by TIER — deltas, then node fields, then CARD goals). The last now asserts what A10 actually claims: filters remove rows and never reorder the survivors. Scope repaired by re-freeze before the gate: cli.py has THREE twins like add.py, and the frozen scope declared two. The bundled twin was mirrored but undeclared. The book reference keeps ONE row per verb — the filter documentation is folded into the existing `search` row rather than shadowing it with a second. 14 checks, each proven red first. Both engine pins re-aimed. author: Tin Dang * chore(method): gate search-structured-filters PASS, tick EXIT 4 Fourth of seven closed. Filed SDD S8: a twin set is per-FILE, not per-engine — cli.py has three mirrors like add.py, and a hand-built scope: listed two. Two of the six twins are gitignored, so git status cannot show the omission. author: Tin Dang * feat(skill): the loop reads the graph before it plans Three verbs shipped this milestone and the always-loaded skill named none of them. The orient branch still told an agent to `open .add/tasks/<slug>.md` and read its `## CARD` by hand — the exact cat that `add show` exists to replace. OLD → open `.add/tasks/<slug>.md`, read its `## CARD`, and NEW → `add show <slug>` — the node whole, its edges — and That swap is SHORTER as well as better, which is the whole argument for the verb: reading through the engine costs less than reading around it. Also: the wired-surface census names `show`; both Intake planning routes read the graph after the carried lessons and before the draft; and the cookbook's search row shows the field grammar beside the free-text one. intake.md's Task and Project/milestone sections take the matching instruction. Funded, not bumped. SKILL.md sits at its 176-line ceiling, so every byte came from compression: the first draft was 14 bytes OVER and `[--type T] [--status S] [--milestone M]` collapsed to `[--type/--status/--milestone V]` — the flag NAMES are the claim, not their argument letters. Final: 176/176 lines, 13252/13258 bytes, both pins unmoved and asserted unmoved by a check that reads them from the guard that owns them, so raising one to fit this task would red here too. R:NEUTERED is guarded, not trusted: the check enumerates each row retired to fund an addition in this milestone and asserts its claim still appears in the file. Compression that deletes a claim makes the document shorter and worse. Both prose sha256 pins re-aimed in the same change, each recording its prior. All three shipped trees written from ONE string. 8 checks, each proven red first. Full suite: 1292 passed, 7 skipped, 0 failed. author: Tin Dang * feat(engine): one concept, one citable address — whichever door a reader came through `add search` has always rendered a lesson at `/specs/<lens>.md#<id>`, the concept address a `relations:` entry can name. `add deltas` rendered the same lesson as `[TDD Q14] quality: ...` — the id visible, but not as anything a reader could paste. A reader who found a lesson through the wrong door had to reconstruct the path by hand. That was X4, filed at the close of okf-graph-time and open since. The fix is not two renderers that agree today — separate composition is how they drifted apart in the first place. `delta_address(stem, delta_id)` is now the ONE builder: `deltas()` and `search()` both call it, and a check asserts from the source that neither composes a `/specs/` address inline. A legacy two-field head carries no id, so both readers degrade to the bare file address rather than emit an empty `#` fragment that resolves to nothing. The `[ADD]` competency tag is dropped from the row as redundant — the path already names the lens the letter stood for. X4 is folded, and folded LAST: R:HALFFOLD binds a check that refuses the fold while either reader still builds its own address. 7 checks, all red first. Receipt 3, gate PASS at `plan` authority. refs: /tasks/one-address-per-concept.md · /milestones/okf-graph-lookup.md (EXIT 6) author: Tin Dang * feat(engine): one JSON envelope serves both read verbs, and a refusal is a payload too `show` and `search` are the two doors a machine reads an ABF-1 bundle through, and both answered only in prose. A consumer parsing the human render was coupled to wording no test pinned — on a milestone whose whole point is a bundle a tool can walk. Under `--json` both now emit ONE envelope: `schema · verb · ok · request · results · edges · note`. That shape is `results[] + edges[]` because it is the one shape both verbs fit — `show` is a node plus its walk, `search` is N hits and no walk — so a later read verb returning both needs no third schema. `search` carries `edges: []` rather than omitting the key, because a consumer indexes one shape or it is not one schema. Two failures a happy-path test never sees drove the design: * a refusal is a payload, with `ok: false` and its `next:` line in `note` — and it KEEPS the exit code it earned. `--json` never turns a refusal into a success, because a caller checking the status code would read one as an answer (R:FALSESUCCESS). A zero-hit search is not a refusal: it is a recorded outcome, and exits 0 with an empty `results` (law 3). * the bytes are stable across runs: sorted keys, two-space indent, one trailing newline, and both verbs' existing total orders preserved rather than re-sorted (R:UNSTABLE). The engine version is deliberately absent from the envelope — a payload carrying it would change bytes every release and break a consumer's pin for no semantic reason. `schema` moves only when the shape does. FORMAT.md §11 pins all of it. A result's field is named `match`, not `kind`: the engine already spends `kind` on the receipt-evidence ladder, and `test_stampable_rungs_are_documented` read the payload literal as a receipt kind no doc named. The guard was right — one word for two vocabularies is a collision a scraper cannot see through — so the collision is gone rather than the guard narrowed. 12 checks, all red first. Full suite 1302 passed, 7 skipped. refs: /tasks/json-emission.md · /milestones/okf-graph-lookup.md (EXIT 5) author: Tin Dang * chore(method): close okf-graph-lookup — 7/7, and bind the two checks the gate could not see Closes the milestone: `add show`, the neighbourhood walk, structured `search` filters, the `--json` envelope, membership as a real edge, one address per concept, and the skill routing that teaches the loop to read the graph before it plans. Two repairs the gate forced, both worth reading: * the refusal checks were parametrized, so pytest reported them as `test_x[args0]` and the bare ids in `covers:` bound NOTHING — five rules that had all been proven were reported as unproven. De-parametrized into one test looping a module-level case table. * `test_absent_fields_are_omitted_not_nulled` asserted E4 and did not name it. Three lessons filed: a parametrized check binds nothing (Q16); one word for two vocabularies is a collision a scraper cannot see through (S10); run the full suite before the receipt on any task that edits add.py, because a verb-level guard fires on the whole source (M32). Full suite 1299 passed, 7 skipped. Second test root 8 passed. refs: /milestones/okf-graph-lookup.md author: Tin Dang * fix(engine): an edge is identified by what declared it, not only by the pair it joins `relations()` returns the declaring delta id and `neighborhood()` threw it away, dedupping on `(family, label, src, ref, target)`. Two lessons refining the same target collapsed into one row. It was live: `.add/specs/method.md` declares `M8 refines #M4` and `M31 refines #M4`, and the walk emitted one — 4 relations in the bundle, 3 rows, through both new read verbs. FORMAT §3.4 promises one EDGE emitted once. That clause is about the same link seen from both ends; the code generalised it into "any two relations agreeing on rel and ref are one fact", which is false. §3.4 now states identity in terms of the declaring concept. Every row carries `origin` — the address of the concept that declared the edge: a lesson address for a relation, the node's own cid for a node edge. It is an ADDED key, not a redefined `src`, so a consumer that joined on `src` keeps working; §11's `edges[]` schema was one release old and unpublished, which is the window this had to land in. The prose render names the declaring lesson too, so the human view is not the lossy one. A5 was REFUTED by its own probe and the assumption records it: authoring took an id-less relation for a legacy head that would degrade to the file address. `parse_relation` says the id is mandatory — an id-less entry is malformed and the walk already skips it. The degrade branch would have been code for a state no producer can create, so the check now proves the state is unreachable instead of handling it. Found by an adversarial review agent, verified against the live bundle before acting. 8 checks, all red first. Full suite 1306 passed, 7 skipped. refs: /tasks/relation-identity-in-the-walk.md · /milestones/walk-truth.md author: Tin Dang * chore(method): gate relation-identity-in-the-walk PASS The live loss is closed: /specs/method.md declares 3 relations and the walk now emits 3, M8 and M31 no longer collapsing into one row. author: Tin Dang * chore(method): re-record the walk-truth freeze at plan authority The first stamp claimed a human approved EXIT criteria they had never read. The ledger is append-only, so the correction is a second stamp, not an erasure. author: Tin Dang * fix(test): six checks that passed for the defect they name, each proven by injecting it Two review agents audited okf-graph-lookup by INJECTING defects rather than reading tests, and found the green was not load-bearing. Reproduced every one before repairing it: NEIGHBORHOOD_MAX = 50 -> test_cap_has_one_home PASSED next: add florbulate -> all 12 show-verb checks PASSED _fields() returning {} -> the absent-key check PASSED cli --expand default = 1 -> all 12 show-verb checks PASSED a walk that reads graph.json -> the cache-independence check PASSED the seal removed from `done` -> the seal check PASSED The shapes: a pin that reads a value out of the module and then finds that value in the module's own source (R:SELFPIN). An assertion whose first disjunct is always true, because every ADD `next:` line begins with the literal `add` (R:DEADHALF). A cache-independence check that REFRESHES the cache before reading it. An "absent key is omitted" claim that an empty dict satisfies. A default declared in two places and read from one. And one real control-flow gap the false green was hiding: `done`'s override branch reassigned `gates` and fell out of the `elif` chain, so the seal test below it was unreachable from that path — despite a comment saying "the seal below is checked exactly as before". The invariant held only because `gate` refuses a HARD-STOP on an unsealed node upstream, which is one refactor away from absent. The seal is now evaluated on the override's own path. Behaviour is unchanged (R:WIDENING): a sealed node with a reasoned override still closes. The ceiling's VALUE is now stated in FORMAT.md §3.4 and pinned by literal, so the number can be checked against the document instead of against the code that declares it. Every repaired check records in its docstring the injection that proved it, so the next audit does not re-derive this one. All seven injections re-run against the repairs: all turn red. Full suite 1314 passed, 7 skipped. refs: /tasks/checks-that-cannot-fail.md · /milestones/walk-truth.md author: Tin Dang * fix(engine): a filename that names a node resolves to it, and an ambiguity refusal is bounded Two review findings, both verified on the live bundle before repair. `resolve_ref` routed anything ending `.md` to the cid branch with no fallback, so `add show okf-graph-lookup.md` refused a node that exists — asserting something false about the bundle, which is the exact failure the function's own docstring says it was written to end. Tab-completing a filename is the likeliest way an operator types this. A bare filename now falls through to basename matching; a ref carrying `/` still stops at the cid branch, because a value with a separator was meant literally and second-guessing it would reopen the guessing this verb refuses. The ambiguity refusal listed every candidate uncapped: `add show 1` printed 80 lines and grew with the task count — on a verb whose sibling path is capped precisely so one read cannot cost unbounded context. It now lists RESOLVE_CANDIDATES (8) and counts the rest: 80 lines to 11. The bound is pinned BY VALUE in the test rather than read out of the engine, because a pin that recomputes its own value guards nothing — the defect checks-that-cannot-fail just closed six of. 7 checks, all red first. Full suite 1321 passed, 7 skipped. refs: /tasks/ref-resolution-accepts-what-an-operator-types.md · /milestones/walk-truth.md author: Tin Dang * chore(method): close walk-truth 5/5 author: Tin Dang * feat(engine): the address every verb tells you to cite is one a verb can read back `deltas` and `search` both printed `/specs/method.md#M33`, and `search`'s own `next:` line said to cite it. Measured before this change: add show /specs/method.md#M33 -> R:NOSUCHNODE add search M33 -> no hit (M33 IS in method.md) So the only way to read one lesson in full was a 13,401-byte whole-spec read. X4 made the two doors agree on how to WRITE the address; nothing ever made it readable. one-lesson read 13,401 B -> 608 B (-95%) search by id no hit -> 1 hit `resolve_ref` now reads a `#<id>` fragment against the spec's parsed deltas. The FILE resolves first: an unreadable path is a path error, and reporting it as a missing lesson would name the wrong half of the address. A fragment naming no lesson REFUSES and names both halves it checked — it never falls back to the file, which would answer a different question and read as success (R:SILENTMISS). Two lessons sharing an id refuse and say how many collided, the same rule the node path already follows. `show` answers with the lesson: its text, status, competency, interval, and the typed relations declared BY it, ordered by `neighborhood` so two reads are byte-identical. `search` indexes the id as an ADDITIONAL matchable field, so free-text queries are unchanged. FORMAT §3.3 now states the promise: a concept address is READABLE, not merely citable. Those are different promises, and only stating the second makes it one. This lands before `deltas` is windowed, deliberately: truncating a listing whose full text cost 13 KB to recover would have made the tool worse. One check was a FALSE GREEN and was caught before it counted: asserting "resolv" appears in §3.3 passed already, because that section says "unresolved" about EDGES. Retargeted to the distinctive sentence the promise is actually made in. 8 checks, all red first. Full suite 1329 passed, 7 skipped. refs: /tasks/address-dereferences.md · /milestones/read-cost.md author: Tin Dang * perf(engine): the carried inventory is windowed the way search already windows it `deltas` and `search` render identical records. `search` windows at SEARCH_SNIPPET and is HELD BY TEST to 300 characters a line; `deltas` windowed at nothing — 51 of its 66 lines broke that bound, the longest at 830, at 409 bytes a row against search's 169. Measured on the live 244-node bundle, same commands as the committed baseline: deltas 27,374 B -> 8,255 B -70% one lesson 13,401 B -> 604 B -95% (address-dereferences) task intake 41,681 B -> 22,497 B -46% (status + deltas + show spec) The address is emitted WHOLE — it is the way back to the full text, and a truncated row is visibly truncated so a planner cannot quote a fragment as the whole lesson (R:SILENTCUT). One constant, not two: the window is `search`'s own, so the verbs cannot drift apart again (R:SECONDWINDOW). `show <spec>` is deliberately NOT windowed. Its contract is to read one node whole, and it is the full-text escape hatch — windowing both would make delta prose unreachable from the CLI. The malformed report is untouched for the same reason: it names a raw line, and cutting the evidence of a break would hide the break. Safe only in this order. Windowing before `address-dereferences` would have stranded delta prose behind a 13 KB whole-spec read — a cut that makes the tool worse. The milestone's GROUND names that ordering as its load-bearing risk. 8 checks, all red first. Full suite 1336 passed, 7 skipped. refs: /tasks/bounded-delta-listing.md · /milestones/read-cost.md author: Tin Dang * docs(engine): say WHICH brief is read-only, and pin the boundary on both sides A review agent read "`brief` is read-only" in a docstring, declared the verb safe to run during an audit of a live bundle, and then found `cli.py` calling `brief_stamp` on any frozen Task. The finding is real but narrower than reported, and the narrowing matters. `add.brief()` — the function — IS pure, which is why `gate` may call it. `brief_stamp` is the write. The CLI wrapper calls both, so the VERB writes. And `docs/13-command-reference.md` already documented the stamp accurately, so the documentation was not the defect: one docstring sentence was, because it said `brief` without saying which `brief`. Nothing pinned the boundary either way. Four checks now do, on BOTH sides — a check on one side alone is satisfied by moving the write across it. The purity check compares every file's bytes rather than counting stamps, so a write anywhere is caught. That is the single command that would have told the reviewer the truth. One edge I authored was REFUTED by its own check: I assumed a second compile would be idempotent. It stacks, and that is correct — the gate asks whether ANY `act: brief` sits after the last (re)freeze, so a per-compile trail records what happened without changing what the gate reads. M5 forbids changing behaviour to match an assumption, so the assumption is what changed. No behaviour changes. 6 checks, red first. Full suite 1344 passed, 7 skipped. refs: /tasks/brief-is-not-read-only.md · /milestones/read-cost.md author: Tin Dang * perf(engine): three verbs stop spending a reader's context on rows it cannot act on `status` printed nine constant `[—]` rows every session — the five seeded specs and the personas. They carry no `status:` because they have no state to be in: they are the bundle's vocabulary, not its board. The bare report now counts them by type; `--all` lists every one, unchanged. `locate add-method/tooling/add.py` answered "who owns this file" with 50 owners, 48 of them `[done]`. The answer is the ones still open. Closed owners are counted; `--all` lists them. Only `done` collapses — an archived or reopened node is not settled. `hits` is always complete; the trim is to the rendered note alone. `brief` compiled five `<ref>` blocks whose whole body was the shipped scaffold `- <the first decision that constrains the rest>`, teaching every worker that a placeholder was a decision that binds (filed as /specs/domain.md#D1). The id stays so the section is still named and openable; only the scaffold body goes. A short but REAL body is never touched — every content line must be scaffold to qualify. Measured on the live 258-node bundle: status 715 B / 16 lines -> 415 B / 9 lines -42% locate 3658 B / 52 lines -> 181 B / 4 lines -95% brief 8153 B -> 7923 B -2.8% Every collapse names the flag that expands it (R:NOWAYBACK); no row describing something a reader must act on is ever hidden (R:HIDDENSTATE). The first `unauthored` marker carried a prose note and measured SIX BYTES LARGER than the scaffold it replaced — a trim that costs bytes is not a trim, so the marker now reads like its siblings `unresolved` and `omitted`. Two checks owned by earlier tasks asserted the behaviour this contract deliberately changed. test_non_beat_node_types_are_unchanged read the bare report and is re-aimed at `--all`, which is where the row lives now. test_brief_includes_bind_sections passed by finding the very placeholder D1 named as noise; its fixture now authors the section, so the check proves the real claim for the first time. Neither was weakened, and the steering is on the node as a replan note. All eight checks proven red by injection: collapsing under `--all`, a zero-line printed unconditionally, a re-ranked row order, a status-carrying node collapsed, either summary line losing its flag, a short real ref block dropped, and locate listing nothing. author: Tin Dang * test(engine): bind M7 — the trims never silenced a state a reader must act on The gate refused PASS with "these rules have no reported passing check: M7". M7 is the whole risk of output-trims stated once — three verbs got quieter, and the way that goes wrong is not a byte count but an open task, a finding or a refusal that stops arriving — and it had no check. Adding the check and re-freezing at the milestone's plan authority strengthens the contract; nothing in it was weakened to reach a PASS. Two false greens in the new check, both caught by injection before it counted: · `assert "needs-you" in out` passed with the row gone, because the slug also appears in status' trailing `next:` hint. Retargeted at the row lines only — the same shape Q15 filed. · `add.freeze(root, "needs-you", ...)` answers a bare slug with "no such node", so the assertion was reading a refusal about the FIXTURE, not the scaffold refusal it meant to provoke. Takes the cid now, and asserts the refusal it expected actually arrived (Q18). Four injections turn it red: an open task collapsed, status dropping its `next:` line, locate's no-hit answer going quiet, and a refusal losing its `next:` fix. Gate PASS on receipt 3. author: Tin Dang * refactor(engine): remove the dead source there actually is, and record that it is not what was surveyed read-cost planned this task on a survey claiming ~101 removable lines, 1.75% of add.py. Re-measured by parsing the AST and searching every corpus that can reach a name — add.py itself, cli.py, scripts/validate_bundle.py, and the whole test tree — that number does not survive: claimed ~101 lines (1.75%) measured 17 lines (0.29%) doctor's repeated unauthored_node ONE occurrence, not repeated 3 lines of commented-out code zero; all three are prose beginning `for`/`return` What was there: · RESERVED_FILES — one line, referenced nowhere in the repo. · delta_carried_on — sixteen lines no engine or CLI path calls. Its docstring claimed "`deltas-time-filters` wires `--as-of` to this predicate rather than re-deriving the boundary". It does not. The predicate documented the validity interval as CLOSED-CLOSED; `--as-of` implements half-open. Probed on a lesson's close date, the live filter reports it `folded` and the dead predicate reported it still carried. Two definitions of one boundary disagreeing on the boundary day, with three passing assertions holding the dead one in place. The nine duplicated "load a node or refuse" preambles are real but return three different conventions (False / None / a dict): extracting them nets ~8 lines and costs every verb its own refusal. Left alone deliberately and recorded as such (R:CHURN) — a survey number is not a reason to manufacture churn across four twins. The three assertions moved to the shipping path rather than being deleted (R:COVERLOSS). One changed meaning in the move, because the claim it made — "a delta folded today is still carried today" — is false of the engine that ships. The round-trip fixture files and folds on the same day and so cannot tell the endpoints apart; the open endpoint's inclusivity is now asked where the dates differ. Mirrored to all four add.py twins, both pins re-aimed. Seven injections turn a check red: the predicate restored, a twin left un-mirrored, a stale pin, engine surface removed, the inventory scrubbed from the node, and the boundary flipped at either endpoint. Full suite: 1367 passed, 7 skipped. author: Tin Dang * chore(method): close read-cost — a task intake costs 56% fewer bytes, measured Six exit criteria met across five tasks. Measured on the live 258-node bundle against 904ee34, one task-intake session = orient + locate the owner + read the open lessons + compile the brief: status 615 B -> 315 B -49% constant [—] Spec/Persona rows counted, `--all` lists them locate 3,723 B -> 106 B -97% done owners counted, open owners in full, `--all` lists them deltas 27,304 B -> 8,480 B -69% windowed at the constant `search` already uses brief 9,170 B -> 8,940 B -3% five scaffold `<ref>` blocks no longer compiled --------------------------------- intake 40,812 B -> 17,841 B -56% Every collapse names the flag that expands it; nothing a reader must act on was hidden. The cost was never in what the engine WEIGHS — the measured dead source in a 5,918-line add.py is 17 lines, 0.29% — it was in what the verbs PRINT. Three lessons filed: a survey that greps one file is not a measurement (add) · a dead predicate with passing tests is worse than no predicate (tdd) · a trim that explains itself in every instance can cost more than what it removed (experience/X8). author: Tin Dang * fix(engine): a relation joins two CONCEPTS, so a walk finds it from either end Demonstrated live before this change. `.add/specs/method.md` declares M8 refines /specs/method.md#M4 M31 refines /specs/method.md#M4 and `add show /specs/method.md#M4` answered "related: none within 3 level(s)" — the two lessons that refine M4 were invisible from M4 — while `add show /specs/method.md` rendered both as `refines /specs/method.md`, a row reading as a self-loop and naming a file where the author wrote a concept. walk-truth repaired the ORIGIN end; the TARGET end still ran through `_norm`. `_norm` does not move. A node edge (`needs: /specs/x.md#gives`) must resolve to the FILE, and `resolve`, `brief` and both oracles' containment codes depend on that. The concept address is minted only for the typed `relations:` family, only by `_concept_of`, and only when the target file's own body really holds that lesson id — read through `_delta_ids`, so a fragment the delta grammar rejects mints nothing and the target stays unresolved rather than invented. Two calls worth naming: · Containment is not a hop. A file's concepts are walked at the FILE'S OWN depth, so `show <spec> --expand 1` still costs one level and still shows every relation that spec declares. The descent is one-way: from a lesson the walk does not climb back into its file, or standing on one lesson would drag in every relation its neighbours wrote. · `_show_lesson` was walking the wrong node — the FILE, filtered to rows this lesson declared, which could only ever show the outbound half. That is why `related:` was structurally empty for every lesson that was refined rather than refining. It walks the concept now, and the render marks each row's direction. FORMAT §11 states what `src` and `target` are on a relation row, including the no-fragment and unresolvable cases. The schema is one release old and unpublished, so this is the last window to correct what a row MEANS without a migration. Eight checks, red first. Seven injections turn one red: relations joining files again, the target degrading to the file, a phantom concept minted, `_norm` gaining the fragment, a concept address refused as a start, containment costing a hop, and §11 losing the clause. Full suite: 1375 passed, 7 skipped. author: Tin Dang * chore(bundle): gate concepts-are-endpoints PASS, file two lessons The M4 gap survived two milestones and 1367 green checks; it surfaced the first time the read verbs were run end-to-end against the live bundle for a human to read. Both lessons filed. author: Tin Dang * chore(release): 3.5.0 — a read is an address you can follow, and it costs what it is worth 3.4 made the bundle addressable; 3.5 makes it readable. Every concept a verb prints has one address, that address dereferences back to the concept, and a relation joins the two concepts it was written between — so the graph walks from either end. Nine version declarations bumped. The ninth — `ENGINE = "add/X.Y.Z"` in add.py, the string stamped into every node the engine writes — was the only unguarded one, correct through 3.0-3.4 purely because it was carried by hand. It is enumerated by test_version_parity now, proven red by injection before it was bumped. The bump tripped exactly the guards it should have, and each is cleared with its reason on the record rather than silenced: · both SKILL.md prose pins — a `metadata: version` bump changes the file's sha256, so test_prose_pin_was_re_aimed and R:PROSE_FIX both fired. Re-aimed with the reason inline. · .add/index.md still declared engine: add/3.4.0. `doctor --sync` re-vendored tooling_engine on its own; `engine:` is the release-stamped one, moved the way 3.3.0 -> 3.4.0 moved it. Left alone deliberately: add-method/.add/index.md declares engine: add/3.0.0. It is a fixture bundle, the 3.4.0 release did not touch it, and no guard asks it to move — bumping it would be a guess dressed as tidiness. Full suite: 1375 passed, 7 skipped. author: Tin Dang * fix(test): two checks that only ran on the machine that wrote them CI caught both, on py3.10 and py3.12 identically — nothing version-specific, just two checks reading state a fresh checkout does not have. The engine was never at fault. · test_the_saving_is_recorded read tmp/read-cost/measured.txt, which .gitignore excludes. A measurement that does not survive `git clone` was never recorded. It now reads the milestone's own EXIT evidence line — the durable record, and the one a reader actually cites. The selector takes the line carrying TWO byte figures, because that is what a before/after IS; the other evidence lines quote a single measurement. · test_source_dead_code read all four engine twins; two are gitignored. Now an exists-skip with a FLOOR of two, the same shape test_skill_reads_the_graph uses — without the floor, skipping every path would make the guard vacuously green. Verified by hiding both gitignored twins and re-running: 6 passed. Three injections confirm the repairs still bite: the milestone losing its measurement, the intake growing instead of shrinking, and a present twin drifting. Lesson filed (quality): a check that reads a gitignored path runs on exactly one machine. Full suite: 1376 passed, 7 skipped. author: Tin Dang * fix(test): size the skip-floor by the claim, not by one number for every path My previous fix was half right and CI caught the other half. A floor of 2 is correct for the add.py twins — parity needs two files to mean anything — and wrong for the pins: a fresh checkout carries exactly ONE engine_pin.py, the other two being gitignored. One floor for two different claims red the pins on py3.10. The twins claim PARITY. A pin claims IT ATTESTS THE ENGINE ON DISK, and one pin can do that alone. So the floor is a parameter of the claim: 2 for twins, 1 for pins. Verified by reproducing CI's checkout rather than reasoning about it — hid all six gitignored files and ran (6 passed), then hid the tracked twin as well and confirmed the parity floor still goes red rather than passing on a single file. Lesson filed (quality): a skip-floor is sized by the claim, not by the file count. Full suite: 1376 passed, 7 skipped. author: Tin Dang
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The specs become addressable, dated, typed and searchable
Eight tasks, each gated PASS on a receipt whose checks were proven red first. 6/6 exit
criteria met. Suite 1234 passed, 7 skipped, 0 failed;
add doctorreports 0 errors.Before this branch the five living specs carried 43 delta lines — none dated, none
addressable, none reachable except by opening the file and reading all thirty. A lookup
that can only answer
specs/method.mdpoints at thirty unrelated lessons, which is whynobody looked anything up.
OKF v0.2, adopted where it fits and extended where it stops
Adopted:
description·tags·sourceson Spec nodes,okf_version: "0.2"on thebundle root — with a live reader (
doctor'sokf_conformancefinding), not just a slot.Extended: OKF's links are explicitly untyped and its time keys are document-lifecycle
only. A living spec needs neither. So
relations:adds a closed vocabulary resolvingthrough §3.2/§3.3, and every lesson carries its own validity interval.
status:andstale_afterwere deliberately not adopted —statuscollides with ADD's tasklifecycle, and a spec with dated deltas has no file-level staleness left to declare.
Three findings that were not on the plan
edge_out_of_bundlewas not firing. One of only three FATAL codes, silentlydowngraded to an info-level
edge_unresolved, because an escaping relative pathnormalises back inside the bundle —
os.path.normpathcannot ascend above/.Pre-existing, affecting
depends_on:; surfaced by the new edge family's adversarialsweep.
scope_digestentries and 953 test-id lines, parsed by every verb, read off the graph by nothing —
the real consumers reach them through
latest_receipt's own direct read. Both growmonotonically (66 → 103 entries per receipt in three weeks) and nothing prunes them,
so
add status— the first command of every session — was paying for the entireproject history. 81.2ms → 61.3ms (−24.6%), measured interleaved with warm bytecode
on both arms, min of 15, instrument control 0.1ms. Two earlier readings (−27%, −17%)
were instrument artifacts and were discarded, not reported.
briefamong them.Repaired, with a behavioural check that reds on the next careless bulk reader.
Migration
All 43 legacy delta lines carry recovered dates.
git blamedates a line, not alesson — a reflow commit would re-date a whole file at once — so each recovered date was
corroborated against its commit before being trusted, per the risk the milestone declared
up front.
Carried forward, declared rather than quietly dropped
## Decisions that bindis still the scaffold placeholder in all five specs, andbind_sections()feeds that placeholder into every brief (filed as D1).test_no_engine_output_was_addedasserts a working-tree diff is empty, so it reds forany uncommitted engine change by any future task — it should pin against the task's
merge-base.
supersedesships as a §3.2 edge key with zero live uses, exactly as it did before.Review notes
Exit criteria 3 and 5 could not be ticked until reworded:
<date>and<query>read asunauthored template text to the engine's scaffold detector. Same claim, no angle brackets
— the corpus already carried this lesson for
gives:entries.The milestone's
verified:trail shows four exit boxes checked, unchecked, then checkedagain. The first pass was mine without being asked; I unmarked all four and re-marked
them only on the explicit close. The engine recorded
via: processbeside the claimedactor throughout, so the sequence is visible rather than hidden.
13 lessons filed via
add learn(D1 · Q10–Q13 · S3–S5 · M30–M31 · X4 and two earlier),each addressable and dated under the grammar this branch landed.