Skip to content

Latest commit

 

History

History
250 lines (206 loc) · 30.1 KB

File metadata and controls

250 lines (206 loc) · 30.1 KB

Migration plan — organised by compaction boundary

Important

This file is the durable spine of the ReScript → AffineScript → wasm migration. It is written to survive context compaction: the repo is the memory, not the model’s window. Each PHASE below ends at a committed, verified checkpoint and updates the Ledger at the bottom. After any compaction, the resuming agent reads (1) this file’s Ledger and (2) the latest proposals/idaptik/migration-map.json, and continues — it does not rely on the auto-summary.

Cadence: roughly one PHASE per context window ("to compact"). Each PHASE = goal + deliverables + a green checkpoint + a one-line handoff. The arc runs until "CLEAR": the migratable corpus is exhausted, the two compiler walls are down, and the cutover has retired the ReScript shadows.

Standing gates (re-check every phase)

  • Decision gate 1 — production language. The evangelist tools + harnesses are Deno trials today; production should be Rust or AffineScript (owner’s call). Until decided: keep the Deno trials, do NOT re-author. Blocks only the "harden the tooling" sub-tasks, not the migration.

  • Decision gate 2 — evangelist home repo (dedicated repo vs under nextgen-languages). Cosmetic; blocks nothing.

  • Access gate — idaptik write-access. Current rule: writes land in affinescript only; everything idaptik-bound is staged as applyable units under proposals/idaptik/. The actual apply + cutover steps (Phases C+ "apply", Phase Ω) need either idaptik opened for writes or the owner accepting the staged patches. Until then, "migrated" means "verified + staged + applyable", not "applied".

The two walls (what ultimately gates "CLEAR")

  1. Variable-string backendString.length, read-side indexing (string_char_code_at / char_to_int, slice 1), single-byte construction (string_from_char_code, slice 2), runtime-length substring (string_sub, slice 3), ASCII case-folding (to_lowercase / to_uppercase, slice 4), and whitespace trimming (trim — scan-and-copy, slice 5) now lower to wasm (Phase F, see Ledger); string_find, int_to_string, concat / +`, and polymorphic `slice` do not yet (the last two are gated on a type-access design decision — see Ledger F). Gates every STRING-GATED kernel (e.g. `Kernel_IO, DeviceType, i18n) whose work is building/transforming strings rather than scanning them. Lives in this repo’s compiler, so it is attackable here.

  2. Effect codegen — module-level mutable state / Date.now / Console.log cannot lower ("references an unbound binding"). Gates every EFFECT-GATED kernel (e.g. Kernel_Quantum). Also a compiler task.

Until a wall falls, its kernels stay staged with the honest "compiler-gated, not effort-gated" label.

Phases

PHASE A — now → compact 1 : consolidate + make the plan durable

Goal: a clean baseline that compaction can’t lose.

  • Write this file + the Ledger (done by creating it).

  • Settle PR #533 (evangelist toolkit): CI green; leave as draft for owner review (merge on owner word).

  • Fold the three architecture artefacts (multiplayer-determinism.adoc, state-vs-digest.svg, game-vs-deepspace.svg) into the determinism doc as a captured "latency regimes" appendix.

  • Record the standing gates above.

Checkpoint: plan + ledger committed & pushed; staged work coherent. Handoff: "Read §Ledger; next is PHASE B (full-corpus triage)."

PHASE B — compact 1 → 2 : broad sweep (the master worklist)

Goal: know the whole backlog, prioritised.

  • Run affine-migratability over all ~571 idaptik .res; bucket by area (src/app, shared/src, vm, src/app/multiplayer, …) x verdict (MIGRATABLE-NOW / STRING-GATED / EFFECT-GATED) x shape (pure-integer core / binding shim / render-glue).

  • Emit proposals/idaptik/migration-map.adoc (human)
    migration-map.json (machine, the resumable worklist) + the headline %.

  • Order the MIGRATABLE-NOW set into clusters: leaf pure-integer cores first, then up the dependency tree.

