You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build a migration assistant: a tool that reads .res files, recognises common patterns, and emits .affine skeletons. Not a transliterator — the human picks the decomposition; the tool emits the AffineScript syntax.
Capabilities
Read .res AST (via ReScript's bs-tools or similar)
Map known patterns:
let _ = X.constructor → call out "explicit registration needed"
Promise.catch(e => ...) → "Promise umbrella, propose Result + sum"
Named-arg lambda blob → "row-poly handler record candidate"
4+ tuple match → "Validation chain candidate"
Surface patterns without a clean translation (effect handlers, exceptions, mutable globals) for human decision
Output .affine skeletons + a comment-block listing the migration considerations
Why this is also the metaparser story
This tool is a parser for .res that produces an AST and emits .affine syntax — a metaparser, in the architectural-recommendation sense. Once it exists, the same machinery can drive other migrations (.ts→.affine, .js→.affine, etc.). It's the first concrete user of the "tree-sitter as canonical grammar source" architecture.
Why this matters for idaptik
idaptik has 542 .res + ~80 .ts files to migrate. By-hand is impractical; a per-pattern transliterator misses the point of re-decomposition. The migration assistant is the middle ground.
Filed from idaptik Wave 3 pilot. See LESSONS.md and PILOT.md. Idaptik commit hyperpolymath/idaptik@aef38db.
Phase 1 (#314) ran end-to-end against the estate's 491 deduplicated .res files (idaptik + gitbot-fleet). Two high-impact false-positive sources surfaced; precision pass landing in #319.
Before
After
Total markers
2,146
348
Files w/ markers
216
94
side-effect-import
1,181
36
mutable-global
653
0
raw-js
198
198
untyped-exception
114
114
Full report and follow-ups in tools/res-to-affine/CORPUS-RUN.md (with CORPUS-RUN.json machine-readable sidecar) landing in #319. Phase 2 (tree-sitter AST walker) and Phase 3 (partial translation) remain as originally scoped.
First AST-based detection — tree-sitter walker for side-effect-import (--engine=walker); structural elimination of the column-0 regex false-positive class
Walker ports for raw-js, untyped-exception, mutable-global; the two anti-patterns deferred from Phase 1 (inline lambda callback records, oversized functions); flip --engine=walker to default
—
🟡 open
3
Partial translation of pure-structural forms — the phase at which mechanical porting becomes meaningful (the explicit gate in sustainabot's tracker exit criteria)
—
🟡 open
Why this issue stays open. The original proposal is explicitly multi-phase; every landed PR uses Refs #57, not Closes #57. Phase 2c brings the walker to parity with the Phase-1 scanner across all six anti-patterns (and lets the default switch from scanner to walker); Phase 3 is the partial-translation phase that lets a consumer like gitbot-fleet/sustainabot actually do the mechanical port + diff-against-hand-port re-validation step its tracker calls for.
bots/sustainabot/bot-integration/src/ hand-port (gitbot-fleet#148 / affinescript#199) is unblocked as a one-off today against the Phase-1+2a+2b surface (Json/Dict primitives now landed, see #161#162); the residual re-validation against the mechanical path remains gated on Phase 3 here.
res-to-affine --translate now turns the fully-structural type declarations into compiler-verified AffineScript. Two slices merged to main, both with the generated output validated by the compiler itself (main.exe check → Type checking passed):
primitive type aliases (type userId = int → type UserId = Int), simple sum types (type color = Red | Green → type Color = | Red | Green, payloads mapped)
SessionStart hook provisioning the OCaml toolchain + tree-sitter so web sessions can dune build/dune runtest; STATE.a2ml note
Conservative by construction: a declaration translates only when every part is representable; qualified-path RHS, non-primitive refs, nested generics (array<int>), object types, GADT returns, variant spreads, and mutable/optional-? records are skipped (kept in the marker block + quoted original), never mis-translated.
Remaining Phase-3 work — this issue stays open:
let-to-const for literal bindings
switch→match (needs arm-body translation, not just declaration shapes)
Per the repo's ISSUE-CLOSURE rule I'm not self-closing. If you consider the type-declaration coverage sufficient to call the assistant "done", close at your discretion (or say the word and I'll keep going on the next slice).
res-to-affine --translate now translates every self-contained top-level declaration that can be lowered to compiler-verified AffineScript. Three slices landed; each generated form is confirmed by the compiler itself (main.exe check → Type checking passed):
SessionStart hook provisioning the OCaml toolchain so web sessions build/test out of the box
Conservative throughout — qualified paths, non-primitive refs, nested generics, GADT returns, variant spreads, mutable/optional records, and non-literal/ref/destructuring lets are skipped (kept in the marker block + quoted original), never mis-translated.
Scope boundary — the two items that remain
These are out of the "every emitted form type-checks standalone" model by construction, not just unimplemented:
switch→match — a match is an expression, only meaningful inside a function body. Producing a type-checkable result means translating the whole enclosing function, but ReScript bindings are usually un-annotated (let f = x => …) while AffineScript fn requires param/return types — so the output wouldn't type-check. This belongs to a future partial-port mode (translate-with-TODO-holes) that drops the standalone-type-check guarantee. Different model, worth its own issue.
The declaration-translation milestone is complete within the type-checks-guarantee scope. The remaining work (partial-port mode for expressions/functions + module mapping) is a distinct effort. Per the repo's ISSUE-CLOSURE rule I'm not self-closing — your call whether to close this and track the partial-port mode as a fresh issue, or keep it open as the umbrella for those two items.
The goal of this issue — the .res→.affine migration assistant (parser + metaparser + skeleton emission, through to translating self-contained top-level declarations) — is delivered:
The remaining work — switch→match / function translation (partial-port mode) and module-qualified reference resolution — is a distinct model (it deliberately drops the standalone-type-check guarantee) and is now tracked in the successor #488.
Closed as completed; further work continues under #488.
affinescript#463 unblocked non-ASCII string emit in Deno-ESM (Unicode escapes, not octal).
affinescript#421 put stdlib/json on the Zig-FFI rails (hpm-json-rsr).
What this means for the gap tracked in this ticket:
The codegen pattern is now a known repeatable shape: declare in stdlib/*.affine → wire in deno_builtins table + prelude → smoke-test in tests/codegen-deno/*.
This ticket remains open as a tracked gap; no new mass-port is being kicked off from this post.
Surfacing status so the broader picture stays accurate as more sub-tickets / dependent campaigns reference it.
Activity
feat(res-to-affine): Phase-1 migration assistant skeleton (Refs #57) (#…
hyperpolymath commentedon May 21, 2026
Corpus run 2026-05-21
Phase 1 (#314) ran end-to-end against the estate's 491 deduplicated
.resfiles (idaptik + gitbot-fleet). Two high-impact false-positive sources surfaced; precision pass landing in #319.side-effect-importmutable-globalraw-jsuntyped-exceptionFull report and follow-ups in
tools/res-to-affine/CORPUS-RUN.md(withCORPUS-RUN.jsonmachine-readable sidecar) landing in #319. Phase 2 (tree-sitter AST walker) and Phase 3 (partial translation) remain as originally scoped.docs(res-to-affine): corpus run + regex precision fixes (Refs #57) (#319
feat(res-to-affine): land tree-sitter grammar build pipeline (Phase 2…
feat(res-to-affine): tree-sitter AST walker for side-effect-import (P…
hyperpolymath commentedon May 25, 2026
Status update (2026-05-25)
Re-checked at the owner's request while resolving the AffineScript-side blockers for the
gitbot-fleet/sustainabotmigration tracker. The picture:.affineskeleton + migration markersside-effect-import/mutable-global(markers 2,146 → 348, −84%)tree-sitter-clibuild pipeline +migration-assistantCI job + grammar-pin drift signalside-effect-import(--engine=walker); structural elimination of the column-0 regex false-positive classraw-js,untyped-exception,mutable-global; the two anti-patterns deferred from Phase 1 (inline lambda callback records, oversized functions); flip--engine=walkerto defaultWhy this issue stays open. The original proposal is explicitly multi-phase; every landed PR uses
Refs #57, notCloses #57. Phase 2c brings the walker to parity with the Phase-1 scanner across all six anti-patterns (and lets the default switch fromscannertowalker); Phase 3 is the partial-translation phase that lets a consumer likegitbot-fleet/sustainabotactually do the mechanical port + diff-against-hand-port re-validation step its tracker calls for.bots/sustainabot/bot-integration/src/hand-port (gitbot-fleet#148 /affinescript#199) is unblocked as a one-off today against the Phase-1+2a+2b surface (Json/Dict primitives now landed, see #161 #162); the residual re-validation against the mechanical path remains gated on Phase 3 here.Generated by Claude Code
fix(res-to-affine): walker — explicit record-literal pattern matches …
fix(ci): three foundational unblocks for the main-branch baseline (PR…
fix(ci): unblock the PR queue — bench/dune + adapter-load + .res-fixt…
feat(res-to-affine): Phase 2c — walker covers all 6 anti-patterns + d…
fix(res-to-affine): walker — explicit record-literal pattern matches …
feat(res-to-affine): Phase 2c — walker covers all 6 anti-patterns + d…
fix(res-to-affine): walker — explicit record-literal pattern matches …
docs(claude): refresh language-policy tables for 2026-05-25 estate po…
docs(claude): refresh language-policy tables for 2026-05-25 estate po…
92 remaining items
hyperpolymath commentedon May 30, 2026
Phase 3 state update (2026-05-30) — slices 1+2 landed, tooling provisioned
res-to-affine --translatenow turns the fully-structural type declarations into compiler-verified AffineScript. Two slices merged tomain, both with the generated output validated by the compiler itself (main.exe check→ Type checking passed):2763909type userId = int→type UserId = Int), simple sum types (type color = Red | Green→type Color = | Red | Green, payloads mapped)3484010struct(type point = {x: int}→struct Point { x: Int }), generics ('a→[A], threaded through aliases / sums / records)138baacdune build/dune runtest; STATE.a2ml noteConservative by construction: a declaration translates only when every part is representable; qualified-path RHS, non-primitive refs, nested generics (
array<int>), object types, GADT returns, variant spreads, andmutable/optional-?records are skipped (kept in the marker block + quoted original), never mis-translated.Remaining Phase-3 work — this issue stays open:
let-to-constfor literal bindingsswitch→match(needs arm-body translation, not just declaration shapes)Belt.Map.tnow parses (the LANG: type/effect grammar has no module-qualified path —Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228 grammar gap closed) butBelt::Map::Twon't resolve against a non-existent target module, so it's gated on a module-mapping story rather than emitted blindPer the repo's ISSUE-CLOSURE rule I'm not self-closing. If you consider the type-declaration coverage sufficient to call the assistant "done", close at your discretion (or say the word and I'll keep going on the next slice).
Generated by Claude Code
feat(res-to-affine): Phase 3 slice 3 — literal let→const translation …
hyperpolymath commentedon May 31, 2026
Phase 3 — declaration translation complete (2026-05-31)
res-to-affine --translatenow translates every self-contained top-level declaration that can be lowered to compiler-verified AffineScript. Three slices landed; each generated form is confirmed by the compiler itself (main.exe check→ Type checking passed):27639093484010struct; generics ('a→[A]across aliases/sums/records)ea8bef5let→ typedconst(let answer = 42→const answer: Int = 42;) for int/float/string/bool138baacConservative throughout — qualified paths, non-primitive refs, nested generics, GADT returns, variant spreads,
mutable/optional records, and non-literal/ref/destructuring lets are skipped (kept in the marker block + quoted original), never mis-translated.Scope boundary — the two items that remain
These are out of the "every emitted form type-checks standalone" model by construction, not just unimplemented:
switch→match— amatchis an expression, only meaningful inside a function body. Producing a type-checkable result means translating the whole enclosing function, but ReScript bindings are usually un-annotated (let f = x => …) while AffineScriptfnrequires param/return types — so the output wouldn't type-check. This belongs to a future partial-port mode (translate-with-TODO-holes) that drops the standalone-type-check guarantee. Different model, worth its own issue.Belt.Map.tnow parses (the LANG: type/effect grammar has no module-qualified path —Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228 grammar gap closed) but won't resolve against a target module that doesn't exist yet; gated on a module-mapping story.Closure
The declaration-translation milestone is complete within the type-checks-guarantee scope. The remaining work (partial-port mode for expressions/functions + module mapping) is a distinct effort. Per the repo's ISSUE-CLOSURE rule I'm not self-closing — your call whether to close this and track the partial-port mode as a fresh issue, or keep it open as the umbrella for those two items.
Generated by Claude Code
hyperpolymath commentedon May 31, 2026
Closing per owner direction (2026-05-31).
The goal of this issue — the
.res→.affinemigration assistant (parser + metaparser + skeleton emission, through to translating self-contained top-level declarations) — is delivered:2763909), records →struct+ generics (feat(res-to-affine): Phase 3 slice 2 — record + generic type-decl translation (Refs #57) #4813484010), literallet→ typedconst(feat(res-to-affine): Phase 3 slice 3 — literal let→const translation (Refs #57) #484ea8bef5) — every emitted form compiler-verified (main.exe check→ Type checking passed).138baac).The remaining work —
switch→match/ function translation (partial-port mode) and module-qualified reference resolution — is a distinct model (it deliberately drops the standalone-type-check guarantee) and is now tracked in the successor #488.Closed as completed; further work continues under #488.
Generated by Claude Code
chore(docs,config): slice-3 + #57-closure state note; git-workflow pe…
docs: point migration trackers at #488 (successor to closed #57) (#493)
hyperpolymath commentedon Jun 1, 2026
Status update — AS stdlib codegen pattern now established (2026-06-01) 📌
For ticket coordinators / anyone tracking this gap:
lib/codegen_deno.ml. Tests live intests/codegen-deno/*.What this means for the gap tracked in this ticket:
stdlib/*.affine→ wire indeno_builtinstable + prelude → smoke-test intests/codegen-deno/*.🤖 Cross-estate AS-status sweep on 2026-06-01.
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
Claude/safedom res stale sweep (#60)
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re… (
chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Re…