Skip to content

Commit f17bf3a

Browse files
claudehyperpolymath
authored andcommitted
migrate(fan-out C6+C8): 16 combat/enemy + device/network kernels, 4-gate green
Clusters C6 (combat/enemy) and C8 (device/network), re-decomposed to the brain/senses split and staged under proposals/idaptik/migrated/. All 16 kernels re-verified by the parent (not just agent-reported): - 16/16 compile - 34280/34280 parity vs independent ReScript-derived oracles (C6 8185, C8 26095) - 7 echo-boundary LOSSLESS proofs - 16/16 assail-clean Re-verification caught 3 PA-AFF-001 findings the agent reports missed (SecurityAi, SecurityRank, NetworkManager) and fixed them with the established guard-helper clamp declaration. NetworkManager parity held at 2645/2645 after removing the dead Cat enum, confirming semantics preserved. 4th compiler finding: Float->wasm codegen is broadly incomplete (pub-fn exports always type i32; float-literal operands mis-emit; trunc()/float() absent), which drives the *Int.affine parity subsets and keeps floats host-side. C7 (player) drafts were produced but the agent timed out before writing parity configs/evidence; left untracked and unverified, tracked as TODO in the ledger and migration-map. https://claude.ai/code/session_01WoKhFQePiRsAj7aqnxbG8s
1 parent 2a01242 commit f17bf3a

46 files changed

Lines changed: 4399 additions & 3 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

