Skip to content

Rewrite the handoff as one document - #6

Open
admercs wants to merge 119 commits into
masterfrom
handoff
Open

Rewrite the handoff as one document#6
admercs wants to merge 119 commits into
masterfrom
handoff

Conversation

@admercs

@admercs admercs commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

HANDOFF.md had become a 2026-08-04/05 handoff with two sessions bolted onto it: an open item struck through in place, a bug table that grew by accretion, and a "found and left alone" list holding one entry that is not a finding at all — the pub-only effect rule, now properly specified in MAGE_SPEC.md §11.4.

Rewritten for the reader it claims to be for: someone picking this up cold.

What changed

Open items are grouped by what would unblock them, because "open" was doing three different jobs:

  • Waiting on a decision, not work (3): GPU runner, TLS posture, RAP error shape.
  • Deliberate, not defects (4): rmi's vendored warnings, declined migration steps, separate workspaces, external dependency resolution.
  • Real work, unstarted (2): resume for effect handlers — the largest unbuilt thing in the language — and the int literal constraint.
  • Small, sharp, cheap (4): promoted out of prose into an actionable list.

/ agent is called out as the best next task. agent is a documented effect in §11.2 that the parser rejects, because it lexes as a keyword. Same spec-versus-implementation gap as §11.4, and much smaller.

The bug table runs to fourteen, and the class --check cannot see is now counted — seven of the fourteen — rather than merely described.