Checkpoint: full triage committed; backlog + order fixed. Handoff: "worklist = migration-map.json; cluster 1 = <top N>; PHASE C."

PHASE C — compact 2 → 3 : deep wave 1 (first clean cluster, end-to-end)

Goal: migrate + verify the first batch via the 4-gate recipe.

The recipe (every kernel, every wave): . re-decompose .res.affine (NOT transliterate — collapse incidental Promise/async, thread service-locator state as params, enum the options, hand-ladder int rendering, keep wire bytes identical); . affinescript compile → wasm (gate 1: builds); . affine-parity oracle-vs-wasm sweep green (gate 2: parity); . echo-boundary proves each host-boundary encoding LOSSLESS or declared-clamp (gate 3: faithful); . affine-assail clean — no undeclared clamp / unguarded decoder (gate 4).

  • Apply recipe to cluster 1 (~6-10 pure-integer cores). Stage each under proposals/idaptik/migrated/<name>/ with .affine + harness + evidence.

Checkpoint: cluster 1 four-gate-green + staged. Ledger % updated. Handoff: "cluster 1 done (list); cluster 2 next; recipe in §Phase C."

PHASE D — compact 3 → 4 : deep wave 2 + the TS-0 harness path

Goal: more kernels + start retiring the 17.6k LOC of TS harness.

  • Cluster 2 of MIGRATABLE-NOW (same recipe).

  • Trial --deno-esm struct → export class + extern fn lowering; re-author ONE parity harness in AffineScript → Deno-ESM; prove it reproduces the TS harness’s verdict. TS-0-for-harnesses becomes real.

Checkpoint: cluster 2 green + the AffineScript-authored harness PoC.

PHASE E — compact 4 → 5 : the multiplayer determinism spine (wasmex + SNIFS)

Goal: make server-authoritative determinism concrete before the big multiplayer-client migration.

  • vm.affine reversible counter; run the identical wasm in Deno (client) and under SNIFS/wasmex in a one-file Elixir (server); demonstrate identical step, reverse (rollback), and the per-tick hash tripwire catching an injected desync.

Checkpoint: same-binary-both-sides PoC + hash-tripwire demo verified.

PHASE F — compact 5 → 6 : attack wall 1, slice 1 (variable-string backend)

Goal: stop documenting the wall; start removing it (compiler work, in scope).

  • Implement String.length + string indexing end-to-end in the compiler (lexer/typer/codegen) on the [len:i32 LE][utf8] read-side ABI, with tests; demonstrate a previously STRING-GATED kernel now builds
    parity-greens.

Checkpoint: first string primitive lands (compiler PR) + one string-kernel unblocked.

PHASES G..N — compact → compact : the repeating grind

Each window, in order: . migrate the next cluster the latest compiler capability unblocked (recipe); . advance the next wall-slice (startsWithfromCharCode; then the effect-codegen slices for Date.now/log/module-state); . re-run the evangelist readiness map (the % climbs; gated counts fall); . update the Ledger.

Repeat until the MIGRATABLE set is exhausted and both walls are down — at which point Kernel_IO (string) and Kernel_Quantum (effect) finally fall.

PHASE Ω — → CLEAR : cutover + extinction (the goodbye)

Goal: flip the switches, delete the shadows. (Gated on idaptik write-access; until then each cutover is a staged applyable bundle for sign-off.)

  • Per area, once all cores are parity-green + bindings wired: enable the FeaturePacks flag, delete the .res shadows, drop the TS harnesses, remove the lone .py holdout; re-run the census (ReScript → 0 in that area).

  • Final census target: ReScript 0 in migrated areas, AffineScript primary, TypeScript 0, Python 0; agreed-keep set (Elixir/Zig/Idris2/Julia) intact.

CLEAR = migratable corpus migrated + applied, walls down, shadows retired.

Model per phase (advise at each compact)

