Skip to content

Commit 92d7926

Browse files
committed
docs(adr): ADR-0177 — capability claims carry a gated status marker
Smell-audited: depth 3: new ADR. The smell was Reservation-as-bias in reverse — the ROADMAP asserted six capabilities in the present tense that build.zig cannot produce, and the previous commit fixed the numeric half of the same rot without touching the capability half. Devil's-advocate fork run with fresh context (mandatory at depth >= 2); its output is reproduced verbatim in Alternatives considered and it changed the outcome three times over the draft, so it is worth naming what it caught: 1. The draft scoped ROADMAP §1.1 and §8.1 only. The fork found the SAME false claim standing in §1.2 axis 1 ("v2 makes Wasm Component a first-class output"), §1.3, §8.3 and §10.3 — landing the draft as scoped would have marked §8.1 NOT IMPLEMENTED while the differentiation table two pages above still asserted the artifact exists. 2. The draft justified the amendment partly with Cloudflare's 3 MB gzipped limit against the 7,368,808-byte native binary. That compares two different artifacts — a wasm32 build would carry neither the native codegen paths nor necessarily the JIT, and would be gzipped. Dropped, rather than shipping a refuted premise for a future reader to find. 3. The draft would have restated a corrected LOC figure in an ungated sentence — re-arming the identical rot. The LOC target is retired in favour of the property it proxied (§1.2 axis 3), and the measured 124,712 appears once, dated, in the ADR. Its F-003 reading is the ADR's spine: retiring a false present-tense CLAIM is not the same act as DECIDING an unbuilt structural plan. So the claims go and the aim stays, owned by D-552. No scope changes here — F-010 (2026-05-29) and D-552 (2026-07-02) were both user-directed and already sequenced this. The mechanism, mirroring SIZE_EXEMPT_MARKER so it is one idea and not two: - `<!--claim:shipped-->` / `<!--claim:planned:D-NNN-->` inline markers. - `scripts/check_capability_claims.sh` (new, wired as a `run_all.sh` step): every planned marker must name a debt row that exists AND is still open — a planned claim whose owner was discharged is how "planned" quietly becomes "abandoned, still advertised" — plus a curated forbidden-phrase list that may not appear unmarked. Deliberately not a general prose scanner: a heuristic over a 1,600-line ROADMAP would false-positive until disabled. All three branches negative-tested (unmarked phrase, nonexistent owner, discharged owner). - `.dev/ROADMAP.md` joins SIZE_CLAIM_FILES; "9.5 MB at v1.3.1" appeared twice in it and neither was checked. handover trimmed 118 -> 96 lines (over the 100 cap): the v1.5.0 release detail duplicated CHANGELOG, and the stale `## Stopped` section belonged to the previous session per handover_framing.md.
1 parent e897cbf commit 92d7926

6 files changed

Lines changed: 503 additions & 90 deletions

File tree

‎.dev/ROADMAP.md‎

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,35 @@ per CLAUDE.md `§ When the active work unit completes`.
7171
and Wasm support, implemented in Zig 0.16.0.**
7272