Two new traps: gh pr merge --auto merges immediately when a repo has no required status checks (this is how PR #4 landed with CI pending), and tight timeouts go flaky under whole-suite load.

Three counter-lessons added to "the pattern worth carrying forward", two of them mine:

  • A comment agreeing with your expectation is not evidence.
  • Reading is not verification, including when you wrote the thing — the f-string claim was false and was written into a document about not doing that.
  • Fixing a class of bug confers no immunity to committing another instance of it. Bug 14 went in during the feature built to eliminate bug 14's class, and shipped past twelve new tests, a green CI run, and the output pin.

Claims checked rather than carried over

The doc's own standard is that measured claims get run, so:

  • / agentparse error: expected identifier, found KwAgent; / unsafe likewise. Verified.
  • agent is in the §11.2 effects table — MAGE_SPEC.md:1325. Verified.
  • 10 CI jobs — counted under jobs: rather than grepped. My first grep said 11; it was including push: and pull_request: under on:. The inherited figure was right and my check was wrong.
  • 12 of 12 examples typecheck, run, and print their recorded answers. Verified.
  • 0 clippy warnings across the four owned crates. Verified.
  • scripts/test-all.sh --check-docs green — all 45 pinned counts still match, so the rewrite preserved every pattern check-doc-counts.sh keys on.

Docs only; no code changes and no test-count changes.

🤖 Generated with Claude Code

NERVOSYS and others added 30 commits August 12, 2026 11:13
…f patches

It had become a 2026-08-04/05 handoff with two sessions bolted onto it, an open
item struck through in place, and a "found and left alone" list holding one
entry that is not a finding at all — the pub-only effect rule, which is now
properly specified in MAGE_SPEC.md §11.4.

Rewritten for the reader it claims to be for: someone picking this up cold.

The open items are now grouped by what would actually unblock them, because
"open" was doing three different jobs — three waiting on a decision, four that
are deliberate and not defects, two that are real unstarted work, and four small
contained fixes. `/ agent` is called out as the best next task: `agent` is a
documented effect in §11.2 that the parser rejects, which is the same
spec-versus-implementation gap as §11.4 and much smaller.

The bug table runs to fourteen, with the class that `--check` cannot see now
counted (seven of the fourteen) rather than described. Two new traps: `--auto`
merging immediately when a repo has no required status checks, and tight
timeouts going flaky under whole-suite load.

"The pattern worth carrying forward" gains three counter-lessons, all of which
cost time this week and two of which are mine: reading is not verification even
when you wrote the thing, and fixing a class of bug confers no immunity to
committing another instance of it — bug 14 went in during the feature built to
eliminate bug 14's class.

Claims checked rather than carried over: `/ agent` and `/ unsafe` are parse
errors (verified), `agent` is in the §11.2 table (line 1325), 10 CI jobs
(counted under `jobs:`, not grepped), 12 of 12 examples green, 0 clippy, and
`--check-docs` still matches all 45 pinned counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the rest

`agent` is a documented effect in MAGE_SPEC.md §11.2 and could not be written.
Two independent failures on the one row: `agent` lexes as the keyword that
introduces an `agent` item, so the annotation never parsed — and it was not a
built-in kind either, so it would have failed the unknown-effect check if it
had. Effect-annotation position is unambiguous, so a name there is now taken as
an effect name whatever it lexes as elsewhere; `/ 3` still fails, and with a
better diagnostic than before.

Then the other sixteen rows of §11.2 were run, which is what the handoff asked
for. The names all worked. The column labelled "Operations" did not: of the 41
operations it names, 22 perform nothing. `dispatch`, `generate`, `lifecycle`,
`random`, `forward` and the rest have no `effect` block behind them and are
attributed by nothing, so a function calling them is pure. The column now says
"domain", and a second table lists the names really attributed on call — which
is deliberately short, because attribution is by bare name and every entry
claims a word out of the whole program's namespace. Both tables are pinned.

MAGE_ONTOLOGY.json published ten effect names, four of which — `db`, `log`,
`tools`, `rand` — the compiler rejects as unknown effects, while ten real
built-in kinds were absent. Three of the four are capability *namespaces*
(`db.query(…)`, resolve.rs), a different list folded into this one; `rand` was
`rng` under a name nothing has ever accepted.

That one is not a documentation bug. The ontology is what an agent grounds on,
and CI already pinned it byte-for-byte against a fresh --emit-ontology. The
check was working perfectly and guaranteeing a wrong answer stayed identical.
A pin guarantees agreement, not truth. The missing check was the one that
crosses the boundary: not "does the file match its generator" but "does every
name the file publishes actually work" — which is what the new test does, in
both directions. The old test asserted only that `db` *appeared in the file*.

Six new tests; prototype 1,107 -> 1,113, all counts re-checked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
internals/03 and internals/05 both print a `pub enum Effect` as if quoting the
source. Neither matches: `Io`, `Unsafe`, `Db` and `Log` are not variants, and
eleven real ones are missing. Corrected to hir::Effect as it is.

Three sections of internals/05 are design, not implementation, and are now
marked as such rather than rewritten — whether to build them is somebody's
call, not a cleanup:

  §5.3  effect hierarchy — `is_sub_effect` does not exist; effects are flat,
        `fs` is a kind in its own right rather than `Custom("fs")` under `io`
  §5.4  Forge.toml `[capabilities] allow-io` — forge parses no such table
  §5.5  effect polymorphism — `/ *` is a parse error
  §5.7  E0401/E0402/W0410 and caret diagnostics — none exist, and W0410
        contradicts §11.4, where over-declaration is deliberately allowed

§5.6 is the opposite case: user-defined effects *do* exist, and every snippet
had the wrong syntax (`handle / Rng { … } with { Rng.op(…) => … }`, operations
without `;`). Rewritten to the real form and every snippet run — the example
evaluates to 5. The §5.2 algorithm gains the two steps it never had, effect
operations and handle regions, and loses the over-declaration warning.

The handoff's small-and-sharp item for `/ agent` is done; §11.2 has been read
with the suspicion it asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handoff reported this as "a `(` after a block parses as calling the block",
and noted that any statement in between separates them. The second half was
wrong, and it was hiding the bug:

    v a = 1
    (2 + 3)

called the literal `1`. Nothing about blocks is involved. Statements here are
newline-terminated and the postfix loop could not see the terminator, so any
expression followed by a line opening with `(` was a call. The block case is
just the one anybody noticed, because a block is what you most often want to
follow with a parenthesised line. `[` had it too: an array literal opening a
line was an index on the previous statement.

The fix is the rule the parser already applies two arms up. Postfix `?` breaks
on `newline_before_current()` — its comment explains that `7\n ? c {}` would
otherwise parse as `(7?)…` — and `expect_stmt_end` treats a newline as a
terminator. `(` and `[` now break on the same condition. Multi-line argument
lists are untouched: their `(` hugs the callee on the callee's own line.

Both tests were run against a build with the two guards deleted, and both fail
there. That check was not ceremony — the first version of these tests passed
without the fix, because they asserted statement counts and the thing that
changes is the tail expression.

prototype 1,113 -> 1,115.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handoff said naming a variable `f` yields `unresolved name: val`. Running
it gives two different wrong errors:

    v f = 3      expected expression, found KwF 'f'   right token, wrong story
    var v = 3    unresolved name: `var`               wrong token entirely

One mechanism behind both. `is_let_statement` decides whether `v`/`val` starts
a binding by peeking at the next token against a list of keywords that may
serve as names — `val`, `guard`, `data`, `query` and two dozen more, because
the lexer eagerly tokenises ordinary variable names as keywords. The sigil
letters are not on that list, so the statement is not recognised as a binding,
the binding keyword falls through to expression position, and the resulting
error is about the wrong token by construction. `unresolved name: var` is not
a bad message — it is a correct message about a parse that should never have
happened.

The list is extracted to `is_binding_name` so it has one home rather than
being inlined in the peek, and the failing shape is claimed deliberately: a
binding keyword, a keyword, then `=` or `:` is unambiguously a binding whose
name is a keyword, so it routes into `parse_let_stmt` and reports there,
pointing at the name. The hint naming the other spelling (`f` is `fn`, `S` is
`struct`, `ret` is `return`) is computed from `lexer::KEYWORDS`, so it cannot
drift from the table that causes the collision.

All thirteen names now report at the name's own line and column. They still
cannot be used as identifiers; that is the larger change, and the handoff was
right that the diagnostic is separable from it.

Test verified by deleting the new check and watching it fail. Separately,
`v guard = 2` followed by `guard + 1` is broken — confirmed pre-existing by
stashing and rebuilding, not by reasoning — and is recorded as its own item.

prototype 1,115 -> 1,117.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`resolve.rs` registered twenty capability namespaces and said their "use is
tracked by the effect system". Nothing tracked them. With a control showing the
checker was live, on a `pub` function declared pure:

    println(s)        caught - performs undeclared effects: [IO]
    io.println(s)     clean
    fs.open(p)        clean
    net.connect(h)    clean
    llm.generate(p)   clean
    gpu.dispatch(k)   clean

Same shape, same visibility. The bare name was caught; the namespaced form —
the one the design calls primary — passed. A generated program could open
sockets, reach an LLM, or dispatch to the GPU while advertising itself as pure.
The gate was open at exactly the seam the language documents as the way through
it, which is the worst place for it: the safe-looking code was the unchecked
code.

Attribution now comes from the receiver, the rule `Audit.record(x)` already
used for declared effects. `hir::CAPABILITY_NAMESPACES` is the single source of
both the registered names and the attribution, so a namespace cannot be added
without deciding what it performs.

Three things fell out:

  * A seventeenth built-in kind, `proc`, for `os` / `sys` / `process` /
    `tools`. Nothing named them, and attributing them to `io` understates them:
    `process.spawn(…)` is arbitrary code execution, which for a program a human
    did not write is the question. `tools` belongs with it — invoking an
    external tool is the agentic spelling of the same capability.

  * `agent.spawn(…)` did not parse, and the new test is what found it. `agent`
    lexes as the keyword introducing an `agent` item, so it failed in
    expression position exactly as it failed in annotation position earlier
    this session — same root cause one seam over, and the earlier fix had not
    generalised. `swarm` and `kb` were the same.

  * `json`, `kb`, `db` attribute nothing, deliberately. Inventing a `Custom`
    would infer an effect that §11.4 then refuses in an annotation, leaving no
    way to declare what you perform. `effect Db { … }` is the path that works.

Nothing shipped and verified broke. The only in-repo callers are in `stdlib/`,
which does not parse today and which no script or CI checks — the examples'
story again, one directory over.

prototype 1,117 -> 1,121.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The effects section had been 40% wrong, so the question was what the other
twenty-one would say if anyone ran them.

  project_layout, docs (29 paths)  clean
  vocabulary (31)                  clean
  rap_methods (37)                 clean
  keywords (102)                   19 wrong
  types (30)                       3 unusable
  sigils (38)                      2 did not parse

`keywords.introduces` was two fields wearing one name. For the 83 keywords with
no curated entry it was the real token kind; for the 19 with one it was a
hand-written name — `agent` claimed `AgentDef`, `match` claimed `Match` (it is
`QuestionEq`), and `val`/`var` claimed `Let`, a token this language removed and
now rejects on sight. Nothing marked which kind of answer you were reading, so
the field could not be joined against anything. It is now always the token the
spelling produces; the curated prose stays in `summary`, where it was always
the useful part.

Three documented types could not be written. `S` was published as a shorthand
for `String`, but `S` is the `struct` keyword and can never be a type;
`Map[K,V]` and `Set[T]` are spelled `{K: V}` and `{T}`.

`!` is published as Break, the spec agrees, and it parsed nowhere — prefix `!`
always demanded an operand, so only the spelling the lexer itself calls legacy
(`break`) worked. Implemented: `!` with no operand is break, which is
unambiguous because logical-not requires one and break cannot take one.

`^` was the ontology being wrong, and I got it backwards first: I implemented
`^` as return to make the claim true, then had to add a newline guard when
`m x = 7` / `^ x` parsed as `7 ^ x`. That guard was the warning sign. The spec
names `ret` in both sigil tables and mentions `^` once, as bitwise XOR — and
`^` is already the `^T` Box prefix, so it was double-booked before a third
meaning was proposed. Reverted; the entry now reads `ret`.

The ontology describes the language, it does not define it. When a generated
artifact and the spec disagree, the artifact is the likelier suspect, and
reaching for a special-case parser guard to support a one-character spelling
means the spelling is wrong, not the parser.

Four sections now have executable pins: keywords introduce the token they
claim, documented types can be written in a signature, published control sigils
parse, published paths exist.

prototype 1,121 -> 1,125.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`check-examples.sh` pins the twelve in `examples/`. Nothing looked at the other
eighty-nine, and 36 of those do not typecheck. The largest block is the
standard library: `stdlib/` is 25 files, 4,402 lines, and all of it is Rust —
`pub trait Read`, `&mut self`, `let total = 0usize;`, `pub mod agent;`.

Nothing read it. The compiler never opens `stdlib/`, no script checked it, and
`u std.io` resolves without it because imports are nominal. So 25 files claimed
to be the standard library of a language they were not written in, and an agent
reading `stdlib/std/io.mg` to learn MAGE idiom learned Rust. A file with no
consumer has no error message.

`scripts/check-mg-sources.sh` is the consumer, and runs in CI. Every `.mg` file
typechecks or is listed with a stated reason; the list can only shrink. Its
first run found eleven more beyond `stdlib/`: an older example set in
`prototype/examples/` that drifted exactly as the shipped twelve had, and four
`framewerx` modules referencing `Tensor` / `Module` / `ParamStore` — type names
nothing defines and neither the ontology nor the spec publishes.

Two compiler bugs came out of reading the failures.

Keyword-as-identifier, fourth and fifth positions: `M agent { }` and
`u std.agent` were parse errors. Same collision fixed twice already today, each
time patched only where it had been noticed. `agent` and `swarm` now go in
`expect_ident`, covering every identifier position at once — patching
per-position had a perfect record of leaving another position broken.

The prelude reserved eighty words globally: `M net { }` reported `duplicate
definition: net` against a definition the author never wrote and could not see,
because the capability namespaces, the vocabulary and the builtin functions are
registered into the same root scope as the program's own items. That makes the
obvious module names for a standard library unusable. Source definitions now
shadow prelude names; two source definitions of one name are still a duplicate,
with a test for that specifically — a shadowing rule that quietly disabled
duplicate detection would be worse than the bug it fixes.

Also corrected: last commit said nothing shipped and verified broke when the
capability gate closed. That came from a grep over `examples stdlib framework`,
which misses `prototype/examples/`. `hello.mg` there calls `io.println` and
declared nothing, so the gate caught it — working as intended, but the claim
was made from an incomplete search. It now declares `/ io`.

And the committed ontology was stale: `--emit-ontology` had been run from a
binary built before the `^` -> `ret` sigil fix landed. Regenerated. That is the
CI ontology step's whole purpose, and it earned its keep.

prototype 1,125 -> 1,128.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chasing why `stdlib/` could rot undetected reached the cause. `use` brings
nothing into scope, and said nothing about it:

    u totally.made.up.path        accepted, silently
    u std.io.read_to_string
    read_to_string("x")           error: unresolved name: `read_to_string`

The import is accepted and the failure surfaces at the call site — pointing at
the one line the author wrote correctly. An import naming nothing was
indistinguishable from one naming something real. `resolve_use` was an empty
function under a comment describing what it would do, and internals/03 §3.2
documented four resolution steps and six import styles, none of which happen.

`use` now warns. Not an error: rejecting the syntax would break the corpus for
no gain, and the syntax is not the problem — the silence was.

The finding underneath is a design fact that was nowhere written down: MAGE has
no module system, and the library surface is global. The 31 vocabulary
combinators, the 20 capability namespaces and the builtin functions are in
scope everywhere, with no import and no tokens spent on one. For a language
optimising for token efficiency that is plausibly the right answer rather than
a gap — but it is also what makes a hand-written `stdlib/` unreachable by
construction. §3.2 now states it, with the import table marked parses/resolves.

So `stdlib/` is not rewritten. Translating 4,402 lines into MAGE would produce
a library nothing can import, which is exactly how the directory got here;
rewriting without a consumer reproduces the bug. It carries a README saying
what it is, why it is not source, and the ordered prerequisites for making it
real — module system, then a consumer, then port module by module, removing
each from the sketch list as it starts checking. Steps 2 and 3 are worthless
without step 1, and step 1 is a decision rather than work, so it is now open
item 10 instead of an implied backlog.

prototype 1,128 -> 1,129.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Finishing the ontology audit — `ir_ops` (107, generated from `Op::ALL`),
`framewerx_modules` (256 over 56 paths) and `layer_map` (21) all came back
clean — but running the last one found this:

    net N { layer a: Linear(8, 128); layer b: Lienar(128, 64); ... }
    Status: OK

`Lienar` is a typo of `Linear`. It resolved to nothing, lowered to
`Op::IDENTITY` — a pass-through — and the net compiled and ran with that layer
silently doing nothing. So did `NotALayer`. Not a crash: a wrong answer.

The translator recorded these in `unknown_layers` all along. The only readers
were the ABL-lowering path and a `train` warning, so `--check` never mentioned
it and nothing on the ordinary path did either. Detected, recorded, and never
surfaced is indistinguishable from not detected. It is now a check-time error,
emitted from `check_module_shapes` before the shape pass so that a net whose
input shape cannot be inferred still gets told. Documented aliases are
unaffected: `ReLU`/`Relu` both resolve, `Dense` is `Linear`.

The test for this was wrong twice, in ways worth recording:

  * It passed with a bogus layer name deliberately injected, because it called
    `types::check` and the validation lives in `abl_shape::check_module_shapes`
    — a different pass. Verifying against the wrong entry point is a way to be
    green about nothing, and it is invisible unless you break the thing on
    purpose and watch.

  * Pointed at the right pass, it was circular: `layer_map` is filtered by
    `layer_name_to_op` and the check rejects exactly when that returns `None`,
    so the published list cannot contain a name the check rejects. Its doc
    comment now says so, and the real test uses names in no list.

A pin over a generated list tends toward tautology, because both sides come
from one source. The escape is inputs the generator has never seen.

prototype 1,129 -> 1,132.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sketch list is designed to shrink, so I started clearing it.
`prototype/examples/structs.mg` — 33 lines, the smallest — claimed four
features in its header, two of which do not exist (`-> T = expr` expression
bodies and the `|>` pipeline operator, documented nowhere). The file had never
compiled. Rewriting it to demonstrate only what is real found three bugs.

`data` is published as "record or sum type". The sum half registered no
variants. `data Point(x: f64, y: f64)` worked, so `data` looked implemented;
`data Shape = Circle(f64) | Rect(f64, f64)` parsed and then gave `unresolved
variant in pattern: Circle` and `unresolved name: Rect`. `resolve.rs`
registered the type name and nothing else, while the `E` enum arm three cases
up registered every variant.

Fixing resolution alone got `Errors: 0` and `eval error: unknown function
`Rect``: the evaluator collects variants for `ItemKind::Enum` and had no arm
for `ItemKind::Data`. That is the bug-7 class from the example rewrite, and the
reason the example pin runs `--eval` instead of trusting an exit status.

A third bug sat underneath and affected `E` enums equally: a *bare* constructor
(`Rect(3.0, 4.0)` rather than `Shape.Rect(3.0, 4.0)`) typechecked and died at
run time, with only `Ok`/`Err`/`Some`/`None` special-cased into working. The
bare form is what the concise `data Shape = …` syntax invites, since that
syntax never names the type. Bare constructors now resolve, with two guards:

  * Ambiguity is an error naming both enums, not a silent pick — choosing one
    would resurrect bug 6, where variants keyed by name alone let `Left { X }`
    and `Right { X }` evict each other.
  * The message is sorted. `enum_variants` is a HashMap, and an ambiguity
    report naming the enums in a different order each run cannot be acted on or
    tested. Verified over three runs.

`structs.mg` checks, evaluates to 37, and is off the sketch list: 36 -> 35.

prototype 1,132 -> 1,137.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Next file off the sketch list, `prototype/examples/effects.mg` (45 lines),
claimed five features. Rewriting it found the biggest single gap yet.

`MAGE_SPEC.md` defines the closure form in its formal grammar —
`closure_expr = '|' [ param_list ] '|' ( expression | block )` — and lists it
again under supported features, "Closures (`|x| expr`)". It was a parse error.
Only `f(x) => expr` worked, which the spec never mentions anywhere.

The standard vocabulary is built on higher-order functions: map, filter, fold,
any, all, find, group, scan. So the documented way to pass a function to any of
them was the one that failed, and it is the form every model emits by default.

Implemented. Unambiguous for the same reason prefix `!` is: `|` is a binary
operator needing a left operand, so it never begins an expression; `||` lexes
as `Or` and is the zero-parameter closure. Binary `|` and `||` are untouched,
including inside a closure body (`|x| x | 1`), with a test for each. The
`f(x) => expr` spelling still works.

Deliberately not the `^`-as-return mistake repeated: there a single ontology
line claimed a meaning the spec contradicted, so the entry was wrong. Here the
spec is the claimant, twice, and the spec defines the language — so the
compiler was what had fallen behind. The check is the same either way: find out
which artifact is authoritative before changing anything.

`effects.mg` checks and evaluates to 24. Sketch list 35 -> 34.

Three small example files rewritten so far, six compiler bugs out of them. The
rate has not dropped and three files remain.

prototype 1,137 -> 1,139.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`prototype/examples/analysis.mg`, 104 lines, the broadest of the sketch set.
Four bugs, in ascending order of how badly they fail.

Default arguments were parsed and discarded. `Param::default` has always been
in the AST, with a parser test asserting it was *stored* — and read by nothing.
`f g(a: i32, b: i32 = 2)` parsed, and `g(1)` failed with `expected 2
argument(s), found 1`, pointing at the call rather than at the default that had
been ignored. Now honoured in the checker and the evaluator. Only trailing
defaults may be omitted, since a middle default would make a positional call
ambiguous. The spec's `param` rule said `IDENT ':' type`; it now says what the
language does, with a worked example that was run.

Generic calls do not typecheck: `identity(1)` evaluates to 1 but reports `type
mismatch: I32 vs sym1` — the type variable is never instantiated at the call
site. Left declared-but-uncalled with a comment. Real type-system work, not a
cleanup; belongs with open item 9.

`println` did not evaluate. Registered as a builtin, typed, attributed IO — and
the evaluator had no arm, so `println("hi")` checked clean and died with
`unknown function`. The most common function in the language. It survived
because no shipped example calls it: the example pin records each example's
returned value, and all twelve return theirs rather than printing. A pin covers
only what it exercises. `print`/`eprint`/`eprintln` were the same.

And the one that matters most: a bare unit-variant pattern bound instead of
testing. `?= s { Circle => 0, Square => 4, Triangle => 3 }` bound `s` to a fresh
variable named `Circle` and took the first arm — every time, for every input.
`sides(Square)` returned 0. Checked clean, ran, silently wrong: bug 5 from the
example rewrite in a different spelling, in a file nothing had ever executed.

A name that *is* a zero-field variant now tests rather than binds. Ordinary
binding patterns are untouched, and a name two enums both declare keeps binding
rather than being silently resolved to one — same ambiguity rule as the bare
constructor.

analysis.mg checks and evaluates to 102. Sketch list 34 -> 33.

Four example files rewritten, ten compiler bugs out of them. Two files remain.

prototype 1,139 -> 1,145.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It had grown back into what the last rewrite removed: eight chronological
"here is what I found today" sections, useful to whoever wrote them and to
nobody picking this up cold. 869 lines -> 378.

The change that matters is structural. Instead of narrating twelve commits,
the bugs are grouped into six shapes, ordered by how badly they fail:

  1. silently wrong answer        (compiles, runs, wrong number)
  2. typechecks, does not evaluate
  3. accepted and silently discarded
  4. documented but unimplemented
  5. the document is wrong, not the compiler
  6. parse ambiguity / diagnostics naming the wrong token

The shapes repeat, so knowing them tells you where to look next — which a
chronology cannot. A new section states what the compiler *actually is*
(effect system, no module system, global library surface), because several
documents said otherwise and someone reading cold has no way to tell.

While writing it I changed "CI | 10 jobs" to "11" without measuring — counting
the `push`/`pull_request` trigger keys as jobs. That is exactly the class of
error the document is about, committed inside the document about it. Corrected
to 10, and the figure is now measured by `test-all.sh` and checked, per the
document's own rule that a measured claim goes into CHECKS in the same commit.
Verified in both directions: 45 checks -> 46, and an 11 fails.

The first attempt at wiring it appended to CUDA_CHECKS rather than CHECKS, so
it was silently skipped and the count stayed at 45. Noticed because the number
did not move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`prototype/examples/pipeline_demo.mg` is named for the pipeline operator, and
the operator turned out to be the bug — in a shape the taxonomy did not have.

`|>` is in the spec four times: prose listing it among the modern control
constructs, a token definition (`PIPE = '|>'`), a grammar rule
(`pipe_expr = expression '|>' expression`), and a worked example. The lexer
emits the token. The parser builds an `Expr::Pipeline`. The evaluator desugars
it correctly to `f(x, a)`.

The typechecker inferred the two sides independently, so the right side was
checked as a standalone call with the piped argument missing:

    10 |> add(5)     check: call `add`: expected 2 argument(s), found 1
                     eval:  15

Every program using the documented operator ran correctly and failed `--check`.
That is the inverse of the shape this repo keeps producing — "typechecks and
does not evaluate" — and it lands in the same place: `--check` and `--eval` are
two different oracles, and agreeing with one says nothing about the other.

The checker now typechecks the call the pipeline desugars to, exactly as the
evaluator does, with a bare function reference (`x |> f`) unified against a
one-argument function type. Arity and type errors *through* the pipe are still
caught — there is a test for each, because routing around the standalone-call
path could easily have silenced them.

pipeline_demo.mg checks and evaluates to 8159. Sketch list 33 -> 32.

Five example files rewritten, eleven compiler bugs. One file remains.

prototype 1,145 -> 1,146.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Last sketch example, `spec_synthesis.mg`, and the bug is in the feature the
file exists to demonstrate.

A `sp` block names the function it applies to — that is the whole mechanism by
which contracts attach. And `sp search { … }` beside `f search(…)` reported
`duplicate definition: search`, so the contract feature could not be used as
designed at all.

The cause is general, not spec-specific. `define_type` mirrors its name into
the value namespace so enum constructors resolve, with a one-line comment
saying so — and duplicate detection could not tell that copy from a real
definition. Every `S`, `T`, `Y`, `effect` and `sp` declaration therefore
reserved its name against functions, so `S Point { … }` beside
`f Point(…) -> Point` — the ordinary constructor pattern — was rejected too.

Mirrored names are now tracked separately and forgiven exactly once. Two real
definitions in either namespace are still duplicates, including the case that
matters: a struct, then a function of that name, then a *second* function of
that name. A rule that quietly disabled duplicate detection for every type name
would be worse than the bug it fixes, so there is a test for each direction.

The file itself was worse than the others: 112 of its 121 remaining lines were
commented-out implementations, so almost nothing in it had ever reached the
compiler. Rewritten to exercise `sp` blocks with `@req`/`@ens`/`@fx`, plus
implementations that satisfy them. Checks with 0 errors, reports 4 contracts,
evaluates to 11.

`prototype/examples/` is now free of sketches. All six files rewritten,
thirteen compiler bugs out of them, and the rate never dropped: `data` sums,
bare variant constructors, unit-variant patterns, closures, default arguments,
`println`, the pipeline typecheck, and this.

Sketch list 32 -> 31. prototype 1,146 -> 1,148.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Open item 11, and the same shape as the pipeline bug: evaluates correctly,
fails `--check`.

`f id[T](v: T) -> T { v }` declared fine and `id(1)` evaluated to 1, but every
call reported `type mismatch: I32 vs sym1`. Signature collection ran
`lower_type` with no generic binding, so `T` fell through to the interning arm
of `resolve_named_type` and became a nominal `Ty::Named` — a distinct type that
unifies with nothing. `check_function` *did* bind the generics to fresh
variables, but only for the body, so the body checked and the callers could
not.

Two changes. Signature collection now binds each generic parameter to a fresh
type variable while lowering, and records those variables as the function's
quantified set. Each call site then instantiates a fresh copy of them before
unifying.

The instantiation is the half that is easy to skip. Lowering `T` to a single
shared variable fixes one call and breaks the next: `id(1)` would bind
`T := I32` and `id("ab")` in the same program would then fail. There is a test
for exactly that pair, because it is what separates a real instantiation from a
variable that merely happens to unify once.

Real mistakes through a generic call are still errors — wrong return type,
wrong arity — with a test for each, since instantiation routes around the path
that checks them.

`prototype/examples/analysis.mg` had been carrying a declared-but-uncalled
generic with a comment pointing at this item. It now calls it, and evaluates to
103 rather than 102.

prototype 1,148 -> 1,151.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…osed

Three items, two of them worse than recorded.

`+=` reported `unknown operator: \`+=\`` — a claim that is simply false, since
the parser produces it and the evaluator runs it. The handoff had this down as
"the operand type is unknown, which happens with untyped parameters". Wrong: it
failed with fully annotated types too, and so did `-=`, `*=`, `/=` and `%=`.
The program it was found in had two problems at once and the diagnosis stopped
at the first. Every program using a compound assignment evaluated correctly and
failed `--check` — the fourth "evaluates but does not typecheck" bug this
session, after the pipeline operator and generic calls.

`benchmarks/cross_lang/tasks.mg` was in the sketch list for this exact reason
and now checks with 0 errors, so it is off the list.

`guard` could be bound but not referenced: `v guard = 2` was fine, `guard + 1`
on its own line took the guard-statement arm and died with `expected
expression, found Plus '+'`. Using it anywhere else on the line always worked,
which is the giveaway — position was the whole problem. `guard` now starts a
statement only when a condition can follow; a binary operator, a delimiter or a
line break means it is a name. Both directions tested.

`scan` emits its seed, so its result is one element longer than its input:
`scan([1,2,3], 0, +)` is `[0, 1, 3, 6]`. Documented at the definition and in
the ontology summary, which is what an agent reads.

Sketch list 31 -> 30. prototype 1,151 -> 1,153.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Item 10 was recorded as the largest piece of unbuilt work: "Handlers dispatch
and return like ordinary calls; nothing captures a continuation." The spec put
it more bluntly: "**Handlers do not resume.**"

Running it says otherwise. An operation call dispatches to its arm and the
arm's value becomes the value of the call, so the body carries on:

    f work() -> i32 / a { v got = A.ask()
                          got + 100 }
    handle { work() } with A { ask() => 7 }        // 107

That *is* single-shot tail resumption. Two operations in sequence each resume,
and the arm is re-evaluated per call rather than computed once. An arm may also
abort with `ret`, which discards the rest of the handled body and makes that
the value of the `handle` expression — without returning from the enclosing
function:

    v r = handle { work() } with A { fail() => ret 7 }
    r + 1000                                       // 1007, not 7

None of this was tested. Abort in particular could have regressed to "returns
from the whole function" with nothing noticing, since the two are
indistinguishable in any program that has nothing after the `handle`. Four
tests now pin it.

What is actually missing is *multi-shot* resumption: the continuation is never
reified, so it cannot be stored or invoked twice. Generators and backtracking
need that. State, reader, logging, tracing and test-mocking handlers do not,
and all work today.

This is the mirror of everything else found this session — for once a document
*understated* the implementation. The cost is the same shape either way: a
reader would have concluded handlers were unusable and either avoided them or
started a large refactor that is mostly already done.

Spec §11.5 now describes what handlers do, with both examples run (107, 1007).
Item 10 is rewritten to name the part that is genuinely missing.

prototype 1,153 -> 1,156.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 17 CLI flags had a test asserting they are not mistaken for file paths, and
nothing asserting they *work*. Running them found this.

`--backend=<name>` is documented as "Select hardware accelerator for dispatch
(default: cpu)". It reaches exactly one dispatch path — `--run=abl-bytes`, which
calls `select_backend` and reports honestly, including for an unknown name.
Every `--target=abl-*` path constructs `CpuBackend::new()` directly and never
looks at the flag. So:

    --target=abl-compute --backend=cuda
    // MAGE → Agentic Binary Language → CpuBackend dispatch

Accepted, ignored, CPU. For a repository whose headline numbers include GPU
results, the failure mode is someone believing they measured a GPU — a wrong
answer, not an error.

Honouring the flag on those paths means threading `SelectedBackend` through
each dispatch loop, and verifying the GPU half needs a CUDA build, which is not
available here. So the fix is the honest half: the flag now says when it is
inert, and the ontology no longer describes it as global. Recorded as item 14.

Verified in all three states: warns when ignored, silent when honoured
(`--run=abl-bytes`), silent for a plain `cpu` default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 37 published RAP methods were "verified" earlier this session by grepping
`rap.rs` for each method name. That proves the string is in the file, not that
the server answers to it — the same agreement-not-truth trap as pinning the
ontology against its own generator. Dispatching them instead found this.

Four of the 37 had no test at all: the `nl/*` methods, which are the
natural-language surface an agent reaches for first. Two of those four were
broken outright.

`nl/explain` and `nl/refactor` take a `source` parameter and interpolate it
bare into a prompt. The engine's `extract_code_block` reads source *only* from
a ``` fence, so `intent.source` was always `None` and both answered "No source
code provided" for every input, including well-formed ones. Not a degraded
path: there was no input that worked.

Both now fence the source. Two tests added: one dispatching every method the
ontology publishes and asserting none returns "unknown method" (params omitted
deliberately — a missing argument is fine, an unrecognised name is not), and
one exercising all four `nl/*` methods.

Verified by breaking it: with the fence removed the test fails, with it
restored it passes. Worth recording that the first attempt at that check was
itself broken — the revert script silently matched nothing because of backtick
escaping, so the "passing" result was the fixed code being tested twice. Only
grepping the file afterwards caught it. That is the third time this session a
break-the-fix check needed checking.

prototype 1,156 -> 1,158.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`check-mg-sources.sh` covers `.mg` files. Nothing covered the MAGE code blocks
inside the markdown — which is where an agent actually learns the language.

258 blocks are tagged ```mg or ```MAGE. Nine are deliberate fragments (they
contain `...`). Of the remaining 249, **168 do not parse**:

    41  use std::llm::{LLM, Prompt, Response};    Rust `::` paths
    18  u std.agent.{Agent, Message}              brace imports
    10  pub fn save(data: &str) -> Result<(), E>  Rust signatures
     9  #-prefixed attributes
     ...

By location: agent-guide/ 86 blocks (the guide *for agents*), cookbook/ 61,
quick-start/ 19, and MAGE_SPEC.md itself with 24 failing. Worst is
`training/prompts/`, whose few-shot blocks teach a model what MAGE looks like:
one shows `I ~ Counter { … }` and `Counter @{ count: 0 }`, and neither parses.
The working spellings are `I Counter { … }` and `@Counter { count: 0 }`. A
model trained or prompted on those blocks learns to emit code the compiler
rejects — which is the failure this whole language is meant to avoid.

This is the shipped examples' story a third time, after `examples/` and
`prototype/examples/`, and by volume the largest instance. Both of those
rewrites produced compiler bugs at a steady rate (fourteen, then thirteen), so
the expected yield here is high — but 168 blocks is its own piece of work, not
a tail-end fix.

`scripts/check-doc-blocks.sh` ratchets the count in the meantime: it may go
down, never up. A new failing block or a new file fails CI. Verified by adding
a broken block (caught, 6 -> 7 in that file) and removing it again.

Two bugs in the checker itself, found by not trusting it: the first version
reported "0 (baseline 0)" and passed, because a blank line made
`$((total + was))` fail and the totals never accumulated. A checker that cannot
count is worse than no checker. It counts with awk now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixed `training/prompts/` first out of the 168 unparseable documentation
blocks, because it is the most damaging: those blocks are the few-shot examples
that teach a model what the language looks like. All four files now parse
completely.

`system-prompt.md` was the worst, and it is the file most likely to be pasted
verbatim into a model's context. It taught:

  handle Db { f query(sql: &s) -> ... }   the real form is
                                          handle { … } with Db { query(s) => … }
  Point @{ x: 10, y: 20 }                 the sigil goes first: @point { x: 10 }
  @ item ~ collection { … }               `~` is not the separator; `in` is

Three of its eight numbered "Rules for Generating MAGE Code" were wrong,
including the turbofish it recommends. Rules 4, 5 and the generic syntax check
out.

`few-shot-generation.md` and `few-shot-translation.md` taught `I ~ Counter`
(it is `I Counter`), `? self { … }` for a match (it is `?=`), and `@d(Debug)`
attributes that do not parse. Blocks 5 and 6 of the generation file were
replaced with different examples, so their **Task:** headings were rewritten to
match — a corrected block under a heading describing something else is its own
kind of wrong.

`few-shot-repair.md` needed care: it pairs broken code with its fix, so the
broken halves are *supposed* to fail. Four of its six failures are those. The
other two were **Fixed MAGE:** blocks that did not parse — repairs that did not
repair. One of them also mislabelled the effect: writing a file is `/ fs`, not
`/ io`.

`check-doc-blocks.sh` now skips blocks whose nearest heading marks them broken,
so intentional errors stay out of the baseline. Verified in both directions: an
unlabelled broken block fails the check, a `**Broken MAGE:**` one does not.

Baseline 168 -> 145.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second of the documentation files, chosen for the same reason as the first: it
is a system prompt, so it is the text most likely to be pasted verbatim into a
model's context.

All four of its MAGE blocks were Rust with `/ io` annotations bolted on —
`pub async fn`, `impl Agent for WebScraper`, `use std::agent::{...}`, `Vec<T>`.
Replaced with checked MAGE covering the same ground: effect annotations on pure
/ single / multiple-effect functions, contracts in a `sp` block sharing the
function's name, and real `agent` / `swarm` blocks. The agent grammar takes
`capabilities:` and `requires_approval:` as bracketed lists, which is not what
the old block showed and not something I would have guessed — it came out of
the parser's own field table.

Two prose rules were wrong, and prose in a system prompt is as load-bearing as
the code beside it:

  "Effect hierarchy: `net` implies `io`"   There is no hierarchy. A function
                                           performing both declares both;
                                           verified by making one call the
                                           other and watching it fail.
  "Built-in effects: ... `process`"        `process` is a capability namespace,
                                           not an effect. The kind is `proc`,
                                           and `/ process` is an unknown-effect
                                           error. The real list is 17 names.

Nothing checks prose, and this is the second file in a row where the sentences
around the code were wrong in ways the code checker cannot see.

Baseline 145 -> 142.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more documentation files. `syntax-quick-ref.md` is the reference an agent
consults mid-task, and four of its six "Canonical Examples" — Read File, Struct
with Methods, Error Handling, Agent — were Rust: `#[derive]`, `impl Point`,
`use std::agent::{Agent, Swarm}`, `Vec<f64>`. Replaced with checked MAGE.

`anti-patterns.md` is the sharper one. It pairs a **WRONG:** block with a
**CORRECT:** block, ten times. The WRONG blocks are supposed to fail and the
checker now skips them. All ten **CORRECT** blocks failed too — a file teaching
what not to write, in which every correction was as invalid as the mistake it
was correcting.

Anti-Pattern 10 is the sharpest of those: titled "Mixing Rust Crate Paths with
MAGE Stdlib", it shows `use tokio::fs;` as wrong and `use std::fs;` as right.
Both are Rust `::` paths, and neither is MAGE. The deeper problem is that the
whole anti-pattern is moot — `use` brings nothing into scope, so there is no
import to get right. Rewritten to say so.

Also corrected while there: Anti-Pattern 3 recommended `Capability::request`
in place of `unsafe`, which does not exist; the real mechanism is a capability
handle whose effect the function declares (`mem.alloc(n)` with `/ alloc`).

Baseline 142 -> 128.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The effect system is the capability gate, so its guide is the highest-stakes
documentation in the repo. It taught the effect hierarchy myth in four places:

    // CORRECT — net implies io
    pub async fn fetch(url: &str) -> ... / net { ... }

    // CORRECT — agent implies async
    pub async fn run(a: &mut Agent) -> ... / agent { ... }

    "But apply the hierarchy rule — don't list implied effects"

There is no hierarchy. `/ net` does not cover an inferred `io` and `/ agent`
does not cover an inferred `async` — both verified by making one function call
another and watching the checker object. So every block marked CORRECT was
advice to *omit* a required annotation, which for a capability system is the
worst direction to be wrong in: it teaches under-declaring, and the whole point
of the gate is that a declaration is an upper bound on what a caller may reach.

Also corrected in that file:

  - `handle io { get(_) => … } { … }` — not the syntax. It is
    `handle { … } with Io { get(u) => … }`, and one handle discharges one
    effect, so mocking two means nesting them.
  - `fn(&str) -> T / io` as a closure *parameter* type. There is no effect
    polymorphism; the annotation does not parse there. The caller declares.
  - The "Violation" example was Rust. It is now MAGE that parses and fails on
    the *effect* check, which is what it is demonstrating — a violation
    example should fail for the reason it names, not because it is the wrong
    language.

Baseline 128 -> 120. `agent-guide/effects.md` is fully clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit shipped `effects.md` fixed but left HANDOFF.md saying 128.
The script that was supposed to update it asserted on a stale anchor, wrote
nothing, and exited — and because the assertion fired *after* the earlier
replacements were computed but before the write, none of them landed either.
The commit went ahead regardless, since the doc-count checker only pins test
totals, not this section.

That is the same shape as the two verification scripts earlier this session
that silently matched nothing: a step that appears to run, reports nothing
alarming, and changes nothing. `str.replace` and a failed assertion look very
different in a terminal and identical in a diff you do not read.

The section is now rewritten rather than patched, and says what is actually
true: 120 of 258, with the five completed files named and the three
non-syntactic defects called out — under-declaring effects, corrections that
were themselves wrong, and false rules in prose that no checker can see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rewrote every remaining `agent-guide/` and `quick-start/` document against the
compiler, and fixed the seven defects that fell out of running the examples.

The documents. `patterns.md`, `migration.md`, `syntax-quick-ref.md`,
`quick-start/03-syntax-tour.md` and all three `agent-guide/examples/` files
were Rust — or agent-mode sigils over a standard library that does not exist.
The prose was worse than the code, because nothing checks prose:

  - `03-syntax-tour.md`: "the compiler tracks effects automatically — you
    don't need to annotate them". It requires them at every public boundary,
    and that requirement is the capability gate.
  - `migration.md`: a 25-row table asserting `let`, `mod`, `use path::to`,
    `#[derive]`, `println!`, `Foo { x: 1 }`, `async fn` and `where` were
    identical in Rust and MAGE, and six of eight worked migrations answered
    "no changes needed". Every row is now measured.
  - `syntax-quick-ref.md`: four fictional `std::` module tables and the effect
    hierarchy again.

The compiler defects, each found by running an example and checking its
answer, not by reading code:

  - Method bodies were never effect-checked. `impl` and `extend` items never
    reached `infer_module`, so `--check` on a module of methods printed
    "Functions analyzed: 0" and a `pub fn` inside one could read the
    filesystem while declaring nothing. The two ways to reach a capability
    were `namespace.op(…)` and `receiver.method(…)`; neither was checked.
  - `p"…"` / `ep"…"` printed nothing and interpolated nothing — folded into a
    plain string literal, so the statement was a no-op whose value was the raw
    text.
  - `println!("hi")` printed nothing and returned a bool: `!` parsed as
    logical not applied to the call. Macro calls are now a parse error that
    names the macro and its replacement.
  - A string ending in an escaped quote lost it (`trim_matches` strips every
    delimiter), so `split(html, "href=\"")` never split and a link extractor
    returned `[]`.
  - `range(1, 101)` ran as `0..1` — the one vocabulary arm with no arity check.
  - `len` committed an open type to a collection, so statement order decided
    whether a program compiled.
  - The undeclared-effect fix suggestion named the *function* as the effect.

And the instrument that certified the earlier documents was too weak:
`check-doc-blocks.sh` counted parse errors only, so 43 blocks that parsed and
then failed the checker scored as passes. It now requires `Errors: 0`, which
is why the baseline reads 104 rather than 82.

Verified: 2,876 tests green, 46 documented counts match, 12/12 examples,
96 `.mg` sources check, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t exist

Rewrote all eight `cookbook/` files and the migration guide's effects and
case-study chapters against the compiler. With this, every document an agent
reads to learn MAGE parses *and* typechecks — 177 blocks. Only `MAGE_SPEC.md`
is left.

The cookbook was the most detailed documentation in the repository and none of
it ran: `u std.io.{File, BufReader, Read}`, `s.new()`, `[T]~.new()`,
`TempFile`, `fs.watch`, `Command.new("git")`, `Mutex`, `channel`, `join_all`,
`I Agent ~ Greeter`, `AgentRuntime`, `Bus`, `Capability.request(…,
Lease.new(…))`. The migration guide documented a capability *runtime* —
`[capabilities] grants` in Forge.toml, `cap.require("fs.read")?`, a table of 14
capability strings — which is a weaker mechanism than the one that exists, and
teaches an agent to look for a permission call that is not there.

Rewriting them found five defects, each by running something:

  - **No capability call could be evaluated.** `fs.read_to_string(p)`,
    `env.get_env(k)`, `time.now()` — every namespace call typechecked and then
    died with `unknown function`, because the receiver is not a value and the
    call fell through to the builtin table. Every documented way for an agent
    to reach a resource was checkable and unrunnable. `io`, `fs`, `env` and
    `time` now evaluate; the rest report that the checker tracks the
    capability and the interpreter cannot perform it.
  - **The vocabulary enforced nothing.** All 15 argument unifications in
    `infer_vocab_call` discarded their failures, so `join(xs, 7)` and
    `upper(5)` checked clean. The one thing a closed set of 31 combinators is
    for is catching misuse. Making them report immediately caught three blocks
    written earlier in this session.
  - **`contains` on a string failed the checker** while evaluating correctly —
    the evaluator has always handled string, map and list; the checker knew
    only list.
  - **`contains` also committed an open type** to a collection, the same way
    `len` did, so `filter(xs, |p| contains(p, ","))` inferred the closure as
    `f([str]~) -> bool`.
  - **`async` is a keyword, not a capability namespace.** `async.spawn(…)` was
    documented and cannot parse; the effect is attributed by the bare name
    `spawn`. The quick reference's namespace list was wrong in both
    directions and is now the table from `hir::CAPABILITY_NAMESPACES`.

Verified: 2,878 tests green, 46 documented counts match, 12/12 examples,
96 `.mg` sources check, clippy clean, 34 doc blocks failing (all in
MAGE_SPEC.md bar one labelled design sketch).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every MAGE code block in every markdown file in this repository now parses and
typechecks. `check-doc-blocks.sh` reads 0 (baseline 0) and its baseline file is
empty; it started at 177 of 258 failing.

The spec was the last document and the one that needed judgment rather than
rewriting: a disagreement here has to be *resolved*, because either side may be
the one that is wrong. Eight sections were stale and the compiler was right:

  §5.1  `layer dense(784, 256, relu)`   → `layer name: Linear(784, 256)`
        Every layer is named, the kind comes from the layer map and is
        case-sensitive, and activations are layers rather than arguments.
  §5.4  `model:` / `data:` / callbacks  → `net:` / `dataset:`, 25 fields, none
  §5.5  `use std::llm::{LLM, Prompt}`   → `llm.generate(…)`, a capability
  §6.1  `Tensor<f32, [3, 224, 224]>`    → `tensor[f32, 3, 224, 224]`
  §7.1  `rule integer(T) :- numeric(T)` → `rule integer(t: str) { numeric(t) }`
  §8.1  `select tournament(k: 8)`       → `select { 8 }`; no `target`
  §9.1  `agent` with fields and methods → two fields, no code at all
  §9.2  `dispatch`/`aggregate` blocks   → four fields; `map` and `fold`
  §12.1 `@req(…)` above the signature   → a `spec` block sharing the name

Five constructs the spec documented do not exist at all — `grad(…)`, `rl`
blocks, the compile-time SKB query API, SIMD types, and the module system.
Each is now labelled invalid where it appears, with the parse error it
produces, rather than deleted: the design intent is worth keeping, the false
impression is not.

And Appendix D's 33 Greek/mathematical symbol rows — `Ψ` for `net`, `Ω` for
`evolve`, `⊗` for matmul — are **lexed and never parsed**. Fifteen are real
tokens (`KwPsi`, `KwSigma`, …) that no parser arm matches. The tables now say
so; the agent mode that works is the ASCII half, verified row by row.

Appendix E is now three human/agent pairs that both compile, which is what a
side-by-side is for.

Verified: 2,878 tests green, 46 documented counts match, 12/12 examples,
96 `.mg` sources check, 206 doc blocks check, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NERVOSYS and others added 30 commits August 19, 2026 12:46
`check-orphan-sources.sh` found `compute/wasm.rs` on its first run — 208
lines no `mod` declaration reaches. Following it up found the rest.

Measured rather than assumed: `cargo build --features wasm` **succeeds**,
exit 0, and `cargo tree --features wasm -i wasm-bindgen` puts
wasm-bindgen v0.2.126 in the graph. So the feature costs a dependency and
enables zero compiled lines.

That is worse than item 15's `cuda`, which at least fails to build and
therefore tells you. This one is silent: a consumer enables it, gets a
heavier tree, and reasonably concludes the backend is there.

And `core/discoverability.rs` publishes ("wasm_backend", "WASM Backend",
"WebAssembly SIMD") into the ontology *unconditionally* — not behind the
feature — so an agent asking the crate what compute backends it has is
told about one whose implementation no compiler has ever read. The
ontology is the thing agents ground in, which is the whole reason this
repository pins ontologies against reality elsewhere.

Disposition matches item 15 and is not mine to change: `rmi` is vendored
and must stay syncable against its own upstream, so wiring the `mod` in,
deleting the file, or dropping the feature is its owner's call. Both
orphans stay baselined so that a third one fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decided rather than deferred. MAGE_SPEC.md §11.6 listed multi-shot under
"what is missing", HANDOFF filed it as real work unstarted, and between
them they promised an evaluator rewrite nobody had agreed to. The item is
closed the way item 1 closed: by making the current behaviour official and
saying why.

The cost is what settled it. Multi-shot needs the continuation as a
first-class value, which in a dual-surface language is a keyword *and* a
sigil on every arm that uses it, plus a rule in the effect system for what
a resumed continuation performs and whether those effects are
re-attributed. It also needs the continuation somewhere copyable, and it
currently lives in the Rust call stack — a CPS or CEK rewrite across 39
expression forms and 53 recursive eval sites, with 1,200 tests riding on
present behaviour. The purchase is generators and backtracking. No MAGE
program has wanted either. Handlers that do the work this language exists
for — state, reader, logging, tracing, retry, capability interception,
test mocking — need none of it and all work today.

Re-measured before deciding, rather than trusting the numbers recorded on
2026-08-18: 107, 1007, and two operations resuming to 5 + 5 = 10, all
exact. `resume` still evaluates as an ordinary identifier, which is what
"there is no `resume` keyword" has to mean if it is to be falsifiable at
all.

`single_shot_resumption_is_a_decision_not_a_gap` pins both halves. The
keyword half fails the moment someone reserves the word, which is the
first edit multi-shot requires — before the 39 expression forms get
touched. The spec half asserts the normative sentence is still there,
verified by rewording it to "single-shot for now" and watching it fail.

Both halves, because a decision recorded only in prose decays back into a
gap as soon as someone reads the evaluator and sees the absence. That is
exactly how this got filed as unstarted work in the first place.

Nothing here is irreversible, and the spec says so: if a real program
needs multi-shot, §11.6 is the thing to change first, with the program in
hand rather than on the strength of the feature existing elsewhere.

Test counts 1,199 -> 1,200 across six files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were filed as "vendored, owner's call", and I repeated that framing
three times before checking what the blocker actually was. It was much
narrower than either item claimed.

Item 17 — `wasm`. `compute/wasm.rs` was 208 lines no `mod` declaration
reached, so `--features wasm` succeeded, pulled wasm-bindgen v0.2.126 into
the graph, and enabled nothing, while `core/discoverability.rs` advertised
a "WASM Backend" to callers. The fix is one `#[cfg(feature = "wasm")] pub
mod wasm;`. It compiles clean; `--features wasm` and the default build are
both warning-free; all 1,230 lib tests pass. The file was never broken,
only unreferenced — which is exactly why it survived. Removed from the
orphan baseline in this commit, as that ratchet requires.

Item 15 — `cuda`. `cuda = ["dep:cudarc"]` made cudarc 0.10 mandatory, and
its build script wants `include/cuda.h` from an installed toolkit, so
`cargo build --features cuda` failed before compiling a line of this
crate. The only file using cudarc is `cuda_full.rs`, which no `mod`
reaches — so a heavyweight build-time dependency was mandatory for a
feature that gated nothing capable of running.

Measured before changing it: no compiled file mentions cudarc, and
`compute/cuda.rs` — the file the feature actually enables — does not
either. Now `cuda = []`. The feature builds, cudarc is out of the
dependency graph, and it stays declared optional so `--features cudarc`
still reaches it.

Corrected a false claim while there. The Cargo.toml comment said the 0.10
pin "keeps the existing kernels building on CUDA 11/12 hosts". It kept
nothing building: those kernels are in `cuda_full.rs` and have never been
compiled on any host.

What stays upstream's, and item 15 now says only this: `cuda_full.rs`
itself — 1,812 lines, 16 `unsafe` blocks, 6 `#[ignore]`d tests that have
never run anywhere. Wiring it up needs a CUDA toolkit and porting it needs
a cudarc 0.19 migration. It remains baselined.

The lesson matches items 1 and 10. "Someone else decides this" did the
same work there that "unstarted implementation" did here: it made a small
decidable thing look large and blocked, and nothing re-examined the
framing because the item read as already triaged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I set out to answer a small design question — should the ontology gate
backends by feature, or describe the crate? It already published
`feature_gated` and `feature_flag`, so the question was less open than I
had claimed. Both values were wrong, in opposite directions.

The gating first. `metal_backend` and `vulkan_backend` published
`feature_flag: "gpu"`. Neither `compute/metal.rs` nor `compute/vulkan.rs`
contains a single `#[cfg(feature = ...)]`, and `compute/mod.rs` declares
both unconditionally — they are in every default build, and `--features
gpu` does not touch them, it adds `wgpu_backend.rs`. An agent reading this
was told to enable a feature it did not need.

Then the part that matters. `technology` named a real API for each:
"Apple Metal MSL", "Vulkan SPIR-V". `metal.rs` imports no Metal binding,
`vulkan.rs` no Vulkan binding, and `Cargo.toml` declares neither — the
only GPU crate in the manifest is optional `wgpu`. Five of these are CPU
implementations wearing hardware names: metal, vulkan, webgpu, apple_ane,
qualcomm. Each constructor comments what a real one *would* do —
`MTLCreateSystemDefaultDevice()`, query the device, create a command
queue — and then returns fabricated `DeviceInfo`. `metal.rs` announces
itself as "Metal (Apple GPU)" with 16 GB of unified memory and 10 compute
units. `vulkan.rs` claims 32.

So an agent that selects "Metal Backend" for GPU acceleration gets none,
and gets a device description that agrees with its mistake. This
repository pins ontologies against reality precisely because the ontology
is what agents ground in; this one had drifted to fiction on both fields
it published about hardware.

`add_extra_compute_backends` now says what the code does, with an
`implementation` field naming each scaffold and what would make it real.
The scaffolds stay published — they exist and they compute, and hiding
them would be its own inaccuracy — but they are no longer published as
hardware paths. The false `gpu` flag is gone from Metal and Vulkan, which
are not gated at all.

Corrected one of my own claims while there: I described `wasm.rs` as a
backend I had enabled in the previous commit. It is real wasm-bindgen
interop exporting `WasmTensor`, but it does not implement the `Backend`
trait, so it is not interchangeable with the others and calling it a
compute backend was wrong. The ontology now says that too.

Filed as item 18, ranked above 15 and 17: fixing what the backends
*report* is a behaviour change in a vendored crate, because a caller that
reads `DeviceInfo` to size a workload is being handed numbers for hardware
that is not there. Items 15 and 17 cost a dependency. This one returns
wrong answers to anyone who asks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`check-orphan-sources.sh` finds files nothing compiles. Its sibling
failure is a file that compiles fine, holding a test nothing schedules —
and there was no instrument for that either.

`eval_bench` is the case already on record: red for 76 commits behind
three hiding places at once. `#[ignore]` so `cargo test` skips it, an
additional `--bench` flag nobody passed, and a branch never pushed. Each
defensible alone; together the figure it certifies had no runner while
appearing in three documents.

`scripts/check-ignored-tests.sh` asks the narrow question: does some CI
job name this test? Its first run found `perf_report` — the harness that
produces the ABL artifact-scaling figures in MEASUREMENTS.md and
ARCHITECTURE_DSL.md. `cargo test` compiles it, so it could not rot into a
build error unnoticed, but nothing had ever executed it. The step above it
names `eval_bench` specifically and no job passes `--bench`. A runtime
panic would have surfaced when someone next tried to regenerate those
numbers, which is when a broken harness is most expensive. Now a CI step:
three seconds.

Two design choices, both deliberate.

It skips files listed in the orphan baseline. `cuda_full.rs` holds six
`#[ignore]`d tests that cannot run for a prior reason, already reported by
the other check. Routing one defect through two instruments makes both
noisier and neither more informative. The skip list is read from that
baseline, so fixing an orphan hands its tests to this check by
construction rather than by anyone remembering.

And there is no baseline file. The live set is two and both are covered,
so green is the honest state; a baseline would hold exemptions that do not
exist. If that changes, add one — do not weaken this.

The match is loose on purpose: a name appearing anywhere in ci.yml
satisfies it, a comment included. This exists to catch absence, not to
prove a runner correct, and pretending otherwise would be the weak
instrument rule 9 warns about.

Verified by deleting each CI step in turn and watching it name exactly
that test.

Also corrected rule 7, which still said the orphan check "reports exactly
those two". It reports one — `wasm.rs` was fixed the same day. I had
updated the script's comment and not the prose, which is the two-copies
problem the section is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SECURITY_AUDIT.md §1 names this gap itself: "cargo audit in CI catches new
*vulnerabilities*; nothing compares this table against the warnings actually
reported." CI's audit job deliberately omits -D warnings, for a good reason —
the remaining findings are unmaintained/unsound advisories with nowhere to move
to, and a permanently red job is an ignored job. The consequence is that the
whole warning surface is printed and nothing reads it.

That register had already drifted in both directions at once, which is the
argument for a two-directional check: RUSTSEC-2026-0097 was listed as accepted
after it had stopped firing, and RUSTSEC-2026-0190 was being reported and was
in no row at all, for two months, with a patched version available throughout.

scripts/check-security-register.sh reads the rows *out of* §1 rather than
keeping a copy — adding a row is what tells the check the row exists — and
fails on:

  - an advisory reported by any of the five surfaces with no row here
  - an **Accepted** row that no surface reports any more
  - a Status column it cannot classify as FIXED / Accepted / no-longer-applies

plus npm audit on video/, whose total must be zero.

The register agrees today, and that is the finding rather than a formality: all
five surfaces re-run, four committed lockfiles at zero findings of any kind,
paste (RUSTSEC-2024-0436) reported only by rmi's git-ignored lockfile exactly
as §1 records, video/ at 0. Nothing had changed since 2026-08-18 — but nothing
had established that either.

Three things the build taught:

The "Accepted" direction only works over the union of all five surfaces. paste
is reported by exactly one, the git-ignored one, so a check scoped to the four
committed lockfiles would have called a correct row stale. The narrower scope
is the more defensible-sounding one and it would have been wrong.

A row the check cannot classify is a failure, not a pass — rule 8 at row
granularity.

The npm total is summed from the severity buckets rather than read from
"total", because npm audit --json has two keys by that name and the other one
is the package count (293). Telling them apart by document order is a parse
that reports 293 vulnerabilities the day the order changes.

Verified by breaking it five ways, each failing for its own reason: a reported
advisory deleted from the table, an Accepted row flipped to FIXED, an invented
Accepted row nothing reports, an unclassifiable Status column, and the "## 1."
heading renamed. The npm arm reads 0 against the live surface whatever the
table says, so no break test reaches it; its parser was tested separately
against synthetic reports with the two blocks in both orders, and against a
changed shape, which must yield "cannot parse" rather than 0.

Two corrections to the documents while here, both under-claims:

§1's "CI's audit job covers the five Cargo lockfiles and not this one" had been
false since the npm step was added the same day.

§1 said "four committed lockfiles" while the CI job is named "all five". Both
numbers are right and count different things; neither said so. §1 now explains
that the fifth is git-ignored so the vendored crate does not inherit this
repo's pins, and that CI resolves it fresh — which is why a finding on the four
is a repository fact and a finding on the fifth may not be. The script labels
that surface (git-ignored) wherever it reports one.

What it does not check is the *rationale* in the Status column. "Only under the
non-default gpu feature" is a claim about a dependency graph, and re-deriving
it per row is a cargo tree reachability argument with a false-positive class.
Per rule 10 the gap is documented rather than weakly instrumented.

Also carries the HANDOFF.md rewrite for 2026-08-19 → 24, which the previous
session left in the working tree uncommitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Testing the path CI actually depends on — the git-ignored rmi lockfile absent,
because CI generates it in a prior step — check-security-register.sh reported
the missing surface and then reported paste's Accepted row as an acceptance
that had stopped applying, advising it be struck through. Both messages came
from one cause, and the second sends someone to edit a row that was right.

That is the trap already recorded in HANDOFF.md's Traps section, arrived at
from the other side: a checker must distinguish "wrong" from "not checked",
because the two have opposite remedies — edit the register versus generate a
lockfile — and reporting the wrong one sends people to change rows that were
correct. The check that exists to stop a security document from drifting was
itself pushing it in the wrong direction the moment a surface was unavailable.

The "accepted, and reported nowhere" direction now runs only when every surface
was audited, and prints what it skipped and why when it does not. The other
direction — "recorded as fixed and reported again" — still runs on a partial
set, because a finding that is there is there regardless of what else was
missed.

Re-verified all six cases against the amended script: the five breaks (row
deleted, Accepted flipped to FIXED, invented Accepted row, unclassifiable
Status column, heading renamed) plus the control, and the missing-lockfile case
that prompted this.

Separately, running the check in CI corrected §1. The row said the paste
warning is "a property of a local resolve rather than of this repository" —
right for crossbeam-epoch 0.9.18, which was a stale artifact in one working
copy that a fresh resolve did not reproduce. paste is not that. CI generates
rmi's lockfile from scratch and this check reports the identical single
advisory there (run 32902128162), so a fresh consumer resolve gets it too: it
is a property of rmi's dependency graph via wgpu->metal, and it is git-ignored
rather than local. The two cases look the same in a report and are not, and
only running the fresh resolve tells them apart — which is the argument for CI
auditing five surfaces while §1 counts four.

CI was green on the previous commit, including the new step, before either of
these was found. Neither was visible from a passing run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§1 now has an instrument; §2-§4 never had one, and their claims are specific
enough to check, which the taxonomy says is the one thread worth pulling
through code rather than by running something. Four results.

§3's unsafe count was still wrong after being corrected once. It said "the real
surface is 9 unsafe blocks in memory_pool.rs". There are 13: 9 blocks plus 4
unsafe impl Send/Sync on Slab and TensorBuffer. The omitted four are the
higher-risk half — a block's soundness is local, an unsafe impl is an unchecked
global assertion — and they are exactly where the fifth defect was. This is the
same row that previously asserted the allocator did not exist at all
("1 audited unsafe in lib.rs, 3 in the CUDA FFI shim"), a claim whose review
found four memory-safety defects. Corrected twice, understated both times, and
both times the omitted part was where a real bug lived.

§5 described a fixed data race as an open question. It called the four unsafe
impl "defensible under the refcount discipline" and said reading the counter
Relaxed while gating mutation was "a synchronisation decision rather than a
counter — worth a second opinion from someone who owns this code". That hedge
is how it survived. Item 16 wrote the interleaving down and it is a data race
on the ordinary sharing path: as_bytes_mut is Arc::get_mut by hand, and a
Relaxed load can observe 1 without synchronizing-with the Release in a
concurrent Drop. Fixed 2026-08-19 by making the load Acquire — verified at
memory_pool.rs:516. A security document saying "possibly fine, someone should
look" about a defect found and fixed six days earlier is decay in the direction
that reads as caution.

§2's "no TLS" claim is true, and is now recorded as checked. --rap really is
plaintext JSON-RPC over TCP; the rustls implementation behind --features tls is
ribosome's worker transport (ribosome/src/tls.rs), not RAP, so it does not
contradict §2 — which it looks like it might, given open item 3. A negative
result, written down so nobody re-derives it.

The inventory is now pinned rather than typed in. test-all.sh emits
unsafe_memory_pool, unsafe_cuda_full and unsafe_cuda_backend from the same
expression §3 tells a reader to run, and four CHECKS rows compare them against
the two documents that state them, with both of cuda_full.rs's mentions pinned
to one key as the rule requires. Verified in both directions: a measured value
that disagrees is reported as a mismatch (documented 13, measured 9 — the exact
regression that happened), and a reworded claim is reported as a pattern that
no longer matches. Pins go from 80 to 84.

Measured while there, so the surface claim is derived rather than asserted: the
only Rust unsafe anywhere in the owned crates is 3 blocks in
prototype/src/cuda_backend.rs, 13 in memory_pool.rs and 16 in the
never-compiled cuda_full.rs. forge, ribosome and germline have none, and
neither do rmi's benches or examples. Everything else matching "unsafe" in
prototype/src — 74 hits across 16 files — is the MAGE keyword. §3 named three
files as keyword-only hits; there are sixteen.

A trap, caught before it landed: inserting the CHECKS rows with
awk -v new="$ROWS" silently stripped every backslash, turning \*\*[0-9,]+ into
**[0-9,]+ — a different and invalid regex, in a tab-separated table where a
wrong field also fails silently. awk -v processes escape sequences in the
value. Reverted and redone in Python, which also had to take the row terminator
from the anchor line rather than assume \n, because the working copy is CRLF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SECURITY_AUDIT.md §3 cites run_dispatch_abl_bytes as evidence that Agentic
Binary Language decode is "length-checked, bounds-validated (take() guards
every field)". It is, and that is now measured rather than read: eight
malformed containers — truncated header, bad magic, bad version, a
four-billion item count, and name_len/expr_len at the u32 ceiling — each
produce a clean diagnostic and no panic, abort or hang, through both
--from=abl-bytes and --run=abl-bytes, which hand-roll the same decoder.

Every one of those eight also exited 0, while the same match arm exits 1 when
the file merely cannot be read. A caller driving the tool by exit status —
which is how an agent drives it — could not tell a decoded container from a
rejected one. The guard fired, printed, and reported success: "detected,
recorded, and never surfaced", the same shape as the unknown net layer that
lowered to Op::IDENTITY.

Both decoders now exit 1 at every corruption site (9 each: the seven take()
EOF arms plus bad magic and bad version). The bare return that ends the
subprocess-dispatch block is a success path and is deliberately untouched,
which is why the patch matched on the diagnostic above each return rather than
on "return;".

prototype/tests/abl_container_exit_status.rs pins both directions — corrupt
exits non-zero, valid still exits zero, through both entry points. It is the
first integration test here that spawns the binary, because the failure is a
process exit status and std::process::exit cannot be observed from inside the
binary's own harness. Verified by reverting the fix and watching the corrupt
case fail; the valid-container half is what catches an over-eager fix that
makes the decoder always fail. Prototype 1,200 -> 1,202, total 2,913 -> 2,915,
and check-doc-counts.sh named all 15 rows across 7 files that had to move.

A near-finding, recorded as a non-finding: take's bound is *pos + n >
buf.len(), an unchecked add, and that identical shape was already a real defect
in rmi (TensorBuffer::slice, fixed with checked_add). Here it is unreachable —
n comes from a u32 field and pos from the buffer, so it cannot overflow a
64-bit usize, and on a 32-bit target it would wrap to a panicking slice range
rather than an out-of-bounds read. Written down as a parenthetical rather than
reported as a defect.

Three more §4 claims, none of which anything checked:

The RA row still recommended "wiring cargo audit/cargo deny into CI as a gate
(the one concrete CI action item)" twenty days after the gate was wired, while
the Open recommendations section on the same page already said it was done.

The CM row claimed Cargo.lock committed -> reproducible builds, unqualified.
git ls-files '*Cargo.lock' returns four; the fifth is git-ignored by design.
The same four-versus-five distinction as §1.

"Confirmed zero secret/credential material (leak scan)" named no tool, no
corpus and no date. Re-run over 559 tracked files against high-signal issuer
patterns — AWS key ids, ghp_/sk-/xox* tokens, PEM private-key headers — and
still zero, but the line now says what it does not cover: no entropy analysis,
no history scan, no gitleaks or trufflehog installed here. A vague claim is
unfalsifiable and therefore safe to ignore, which is the worst thing a security
assertion can be.

Two §3 claims verified true and left alone: the RAP non-loopback refusal
(rc=2), re-run including the MAGE_RAP_ALLOW_REMOTE=1 override path, which warns
and then binds — the row had only ever exercised the refusal half; and the
subprocess backend passing argv rather than a shell string
(Command::new(prog).args(..), the only two Command::new sites in
prototype/src). Recorded so the next person does not re-derive them.

And a separate one, found by accident: check-ci-floors.sh rewrote a tracked
file. After the suite ran, git status showed benchmarks/RELIABILITY_REPORT.md
modified and nothing had edited it. That script runs reliability-bench twice to
measure the parse and heal floors, and the binary writes the report every time.
The same script already save-and-restores TOKEN_REPORT.md around the token
bench, under a comment explaining exactly why -- a check that rewrites a
tracked file is not a check either. The rule was applied to one artifact of
that script and not the other.

It matters more here because the report embeds p50/p95/p99 latencies, so the
rewrite is load-dependent: running the suite on a busy machine moved them
30/247/348 -> 51/551/1089 microseconds, and `git add -A` would have committed
timing noise as a measurement.

Restored on every exit path via a trap, verified on both -- a passing run
leaves the file clean, and a forced floor breach (MIN_PARSE=200, exit 1) leaves
it clean too, which is the path the trap exists for. Restored, not compared:
TOKEN_REPORT.md earns a staleness check because it is byte-stable, and a
latency table would be permanently red. The two artifacts need different
treatment, not the same treatment.

An instrument with a side effect on tracked state is a defect even when its
verdict is correct, and the side effect is invisible in the instrument's own
output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SECURITY_AUDIT.md §2 -- the FIPS/cryptographic-posture section -- inventoried
three primitives (SHA-256 for content-addressing, xxHash, an LCG) and concluded
"No secret keying, no signatures, no KDF", and from that, that the FIPS gap
"affects compliance posture, not present-day confidentiality".

What the repository actually holds:

  HMAC-SHA256 under a fleet secret key, hand-rolled (RFC 2104), in
  ribosome/src/mac.rs, used by three call sites: ribosome::provenance for
  build-cache claims, germline::attest for verdicts, and ribosome::remote as a
  challenge-response worker handshake.

  Ed25519 signatures over build provenance, ed25519-dalek 3.0, with per-worker
  32-byte private seeds, a TrustStore of public keys, and revocation.

  Constant-time comparison, and TLS 1.3 (rustls + ring) behind ribosome's tls
  feature.

There is secret key material in this system, and the section whose job is to
inventory cryptography listed none of it. The conclusion drawn from the
omission -- that the FIPS gap is purely a compliance abstraction -- does not
hold: the HMAC and Ed25519 paths are in scope for validated-module
requirements.

The code is not the problem, and saying so matters. The hand-rolled HMAC is
verified against RFC 4231 cases 1-3 including the long-key case, which its own
module comment claims and which I checked because it is specific enough to
check. Comparison is constant-time. provenance.rs states its trust model at the
top -- including that HMAC is symmetric so any verifier can also mint -- names
asymmetric keys as the fix, and then implements them. The defect is entirely in
the document.

Why it happened is the mechanism rather than the oversight. The Scope line
named three surfaces: rmi, prototype, agentic-eval. ribosome and germline were
extracted from forge later (§1's own note, steps 148-149). §1 was widened to
audit five lockfiles; §2's scope never moved. Every cryptographic mechanism in
the repository arrived inside crates the crypto section did not consider itself
to cover.

An absence claim cannot fail loudly. "There is no X" stays green by doing
nothing, and a scope change expires it with no diff, no failing test, and no
way for a reader to tell. That is why this survived while numeric claims in the
same document were caught by pins.

scripts/check-crypto-inventory.sh now fails when a crypto dependency in any
Cargo.toml is not named in §2, when §2's inventory table names a crate no
lockfile contains, and when a hand-rolled primitive exists that §2 never
mentions -- the last because mac.rs's HMAC has no manifest entry and is
invisible to every dependency scan, which is exactly how it went unlisted.
Verified by breaking all five cases, plus a sixth to isolate the second
direction, which the obvious break did not reach (it tripped direction one
first).

Its first draft cried wolf four times on a correct document, and that is the
more useful lesson. Comparing every crypto name anywhere in §2 against the
direct dependencies reported ring (real, reached through rustls's feature
rather than declared), ed25519 (real, pulled in by ed25519-dalek), signature (a
real crate and an ordinary English word in a section about signatures), and
aws-lc-rs (the FIPS migration target §2 recommends -- a dependency the
repository deliberately does not have). Per rule 10 that is worse than no
check: an instrument that cries wolf converts "unknown" into "passing" as soon
as people learn to skip it. The fix narrows what counts as a claim -- the
table's Crate column is where the document asserts a dependency, prose is where
it discusses one -- and compares against the lockfiles rather than the
manifests, so a transitive or feature-gated crate is not called missing.

Also corrected here:

The Scope line now names every surface this document actually assesses, with a
note that it named three until today and why that matters.

§2's "no transport encryption" was true of RAP and false of the repository:
ribosome ships a TLS 1.3 worker transport. RAP itself is still plaintext
JSON-RPC over TCP, verified 2026-08-25.

§2's recommended actions named SHA-256 only; they now name the HMAC and Ed25519
paths, and add key provisioning and rotation for the fleet HMAC key and the
per-worker seeds, which nothing documents.

ribosome/src/mac.rs said "two subsystems" where there are three. The third is
the worker handshake -- a different use of the key, which is precisely what an
auditor tracing key material needs the list for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Having found the cryptography §2 had never inventoried, the next question is
the one a posture section exists to answer: where do the keys come from. Four
results, and the code that handles keys turns out to be better than the code
that accepts them.

There is no production key source. Every Signer::new, every
AsymmetricSigner::from_seed and every auth_key call site in this repository is
in a test. No environment variable, config field or CLI flag provisions a key.
That is defensible for a library -- the fleet key is the embedder's -- but it
means there is no reference path, which is why none of the below was written
down anywhere.

An empty key is accepted and verifies. Measured with a throwaway probe rather
than inferred from the constructor: Signer::new("w", Vec::new()) signs, the
record verifies, and an independent signer holding the same empty key forges
successfully. A one-byte key likewise. What makes this matter is that an empty
Vec is what an unset environment variable or a missing config field naturally
becomes, so a fleet whose key was never provisioned authenticates every claim
and reports success while providing nothing -- and nothing downstream can tell,
because a MAC over an empty key is a well-formed MAC. That is the guard
fall-through shape in a security primitive: the system says yes and means
nothing.

Left as an owner decision, and worth saying why rather than filing it blank.
Refusing a weak key means Signer::new returns a Result, a breaking change to a
public API of the build engine. Having already changed one CLI contract this
session, a second unilateral change to a security primitive is not mine to
make. The constructor's rustdoc now carries the measured behaviour and the RFC
2104 minimum, SECURITY_AUDIT.md §2 records it, and the recommendation ranks it
first of three because the failure mode is silent success. Filed as item 19,
explicitly not as a "design question": the check has been run and the behaviour
is measured; only the API decision is open. The last item filed here as needing
judgement turned out to be a defect nobody had run the check on.

The two schemes have opposite rotation stories and only one was written down.
Ed25519 rotates properly -- per-worker keys, a TrustStore, and revoke that
records a revocation rather than deleting the key, so a rejoining compromised
node cannot re-trust itself by re-announcing. verify fails closed on revoked,
unknown worker, substituted key, wrong subject and bad signature; every branch
read. The HMAC path has no rotation at all: one key per Signer, one auth_key
per WorkerServer, no key id, no overlapping acceptance window. Changing the
fleet secret requires every worker and verifier to change at once, so the
hardest operation to perform safely is the one with no support. Filed as item
20 -- adding a key id is a wire-format change.

And one recorded as an assumption rather than a defect. remote::next_nonce is
HMAC(b"ribosome-nonce", clock || counter) under a constant, public key, with a
comment arguing that unpredictability is not required "because the secret is
the key". That holds against replay. It is weaker against a pre-play attacker
who predicts the next nonce and induces a legitimate client to answer it first,
which needs a rogue endpoint and a guess at the issuing nanosecond, and is moot
under the tls transport. Whether it matters is a threat-model question, and the
reasoning in the code is explicit rather than accidental -- which is the
difference between an assumption to confirm and a bug to fix. This document has
warned six times that a comment agreeing with its author is not evidence; the
honest answer here is that the comment states a real trade-off and the owner
knows the deployment.

ribosome stays at 164 tests -- these are doc comments and audit prose, no
behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HANDOFF.md rule 10 declined to build an arity checker and recorded a
measurement instead, "to be repeated by hand". This is that repeat, and the
first thing it found is about the instruction.

The measurement cannot be repeated, because the method was not recorded. The
obvious reconstruction -- unique `pub fn` names in rust blocks across
RecursiveMachineIntelligence/docs/*.md, the same extraction
check-rmi-api-doc.sh uses -- gives 210 documented functions and 119 defined
exactly once, against the recorded 162 and 66. Nothing distinguishes drift from
a different counting rule. A number recorded without its method is not a
measurement anyone else can take, which is the one thing an instruction to
repeat by hand has to supply.

Re-measuring from scratch found 7 arity disagreements, and 3 were false
positives of exactly the class rule 10 named: find_similar, get_subgraph and
put_raw are documented as `pub fn name(/* ... */);`, an elided parameter list a
naive parser reads as one argument. Rule 10 said an arity checker "has a
false-positive class that tripped twice on my own placeholders"; it tripped on
the same placeholders again. That judgment is now confirmed rather than
asserted, which is the useful outcome of repeating a measurement whose
conclusion was "do not build this".

The other four were real, and pulling them opened something larger.

check-rmi-api-doc.sh's baseline of 0 was partly held up by prose. Its existence
test was a word-boundary search for the documented name anywhere in src/. Of
275 documented items, 8 were satisfied by an occurrence that was not a
definition, six of them ordinary English words inside comments:

  KnowledgeBase::facts     "// Find most similar facts"
  KnowledgeBase::rules     "...types, operations, composition rules, and"
  Literal::positive        "/// Matrix must be positive definite."
  Literal::negative        "...log of non-positive, sqrt of negative"
  State::satisfies         "/// Algebraic properties this operation satisfies."
  Term::variable           "/// Type variable for polymorphism"
  Term::symbol             inside a string literal
  NetworkArchitecture::add_edge   `graph.add_edge(...)`, a petgraph call

Every one is a public method that does not exist. The real names are get/all;
Literal is a private enum with Positive/Negative variants rather than a public
struct with constructors; holds_all; var; constant; connect. The check reported
0 missing throughout. A baseline of zero held by matching comments is rule 8
exactly -- a passing result on a subject outside the assertion's reach, wearing
a green tick.

Pulling the same thread through those blocks found more the name check cannot
see by design:

NodeId is not a type anywhere in the crate. It appears in three documented
signatures; the real ids are Uuid. The checker never looks at types used in
signatures, only at names the docs define.

Substitution is a type alias for HashMap<String, Term>, documented with an
entire impl block -- empty, bind, lookup, apply, compose -- none of which exist
as methods. compose is a free function taking two substitutions.

plan was documented twice, inconsistently, and neither matched. api.md gave
four free-function parameters, architecture.md gave three, and the
implementation is a method taking &Domain and &Goal returning Option<Plan>.
Rule 9 warns that two documents agreeing is not corroboration when one is a
copy; here they did not even agree with each other.

InferenceEngine::backward_chain does not exist -- the real name is prove -- and
the name check passed it because lang/grad.rs contains
`#[test] fn backward_chain()`, an unrelated gradient-tape test. A definition of
the wrong kind, in the wrong module, answering for a public API. Its neighbour
forward_chain had receiver and argument mutability reversed: documented
`&mut self, &KnowledgeBase`, actual `&self, &mut KnowledgeBase`.

