@its-not-rocket-science/ananke 0.1.50 → 0.1.52

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 CHANGED
@@ -6,6 +6,39 @@ Versioning follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.1.52] — 2026-03-28
10
+
11
+ ### Added
12
+
13
+ - **PA-3 — Stable Schema, Save & Wire Contract (complete):**
14
+ - `src/schema-migration.ts` (new): schema versioning and migration utilities — `SCHEMA_VERSION`, `stampSnapshot`, `validateSnapshot` (returns `ValidationError[]` with JSONPath paths), `migrateWorld` (chains registered migrations; legacy saves treated as version `"0.0"`), `registerMigration`, `detectVersion`, `isValidSnapshot`.
15
+ - `schema/world.schema.json` (new): JSON Schema 2020-12 for `WorldState` — documents `@core` fields (`tick`, `seed`, `entities` with per-entity validation), `@subsystem` fields, and Q-value semantics.
16
+ - `schema/replay.schema.json` (new): JSON Schema 2020-12 for `Replay` / `ReplayFrame` / `Command`.
17
+ - `docs/wire-protocol.md` (new): Q-value serialisation rules (store raw integers, never divide by `SCALE.Q`), binary diff format (ANKD magic, tag-value encoding), multiplayer lockstep message types (`cmd`/`ack`/`resync`/`hash_mismatch`), save-format recommendations, and full load-with-migration code sample.
18
+ - `"./schema"` subpath added to `package.json` exports.
19
+ - `schema/` directory and `docs/wire-protocol.md` added to `package.json` `"files"`.
20
+ - 39 new tests (183 test files, 5,300 tests total). Build: clean.
21
+
22
+ ---
23
+
24
+ ## [0.1.51] — 2026-03-28
25
+
26
+ ### Added
27
+
28
+ - **PA-2 — Modular Package Architecture (Phase 1 complete):**
29
+ - `packages/core/` — `@ananke/core` stub; re-exports the full main `"."` entry point (kernel, entity model, units, RNG, replay, bridge).
30
+ - `packages/combat/` — `@ananke/combat` stub; re-exports `"./combat"`, `"./anatomy"`, `"./competence"`, `"./wasm-kernel"`.
31
+ - `packages/campaign/` — `@ananke/campaign` stub; re-exports all 32 campaign-scale subpaths (polity, social, narrative, feudal, demography, economy, military…).
32
+ - `packages/content/` — `@ananke/content` stub; re-exports `"./species"`, `"./catalog"`, `"./character"`, `"./crafting"`.
33
+ - Each stub ships a pre-built `index.js` + `index.d.ts` (no separate compilation step); `@its-not-rocket-science/ananke` is a peer dependency.
34
+ - Root `package.json` gains `"workspaces": ["packages/*"]` for local linking.
35
+ - `docs/package-architecture.md` (new): canonical package boundary design — dependency graph, monolith subpath → package mapping table, full source-file → package mapping for Phase 2 migration, and a before/after import example.
36
+ - `docs/migration-monolith-to-modular.md` (new): step-by-step migration guide from the monolith to `@ananke/*` packages, with a complete old-import → new-package lookup table and Phase 2 expectations.
37
+ - `docs/package-architecture.md` and `docs/migration-monolith-to-modular.md` added to `package.json` `"files"` so they ship with the published package.
38
+ - Build: clean. Tests: 5,261 passing. Coverage unchanged.
39
+
40
+ ---
41
+
9
42
  ## [0.1.50] — 2026-03-28
10
43
 