7373
- **No JVM**: single static binary within the ADR-0172 per-component size
74-
budget (measured 9.5 MB at v1.3.1, derived ceiling ≈ 12 MB → ≈ 10 MB after
75-
queued levers; the pre-engine "≤ 5 MB" target was retired when the embedded
76-
Wasm JIT engine — ~3 MB of code by itself, F-001 — landed), cold start ≤ 10 ms
77-
- **Edge execution**: runs on Cloudflare Workers / Fastly / Fermyon Spin
78-
and other Wasm Component Model hosts
74+
budget (measured 7.37 MB at v1.5.1, enforced ceiling
75+
8,800,000 bytes; the pre-engine "≤ 5 MB" target <!--size:other--> was retired when the embedded
76+
Wasm JIT engine — roughly 3 MB of code by itself <!--size:other-->, F-001 — landed), cold start
77+
≤ 10 ms
78+
- **Wasm interop**: load a Wasm core module or component in-process and call it
79+
like a namespace <!--claim:shipped--> (§8.2, ADR-0135). Running *cljw itself*
80+
as a Wasm guest — the artifact that would let it run on Cloudflare Workers /
81+
Fastly / Fermyon Spin — remains an **aim, not a capability**
82+
<!--claim:planned:D-552-->; it is research-first and user-nod-gated, and no
83+
`wasm32` target exists in `build.zig` today.
7984
- **Language semantics compatible**: preserve Clojure JVM's *observable*
8085
behaviour. The Java interop surface (`.method`, `Class/`) is mapped onto
8186
v2's internal `Class` concept, not Java itself.
82-
- **Teachable**: shrink code volume to 30–40 % of v1 (89K LOC) and document
83-
the design decision behind every phase.
87+
- **Teachable**: every subsystem readable end-to-end, with the design decision
88+
behind each one written down. (The original "shrink to 30–40 % of v1 (89K
89+
LOC)" target is **retired**, not met: measured 124,712 LOC of Zig at v1.5.1,
90+
of which 18,534 is generated Unicode tables — ~119 % of v1 even excluding
91+
those. The redesign bought a Wasm engine, a full numeric tower, STM, and an
92+
AOT path that v1 did not have. A LOC count was always a proxy for
93+
readability; §1.2 axis 3 states the property directly, so the proxy goes.)
8494

8595
### 1.2 Differentiation (3 axes)
8696

87-
| # | Axis | Edge over the field |
88-
|---|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
89-
| 1 | **Edge-native Clojure** | Babashka is native but produces no Wasm. SCI is JS-only. v2 makes Wasm Component a first-class output. |
90-
| 2 | **Wasm-native interop** | `require` a Wasm **component** as a Clojure ns (finished form: **ADR-0135** + **Amendment 1**, 2026-06-21 user-directed — `(:require ["x.wasm" :as x])`, resolution order, type leverage, single-binary embed **ADR-0158**, always-latest **F-016**). **ACTIVE epic = D-404** (blocker dissolved; phases A ns-`:require` wiring → B resolution → C types → D single-binary → E resource/registry). Inversely, expose Clojure functions as WIT exports (later). |
91-
| 3 | **Comprehensible runtime** | Codebase is small enough to be read end-to-end. Each phase ships a written walkthrough. |
92-
| 4 | **Zig-level optimisation ceiling** | The *meta*-differentiator. Babashka rides GraalVM, clj rides the JVM: to make a hot path fast you must work *within* the host's object model / bytecode / GC. cljw is Zig from the metal up, so any primitive can be rewritten / fused / comptime-specialised (NaN-box, superinstructions, D-133 JIT) as a **Zig PR, not a host-engineering project** — a higher optimisation ceiling than any JVM-bound Clojure, provable per-workload. |
97+
| # | Axis | Edge over the field |
98+
|---|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
99+
| 1 | **Edge-native Clojure** | **AIM, NOT BUILT** <!--claim:planned:D-552-->. Babashka is native but produces no Wasm; SCI is JS-only; making a Wasm Component a first-class *output* of cljw would be the differentiator — and it does not exist. No `wasm32` target in `build.zig`; `cljw build` emits a native binary with a bytecode trailer. Owner: **D-552** (research-first, user-nod-gated). Sequenced behind axis 2 by F-010. Compiling *Clojure source* to a component is a sibling project's scope (ClojureWit), not a cljw plan. |
100+
| 2 | **Wasm-native interop** | `require` a Wasm **component** as a Clojure ns (finished form: **ADR-0135** + **Amendment 1**, 2026-06-21 user-directed — `(:require ["x.wasm" :as x])`, resolution order, type leverage, single-binary embed **ADR-0158**, always-latest **F-016**). **ACTIVE epic = D-404** (blocker dissolved; phases A ns-`:require` wiring → B resolution → C types → D single-binary → E resource/registry). Inversely, expose Clojure functions as WIT exports (later). |
101+
| 3 | **Comprehensible runtime** | Codebase is small enough to be read end-to-end. Each phase ships a written walkthrough. |
102+
| 4 | **Zig-level optimisation ceiling** | The *meta*-differentiator. Babashka rides GraalVM, clj rides the JVM: to make a hot path fast you must work *within* the host's object model / bytecode / GC. cljw is Zig from the metal up, so any primitive can be rewritten / fused / comptime-specialised (NaN-box, superinstructions, D-133 JIT) as a **Zig PR, not a host-engineering project** — a higher optimisation ceiling than any JVM-bound Clojure, provable per-workload. |
93103