All corrected, and the checker now requires a definition rather than a mention.
Verified by reintroducing one phantom and watching the tightened criterion fail
where the old one passes. The baseline stays 0 because the entries were fixed,
not because the bar moved -- the same shape as check-doc-blocks.sh, where the
count went up when the criterion did.

It is still not a signature check, and now says so: backward_chain would
survive the new criterion too, since a test function is a definition. Arity and
receiver remain deliberately unchecked for rule 10's reasons, which this pass
re-confirmed by tripping over the placeholders.

The count moved 275 -> 269, and that figure had no pin, which is how the
criterion could tighten under it unnoticed. It has one now -- rmi_api_items,
measured by running the checker -- taking documented-count pins to 85, verified
in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HANDOFF.md rule 10 declined to build an arity checker -- 41% coverage and a
false-positive class that tripped twice on the author's own placeholders -- and
recorded the measurement instead, "to be repeated by hand". Repeating it found
the flaw in that instruction: only the numbers were recorded. The obvious
reconstruction gives 210 documented functions and 119 uniquely defined against
the recorded 162 and 66, and nothing distinguishes drift from a different
counting rule. A number without its method is not a measurement anyone else can
take.

scripts/measure-rmi-doc-arity.sh is that method. It prints the figures, exits 0
whatever it finds, and is deliberately not wired into CI -- rule 10's decision
stands and is not overturned by whoever happens to be holding the numbers.