Heuristic:

  • Opus — re-decomposition design, compiler-internals (the walls), novel cross-runtime integration, cutover review. Anywhere a wrong or subtle call is expensive. ("Fast" Opus when you want that reasoning with quicker output.)

  • Sonnet — known-pattern multi-file grinding: the triage sweep, the rote waves once the recipe is set, the mechanical cutover apply.

  • Haiku — pure mechanical sub-steps (bulk tool runs, boilerplate, status checks). Rarely a whole phase.

Phase Model Why

A consolidate/plan

Opus

synthesis + architecture

B broad triage

Sonnet

run one tool over ~571 files, bucket + prioritise; systematic, low-novelty

C/D deep waves

Opus for the re-decomposition; Sonnet once rote

re-decompose is subtle (a transliteration is rejected); the 4 gates are mechanical

E wasmex/SNIFS PoC

Opus

novel Elixir+wasm+Deno integration + determinism reasoning

F + walls (compiler)

Opus

real OCaml compiler engineering (lexer/typer/codegen/ABI), correctness-critical

G..N grind

alternate: Sonnet (migrate) / Opus (wall-slice)

per window

Ω cutover

Sonnet to execute, Opus to sign off the diff

high-stakes deletes; mechanical apply

Ledger (each phase updates this; the resume point lives here)

Phase State Notes

Pre

DONE

#531 echo proof (merged); #532 migration practice + guide + proposals (merged); #533 evangelist toolkit (merged 2026-06-05).

A

DONE

Plan + model-per-phase guidance written; determinism doc gained a latency-regimes appendix (verify-don’t-transfer + game-vs-deep-space + the 2 SVGs); #533 merged. NEXT: PHASE B.

B

DONE

Full-corpus triage complete (2026-06-05). 571 files: 389 MIGRATABLE NOW (68%), 71 STRING-GATED (12%), 111 EFFECT-GATED (19%). Non-test: 358 files, 196 migratable (55%). Clusters C1–C12 ordered, leaf-first. Worklist: proposals/idaptik/migration-map.json. NEXT: PHASE C — cluster 1 = C1 (shared/src, 11 files) + C2 (vm instructions, 31 files). Switch to Opus for re-decomposition.

C (C1)

DONE

Deep wave 1 complete (2026-06-05, Opus). Cluster C1 re-decomposed: 8 pure-integer kernels staged under proposals/idaptik/migrated/ (DeviceType, PuzzleFormat, PortNames, GameEvent, Kernel_Compute, Kernel, RetryPolicy, Diagnostics); 3 C1 files are host-side "senses" with no brain (Coprocessor_Backends, PortNamesCoprocessor, DLCLoader). Four gates green: 8/8 compile, 1223/1223 parity, 6 echo-boundary proofs (agda exit 0; 3 LOSSLESS + 3 CONTROLLED-LOSS), 8/8 assail-clean. Evidence: migrated/EVIDENCE.adoc. The per-shape recipe is now established (enum-taxonomy / status-gate / classifier / predicate). NEXT: wave 2 = C2.

D (C2 wave 1)

DONE

Deep wave 2 (2026-06-05, Opus). Cluster C2 wave 1 — the reversible VM value-transform opcodes — re-decomposed into 4 kernels under proposals/idaptik/migrated/ (VmArith, VmBitwise, VmAncilla, VmInstruction), covering 11 opcodes (Add/Sub/Negate/Noop/Swap/Flip/Xor/Rol/Ror/And/Or) + the 23-opcode taxonomy. Brain = the reversible scalar-int value transform per opcode; the register-name dict stays host-side. Reversibility (invert∘execute = id) pinned as *_roundtrip exports. Four gates green: 4/4 compile, 2100/2100 parity (incl. every round-trip, over i32 extremes), 1 echo-boundary LOSSLESS (23-opcode encoding, agda exit 0), 4/4 assail-clean. Evidence: migrated/EVIDENCE-C2.adoc. Surfaced 3 compiler facts: unary ~ codegen bug (workaround -a-1), arithmetic >> + no >>> (logical-shift-right emulated). NEXT: C2 wave 2.

C6+C8 (fan-out)

DONE

