@its-not-rocket-science/ananke 0.1.63 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/conformance/README.md +44 -0
- package/conformance/bridge-snapshot.json +40 -0
- package/conformance/command-round-trip.json +47 -0
- package/conformance/lockstep-sequence.json +396 -0
- package/conformance/replay-parity.json +66 -0
- package/conformance/state-hash.json +23 -0
- package/dist/src/conformance.d.ts +108 -0
- package/dist/src/conformance.js +13 -0
- package/dist/src/content-pack.d.ts +68 -1
- package/dist/src/content-pack.js +158 -0
- package/dist/tools/pack-cli.js +9 -0
- package/package.json +8 -1
- package/schema/pack.schema.json +45 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,48 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.65] — 2026-04-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **PM-6 — Content-Pack Registry Format (complete):**
|
|
14
|
+
- `PackRegistryMeta` interface (new): optional `registry` block in `AnankePackManifest` with fields:
|
|
15
|
+
- `compatRange` (string): semver range enforced at runtime by `validatePack` — rejects packs incompatible with the running engine version.
|
|
16
|
+
- `stabilityTier` (`"stable"` | `"experimental"` | `"internal"`): controls listing in a public registry.
|
|
17
|
+
- `requiredExports` (string[]): subpath exports the pack's content depends on — informational.
|
|
18
|
+
- `checksum` (string): SHA-256 hex digest of the pack JSON — computed by `npx ananke pack bundle`, verified by the host.
|
|
19
|
+
- `license` (string): SPDX identifier.
|
|
20
|
+
- `provenance` (object[]): dataset / paper references for empirically grounded content.
|
|
21
|
+
- `PackStabilityTier` and `PackProvenanceRef` types (new, exported from `"./content-pack"`).
|
|
22
|
+
- `ANANKE_ENGINE_VERSION = "0.1.65"` constant (new, exported from `"./content-pack"`): current engine version used in `compatRange` evaluation.
|
|
23
|
+
- `semverSatisfies(version, range)` (new, exported): lightweight semver range evaluator — supports `>=`, `>`, `<=`, `<`, `=`, `^` (caret), `~` (tilde), bare version, and compound space-separated ranges. No external dependencies.
|
|
24
|
+
- `validatePack` extended to validate all registry sub-fields and reject incompatible `compatRange`.
|
|
25
|
+
- `tools/pack-cli.ts` `bundle` command: automatically computes SHA-256 checksum and embeds it in `registry.checksum` before writing the bundle.
|
|
26
|
+
- `schema/pack.schema.json`: `registry` block with full JSON Schema definition for all sub-fields.
|
|
27
|
+
- `docs/pack-registry-spec.md` (new): full specification — field reference, checksum algorithm, runtime enforcement table, future online registry design.
|
|
28
|
+
- 24 new tests (5,593 total). Coverage: 97.11%/88.07%/95.83%/97.11%. Build: clean.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## [0.1.64] — 2026-04-01
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **PM-5 — Deterministic Conformance Suite (complete):**
|
|
37
|
+
- `conformance/` directory (published in package): 5 self-contained JSON fixture files that any third-party host SDK can use to verify deterministic compatibility with the reference TypeScript engine.
|
|
38
|
+
- `state-hash.json`: given a canonical `WorldState`, `hashWorldState` must return specific hex values at tick 0 and tick 1.
|
|
39
|
+
- `replay-parity.json`: re-simulating a recorded 10-tick replay with `replayTo` must reproduce the per-tick hash trace exactly. Uses `noMove` commands to avoid AI-state side-effect divergence.
|
|
40
|
+
- `command-round-trip.json`: verifies `SCALE` constants and `CommandMap` JSON serialisation round-trips without loss for all 4 command kinds.
|
|
41
|
+
- `bridge-snapshot.json`: `serializeBridgeFrame` must produce a `BridgeFrame` with the correct schema, tick, entity count, and entity IDs.
|
|
42
|
+
- `lockstep-sequence.json`: stepping the simulation 20 ticks with `lineInfantry` AI must produce matching entity positions, dead flags, shock values, and world-state hashes at each tick.
|
|
43
|
+
- `tools/generate-conformance-fixtures.ts`: reference fixture generator; run `npm run generate-conformance-fixtures` to regenerate after changes to the hash algorithm, `stepWorld`, or `serializeBridgeFrame`.
|
|
44
|
+
- `tools/conformance-runner.ts`: standalone runner with `--json` (machine-readable) and `--fixture=<kind>` (single fixture) flags; exits 0 on all-pass, 1 on any failure.
|
|
45
|
+
- `src/conformance.ts` (new subpath export `"./conformance"`): public TypeScript types for third-party runners — `FixtureKind`, `ConformanceFixtureHeader`, `StateHashFixture`, `ReplayParityFixture`, `CommandRoundTripFixture`, `BridgeSnapshotFixture`, `LockstepSequenceFixture`, `ConformanceResult`, `ConformanceSummary`, and `CONFORMANCE_VERSION`.
|
|
46
|
+
- npm scripts: `generate-conformance-fixtures`, `conformance-runner`.
|
|
47
|
+
- 0 new tests (5,569 total). Coverage: 97.11%/88.08%/95.82%/97.11%. Build: clean.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
9
51
|
## [0.1.63] — 2026-04-01
|
|
10
52
|
|
|
11
53
|
### Added
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Ananke — Conformance Fixtures
|
|
2
|
+
|
|
3
|
+
Test fixtures for verifying host-SDK determinism. Any implementation that
|
|
4
|
+
passes all fixtures is guaranteed to produce the same simulation state as the
|
|
5
|
+
reference TypeScript engine.
|
|
6
|
+
|
|
7
|
+
## Fixture files
|
|
8
|
+
|
|
9
|
+
| File | Kind | What it tests |
|
|
10
|
+
|------|------|---------------|
|
|
11
|
+
| `state-hash.json` | `state-hash` | `hashWorldState` output for a known WorldState |
|
|
12
|
+
| `replay-parity.json` | `replay-parity` | Per-tick hash trace when re-simulating a recorded replay |
|
|
13
|
+
| `command-round-trip.json` | `command-round-trip` | CommandMap wire encoding and field semantics |
|
|
14
|
+
| `bridge-snapshot.json` | `bridge-snapshot` | `serializeBridgeFrame` output shape and invariants |
|
|
15
|
+
| `lockstep-sequence.json` | `lockstep-sequence` | Entity positions and shock at each tick of a 20-tick run |
|
|
16
|
+
|
|
17
|
+
## Running the suite
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run build
|
|
21
|
+
npm run conformance-runner # TypeScript reference implementation
|
|
22
|
+
npm run conformance-runner -- --json # machine-readable output
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Integrating from a non-TypeScript host
|
|
26
|
+
|
|
27
|
+
1. Load the fixture JSON.
|
|
28
|
+
2. Reconstruct the initial `WorldState` from the fixture's `input` section.
|
|
29
|
+
3. Step the simulation exactly as described.
|
|
30
|
+
4. Compare your output against the `expected` / `snapshots` / `hashTrace` fields.
|
|
31
|
+
5. A mismatch means your fixed-point arithmetic or RNG seeding diverges.
|
|
32
|
+
|
|
33
|
+
## Fixture format version
|
|
34
|
+
|
|
35
|
+
All fixtures carry `"version": "conformance/v1"`. A breaking change in the
|
|
36
|
+
hash algorithm or wire format will bump to `v2` with a migration note.
|
|
37
|
+
|
|
38
|
+
## Regenerating
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run build && npm run generate-conformance-fixtures
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Re-run after any change to `hashWorldState`, `stepWorld`, or `serializeBridgeFrame`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "conformance/v1",
|
|
3
|
+
"id": "bridge-snapshot-01",
|
|
4
|
+
"description": "serializeBridgeFrame must produce a BridgeFrame with these invariants.",
|
|
5
|
+
"kind": "bridge-snapshot",
|
|
6
|
+
"notes": [
|
|
7
|
+
"Check: frame.schema === 'ananke-bridge/v1'.",
|
|
8
|
+
"Check: frame.tick === 0.",
|
|
9
|
+
"Check: frame.entities.length === 2.",
|
|
10
|
+
"Check: entity positions match input WorldState positions.",
|
|
11
|
+
"The generatedAt timestamp is non-deterministic — do not compare it."
|
|
12
|
+
],
|
|
13
|
+
"input": {
|
|
14
|
+
"seed": 42,
|
|
15
|
+
"tick": 0,
|
|
16
|
+
"entityCount": 2,
|
|
17
|
+
"entityPositions": [
|
|
18
|
+
{
|
|
19
|
+
"id": 1,
|
|
20
|
+
"x_m": -0.5,
|
|
21
|
+
"y_m": 0
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": 2,
|
|
25
|
+
"x_m": 0.5,
|
|
26
|
+
"y_m": 0
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"expected": {
|
|
31
|
+
"schema": "ananke.bridge.frame.v1",
|
|
32
|
+
"tick": 0,
|
|
33
|
+
"entityCount": 2,
|
|
34
|
+
"scenarioId": "conformance-test",
|
|
35
|
+
"entityIds": [
|
|
36
|
+
1,
|
|
37
|
+
2
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "conformance/v1",
|
|
3
|
+
"id": "command-round-trip-01",
|
|
4
|
+
"description": "CommandMap entries must serialise to and from JSON without loss.",
|
|
5
|
+
"kind": "command-round-trip",
|
|
6
|
+
"notes": [
|
|
7
|
+
"CommandMap is Map<entityId, Command[]>.",
|
|
8
|
+
"For wire transport, serialise as [[entityId, commands[]], ...].",
|
|
9
|
+
"All numeric fields are fixed-point integers (SCALE.Q = 10000)."
|
|
10
|
+
],
|
|
11
|
+
"scale": {
|
|
12
|
+
"Q": 10000,
|
|
13
|
+
"kg": 1000,
|
|
14
|
+
"m": 10000,
|
|
15
|
+
"mps": 10000
|
|
16
|
+
},
|
|
17
|
+
"commands": [
|
|
18
|
+
{
|
|
19
|
+
"entityId": 1,
|
|
20
|
+
"kind": "attack",
|
|
21
|
+
"mode": "strike",
|
|
22
|
+
"targetId": 2,
|
|
23
|
+
"intensity_Q": 10000,
|
|
24
|
+
"description": "Full-intensity strike at entity 2"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"entityId": 2,
|
|
28
|
+
"kind": "move",
|
|
29
|
+
"direction_x": 5000,
|
|
30
|
+
"direction_y": 0,
|
|
31
|
+
"direction_z": 0,
|
|
32
|
+
"mode": "walk",
|
|
33
|
+
"description": "Walk toward +x at 0.5 m/s"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"entityId": 1,
|
|
37
|
+
"kind": "defend",
|
|
38
|
+
"mode": "active",
|
|
39
|
+
"description": "Active defence stance"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"entityId": 2,
|
|
43
|
+
"kind": "idle",
|
|
44
|
+
"description": "No action this tick"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "conformance/v1",
|
|
3
|
+
"id": "lockstep-sequence-01",
|
|
4
|
+
"description": "Stepping the simulation must produce identical entity state at each tick.",
|
|
5
|
+
"kind": "lockstep-sequence",
|
|
6
|
+
"notes": [
|
|
7
|
+
"seed=42; 2 entities (lineInfantry AI); 20 ticks.",
|
|
8
|
+
"tractionCoeff = q(0.90) = 9000.",
|
|
9
|
+
"At each tick: verify hashWorldState === hashHex and entity fields match.",
|
|
10
|
+
"x_m values are rounded to 3 decimal places for cross-language comparison.",
|
|
11
|
+
"shock_Q is raw fixed-point (SCALE.Q = 10000); 10000 = 100% shock."
|
|
12
|
+
],
|
|
13
|
+
"context": {
|
|
14
|
+
"tractionCoeff_Q": 9000
|
|
15
|
+
},
|
|
16
|
+
"snapshots": [
|
|
17
|
+
{
|
|
18
|
+
"tick": 0,
|
|
19
|
+
"hashHex": "0xec578e005fcbef34",
|
|
20
|
+
"entities": [
|
|
21
|
+
{
|
|
22
|
+
"id": 1,
|
|
23
|
+
"x_m": -0.5,
|
|
24
|
+
"dead": false,
|
|
25
|
+
"shock_Q": 0
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": 2,
|
|
29
|
+
"x_m": 0.5,
|
|
30
|
+
"dead": false,
|
|
31
|
+
"shock_Q": 0
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"tick": 1,
|
|
37
|
+
"hashHex": "0x94e31245fae65074",
|
|
38
|
+
"entities": [
|
|
39
|
+
{
|
|
40
|
+
"id": 1,
|
|
41
|
+
"x_m": -0.478,
|
|
42
|
+
"dead": false,
|
|
43
|
+
"shock_Q": 0
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": 2,
|
|
47
|
+
"x_m": 0.479,
|
|
48
|
+
"dead": false,
|
|
49
|
+
"shock_Q": 0
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"tick": 2,
|
|
55
|
+
"hashHex": "0x5f433b5815b27103",
|
|
56
|
+
"entities": [
|
|
57
|
+
{
|
|
58
|
+
"id": 1,
|
|
59
|
+
"x_m": -0.433,
|
|
60
|
+
"dead": false,
|
|
61
|
+
"shock_Q": 0
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": 2,
|
|
65
|
+
"x_m": 0.437,
|
|
66
|
+
"dead": false,
|
|
67
|
+
"shock_Q": 0
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"tick": 3,
|
|
73
|
+
"hashHex": "0xacb3ed02702144c4",
|
|
74
|
+
"entities": [
|
|
75
|
+
{
|
|
76
|
+
"id": 1,
|
|
77
|
+
"x_m": -0.366,
|
|
78
|
+
"dead": false,
|
|
79
|
+
"shock_Q": 0
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": 2,
|
|
83
|
+
"x_m": 0.373,
|
|
84
|
+
"dead": false,
|
|
85
|
+
"shock_Q": 0
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"tick": 4,
|
|
91
|
+
"hashHex": "0xd8cd8894e32b5d24",
|
|
92
|
+
"entities": [
|
|
93
|
+
{
|
|
94
|
+
"id": 1,
|
|
95
|
+
"x_m": -0.276,
|
|
96
|
+
"dead": false,
|
|
97
|
+
"shock_Q": 0
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": 2,
|
|
101
|
+
"x_m": 0.289,
|
|
102
|
+
"dead": false,
|
|
103
|
+
"shock_Q": 0
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"tick": 5,
|
|
109
|
+
"hashHex": "0xd5e1600766daf978",
|
|
110
|
+
"entities": [
|
|
111
|
+
{
|
|
112
|
+
"id": 1,
|
|
113
|
+
"x_m": -0.164,
|
|
114
|
+
"dead": false,
|
|
115
|
+
"shock_Q": 0
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": 2,
|
|
119
|
+
"x_m": 0.183,
|
|
120
|
+
"dead": false,
|
|
121
|
+
"shock_Q": 0
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"tick": 6,
|
|
127
|
+
"hashHex": "0xf775238c6b6c2339",
|
|
128
|
+
"entities": [
|
|
129
|
+
{
|
|
130
|
+
"id": 1,
|
|
131
|
+
"x_m": -0.049,
|
|
132
|
+
"dead": false,
|
|
133
|
+
"shock_Q": 0
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": 2,
|
|
137
|
+
"x_m": 0.076,
|
|
138
|
+
"dead": false,
|
|
139
|
+
"shock_Q": 0
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"tick": 7,
|
|
145
|
+
"hashHex": "0xa4adf7d41c67f090",
|
|
146
|
+
"entities": [
|
|
147
|
+
{
|
|
148
|
+
"id": 1,
|
|
149
|
+
"x_m": 0.031,
|
|
150
|
+
"dead": false,
|
|
151
|
+
"shock_Q": 0
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": 2,
|
|
155
|
+
"x_m": 0.004,
|
|
156
|
+
"dead": false,
|
|
157
|
+
"shock_Q": 0
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"tick": 8,
|
|
163
|
+
"hashHex": "0x5f39532dfa0cf053",
|
|
164
|
+
"entities": [
|
|
165
|
+
{
|
|
166
|
+
"id": 1,
|
|
167
|
+
"x_m": 0.168,
|
|
168
|
+
"dead": false,
|
|
169
|
+
"shock_Q": 0
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": 2,
|
|
173
|
+
"x_m": -0.131,
|
|
174
|
+
"dead": false,
|
|
175
|
+
"shock_Q": 0
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"tick": 9,
|
|
181
|
+
"hashHex": "0xa87941a6b840c6d7",
|
|
182
|
+
"entities": [
|
|
183
|
+
{
|
|
184
|
+
"id": 1,
|
|
185
|
+
"x_m": 0.309,
|
|
186
|
+
"dead": false,
|
|
187
|
+
"shock_Q": 0
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": 2,
|
|
191
|
+
"x_m": -0.272,
|
|
192
|
+
"dead": false,
|
|
193
|
+
"shock_Q": 0
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"tick": 10,
|
|
199
|
+
"hashHex": "0x49d767bfed374445",
|
|
200
|
+
"entities": [
|
|
201
|
+
{
|
|
202
|
+
"id": 1,
|
|
203
|
+
"x_m": 0.429,
|
|
204
|
+
"dead": false,
|
|
205
|
+
"shock_Q": 0
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"id": 2,
|
|
209
|
+
"x_m": -0.391,
|
|
210
|
+
"dead": false,
|
|
211
|
+
"shock_Q": 0
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"tick": 11,
|
|
217
|
+
"hashHex": "0xce74d9688b813796",
|
|
218
|
+
"entities": [
|
|
219
|
+
{
|
|
220
|
+
"id": 1,
|
|
221
|
+
"x_m": 0.526,
|
|
222
|
+
"dead": false,
|
|
223
|
+
"shock_Q": 0
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"id": 2,
|
|
227
|
+
"x_m": -0.49,
|
|
228
|
+
"dead": false,
|
|
229
|
+
"shock_Q": 0
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"tick": 12,
|
|
235
|
+
"hashHex": "0x1efbc95cd539db56",
|
|
236
|
+
"entities": [
|
|
237
|
+
{
|
|
238
|
+
"id": 1,
|
|
239
|
+
"x_m": 0.6,
|
|
240
|
+
"dead": false,
|
|
241
|
+
"shock_Q": 0
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"id": 2,
|
|
245
|
+
"x_m": -0.567,
|
|
246
|
+
"dead": false,
|
|
247
|
+
"shock_Q": 0
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"tick": 13,
|
|
253
|
+
"hashHex": "0xd4f1d44a8c11b01e",
|
|
254
|
+
"entities": [
|
|
255
|
+
{
|
|
256
|
+
"id": 1,
|
|
257
|
+
"x_m": 0.652,
|
|
258
|
+
"dead": false,
|
|
259
|
+
"shock_Q": 0
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": 2,
|
|
263
|
+
"x_m": -0.623,
|
|
264
|
+
"dead": false,
|
|
265
|
+
"shock_Q": 0
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"tick": 14,
|
|
271
|
+
"hashHex": "0xc9fd237324abe444",
|
|
272
|
+
"entities": [
|
|
273
|
+
{
|
|
274
|
+
"id": 1,
|
|
275
|
+
"x_m": 0.682,
|
|
276
|
+
"dead": false,
|
|
277
|
+
"shock_Q": 0
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"id": 2,
|
|
281
|
+
"x_m": -0.659,
|
|
282
|
+
"dead": false,
|
|
283
|
+
"shock_Q": 0
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"tick": 15,
|
|
289
|
+
"hashHex": "0xaf66c06fb25eac2d",
|
|
290
|
+
"entities": [
|
|
291
|
+
{
|
|
292
|
+
"id": 1,
|
|
293
|
+
"x_m": 0.689,
|
|
294
|
+
"dead": false,
|
|
295
|
+
"shock_Q": 0
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"id": 2,
|
|
299
|
+
"x_m": -0.672,
|
|
300
|
+
"dead": false,
|
|
301
|
+
"shock_Q": 0
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"tick": 16,
|
|
307
|
+
"hashHex": "0xc6a0cb965ff544d1",
|
|
308
|
+
"entities": [
|
|
309
|
+
{
|
|
310
|
+
"id": 1,
|
|
311
|
+
"x_m": 0.689,
|
|
312
|
+
"dead": false,
|
|
313
|
+
"shock_Q": 0
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"id": 2,
|
|
317
|
+
"x_m": -0.672,
|
|
318
|
+
"dead": false,
|
|
319
|
+
"shock_Q": 0
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"tick": 17,
|
|
325
|
+
"hashHex": "0xc067905d49583c8a",
|
|
326
|
+
"entities": [
|
|
327
|
+
{
|
|
328
|
+
"id": 1,
|
|
329
|
+
"x_m": 0.689,
|
|
330
|
+
"dead": false,
|
|
331
|
+
"shock_Q": 0
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"id": 2,
|
|
335
|
+
"x_m": -0.672,
|
|
336
|
+
"dead": false,
|
|
337
|
+
"shock_Q": 0
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"tick": 18,
|
|
343
|
+
"hashHex": "0x8e104127ec08a2c7",
|
|
344
|
+
"entities": [
|
|
345
|
+
{
|
|
346
|
+
"id": 1,
|
|
347
|
+
"x_m": 0.689,
|
|
348
|
+
"dead": false,
|
|
349
|
+
"shock_Q": 0
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"id": 2,
|
|
353
|
+
"x_m": -0.672,
|
|
354
|
+
"dead": false,
|
|
355
|
+
"shock_Q": 0
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"tick": 19,
|
|
361
|
+
"hashHex": "0xb3468a0cce37d1ea",
|
|
362
|
+
"entities": [
|
|
363
|
+
{
|
|
364
|
+
"id": 1,
|
|
365
|
+
"x_m": 0.667,
|
|
366
|
+
"dead": false,
|
|
367
|
+
"shock_Q": 0
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"id": 2,
|
|
371
|
+
"x_m": -0.651,
|
|
372
|
+
"dead": false,
|
|
373
|
+
"shock_Q": 0
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"tick": 20,
|
|
379
|
+
"hashHex": "0xacb04548fd68a7fb",
|
|
380
|
+
"entities": [
|
|
381
|
+
{
|
|
382
|
+
"id": 1,
|
|
383
|
+
"x_m": 0.622,
|
|
384
|
+
"dead": false,
|
|
385
|
+
"shock_Q": 0
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"id": 2,
|
|
389
|
+
"x_m": -0.609,
|
|
390
|
+
"dead": false,
|
|
391
|
+
"shock_Q": 0
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
]
|
|
396
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "conformance/v1",
|
|
3
|
+
"id": "replay-parity-01",
|
|
4
|
+
"description": "Re-simulating this replay must reproduce the hash after each recorded frame.",
|
|
5
|
+
"kind": "replay-parity",
|
|
6
|
+
"notes": [
|
|
7
|
+
"seed=42; 2 entities; 10 ticks of noMove commands.",
|
|
8
|
+
"Fixed commands are used (not AI) so replayTo starts from a clean initial state.",
|
|
9
|
+
"For each entry: call replayTo(replay, recordedAtTick, ctx).",
|
|
10
|
+
"The returned world.tick must equal expectedWorldTick.",
|
|
11
|
+
"hashWorldState(world) must equal hashHex."
|
|
12
|
+
],
|
|
13
|
+
"replayJson": "{\"initialState\":{\"tick\":0,\"seed\":42,\"entities\":[{\"id\":1,\"teamId\":1,\"attributes\":{\"morphology\":{\"stature_m\":17400,\"mass_kg\":74977,\"actuatorMass_kg\":30125,\"actuatorScale\":9946,\"structureScale\":20000,\"reachScale\":10233},\"performance\":{\"peakForce_N\":165802,\"peakPower_W\":3600,\"continuousPower_W\":128,\"reserveEnergy_J\":15800,\"conversionEfficiency\":8651},\"control\":{\"controlQuality\":6848,\"reactionTime_s\":2329,\"stability\":6855,\"fineControl\":7624},\"resilience\":{\"surfaceIntegrity\":9459,\"bulkIntegrity\":9437,\"structureIntegrity\":10163,\"distressTolerance\":4629,\"shockTolerance\":5169,\"concussionTolerance\":5526,\"heatTolerance\":4758,\"coldTolerance\":4505,\"fatigueRate\":9668,\"recoveryRate\":10195},\"perception\":{\"visionRange_m\":2000000,\"visionArcDeg\":120,\"halfArcCosQ\":5000,\"hearingRange_m\":500000,\"decisionLatency_s\":5000,\"attentionDepth\":4,\"threatHorizon_m\":400000},\"cognition\":{\"linguistic\":6500,\"logicalMathematical\":6000,\"spatial\":6000,\"bodilyKinesthetic\":6000,\"musical\":5000,\"interpersonal\":6000,\"intrapersonal\":5500,\"naturalist\":5000,\"interSpecies\":3500}},\"energy\":{\"reserveEnergy_J\":15800,\"fatigue\":0},\"loadout\":{\"items\":[{\"id\":\"wpn_longsword\",\"kind\":\"weapon\",\"name\":\"Longsword\",\"mass_kg\":1500,\"bulk\":15000,\"reach_m\":9000,\"handedness\":\"twoHand\",\"momentArm_m\":5500,\"handlingMul\":10500,\"strikeEffectiveMassFrac\":1500,\"strikeSpeedMul\":10000,\"readyTime_s\":7500,\"damage\":{\"surfaceFrac\":3500,\"internalFrac\":4500,\"structuralFrac\":2000,\"bleedFactor\":7000,\"penetrationBias\":4000}}]},\"traits\":[],\"position_m\":{\"x\":-5000,\"y\":0,\"z\":0},\"velocity_mps\":{\"x\":0,\"y\":0,\"z\":0},\"intent\":{\"move\":{\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"},\"defence\":{\"mode\":\"none\",\"intensity\":0},\"prone\":false},\"action\":{\"attackCooldownTicks\":0,\"defenceCooldownTicks\":0,\"grappleCooldownTicks\":0,\"facingDirQ\":{\"x\":10000,\"y\":0,\"z\":0},\"weaponBindPartnerId\":0,\"weaponBindTicks\":0,\"swingMomentumQ\":0,\"shootCooldownTicks\":0,\"aimTicks\":0,\"aimTargetId\":0},\"condition\":{\"onFire\":0,\"corrosiveExposure\":0,\"radiation\":0,\"electricalOverload\":0,\"suffocation\":0,\"stunned\":0,\"prone\":false,\"pinned\":false,\"standBlockedTicks\":0,\"unconsciousTicks\":0,\"suppressedTicks\":0,\"blindTicks\":0,\"fearQ\":0,\"suppressionFearMul\":10000,\"recentAllyDeaths\":0,\"lastAllyDeathTick\":-1,\"surrendered\":false,\"rallyCooldownTicks\":0},\"injury\":{\"byRegion\":{\"head\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"torso\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"leftArm\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"rightArm\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"leftLeg\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"rightLeg\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0}},\"fluidLoss\":0,\"shock\":0,\"consciousness\":10000,\"dead\":false,\"hemolymphLoss\":0},\"grapple\":{\"holdingTargetId\":0,\"heldByIds\":[],\"gripQ\":0,\"position\":\"standing\"}},{\"id\":2,\"teamId\":2,\"attributes\":{\"morphology\":{\"stature_m\":17440,\"mass_kg\":74182,\"actuatorMass_kg\":29420,\"actuatorScale\":9904,\"structureScale\":20000,\"reachScale\":9895},\"performance\":{\"peakForce_N\":154468,\"peakPower_W\":3600,\"continuousPower_W\":129,\"reserveEnergy_J\":14502,\"conversionEfficiency\":8557},\"control\":{\"controlQuality\":7058,\"reactionTime_s\":2091,\"stability\":6517,\"fineControl\":7109},\"resilience\":{\"surfaceIntegrity\":9544,\"bulkIntegrity\":10338,\"structureIntegrity\":9742,\"distressTolerance\":5605,\"shockTolerance\":5235,\"concussionTolerance\":4836,\"heatTolerance\":5353,\"coldTolerance\":4988,\"fatigueRate\":9233,\"recoveryRate\":9064},\"perception\":{\"visionRange_m\":2000000,\"visionArcDeg\":120,\"halfArcCosQ\":5000,\"hearingRange_m\":500000,\"decisionLatency_s\":5000,\"attentionDepth\":4,\"threatHorizon_m\":400000},\"cognition\":{\"linguistic\":6500,\"logicalMathematical\":6000,\"spatial\":6000,\"bodilyKinesthetic\":6000,\"musical\":5000,\"interpersonal\":6000,\"intrapersonal\":5500,\"naturalist\":5000,\"interSpecies\":3500}},\"energy\":{\"reserveEnergy_J\":14502,\"fatigue\":0},\"loadout\":{\"items\":[{\"id\":\"wpn_longsword\",\"kind\":\"weapon\",\"name\":\"Longsword\",\"mass_kg\":1500,\"bulk\":15000,\"reach_m\":9000,\"handedness\":\"twoHand\",\"momentArm_m\":5500,\"handlingMul\":10500,\"strikeEffectiveMassFrac\":1500,\"strikeSpeedMul\":10000,\"readyTime_s\":7500,\"damage\":{\"surfaceFrac\":3500,\"internalFrac\":4500,\"structuralFrac\":2000,\"bleedFactor\":7000,\"penetrationBias\":4000}}]},\"traits\":[],\"position_m\":{\"x\":5000,\"y\":0,\"z\":0},\"velocity_mps\":{\"x\":0,\"y\":0,\"z\":0},\"intent\":{\"move\":{\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"},\"defence\":{\"mode\":\"none\",\"intensity\":0},\"prone\":false},\"action\":{\"attackCooldownTicks\":0,\"defenceCooldownTicks\":0,\"grappleCooldownTicks\":0,\"facingDirQ\":{\"x\":10000,\"y\":0,\"z\":0},\"weaponBindPartnerId\":0,\"weaponBindTicks\":0,\"swingMomentumQ\":0,\"shootCooldownTicks\":0,\"aimTicks\":0,\"aimTargetId\":0},\"condition\":{\"onFire\":0,\"corrosiveExposure\":0,\"radiation\":0,\"electricalOverload\":0,\"suffocation\":0,\"stunned\":0,\"prone\":false,\"pinned\":false,\"standBlockedTicks\":0,\"unconsciousTicks\":0,\"suppressedTicks\":0,\"blindTicks\":0,\"fearQ\":0,\"suppressionFearMul\":10000,\"recentAllyDeaths\":0,\"lastAllyDeathTick\":-1,\"surrendered\":false,\"rallyCooldownTicks\":0},\"injury\":{\"byRegion\":{\"head\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"torso\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"leftArm\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"rightArm\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"leftLeg\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0},\"rightLeg\":{\"surfaceDamage\":0,\"internalDamage\":0,\"structuralDamage\":0,\"bleedingRate\":0,\"fractured\":false,\"infectedTick\":-1,\"bleedDuration_ticks\":0,\"permanentDamage\":0}},\"fluidLoss\":0,\"shock\":0,\"consciousness\":10000,\"dead\":false,\"hemolymphLoss\":0},\"grapple\":{\"holdingTargetId\":0,\"heldByIds\":[],\"gripQ\":0,\"position\":\"standing\"}}]},\"frames\":[{\"tick\":0,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":1,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":2,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":3,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":4,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":5,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":6,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":7,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":8,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]},{\"tick\":9,\"commands\":[[1,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]],[2,[{\"kind\":\"move\",\"dir\":{\"x\":0,\"y\":0,\"z\":0},\"intensity\":0,\"mode\":\"walk\"}]]]}]}",
|
|
14
|
+
"hashTrace": [
|
|
15
|
+
{
|
|
16
|
+
"recordedAtTick": 0,
|
|
17
|
+
"expectedWorldTick": 1,
|
|
18
|
+
"hashHex": "0xfdf533148d08f459"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"recordedAtTick": 1,
|
|
22
|
+
"expectedWorldTick": 2,
|
|
23
|
+
"hashHex": "0xfdf233148d06be82"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"recordedAtTick": 2,
|
|
27
|
+
"expectedWorldTick": 3,
|
|
28
|
+
"hashHex": "0xfdee2f148d02cedf"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"recordedAtTick": 3,
|
|
32
|
+
"expectedWorldTick": 4,
|
|
33
|
+
"hashHex": "0xfe062f148d1760c0"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"recordedAtTick": 4,
|
|
37
|
+
"expectedWorldTick": 5,
|
|
38
|
+
"hashHex": "0xfe0333148d1531b5"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"recordedAtTick": 5,
|
|
42
|
+
"expectedWorldTick": 6,
|
|
43
|
+
"hashHex": "0xfdff33148d1148de"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"recordedAtTick": 6,
|
|
47
|
+
"expectedWorldTick": 7,
|
|
48
|
+
"hashHex": "0xfdfc2f148d0f0c3b"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"recordedAtTick": 7,
|
|
52
|
+
"expectedWorldTick": 8,
|
|
53
|
+
"hashHex": "0xfddd2f148cf45bac"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"recordedAtTick": 8,
|
|
57
|
+
"expectedWorldTick": 9,
|
|
58
|
+
"hashHex": "0xfdda33148cf22ca1"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"recordedAtTick": 9,
|
|
62
|
+
"expectedWorldTick": 10,
|
|
63
|
+
"hashHex": "0x8fe5f3eba59e0b75"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "conformance/v1",
|
|
3
|
+
"id": "state-hash-01",
|
|
4
|
+
"description": "Given a canonical WorldState, hashWorldState must return the specified hex value.",
|
|
5
|
+
"kind": "state-hash",
|
|
6
|
+
"notes": [
|
|
7
|
+
"seed=42; two humanoid entities at ±0.5 m with chainmail and longsword.",
|
|
8
|
+
"hash is FNV-64 over canonical JSON (sorted keys, Map→sorted entries).",
|
|
9
|
+
"A conforming implementation must produce identical hashes."
|
|
10
|
+
],
|
|
11
|
+
"cases": [
|
|
12
|
+
{
|
|
13
|
+
"tick": 0,
|
|
14
|
+
"description": "Initial world state before any tick",
|
|
15
|
+
"hashHex": "0xec578e005fcbef34"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"tick": 1,
|
|
19
|
+
"description": "World state after one idle tick (noMove commands)",
|
|
20
|
+
"hashHex": "0xfdf533148d08f459"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Discriminated union of all supported fixture kinds. */
|
|
2
|
+
export type FixtureKind = "state-hash" | "replay-parity" | "command-round-trip" | "bridge-snapshot" | "lockstep-sequence";
|
|
3
|
+
/** Version identifier embedded in every fixture file. */
|
|
4
|
+
export declare const CONFORMANCE_VERSION: "conformance/v1";
|
|
5
|
+
/** Common header shared by all conformance fixtures. */
|
|
6
|
+
export interface ConformanceFixtureHeader {
|
|
7
|
+
/** Fixture format version — check this before reading. */
|
|
8
|
+
version: typeof CONFORMANCE_VERSION;
|
|
9
|
+
/** Unique fixture identifier. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Human-readable description of what this fixture tests. */
|
|
12
|
+
description: string;
|
|
13
|
+
/** Fixture kind — determines which runner handles it. */
|
|
14
|
+
kind: FixtureKind;
|
|
15
|
+
/** Implementation notes — read before writing a custom runner. */
|
|
16
|
+
notes: string[];
|
|
17
|
+
}
|
|
18
|
+
/** State-hash fixture: given an initial world, hashWorldState must return a known hex. */
|
|
19
|
+
export interface StateHashFixture extends ConformanceFixtureHeader {
|
|
20
|
+
kind: "state-hash";
|
|
21
|
+
cases: Array<{
|
|
22
|
+
tick: number;
|
|
23
|
+
description: string;
|
|
24
|
+
/** FNV-64 hash as a 0x-prefixed hex string. */
|
|
25
|
+
hashHex: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/** Replay-parity fixture: re-simulating a replay must produce the same per-tick hashes. */
|
|
29
|
+
export interface ReplayParityFixture extends ConformanceFixtureHeader {
|
|
30
|
+
kind: "replay-parity";
|
|
31
|
+
/** Serialised Replay JSON produced by ReplayRecorder. */
|
|
32
|
+
replayJson: string;
|
|
33
|
+
hashTrace: Array<{
|
|
34
|
+
tick: number;
|
|
35
|
+
hashHex: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** Command round-trip fixture: verifies scale constants and JSON round-trips. */
|
|
39
|
+
export interface CommandRoundTripFixture extends ConformanceFixtureHeader {
|
|
40
|
+
kind: "command-round-trip";
|
|
41
|
+
scale: {
|
|
42
|
+
Q: number;
|
|
43
|
+
kg: number;
|
|
44
|
+
m: number;
|
|
45
|
+
mps: number;
|
|
46
|
+
};
|
|
47
|
+
commands: Array<Record<string, unknown>>;
|
|
48
|
+
}
|
|
49
|
+
/** Bridge-snapshot fixture: serializeBridgeFrame must produce this shape. */
|
|
50
|
+
export interface BridgeSnapshotFixture extends ConformanceFixtureHeader {
|
|
51
|
+
kind: "bridge-snapshot";
|
|
52
|
+
input: {
|
|
53
|
+
seed: number;
|
|
54
|
+
tick: number;
|
|
55
|
+
entityCount: number;
|
|
56
|
+
entityPositions: Array<{
|
|
57
|
+
id: number;
|
|
58
|
+
x_m: number;
|
|
59
|
+
y_m: number;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
expected: {
|
|
63
|
+
schema: string;
|
|
64
|
+
tick: number;
|
|
65
|
+
entityCount: number;
|
|
66
|
+
scenarioId: string;
|
|
67
|
+
entityIds: number[];
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Lockstep-sequence fixture: entity state at each tick of a deterministic run. */
|
|
71
|
+
export interface LockstepSequenceFixture extends ConformanceFixtureHeader {
|
|
72
|
+
kind: "lockstep-sequence";
|
|
73
|
+
context: {
|
|
74
|
+
tractionCoeff_Q: number;
|
|
75
|
+
};
|
|
76
|
+
snapshots: Array<{
|
|
77
|
+
tick: number;
|
|
78
|
+
hashHex: string;
|
|
79
|
+
entities: Array<{
|
|
80
|
+
id: number;
|
|
81
|
+
x_m: number;
|
|
82
|
+
dead: boolean;
|
|
83
|
+
shock_Q: number;
|
|
84
|
+
}>;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
/** Union of all fixture types. */
|
|
88
|
+
export type ConformanceFixture = StateHashFixture | ReplayParityFixture | CommandRoundTripFixture | BridgeSnapshotFixture | LockstepSequenceFixture;
|
|
89
|
+
/** Result of running a single fixture. */
|
|
90
|
+
export interface ConformanceResult {
|
|
91
|
+
id: string;
|
|
92
|
+
kind: FixtureKind;
|
|
93
|
+
status: "pass" | "fail" | "skip" | "error";
|
|
94
|
+
checks: number;
|
|
95
|
+
failures: string[];
|
|
96
|
+
durationMs: number;
|
|
97
|
+
}
|
|
98
|
+
/** Summary of a full conformance run. */
|
|
99
|
+
export interface ConformanceSummary {
|
|
100
|
+
_generated: string;
|
|
101
|
+
passed: number;
|
|
102
|
+
failed: number;
|
|
103
|
+
errored: number;
|
|
104
|
+
skipped: number;
|
|
105
|
+
total: number;
|
|
106
|
+
conformant: boolean;
|
|
107
|
+
results: ConformanceResult[];
|
|
108
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/conformance.ts
|
|
2
|
+
// PM-5: Conformance suite public API
|
|
3
|
+
//
|
|
4
|
+
// Exports types and helpers used by third-party host SDKs to verify
|
|
5
|
+
// deterministic compatibility with the reference Ananke engine.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// import type { ConformanceFixture, FixtureKind } from "@its-not-rocket-science/ananke/conformance";
|
|
9
|
+
//
|
|
10
|
+
// The full conformance fixtures live in conformance/*.json (published in the package).
|
|
11
|
+
// Run the suite with: npx ananke conformance (via pack-cli)
|
|
12
|
+
/** Version identifier embedded in every fixture file. */
|
|
13
|
+
export const CONFORMANCE_VERSION = "conformance/v1";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { WorldState } from "./sim/world.js";
|
|
2
|
+
/** Current Ananke engine version — used to evaluate pack compatRange at runtime. */
|
|
3
|
+
export declare const ANANKE_ENGINE_VERSION = "0.1.65";
|
|
2
4
|
/** A single actionable validation failure from `validatePack`. */
|
|
3
5
|
export interface PackValidationError {
|
|
4
6
|
/** JSONPath-style location, e.g. `"$.weapons[2].mass_kg"`. */
|
|
@@ -6,6 +8,61 @@ export interface PackValidationError {
|
|
|
6
8
|
/** Human-readable explanation of what is wrong. */
|
|
7
9
|
message: string;
|
|
8
10
|
}
|
|
11
|
+
/** Stability tier for a content pack — controls how it is listed in a registry. */
|
|
12
|
+
export type PackStabilityTier = "stable" | "experimental" | "internal";
|
|
13
|
+
/** Dataset or paper reference for empirically grounded pack content. */
|
|
14
|
+
export interface PackProvenanceRef {
|
|
15
|
+
/** Short description of the source. */
|
|
16
|
+
title: string;
|
|
17
|
+
/** URL of the source, if available. */
|
|
18
|
+
url?: string;
|
|
19
|
+
/** DOI of the source, if applicable. */
|
|
20
|
+
doi?: string;
|
|
21
|
+
/** Free-text notes about what this source grounds. */
|
|
22
|
+
notes?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Registry metadata block — optional top-level section of a pack manifest.
|
|
26
|
+
*
|
|
27
|
+
* Including a `registry` block enables:
|
|
28
|
+
* - Runtime compatibility checking via `compatRange`
|
|
29
|
+
* - Deterministic integrity verification via `checksum` (SHA-256)
|
|
30
|
+
* - Licensing and provenance attestation for empirical content
|
|
31
|
+
*
|
|
32
|
+
* Generate the checksum with:
|
|
33
|
+
* `npx ananke pack bundle <dir>` (embeds it automatically)
|
|
34
|
+
*
|
|
35
|
+
* or manually with `computePackChecksum(manifest)` from `@ananke/content-pack`.
|
|
36
|
+
*/
|
|
37
|
+
export interface PackRegistryMeta {
|
|
38
|
+
/**
|
|
39
|
+
* Semver range of Ananke engine versions this pack targets.
|
|
40
|
+
* Examples: `">=0.1.50"`, `">=0.1 <0.2"`, `"^0.1.60"`.
|
|
41
|
+
* `validatePack` rejects packs whose `compatRange` excludes the running version.
|
|
42
|
+
*/
|
|
43
|
+
compatRange?: string;
|
|
44
|
+
/** Stability guarantee — governs how the pack appears in a public registry. */
|
|
45
|
+
stabilityTier?: PackStabilityTier;
|
|
46
|
+
/**
|
|
47
|
+
* Subpath exports from `@its-not-rocket-science/ananke` that this pack's
|
|
48
|
+
* content references, e.g. `["./combat", "./catalog"]`.
|
|
49
|
+
* Informational only — not enforced at runtime.
|
|
50
|
+
*/
|
|
51
|
+
requiredExports?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* SHA-256 hex digest of the pack JSON (with `registry.checksum` set to `""`
|
|
54
|
+
* before hashing, so the field is present but blank).
|
|
55
|
+
* Compute with `npx ananke pack bundle` or `computePackChecksum`.
|
|
56
|
+
*/
|
|
57
|
+
checksum?: string;
|
|
58
|
+
/** SPDX license identifier, e.g. `"MIT"`, `"CC-BY-4.0"`. */
|
|
59
|
+
license?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Dataset or paper references for empirically grounded pack content.
|
|
62
|
+
* Include when your pack derives parameters from research data.
|
|
63
|
+
*/
|
|
64
|
+
provenance?: PackProvenanceRef[];
|
|
65
|
+
}
|
|
9
66
|
/**
|
|
10
67
|
* The `.ananke-pack` manifest schema.
|
|
11
68
|
*
|
|
@@ -24,9 +81,14 @@ export interface AnankePackManifest {
|
|
|
24
81
|
description?: string;
|
|
25
82
|
/**
|
|
26
83
|
* Minimum Ananke version required, as a semver range string.
|
|
27
|
-
*
|
|
84
|
+
* @deprecated Use `registry.compatRange` instead — this field is informational only.
|
|
28
85
|
*/
|
|
29
86
|
anankeVersion?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Registry metadata — compatibility, checksum, license, and provenance.
|
|
89
|
+
* `registry.compatRange` is enforced at runtime by `validatePack`.
|
|
90
|
+
*/
|
|
91
|
+
registry?: PackRegistryMeta;
|
|
30
92
|
/** Weapon definitions — each passed to `registerWeapon`. */
|
|
31
93
|
weapons?: unknown[];
|
|
32
94
|
/** Armour definitions — each passed to `registerArmour`. */
|
|
@@ -59,6 +121,11 @@ export interface LoadPackResult {
|
|
|
59
121
|
/** Validation and registration errors. Empty on full success. */
|
|
60
122
|
errors: PackValidationError[];
|
|
61
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Test whether `version` satisfies `range`.
|
|
126
|
+
* Returns `false` if the range string is unparseable.
|
|
127
|
+
*/
|
|
128
|
+
export declare function semverSatisfies(version: string, range: string): boolean;
|
|
62
129
|
/**
|
|
63
130
|
* Validate a pack manifest for structural conformance without loading it.
|
|
64
131
|
*
|
package/dist/src/content-pack.js
CHANGED
|
@@ -13,6 +13,95 @@ import { registerWeapon, registerArmour, registerArchetype } from "./catalog.js"
|
|
|
13
13
|
import { validateScenario, loadScenario } from "./scenario.js";
|
|
14
14
|
import { hashMod } from "./modding.js";
|
|
15
15
|
import { registerWorldArchetype, registerWorldItem } from "./world-factory.js";
|
|
16
|
+
// ── Version constant ──────────────────────────────────────────────────────────
|
|
17
|
+
// Must be kept in sync with package.json "version" field.
|
|
18
|
+
/** Current Ananke engine version — used to evaluate pack compatRange at runtime. */
|
|
19
|
+
export const ANANKE_ENGINE_VERSION = "0.1.65";
|
|
20
|
+
// ── Semver utilities ──────────────────────────────────────────────────────────
|
|
21
|
+
// Lightweight range evaluator — no external dependencies.
|
|
22
|
+
// Supports: >=X.Y.Z >X.Y.Z <=X.Y.Z <X.Y.Z =X.Y.Z ^X.Y.Z ~X.Y.Z
|
|
23
|
+
// Short forms X.Y and X treated as X.Y.0 and X.0.0 respectively.
|
|
24
|
+
// Compound ranges (space-separated) require all constraints to match.
|
|
25
|
+
function parseSemverTuple(v) {
|
|
26
|
+
const parts = v.replace(/^v/, "").split(".").map(Number);
|
|
27
|
+
if (parts.some(isNaN))
|
|
28
|
+
return null;
|
|
29
|
+
const [major = 0, minor = 0, patch = 0] = parts;
|
|
30
|
+
return [major, minor, patch];
|
|
31
|
+
}
|
|
32
|
+
function cmpSemver(a, b) {
|
|
33
|
+
if (a[0] !== b[0])
|
|
34
|
+
return a[0] - b[0];
|
|
35
|
+
if (a[1] !== b[1])
|
|
36
|
+
return a[1] - b[1];
|
|
37
|
+
return a[2] - b[2];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Test whether `version` satisfies `range`.
|
|
41
|
+
* Returns `false` if the range string is unparseable.
|
|
42
|
+
*/
|
|
43
|
+
export function semverSatisfies(version, range) {
|
|
44
|
+
const ver = parseSemverTuple(version);
|
|
45
|
+
if (!ver)
|
|
46
|
+
return false;
|
|
47
|
+
const constraints = range.trim().split(/\s+/);
|
|
48
|
+
for (const constraint of constraints) {
|
|
49
|
+
if (!evalConstraint(ver, constraint.trim()))
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function evalConstraint(ver, c) {
|
|
55
|
+
// Caret: ^X.Y.Z — compatible within the leftmost non-zero component.
|
|
56
|
+
// ^1.2.3 → >=1.2.3 <2.0.0 (major locked when major > 0)
|
|
57
|
+
// ^0.2.3 → >=0.2.3 <0.3.0 (minor locked when major == 0, minor > 0)
|
|
58
|
+
// ^0.0.3 → >=0.0.3 <0.0.4 (patch locked when both major and minor == 0)
|
|
59
|
+
if (c.startsWith("^")) {
|
|
60
|
+
const lo = parseSemverTuple(c.slice(1));
|
|
61
|
+
if (!lo)
|
|
62
|
+
return false;
|
|
63
|
+
let hi;
|
|
64
|
+
if (lo[0] > 0)
|
|
65
|
+
hi = [lo[0] + 1, 0, 0];
|
|
66
|
+
else if (lo[1] > 0)
|
|
67
|
+
hi = [0, lo[1] + 1, 0];
|
|
68
|
+
else
|
|
69
|
+
hi = [0, 0, lo[2] + 1];
|
|
70
|
+
return cmpSemver(ver, lo) >= 0 && cmpSemver(ver, hi) < 0;
|
|
71
|
+
}
|
|
72
|
+
// Tilde: ~X.Y.Z → >=X.Y.Z <X.(Y+1).0
|
|
73
|
+
if (c.startsWith("~")) {
|
|
74
|
+
const lo = parseSemverTuple(c.slice(1));
|
|
75
|
+
if (!lo)
|
|
76
|
+
return false;
|
|
77
|
+
const hi = [lo[0], lo[1] + 1, 0];
|
|
78
|
+
return cmpSemver(ver, lo) >= 0 && cmpSemver(ver, hi) < 0;
|
|
79
|
+
}
|
|
80
|
+
// Comparators
|
|
81
|
+
if (c.startsWith(">=")) {
|
|
82
|
+
const t = parseSemverTuple(c.slice(2));
|
|
83
|
+
return t !== null && cmpSemver(ver, t) >= 0;
|
|
84
|
+
}
|
|
85
|
+
if (c.startsWith(">")) {
|
|
86
|
+
const t = parseSemverTuple(c.slice(1));
|
|
87
|
+
return t !== null && cmpSemver(ver, t) > 0;
|
|
88
|
+
}
|
|
89
|
+
if (c.startsWith("<=")) {
|
|
90
|
+
const t = parseSemverTuple(c.slice(2));
|
|
91
|
+
return t !== null && cmpSemver(ver, t) <= 0;
|
|
92
|
+
}
|
|
93
|
+
if (c.startsWith("<")) {
|
|
94
|
+
const t = parseSemverTuple(c.slice(1));
|
|
95
|
+
return t !== null && cmpSemver(ver, t) < 0;
|
|
96
|
+
}
|
|
97
|
+
if (c.startsWith("=")) {
|
|
98
|
+
const t = parseSemverTuple(c.slice(1));
|
|
99
|
+
return t !== null && cmpSemver(ver, t) === 0;
|
|
100
|
+
}
|
|
101
|
+
// Bare version: exact match
|
|
102
|
+
const t = parseSemverTuple(c);
|
|
103
|
+
return t !== null && cmpSemver(ver, t) === 0;
|
|
104
|
+
}
|
|
16
105
|
const _packs = new Map();
|
|
17
106
|
// ── Validation ────────────────────────────────────────────────────────────────
|
|
18
107
|
/**
|
|
@@ -39,6 +128,75 @@ export function validatePack(manifest) {
|
|
|
39
128
|
!/^\d+\.\d+(\.\d+)?$/.test(m["version"])) {
|
|
40
129
|
errors.push({ path: "$.version", message: 'must be a semver string like "1.0.0" or "1.0"' });
|
|
41
130
|
}
|
|
131
|
+
// Optional: registry block
|
|
132
|
+
if (m["registry"] !== undefined) {
|
|
133
|
+
if (typeof m["registry"] !== "object" || m["registry"] === null || Array.isArray(m["registry"])) {
|
|
134
|
+
errors.push({ path: "$.registry", message: "must be a plain object if present" });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const reg = m["registry"];
|
|
138
|
+
// compatRange — semver range string; must include the running engine version
|
|
139
|
+
if (reg["compatRange"] !== undefined) {
|
|
140
|
+
if (typeof reg["compatRange"] !== "string") {
|
|
141
|
+
errors.push({ path: "$.registry.compatRange", message: "must be a string" });
|
|
142
|
+
}
|
|
143
|
+
else if (!semverSatisfies(ANANKE_ENGINE_VERSION, reg["compatRange"])) {
|
|
144
|
+
errors.push({
|
|
145
|
+
path: "$.registry.compatRange",
|
|
146
|
+
message: `engine version ${ANANKE_ENGINE_VERSION} does not satisfy range "${reg["compatRange"]}"`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// stabilityTier — must be one of the known tiers
|
|
151
|
+
const TIERS = ["stable", "experimental", "internal"];
|
|
152
|
+
if (reg["stabilityTier"] !== undefined && !TIERS.includes(reg["stabilityTier"])) {
|
|
153
|
+
errors.push({
|
|
154
|
+
path: "$.registry.stabilityTier",
|
|
155
|
+
message: `must be one of: ${TIERS.join(", ")}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// requiredExports — must be array of strings
|
|
159
|
+
if (reg["requiredExports"] !== undefined) {
|
|
160
|
+
if (!Array.isArray(reg["requiredExports"])) {
|
|
161
|
+
errors.push({ path: "$.registry.requiredExports", message: "must be an array" });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
for (let i = 0; i < reg["requiredExports"].length; i++) {
|
|
165
|
+
if (typeof reg["requiredExports"][i] !== "string") {
|
|
166
|
+
errors.push({ path: `$.registry.requiredExports[${i}]`, message: "must be a string" });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// checksum — must be a 64-char hex string (SHA-256) if present
|
|
172
|
+
if (reg["checksum"] !== undefined) {
|
|
173
|
+
if (typeof reg["checksum"] !== "string" || !/^[0-9a-f]{64}$/.test(reg["checksum"])) {
|
|
174
|
+
errors.push({ path: "$.registry.checksum", message: "must be a 64-character lowercase hex string (SHA-256)" });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// license — must be a non-empty string
|
|
178
|
+
if (reg["license"] !== undefined && (typeof reg["license"] !== "string" || reg["license"].trim() === "")) {
|
|
179
|
+
errors.push({ path: "$.registry.license", message: "must be a non-empty SPDX identifier string" });
|
|
180
|
+
}
|
|
181
|
+
// provenance — must be array of objects with at least a title
|
|
182
|
+
if (reg["provenance"] !== undefined) {
|
|
183
|
+
if (!Array.isArray(reg["provenance"])) {
|
|
184
|
+
errors.push({ path: "$.registry.provenance", message: "must be an array" });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
for (let i = 0; i < reg["provenance"].length; i++) {
|
|
188
|
+
const ref = reg["provenance"][i];
|
|
189
|
+
if (typeof ref !== "object" || ref === null) {
|
|
190
|
+
errors.push({ path: `$.registry.provenance[${i}]`, message: "must be an object" });
|
|
191
|
+
}
|
|
192
|
+
else if (typeof ref["title"] !== "string") {
|
|
193
|
+
errors.push({ path: `$.registry.provenance[${i}].title`, message: "must be a string" });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
42
200
|
// Optional arrays — must be arrays if present
|
|
43
201
|
for (const key of ["weapons", "armour", "archetypes", "scenarios"]) {
|
|
44
202
|
if (m[key] !== undefined && !Array.isArray(m[key])) {
|
package/dist/tools/pack-cli.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// npx ananke replay diff <a.json> <b.json>
|
|
13
13
|
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join, resolve, extname, basename } from "node:path";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
15
16
|
import { validatePack, loadPack } from "../src/content-pack.js";
|
|
16
17
|
import { diffReplayJson } from "../src/netcode.js";
|
|
17
18
|
import { q } from "../src/units.js";
|
|
@@ -97,6 +98,13 @@ function cmdBundle(args) {
|
|
|
97
98
|
if (!bundle.name && typeof partial.name === "string")
|
|
98
99
|
bundle.name = partial.name;
|
|
99
100
|
}
|
|
101
|
+
// Compute SHA-256 checksum: serialise with checksum="" (placeholder), then hash.
|
|
102
|
+
// Store in registry block so consumers can verify integrity.
|
|
103
|
+
if (!bundle.registry)
|
|
104
|
+
bundle.registry = {};
|
|
105
|
+
bundle.registry.checksum = ""; // placeholder — field present but blank for hashing
|
|
106
|
+
const checksumInput = JSON.stringify(bundle, null, 2);
|
|
107
|
+
bundle.registry.checksum = createHash("sha256").update(checksumInput).digest("hex");
|
|
100
108
|
// Pre-validate before writing
|
|
101
109
|
const errors = validatePack(bundle);
|
|
102
110
|
if (errors.length > 0) {
|
|
@@ -107,6 +115,7 @@ function cmdBundle(args) {
|
|
|
107
115
|
writeFileSync(outFile, json, "utf8");
|
|
108
116
|
console.log(`✓ Bundle written to ${outFile}`);
|
|
109
117
|
console.log(` weapons: ${bundle.weapons.length}, armour: ${bundle.armour.length}, archetypes: ${bundle.archetypes.length}, scenarios: ${bundle.scenarios.length}`);
|
|
118
|
+
console.log(` checksum: ${bundle.registry.checksum}`);
|
|
110
119
|
}
|
|
111
120
|
function cmdLoad(args) {
|
|
112
121
|
const filePath = args[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@its-not-rocket-science/ananke",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.65",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -201,6 +201,10 @@
|
|
|
201
201
|
"./netcode": {
|
|
202
202
|
"import": "./dist/src/netcode.js",
|
|
203
203
|
"types": "./dist/src/netcode.d.ts"
|
|
204
|
+
},
|
|
205
|
+
"./conformance": {
|
|
206
|
+
"import": "./dist/src/conformance.js",
|
|
207
|
+
"types": "./dist/src/conformance.d.ts"
|
|
204
208
|
}
|
|
205
209
|
},
|
|
206
210
|
"workspaces": [
|
|
@@ -209,6 +213,7 @@
|
|
|
209
213
|
"files": [
|
|
210
214
|
"dist/src",
|
|
211
215
|
"dist/as",
|
|
216
|
+
"conformance",
|
|
212
217
|
"docs/project-overview.md",
|
|
213
218
|
"docs/host-contract.md",
|
|
214
219
|
"docs/integration-primer.md",
|
|
@@ -279,6 +284,8 @@
|
|
|
279
284
|
"ref:species-lab:quick": "node dist/examples/reference/species-lab/index.js --quick",
|
|
280
285
|
"generate-module-index": "node dist/tools/generate-module-index.js",
|
|
281
286
|
"generate-recipes-matrix": "node dist/tools/generate-recipes-matrix.js",
|
|
287
|
+
"generate-conformance-fixtures": "node dist/tools/generate-conformance-fixtures.js",
|
|
288
|
+
"conformance-runner": "node dist/tools/conformance-runner.js",
|
|
282
289
|
"pack": "node dist/tools/pack-cli.js pack",
|
|
283
290
|
"generate-fixtures": "node dist/tools/generate-fixtures.js",
|
|
284
291
|
"generate-zoo": "node dist/tools/generate-zoo.js",
|
package/schema/pack.schema.json
CHANGED
|
@@ -26,7 +26,51 @@
|
|
|
26
26
|
},
|
|
27
27
|
"anankeVersion": {
|
|
28
28
|
"type": "string",
|
|
29
|
-
"description": "Minimum Ananke version required (semver range)
|
|
29
|
+
"description": "Deprecated: use registry.compatRange instead. Minimum Ananke version required (semver range)."
|
|
30
|
+
},
|
|
31
|
+
"registry": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "Registry metadata — compatibility, checksum, license, and provenance. registry.compatRange is enforced at runtime by validatePack.",
|
|
34
|
+
"properties": {
|
|
35
|
+
"compatRange": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Semver range of Ananke engine versions this pack targets, e.g. \">=0.1.50\", \"^0.1.60\". Validated at runtime against the running engine version."
|
|
38
|
+
},
|
|
39
|
+
"stabilityTier": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"enum": ["stable", "experimental", "internal"],
|
|
42
|
+
"description": "Stability guarantee — governs how the pack appears in a public registry."
|
|
43
|
+
},
|
|
44
|
+
"requiredExports": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "type": "string" },
|
|
47
|
+
"description": "Subpath exports from @its-not-rocket-science/ananke this pack's content depends on. Informational only."
|
|
48
|
+
},
|
|
49
|
+
"checksum": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"pattern": "^([0-9a-f]{64}|)$",
|
|
52
|
+
"description": "SHA-256 hex digest of the pack JSON (with registry.checksum set to \"\" before hashing). Computed by `npx ananke pack bundle`."
|
|
53
|
+
},
|
|
54
|
+
"license": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "SPDX license identifier, e.g. \"MIT\", \"CC-BY-4.0\"."
|
|
57
|
+
},
|
|
58
|
+
"provenance": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": ["title"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"title": { "type": "string" },
|
|
65
|
+
"url": { "type": "string" },
|
|
66
|
+
"doi": { "type": "string" },
|
|
67
|
+
"notes": { "type": "string" }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"description": "Dataset or paper references for empirically grounded pack content."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"additionalProperties": false
|
|
30
74
|
},
|
|
31
75
|
"weapons": {
|
|
32
76
|
"type": "array",
|