What the repeat established that the original could only assume:

The false-positive class is a one-line filter. All three false positives were
`pub fn name(/* ... */);`, an elided parameter list a naive parser reads as one
argument -- the same placeholders that tripped the original author. Skipping
them removes every false positive on the corpus.

Coverage is 60%, not 41% (117 of 196 comparable), because the corpus grew.

0 of the 117 now disagree, after this pass fixed the four that did.

So the ledger has changed: 60% coverage with no false positives is not the
41%-and-cries-wolf that was declined. The decision is left open rather than
reversed, because the tool's soundness rests on an assumption worth staring at:
it matches by bare name, so a function defined once anywhere in the crate is
assumed to be the documented one. That is why backward_chain was flagged, and
the flag was correct by luck -- the only definition of that name is a
`#[test] fn` in an unrelated module, and had the test taken two arguments the
check would have passed.

Also fixed here: architecture.md documented the same InferenceEngine block as
api.md did, with backward_chain (really prove) and forward_chain's receiver and
argument mutability reversed. Correcting it dropped the documented-item count
269 -> 268, since prove was already counted from api.md; the pin moved with it.

The break test for the new script failed first, informatively. Dropping a
parameter from `connect` produced no report, which looked like a broken tool
and was a badly chosen test: connect is defined twice, in transport.rs and
architecture.rs, and is one of the 79 names the script deliberately skips
rather than guesses at. Re-broken on add_fact, which is in the comparable set,
it reported immediately. Verifying against the wrong entry point is a way to be
green about nothing -- this document's own words, earned again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check-orphan-sources.sh finds files no `mod` reaches. check-ignored-tests.sh
finds tests no schedule names. HANDOFF.md says together they "cover the two ways
code sits in the tree without ever running". There is a third, and it had ten
files in it.