11
44
  ### Docs
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Current schema major.minor version.
3
+ *
4
+ * Patch releases (0.1.x → 0.1.y) never change the schema.
5
+ * Minor releases (0.1.x → 0.2.0) may add optional fields (non-breaking).
6
+ * Major releases (0.x → 1.0.0) may alter required fields (breaking; migration required).
7
+ */
8
+ export declare const SCHEMA_VERSION = "0.1";
9
+ /** Schema discrimination tag added by `stampSnapshot`. */
10
+ export type SchemaKind = "world" | "replay" | "campaign";
11
+ /**
12
+ * Metadata fields stamped onto a persisted snapshot.
13
+ * Present on any object returned by `stampSnapshot`.
14
+ */
15
+ export interface VersionedSnapshot {
16
+ /** Schema version at save time, e.g. `"0.1"`. */
17
+ _ananke_version: string;
18
+ /** Which schema this snapshot conforms to. */
19
+ _schema: SchemaKind;
20
+ }
21
+ /**
22
+ * A single actionable validation failure.
23
+ *
24
+ * `path` uses JSONPath dot-notation, e.g. `"$.entities[2].id"`.
25
+ */
26
+ export interface ValidationError {
27
+ path: string;
28
+ message: string;
29
+ }
30
+ /**
31
+ * Add `_ananke_version` and `_schema` metadata to a snapshot before persisting.
32
+ *
33
+ * Does not mutate the original object.
34
+ *
35
+ * @example
36
+ * const save = JSON.stringify(stampSnapshot(world, "world"));
37
+ */
38
+ export declare function stampSnapshot<T extends Record<string, unknown>>(snapshot: T, schema: SchemaKind): T & VersionedSnapshot;
39
+ /**
40
+ * Check structural conformance of a deserialized world snapshot.
41
+ *
42
+ * Validates only the `@core` fields that `stepWorld` requires. Subsystem
43
+ * fields are not validated — unknown extra fields are silently permitted
44
+ * (hosts may attach extension data).
45
+ *
46
+ * @returns An array of `ValidationError`. An empty array means valid.
47
+ */
48
+ export declare function validateSnapshot(snapshot: unknown): ValidationError[];
49
+ type MigrationFn = (snapshot: Record<string, unknown>) => Record<string, unknown>;
50
+ /**
51
+ * Register a migration function between two schema versions.
52
+ *
53
+ * Migrations are chained automatically when `migrateWorld` is called.
54
+ * For a simple non-breaking addition, the migration only needs to add
55
+ * default values for the new fields.
56
+ *
57
+ * @example
58
+ * registerMigration("0.1", "0.2", snap => ({
59
+ * ...snap,
60
+ * __newField: snap["__newField"] ?? 0,
61
+ * }));
62
+ */
63
+ export declare function registerMigration(fromVersion: string, toVersion: string, fn: MigrationFn): void;
64
+ /**
65
+ * Migrate a deserialized world snapshot to `toVersion` (default: current `SCHEMA_VERSION`).
66
+ *
67
+ * - If the snapshot already carries `_ananke_version === toVersion`, it is returned unchanged.
68
+ * - Legacy snapshots without `_ananke_version` are treated as version `"0.0"`.
69
+ * - Throws a descriptive error when no registered migration path exists.
70
+ *
71
+ * The snapshot is not mutated; a new object is returned.
72
+ *
73
+ * @example
74
+ * const raw = JSON.parse(fs.readFileSync("save.json", "utf8"));
75
+ * const world = migrateWorld(raw) as WorldState;
76
+ */
77
+ export declare function migrateWorld(snapshot: Record<string, unknown>, toVersion?: string): Record<string, unknown>;
78
+ /**
79
+ * Read the `_ananke_version` stamp from a deserialized snapshot.
80
+ * Returns `undefined` for legacy snapshots saved before PA-3.
81
+ */
82
+ export declare function detectVersion(snapshot: unknown): string | undefined;
83
+ /**
84
+ * Returns `true` when the snapshot carries a valid version stamp and passes
85
+ * structural validation (no `ValidationError` entries).
86
+ *
87
+ * Convenience wrapper for `detectVersion` + `validateSnapshot`.
88
+ */
89
+ export declare function isValidSnapshot(snapshot: unknown): boolean;
90
+ export {};
@@ -0,0 +1,162 @@
1
+ // src/schema-migration.ts — PA-3: Stable Schema, Save & Wire Contract
2
+ //
3
+ // Provides schema versioning, structural validation, and migration utilities
4
+ // for WorldState snapshots, Campaign saves, and Replay files.
5
+ //
6
+ // Usage pattern:
7
+ // const raw = JSON.parse(saveFile);
8
+ // const migrated = migrateWorld(raw);
9
+ // const errors = validateSnapshot(migrated);
10
+ // if (errors.length === 0) stepWorld(migrated as WorldState, commands);
11
+ // ── Version ───────────────────────────────────────────────────────────────────
12
+ /**
13
+ * Current schema major.minor version.
14
+ *
15
+ * Patch releases (0.1.x → 0.1.y) never change the schema.
16
+ * Minor releases (0.1.x → 0.2.0) may add optional fields (non-breaking).
17
+ * Major releases (0.x → 1.0.0) may alter required fields (breaking; migration required).
18
+ */
19
+ export const SCHEMA_VERSION = "0.1";
20
+ // ── Stamp ─────────────────────────────────────────────────────────────────────
21
+ /**
22
+ * Add `_ananke_version` and `_schema` metadata to a snapshot before persisting.
23
+ *
24
+ * Does not mutate the original object.
25
+ *
26
+ * @example
27
+ * const save = JSON.stringify(stampSnapshot(world, "world"));
28
+ */
29
+ export function stampSnapshot(snapshot, schema) {
30
+ return {
31
+ ...snapshot,
32
+ _ananke_version: SCHEMA_VERSION,
33
+ _schema: schema,
34
+ };
35
+ }
36
+ // ── Validate ──────────────────────────────────────────────────────────────────
37
+ /**
38
+ * Check structural conformance of a deserialized world snapshot.
39
+ *
40
+ * Validates only the `@core` fields that `stepWorld` requires. Subsystem
41
+ * fields are not validated — unknown extra fields are silently permitted
42
+ * (hosts may attach extension data).
43
+ *
44
+ * @returns An array of `ValidationError`. An empty array means valid.
45
+ */
46
+ export function validateSnapshot(snapshot) {
47
+ const errors = [];
48
+ if (typeof snapshot !== "object" || snapshot === null || Array.isArray(snapshot)) {
49
+ errors.push({ path: "$", message: "must be a plain object" });
50
+ return errors;
51
+ }
52
+ const s = snapshot;
53
+ // tick
54
+ if (typeof s["tick"] !== "number" || !Number.isInteger(s["tick"]) || s["tick"] < 0) {
55
+ errors.push({ path: "$.tick", message: "must be a non-negative integer" });
56
+ }
57
+ // seed
58
+ if (typeof s["seed"] !== "number" || !Number.isInteger(s["seed"])) {
59
+ errors.push({ path: "$.seed", message: "must be an integer" });
60
+ }
61
+ // entities
62
+ if (!Array.isArray(s["entities"])) {
63
+ errors.push({ path: "$.entities", message: "must be an array" });
64
+ }
65
+ else {
66
+ for (let i = 0; i < s["entities"].length; i++) {
67
+ const e = s["entities"][i];
68
+ if (typeof e !== "object" || e === null || Array.isArray(e)) {
69
+ errors.push({ path: `$.entities[${i}]`, message: "must be a plain object" });
70
+ continue;
71
+ }
72
+ const ent = e;
73
+ if (typeof ent["id"] !== "number" || !Number.isInteger(ent["id"]) || ent["id"] < 0) {
74
+ errors.push({ path: `$.entities[${i}].id`, message: "must be a non-negative integer" });
75
+ }
76
+ if (typeof ent["teamId"] !== "number" || !Number.isInteger(ent["teamId"])) {
77
+ errors.push({ path: `$.entities[${i}].teamId`, message: "must be an integer" });
78
+ }
79
+ if (typeof ent["attributes"] !== "object" || ent["attributes"] === null) {
80
+ errors.push({ path: `$.entities[${i}].attributes`, message: "must be an object" });
81
+ }
82
+ if (typeof ent["energy"] !== "object" || ent["energy"] === null) {
83
+ errors.push({ path: `$.entities[${i}].energy`, message: "must be an object" });
84
+ }
85
+ if (typeof ent["loadout"] !== "object" || ent["loadout"] === null) {
86
+ errors.push({ path: `$.entities[${i}].loadout`, message: "must be an object" });
87
+ }
88
+ if (!Array.isArray(ent["traits"])) {
89
+ errors.push({ path: `$.entities[${i}].traits`, message: "must be an array" });
90
+ }
91
+ }
92
+ }
93
+ return errors;
94
+ }
95
+ /** Internal registry: `"from->to"` → migration function. */
96
+ const MIGRATIONS = new Map();
97
+ /**
98
+ * Register a migration function between two schema versions.
99
+ *
100
+ * Migrations are chained automatically when `migrateWorld` is called.
101
+ * For a simple non-breaking addition, the migration only needs to add
102
+ * default values for the new fields.
103
+ *
104
+ * @example
105
+ * registerMigration("0.1", "0.2", snap => ({
106
+ * ...snap,
107
+ * __newField: snap["__newField"] ?? 0,
108
+ * }));
109
+ */
110
+ export function registerMigration(fromVersion, toVersion, fn) {
111
+ MIGRATIONS.set(`${fromVersion}->${toVersion}`, fn);
112
+ }
113
+ /**
114
+ * Migrate a deserialized world snapshot to `toVersion` (default: current `SCHEMA_VERSION`).
115
+ *
116
+ * - If the snapshot already carries `_ananke_version === toVersion`, it is returned unchanged.
117
+ * - Legacy snapshots without `_ananke_version` are treated as version `"0.0"`.
118
+ * - Throws a descriptive error when no registered migration path exists.
119
+ *
120
+ * The snapshot is not mutated; a new object is returned.
121
+ *
122
+ * @example
123
+ * const raw = JSON.parse(fs.readFileSync("save.json", "utf8"));
124
+ * const world = migrateWorld(raw) as WorldState;
125
+ */
126
+ export function migrateWorld(snapshot, toVersion = SCHEMA_VERSION) {
127
+ const fromVersion = typeof snapshot["_ananke_version"] === "string"
128
+ ? snapshot["_ananke_version"]
129
+ : "0.0";
130
+ if (fromVersion === toVersion)
131
+ return snapshot;
132
+ const key = `${fromVersion}->${toVersion}`;
133
+ const fn = MIGRATIONS.get(key);
134
+ if (fn === undefined) {
135
+ const known = [...MIGRATIONS.keys()];
136
+ throw new Error(`No migration from schema ${fromVersion} to ${toVersion}.` +
137
+ (known.length > 0 ? ` Registered paths: ${known.join(", ")}` : " No migrations registered."));
138
+ }
139
+ return fn(snapshot);
140
+ }
141
+ // ── Utilities ─────────────────────────────────────────────────────────────────
142
+ /**
143
+ * Read the `_ananke_version` stamp from a deserialized snapshot.
144
+ * Returns `undefined` for legacy snapshots saved before PA-3.
145
+ */
146
+ export function detectVersion(snapshot) {
147
+ if (typeof snapshot !== "object" || snapshot === null)
148
+ return undefined;
149
+ const v = snapshot["_ananke_version"];
150
+ return typeof v === "string" ? v : undefined;
151
+ }
152
+ /**
153
+ * Returns `true` when the snapshot carries a valid version stamp and passes
154
+ * structural validation (no `ValidationError` entries).
155
+ *
156
+ * Convenience wrapper for `detectVersion` + `validateSnapshot`.
157
+ */
158
+ export function isValidSnapshot(snapshot) {
159
+ if (detectVersion(snapshot) === undefined)
160
+ return false;
161
+ return validateSnapshot(snapshot).length === 0;
162
+ }
@@ -0,0 +1,120 @@
1
+ # Migration Guide: Monolith to Modular Packages
2
+
3
+ This guide covers how to migrate from `@its-not-rocket-science/ananke` to the
4
+ focused `@ananke/*` packages. Migration is optional — the monolith package is
5
+ maintained indefinitely for backwards compatibility.
6
+
7
+ ---
8
+
9
+ ## Should you migrate?
10
+
11
+ | Situation | Recommendation |
12
+ |-----------|----------------|
13
+ | Existing project, everything working | Stay on the monolith. No action required. |
14
+ | New project, only needs combat | Start with `@ananke/combat` + `@ananke/core` |
15
+ | New project, full simulation stack | Use the monolith; migrate to modular when convenient |
16
+ | Bundle size is a concern (Phase 2+) | Migrate after Phase 2 (source migration) for real tree-shaking |
17
+
18
+ ---
19
+
20
+ ## Phase 1 Migration (stubs — available now)
21
+
22
+ Phase 1 packages are thin wrappers that re-export from the monolith. Bundle
23
+ size is unchanged, but import paths are cleaner and signal which API tier you
24
+ depend on.
25
+
26
+ ### Step 1 — Install the sub-package(s) you need
27
+
28
+ ```bash
29
+ # Combat-only host
30
+ npm install @ananke/core @ananke/combat
31
+
32
+ # Full world simulation
33
+ npm install @ananke/core @ananke/combat @ananke/campaign @ananke/content
34
+ ```
35
+
36
+ The monolith is installed automatically as a peer dependency.
37
+
38
+ ### Step 2 — Update imports
39
+
40
+ Replace monolith paths with the appropriate package name:
41
+
42
+ | Old import | New import |
43
+ |-----------|-----------|
44
+ | `@its-not-rocket-science/ananke` | `@ananke/core` |
45
+ | `@its-not-rocket-science/ananke/combat` | `@ananke/combat` |
46
+ | `@its-not-rocket-science/ananke/anatomy` | `@ananke/combat` |
47
+ | `@its-not-rocket-science/ananke/competence` | `@ananke/combat` |
48
+ | `@its-not-rocket-science/ananke/wasm-kernel` | `@ananke/combat` |
49
+ | `@its-not-rocket-science/ananke/species` | `@ananke/content` |
50
+ | `@its-not-rocket-science/ananke/catalog` | `@ananke/content` |
51
+ | `@its-not-rocket-science/ananke/character` | `@ananke/content` |
52
+ | `@its-not-rocket-science/ananke/crafting` | `@ananke/content` |
53
+ | `@its-not-rocket-science/ananke/campaign` | `@ananke/campaign` |
54
+ | `@its-not-rocket-science/ananke/polity` | `@ananke/campaign` |
55
+ | `@its-not-rocket-science/ananke/social` | `@ananke/campaign` |
56
+ | `@its-not-rocket-science/ananke/narrative` | `@ananke/campaign` |
57
+ | `@its-not-rocket-science/ananke/narrative-prose` | `@ananke/campaign` |
58
+ | `@its-not-rocket-science/ananke/renown` | `@ananke/campaign` |
59
+ | `@its-not-rocket-science/ananke/kinship` | `@ananke/campaign` |
60
+ | `@its-not-rocket-science/ananke/succession` | `@ananke/campaign` |
61
+ | `@its-not-rocket-science/ananke/calendar` | `@ananke/campaign` |
62
+ | `@its-not-rocket-science/ananke/feudal` | `@ananke/campaign` |
63
+ | `@its-not-rocket-science/ananke/diplomacy` | `@ananke/campaign` |
64
+ | `@its-not-rocket-science/ananke/migration` | `@ananke/campaign` |
65
+ | `@its-not-rocket-science/ananke/espionage` | `@ananke/campaign` |
66
+ | `@its-not-rocket-science/ananke/trade-routes` | `@ananke/campaign` |
67
+ | `@its-not-rocket-science/ananke/siege` | `@ananke/campaign` |
68
+ | `@its-not-rocket-science/ananke/faith` | `@ananke/campaign` |
69
+ | `@its-not-rocket-science/ananke/demography` | `@ananke/campaign` |
70
+ | `@its-not-rocket-science/ananke/granary` | `@ananke/campaign` |
71
+ | `@its-not-rocket-science/ananke/epidemic` | `@ananke/campaign` |
72
+ | `@its-not-rocket-science/ananke/infrastructure` | `@ananke/campaign` |
73
+ | `@its-not-rocket-science/ananke/unrest` | `@ananke/campaign` |
74
+ | `@its-not-rocket-science/ananke/research` | `@ananke/campaign` |
75
+ | `@its-not-rocket-science/ananke/taxation` | `@ananke/campaign` |
76
+ | `@its-not-rocket-science/ananke/military-campaign` | `@ananke/campaign` |
77
+ | `@its-not-rocket-science/ananke/governance` | `@ananke/campaign` |
78
+ | `@its-not-rocket-science/ananke/resources` | `@ananke/campaign` |
79
+ | `@its-not-rocket-science/ananke/climate` | `@ananke/campaign` |
80
+ | `@its-not-rocket-science/ananke/famine` | `@ananke/campaign` |
81
+ | `@its-not-rocket-science/ananke/containment` | `@ananke/campaign` |
82
+ | `@its-not-rocket-science/ananke/mercenaries` | `@ananke/campaign` |
83
+ | `@its-not-rocket-science/ananke/wonders` | `@ananke/campaign` |
84
+ | `@its-not-rocket-science/ananke/monetary` | `@ananke/campaign` |
85
+
86
+ ### Step 3 — Verify
87
+
88
+ ```bash
89
+ npm run build # or tsc --noEmit
90
+ npm test
91
+ ```
92
+
93
+ No other changes are needed.
94
+
95
+ ---
96
+
97
+ ## Phase 2 Migration (source migration — planned)
98
+
99
+ When Phase 2 lands, `@ananke/combat` will no longer depend on the monolith.
100
+ The import paths remain identical — only the transitive dependency graph changes.
101
+
102
+ **No code changes required in your project for Phase 2.**
103
+
104
+ Run `npm update` when the new versions are published; your bundler will
105
+ automatically produce a smaller output.
106
+
107
+ ---
108
+
109
+ ## Staying on the monolith
110
+
111
+ If you prefer to keep using `@its-not-rocket-science/ananke` directly, nothing
112
+ changes. The 41 subpath exports are stable and will not be removed.
113
+
114
+ ---
115
+
116
+ ## See also
117
+
118
+ - [`docs/package-architecture.md`](package-architecture.md) — full design document and source file mapping
119
+ - [`docs/module-index.md`](module-index.md) — all exports with stability tiers and use cases
120
+ - [`STABLE_API.md`](../STABLE_API.md) — stable API surface (Tier 1)
@@ -0,0 +1,323 @@
1
+ # Ananke — Modular Package Architecture
2
+
3
+ > **Status: Phase 1 complete** — Package stubs with re-exports are published.
4
+ > Phase 2 (source migration) moves code into individual packages so combat has
5
+ > no campaign dependency at the module level.
6
+
7
+ ---
8
+
9
+ ## Problem
10
+
11
+ `@its-not-rocket-science/ananke` ships 41 subpath exports in a single package.
12
+ A host that only needs tactical combat transitively depends on feudal succession,
13
+ epidemic simulation, and monetary policy. A renderer integration author has no
14
+ clean way to depend only on the bridge layer.
15
+
16
+ ---
17
+
18
+ ## Package Overview
19
+
20
+ | Package | Stability | Description | Key entry point(s) |
21
+ |---------|-----------|-------------|-------------------|
22
+ | `@ananke/core` | **Stable** | Kernel, entity model, fixed-point units, RNG, replay | `"@ananke/core"` |
23
+ | `@ananke/combat` | Experimental | Combat resolution, anatomy, grapple, ranged, competence | `"@ananke/combat"` |
24
+ | `@ananke/campaign` | Experimental | World simulation — polity, economy, social, demography | `"@ananke/campaign"` |
25
+ | `@ananke/content` | Experimental | Species, equipment catalogue, archetypes, crafting | `"@ananke/content"` |
26
+ | `@ananke/bridge` | Experimental | Renderer bridge, interpolation, animation hints *(Phase 2)* | `"@ananke/bridge"` |
27
+ | `@its-not-rocket-science/ananke` | **Meta-package** | Re-exports all of the above for backwards compatibility | unchanged |
28
+
29
+ > `@ananke/bridge` is not yet a standalone stub — bridge exports are part of
30
+ > `@ananke/core` until Phase 2 adds a dedicated `"./bridge"` subpath to the
31
+ > monolith.
32
+
33
+ ---
34
+
35
+ ## Package Dependency Graph
36
+
37
+ ```
38
+ @ananke/core
39
+
40
+ ├── @ananke/combat (peer: @ananke/core)
41
+ ├── @ananke/campaign (peer: @ananke/core)
42
+ ├── @ananke/content (peer: @ananke/core)
43
+ └── @ananke/bridge (peer: @ananke/core) [Phase 2]
44
+
45
+ @its-not-rocket-science/ananke (meta: re-exports all four)
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Monolith Subpath → Package Mapping
51
+
52
+ ### @ananke/core
53
+
54
+ | Monolith subpath | Notes |
55
+ |-----------------|-------|
56
+ | `"."` | Entire main export — kernel, entity, units, RNG, replay, bridge |
57
+
58
+ ### @ananke/combat
59
+
60
+ | Monolith subpath | Notes |
61
+ |-----------------|-------|
62
+ | `"./combat"` | resolveHit, resolveBlock, CombatContext |
63
+ | `"./anatomy"` | BodyPlan, AnatomyRegion, injury regions |
64
+ | `"./competence"` | skill contest resolution, interspecies signalling |
65
+ | `"./wasm-kernel"` | WASM-accelerated combat math |
66
+
67
+ ### @ananke/campaign
68
+
69
+ | Monolith subpath | Notes |
70
+ |-----------------|-------|
71
+ | `"./campaign"` | Campaign layer, strategic tick |
72
+ | `"./polity"` | Polity, stepPolityDay, tech diffusion |
73
+ | `"./social"` | Social relationships, dialogue |
74
+ | `"./narrative"` | Narrative event system |
75
+ | `"./narrative-prose"` | Prose generation |
76
+ | `"./renown"` | Fame and reputation |
77
+ | `"./kinship"` | Family trees, genealogy |
78
+ | `"./succession"` | Inheritance rules |
79
+ | `"./calendar"` | In-world calendar and date tracking |
80
+ | `"./feudal"` | Feudal hierarchy |
81
+ | `"./diplomacy"` | Treaties and diplomatic acts |
82
+ | `"./migration"` | Population movement |
83
+ | `"./espionage"` | Espionage and spycraft |
84
+ | `"./trade-routes"` | Trade route simulation |
85
+ | `"./siege"` | Siege warfare mechanics |
86
+ | `"./faith"` | Religion and doctrine |
87
+ | `"./demography"` | Population simulation |
88
+ | `"./granary"` | Food storage and distribution |
89
+ | `"./epidemic"` | Disease spread |
90
+ | `"./infrastructure"` | Buildings and construction |
91
+ | `"./unrest"` | Civil unrest |
92
+ | `"./research"` | Technology research |
93
+ | `"./taxation"` | Tax collection |
94
+ | `"./military-campaign"` | Military campaign mechanics |
95
+ | `"./governance"` | Governance and edicts |
96
+ | `"./resources"` | Resource management |
97
+ | `"./climate"` | Climate and weather effects |
98
+ | `"./famine"` | Famine simulation |
99
+ | `"./containment"` | Disease containment |
100
+ | `"./mercenaries"` | Mercenary companies |
101
+ | `"./wonders"` | Wonders and monuments |
102
+ | `"./monetary"` | Monetary policy and currency |
103
+
104
+ ### @ananke/content
105
+
106
+ | Monolith subpath | Notes |
107
+ |-----------------|-------|
108
+ | `"./species"` | Species definitions, stat profiles |
109
+ | `"./catalog"` | Equipment and item catalogue |
110
+ | `"./character"` | Character generation and archetypes |
111
+ | `"./crafting"` | Crafting recipes, workshops, manufacturing |
112
+
113
+ ---
114
+
115
+ ## Source File → Package Mapping (Phase 2 migration)
116
+
117
+ ### @ananke/core
118
+ ```
119
+ src/units.ts
120
+ src/rng.ts
121
+ src/types.ts
122
+ src/replay.ts
123
+ src/sim/entity.ts
124
+ src/sim/kernel.ts
125
+ src/sim/seeds.ts
126
+ src/sim/world.ts
127
+ src/sim/kinds.ts
128
+ src/sim/condition.ts
129
+ src/sim/body.ts
130
+ src/sim/bodyplan.ts
131
+ src/sim/limb.ts
132
+ src/sim/tick.ts
133
+ src/sim/indexing.ts
134
+ src/sim/events.ts
135
+ src/sim/commands.ts
136
+ src/sim/commandBuilders.ts
137
+ src/sim/context.ts
138
+ src/sim/intent.ts
139
+ src/sim/vec3.ts
140
+ src/sim/spatial.ts
141
+ src/sim/skills.ts
142
+ src/sim/traits.ts
143
+ src/sim/terrain.ts
144
+ src/sim/action.ts
145
+ src/sim/step/ (all 10 step files)
146
+ src/bridge/ (all 5 bridge files)
147
+ src/presets.ts
148
+ src/generate.ts
149
+ src/derive.ts
150
+ src/describe.ts
151
+ src/traits.ts
152
+ src/metrics.ts
153
+ src/dist.ts
154
+ src/wasm-kernel.ts
155
+ ```
156
+
157
+ ### @ananke/combat
158
+ ```
159
+ src/sim/combat.ts
160
+ src/sim/injury.ts
161
+ src/sim/wound-aging.ts
162
+ src/sim/medical.ts
163
+ src/sim/morale.ts
164
+ src/sim/grapple.ts
165
+ src/sim/ranged.ts
166
+ src/sim/stamina.ts (if present)
167
+ src/sim/impairment.ts
168
+ src/sim/knockback.ts
169
+ src/sim/cover.ts
170
+ src/sim/cone.ts
171
+ src/sim/formation.ts
172
+ src/sim/formation-combat.ts
173
+ src/sim/formation-unit.ts
174
+ src/sim/frontage.ts
175
+ src/sim/density.ts
176
+ src/sim/occlusion.ts
177
+ src/sim/ai/ (all 8 AI files)
178
+ src/combat.ts
179
+ src/equipment.ts
180
+ src/weapons.ts
181
+ src/anatomy/ (all 5 anatomy files)
182
+ src/competence/ (all 13 competence files)
183
+ src/arena.ts
184
+ src/dialogue.ts
185
+ src/party.ts
186
+ src/faction.ts
187
+ src/downtime.ts
188
+ ```
189
+
190
+ ### @ananke/campaign
191
+ ```
192
+ src/campaign.ts
193
+ src/campaign-layer.ts
194
+ src/polity.ts
195
+ src/polity-vassals.ts
196
+ src/social.ts
197
+ src/relationships.ts
198
+ src/relationships-effects.ts
199
+ src/emotional-contagion.ts
200
+ src/narrative.ts
201
+ src/narrative-layer.ts
202
+ src/narrative-prose.ts
203
+ src/narrative-render.ts
204
+ src/narrative-stress.ts
205
+ src/story-arcs.ts
206
+ src/quest.ts
207
+ src/quest-generators.ts
208
+ src/chronicle.ts
209
+ src/legend.ts
210
+ src/mythology.ts
211
+ src/renown.ts
212
+ src/kinship.ts
213
+ src/succession.ts
214
+ src/calendar.ts
215
+ src/feudal.ts
216
+ src/diplomacy.ts
217
+ src/migration.ts
218
+ src/espionage.ts
219
+ src/trade-routes.ts
220
+ src/siege.ts
221
+ src/faith.ts
222
+ src/demography.ts
223
+ src/granary.ts
224
+ src/epidemic.ts
225
+ src/infrastructure.ts
226
+ src/unrest.ts
227
+ src/research.ts
228
+ src/taxation.ts
229
+ src/military-campaign.ts
230
+ src/governance.ts
231
+ src/resources.ts
232
+ src/climate.ts
233
+ src/famine.ts
234
+ src/containment.ts
235
+ src/mercenaries.ts
236
+ src/wonders.ts
237
+ src/monetary.ts
238
+ src/collective-activities.ts
239
+ src/economy.ts
240
+ src/economy-gen.ts
241
+ src/tech-diffusion.ts
242
+ src/culture.ts
243
+ src/settlement.ts
244
+ src/settlement-services.ts
245
+ src/channels.ts
246
+ src/inheritance.ts
247
+ src/progression.ts
248
+ src/sim/disease.ts
249
+ src/sim/aging.ts
250
+ src/sim/sleep.ts
251
+ src/sim/mount.ts
252
+ src/sim/hazard.ts
253
+ src/sim/nutrition.ts
254
+ src/sim/thermoregulation.ts
255
+ src/sim/toxicology.ts
256
+ src/sim/systemic-toxicology.ts
257
+ src/sim/substance.ts
258
+ src/sim/weather.ts
259
+ src/sim/biome.ts
260
+ src/sim/tech.ts
261
+ ```
262
+
263
+ ### @ananke/content
264
+ ```
265
+ src/species.ts
266
+ src/catalog.ts
267
+ src/character.ts
268
+ src/archetypes.ts
269
+ src/crafting/ (all 5 crafting files)
270
+ src/inventory.ts
271
+ src/item-durability.ts
272
+ src/snapshot.ts
273
+ src/world-generation.ts
274
+ src/world-factory.ts
275
+ src/scenario.ts
276
+ src/modding.ts
277
+ src/lod.ts
278
+ src/model3d.ts
279
+ ```
280
+
281
+ ---
282
+
283
+ ## Phase 2: Source Migration Plan
284
+
285
+ 1. **Create workspace package directories** with their own `tsconfig.build.json`.
286
+ 2. **Move source files** from `src/` into `packages/NAME/src/` following the table above.
287
+ 3. **Update internal imports** — use `@ananke/core` etc. instead of relative paths that cross
288
+ package boundaries.
289
+ 4. **Wire inter-package dependencies** — `@ananke/combat` lists `@ananke/core` as a dependency.
290
+ 5. **Update the monolith meta-package** (`@its-not-rocket-science/ananke`) to re-export from
291
+ the five sub-packages instead of from `src/`.
292
+ 6. **Verify** that all existing tests pass without modification (test paths remain unchanged).
293
+
294
+ The most complex step is (3) — identifying which imports cross package boundaries. A planned
295
+ tool (`tools/check-package-boundaries.ts`) will analyse the import graph and report violations.
296
+
297
+ ---
298
+
299
+ ## What Changes for Package Consumers
300
+
301
+ ### Phase 1 (now — stubs)
302
+ ```typescript
303
+ // Before (monolith)
304
+ import { resolveHit } from "@its-not-rocket-science/ananke/combat";
305
+
306
+ // After (modular stub — same bundle size, cleaner import path)
307
+ import { resolveHit } from "@ananke/combat";
308
+ ```
309
+
310
+ ### Phase 2 (source migration — smaller bundles)
311
+ ```typescript
312
+ // Same import — but now @ananke/combat has no campaign dependency
313
+ import { resolveHit } from "@ananke/combat";
314
+ ```
315
+
316
+ The import path is the same in Phase 1 and Phase 2; only the bundle contents change.
317
+
318
+ ---
319
+
320
+ ## Backwards Compatibility
321
+
322
+ `@its-not-rocket-science/ananke` will remain published indefinitely as a meta-package.
323
+ All 41 subpath exports will continue to work. Existing hosts do not need to migrate.
@@ -0,0 +1,209 @@
1
+ # Ananke — Wire Protocol & Save Format
2
+
3
+ This document specifies how Ananke state is serialised for persistence, replay, and
4
+ network transport. All formats are deterministic: the same simulation state always
5
+ produces the same bytes.
6
+
7
+ ---
8
+
9
+ ## 1. Concepts
10
+
11
+ | Term | Meaning |
12
+ |------|---------|
13
+ | **Snapshot** | A serialised `WorldState` — complete enough to resume simulation |
14
+ | **Replay** | An initial snapshot + a sequence of command frames |
15
+ | **Diff** | A compact binary diff between two consecutive snapshots (CE-9) |
16
+ | **Wire message** | A single unit transmitted between host and client over the network |
17
+ | **Q value** | A fixed-point integer scaled by `SCALE.Q = 10 000` (e.g. `q(0.75) = 7500`) |
18
+
19
+ ---
20
+
21
+ ## 2. JSON Snapshot Format
22
+
23
+ JSON is the recommended format for long-term save files and editor tooling.
24
+
25
+ ### 2.1 Deterministic key ordering
26
+
27
+ When computing hash-checks across clients, keys must appear in insertion order.
28
+ The canonical TypeScript implementation (`JSON.stringify`) preserves insertion
29
+ order for string keys. Third-party deserializers must preserve or sort keys
30
+ identically.
31
+
32
+ ### 2.2 Q values
33
+
34
+ All `Q`-typed fields are serialised as plain integers. Do **not** divide by
35
+ `SCALE.Q` before saving — the raw integer is the canonical representation.
36
+
37
+ ```json
38
+ { "fearQ": 7500 } // correct — q(0.75)
39
+ { "fearQ": 0.75 } // WRONG — will cause precision loss and replay divergence
40
+ ```
41
+
42
+ ### 2.3 Maps
43
+
44
+ JavaScript `Map` instances do not serialise to JSON automatically. Ananke
45
+ serialises `Map<K, V>` as an array of `[K, V]` pairs:
46
+
47
+ ```json
48
+ { "__nutritionAccum": 0 }
49
+ ```
50
+
51
+ > Note: `__nutritionAccum` was simplified to a scalar in v0.1. If a `Map`
52
+ > field is added in a future version, its pairs will use the array format above.
53
+
54
+ ### 2.4 Version stamping
55
+
56
+ Always call `stampSnapshot(world, "world")` before persisting. This adds
57
+ `_ananke_version` and `_schema` fields that enable forward migration:
58
+
59
+ ```typescript
60
+ import { stampSnapshot } from "@its-not-rocket-science/ananke/schema";
61
+ // or: import { stampSnapshot } from "@ananke/core"; (when published)
62
+
63
+ const save = JSON.stringify(stampSnapshot(world, "world"), null, 2);
64
+ ```
65
+
66
+ ### 2.5 JSON Schema files
67
+
68
+ Canonical schemas ship with the package:
69
+
70
+ | File | Validates |
71
+ |------|-----------|
72
+ | `schema/world.schema.json` | `WorldState` snapshots |
73
+ | `schema/replay.schema.json` | `Replay` objects |
74
+
75
+ Use `validateSnapshot(raw)` from `@its-not-rocket-science/ananke/schema` to
76
+ check conformance programmatically before calling `stepWorld`.
77
+
78
+ ---
79
+
80
+ ## 3. Binary Diff Format
81
+
82
+ For tick-to-tick state synchronisation (multiplayer, streaming), use the binary
83
+ diff format implemented in `src/snapshot.ts`.
84
+
85
+ ### 3.1 Encoding
86
+
87
+ ```
88
+ [magic: "ANKD" (4 bytes)] [version: 1 (u8)] [payload: tag-value stream]
89
+ ```
90
+
91
+ Tag values:
92
+
93
+ | Tag | Byte | Encodes |
94
+ |-----|------|---------|
95
+ | NULL | 0x00 | `null` |
96
+ | TRUE | 0x01 | `true` |
97
+ | FALSE | 0x02 | `false` |
98
+ | UINT8 | 0x10 | Unsigned integer 0–255 |
99
+ | INT32 | 0x11 | Signed 32-bit integer (big-endian) |
100
+ | FLOAT64 | 0x12 | IEEE 754 double (big-endian) — use only for non-Q floats |
101
+ | STRING | 0x20 | Length-prefixed UTF-8 |
102
+ | ARRAY | 0x30 | Length-prefixed sequence of tag-value items |
103
+ | OBJECT | 0x40 | Length-prefixed sequence of (string key, tag-value) pairs |
104
+
105
+ ### 3.2 Usage
106
+
107
+ ```typescript
108
+ import { diffWorldState, packDiff, unpackDiff, applyDiff } from "@its-not-rocket-science/ananke";
109
+
110
+ // Sender
111
+ const diff = diffWorldState(prevState, nextState);
112
+ const bytes = packDiff(diff);
113
+ socket.send(bytes);
114
+
115
+ // Receiver
116
+ const diff2 = unpackDiff(bytes);
117
+ const state2 = applyDiff(prevState, diff2);
118
+ ```
119
+
120
+ ### 3.3 Determinism guarantee
121
+
122
+ A diff produced from identical states must produce identical bytes. Do not
123
+ include wall-clock timestamps or random nonces in diff payloads.
124
+
125
+ ---
126
+
127
+ ## 4. Multiplayer Message Protocol
128
+
129
+ For lockstep multiplayer, hosts exchange command frames rather than full state.
130
+
131
+ ### 4.1 Message types
132
+
133
+ | `kind` | Direction | Payload |
134
+ |--------|-----------|---------|
135
+ | `"cmd"` | Client → Server | `{ tick, commands: Command[] }` |
136
+ | `"ack"` | Server → Client | `{ tick, stateHash: number }` |
137
+ | `"resync"` | Server → Client | `{ tick, snapshot: WorldState }` |
138
+ | `"hash_mismatch"` | Server → Client | `{ tick, expected: number, got: number }` |
139
+
140
+ ### 4.2 State hash
141
+
142
+ Use the built-in tick counter and entity count as a cheap hash for divergence
143
+ detection:
144
+
145
+ ```typescript
146
+ function stateHash(world: WorldState): number {
147
+ return world.tick * 0x10000 + (world.entities.length & 0xFFFF);
148
+ }
149
+ ```
150
+
151
+ A full structural hash is more robust but expensive; use it only on resync.
152
+
153
+ ### 4.3 Lockstep loop
154
+
155
+ ```
156
+ ┌──────────────────────────────────────────────────────────┐
157
+ │ Client Server │
158
+ │ │
159
+ │ collect commands ──── cmd ──► apply to authoritative │
160
+ │ state │
161
+ │ ◄── ack ─── broadcast stateHash │
162
+ │ verify hash │
163
+ │ if mismatch ─── resync req ─► send full snapshot │
164
+ │ ◄── resync ── │
165
+ │ restore snapshot │
166
+ └──────────────────────────────────────────────────────────┘
167
+ ```
168
+
169
+ ### 4.4 Transport encoding
170
+
171
+ Use JSON for development and debugging. For production, encode wire messages
172
+ as CBOR (RFC 8949) or MessagePack for ~30% size reduction. The message
173
+ structure is identical; only the outer encoding changes.
174
+
175
+ ---
176
+
177
+ ## 5. Save File Recommendations
178
+
179
+ | Scenario | Format | Compression |
180
+ |----------|--------|-------------|
181
+ | Development / debugging | JSON (pretty-printed) | none |
182
+ | Production saves | JSON (compact) | gzip or zstd |
183
+ | Network sync (full state) | JSON or CBOR | none (already compact) |
184
+ | Network sync (incremental) | Binary diff (`packDiff`) | none |
185
+ | Replay archives | JSON replay schema | zstd |
186
+
187
+ ---
188
+
189
+ ## 6. Migration
190
+
191
+ Load a save and bring it to the current schema version before simulating:
192
+
193
+ ```typescript
194
+ import {
195
+ migrateWorld, validateSnapshot, stampSnapshot,
196
+ } from "@its-not-rocket-science/ananke/schema";
197
+
198
+ function loadSave(json: string): WorldState {
199
+ const raw = JSON.parse(json) as Record<string, unknown>;
200
+ const migrated = migrateWorld(raw); // no-op until 0.2 is released
201
+ const errors = validateSnapshot(migrated);
202
+ if (errors.length > 0) {
203
+ throw new Error(`Invalid save: ${errors.map(e => `${e.path}: ${e.message}`).join("; ")}`);
204
+ }
205
+ return migrated as WorldState;
206
+ }
207
+ ```
208
+
209
+ See `docs/migration-monolith-to-modular.md` for package-level migration guidance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@its-not-rocket-science/ananke",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "type": "module",
5
5
  "description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
6
6
  "license": "MIT",
@@ -170,8 +170,15 @@
170
170
  "./monetary": {
171
171
  "import": "./dist/src/monetary.js",
172
172
  "types": "./dist/src/monetary.d.ts"
173
+ },
174
+ "./schema": {
175
+ "import": "./dist/src/schema-migration.js",
176
+ "types": "./dist/src/schema-migration.d.ts"
173
177
  }
174
178
  },
179
+ "workspaces": [
180
+ "packages/*"
181
+ ],
175
182
  "files": [
176
183
  "dist/src",
177
184
  "dist/as",
@@ -182,6 +189,11 @@
182
189
  "docs/performance.md",
183
190
  "docs/versioning.md",
184
191
  "docs/emergent-validation-report.md",
192
+ "docs/package-architecture.md",
193
+ "docs/migration-monolith-to-modular.md",
194
+ "docs/wire-protocol.md",
195
+ "schema/world.schema.json",
196
+ "schema/replay.schema.json",
185
197
  "CHANGELOG.md",
186
198
  "STABLE_API.md"
187
199
  ],
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://its-not-rocket-science.github.io/ananke/schema/replay.schema.json",
4
+ "title": "Replay",
5
+ "description": "Ananke deterministic replay — an initial WorldState plus a sequence of command frames. Replaying all frames against the initial state must reproduce the exact final state (same tick, same entity conditions, same RNG sequence).",
6
+ "type": "object",
7
+ "required": ["initialState", "frames"],
8
+ "properties": {
9
+ "_ananke_version": {
10
+ "type": "string",
11
+ "pattern": "^\\d+\\.\\d+$",
12
+ "description": "Schema version stamped by stampSnapshot()."
13
+ },
14
+ "_schema": {
15
+ "type": "string",
16
+ "const": "replay",
17
+ "description": "Schema discrimination tag — always \"replay\" for Replay snapshots."
18
+ },
19
+ "initialState": {
20
+ "$ref": "world.schema.json",
21
+ "description": "The WorldState at tick 0 (or whatever tick the recording began). Must itself be a valid world snapshot."
22
+ },
23
+ "frames": {
24
+ "type": "array",
25
+ "items": { "$ref": "#/$defs/ReplayFrame" },
26
+ "description": "Ordered sequence of command frames. Frame indices correspond to ticks relative to initialState.tick."
27
+ }
28
+ },
29
+ "unevaluatedProperties": true,
30
+ "$defs": {
31
+ "ReplayFrame": {
32
+ "title": "ReplayFrame",
33
+ "type": "object",
34
+ "required": ["tick", "commands"],
35
+ "properties": {
36
+ "tick": {
37
+ "type": "integer",
38
+ "minimum": 0,
39
+ "description": "Absolute simulation tick this frame was recorded at."
40
+ },
41
+ "commands": {
42
+ "type": "array",
43
+ "items": { "$ref": "#/$defs/Command" },
44
+ "description": "Commands dispatched during this tick. May be empty."
45
+ }
46
+ }
47
+ },
48
+ "Command": {
49
+ "title": "Command",
50
+ "description": "A single world-mutating command dispatched to stepWorld(). Commands are discriminated by their `kind` field.",
51
+ "type": "object",
52
+ "required": ["kind"],
53
+ "properties": {
54
+ "kind": {
55
+ "type": "string",
56
+ "description": "Command type identifier, e.g. \"attack\", \"move\", \"grapple\"."
57
+ },
58
+ "entityId": {
59
+ "type": "integer",
60
+ "description": "Issuing entity id (where applicable)."
61
+ },
62
+ "targetId": {
63
+ "type": "integer",
64
+ "description": "Target entity id (where applicable)."
65
+ }
66
+ },
67
+ "unevaluatedProperties": true
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,105 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://its-not-rocket-science.github.io/ananke/schema/world.schema.json",
4
+ "title": "WorldState",
5
+ "description": "Ananke world snapshot — the complete state required to resume or replay a simulation. See docs/wire-protocol.md for serialisation rules.",
6
+ "type": "object",
7
+ "required": ["tick", "seed", "entities"],
8
+ "properties": {
9
+ "_ananke_version": {
10
+ "type": "string",
11
+ "pattern": "^\\d+\\.\\d+$",
12
+ "description": "Schema version stamped by stampSnapshot(), e.g. \"0.1\". Absent on legacy saves."
13
+ },
14
+ "_schema": {
15
+ "type": "string",
16
+ "const": "world",
17
+ "description": "Schema discrimination tag — always \"world\" for WorldState snapshots."
18
+ },
19
+ "tick": {
20
+ "type": "integer",
21
+ "minimum": 0,
22
+ "description": "@core — Current simulation tick. Incremented by stepWorld() each call."
23
+ },
24
+ "seed": {
25
+ "type": "integer",
26
+ "description": "@core — Deterministic RNG seed. Same seed + same commands → identical output."
27
+ },
28
+ "entities": {
29
+ "type": "array",
30
+ "items": { "$ref": "#/$defs/EntityCore" },
31
+ "description": "@core — All live and dead entities."
32
+ },
33
+ "activeFieldEffects": {
34
+ "type": "array",
35
+ "items": { "type": "object" },
36
+ "description": "@subsystem(capability) — Active suppression zones and field-effect modifiers."
37
+ },
38
+ "__sensoryEnv": {
39
+ "type": "object",
40
+ "description": "@subsystem(sensory) — Ambient lighting and visibility environment."
41
+ },
42
+ "__factionRegistry": {
43
+ "type": "object",
44
+ "description": "@subsystem(faction) — Global faction standing registry."
45
+ },
46
+ "__partyRegistry": {
47
+ "type": "object",
48
+ "description": "@subsystem(party) — Global party registry for morale and formation bonuses."
49
+ },
50
+ "__relationshipGraph": {
51
+ "type": "object",
52
+ "description": "@subsystem(relationships) — Inter-entity relationship graph."
53
+ },
54
+ "__nutritionAccum": {
55
+ "type": "number",
56
+ "description": "@subsystem(nutrition) — Cross-tick nutrition accumulator (scalar)."
57
+ }
58
+ },
59
+ "unevaluatedProperties": true,
60
+ "$defs": {
61
+ "EntityCore": {
62
+ "title": "Entity (@core fields)",
63
+ "description": "Core entity fields required by stepWorld() every tick. Subsystem and host-extension fields are permitted but not listed here — see src/sim/entity.ts.",
64
+ "type": "object",
65
+ "required": ["id", "teamId", "attributes", "energy", "loadout", "traits"],
66
+ "properties": {
67
+ "id": {
68
+ "type": "integer",
69
+ "minimum": 0,
70
+ "description": "Unique entity identifier (positive I32)."
71
+ },
72
+ "teamId": {
73
+ "type": "integer",
74
+ "description": "Team allegiance. Entities on the same teamId do not auto-target each other."
75
+ },
76
+ "attributes": {
77
+ "type": "object",
78
+ "description": "IndividualAttributes — physical and cognitive stats. All Q values are fixed-point integers scaled by SCALE.Q = 10 000."
79
+ },
80
+ "energy": {
81
+ "type": "object",
82
+ "description": "EnergyState — current stamina and metabolic state."
83
+ },
84
+ "loadout": {
85
+ "type": "object",
86
+ "description": "Loadout — equipped weapons and armour."
87
+ },
88
+ "traits": {
89
+ "type": "array",
90
+ "items": { "type": "string" },
91
+ "description": "Trait string identifiers, e.g. [\"leader\", \"berserker\"]."
92
+ },
93
+ "condition": {
94
+ "type": "object",
95
+ "description": "@subsystem(condition) — fear, morale, consciousness, surrender state."
96
+ },
97
+ "injury": {
98
+ "type": "object",
99
+ "description": "@subsystem(injury) — per-region damage fractions (all Q values)."
100
+ }
101
+ },
102
+ "unevaluatedProperties": true
103
+ }
104
+ }
105
+ }