Status of this document
The single authoritative plan for this project. It collapses the mission, principles, architecture, scope, phase plan, quality bar, and future decision points onto one page. The standard Claude Code rule applies: if anything elsewhere disagrees with this file, this file wins.
Detailed implementation discussions presuppose this document. Anything that contradicts it must go through an ADR (
.dev/decisions/); ad-hoc deviations are not allowed.History lives in git — see
git log -- .dev/ROADMAP.mdfor diffs,docs/ja/learn_clojurewasm/NNNN_*.mdfor the story behind each change, and.dev/decisions/for load-bearing decisions.
A cold-context AI (or new contributor) starting from /continue
hits this stack in order:
.claude/CLAUDE.md(every-turn auto-load) —§ Autonomous WorkflowStep 0→7 loop +§ The only stop(single condition: explicit user request).docs/architecture.md(5-minute orientation) — zones / backends / error / tier map. Pointers to all the documents below..dev/principle.md— working principles + Bad Smell catalogue + four depths of revision. Re-read at Step 1 / 4 / 6..dev/handover.md— current state, Active task, First task on resume. TheSessionStarthook auto-prints the Current state + Active task sections.- This file (
.dev/ROADMAP.md) — authoritative plan, with §15.0 listing all entry points if you need to scan further. .dev/decisions/NNNN_*.md— the ADRs (browse on demand, §15.1 ADR category index).
The autonomous TDD loop runs entirely from this stack — there are
no other "must-read" documents. The phase-queue model is retired
(ADR-0142); the loop self-selects the next gap-area unit / debt row
per CLAUDE.md § When the active work unit completes.
- Mission and differentiation
- Inviolable principles
- Scope: what we build, what we do not
- Architecture
- Directory layout (final form)
- Ecosystem compatibility: tier system
- Concurrency design
- Wasm / edge strategy
- Phase plan
- Performance and benchmarks
- Test strategy
- Commit discipline and work loop
- Forbidden actions (inviolable)
- Future go/no-go decision points
- References
- Glossary
- Amendment policy
A Clojure runtime that does not depend on the JVM, with first-class edge and Wasm support, implemented in Zig 0.16.0.
- No JVM: single static binary within the ADR-0172 per-component size budget (measured 8.14 MB at 2026-09-14, enforced ceiling 8,800,000 bytes; the pre-engine "≤ 5 MB" target was retired when the embedded Wasm JIT engine — roughly 3 MB of code by itself , F-001 — landed), cold start ≤ 10 ms
- Wasm interop: load a Wasm core module or component in-process and call it
like a namespace (§8.2, ADR-0135). Running cljw itself
as a Wasm guest — the artifact that would let it run on Cloudflare Workers /
Fastly / Fermyon Spin — remains an aim, not a capability
; it is research-first and user-nod-gated, and no
wasm32target exists inbuild.zigtoday. - Language semantics compatible: preserve Clojure JVM's observable
behaviour. The Java interop surface (
.method,Class/) is mapped onto v2's internalClassconcept, not Java itself. - Teachable: every subsystem readable end-to-end, with the design decision behind each one written down. (The original "shrink to 30–40 % of v1 (89K LOC)" target is retired, not met: measured 124,712 LOC of Zig at v1.5.1, of which 18,534 is generated Unicode tables — ~119 % of v1 even excluding those. The redesign bought a Wasm engine, a full numeric tower, STM, and an AOT path that v1 did not have. A LOC count was always a proxy for readability; §1.2 axis 3 states the property directly, so the proxy goes.)
| # | Axis | Edge over the field |
|---|---|---|
| 1 | Edge-native Clojure | AIM, NOT BUILT . 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. build.zig DOES have a wasm32 target since ADR-0193 (v1.11.0, -Dtarget=wasm32-wasi), but that produces a wasi program — a _start entry point a host executes — not a component that EXPORTS named functions a host can call. cljw build likewise emits a native binary with a bytecode trailer, and there is no library artifact at all: build.zig declares only addExecutable, and nothing in the tree is export fn. The verdict is unchanged; only the evidence for it moved. 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. |
| 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). |
| 3 | Comprehensible runtime | Codebase is small enough to be read end-to-end. Each phase ships a written walkthrough. |
| 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. |
Axis 2 is the north star (ADR-0099 / ADR-0135): WebAssembly component as
a first-class namespace — the analogue of clj↔Java / cljs↔JS / cljd↔Dart, but
cleaner. Among the four dialects only cljw's host binding is a language-neutral,
spec-defined, self-describing contract (no .wit sidecar; the component binary
embeds its own interface types), not a single vendor's runtime.
Compatibility is the adoption table-stakes, not a second-class chore. A
processor only gets used if real Clojure assets run on it; the down-and-dirty
"make existing libraries work" grind is first-class work, and discovering that
upstream core features were indirectly broken is itself valuable. Binary size
& speed are deliberately deferrable — the foundation is already small/fast and
tunable later by a concentrated comptime/JIT campaign (§10, D-133); the only
discipline is to keep the hot paths clean while the compat grind adds slow-OK
.clj wrappers over fast Zig primitives.
- Clojurians shipping to edge / serverless who find Babashka / SCI short on concurrency or Wasm interop. (Today cljw serves them as a native binary with fast cold start; the Wasm-guest deployment path is D-552.)
- Wasm-ecosystem users who want to pull a Wasm component into a Clojure program . The inverse — calling a Clojure runtime as a component, or distributing a Clojure DSL as one — is not built .
- Learners of runtime implementation who want design decisions and implementation in lockstep.
v0.1.0 is the first stable release. SemVer applies from v0.1.0 onward. Pre-v0.1.0 phases (1-20 in §9) are internal development; ROADMAP and ADR amendments may break behaviour. After v0.1.0:
- MAJOR: Tier A / B namespace removal, special-form removal,
binary-format change (e.g.,
cljw buildoutput). - MINOR: Tier C / D shift, new namespace, new special form, new builtin.
- PATCH: bug fix only, no behaviour change.
Tier D is permanent (per ADR-0013): vars and packages listed there will not be added in any MINOR / PATCH release. Adding a Tier D entry requires a MAJOR release and an amendment to ADR-0013.
The v0.1.0 deliverable specification (which subset of Tier A is the minimum success criterion) is finalized in Phase 11+; the SemVer rule above holds independently of that decision.
How the differentiation (§1.2) and compatibility are pursued in practice, so a fresh session re-recognises the direction, not just the next task. Three standing tracks run in parallel; none is a one-time phase.
-
Compat grind, made systematic + measured (not reactive). The 2026-06-12 finding: function-level triage (load a real lib, run its core functions, diff vs the
cljoracle) is strictly more productive than load-only triage — it founddata.json/pprintcore gaps that load-triage missed. So the grind gets a standing library-conformance harness (D-405): a corpus of "load lib → exercise its functions → diff vs clj", reported as a coverage number + worklist, so "what real Clojure code still does not run" is a measured, converging metric rather than an accidental discovery. Host-frontier gaps (clojure.lang.*markers, reflection,Compiler, JVM internals) are cleared big-bang (not drip-feed, the Micro-coverage-grind smell) and the language-feature ↔ Tier-D boundary is documented as it is hit (D-406): deftype implementing aclojure.lang.*interface = a language feature; reflection / bytecode generation = Tier D (cljw-native alternative). Drawing that line is what makes "Clojure assets run as-is" converge instead of being an unbounded treadmill. -
Differentiation as a standing proof, not a future campaign. The edges are already structurally present (§1.2) — the work is to prove them continuously, cheaply: (a) one "fast Zig primitive" benchmark showing a hot path (e.g. JSON parse / regex) beating the JVM because the runtime is rewritable — the axis-4 thesis made measurable; (b) one Wasm-component-FFI demo (ADR-0135) loading a real component and calling it as a namespace; (c) a one-line startup-ms + binary-size bench so "lightweight" is a number that also guards against bloat. Tracked by D-407.
-
Keep the foundation tunable for the later concentrated campaign. Size / speed are solved later by a focused comptime/JIT/superinstruction push (§10, D-133), not by daily perf work. The only standing discipline: compat wrappers may be slow
.clj(json options, pprint dispatch are the pattern — raw fast Zig primitive + thin slow wrapper), but the core primitives + the dual-backend hot paths stay fast and un-polluted (PERF marker + the TreeWalk≡VM diff oracle already protect this).
The Wasm-as-namespace finished form (track 2b, ADR-0135) is paced by zwasm's Component-Model embedding-API freeze (zwasm ADR-0170) — cljw drafts the design now (ADR-0135's mapping table) and implements when that API freezes (D-404).
These do not change between phases. Changing one requires an ADR.
| # | Principle | Effect |
|---|---|---|
| P1 | Move forward only with understanding | Continuous autonomous execution; understanding enforced per-commit by the gate (ADR-0107) + Step-6 smell-audit, not human pacing (rescoped, ADR-0168). |
| P2 | See the final shape on day 1 | Final directory layout fixed in §5. Adding a file ≠ adding a feature. |
| P3 | Core stays stable | The core, once built, stops changing. Extensions go to src/lang/clj/ (in-source libs) or pods. |
| P4 | No ad-hoc patches | Solve structurally. Ad-hoc fixes are escalated to ADRs or rejected. |
| P5 | Modular by build | Only the bytes you need land in the binary (modules + comptime flags + pods). |
| P6 | Error quality is non-negotiable | From day 1: file/ns/line/col/source-context/colour/stack trace. |
| P7 | Upstream fidelity is not a constraint | Practicality first. Compatibility differences are documented via tiers. |
| P8 | One cljw binary |
Single binary serves REPL / nREPL / eval / build / wasm-component-out. |
| P9 | One commit = one task | Structural change and behavioural change live in separate commits. Never commit when tests are red. |
| P10 | Honour Zig 0.16 idioms | std.Io DI, *std.Io.Writer, packed struct, comptime, @branchHint, etc. |
| P11 | Observable-semantics compatibility | Match what callers can observe; the inside of .toString is ours to choose. |
| P12 | Dual backend from Phase 4 onward | TreeWalk and VM agree on every test, verified by --compare (CI mandatory; ADR-0005, ADR-0022). |
The principles split into two tables. §2.1.a is the original cw v0 inheritance — structural invariants verified by a gate. §2.1.b is the Phase 4 entry batch — ADR-backed extensions, each row pointing at its source ADR.
| # | Principle | Verified by |
|---|---|---|
| A1 | Lower zones do not import upper zones | scripts/zone_check.sh --gate (CI) |
| A2 | New features go via new files, not edits to existing ones | ModuleDef + comptime flags + pods |
| A3 | Optimisation code lives in a dedicated subtree | src/eval/optimize/ only |
| A4 | GC is an isolated subsystem | runtime/gc/{arena, mark_sweep, roots}.zig |
| A5 | Tests mirror the source layout | test/ mirrors src/ |
| A6 | One file ≤ 1,000 lines (soft limit) | Avoids the cw v0 collections.zig (6K LOC) trap; hard cap 2,000 per ADR-0016 |
| A7 | Concurrency and errors are designed in on day 1 | Runtime handle + threadlocal binding + SourceLocation |
| A8 | Interop is a single deep module | lang/interop.zig routes through TypeDescriptor per ADR-0007 (Option β) |
| A9 | External modules go through a single ExternalModule interface |
comptime / .clj source / wasm pod loaded uniformly |
| # | Principle | ADR source |
|---|---|---|
| A10 | Dual-backend differential testing is the oracle | Evaluator.compare() CI mandatory; mismatch = build failure (ADR-0005) |
| A11 | Day-one enum reservation | SpecialFormTag / Opcode / ValueTag sized for phases 4-20 (ADR-0004, ADR-0012) |
| A12 | File size is a smell detector, not a metric | 1,000-line soft cap / 2,000-line hard cap, FILE-SIZE-EXEMPT marker (ADR-0016) |
| A13 | Debt ledger maintenance | .dev/debt.yaml row-level predicates; phase boundary audit per row |
| A14 | Structural discipline markers | FILE-SIZE-EXEMPT, SIBLING-PUB, SKIP-<reason> markers, grep-indexed |
| A15 | Error catalog as Single Source Of Truth | src/runtime/error_catalog.zig owns every user-facing message; returns ClojureWasmError (ADR-0018). Crash policy split across Layer 1 / 2 / 3 (ADR-0019) |
| A16 | Test taxonomy is 5-layer at Phase 4 entry | test/README.md lists Layer 1-5 with Phase activation (ADR-0021) |
| A17 | Differential testing is CI mandatory | test/diff/runner.zig + cases.yaml (ADR-0005, ADR-0022) |
| A18 | Source-scan gates share a framework | scripts/scan_lib.sh (ADR-0024); existing 4 check_*.sh adopt at Phase 5 |
| A19 | ARCHITECTURE.md is the 5-minute orientation | repo root ARCHITECTURE.md (ADR-0020) |
| A20 | run_step is the runner dispatch pattern | test/run_all.sh run_step + summary (ADR-0024) |
| A21 | Comptime conditional import + stub struct for phase staging | runtime/X/stub.zig parallels real X, build_options.phase_at_least_N switches (ADR-0023) |
| A22 | ADRs carry "Affected files" from 0020 onward | every new decision entry carries its affected files (ADR-0020) |
| A23 | Bad Smell sensor + four depths of revision | memory query :tags ["bad-smell"] + 20260911004145-29ffd52a (no ADR, meta layer above the rest) |
| A24 | Autonomous workflow is direct sequential | CLAUDE.md § Autonomous Workflow (cw v0-style, in-loop reflexive re-read of the smell corpus at Steps 1 / 4 / 6) |
| A25 | Existing code is mutable; rewrite is part of design | Skeleton activation (e.g. Phase 5 activates TypeDescriptor.lookupMethod against Phase 1-3 primitives) and ADR Supersedes chains rewrite already-written src/, not just add. Apply via the depth 1-4 recipe, memory 20260901165122-24182b0a. Phase N+ migration note section in the entry ADR narrates the rewrite scope; ROADMAP §A4 (isolated subsystem) and §A11 (day-1 enum) pin slot boundaries, everything between the slots is mutable. No "stays additive" default. |
| A26 | cw-v0 gap incorporation is plan-driven (not v0-copy) | When opening Phase 15+ or minting quality-loop rows, consult memory 20260911004837-4f399769 (redesign-first, never a v0 copy) and 20260911004838-1e222586 (which v0 gaps are still unincorporated, verified 2026-09-11) (user directive: incorporate most v0 gaps, but redesigned on cw v1's architecture with better design/ordering, never copied). Amend §9 in place per §17 referencing that doc. F-003 foresight — the owning phase decides the concrete design (Devil's-advocate at depth ≥ 2). Gaps with no current §9 row (deps/test/--list-vars/some namespaces) get NEW rows minted at incorporation. |
- The bulk of
clojure.core(~700 vars) clojure.string,clojure.set,clojure.walk,clojure.zip,clojure.ednclojure.test(deftest, is, are)clojure.pprint(basic)clojure.spec.alpha(core operators;fdef/instrumentstart at Tier C)clojure.tools.cliclojure.java.ioequivalent (Zig-native I/O backing, same names)clojure.java.shellequivalentclojure.data.json- Concurrency primitives: atom / agent / future / promise / delay / volatile / dynamic var
- Persistent collections: PersistentList / Vector (32-way trie + tail) / HashMap (HAMT) / HashSet
- Lazy seq + chunked seq + transducers (with fused reduce)
- Protocol / Multimethod / Record (Tier A)
- ExceptionInfo / try / catch / throw / finally
- Reader macros:
',`,~,~@,^,#(),#',#"re",#inst,#uuid - nREPL (CIDER-compatible, 14 ops)
deps.ednresolution (basic)- Wasm Component pod loading
- CLI (
cljw eval / repl / nrepl / build / component)
Authoritative list lives in compat_tiers.yaml tiers.D.excluded_*
(per ADR-0013). Summary at the ROADMAP level:
Excluded special forms (5):
gen-class— requires JVM Class system and bytecode emission.gen-interface— same.clojure.core/compile— JVM .class emission.proxywhen targeting deep Java extension (Apache HC base classes, Swing GUI base classes,java.util.logging.Formatter / Handler). Otherproxyuses go throughreifyagainst cw-native protocols.bean's reflection-deep variant. Basic field walk viaTypeDescriptorstays Tier C.
Excluded Java packages:
java.awt.*,javax.swing.*,java.applet.*(GUI, out of scope).- Deep reflection internals beyond what
TypeDescriptorexposes.
Other:
- AOT compilation (lein-aot equivalent).
- Dynamic Wasm generation in-runtime (security and complexity; Pod boundary covers the legitimate uses per ADR-0006).
Forms moved OUT of Tier D since the original ROADMAP:
reify,definterface,class?,supers,bases,bean(basic),(.method obj),import,new,set!,doto,..— now Tier A viaTypeDescriptor(ADR-0007) and unified.methoddispatch (ADR-0008).monitor-enter,monitor-exit,locking— now Tier A on heap values only (ADR-0009).- STM (
ref,dosync,alter,commute,ensure,ref-set) — now Tier A with full MVCC (ADR-0010); Phase 13-15 implementation.
- ClojureScript → JS compiler (v0.2.0 or later)
- RRB-Tree vector (only when vector slicing performance demands it)
- Generational GC (only after mark-sweep is stable)
- ARM64 / x86_64 JIT (gated by Phase 17 outcome)
- WasmGC backend (current line: linear memory + NaN boxing)
Layer 3: src/app/ CLI, REPL, nREPL, deps, builder
↓ may import anything below
Layer 2: src/lang/ Primitives, Interop, Bootstrap, NS Loader
↓ imports runtime/ + eval/
Layer 1: src/eval/ Reader, Analyzer, Compiler, VM, TreeWalk
↓ imports runtime/ only
Layer 0: src/runtime/ Value, Collections, GC, Env, Dispatch, Module
↑ imports nothing above
modules/ comptime-gated optional (math, c-ffi, wasm)
imports runtime/ + eval/ only
The optional modules landed under src/runtime/cljw/<area>/ rather than a
top-level modules/: wasm/ behind -Dwasm (ADR-0099) and ffi/ behind
-Dffi (ADR-0202, on by default for x86_64 / aarch64 Linux and macOS).
When a lower zone needs to call an upper zone: vtable pattern. The lower
zone declares the VTable type, the upper zone injects function pointers
at startup. scripts/zone_check.sh --gate blocks any violation in CI.
All values fit in u64 (8 bytes).
| top16 band | Kind | Payload |
|---|---|---|
< 0xFFF8 |
f64 raw | — |
0xFFF8 |
int48 | i48 |
0xFFF9 |
char21 | u21 Unicode codepoint |
0xFFFA |
const | nil(0) / true(1) / false(2) |
0xFFFB |
builtin_fn | 48-bit function pointer |
0xFFFC Group A |
heap (8 subtypes) | string / symbol / keyword / list / vector / array_map / hash_map / hash_set |
0xFFFD Group B |
heap | fn_val / multi_fn / protocol / protocol_fn / var_ref / ns / delay / regex |
0xFFFE Group C |
heap | lazy_seq / cons / chunked_cons / chunk_buffer / atom / agent / ref(*) / volatile |
0xFFFF Group D |
heap | transient_vector / transient_map / transient_set / reduced / ex_info / wasm_module / wasm_fn / class |
(*) The ref slot is reserved but STM is not implemented.
Heap addresses assume 8-byte alignment, shifted right by 3 bits → fits in 48 bits.
1:1 slot mapping (avoiding v1's slot-sharing + discriminant): type checks reduce to a bit comparison.
HeapHeader (extern struct):
pub const HeapHeader = extern struct {
tag: HeapTag, // u8
flags: packed struct(u8) {
marked: bool,
frozen: bool,
_pad: u6,
},
};Runtime is a process-wide singleton:
pub const Runtime = struct {
io: std.Io, // 0.16 IO hub
gpa: std.mem.Allocator, // infrastructure allocator
keywords: KeywordInterner, // owns its mutex
symbols: SymbolInterner, // Phase 3+
gc: ?*MarkSweepGc, // Phase 5+
interop: InterOp, // Phase 9
vtable: VTable, // backend dispatch
};VTable is a struct (not pub var) so tests can build a mock and
inject it.
std.Io is DI'd through every layer — no global variables.
std.Io.Mutex.lock(io) works because the caller has rt.io already.
threadlocal is reserved for Clojure dynamic vars (*ns*, *err*,
binding frames) only: pub threadlocal var current_frame: ?*BindingFrame.
- TreeWalk (
eval/backend/tree_walk.zig): reference implementation, exists from Phase 2. Simple, easy to debug. - VM (
eval/backend/vm.zig): stack machine, ~75 opcodes. From Phase 4. - Evaluator.compare() (Phase 8+): runs the same expression in both and asserts equal results. Critical for catching silent bugs.
CLI: cljw eval --tree-walk switches backends; default is VM after Phase 4.
cw v1 adopts Option β (ADR-0007): a Zig-native TypeDescriptor
plays the role JVM's Class plays for Clojure, without inheriting
JVM bytecode emission or the runtime reflection surface. Class-like
behaviour (class / instance? / type / deftype / defrecord
/ reify / definterface) routes through this descriptor.
TypeDescriptor (per ADR-0007) holds:
fqcn(interned symbol),kind(native | deftype | defrecord | reify_anon)field_layout(optional, fixed at analyzer time)protocol_impls,method_table(sorted slice + per-call-siteCallSitecache from Phase 7, per ADR-0008)parent(optional),meta
(.method obj args) and (ClassName. ...) and import / doto
/ .. all dispatch through method_table.lookup per ADR-0008.
Host-imported types (UUID / Date / File / …) register a
TypeDescriptor from src/runtime/java/<pkg>/<Class>.zig per
ADR-0029 (supersedes ADR-0011); from the user's perspective the
dispatch is identical to cw-native types. cljw-original surfaces
(wasm / build / edge / pod) follow the symmetric pattern under
src/runtime/cljw/<area>/<Item>.zig.
The Phase-4 skeletons for these structs land in tasks 4.17 / 4.18
/ 4.25; full behaviours activate in Phase 5 (deftype / defrecord
/ reify) and Phase 7 (protocol dispatch with CallSite cache).
pub const ExternalModule = struct {
name: []const u8,
load: *const fn(rt: *Runtime, env: *Env) anyerror!*Namespace,
kind: enum { comptime_zig, clj_source, wasm_component_pod },
};(require '[my-lib :as l]) resolves through this single interface.
- comptime_zig:
modules/<name>/module.zig(enabled by build flag) - clj_source:
lang/clj/<name>.cljvia@embedFile+ eval - wasm_component_pod:
(require '[my-pod :as p :pod "my.wasm"])loads a Wasm Component
Phase 1: arena GC (bulk free). End of Phase 5: mark-sweep GC + free-pool recycling.
src/runtime/gc/
├── arena.zig Arena GC (Phase 1)
├── mark_sweep.zig Mark-sweep + free pool (Phase 5)
└── roots.zig Root set definition + per-type mark walk
- Mark bit lives in
ObjectHeader.gc_and_locklow-30-bit range (per ADR-0009 / ADR-0017):u32 gc_and_lockpacked aslock_state: u2at bits 0-1 andgc_mark: u30at bits 2-31. suppress_count: u32blocks collection during macro expansion.--gc-stressruns collect on every allocation (test only).gc.collect(rt)takes*Runtimeand usesstd.Thread.Mutex(Zig 0.16's primitive; cf. JVM_TO_ZIG.md §3.3 / §8.2).- Allocator vtable callbacks do NOT take the mutex (per-thread arena or lock-free bump).
- Stop-the-world is acceptable at cw v1 scale (batch / REPL); the Phase 15+ re-evaluation considers generational GC if pause time becomes a problem (per ADR-0017 Consequences).
| Tier | Contents | GC? | Lifetime |
|---|---|---|---|
| GeneralPurposeAllocator | Env, Namespace, Var, HashMap backing | No | Process |
| ArenaAllocator (per-eval) | Reader Form, Analyzer Node, short-lived seq pipeline | No | Per-eval (bulk freed at REPL prompt return) |
| GcHeap (mark-sweep) | Runtime Values reachable beyond one eval | Yes | Mark-sweep |
Nodes are not Values, so the GC will not trace them — false-liveness is structurally avoided. Per-eval arena absorbs the majority of short-lived allocation; the mark-sweep heap holds only objects that outlive a single evaluation. See ADR-0017 for the rationale against generational GC at Phase 5 scale.
2026-05-24 amendment (ADR-0029): this section is the Phase-1-era snapshot and has partially drifted from current reality. The authoritative Phase-5-20 imagination tree lives in
.dev/structure_plan.md. Notable deltas codified by ADR-0029 + F-009:
runtime/host/is retired (was reserved by ADR-0011 with 13_placeholder.zigfiles). Replaced by:
runtime/java/<pkg>/<Class>.zig— Java-compat surface (thin wrappers, 1:1 with Java FQCN)runtime/cljw/<area>/<Item>.zig— cljw-original surface (Wasm, build, edge, pod, repl, …)- Neutral implementation layer lands flat under
src/runtime/:runtime/uuid.zig,runtime/clock.zig,runtime/file_io.zig,runtime/uri_parse.zig,runtime/path.zig,runtime/charset.zig,runtime/random.zig. Multi-file features go to sub-directories:runtime/regex/,runtime/crypto/,runtime/time/,runtime/error/,runtime/io/,runtime/wasm/.error.zig+error_catalog.zig+error_print.zigconsolidate intoruntime/error/{info, catalog, print}.zig.io_interface.zigmoves toruntime/io/interface.zig(runtime/io/default.ziglands later in Phase 5+).- F-009 (feature-implementation neutrality) is the invariant that governs which layer a new feature file goes into.
Per P2 (see the final shape on day 1), the full directory tree at the end of all phases is fixed below. Phase 1 stubs out the directories; later phases fill the contents without adding new directories.
ClojureWasm/ (working dir on disk)
├── src/
│ ├── runtime/ [Layer 0]
│ │ ├── runtime.zig Runtime handle (io, gpa, keywords, gc, interop, vtable)
│ │ ├── value.zig NaN-boxed Value type
│ │ ├── hash.zig Murmur3
│ │ ├── env.zig Namespace, Var, dynamic binding
│ │ ├── dispatch.zig VTable type
│ │ ├── error/ SourceLocation, BuiltinFn, helpers, catalog SSOT (per ADR-0029 consolidation; split into info/catalog/print.zig)
│ │ ├── keyword.zig KeywordInterner
│ │ ├── symbol.zig SymbolInterner
│ │ ├── module.zig ExternalModule interface
│ │ ├── gc/
│ │ │ ├── arena.zig
│ │ │ ├── mark_sweep.zig
│ │ │ └── roots.zig
│ │ └── collection/
│ │ ├── list.zig PersistentList + ArrayMap
│ │ ├── hamt.zig HAMT (HashMap, HashSet)
│ │ └── vector.zig PersistentVector (32-way trie + tail)
│ │
│ ├── eval/ [Layer 1]
│ │ ├── form.zig Form + SourceLocation
│ │ ├── tokenizer.zig
│ │ ├── reader.zig
│ │ ├── node.zig Node tagged union
│ │ ├── analyzer.zig
│ │ ├── macro_dispatch.zig Layer-1 macro Table + dispatch type
│ │ ├── backend/
│ │ │ ├── tree_walk.zig
│ │ │ ├── compiler.zig
│ │ │ ├── opcode.zig
│ │ │ ├── vm.zig
│ │ │ └── evaluator.zig dual backend + compare()
│ │ ├── cache/
│ │ │ ├── serialize.zig
│ │ │ └── generate.zig build-time cache
│ │ └── optimize/
│ │ ├── peephole.zig
│ │ ├── super_instruction.zig
│ │ ├── jit_arm64.zig (conditional)
│ │ └── jit_x86_64.zig (conditional)
│ │
│ ├── lang/ [Layer 2]
│ │ ├── primitive.zig registerAll entry
│ │ ├── primitive/ ~160 functions
│ │ │ ├── core.zig apply, type, identical?
│ │ │ ├── seq.zig first, rest, cons, seq, next
│ │ │ ├── coll.zig assoc, get, count, conj
│ │ │ ├── math.zig
│ │ │ ├── string.zig
│ │ │ ├── pred.zig
│ │ │ ├── io.zig
│ │ │ ├── meta.zig
│ │ │ ├── ns.zig
│ │ │ ├── atom.zig
│ │ │ ├── protocol.zig
│ │ │ ├── error.zig
│ │ │ ├── regex.zig
│ │ │ └── lazy.zig
│ │ ├── interop.zig InterOp deep module (§4.5)
│ │ ├── bootstrap.zig 7-stage bootstrap
│ │ ├── ns_loader.zig
│ │ ├── macro_transforms.zig Zig-level transforms (ns, defmacro, ...)
│ │ └── clj/
│ │ ├── clojure/
│ │ │ ├── core.clj ~600 defns (adapted from upstream)
│ │ │ ├── string.clj
│ │ │ ├── set.clj
│ │ │ ├── walk.clj
│ │ │ ├── zip.clj
│ │ │ ├── edn.clj
│ │ │ ├── test.clj
│ │ │ ├── pprint.clj
│ │ │ └── spec.clj
│ │ └── cljs/ (v0.2 onward)
│ │
│ ├── app/ [Layer 3]
│ │ ├── cli.zig
│ │ ├── runner.zig
│ │ ├── repl/
│ │ │ ├── repl.zig
│ │ │ ├── line_editor.zig
│ │ │ ├── nrepl.zig
│ │ │ └── bencode.zig
│ │ ├── builder.zig single binary + wasm component build
│ │ ├── deps.zig deps.edn
│ │ └── pod.zig Wasm Component pod loader (Phase 14+)
│ │
│ └── main.zig entry point (Juicy Main)
│
├── modules/ comptime-gated optional
│ ├── math/ clojure.math
│ ├── c_ffi/ landed as src/runtime/cljw/ffi/ (cljw.ffi, ADR-0202)
│ └── wasm/ cljw.wasm namespace
│
├── test/
│ ├── run_all.sh unified runner
│ ├── upstream/ upstream Clojure JVM tests (Tier A check)
│ ├── clj/ Clojure-level tests (clojure.test)
│ └── e2e/ CLI / error output / file exec
│
├── bench/
│ ├── bench.sh run / record / compare entry
│ ├── history.yaml baseline log
│ ├── compare.yaml cross-language snapshot
│ └── suite/NN_name/ meta.yaml + bench.clj
│
├── scripts/
│ ├── zone_check.sh
│ ├── coverage.sh vars.yaml coverage
│ ├── tier_check.sh compat_tiers.yaml validation
│ └── check_learning_doc.sh commit gate for docs/ja/learn_clojurewasm/
│
├── docs/
│ ├── README.md
│ └── ja/ Japanese textbooks (shidori)
│ ├── README.md top-level shidori
│ ├── learn_clojurewasm/ main: NNNN_<slug>.md chapter series
│ │ ├── README.md
│ │ └── NNNN_<slug>.md ...
│ └── learn_zig/ companion: Zig 0.16 reference + samples
│
├── .dev/
│ ├── README.md
│ ├── ROADMAP.md ← this document
│ └── decisions/ ADRs (NNNN_<slug>.md + 0000_template.md)
│
│ (created on demand; see §15.2)
│ ├── compat_tiers.yaml per-namespace tier (created at Phase 10)
│ ├── handover.md session-state memo (created when needed mid-task)
│ ├── debt.yaml row-level debt ledger (ADR-0072; replaced the planned known_issues.md)
│ ├── placement.yaml GENERATED per-var placement index (ADR-0178; supersedes status/vars.yaml)
│ ├── principle.md Bad Smell catalogue + revision depths
│ └── project_facts.md user-declared invariants (F-001…)
│
├── .claude/
│ ├── settings.json permissions, env, hooks
│ ├── rules/ auto-loaded path-matched rules
│ │ ├── zone_deps.md (loads on src/**/*.zig, build.zig)
│ │ ├── zig_tips.md (loads on src/**/*.zig, build.zig)
│ │ └── tier_classification.md (tier discipline; replaced the planned compat_tiers.md)
│ │ (… ~31 rules total; see `ls .claude/rules/`)
│ └── skills/code_learning_doc/ skill defining the docs/ja/ workflow
│
├── build.zig
├── build.zig.zon
├── flake.nix
├── .envrc
├── .gitignore
├── README.md
└── LICENSE
| Layer | Target (Zig) | Note |
|---|---|---|
| runtime/ | ~14 | |
| eval/ | ~12 + 4 optimize | |
| lang/ | ~21 (Zig) + ~13 .clj | |
| app/ | ~9 | |
| modules/ | ~7 (3 module entries + bodies) | |
| Zig total | ~64 | 47% smaller than v1 (120 files) |
Tier classification is data-driven. The authoritative data lives in
compat_tiers.yaml at the repository root. The narrative in §6.1-§6.4
documents the framework; the YAML carries the per-var / per-class
classification.
compat_tiers.yaml is read by:
- the test runner (
test/clj/for the Tier A 100% PASS gate), - the catalog's per-form
tier_d_<form>Codes (per ADR-0018 amendment 2) — each Tier D form has its own Code with a hand-written user-facing template, - the future
cljw --list-varsandcljw --list-host-classescommands (the latter planned by Phase 14, per ADR-0029 D5), - the G3 gate
scripts/check_feature_keyword.sh(per ADR-0029 D4) that validates thekeyword:field appears in every file path under each entry'sfiles:map.
host_classes entry schema (extended per ADR-0029 D5, 2026-05-24):
- fqn: java.util.UUID
cljw_ns: cljw.java.util.UUID # was: native_ns: cljw.host.* (host. prefix dropped)
keyword: uuid # validated by G3
tier: A
phase: 5
files: # validated by G3
surface: runtime/java/util/UUID.zig
impl: runtime/uuid.zig
impl_extras: [runtime/crypto/secure_random.zig]
wrap: runtime/collection/string.zig
clojure_peer: lang/primitive/uuid.zig
methods: [randomUUID, fromString, toString, ...]
clojure_peer_vars: [clojure.core/random-uuid]Entry migration to the extended schema is incremental — each
host class landing in Phase 6+ ships its entry in the new schema in
the same commit. The old {fqn, native_ns, phase} shape remains
readable during the transition (per Phase 6 entry plan).
Amendment process:
- Edit the YAML entry.
- Open or amend the rationale ADR (typically ADR-0013 for Tier D).
- If a Tier D promotion is involved, that is a MAJOR release per §1.4.
- Reference the ADR in the commit message.
| Tier | Meaning | Test requirement |
|---|---|---|
| A | Full semantic compatibility. Upstream tests pass as-is (or with cosmetic edits). | Upstream-ported tests must be green. |
| B | Same names and shapes; v2-native implementation. Observable behaviour matches. | Upstream-ported tests + ;; CLJW: annotations. |
| C | Best-effort with documented gaps. | Limited subset only; gaps noted. |
| D | Not provided. Throws UnsupportedException. |
Only the throw-message test. |
compat_tiers.yaml (repo root) is the source of truth:
clojure.core: A # complete by Phase 14
clojure.string: A
clojure.set: A
clojure.walk: A
clojure.zip: A
clojure.edn: A
clojure.test: A # Phase 11
clojure.pprint: B # cosmetic differences allowed
clojure.spec.alpha: B # core operators only; fdef/instrument start at C
clojure.core.async: C # go macro becomes a thread fallback (same as Babashka)
clojure.tools.cli: A
clojure.java.io: B # same names, Zig-native I/O backing
clojure.java.shell: B
clojure.data.json: A
java.lang.String: D # use Clojure string instead
java.util.Date: B # provided via #inst
java.io.File: B # via clojure.java.io
java.util.UUID: B
java.util.regex: A # Tier-A-compatible custom regex engineThird-party libraries live in the same yaml. Adding one requires an ADR (see §6.3).
- Stay at A: upstream parity is observable, removal would hit multiple callers.
- A → B (demotion): a behaviour is JVM-specific and the test needs annotation.
- C → B (promotion): gap is closed. ADR with evidence.
- D → C (promotion): at least one caller (test) works. ADR + partial implementation.
Each tier change is one ADR (.dev/decisions/NNNN_promote_X.md) recording
reason / tests / impact.
Do not write a branch in existing .clj/.zig to make a Tier-D library
work. Instead:
- Write an ADR to add it to the tier table (= official commitment), or
- Implement it as a Wasm Component pod (= outside the runtime).
This prevents if cljw then ... branches from sprawling. Physical fence
against ad-hoc rot.
Which clojure.lang.* / java.* surface is a language feature cljw must
provide vs Tier D = JVM implementation detail is decided by a 4-rule
procedure, run in order (first match wins). This is what makes "Clojure
assets run as-is" a finite, converging goal instead of a treadmill
(D-406; the rules were derived from the 2026-06-12/13 conformance
campaign's classifications).
- R1 — Artifact test → Tier D. Satisfying the name requires a Java artifact (joda-time, Jackson, …) or JVM machinery (bytecode emission, classloaders, reflection metadata). The refusal names the cljw-native path (pure-Clojure alternative or a future Wasm component, ADR-0135). Canonical example: bouncer → clj-time → joda. cljw never shims a Java library.
- R2 — Polymorphism-seam test → language feature. A
clojure.lang.*abstraction clojure.core itself dispatches on (deftype/reify/extend supertype position, or the class facet:instance?/isa?/extend-protocol/print-method). Coverage is definition-wide (F-013); closed set =host_interfaces.yaml(G4). - R3 — Value-semantics test → language feature. The observable
behaviour is a pure function of Clojure values (
Util/equiv,Murmur3, boxed-number statics,StringBuilder/StringWriter,Pattern/Matcher,UUID,Date/Instant, java.util collection-VIEW methods). Thin surface over a neutral impl (F-009); closed set =compat_tiers.yaml(G2/G3). Tiebreak: a name both value-semantic and arguably JVM-internal goes R3 iff its behaviour is clj-oracle-checkable (F-011), else R4. - R4 — Implementation-leak default → Tier D. Everything else
(
clojure.lang.Compiler/*, host-class reflection, JVM-internal statics). Default-deny: a name no rule claims is Tier D until an ADR amendment claims it.
Each closed set declares its admitting rule at the file level
(host_interfaces.yaml = R2; compat_tiers.yaml host_classes = R3); future
tier: D rows carry a per-row frontier_rule: R1|R4 (the refusal text
differs). Worked-example table: ADR-0136. The
upstream-corpus scan (does clojure/src/clj/** use the name?) is
non-normative evidence for R2/R3 judgements, never the rule.
| Clojure prim | pinned-Zig-0.16 mechanism (via io_default singleton where an io arg is unavailable) |
File | Phase |
|---|---|---|---|
| atom | std.atomic.Value CAS + validators + watches |
runtime/atom.zig |
B |
| ref / STM | MVCC LockingTransaction (per-ref std.Io.Mutex + Ref/TVal ring), retry-only (AD-013) |
runtime/stm/*, runtime/concurrency/lock_tx.zig |
B |
| agent | thread pool (std.Io.Threaded) + action queue (send / send-off / error-mode) |
runtime/agent.zig, runtime/concurrency/thread_pool.zig |
B |
| future / pmap | std.Thread.spawn (KEPT in 0.16) + std.Io.Condition result cell |
runtime/future.zig |
B |
| promise | std.Io.Mutex + std.Io.Condition |
runtime/promise.zig |
B |
| delay | single std.Io.Mutex + state machine |
runtime/delay.zig |
B |
| volatile! | @atomicLoad / @atomicStore |
runtime/volatile.zig |
B |
| binding | threadlocal current_frame + conveyor-fn frame clone on spawn (D-241) |
runtime/env.zig |
done + B conveyance |
| locking | heap-value std.Io.Mutex (ADR-0009 lock_state), heap values only — NOT a JVM monitor |
runtime/locking.zig |
B |
| GC thread-safety | global alloc std.Io.Mutex + ThreadGcContext root-publication handshake (no safepoint) |
runtime/gc/*, runtime/concurrency/gc_thread.zig |
B |
Redesigned by ADR-0090 (the Phase-B-entry §7 redesign, landed 2026-06-04). Corrected premise: the pinned Zig 0.16.0 (what cljw compiles against) KEEPS
std.Thread.spawn+std.atomic.Value; it moved the sync primitives tostd.Io.Mutex/Io.Condition/Io.Threaded, reached via a process-wideio_defaultsingleton (soAllocator.VTablecallbacks with noioin hand can still lock). cw v0 ships this on the same pinned compiler; cljw re-derives it clean (no_copy_from_v1), spike-validated this session. The GC gains a root-publication handshake (collection walks the union of all live threads'ThreadGcContextroot sets; the alloc/collect shared lock makes the install-window collection-free) for multi-thread mark-safety with no hot-loop safepoint — still single-gen mark-sweep (F-006; concurrent mark stays §89.2). Todayfuture/promise/delay/pmapstill run synchronously and the GC is single-threaded; ADR-0090 §1-7 is the build plan (rework-OK + per-commit gate, F-002). (std.Thread.{Mutex,Condition,Pool}andruntime/binding_stack.zigare gone — seezig_tips.md+ ADR-0090 Context; the OSS~/Documents/OSS/zigclone is post-0.16 master and the WRONG tree for 0.16 API questions.)
Status correction (ADR-0089): this section previously read "STM is implemented". That was false. As of 2026-06-04 only the
refshell exists;dosync/alter/commute/ensure/ref-setdo not resolve. The transaction machinery is Phase B work (the staging table below already assigned it to Phase 14/15). The design (MVCC per ADR-0010) is decided; the §7.1 Zig-mechanism mapping is pre-Zig-0.16 and is redesigned at Phase B entry (see §7.1 note).
ref / dosync / alter / commute / ensure / ref-set are designed
as Tier A per ADR-0010 (not yet built). The cw v1 charter explicitly aims
beyond Babashka's subset; STM is the signature Clojure concurrency
primitive and is included — landing in Phase B.
The implementation reproduces JVM LockingTransaction.java observable
semantics (internals free, F-011 §2): MVCC with TVal ring history per ref,
thread-local transaction context, retry loop with snapshot validation, ordered
locking by ref identity (deadlock-free). cljw is retry-only — JVM's barge
(younger-transaction preemption for starvation control) is dropped per
ADR-0090 (result-equivalent; only contention fairness/throughput differs;
AD-013 reserved — its accepted_divergences.yaml entry + pin land with the
Phase B concurrent STM test). MVCC is chosen for observable snapshot-semantics fidelity under the
F-010 real-library loop, not because the ring is already built (that would be
Reservation-as-bias; see ADR-0090 Alternatives).
Phase staging (the data-structure shells landed through Phase 13-14; the transaction engine + commit/retry is Phase B per ADR-0090):
- Done (≤ Phase 14):
Ref/TValdata structures +doGetshell. - Phase B:
doSet/doCommute/doEnsure+ commit + retry loop + commute fast path + the concurrent integration test (ADR-0090 §3, over theruntime/concurrency/lock_tx.zigengine; the GC root-publication handshake is a prerequisite and lands first).
*ns*, *err*, *print-length* and friends are implemented with
threadlocal binding frames. This is a Clojure-semantics requirement, not
incidental — abolishing threadlocal is not an option.
- Development / tests / default:
std.Io.Threaded(most stable). - Production (Linux): re-evaluate
std.Io.Evented(io_uring) at the end of Phase 15 (currently experimental). - Production (darwin): re-evaluate
std.Io.Evented(kqueue / GCD) likewise. wasm32-wasi: dedicated backend after WASI 0.3 stabilises.
build.zig accepts -Dio-backend=threaded|uring|kqueue|wasi as a
comptime gate.
One artifact ships; a second is an aim.
- Native CLI (
cljw) — the usual binary for macOS / Linux x86_64 / aarch64. - Wasm Component (
cljw.wasm) — Component-Model conformant, exportingclojure.evaland friends via WIT. NOT IMPLEMENTED . `build.zig` has no `wasm32` target, andcljw buildemits a native binary with an embedded bytecode payload (src/app/builder.zig), not a.wasm. Owner: D-552 — research-first, implementation user-nod-gated.
The hybrid remains the intent: the Runtime struct does not depend on the
backend, so in principle both fall out of the same std.Io abstraction. What
D-552 has to settle first is not the build flag but the architecture behind it
— memory management and the GC/allocator story in linear memory, sandbox-first
assumptions for the interpreter, and the browser / edge / Node host models.
Note the standing tension recorded in §8.3: the NaN-boxed value representation
and the mark-sweep heap live in linear memory, so such a build would not share
objects with WasmGC-based languages.
The north-star (§1.2 axis 2): require a Wasm component and call its
exports with Clojure data, the types negotiated by the Canonical ABI. The
load-bearing design — surface (deps.edn :cljw/wasm-deps + require;
load-component REPL hatch), the WIT ↔ Clojure value mapping table, and the
key property that a component binary is self-describing (no .wit sidecar
needed) — lives in ADR-0135. Today's (wasm/load)+(wasm/call handle "export") (ADR-0099) is the minimal core-module layer below it.
- WIT pod-invoke shape (escape hatch for un-portable Tier-C/D libs):
interface clojure-pod { invoke: func(name: string, args: list<value>) -> result<value>; }, load with(require '[my-lib :as lib :pod "my.wasm"]). Faster/safer/edge- compatible vs Babashka's subprocess pods. - Gating dependency: zwasm's Component-Model embedding-API freeze (zwasm
ADR-0170,
-Dcomponent— functional, not yet frozen). cljw drafts now (ADR-0135), implements on freeze (D-404).
Everything here is cljw consuming Wasm except the row marked otherwise.
| Capability | Status | Note |
|---|---|---|
wasm/load+wasm/call core FFI |
BUILT | ADR-0099 minimal core-module surface; the layer below ADR-0135 |
| Component-as-namespace (ADR-0135) | BUILT | introspect component → ns; WIT↔clj mapping. Param-marshalling tail = D-404 |
| Pod loader | BUILT | app/pod.zig |
| WIT auto-binding | not built | adopt wit-bindgen or similar; gap area II |
| WASI 0.3 (concurrency) | not built | when the std.Io WASI backend stabilises |
| cljw itself as a component | not built | the §8.1 artifact 2 — no wasm32 target exists |
| WasmGC | not built | conflicts with NaN boxing; linear memory leads |
Each phase has a goal and exit criteria. Phases marked 🔒 require an
x86_64 Gate: bash test/run_all.sh must pass on the cross-arch
Linux x86_64 verification host before the next phase begins. As of
ADR-0049 (2026-05-28) that host is ubuntunote (native x86_64 SSH
box) reached via bash scripts/run_remote_ubuntu.sh; the original
OrbStack my-ubuntu-amd64 path is retired (orphan / fan hazard).
Historical [x]-marked rows still reference OrbStack runs because
that was the gate at the time they closed — preserved as the
audit trail, not as a forward-looking instruction.
| Phase | Name | Exit criteria (summary) | Gate |
|---|---|---|---|
| 1 | Value + Reader + Error + Arena GC | Reads / prints (+ 1 2) as a Form |
🔒 |
| 2 | TreeWalk + Analyzer + Bootstrap Stage 0 | (let [x 1] (+ x 2)) → 3, ((fn* [x] (+ x 1)) 41) → 42 |
|
| 3 | defn + Bootstrap Stage 1-3 + ExceptionInfo | (defn f [x] (+ x 1)) (f 2) → 3; try/catch works |
|
| 4 | VM + Compiler + Opcodes | Every TreeWalk test passes on the VM too | 🔒 |
| 5 | Collections (HAMT, Vector) + Mark-Sweep GC | (get {:a 1} :a) → 1; large collections do not OOM |
🔒 |
| 6 | LazySeq + concat + higher-order foundations | (take 5 (iterate inc 0)) → (0 1 2 3 4) |
|
| 7 | map / filter / reduce / range + transducers base | Fused reduce produces zero intermediate seqs (target: v1's 391x) | |
| 8 | Evaluator.compare() + dual-backend verify | Every test agrees on TreeWalk and VM. bench/history.yaml initialised. |
🔒 |
| 9 | Modules layer + external standard libraries (clojure.edn / .data.json / .data.csv / .tools.cli) | All 4 external Tier-A namespaces ship; modules/ top-level dependency rule enforced; self-host re-verified |
|
| 10 | Namespaces + require + standard libraries (Tier A) | clojure.string / clojure.set etc. tests are green | |
| 11 | clojure.test framework + start porting upstream tests | deftest / is / are work; 10+ upstream tests ported | |
| 12 | Bytecode cache (serialize + cache_gen) | Cold start < 12 ms; cache format versioning established |
|
| 13 | VM optimisation: peephole.zig + STM Ref/TVal data structures (ADR-0010) |
Five canonical benchmarks within 110 % of v1 24C.10; Ref/TVal + read-only deref land |
|
| 14 | CLI + REPL + nREPL + deps.edn + Wasm Component build + v0.1.0 | cljw repl, cljw nrepl, cljw component build all work; compat_tiers.yaml complete |
🔒 |
| 15 | Concurrency — BUILT + race-hardened (gap area I; ADR-0142) | atom/ref+STM/agent/future/promise/delay/locking/volatile ship; gap = parity/load (R1) + D-442/D-105 (:volatile-mutable happens-before DONE 2026-06-16: ADR-0152) |
🔒 |
| 16 | Wasm/edge-native BUILT (gap area II) · ClojureScript→JS = future | component build/run/require ship (cljw.wasm/*); CLJS→JS genuinely unbuilt |
|
| 17 | VM perf: fusion → JIT PARTIAL (gap area III) | superinstruction/fusion slice landed (D-386 / O-018/019/021/023); narrow ARM64 JIT = milestone M | |
| 18 | math + module/deps DONE · C FFI BUILT (register-only shape) | clojure.math + deps.edn ship; cljw.ffi dlopen + 6-int/8-double call shape, no libffi (ADR-0202); structs/varargs/callbacks unbuilt |
|
| 19 | Wasm/edge-native cont. — WIT auto-binding (gap area II) | component require BUILT; gap = WIT marshalling (D-404); command/handle embedding shape settled by ADR-0124/D-350 | |
| 20 | broad JIT = future (distal; gated on gap-area-III fusion outcome) | narrow ARM64 JIT (milestone M) is the near-term scope; broad JIT decided after fusion lands |
The project is near-complete (R2 accurate-position survey,
private/notes/p14-r2-accurate-position-survey.md). Phases 9-13 are DONE,
Phase 14 is done modulo the v0.1.0 tag, and the old "Phase 15-20 = future
work, expand at entry" framing is retired: most of that work is BUILT.
What remains is organized as three completion-grade gap areas + a small
genuinely-future bucket — not a forward phase queue. Old phase NUMBERS are
preserved only as stable section anchors for existing citations (F-015 cl.4 —
numbering is an input, not a constraint); the gap area is the real unit.
Phase-number → gap-area redirect (so existing "Phase N" / §9.N citations in ADRs / debt rows / overlays still resolve while R4/R5 rewrite them at source):
| Former phase / § | Gap area / status |
|---|---|
| Phase 9-13 (§9.11-9.15) | DONE (Phase 12 = DONE, not "DONE-PARTIAL") |
| Phase 14 (§9.16) | DONE modulo the v0.1.0 tag — release mechanics (tag + version reconcile) |
| Phase 15 (§9.17) | Gap area I — Concurrency hardening (BUILT + race-hardened) |
| Phase 16 (§9.18) | Gap area II — Wasm/edge-native (BUILT) · ClojureScript→JS = future bucket |
| Phase 17 (§9.19) | Gap area III — VM perf: fusion → JIT (PARTIAL) |
| Phase 18 (§9.20) | math + module/deps DONE · C FFI BUILT (cljw.ffi, ADR-0202) |
| Phase 19 (§9.21) | Gap area II — Wasm/edge-native (WIT auto-binding; D-404) |
| Phase 20 (§9.22) | future bucket — broad JIT (distal; narrow ARM64 JIT = milestone M) |
Gap areas (the live hardening fronts):
- (I) Concurrency hardening — atom/ref+STM(MVCC)/agent/future/promise/delay/
locking/volatile/pmap all ship + race-hardened (
phase16_concurrency_stress.shcaught+fixed real races). Gaps: clj-parity/load (Track R R1 — done this session: agent ctor options/D-441, await-for, swap-vals!/reset-vals!, io!),future-cancel/seque/legacy-agent surface (D-442), java.time trio (D-105). (:volatile-mutablecross-thread happens-before DONE 2026-06-16 — ADR-0152 / D-444; un-accepted AD-018, the residual IRIW gap = AD-034.) Hardening defers (gated, engine correct without them): D-244 #4a' auto-collect, D-245 Option C blocking monitor. - (II) Wasm / edge-native —
cljw.wasm/*component build/run/require BUILT (src/runtime/cljw/wasm/, embeds zwasm v2 per F-001). Gap: WIT param marshalling (D-404). The core command/handle shape and I/O responsibility split are settled (ADR-0124 / D-350 / D-039); D-036 retains only JIT coordination. - (III) VM perf (fusion → JIT) — arith/compare-branch superinstructions + reduce/lazy-seq fusion landed (D-386 / O-018/019/021/023). Gaps: remaining fusion surface; narrow ARM64 JIT (F-010 milestone M) → broad JIT go/no-go (distal, D-005 / D-035).
Genuinely-future bucket (no impl; honest future): ClojureScript→JS compiler,
broad JIT (Phase 20). C FFI left this bucket with cljw.ffi (ADR-0202); its
own future tail is structs by value, varargs and C-to-Clojure callbacks.
Milestone M (F-010) = concurrency complete (gap area I drained to parity)
- the cw-v0-level narrow ARM64 JIT. M is a named milestone token, not a phase number — F-010's gate references it by name, not by "Phase 15".
The §9.17-9.22 sections below keep their numbered anchors but their bodies are
reframed to the gap-area role (stale stub-swap "Final activation step" /
phase_at_least_N framing dropped — those impls shipped). Sequencing
(ADR-0142): R4 (debt re-barrier) + R5 (instruction 大整理) run in the same
D-440 arc to rewrite "Phase N target" barriers + the phase-entry machinery to
gap-area terms; until then this redirect table is the bridge. The capability-
matrix successor model (drop phase numbers entirely) is forward debt D-443.
Phase 14 = first publishable v0.1.0. Minimum line for a Conj talk.
- CLI / REPL / nREPL working
- compat_tiers.yaml has Tier A/B declarations done
- Wasm Component output supported (even if minimal)
- bench/history.yaml has a locked baseline
Version-string reconcile (ADR-0142, 2026-06-15): the deliverables above all ship; the single remaining item is cutting the tag.
build.zig.zonis currently1.0.0-alpha.1(the working pre-release string), NOTv0.1.0. The "v0.1.0" label here + the v0.x ladder in §9.2 are the original milestone naming kept for history; the actual published version is whatever the tag-cut step stamps (user-owned). Treat1.0.0-alpha.1as the live version, "v0.1.0" as the milestone name, not a literal version assertion.
Re-sequenced by F-010 (2026-05-29) — see §9.2.R. The mapping below is the pre-re-cut plan, kept for history. The live post-v0.1.0 order is in §9.2.R.
- Phase 15-16 (Concurrency + CLJS): v0.2.0
- Phase 17-19 (super_instruction + module + advanced Wasm): v0.3.0
- Phase 20 (JIT): v0.4.0 or skipped
Restore note (ADR-0089, 2026-06-04): this section's intent — Phase 15 (concurrency) BEFORE the quality-elevation loop — was inverted in practice (the session ran the quality loop pre-Phase-15). ADR-0089 restores the order as Phase A (consolidation) → Phase B (KNOWN-unimplemented CORE, concurrency-led = this section's Phase 15) → Phase C (library-driven gap-hunt = the quality loop). The clean-bounded clj-parity frontier is drained, so the quality loop is re-cast as library-driven (Phase C) on a concurrency-capable base, not an open-ended
clojure.coregrind.
User-directed re-cut. Records the post-Phase-14 execution order + direction; granularity (exact renumber, sub-phase structure, floor-vs-superinstruction ordering) is deferred to each owning Phase entry per F-003 — this section seizes nothing, it records direction + foresight.
Priority refinement (user chat, 2026-06-05) — after the Phase B concurrency core + the complete #4a' GC-rooting landed:
- Docs / release-prep OUTRANKS the low-value concurrency tail. First land a single-sheet Clojure-vs-ClojureWasm differences doc (ClojureScript-style; debt D-249). The low-value tail (agent watches/ validator,
await-for,shutdown-agents, with-local-vars) is NOT skipped — implemented AFTER completeness rises; a tentative completion that leaves them as tracked tasks is fine first.- Phase C = library-driven gap-hunt FIRST, THEN Wasm / edge-native. Wasm COMPILATION itself is further off; before the release, integrate zwasm; the rest of the Wasm story is a post-release best-effort goal.
- GC completeness: the #4a' rooting is done; gate the production auto-collect-ON behind a GC torture mode validation (debt D-250)
- user-awareness. Heap-tag layout stays 64 slots (F-004 Revision 2026-06-05; 128 + region allocator = D-247, only when slots run out); the Group D wasm→tail reorg (D-248) is a ready, non-breaking cleanup. Detail:
private/notes/layout-gc-decisions-2026-06-05.md.
Milestone M = Phase 15 完遂 + a cw-v0-level narrow ARM64 JIT.
Phase 14 (v0.1.0)
→ Phase 15 concurrency (atom/STM/agent/future/promise/locking/pmap)
→ JIT chain superinstruction/fusion → go/no-go → cw-v0-level narrow
ARM64 JIT ───────────────────────── completes M
→ quality-elevation loop (repeatable standing mode, refactor-gated):
coverage→cw-v0-parity-PLUS · clojuredocs-example differential vs
JVM · clojure-corpus real-lib load · differential fuzzing ·
docs/works/ walkthrough ledger
→ wasm FFI breadth: ClojureScript (old Phase 16), zwasm v2 import
(old Phase 19), broad JIT if chosen (old Phase 20)
What this changes vs the §9.2 mapping:
- The JIT is pulled into the M window (immediately after Phase 15),
drawing on the existing §9.19 Phase 17 (
super_instruction.zig+ JIT go/no-go) + §9.22 Phase 20 (narrow ARM64 JIT) content. cw-v0-level = narrow, ARM64, integer-loop (NOT broad/optimising). - The quality-elevation loop is a new, repeatable standing mode inserted after M and before wasm FFI breadth. Its sub-phase structure is open (D-132, decide at the M-exit entry).
- wasm FFI breadth is de-prioritised, not cancelled. F-001 stays law (eventually unavoidable); the §9.18 Phase 16 (CLJS) + §9.19 wasm content + §9.22 zwasm import + their D-036..D-039 entry-debt cluster relocate intact to after the quality loop at their owning entry (header/pointer move only — no body renumber here, per F-003).
Recorded foresight (debt, NOT decided here):
- D-132 — quality-loop phase structure (count / numbering / ownership): open, decide at M-exit entry.
- D-133 — JIT coverage-floor prerequisite: the daily-driver
coverage floor (interop syntax
./new/set!;get-in/assoc-in/concat/mapcatD-126/127; true lazy-seq) must be green so the JIT lands on a runtime that runs real code (avoids cw v0's 1.0x-on-fib_recursivetrap). The ordering of this floor vs the superinstruction pass is the JIT-phase entry owner's call. - The D-035 JIT-vtable extraction + superinstruction module boundary remain §9.19's Entry debts — deferred to that owner.
Resources wired for the quality loop (.dev/reference_clones.md):
~/Documents/OSS/clojure-corpus/ (200+ libs) +
~/Documents/OSS/clojuredocs-export-edn/ (~1528 vars with :examples).
Walkthrough docs land under docs/works/ (code-主体 capability
ledger, explicitly outside F-007's dormant learn_clojurewasm
chapter cadence). Full grounding: private/notes/recut-goal-synthesis.md.
9.2.S Performance tuning campaign (PARKED into Phase B; ADR-0063, 2026-05-31; re-cut ADR-0089 2026-06-04)
ACTIVE sub-strategy (2026-06-24): collection-perf — ADR-0165 / D-520. The fastest-script campaign (ADR-0148) won the STARTUP axis (cold-start arc) and ratio_sum; cljw's VM beats babashka's SCI on pure compute (fib_loop/tak 2×). The remaining losses (sieve/destructure/gc_*/bigint, 1.02–1.16×) are the COLLECTION/library layer. Strategy: keep best-of-breed algorithms (cljw already has CHAMP/radix+tail/array-map/std.math.big; RRB rejected), win on Zig-native LAYOUT, transients-first. The JIT (D-386) is the separate bigger compute frontier. Full plan + experiment/regression protocol: ADR-0165.
Re-cut (ADR-0089, 2026-06-04): the "resume here" marker is stale — the session ran the F-010 quality loop, not this perf campaign. Under the A→B→C re-cut, perf (D-163 lazy-chain reduce, D-140 startup cache) folds into Phase B (it shares the concurrency/VM substrate) or runs as a parallel overlay there. It is no longer the standing "resume here" line — Phase A (consolidation) is.
User-directed pull-forward. The §9.2.R sequence parks perf (the
JIT/fusion chain) after Phase 15. But per-element interpreter overhead
(reduce/map/into/vec over large inputs, lazy-seq realisation,
cljw startup) became a dev-iteration bottleneck before then, so the
user pulled a perf campaign forward (2026-05-31): "あからさまに遅いと
数々のこれからのイテレーションのボトルネックになるので、 前倒しでもやって
おく価値があります … cw v0 なども参考に、 このプロジェクトの作り方で最適な
もので速度チューニング … ROI の高いものから自律的に判断して進め、 手戻りも
していい(コミットこまめにするとrevertしやすい)".
This is a repeatable, ROI-ordered, refactor-gated cluster (the F-010
discipline applied to perf): each unit is its own commit (revert-
friendly), held to F-002 finished-form + F-011 commonisation, gated
individually, and measured before/after. Every speed-for-simplicity
trade carries a // PERF: marker + a row in
.dev/optimizations.md (the SSOT, ADR-0063); the
naive form stays the behavioural contract (F-011 equivalence vs clj).
cw v0 (Meta.range / fusedReduce / incremental-trie transients) is
the precedent, re-derived cljw-appropriately (not copied; F-004 standalone
slots, not slot-cram).
Units, ROI-ordered (impact × frequency / effort·risk). Measured on mac-arm-m4pro, 0.48s startup baseline subtracted:
| Unit | What | Status | ROI note |
|---|---|---|---|
| O-001 | Compact .range value (O(1) count/nth, tight reduce, chunked seq) |
DONE 72d7bfcc |
(count (range 1e6)) ~118s → 0ms |
| O-002 | reduce over a vector index-walks (no seqFn→vectorToList eager N-cons) |
DONE 0898ba2c |
(reduce + (vec (range 1e6))) 182s → 123s (residual = build side, D-180) |
| O-003 | Bulk persistent! / vector.fromSlice — toPersistent builds the HAMT from the transient's flat buffer in O(n) (was N persistent conjs O(n log n)) + into/vec route editable targets through transients |
DONE (D-180) | (count (vec (range 1e6))) 121s → 2.4s; (reduce + (vec 1e6)) 123s → 2.5s. Boundary unit test (n ∈ {0,1,31,32,33,63,64,65,1023,1024,1025,1e5}) + diff oracle green |
| D-163 | Lazy-chain reduce perf (ADR-0065 re-laid: chunked seqs for the implicit (reduce f (map g coll)) path + existing transducers for the explicit 0-alloc opt-in — JVM finished form, NOT cw-v0 meta-fuse = F-011 second mechanism). First cycle (Alt C): chunked_cons reduceFn arm + .clj chunk-builder primitives + chunk-aware map/filter/keep/remove |
ACTIVE (ADR-0065) | (count (map inc (range 1e5))) = 42s ≈ 420µs/elem. Realistic ~4-7× (chunking kills the per-thunk tree-walk; per-element f vtable residual is D-133). Own ADR-0065 |
| D-140 | cljw startup bootstrap cache — re-parse+analyse+eval ~1000-line core.clj per invocation ≈ 0.48s |
after D-163 (architectural) | Highest dev-velocity ROI (every test/probe pays it; e2e suite's ~138s parallel block is dominated by it). Pre-analysed bootstrap cache à la ClojureScript |
Resume contract: O-001/O-002/O-003 (D-180) DONE. Start at D-163
(map/filter/take reduce-fusion — its own ADR; .range O-001 + the
chunked-cons seq are the substrate), then D-140 startup bootstrap cache.
Re-measure each before/after; record the win in optimizations.md.
This cluster runs ahead of the §9.2.R Phase-15/JIT sequence and does not
renumber it (F-003: §9.2.R's ordering is intact; this is a pulled-forward
overlay). Granularity (whether D-163/D-140 become numbered phases) defers
to their entry per F-003.
User-directed. After §9.2.S's perf campaign reached the boundary of its cheap
levers (O-051 the last clean one; the rest GC-arch / JIT-fenced — ADR-0165
Amendment 1 / D-520), the standing /continue mode shifts to polishing the
project as a public artifact (anyone clones it into their own environment).
Repeatable like the F-010 quality-loop floor — each /continue self-selects ONE
category and drains it EASIEST-FIRST (by tractability, not value — 2026-06-25
user decision; drain the whole list, niche/deferred included, to clear 残件; Step
0.5 quality_floor: drain). SSOT
= ADR-0166; drain rows D-522…D-529 (comment de-pointering + condensation /
doc audit vs code-truth / private/ decouple + per-task-note retire / rules +
skills review / java-interop static-member gaps / clj-parity upstream alignment /
real-deps.edn library usage / marker-comment inventory). Mostly no behaviour
change; code-touching rows (interop / parity / lib bugs) take the diff-oracle gate.
The §9.2.S perf levers stay in standing debt for a future deliberate decision.
Release mechanics for 1.0.0-rc.1 (ADR-0167, 2026-07-01, user-directed). Sibling
to ADR-0166: 0166 = internal quality (comments/docs/scaffolding/interop/parity);
0167 = the release mechanics an external clone needs (CI / community-health
files / CHANGELOG / third-party attribution / personal-environment decoupling /
version staging), gap-analysed against the zwasm v2.0.0-rc.1 series (S0…S7). The
rc.1 readiness gate is FINITE and explicit = Track A rows D-536…D-540 +
version staging D-541 + release-artifact workflow D-542 + dep-pin coherence
D-543 — this finite set is the SSOT for "can the user cut the tag?". Track B
(the ADR-0166 D-522…D-529 drain) is fully drained but does NOT gate the rc.1
signal (F-compliant: F-002 governs per-task quality, not inter-task order; a
release candidate cannot wait on the open-ended D-529 real-lib discovery). The
final 1.0.0-rc.1 bump + git tag/publish is USER-OWNED — the loop prepares
everything but never tags.
9.2.P clj-parity root-cause campaign (largely worked; folded into gap-area-I hardening per ADR-0142; ADR-0076, 2026-06-02)
User-directed. A periodic audit surfaced the user's concern that small
cljw↔clj mismatches "利用されるときの不信感につながりそう". The user directed
(2026-06-02): root-cause-resolve the real gaps ("A系は解消", big surgery
accepted per F-002), and record the intentional ones ("B…妥当な差異なので、
しっかり許容した、と記録…ルール化や自動防御"). This campaign is the A half; the
B half is the accepted-divergence framework (ADR-0076 §1 — SSOT
.dev/accepted_divergences.yaml + rule + gate).
Like §9.2.S this is a repeatable, refactor-gated cluster anchored on a
standing quality-loop floor: clj-parity debt Barrier (F-010), so it
cannot rot half-done and new sweep-found divergences drain through it. Each
unit is its own commit (revert-friendly), F-002/F-011-held, gated, and leaves
a corpus line behind (the pin — anti-D-177 mechanical re-check via
check_corpus_regression.sh).
Units (ADR-0076 table; the honest loop-vs-user split):
| Unit | Debt | Gap (clj-visible) | あるべき論 | Owner |
|---|---|---|---|---|
| C1 (lead) | D-164 | empty ()/seq → nil; (seq? '())→false |
interned distinct empty value in existing .list/seq tags (no new slot); uniform across every seq fn — highest leverage |
loop |
| C2 | D-205 | BigDecimal map-key (get {1.5M :v} 1.5M)→nil |
numeric arm in rt-free keyEqValue + scale-normalized hash |
loop |
| C3 | D-207 | .toString/.equals/.hashCode/.getClass |
dispatch-level Object fallback → str/=/hash/class (F-009) |
loop |
| C4 | D-209 | map-entry? + distinct MapEntry |
activate the reserved Group-A .map_entry slot (≠ F-004 amend) |
loop |
| C5 | D-198 | (Exception. "x") host-class ctors |
D-048 host-class machinery (dependency-ordered) | loop (after D-048) |
| C6 | D-200 | #inst/Date |
ship the no-slot cljw-native typed_instance Date; dedicated .date slot = user F-004 option |
loop + user (slot) |
| C7 | D-165 | long in (2^47, 2^63] -> BigInt ...N |
heap-boxed Long (B2 flag on heap-int; NO new slot, F-004 unchanged); stays Long to i64, BigInt past i64; overflow-promote = AD-008 | loop |
Resume contract: start at C1 (D-164) — one representation fix
clears every seq predicate (seq?/list?/empty?/= on ()), the single
highest-trust win. Then C2 → C3 → C4 → C6(no-slot Date) → C5(after D-048).
All 7 units are now loop-resolvable (user-decided 2026-06-02, ADR-0076
am1): C7 D-165 = heap-boxed Long (B2 flag, NO F-004 amendment; F-005 surface
already wants Long-to-i64); C6 #inst = no-slot typed_instance Date. The
Long-overflow-past-i64 promote-vs-throw is the accepted divergence AD-008
(cljw promotes per F-005; clj throws); the promoting prime-arithmetic family
is deferred D-211. This overlay runs ahead of §9.2.R and does not renumber it (F-003).
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
9.7 Phase 5 — task list (DONE; foundational — code landed; §9 tracker records Phase 5 DONE. Expanded rows flipped 2026-06-04 to match the tracker: GC/numeric-tower/TypeDescriptor/deftype/analyzer-split all present, and Phases 6-13 build on them.)
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
9.14 Phase 12 — task list (DONE; closed 2026-05-27; "DONE-PARTIAL" corrected to DONE per ADR-0142 — the bytecode cache is complete: AOT envelope embedded + interleaved per-chunk restore)
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
DONE; expanded task detail archived →
ROADMAP_archive_phases_1-13.md. §9 tracker +git log= SSOT.
9.16 Phase 14 — task list (DONE modulo the v0.1.0 tag; opened 2026-05-28, v0.1.0 milestone; ADR-0142 — CLI/REPL/nREPL/deps.edn/Wasm-component-build all ship; the single remaining item is the tag-cut + version-string reconcile, user-owned)
Resume wiring (read before picking a
[ ]task): the only two open rows — 14.12 (cljw component build, blocked-by zwasm CM readiness → D-036 / D-404 / ADR-0135) and 14.14 (exit-smoke + v0.1.0 tag, user-deferred — not cut yet) — are both BLOCKED. So/continuedoes not start them. The active work is the post-M quality-loop operating mode (F-010 / §1.5 3-track working strategy), handover + debt-driven (thequality-loop floor:rows + D-400 / D-404–D-407), not §9.16 row order. The handover Resume contract is the authoritative pointer; this note just keeps the §9 mechanism from misdirecting a fresh session to the two blocked rows.Phase 14 entry note: this is cljw's v0.1.0 milestone — the largest Phase by deliverable count. Rows 14.0-14.14 below are the entry-time first cut; row-internal cycles may subdivide as each deliverable's depth surfaces. The Phase closes only with the v0.1.0 release commit + tag (row 14.14). Phase 13 D-100 sub-deliverables that closed enumeration-only at Phase 12 fold back into this Phase as concrete rows (14.11 cluster).
Entry ADRs: 0015 (io_interface — REPL / nREPL wiring) ·
0021 (Test taxonomy — Conformance gate matures) · 0034 (cljw
build single-mode + Tier 0 metadata + EDN + decode — D-100
substantive cycles land here).
ADRs to issue at this entry: ADR-0048 (State machine
domain) — nREPL session / REPL prompt / build-pipeline state
charts. (Number minted at row 14.9 issuance per the time-ordered
allocation rule; the §9.17 placeholder's "ADR-0028" was a stale
reservation discarded per F-002 / Reservation-as-bias.)
Entry debts: D-014a numeric tower (BigDecimal Tier B,
JVM auto-promotion per F-005) · D-014b ex-info :type +
catch :type dispatch · D-066 CLJW_ERROR_FORMAT / _LOG
spec + man page (v5 §13.2 + §24.2) · D-079 ___HOST_EXTENSION
aggregator (prerequisite for D-097) · D-097 host stdlib
second wave · D-098 (ns …) directive surface · D-099
user-defined defmacro dispatch · D-100 Phase-12 substantive
deliverables (a)..(e) — bytecode/build/render-error/cold-start/
archive · D-102 Ref → TVal ring rewrite (Phase 14 doSet) ·
D-104 5-canonical workload-matched bench parity · D-036 /
D-037 / D-038 zwasm v2 integration (gates 14.12) ·
F-008 zwasm v2 spec review (project_facts invariant).
Reference: private/JVM_TO_ZIG.md §7 (future / promise /
delay) · v5 §11-§14 + §20.4 (CLI surface) · ADR-0015 amendment 2
(F140-F144 table).
Deliverables: CLI cljw repl + cljw nrepl + cljw build +
cljw render-error + cljw component build all work; future /
promise / delay; compat_tiers.yaml Tier A/B declarations done;
Wasm Component output (minimal); bench/history.yaml v0.1.0
locked baseline; host stdlib second + third waves; F140-F144
re-introduction per ADR-0015 a2; cljw-formats/0.1.0.edn archive
lock; CLJW_ERROR_FORMAT / CLJW_ERROR_LOG env var spec + man
page; cljw.error/with-context macro; v0.1.0 release. 🔒
ubuntunote gate (per ADR-0049; was OrbStack).
Final activation step: flip
build_options.phase_at_least_14 = true (per ADR-0023) at row
14.14. Per ADR-0015 a3 + a5, this is now a milestone marker
flip, not a stub swap: the io-stub-swap it nominally gated never
materialised (runtime/io/stub.zig was never written — runtime/io/
holds interface.zig Tier-1 only), and F142 nREPL (14.10) / F143 REPL
(14.9) / F144 cljw build (14.11) all landed ungated as
src/app/cli.zig dispatch arms. So the flip is build.zig false→true
- the
src/main.zigmanifest test, with zero behavioural consumer gates. F140/F141 (HTTP server/client) are out of v0.1.0 scope (no backing impl; not gated by this flag).
| # | Description | Status |
|---|---|---|
| 14.0 | Phase 13 → 14 boundary review chain follow-ups: handover refreshed (in this open commit); Step 0.5 debt sweep of stale-Phase Active rows (audit listed ~14 — D-008 / D-014a / D-014b / D-017 / D-022 / D-023 / D-024 / D-025 / D-026 / D-030 / D-033 / D-045 / D-048 / D-069 / D-070 / D-079); Opcode.isPositionRelative() extraction (parallel to isPurePush, simplify-arm [should]); peephole.zig defensive negative-offset + i16 overflow comments |
[x] (D-082 moved Discharged at 1d0b9d1; isPositionRelative + invariant comments at 17ed822; Step 0.5 sweep: D-008/D-017/D-026/D-030/D-069/D-070 Discharged + D-022/D-023/D-024/D-025/D-033/D-045/D-048 Opportunistic + D-014a/D-014b/D-079 promoted to Phase 14 rows) |
| 14.1 | D-079 discharge — ___HOST_EXTENSION aggregator wired in src/runtime/java/_host_api.zig::installAll(env) + inline for over each @import(...).___HOST_EXTENSION. Prerequisite for 14.2/14.3 host-class surface emission. ADR-0029 D5 schema completion |
[x] (installAll over 7 surfaces; Pattern.zig stale schema retrofitted; heap-copy on install reconciles rt.deinit ownership; 2 unit tests cover full set + idempotency) |
| 14.2 | D-097 discharge — host stdlib second wave: java.util.regex.Matcher, java.time.LocalDateTime, java.time.Duration, java.time.ZonedDateTime, java.math.BigDecimal (thin runtime/java/math/BigDecimal.zig wrapper over existing runtime/numeric/big_decimal.zig). Per ADR-0029 D5 |
[x] (5 TypeDescriptor reservations ship: Matcher / BigDecimal land with backing impls present; LocalDateTime / Duration / ZonedDateTime ship as TypeDescriptor-only — backing runtime/time/* impls deferred to D-105. java_surfaces[] extended; installAll registers all 12 surfaces) |
| 14.3 | Host stdlib third wave — java.net.Socket / java.security.MessageDigest + remaining Tier B host classes per compat_tiers.yaml. Network + crypto surface via cw-native impl per F-009 |
[x] (TypeDescriptor reservations: net/Socket.zig + security/MessageDigest.zig ship per ADR-0029 D5; backing runtime/net/ + runtime/crypto/ impls deferred to D-106. Remaining Tier B host classes — URL/URI/SecureRandom/etc — ride row 14.13 polish or focused cycles) |
| 14.4 | D-014a discharge — numeric tower completion: BigDecimal Tier B observable surface; JVM-shape auto-promotion ((* Long/MAX_VALUE 2) → BigInt; (/ 1 3) → Ratio; 1.5M → BigDecimal). F-005 internal = std.math.big.int.Managed + Ratio (BigInt × BigInt) simplified + BigDecimal (unscaled BigInt, i32 scale) |
[x] (3 gaps closed: BigDecimal toPlainString printer f9085e6, Ratio literal parser 8c9a258, Long-overflow→BigInt promotion at integerLiteralToValue. D-014a fully Discharged. Ratio×Int arithmetic bug noted as separate concern) |
| 14.5 | D-014b discharge — ex-info :type keyword + catch dispatch via :type (ADR-0007 / 0018). Tier A throw/catch completeness |
[x] (ba5b9d99: CatchTarget union landed; analyzer accepts keyword; TreeWalk catchMatches keyword arm; VM rides VM-DEFER per feature_deps.yaml#runtime/eval/catch_type_keyword; 5 e2e cases in test/e2e/phase14_catch_keyword.sh) |
| 14.6 | D-099 discharge — user-defined defmacro dispatch via rt.vtable.callFn (macro_dispatch.zig:107 referenced site). Unblocks clojure.test/deftest / clojure.test/are / clojure.test/testing / clojure.core/declare. Tier A test corpus matures |
[x] (32852436: defmacro analyzer arm + valueToForm adapter + expandIfMacro user-fn fallback; 5 e2e cases; &form/&env deferred as D-111) |
| 14.7 | D-098 discharge — (ns …) directive surface: :refer-clojure :exclude / :only, (:require [ns :as alias :refer […]]), (:rename {old new}). Extends analyzeNs (special_forms.zig:350-395) + env.referAll-with-filter. JVM-idiom .clj corpora become buildable |
[x] (4f2ff916: :exclude/:only filters + ns-level :require landed TreeWalk; VM rides VM-DEFER per feature_deps.yaml#runtime/vm/ns_filter_and_libspec; :rename split to D-112) |
| 14.8 | future / promise / delay — Phase 14 Tier A concurrent primitives. JVM idiom on a single-thread runtime: (deref (future …)) blocks synchronously; promise: (deliver p v) + (deref p); delay: lazy memoization. Per private/JVM_TO_ZIG.md §7. Concurrency activation rides Phase 15 |
[x] (3235f9ff: 3 heap types delay/promise/future + 5 primitives + 2 macro transforms + 9 e2e cases; Phase 15.1 swap path = D-113/114/115; future/error_value_channel PROVISIONAL marker) |
| 14.9 | ADR-0048 issuance + cljw repl — REPL line editor (F144 re-introduction) + state-machine ADR for REPL prompt / nREPL session / build-pipeline (next ADR id = 0048 per time-ordered allocation; the prior placeholder "ADR-0028" reservation discarded per Reservation-as-bias) |
[x] (208f33bd: ADR-0048 minted with REPL chart + nREPL/build placeholders; line-buffered REPL in src/app/repl.zig; 5 e2e cases; line editor polish = D-116) |
| 14.10 | cljw nrepl — F142 nREPL server re-introduction per ADR-0015 a2. State machine per ADR-0048 (14.9 issuance) |
[x] (76e0c64c: runtime/bencode/ neutral codec + src/app/nrepl.zig 4-op server; ADR-0015 a3 + ADR-0048 nREPL chart fill; e2e via Python bencode driver; multi-session/CIDER ops = D-117, stdout/stderr capture = D-118) |
| 14.11 | D-100 cluster discharge — Phase-12 substantive deliverables land in dedicated cycles here: (a) full BytecodeChunk coverage (constants pool serializer with NaN-box Value round-trip; call_sites + libspecs side-tables); (b) cljw build app.clj -o app CLI (src/app/builder.zig; Deno-style binary trailer; bootstrap cache build.zig integration); (c) cljw render-error decoder (src/app/render_error.zig + runtime/error/event.zig + render.zig TTY/pipe split); (d) cold-start bench < 12 ms verified; (e) cljw-formats/0.1.0.edn archive v0.1.0 lock |
[x] (D-100 cluster fully discharged: (a) @194eefaf BytecodeChunk serializer + (c) @a9045b10 render-error + (d) @ebf2979b cold-start; (b) cljw build — per-form eval @41092c86, fn_val serialization @19d6b8cc (ADR-0034 am2), EnvelopeIterator @8758a03b, CLI+self-exe trailer+startup-run @d5f0612e, F-009 setupCore @924992bb; (e) cljw-formats/0.1.0.edn @9aa4f6c1) |
| 14.11.5 | D-102 Ref→TVal history-ring rewrite (row-assigned per D-122 schedule fix; ADR-0010 amendment 4). Lands src/runtime/stm/tval.zig (new — TVal struct with val/point/msecs/prior/next doubly-linked self-loop ring); rewrites src/runtime/stm/ref.zig (Ref carries tvals: *TVal + min_history=0 + max_history=10 + lock: std.atomic.Mutex); names HeapTag Group D slot 63 as tval; GC trace verified via mark-bitmap cycle detection. Phase 15.1 transaction control flow (doGet/doSet/doCommute/doEnsure/commit/retry) lands on this unchanged Ref/TVal shape (D-114). Sequenced before D-100(a)/(b)/(e) is acceptable — no dependency either direction; landing 14.11.5 first closes a structural row that would otherwise sit silent at v0.1.0 tag |
[x] (540f5739) |
| 14.12 | cljw component build — Wasm Component output (minimal). Gated on zwasm v2 readiness (D-036 / D-037 / D-038 / F-008). May ship as PROVISIONAL via wasm-c-api veneer if zwasm v2 rewrite (ADR-0109) is incomplete at landing time |
[ ] |
| 14.13 | v0.1.0 polish bundle — compat_tiers.yaml Tier A/B declarations comprehensive review/finish; bench/history.yaml v0.1.0 lock-point entry per ADR-0044 schema; D-066 discharge (CLJW_ERROR_FORMAT / CLJW_ERROR_LOG env var spec + man page); cljw.error/with-context macro (v5 §13.6 user runtime error injection API) |
[x] done 2026-05-29 (bench 14A.0 3f5aa415; binding 15d7e21f; with-context c3aa601c; ADR-0055, ADR-0042 am1; compat + D-066 earlier) |
| 14.13.5 | lazy-seq Layer-2 (ADR-0054, ~4 cycles) — wire the lazy_seq PRODUCER (Phase-6 left consumer-only): lazy-seq macro + __lazy-seq-create primitive (delay/future triad), iterate, then convert the eager seq surface (map/filter/take/drop/keep/remove/concat/mapcat/map-indexed/keep-indexed) to lazy .clj deleting the -*-eager leaves, + infinite range/repeat/repeatedly/cycle/take-while/drop-while/partition. Cycle 1 folds in the load-bearing print.zig rt/env threading (force-to-print lazy, bounded by *print-length* — CORRECTION (ADR-0089, 2026-06-04): the *print-length* BOUND was NOT delivered here; deepRealize realized infinite seqs fully and hung. Fixed 2026-06-04 @0e3edad8 by bounding realizeSeqWalk to length+1 — D-222 b); cycle 3 the lazy-= force-walk. Chunking DEFERRED to a perf cycle. The last unmet Phase-6 exit criterion + D-133 coverage floor. Proof: (take 5 (iterate inc 0)) → (0 1 2 3 4); (first (map inc (range))) → 1. STATUS 2026-05-29: ALL 4 cycles DONE — cycle 1 (producer+iterate) @e2e99de8; cycle 2 (lazy map/filter/keep/remove + print realize) @e14d024c; cycle 3 (concat/mapcat/drop lazy + infinite 0-arg range + lazy = force-walk in equal.zig) + cycle 4 (repeat/repeatedly/cycle/take-while/drop-while/partition) landed across @8e0ce4db (cycle 3) + @3661d7f5 (cycle 4) — BOTH shipped red (range iterate forward-ref + the cycle-4 fns never in core.clj) — then @8f7a1e63 moved iterate above range but its commit message claimed the cycle-4 fns while the edit did NOT land, so the gate stayed red until @ec9ccfed actually added repeat/repeatedly/cycle/take-while/drop-while/partition + list (caught on resume by reading run_all.sh from the on-disk log). Lazy-seq Layer-2 COMPLETE; gate green. Row closed. |
[x] |
| 14.14 | Phase 14 exit smoke + v0.1.0 release + final activation — (a) exit-smoke 5+ cases covering repl/nrepl/build/render-error/component-build(error-path; row 14.12 zwasm-v2-gated)/future-promise-delay/host-stdlib; (b) flip build_options.phase_at_least_14 = true — milestone marker per ADR-0015 a5 (NOT a stub swap: runtime/io/stub.zig never existed; F142/F143/F144 landed ungated at 14.10/14.9/14.11; F140/F141 HTTP out of v0.1.0 scope), so the flip is build.zig + the src/main.zig manifest test only; (c) tag v0.1.0; flip §9.16 header DONE. 🔒 ubuntunote gate (per ADR-0049) |
[ ] |
Exit criterion: all v0.1.0 deliverables shipped + v0.1.0
tagged + phase_at_least_14 = true (milestone marker per
ADR-0015 a5). F142/F143/F144 landed ungated (rows 14.9-14.11);
F140/F141 (HTTP) are out of v0.1.0 scope. 🔒 ubuntunote
x86_64 gate passes (per ADR-0049; OrbStack retired).
§9.17-9.22 reframed to the §9.0 gap-area model (ADR-0142). These keep their numbered anchors for citation stability; the "PENDING, expand at entry" placeholders + the
phase_at_least_Nstub-swap "Final activation step" sentences are dropped because those impls SHIPPED. Each section now records BUILT status + the named gaps + the debt rows that drain them.
Status: BUILT. atom (swap!/swap-vals!/reset!/reset-vals!/CAS/
add-watch/remove-watch/set-validator!/get-validator) · volatile · ref+STM (full
MVCC dosync/alter/commute/ensure/ref-set, runtime/stm/ +
concurrency/lock_tx.zig) · agent (send/send-off/await/await-for/agent-error/
restart-agent/error-mode/error-handler/validator/meta) · future/promise/delay ·
locking (concurrency/object_monitor.zig) · reduced · pmap/pcalls/pvalues
(sequential, result-correct). Race-hardened: phase16_concurrency_stress.sh
caught+fixed real races (atom non-atomic swap, STM doGet stale read); corpus
test/diff/clj_corpus/concurrency.txt.
Gaps (the hardening front):
- clj-parity / load — Track R R1 (this session: agent ctor options/D-441, await-for, swap-vals!/reset-vals!, io! — corpus-locked).
- D-442 —
future-cancel/future-cancelled?/seque(infra-gated) + the low-value legacy/executor agent surface (classify implement/AD/stub). :volatile-mutablecross-thread happens-before — DONE 2026-06-16 (ADR-0152 / D-444): volatile fields route through@atomicLoad(.acquire)/@atomicStore(.release), distinct from the plain:unsynchronized-mutable. Un-accepted AD-018; the residual JMM IRIW cross-volatile total-order gap (acquire/release, not seq_cst) = AD-034.- D-105 — java.time trio (LocalDateTime/Duration/ZonedDateTime are skeletons; Instant + Date ship).
- Gated-defer (engine correct without them): D-244 #4a' auto-collect (higher-risk; collect is explicit/test-triggered today), D-245 Option C blocking monitor (Option A spinning ships; gated on a real contention workload).
Reference: private/JVM_TO_ZIG.md §4-6. Entry ADRs 0009 (object-header lock)
/ 0010 (STM MVCC) are LANDED (ADR-0090/0092/0093). 🔒.
Status: Wasm component BUILT. cljw.wasm/* (load/call/run/
component-exports/component-invoke/load-component/component-call/
require-component) over src/runtime/cljw/wasm/{engine,component,marshal, surface}.zig, embedding zwasm v2 (@import("zwasm"), F-001 — SHA-pinned dep,
interp-only). e2e phase16_wasm_{run,component,ffi,require_component}.sh (real,
build -Dwasm). Gated -Dwasm opt-in by design (lazy dep), NOT unimplemented.
Gap: WIT param-type marshalling raises feature_not_supported
(component.zig) for some types → D-404 (full WIT↔EDN table). The integration
finished form is settled: persistent core-library handles, one-shot captured WASI
commands, and persistent WASI components (ADR-0124 / D-350); cw Tier-1 I/O
and zwasm WASI remain separate (D-039). D-036 retains only JIT coordination.
Entry facts: F-001 (zwasm unavoidable) · F-004 · F-006 · F-008
(zwasm v2 spec review). Consult zwasm docs/zig_api_design.md +
docs/consuming_prerelease_zwasm.md (the SHA-pin procedure).
ClojureScript → JS compiler = genuinely-future bucket. No emit-js/cljs
backend exists (only #?(:clj/:cljs) reader-conditional handling). The original
.clj-source transpiler design (ADR-0037 candidate, D-068) stands as future work;
F-010 de-prioritizes CLJS. cljw user-code path and zwasm wasm path stay separate
(F-001).
Status: PARTIAL. A real fusion/superinstruction slice landed: local-const /
local-LOCAL arith fusion + compare-and-branch fusion (D-386 / O-018/019/021,
vm.zig + intrinsic.zig + opcode.zig) + reduce/lazy-seq fusion (O-023).
The named super_instruction.zig file does not exist; the work it represents is
partly done.
Gaps: remaining fusion surface; the narrow ARM64 JIT (~1000 LOC, hot-loop
pattern-match, cw v0 Phase 37.4 shape — arith_loop ~10x evidence) = F-010
milestone M near-term; broad JIT go/no-go is distal (gap area / §9.22),
decided after fusion lands. D-035 (extract a backend-shared callable dispatch layer — src/runtime/dispatch/callable.zig + src/eval/backend/jit/
subtree — before a 3rd backend joins) · D-005 (ARM64 JIT decision). If JIT go:
ADR-0022 amendment for 3-way differential (TreeWalk == VM == JIT). cljw's JIT is
an independent cljw-bytecode→native path (NOT via zwasm; F-001); bytecode ABI is
decoder-only-compatible (ADR-0033 D10 / ADR-0034 D6) so JIT go/no-go does not
perturb placement/build.
Status: math + module/deps DONE. clojure.math ships (clojure/math.clj,
e2e phase14_math*); the deps.edn resolver + the modules/ zone rule already
exist (Phase 14). C FFI BUILT (ADR-0202): cljw.ffi (open close sym function call string bytes) dlopens a library and calls any signature of at
most 6 integer-class and 8 double arguments through two function pointer
types, no libffi. Comptime-gated by -Dffi (default on for x86_64 / aarch64
Linux and macOS, absent on wasm). The surface is shared with clojurust's
clojure.rust.ffi. Future tail: structs by value, varargs, float32,
callbacks into Clojure.
Status: component require BUILT (see §9.18). Gap: full WIT auto-binding
((wasm/component "x.wasm") → bindgen → Clojure namespace) — some WIT param
marshalling is fixture-blocked → D-404. The zwasm embedding shape is settled
by ADR-0124 / D-350. This is the same Wasm/edge-native gap area as §9.18.
Status: UNBUILT (intentionally distal). No jit/ impl (only forward
comments). The near-term JIT scope is the narrow ARM64 JIT (milestone M,
gap area III / §9.19); a broad ARM64/x86_64 JIT with 3-way differential
(TreeWalk == VM == JIT) is decided only after the gap-area-III fusion outcome.
D-005 (ARM64 JIT decision) is the entry debt.
bench/history.yaml records before/after for every optimisation.
1.2x regression on a single bench = STOP.
Before the full Phase-8 harness, a bench/quick.sh covering 5–6 microbenchmarks
goes in just before Phase 4. Used during Phases 4-7.
| Bench | v0.1.0 target | Stretch | |
|---|---|---|---|
| Cold start | < 12 ms | < 8 ms | |
| Warm start | < 4 ms | < 2 ms | |
| Binary size | superseded | — | |
| fib_recursive | 24 ms | 18 ms | |
| map_filter_reduce | 17 ms | 10 ms | |
| transduce | 16 ms | 10 ms | |
| lazy_chain | 16 ms | 10 ms | |
| Idle memory | < 25 MB | < 15 MB |
The binary-size row was a v0.1.0-era figure that predates the embedded zwasm
JIT engine (roughly 3 MB of code by itself ). It is superseded by ADR-0172's
per-component ledger; the live figure is 8.14 MB at v1.14.7
against an enforced ceiling of 8,800,000 bytes, checked on every full gate by
scripts/binary_size_report.sh --check. A target this table states and nothing
measures is the exact rot the 2026-08-04 audit found in three other documents.
The old "Wasm cold start < 50 ms" row is removed: it set a performance target for the §8.1 artifact 2 (cljw running as a Wasm guest), which does not exist . It belongs to D-552 and returns with it.
Required by Phase 7. LazySeq carries meta: ?*const SeqMeta
(.lazy_map | .lazy_filter | .lazy_take | .range). At reduce time, the op
chain is walked and the base source (range, vector) is iterated directly,
producing zero intermediate lazy seqs. This is the mechanism that won v1's
391x on lazy_chain.
- Red: write one failing test first.
- Green: minimal code to pass.
- Refactor: improve while green.
5-layer taxonomy per ADR-0021 (test/README.md is the operational
reference):
| # | Layer | Files | Open at |
|---|---|---|---|
| 1 | Unit (Zig) | test "..." { ... } blocks in src/**/*.zig |
Phase 0 |
| 2 | E2E (CLI) | test/e2e/*.sh |
Phase 2 |
| 3 | Differential | test/diff/ (Evaluator.compare, ADR-0022) |
Phase 4 |
| 4 | Bench quick | bench/quick.sh (informational) |
Phase 4 |
| 5 | Conformance | test/clj/ (clojure.test port) |
Phase 11 |
test/run_all.sh is the unified runner (run_step pattern per
ADR-0024, with --list / --skip / --only flags and a summary).
From Phase 4, the differential layer (test/diff/, ADR-0021 Layer 3,
ADR-0022 implementation) runs every case on both VM and TreeWalk
and asserts equality. Any divergence → identify the root cause
(decide which is correct; fix the other). CI mandatory per ADR-0005.
Phase 17 extends the comparison to a third backend (JIT) per
ADR-0022's compareThree path.
- The first line of each ported file:
;; CLJW: Tier A from <upstream path>. - For a Tier-B difference, mark with
;; CLJW: <reason>per-test. - NEVER work around a failing test. The choice is implement-the-feature
or write-a-tier-demotion-ADR. Commenting out /
:skipalone is forbidden.
Phases marked 🔒 (x86_64 Gate): zig build test must pass on OrbStack Ubuntu
x86_64 (Rosetta on Apple Silicon) before moving on. NaN boxing, HAMT, GC,
VM dispatch, and packed-struct alignment are all arch-sensitive.
Every quality gate this project will need, listed here so they cannot be forgotten when their phase arrives. Move rows from Planned → Active as they are wired.
| # | Gate | Wired as |
|---|---|---|
| 1 | Source-commit → doc-commit pairing | scripts/check_learning_doc.sh (PreToolUse hook on Bash). Defined by skill code_learning_doc. |
| 2 | Zone-dependency check | scripts/zone_check.sh --gate invoked from test/run_all.sh. |
| 3 | zig build test green |
test/run_all.sh. |
| # | Gate | Owner / wiring (planned) | Prepare by |
|---|---|---|---|
| 4 | zig fmt --check src/ |
scripts/format_check.sh, called from test/run_all.sh |
Phase 1 (when src/ grows past bootstrap) |
| 5 | x86_64 cross-arch test (OrbStack Ubuntu) | manual orb run ... zig build test |
Phase 1.12 |
| 6 | Dual-backend --compare (TreeWalk == VM) |
test/diff/runner.zig + cases.yaml |
Phase 4 (CI mandatory; ADR-0005) |
| 7 | Bench regression ≤ 1.2x | bench/bench.sh record + bench/history.yaml diff |
Phase 8 (full); Phase 4 quick harness |
| 8 | Tier-A upstream test green | inline in test/run_all.sh |
Phase 11 |
| 9 | Tier-change ADR present | scripts/tier_check.sh |
Phase 9 |
| 10 | compat_tiers.yaml complete (every listed namespace has impl) |
scripts/tier_check.sh |
Phase 14 |
| 11 | GC root coverage (every heap type traced) | unit tests + --gc-stress |
Phase 5 |
| 12 | Bytecode cache versioning | cache header version field | Phase 12 |
| 13 | JIT go/no-go ADR | .dev/decisions/NNNN_jit_decision.md |
Phase 17 end |
| 14 | Wasm Component build green | test/run_all.sh extension |
Phase 14 |
| 15 | WIT auto-binding correctness | inline test | Phase 19 |
| 16 | nREPL operation parity (CIDER 14 ops) | inline test | Phase 14 |
Maps each gate to its current activation state (per ADR-0005 / 0009 / 0013 / 0016 and the Wave 2 scripts).
| Gate | Mac | OrbStack | Wired by |
|---|---|---|---|
| zig build test | Active | Active | test/run_all.sh |
zone_check.sh --gate |
Active | Active | test/run_all.sh + .githooks/pre-push |
zlinter no_deprecated |
Active | (skip) | ADR-0003 (Mac-only) |
phase3_cli.sh / phase3_exit.sh |
Active | Active | test/run_all.sh |
check_learning_doc.sh |
Active | Active | PreToolUse Bash hook |
check_md_tables.sh |
Active | Active | PreToolUse Bash hook |
check_stale_git_lock.sh |
Active | Active | PreToolUse Bash hook |
check_roadmap_amendment.sh |
Active | Active | PreToolUse Edit|Write hook |
check_adr_history.sh |
Active | on-demand | on-demand audit (unwired; D-547) |
bench/quick.sh |
Phase 4 | Phase 4 | task 4.0 |
Evaluator.compare() |
Phase 4 | Phase 4 | task 4.10, ADR-0005 |
check_compat_tiers_sync.sh |
on-demand | on-demand | informational scan (ADR-0142: no promotion) |
check_no_op_stub.sh |
on-demand | on-demand | informational scan (ADR-0142: no promotion) |
check_tier_d_error_msg.sh |
on-demand | on-demand | informational scan (ADR-0142: no promotion) |
file_size_check.sh |
on-demand | on-demand | informational scan (ADR-0016) |
| Clojure upstream test (Tier A) | Phase 11 | Phase 11 | Skip taxonomy ADR (future) |
Every Phase boundary (or every ~10 ja docs, or before a release tag),
invoke skill audit_scaffolding. It detects four rot patterns across
CLAUDE.md / .dev/ / .claude/ / docs/ / scripts/: staleness (refs
that don't match reality), bloat (files past their soft limit,
duplicated facts drifting), lies (absolute claims overtaken by
reality), false positives (gate / rule triggers firing when they
shouldn't). The audit produces a report; the user decides what to fix.
- One source commit per logical step — red, green, refactor each get their own commit if that maps to the work.
- Structural changes (rename / move / split) and behavioural changes go in separate commits.
- Never commit when tests are red.
- Never bypass the pre-commit hook with
--no-verify— fix the issue.
Chapter cadence suspended. The per-concept chapter half of the
code_learning_docskill is dormant at Phase-4 critical-path close per ADR-0025. Existing chapters (Phase 1-3) live read-only underdocs/ja/archive/. The pre-commit pairing gate (scripts/check_learning_doc.sh) is a no-op. A future resumption ADR re-activates the cadence; the rest of this section is the pre-dormancy reference.
Source-bearing commits accumulate freely; when a unit of work is ready
to be told as one story (and the cadence is active), write
docs/ja/learn_clojurewasm/NNNN_<slug>.md in a separate commit whose
commits: front-matter cites every source SHA it covers.
The full definition (source-bearing file set, the two gate rules, the
template, the workflow) lives in
.claude/skills/code_learning_doc/SKILL.md.
Do not duplicate it here — point to the skill instead. The gate
(scripts/check_learning_doc.sh) is the executable specification.
<type>(<scope>): <one-line summary>
<optional body explaining WHY (not WHAT)>
<type>: feat | fix | refactor | docs | chore | test | bench
<scope>: runtime | eval | lang | app | build | tests | bench | dev
Doc commits use:
docs(ja): NNNN — <title> (#<first-sha>..<last-sha>)
The full resume procedure + per-task TDD loop lives in
.claude/skills/continue/SKILL.md;
the step-by-step spec lives in CLAUDE.md § Autonomous Workflow and
is loaded into every turn's system prompt. The user invokes the
skill with "続けて" / "/continue" / "resume"; the skill reads
handover, finds the next task, runs tests, prints a brief summary,
then immediately enters the TDD loop and runs autonomously.
The TDD loop has seven steps per task (this table mirrors CLAUDE.md § Autonomous Workflow — the SSOT for the loop procedure + stop model; change the procedure there, not here):
| # | Step | Where |
|---|---|---|
| 0 | Survey | Subagent (general-purpose) |
| 1 | Plan | Main |
| 2 | Red | Main |
| 3 | Green | Main |
| 4 | Refactor | Main |
| 5 | Test gate (Mac smoke; full+Linux batched) | Main or Subagent (Bash) if log > 200 lines |
| 6 | Commit + push | Main; atomic — push runs on every commit |
| 7 | Per-task note | Main → private/notes/<phase>-<task>.md |
Chapters (docs/ja/learn_clojurewasm/NNNN_*.md) are written per
concept (every 3–5 source commits or at phase boundary), not
per task. The chapter pulls from per-task notes; that's why
per-task notes exist.
Phase-boundary review chain runs as a multi-agent fan-out:
audit_scaffolding, simplify on the phase diff, security-review
on unpushed commits, and outstanding chapter writing — all in
parallel subagents. The loop continues into §9.<N+1> immediately
after the fan-out synthesises; the phase boundary is not a stop
point.
The loop stops on the SINGLE CLAUDE.md § The only stop condition:
an explicit user request. Gate-red does NOT stop — the loop diagnoses
and fixes in-flight; a genuinely underivable external fact uses an
in-task AskUserQuestion, not a session stop. ADR-level design choices are
handled inline (the AI drafts and accepts the ADR autonomously per
CLAUDE.md "ADR-level designs are handled inline, not as a stop").
Push to main runs on every commit as part of Step 6.
If .claude/CLAUDE.md and this file conflict, this file wins.
- ❌ Branching code in existing
.clj/.zigfor a Tier-D library (§6.4) - ❌ Ad-hoc workarounds to make a test pass (§11.4)
- ❌ Committing with
--no-verify - ❌
git push --forcetomain - ❌
git reset --hardto throw away commits - ❌ No-op stubs that mask missing semantics
(per
.claude/rules/no_op_stub_forbidden.md) - ❌ Providing the JVM Class hierarchy verbatim (e.g.
java.lang.Classwith full reflection). cw v1 providesTypeDescriptorper ADR-0007 instead. - ❌ Using
std.io.AnyWriter/std.io.fixedBufferStream(removed in 0.16) - ❌ Using
pub varas a vtable (use structVTable+ Runtime field) - ❌ Letting any single file drift past 2,000 lines without a
FILE-SIZE-EXEMPTmarker (per ADR-0016) - ❌ Running with only one backend after Phase 4 (per ADR-0005)
- ❌ Leaving local commits on
mainunpushed (Step 6 pushes immediately; accumulation invites a "should I push?" pseudo-decision that the closed stop list does not authorise) - ❌ Writing a doc commit that omits any unpaired source SHA from
commits:(§12.2 Rule 2) - ❌ Mixing source and a
docs/ja/learn_clojurewasm/NNNN_*.mdin the same commit (§12.2 Rule 1)
Each row carries a per-row predicate (no aggregate count gates).
Re-sequenced by F-010 / ADR-0051 (§9.2.R): the JIT go/no-go is now M-internal (the JIT chain completes milestone M right after Phase 15), not gated behind a later Phase 17. The trigger predicate below still applies; "Phase 17" reads as "the JIT-chain sub-phase of the M window". cw-v0-level = narrow ARM64 integer-loop JIT.
- Trigger event: Phase 17 end with
bench/jit.yamlshowing2x speedup over VM on
bench/fixtures/arith_loop.cljAND σ < 5%. Equivalently: v0.1.0 benches (Phase 14) within 110% of cw v0 24C.10 → JIT not needed (transducer + super-instruction were enough). Otherwise → consider JIT (start with ARM64). - Go decision: ADR amendment + Phase 18-20 task table expansion.
- No-go decision: Tier D classification of JIT, removal of stub code.
- Owner: Shota.
- Last reviewed: 2026-05-23.
Criteria:
- The
experimentallabel is gone in Zig 0.16.x. - Real benches show a clear win over Threaded.
- Stability is acceptable.
Criteria:
- WasmGC is stable in major runtimes (wasmtime / wasmer / V8).
- Benchmarks justify it over linear memory + NaN boxing.
- Binary size benefit.
Decision recorded before starting v0.2.0.
Porting cljs.analyzer + cljs.compiler is large. If yes, dedicate v0.2 to it.
A new contributor (or a cold-context AI session) reaches the project through this short stack. Everything else extends from it.
| Order | File | Purpose |
|---|---|---|
| 1 | CLAUDE.md |
Project memory, every-turn auto-loaded |
| 2 | ARCHITECTURE.md |
5-minute orientation (zones / backends / error / tier / phase) |
| 3 | .dev/principle.md |
Working principles + Bad Smell catalogue + depth 4 段 |
| 4 | .dev/handover.md |
Current state + Active task + Next Phase Queue |
| 5 | .dev/ROADMAP.md (this file) |
Authoritative plan |
| 6 | .dev/decisions/NNNN_*.md |
Load-bearing decisions (browse on demand) |
The minimum surface that must always exist (§15.0 lists the entry points; this section enumerates every required file).
Top-level / project memory:
CLAUDE.md— Claude Code project memory (short, points to this file)README.md— public-facing descriptionARCHITECTURE.md— 5-minute orientation (ADR-0020 / ADR-0023)LICENSE— EPL-2.0compat_tiers.yaml— authoritative Tier classification
.dev/ (project state):
.dev/ROADMAP.md(this file) — single source of truth.dev/principle.md— working principles + Bad Smell catalogue (meta layer).dev/handover.md— current state, Active task, Next Phase Queue.dev/debt.yaml— debt ledger (row-level predicates, per A13).dev/reference_clones.md— usage purpose ofadditionalDirectories- observational lessons: hive memory + KG (the
.dev/lessons/directory was migrated into memory and deleted 2026-09-09); search, do not read a file .dev/orbstack_setup.md— Linux x86_64 gate setup.dev/README.md— index / convention pointer.dev/decisions/{README.md, 0000_template.md}— ADR infrastructure.claude/(auto-loaded rules, skills, settings):.claude/settings.json— permissions / hooks.claude/rules/zone_deps.md— layering rules (auto-load on src/).claude/rules/zig_tips.md— Zig 0.16 idioms (auto-load on src/).claude/rules/textbook_survey.md— Step 0 survey policy + anti-pull guardrails (auto-load onsrc/**/*.zig).claude/rules/cljw_invocation.md—cljwinvocation safety (auto-load on test/e2e + bench).claude/rules/markdown_format.md— md-table-align + commit gate.claude/rules/clojure_spec_citation.md— Clojure semantics citation per primitive (per ADR-0013 / A11).claude/rules/debt_dedup.md— debt.yaml row de-duplication discipline (per A13).claude/rules/exploration_vs_done.md— exploration / Done boundary (Pollaroid-derived).claude/rules/feature_name_consistency.md— keyword consistency + Backend marker +src/runtime/{java,cljw}/surface layout (per ADR-0029 D1-D6; absorbed the formerjava_cljw_surface_layout.md, deleted per ADR-0062).claude/rules/error_catalog_only.md— catalog SSOT enforcement (per ADR-0018).claude/rules/no_copy_from_v1.md— re-derive, do not verbatim-copy cw v0.claude/rules/no_jvm_specific_assumption.md— cw v1 is not a JVM reimplementation.claude/rules/no_op_stub_forbidden.md— stub vs no-op boundary.claude/rules/plan_revision_thinking.md— Bad Smell sensor hook (per A23).claude/rules/test_taxonomy.md— 5-layer test placement (per ADR-0021).claude/rules/tier_classification.md— A/B/C/D classification per public fn (per ADR-0013).claude/rules/extended_challenge.md— pre-stop provisioning.claude/skills/code_learning_doc/{SKILL,TEMPLATE_TASK_NOTE,TEMPLATE_PHASE_DOC}.md— two-cadence learning material.claude/skills/continue/SKILL.md— thin invocation trigger; loop spec lives inCLAUDE.md § Autonomous Workflow.claude/skills/audit_scaffolding/{SKILL,CHECKS}.md— periodic scaffolding audit
scripts/ (gates and helpers):
scripts/check_learning_doc.sh— pairing gate (PreToolUse hook)scripts/check_md_tables.sh— md-table-align gatescripts/check_adr_history.sh— ADR Revision history audit (on-demand; not a wired gate, D-547)scripts/check_roadmap_amendment.sh— ROADMAP edit reminder hookscripts/check_stale_git_lock.sh— stale.git/index.lockcleanupscripts/zone_check.sh— zone checker (info / --strict / --gate)scripts/scan_lib.sh— shared library for source-scan gates (per ADR-0024)scripts/scan_catalog_only.sh— ADR-0018 enforcement (informational at Phase 4)scripts/scan_panic_audit.sh— ADR-0019 enforcement (informational at Phase 4)scripts/check_compat_tiers_sync.sh/check_no_op_stub.sh/check_tier_d_error_msg.sh/file_size_check.sh— informational on-demand scans (the Phase-5 promotion is retired, ADR-0142)scripts/print_handover_brief.sh— SessionStart / PostCompact hook.githooks/pre-commit,.githooks/pre-push
ADRs (browse on demand, see .dev/decisions/) — 25 ADRs landed at
Phase 4 entry. Grouped by category:
- Governance / process: 0020 (ADR template + Affected files), 0024 (Source-scan + run_step)
- Error / crash: 0018 (Catalog SSOT), 0019 (Crash policy)
- Architecture decisions: 0007 (TypeDescriptor Option β), 0008 (Protocol dispatch), 0009 (Object header lock), 0010 (STM Tier A), 0011 (Host extension), 0012 (NaN-box ValueTag day-1), 0013 (Tier D permanent), 0014 (UTF-8 internal), 0017 (Allocator strategy)
- Build / I/O / staging: 0015 (io_interface Zone 0 vtable), 0023 (Comptime stub)
- Test: 0005 (Dual-backend differential), 0021 (Test taxonomy), 0022 (Differential wiring)
- Discipline: 0016 (File size smell)
- Day-1 reserve: 0004 (Enum reservation)
- Phase scope: 0006 (Wasm FFI defer)
- Pre-Phase-4 ADRs: 0001 (Macroexpand routing), 0002 (Phase 3 exit no map literal), 0003 (zlinter no_deprecated gate)
Other tracked:
test/{README.md, e2e/*, diff/README.md}+bench/{README.md, quick.sh}docs/ja/{README.md, learn_clojurewasm/, learn_zig/}— learning docsbuild.zig,build.zig.zon,flake.nix,.envrc,.gitignoresrc/main.zigand the rest ofsrc/
Empty files rot. These are created the moment they have real content, using the templates below.
.dev/handover.md — when a session ends mid-task and the next session needs context that git log + ROADMAP cannot convey
Framing discipline is enforced by
.claude/rules/handover_framing.md
(≤ 100 lines hard limit; driving doc; no log accumulation; no
forecast tables; no stop-rationalisation phrases). The current
.dev/handover.md is the canonical shape — start from a copy of
it (sections: cold-start reading order / current state / active
task / next phase queue / open questions) and shrink to fit the
session's actual state.
The template below is retained for historical context only. The row-level
debt.yamlreplaced this plan.
# Known issues & technical debt
## P0 — User-facing bugs (none)
## P1 — Development infrastructure (none)
## P2 — Correctness gaps (none)
## P3 — Design debt
- **<title>** (<file:line>) — what is wrong, why we live with it now, trigger to fixclojure.core: { tier: A, phase: 14 }
clojure.string: { tier: A, phase: 10 }
# ... one line per namespace; java.* default to D(This file exists at the repo root.) The planned companion rule
.claude/rules/compat_tiers.md was SUPERSEDED by
.claude/rules/tier_classification.md (the tier discipline rule that actually
landed); do not create compat_tiers.md.
Per-var tracking is covered by placement.yaml + compat_tiers.yaml (var/class
tier). The originally-planned status/vars.yaml + generate_vars_yaml.clj
generator were not built — and per ADR-0178 they no longer need to be:
placement.yaml IS a generator's output now (scripts/gen_placement.sh reading
(cljw.internal/__dump-placement)), covering every var in every bundled
namespace, with the placement_drift gate failing on any disagreement with the
code. The hand-maintained form it replaced covered 49 of ~683 clojure.core
publics and had drifted to contradict its own source tree.
| Path | Purpose |
|---|---|
cw v0 — git tag v0.5.0 |
ClojureWasm v0 (89K LOC). Design reference; NOT in main's tree (-s ours merge) — reach via git worktree add ../cw-v0 v0.5.0. |
~/Documents/OSS/clojure/ |
Upstream Clojure JVM. core.clj / LispReader.java / Numbers.java. |
~/Documents/OSS/babashka/ |
Babashka (SCI-based). Pod / native / compatibility precedent. |
~/Documents/OSS/spec.alpha/ |
clojure.spec.alpha source. |
$(zig env | grep std_dir) |
Zig stdlib source — the pinned toolchain's own, not a clone (a clone drifts from the pin; ADR-0090 §Context). |
~/Documents/OSS/wasmtime/ |
Wasm runtime reference. Clone on demand (git clone --depth 1 https://github.com/bytecodealliance/wasmtime) — not present by default. |
~/Documents/OSS/malli/ |
Spec alternative. |
~/Documents/OSS/mattpocock_skills/improve-codebase-architecture/ |
Module/Interface/Depth vocabulary and deepening principles. |
- Zig 0.16.0 release notes: https://ziglang.org/download/0.16.0/release-notes.html
- Clojure: https://clojure.org/reference
- WebAssembly Component Model: https://component-model.bytecodealliance.org/
- WASI: https://wasi.dev/
- Babashka pods: https://github.com/babashka/pods
Architecture vocabulary follows mattpocock's definitions:
| Term | Meaning |
|---|---|
| Module | Anything with an interface and an implementation (function / struct / package / slice). |
| Interface | Everything a caller must know (types, invariants, ordering, error modes, performance). |
| Implementation | The body of a module. |
| Depth | Leverage at the interface. Deep = much behaviour behind a small interface. |
| Seam | Where an interface lives (place behaviour can be altered without editing in place — Michael Feathers). |
| Adapter | A concrete thing satisfying an interface at a seam. |
| Leverage | What callers get from depth. |
| Locality | What maintainers get from depth (changes / knowledge concentrate in one place). |
Project-specific:
| Term | Meaning |
|---|---|
| NaN Boxing | Encoding all values in 8 bytes by hiding tags inside IEEE-754 NaN space. |
| Tier | Per-namespace Clojure compatibility level (A/B/C/D). |
| Pod | An external Clojure library implemented as a Wasm Component. |
| InterOp | The dot/static/field/instance? surface, expressed via Class-as-Value. |
| Dual backend | TreeWalk (reference) and VM (production) running side by side under --compare. |
| Fused Reduce | Walking the structural metadata chain on LazySeq directly, avoiding intermediate seq materialisation. |
| Bootstrap stage | How far core.clj is evaluated by TreeWalk before the VM takes over (Stage 0–6). |
| x86_64 Gate | A phase-completion gate: zig build test on OrbStack Ubuntu x86_64. |
| Juicy Main | pub fn main(init: std.process.Init) (a Zig 0.16 idiom). |
| Learning doc | docs/ja/learn_clojurewasm/NNNN_<slug>.md, the Japanese learning narrative required by §12.2. |
Phase 4 entry batch additions:
| Term | Meaning |
|---|---|
| Bad Smell | Mid-implementation "this feels off" signals catalogued in .dev/principle.md. Memory aid, not a checklist. Triggers Step 1 / 4 / 6 self-audit (per A23 / A24). |
| depth (1-4) | Revision depth picked when a Bad Smell triggers: 1 local fix → 2 ADR amend → 3 ADR cascade → 4 Big rewrite. Per .dev/principle.md. |
| catalog Code | A variant of Code in src/runtime/error_catalog.zig identifying a user-facing message. Naming: <target>_<state-adjective> per ADR-0018 amendment 2. |
| ClojureWasmError | The Zig error union returned by every raise() site in src/runtime/error_catalog.zig (per ADR-0018). |
| TypeDescriptor | cw v1's Option β Class-equivalent (per ADR-0007). Holds fqcn / kind / field_layout / protocol_impls / method_table / parent / meta. |
| Tier D form | A form permanently excluded from cw v1 (per ADR-0013). Each Tier D form has a dedicated tier_d_<form> catalog Code with a hand-written user-helpful template. |
| Phase open procedure (RETIRED, ADR-0142) | The former phase-boundary expansion move; superseded by the gap-area self-selection model (CLAUDE.md § When the active work unit completes). |
| Layer (test) | One of the 5 test layers in ADR-0021: Unit / E2E / Differential / Bench quick / Conformance. |
| principle.md | .dev/principle.md: the meta layer above ROADMAP / ADR / CLAUDE.md. Defines premises, Bad Smell catalogue, depth 1-4, three questions to picture the finished form. |
This document is a "now" snapshot. Early-phase planning will inevitably miss dependencies that only become visible when later phases are implemented. Correcting such mismatches IS the maintenance work, not an ad-hoc patch. This section governs how to do that without eroding the document's role as the single source of truth.
Amend in place when the document already disagrees with reality:
- An exit form, scope row, or task description references a feature
whose implementation is scoped to a later phase (e.g., a Phase 3
exit citing
{}while map literals are Phase 5 — see ADR 0002). - A directory / file name in §5 has been superseded by an ADR.
- A principle in §2 needs sharpening because a later phase exposed an edge case the principle did not anticipate.
Add an ADR instead of amending when:
- A genuinely new design decision is being made (e.g., ADR 0001 macroexpand routing).
- A deviation from a §2 principle is justified as a one-time trade-off and should not generalise into the document.
When amending, do all four — none of them are optional:
- Edit ROADMAP in place. Write the corrected text as if it had
always been so. The document is a "now" snapshot; consistency
matters more than preserving past wording. Do not add inline
change-bars, dated comments, or
~~strikethrough~~. - Open an ADR (
.dev/decisions/NNNN_<slug>.md) recording the original wording, the new wording, and why the mismatch existed. The ADR is the changelog; ROADMAP is not. - Sync
handover.mdif its "Active task" / "Current state" sections cited the amended text. - Reference the ADR in the commit message that lands the ROADMAP
edit so
git log -- .dev/ROADMAP.mdis browseable for cause.
- Editing ROADMAP without an accompanying ADR for load-bearing changes (anything in §1, §2, §4, §5, §9 phase rows, §11.6 gates).
- Adding a "revision history" section back to this document — the
trail is git log + ADR +
docs/ja/. - Editing principle text in §2 without an ADR (always load-bearing).
- "Quiet" renumbering of
§Nheadings; if a renumber is unavoidable, it gets its own ADR and a sweep of every§N.Mreference under.claude/,.dev/,docs/ja/, and source comments.
ADRs progress through these statuses:
- Proposed — under discussion, not yet implemented.
- Accepted — implemented and active deviation from the baseline ROADMAP.
- Superseded by ADR-NNNN — replaced by a later ADR.
- Closed (Phase N DONE) — phase boundary made the ADR irrelevant.
- Demoted to a hive memory entry (observational learning only; cite the id).
Status changes are recorded in the ADR's ## Revision history
section. scripts/check_adr_history.sh (on-demand audit — not a wired
gate; see D-547) checks the section on every ADR.
Without 17.1–17.3 the project drifts in one of two failure modes:
- ROADMAP turns into an aspirational document that nobody updates because every change "feels like ad-hoc" and is delayed indefinitely.
- ROADMAP becomes a free-form scratchpad with edits scattered across history, and the "single source of truth" claim quietly dies.
The four-step amendment keeps the ROADMAP correct as a present-tense plan while preserving full traceability through ADRs and git log.
Note on history: this document is a "now" snapshot, not a changelog. What changed and why lives in
git log -- .dev/ROADMAP.md(mechanical diff), the correspondingdocs/ja/learn_clojurewasm/NNNN_<slug>.mdlearning docs (the story behind the change), and.dev/decisions/NNNN_<slug>.mdADRs (load-bearing rationale). The amendment process itself is §17.