cargo build and cargo test do not compile examples or benches. rmi has 9
examples and 1 bench; the only --all-targets anywhere in CI is the CUDA
compile-check, and prototype has no examples at all. So nothing in this
repository -- not CI, not test-all.sh -- had ever compiled any of the ten.

They fall exactly between the two existing instruments. An example lives
outside src/, so no `mod` needs to reach it and check-orphan-sources.sh
structurally cannot see it; it is not an #[ignore]d test, so check-ignored-
tests.sh cannot either. Both scripts were correct about their own subject.

All ten compile clean today, and that was verified by breaking one and watching
the check fail -- `error: could not compile rmi (example "swarm_collaboration")`
-- because a green cargo check proves nothing until you know it reached the
targets you meant. CI now has an `--examples --benches` step on the rmi job.

check-orphan-sources.sh's header now states its scope: it walks <crate>/src
only, examples and benches are invisible to it and always will be, and a green
run there must not be read as covering them. HANDOFF.md's "the two ways"
sentence now says three, and says what it used to say -- that phrase is the
confidence a pair of instruments buys you whether or not it is warranted.

The negative result is the point. Nothing was broken, and nothing was keeping it
that way, which is the state compute/wasm.rs and perf_report were both found in
-- and both of those were broken by the time anyone looked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by asking the neighbourhood question. check-rmi-api-doc.sh covers
RecursiveMachineIntelligence/docs and nothing else -- so what about the other
crates' documentation? Seventeen markdown files outside that directory carry
Rust `pub` items in fenced blocks and nothing checks any of them. Most are
design documents and legitimately so. internals/ is not: README.md lists it as
"Compiler-internals documentation", and it is written throughout in the present
tense.

