A bundle a tool can walk — show, a bounded graph walk, structured search, and one JSON envelope - #215
Conversation
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
`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
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
…e 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
…oth 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
…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
`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
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
… 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
…ing 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
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
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
…der 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
… 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
…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
| """ | ||
|
|
||
| import re | ||
| import subprocess |
…he 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
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
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
…en 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
…guity 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
author: Tin Dang
Update — the review found three blockers, and they are fixed on this branchAfter opening this PR I ran two adversarial reviews over the diff. Both found real defects, and The blocker: the walk was losing concept edges
4 relations declared, 3 emitted. That contradicts FORMAT §3.4's "one edge is emitted once" — Fixed: an edge is now identified by what declared it. Every row carries Six checks that could not failA second reviewer audited by injecting each defect rather than reading the tests. Every one of
The shapes: a pin that reads a value out of the module and then finds it in that module's own All six repaired, each re-verified by re-injecting its defect and observing red. Each records its One real gap the false green was hiding
Not raised as a security stop — I could not construct a reachable bypass, so this is Two ergonomics defects
VerificationFull suite 1321 passed, 7 skipped. Second test root 8 passed. All four Still open, declared not repaired
|
|
|
||
| import inspect | ||
| import json | ||
| import re |
| import inspect | ||
| import json | ||
| import re | ||
| import subprocess |
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import pytest |
…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
…y 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
…oth 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
… 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
…t 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
…d 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
… 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
…ither 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
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
…osts 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
| 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. | ||
| """ | ||
| import ast |
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
…ery 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
OKF for the living specs, part two — a bundle a tool can walk
okf-graph-time(#214) made every lesson addressable, dated and typed. This milestone makes thewhole bundle readable by a machine: query it, read a node whole, walk its relationships to
three levels, and get the answer as a pinned JSON payload instead of prose you have to parse.
Seven tasks, 7/7 exit criteria, each on a receipt whose checks were proven red first.
What ships
add show REF— the 26th verb--expandlevels. Default 3, hard cap 5 — an N above the cap refuses, never clamps. A ref naming no node, or several, refuses and lists the candidates rather than guessing.add.neighborhood(graph, cid, expand)EDGE_KEYSuntyped node edges +relations:typed concept edges) in both directions, readingscan()and nevergraph.json(law 1 holds). Emits a total order, so two runs over an unchanged bundle are byte-identical.add searchfilters--type,--statusand--milestonebeside the free-text grammar, positional now optional. An off-taxonomy--typerefuses rather than answering zero. Naming neither query nor filter still refuses.--jsonon both read verbsschema · verb · ok · request · results · edges · note— pinned in FORMAT.md §11, byte-stable across runs.add deltasnow cites a lesson at the same addressadd searchdoes.The finding that reshaped the plan
milestone:was inEDGE_KEYS, declared on 45 of 220 nodes, and produced zero traversableedges — every value is a bare slug, and both oracles skip refs without
.md. A three-level walkfrom a milestone returned nothing. Measuring that before drafting turned it into task L0 instead of
a mid-build surprise. After the fix,
edges()goes 129 → 188 on the live bundle.Membership is now the one edge key whose value may be a bare slug, and the reason is a property of
the key, not a convenience: membership implies exactly one directory, so the slug names a cid
without guessing. Every other key may point at more than one node type, so a bare value stays
unresolved — which is what keeps
edge_unresolvedmeaningful.I had claimed in the milestone draft that this would "invent 45 false cycles". Measurement said
otherwise:
cycles()already allowlistsdepends_on|needs|supersedes, so membership was excludedby construction. The guards stayed as regression pins, plus a non-vacuity check.
Two failures a happy-path test never sees
The
--jsondesign is shaped by them:ok: false, itsnext:line innote, and itkeeps the exit code it earned.
--jsonnever turns a refusal into a success, because acaller checking the status code would read one as an answer. A zero-hit search is not a
refusal: it is a recorded outcome (law 3), exit 0 with
results: [].existing total orders preserved rather than re-sorted. The engine version is deliberately
absent from the envelope: a payload carrying it would move bytes every release and break a
consumer's pin for no semantic reason.
schemamoves only when the shape does.Design decisions worth a second opinion
--expand, not--depth—add new --depth quick|standard|deepalready owns that name.results[] + edges[]— the one shape both verbs fit:showis a node plusits walk,
searchis N hits and no walk.searchcarriesedges: []rather than omitting thekey, so a consumer indexes one shape.
match, notkind— the engine already spendskindon thereceipt-evidence ladder.
test_stampable_rungs_are_documentedread my payload literal as areceipt kind no doc named, and failed correctly. I removed the collision rather than narrow the
guard: a deliberately broad extractor is broad so a kind stamped in an unseen branch cannot
shrink its set.
Please look hardest at
An over-broad mirroring glob clobbered 120 gitignored engine copies — benchmark run
workspaces, two sibling worktrees, the archived 2.x bundle.
git statusshowed 2 modified files,so the usual check was blind to it. All 109 recoverable copies are restored and the restore is
self-verifying: every vendored bundle carries its own
engine_pin.py, so recovery was an MD5match against historical blobs, not a guess. Only disposable
tmp/smoke/scratch still carriestoday's engine. Filed as SDD S9 — a twin set is an explicit list, never a glob.
Lessons filed
S9twin sets are lists ·S10one word for two vocabularies is a collision a scraper cannot seethrough ·
Q15don't pick a row by the text you searched for —searchechoes the query in itsheader ·
Q16a parametrized check binds nothing (pytest renames ittest_x[args0], so fiveproven rules read as unproven at the gate) ·
M32run the full suite before the receipt on anytask that edits
add.py, because a verb-level guard fires on the whole source ·X6a guard'sown prose ages like a README and nothing pins it.
Known and declared, not repaired
requires-python >=3.10).A backslash in an f-string expression part is fatal before PEP 701, and that bit twice this
milestone. This is the highest-value follow-up (SDD S7) — happy to take it as its own task.
test_front_door_claims_hold.pystill tells authorsdeltasprints[LENS] spec: text— trueuntil this branch. It sits in a docstring and a failure message, so no assertion caught it. Out
of the frozen scope (X6).
13-command-reference.mdat the repo root lagsadd-method/docs/, from feat: RISK-ACCEPTED signs for weak evidence never a missing seal, and the ONE approval asks its questions out loud #208.Verification
Full suite 1299 passed, 7 skipped, 0 failed. Second test root (
tooling/, the MD5 pins) 8passed.
ENGINE_MD5andENGINE_PKG_MD5re-aimed; all fouradd.pytwins and all fourcli.pytwins byte-identical.