Parallel deep wave (2026-06-05): two Sonnet agents migrated clusters C6 (combat/enemy) + C8 (device/network); parent re-verified + consolidated. 16 kernels staged under proposals/idaptik/migrated/ — C6: CombatFx, Detection, DifficultyScale, Distraction, DualAlert, HitboxGeom, PlayerHp, SecurityAi; C8: GlobalNetworkData, NetworkManager, SecurityRank, DeviceCaps, LaptopState, NetworkTransfer, PowerManager, CovertLink. Four gates green (re-run by parent, not just agent-reported): 16/16 compile, 34280/34280 parity (C6 8185 + C8 26095, independent oracles), 7 echo-boundary LOSSLESS proofs, 16/16 assail-clean. Re-verification CAUGHT 3 PA-AFF-001 findings the agent reports missed (SecurityAi, SecurityRank, NetworkManager) — fixed with the established guard-helper clamp declaration; NetworkManager parity held at 2645/2645 after dropping the dead Cat enum, confirming semantics preserved. 4th compiler finding: Float→wasm codegen broadly incomplete (pub-fn exports always type i32; float-literal operands mis-emit; trunc()/float() absent) — drives the *Int.affine parity subsets, keeps floats host-side. Evidence: migrated/EVIDENCE-C6.adoc + EVIDENCE-C8.adoc. NEXT: complete C7, then C2 wave 2.

C7

DONE

Player cluster complete (2026-06-05): two scoped agents (5 verify + 3 float-re-decompose) + parent re-verification. 8 kernels — CriticalRoll, PlayerAttributes, QCertifications, SkillRank, SkillAbilities, QPrograms, JessicaLoadout, JessicaBackground. Four gates green (every gate re-run by parent): 8/8 compile, 4348/4348 parity (independent oracles), 6 echo-boundary LOSSLESS proofs across 4 kernels (QCertifications, SkillRank, JessicaLoadout×3, JessicaBackground — agda re-typechecked by parent, exit 0); the other 4 transform/classifier kernels G3-n/a, 8/8 assail-clean. 3 kernels (CriticalRoll/PlayerAttributes/QPrograms) hit the Float→wasm wall (min_float/max_float/trunc) and were re-decomposed Int-native (milli-unit convention, floats host-side) per the C6 *Int pattern; parity caught + fixed an off-by-100 in PlayerAttributes. Evidence: migrated/EVIDENCE-C7.adoc. NEXT: C2 wave 2.

C2 wave 2a

DONE

Scalar multiply/divide (2026-06-05, Opus). Mul/Div turned out to be pure scalar value-transforms (not memory ops), so they need no array ABI — split out of "wave 2" and landed now. 1 kernel VmMulDiv (11 exports) under proposals/idaptik/migrated/. Brain = the reversible ancilla multiply (c := c + a*b, inverse c := c - a*b) + the quotient/remainder divide whose dividend is reconstructable (q*b + r == a for all b incl. 0); the intentional-flaw in-place/simple variants migrated as value transforms with no rt==id claim. Four gates green: 1/1 compile, 3322/3322 parity (incl. mul reversibility roundtrip + div reconstruction), G3 n/a (numeric transform), 1/1 assail-clean. 2 new i32 ABI facts: (a) JS a*b loses precision >2^53 → oracle must use Math.imul to match i32.mul; (b) i32.div_s TRAPS on b==0 and INT_MIN/-1 (ReScript wraps the latter) → brain guards both (nested-if, avoiding the unverified && codegen path) so the wasm is total. Evidence: migrated/EVIDENCE-C2-wave2a.adoc. NEXT: C2 wave 2b.

C2 wave 2b

DONE