94104
**Axis 2 is the north star** (ADR-0099 / **ADR-0135**): WebAssembly component as
95105
a first-class namespace — the analogue of clj↔Java / cljs↔JS / cljd↔Dart, but
@@ -109,9 +119,12 @@ discipline is to keep the hot paths clean while the compat grind adds slow-OK
109119
### 1.3 Intended users
110120

111121
- **Clojurians shipping to edge / serverless** who find Babashka / SCI
112-
short on concurrency or Wasm interop.
113-
- **Wasm-ecosystem users** who want to call a Clojure runtime as a
114-
component, or distribute a Clojure DSL as a Wasm component.
122+
short on concurrency or Wasm interop. (Today cljw serves them as a native
123+
binary with fast cold start; the Wasm-guest deployment path is D-552.)
124+
- **Wasm-ecosystem users** who want to pull a Wasm component into a Clojure
125+
program <!--claim:shipped-->. The inverse — calling a Clojure runtime *as* a
126+
component, or distributing a Clojure DSL as one — is **not built**
127+
<!--claim:planned:D-552-->.
115128
- **Learners of runtime implementation** who want design decisions and
116129
implementation in lockstep.
117130

@@ -942,13 +955,25 @@ comptime gate.
942955

943956
### 8.1 Adopted: hybrid
944957

945-
**Two artifacts from one source tree**:
946-
1. **Native CLI** (`cljw`): the usual binary for macOS / Linux x86_64 / aarch64.
947-
2. **Wasm Component** (`cljw.wasm`): Component-Model conformant; exports
948-
`clojure.eval` and friends via WIT.
949-
950-
The `Runtime` struct does not depend on the backend, so both fall out of
951-
the same `std.Io` abstraction.
958+
**One artifact ships; a second is an aim.**
959+
960+
1. **Native CLI** (`cljw`) — the usual binary for macOS / Linux x86_64 /
961+
aarch64. <!--claim:shipped-->
962+
2. **Wasm Component** (`cljw.wasm`) — Component-Model conformant, exporting
963+
`clojure.eval` and friends via WIT. **NOT IMPLEMENTED**
964+
<!--claim:planned:D-552-->. `build.zig` has no `wasm32` target, and
965+
`cljw build` emits a native binary with an embedded bytecode payload
966+
(`src/app/builder.zig`), not a `.wasm`. Owner: **D-552** — research-first,
967+
implementation user-nod-gated.
968+
969+
The hybrid remains the *intent*: the `Runtime` struct does not depend on the
970+
backend, so in principle both fall out of the same `std.Io` abstraction. What
971+
D-552 has to settle first is not the build flag but the architecture behind it
972+
— memory management and the GC/allocator story in linear memory, sandbox-first
973+
assumptions for the interpreter, and the browser / edge / Node host models.
974+
Note the standing tension recorded in §8.3: the NaN-boxed value representation
975+
and the mark-sweep heap live in linear memory, so such a build would not share
976+
objects with WasmGC-based languages.
952977

953978
### 8.2 Component as a first-class namespace (the finished form — ADR-0135)
954979

@@ -970,15 +995,17 @@ needed)** — lives in **ADR-0135**. Today's `(wasm/load)`+`(wasm/call handle
970995

971996
### 8.3 WIT / Component Model timeline
972997

