More
Every goal, measured.
MASTERPLAN.md §2.3 sets ten measurable goals and
§19.3 adds an eleventh; two of them split in two, so the scorecard has
13 rows. 9 are met, 2 are
narrowly over, and 2 cannot be measured at all without users.
scripts/northstar.sh takes the readings it can take and prints a
scorecard rather than failing on a miss — a gate that is red on purpose is a
gate people learn to ignore — but it does fail when a reading cannot
be taken, which is how a metric quietly stops being collected.
Three of those rows carry a qualification that the bar cannot show, and they are the three worth reading twice. G3's 50 ms is met at this repository's scale — p50 21 ms, p95 40 ms on its largest program — and the plan frames that goal at a million lines, which a whole-program re-check will never reach; that needs the query engine nobody has built. G6 is met for the task count and the uncolored half is still open: tasks refuse every effect. G8's mechanism is met on all four engines, and the ≥99% figure is unmeasurable because nobody has run a Grasm program in production.
The gates, and what each one would catch
Judged by exit code alone, 113 of them. These are the figures from the last recorded run on this machine:
| Gate | Scope | Result |
|---|---|---|
| difftest / corpus | 511 programs | 0 differences across the engines |
leakcheck | 226 programs | 1 leaking — spec/conformance/E-VALUE-SEMANTICS.gr holds 16 allocations at exit, so this gate is red. The ratchet is at zero, which is why one leak fails it. See docs/known-issues.md. |
usecheck, the poison build | 197 programs | 0 differences |
crosstest x86_64 under Rosetta | 224 programs | 195 pass, 0 real failures, 60 declined — retaken on a quiet machine on 15 October 2026: all 511, nothing declined |
clippy --all-targets | native and wasm32 | 0 |
| unit tests | every crate | 609 pass |
replaytest | every journalled builtin | each one refuses under an empty journal |
| fuzzers | 15 generators | clean |
| conformance | 258 programs | one per normative clause |
leakcheck and usecheck are the two no other check
replaces: a leaking program produces exactly the right output, and a premature
free makes a value less live rather than more, so neither shows up in a
diff of what was printed.
One caveat on that table, stated rather than buried.
The crosstest figure was taken on a machine at load 38 rising to
134, where two parallel tests differed and then agreed on re-run, 3 of
3 and 2 of 2. They are recorded as load artefacts because that is what they
are, and the figure wants taking again on a quiet machine before anyone calls
it green.
The three that moved, and what moved them
G5: 351.1 KB → 85.7 KB
About 135 KB of that binary was Rust's own backtrace symbolizer — gimli,
addr2line, rustc_demangle — reached from std's default panic hook, and nothing
in a hello-world ever called it. All four crates a compiled program links are
now no_std, with a new grasm_sys crate underneath
them for the operating system and grasm_rt_abi supplying the
allocator and the panic handler. nm shows zero symbolizer symbols.
Nightly was not needed.
The conversion found nine defects, and the gates would have
caught none of them — they are all below the level the corpus exercises.
Five of the nine: open and fcntl were declared
non-variadic, and arm64 passes variadic arguments on the stack, so the mode
argument was garbage; RwLock::write released a mutex rather than
the lock it took, so a second writer hung; an exec failure was
indistinguishable from exit 127, which the specification requires to be
distinguishable; AF_INET6 was 30 on Linux, where it is 10; and
thread::scope let its reference escape, which needed somebody to
write the line that escapes before anything noticed.
A figure in that story was wrong in public first. It was
reported as 68.3 KB for several commits — the branch head was
85.7 KB. The
difference is one 16 KB page of __TEXT quantisation around 1.9 KB
of real code: on Mach-O a segment is 16 KB-aligned, so a kilobyte of code can
move a file by seventeen. docs/binary-size.md bisects it to the
commit.
G6: 40,000 tasks in 199 s → 1,000,000 in 22 s
The scheduler resumed every live task every round, so N tasks needed N rounds of N resumes — quadratic, and at that rate a million tasks is hours. The fix that looked obvious was built first and measured at nothing: a version per channel still wakes every blocked sender when the receiver takes one value. What worked is a wait queue per channel and per side, so a send wakes a receiver rather than another sender, and a round costs what is runnable.
That changed an observable rule, so the specification changed with it:
[E-SCHEDULE] now wakes the longest-waiting sender or receiver
instead of going round-robin. It is an experimental clause, and all
258 conformance programs produce identical output across the change.
The interpreter stops near 285,000 tasks and is allowed to: an
interpreter frame costs about 10 KB where a compiled one costs 30 bytes.
G2: the compiler did not change, the ruler did
This row used to read "4 of 7 programs at or above Rust", and that was an
artefact. bench.sh was adding a constant 21 ms to every cell, and a
constant added to every measurement compresses every ratio toward 1 — a 5 ms
program and a 10 ms program read as 26 and 31, which is "about the same" if you
squint. With the timer fixed, the geomean read 2.49× behind.
What closed it afterwards was real work, and the same 21 ms had been
published in two other tables: a wrong measurement does not stay where
it was made. It is 0.965× today, or 0.955× with the
harness's own floor subtracted from both sides, with five of eight programs at
or above parity. tree at 0.51× is what the geomean still carries:
a box per node in tree-shaped data. Storing those inline was built, measured at
1.33× slower, and not kept.
Every benchmark row, and the measurement rules each
mistake bought →
What is still open
| Open | What it would take |
|---|---|
| Tasks refuse every effect — G6's uncolored half | The task count is met; the goal is not. A task that cannot do I/O is not the thing the goal asks for. |
| Sub-50 ms at a million lines — G3's other half | A query engine. What exists is a batch compiler with an object cache, re-checking the whole program on every keystroke, and at this repository's size that is fast enough to hide the problem. |
| G11's last 4% and 2% | Priced before building: list repetition and a stepped range are what Python has and Grasm does not on the worst row. Not closable by adding library methods. |
| Linux has never been compiled here | About 2,100 lines of the operating-system layer are written for it and no target is installed on this machine. CI's ubuntu runner compiles it; a compile catches code that does not build, and does not catch a wrong constant — which is exactly how AF_INET6 survived. |
| No remote registry | Path and local-registry dependencies work. Nothing fetches or publishes. |
| G1 and G10 need users | A week to ship a service, and 500 companies. Neither is a number this repository can produce by working harder on it. |
How any of this is kept honest
All 101 findings are written down in
ProjectGold/FINDINGS.md — including the ones that were the test
harness's fault, the ones withdrawn as wrong, and the ones caught before they
shipped. Every gate is verified by breaking the thing it guards and confirming
it fails, because a gate nobody has broken is a gate nobody has tested. One of
them had to be: replaytest was matching 0 of 39
builtins and exiting 0, for weeks, because its pattern expected a struct on one
line. It now asserts its own count.
The bar for calling this finished is not on this page, and no number here moves it: zero known defects, every gate green, and two consecutive independent review rounds finding nothing new — each conducted by somebody who has seen only the language pack and not this repository's history. Those rounds have not run.