Skip to content

Commit 8682584

Browse files
committed
migrate(phase-c/C1): re-decompose 8 shared/src kernels, 4-gate green
Deep wave 1 of the ReScript -> AffineScript -> wasm migration. Cluster C1 (shared/src leaf modules) re-decomposed into eight pure-integer co-processor kernels staged under proposals/idaptik/migrated/, each verified end-to-end by the 4-gate recipe. Kernels (brain = the integers that cross the wasm boundary; host keeps the strings/floats/DOM/state — the senses): DeviceType 12-device taxonomy: count, validity, code-clamp (lossless) PuzzleFormat difficulty(4)/tier(5) encodings + DLCLoader clamp-to-default PortNames 10 coprocessor-domain port taxonomy (= portnames.wasm, the kernel PortNamesCoprocessor.res's JS bridge already targets) GameEvent alertLevel(4, ordered clamp) + direction(6, flat) Kernel_Compute per-domain element caps + the 504/413/404/0 scheduler gate Kernel the 503/404/0 pre-flight gate + domain -> handler router RetryPolicy the 503/504/429 transient classifier + 3-policy config table Diagnostics the <70/<90/>=90 health-band classifier + registry check Re-decomposed, not transliterated: Promise/async collapsed to the synchronous status decision; service-locator reads (ResourceAccounting, backend registry) threaded as explicit integer params; variants re-encoded as closed integer bands; floats + wall-clock timing kept host-side (timing is a sense). Four gates green (captured in migrated/EVIDENCE.adoc): G1 compile 8/8 -> wasm G2 parity 1223/1223 oracle-vs-wasm inputs (independent JS oracles) G3 echo-boundary 6 Agda proofs, agda exit 0: 3 LOSSLESS (DeviceType, PortNames, GameEvent direction) + 3 CONTROLLED-LOSS (PuzzleFormat tier clamp, GameEvent alert clamp, Kernel route fan-in) — each collision named and certified G4 assail 8/8 clean (no undeclared clamp / unguarded decoder) Three C1 files are host-side "senses" with no extractable brain and stay in the host: Coprocessor_Backends.res (registration wiring), PortNamesCoprocessor .res (JS bridge shim, consumes PortNames.affine), DLCLoader.res (JSON parser; its one integer fragment is delivered as PuzzleFormat.clamp_tier). Staged, not applied — per the plan's access gate, the apply + cutover wait on idaptik write-access (Phase Omega). Ledger: Phase B + Phase C wave 1 DONE; next = C2 (vm/lib/ocaml instruction set). migration-map.json C1 -> DONE. https://claude.ai/code/session_01WoKhFQePiRsAj7aqnxbG8s
1 parent 584af05 commit 8682584

26 files changed

Lines changed: 1429 additions & 4 deletions

proposals/MIGRATION-PLAN.adoc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,11 @@ Heuristic:
185185
[cols="1,1,3",options="header"]
186186
|===
187187
| Phase | State | Notes
188-
| Pre | DONE | #531 echo proof (merged); #532 migration practice + guide + proposals (merged); #533 evangelist toolkit (draft, CI green).
189-
| 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 green (draft, owner to merge). NEXT: PHASE B.
190-
| B | TODO | Full-corpus triage -> migration-map.{adoc,json}.
191-
| C+ | TODO | Deep waves via the 4-gate recipe.
188+
| Pre | DONE | #531 echo proof (merged); #532 migration practice + guide + proposals (merged); #533 evangelist toolkit (*merged* 2026-06-05).
189+
| 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.
190+
| 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.
191+
| 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.
192+
| C2+ | TODO | Remaining deep waves via the 4-gate recipe; next = *C2* (vm/lib/ocaml instruction set, 31 files — the pure-integer reversible VM opcodes). Opus for any novel re-decomposition; Sonnet for the now-rote gate passes (C1 set the pattern). Then C3..C12.
192193
| F+ | TODO | Compiler walls (string backend, then effects).
193194
| Ω | TODO (access-gated) | Cutover + ReScript extinction.
194195
|===
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Compiled wasm is build output, regenerated by every affine-parity run
2+
# (compile:true) from the staged .affine + config. Not committed.
3+
*.wasm
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
//
3+
// DeviceType -- the device-type taxonomy co-processor, the pure-integer core
4+
// extracted from idaptik shared/src/DeviceType.res. Per the DESIGN-VISION
5+
// ("AffineScript is the brain, JS/Pixi the senses; they pass primitives across
6+
// the wasm boundary"), the JS host keeps every device-type STRING (portName,
7+
// fromString) and the per-device mutable defenceFlags record; AffineScript owns
8+
// only the canonical integer encoding of the twelve device kinds and its
9+
// validity.
10+
//
11+
// The ReScript original is a 12-constructor variant `t` plus a `portName`
12+
// switch and a `fromString` parser. We re-decompose the variant as the
13+
// canonical 0..11 integer the constructor order already implies, so the
14+
// taxonomy is a closed band and validity is one range test. The variant IS the
15+
// integer; no strings cross the boundary. (We use range guards rather than an
16+
// enum round-trip because, the band being contiguous, the round-trip would be
17+
// the identity -- the same flat shape PortNames.affine uses for its 0..9 band.)
18+
//
19+
//## Device-type encoding (the header contract for the JS host)
20+
// code device code device
21+
// 0 Laptop 6 Camera
22+
// 1 Desktop 7 AccessPoint
23+
// 2 Server 8 PatchPanel
24+
// 3 Router 9 PowerSupply
25+
// 4 Switch 10 PhoneSystem (PBX)
26+
// 5 Firewall 11 FibreHub
27+
// The encoding is LOSSLESS: twelve distinct kinds map to twelve distinct codes,
28+
// so the host round-trips device-type <-> code with no collision (certified by
29+
// echo-boundary, DeviceTypeBoundary.agda). A code outside 0..11 is not a device
30+
// type: is_valid_device_type reports 0 and clamp_device_type returns the
31+
// out-of-band sentinel -1 -- never an in-band code, so no in-band collision is
32+
// introduced (this is a sentinel, not a clamp; assail stays clean).
33+
34+
// The number of canonical device kinds in the taxonomy.
35+
pub fn device_type_count() -> Int { 12 }
36+
37+
// Whether a host integer names a defined device type. 1 = valid, 0 = out of band.
38+
pub fn is_valid_device_type(code: Int) -> Int {
39+
if code < 0 { 0 } else { if code > 11 { 0 } else { 1 } }
40+
}
41+
42+
// Canonicalise a host integer: identity on a valid 0..11 code, the out-of-band
43+
// sentinel -1 otherwise. -1 is not an in-band code, so out-of-band input can
44+
// never be confused with a real device type (this is a sentinel, not a clamp).
45+
pub fn clamp_device_type(code: Int) -> Int {
46+
if is_valid_device_type(code) == 1 { code } else { -1 }
47+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{-# OPTIONS --safe --without-K #-}
2+
-- SPDX-License-Identifier: MPL-2.0
3+
-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
--
5+
-- GENERATED by echo-boundary (boundary.mjs). Do not edit by hand.
6+
--
7+
-- This module certifies an @boundary encoding-faithfulness verdict for a
8+
-- concrete host-value -> integer-code table, by reusing the
9+
-- EchoEncodingFaithfulness framework. It follows the SHAPE of that module's
10+
-- worked Rank / rankCode / rankCode-injective instance.
11+
--
12+
-- Source table (host name -> code):
13+
-- {
14+
-- "Laptop": 0,
15+
-- "Desktop": 1,
16+
-- "Server": 2,
17+
-- "Router": 3,
18+
-- "Switch": 4,
19+
-- "Firewall": 5,
20+
-- "Camera": 6,
21+
-- "AccessPoint": 7,
22+
-- "PatchPanel": 8,
23+
-- "PowerSupply": 9,
24+
-- "PhoneSystem": 10,
25+
-- "FibreHub": 11
26+
-- }
27+
28+
module DeviceTypeBoundary where
29+
30+
open import Echo using (Echo)
31+
open import EchoImageFactorization using (Injective)
32+
open import EchoEncodingFaithfulness using (Encoding; module EncodingTheorems)
33+
34+
open import Data.Nat.Base using (ℕ)
35+
open import Data.Product.Base using (Σ; _,_; _×_; proj₁)
36+
open import Relation.Binary.PropositionalEquality
37+
using (_≡_; _≢_; refl)
38+
open import Relation.Nullary using (¬_)
39+
40+
41+
-- The host-side enumerated value (one constructor per table name).
42+
-- c0 = Laptop
43+
-- c1 = Desktop
44+
-- c2 = Server
45+
-- c3 = Router
46+
-- c4 = Switch
47+
-- c5 = Firewall
48+
-- c6 = Camera
49+
-- c7 = AccessPoint
50+
-- c8 = PatchPanel
51+
-- c9 = PowerSupply
52+
-- c10 = PhoneSystem
53+
-- c11 = FibreHub
54+
data Host : Set where
55+
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 : Host
56+
57+
-- The integer code the pure kernel computes on.
58+
code : Host
59+
code c0 = 0
60+
code c1 = 1
61+
code c2 = 2
62+
code c3 = 3
63+
code c4 = 4
64+
code c5 = 5
65+
code c6 = 6
66+
code c7 = 7
67+
code c8 = 8
68+
code c9 = 9
69+
code c10 = 10
70+
code c11 = 11
71+
72+
-- Injectivity by exhaustive case analysis. The codes are literal
73+
-- numerals, so listing the diagonal refl clauses is enough: Agda's
74+
-- coverage checker discharges the off-diagonal pairs as absurd
75+
-- numeral equalities (mirrors rankCode-injective).
76+
code-injective : Injective code
77+
code-injective {c0} {c0} _ = refl
78+
code-injective {c1} {c1} _ = refl
79+
code-injective {c2} {c2} _ = refl
80+
code-injective {c3} {c3} _ = refl
81+
code-injective {c4} {c4} _ = refl
82+
code-injective {c5} {c5} _ = refl
83+
code-injective {c6} {c6} _ = refl
84+
code-injective {c7} {c7} _ = refl
85+
code-injective {c8} {c8} _ = refl
86+
code-injective {c9} {c9} _ = refl
87+
code-injective {c10} {c10} _ = refl
88+
code-injective {c11} {c11} _ = refl
89+
90+
encoding : Encoding
91+
encoding = record
92+
{ Source = Host
93+
; Code =
94+
; enc = code
95+
}
96+
97+
-- Re-export the lossless-half headline, instantiated and proved:
98+
-- every integer code determines its host value uniquely.
99+
module Theorems = EncodingTheorems encoding
100+
101+
boundary-lossless :
102+
(i : ℕ) (e₁ e₂ : Echo code i) proj₁ e₁ ≡ proj₁ e₂
103+
boundary-lossless = Theorems.encoding-lossless code-injective
104+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// hypatia: allow cicd_rules/javascript_detected -- Deno trial component for nextgen-evangelist; production target is Rust/AffineScript (see proposals/nextgen-evangelist/README.adoc)
3+
//
4+
// affine-parity config for DeviceType.affine (idaptik device-type taxonomy;
5+
// scalar i32 ABI). The oracle re-derives the 0..11 closed band in plain JS so a
6+
// codegen regression surfaces as a differential mismatch.
7+
const valid = (c) => c >= 0 && c <= 11;
8+
export default {
9+
affine: "DeviceType.affine",
10+
cases: [
11+
{ name: "device_type_count()", export: "device_type_count", args: [], oracle: () => 12 },
12+
{
13+
name: "is_valid_device_type over [-3..15]",
14+
export: "is_valid_device_type",
15+
args: [[-3, 15]],
16+
oracle: (c) => (valid(c) ? 1 : 0),
17+
},
18+
{
19+
name: "clamp_device_type over [-3..15]",
20+
export: "clamp_device_type",
21+
args: [[-3, 15]],
22+
oracle: (c) => (valid(c) ? c : -1),
23+
},
24+
],
25+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
//
3+
// Diagnostics -- the device-health band classifier + registry sanity check, the
4+
// pure-integer core extracted from idaptik shared/src/Diagnostics.res. The
5+
// ReScript original wraps raw float utilisation percentages and per-backend
6+
// stats into typed report structures and formatted strings; those reports and
7+
// strings are host-side (the senses). What IS pure integer is the threshold
8+
// classification `healthOf` and the `allDomainsRegistered` count check.
9+
//
10+
// We re-decompose `healthOf : float -> healthLevel` onto an integer utilisation
11+
// percentage 0..100 (the host floors the float before crossing the boundary --
12+
// Math.floor is a sense). The three bands and their cutoffs are unchanged.
13+
//
14+
//## Health band encoding (Diagnostics.healthLevel)
15+
// band level range
16+
// 0 Healthy pct < 70
17+
// 1 Warning 70 <= pct < 90
18+
// 2 Critical pct >= 90 (>= 95 is the hard 503 cutoff)
19+
// health_of is deliberately many-to-one (a whole range folds onto one band);
20+
// that is the intended classification, not an encoding, so no injectivity is
21+
// claimed for it. The expected full registry is exactly ten domain backends.
22+
23+
pub fn health_band_count() -> Int { 3 }
24+
pub fn expected_domain_count() -> Int { 10 }
25+
26+
// Classify an integer utilisation percentage into a health band. Negative input
27+
// (impossible from a real percentage, but total here) lands in Healthy; >=90
28+
// in Critical, mirroring the ReScript `< 70 / < 90 / else` ladder.
29+
pub fn health_of(pct: Int) -> Int {
30+
if pct < 70 { return 0; }
31+
if pct < 90 { return 1; }
32+
2
33+
}
34+
35+
// 1 iff all ten expected domain backends are registered (Diagnostics
36+
// .allDomainsRegistered: registeredBackendCount() == 10).
37+
pub fn all_domains_registered(count: Int) -> Int {
38+
if count == expected_domain_count() { 1 } else { 0 }
39+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// hypatia: allow cicd_rules/javascript_detected -- Deno trial component for nextgen-evangelist; production target is Rust/AffineScript (see proposals/nextgen-evangelist/README.adoc)
3+
//
4+
// affine-parity config for Diagnostics.affine (idaptik health-band classifier +
5+
// registry sanity check; scalar i32 ABI). Oracle re-derives the <70 / <90 / else
6+
// ladder and the ==10 registry check.
7+
export default {
8+
affine: "Diagnostics.affine",
9+
cases: [
10+
{ name: "health_band_count()", export: "health_band_count", args: [], oracle: () => 3 },
11+
{ name: "expected_domain_count()", export: "expected_domain_count", args: [], oracle: () => 10 },
12+
{
13+
name: "health_of over [-5..105]",
14+
export: "health_of",
15+
args: [[-5, 105]],
16+
oracle: (pct) => (pct < 70 ? 0 : pct < 90 ? 1 : 2),
17+
},
18+
{
19+
name: "all_domains_registered over [0..15]",
20+
export: "all_domains_registered",
21+
args: [[0, 15]],
22+
oracle: (count) => (count === 10 ? 1 : 0),
23+
},
24+
],
25+
};
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= Cluster C1 — four-gate evidence (captured 2026-06-05)
4+
:toc: macro
5+
6+
[IMPORTANT]
7+
====
8+
*Captured run of the 4-gate recipe over the eight cluster-C1 pure-integer
9+
kernels.* Every number here is reproducible from this directory; the
10+
re-run commands are in `README.adoc`. Toolchain: AffineScript compiler
11+
`_build/default/bin/main.exe`, Deno 2.8.2, Agda 2.6.3, echo mirror at
12+
`/tmp/mirror-boundary`.
13+
====
14+
15+
toc::[]
16+
17+
== Summary
18+
19+
[cols="2,1,1,1,1",options="header"]
20+
|===
21+
| Kernel | G1 compile | G2 parity | G3 boundary | G4 assail
22+
| `DeviceType` | OK | 39/39 | LOSSLESS (12) | clean
23+
| `PuzzleFormat` | OK | 74/74 | CONTROLLED-LOSS @0 | clean
24+
| `PortNames` | OK | 52/52 | LOSSLESS (10) | clean
25+
| `GameEvent` | OK | 50/50 | LOSSLESS (6) + CL @0 | clean
26+
| `Kernel_Compute` | OK | 736/736 | (decision; see note) | clean
27+
| `Kernel` | OK | 19/19 | CONTROLLED-LOSS @4 | clean
28+
| `RetryPolicy` | OK | 124/124 | (decision; see note) | clean
29+
| `Diagnostics` | OK | 129/129 | (decision; see note) | clean
30+
| *Total* | *8/8* | *1223/1223* | *6 proofs, agda exit 0* | *8/8 clean*
31+
|===
32+
33+
== Gate 1 — compile (`main.exe compile … -o …`)
34+
35+
All eight `.affine` sources lower to wasm, exit 0:
36+
37+
----
38+
[OK] DeviceType [OK] GameEvent [OK] RetryPolicy
39+
[OK] PuzzleFormat [OK] Kernel_Compute [OK] Diagnostics
40+
[OK] PortNames [OK] Kernel
41+
----
42+
43+
== Gate 2 — parity (oracle-vs-wasm, scalar i32 ABI)
44+
45+
Each kernel swept against an *independent* JS oracle (re-derived in the
46+
`*.config.mjs`, never reusing AffineScript code) so a codegen regression
47+
surfaces as a differential mismatch. 1223 inputs, 0 mismatches.
48+
49+
----
50+
DeviceType ........ 39/39 Kernel_Compute ... 736/736
51+
PuzzleFormat ...... 74/74 Kernel ........... 19/19
52+
PortNames ......... 52/52 RetryPolicy ...... 124/124
53+
GameEvent ......... 50/50 Diagnostics ...... 129/129
54+
----
55+
56+
`Kernel_Compute.compute_gate` is the widest sweep: the full
57+
domain × active_calls × data_len × has_backend grid (10 × 4 × 9 × 2 = 720),
58+
exercising the 504 > 413 > 404 > 0 precedence at every boundary.
59+
60+
== Gate 3 — echo-boundary (Agda-certified encoding faithfulness)
61+
62+
Six host-value→code tables certified against the `EchoEncodingFaithfulness`
63+
framework; every generated module typechecks (`agda exit 0`). The generated
64+
proofs are staged next to each kernel (`*Boundary.agda`).
65+
66+
[cols="2,1,3",options="header"]
67+
|===
68+
| Module | Verdict | What it certifies
69+
| `DeviceTypeBoundary` | LOSSLESS | 12 device kinds → 12 distinct codes; host round-trips device ↔ code.
70+
| `PortNamesBoundary` | LOSSLESS | 10 coprocessor domains → ids 0..9; the portnames.wasm bridge contract.
71+
| `GameEventDirectionBoundary` | LOSSLESS | 6 directions → codes 0..5.
72+
| `PuzzleFormatTierBoundary` | CONTROLLED LOSS | DLCLoader's `parseTier` clamp: unknown numeral collapses to `Tier0` (code 0). Collision certified → no section.
73+
| `GameEventAlertBoundary` | CONTROLLED LOSS | ordered alert clamp: below-Green collapses to Green (code 0). Collision certified → no section.
74+
| `KernelRouteBoundary` | CONTROLLED LOSS | the router's compute fan-in: six domains share handler class 4. Deliberate forgetting, certified → no section.
75+
|===
76+
77+
*Note (decision kernels).* `Kernel_Compute.compute_gate`,
78+
`RetryPolicy.is_transient` and `Diagnostics.health_of` are intentionally
79+
many-to-one *decisions*, not encodings — they classify rather than name, so no
80+
injectivity is claimed for them and echo-boundary's encoding lens does not
81+
apply. Their faithfulness is established by Gate 2 (full-grid parity against an
82+
independent oracle). Where one of these kernels *does* carry an encoding (the
83+
shared domain code, the alert/tier taxonomies), that encoding is certified
84+
above.
85+
86+
== Gate 4 — affine-assail (no undeclared clamp / unguarded decoder)
87+
88+
All eight kernels scan clean (exit 0):
89+
90+
----
91+
[CLEAN] DeviceType [CLEAN] Kernel_Compute
92+
[CLEAN] PuzzleFormat [CLEAN] Kernel
93+
[CLEAN] PortNames [CLEAN] RetryPolicy
94+
[CLEAN] GameEvent [CLEAN] Diagnostics
95+
----
96+
97+
*On clamps and the gate-4 signal.* The in-band clamps in this cluster
98+
(`PuzzleFormat.clamp_tier`/`clamp_difficulty` → 0, `GameEvent.clamp_alert_level`
99+
→ 0/3) are written *guard-style* — an explicit validity test then the default —
100+
rather than as a catch-all `match` arm. assail's `PA-AFF-001` heuristic targets
101+
the match-arm shape (e.g. SecurityRank's `Invalid(v) => …`, which it correctly
102+
flags as a declared clamp); guard-style clamps present no catch-all arm and so
103+
scan clean. This is sound here because the controlled-loss of every one of these
104+
clamps is *independently certified by Gate 3* (the `…Boundary.agda` proofs
105+
above). Gate 3 (the clamp is controlled-loss-certified) and Gate 4 (no
106+
*undeclared* match-arm clamp) together account for every boundary in the
107+
cluster. The out-of-band *sentinels* (`-1` in DeviceType/PortNames/GameEvent/
108+
PuzzleFormat) are not clamps — they map outside the in-band range, so no in-band
109+
collision is introduced and nothing is lost.
110+
111+
== Host-side files (no pure-integer brain — the "senses")
112+
113+
Three C1 files carry no extractable integer core and stay host-side:
114+
115+
* `Coprocessor_Backends.res` — backend-registration wiring (ten `register()`
116+
side-effects; the order is the only datum, and it is fixed).
117+
* `PortNamesCoprocessor.res` — the JS-bridge binding shim that *consumes*
118+
`PortNames.affine` (→ portnames.wasm). The shim stays; the kernel it binds is
119+
delivered here.
120+
* `DLCLoader.res` — a JSON parser/serialiser (`Js.String2.*` / `Js.Json.*`
121+
throughout). Its one integer fragment, `parseTier`'s numeral clamp, is
122+
delivered as `PuzzleFormat.clamp_tier` (certified controlled-loss above); the
123+
rest is irreducibly string/JSON host work. (The Phase-B migratability tool
124+
bucketed this MIGRATABLE-NOW because it scans the modern `String.*` API, not
125+
the legacy `Js.String2.*` this file uses — recorded here as a triage erratum.)

0 commit comments

Comments
 (0)