Conversation
…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>
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HANDOFF.mdhad 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 inMAGE_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:
rmi's vendored warnings, declined migration steps, separate workspaces, external dependency resolution.resumefor effect handlers — the largest unbuilt thing in the language — and theintliteral constraint./ agentis called out as the best next task.agentis 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
--checkcannot see is now counted — seven of the fourteen — rather than merely described.Two new traps:
gh pr merge --automerges 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:
Claims checked rather than carried over
The doc's own standard is that measured claims get run, so:
/ agent→parse error: expected identifier, found KwAgent;/ unsafelikewise. Verified.agentis in the §11.2 effects table —MAGE_SPEC.md:1325. Verified.jobs:rather than grepped. My first grep said 11; it was includingpush:andpull_request:underon:. The inherited figure was right and my check was wrong.scripts/test-all.sh --check-docsgreen — all 45 pinned counts still match, so the rewrite preserved every patterncheck-doc-counts.shkeys on.Docs only; no code changes and no test-count changes.
🤖 Generated with Claude Code