973-
| Capability | Phase | Note |
974-
|---------------------------------------|---------------------------|----------------------------------------------------------------|
975-
| WASI 0.2 (preview2) | Phase 14 | Component build begins. Minimal exports. |
976-
| `wasm/load`+`wasm/call` core FFI | done (spike) | ADR-0099 minimal core-module surface; the layer below ADR-0135 |
977-
| **Component-as-namespace** (ADR-0135) | when zwasm CM API freezes | introspect component → ns; WIT↔clj mapping; D-404 |
978-
| Pod loader | Phase 14-15 | `app/pod.zig` |
979-
| WIT auto-binding | Phase 19 | adopt wit-bindgen or similar |
980-
| WASI 0.3 (concurrency) | Phase 19+ | when std.Io WASI backend stabilises |
981-
| WasmGC | v0.2+ | conflicts with NaN boxing; linear memory leads |
998+
Everything here is cljw **consuming** Wasm except the row marked otherwise.
999+
1000+
| Capability | Status | Note |
1001+
|---------------------------------------|---------------|---------------------------------------------------------------------------------------------------|
1002+
| `wasm/load`+`wasm/call` core FFI | **BUILT** | ADR-0099 minimal core-module surface; the layer below ADR-0135 <!--claim:shipped--> |
1003+
| **Component-as-namespace** (ADR-0135) | **BUILT** | introspect component → ns; WIT↔clj mapping. Param-marshalling tail = D-404 <!--claim:shipped--> |
1004+
| Pod loader | **BUILT** | `app/pod.zig` <!--claim:shipped--> |
1005+
| WIT auto-binding | not built | adopt wit-bindgen or similar; gap area II <!--claim:planned:D-404--> |
1006+
| WASI 0.3 (concurrency) | not built | when the `std.Io` WASI backend stabilises <!--claim:planned:D-036--> |
1007+
| **cljw itself as a component** | **not built** | the §8.1 artifact 2 — no `wasm32` target exists <!--claim:planned:D-552--> |
1008+
| WasmGC | not built | conflicts with NaN boxing; linear memory leads <!--claim:planned:D-552--> |
9821009

9831010
---
9841011

@@ -1569,21 +1596,27 @@ goes in just before Phase 4. Used during Phases 4-7.
15691596
15701597
### 10.3 v0.1.0 targets
15711598
1572-
| Bench | v0.1.0 target | Stretch |
1573-
|-------------------|---------------|---------|
1574-
| Cold start | < 12 ms | < 8 ms |
1575-
| Warm start | < 4 ms | < 2 ms |
1576-
| Binary size | < 3.5 MB | < 2 MB |
1577-
| fib_recursive | 24 ms | 18 ms |
1578-
| map_filter_reduce | 17 ms | 10 ms |
1579-
| transduce | 16 ms | 10 ms |
1580-
| lazy_chain | 16 ms | 10 ms |
1581-
| Idle memory | < 25 MB | < 15 MB |
1582-
| Wasm cold start | < 50 ms | < 20 ms |
1583-
1584-
The binary-size row is a v0.1.0-era figure that predates the embedded zwasm
1585-
JIT engine (~3 MB of code by itself); the live size budget is ADR-0172's
1586-
per-component ledger (measured 9.5 MB at v1.3.1).
1599+
| Bench | v0.1.0 target | Stretch | |
1600+
|-------------------|---------------|---------|-------------------|
1601+
| Cold start | < 12 ms | < 8 ms | |
1602+
| Warm start | < 4 ms | < 2 ms | |
1603+
| Binary size | superseded | — | |
1604+
| fib_recursive | 24 ms | 18 ms | |
1605+
| map_filter_reduce | 17 ms | 10 ms | |
1606+
| transduce | 16 ms | 10 ms | |
1607+
| lazy_chain | 16 ms | 10 ms | |
1608+
| Idle memory | < 25 MB | < 15 MB | <!--size:other--> |
1609+
1610+
The binary-size row was a v0.1.0-era figure that predates the embedded zwasm
1611+
JIT engine (roughly 3 MB of code by itself <!--size:other-->). It is **superseded** by ADR-0172's
1612+
per-component ledger; the live figure is 7.37 MB at v1.5.1
1613+
against an enforced ceiling of 8,800,000 bytes, checked on every full gate by
1614+
`scripts/binary_size_report.sh --check`. A target this table states and nothing
1615+
measures is the exact rot the 2026-08-04 audit found in three other documents.
1616+
1617+
The old **"Wasm cold start < 50 ms"** row is **removed**: it set a performance
1618+
target for the §8.1 artifact 2 (cljw running as a Wasm guest), which does not
1619+
exist <!--claim:planned:D-552-->. It belongs to D-552 and returns with it.
15871620
15881621
### 10.4 Fused reduce via structural metadata
15891622

0 commit comments

Comments
 (0)