Skip to content

OKF for the living specs — addressable, dated, typed, searchable - #214

Merged
TinDang97 merged 13 commits into
mainfrom
feat/okf-graph-time
Sep 4, 2026
Merged

OKF for the living specs — addressable, dated, typed, searchable#214
TinDang97 merged 13 commits into
mainfrom
feat/okf-graph-time

Conversation

@TinDang97

Copy link
Copy Markdown
Collaborator

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 doctor reports 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.md points at thirty unrelated lessons, which is why
nobody looked anything up.

add search "registry"                              # a citable lesson 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, adopted where it fits and extended where it stops

Adopted: description · tags · sources on Spec nodes, okf_version: "0.2" on the
bundle root — with a live reader (doctor's okf_conformance finding), 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 resolving
through §3.2/§3.3, and every lesson carries its own validity interval. status: and
stale_after were deliberately not adopted — status collides with ADD's task
lifecycle, 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_bundle was not firing. One of only three FATAL codes, silently
    downgraded to an info-level edge_unresolved, because an escaping relative path
    normalises back inside the bundle — os.path.normpath cannot ascend above /.
    Pre-existing, affecting depends_on:; surfaced by the new edge family's adversarial
    sweep.
  • 97 Run receipts were 68% of every command's graph-scan cost. 7979 scope_digest
    entries 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 grow
    monotonically (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 entire
    project 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.
  • FORMAT §4's "T2 is single-node" was false in six shipped sites, brief among them.
    Repaired, with a behavioural check that reds on the next careless bulk reader.

Migration

All 43 legacy delta lines carry recovered dates. git blame dates a line, not a
lesson — 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 bind is still the scaffold placeholder in all five specs, and
    bind_sections() feeds that placeholder into every brief (filed as D1).
  • test_no_engine_output_was_added asserts a working-tree diff is empty, so it reds for
    any uncommitted engine change by any future task — it should pin against the task's
    merge-base.
  • supersedes ships 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 as
unauthored 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 checked
again. 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: process beside the claimed
actor 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.

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
TinDang97 merged commit 904ee34 into main Sep 4, 2026
8 checks passed
@TinDang97
TinDang97 deleted the feat/okf-graph-time branch September 4, 2026 04:17
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant