Changes
Every change to the specification since it was written, dated. Useful precisely because it shows the design moving and why.
Two kinds of change are recorded. The change logs are the spec author’s: each entry says what changed in the documents and what the codebase must do about it, and is marked (to apply) until the code has caught up. The changes forced by implementation are the compiler’s: each is a rule that turned out unworkable or underspecified when built, marked in the spec text with changed: and pinned by a fixture. Nothing is changed silently, in either direction.
Contents
- Change log 1 — effects, the loop, targets
- 2026-09-03 — Effect marker ! replaced by may (applied to docs)
- 2026-09-03 — Regeneration loop specified as a separate document (applied to docs)
- 2026-09-03 — Targets: dual backends as a design goal (applied; M11 and M12 done 2026-09-05, docs/CHANGES.md items 93–104)
- 2026-09-03 — Documents added, no spec impact
- Not changed, but decided
- Change log 2 — testing
- Change log 3 — trust zones
- 2026-09-05 — Trust zones (applied; 2026-09-07, docs/CHANGES.md items 201–203: the hardened modifier, then the manifest, the zones pass and the reports, then the zone commands, the promotions ledger, the loop and the review page; onus zone promote runs the audit's static half, and its regeneration half through the loop when given a model, item 204)
- Not changed, but decided
- Change log 4 — views and the DOM
- Changes forced by implementation
- M1 — front end
- M2 — resolve and types
- M3 — effects
- M4 — const evaluator
- M5 — codegen, everything checked
- M6 — verification
- M7 — reports
- M8 — claims, capabilities, paths
- M9 — onus next
- M10 — the review tool
- Testing model (docs/CHANGE-LOG-02.md, applied 2026-09-04)
- M11 — native backend
- M12 — targets complete
- M13 — contract mutation and coverage
- M14 — regeneration loop
- M15.0 — prerequisites for the compiler in Onus
- M15.1 — the front end in Onus, first part: the lexer
- M15.1 — the front end in Onus, second part: the parser
- M15.1 — the front end in Onus, third part: structural recursion and the printer
- M15.2 — the checker in Onus, first part: loading, resolution and the type layer
- M15.2 — the checker in Onus, second part: types, constants and effects
CHANGES.md — Onus specification changes
Changes to onus-spec-v0.md and onus-impl-spec-v0.md made since the implementation started. Apply in order. Each entry states what changed, why, and what to do in the codebase. Entries marked (applied to docs) are already in the spec files; entries marked (to apply) need the spec text added as well as the code.
2026-09-03 — Effect marker ! replaced by may (applied to docs)
Change. The effect list after a return type is introduced by the keyword may instead of !. may is a reserved word.
fn main(args: List[Text], files: io.Files) -> Result[Unit, io.Error] may io.file, alloc
fn map[T, U, e](xs: List[T], f: fn(T) -> U may e) -> List[U] may e, allocGrammar (§2.3): every [ "!" effects ] is now [ "may" effects ] — in fn_decl, type (function types), iface_item, and the closure form of primary. Stream[T] may e likewise.
Why. ! reads as “not” in every language a model knows; may reads correctly for every effect (may allocate, may panic, may write) and states the declaration as the claim it is.
Codebase. Lexer: add may to keywords; ! is no longer a token outside !=. Parser: four productions. Printer: emit may. All fixtures and the three examples updated. No semantic change.
2026-09-03 — Regeneration loop specified as a separate document (applied to docs)
Change. docs/onus-loop-v0.md added. It is a candidate spec for the component that drives the model against the compiler. It does not change the language.
Codebase. Nothing now. It depends on the compiler’s JSON outputs (§9.1, §11.1, §13) being stable and on onus next (M9), which are already in the plan. Do not start on it until M10 is done.
2026-09-03 — Targets: dual backends as a design goal (applied; M11 and M12 done 2026-09-05, docs/CHANGES.md items 93–104)
Change. Onus programs compile to JavaScript and to native code from the same source, and later to WebAssembly. This is a stated goal, not an accident of architecture, and it adds a section to the language spec and two milestones to the implementation spec.
Language spec: new §19 “Targets” (insert after §18 Worked examples)
19. Targets
An Onus program compiles unchanged to every supported target. Observable behaviour is defined by this specification, never by the host. Where this specification is silent on something a program can observe, that is a defect in the specification.
19.1 Runtime primitive surface
Each target provides a runtime implementing exactly the following primitives. Everything else in
std.*is written in Onus and compiled by the same backend as user code.
- Memory: allocate, free-at-scope-exit (native) or no-op (collected hosts).
Int: 64-bit signed arithmetic with overflow detection; see 19.3.Float: IEEE 754 binary64 arithmetic;classify; formatting to text per the algorithm instd.float(shortest round-trip representation).Text: UTF-8 storage; grapheme-cluster segmentation per Unicode 16.0 (pinned; the runtime carries the tables); byte and grapheme views; equality by code point sequence.Bytes: contiguous byte sequence with bounds-checked access.- Panic: raise with an obligation id and optional model;
recoverboundary.- Capabilities: opaque handles for
io.Files,io.Env,io.Net,io.Clock,io.Rand,sql.Db, plus the__fakeconstructor available only to test modules.io.*andsql.*raw calls, each mapped one-to-one from anassumeleaf instd.io/std.sql.The primitive surface is versioned with the specification. A backend that lacks a primitive reports
E0800 primitive unavailable on targetat build time for any program reaching it.19.2 Host claims
Code that can only run on one host declares it with a claim:
host.js,host.native,host.wasm. These are asserted claims (§7.1) introduced only atassumeleaves that call host-specific facilities, and they propagate like any claim. Apathmayforbid { host.js, host.native, host.wasm }to require portability, and the compiler then rejects anything reachable that depends on a host.19.3 Integer representation
Intis 64-bit signed on every target. On targets without native 64-bit integers (JavaScript), the backend chooses a representation per value: a double-precision number where the verifier has proved|x| <= 2^53 - 1for every value the binding can hold, and an arbitrary-precision integer otherwise. The choice appears in the ledger as an obligation of kindrepresentation, so a reviewer can see which values are running on the slow path and tighten refinements to move them.19.4 Fully specified behaviour
The following are specified so that all targets agree:
Mapiteration is in key order underOrd[K]; integer division truncates toward zero and%takes the sign of the dividend;FloattoTextis the shortest round-trip form;exampleblocks run in source order;Streamelements are produced on demand and never buffered beyond one element by the runtime.19.5 Differential testing
Every
exampleandpropertyruns on every built target. Any disagreement between targets on a program the compiler accepted is a backend defect, reported asE0801 target disagreementwith the example, the two results and the targets.
Language spec: §17 Open questions
Add: Concurrency, when designed, must fit both a single-threaded event-loop host and a native multi-threaded one; structured concurrency over immutable inputs with channels by value is the working assumption.
Implementation spec: decisions table
Add a row:
| Native target | LLVM IR text emitted by the compiler; clang assembles and links against a small C runtime | Own lowering (semantics stay ours), borrowed instruction selection and optimisation; nothing to install beyond Xcode CLT / clang on Linux |
Add to §6 Codegen: the codegen pass has two emitters behind one interface, emit(ctx, target). The lowering from checked AST plus obligation statuses to a target-neutral form is shared; only the final emission differs. Do not duplicate lowering logic per target.
Implementation spec: milestones
M11 — Native backend. LLVM IR emitter; C runtime for the 19.1 primitive surface (no sql yet); onus build --target native produces an executable via clang. proved obligations emit no code; checked obligations emit compare-and-branch to onus_panic with the obligation id; recover via setjmp/longjmp. Int is i64 with llvm.*.with.overflow intrinsics. Accept: Mandelbrot builds natively and writes an identical PGM to the JS build; every example passes on both targets; E0801 fires on a deliberately broken runtime primitive.
M12 — Targets complete. sql primitives in the C runtime over libpq; host claims; Int representation obligations in the JS backend; differential test harness running all fixtures on both targets; WebAssembly emission via the same LLVM path (--target wasm), with io.* mapped to WASI. Accept: all three examples build and agree on both native and JS; the reporting example runs natively against Postgres; a path with forbid { host.js } rejects a JS-only assume leaf.
Codebase now. Nothing changes before M10. When starting M11: the shared lowering in codegen/ is the design constraint — if the JS emitter has lowering logic tangled into emission, separate it first, and add a fixture set for the target-neutral form.
2026-09-03 — Documents added, no spec impact
docs/onus-pitch.md— a short pitch. Its example usesE0201on amay sql.read, allocsignature; if the compiler’sE0201text ends up different from the illustrative one, the pitch follows the compiler, not the reverse.
Not changed, but decided
- The syntax borrows F#’s data model only (unions with
of,match ... with,when,{ x with ... }) and is deliberately not F# elsewhere. Already in §2 “Borrowing policy”. Do not import F# conventions the spec does not name. - Product output is JavaScript in one step;
--emit tsis a fixture-suite oracle only. Already in the implementation spec and CLAUDE.md. - The compiler is the only checker. No warnings, no lint, ever.
CHANGE-LOG-02.md — Onus specification changes: testing
Follows CHANGES.md. Apply after it. Adds the testing model to onus-spec-v0.md and onus-impl-spec-v0.md. Nothing here changes the type system or the verifier; it adds two syntax forms, two ledger fields, two onus test modes, and one reported metric.
2026-09-03 — Testing model (applied; M13 done 2026-09-05, docs/CHANGES.md items 105–109)
Principle. Behaviour is established by contracts (proved) or by examples and properties (checked); dependencies are supplied as capabilities and faked in test modules; only assume leaves need testing against reality. There is no assertion library, no mocking library, and no separate test runner: the assertion language is the contract language, the mock mechanism is fake, and the runner is the compiler.
Language spec: new §20 “Testing” (insert after §19 Targets)
20. Testing
20.1 What is tested and where
Concern Mechanism Lives in Behaviour, all inputs requires/ensures/invariant, provedthe function’s interface Behaviour, specific inputs example(§5.2)the function’s interface Behaviour, generated inputs property(§5.2)the function’s interface Dependencies capabilities passed as parameters; fake(§8.4)test moduleScenarios across modules exampleblocks in atest module, with fakes at the edgestest moduleContact with reality verifyblocks onassumeleaves (20.2)next to the assumption Regressions exampleblocks pinned from counterexamples (loop spec §7)the function’s interface Strength of the contracts regeneration audits (loop spec §8) and contract mutation (20.4) onus testThere is no test tree parallel to the source. An example is attached to what it exemplifies.
Functions with the
nondeteffect take their source of nondeterminism (io.Clock,io.Rand) as a capability, so a test supplies a fixed one. A test that could be flaky is not expressible.20.2 Assumption verification
An
assumemay carry averifyblock: an Onus function body that exercises the assumption against the real resource and yieldsBool.assume Idempotent "Vendor API deduplicates on req.key for 24h; see contract §4.2" verify(client: payments.Client) may io.net, alloc { let a: Receipt = try payments.charge(client: client, key: "verify-1", amount: 100) else _: false let b: Receipt = try payments.charge(client: client, key: "verify-1", amount: 100) else _: false a.id == b.id }
- The block’s parameters are capabilities, supplied by the environment running
onus test --assumptions, never constructed by the block.- The block declares its effects like any function and may not exceed the effects of the function containing the
assume.verifyblocks are never run byonus check; they run only underonus test --assumptions, which is expected to be pointed at a staging or test environment.- An
assumewithout averifyblock is permitted and is reported as unverifiable in the ledger.20.3 Ledger fields
Each
assumeentry in the ledger (§9.1, §11.1) gains:
verifiable: bool— whether averifyblock exists.last_verified: { at: timestamp, target: string, result: "passed" | "failed" } | null— recorded byonus test --assumptions, persisted in.onus/ledger/.The review tool shows assumptions as assumed, verified
against or assumed, unverified. Apathmay requirepolicy verified_assumptions_only, which fails the build if any reachableassumelacks a passing verification within a repository-configured age.20.4 Contract mutation
onus test --mutateweakens contracts one at a time and reports which weakenings no example or property detects. Mutations applied, per obligation: drop anensuresclause; replace a refinement bound with its base type; negate a guard in aproperty; drop alaw. A mutation that survives — every example and property still passes — is reported asM0001 undetected contract weakeningwith the mutation and the function. It is not an error; it is the signal that the examples are not carrying the contract’s meaning.Mutation never touches bodies. Bodies are the model’s; weakening them is what the loop already does implicitly by regenerating.
20.5 Obligation coverage
The reported test metric is obligation coverage, per module and per path:
- obligations proved;
- obligations checked, and of those, how many are exercised by at least one
exampleorproperty;- assumptions, and of those, how many are verifiable and how many have a current passing verification;
- contract mutations detected versus surviving.
Line coverage is not reported and cannot be enabled.
20.6 The runner
onus testevaluatesexampleandpropertyblocks (already done byonus check), runstest modules, and on multi-target builds runs everything on each target, reporting disagreement asE0801(§19.5).onus test --assumptionsrunsverifyblocks against supplied capabilities.onus test --mutateruns contract mutation. There is no plugin mechanism and no configuration file beyond the repository’s target and environment settings.
Grammar (§2.3)
stmt = ...
| "assume" TNAME STRING [ NL verify_block ]
verify_block = "verify" "(" [ params ] ")" [ "may" effects ] block ;verify is a reserved word.
Implementation spec
§4 Passes. Pass 9 (claims) records verify blocks on assume sites; they are type- and effect-checked like functions in pass 4/6 but excluded from codegen except under --assumptions.
§5 Runtime. .onus/ledger/ gains assumptions.json, keyed by module and assume location hash, holding last_verified.
§7 Reports. interface.json and path.json gain the two ledger fields per assumption and an obligation_coverage block per module/path.
Milestones. Add to M8 (claims, capabilities, paths): verify blocks parsed, checked, and stored; onus test --assumptions runs them against capabilities constructed from a repository config; ledger fields populated; policy verified_assumptions_only. Accept: the checkout example’s Idempotent assumption has a verify block that passes against a fake payments service and the path report shows it as verified.
Add to M10 (review tool): assumption freshness shown in the path and ledger views.
Add M13 — Contract mutation and coverage. onus test --mutate with the four mutation kinds; M0001 reporting; obligation coverage in interface.json, path.json and the review tool. Accept: dropping the ensures on recent_orders is detected by its property; dropping a deliberately unexercised refinement in a fixture survives and is reported.
Codebase now
Nothing until M8. The fake mechanism and test module already planned for M8 are the foundation; verify blocks reuse the same capability-construction path.
Not changed, but decided
- There is no assertion library. Contracts are the assertion language.
- There is no mocking library. Capabilities and
fakeare the whole mechanism. - There is no separate test runner or plugin system.
onus testis the compiler. - Line coverage is not a concept in Onus.
CHANGE-LOG-03.md — Onus specification changes: trust zones
Follows CHANGE-LOG-02.md. Apply after it. Adds zones — per-module levels of strictness — to onus-spec-v0.md, onus-impl-spec-v0.md and onus-loop-v0.md. The type system and verifier are unchanged; zones add a manifest, a dependency rule, per-zone policy bundles, a promotion command, and zone-aware loop behaviour.
2026-09-05 — Trust zones (applied; 2026-09-07, docs/CHANGES.md items 201–203: the hardened modifier, then the manifest, the zones pass and the reports, then the zone commands, the promotions ledger, the loop and the review page; onus zone promote runs the audit’s static half, and its regeneration half through the loop when given a model, item 204)
Principle. Trust in Onus is per artefact: interfaces are the human’s, bodies are the model’s, and the ledger records what each obligation rests on. A project is not uniformly trusted at any moment in its life — a hardened core coexists with a subsystem being prototyped against it — so strictness is declared per module, and the only rule that matters is that nothing at a higher level of trust ever rests on a claim from a lower one.
Language spec: new §21 “Zones” (insert after §20 Testing)
21. Zones
Every module belongs to exactly one zone. A zone is a level of strictness. There are three:
Zone Meaning draftBeing designed. The ledger is recorded but not authoritative. Interfaces may change freely, by human or model. Bodies may be human-edited. hardenedIn service. Interfaces are the human’s, bodies are the model’s (loop spec §1). The ledger is authoritative. criticalIn service and load-bearing. hardened, plus every public entry is on apath; no unverified assumptions; norecover; nocheckedobligation without an exercisingexampleorproperty.Zones are declared in the project manifest (21.4), not in modules, because a zone change is a decision about the project and its diff is what a reviewer approves.
21.1 The dependency rule
A module may depend on another module’s interface only if that interface is at the same zone or higher.
draftmay depend on anything.hardenedmay depend onhardenedandcritical.criticalmay depend only oncritical.One exception makes integration possible: a
draftmodule may mark individual public itemshardened. A hardened or critical module may depend on those items — and only those — from the draft module. A hardened item in a draft module is checked to the hardened standard: its contracts may not be changed by the loop, its obligations appear in the authoritative ledger, and itsassumeleaves are subject to the depending zone’s policies. Its body remains draft.-- in a draft module pub hardened fn charge(client: Client, req: ChargeRequest) -> Result[Receipt, Error] may io.net, alloc ensures ...This is the mechanism for building a new subsystem against a stable core: harden the boundary first, and the core is permitted to see only the boundary.
Violations are
E0900 dependency crosses zone boundary, naming both modules, the zones, and the item.21.2 Zone policies
Each zone applies a fixed bundle of the policies that already exist:
draft: none.assumeunrestricted;recoverunrestricted;checkedunrestricted.hardened:no_loop_authored_claims(the loop may not edit interfaces; loop spec §1 and §5); assumptions must have a justification string.critical:hardenedplusverified_assumptions_only(§20.3),forbid { recover }on every path,checked_requires_example, andno_third_party_assumesunless individually excepted in the manifest.A module may add policies beyond its zone’s bundle. It may not remove any.
21.3 Promotion and demotion
onus zone promote <module> <zone>runs the regeneration audit (loop spec §8) on the module at the target zone’s standard: bodies are regenerated from interfaces alone, and every finding becomes a proposal. Promotion succeeds only when the audit reports no findings and the zone’s policies pass; the manifest change is opened for review like any other change. The audit result is stored in the ledger as the promotion record.
onus zone demote <module> <zone>is always permitted and always recorded. Demoting a module that others depend on does not break the build; it marks every dependent’s guarantees that rest on the demoted module as conditional in the ledger and the path reports, until the module is promoted again.Zones only ever change through these commands. Editing the manifest directly is
E0901 manifest edited outside zone command.21.4 Manifest
onus.tomlat the repository root:[zones] "app.core.*" = "critical" "app.reporting" = "hardened" "app.payments.*" = "draft" default = "draft" [zones.exceptions] "app.core.checkout" = { third_party_assumes = ["vendor.payments.charge"] }Patterns match module names; the most specific match wins.
defaultapplies to modules not matched. A new module isdraftunless the manifest says otherwise.21.5 Reporting
The interface document, path report and ledger carry the zone of every item. A path report additionally lists the zones it crosses and every hardened-item-in-draft-module it depends on. The review tool renders zones as regions, with draft regions visibly distinct, and the promotion history of each module.
Grammar (§2.3)
visibility = [ "pub" ] [ "hardened" ] [ "sealed" ] ;hardened as a visibility modifier is permitted only on pub items in draft modules; elsewhere it is E0902 hardened modifier outside draft zone. hardened is a reserved word.
Implementation spec
§2 Layout. Add zones/ under compiler/src/: manifest parsing, zone resolution, dependency rule, policy bundles.
§4 Passes. Add pass 12a, after paths: zones — resolve every module’s zone from the manifest, check the dependency rule (E0900), apply zone policy bundles (feeding the same checks paths and policies already run), record zone per item for reports.
§7 Reports. interface.json items gain zone; path.json gains zones_crossed and draft_dependencies; ledger gains promotions (module, from, to, audit result, timestamp) and conditional flags on obligations resting on demoted modules.
CLI. onus zone promote, onus zone demote, onus zone show. Promote depends on the loop for the regeneration audit; before the loop exists it runs the audit’s static half only (policies) and records that the body-regeneration half was skipped.
Milestones. Add M16 — Zones (M14 is the loop and M15 the compiler in Onus in the implementation spec; edited 2026-09-07). Manifest, dependency rule, policy bundles, hardened modifier, zone fields in reports, onus zone commands (static half). Accept: the checkout example split into app.core.* critical, app.reporting hardened and a new app.payments draft module with one hardened item that app.core.checkout depends on; a dependency on a non-hardened draft item fails E0900; demoting app.reporting marks the reporting path conditional.
Add to M10 (review tool): zones as regions; promotion history.
Loop spec
§3 What the model sees. Context policy defaults by zone: draft → scope, and the model may also see the conversation history for the module (design mode); hardened → module; critical → none.
§1 and §5. The rule “the loop never edits claims” applies in hardened and critical. In draft the loop may edit interfaces directly and proposals are unnecessary; the ledger records the edits as loop-authored so the promotion audit can find them.
§4.1 Escalation. In critical, escalation goes to the frontier model on the first stall, and widen_effects proposals are never emitted — an effect widening on a critical module is a human decision from the start.
§8 Regeneration audits. The audit is the promotion mechanism; its standard is the target zone’s policy bundle.
Codebase now
The manifest format and the dependency rule can be implemented as soon as M8 is done, since they reuse policy checks. The hardened modifier is a one-token grammar change; do it with the next grammar touch rather than separately.
Not changed, but decided
- Zones are per module, declared in the manifest, changed only by command. There is no per-function zone; the
hardenedmodifier is the sole finer grain, and it exists only to expose a boundary from a draft module. - Nothing at a higher zone rests on a claim from a lower one. This is the invariant everything else serves.
- Promotion is earned by the regeneration audit; there is no manual override.
CHANGE-LOG-04.md — Onus specification changes: views and the DOM capability
Follows CHANGE-LOG-03.md. Apply after it. Adds what the workbench needs to be written in Onus: a dom capability, a View tree type, and a diff-and-patch runtime primitive. No framework. The type system and verifier are unchanged; this is one capability, one stdlib module, one runtime primitive, and a host claim.
2026-09-05 — Views and the DOM (applied; 2026-09-08, docs/CHANGES.md items 208 (the capability, the primitive, std.view and the counter), 209 (the vocabulary for documents and std.html) and 210 (the workbench in Onus))
Principle. UI frameworks exist for human authors: syntax that reads like markup, components to reason about one piece at a time, lifecycle hooks, an ecosystem. A model needs none of that. What remains is the one non-ergonomic problem — keeping the DOM in sync with state without rebuilding it — and that is a small runtime primitive, not a dependency. A view in Onus is a pure function from state to a tree value; the tree is verifiable like any other value; only the patch step touches the DOM.
Language spec: new §22 “Views” (insert after §21 Zones)
22. Views
22.1 The
domcapability
dom.Rootis a capability granting access to one DOM subtree. It is constructed by the runtime formainon thejshost only (host claimhost.js, §19.2) and attenuated like any capability:dom.subtree(root: Root, id: Text) -> Rootnarrows to a child.Effects:
dom.read(query layout, focus, scroll position),dom.write(patch the tree),dom.event(receive events). All three require adom.Rootin scope.22.2 The
Viewtype
std.viewdefines an immutable tree:union View[Msg] = | Element of tag: Tag, attrs: List[Attr], children: List[View[Msg]], key: Option[Text] | Text of value: Text | Keyed of key: Text, child: View[Msg] union Attr[Msg] = | Attribute of name: AttrName, value: Text | On of event: EventKind, msg: Msg | Property of name: PropName, value: Text
Tag,AttrName,PropNameandEventKindare closed unions of the HTML vocabulary; there is no string-tagged element. A view function is any pure function returningView[Msg]. Handlers are messages, not closures: anOncarries a value of the application’sMsgtype, so a view cannot capture capabilities or perform effects, and the set of things a UI can do is the closedMsgunion.22.3 The program shape
pub fn view(state: State) -> View[Msg] may alloc pub fn update(state: State, msg: Msg, api: ModelApi, clock: io.Clock) -> Result[State, AppError] may io.net, io.clock, alloc
std.view.run(root: dom.Root, init: State, view: fn(State) -> View[Msg] may alloc, update: fn(State, Msg) -> Result[State, E] may e) may dom.read, dom.write, dom.event, e, alloc, divergedrives the loop: render, diff against the previous tree, patch, wait for an event, dispatch itsMsgtoupdate, repeat.runis the only function instd.viewwithdom.write; apathover a workbench entry mayforbid { dom.write }on everything exceptstd.view.runviaexcept.Capabilities the UI needs (
ModelApi,io.Net, the ledger store) are parameters ofupdate, never ofview. A view cannot reach anything; it can only describe.22.4 Verifiable views
Because a
Viewis a value, properties over it are ordinary contracts:property every_button_has_handler(s: State) { forall v: View[Msg] in View.descendants(of: view(state: s)) where v is Element(tag: Button, ..): exists a: Attr[Msg] in v.attrs: a is On(event: Click, ..) } property labelled_inputs(s: State) { forall v: View[Msg] in View.descendants(of: view(state: s)) where v is Element(tag: Input, ..): View.has_label(tree: view(state: s), for: v) }
std.viewships a small set of such properties (buttons have handlers, inputs have labels, images have alt text, no nested interactive elements) that an application opts into per view function withclaims view.accessible. Accessibility becomes a claim, propagated and reported like any other.22.5 Runtime primitive
One addition to the primitive surface (§19.1):
dom.patch(root, previous: View, next: View)— diff two trees and apply the minimal set of DOM mutations. Keyed children reorder by key; unkeyed children reconcile positionally. Events are delivered as(EventKind, path-to-node)pairs and mapped back toMsgby the runtime from the current tree. The primitive is perhaps two hundred lines in the JS runtime; on native and WASM targets it is unavailable (E0800) unless a host provides a DOM.22.6 What is absent
No component abstraction beyond functions; no local component state (all state is in
State); no lifecycle hooks; no context or dependency injection (capabilities are parameters); no string-tagged elements; no inline styles as strings (aStyleunion instd.view, closed, with aCustomvariant that carrieshost.js); no templating syntax.
Grammar (§2.3)
No change. Views are ordinary values and functions. is in the property examples is the existing pattern test on unions; if the parser does not yet support expr is pattern in expression position, add:
cmp_expr = add_expr [ CMP_OP add_expr | "is" pattern ] ;Implementation spec
§5 Runtime. Add dom.ts to the JS runtime: Root capability, patch, event delivery, subtree. Native and WASM runtimes return E0800 for any dom.* primitive.
Stdlib. Add std/view.onus: View, Attr, Tag, AttrName, PropName, EventKind, Style, run, descendants, has_label, and the accessibility properties. Written in Onus, verified like everything else. This is the first stdlib module written against the self-hosted compiler and should be treated as a fixture for it.
Milestones. Add M17 — Views (M15 is the compiler in Onus and M16 zones in the implementation spec; edited 2026-09-08). dom capability, dom.patch, std.view, the accessibility claim set. Accept: a counter application (state, view, update) runs in a browser from the JS target; every_button_has_handler is proved for it; forbid { dom.write } on a path rejects a direct dom.write outside std.view.run; the same source built for native fails with E0800 naming the primitive.
M10 (review tool) is redefined: the workbench’s logic — path layout, interface diff rendering, ledger queries, the task queue — is written in Onus against std.view, in a draft zone, and promoted when its audit is clean. The hand-written TypeScript shell is reduced to bootstrapping main and nothing else. Reorder: M15 before M10’s rewrite.
Loop spec
No change. The workbench’s update is where the loop’s task-creating moves live; they were already structured. Design mode (draft zone) is the state in which the workbench itself is being built.
Codebase now
Nothing until the self-hosted compiler is stable enough to compile std.view. When it is, dom.patch in the JS runtime is the only piece of TypeScript to write; everything else is Onus.
Not changed, but decided
- No UI framework, ever.
std.viewis a tree type and a patch primitive. - Views are pure and handlers are messages. A view cannot perform effects or hold capabilities.
- Accessibility is a claim, not a lint. It propagates and it appears in the ledger.
- The workbench is written in Onus. The only TypeScript in it is
dom.patchand the bootstrap.
Spec changes
Changes to onus-spec-v0.md forced by implementation, by milestone. Each is
marked in the spec with <!-- changed: reason --> and pinned by a fixture.
Changes the spec author makes are logged in CHANGE-LOG.md and its sequels, dated, with
what the codebase must do about each; this file records only what
implementation forced.
M1 — front end
Grammar (§2.3)
The provisional EBNF was made LL(1) and brought into line with the spec’s
own examples. The grammar as implemented is grammar-v0.md. Differences:
- Continuation newlines. A newline is not significant before
->,else,{,claims,requires,ensures,invariantordecreases. The EBNF placedNLtokens insidefn_decl,iface_itemandloopin ways that did not tokenise consistently (e.g. a signature followed by contracts needed two consecutive newlines). Fixtures:roundtrip/messy/continuations. - Single-line blocks.
blockaccepts{ stmt }on one line (the §18.3 example writesif ... { return Err(Empty) }); the canonical form is always multi-line. Fixture:roundtrip/messy/single_line_block. inoutposition.param = NAME ":" ["inout"] type, matching §4.1’sgrid: inout Grid[T, w, h]and the call-site formgrid: inout grid, which is now also in the grammar (call_args). The EBNF hadinoutbefore the name. Fixtures:roundtrip/14_fn_signatures,roundtrip/23_expr_postfix.- Labelled and explicit type arguments.
targ = [NAME ":"] (type | expr)forDb[ReadOnly, schema: "orders"](§8.2), and a call may carry explicit[...]arguments forsql.select[text: "..."](...)(§18.2). Fixtures:roundtrip/23_expr_postfix,roundtrip/34_types. - Ranges.
a ..< bis a domain form offorand of quantifierinclauses (§5.1, §5.3), not an expression. Fixtures:roundtrip/19_for,27_quantifiers. impliesandis.impliesis the lowest-precedence, non-associative operator (§3.6 laws);x is Patternsits at comparison level (§3.8.1). ChainingimpliesisE0011. Fixtures:roundtrip/22_expr_logic,syntax/e0011.- Claim predicates. A derived claim’s body is the small effect-predicate
language of §6.3 (
effects == { ... }, effects, claims,and/or/not), not a general expression. Fixture:roundtrip/09_claims. - Policy scopes.
outside { self, std.* }isscope = "self" | QNAME [".*"]. Fixture:roundtrip/12_policy. - Test modules and
fake.["test"] "module"and thefake QTNAME { ... }primary of §8.4 are in the grammar;fakeoutside a test module isE0012. Fixtures:roundtrip/31_test_module_fake,syntax/e0012. recoveras an effect name in effect sets, forforbid { recover }(§10.2). Fixture:roundtrip/11_path.- Effect lists inside parameter lists.
fn(T) -> U ! e, xs: List[T]is ambiguous; a comma followed byNAME ":"ends the effect list, and effect names are lowercase (QNAME), never claims. Fixture:roundtrip/34_types. - Quantifier binder types have no
whereclause of their own;whereafter the binder belongs to the quantifier (§5.3). - Mixed
and/or(E0007) and chained comparisons (E0006) are parse errors as §2.1 requires. Fixtures:syntax/e0006,syntax/e0007. E0002(bare expression statement is not a call) is checked by the parser;example,propertyandlawblocks are exempt because their bare expressions are assertions (§5.2). Fixture:syntax/e0002.- Soft keywords. The spec’s examples use
of,requireandpathas names (Float.of,auth.require,path: "..."). Item and clause keywords that cannot occur inside an expression are therefore reserved only where an item or clause can begin. Listed ingrammar-v0.md. - Parentheses are not AST nodes; the printer emits the minimal set.
and/orare n-ary nodes. Fixture:roundtrip/messy/parens.
Lexical (§2, §3.1)
- Text literals are single-line. A raw newline in a text literal is
E0004; use\n. The §18.2 SQL literal is rewritten on one line. This keeps one canonical spelling per string value. Fixture:syntax/e0004. - Comments are preserved by the canonical printer (attached to the
line-level construct they precede or follow) and excluded from hashes.
Fixture:
roundtrip/29_comments. - Literal normalisation in canonical form:
_separators dropped, durations in the largest exact unit, floats in shortest form. Fixture:roundtrip/messy/literals.
Canonical form (§2.2)
- The layout rules are stated precisely in
grammar-v0.md(“Canonical form”). Notably: bracketed lists break one element per line at 100 columns,else ifcanonicalises to a nested block (as §2.3 already said), and blank lines inside blocks are removed.
Named arguments (§5, §10.1, §18)
Ok(x)/Err(e)in the prose and examples contradicted “arguments are passed by name at every call” and thecall_argsgrammar. The examples now writeOk(value: x)andErr(error: e);Result’s fields arevalueanderror. Fixtures: the three worked examples.
Claims are type names (§6.3, §9, §18.3)
- §2 and the grammar make claims
TNAMEs; the §6.3 examples used lowercase (pure,total). The examples now readPure,Total,RealtimeSafe, andrequire { Total, Idempotent }.
Capabilities (§8)
capability Db[mode: DbMode]is writtencapability Db[const mode: DbMode]per thetparamsgrammar, andmode in { ReadOnly, ReadWrite }is writtenmode == ReadOnly or mode == ReadWrite(there is no set-membership expression in v0). Fixture:roundtrip/10_capability.
Diagnostics (§13)
location.defisnullfor a diagnostic outside any definition (e.g. a malformed module header).
M2 — resolve and types
- Intrinsics (§3.12, new).
intrinsic fn(no body) andintrinsic typedeclare runtime-provided primitives, legal only undermodule std.…(E0102elsewhere). Their contracts and effects are assumed obligations in the ledger (§12.2 extended). Chosen over a hardcoded primitive table (contracts would live outside Onus) and over a generalextern(the FFI §17 defers). Fixtures:roundtrip/35_intrinsic,syntax/e0102,syntax/e0003_intrinsic_with_body. - Function types carry parameter names (§3.7).
fn(x: T) -> Urather thanfn(T) -> U: calls are named, so a call through a function value needs labels. A closure assigned to a function type may use its own parameter names. Fixtures:roundtrip/03_type_alias,28_closures,34_types.
Modules and resolution (§11, §3.4, §3.6, §3.10)
- Module files.
a.b.clives at<root>/a/b/c.onus;std.*lives under the standard library root and no other file may declare astd.*name (E0112). A file declaring a name other than its path isE0104; an import that finds no file isE0103. The root is--rootor is inferred from the entry file and its module name. Fixtures:checker/e0103,e0104,e0112. - Prelude. Every module implicitly sees the public types and variants
(not the functions) of
std.results,std.option,std.list,std.grid,std.map,std.int,std.float,std.text,std.bool,std.bytesandstd.duration. These implicit imports are type-only and are not edges for cycle detection. TheResultmodule isstd.resultsbecauseresultis a keyword. - Companion functions.
T.fdenotes functionfof the module that declaresT; for a primitive, that module isstd.<lowercase name>(Int.to_text→std.int.to_text). Functions are never in scope unqualified across modules. - Variant scope. A bare variant resolves in this module’s unions, then
the prelude’s, then the imports’ public unions; more than one candidate is
E0108and must be qualified with the module alias. Two unions in one module may not share a variant name (E0107), since there is noUnion.Variantsyntax. - Module aliases win in dotted names.
auth.require(...)denotes the module even when a parameterauthis in scope (§18.3 relies on this); a bareauthis the parameter. Aliases live in their own namespace. - No shadowing. A local may not reuse the name of another local or
parameter (
E0113). Module-level functions and constants may be shadowed, because parameters are the labels callers read and the spec’s own API pairsselect(..., statement:)withsql.statement. Fixture:checker/e0113. - Examples and properties share a namespace separate from functions, so
example escape_countmay accompanyfn escape_count(§18.1); paths and policies likewise. Unitis a built-in value;TypeInfoandSpecare nameable types.- Interface dispatch is written
Ord.compare(a: x, b: y): the interface’s type parameter is instantiated from the arguments (or an explicit argument) and animplmust exist (E0333) unless the type is a parameter bounded by that interface. Inside an interface or impl its functions are in scope bare. Fixture:checker/e0333.
Typing (§3, §4, §5, §10)
- Generic instantiation takes type arguments from an explicit
[...], then from the expected type in checking position, then from the arguments; an unbound parameter isE0324. This is instantiation, not inference onto declarations. Fixture:checker/ok_types,e0324. - Type indices (
Grid[T, width, height]) must be literals,consts or parameters; at a call whose result type uses a parameter as an index, the argument must be such an expression (E0337). Fixture:checker/e0337. - Capability restrictions. Labelled arguments beyond a capability’s
declared parameters (
schema: "orders") are restrictions; a capability with more restrictions is accepted where one with fewer is required (§8.2). This is the one subtyping rule beyond refinement subsumption in impl spec §3.3. - Expression statements must have type
Unit(E0339): a discardedResultis never silent.example,propertyandlawbodies are assertions and must beBool. - Unreachable code is an error, not a warning: a statement after a
returnon every path (E0332) and amatcharm no value can reach (E0327). Fixtures:checker/e0332,e0326_e0327_match. recoverblocks yield the value of their final expression statement and may notreturn;Panickedis a record instd.results.- Closures may not capture capabilities (
E0330), in addition tovars andinoutparameters (§3.7). Fixture:checker/e0330_capture_capability.
M3 — effects
Function-level
decreases(§5.1). Recursion needs a measure but the grammar only haddecreasesas a loop clause; it is now also a contract clause of a function (decreases nafterrequires/ensures). A recursive cycle whose functions lack one and do not declaredivergeisE0320. Fixtures:roundtrip/14_fn_signatures,checker/e0320.Resource effects are declared by
grants(§6.1, §8).sql.readis the effectreadof modulesql, declared by a capability in that module grantingsql.read; it is spelled the same everywhere and is reachable only where that module is imported. Any other effect name isE0202. The primitive set stays closed.mutateis about the caller’s own parameters (§6.1). A function needsmutateiff it assigns to or passes on one of itsinoutparameters; a callee’smutatedoes not propagate through a localvar(Mandelbrot’srendercallsGrid.setwithmay alloconly).What allocates (§6.1). List literals,
++and closure creation arealloc; records and variants are values and are not. Aloop whilewithoutdecreasesisdiverge.recoverabsorbspanic.Effect polymorphism (§6.2). Passing a function value to a parameter of type
fn(...) -> U ! ebindseto the value’s effects beyond those the parameter lists; the call contributes the callee’s effects withesubstituted. A function value may not flow into a function-typed position (binding, argument, return) declaring fewer effects (E0201). Fixtures:checker/ok_effects,e0201_fn_value_flow.Purity of contracts. A
const fndeclares no effects;requires,ensures,decreasesandwhereclauses may allocate and nothing else.Impl effects (§3.6). An impl function declaring effects beyond the interface’s is
E0334. Fixture:checker/e0334_impl_effects.Examples completed (§18.2, §18.3). The reporting and checkout examples referenced modules and functions the spec did not show;
app.config,app.auth,vendor.payments,Request,Order,Basket,load_basket,record_orderand theno_third_party_assumespolicy are now inexamples/, andReceiptispayments.Receipt.Contract conveniences (§3.9, §5.3). A bare variant in a pattern (
result is Ok,| Ok ->) matches any payload, likeOk(..); and a quantifier whose domain has typeResult[List[T], E]orOption[List[T]]ranges over the contained list and is vacuously true forErr/None. Both appear in the spec’s own examples (§3.8.1, §18.2, §18.3).
M4 — const evaluator
const fnmay allocate (§3.8.1). The spec’s ownparse_selectreturns an AST, which allocates; aconst fntherefore declares at mostalloc(M3 item 48 narrowed). Its signature in §3.8.1 gainsmay alloc.ConstErrorandTypeInfoin the library (§3.8.1).ConstErroris the recordstd.check.ConstError { offset, message }(prelude);offsetindexes the graphemes of the constant text. Aconst fnreads a type throughstd.typeinfo(TypeInfo.name,TypeInfo.fields), whose intrinsics exist only at check time.Specvalues wait for the verifier.- When check-time checks run. At a call whose arguments are all
constant, the callee’s
requires provedclauses are evaluated; false isE0700, located at the offending grapheme of the literal passed for the callee’s firstconstText parameter when aConstErrorwas produced. Clauses with runtime arguments are left to the verifier.selectno longer needs.ok:columns_matchtakes the text and the row type. - Check-time failures. A contract failing or an intrinsic panicking
during evaluation is
E0701; aconstthat is not evaluable isE0701; exceeding the step budget isE0501, naming the function. - Examples at check time (§5.2). An
examplewhose statements are all evaluable (pure functions, constant values) runs at check time and a false assertion isE0702; one that needs the runtime is deferred to the generated tests of milestone 5.
M5 — codegen, everything checked
Obligations are objects (impl spec §3.5). The contracts pass creates one per site of §12.1 with status
checked, exceptrequires provedclauses the const evaluator discharged (proved). Codegen inserts a runtime check iffchecked.Where checks live. A callee checks its own non-pinned
requiresand parameter refinements on entry, on behalf of every call site; call-site refinement obligations stay in the ledger but emit no second check.ensures, the return type’s refinement,let/var/assignment flows, record and variant field refinements, loop invariants anddecreasesare checked at their sites. Int and Duration+ - * / %go through checked runtime arithmetic (overflow).inoutconvention (impl spec §6). A function withinoutparameters returns[result, ...parameters]and the caller reassigns its variables; intrinsics follow the same convention (Grid.set). Intrinsic shims passconsttype parameters first, then parameters, positionally.tryunwinds with an exception (EarlyReturn) caught by the enclosing function, instead of the impl spec’sif (r.tag === 'Err') return r;, so that atrynested inside a larger expression keeps evaluation order.matchis a labelled block of pattern tests in arm order, which is how guards fall through.Generics and interfaces. Type parameters are erased; a bounded parameter
T: Ibecomes a hidden dictionary argument andI.f(...)dispatches through it; impl functions are emitted asI$Type$fand each impl exports its dictionaryI$Type.Tests and
main. Everyexample,propertyandlawbecomes a vitest case in<module>.examples.test.js(properties and laws under fast-check generators derived from parameter types and filtered by their refinements).onus runemits a launcher that constructs the root capabilitiesmainnames (§8.3) and mapsOk/Err/Panicto exit codes 0/1/2.std.sqlat runtime has no driver in v0:connectreturnsErr(Connection).Function values are positional at runtime. A closure takes its parameters positionally, a call through a function value passes the arguments in the type’s parameter order, and a declared function used as a value is wrapped in an adapter to its named-argument form. This is what lets a closure use its own parameter names against a function type (§3.7, item 26) without a runtime mismatch; the
tsc --strictoracle caught the original defect.
M6 — verification
Lowering (impl spec §7.1). Records and unions are not SMT datatypes: field access is an uninterpreted projection per type instantiation, a variant test compares an uninterpreted integer tag, lists have uninterpreted
len/get,Textis an opaque sort whose literals are pairwise distinct, and every call is an uninterpreted function per instantiation (a fresh constant when effectful) with the callee’sensuresand return refinement asserted about the result. A value’s declared refinements are facts, recursively through record fields, union payloads and list elements. Floats are opaque values; a float operation makes only the operand it appears in unknown.Path knowledge (§3.2.1). A body is walked once with fresh SMT constants per
varassignment;ifconditions,matcharms (with the failure of earlier arms), loop conditions and invariants inside loops, their negation and the invariants after exit,forranges and list membership, andtrysuccess are facts. Loops and branch joins forget the variables they assign. An early-returning branch leaves its negated condition in force afterwards.Constant discharge. An obligation without a solver condition whose predicate and inputs are constants (the
Viewportliteral of §18.1) is decided by evaluation; this is how float refinements over constants are proved.Statuses and codes.
unsat→ proved;sat→ checked, or for a pinned clausefailedwith the model as counterexample (E0302ensures,E0342requires);unknown/timeout → checked for unpinned nonlinear obligations, otherwiseE0501. The panic rule of §6.1 isE0343(a checked obligation in a function withoutpanic); aconst fnwith a checked obligation isE0703. Overflow obligations are exempt from both in v0: the ±2^53 range is the runtime’s assumption (impl spec §12.1) and they stay runtime checks.Codegen consumes statuses. A callee’s entry check for a
requiresclause or a parameter refinement is omitted when every call site proved it (whole-program), so Mandelbrot’s generated code carries no checks.CLI.
onus check --ledgerprints the obligations of the entry file with their statuses and provenance;--budget <ms>sets the per-obligation solver budget (default 500);--no-cachebypasses.onus/cache/;ONUS_DUMP_SMT=<dir>writes every problem for inspection.Checkout example (§18.3).
recent_orders’sensures forall o: Order in result: o.customer == who.idneeds theSpecmechanism and is commented out until it exists; its proof from the statement’swhereclause is the open item of item 53.Sequential solving. The impl spec (§7.2) runs obligations in parallel up to the CPU count; v0 runs one
z3 -in -smt2process at a time withspawnSync, relying on the proof cache for repeat runs. Mandelbrot, reporting and checkout verify in a few seconds each; parallelism is a performance item for later.
M7 — reports
- Elided bodies (§2.3, §11.1).
onus interfacemust render “canonical source syntax with bodies elided to{ ... }” and the rendering must be valid Onus, so...is a token and{ ... }is a function body the parser accepts (Block.elided). Outside an interface document it isE0115 elided body outside an interface document, reported by the resolver; the checker never sees an elided body. - Interface document shape (§11.1). Beyond the example in the spec the
document carries: every item of the module with its
visibility(private items included, since the ledger and the assumptions must be complete); a module-levelledgerof every obligation with status and provenance and moduleobligationstotals, both of which the prose of §11 asks for; afailedcount; on each contractpinned,sites(how many obligations the clause generated) andchecked_atas afile:line:colof the first runtime check; loopinvariant/decreasesclauses listed as contracts of their function;examples andpropertys reported under the function of the same name (§18.1) and as items of their own otherwise.hashisb3:+ BLAKE3 of the module’s canonical text. - Diagnostics (§13).
canonical_hashis filled for every diagnostic whose file has a canonical form.onus check --jsonprints one object per line.repairsare still only produced for E0001. - Schemas. JSON Schema (draft-07) for both documents lives in
packages/compiler/src/report/schema/; the test suite validates every fixture’s diagnostics and the three examples’ interfaces against them.
M8 — claims, capabilities, paths
- Claim participation (§7.1). “Participates in the relevant effect” is
given a definition: a callee participates in an asserted claim when it has
an observable effect —
io.file,io.netor a resource effect. The quiet effects (alloc,mutate,panic,diverge,nondet,io.env,io.clock,io.rand) change nothing an observer could see twice, so a callee with only those never has to carry the claim. Anassumecovers the function and everything beneath it. Intrinsics carry only what they declare, like their contracts (§3.12). Codes:E0203derived claim not satisfied,E0204asserted claim not propagated,E0205assumeof a derived claim,E0206assumeof an undeclared claim. - Capability rules (§8, §8.3). A record field of capability type is
E0601; apub fn mainparameter of a non-root capability type isE0602; a non-test module importing atest moduleisE0600.fakeoutside a test module was already the parser’sE0012. - Paths (§9). Reachability is breadth-first over calls in bodies,
closures included, with interface calls resolved on concrete receivers
through the impl table. Function values and dispatch on type parameters
are
E0410. New codesE0412–E0415for the bound,forbid,requireandpolicyclauses;E0411for a bound that allows a forbidden effect. Policy scopes:selfis the path’s module;std.*matchesstdand every module beneath it. - Path report (§9.1). Adds
effects.forbid,obligations.failed,ok, andpermitted_by∈ {"scope","except", null }.checked_atandconstructed_ataremodule.fn:line:col. Capability construction sites are every reachable call returning a capability, including attenuation; theirassumesare empty until the stdlib records connect-time assumptions.onus path <file> [<name>] --json. - Checkout example (§18.3). Under item 76,
handle_checkout’s claim requiresauth.require(network) andload_basket(sql.read) to carryIdempotent.Idempotentmoves to a sharedapp.contractsmodule (vendor.payments already imports app.auth, so app.auth cannot import vendor.payments);auth.requireclaims it, justified by having no participating callees;load_basketclaims it with anassumethat a select reads only. The path therefore lists three assumptions —load_basketandrecord_orderin the module’s own scope,chargepermitted byexcept— and “exactly one assumption” (impl spec §9, M8) is read as exactly one external assumption, which is what the reviewer is trusting on another party’s word. The spec’s “1 assumed” presumedstd.sqlderivesrecord_order’s idempotency from the statement (item 53), which v0 does not do.
M9 — onus next
- Constrained decoding (§14, impl spec §8).
onus next <file> --offset <n>takes a UTF-16 index (the implementation plan’s--offset, not §14’s--at file:offset) and returnstokens,expectedTypeandinScope. Tokens are the kinds the parser tests at the cursor after parsing the prefix; because the lexer drops a newline before a continuation token, a position after a newline reports the union of both tokenisations. Names in the vocabulary: keywords and punctuation as themselves,ident(names and soft keywords),type-ident,literal:int|float|text|duration,newline,eof. The expected type comes from aHoleexpression at the cursor with every open bracket and block closed after it; it is null when the cursor is not in expression position or nothing expects a type there (a bare statement start). Refinements are spelled out in the type text and not enforced. Locals in scope are listed outermost first; module items are not. v0 keeps no resident state between calls (impl spec §12, item 5). mayreplaces!(§2.3, §6). Requested by the spec author (docs/CHANGE-LOG.md, 2026-09-03):!reads as negation.mayis a reserved word and!is no longer a token (!=remains). Applied to the grammar, every example in the spec, the standard library, the examples and the fixtures.
M10 — the review tool
- Path report additions (§9.1, §15.1). The path view must draw the
reachable graph, and the tool computes nothing, so the report carries
graph.nodes(qualified name, module, entry/fn/intrinsic, effects, carried claims, obligation counts, assume and recover counts) andgraph.edges(caller, callee, effects at the site, location);gates(a sealed record type some reachable function returns and others demand as a parameter — the typestate of §18.3, drawn as the gate region);recovers; andledgerrows for every obligation of a reachable function. - Interface item locations (§11.1). Each item carries
at, so the review page can show an item’s canonical source when the reviewer opens a body; the interface itself still contains no bodies. - Interface diff (§11.1, §15.1).
onus interface <file> --diff <old.json>andonus review --against <old.json>compare two documents of one module. v0 decides compatibility textually: arequiresadded or anensuresremoved is breaking, the reverse compatible; a widened effect set or a changed signature line is breaking; new assumptions, recover sites and obligations that leftprovedare listed. Implication between clauses (a weakerrequireswritten differently) is not checked; the module is breaking when a public item is. Schema ininterface-diff.schema.json. - The review tool (§15).
packages/reviewis dependency-free and renders one self-contained HTML page from the reports (impl spec §12, item 4 resolved: no framework). Views: paths (graph laid out top-down from the entry, assume leaves in amber and recover sites in purple as the only colours, unresolvable calls as a break, gate regions shaded), interfaces (bodies collapsed to{ ... }, opening counted per module in the page), ledger (filterable by state, with assumptions, recover sites and capability construction sites), diff, and diagnostics with the solver’s counterexample.onus review <entry> [--out <dir>] [--against <old.json>]writesindex.htmlandreview.json. Not in v0: the path condition in the counterexample view, promotion drafts, and decisions or contract edits flowing back as tasks (§15.1); an invalid program’s page shows its diagnostics only.
Testing model (docs/CHANGE-LOG-02.md, applied 2026-09-04)
_intry ... else(§2.3). The verify example in §20.2 writeselse _: false; the binder may now be_, which binds nothing. The printer keeps it.verifyblocks (§20.2).verifyis a reserved word and a continuation token, soverify(...)on the line after anassumeattaches to it. A block is a definition of its own (kindverify, parent the function): it sees the module and its parameters, not the function’s locals; its calls are not the function’s (no false recursion); its obligations are its own and thepanicrule applies to it; it yields Bool, each bare expression being an assertion andtry ... else _: vyieldingv. Its declared effects must contain its body’s and may not exceed its function’s (E0207); its parameters must be capabilities (E0208).The environment (§20.2, §20.6).
onus test --assumptionssupplies each parameter from atest modulewhose public zero-parameter functions return capabilities —fakes in practice — named byonus.json(test.env) or--env;io.Files,io.Env,io.Netandio.Clockcome from the runtime when the environment gives none. A parameter with no source isE0603. Generated code exports each block asverify$<n>only in that mode, and a generated launcher runs them and prints the outcomes.The ledger (§20.3).
.onus/ledger/assumptions.json, keyed by module name and the BLAKE3 of the assumption’s canonical text; each record hasat,target,result,claim,def. The interface and path reports carryverifiableandlast_verifiedper assumption; the review page shows assumed, verifiedagainst or unverified.policy verified_assumptions_onlyis the compiler’s own policy name:E0416when a reachable assumption has no passing record younger thanonus.jsontest.max_assumption_age_days(default 7).onus test(§20.6) and the checkout example. Without flags it builds and runs the generated vitest suite;--mutatewaits for M13.charge’s assumption gained averifyblock that callschargetwice with one key;auth.requireno longer declaresio.clockit never used, so the block’s effects fitcharge’s;examples/checkout/test_env.onusandonus.jsonsupply the fakes. The block passes, and the path report lists it as verified.Verify blocks in the reports (§20.2). Bodies are elided from the interface, so the block a reviewer must read to judge a verification travels with the assumption: interface and path assumption entries carry
verify, the block’s canonical text or null, and the review page shows it under the assumption in the path, interface and ledger views.
M11 — native backend
One lowering, two emitters (impl spec §6). Code generation is now
lower.ts(checked AST plus obligation statuses → the target-neutral form inir.ts) and two renderers,js.tsandnative.ts. Every decision about what generated code does is made once in the lowering; the form is printed byonus build --emit irand pinned for the fixture suite intest/codegen/lowered/. The JavaScript output is unchanged in behaviour.Native representation (§19.1).
Int/Durationarei64,Floatisdouble,Boolisi1, and everything else is a pointer to an array of 64-bit slots (a variant’s tag first), so generic code and runtime primitives move slots and callers convert at the boundary.inoutparameters are pointers.provedobligations emit nothing;checkedones branch toonus_panic, whose message matches the JavaScript runtime’s;Intarithmetic uses the overflow intrinsics;tryis a branch that returns the error. The runtime ispackages/runtime/native/(onus.c,onus.h), compiled and linked byclangfrom the emitted.ll.FloattoTextfollows JavaScript’s shortest round-trip layout (§19.4).The v0 native subset. Programs reaching closures or function values, interfaces, runtime quantifiers,
recover,fake,TypeInfo,old(...)in a checked postcondition, structural equality on aggregates,Map,Bytes,sql, orTextoperations needing grapheme tables are refused withE0800 primitive unavailable on target, as §19.1 allows. Allocation is never freed (free-at-scope-exit is deferred).recoverviasetjmp/longjmpand theIntrepresentation obligations are M12.Differential testing (§19.5).
onus test --target allruns the examples on both targets and reports each disagreement asE0801; properties and laws run on JavaScript only.onus build --target nativewrites<out>/native/<module>andonus run --target nativeruns it. The C runtime’s-DONUS_BROKEN_INT_TO_TEXTexists for the acceptance test that E0801 fires on a broken primitive.M12 names the JavaScript
sqlimplementation. M12’s text listed only the C runtime’slibpqprimitives, but its acceptance (all three examples agree on both targets; reporting runs against Postgres) needssqlreal on the JavaScript side too, which impl spec §5 has always described overpgand which v0 shipped as a stub (item 62). The milestone now says so, and takesrecoverfrom M11.
M12 — targets complete
- Host claims (§19.2).
std.hostdeclares the asserted claimsjs,nativeandwasm; a claim’s name may be lowercase (grammar §2.3) so they read ashost.js. The JavaScript-only intrinsics (Text.len,graphemes,bytes,lower,trim,Map.*,Bytes.len,TypeInfo.*) carryclaims host.js; the native emitter refuses any reached function carrying it (E0800). Aforbidclause may name claims as well as effects: a reachable function carrying one isE0413. Derived-claim predicates may name a claim by a lowercase qualified name. - Representation obligations (§19.3). Every
IntorDurationparameter andlet/vargets an obligation of kindrepresentation, proved when the binding’s declared type keeps every value within ±2^53 - 1 and otherwisechecked. They are reported in the ledger and exempt from thepanicrule like overflow. The slow path is not implemented: a checked binding keeps the number representation, and the existing overflow checks panic rather than switch to arbitrary precision. The ledger says which values that concerns. std.sqlon both targets (§8.1, §18.2; impl spec §5). JavaScript:sql.tsoverpg, driven synchronously by a worker thread withAtomics.wait(Onus calls are synchronous;pgis not). Native:onus_sql.coverlibpq, found throughpg_config, Homebrew’s keg orONUS_LIBPQ; without itstd.sqlisE0800.connect(mode: ReadOnly)setsdefault_transaction_read_only = on, verifies it, and refuses a superuser role, which could not be held to it; the remaining assumption is named at the construction site in the path report.restrictsets the search path,deadlinethe statement timeout (Err(Timeout)).- Row decoders (§18.2). For each
sql.selectwhose row type is a record the compiler generates a decoder in the target-neutral form (decoderon the call,rejectstatements): one column per primitive field, then the record’s refinements; a failure isErr(Refinement)with the row and column, a missing or ill-typed columnErr(Malformed). JavaScript passes it as$decode; natively it is a generated function the C runtime calls per row. recovernatively (§10.2). The body becomes a function over the enclosing locals’ addresses, run undersetjmp; a panic insidelongjmps back and becomesErr(Panicked { obligation, location })with the same texts as the JavaScript runtime.- WebAssembly (§19).
--target wasmcompiles the same LLVM IR with a WASI SDK (WASI_SDK_PATHor/opt/wasi-sdk) toprogram.wasmand writesrun_wasm.mjsfor Node’s built-in WASI;std.sqlisE0800there. No SDK was available where this was written, so the path is untested end to end;onus build --target wasmreports the missing SDK. - Differential harness (§19.5). Every fixture and example with
exampleblocks is built for both targets: those in the native subset must agree on every example, the rest must be refused withE0800. The SQL tests run against a Postgres atONUS_TEST_DSN(default: thepostgres:17Docker container with passwordonus) and skip with a notice otherwise.
M13 — contract mutation and coverage
- Assertion obligations (§5.2, §20.4). Every bare Bool statement of
an
example,propertyorlawbody is an obligation of kindassertion: proved when the contracts of what it calls entail it, and otherwisechecked“run as a test”. A proved assertion is a fact for the assertions after it. Tests are not functions, so these are exempt from the panic rule. Lowering test bodies exposed a contract that calls its own function (ensures compare(a: a, b: a) == 0); the verifier now states such a contract once instead of unfolding it forever. - What “detected” means (§20.4). Weakening a contract never changes a
body, so re-running the tests cannot notice it. A mutation of an
ensuresclause, a result refinement or a record field refinement is detected when an assertion the verifier proved from the contracts stops being provable without the clause: the test restates what the clause promised. Negating a property’s guards is the one dynamic mutation: the property is re-run over the complement of its domain and detects the mutation by failing.onus test --mutateprints one row per mutation,M0001 undetected contract weakeningfor the survivors, exits 0, and writes.onus/ledger/mutations.json, which the reports read. Static mutations need z3 and are skipped with a notice without it. - Two of §20.4’s mutations are not applied. Laws are not dropped: a law is itself the only test of the interface clause it states, so its absence could never be detected and every law would be reported. And parameter refinements are not widened: accepting more inputs is a stronger promise by the callee, not a weaker one, and nothing a caller’s test asserts can depend on it. Result and field refinements are widened.
- Obligation coverage (§20.5). The runtime records a hit per check
reached when
ONUS_COVERAGE_DIRis set; the generated test file writes them after its tests, since test runners end their workers without running exit handlers.onus testmerges the hits into.onus/ledger/coverage.json, keeping the larger count per check across runs, and prints the coverage line.interface.json,path.jsonand the review page carryobligation_coverage: proved; checked and how many of those a test reached; assumptions, verifiable and verified; and mutations detected and surviving. Representation obligations have no runtime check and are not counted as checks. Coverage is measured on the JavaScript target only. - The acceptance test is pinned on Mandelbrot. The
ensuresonrecent_ordersstays deferred (item 36), so the milestone’s acceptance is droppingensures result <= limitonescape_count, whichproperty escape_boundeddetects, and widening the result refinement of a fixture function no test restates, which survives and is reported. The build directory foronus testis resolved to an absolute path, which vitest needs, and the mutated programs are written beside it inout-mutateso the program’s own test run does not see them.
M14 — regeneration loop
- The loop package (loop spec §1, §10).
packages/loopwithonus-loop run <task.json>;onus loop runforwards to it, since the compiler cannot depend on a package that depends on the compiler.watchneeds task intake and is not in v0. Model access is one interface with three implementations: scripted, for the tests; Claude Code as a subprocess (claude -p, the nested-session markers stripped from its environment); the Anthropic Messages API overfetch, which could not be exercised here for want of a key. The constrained-decoding hook of §3.7 is declared and supplied by nothing. - The context (§3). Assembled through the compiler library, never
from an import’s source: the targets with bodies elided and the
examples and properties that name them; the interfaces of every module
in scope and every import; sibling bodies per the context policy; every
diagnostic of the last check as §13 JSON, except
E0115on a target, which is the task itself; failing examples with their text; counterexamples from the task and from the diagnostics; the standard library interfaces the targets’ types select. The one fixed text describes the rules and Onus syntax, which is language knowledge, not a convention. Simplification: diagnostics are not narrowed to callees; everything in scope is shown. - Never a claim (§1, §4). Model output is parsed as Onus. A target
whose signature, contracts, effects or claims differ from the baseline
is refused with a note; a second refusal is out of scope, with a
proposal built from the difference. An added function is refused once
the same way, since helper introduction (§4.1 step 3) is off. Only
bodies are spliced, under the target’s own signature, and the file is
put in canonical form so
E0001never reaches the model. Mechanical repairs apply only to spans inside a target body. - Classification and the ladder (§4, §4.1). A stall is an outcome
identical to an earlier one, or one that grew twice running. A contract
conflict is a counterexample against a target’s clause with the same
body proposed twice; it ends in a
weaken_postconditionoradd_preconditionproposal carrying the counterexample. The ladder walks full history, then a wider context policy, skips steps 3 and 4 as configured off, and stops. Examples are evaluated at check time (E0702), so a wrong body usually fails there before the verifier runs; the loop treats those like any other diagnostic. - Changes (§6).
.onus/changes/<task>/change.json: the interface diff per module in scope (empty by construction when the baseline had diagnostics, as animplementbaseline always has, since only target bodies are spliced and signatures are compared textually), the ledger delta, the body diff, the trace, metrics, proposals and audit findings. A blocked report adds the cause, the last diagnostics and the best attempt, and the working tree is left as found.onus reviewgains a Changes view, proposals marked proposed by loop. - Regeneration audits (§8). Findings are
obligation_regressedandexample_failed, each becoming a proposal (add_example,add_claim). Bodies that differ in callees are not reported: the interface documents carry no call graph. - A live run. With Claude Code as the model,
escape_countwas regenerated from its interface alone and was green on the first iteration in 41 seconds, with the loop invariant and measure intact. The test runs only withONUS_LOOP_LIVE=1. - Deferred. Production feedback (§7),
onus loop watch, and the per-repository aggregates of §11; metrics are per change. - OpenRouter and key files. A fourth model,
openrouter[:<model>], over the chat-completions protocol; the default model isOPENROUTER_MODELordeepseek/deepseek-v4-flash, chosen on the results in item 119;moonshotai/kimi-k2.7-codeis the alternative. The CLI reads.envand.env.localfrom the project root and the current directory, never overriding the real environment, so keys stay off the command line; both files are ignored by git. The live test takes its model fromONUS_LOOP_MODEL. - What running four open-weight models taught the loop. (a) A
provider that never answers hangs the task; API requests now time out
after three minutes and the task ends as a model error. (b) Each
iteration’s prompt and answer are kept in the change’s work directory
(
change.jsonstill carries only the prompt hash, §6), so a blocked task can be read. (c) A blocked report’slast_diagnosticsare the last iteration’s, including syntax diagnostics of an answer that never parsed. (d) When an answer does not parse, the notes quote each offending line and the tokens the grammar admits there, fromonus next’s machinery (§14): a model that does not know Onus writeswhile, is told “expected an expression”, and writeswhileagain; told that a line may start withloop, it has what it needs. Results on the Mandelbrot task: DeepSeek V4 Flash and Kimi K2.7 Code green on the first iteration (5 s and 20 s), GLM 5.3 Flash green on the second (98 s), Qwen3 Coder Next blocked on the budget without a parseable answer before or after (d), and Claude Sonnet 5 through OpenRouter green on the second (15 s) after a syntax slip on the first. The OpenRouter default is DeepSeek V4 Flash on these results. The runs are logged indocs/BENCHMARK.md, andpackages/loop/bench/run.mjsappends a row per model so the log can be revisited.
M15.0 — prerequisites for the compiler in Onus
- Recursion measures are obligations (§5.1). The effects pass
records every recursive cycle; the contracts pass puts a
decreasesobligation at every call within a cycle (the callee’s measure over the arguments strictly below the caller’s measure taken at entry) and one at entry (the measure non-negative); the verifier discharges both; a checked one becomes a runtime check on both targets, the arguments bound to temporaries so they are evaluated once. Before this, a recursive function only had to declare a measure. Fixtures: direct, mutual, structural over a list throughslice’s length contract, and a measure over record fields, all proved; a measure that grows, checked and panicking on both targets. - A cycle shares one measure (§5.1).
E0320now also fires when the functions of a cycle declare measures that differ once parameters are numbered by position, which is the spec’s “same expression up to renaming” made mechanical. - The standard library grew (§16, provisional).
std.text:count,code_points,of_code_points,of_code_point,slice,index_of,contains,ends_with,split,join,repeat,replace,compare,upper, positions counted in code points (lenstays grapheme-based and JavaScript-only); an empty separator splits into code points and an emptyfromleavesreplacealone, since a refinementcount(t: it) > 0on those arguments cannot be proved for a literal.std.int.parseandstd.float.parsereturnOption.std.list:Builder[T]withbuilder,push,built,finish, andmap,filter,fold,index_of,contains,reversewritten in Onus.std.io:read, and aConsolecapability withprintandeprint, a fifth root the runtime supplies tomain. Every new function has contracts and examples; the examples run at check time, as generated tests on JavaScript, and natively through the harness. A builder is a value the runtime shares: bind it once and push through that binding (a rule the type system does not yet enforce). - Structural equality natively (§19.1). The native emitter
generates a comparer per concrete type: primitives by value,
Textby the runtime, records field by field, unions by tag then fields, lists element by element throughonus_rt_list_eqwith the element comparer. Equality on a value of a type parameter staysE0800, soList.index_ofandList.containsare JavaScript-only, like the closure-taking combinators. - Two runtime fixes the library work exposed. A native file write
is flushed at once, so a read after a write sees it as on JavaScript;
and the JavaScript emitter gives each
inoutcall its own temporaries, since two such calls on one variable in a block redeclared them. A program’s build no longer emits the standard library’s own test files, which the library’s examples had just introduced. - Deferred from M15.0.
Mapon the native target and theProcesscapability for z3, which the checker and verifier stages need, not the front end; the stack-depth story, which the parser stage will settle.
M15.1 — the front end in Onus, first part: the lexer
- The lexer in Onus (
self/).self/tokens.onus,self/lexer.onusandself/lexdump.onus: the token stream oflexer.tsreproduced, positions in code points rather than UTF-16 units, every loop and helper contracted so that the verifier proves termination and every index obligation; the file checks with no diagnostic and nomay panic. The differential test runs the dump program over every.onusfile in the repository and compares it with the TypeScript lexer’s stream, tokens, comments and diagnostics alike, and the same natively on Mandelbrot. One agreed-on limit: past 2^53 the JavaScript runtime’sIntcannot hold a literal exactly (item 99), so the dump compares digits as written and the value stays a known gap. - What writing it taught the compiler. (a) The verifier now gives the
right operand of
and,orandimpliesthe left operand (or its negation) as a fact, soi < n and List.get(xs: xs, i: i)proves its index. (b) A float literal lowers to an opaque constant instead of sinking every obligation around it; an obligation the solver cannot settle is still tried by constant evaluation afterwards. (c) The native emitter labels every function’s entry block, without which a short-circuit as a function’s first statement referred to a block that did not exist; it emits aggregate and computedconstitems as slots filled on first use, and builds compile-time lists, records and variants. (d) Grammar facts a model would also need: constants are names, not upper-case; a multi-statement match arm needs braces; there is no conditional expression; patterns bind a variant’s field by its own name and may not rename it; a name from an imported module is always qualified by the alias. - Still to come in M15.1. The canonical printer in Onus, then
onus fmtreimplemented and the byte-identical acceptance.
M15.1 — the front end in Onus, second part: the parser
- The parser in Onus (
self/ast.onus,self/parser.onus,self/astdump.onus). The syntax tree ofast.tsas records and recursive unions (field names that are reserved words carry a suffix or prefix:ty,where_,is_pub), and the recursive-descent parser ofparser.tsreproduced: the thrownParseErrorbecameResultandtry, with recovery at statement and item boundaries; the parser state is one record passedinout, its position bound by the record’s own refinements; and every function of the recursive cycle takes arankand shares the measure(tokens left) * 64 + rank, so a call at a lower rank, or after a token was consumed, is strictly smaller. The verifier proves the whole parser: 160 measure obligations, 287 postconditions, every index. Not carried over: the cursor and hole ofonus next(§14). A dump program prints the tree one node per line, spans in code points, and the differential test compares it with the TypeScript parser’s tree on every source in the repository, syntax diagnostics included; they agree on all of them. - What the parser taught the compiler. (a) The verifier’s join after
an
ifforgot everything a branch assigned; it now keeps each branch’s new facts under that branch’s condition and relates a joined variable to each branch’s value, which is whatif p.pos < n { p = { p with pos: p.pos + 1 } }needs to keepp.toksknown. (b) The code generator snapshotted everyold(x)at entry whether or not any obligation would read it; a recursive-descent parser copying its token list on every call ran in quadratic time (76 s on the lexer’s own source, now 70 ms). Snapshots are taken only for contracts some obligation of which is checked at runtime. (c) Atrythat returned early from a function withinoutparameters returned the bare value on JavaScript, not the value with the parameters; pinned bytest/codegen/inout_try.onus. (d) More language facts a model needs:itis reserved even as a variable name; a call’s non-Unitresult may not be discarded, so token-consuming helpers come inUnitflavours;old(...)is not allowed in a loop invariant, so a loop binds what it needs before it; a pattern cannot shadow an enclosing binding, so nested matches on twoOptions move into a helper. - Contract shapes that proved.
advancepromises a step only below the last token, since the stream’s finaleofis a lexer invariant the record does not state; the helpers that consume exactly one token sayresult implies p.pos == old(p).pos + 1; every production that always consumes saysresult is Ok implies p.pos > old(p).pos, which is what the loops’ measures and the up-rank calls rest on. - Still to come in M15.1. The canonical printer in Onus, then
onus fmtreimplemented and the byte-identical acceptance.
M15.1 — the front end in Onus, third part: structural recursion and the printer
- Structural measures (§5.1, spec change).
decreasesmay name a value of a record, union or list type, meaning the structural order: at each recursive call the argument must be a proper part of the measure at entry, reached by pattern matching, field access orList.get. The verifier decides this from the terms themselves (a pattern field is a projection of the scrutinee, an element read is a projection of the list) and marks the obligation proved by the structural order; otherwise it reportsE0344and never falls back to a runtime check, since no general size exists to compare. No entry obligation is generated for a structural measure. Every walk over a syntax tree in the compiler in Onus needs this; the dump program declareddivergeuntil now. - The document renderer and comment attachment in Onus.
self/doc.onusisdoc.ts: the sameDocforms, the samefitsandrender, driven by aBuilderused as a stack (List.atandList.pop, added tostd.listfor it).self/comments.onusiscomments.ts: the site walk over the tree, keyed by node kind and span, the leading, trailing and dangling sets, and the same choice of owner for every comment. One known divergence: the renderer measures width in code points where the TypeScript renderer counts UTF-16 units, so a line holding a character outside the basic plane could break differently; no source in the repository has one. - The printer and
fmtin Onus.self/printer.onusisprinter.tswith one structural change: where the TypeScript printer has anoperandhelper, the Onus printer haswrap(d, paren)with the precedence test at each call, so that every recursive call passes a proper part of the node anddecreases <node>is proved for every walk.self/fmt.onusisonus fmt --stdout: read, lex, parse, attach comments, print; syntax diagnostics go to the error stream and the exit code is nonzero.packages/compiler/test/self/printer.test.tsbuilds it once and runs it over every source in the repository: a source without syntax errors must print byte-for-byte as the TypeScript printer prints it, and a source with syntax errors must be refused by both. All agree, which is the M15.1 acceptance. - Integer literals carry their digits.
tokens.IntLitandast.IntLitin Onus have atextfield besidevalue: the digits with underscores and leading zeros removed, which is what the canonical printer writes.valuealone was not enough:Inton the JavaScript target is a double (item 99), so a literal above 2^53 such as the9223372036854775807intest/roundtrip/21_expr_arith.onusloses precision in the lexer’s own arithmetic and printed wrongly. The same limitation means the lexer in Onus does not hold such a literal’s value exactly; it is therepresentationconcern of §19.3 and is left for the arbitrary-precision path. - What the printer taught the compiler. (a) A pattern binder
shadows a top-level function of the same name, and a call to the
function inside the arm is then
E0323 not callable: Expr is not a function; binders cannot be renamed (item 130), so the walkers areexpr_doc,block_doc,stmt_doc,params_doc,effects_docandpattern_doc. The diagnostic is correct but does not say why the name changed meaning; a better message is deferred. (b) A closure’s parameters put the parameter walk into the expression cycle, socollect_paramsneeds a structural measure too; the effects pass reports the cycle member without one (E0320). (c) An example that calls a generic function evaluates that function’s body at check time with its type parameter unsubstituted, so an intrinsic returningT(List.getinhead[T]) handed the evaluator a value it could not convert, and the pass threw (cannot convert a T, reported asE0999). Such a call is now not a constant: the example is left to test time, as any non-constant example is. (d)ensures result is Nonein a function returningOption[T]for a type parameterTlowered the variant test in the wrong sort and z3 rejected the query (E0999); the test is now lowered in the context of the scrutinee’s type. Both are pinned bytest/verify/ok_generic_is_in_ensures.onus, whose example reached the first and whose contracts reached the second. - No
divergeleft inself/.self/astdump.onusnow gives every walk the node as its measure (item 133) and dropsdiverge; the parser’s differential test is unchanged.
M15.2 — the checker in Onus, first part: loading, resolution and the type layer
- A structural measure may be passed on unchanged (§5.1, spec change).
The type checker in Onus, like the one in TypeScript, routes
expr(e)through helpers that take the same node and recurse on its parts, soexprcallscall(e), which callsexpr(a.value). The structural rule of item 133 rejected the first call: the argument is the measure itself, not a proper part. The verifier now classifies every call on a structural measure as strict (a proper part), equal (the measure itself, through aliases) or neither; strict calls are proved as before, neither isE0344as before, and equal calls are settled once every function is lowered: they are proved when the calls passing the measure unchanged form no cycle of their own, since every cycle then takes a proper part somewhere, and areE0344when they do. Pinned bytest/verify/ok_structural_helper.onusandtest/verify/e0344_equal_cycle.onus. Dictinstd.map.Map.putcopies the whole map on every write, which a checker’s definition and scope tables cannot afford.Dict[K, V]is an in-place table with value keys forIntandText, on theBuildermodel (bind once, write through that binding):dict,count,set,find,contains,remove,keysandvaluesin insertion order. JavaScript runtime and--emit tstype; pinned bytest/stdlib/map_ops.onus.- Nodes are keyed, not numbered. The syntax tree in Onus has no node
ids. Side tables are keyed by
defs.node_key(file, tag, span): the file, a syntactic class (expression, type, pattern, statement, item, signature part, other) and the span, packed into oneInt(a file is limited to 2^20 code points and a compilation to 1024 files). Two nodes of one class never share a span, so the key is unique; comment attachment already keyed sites the same way. A definition carries its declaring node (defs.DeclNode) so that later passes reach the declaration without a node table. - Loading and resolution in Onus.
self/report.onus(diagnostics and source files),self/defs.onus,self/context.onus(the compilation context, one record of tables that dicts and builders make writable through a copy),self/loader.onus(resolve/loader.ts: the module graph, the prelude,E0101,E0103,E0104,E0112, and pass 2 for every loaded file) andself/resolve.onus(resolve/resolve.ts: definition collection and every rule of §3.10 and §11).self/check.onusisonus checkin Onus: it runs the passes implemented so far and prints every diagnostic as one line.packages/compiler/test/self/checker.test.tsbuilds it once and runs it over every source in the repository against the TypeScript pipeline up to the same pass; codes, files, spans and order must agree, and they do up toresolve. - What the resolver taught. (a)
fn,claimsandmoduleare keywords, so they cannot be field names; the records usefn_def,claim_tableandmod. (b) ASome(value)arm inside anotherSome(value)arm is shadowing (item 130), so nested optionals go through small total accessors (or_neg,find_or) or one match per level in a helper. (c) The measure of a mutual recursion is a parameter position (item 121), so afuelmeasure must sit at the same position in every function of its cycle; the basic evaluator in Onus puts it first. (d) A dict inside a record is written through a local copy (var d = r.table; Map.set(d: inout d, …)), which the runtime shares; the verifier does not see the record change, so no contract speaks about a dict’s contents.
M15.2 — the checker in Onus, second part: types, constants and effects
- The type checker in Onus.
self/effectset.onus(effects/set.ts),self/types.onus(types/type.ts: the type representation, equality after stripping refinements, assignability, substitution, the type variable tests),self/basic.onus(consteval/basic.ts) andself/typecheck.onus(types/check.ts, with exhaustiveness fromtypes/exhaustive.ts). The port keeps the TypeScript structure, which item 139 made possible:check_expr(e)handsetoctor,call,binaryand the rest, and each recurses on the parts. One structural change: a refinement’s predicate is not checked where the type is elaborated but from a queue once the enclosing item is done, so that elaborating a type never re-enters the expression checker and the elaboration functions form a cycle of their own with a depth measure. The diagnostics are the same, in another order, so the differential test compares them sorted. - The constant evaluator and pass in Onus.
self/values.onus(consteval/values.ts),self/evaluator.onus(consteval/eval.tswithconsteval/intrinsics.ts) andself/consteval.onus(consteval/pass.tswithconsteval/offsets.ts). What TypeScript does with exceptions the evaluator does with aResultwhose error isNotConstF,PanicF,BudgetForReturnF, propagated bytry; integer overflow is caught withrecover, so a constant that leaves ±2^53 isE0701as before instead of a panic in the compiler. An intrinsic is evaluated by calling the standard library function it names, after its refinements are checked by hand so that the evaluator itself has nopaniceffect. Union results (Int.parse,Float.classify) are rebuilt from the union’s variants by name. - The effects pass in Onus.
self/effects.onus(effects/check.ts): sites, containment, closures, verify blocks, the flow of function values into function-typed positions, the call graph, Tarjan’s strongly connected components with a depth measure, and the measure keys of §5.1 computed over the printed measure.printer.print_expris the printer’s new entry point for it. - M15.2 acceptance.
self/check.onusruns load, resolve, types, constants and effects in the order ofdriver.ts, stopping at the first pass that reports as the driver does.packages/compiler/test/self/checker.test.tsruns it over every source in the repository against the TypeScript pipeline up toeffects; codes, files and spans agree on all of them. - A function named
eval(codegen bug). The JavaScript emitter renamed reserved words where a local was declared and where a function was referenced, but not where a function was declared, so a module withfn evalproduced a file Node refuses in strict mode. Every declaration site now goes through the same renaming. Pinned bytest/codegen/reserved_names.onus, whose functions are namedeval,deleteandnew. - What the checker taught. (a) A loop or a recursion whose measure
is a field of an
inoutrecord cannot be proved to decrease once the body passes that record to a callee, since the callee may change the field; a local counter written back afterwards is the shape that proves. (b) A counter declaredvar i: Int = 0losesi >= 0at a loop head; declaring itvar i: Int where it >= 0 = 0keeps it. (c) The evaluator’sEvrecord refinesstepsandbudgetto be non-negative for the same reason. (d) A function that takes the same node as a helper and re-matches it is the idiom that lets a largematchbe split, now that item 139 admits the call.
2026-09-05 — M15.3, first part: the verifier in Onus, and what porting it found
- A callee’s
ensuresabout aninoutparameter was a contradiction (verifier soundness bug). The lowering bound both a parameter andold(param)to the argument’s term, soList.push’sensures built(b: b) == built(b: old(b)) + 1becamebuilt(b) == built(b) + 1; and since a definition’s callee axioms are shared by every condition of its body, every obligation in a function that pushed to a builder was proved vacuously. Found when the verification-condition builder in Onus provedfuel < fuel. Now aninoutargument gets a fresh post-call term: the callee’sensuressees it for the parameter and the passed term forold(param), and the body walker re-binds the variable to it after the expression (§3.2.1: a call throughinoutis an assignment).verify/lower.ts(rebound,calleeFactswith pre and post bindings),verify/vc.ts(lower), and the same inself/lower.onusandself/vc.onus. Pinned bytest/verify/ok_inout_post.onus(the exit value is known through theensures) andtest/verify/e0343_inout_stale.onus(the value known before the call is stale, and nothing after the call is vacuous). - The
matchjoin keeps what each arm learned (verifier precision). After amatchwhose arms assigned a variable the walker forgot everything about it, unlike theifjoin; the parser’s loops overmatch parse_x(p: inout p) withhad only ever “verified” through item 150’s contradiction. The join now mirrorsif: a fresh constant per assigned variable, equal to each fall-through arm’s value under that arm’s condition (the earlier arms’ failure, its test and its guard), the arm’s facts under the same condition, and the disjunction of the fall-through conditions, which is sound because arms are exhaustive (§4.4).verify/vc.ts(match) andself/vc.onus(match_stmt). Pinned bytest/verify/ok_match_join.onus. - What the two fixes uncovered in
self/. Fifty obligations across the compiler in Onus had been proved vacuously; the match join settled fifteen, and the rest were real gaps, fixed at the source: loop invariants that lost a bound (j <= n,i < List.len(xs: parts),List.built(b: segs) >= 1,steps <= 100000000), counters declaredIntwhereInt where it >= 0(orit < List.len(...)) was meant, avar end: Intthat lost>= 0,digits_endgainingensures is_digit(at(src, start)) implies result > start, a slice in the constant evaluator’sgrapheme_spanwhose lower bound could exceed the text (a bug), and in the parser:take(consume or fail at the end of input) in place ofskipwhere the next token is known, sop.pos > old(p).posfollows;type_argsandcall_argspromisingresult is Ok implies p.pos > old(p).pos; and the measure(List.len(xs: p.toks) - p.pos) * 2 + flag(b: more)for loops that end by clearingmore, since adecreasesclause must fall on every iteration, the last included. - The verifier in Onus.
self/formula.onus(formulas and SMT-LIB text),self/z3.onus(finding and running z3 throughio.Process, the proof cache),self/lower.onus(lowering expressions, callee contracts, type facts, structural measures),self/vc.onus(the body walker ofverify/vc.ts: bindings, havoc, joins, loops,for, obligations at calls, fields and arithmetic),self/constant.onus(constant discharge),self/verifier.onus(the pass: z3 outcomes, counterexamples, the equal-measure cycle rule, the panic and const-fn rules). Obligation ids and node keys replace object identity; the contracts pass records the expression at each refinement andrequiressite and the constructor of each field initialiser (expr_nodes,ctor_of) for constant discharge.report.Diagnosticcarries an optionalObligationInfo(kind, text, status, counterexample) as the TypeScript one does. Left out for now: theONUS_DUMP_SMTdebugging dump and contract mutation (§20.4, an option ofbuildVCsthe Onus builder does not take). - Claims, capabilities and paths in Onus.
self/claimcheck.onus(tiers, carried claims,assumesites keyed by module and BLAKE3 of the canonical statement, propagation, E0203–E0206;claimsis a keyword, hence the module name),self/capabilities.onus(E0600–E0602),self/paths.onus(reachability, bounds, forbids, required claims, policies, gates and capability sites intoPathAnalysisrecords for the path report; E0410–E0416). The driver in Onus reads no assumption ledger yet, sopolicy verified_assumptions_onlyreports E0416 for every assumption, as the TypeScript compiler does without a ledger.self/check.onusruns the passes in the driver’s order (contracts, claims, capabilities, verify, paths) with--z3,--budgetand--cache;maintakesio.Process. Text.splitfor a prefix. Adecreasestext is trimmed of itsat the call to …suffix by taking the first piece ofText.split, which needs no index proof;Text.index_ofplusText.slicewould have required one the verifier cannot give.- The reports in Onus.
self/json.onus(JSON values, compact and two-space-indented text asJSON.stringifywrites them),self/loc.onus(line tables per file, locations as the reports print them),self/codes.onus(the code titles, generated fromreport/codes.ts),self/interface.onus(the §11.1 document and the elided canonical text; the coverage line, with the test coverage table and mutation records the driver in Onus does not read yet at zero),self/pathreport.onus(the §9.1 document),self/diagjson.onus(the §13 object, with the E0001 repair and the canonical hash). The printer gainedprint_item,print_signature,print_verify,print_typeandprint_module_elided; the loader keeps each file’s comment table for them;report.Diagnosticcarries repairs.self/check.onusprints the documents with--interface-json,--path-jsonand--diag-json, andpackages/compiler/test/self/reports.test.tscompares them byte for byte withonus interface --json,onus path --jsonandonus check --jsonon every source in the repository. The one known difference, not exercised by any source: positions count code points here and UTF-16 units there, so a line with a character outside the basic plane would place a later column differently. - What the report differential found in the checker in Onus. The
earlier differentials compared codes and spans; the JSON compares
everything. Brought into line with the TypeScript compiler: a parser
diagnostic names the definition being parsed (
tokens.Diagnosticcarries it; animplisIface[Target]with the target’s source text rebuilt from its tokens); a file that already carries a diagnostic gets no canonical text, hence nocanonical_hash; E0113 says at which line the earlier binding is; E0700 carries its obligation (requires, failed); the evaluator’s precondition messages forList.get,List.replicate,List.sliceandBytes.getare the runtime’s, word for word. The verifier’s body walker looked parameters of examples, properties and laws up under the wrong key tag, so their names were “not in the verifier’s scope” and their obligations stayedchecked; the fixture suite had not caught it because every property and law fixture is exercised through the TypeScript walker. - A nested generic call evaluates at check time (TypeScript
evaluator).
first_or(xs: [3, 4], fallback: 7)callinghead[T]callingList.getconverted the intrinsic’s result with the typeTofhead, not theIntit was called at, and threw “cannot convert a T”, so the example wasdeferred; the evaluator in Onus, whose values need no conversion, ran it. The TypeScript evaluator now keeps the instantiation of each frame and resolves a nested call’s type arguments through it, so the two agree and the example passes at check time. - One differential for the verifier and the reports.
packages/compiler/test/self/verifier.test.tsnow compares, for every source, the §13 diagnostics, the obligation ledger and the interface and path documents from one run ofself/check.onus(--diag-json --ledger --interface-json --path-json), running the compiler in Onus on several sources at a time; the earlier separate reports test is folded in. It is slow (eachself/module verifies most of the compiler again, twice) and carries a four-hour timeout; the follow-ups are a per-module verification cache, cached timeouts and a faster body walker in Onus.
2026-09-06 — the module verification cache
- A module verified once is replayed.
verify/modcache.tsandself/modcache.onus(impl spec §7.3): a module’s key is the BLAKE3 of its canonical text, its imports’ keys, the compiler (tsorself), the z3 version, the budget and a format number; the entry is the final status and provenance of each of its obligations in creation order, written only when verifying the module reported no diagnostic. On a hit the verifier assigns the stored statuses, builds no conditions for the module and discharges nothing of it; the rules run as before and report nothing, being deterministic on statuses. The two compilers keep separate entries, so the differential still verifies everything on both sides once. Pinned bytest/verify/modcache.test.ts: a replay yields the same ledger and no new entry, a changed module a new entry, a module with a diagnostic no entry. The differential over every source, which took 37 minutes with only the proof cache warm, takes 9 with the module cache filling as it goes and under a minute once it is warm.
2026-09-06 — verifying the compiler’s own code generator
- Sorts and projections are named by the qualified type. Verifying
self/lowerir.onus, which imports bothastandir, found that the lowering named the SMT sort of a type by its bare name:ast.Stmtandir.Stmtboth becameStmt, theirLet.nameprojections collided (IdentagainstText), and z3 rejected every problem that used the second as ill-sorted — 627E0999s on one module, and a soundness hole had the sorts agreed.verify/lower.tsslugandself/lower.onusslugnow use the qualified name (ast.Stmt,std.list.List_ast.Arg), for constant variant arguments too. Pinned bytest/verify/ok_same_name_sorts.onuswithlib/stmt_a.onusandlib/stmt_b.onus. The module cache format is 2. - Type facts are asserted only where a refinement is reachable.
Verifying
self/jsemit.onusexhausted a 4 GB heap before the first z3 call:typeFactswalked every variant and field of the recursiveirandasttypes to depth four for each bound name, declaring millions of projections whose only fact was a list length bound. The bound is now an axiom of the list sort, stated once when the sort’s functions are declared (forall xs. len(xs) >= 0, printed with the declarations), andtypeFacts/type_factsfirst askhasFacts/has_facts— memoised by type and remaining depth — whether a refinement is reachable through fields and elements within the depth left, and skip the walk otherwise. The conditions built are identical (the emitter module: 345 conditions, the same 5,218 formula nodes) in 72 ms and 92 MB instead of 217 s and 1.6 GB retained; the fixture suite’s ledgers are unchanged.
2026-09-06 — M15.4: the JavaScript emitter in Onus and the fixed point
Codegen in Onus.
self/ir.onusis the target-neutral form ofcodegen/ir.ts,self/irtext.onusits text (onus build --emit ir),self/lowerir.onusthe lowering ofcodegen/lower.ts(obligation statuses to runtime checks, entry checks, dictionary passing,inoutresults,matchcompilation, measures,oldsnapshots, early returns, generators for properties,verifyblocks),self/jsemit.onusthe JavaScript emitter ofcodegen/js.ts, andself/build.onustheemitAllofcodegen/build.ts(package.json, the vitest config, every module, every.examples.test.js, the launcher ofmain).self/check.onusgained--ir(print the form of every non-library module) and--emit <dir> --runtime <specifier>. Every function of the port is verified:decreases fuelat each call of a recursive cycle, with a zero-fuel guard returning the empty form, and the measures the first verification found not strictly falling (68 sites passedfuelon unchanged) now fall.The codegen differential.
test/self/codegen.test.tsruns both compilers topathson every source in the repository that checks clean, with the same budget and proof cache, and requires the printed form of every non-library module and every fileonus buildwrites to agree byte for byte, name for name. It does, on every source.The fixed point.
scripts/bootstrap.shbuildsself/with stage0 (the TypeScript compiler untilbootstrap/exists) into stage1, with stage1 into stage2, with stage2 into stage3, and requires stage2 and stage3 to be identical file for file; it is, and stage1 — the compiler as the TypeScript compiler emits it — is identical to stage2 as well: the Onus compiler compiled by itself is byte-identical to itself compiled by the TypeScript compiler (M15.4’s acceptance, for the JavaScript target). The chain runs in about seven minutes with the module cache warm. The native emitter and the remaining CLI commands are not yet in Onus.No integer above ±2^53 in the compiler’s own source. The one disagreement the codegen differential found was the Onus lexer’s duration overflow check,
value > 9223372036854775: the TypeScript compiler prints the literal’s digits, the compiler in Onus prints the JavaScript number it holds (…776). This is theIntrepresentation gap of item 99, not a codegen bug, and the compiler’s own source now avoids it:push_durationcompares the literal’s digits against the limit as text and reads the nanoseconds with the unit’s zeros appended, so the check is exact on every target and needs no multiplication. Pinned by the lexer differential and the codegen differential onself/.The native emitter in Onus.
self/native.onusiscodegen/native.ts: the LLVM IR text forclang, reachability frommainand the non-library examples, lazily filled aggregate constants, row decoders,recoverbodies as functions over the captured locals, structural comparers per type, andE0800for what the native subset does not compile (the same words, fromunsupported_detail). Two things the port could not copy: a double literal’s IEEE bits are found with exact operations (scaling by powers of two, halving, and thirteen nibbles extracted by doubling), since the runtime has no bit cast and anInton the JavaScript target cannot hold them; and a text’s UTF-8 bytes are computed from its code points, sinceText.bytesclaimshost.js.self/check.onusgained--native <dir>(writes<dir>/native/program.lland reports E0800) and--no-libpq. The codegen differential (test/self/codegen.test.ts) now also requires the native program and the E0800 diagnostics to agree byte for byte withemitNative, and they do on every source. Runningclangand comparing the targets (§19.5) stay with the CLI, which is the next port.A recursive union’s comparer no longer unfolds the type. The codegen differential over every source made the TypeScript native emitter overflow its stack:
eqSlotscompared a record’s or variant’s fields inline, so a union with a field of its own type recursed without end — the memoised per-type comparer existed but was used only at the top and for list elements. Both emitters now compare a nested record, union or list field through the type’s comparer (eqField,eq_field), which refers to itself for a recursive type; primitives stay inline. Pinned natively bytest/native/eq_recursive.onus, whose examples compare recursive trees on both targets.The command line in Onus.
self/cli.onusisonus check,fmt,build,run,interfaceandpath(cli/main.ts), with the pieces those commands needed and the reports in Onus lacked:diagtext.onus(the text rendering of a diagnostic), a JSON reader and accessors injson.onus,idiff.onus(the §15.1 interface diff over two documents as JSON values, so a document read from a file and one just produced compare alike),pathtext.onus(the path report’s text, read from its own JSON), andnativebuild.onus(clang,libpqand the WASI SDK found by running them, the same flags asnative-build.ts, E0800 and E0999 as there).test,review,loopandnextstay with the TypeScriptonusuntil M15.5. Two things wait on language changes and are written into the module’s header: the runtime turns a returnedErrinto exit status 1, so usage and I/O failures cannot be 2 as the TypeScript command line reports them, andrunrelays a program’s output after it finishes becauseio.runcaptures it — amainthat chooses its exit status, and a primitive that inherits the standard streams, are the first candidates for the language-change process. The standard library is--stdliborONUS_STDLIB, the runtime forbuildandrunis--runtimeorONUS_RUNTIME.test/self/cli.test.tsruns both command lines on a fixed set of invocations and requires identical standard output, agreement on failure, and identical files written — the JavaScript build, the LLVM IR, and the image mandelbrot renders on both targets.scripts/bootstrap.shnow builds the compiler withonus buildat every stage.The reports in Onus read the ledgers.
ledger.onusreads<root>/.onus/ledger/{assumptions,coverage,mutations}.jsonasreport/ledger.tsdoes (absent or malformed reads as empty); the command line puts them in the context, and the interface and path documents now carrylast_verified,checked_exercised,assumptions_verifiedand the mutation counts from them, in the same scope as the TypeScript reports (a module’s name prefix, a path’s reachable functions). The CLI differential found this: the mandelbrot example keeps a mutation ledger from an earlieronus test --mutate. Underpolicy verified_assumptions_onlyan assumption with a passing record is now current; its age is not checked, becauseio.Clockhas no reading primitive — the third candidate for the process.
2026-09-06 — polymorphic recursion (M15.5, the first change through the process)
- Polymorphic recursion is
E0345(spec §3.6). A generic function calling itself, directly or through other generic functions, at an instantiation that places one of its own type parameters inside a type constructor (depth[T]callingdepth[Pair[T]]) needs an unbounded set of instantiations, which the native target’s monomorphisation (impl spec §6.1) cannot compile; Rust and Go reject it likewise. The types pass builds the instantiation graph after every module — a node per (generic function, type parameter); for a call infto a genericgpassingτforg’s parameterQ, an edge from(f, P)to(g, Q)for each type parameterPoffoccurring inτ, expansive whenτis notPitself — and reports an expansive edge whose target reaches its source, once per call, on every target. What is lost is nested data types whose type encodes a shape invariant; Onus states those as contracts. Fixtures:test/checker/e0345_polymorphic_recursion.onus(the nestedNest[T]) andtest/checker/ok_generic_recursion.onus(recursion at the same type, mutual recursion between generics, generic code calling generics at concrete types, andTree[T]). Made in both compilers: the process’s freeze of the TypeScript compiler starts when M15.7’s fixture runner exists, since until then the fixture suite and the differentials run over the TypeScript library (the skill’s text is corrected). Review artefacts: the interface diff ofself/typecheck.onusis+ fn polymorphic_recursion, compatible; the ledger delta of the module is 23 obligations added, none removed, no status changed — 12 proved (10 measures, the rest refinements and a loop invariant) and 11 checked, all of themrepresentationandoverflowobligations, which v0 checks at runtime by rule (§6.1).self/codes.onuschanged in the body oftitleonly. - A structural measure is never a runtime check. Found by the
accepting fixture above under
--emit ts: when a function’sdecreasesmeasure is a record, union or list and its obligations are not yet final (a pipeline stopped before verification, as the TypeScript-oracle test stops), both lowerings emitted$measureas anIntlocal and a<check against it at each recursive call, whichtsc --strictrefuses and which at runtime would compare aggregates. The verifier settles a structural measure as proved or asE0344; only a numeric measure (IntorDuration, §5.1) is ever checked at runtime, socodegen/lower.tsandself/lowerir.onusnow emit the measure local and the checks only for numeric measures. Product output is unchanged: after verification a structural measure has no unproved obligation.
2026-09-06 — Map on the native target (M15.5)
MapandDictnatively; calls carry their type arguments. The C runtime gains an insertion-ordered hash table keyed by value (Int,Duration,Bool,Unitas slots,Textby its bytes), with tombstones for removal andMap.putcopying (spec §19.1). The twelvestd.mapintrinsics no longer claimhost.js. The emitters pass a key kind on every keyed operation, taken from the call’s type arguments, which the target-neutral form now records:callgainstargs, the instantiation of the callee’s type parameters at the call (empty for a non-generic callee), printed asf[Int, Text](…)in the form’s text. ADictkeyed by any other type —Floatincluded, whose JavaScript equality is not slot equality — isE0800on the native target. Fixtures:test/native/dict.onusruns on both targets (text and integer keys, insertion order under replacement and removal, the persistentMap);test/native/e0800_dict_key.onuspins the refusal. No generic function over aDict[K, V]compiles natively yet: the key kind of a type parameter waits on the specialisation step (impl spec §6.1). Review artefacts: the interface diff ofself/ir.onusis theCallvariant ofExprgainingtargs(reported breaking, as a public union’s shape is); ofself/native.onus, two private functions added andcall_val’s signature widened by the type arguments;self/lowerir.onus,self/irtext.onusandself/jsemit.onuschanged in bodies and pattern arity only. Ledger deltas: ir: 0 -> 0 obligations; added 0 ({}; checked kinds {}), removed 0, status changes 0; native: 240 -> 242 obligations; added 2 ({‘checked’: 1, ‘proved’: 1}; checked kinds {‘representation’: 1}), removed 0, status changes 0 — the checked additions are representation and overflow obligations, checked at runtime by rule. The chain frombootstrap/reached the fixed point and stage2 was promoted.Generic code compiles natively by specialisation (impl spec §6.1).
codegen/specialise.tsandself/specialise.onusrun first insideemitNative: every call to a generic function is retargeted to a copy of it specialised to the call’s type arguments (from thetargsitem 173 added to calls), copies are namedf[Int, Text], appended to their module in discovery order, and specialised in turn until the set is closed — finite by §3.6, the rule item 171 made for this. Inside a copy the type parameters are concrete, so equality on a type parameter, aDictkeyed by one and the rest resolve to the concrete type; generic functions themselves are dropped, and the emitters’ reachability is keyed by emitted name rather than definition, since a definition now has many copies. The JavaScript target is untouched. Fixture:test/native/generics.onus—swap[T],first_or[T], equality on aT,count_of[T],List.index_ofatTextandBool, and aDict[K, Int]keyed by a type parameter — runs on both targets. The native census of the compiler itself fell from 481 refused functions to 14 (fiveText.trim, fivehash.blake3_hex, one each ofText.lower,len,graphemes, and aBytesvalue). Review artefacts: the interface diff ofself/native.onusis the five reachability visitors taking a context and a queue of names (private, reported breaking) andemit_nativeunchanged;self/specialise.onusis new, 266 obligations, 177 proved and 89 checked, every checked one a representation or overflow obligation; the native ledger lost one obligation with no status change. Fixed point reached frombootstrap/; promoted.The rest of the primitive surface natively; no stdlib module claims
host.js(§19.1). The C runtime gainsonus_lib.c:Text.len,graphemes,lower,upperandtrimover Unicode 16.0 tables generated from the UCD byscripts/unicode-tables.mjsintounicode_tables.h(grapheme clusters per UAX #29 with the GB9c, GB11 and GB12–13 state; default case conversion with SpecialCasing’s one-to-many mappings and Final_Sigma;trimover JavaScript’s WhiteSpace and LineTerminator set, which is what the host does);Bytesas the text layout, soText.bytesis a view andBytes.len/getread it;hash.blake3_hexover the vendored BLAKE3 1.8.2 reference implementation inblake3/, built portable (-DBLAKE3_NO_*,-DBLAKE3_USE_NEON=0);io.runoverposix_spawnpwith three pipes,polland a monotonic deadline, returning theOutputrecord,NotFoundfor a program that cannot start andOtherwith “progdid not finish within N ms” on timeout, exactly the host’s messages, andOtherfor every call under WASI, which has no processes;io.mkdircreating parents; andtypeinfo.name/fieldsreading a two-slot object. Both native emitters build that object where a type is used as a value (the expression form and the compile-time value, throughtypeSlug/lowerir.type_slug) in place of thenullthey emitted, emitBytesconstants as text-layout globals, and mapBytesto a pointer; both build drivers add the new C sources and flags. The ten remainingclaims host.jsinstd.text,std.bytes,std.hashandstd.typeinfoare removed with theirstd.hostimports. Spec §19.1 lists the primitives. Fixtures:test/native/text_native.onus(lengths, cases, bytes, BLAKE3 vectors,TypeInfo; runs on both targets),test/native/process_native.onus(amainexercisingrunfive ways andmkdir, compared to the JavaScript build’s output and files), andtest/native/unicode.test.tsdrivingunicode_probe.onus: case conversion andtrimfor every code point of the cased blocks (over 14,000) and Final_Sigma, SpecialCasing and grapheme clustering over 31 texts, each line compared with what the Node host (ICU, Unicode 16.0) computes. Review artefacts: the interface diff ofself/native.onusis three private functions added (bytes_global,typeinfo_obj,typeinfo_owner) and the module comment (reported breaking, as any doc change is);self/nativebuild.onusunchanged; ledger delta ofnative+6 obligations, 4 proved, 2 checked (bothoverflowon a counter and a length inbytes_global, like every checked obligation in the module), no status change;nativebuildunchanged. The native census ofself/cli.onusis zero:onus build self/cli.onus --target nativewritescli, and that binary, with node absent, checks mandelbrot with every obligation proved through z3 byio.runin 0.7 s and builds it for the native target to an image byte-identical to the node-hosted build’s. It still needs--stdliband--runtime; embedding them is the release work of M15.5. Fixed point reached frombootstrap/; promoted.A column counts code points (§13). The first fixture with characters outside the Basic Multilingual Plane (item 175’s
text_native.onus) made the two compilers disagree on one column: the TypeScript compiler computed columns as UTF-16 units from the offset, the compiler in Onus as code points, and the specification said neither. Code points it is, since a column must not depend on the host’s string representation;source.tsnow counts them and the spec says so. Fixture:test/checker/e0105_columns_code_points.onus, an unknown name after four flag emoji at column 20 rather than 24. The compiler in Onus is unchanged.Interface calls inside obligations lower per receiver sort. The first fixture that did arithmetic on an interface method’s result (
total + Shape.area(s: x)withx: Tbounded byShape) was E0999 in both compilers: the verifier names the uninterpreted function for a call by the sorts of its instantiated parameters, but for a call through an interface the checker appends the interface’s own type parameter to the call’s instantiation (callSignature) and the verifier’s substitution stopped at the method’s own type parameters, so the function was declared over the interface’s parameter sort and applied at the caller’s. Both verifiers now bind the interface’s parameter to that last type argument (verify/lower.ts,self/lower.onus:res_iface,iface_param_of), soShape.areaatRectand at a caller’sTare two functions and anensuresstated through the interface is proved. Fixture:test/verify/ok_interface_calls.onus, 13 proved and 7 checked, no diagnostic. The Onus verifier’s interface gains two private functions; its ledger +4 obligations, 1 proved and 3checkedrepresentation obligations on the new loop indices, no status change.Function values, closures, interfaces, runtime quantifiers,
fakeandold(...)compile natively (§19.1). Both native emitters: a value of a function type is a pointer to a closure object, slot 0 the code address and the rest the captured values, and its code takes the object and one slot per parameter (ptrforinout) and returns a slot. A declared function used as a value gets an adapter@"m.f$fn"from that convention to its signature and a constant object@"m.f$val", emitted once; a closure is lifted to@"closure$N"over its free locals (a walk of its body in first-use order, less what it binds), captured by value at construction as §3.7 requires, and returns slots, sotryandreturninside it convert through the function’s own return type. An impl’s dictionary is a constant table of its methods’ objects in interface order, reached with the dictionary; a bounded type parameter’s dictionary is a hidden leadingptrparameter, and a call through a dictionary loads the method’s slot and calls it as a closure. A quantifier evaluated at runtime is a loop over its domain (range, list, the list inside anOk/Someor nothing, the two Booleans) stopping at the first counterexample or witness.fakeis an object of its behaviours, which no primitive reads.old(x)in a checked postcondition copiesxwhere its type can be mutated in place — builders, grids and maps through new runtime primitives (onus_list_builder_copy,onus_grid_copy,onus_map_clone), lists, records and unions holding them through a generated copier per type, everything else shared, which is indistinguishable from the JavaScript host’s deep copy. What E0800 still refuses:Specvalues, a generic function used as a value, andsqlwithout libpq. Fixtures:test/native/function_values.onus(declared functions and intrinsics as values, closures capturing lets and parameters, closures returned, composed, stored in records and lists, passed withinout, two impls of an interface with bounded generic callers, quantifiers over lists, ranges, results andBool) andtest/native/old_native.onus(a builder and a record holding one, with postconditions the verifier cannot prove so the copies run); both run on both targets, as docodegen/features.onus,stdlib/list_generic.onusand the interface fixture of item 177, which the native suite now lists.test/native/unsupported.onusis gone with its test, since its program compiles; the E0800 path is pinned by theDictkey fixture. Review artefacts: the interface diff ofself/native.onusis two private records and 27 private functions added,call_valtaking the call’s dictionaries, and the state record and module comment changed;self/lower.onusas item 177. Ledger delta ofnative: +401 obligations, 254 proved and 147 checked, every checked one an overflow or representation obligation on counters, indices and sizes, like the rest of the module; no status change. The codegen differential found one disagreement while porting — the Onus emitter generated a copier before allocating the call’s temporary, one counter apart from the oracle — fixed in the port. Fixed point reached frombootstrap/; promoted.The native stage of the bootstrap chain, and the differentials run natively (impl spec M15.5).
scripts/bootstrap.sh, after the fixed point, has stage2 buildself/cli.onusfor the native target and then runs that executable with a path holding onlyclang,z3and the system directories — no node, no TypeScript — to build the compiler for JavaScript, which must equal stage2 file for file, and for the native target, whose LLVM IR must equal what stage2 emitted for it; the stage is skipped with a notice withoutclang. The seven differential tests undertest/self/take their driver fromtest/self/driver.ts: under node as before, or, withONUS_SELF_NATIVE=1(pnpm --filter compiler test:self-native), the program ofself/built withonus build --target native—lexdump,astdump,fmt,checkandcli— so that lexing, parsing, printing, checking, verification, the reports, code generation for both targets and the command line are compared with the TypeScript compiler on every source with the compiler in Onus running natively; the assertions that run a driver’s own examples as generated JavaScript tests are skipped in that mode. Measured on this machine: the JavaScript-hosted stage2 builds the compiler natively in 5 s, and the native compiler builds itself for JavaScript in 1.2 s and for the native target in 3.6 s. The native run found one disagreement in the repository:e0701_const_evaluation.onusdeclared the constant 9007199254740993, which the JavaScript host holds as the double 9007199254740992 (item 99’s deferred arbitrary-precision path) and the native host holds exactly, so the E0701 text differed by one. The fixture now uses 2^53 itself, outside the safe range and the same number on every host, so it pins the rule rather than the host’s rounding; the inexactness ofIntbeyond 2^53 on the JavaScript target stays item 99. No language or compiler change; the skill’s steps 5 and 6 name the stage and the native differentials.The compiler carries the standard library and both runtimes (impl spec M15.5, release).
self/bundle.onus, generated byscripts/bundle.mjsand committed, holds every file ofpackages/stdlib/std, the JavaScript runtime’sdist/*.jsand the C runtime (onus.c,onus.h,onus_lib.c,onus_sql.c,unicode_tables.h,blake3/, its licence) as text constants, with the root package’s version; a test regenerates it and compares. The loader readsstd.*from the bundle when no--stdlib(orONUS_STDLIB) is given, under the pathsonus:std/…, and the prelude is always imported.onus buildwith no--runtime(orONUS_RUNTIME) writes the JavaScript runtime to<out>/onus-runtime/and generated modules import./onus-runtime/index.js, climbing out of their own directories (../onus-runtime/index.jsfromstd/);--target nativewith no runtime location writes the C sources to<out>/native/runtime/and compiles against them.onus --versionprints the version. So that a generated JavaScript program depends on nothing installed, the runtime hashes with its own BLAKE3 (packages/runtime/src/blake3.ts, checked against the reference implementation at every structural boundary intest/stdlib/blake3.test.ts) and@noble/hashesis no longer a dependency of the runtime;pgremains one for programs that usesql, loaded only when they do. Found on the way and fixed: the command line in Onus rejected a bare entry name (onus check main.onus) with E0104, since the root it derived was the working directory and the path had no directory; a bare name now lives at.. The TypeScript compiler is unchanged: it resolves its runtime from the workspace, and the differentials always pass--stdliband--runtime. Fixture:test/self/cli.test.tsbuilds the command line natively, removes node from the path, and checks, builds for JavaScript, runs under node, builds natively and runs mandelbrot from a directory holding only its source, the two images equal;test/self/bundle.test.tspins the bundle. Review artefacts:self/bundle.onusis new (a record, two constants, two functions, no obligations); the interface diffs ofself/loader.onus,self/nativebuild.onusandself/cli.onusare empty but for theusagetext, andself/build.onusgainswrite_bundled_runtime(public) andruntime_for; ledger deltas:build+2 obligations, both checked (a representation and an overflow obligation on the directory depth), the other three unchanged. Fixed point reached frombootstrap/, the native stage agrees; promoted.A self-contained
bootstrap/, and the build on GitHub (impl spec M15.5, release). The chain no longer passes--runtime: every stage after the first is built by the compiler in Onus, which carries its runtime (item 180), so a stage holdsonus-runtime/and imports it relatively, and the chain runs from the repository root with relative paths, so nothing it emits names a machine.bootstrap/therefore builds the compiler wherever node runs, which the workflows rely on..github/workflows/ci.ymlruns the gates on every push and pull request on Linux: the packages build, the whole suite, the chain with its native stage, and the self differentials against the native compiler, withclangandz3installed and the proof caches kept between runs under a key of the sources they were computed from..github/workflows/release.ymlruns on a tagv*(and by hand): Linux x86_64, Linux arm64 and macOS arm64 each buildonusfrombootstrap/with node andclangas build-time dependencies only, refuse a tag that does not name the root package’s version, smoke-test the binary from a bare directory with no node on its path, and packageonus, the licences and the README asonus-<version>-<target>.tar.gz; a final job creates the GitHub Release with one archive per platform. macOS on Intel is not built (decided 2026-09-06); Windows waits on the C runtime’s port and is the next item. The first run publishedv0.0.0for the three platforms; the Linux builds verified the compiler cold in 10.5 minutes and, with the proof cache restored, in under a minute. The first CI run found two host differences and no compiler fault: vitest colours its output on GitHub, so the generated-tests check now runs it without colour; and the runner’s node carries Unicode 17.0 while §19.1 pins 16.0 (six Latin letters given case mappings in 17.0 differed), so the comparison of the native text tables with the host runs only on a host at 16.0 (process.versions.unicode) andci.ymlpins node 22.13.0, which has it. When the specification moves to a newer Unicode, the tables, the guard and the pin move together.A Homebrew tap.
brew install JamesRandall/onus/compilerinstallsonuson macOS (Apple Silicon) and Linux (x86_64, arm64) from the release archives, withz3as a dependency, so a Mac user is never asked by Gatekeeper about an unsigned download; the formula is namedcompilerin the tapJamesRandall/onus(repositoryhomebrew-onus), as decided 2026-09-06.scripts/brew-formula.sh <version> <archives>rendersFormula/compiler.rbfrom the archives’ checksums, and the release workflow’s last job pushes it to the tap when the secretHOMEBREW_TAP_TOKEN(a fine-grained token with write access to the tap) is present, otherwise attaches it to the run for a manual update. The formula’s test checks the version and checks a small program. Signing and notarising the macOS binary would need an Apple developer account and is not done; Windows, once ported, gets no Homebrew route.A clock reading:
io.now(§19.1; the first M15.6 language change).io.now(clock: io.Clock) -> Duration may io.clock, nondetis a monotonic reading since the program started, at the host’s resolution, which two readings compare and subtract; it is not a calendar time. Chosen over nanoseconds since the epoch because a monotonic reading is an exactInton every target, while epoch nanoseconds exceed the JavaScript host’s exact range (item 99), and because timing — budgets, timeouts, elapsed time — is what the compiler needs. The JavaScript runtime readsperformance.now(); the C runtime readsCLOCK_MONOTONICagainst the originonus_startrecords. Neither compiler changes: the intrinsic is declared instd.ioand both emitters map it as they map every primitive. Fixture:test/native/clock_native.onus, amainwhose two readings are ordered and small, run on both targets. The fixture found that the C runtime had never carried the twostd.durationprimitives,nanosandof_millis, since no native program had used aDuration; they are an identity and a multiplication over the nanosecondi64, and §19.1’s surface is complete again. Process: made through the skill with the chain’s native stage and the native differentials;self/bundle.onusregenerated so the compiler carries the new runtime;self/does not useio.nowin this change.A process on the standard streams:
io.exec(§19.1; the second M15.6 language change).io.exec(process: io.Process, program: Text, args: List[Text]) -> Result[Int, Error] may io.file, nondet, allocruns the program with this process’s own standard streams inherited — what it reads and writes is the user’s terminal, not captured — and returns its exit status, -1 when a signal ended it; a program that cannot start isNotFound, any other failureOther, and under WebAssembly every call isOther. The JavaScript runtime spawns with the streams inherited; the C runtime flushes its own buffers and spawns without file actions. This is whatonus runneeds to hand a program the terminal instead of relaying its captured output after it finishes (item 169); the command line takes it up in a later change, sinceself/may not use a primitive in the change that introduces it. Neither compiler changes. Fixture:test/native/exec_native.onus, amainwhose child writes to the shared output and exits 3 and whose second program does not exist, the same output on both targets. Process: through the skill with the native stage and the native differentials;self/bundle.onusregenerated.mainchooses its exit status (§8.3; the third M15.6 language change).mainreturnsResult[Unit, E]orResult[Int, E]:Ok(Unit)ends the process with status 0,Ok(n)with statusnas the platform takes it,Err(e)writes the error to standard error and ends with 1, a panic with 2; any other return type isE0604, new in both checkers’ capabilities pass (mainStatus,main_status), which before accepted any return type while both runtimes reported everyOkas 0. The lowering’s description ofmaingainsstatus, printed asstatusin the target-neutral form and written into the launcher’s JSON; the JavaScript runtime returns theOkvalue as the exit code when the description says so; the native emitters pass the flag toonus_finish(ptr, i1), which reads theOkslot. Fixtures:test/checker/e0604_main_return.onus(amainreturningInt) andtest/native/status_native.onus(amainreturningOk(3), exit status 3 on both targets with nothing on standard error). This is what the command line in Onus needs to report usage and I/O failures as 2, as the TypeScript one does (item 169); it takes the feature up in a later change, withio.execforrun. Review artefacts, under.onus/changes/185/(item 186): the interface diffs ofself/areMainSpecgainingstatus(ir), and one private function each inlowerir,irtextandnative, plus the publicmain_statusincapabilities, which the lowering shares; ledger deltas are +2 incapabilities(one proved, one checked representation obligation on the looked-up definition), the other six unchanged. Fixed point reached, native stage agrees, promoted;self/bundle.onusregenerated.Review artefacts under
.onus/changes/<item>/. Until now the interface diff and the ledger delta the process makes the human review were computed in passing and paraphrased in theCHANGES.mditem, with the before-snapshots in an ignored directory and the after-documents nowhere.scripts/change-review.mjs begin <item> <module>...snapshots the interface documents before a change, andfinish <item>writes, under.onus/changes/<item>/(ignored by git, as the loop’s proposals are; decided 2026-09-06), each module’s §15.1 interface diff, its interface after the change as JSON and as canonical text with bodies elided, andledger.md: every obligation added, removed or changed in status, per module, keyed by definition, kind and text. The documents come from the compiler underbootstrap/. The skill’s steps 3, 6 and 7 name the script and the directory. Change 185’s artefacts were produced from its snapshots after the fact; items 183 and 184 touched no module ofself/but the generated bundle.The command line reports exit statuses and hands
runthe terminal. With items 184 and 185 promoted,self/cli.onususes them: itsmainreturnsResult[Int, io.Error]and reports what the TypeScript command line does — 0 success, 1 diagnostics (or a breaking interface, or a native build that failed), 2 usage or I/O failure — by mapping each command’s outcome through one table of the markers the commands already returned, printingonus: <error>for a failure no command reported itself;checkandfmtnow return the diagnostics marker after printing their counts rather than the count as the error the runtime used to print.runstarts the program throughio.execon the user’s own streams and ends with the program’s status, 1 when a signal ended it or it could not start, as the TypeScript one does; the captured relay is gone. That closes both notes of item 169. Fixture:test/self/cli.test.tsnow requires identical exit statuses on every invocation, not agreement on failure alone; it caught the two commands whose markers had not been mapped. Review artefacts under.onus/changes/187/: the interface diff ofself/cli.onusismainreturningResult[Int, io.Error],build_commandandrun_programreturning the status, the module header (the doc comment ofusage), and four private functions added (failure_status,exit_status,exit_status_of,usage_failure); the ledger delta is one obligation fewer, the proved refinement onio.run’s timeout argument thatexecdoes not take, no status change. Not a language change; made through the process for the chain and the differentials.io.remove_allandio.list_dir(§19.1).io.remove_all(files: io.Files, path: Text) -> Result[Unit, Error] may io.fileremoves a file, or a directory and everything under it, and isOkwhen nothing is there;io.list_dir(files, path) -> Result[List[Text], Error] may io.file, allocis a directory’s names in code point order (UTF-8 byte order natively, the same order), without.and..,NotFoundwhen the directory is not there; a permission failure isDenied.onus testneeds both, as the TypeScript command line has them: to clear the coverage files of the previous run before merging the new ones, so the ledger reflects the last run alone, and to find the files the test workers wrote (item 189). The JavaScript runtime removes recursively and forcibly and reads the directory; the C runtime walks the tree and sorts. Neither compiler changes. Fixtures:test/native/remove_native.onus(a tree, a file and something absent removed) andtest/native/list_native.onus(names listed in order, a missing directoryNotFound), the same output on both targets. Process: through the skill;self/bundle.onusregenerated;self/does not use them in this change.onus testin Onus: examples, properties and laws, and obligation coverage (M15.6; §19.5, §20.5, §20.6).self/testcmd.onus(new) andself/cli.onusprovideonus test <entry> [--out <dir>] [--target js|native|all]as the TypeScript command line does. Onjs: build, run the generated example, property and law tests under vitest on the user’s own streams (io.exec), fold the hit files the test workers wrote under<out>/coverage/(cleared first, so the ledger reflects the last run alone) into<root>/.onus/ledger/coverage.json, keeping the larger count per check, and printobligation coverage: …over the program’s own obligations and assumptions; the status is vitest’s. Onnative: build the executable and run its examples, printing what it prints; 1 when any fails. Onall: also run the examples under vitest’s JSON reporter (<out>/examples.report.json), print each outcome with(js), and report every disagreement asE0801(§19.5); 1 on a disagreement or a failure. A program with no example, property or law says so and succeeds.--assumptionsand--mutateare refused with status 2 and run with the TypeScriptonusuntil items 190 and 191;build’s runtime selection moved intoruntime_choice, shared withtest. One change to both compilers, forced by the port:onus testused to hand the coverage directory to the generated tests through the environment, whichio.execdoes not pass on (item 184); the generatedvitest.config.mjsnow setsONUS_COVERAGE_DIRitself, tocoverage/beside the config (fromimport.meta.url) when the environment does not, so a run of the generated tests by any means records beside the program. The first attempt wrote the output directory’s absolute path into the config and the chain caught it: stage2 and stage3 differed in that one line. Two deliberate differences from the TypeScript helpers: the module of a vitest test file is found by matching its path against the emitted modules rather than by the output directory’s length, and the JSON-reporter run does not setCI=1, which only affected the terminal reporter. Fixtures:test/self/cli.test.tsgainstestontest/native/primitives.onusstaged in a fresh directory (equal exit status, equalobligation coverageline, equalcoverage.jsonbyte for byte; a program without tests; the refused switches and an unknown target) andtest --target nativeand--target allonprimitivesand mandelbrot (standard output and status identical). Review artefacts under.onus/changes/189/: the interface diff ofcliisRuntimeChoice,runtime_choice,test_commandandjs_flagadded and the module comment and usage text changed;buildhas no interface change; the ledger delta iscli45 → 48 obligations (28 proved, 20 checked: one index refinement proved, two representation obligations on counters checked) andtestcmdnew with 22 (10 proved, 12 checked: the representation and overflow obligations on counters and indices, as throughoutself/). Fixed point reached frombootstrap/, native stage agrees; promoted.A calendar time:
io.time(§19.1; an M15.6 language change).io.time(clock: io.Clock) -> Int may io.clock, nondetis the calendar time as whole milliseconds since 1970-01-01T00:00:00 UTC, anIntthat is exact on every target (item 99 is why it is milliseconds in anIntand not nanoseconds in aDuration);io.now(item 183) stays the monotonic reading for timing.onus test --assumptionsneeds it to date the ledger’slast_verifiedrecords (§20.3), which the TypeScript command line takes from the host’s clock, and the checker’s age rule on those records (test.max_assumption_age_days, item 90), which the compiler in Onus has not enforced for want of a clock (self/ledger.onus), can follow. The JavaScript runtime returns the host’s epoch milliseconds; the C runtime readsCLOCK_REALTIME. Neither compiler changes: the intrinsic is declared instd.ioand both emitters map it as they map every primitive. Fixture:test/native/time_native.onus, amainwhose reading lies between 2020 and 2100 and whose two readings are ordered, run on both targets. Process: through the skill with the chain’s native stage and the native differentials;self/bundle.onusregenerated; noself/module changes, so there are no review artefacts beyond the bundle;self/does not useio.timein this change.onus test --assumptionsin Onus (M15.6; §20.2, §20.3, §20.6).self/assumptions.onus(new;codegen/assumptions.ts),self/config.onus(new;config.ts:onus.json’stest.env,test.targetandtest.max_assumption_age_days, with the defaults) andself/cli.onusprovideonus test <entry> --assumptions [--env <test module>]: the entry and the environment module (--env, elseonus.json’stest.envunder the root) are loaded and checked through paths; the environment’s producers are its public zero-parameter functions returning a capability; every module is emitted with itsverifyblocks as functions; a verify parameter with neither a producer nor a root capability ofstd.ioisE0603; the launcherrun_assumptions.jsis written as the TypeScript one is, byte for byte, and run undernodethroughio.run(an hour’s timeout, where the TypeScript command line has none); its outcomes are printed one per line and recorded in.onus/ledger/assumptions.jsonunder the calendar time of the run,io.time(item 190) formatted asDate.toISOStringprints it (assumptions.iso_of_millis, pinned by an example on four dates); the ledger keeps the records of earlier runs, in file order. The command line’smaintakesio.Clockfor it. Two loader fixes the port exposed, in both compilers so the differential stays meaningful: a second bare file name (onus check checkout.onus test_env.onus) is at the working directory’s root like the first (item 180 covered the first alone,loader.root_for), and the inferred root is the entry’s directory as the entry names it, so imported files are reported the way the entry was given (vendor/payments.onus, not./vendor/payments.onusand not the absolute path the TypeScript loader used to print for a relative entry). Fixtures:test/self/cli.test.tsgainstest --assumptionson the checkout example staged in a fresh directory: standard output and status identical, the launcher byte for byte, the ledger equal but for the time, the time in the ISO form; the environment named with--envand a second run over the ledger the first left; withoutonus.json,E0603on both, identical; and the two-bare-filescheck. Review artefacts under.onus/changes/191/: the interface diff ofcliistest_assumptions,config_env_path,module_at_path,file_id_of,module_of_fileandcopy_recordadded,mainandtest_commandtakingclock: io.Clockand claimingio.clock, and the module comment and usage text changed;loaderhas no interface change; the ledger delta iscli48 → 54 obligations (29 proved, 25 checked: a refinement proved, four counter representations and overflows checked),loaderunchanged,confignew with 1 (checked, a representation) andassumptionsnew with 81 (45 proved, 36 checked: the representation and overflow obligations of the date arithmetic and of counters, and the four example assertions, which run rather than prove). Fixed point reached frombootstrap/, native stage agrees; promoted.--mutateis next (item 192).onus test --mutatein Onus (M15.6; §20.4).self/mutate.onus(new;mutate/mutate.ts) andself/cli.onusprovideonus test <entry> --mutate: every contract weakening §20.4 names over the program’s own modules — eachensuresclause dropped, each refined result or record field widened to its base type, each guarded property’s guards negated — assessed one at a time. A static weakening is detected when a test assertion the verifier proved becomes unprovable: the conditions are rebuilt with the weakening in force and the proved assertions put to z3 again, the first that is no longerunsatnaming the detector; without z3 the static weakenings are skipped and a line on stderr says so. A guard negation is detected when the property, re-emitted with its guards negated into<out>-mutate/mutate/<i>and run under vitest’s JSON reporter, fails over the complement of its domain. Survivors areM0001 undetected contract weakeningrows of the run, not diagnostics; the records go to.onus/ledger/mutations.jsonunder the calendar time of the run, and the status is 0. For the static weakenings the verifier in Onus gains what the TypeScript one has had since M13:lower.Mutation(kind, definition, clause span, field) is carried by the lowerer’s state, and three places withhold a fact under it — the callee’s result refinement (widened_ret), the callee’sensuresclause at the span (dropped) and a record field’s refinement (field_type);vc.build_vcstakes the mutation,Nonefrom the verifier. The port found nothing to change in either compiler. Fixtures:test/self/cli.test.tsgainstest --mutateontest/mutate/unexercised.onus(the guard ofhalf_is_smallerdetected,plus_zero’s surviving, the widened result ofhalfsurviving under z3; standard output and status identical, the records equal, the time in the ISO form, the programs underout-mutate/) and on mandelbrot (dropping theensuresofescape_countdetected byescape_bounded, M13’s acceptance, identical on both). Review artefacts under.onus/changes/192/: the interface diff islowerwithMutationadded,Lwcarrying it andnew_lwtaking it,vc.build_vcstaking it, andcliwithtest_mutateadded and the usage text changed;verifierunchanged; the ledger delta iscli54 → 59 (30 proved, 29 checked: a refinement proved, counters checked),lower349 → 352 (three representation obligations on definition ids checked),vcandverifierunchanged,mutatenew with 12 (3 proved, 9 checked: representation and overflow on ids, budgets and the counter). Fixed point reached frombootstrap/, native stage agrees; promoted.testis now whole in Onus;review,nextandloopremain.onus reviewin Onus (M15.6; §15).self/review.onus(new;report/review.tsand the@onus/reviewpackage’srender.tsandlayout.ts) andself/cli.onusprovideonus review <entry> [--out <dir>] [--against <interface.json>]: after the pipeline ran to paths the bundlereview.jsonis collected — every user module’s interface document (item 92’sinterface_of) and canonical source, every path report, the diagnostics as JSON, the interface diff of the entry module against the given document, and the changes the regeneration loop wrote under.onus/changes/*/change.json, narrowed as the page shows them and newest first — andindex.htmlrenders every view of it: the path view with its layered graph (breadth first from the entry, as authority flows; unresolvable calls as breaks; gate regions around the guarded nodes), the interface view with bodies collapsed and their opening counted, the ledger view with its filters, the diff view, the changes view with proposals marked as the loop’s, and the diagnostics view with the solver’s model. The renderer reads the JSON documents directly (json.Json), since every document is produced by a moduleself/already has, and its page is byte for byte the TypeScript page: the same style sheet and script, the same markup. Diagnostics are reported on the terminal and the page then holds them alone; the status is 1 when any was reported, 2 when--againstdoes not name an interface document. The run’s time (io.time, item 190) dates the bundle. Neither compiler changed. Fixtures:test/self/cli.test.tsgainsreviewon the checkout example staged with a loop change written under.onus/changes/task_test/(the bundle and the page identical but for the time; the assumed leaf, the gate, the blocked change and its proposal, the collapsed body present),--againsta renamed copy of its own interface (identical; the diff view present),--againsta file that is not a document (status 2), and an invalid program (identical; status 1; the page holdsE0416). Review artefacts under.onus/changes/193/: the interface diff ofcliisreview_commandadded and the usage text changed; the ledger delta iscli59 → 60 (an index refinement proved) andreviewnew with 123 obligations (26 proved, 97 checked: the representation and overflow obligations of the layout arithmetic and the counters, as throughoutself/). Fixed point reached frombootstrap/, native stage agrees; promoted.nextandloopremain.Conditions with
inoutcalls run when they are tested (impl spec §6, codegen; found by item 195). Two lowering bugs, in both compilers, the same way. A call withinoutarguments lowers to a statement that assigns its results back, placed before the statement its expression belongs to. In aloop whilecondition that put the call before the loop, so the condition was evaluated once and every later test read a stale value: a loop guarded bybelow(c: inout c) and c.n < 10ran to 10 and askedbelowonce. As a later operand ofand,ororimpliesit put the call before the whole expression, so it ran even when the earlier operands had decided the result. Now the condition’s own statements are collected: a loop whose condition has any runs them before the loop into a flag and again at the end of every iteration, the loop testing the flag; a chain whose later operand has any keeps the result in a flag and runs that operand underif flag(and),if not flag(or) or, forimplies, the negated left side. A condition without such statements lowers as before, so every pinned form (test/codegen/lowered/) and every differential is unchanged. The compiler’s own parser never had aninoutcall in a condition until item 195 made its token tests record, which is how the bugs surfaced: theimportloop stopped after one iteration and lookahead tests ran past a false left operand. Fixture:test/native/inout_conditions.onus, an example on every case (seencounts the calls,nthe outcome), run on both targets. Neither the verifier nor the emitters change: the flag is a local of the IR the emitters already have. Review artefacts under.onus/changes/194/:lowerirhas no interface change (Later,short_circuitandimplies_exprare private); the ledger delta is 572 → 593 obligations (361 proved, 232 checked: the new functions’ fuel and index refinements proved, their representation and overflow obligations checked, no status changed). Process: promoted on its own before item 195, since a compiler whose conditions hoist would miscompile the parser that item instruments — the chain’s stage1 could not parse its ownimportline — which is the self-application rule of the skill in practice.onus nextin Onus (M15.6; §14).self/nextcmd.onus(new;next/next.ts),self/parser.onus,self/tokens.onus,self/loader.onusandself/cli.onusprovideonus next <file> --offset <n> [--json]: the legal next tokens at the offset, the expected type there and the local names in scope, as the TypeScript command line prints them. The parser gains the two features its port left out (item 158’s note): the legal set and the hole. With aCursortoken at the offset (andeofafter it, so the parser’s loops still run at the cursor), every grammar test of a token kind — theat_*functions, which now take the parserinoutand record the kind when the tested token is the cursor, the set testsat_cmp_op,at_add_op,at_mul_opandat_literal_pattern, andexpectingbefore the dispatches over items, statements, patterns and primaries — records intoParser.legaluntil the first syntax error or until the cursor is consumed (advancestops recording;takegoes through it); loop guards and negative lookahead keep usingkind_at, which never records, and the chained-comparison re-test peeks as the TypeScript one does. When the prefix ends with a newline the run without it adds the continuation kinds, aslegalTokensAtdoes.parse_with_holeparses the prefix with aHoletoken, closers for every open bracket and block, a newline andeof;primarymakes it anast.Hole, which the resolver and the checker already record under its span.nextlexes the prefix (offsets are code points, item 181), adds the file with the holed parse (loader.add_parsed, split fromadd_file), runs load, resolve and types on it whatever the diagnostics, and reads the hole’s tables by the key of its zero-width span; the type text spells the refinements out innermost first (typecheck.showon the base).tokens.decoder_name_ofmaps the grammar names to the decoder vocabulary. Fixtures:test/self/cli.test.tsgainsnextat every token oftest/next/positions.onusand at its end,--jsonoutput identical on both, plus the text form, an expected type with its refinement and the names in scope, and the refused offsets. The instrumentation is what surfaced item 194. The resolver’s list of names visible at a hole now walks every scope below the module’s, as the TypeScript one does: a record field’s refinement sees the earlier fields (the two positions the sweep found). Review artefacts under.onus/changes/195/: the interface diff ofparserisParsercarryingcursor,recordingandlegal, theat_*functions taking the parserinoutand claimingalloc(withaccept_*,skip_newline,visibilityand the recoveries),qtname_aheadandeffect_followslikewise, andprefix_tokens,legal_tokens_atandparse_with_holeadded;tokens.KindgainsCursorandHoleanddecoder_nameanddecoder_name_ofare added;loader.add_parsedis added;cligainsnext_command;resolvehas no interface change. The ledger delta isparser1153 → 1218 obligations (1053 proved, 165 checked: 48 proved and 12 checked added, the checked ones the representation and overflow obligations of offsets and counters; six proved obligations oftake,parse_stmt,add_exprandmul_exprgone with the tests that moved into the helpers; no status changed),cli60 → 62,loader63 → 64,resolveandtokensunchanged, andnextcmdnew with 14 (9 proved, 5 checked representations). Fixed point reached frombootstrap/, native stage agrees; promoted.loopremains.std.http(§19.1; an M15.6 language change).http.request(net: io.Net, method: Text, url: Text, headers: List[Text], body: Text, timeout_ms: Int) -> Result[http.Response, io.Error] may io.net, nondet, allocsends one request and waits for the whole response,Response {status, body};http.postandhttp.getare the two shapes over it. Any HTTP status is a response; a host that cannot be reached isNotFoundnaming the URL, a request that does not complete withintimeout_ms(0 for no limit) isOther, as is anything else that keeps the request from completing. The regeneration loop in Onus (item 197) needs it for the Anthropic and OpenRouter providers; the loop in TypeScript usedfetch. The JavaScript runtime is synchronous, so the request runs in a childnodedoingfetch, the request on its standard input and the answer one JSON line back; the native runtime runscurlwith its options in a configuration file (the headers, which carry keys, never touch a command line) and reads the status code--write-outappends to the captured body;curlmust be on the path, asclangandz3must. Neither compiler changes: the intrinsic is declared instd.httpand both emitters map it as they map every primitive ($rt.http.request,@onus_http_request). Fixture:test/native/http_native.onus, amainthat posts to a server the test starts on a local port, gets a missing path and a port nothing listens on, with the same output on both targets. Process: through the skill with the chain’s native stage and the native differentials;self/bundle.onusregenerated;self/does not usestd.httpin this change.onus loopin Onus (M15.6; docs/onus-loop-v0.md).self/regen.onus(new;packages/loop/src:task,env,model,edit,project,context,change,cycle) andself/cli.onusprovideonus loop run <task.json> [--root <dir>] [--model <spec>] [--budget <ms>] [--json] [--no-write], the regeneration loop of the loop spec run in the compiler’s own process: the task validated astask.schema.jsonadmits it (by hand: the same required keys, enumerations, minimums and forbidden extras), the scope read and put in canonical form, the baseline checked through paths with the working texts as entries (an import of a scope module finds the entry already registered, so the texts overlay the tree as the TypeScriptreadFilehook did), the targets selected, then the cycle of §4 — the model context of §3 assembled from the compiler’s own interface documents and reports, the answer’s fenced Onus parsed as a fragment, the target bodies spliced under their baseline signatures and the file put back in canonical form, the check with up to three rounds of high-confidence repairs inside the target bodies, the classification, the ladder of §4.1, the budgets of §4.2 — and the conclusion: the bodies written andchange.jsonunder.onus/changes/<task>/, or a blocked report with the last diagnostics and the best attempt. Proposals (§5), the regeneration audit (§8), tickets and the syntax notes with the legal tokens at the offending line (onus next, item 195) are as the TypeScript loop has them;change.jsonis the same document, byte for byte but for the time and the milliseconds, the prompt hash carrying theb3:prefix of the compiler’s hashes. The model sits behindgenerate: a scripted list of answers (the tests), Claude Code as a subprocess (shunsets theCLAUDE*markers so a nested run may start), and the Anthropic and OpenRouter APIs overstd.http(item 196), the keys from the environment or.env/.env.localin the root and the working directory, never a command line. Two deliberate differences:--rootis taken as given rather than resolved, so the files a change names are as the root names them (the differential passes each side its own absolute root), and the model’s request timeout is the loop’s constant on every provider. The command line’smaintakesio.Netfor it. Fixtures:test/self/cli.test.tsgainsloop runon the loop package’scalcfixture with a scripted model: a wrong body then a right one opening a change in two iterations (standard output, the written body andchange.jsonidentical), a stall walking the ladder to a blocked report (--jsonidentical), a ticket answered with a proposal and no edit, an invalid task, an unknown model andwatch. Review artefacts under.onus/changes/197/: the interface diff ofcliisloop_commandand six helpers added,maintakingnet: io.Netand claimingio.net, and the usage text; the ledger delta iscli62 → 68 (35 proved, 33 checked: three index refinements proved, three representations checked) andregennew with 218 obligations (58 proved, 160 checked: the representation and overflow obligations of offsets, counters and budgets throughout, as everywhere inself/). Fixed point reached frombootstrap/, native stage agrees; promoted. With it every command of the TypeScriptonushas its counterpart in Onus: M15.6 is complete, and M15.7 — retiring the TypeScript compiler behind a fixture runner in Onus — is next.The fixture runner in Onus (M15.7, first step; impl spec §10).
self/fixtures.onusis a program,run_fixtures <dir>... [--stdlib] [--out] [--runtime] [--update], that runs the compiler’s own test suite from afixtures.jsonmanifest in each fixture directory, in place of vitest over the TypeScript library. A manifest is one section or a list ofsections, each of a kind:diagnostics(every.onusfile, orsources, checked to the passtounderroot, its diagnostics as{code, span, file?}and withledgerits obligations compared with.expect.json;ok_fixtures, or thecleanprefixes, and everything underexpect_nonemust produce none; every code of the catalogue under thecoversprefixes lessexceptmust be produced;budget/budgets,also,dir),canonical(own canonical form,messy/pairs),lowered(the entry module’s target-neutral form against<golden>/<name>.ir.txt,<repo>/standing for the working directory),examples(each source’s examples pass on thetargetsand the targets agree;dirssweeps directories for sources with examples,skip_prefixes,allow_refusaladmits E0800,propertiesincludes properties and laws), andprograms(cases whosemainruns withargson the targets, writing thestdoutfile’s text and exiting withstatus;viaa wrapper script). A section thatrequiresz3 or clang that is absent is skipped with a notice;--updaterewrites the expectation files exactly as the TypeScript harness did (verified byte for byte over every expectation in the tree).self/codes.onusgainsall_codes(), the catalogue in order, for the coverage check. The manifests cover the fixture-shaped suites oftest/syntax,roundtrip,checker(withstd/reserved.onus, whose expectation is now a file),paths,verify,examples,codegen(goldens and the generated tests),stdlib(every std module checks; the examples of the five fixture modules) andnative(the twelve programs that must agree on both targets, and the sweep of every fixture with examples under checker, paths, codegen, stdlib and native, refusal allowed); every existing expectation passes unchanged. Not reproduced: the AST equality of the round-trip property (the printed text’s equality with the fixture and its idempotence are), and the bespoke vitest cases (counterexample text, the worked-example paths, the native primitive programs,next,loop,review,mutate, the assumptions ledger), which becomeprogramscases and CLI expectation fixtures in the next changes of M15.7.scripts/bootstrap.shbuildsself/fixtures.onusat every stage besideself/cli.onus, sorun_fixtures.jsis in the fixed point, in the native compiler’s JavaScript build, and inbootstrap/;scripts/fixtures.shruns the suite with it (ONUS_FIXTURESnames another runner, such as a chain’s stage2 for the acceptance step of the skill);test/self/fixtures.test.tsbuilds the runner with stage0 and requires the suite to pass under it, the bridge while vitest exists. Review artefacts under.onus/changes/198/: the interface diff ofcodesisall_codesadded;fixturesis new with 36 obligations (18 proved, the index refinements; 18 checked, the overflow obligations of counters and lengths and the representation obligations of pass indices, budgets and statuses, as everywhere inself/). Fixed point reached frombootstrap/, native stage agrees; promoted.The bespoke tests as fixtures (M15.7, second step; impl spec §10). Every vitest case that asserted something in code rather than through a fixture is now a manifest case the runner in Onus (item 198) runs, and the runner grew the kinds they need.
clicases run the compiler under test (--cli, therun_cli.jsbeside the runner) in a staged working directory — a copy ofstage_diror thestagefiles, an earlier case’s directory (in), thenwrite,removeandedit(from/to,dates_to,append) — with$dir,$stage,$root,$nowand a marker-computed$offsetsubstituted into the arguments, and check the exit status, the standard streams (stdoutpinned to a file,stdout_text,stdout_contains,stdout_lacks,stdout_canonical,stderr_*) and the files left behind (filespinned,files_text,files_contain,files_exist,files_absent,file_hashes,count_files) afternormaliserules (dates,ms,tokens,hash,stage,out,root,last_line).programscases run each target in its own directory withenv,via(a wrapper script: thestd.httpfixture’s local server),cflags, and compare the targets (same_stdout,same_files); a section may name theruntimeits programs import and aprobeprogram that must exit 0 for it to run (thestd.sqlcases lay out their schema in the probe and are skipped with its reason when no Postgres answers).buildscases must build, or be refused natively with theexpected codes (E0800 for aDictkeyed by a record);examplessections takecflagsandexpect_disagreement(E0801 from the deliberately broken runtime);next_sweepchecks that the token present is legal at every position of every source and pins the complete answer ofonus nextat every token ofpositions.onus;scriptcases pin a shell script’s output (the bundle’s freshness; the released native compiler with clang alone on the path, when--native-clinames one).requiresmay also namelibpq,cli,native_cliandunicode16, on a section or a case.nativebuild.build_nativetakescflags. The manifests:test/cli(the command line: check, fmt,--emit ir, interface and its diffs, path, build and run on both targets with the image’s hash, test with its coverage ledger, the assumptions of checkout and its path report, the policyverified_assumptions_onlywith a fresh, a stale and a failed record, mutation and the interface’s coverage counts, review with a loop change, against a previous document and of an invalid program,nextat twenty-five positions, and the loop with a scripted model — 93 cases, 9 more needing z3),test/native(the primitive programs, the Unicode probe compared with the host’s tables on both targets, the broken runtime),test/codegen(the worked examples build, a violatedrequirespanics with its obligation),test/stdlib(the process and file programs, BLAKE3 over the lengths that exercise every branch of the algorithm on both targets,blake3_lengths.onus),test/sqlandtest/next. The expectation files were generated with--updateand read. Found by the conversion and fixed: the compiler in Onus never checked a verification record’s age —ledger.is_verifiedtook any passing record — where the TypeScript compiler required one no older thanmax_assumption_age_days(§20.3);Contextgainsnow_msandassumption_max_age_ms, set by the command line from the clock andonus.json,ledger.is_currentandledger.millis_of_iso(the inverse ofassumptions.iso_of_millis, with an example) decide, and the policy cases pin it. Also found: a program whose bundled runtime could not loadpghung forever instd.sql.connect, the main thread waiting on a worker that had died importing it; the worker now signals when it starts (the bridge gives up after 30 s) and importspgon the first connect, so the failure is the connect’sConnectionerror. Not reproduced, and dropped with the TypeScript compiler: the fast-check round-trip over generated syntax trees, the JSON-schema validation of the interface, diagnostic and path documents (their shapes are pinned by theclicases), the E0999 injection (the compiler in Onus has no exception to catch), and--emit ts. Review artefacts under.onus/changes/199/: the interface diffs arenativebuild.build_nativeandbuild_withtakingcflags(breaking, two callers incli),Contextgainingnow_msandassumption_max_age_ms,ledger.is_verifiedgainingallocwithis_current,millis_of_isoand two helpers added,cli.setupand the five commands that call it taking the clock, andfixturesreworked (46 definitions added,run_programandoutcome_okremoved,Options,Argsandmainchanged). The ledger deltas:cli68 → 69 (one overflow of the configured age in milliseconds, checked);paths88 → 90 (the days of the message, one proved, one overflow checked);ledger5 → 76 (27 proved: the index refinements and the day-of-year arithmetic; 49 checked: the representation and overflow obligations of the parsed fields and the millisecond products, and the seven assertions of the example);fixtures36 → 125 (42 proved: the index and slice refinements,copy_tree’sdecreasesand the lexer’s precondition; 83 checked: the representation and overflow obligations of scanner indices and counters, as everywhere inself/);context,assumptions,configandnativebuildunchanged. The suite under stage2: 445 passed, 3 skipped (the fixture that does not check, the two Postgres sections). Fixed point reached frombootstrap/, native stage agrees; promoted.The TypeScript compiler is removed (M15.7, done).
packages/compiler,packages/loopandpackages/revieware gone with their vitest suites: the compiler isself/, andbootstrap/— the JavaScript it emitted for itself at the last fixed point — is what runs it (pnpm onusisnode bootstrap/run_cli.js). The fixture suite moved frompackages/compiler/testtotest/, manifests and expectations unchanged but for the paths they name ($dirkept the pinned outputs as they were; the lowered goldens name<repo>/test/…); the JSON schemas of the interface, diagnostic and path documents moved todocs/schema/, the loop’s todocs/schema/loop/; the loop benchmark isscripts/bench.mjs, runningonus loop run --jsonfrombootstrap/per model.pnpm testisscripts/fixtures.sh;scripts/bootstrap.shrequiresbootstrap/as stage0 and has no TypeScript fallback;scripts/bundle.mjsformats the bundle withbootstrap/; CI builds the runtime, runs the chain, and runs the suite under the chain’s stage2 with its native compiler for the release case (the self differentials, which compared the two compilers, are gone with one of them). vitest and fast-check stay as the workspace’s dev dependencies because generated tests import them by name (onus test); typescript stays for the runtime. The documents follow:CLAUDE.mdandAGENTS.md(the repository map, the commands, the hard rules that named TypeScript files or--emit ts, which no longer exists), impl spec §2 (the layout), §10, §11 and M15.7 (done), the skill (stage0, where codes and fixtures live, the acceptance step, and a history note in place of “While the TypeScript compiler exists”), the README, the grammar’s and the loop spec’s pointers, the website’s status and loop pages, and the comments inself/that named their TypeScript originals. M15.7’s second acceptance — a deliberate change to an emitter inself/is caught by the stage comparison before promotion — was exercised by item 194, whose loop-lowering fix first surfaced as a stage difference. Fixed point reached frombootstrap/, native stage agrees, the suite under stage2 green; promoted.The
hardenedmodifier (M16, first change; spec §2.3, §21; docs/CHANGE-LOG-03.md). Spec §21 “Zones” is inserted after §20 as the change log wrote it, and the grammar’s visibility becomes[ "pub" ] [ "hardened" ] [ "sealed" ]withhardeneda reserved word (the grammar document likewise). The compiler in Onus parses, prints and carries it:ast.Visibility.is_hardened,defs.Defis_hardened(from the declaration’s visibility;falseon every synthetic definition), the keyword intokens, the parser’s visibility and its legal-token lists, the printer (pub hardened sealed record), soonus fmtandonus interfacerender it andonus nextoffers it. Nothing checks it yet:E0902(the modifier outside apubitem of adraftmodule) needs the manifest and is item 202’s, with the dependency rule,E0900andE0901. Under the self-application ruleself/does not use the modifier. Fixtures:test/roundtrip/36_hardened.onus(a fn, a record and a sealed record markedhardened, its own canonical form),test/syntax/e0003_hardened_reserved.onus(let hardenedis a syntax error), and theclicasesinterface_hardened_text(canonical text with the modifier) andinterface_hardened_json. The milestone is M16 — Zones in the implementation spec (the change log said M14, which is the loop). The acceptance of the change log namesapp.core.*,app.reportingandapp.payments; the worked examples of §18 keep their module names and are zoned as they are (impl spec M16). Review artefacts under.onus/changes/201/: the interface diffs areast.Visibilityanddefs.Defgainingis_hardened(breaking, as any record field is),resolve.addtaking it, andtokens.keywordsgaining the word; the ledger delta is empty in all twelve modules snapshotted (the parser’s 1218 obligations, the printer’s 92, the lexer’s 311, unchanged). The suite under stage2: 451 passed, 3 skipped; thenextsweep’s pinned answers gainhardenedat the eleven item positions. Fixed point reached frombootstrap/, native stage agrees; promoted.The manifest and the zones pass (M16; spec §21, impl spec §4 pass 12a; docs/CHANGE-LOG-03.md).
self/toml.onusreads the subset of TOML the manifest uses (tables, quoted or bare keys, strings, one-line arrays, inline tables; anything else is an error naming the line).self/zones.onusis the pass:onus.tomlat the project root gives every module its zone (an exact rule, else the longestprefix.*rule, elsedefault, elsedraft; the standard library iscritical), and the pass checks the dependency rule over every reference the resolver recorded — a node key decodes to its file and span (defs.key_file,key_span), so the report lands on the use — once per (module, definition), with the hardened items of draft modules as the one exception (E0900); the modifier’s placement, on a public item of a draft module only (E0902); the hardened bundle, a justification on every assumption (E0905); and the critical bundle: every public function on a path (E0903), every checked contract obligation exercised by the coverage ledger (E0904; the overflow and representation obligations of arithmetic are exempt, a narrowing recorded in §21.2), and on every path whose entry a critical module holds,recoverforbidden (E0413), third-party assumptions forbidden unless the manifest’s[zones.exceptions]names them (E0415), and verification required (E0416, unless the path declares the policy itself and pass 12 reported it). A manifest that cannot be read isE0906. The pass runs after paths as pass 12 (--to zones, now the default of every command) and needs the files capability for the manifest. The reports: the interface document carries the module’szoneand each item’szoneandhardened(a hardened item of a draft module is held tohardened); the path report carrieszone,zones_crossed,draft_dependenciesandconditional(alwaysfalseuntil item 203).Contextgainszonesandzone_exceptions. The fixture runner reads a manifest-levelcoversacross sections, since each zone fixture is a project of its own. Fixtures undertest/zones/: a zoned project that checks (ok_zoned: criticalcore.*, hardenedsvcdepending oncoreand on the hardenedpayments.chargeof a draft module), one per diagnostic, andok_exception(the third-party assumption the manifest excepts). Every existing expectation is unchanged but the interface and path documents, which gain the fields. Review artefacts under.onus/changes/202/: the interface diffs areContextgainingproject_root,zonesandzone_exceptions(breaking, as a record field is),defsgainingkey_file,key_tagandkey_span(its ledger 8 → 26: 13 proved, 13 checked, the decoders’ arithmetic),interfacegainingis_hardenedanditem_zone,cligaining the pass name inusageandpasses, and the runner’sdiagnostics_dirandrun_sectiontaking the sharedseenwithcoverage_ofadded; the ledger delta is empty in every snapshotted module;tomlis new with 98 obligations (53 proved, 45 checked: the scanner’s index arithmetic) andzoneswith 24 (5 proved, 19 checked: the representation and overflow obligations of key decoding, ranks and the age in days).onus test,test --assumptions,test --mutateand the loop’s inner check stop at the paths pass, since the coverage and verification ledgers the critical bundle reads are what they write;check,build,run,interface,path,reviewandnextrun to zones. The suite under stage2: 470 passed, 3 skipped; the interface and path documents pinned by theclicases gain the fields, and three cases pin thatonus testclearsE0904and that the path report of a critical entry says so. Fixed point reached frombootstrap/, native stage agrees; promoted.The zone commands, the promotions ledger and the zone-aware loop and review page (M16, done; spec §21.3–§21.5; loop spec §1, §3, §4.1, §5, §8; docs/CHANGE-LOG-03.md).
onus zone showprints the manifest as the commands write it and the promotion history;onus zone promote <module> <zone>writes the candidate manifest under.onus/zone-candidate/, runs the pipeline over the module’s source with the candidate as the project’s manifest, refuses on any diagnostic (the target zone’s policy bundle, §21.2, is what the zones pass reports), and otherwise writes the manifest at the root, signed, and appends the promotion record to.onus/ledger/promotions.json(module, from, to, at, kind, audit);onus zone demote <module> <zone>writes and records without checking. The audit is the static half — “policies passed; regeneration skipped” — because the regeneration half of loop spec §8 needs a model, and a second model is not configurable in v0; it is deferred, not dropped. The signature is the BLAKE3 of the[zones]and[zones.exceptions]tables aszones.canonical_textserialises them (rules sorted by pattern,defaultlast, exceptions sorted); a manifest whosesignaturedoes not match isE0901, and a manifest with no signature — written by hand before the first command, as the worked examples’ are — is accepted (§21.3, a note). Demotion: the zones pass reads the promotions ledger, and a dependency on a module whose latest record is a demotion is notE0900but a conditional guarantee, recorded per dependent module (Context.conditional_on) and reported: the interface document’sconditional_onand each ledger row’sconditional, the path report’sconditionalandconditional_onover every reachable module (§21.3, §21.5). The review page draws the zones of a path as regions around its nodes (draft regions dashed) and lists the promotion history beside the ledger, from the review data’spromotions. The loop (self/regen.onus) reads the zone of the target’s module from the baseline: the context policy’s default isscopeindraft,moduleinhardenedandnoneincriticalwhen the task gives none (itscontext_policyis empty until resolved, then the change records the resolved one); indrafta model answer that changes the target’s signature is spliced, signature and body, and the change lists the item underloop_authored; incriticala stall does not walk the ladder — it would go to the frontier model, which is not configured, so the task is blocked with that message — and awiden_effectsproposal is never emitted (loop spec §4.1); the change document carrieszone. The worked examples are zoned:examples/checkout/onus.tomlmakescheckoutandapp.*hardened andvendor.*draft with the exception forvendor.payments.charge, andvendor/payments.onusmarksClient,Error,Receiptandchargehardened;examples/reporting/onus.tomlmakesreportingandapp.confighardened. The M16 acceptance is thecli-zonessection oftest/cli: the checkout project is tested (coverage), verified (--assumptions) and promoted module by module —app.contractsandapp.authreachcritical(the promotion check loads every source under the root, since the paths that cover a module’s functions are declared wherever the project declares them), andcheckoutitself is refused withE0416, because its two structural assumptions (load_basket’s andrecord_order’sIdempotent) have noverifyblock andcriticaladmits no unverified assumption, and withE0903, becauserecent_ordersis public and on no path; the refusal is pinned,checkoutstayshardened, its path report namesvendor.payments.chargeunderdraft_dependenciesand crosses all three zones, and the review page draws the regions. Reaching the change log’s “checkout critical” needsverifyblocks on those two assumptions, against a fakesql.Dbin the example’s environment module, which is an edit to the worked example of §18.3 and is left for its author to decide; a promotion of a module with a public function on no path is refused withE0903; a signed manifest edited by hand isE0901; demotingapp.configunderreportingmarks the reporting path conditional, and demotingcore.mathundersvcin the zoned fixture project markssvc’s interface conditional with noE0900. Found by the zoned example and fixed: item 201 propagated thehardenedmodifier to the definitions of functions and records but not to claims, capabilities, unions, interfaces, constants and type aliases (resolvepassedfalsefor them), socheckout’s use of the hardenedvendor.payments.ClientwasE0900; every item definition now carries its declaration’s modifier. A module that carrieshardeneditems cannot itself be promoted — the modifier isE0902outsidedraft— which is the rule working as written: the boundary is hardened first, the module later, once its modifiers are dropped. Fixtures:test/zones/e0901_edited_manifest, andsvcintest/zones/ok_zonedgains a path so it can be promoted tocritical. Review artefacts under.onus/changes/203/: the interface diffs arecligainingzone_command,project_sourcesandmanifest_or_emptywith the usage text;ledgergaining the promotions reader and writer,promotion_jsonanddemoted_modules, withLedgercarryingpromotions;zonesgaining the signature, the canonical text,write_manifest,set_zoneand the conditional helpers, withManifestcarryingsignature;reviewgaining the regions, the promotions view and its data;regengainingtarget_zoneanddefault_policy, withRuncarryingzoneandloop_authored;tablesgainingPromotionRecord;Contextgainingpromotions,demotedandconditional_on. The ledger delta:cli69 → 83 (10 proved, 4 checked added: the command’s indices and its budget),review123 → 137 (14 checked: the regions’ coordinates),zones24 → 28 (2 proved, 2 checked), the rest unchanged, nothing regressed. The suite under stage2: 492 passed, 3 skipped. Fixed point reached frombootstrap/, native stage agrees; promoted.The regeneration half of
onus zone promote, as an instrument (M16; spec §21.3, loop spec §8). With--model <spec>(the loop’s specs:claude-code,anthropic,openrouter[:<model>],scripted:<file>),promoteruns, after the static check passes, aregeneratetask over the module throughregen.run_task: the module’s bodies are discarded and rebuilt from the interfaces alone (context_policy: none) and the change is written under.onus/changes/promote_<module>/. Decided 2026-09-07 with the spec author, and recorded in §21.3: the audit does not gate the promotion. What promotion rests on is what the compiler can check — the zone’s policy bundle, verified assumptions, exercised obligations — and the promotion record carries, for the reviewer, the module’s contract mutations from the ledger (“mutations: d detected, s surviving”) and the regeneration’s outcome: “passed in N iteration(s)”, “N finding(s)” (each printed), why it did not conclude (blocked, with the cause), or “skipped” without a model. The reasoning: regeneration measures how much of a module’s meaning is in its interface; our own compiler, whose specification is its fixture suite, would read low on it correctly, as would any module specified by its output, so a reading is a result and not a bar, and mutation testing is the cheaper twin of the same question. Fixtures:zone_promote_audit(the scripted model answerssvc.billcorrectly: “regeneration passed in 1 iteration(s)”) andzone_promote_audit_findings(a wrong answer: the run is blocked as a stall, the promotion still succeeds and the record says so). Found by the first of them: the loop took the target’s zone from the baseline’s context, whose check stops at the paths pass, so every module read asdraft; it now reads the manifest itself (zones.read_manifeston the project root). The change document carrieszoneand the resolvedcontext_policy(nonefor a regeneration), which the case pins. Found by the first real readings (docs/BENCHMARK.md, “The regeneration audit”): the audit filed anE0115left by a body the model never produced asobligation_regressed, “the interfaces were sufficient to check the old body but not to reconstruct it”, which is not what happened; such a diagnostic is now the findingnot_reconstructed, naming the function no usable body came back for, so a reviewer can tell the model’s silence from the interfaces’ insufficiency. Review artefacts under.onus/changes/204/: the interface diffs arecligainingaudited_regeneration,audit_of_changeandmutation_summarywithzone_commandchanged, andregengainingmanifest_or_emptywithtarget_zonetaking the files capability; the ledger delta iscli83 → 90 (1 proved, 6 checked added: the iteration and mutation counts),regenunchanged, nothing regressed. The suite under stage2: 494 passed, 3 skipped. Fixed point reached frombootstrap/, native stage agrees; promoted.The working tree is the loop’s state during a run (loop spec §9). The first honest-looking reading of the regeneration audit was not one: Claude Code, run as a subprocess in a directory where the original
mandelbrot.onusstill lay on disk, returned the three bodies byte-identical to the originals, variable names included (docs/BENCHMARK.md). The loop had elided the bodies only in memory. Nowaskwrites the working texts to their files before every model call — a model with a file system reads exactly what the prompt shows, elided bodies included — andconcludewrites the texts the run started from back whenever it does not open a change (blocked, error, or--no-write), which is what “restoring the working tree when blocked” meant all along. The scripted-model cases pin that a blocked run leaves the file as found and an opened change writes it. It was not enough: with the elided tree on disk the subprocess read the repository’s own copy of the example instead (itsmaincame back with the viewport, the image size and the PGM header, which no interface states), which item 206 answers by restricting the subprocess’s tools. Review artefacts under.onus/changes/205/: the interface diff isregengainingwrite_workingandrestore_tree; the ledger delta is empty. The suite under stage2: 494 passed, 3 skipped. Fixed point reached frombootstrap/, native stage agrees; promoted.The subprocess model reads nothing but the prompt (loop spec §9). Item 205 was necessary and not sufficient: Claude Code as a subprocess ran in the repository, where its read tools found the example’s original in
examples/. Tried directly: in print mode Claude Code refuses to read outside its working directory, and--disallowedToolsremoves the tools altogether (--tools ""makes it write tool calls as text instead). SoClaudeCodecarries asandbox,<root>/.onus/model-sandbox, an empty directorymodel_from_speccreates, andgeneratechanges into it beforeexec claudewithRead,Glob,Grep,Bash,Task,WebFetch,WebSearch,Edit,WriteandNotebookEditdisallowed; the prompt on stdin is all it has. The API models never had a file system. With it the first honest reading was taken (docs/BENCHMARK.md): Claude Code reconstructed mandelbrot green in two answers,renderthe same computation refactored andmaina different program — an ASCII rendering to a text file where the original writes an 800×600 PGM — with no finding, since nothing inmain’s interface states what it writes; loop spec §8’s third finding, “green but different”, is the one to build next. Review artefacts under.onus/changes/206/: the interface diff isregen.Model’sClaudeCodevariant gainingsandbox(breaking); the ledger delta is one index refinement proved more. The suite under stage2: 494 passed, 3 skipped. Fixed point reached frombootstrap/, native stage agrees; promoted.The finding “green but different” (loop spec §8; spec §11.1). Loop spec §8’s third finding was not reported in v0 (item 115), and the first honest regeneration showed why it is the one that matters: mandelbrot’s
maincame back as a different program with a green ledger. Now, when a regeneration checks clean, the audit compares each target’s body with the one it replaced — the functions it calls (every resolved reference inside the body that names a function), the effects it uses (the effects pass’s inference) and the literal constants it writes (as the printer prints them) — and a difference is the findinggreen_but_different, naming the function and what was added or removed, proposed asadd_example. The comparison lives ininterfaceascallees_of,effects_usedandconstants_of, and the interface document’s function items carrycalleesso the interface diff reports callees added and removed (compatible, never breaking) and the review page’s diff view shows them for any loop change. Fixture:zone_promote_audit_differentintest/cli, the scripted model regeneratingsvc.billaspayments.charge(pence: x) + payments.charge(pence: x)— green, and no longer callingcore.math.double— which the promotion record reports as one finding. With it the reading of docs/BENCHMARK.md was taken again:mainis flagged for the callees and constants that changed,escape_countfor one literal a refactor no longer writes — the noise a constants comparison carries, and the decisive signal onmain. Review artefacts under.onus/changes/207/: the interface diffs areregengainingdifferent_findings,target_finding,def_named,shape_differenceandpush_differencewithaudittaking the targets,interfacegaining the twelve callee, constant and effect helpers, andidiff.ItemChangegainingcallees_addedandcallees_removed(breaking, as a record field is); the ledger delta isinterface61 → 67 andregen219 → 223 (ten checked obligations added: the helpers’ index arithmetic), nothing regressed. The suite under stage2: 495 passed, 3 skipped; every pinned interface document gainscallees. Fixed point reached frombootstrap/, native stage agrees; promoted.Views and the DOM capability (M17; spec §22; docs/CHANGE-LOG-04.md).
std/dom.onus: the capabilityRootgrantingdom.read,dom.writeanddom.event, andsubtree;std/view.onus: the immutable treeView[Msg](Element,Text,Keyed) andAttr[Msg](Attribute,Oncarrying a message,Property) over the closed vocabulariesTag,AttrName,PropNameandEventKind, the constructors, the spellings,descendants, the predicates (is_element,has_handler,attribute_of,has_label,is_interactive), the accessibility conditions (buttons_have_handlers,inputs_have_labels,images_have_alt,no_nested_interactive, togetheraccessible) andclaim accessible; the intrinsicspatchandrun, both claiminghost.js. The JavaScript runtime’sdom.tsimplements them againstglobalThis.document— a browser’s, or a fake a test supplies:patchdiffs two trees and applies the minimal mutations (keyed children by key, unkeyed positionally, attributes, properties and listeners reconciled),runrenders and on every message updates and re-renders, installed on the host’s loop and returning at once.dom.Rootis a root capability ofmain(§8.3) on the JavaScript target, supplied as the document’s body; the native emitter refuses amainthat takes one withE0800. Three deviations from the change log, each marked in §22: the element’s field iselement, nottag, since a union’s discriminant istagin the JavaScript representation;runhas nodivergeeffect, since it installs the loop and returns; and the accessibility properties use predicate functions rather thanv is Element(tag: Button, ..), since patterns with field values are not in the grammar (deferred). The variantInputofEventKindisEditedandOptionofTagisOptionItem, since a module’s variants share one namespace. The milestone is M17 in the implementation spec (the change log said M15). Fixtures:test/paths/e0413_dom_write(a view function that patches the document itself, underforbid { dom.write }) andtest/paths/ok_view_pure(a path from a view function forbidding everydomeffect);test/stdlib/view_ops(the spellings, the predicates, the conditions anddescendantsas examples, keyed lists accessible as a property);test/native/e0800_dom_root; the counter intest/examplesand, intest/codegen, run from the JavaScript target against the fake documentfake_dom.mjs(viafake_dom.sh), the tree after each click pinned incounter_dom.out. Review artefacts under.onus/changes/208/: interface diffs ofcapabilities,lowerir,native(and, unchanged,ir,build,nativebuild,jsemit,codes),ledger.md; no obligation ofself/changed status. Fixed point reached frombootstrap/, native stage agrees; promoted.std.viewfor documents: the vocabulary,to_html, nostyleattribute (spec §22.2, §22.6). Preparing the review page to be a view (docs/CHANGE-LOG-04.md, M10):TaggainsH4,H5,Article,Details,Summary,Em,Strong,Small,Brand the inline SVG subsetSvg,G,Rect,Path,Line,Circle,SvgText;AttrNamegainsOpen,Hidden,Colspan, the geometryWidth,Height,X,Y,Rx,D,Transform,ViewBoxand the hooksDataId,DataModule,DataItem,DataView,DataStatus;EventKindgainsToggle; the constructorpropjoinsattrandon.is_svg,is_voidandis_booleandescribe the vocabulary;std.html(escape,to_html) writes a tree as HTML text (handlers unwritten, boolean attributes bare, void elements unclosed, a depth beyond 64 cut), so the function that describes a live view also pre-renders a static page. Thestyleattribute of item 208 is removed: §22.6 lists inline styles as strings among what is absent, and nothing used it. The runtime creates the SVG elements in their namespace when the document offerscreateElementNS; the fake document of the counter fixture offers it. Fixtures:test/stdlib/view_ops(the new spellings,to_htmlover nesting, escaping, a keyed child, a boolean attribute, a void element and a handler). No compiler module changed; review artefacts under.onus/changes/209/: the interface documents ofstd.viewandstd.htmlafter the change andREADME.mdlisting the delta, sincechange-review.mjscoversself/only. Fixed point reached frombootstrap/, native stage agrees; promoted.The workbench in Onus (M10 redefined; spec §15, §22; docs/CHANGE-LOG-04.md).
self/workbench.onusis the review page as a program againststd.view:State(the review bundle, the view shown, the ledger filter, the bodies opened), the closedMsg(Show,Filter,Toggled),update, andpage, a pure function of the state returning the tree — the graph layout and every view of item 86 ported from text toViewvalues, one function per part.main(args, root)parses the bundle from its argument and hands the three toview.run.self/review.onuskeeps the data assembly and writes the shell: the stylesheet, the page pre-rendered throughstd.html.to_htmlfrom the same function, the bundle as a JSON script, and a module script that bootsmain; the hand-written view switcher, filter and counter are gone.scripts/bundle.mjsbuilds the workbench with stage0 and carries its JavaScript underworkbench/;onus reviewwrites it beside the page with the runtime, so the directory is the live page. Theviewparameter ofstd.view.runadmitsmutate(§22.3): a tree of any size is built with builders. The module is in the draft zone (no manifest names it). Fixtures: the review cases oftest/clire-pinned from the view with the program’s files expected, andworkbench_dom(ascriptcase): the live page under the fake document switches views, filters the ledger and counts an opened body. Review artefacts under.onus/changes/210/: the interface diffs ofreview(the renderers removed,render_pagekept) andcli(unchanged surface), the interface of the newworkbench,ledger.md; no obligation ofself/changed status, and every obligation ofworkbenchis proved or checked under stage0. Fixed point reached frombootstrap/, native stage agrees; promoted.
Deferred, not changed
Decided 2026-09-06, to apply in M15.5: generics compile natively by monomorphisation (impl spec §6.1). The rule it needs, polymorphic recursion as a checker error, is item 171 (applied the same day).
Noted 2026-09-06, not scheduled: definitional unfolding of proved-terminating functions in the verifier (impl spec §12), so that a shape invariant stated as a recursive predicate can be proved where a nested type would have enforced it.
Stream[T] ! eas a type (§3.11) is not parsed:-> Stream[T] ! eis ambiguous between the stream’s effect and the function’s. To be settled when streams land.Multi-binder quantifiers and tuple comparisons in §4.1 (
forall px: Int, py: Int where (px, py) != (x, y)) are not in the grammar; nested quantifiers (§5.3 allows depth two) express the same thing.budgetannotations (§12.3) andproves float(§3.2) have no syntax yet.Generated tests import
vitestandfast-checkby name and resolve them from the nearestnode_modules; a project outside this repository needs both installed.