Memory/stack/port/control opcodes (2026-06-05, Opus). The "needs an array/linear-memory ABI" premise was WRONG and re-decomposition overturned it: reading the sources, the VM’s memory/stack/port buffers are not arrays in the brain — VmState.res stores every one as string-keyed dict slots (_mem:N, _s:N, _pin:port), i.e. host-side STATE (senses). The integer brains are all scalar. 4 kernels VmMemory (LOAD/STORE), VmStack (PUSH/POP), VmPort (RECV/SEND), VmControl (IF_POS/IF_ZERO/LOOP) covering 9 opcodes. Four gates green: 4/4 compile, 1568/1568 parity (incl. load/store/sp/recv/send round-trips), G3 n/a (transforms/predicates), 4/4 assail-clean. No-kernel senses (classified, not migrated): Call (orchestration), CoprocessorCall (tombstone — never implemented), State/VmState/VM/SubroutineRegistry/VmStateCoprocessor/InstructionCoprocessor (state containers / bridges). Evidence: migrated/EVIDENCE-C2-wave2b.adoc. No array ABI was required (the blocker was a triage-bucket artefact). Cluster C2 COMPLETE. NEXT: C3.

C3

DONE

Coprocessor wiring layer (2026-06-05, Opus) — classified, no new brains to extract. C3’s 17 src/shared/ files are the host-side bridge layer that exposes C1’s already-migrated wasm brains to the game engine. Verified de-dup (by constant, not name): the integer cores C3 needs (compute_gate/data_limit_for_domain 513/16/259/1024, max_concurrent_compute 10, is_transient 503/504/429, policy table, DeviceType/GameEvent ordinals) are all already migrated + 4-gate-verified in C1. The 12 *Coprocessor.res are pure-FFI passthrough (0 logic lines); Kernel_Compute/RetryPolicy (src/shared) are async/float wrappers over C1 brains; DeviceType/GameEvent add string-gated ops (string wall, Phase F). No 4-gate run (nothing new to verify). Bridges stay host-side as the essential wasm-loading glue (ADDITIVE, FeaturePacks-flagged); their ReScript→glue fate is a Phase-Ω cutover decision. Evidence: migrated/EVIDENCE-C3.adoc. NEXT: C5.

C5

DONE

Engine coprocessors (2026-06-05, Opus). 2 integer brains extracted + 9 senses classified. Maths (clamp/lerp-milli/dist_sq — float-wall convention, sqrt host-side) and — the strategic one — Random, the deterministic xmur3 + mulberry32 PRNG, the multiplayer-reproducibility backbone (host iterates the seed string + does the final u32→[0,1) float div; brain owns the i32 mixing). Four gates green: 2/2 compile, 2108/2108 parity (Random bit-exact vs the JS source over the full i32 domain + UTF-16 code units, incl. the >>> emulation + signed-i32 multiplier constants), G3 n/a (transforms/mixing), 2/2 assail-clean. Senses (classified, not migrated): 5 *Coprocessor FFI bridges, Resize/GetResolution (float+DOM), Audio (Pixi), Navigation (screen-stack orchestration + async asset load, effect-gated), WaitFor (timing). Evidence: migrated/EVIDENCE-C5.adoc. NEXT: C11 (done below).

C11

DONE

UI and accessibility coprocessors (2026-06-05) — classified, NO_NEW_BRAINS. All 10 files are host-side senses. DualAlert.res integer core (alert-level ordinals, escalation, trigger-milli, scrub, overall-threat, dispatch predicates) was already extracted in C6 (DualAlert.affine, 613/613 parity, LOSSLESS boundary proof). DualAlertCoprocessor.res is the existing ReScript FFI bridge to that wasm. ForceLayoutCoprocessor.res wraps forcelayout.wasm, a float-only physics kernel (euclidean/repulsion/attraction/velocity/alpha — f64 throughout; float-wall: brain stays host-side). Remaining 7 files (Button, HubButton, NavButton, Label, VolumeSlider, InventoryUI, HardwareWiring) are pure PixiJS/PixiUI senses with zero separable integer computation. No 4-gate run. Evidence: migrated/EVIDENCE-C11.adoc. NEXT: C9.

C9

DONE