15 of its 36 documented `pub` items do not exist. And the prose is further from
the code than the signatures are. Chapter 1's opening three claims, each
checked:

  "Each stage is a separate crate with a clean query-based interface"
      prototype is ONE crate: 64 files, a single [package].

  "The query engine (based on Salsa) tracks dependencies automatically"
      salsa is not a dependency of any crate in this repository, and nothing
      in prototype/src implements a query cache.

  "The CompileSession holds all configuration for a compilation"
      There is no CompileSession.

CompileSession, DefId, InferCtxt, TraitObligation, EffectChecker are
rustc/salsa-shaped names for an architecture that was designed and not built.
Writing that down was reasonable; presenting it as documentation of what exists
is the cookbook/ failure again -- the most detailed documentation in the
repository, describing a system that was never there. An agent reading
internals/ to learn the codebase learns a compiler it will not find, and learns
it in the register of fact.

Not fixed. Recorded, bounded and labelled, which is the proportionate response
to a documentation set this size found late in a session:

scripts/check-internals-doc.sh, baseline 15, shrink-only, wired into the rmi CI
job. It requires a definition rather than a mention from the start -- earning
that criterion the hard way in check-rmi-api-doc.sh, where 8 items turned out to
be satisfied by English words in comments, was enough. Verified in both
directions: a new phantom fails, and a baseline entry that has stopped applying
fails too.

