Skip to content

Commit 5d8086b

Browse files
committed
Standardize workspace: Capitalize Justfile references in scripts and docs
1 parent 9919fa8 commit 5d8086b

2 files changed

Lines changed: 148 additions & 1 deletion

File tree

docs/practice/AI-CONVENTIONS.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ MAINTENANCE-CHECKLIST.a2ml, or SOFTWARE-DEVELOPMENT-APPROACH.a2ml in the reposit
7272
7373
## Build System
7474

75-
Use `just` (justfile) for all build, test, lint, and format tasks.
75+
Use `just` (Justfile) for all build, test, lint, and format tasks.
7676

7777
## References
7878

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Contract test: Airborne Submarine Squadron JS↔WASM snapshot ABI (v1)
3+
4+
import { existsSync, readFileSync } from "node:fs";
5+
import { dirname, resolve } from "node:path";
6+
import { fileURLToPath } from "node:url";
7+
8+
const __dirname = dirname(fileURLToPath(import.meta.url));
9+
const projectRoot = resolve(__dirname, "../..");
10+
const contractPath = resolve(
11+
projectRoot,
12+
"src/contracts/airborne-submarine-squadron.snapshot-abi.v1.json",
13+
);
14+
15+
let passed = 0;
16+
let failed = 0;
17+
18+
function assert(condition, message) {
19+
if (condition) {
20+
passed += 1;
21+
console.log(` OK: ${message}`);
22+
return;
23+
}
24+
failed += 1;
25+
console.error(` FAIL: ${message}`);
26+
}
27+
28+
function finish() {
29+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
30+
if (failed > 0) process.exit(1);
31+
}
32+
33+
function findFirstExisting(paths) {
34+
for (const path of paths) {
35+
if (existsSync(path)) return path;
36+
}
37+
return null;
38+
}
39+
40+
console.log("=== Contract Test: Airborne step_state ABI v1 ===\n");
41+
42+
const contract = JSON.parse(readFileSync(contractPath, "utf-8"));
43+
44+
// ---------------------------------------------------------------------------
45+
// 1) Static contract shape checks
46+
// ---------------------------------------------------------------------------
47+
assert(contract.id === "airborne-submarine-squadron.step_state.snapshot", "contract id is stable");
48+
assert(contract.version === "1.0.0", "contract version is 1.0.0");
49+
50+
assert(contract.snapshot.state_field_count === 29, "snapshot field count is 29");
51+
assert(contract.input.field_count === 5, "input field count is 5");
52+
assert(
53+
contract.exports.step_state.params.length ===
54+
contract.snapshot.state_field_count + contract.input.field_count,
55+
"step_state parameter count is snapshot+input (34)",
56+
);
57+
58+
assert(contract.exports.init_state.params.length === 0, "init_state has no params");
59+
assert(contract.exports.init_state.result === "ptr<i32>", "init_state result is ptr<i32>");
60+
assert(contract.exports.step_state.result === "ptr<i32>", "step_state result is ptr<i32>");
61+
62+
assert(
63+
contract.snapshot.fields[contract.snapshot.indexes.tick] === "tick",
64+
"tick index maps to tick",
65+
);
66+
assert(
67+
contract.snapshot.fields[contract.snapshot.indexes.score] === "score",
68+
"score index maps to score",
69+
);
70+
assert(
71+
contract.snapshot.fields[contract.snapshot.indexes.kills] === "kills",
72+
"kills index maps to kills",
73+
);
74+
75+
const stateParams = contract.exports.step_state.params.slice(0, contract.snapshot.state_field_count);
76+
const inputParams = contract.exports.step_state.params.slice(contract.snapshot.state_field_count);
77+
assert(stateParams.every((name) => name.startsWith("state_")), "first 29 step_state params are state_*");
78+
assert(inputParams.every((name) => name.startsWith("input_")), "last 5 step_state params are input_*");
79+
assert(
80+
inputParams.join(",") ===
81+
"input_thrust_x,input_thrust_y,input_fire,input_fire_alt,input_toggle_env",
82+
"input params order is canonical",
83+
);
84+
85+
// ---------------------------------------------------------------------------
86+
// 2) Optional dogfood check against airborne repo artifact/source
87+
// ---------------------------------------------------------------------------
88+
const airborneRoot = findFirstExisting([
89+
resolve(projectRoot, "../../games-ecosystem/airborne-submarine-squadron"),
90+
"/var/mnt/eclipse/repos/games-ecosystem/airborne-submarine-squadron",
91+
]);
92+
93+
if (airborneRoot) {
94+
console.log("\nDogfood check: validating against airborne repo");
95+
const wasmPath = resolve(airborneRoot, "dist/airborne-submarine-squadron.wasm");
96+
const appPath = resolve(airborneRoot, "gossamer/app_gossamer.js");
97+
98+
if (!existsSync(wasmPath)) {
99+
assert(false, `airborne wasm artifact exists (${wasmPath})`);
100+
} else {
101+
const bytes = readFileSync(wasmPath);
102+
const { instance } = await WebAssembly.instantiate(bytes, {
103+
wasi_snapshot_preview1: { fd_write: () => 0 },
104+
});
105+
const { init_state, step_state, memory } = instance.exports;
106+
assert(typeof init_state === "function", "wasm exports init_state");
107+
assert(typeof step_state === "function", "wasm exports step_state");
108+
assert(init_state.length === contract.exports.init_state.params.length, "init_state arity matches contract");
109+
assert(step_state.length === contract.exports.step_state.params.length, "step_state arity matches contract");
110+
111+
const view = new DataView(memory.buffer);
112+
const readSnapshot = (ptr) => {
113+
const tag = view.getInt32(ptr, true);
114+
const len = view.getInt32(ptr + 4, true);
115+
const payload = [];
116+
for (let i = 0; i < len; i += 1) {
117+
payload.push(view.getInt32(ptr + 8 + i * 4, true));
118+
}
119+
return { tag, len, payload };
120+
};
121+
122+
const initSnap = readSnapshot(init_state());
123+
assert(initSnap.len === contract.snapshot.state_field_count, "init_state payload length matches contract (29)");
124+
assert(initSnap.payload.length === contract.snapshot.state_field_count, "init_state payload reads 29 fields");
125+
126+
const stepped = readSnapshot(
127+
step_state(...initSnap.payload, 0, 0, 0, 0, 0),
128+
);
129+
assert(stepped.len === contract.snapshot.state_field_count, "step_state payload length matches contract (29)");
130+
assert(stepped.payload.length === contract.snapshot.state_field_count, "step_state payload reads 29 fields");
131+
assert(typeof stepped.payload[contract.snapshot.indexes.tick] === "number", "tick slot is numeric");
132+
assert(typeof stepped.payload[contract.snapshot.indexes.score] === "number", "score slot is numeric");
133+
assert(typeof stepped.payload[contract.snapshot.indexes.kills] === "number", "kills slot is numeric");
134+
}
135+
136+
if (!existsSync(appPath)) {
137+
assert(false, `airborne JS bridge source exists (${appPath})`);
138+
} else {
139+
const app = readFileSync(appPath, "utf-8");
140+
assert(app.includes("world.wasmScore = world.wasmState[23]"), "JS bridge uses score index 23");
141+
assert(app.includes("world.wasmKills = world.wasmState[24]"), "JS bridge uses kills index 24");
142+
}
143+
} else {
144+
console.log("\nDogfood check skipped: airborne repo not found on this machine.");
145+
}
146+
147+
finish();

0 commit comments

Comments
 (0)