Game-loop / AffineTEA / VeriSim types (2026-06-05) — classified, NO_NEW_BRAINS. All 16 files are host-side senses. 4 bridge files wrap already-live wasm brains: AffineTEA.restitlescreen.wasm (TEA title-screen state machine), AffineTEARouter.resrouter.wasm (affine nav back-stack), PixiCoprocessor.respixi.wasm (AffineScript PixiJS scene driver), VmCoprocessor.resvmcoprocessor.wasm (C2). 8 VeriSimDB/Burble HTTP clients (SaveGame, VeriSimDrift, VeriSimFederation, VeriSimHexad, VeriSimProvenance, VeriSimSearch, VeriSimVcl, BurbleIntegration) are async fetch wrappers — effect-gated throughout. VeriSimTypes.res enum variants (modality/hexadStatus/driftLevel) appear integer-clean but modalityToString/modalityFromString use string literals and all enums are used only in string-bearing records — string-wall gated, no standalone computation. VeriSimError.res constructors all carry string payloads — string-wall gated. Main.res / GameLoop.res are effect-gated orchestration; GameLoop integer counters are trivial increments interleaved with float physics and mutable JS objects. No 4-gate run. Evidence: migrated/EVIDENCE-C9.adoc. NEXT: C10.

C10

DONE

Utils/tools/companions/narrative/proven (2026-06-05) — classified, NO_NEW_BRAINS. All 21 files are host-side senses. 9 bridge files wrap already-live wasm brains: moletairecoprocessors.wasm (Moletaire behaviour FSM), moletairehunger.wasm (hunger ordinals), missionbriefing.wasm (mission briefing state machine), safefloat.wasm (safe-float arithmetic), passwordcracker.wasm (password-crack score), portscanner.wasm (port-scan classifier), colorpalette.wasm (color lookup), fontscale.wasm (font-scale arithmetic), gamei18n.wasm (plural-form + language-cycle). Float-wall: MoletaireHunger.res (f64 gravity/hunger physics — integer enum functions already in moletairehunger.wasm), SafeAngle.res (Math.atan2/cos/sin/mod_float, all f64). String-wall: ProvenError.res (all constructors carry string fields), MoletairePersistence.res (equipmentToString/FromString), Locales.res (5-language translation dicts, ~650 string pairs), PolyglotI18n.res (String.replaceAll, async fetch of locale JSON). Effect-gated: Announcer.res (AccessibilitySettings + DomA11y), DomA11y.res (%raw DOM), ColorPalette.res (AccessibilitySettings mutable refs; color tables already in wasm), KeyboardNav.res (%raw window events), PanicHandler.res (Console.error), UserSettings.res (Storage/Audio/DesktopIntegration; stance enum string-serialized). No 4-gate run. Evidence: migrated/EVIDENCE-C10.adoc. NEXT: C12.

C12

DONE

Render-glue screens/PixiJS bindings (2026-06-05) — classified, NO_NEW_BRAINS. All 43 files are host-side senses. 12 bridge files wrap already-live wasm brains (companionsrenderlogic, devicesrenderlogic, enemiesrenderlogic, toolspickupsrenderlogic, playerrenderlogic, narrativepopupsrenderlogic, balanceanalyser, locationdata, screenglitchfx, screensrunlooplogic, uirenderlogic, verisimprovenrenderlogic). 23 screen/popup files are effect-gated PixiJS rendering orchestrators (GameI18n string calls, Motion.animate, mutable refs, Navigation APIs). 4 binding files (Motion, Pixi, PixiSound, PixiUI) are pure external FFI declarations wrapping host-side JS libraries. Phase Ω cluster sequence (C11→C9→C10→C12) complete. Genuine compiler gates remain: string wall (71 corpus files — [len:i32][utf8] layout already in codegen.ml:375, only ops missing), effect wall (111 corpus files). Evidence: migrated/EVIDENCE-C12.adoc.

F (string slice 1)

DONE