internals/01-architecture.md now opens with a measured status note naming the
three false claims and pointing at ARCHITECTURE.md for what exists.

The prose has no mechanism and cannot get one from this. None of those three
claims names a `pub` item, so the checker is blind to every one of them -- and
they are the most misleading part, because a reader follows a stated rule where
the examples do not reach. Nothing checks prose. The label is hand-written and
will decay like any other hand-written thing.

Baselines go from four to five.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first thread this session that came back mostly clean, which is worth
recording as such.

DOCS.md opens: "There are 22 Markdown documents at the repository root... This
index says which is which, so nothing here has to be read to find out whether
it is still true." That is a universal claim, and the only thing checking it was
the root_docs count pin, which compares the number in that sentence against
`ls *.md | wc -l`.

Checked by hand: all 22 are indexed, and every root document DOCS.md names
exists. The claim holds.

The hole beside it is narrower than the claim and real. A count catches adding a
document and forgetting the number. It does not catch adding a document,
updating the number, and forgetting the entry -- which is the failure that
sentence exists to prevent -- nor two documents swapped in one commit, which
nets to zero. Rule 4 one size down: a claim that says "every", guarded by a
check that counts.

scripts/check-docs-index.sh closes it in both directions: a root document with
no entry, and an entry pointing at a document that is not there, which is what a
rename leaves behind. Verified by breaking both. The second break needed
isolating -- renaming HANDOFF.md inside DOCS.md removes a real entry and adds a
dead pointer at once, so direction one fired first and direction two proved
nothing until tested alone. That is the second time this session a break test
had to be re-aimed before it tested what it claimed to.

Also measured, and not a defect: 13 documentation directories are indexed
nowhere -- agent-guide/, cookbook/, internals/, migration-guide/, quick-start/
and others, 68 files between them. DOCS.md scopes itself to the root in its
first sentence, so this breaks no promise. It is worth knowing anyway, because
internals/ turned out to describe a compiler that was never built and one reason
it went unexamined this long is that no index points at it. Recorded rather than
fixed: an index nobody has committed to maintaining is the next thing to go
stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The follow-through on the previous commit, which bounded internals/ at 15
undefined items without saying what should happen to them.

First, the part that had to come before any labelling: each of the 15 was
checked individually against prototype/src, and none is a rename. The real
compiler has Effect, EffectAnalysis, Type and TypeAlias; there is no TypeError,
InferCtxt, DefId, EffectChecker, AstVisitor, HirExpr, RuleMatch or
CompileSession under any name, nor anything shaped like them. Correcting beats
labelling wherever the thing exists -- here nothing did.

So the 12 blocks holding those items now carry:

  **Not implemented.** Design sketch -- no such item exists in `prototype/src`.

which is MAGE_SPEC.md's precedent exactly: five constructs it documents and does
not implement, each labelled where it appears, "the design intent is worth
keeping, the false impression is not".

check-internals-doc.sh now skips a labelled block, using the same rule
check-doc-blocks.sh uses for a MAGE block labelled "Invalid MAGE today" -- the
nearest non-blank line above the fence -- so there is one convention to learn
rather than two.

The obvious objection to all of this is that labelling is a way to get a
baseline to zero without doing anything. Three things answer it:

The baseline of 0 means "nothing here claims to exist without existing". It does
not mean "nothing left to do here", and the header says so.

Every run prints "17 documented items, 12 block(s) skipped", so the size of the
gap survives the label. A ratchet that reached zero by relabelling and then went
quiet about it would be the exact defect this repository keeps finding.

Removing a label from a block that still describes nothing now fails the check.
Verified, along with the two existing directions.

Chapter 1's status note is updated to describe the labelled state, and to record
one thing no checker will ever see: its ASCII crate graph -- rdx_driver,
rdx_lexer, rdx_parser, rdx_resolve, rdx_hir, rdx_types, rdx_effects, rdx_mlir --
describes crates that do not exist either. Those are diagrams, not `pub` items.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Small, and mostly a negative result worth recording as one.

Chasing the `rdx_` crate names in internals/ diagrams -- on the theory that a
rename leaves residue in more than one place, which this repository has seen
before -- turned up 37 files using the prefix and no crate named rdx_*. But
every one of those is `rdx_source` / `rdx_files` / `rdx_source_before` and
friends: JSON *field* names, matching benchmarks/corpus-schema.json, matching
what token_bench.rs, token_canonical.rs and reliability_bench.rs actually read.
Legacy prefix, used consistently, live in code. Not fiction, and not worth a
rename that would touch the corpus schema. internals/ remains the only place
rdx_ names crates, and that is covered by its status note.

Two real things fell out:

benchmarks/README.md cited "targets from mage_ECOSYSTEM.md §4.4" -- lowercase
`mage_`, where the file is MAGE_ECOSYSTEM.md. On a case-sensitive filesystem
that is a pointer to nothing. It has the shape of a global lowercasing that
caught a filename, so I checked whether it spread: exactly one instance in the
repository. An isolated typo rather than the multi-costume rename defect this
document warns about. Now a real relative link, and the target verified to
exist.

The same table defined token efficiency as `rdx_tokens / reference_tokens`.
Neither field exists: the schema has `token_count` under both `solution` and
`rust_equivalent`, which is what the benches read. The formula now names the
real fields.

The citation itself checks out, which is the other negative result:
MAGE_ECOSYSTEM.md §4.4 exists and lists all seven metrics with identical
targets. The table is also honestly labelled "Target" rather than presented as
measurement, so the four metrics with no mechanism behind them are not claims
about the current state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The largest remaining piece of honest work in that documentation set, taken one
chapter at a time. Chapter 1 is the entry point and was the most wrong.

What it now says, each claim read out of prototype/src:

The compiler is one crate, mage-prototype, 62 public modules in lib.rs. It
compiles by running a fixed sequence of passes eagerly, front to back. No query
engine, no incremental recomputation, no per-stage crate split.

run_check in main.rs IS the pipeline, and the chapter's phase table is that
function's own numbering: 0 legacy::translate, 1 lexer::lex, 2 parser::parse,
2.5 elision::elide, 3 resolve::resolve, 4 types::check, 5 effects::infer_effects,
5.5 verify::verify_module, 5.6 abl_shape::check_module_shapes, 6 heal. Phases 3
through 5.6 return diagnostics rather than halting, so one run reports
resolution, type, effect, contract and shape problems together; a parse error is
the one hard stop because every later pass takes an ast::Module.

Real data structures replace the invented ones. Span is { offset, len, line,
col } -- not { file: FileId, start, end }, and there is no FileId and no
SourceMap, because the compiler works on one source at a time. ast::Module is
{ items }. hir::Diagnostic and hir::Severity are what the passes return, and
run_check counts only Severity::Error toward the failure total.

The original design is preserved in Appendix 1.A, labelled Not implemented: the
nine-crate rdx_* graph, the Salsa query group, and the query-driven/incremental/
parallel principles that went with them. The design intent is worth keeping;
presenting it as description is what made the file misleading.

Two things worth recording about the writing of it.

I introduced three false claims of exactly the class this session has been
fixing, and caught them by checking my own draft rather than by review: there is
no --check-json flag (JSON is --check plus a --json modifier), no plain --fmt
(only --fmt-compact and --fmt-expand), and the legacy flag is --syntax=legacy,
not --legacy. Every flag the chapter now names was then verified present in
main.rs, and the documented pipeline was run end to end. Writing documentation
is not a safer activity than writing code.

The check caught a label that did not register. check-internals-doc.sh reads the
nearest non-blank line above a fence, and my "Not implemented" label wrapped
across two lines, putting the marker on the line above the one that is read. It
failed loud, as a missing item, rather than passing silently -- the safe
direction -- but the constraint is now written into the script's header and the
labels are single-line.

Eight chapters remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second of the internals/ chapters, and a different result from the first, which
is worth reporting as such: most of this one checks out.

Verified correct against prototype/src: Token and Span match lexer.rs exactly,
the Lexer struct matches field for field, and the Pratt parser is real --
parse_expr_bp(&mut self, min_bp: u8) with precedence climbing on l_bp < min_bp.
The lexer's error recovery is real too: it emits TokenKind::Error in place and
keeps going, and run_check counts those tokens and proceeds to parse.

Three things were wrong.

The Parser block documented `pub struct Parser` owning a Vec<Token> and a
diagnostics list, with `pub fn parse(tokens: Vec<Token>) -> ParseResult<Ast>`.
The real Parser is private, borrows a slice, carries a `blocks` map for `block`
macros, and the entry point is `pub fn parse(tokens: &[Token]) -> Result<Module,
ParseError>`. Neither ParseResult nor Ast exists anywhere in the crate;
ParseError does, and its three fields are now documented.

The Pratt snippet named parse_expr rather than parse_expr_bp and returned the
nonexistent ParseResult.

The parser's "Error Recovery" section described synchronizing on `;`, `}` or a
top-level keyword and resuming, "producing partial ASTs for error-tolerant
tooling". The parser does none of that: the first unexpected token aborts, and
Parser has no diagnostics list to accumulate into. Verified by running a file
with two syntax errors -- only the first is reported, and --check exits before
name resolution.

That section is now replaced with the asymmetry that actually holds, because it
explains something Chapter 1 asserts: the lexer recovers and the parser does
not, which is why a parse error is the pipeline's one hard stop. Every later
pass takes an ast::Module and there is no partial one to hand them. Recovery is
worth having for the reasons the old text gave, and building it means giving
Parser a diagnostics list and deciding what a half-parsed Module means to the
eight passes downstream -- a design question nothing here has answered, now
recorded as one.

Seven chapters remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third internals chapter, and the failure here is systematic rather than
scattered.

Every AST type the chapter documents is real -- Module, Item, ItemKind, Type,
Expr, Block, Stmt, Ty, Effect -- and Item matches the source field for field.
The name-level checker passed all of them, which is exactly its documented
limit: it cannot see that a definition is incomplete.

Every enum except Effect was a subset:

  ItemKind   11 of 20 variants
  Type       17 of 32
  Expr       22 of 35
  Stmt        3 of 5
  Ty         24 of 31
  Effect     18 of 18   (correct)

Each block presents itself as the definition, with no ellipsis and no note.

The nine ItemKind variants missing are exactly MAGE's distinctive ones: Agent,
Net, Kb, Evolve, Train, Swarm, Data, Extend, and Static. The documented set --
Function, Struct, Enum, Trait, Impl, Module, Use, TypeAlias, Const, Effect,
Spec -- is a Rust-shaped subset. A reader learning the AST from it would not
know that `net`, `agent`, `swarm`, `train`, `kb` or `evolve` are items at all,
which is most of what distinguishes this language.

All five are regenerated from source rather than hand-listed, because
hand-listing 35 variants is how the drift happened in the first place. Verified
by comparing the variant name sets, not just the counts -- a count match can
hide a wrong name.

One CLI claim was also wrong. "The entire AST can be emitted as JSON via
`mg parse --emit ast`": the derives are real, but there is no `mg` binary (it
is mage-parse) and no --emit flag, and --check --json returns diagnostics
rather than the tree. The route that does work is the RAP server's
language/parse, which returns the serialized module in an "ast" field. The
capability was real and the invocation was invented.

Six chapters remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fourth internals chapter, and rewriting it turned up a compiler defect rather
than only a documentation one.

f describe[T](v: T) -> str ~> T: TotallyMadeUpTrait { "described" }

reports Errors: 0, Status: OK. TotallyMadeUpTrait exists nowhere. The bound is
neither resolved nor enforced.

where_clause is parsed into Vec<WherePredicate> and stored on the function, and
every consumer of that field either prints it (fmt), strips lifetime predicates
from it (elision), counts its tokens (token_budget), or constructs an empty one
(abl_bridge, manifest, nl_engine). types.rs never mentions bounds. There is no
trait solving anywhere in the crate -- no obligations, no impl table -- and
ItemKind::Trait reaches elision, fmt, mlir and nl_engine but never the type
checker.

Taxonomy §3, accepted and silently discarded: the same shape as a swarm's
dispatch block, which also parsed, was stored, and reached only the formatter. A
constraint that typechecks and means nothing is worse than a missing feature,
because the program looks constrained. Filed as item 21 with the reproduction.
Not fixed unilaterally -- the cheap honest fix is the one `use` got, a warning,
but that is a compiler behaviour change and this session has already made one.

The chapter itself, corrected:

