feat(svalinn): ReScript→AffineScript/typed-wasm migration (phase 1)#46
Merged
Merged
Conversation
Per the decision to move svalinn off ReScript onto AffineScript (compiles to typed WasmGC via hyperpolymath/affinescript, ABI from hyperpolymath/typed-wasm), this lands the build pipeline + host bridge and the first faithful module ports. Pipeline: - Containerfile: 4-stage build — (A) build the affinescript OCaml compiler, (B) build typed-wasm (Rust/Idris2 ABI), (C) compile every src/*.affine → dist/wasm, (D) minimal Deno runtime hosting the WASM. Upstream tool repos pinned by commit. - deno.json: rescript tasks/imports replaced with the affine build tasks and the @hyperpolymath/affine-js bridge. - scripts/affine-build.sh: standalone .affine→.wasm compile driver. - src/host/affine_host.js: Deno host that owns the HTTP listener and supplies every `extern` (JSON arena, file/env, WASI fd_write stub), loading WASM via the affine-js bridge. Plain JS to honour svalinn's language policy (TS banned; JS allowed for Deno-API glue). Ported modules (faithful, idiomatic AffineScript): - src/host/Json.affine host-boundary JSON value protocol - src/gateway/GatewayTypes.affine data types (variant tags → enum + conv) - src/policy/PolicyEngine.affine full pure evaluation core + JSON - src/Main.affine WASM entrypoint + handle_evaluate handler Remaining ~27 modules (auth/JWT/OAuth2, gateway/router/rate-limiter/ metrics, mcp, vordr, validation/Ajv, bridge, ui/*) are explicit follow-up phases tracked in the PR; their routes return 501 until ported. .res files are kept until each module's .affine port lands so the tree never enters a broken half-state. Caveat: the affinescript compiler is alpha (0.1.0-alpha.1) and the OCaml/opam toolchain is unavailable in this environment, so the WASM build was not locally validated; it is exercised by the container build / CI. Documented in the PR. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
Two issues in one: 1. .gitignore `/src/**/*.js` (there to ignore ReScript-generated .res.js) silently excluded src/host/affine_host.js from the phase-1 commit, so the Containerfile entrypoint pointed at an untracked file. Add a negation for src/host/ — that JS is hand-written Deno-API glue, not compiler output. 2. affine_host.js: expand the `put` arrow body's comma operator into a plain function (SonarQube new-issue flag); behaviour unchanged. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
…etrics) Ports three gateway support modules to AffineScript, keeping the established WASM/host boundary (pure logic in .affine; state, clock and I/O in the Deno host): - gateway/SecurityHeaders.affine — OWASP/CORS/rate-limit/error header sets as pure data; host applies them to every Response. - gateway/RateLimiter.affine — pure sliding-window decision; host owns the per-IP map and supplies the wall clock (new now_ms extern). - gateway/Metrics.affine — pure Prometheus text exposition; host owns the running counters/gauge/histogram totals. Host bridge now: rate-limits every request, applies security headers to all responses, and serves /metrics. Vordr container-count refresh (I/O) remains a follow-up with the vordr port. Remaining: auth/*, gateway/Gateway router, mcp/*, vordr/*, validation/Validation, bridge/SelurBridge, ui/*. .res kept until each port lands. Alpha-compiler / no-local-build caveat unchanged. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
Ports the pure, verifiable parts of the auth subsystem: - auth/AuthTypes.affine — full faithful port: AuthMethod / PermissionAction enums + string conversions, Role/Permission/ ApiKeyInfo/TokenPayload/AuthResult/UserContext structs, default RBAC roles and excluded paths. (ReScript `None` auth method → `NoAuth` to avoid the Option::None collision in AffineScript's prelude.) - auth/Authz.affine — the security decision logic in one place: JWT standard-claim validation (exp/iat/iss/aud), scope check with the svalinn:admin super-scope, group membership, OAuth scope split, API-key prefix strip + expiry, mTLS CN extraction, and the alg→ WebCrypto mapping the host uses for importKey/verify. Inherently non-WASM pieces (JWT signature verification + JWKS fetch, OAuth2 token/refresh/introspect/revoke HTTP, secure random, base64url) remain host responsibilities and are a tracked follow-up to wire into affine_host.js. .res kept until the auth route is fully cut over. Alpha-compiler / no-local-build caveat unchanged. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
SonarCloud is unreachable from this environment (host not in the network allowlist), so the issue was pinpointed by static self-audit. Highest-confidence finding: javascript:S3353 — `let containersActive` is never reassigned (the Vordr container-count refresh that mutates it is a deferred follow-up). Fold it into a mutable `gauges` object so the binding is `const` and the gauge is still updatable in place. Also make the json_parse catch binding explicit (`catch (_e)`) to avoid the bare-catch smell. Behaviour unchanged. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
The previous `catch (_e)` introduced an unused-binding smell (SonarCloud
new-issue count went 1→2). The optional bare `catch {}` is valid and
Sonar-clean. Keep the gauges refactor from the prior commit; only the
catch change is reverted.
https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
The actual SonarCloud new issue was not in affine_host.js but in scripts/affine-build.sh:14 — "Use '[[' instead of '[' for conditional tests" (Code Smell, Major, bash best-practices). The script is #!/usr/bin/env bash with set -euo pipefail, so the bash [[ ]] keyword is valid; swap the single POSIX-test usage. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
- vordr/VordrTypes.affine — faithful port: MCP request/error/response structs, Vörðr param structs, container/image types, and the tool-name constants (must match the Vörðr MCP adapter). Js.Json.t payloads → host Json arena handles. - vordr/Client.affine — the pure JSON-RPC shaping: tools/call envelope, MCP response parse + error/result unwrap, monotonic id, and every per-tool argument builder. The transport (Fetch POST, /health ping) is inherently host I/O and stays in affine_host.js as tracked wiring. Remaining: gateway/Gateway router (the orchestrator, 1219 LOC), mcp/*, validation/Validation, bridge/SelurBridge, bindings/*, ui/*, plus host wiring for auth crypto + vordr transport. .res kept until cutover. Alpha-compiler / no-local-build caveat unchanged. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
"Verification is required" but it is impossible in the Claude sandbox (no OCaml/opam, opam repo + wolfi base off the network allowlist). This workflow is the verification path: in CI (which has network and can run the OCaml toolchain) it builds the pinned upstream affinescript compiler and compiles every svalinn src/**/*.affine to WebAssembly, failing the check on any compile error. Deliberately blocking (no continue-on-error) — unlike the advisory smoke build, this exists to actually verify the ports. Pinned to the same affinescript commit as the svalinn Containerfile. No .res files were deleted: a cutover is only justified once this gate is green, which cannot be asserted from this environment. https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
Packages the full remaining ReScript→AffineScript migration as a runnable task for a local Claude Code CLI (where the OCaml/opam/deno/ docker toolchain exists and the work can actually be verified — the cloud sandbox cannot). Captures: toolchain bring-up with the pinned SHAs, the established architecture/conventions, the 11 done modules, the remaining 20 + host wiring, the cutover, and 6 concrete verification gates that constitute "verified". https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
The ocaml/opam image ships a stale opam index; opam install --deps-only can fail to resolve the affinescript deps without a fresh index. Add `opam update -y`. This is a near-certain CI infra fix; it does not address potential .affine codegen failures, which require the local toolchain to iterate (see AFFINE-MIGRATION-TASK.md gate 1). https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What & why
Migrate svalinn off ReScript→JS onto AffineScript→typed-WasmGC
(
hyperpolymath/affinescriptcompiler; ABI/conventions fromhyperpolymath/typed-wasm), per the decision to remove ReScript entirelyfrom svalinn (no "adaptors" carve-out).
This is a phased migration. This PR is phase 1: the full build
pipeline + host bridge + the first faithful module ports. The remaining
~27 modules are tracked below and land in follow-up commits to this branch.
Toolchain (confirmed)
hyperpolymath/affinescript— OCaml/dune compiler, source ext.affine,CLI
affinescript compile <f> -o <f.wasm>. Pinnedd2875a5.hyperpolymath/typed-wasm— Rust+Idris2, the verified cross-languageWasmGC ABI the emitted modules conform to. Pinned
e90e2d1.@hyperpolymath/affine-jsbridge (samemechanism as
affinescript-deno-test).Architecture
AffineScript→WasmGC has no JS interop, no async, no in-language JSON, and
cannot own sockets. So svalinn becomes: pure logic/types in
.affinesrc/host/affine_host.js, plain JS — svalinn policybans new TS) that owns the HTTP listener and supplies every
extern(JSON arena, file/env, WASI
fd_writestub). This is a re-architecture,not a transpile.
Landed in phase 1
Containerfile(4-stage: OCaml → Rust/Idris2 → wasm → Deno)deno.json,scripts/affine-build.shsrc/host/affine_host.js(listener + externs + affine-js loader)src/host/Json.affinesrc/gateway/GatewayTypes.affinesrc/policy/PolicyEngine.affine(full pure core)src/Main.affine(serve,handle_evaluate)Remaining modules (follow-up phases on this branch)
auth/{AuthTypes,JWT,Middleware,OAuth2},gateway/{Gateway,Metrics,RateLimiter,SecurityHeaders},mcp/{McpClient,McpTypes,Server,Tools},vordr/{Client,VordrTypes},validation/Validation(+ Ajv host extern),bridge/SelurBridge,bindings/*(collapse into host),ui/*(browser WASM),tests/*.Their routes currently return 501 in the host.
.resfiles are keptper-module until each port lands so the tree never enters a broken
half-state.
Honest caveats (please read)
0.1.0-alpha.1, "GameDeveloper's Edition") with codegen issues logged upstream; stapeln's
own
ARCHITECTURE.mdflags it as possibly not production-ready.base are off the network allowlist, so the WASM build could not be
locally compiled/validated here. It is only exercised by the
container build / CI (which can reach
cgr.dev+ GitHub)..affineports are written against the studied upstream syntax &stdlib; expect compiler-shakeout fixes once CI runs the real build.
bridge is the pragmatic boundary that makes it tractable.
Draft until the build is green and more modules are ported.
https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv
Generated by Claude Code