String-wall slice 1 — read-side indexing landed in the wasm backend (2026-06-12, Opus). string_char_code_at(s, i) + char_to_int(c) (already wired in resolve/typecheck/interp) gained wasm lowerings on the [len: i32 LE][utf8] ABI: byte i at base + 4 + i, bounds-guarded If load, -1 OOB sentinel shared with the interp oracle; char_to_int is the i32 identity. A previously STRING-GATED shape (byte indexing into a literal) now compiles to wasm and parity-greens vs the interpreter. Gates: G1 builds; G2 parity 7/7 (interp oracle vs wasm: 65/66/67/-1/-1/-1/90); type safety preserved (char_to_int(65) still a Char/Int error). Tests: test/test_e2e.ml group "E2E String-wall slice 1 (indexing)" (interp, dune runtest) + tests/codegen/string_char_code_at.{affine,…mjs} (executable wasm parity, full run_codegen_wasm_tests.sh green, no regressions). Evidence: proposals/EVIDENCE-stringwall-slice1.adoc. Unblocks the indexing/scan family; allocation-returning string ops (string_from_char_code/string_sub/slice/concat/case-fold) are later slices. NEXT: allocation-returning ABI (gen_string_alloc + string_from_char_code).

F (string slice 2)

DONE

String-wall slice 2 — write-side ABI landed in the wasm backend (2026-06-12, Opus). string_from_char_code(n) (already wired in resolve/typecheck/interp) gained a wasm lowering: bump-allocate [len: i32 LE = 1][byte] where the byte is the low 8 bits of n (I32Store8 does the land 0xff mask itself, incl. negatives). First heap-allocating string op; reuses the existing gen_heap_alloc bump allocator (no new memory machinery). Gates: G1 builds; G2 parity 8/8 (interp vs wasm over masking/NUL/255/256/-1/320/length/OOB-after-construction); round-trips with slice-1’s reader. Tests: test/test_e2e.ml group "E2E String-wall slice 2 (string_from_char_code)" (interp, dune runtest) + tests/codegen/string_from_char_code.{affine,…mjs} (executable wasm parity; full run_codegen_wasm_tests.sh green, no regressions). Evidence: proposals/EVIDENCE-stringwall-slice2.adoc. Read+write byte primitives now both present. NEXT: runtime-length allocation ABI for string_sub/slice (slice 3).

F (string slice 3)

DONE

String-wall slice 3 — runtime-length copy op landed in the wasm backend (2026-06-12, Opus). string_sub(s, start, length) (already wired in resolve/typecheck/interp) gained a wasm lowering introducing the two capabilities the rest of the wall needs: a runtime-sized heap allocation (heap = 4 + length'`) and a *byte-copy loop* (`Block`/`Loop`/`BrIf`/`Br`, `I32Load8U`/`I32Store8`), both modelled on the list `+ allocate-then-copy idiom. Interp clamps reproduced exactly: start' = max 0 (min start slen), length' = max 0 (min length (slen - start')) (min/max via Select). Gates: G1 builds; G2 parity 8/8 (interp vs wasm over extraction/length/clamp-length/clamp-start/negative-start/zero-length); round-trips with slice-1’s reader. Tests: test/test_e2e.ml group "E2E String-wall slice 3 (string_sub)" (interp, dune runtest) + tests/codegen/string_sub.{affine,…mjs} (executable wasm parity; full run_codegen_wasm_tests.sh green, no regressions). Evidence: proposals/EVIDENCE-stringwall-slice3.adoc. Runtime alloc + copy loop now established. NEXT: concat/string_find/case-fold (slice 4) reuse these idioms.

F (string slice 4)

DONE