§4.2 documented an InferCtxt with an obligation list. The real context is
TypeChecker, reached through pub fn check(&ast::Module) -> TypeChecker, which
returns the checker rather than a Result so the caller reads .diagnostics --
which is why phase 4 reports every type error in one run. Unification and the
occurs check are free functions over a private Subst, not methods, and unify
returns Result<(), String>: there is no TypeError type. Diagnostics get their
structure where they are recorded, not where they are detected. The InferCtxt
design is kept below, labelled.

§4.4 Trait Solving described obligation collection, a solver and where-clause
handling. None of it exists; the section now says so and carries the
reproduction above.

§4.6 listed seven coercions -- &!T to &T, auto-borrow, [T]~ to &[T], s to &s,
and deref for ^T, $T, @t -- inserted as HirExpr::Coercion nodes. There is no
HirExpr and no Coercion node. Two coercions exist, both on array literals, both
added so that an agent writing [1, 2, 3] for a Vec or slice parameter gets the
obvious thing. Both happen during unification, so nothing downstream sees a
node.

§4.5 named instantiate_generic(def_id, type_args). The real one is
instantiate(&mut self, ty, map), and the universal quantification it implements
is load-bearing: without a fresh copy per call site, id(1) and id("ab") in one
program conflict.

Verified correct and left alone: unification, the occurs check, substitution,
and fresh-per-call-site generic instantiation.

Five chapters remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…that

Fifth internals chapter, and the first that needed essentially nothing. Worth a
commit anyway, because "already correct" is a claim like any other and the way
to hold it is to re-run it.

An earlier session had corrected this chapter thoroughly: §5.3 (effect
hierarchy), §5.4 (capability validation), §5.5 (effect polymorphism) and §5.7's
diagnostic renderings each already carry an accurate "design, not
implementation" caveat naming what does not exist. Four claims were re-run
against the compiler rather than taken from those caveats or from HANDOFF.md,
both of which are documents that can decay:

No hierarchy. A caller annotated / net that calls an / io callee reports
"function `outer` performs undeclared effects: [IO]". Declaring one effect
grants nothing else.

Over-declaration is silent. +f f(x: i32) -> i32 / io { x } gives 0 errors and 0
warnings. The annotation is an upper bound, and the W0410 "unnecessary effect"
warning §5.7 renders as compiler output does not exist -- which §5.7 already
says.

The Effect enum matches the documented table exactly: 18 variants in order, the
seventeen non-Custom ones being §11.2's built-in kinds.

User-defined effects work. An `effect Db { fn query(q: String) -> String; }`
block satisfies / Db, and an undeclared custom effect errors by name: "declares
unknown effect `Nonexistent` -- it is not a built-in kind, and no `effect
Nonexistent { … }` declares it".

The only edit was removing my own noise. This session's automated labelling pass
had stacked a generic "**Not implemented.** Design sketch…" line on top of
§5.2's existing caveat, which was more specific and better -- it names
infer_effects as the real entry point and explains that it walks the AST in
whole-module passes because the call graph has to be closed before any
function's set is final. The generic line is gone and the marker folded into the
last line of the caveat that was already there, which is where
check-internals-doc.sh reads it.

Four chapters remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI failed on 992cae2 and I did not catch it locally.

The reproduction block I added to internals/04-type-system.md §4.4 -- the
`~> T: TotallyMadeUpTrait` program that demonstrates unenforced where-clause
bounds -- is a MAGE block like any other. It raised the documented-block count
205 -> 206, and because it defines `main` it also raised the documentation
entry-point count 57 -> 58. Both are pinned in HANDOFF.md and both were stale
the moment I committed.

Locally I ran check-doc-blocks.sh, which passes because the block typechecks,
and stopped there. What I did not run was the pin that compares the *count* to
what HANDOFF.md claims -- so I verified the thing I had changed and not the
thing that measures it. Adding documentation moves a measured figure, and the
figures are pinned precisely because people forget that.

This is the mechanism working: the failure is exactly the class this session has
spent its time on, committed by me, and caught by a check I helped extend two
days' worth of commits ago rather than by review.

HANDOFF.md now reads 206 and 58, verified against a fresh run of both checkers
and against the pin.

Two commits sat on top of a red CI before I looked -- 435e2dc and this one.
The watch had reported it; I read the notification for Chapter 4's run after
pushing Chapter 5. Check the result before building on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sixth internals chapter. Its opening sentence -- "The backend lowers typed,
effect-checked HIR into MLIR, then progressively lowers through MLIR dialects to
LLVM IR and finally machine code" -- is false in every clause.

prototype/src/mlir.rs exports exactly one public item:

  pub fn emit(module: &ast::Module, effects: &EffectInfer) -> String

It walks the AST, not a lowered HIR, and writes MLIR-flavoured text into a
String beginning with a banner comment. It has one caller, --pipeline, whose
phase 6 does this with the result:

  ▸ Phase 6/7: MLIR lowering
    ✓ 47 lines of MLIR generated

It counts the lines. Nothing parses that text, lowers it, or feeds it anywhere.
No llvm, mlir, inkwell or melior crate appears in any manifest in this
repository; mlir.rs has no pass infrastructure, so §6.4's effect-elimination,
sugar-lowering and agent-lowering passes do not exist; and nothing anywhere
emits an object file or links one, so §6.6's six targets -- native binaries,
.wasm, PTX, AMDGPU ISA, Vulkan compute -- produce nothing.

Two things kept the correction honest rather than sweeping.

Compilation to hardware does happen, through a completely different path, and
saying "the backend does not exist" without that would be its own false claim.
--run=abl-bytes lowers net items to Agentic Binary Language and dispatches them
to a compute backend: CpuBackend, or CudaBackend under --features cuda, routing
matmul through cuBLASLt and elementwise ops through NVRTC, with 1,229 CUDA tests
passing on the hardware. That path shares no code with mlir.rs. The status note
points a reader who wants the GPU story at Chapter 8 and abl_compute instead.

The cost oracle in §6.5 is real -- cost.rs has query_cost, list_costs, compare,
CostEstimate and CostComparison. What is false is §6.1's reason for it: it is a
standalone table keyed by construct and target, and does not read MLIR.

The design is kept and labelled, as with Chapter 1's appendix.

Also recorded in Traps: adding a MAGE block to documentation raises doc_blocks,
and raises doc_evals too if it defines main. Both are pinned. check-doc-blocks.sh
proves the block typechecks and says nothing about the count, which is how the
Chapter 4 commit went red. This commit's counts were checked before pushing --
206 and 58, unchanged, because a ```rust block is not a MAGE block.

Three chapters remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s not exist

Seventh internals chapter, and the failure here is under-claiming rather than
invention -- the opposite of Chapter 6.

§7.4 was a "Planned Methods" table: three implemented, eleven planned. 37
methods are published and dispatch, across 21 namespaces, and the count is
pinned with a test exercising every one against its published parameter list.
None of the eleven exists under the name given, and most of what they described
shipped under a different one:

  query/cost      ->  cost/query, cost/compare
  query/effects   ->  effects/infer, effects/check
  skb/suggest     ->  skb/query, skb/rules, skb/spec
  build/full      ->  build/heal, build/recover, pipeline/recover-and-encode

The convention that won is namespace/verb grouped by subsystem (cost/query)
rather than query/noun grouped by operation (query/cost), which is worth knowing
before adding a method. The five genuinely-absent ones -- query/type,
query/completions, query/hover, query/definition, query/references -- are the
editor-service methods, and that is the part that did not happen.

§7.4 is now the full published surface, generated from MAGE_ONTOLOGY.json's
rap_methods section rather than hand-listed. CI checks that file against a fresh
--emit-ontology, so the table cannot drift from the binary without the ontology
drifting first.

§7.6 claimed a MAGE-vscode extension speaking to RAP, with hover routed through
query/type and completions through query/completions. The extension does not
exist: there is no MAGE-vscode/ directory and no VS Code file is tracked
anywhere in the repository. editors/README.md advertised the same thing, linking
to ../MAGE-vscode/. Both corrected.

What ships is neovim, helix, zed and a tree-sitter grammar. Tree-sitter
highlighting works in all of them. The Neovim LSP registration cannot, for three
independent reasons: rap.rs has zero occurrences of `initialize` or
`textDocument`, so RAP is not an LSP server; there is no `rap` binary, which is
what the config spawns; and RAP listens on TCP while lspconfig's cmd speaks
stdio. Its settings block names completion.autoimport, inlayHints.typeHints and
diagnostics.skb, none of which RAP has methods for.

That file now carries a warning rather than being deleted or rewritten, because
the fix is a decision: write an LSP shim over RAP, or accept that RAP is an
agent protocol and not an editor one -- which is what §7.1's own design goals
describe. Filed as item 22.

Doc counts checked before pushing: 206 and 58, unchanged.

Two chapters remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oints at

Eighth internals chapter, plus a correction to my own Chapter 6 commit.

§8.1 said "Rules live in skb/rules/ as JSON files organised by category" and
listed six files. Nothing reads those files.

  builtin_rules() in skb.rs   255 rules   8 databases   <- what the compiler serves
  skb/rules/*.json             56 rules   6 files       <- read by no crate

The compiled counts are pinned by rule_counts_per_database: Ownership 40,
Borrow 40, Lifetime 35, TypeSafety 40, Concurrency 35, FFI 20, AgentElision 30,
SwarmSafety 15. The last two are the MAGE-specific databases and have no JSON
file at all -- the on-disk tree predates them.

The tree is real and not junk: skb/manifest.json, skb/rule-schema.json, and six
rule files. But editing it changes nothing, and two of the six filenames the old
text gave were wrong anyway -- borrowing.json and lifetimes.json, where the files
are borrow.json and lifetime.json.

This is the stdlib/ shape: a directory that looks authoritative, is read by
nothing, and diverges silently from the thing that is. What made it hard to see
is that "skb/rules" does appear in rap.rs and ontology.rs -- as a RAP method
name, not a path. Filed as item 23. check-orphan-sources.sh cannot catch this
class: it finds .rs files no `mod` reaches, not data nothing loads.

Verified real and left alone: the ACI subsystem. §8.4's five engines map onto
DynamicWarningEngine, IntelligentDebugEngine, PerformanceAdvisor, and the
codebase/swarm structures in aci.rs.

And a correction to Chapter 6. Two commits ago I wrote that a reader wanting the
GPU story should "read Chapter 8 and abl_compute". Chapter 8 is SKB and ACI. No
internals chapter covers the ABL compute path at all. That pointer now says so
and sends readers to ARCHITECTURE.md, UNIFICATION.md and the modules themselves.
I introduced a false cross-reference in the same commit that corrected a chapter
full of them, and caught it only by checking my own work two chapters later.

Doc counts checked before pushing: 206 and 58, unchanged.

One chapter remains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last file in the set, and the one that repeated every error at once: a
13-crate rdx_* map, a pipeline ending in LLVM and a binary, and chapter blurbs
promising a query engine, trait solving, LLVM codegen and IDE integration.

Corrected:

The pipeline diagram is now run_check's actual phases, with its own numbering,
and says what it does not do -- no HIR lowering step, no LLVM, no binary; --check
ends there. It also points out that mlir::emit produces text nothing consumes,
and that the path reaching a GPU is Agentic Binary Language through abl_bridge /
abl_compute / cuda_backend, which no chapter covers.

The crate map is now a module map: one crate, mage-prototype, 62 public modules,
with the thirteen the pipeline uses and their real key types. `Parser` is noted
as private, since `parse` is the only way in.

The chapter blurbs now say what each chapter contains after correction,
including the parts that are design: Chapter 6's LLVM backend, Chapter 4's
absent trait solving, Chapter 2's absent parser recovery.

A dead link: [Book](../book/README.md). There is no book/ directory. Now points
at quick-start/, and every one of the README's fourteen links was checked to
resolve.

The original design is preserved at the bottom under a "not implemented" label,
because it is coherent and someone may still want it -- with a closing note that
the key types which do exist (Token, Span, Expr, Item, Ty, Effect, Rule,
Diagnostic) live in modules of the single crate rather than crates of their own.

That completes internals/. Across nine files: two chapters described systems
that do not exist at all, four had real machinery under wrong names, three
documented features that parse and are then discarded, and one -- Chapter 5 --
was already correct and was re-run rather than trusted. Three compiler findings
came out of it, filed as items 21, 22 and 23: unenforced where-clause bounds, a
Neovim LSP registration that cannot work, and a 56-rule JSON tree that nothing
reads while the compiler serves 255 rules from the binary.

Doc counts checked before pushing: 206 and 58, unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two hours ago I wrote "Labelled, not rewritten" into HANDOFF.md's internals
section. It was true when written and false by the end of the day, because I
then rewrote all nine files. That is the decay this document is about, occurring
inside the section about it, at the shortest interval yet observed here.

Corrected, and the rewrite is now recorded as its own section:

  Described a system that does not exist   ch1 (query engine, nine crates),
                                           ch6 (MLIR -> LLVM -> machine code)
  Real machinery under wrong names         ch2, ch3, ch4, ch7
  Parses and is then discarded             ch2 (parser recovery), ch4 (trait
                                           bounds), ch8 (the rule JSON)
  Already correct, re-run not trusted      ch5

The finding that inverts this document's usual lesson: rewriting documentation
found three compiler defects. Reading code to describe it accurately is a
different activity from reading it to review it, and it reaches places review
does not. Items 21, 22 and 23 all came out of it.

Two patterns worth carrying. Under-claiming is as common as invention -- ch7
documented 3 of 37 RAP methods and listed eleven "planned" ones that had shipped
under other names; ch3's enums were each partial listings presented as
definitions, and ItemKind's nine missing variants were exactly the constructs
that distinguish MAGE. And the name-level checker passed nearly all of it, which
is its documented limit stated in numbers: it cannot see a partial enum, a wrong
signature, or a type used in a signature it never defines.

Both of my own mistakes are recorded where they happened rather than only here:
the false cross-reference introduced in the same commit that corrected Chapter 6,
and the Chapter 4 commit that turned CI red by moving two pinned counts while two
further commits landed on top of it.

Doc counts checked before pushing: 206 and 58, unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dump

`ontology.command` named `mage-parse --emit-ontology`, which is
write_local and therefore requires a grant -- so an agent had to be
trusted with MAGE before it could find out what MAGE is.

The gate is not the bug. --emit-ontology genuinely writes a file, and
this manifest's note already said so. The bug is the pointer: MAGE has
had a grant-free, agent-facing discovery command all along. --manifest is
the token-compact index that cli_manifest.rs calls "read this first",
effect class pure, and --describe expands any entry from it.

So the ontology now names --manifest. --emit-ontology is unchanged and
remains the way to obtain the complete document when writing a file is
what you want.

Found by IronStack's new `discovery must not require authority` check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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