docs: revise jerboa-on-jerboa around dogfooding and veneers
ober
8ee4e321fe7ef32eb94da2d03526f4f3dd640a21
--- a/docs/jerboa-on-jerboa.md +++ b/docs/jerboa-on-jerboa.md @@ -1,305 +1,286 @@ # Jerboa on Jerboa -A staged plan to rewrite as much of Jerboa as possible *in Jerboa itself*. +A staged plan to make the editing experience of Jerboa *be* Jerboa. -## Thesis +## The problem -Today, Jerboa is a Chez-hosted language: `.ss` source is translated to `.sls` -and executed on Chez Scheme. The language implementation and the standard -library are written in R6RS `.sls`. The character of "Jerboa" — `def`, -`defstruct`, `match`, `[...]` lists, `{...}` hashes, `keyword:` args — appears -only in user code (tests, examples, the `jerboa-*` companion repos). +Jerboa has a substantial, distinctive feature set: `def` with destructuring, +`defstruct`, `match`, `[...]` list literals, `{...}` hash literals, `keyword:` +args, `try-catch`, `defmethod`. -This is backwards. A language's stdlib is the most-read corpus of code in -that language. If our stdlib is R6RS, then *Jerboa* is a syntactic veneer -applied to user code, not a language. Self-hosting — in the form of an -idiomatic stdlib and tooling layer written in `.ss` — is what makes Jerboa -its own thing. +**None of these features are used by the language's own implementation.** -This document is a plan, not a commitment. It is meant to make the work -concrete enough that a contributor can pick a stage and execute it. +When you open any file in `lib/`, you see R6RS Chez: `define`, +`define-record-type`, `(library ...)`, `let-values`, `cond` chains, alists. +The Jerboa idiom lives entirely in user code — `tests/`, `examples/`, +companion repos. The language's *own* corpus, the canonical example of "how +Jerboa is written," is a Chez codebase wearing a Jerboa name on the import +path. -## What "self-hosted" means here +This is the thing we are fixing. When editing code under `lib/`, you should +almost always see Jerboa, not Chez. -We use the term in the standard staged-bootstrap sense, the way GCC, Rust, -and Clojure use it: +## The principle: editing surface, not purity -- **Stage 0**: the language's reader/translator/core macros, written in the - host language (Chez R6RS). This *must exist* — something has to read the - first `.ss` file. -- **Stage N**: the same components, re-expressed in the language itself. The - stage-0 implementation is retained as bootstrap, not deleted. +We are not chasing self-hosting purity. We are not trying to replace Chez as +the runtime. We are not racing toward a byte-identical fixed-point bootstrap. -We are not trying to replace Chez Scheme as the runtime. The WASM backend in -`lib/jerboa/wasm/` is a separate question and out of scope for this plan. +We are chasing one number: **the fraction of `lib/` you can read as Jerboa.** -## Current state (2026-05-13) - -Lines of code, excluding `lib/thunderchez/` (now removed): +That goal is satisfied by a tactic earlier plans treated as a fallback: +**the veneer pattern.** If a module genuinely must contain raw Chez code +(FFI declarations, `foreign-procedure`, host-record manipulation, primitive +macros), the Chez code lives in a private `_internal.sls` next to a public +`.ss` file that re-exports it. Even if the `.ss` is a "silly" one-line +re-export per binding, that is the right outcome: the import path users and +contributors touch is `.ss`, and the editing surface is Jerboa. -| Layer | Files | LOC | -| ------------------------------------------- | ----- | -------- | -| Stdlib (`lib/std/**/*.sls`) | ~510 | ~140,000 | -| Language impl (`lib/jerboa/**/*.sls`) | ~30 | ~14,000 | -| Jerboa source (`tests/`, `examples/`, etc.) | ~400 | ~72,000 | +We are willing to write a lot of silly veneers. That is the cost of having a +language whose own stdlib is written in the language. -The 154k lines of `.sls` are the entire language implementation plus stdlib. -The 72k lines of `.ss` are user-level code that *consumes* Jerboa but doesn't -*define* it. +## What this is NOT -## The bootstrap floor +- Not "replace Chez Scheme as the runtime." Chez stays. +- Not "rewrite the compiler in itself for its own sake." Stage-0 stays + in-tree for bootstrap, forever if it has to. +- Not "rebuild the WASM backend on WASM." Out of scope. +- Not "byte-identical reproducible stage-N." That is its own problem; we + are not paying for it here. -These files cannot be written in Jerboa — they define what Jerboa is. +## What this IS -| File | LOC | Why it must stay R6RS | -| --------------------------------- | ---- | ------------------------------------------------------------ | -| `lib/jerboa/reader.sls` | 893 | Parses `.ss` syntax — needs to exist before any `.ss` runs | -| `lib/jerboa/core.sls` | 1248 | Defines `def`, `defstruct`, `match`, `try-catch` macros | -| `lib/jerboa/translator.sls` | 811 | `.ss` → `.sls` transforms | -| `lib/jerboa/runtime.sls` | 426 | Hash/keyword/method-dispatch primitives on Chez records | -| `lib/jerboa/prelude*.sls` | 712 | Re-exports that wire the above together | -| `lib/jerboa/ffi.sls` | 137 | Wraps Chez `foreign-procedure` | +Three things, in order: -**Subtotal: ~4,200 LOC of irreducible stage-0.** +1. **Establish the veneer pattern** so we can claim any file as `.ss` even + when its guts are Chez. +2. **Aggressively migrate `lib/` to `.ss`** with the veneer pattern as + escape hatch. +3. **Bind it with a hard rule going forward**: new code in `lib/` is `.ss`, + enforced by CI. -Note: stage-N versions of `reader.sls`, `translator.sls`, and even pieces of -`core.sls` *can* be written in Jerboa once Jerboa exists. The stage-0 files -are then retained only for bootstrap. This is Stage 3 below, and is the most -ambitious step. +## Current state (2026-05-13) -## What's actually rewritable +``` +lib/std/**/*.sls ~510 files, ~140,000 LOC ← stdlib, ~all R6RS +lib/jerboa/**/*.sls ~30 files, ~14,000 LOC ← language impl, ~all R6RS +tests/, examples/ ~400 files, ~72,000 LOC ← Jerboa source (.ss) +``` -### Tier A — pure logic, would be meaningfully better in Jerboa +`.ss` LOC under `lib/`: **~0%.** -Files in `lib/jerboa/` outside the bootstrap floor: +That is the number to move. -| Module | LOC | Notes | -| ----------------------------------------------- | ---- | ---------------------------------------------------------------------- | -| `pkg.sls` + `lock.sls` + `registry.sls` | 498 | Package manager. Manifests, semver, lockfiles — natural fit for `defstruct` + `match`. | -| `cache.sls` | 146 | Content-addressed compile cache. | -| `build.sls` + `build/musl.sls` | 1000 | Build orchestration. | -| `hot.sls` | 120 | File-watcher. | -| `embed.sls` | 190 | Sandbox API. | -| `cross.sls` | 159 | Target config. | -| `clojure.sls` + `cloj.sls` | 594 | Clojure compat. Already a re-export layer; minimal logic. | -| `wasm/*` | ~6300 | WASM backend. Pure data manipulation over WASM module structures. | +## Target end-state -**Tier A subtotal: ~9,000 LOC.** None of this depends on Chez-specific -features that aren't already exposed through the Jerboa prelude. +- **`.ss` file fraction under `lib/`: >90%.** +- **Every public import path under `lib/std/**` is a `.ss` file.** Chez-only + modules live behind `_internal.sls` / `_chez.sls` private names. +- **Every file under `lib/jerboa/` except the bootstrap kernel is `.ss`.** +- **CI fails any PR adding a new non-private `.sls` to `lib/`.** -### Tier B — stdlib pure code +## What truly must stay `.sls` -Most of `lib/std/**` is pure code that doesn't depend on R6RS specifically. -Rough breakdown of the 140k LOC: +The irreducible minimum — what *cannot* be `.ss` because it implements the +machinery that makes `.ss` runnable in the first place: -| Subtree | LOC | Self-hostable? | -| ---------------------------------------- | ------ | -------------- | -| `std/net/` (HTTP, URI, DNS, TLS glue) | 14,841 | Mostly yes; some TLS/socket FFI stays stage-0 | -| `std/misc/` | 12,799 | Yes — utility code | -| `std/text/` (JSON, YAML, regex, markup) | 7,219 | Yes | -| `std/srfi/` | 6,575 | Yes | -| `std/security/`, `std/secure/` | 8,570 | Mostly yes | -| `std/typed/` | 4,052 | Yes | -| `std/actor/`, `std/csp/`, `std/concur/` | 7,328 | Yes — built on top of Chez threading primitives | -| `std/os/` | 6,792 | Partial — syscall wrappers stay; higher-level path/process logic can move | -| `std/ffi/` | 3,574 | **No** — these *are* the FFI bindings | -| Top-level (`csv`, `time`, `transducer`, etc.) | 41,441 | Yes | +| File | LOC | Why | +| --------------------------- | ---- | ------------------------------------------------------------ | +| `lib/jerboa/reader.sls` | 893 | Parses `.ss`. Must exist before any `.ss` loads. | +| `lib/jerboa/core.sls` | 1248 | Bootstrap macros for `def`/`defstruct`/`match`/etc. | +| `lib/jerboa/translator.sls` | 811 | Bootstrap `.ss` → `.sls`. | -**Tier B rough estimate: ~120k LOC self-hostable**, ~10k FFI/syscall glue -stays as stage-0 `.sls`. +**Hard bootstrap floor at stage 0: ~3,000 LOC.** -### Total +Later, *parts* of `core.sls` and `translator.sls` can be re-expressed in +Jerboa once Jerboa exists (Stage 3). The fraction that stays R6RS shrinks; +the stage-0 versions remain for first-build only. -- **Must stay R6RS**: ~4k (bootstrap floor) + ~10k (FFI/syscall shims) = ~14k LOC -- **Could move to idiomatic Jerboa `.ss`**: ~140k LOC +Everything else — `runtime.sls`, `prelude*.sls`, `ffi.sls`, all of +`lib/std/**` including FFI shims and syscall wrappers — gets a `.ss` +editing surface, even if a private `.sls` companion does the host-talking. ## Staging plan -The plan is incremental. Each stage produces a shippable artifact and a -clear gate before the next stage begins. - -### Stage 0 — already done - -The current state. Chez-hosted, Jerboa as a syntactic frontend. Tests and -companion repos written in `.ss`. - -### Stage 1 — Tier A migration (`lib/jerboa/` non-bootstrap) - -Move pkg/lock/registry/cache/build/hot/embed/cross from `.sls` to `.ss`. -Rewrite *idiomatically* (see "Migration mechanics" below), not as a sed -pass. The WASM backend (`wasm/*`) is its own substage; defer if too large. - -**Why first**: these are the smallest, most-changing modules. They get the -most benefit from being in Jerboa (`defstruct` for manifests, `match` for -dispatch, `keyword:` args for builder APIs). They're isolated — no other -stdlib module imports `(jerboa pkg)` or `(jerboa build)`. - -**Acceptance**: -- `make binary` succeeds. -- Full test suite passes. -- No file in this tier imports `(chezscheme)` directly except via the - prelude. -- Each migrated file uses at least three Jerboa-specific features - (`def` + `[...]` + `keyword:` is the minimum bar). - -**Estimated effort**: ~9k LOC of rewriting. Probably 3–4 weeks of focused -work, mostly mechanical once the idiom is settled. - -### Stage 2 — stdlib pure-logic modules - -Move the Tier B stdlib subtrees in priority order: - -1. **Top-level utilities first** (`std/csv`, `std/transducer`, `std/transit`, - `std/zipper`, `std/variant`) — small, self-contained, no downstream - dependents on their `.sls` paths. -2. **`std/misc/**`** — heavily used internally; migrate once the pattern is - proven. -3. **`std/text/**`** — JSON, YAML, regex. These are widely depended on; do - them carefully with golden-output tests. -4. **`std/srfi/**`** — by SRFI number, in dependency order. -5. **`std/typed/`, `std/security/`, `std/actor/`, `std/csp/`, `std/concur/`, - `std/net/`, `std/os/` (non-syscall portions)** — large, internally - coherent subtrees; migrate one at a time. - -**Why this order**: minimize coordination cost. Each subtree should be a -single PR if possible. The migration order is "fewest reverse dependencies -first." - -**Acceptance per subtree**: -- `make binary` succeeds. -- `make test` passes. -- For text/data modules, golden-output equivalence with the pre-migration - version (byte-for-byte JSON output, regex match results). -- No new performance regression beyond a documented budget (say, 10%). - -**Estimated effort**: many months. This is the bulk of the work. - -### Stage 3 — re-implement the bootstrap floor in Jerboa - -The ambitious step. Write `reader-jerboa.ss`, `translator-jerboa.ss`, -`core-jerboa.ss` that re-implement the same surface in Jerboa. Use them in -the standard build; keep the `.sls` versions as the stage-0 bootstrap (used -to build the stage-N versions for the first time). +The plan is aggressive on purpose. Dogfooding *is* the win — there is no +ergonomic-payoff gate before Stage 2. If the migration produces files that +compile and pass tests, it's working. -**Why last**: cannot be done before Stage 2 because the stage-N versions -should *use* the Jerboa stdlib, and the stage-N reader must be expressible -in Jerboa idioms. Doing it before the stdlib migrates would force the -stage-N reader to be written in R6RS-idiomatic-but-`.ss`, which defeats -the purpose. +### Stage 0 — done -**Acceptance**: -- Two-stage build verifies fixed-point: stage-0 builds stage-N1; stage-N1 - builds stage-N2; stage-N1 and stage-N2 produce byte-identical output. -- `make binary` with the stage-N reader/translator passes the full test - suite. -- Stage-0 sources stay in-tree, used only for first-build bootstrap. +Current state. -**Estimated effort**: ~3k LOC of careful new code. Multiple weeks at minimum. +### Stage 1 — establish the veneer pattern and the migration template -## Migration mechanics - -### What "idiomatic Jerboa" means in a migrated file +Goal: produce the template that every other migration follows. -Mechanical translation (`define` → `def`, `(list ...)` → `[...]`) is theater. -A migrated file should use Jerboa features that the R6RS version couldn't -express: +**Pilot files** (one PR each): +- A pure-logic file: `lib/jerboa/lock.sls` → `lock.ss`. Tests `defstruct` + + `match` + `keyword:`. +- An FFI veneer: pick one small file in `lib/std/ffi/` or `lib/std/os/`, + split into `_chez.sls` (private) + `.ss` (public re-export). Tests the + veneer pattern, including silly one-line re-exports. +- A macro-heavy stdlib helper: a small file with `define-syntax`. Tests + the macro-portability boundary early. -- `def` with destructuring instead of `define` + `let-values` -- `defstruct` for record types instead of `define-record-type` -- `match` on values instead of `cond` chains -- `[...]` list literals where lists are constructed inline -- `{...}` hash literals where alists were used -- `keyword:` arguments for any function with more than ~3 positional args -- `try-catch` instead of `guard` / `with-exception-handler` -- `defmethod` for any informal type dispatch +**Deliverables**: +- `docs/jerboa-on-jerboa-idiom.md` — the "what idiomatic Jerboa looks like" + reference. +- `tools/audit-r6rs-in-ss.sh` — greps for banned R6RS forms in `.ss` files + under `lib/`. Initially a stub that flags `define-record-type` and + `(library`; tightens over time. +- A pre-commit hook (or CI step) that runs the audit script. +- `CLAUDE.md` updated: **new code in `lib/` MUST be `.ss`.** -A migration is not "done" until these features are *used*, not just -*available*. +**Acceptance**: three migrated files; idiom doc; audit script; CI hook; +`CLAUDE.md` rule. -### How to verify equivalence +### Stage 2 — migrate `lib/` aggressively -For pure functions (parsers, serializers, validators), the cheapest verifier -is golden output: run the old `.sls` and new `.ss` against a corpus and -diff. For stateful modules, a unit test pass plus the full integration suite -is what we have. +Two parallel tracks. They do not gate each other. -A new `tools/compare-migration.sh` script would make this repeatable. Not -required for stage 1; will be required for stage 2. +**Track A — `lib/jerboa/` (non-bootstrap)** — ~9k LOC: +- `pkg`, `lock`, `registry`, `cache`, `build`, `build/musl`, `hot`, + `embed`, `cross`, `clojure`, `cloj`, `wasm/*`: all to `.ss`. +- `runtime`, `prelude*`, `ffi`: to `.ss` with private `_*.sls` companions + carrying the Chez-touching parts. -### What stays as `.sls` +**Track B — `lib/std/**`** — ~140k LOC. -- FFI shims in `lib/std/ffi/**` — these *are* the C ABI bindings, not code. -- Syscall wrappers in `lib/std/os/**` that call `foreign-procedure` directly. -- The bootstrap floor (see "The bootstrap floor" above). -- `lib/thunderchez/` (already removed as of 2026-05-13). +Priority is "what does a reader of stdlib see first" — high-traffic +subtrees first, FFI/syscall pattern-stressors last: -## Risks and non-goals +1. `std/srfi/`, `std/misc/` — high-traffic, high-leverage. +2. `std/text/` — JSON, YAML, regex. The "look how parsing is written in + Jerboa" exemplars. +3. `std/net/` — most user code touches HTTP/URI. +4. `std/actor/`, `std/csp/`, `std/concur/` — concurrency primitives. +5. `std/typed/`, `std/security/`, `std/secure/`. +6. Top-level (`std/csv`, `std/transducer`, `std/transit`, `std/time`, …). +7. `std/os/`, `std/ffi/` — last, because the veneer pattern is most + exercised here and by then we have practice. -### Risks - -**Performance regression.** The `.ss` → `.sls` translator may produce -worse code than hand-written R6RS in places. Mitigation: keep the golden -test suite, and benchmark hot paths (JSON parse, regex match, HTTP framing) -before/after each subtree migration. +**Acceptance per subtree** (one PR per subtree): +- `make binary` succeeds. +- `make test` passes. +- Golden-output equivalence for parsers/serializers (regex matches, JSON + output byte-equal to pre-migration). +- `tools/audit-r6rs-in-ss.sh` passes on all migrated files. +- **No public import path changes.** Users still `(import (std text json))`. -**Debug-ability regression.** Stack traces through translated code may be -harder to read than direct Chez. Mitigation: source maps in the translator, -or at least line-preserving translation for runtime errors. +### Stage 3 — shrink the bootstrap floor -**Macro-system mismatches.** Some R6RS macros in the stdlib use -`syntax-case` features that don't have direct Jerboa equivalents. Triage -case by case; some macros may need to stay `.sls` as macro-helpers used by -the `.ss` files. +Once Stage 2 is far enough along that the Jerboa stdlib offers rich idiom, +write `reader-jerboa.ss`, `translator-jerboa.ss`, `core-jerboa.ss` and +default the build to them. Keep stage-0 `.sls` versions for first-bootstrap +only. -**Maintenance fork during migration.** Two-version state (`.sls` + `.ss`) -is bad. Migrate a subtree in one PR, do not leave half-migrated trees in the -tree across releases. +Acceptance: stage-N builds itself, passes the test suite, and the build +graph clearly distinguishes "bootstrap-only" from "production" sources. +**Not** chasing byte-identical fixed-point. -### Non-goals +## Migration mechanics -- **Replacing Chez Scheme.** This plan is about source-level - self-hosting. The runtime stays Chez. -- **Removing `.sls` entirely.** The bootstrap floor and FFI/syscall shims - stay R6RS. ~14k LOC will always be `.sls`, and that's correct. -- **Self-hosting the WASM backend on WASM.** Out of scope. The - `lib/jerboa/wasm/` code is Tier A as a stdlib migration candidate, but its - *output* is and remains WASM bytecode for browsers. -- **Beating Chez at compile time.** The Chez compiler is staying. +### The veneer pattern, in detail + +For a module that needs raw Chez (FFI, host records, primitive macros): + +``` +lib/std/foo/_chez.sls (library (std foo _chez) ...) ← private +lib/std/foo/foo.ss (jerboa prelude) re-exports + Jerboa wrappers +``` + +Rules: +- `_chez.sls` is **not** in the public surface. Users importing + `(std foo)` get `foo.ss` exports. +- The `.ss` veneer adds value when it can — keyword args, `defstruct` + return types, match-friendly sentinels. **But a one-line re-export is + acceptable.** The point is the editing surface. +- Veneer files are exempt from the "must use ≥1 Jerboa-distinct feature" + guideline if all they do is re-export. + +### Idiom rules (enforced by audit script) + +In any non-veneer `.ss` file under `lib/`: + +**Forbidden** (CI failure): +- `define-record-type` — use `defstruct`. +- `(library (...) (export ...) ...)` — use Jerboa module declaration. +- Bare top-level `define` for functions — use `def`. + +**Strongly discouraged** (warning): +- `cond` chains over `car`/`cdr` of a known shape — use `match`. +- Alists with atomic keys — use `{...}` hashes. +- `let-values` for receiving a stable shape — use `def` destructuring. + +**Required** (audit checks): +- At least one of `def` / `defstruct` / `match` / `[...]` / `{...}` / + `keyword:` per non-trivial file. + +Veneers are exempt from "required" — a `.ss` that just re-exports a +`_chez.sls` is allowed to contain nothing but the Jerboa module form and +re-exports. + +### Equivalence verification + +- Pure functions: golden output diff against pre-migration `.sls`. Build + `tools/compare-migration.sh` during Stage 1. +- Stateful modules: unit tests + integration suite. +- Don't migrate a file you can't detect a regression in. + +### What stays `.sls` + +- The bootstrap floor: `reader.sls`, `core.sls`, `translator.sls` + (Stage 3 shrinks the latter two). +- Private `_*.sls` companions paired with `.ss` veneers. +- That's it. + +## Risks (mitigated, not gates) + +- **Translator stability under stdlib-shaped code.** Today most `.ss` is + tests and companion repos — narrow distributions. Stdlib will stress + corners. **Mitigation**: Stage 1's macro-heavy pilot is the canary; if it + breaks the translator, we fix the translator before scaling Stage 2. +- **Performance regression.** Translator output may be slower than + hand-written R6RS on hot paths (JSON parse, regex, HTTP framing). + **Mitigation**: benchmark hot paths before each subtree merge. Budget: + 15% regression OK with a note; worse needs fixing or a design pass on + the translator. +- **Debug-ability.** Translated code may produce harder-to-read stack + traces. **Mitigation**: source maps or line-preserving translation in + the translator. Treat as a first-class translator feature, not an + afterthought. +- **Half-migrated tree across releases.** **Mitigation**: one subtree per + PR. No subtree spans a release boundary half-migrated. +- **Companion-repo breakage.** `jerboa-gitsafe`, `jerboa-websearch`, etc. + depend on stdlib import paths. **Mitigation**: hard rule above — no + public import path changes. The veneer pattern is what makes this + rule keepable. ## Open questions -These need answers before Stage 2 starts in earnest: - -1. **Translator stability.** Is the `.ss` → `.sls` translator stable enough - that 140k LOC depends on it without flux? Today most `.ss` code is tests; - stdlib code will exercise corners the translator hasn't seen. - -2. **Macro hygiene at scale.** Some R6RS modules cross-import macros from - each other. Does the Jerboa macro system handle 500-module compositions - cleanly? Stage 1 will exercise this on a small scale; Stage 2 is the - real test. - -3. **Conditional expansion.** R6RS `cond-expand` and Chez-specific build +1. **Macro hygiene at stdlib scale.** The current Jerboa macro system + handles companion-repo composition. 500-module cross-imports is a + different scale. +2. **Conditional expansion.** R6RS `cond-expand` and Chez build conditionals exist in `lib/std/os/`. The Jerboa equivalent surface may need extension before that subtree migrates. - -4. **Versioned stdlib.** Once the stdlib is in `.ss`, version-pinning a - stdlib for embedded use becomes easier. Worth a design pass before - Stage 2 ends. - -5. **Companion-repo coordination.** `jerboa-gitsafe`, `jerboa-websearch`, - etc. depend on stdlib import paths. Stable import paths during migration - are a hard requirement. - -## How to start - -If picking this up cold, the smallest useful first step is: - -1. Pick one Tier A file (suggested: `lib/jerboa/lock.sls` — small, isolated, - high-`defstruct` payoff). -2. Rewrite it as `lib/jerboa/lock.ss` using the idiom guide above. -3. Verify `make binary && make test` passes. -4. Open a PR. This is the template for every subsequent migration. - -Stage 1 is the proof-of-concept. If Stage 1 doesn't deliver a tangible -ergonomics win on the migrated files, Stage 2 should not start. +3. **Clean-build cost.** 140k LOC of `.ss` → `.sls` translation per clean + build is real. The compile cache covers incremental, but CI clean + builds and `make docker-build` will feel it. Measure during Stage 1. +4. **Versioned stdlib for embedded use.** Easier to version-pin a + Jerboa-source stdlib than a Chez-source one. Worth a design pass once + Stage 2 is well underway. + +## How to start (today) + +1. Pick `lib/jerboa/lock.sls`. Write `lib/jerboa/lock.ss` using + `defstruct`, `match`, keyword args. Verify `make binary && make test`. +2. Pick the smallest file under `lib/std/ffi/`. Split into `_chez.sls` + + `.ss` veneer (even a one-line re-export). Verify the build. +3. Write `tools/audit-r6rs-in-ss.sh` — stub: grep for `define-record-type` + in `*.ss` under `lib/`. Wire into a CI step. +4. Open three small PRs, one per pilot. Update `CLAUDE.md` with the + "new code in `lib/` is `.ss`" rule. + +Stage 1 is done when those three PRs land and the idiom doc exists. +Stage 2 starts the next day.