String-wall slice 4 — ASCII case-folding landed in the wasm backend (2026-06-13, Opus). to_lowercase / to_uppercase (already wired in resolve/typecheck/interp) gained wasm lowerings as a copy-with-transform over the slice-3 runtime-length idiom: allocate 4 + slen, copy each byte through a branchless ASCII case shift (c ± 32*c>=lo)&(c⇐hi, an I32And 0/1 product — no per-byte If). Matches the interp oracle (String.{lowercase,uppercase}_ascii): only 'A'..'Z'/'a'..'z' shift; digits, punctuation, and bytes ≥128 pass through. Gates: G1 builds; G2 parity 7/7 (interp vs wasm), incl. the exclusive @(64)/[(91) boundary probes + length preservation. Tests: test/test_e2e.ml group "E2E String-wall slice 4 (case-fold)" (interp, dune runtest) + tests/codegen/string_case.{affine,…mjs} (executable wasm parity; full run_codegen_wasm_tests.sh green, no regressions). Evidence: proposals/EVIDENCE-stringwall-slice4.adoc. Proves the alloc+copy idiom carries per-byte transforms. NEXT: concat (++/string_concat), then string_find/trim/slice.

F (string slice 5)

DONE

String-wall slice 5 — whitespace trimming landed in the wasm backend (2026-06-13, Opus). trim (already wired in resolve/typecheck/interp) gained a wasm lowering: two flat scans bracket the non-whitespace core (front for lo, back for hi exclusive; whitespace set {32,9,10,12,13} via an is_ws I32Or chain), then a slice-3-style byte copy of [lo, hi). Matches String.trim: internal whitespace preserved, all-whitespace → empty. Gates: G1 builds; G2 parity 7/7 (interp vs wasm: both-end/leading/trailing, all-ws, empty, internal-space preservation, tab+newline). Tests: test/test_e2e.ml group "E2E String-wall slice 5 (trim)" (interp, dune runtest) + tests/codegen/string_trim.{affine,…mjs} (executable wasm parity; full run_codegen_wasm_tests.sh green, no regressions). Evidence: proposals/EVIDENCE-stringwall-slice5.adoc. The clean name-dispatched builtins (read/write/copy/transform/scan) are now done. NEXT decision point — see F+ row.

F+

TODO (design decision)

Remaining string ops split: (a) string_find — clean name-dispatched substring scan (returns Int, no alloc); the next autonomous slice. (b) int_to_string/float_to_string — name-dispatched decimal rendering (digit loop + sign + INT_MIN edge); autonomous if prioritised. (c) *concat ` / polymorphic `slice`* — **blocked on an architectural decision**: codegen has *no per-expression type access* (`ExprBinary` is untyped, no type env threaded in), so string ` (vs the existing list OpConcat) and slice (array

string) need either typed-AST threading or a dedicated string_concat builtin (language-surface call). (d) string_get/int_to_char — deferred (raise on out-of-range; no trap-free wasm analogue). Then the effect-codegen wall (module-state / Date.now / Console.log). See proposals/EVIDENCE-stringwall-slice5.adoc §"remaining string ops". Cluster migration complete; string slices 1-5 landed (read + single-byte write + runtime-length copy + per-byte transform + scan-and-copy).

Ω

TODO (access-gated)

Resume rule: read the highest non-DONE row + migration-map.json; continue from its "Notes".

Important

Completion-drive directive (user-authorised 2026-06-05). The owner authorised driving the migratable corpus to completion autonomously — do NOT stop to re-ask at each cluster boundary. Standing plan for the resuming agent:

  1. Drive the MIGRATABLE-NOW clusters in this order, each via the full 4-gate recipe, staged under proposals/idaptik/migrated/, one PR per cluster, squash-merged when the gates are green (CI on this repo is non-gating; the authoritative gates are the parity/assail/agda runs done locally): C5 → C11 → C9 → C10 → C12. Classify senses/string-gated/effect-gated files honestly (like C3) rather than forcing a migration.

  2. Then attack the two compiler walls in this repo (in scope): the string backend, then effect codegen — each unblocks its STRING-/EFFECT-GATED kernels.

  3. Update this Ledger + migration-map.json after every cluster (repo-as-memory; compaction-proof).

Git hygiene (learned 2026-06-05): base each cluster’s work on the dev-branch tip (origin/claude/cool-keller-gr5sl), NOT on origin/main’s squash SHA — the remote dev branch is force-push-protected, so basing on it keeps pushes fast-forward and avoids the post-squash-merge divergence that nags the commit-identity hook. Rebase onto `origin/main only if main has advanced beyond the staged content.

The one gate I cannot clear myself: Phase Ω cutover (apply staged units to idaptik + delete the .res shadows) needs idaptik write-access. Until the owner opens that, "migrated" = "staged + 4-gate-verified", and true CLEAR (ReScript→0) waits on access. Everything up to that line is drivable here.