proposals/MIGRATION-PLAN.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ Heuristic:
190190
| 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.
191191
| 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.
192192
| 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.
193+
| 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.
194+
| C7 | TODO | Player cluster: 8 `.affine` drafts staged (CriticalRoll, PlayerAttributes, QCertifications, SkillRank, SkillAbilities, QPrograms, JessicaLoadout, JessicaBackground) but the agent timed out *before* writing any parity config or evidence — *unverified, deliberately NOT landed*. Finish the 4 gates over the existing drafts (independent oracles → parity/boundary/assail), then stage. Drafts left untracked in the working tree as the head-start.
193195
| C2b+ | TODO | C2 wave 2 (needs an array/linear-memory ABI): Mul/Div, the stack/memory opcodes (Push/Pop/Load/Store), control flow (Call/Loop/IfPos/IfZero), I/O (Send/Recv/CoprocessorCall), and the structural VM files (State, VM, SubroutineRegistry, *Coprocessor, bindings). Then C3..C12. The unary-`~` codegen bug is a candidate Phase-F compiler fix.
194196
| F+ | TODO | Compiler walls (string backend, then effects).
195197
| Ω | TODO (access-gated) | Cutover + ReScript extinction.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
//
4+
// CombatFx -- the render-decision co-processor for the combat FX scattered
5+
// across the player, combat and training subsystems. Extracted from
6+
// src/app/combat/CombatFxLogicCoprocessor.res (the wasm binding shim) whose
7+
// upstream implementation lives in the original combatfxlogic.wasm.
8+
//
9+
// Per the DESIGN-VISION ("AffineScript is the brain, Pixi the senses; only
10+
// primitives cross the wasm boundary"), every AnimatedSprite.setTint,
11+
// Container.setAlpha, Text.make, Motion.animate and hex palette entry stays in
12+
// ReScript. This co-processor owns only the scalar arithmetic that decides WHAT
13+
// the FX should be this frame; the host feeds each result straight to a draw
14+
// call, so the observable picture is identical while the decision now lives in
15+
// wasm.
16+
//
17+
//## Compiler note: no trunc/float built-ins in wasm codegen
18+
// `trunc(x)` and `float(n)` resolve at the type-checking stage but are absent
19+
// from the wasm code-generator. Both are replaced by proved loop idioms from
20+
// the estate playbook:
21+
// floor_pos(x) -- counts how many whole 1.0s fit below x (non-negative x)
22+
// int_to_float_s(n) -- counts up from 0.0 n times (small non-negative n)
23+
// These lower correctly via i32/f64 Wasm instructions.
24+
//
25+
//## Boundary contract (the header the JS host relies on)
26+
// flash_cycle(timer, period) -> Int
27+
// floor(timer / period): shared cycle index for both flash routines.
28+
// flash_on_phase(timer, period, on_frac) -> Int
29+
// 1 while within the on-window of the current cycle, else 0.
30+
// Host passes period=0.2, on_frac=0.1 to match PlayerSprite exactly.
31+
// flash_alpha_phase(timer, period) -> Float
32+
// 1.0 on even cycle, 0.3 on odd; host passes period=0.1 (PlayerGraphics).
33+
// flash_alpha_phase_v(timer, period, hi, lo) -> Float
34+
// As flash_alpha_phase but with host-supplied hi/lo alpha values.
35+
// flash_is_even_cycle(timer, period) -> Int
36+
// 1 when cycle index is even, 0 when odd.
37+
// flash_advance(timer, dt) -> Float
38+
// timer + dt: per-frame accumulation while invincibility is active.
39+
// flash_reset() -> Float
40+
// 0.0: timer reset when invincibility expires.
41+
// floattext_start_y(y, off) -> Float
42+
// y - off: spawn Y for damage/pickup floating text (off = 40.0).
43+
// floattext_end_y(y, rise) -> Float
44+
// y - rise: rise/fade target Y (rise = 100.0).
45+
// floattext_rise_distance(start_off, rise) -> Float
46+
// rise - start_off: total vertical travel independent of y (= 60.0 default).
47+
// knockback_pop_y(speed) -> Float
48+
// 0.0 - speed: upward pop magnitude; up is negative Y; host passes 80.0.
49+
50+
//## floor_pos: non-negative float -> integer floor
51+
// Counts how many whole 1.0s fit below x. Used instead of trunc() which is
52+
// absent from the wasm code-generator. For the timer/period quotients here
53+
// (order of magnitude 0..~500) the loop completes in microseconds.
54+
fn floor_pos(x: Float) -> Int {
55+
let mut n = 0;
56+
let mut acc = 0.0;
57+
while acc + 1.0 <= x { acc = acc + 1.0; n = n + 1; }
58+
n
59+
}
60+
61+
//## int_to_float_s: small non-negative Int -> Float
62+
// Counts from 0.0 up to n. Used instead of float() which is absent from the
63+
// wasm code-generator. For the cycle indices here (order of magnitude 0..~500)
64+
// the loop completes in microseconds.
65+
fn int_to_float_s(n: Int) -> Float {
66+
let mut f = 0.0;
67+
let mut i = 0;
68+
while i < n { f = f + 1.0; i = i + 1; }
69+
f
70+
}
71+
72+
//## PlayerSprite / PlayerGraphics -- shared flash cycle index
73+
// Both flash routines compute floor(timer / period): PlayerSprite uses period=0.2
74+
// (the full red-tint cycle), PlayerGraphics uses period=0.1 (the alpha cycle).
75+
// This kernel is the shared implementation; the host chooses the period.
76+
pub fn flash_cycle(timer: Float, period: Float) -> Int {
77+
floor_pos(timer / period)
78+
}
79+
80+
//## PlayerSprite.updateDamageFlash -- red-tint on/off phase
81+
// remainder = timer - floor(timer/period) * period; on when remainder < on_frac.
82+
// Reconstructed without a float modulo (unsupported for variables in this backend)
83+
// using floor_pos + int_to_float_s. The host passes period=0.2, on_frac=0.1 to
84+
// match PlayerSprite exactly (first half of each 0.2 s cycle is the on-window).
85+
pub fn flash_on_phase(timer: Float, period: Float, on_frac: Float) -> Int {
86+
let cycles = floor_pos(timer / period);
87+
let remainder = timer - int_to_float_s(cycles) * period;
88+
if remainder < on_frac { return 1; }
89+
0
90+
}
91+
92+
//## PlayerGraphics.updateDamageFlash -- container-alpha flicker (cycle parity)
93+
// alpha = (floor(timer / period) mod 2 == 0) ? 1.0 : 0.3.
94+
// Returns the f64 alpha directly so the host hands it straight to Container.setAlpha.
95+
// The host passes period=0.1 to match PlayerGraphics exactly.
96+
pub fn flash_alpha_phase(timer: Float, period: Float) -> Float {
97+
let cycle = floor_pos(timer / period);
98+
if cycle % 2 == 0 { return 1.0; }
99+
0.3
100+
}
101+
102+
//## PlayerGraphics.updateDamageFlash -- alpha flicker, host-supplied magnitudes
103+
// As flash_alpha_phase but with the two alpha values passed by the host, in case
104+
// a render branch wants different on/off opacities while keeping the same parity
105+
// decision. hi on even cycles, lo on odd.
106+
pub fn flash_alpha_phase_v(timer: Float, period: Float, hi: Float, lo: Float) -> Float {
107+
let cycle = floor_pos(timer / period);
108+
if cycle % 2 == 0 { return hi; }
109+
lo
110+
}
111+
112+
//## PlayerGraphics.updateDamageFlash -- the parity flag alone
113+
// The discrete even/odd decision behind the alpha choice, returned as an Int flag
114+
// for hosts that own both alpha magnitudes and only need the selector.
115+
pub fn flash_is_even_cycle(timer: Float, period: Float) -> Int {
116+
let cycle = floor_pos(timer / period);
117+
if cycle % 2 == 0 { return 1; }
118+
0
119+
}
120+
121+
//## PlayerSprite / PlayerGraphics -- effect-lifetime timer accumulation
122+
// While the invincibility timer is positive both routines accumulate
123+
// damageFlashTimer += deltaTime each frame; this kernel is that step. The host
124+
// owns the > 0.0 guard and the mutable field; the brain owns the addition.
125+
pub fn flash_advance(timer: Float, dt: Float) -> Float {
126+
timer + dt
127+
}
128+
129+
//## PlayerSprite / PlayerGraphics -- flash-timer reset
130+
// When the invincibility timer reaches zero both routines reset damageFlashTimer
131+
// to 0.0 (and the host restores the base tint / full alpha). The value is a
132+
// constant, lifted so the reset point is the same total kernel as the advance.
133+
pub fn flash_reset() -> Float {
134+
0.0
135+
}
136+
137+
//## ScavengerTraining.showFloatingText -- spawn Y
138+
// The floating damage/pickup text is created at y - 40.0 (Text.setY(floatText,
139+
// y - 40.0)). Generalised over the spawn offset; the host passes 40.0.
140+
pub fn floattext_start_y(y: Float, off: Float) -> Float {
141+
y - off
142+
}
143+
144+
//## ScavengerTraining.showFloatingText -- rise/fade target Y
145+
// Motion.animate drives the text to y - 100.0 with alpha 0.0 over 1.5 s. This is
146+
// the target Y of that tween; generalised over the rise, the host passes 100.0.
147+
pub fn floattext_end_y(y: Float, rise: Float) -> Float {
148+
y - rise
149+
}
150+
151+
//## ScavengerTraining.showFloatingText -- total vertical travel
152+
// The distance the text drifts upward over its lifetime: (y - start_off) minus
153+
// (y - rise) = rise - start_off. Independent of y, so the host can size the tween
154+
// or a pooled effect without re-deriving it. With 40.0 / 100.0 this is 60.0.
155+
pub fn floattext_rise_distance(start_off: Float, rise: Float) -> Float {
156+
rise - start_off
157+
}
158+
159+
//## PlayerHP.takeDamage -- upward knockback pop
160+
// On taking a hit the player gets a slight upward pop alongside the signed
161+
// horizontal knockback (already migrated in PlayerHp). The original
162+
// hard-codes knockbackVelY = -80.0; lifted as the negation of a host-supplied
163+
// speed so the FX co-processor owns the sign convention (up is negative Y).
164+
pub fn knockback_pop_y(speed: Float) -> Float {
165+
0.0 - speed
166+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
//
4+
// CombatFxInt -- Integer-input parity subset of CombatFx.affine.
5+
// For flash_cycle, flash_on_phase, flash_is_even_cycle: timer and period are
6+
// passed as whole-number integers. The division timer/period is integer division
7+
// which equals floor(timer/period) for non-negative integers, matching the
8+
// Float-based floor_pos logic for these test inputs.
9+
10+
pub fn flash_cycle_int(timer: Int, period: Int) -> Int {
11+
timer / period
12+
}
13+
14+
pub fn flash_on_phase_int(timer: Int, period: Int, on_frac: Int) -> Int {
15+
let cycles = timer / period;
16+
let remainder = timer - cycles * period;
17+
if remainder < on_frac { return 1; }
18+
0
19+
}
20+
21+
pub fn flash_is_even_cycle_int(timer: Int, period: Int) -> Int {
22+
let cycle = timer / period;
23+
if cycle % 2 == 0 { return 1; }
24+
0
25+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
//
4+
// affine-parity config for CombatFx.affine (Int-only parity subset).
5+
// Points at CombatFxInt.affine which exposes only the Int-returning exports.
6+
// Oracles re-derive from CombatFxLogicCoprocessor.res semantics in plain JS.
7+
8+
export default {
9+
affine: "CombatFxInt.affine",
10+
compile: false,
11+
// Integer-input variants: timer/period as whole-number integers.
12+
// Integer division timer/period == floor(timer/period) for non-negative inputs,
13+
// matching the Float floor_pos logic for these test cases.
14+
cases: [
15+
{
16+
name: "flash_cycle_int(timer, period): integer division = floor(float/float)",
17+
export: "flash_cycle_int",
18+
args: [
19+
{ values: [0, 1, 2, 3, 4, 5, 6, 8, 10] },
20+
{ values: [1, 2] },
21+
],
22+
oracle: (timer, period) => Math.floor(timer / period),
23+
},
24+
{
25+
name: "flash_on_phase_int(timer, period, on_frac): 1 within on-window",
26+
export: "flash_on_phase_int",
27+
args: [
28+
{ values: [0, 1, 2, 3, 4, 5, 6, 8, 10] },
29+
{ values: [1, 2] },
30+
{ values: [0, 1, 2] },
31+
],
32+
oracle: (timer, period, on_frac) => {
33+
const cycles = Math.floor(timer / period);
34+
const remainder = timer - cycles * period;
35+
return remainder < on_frac ? 1 : 0;
36+
},
37+
},
38+
{
39+
name: "flash_is_even_cycle_int(timer, period): 1 if even cycle",
40+
export: "flash_is_even_cycle_int",
41+
args: [
42+
{ values: [0, 1, 2, 3, 4, 5, 6, 8, 10] },
43+
{ values: [1, 2] },
44+
],
45+
oracle: (timer, period) => Math.floor(timer / period) % 2 === 0 ? 1 : 0,
46+
},
47+
],
48+
};

0 commit comments

Comments
 (0)