@its-not-rocket-science/ananke 0.1.69 → 0.5.1

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +254 -0
  2. package/README.md +150 -432
  3. package/STABLE_API.md +113 -490
  4. package/conformance/README.md +46 -44
  5. package/conformance/lockstep-sequence.json +395 -395
  6. package/conformance/phase-order.json +21 -0
  7. package/conformance/replay-parity.json +65 -65
  8. package/conformance/state-hash-regression.json +39 -0
  9. package/conformance/state-hash.json +23 -22
  10. package/dist/src/conformance.d.ts +8 -2
  11. package/dist/src/content/index.d.ts +3 -0
  12. package/dist/src/content/index.js +2 -0
  13. package/dist/src/content/injector.d.ts +19 -0
  14. package/dist/src/content/injector.js +61 -0
  15. package/dist/src/content/loader.d.ts +3 -0
  16. package/dist/src/content/loader.js +170 -0
  17. package/dist/src/content/types.d.ts +56 -0
  18. package/dist/src/content/types.js +1 -0
  19. package/dist/src/content/validator.d.ts +6 -0
  20. package/dist/src/content/validator.js +14 -0
  21. package/dist/src/content-pack.d.ts +3 -2
  22. package/dist/src/content-pack.js +205 -10
  23. package/dist/src/data-governance.d.ts +73 -0
  24. package/dist/src/data-governance.js +188 -0
  25. package/dist/src/debug.d.ts +6 -0
  26. package/dist/src/debug.js +9 -0
  27. package/dist/src/determinism.d.ts +12 -0
  28. package/dist/src/determinism.js +93 -0
  29. package/dist/src/dialogue.d.ts +41 -0
  30. package/dist/src/dialogue.js +113 -2
  31. package/dist/src/economy.d.ts +97 -3
  32. package/dist/src/economy.js +191 -37
  33. package/dist/src/history/autosave.d.ts +19 -0
  34. package/dist/src/history/autosave.js +98 -0
  35. package/dist/src/history/timetravel.d.ts +28 -0
  36. package/dist/src/history/timetravel.js +61 -0
  37. package/dist/src/index.d.ts +19 -31
  38. package/dist/src/index.js +20 -38
  39. package/dist/src/model3d.js +63 -2
  40. package/dist/src/narrative/combat-logger.d.ts +25 -0
  41. package/dist/src/narrative/combat-logger.js +97 -0
  42. package/dist/src/narrative/plausibility.d.ts +30 -0
  43. package/dist/src/narrative/plausibility.js +70 -0
  44. package/dist/src/narrative-layer.d.ts +2 -0
  45. package/dist/src/narrative-layer.js +2 -0
  46. package/dist/src/navigation/causal-chain.d.ts +26 -0
  47. package/dist/src/navigation/causal-chain.js +74 -0
  48. package/dist/src/netcode.d.ts +1 -1
  49. package/dist/src/netcode.js +1 -1
  50. package/dist/src/performance/adaptive-tick.d.ts +23 -0
  51. package/dist/src/performance/adaptive-tick.js +50 -0
  52. package/dist/src/plugins/loader.d.ts +23 -0
  53. package/dist/src/plugins/loader.js +86 -0
  54. package/dist/src/plugins/registry.d.ts +11 -0
  55. package/dist/src/plugins/registry.js +28 -0
  56. package/dist/src/plugins/types.d.ts +31 -0
  57. package/dist/src/plugins/types.js +7 -0
  58. package/dist/src/replay.js +5 -2
  59. package/dist/src/scenario.js +2 -1
  60. package/dist/src/serialization/binary.d.ts +5 -0
  61. package/dist/src/serialization/binary.js +193 -0
  62. package/dist/src/session.d.ts +115 -0
  63. package/dist/src/session.js +228 -0
  64. package/dist/src/sim/ai/decide.js +1 -1
  65. package/dist/src/sim/ai/personality.js +2 -2
  66. package/dist/src/sim/bodyplan.d.ts +30 -0
  67. package/dist/src/sim/context.d.ts +2 -0
  68. package/dist/src/sim/kernel.js +80 -1294
  69. package/dist/src/sim/normalization.d.ts +12 -0
  70. package/dist/src/sim/normalization.js +92 -0
  71. package/dist/src/sim/resolvers/attack-resolver.d.ts +30 -0
  72. package/dist/src/sim/resolvers/attack-resolver.js +310 -0
  73. package/dist/src/sim/resolvers/capability-resolver.d.ts +15 -0
  74. package/dist/src/sim/resolvers/capability-resolver.js +270 -0
  75. package/dist/src/sim/resolvers/grapple-resolver.d.ts +28 -0
  76. package/dist/src/sim/resolvers/grapple-resolver.js +70 -0
  77. package/dist/src/sim/resolvers/shoot-resolver.d.ts +27 -0
  78. package/dist/src/sim/resolvers/shoot-resolver.js +212 -0
  79. package/dist/src/sim/resolvers/treat-resolver.d.ts +16 -0
  80. package/dist/src/sim/resolvers/treat-resolver.js +77 -0
  81. package/dist/src/sim/step/apply/intents.d.ts +6 -0
  82. package/dist/src/sim/step/apply/intents.js +83 -0
  83. package/dist/src/sim/step/kernel-pipeline.d.ts +9 -0
  84. package/dist/src/sim/step/kernel-pipeline.js +15 -0
  85. package/dist/src/sim/step/phases/capability-phase.d.ts +8 -0
  86. package/dist/src/sim/step/phases/capability-phase.js +65 -0
  87. package/dist/src/sim/step/phases/cooldowns-phase.d.ts +3 -0
  88. package/dist/src/sim/step/phases/cooldowns-phase.js +37 -0
  89. package/dist/src/sim/step/phases/prepare-phase.d.ts +10 -0
  90. package/dist/src/sim/step/phases/prepare-phase.js +33 -0
  91. package/dist/src/sim/step/pipeline.d.ts +2 -0
  92. package/dist/src/sim/step/pipeline.js +15 -0
  93. package/dist/src/sim/step/resolvers/impact-resolver.d.ts +9 -0
  94. package/dist/src/sim/step/resolvers/impact-resolver.js +55 -0
  95. package/dist/src/sim/step/world-phases.d.ts +7 -0
  96. package/dist/src/sim/step/world-phases.js +15 -0
  97. package/dist/src/sim/step/world-step-context.d.ts +23 -0
  98. package/dist/src/sim/step/world-step-context.js +1 -0
  99. package/dist/src/sim/systemic-toxicology.js +1 -1
  100. package/dist/src/sim/team.js +2 -2
  101. package/dist/src/sim/testing.js +4 -3
  102. package/dist/src/sim/toxicology.d.ts +1 -1
  103. package/dist/src/sim/toxicology.js +1 -1
  104. package/dist/src/sim/world.d.ts +17 -24
  105. package/dist/src/snapshot.js +3 -6
  106. package/dist/src/tier2.d.ts +19 -0
  107. package/dist/src/tier2.js +21 -0
  108. package/dist/src/tier3.d.ts +10 -0
  109. package/dist/src/tier3.js +12 -0
  110. package/dist/src/version.d.ts +6 -0
  111. package/dist/src/version.js +8 -0
  112. package/dist/src/wasm/bridge.d.ts +14 -0
  113. package/dist/src/wasm/bridge.js +84 -0
  114. package/dist/src/wasm-kernel.d.ts +2 -0
  115. package/dist/src/wasm-kernel.js +1 -0
  116. package/dist/src/world-evolution-backend/engine.d.ts +4 -0
  117. package/dist/src/world-evolution-backend/engine.js +347 -0
  118. package/dist/src/world-evolution-backend/host-schema.d.ts +145 -0
  119. package/dist/src/world-evolution-backend/host-schema.js +375 -0
  120. package/dist/src/world-evolution-backend/index.d.ts +1 -0
  121. package/dist/src/world-evolution-backend/index.js +1 -0
  122. package/dist/src/world-evolution-backend/open-world-host-adapter.d.ts +96 -0
  123. package/dist/src/world-evolution-backend/open-world-host-adapter.js +208 -0
  124. package/dist/src/world-evolution-backend/profiles.d.ts +4 -0
  125. package/dist/src/world-evolution-backend/profiles.js +136 -0
  126. package/dist/src/world-evolution-backend/public.d.ts +11 -0
  127. package/dist/src/world-evolution-backend/public.js +7 -0
  128. package/dist/src/world-evolution-backend/reproducibility.d.ts +8 -0
  129. package/dist/src/world-evolution-backend/reproducibility.js +35 -0
  130. package/dist/src/world-evolution-backend/timeline.d.ts +6 -0
  131. package/dist/src/world-evolution-backend/timeline.js +337 -0
  132. package/dist/src/world-evolution-backend/types.d.ts +138 -0
  133. package/dist/src/world-evolution-backend/types.js +1 -0
  134. package/dist/src/world-evolution-host-backend.d.ts +51 -0
  135. package/dist/src/world-evolution-host-backend.js +97 -0
  136. package/dist/src/world-evolution.d.ts +201 -0
  137. package/dist/src/world-evolution.js +543 -0
  138. package/dist/src/world-factory.js +2 -1
  139. package/dist/tools/pack-cli.js +55 -2
  140. package/docs/bridge-contract.md +140 -332
  141. package/docs/completion-evidence-dashboard.md +63 -0
  142. package/docs/host-contract.md +86 -310
  143. package/docs/host-world-evolution-schema.md +155 -0
  144. package/docs/integration-primer.md +29 -22
  145. package/docs/migration-monolith-to-modular.md +102 -120
  146. package/docs/package-architecture.md +116 -323
  147. package/docs/plugins/README.md +42 -0
  148. package/docs/plugins/hooks.md +22 -0
  149. package/docs/plugins/publishing.md +29 -0
  150. package/docs/plugins/security-model.md +273 -0
  151. package/docs/project-overview.md +30 -30
  152. package/docs/release-dashboard.md +47 -0
  153. package/docs/session-api.md +127 -0
  154. package/docs/stable-api-manifest.json +52 -0
  155. package/docs/trust-dashboard.md +37 -0
  156. package/docs/versioning.md +256 -245
  157. package/docs/wire-protocol.md +140 -209
  158. package/docs/world-evolution-backend.md +200 -0
  159. package/docs/world-evolution-engine.md +150 -0
  160. package/docs/world-evolution-host-backend.md +70 -0
  161. package/docs/world-evolution-orchestration.md +248 -0
  162. package/package.json +125 -6
  163. package/schema/content-pack.schema.json +85 -0
  164. package/schema/plugin.schema.json +57 -0
  165. package/schema/world-evolution-input.schema.json +196 -0
@@ -0,0 +1,56 @@
1
+ export interface ContentPackRegistry {
2
+ compatRange?: string;
3
+ [key: string]: unknown;
4
+ }
5
+ export interface WeaponDamageProfile {
6
+ surfaceFrac: number;
7
+ internalFrac: number;
8
+ structuralFrac: number;
9
+ bleedFactor: number;
10
+ penetrationBias: number;
11
+ }
12
+ export interface ContentWeapon {
13
+ id: string;
14
+ name: string;
15
+ mass_kg: number;
16
+ damage: WeaponDamageProfile;
17
+ [key: string]: unknown;
18
+ }
19
+ export interface ContentArmour {
20
+ id: string;
21
+ name: string;
22
+ mass_kg: number;
23
+ resist_J: number;
24
+ protectedDamageMul: number;
25
+ coverageByRegion: Record<string, number>;
26
+ [key: string]: unknown;
27
+ }
28
+ export interface ContentArchetype {
29
+ id: string;
30
+ base?: string;
31
+ overrides?: Record<string, unknown>;
32
+ [key: string]: unknown;
33
+ }
34
+ export interface ContentTerrain {
35
+ id: string;
36
+ name: string;
37
+ tags?: string[];
38
+ tractionMul?: number;
39
+ cover?: number;
40
+ [key: string]: unknown;
41
+ }
42
+ export interface ContentPack {
43
+ $schema?: string;
44
+ name: string;
45
+ version: string;
46
+ description?: string;
47
+ registry?: ContentPackRegistry;
48
+ weapons?: ContentWeapon[];
49
+ armour?: ContentArmour[];
50
+ archetypes?: ContentArchetype[];
51
+ terrain?: ContentTerrain[];
52
+ }
53
+ export interface ContentPackValidationError {
54
+ path: string;
55
+ message: string;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { ContentPack, ContentPackValidationError } from "./types.js";
2
+ export interface ContentValidationResult {
3
+ schemaErrors: ContentPackValidationError[];
4
+ semanticWarnings: ContentPackValidationError[];
5
+ }
6
+ export declare function runContentSemanticChecks(pack: ContentPack): ContentPackValidationError[];
@@ -0,0 +1,14 @@
1
+ export function runContentSemanticChecks(pack) {
2
+ const warnings = [];
3
+ (pack.weapons ?? []).forEach((weapon, idx) => {
4
+ const damage = weapon.damage;
5
+ const roughDamage = (damage.surfaceFrac + damage.internalFrac + damage.structuralFrac) * 1000;
6
+ if (roughDamage > 1000) {
7
+ warnings.push({
8
+ path: `$.weapons[${idx}].damage`,
9
+ message: `weapon damage > 1000 is likely wrong (score=${roughDamage.toFixed(2)})`,
10
+ });
11
+ }
12
+ });
13
+ return warnings;
14
+ }
@@ -1,6 +1,5 @@
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.69";
2
+ export { ANANKE_ENGINE_VERSION } from "./version.js";
4
3
  /** A single actionable validation failure from `validatePack`. */
5
4
  export interface PackValidationError {
6
5
  /** JSONPath-style location, e.g. `"$.weapons[2].mass_kg"`. */
@@ -135,6 +134,8 @@ export declare function semverSatisfies(version: string, range: string): boolean
135
134
  * @returns Array of `PackValidationError`. Empty means valid.
136
135
  */
137
136
  export declare function validatePack(manifest: unknown): PackValidationError[];
137
+ /** Compute SHA-256 for a manifest's canonical JSON with `registry.checksum` blanked. */
138
+ export declare function computePackChecksum(manifest: AnankePackManifest): string;
138
139
  /**
139
140
  * Validate and load a pack manifest into the active catalogues.
140
141
  *
@@ -13,19 +13,19 @@ 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.69";
16
+ // ── Runtime version constant ─────────────────────────────────────────────────────
17
+ export { ANANKE_ENGINE_VERSION } from "./version.js";
18
+ import { ANANKE_ENGINE_VERSION } from "./version.js";
20
19
  // ── Semver utilities ──────────────────────────────────────────────────────────
21
20
  // Lightweight range evaluator — no external dependencies.
22
21
  // Supports: >=X.Y.Z >X.Y.Z <=X.Y.Z <X.Y.Z =X.Y.Z ^X.Y.Z ~X.Y.Z
23
22
  // Short forms X.Y and X treated as X.Y.0 and X.0.0 respectively.
24
23
  // Compound ranges (space-separated) require all constraints to match.
25
24
  function parseSemverTuple(v) {
26
- const parts = v.replace(/^v/, "").split(".").map(Number);
27
- if (parts.some(isNaN))
25
+ const raw = v.trim().replace(/^v/, "");
26
+ if (!/^\d+(\.\d+){0,2}$/.test(raw))
28
27
  return null;
28
+ const parts = raw.split(".").map(Number);
29
29
  const [major = 0, minor = 0, patch = 0] = parts;
30
30
  return [major, minor, patch];
31
31
  }
@@ -44,13 +44,46 @@ export function semverSatisfies(version, range) {
44
44
  const ver = parseSemverTuple(version);
45
45
  if (!ver)
46
46
  return false;
47
- const constraints = range.trim().split(/\s+/);
47
+ const trimmed = range.trim();
48
+ if (trimmed.length === 0)
49
+ return false;
50
+ const constraints = trimmed.split(/\s+/);
48
51
  for (const constraint of constraints) {
52
+ if (constraint.length === 0)
53
+ return false;
49
54
  if (!evalConstraint(ver, constraint.trim()))
50
55
  return false;
51
56
  }
52
57
  return true;
53
58
  }
59
+ function isSemverRange(range) {
60
+ const trimmed = range.trim();
61
+ if (trimmed.length === 0)
62
+ return false;
63
+ const constraints = trimmed.split(/\s+/);
64
+ for (const constraint of constraints) {
65
+ if (constraint.length === 0)
66
+ return false;
67
+ if (constraint.startsWith("^") || constraint.startsWith("~")) {
68
+ if (parseSemverTuple(constraint.slice(1)) === null)
69
+ return false;
70
+ continue;
71
+ }
72
+ if (constraint.startsWith(">=") ||
73
+ constraint.startsWith("<=") ||
74
+ constraint.startsWith(">") ||
75
+ constraint.startsWith("<") ||
76
+ constraint.startsWith("=")) {
77
+ const offset = constraint.startsWith(">=") || constraint.startsWith("<=") ? 2 : 1;
78
+ if (parseSemverTuple(constraint.slice(offset)) === null)
79
+ return false;
80
+ continue;
81
+ }
82
+ if (parseSemverTuple(constraint) === null)
83
+ return false;
84
+ }
85
+ return true;
86
+ }
54
87
  function evalConstraint(ver, c) {
55
88
  // Caret: ^X.Y.Z — compatible within the leftmost non-zero component.
56
89
  // ^1.2.3 → >=1.2.3 <2.0.0 (major locked when major > 0)
@@ -125,9 +158,23 @@ export function validatePack(manifest) {
125
158
  }
126
159
  // Required: version (semver-ish)
127
160
  if (typeof m["version"] !== "string" ||
128
- !/^\d+\.\d+(\.\d+)?$/.test(m["version"])) {
161
+ parseSemverTuple(m["version"]) === null) {
129
162
  errors.push({ path: "$.version", message: 'must be a semver string like "1.0.0" or "1.0"' });
130
163
  }
164
+ if (m["anankeVersion"] !== undefined) {
165
+ if (typeof m["anankeVersion"] !== "string") {
166
+ errors.push({ path: "$.anankeVersion", message: "must be a semver range string" });
167
+ }
168
+ else if (!isSemverRange(m["anankeVersion"])) {
169
+ errors.push({ path: "$.anankeVersion", message: "must be a valid semver range expression" });
170
+ }
171
+ else if (!semverSatisfies(ANANKE_ENGINE_VERSION, m["anankeVersion"])) {
172
+ errors.push({
173
+ path: "$.anankeVersion",
174
+ message: `engine version ${ANANKE_ENGINE_VERSION} does not satisfy range "${m["anankeVersion"]}"`,
175
+ });
176
+ }
177
+ }
131
178
  // Optional: registry block
132
179
  if (m["registry"] !== undefined) {
133
180
  if (typeof m["registry"] !== "object" || m["registry"] === null || Array.isArray(m["registry"])) {
@@ -140,6 +187,9 @@ export function validatePack(manifest) {
140
187
  if (typeof reg["compatRange"] !== "string") {
141
188
  errors.push({ path: "$.registry.compatRange", message: "must be a string" });
142
189
  }
190
+ else if (!isSemverRange(reg["compatRange"])) {
191
+ errors.push({ path: "$.registry.compatRange", message: "must be a valid semver range expression" });
192
+ }
143
193
  else if (!semverSatisfies(ANANKE_ENGINE_VERSION, reg["compatRange"])) {
144
194
  errors.push({
145
195
  path: "$.registry.compatRange",
@@ -205,20 +255,39 @@ export function validatePack(manifest) {
205
255
  }
206
256
  // Validate weapon entries
207
257
  if (Array.isArray(m["weapons"])) {
258
+ const ids = new Set();
208
259
  for (let i = 0; i < m["weapons"].length; i++) {
209
260
  const w = m["weapons"][i];
210
261
  errors.push(...validateWeaponEntry(w, i));
262
+ if (typeof w === "object" && w !== null) {
263
+ const id = w["id"];
264
+ if (typeof id === "string") {
265
+ if (ids.has(id))
266
+ errors.push({ path: `$.weapons[${i}].id`, message: `duplicate weapon id "${id}"` });
267
+ ids.add(id);
268
+ }
269
+ }
211
270
  }
212
271
  }
213
272
  // Validate armour entries
214
273
  if (Array.isArray(m["armour"])) {
274
+ const ids = new Set();
215
275
  for (let i = 0; i < m["armour"].length; i++) {
216
276
  const a = m["armour"][i];
217
277
  errors.push(...validateArmourEntry(a, i));
278
+ if (typeof a === "object" && a !== null) {
279
+ const id = a["id"];
280
+ if (typeof id === "string") {
281
+ if (ids.has(id))
282
+ errors.push({ path: `$.armour[${i}].id`, message: `duplicate armour id "${id}"` });
283
+ ids.add(id);
284
+ }
285
+ }
218
286
  }
219
287
  }
220
288
  // Validate archetype entries (minimal — full validation is in registerArchetype)
221
289
  if (Array.isArray(m["archetypes"])) {
290
+ const ids = new Set();
222
291
  for (let i = 0; i < m["archetypes"].length; i++) {
223
292
  const arch = m["archetypes"][i];
224
293
  if (typeof arch !== "object" || arch === null) {
@@ -229,19 +298,130 @@ export function validatePack(manifest) {
229
298
  if (typeof o["id"] !== "string" || o["id"].trim() === "") {
230
299
  errors.push({ path: `$.archetypes[${i}].id`, message: "must be a non-empty string" });
231
300
  }
301
+ else {
302
+ if (ids.has(o["id"]))
303
+ errors.push({ path: `$.archetypes[${i}].id`, message: `duplicate archetype id "${o["id"]}"` });
304
+ ids.add(o["id"]);
305
+ }
232
306
  }
233
307
  }
234
308
  // Validate scenario entries via existing validateScenario
235
309
  if (Array.isArray(m["scenarios"])) {
310
+ const ids = new Set();
236
311
  for (let i = 0; i < m["scenarios"].length; i++) {
237
- const scenErrors = validateScenario(m["scenarios"][i]);
312
+ const scenario = m["scenarios"][i];
313
+ if (typeof scenario === "object" && scenario !== null) {
314
+ const id = scenario["id"];
315
+ if (typeof id === "string") {
316
+ if (ids.has(id))
317
+ errors.push({ path: `$.scenarios[${i}].id`, message: `duplicate scenario id "${id}"` });
318
+ ids.add(id);
319
+ }
320
+ }
321
+ const scenErrors = validateScenario(scenario);
238
322
  for (const msg of scenErrors) {
239
323
  errors.push({ path: `$.scenarios[${i}]`, message: msg });
240
324
  }
241
325
  }
242
326
  }
327
+ const checksum = m["registry"]?.["checksum"];
328
+ if (typeof checksum === "string" && /^[0-9a-f]{64}$/.test(checksum)) {
329
+ const computed = computePackChecksum(m);
330
+ if (computed !== checksum) {
331
+ errors.push({
332
+ path: "$.registry.checksum",
333
+ message: `checksum mismatch: expected "${checksum}" but computed "${computed}"`,
334
+ });
335
+ }
336
+ }
243
337
  return errors;
244
338
  }
339
+ /** Compute SHA-256 for a manifest's canonical JSON with `registry.checksum` blanked. */
340
+ export function computePackChecksum(manifest) {
341
+ const clone = JSON.parse(JSON.stringify(manifest));
342
+ const registry = clone["registry"];
343
+ if (typeof registry === "object" && registry !== null && !Array.isArray(registry)) {
344
+ registry["checksum"] = "";
345
+ }
346
+ return sha256Hex(JSON.stringify(clone));
347
+ }
348
+ /** Portable SHA-256 (sync) used by pack checksum validation in both Node and browser/worker runtimes. */
349
+ function sha256Hex(input) {
350
+ const bytes = new TextEncoder().encode(input);
351
+ const K = [
352
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
353
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
354
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
355
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
356
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
357
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
358
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
359
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
360
+ ];
361
+ const H = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
362
+ const paddedLength = (((bytes.length + 9 + 63) >> 6) << 6);
363
+ const data = new Uint8Array(paddedLength);
364
+ data.set(bytes);
365
+ data[bytes.length] = 0x80;
366
+ const bitLenHi = Math.floor((bytes.length * 8) / 0x100000000);
367
+ const bitLenLo = (bytes.length * 8) >>> 0;
368
+ data[paddedLength - 8] = (bitLenHi >>> 24) & 0xff;
369
+ data[paddedLength - 7] = (bitLenHi >>> 16) & 0xff;
370
+ data[paddedLength - 6] = (bitLenHi >>> 8) & 0xff;
371
+ data[paddedLength - 5] = bitLenHi & 0xff;
372
+ data[paddedLength - 4] = (bitLenLo >>> 24) & 0xff;
373
+ data[paddedLength - 3] = (bitLenLo >>> 16) & 0xff;
374
+ data[paddedLength - 2] = (bitLenLo >>> 8) & 0xff;
375
+ data[paddedLength - 1] = bitLenLo & 0xff;
376
+ const w = new Uint32Array(64);
377
+ for (let i = 0; i < data.length; i += 64) {
378
+ for (let t = 0; t < 16; t += 1) {
379
+ const j = i + (t << 2);
380
+ w[t] = ((data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | data[j + 3]) >>> 0;
381
+ }
382
+ for (let t = 16; t < 64; t += 1) {
383
+ const s0 = (rotR(w[t - 15], 7) ^ rotR(w[t - 15], 18) ^ (w[t - 15] >>> 3)) >>> 0;
384
+ const s1 = (rotR(w[t - 2], 17) ^ rotR(w[t - 2], 19) ^ (w[t - 2] >>> 10)) >>> 0;
385
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
386
+ }
387
+ let a = H[0];
388
+ let b = H[1];
389
+ let c = H[2];
390
+ let d = H[3];
391
+ let e = H[4];
392
+ let f = H[5];
393
+ let g = H[6];
394
+ let h = H[7];
395
+ for (let t = 0; t < 64; t += 1) {
396
+ const S1 = (rotR(e, 6) ^ rotR(e, 11) ^ rotR(e, 25)) >>> 0;
397
+ const ch = ((e & f) ^ (~e & g)) >>> 0;
398
+ const temp1 = (h + S1 + ch + K[t] + w[t]) >>> 0;
399
+ const S0 = (rotR(a, 2) ^ rotR(a, 13) ^ rotR(a, 22)) >>> 0;
400
+ const maj = ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
401
+ const temp2 = (S0 + maj) >>> 0;
402
+ h = g;
403
+ g = f;
404
+ f = e;
405
+ e = (d + temp1) >>> 0;
406
+ d = c;
407
+ c = b;
408
+ b = a;
409
+ a = (temp1 + temp2) >>> 0;
410
+ }
411
+ H[0] = (H[0] + a) >>> 0;
412
+ H[1] = (H[1] + b) >>> 0;
413
+ H[2] = (H[2] + c) >>> 0;
414
+ H[3] = (H[3] + d) >>> 0;
415
+ H[4] = (H[4] + e) >>> 0;
416
+ H[5] = (H[5] + f) >>> 0;
417
+ H[6] = (H[6] + g) >>> 0;
418
+ H[7] = (H[7] + h) >>> 0;
419
+ }
420
+ return H.map((n) => n.toString(16).padStart(8, "0")).join("");
421
+ }
422
+ function rotR(x, n) {
423
+ return (x >>> n) | (x << (32 - n));
424
+ }
245
425
  function validateWeaponEntry(w, i) {
246
426
  const errors = [];
247
427
  if (typeof w !== "object" || w === null) {
@@ -297,9 +477,22 @@ function validateArmourEntry(a, i) {
297
477
  */
298
478
  export function loadPack(manifest) {
299
479
  const packId = `${manifest.name}@${manifest.version}`;
480
+ const fingerprint = hashMod(manifest);
300
481
  // Already loaded — return stored summary
301
482
  const existing = _packs.get(packId);
302
483
  if (existing !== undefined) {
484
+ if (existing.fingerprint !== fingerprint) {
485
+ return {
486
+ packId,
487
+ registeredIds: [],
488
+ scenarioIds: [],
489
+ fingerprint: existing.fingerprint,
490
+ errors: [{
491
+ path: "$",
492
+ message: `pack "${packId}" already loaded with different content fingerprint (${existing.fingerprint} != ${fingerprint})`,
493
+ }],
494
+ };
495
+ }
303
496
  return {
304
497
  packId,
305
498
  registeredIds: existing.registeredIds,
@@ -359,7 +552,9 @@ export function loadPack(manifest) {
359
552
  scenarioMap.set(id, scen);
360
553
  scenarioIds.push(id);
361
554
  }
362
- const fingerprint = hashMod(manifest);
555
+ if (loadErrors.length > 0) {
556
+ return { packId, registeredIds, scenarioIds: [], fingerprint, errors: loadErrors };
557
+ }
363
558
  _packs.set(packId, {
364
559
  manifest,
365
560
  registeredIds,
@@ -0,0 +1,73 @@
1
+ export type RegionId = string;
2
+ export type TenantId = string;
3
+ export interface RetentionPolicy {
4
+ /**
5
+ * Default time-to-live in milliseconds for records.
6
+ */
7
+ defaultTtlMs: number;
8
+ }
9
+ export interface DataEnvelope {
10
+ id: string;
11
+ tenantId: TenantId;
12
+ regionId: RegionId;
13
+ createdAtMs: number;
14
+ expiresAtMs: number;
15
+ legalHold: boolean;
16
+ piiDetected: boolean;
17
+ piiFindings: PIIFinding[];
18
+ payload: unknown;
19
+ }
20
+ export interface PIIFinding {
21
+ path: string;
22
+ kind: "email" | "phone" | "ssn" | "creditCard" | "sensitiveKey";
23
+ }
24
+ export interface AccessEvent {
25
+ timestampMs: number;
26
+ actor: string;
27
+ action: "write" | "read" | "list" | "legal_hold_set" | "legal_hold_released" | "purge";
28
+ tenantId: TenantId;
29
+ regionId: RegionId;
30
+ recordId?: string;
31
+ details?: string;
32
+ }
33
+ export interface IngestOptions {
34
+ nowMs?: number;
35
+ ttlMs?: number;
36
+ redactSensitiveFields?: boolean;
37
+ actor?: string;
38
+ }
39
+ export interface ReadOptions {
40
+ nowMs?: number;
41
+ actor?: string;
42
+ }
43
+ export interface ListOptions {
44
+ nowMs?: number;
45
+ actor?: string;
46
+ }
47
+ export interface LegalHoldOptions {
48
+ actor?: string;
49
+ nowMs?: number;
50
+ }
51
+ export interface TenancyModel {
52
+ isolation: "tenant-region-partition";
53
+ partitionKey: "regionId/tenantId";
54
+ replication: "none_cross_region";
55
+ accessAudit: "immutable_append_only";
56
+ }
57
+ export declare function buildTenancyModel(): TenancyModel;
58
+ export declare class DataGovernanceStore {
59
+ private readonly retention;
60
+ private readonly records;
61
+ private readonly byPartition;
62
+ private readonly accessLog;
63
+ constructor(retention: RetentionPolicy);
64
+ ingest(tenantId: TenantId, regionId: RegionId, recordId: string, payload: unknown, options?: IngestOptions): DataEnvelope;
65
+ get(tenantId: TenantId, regionId: RegionId, recordId: string, options?: ReadOptions): DataEnvelope | undefined;
66
+ listByPartition(tenantId: TenantId, regionId: RegionId, options?: ListOptions): DataEnvelope[];
67
+ setLegalHold(tenantId: TenantId, regionId: RegionId, recordId: string, options?: LegalHoldOptions): boolean;
68
+ releaseLegalHold(tenantId: TenantId, regionId: RegionId, recordId: string, options?: LegalHoldOptions): boolean;
69
+ purgeExpired(nowMs: number, actor?: string): number;
70
+ getAccessLog(): readonly AccessEvent[];
71
+ private updateLegalHold;
72
+ private log;
73
+ }
@@ -0,0 +1,188 @@
1
+ export function buildTenancyModel() {
2
+ return {
3
+ isolation: "tenant-region-partition",
4
+ partitionKey: "regionId/tenantId",
5
+ replication: "none_cross_region",
6
+ accessAudit: "immutable_append_only",
7
+ };
8
+ }
9
+ const SENSITIVE_KEY_PATTERN = /(email|phone|ssn|social|taxid|credit|card|password|secret|token|passport)/i;
10
+ const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
11
+ const PHONE_PATTERN = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/;
12
+ const SSN_PATTERN = /\b\d{3}-\d{2}-\d{4}\b/;
13
+ const CREDIT_CARD_PATTERN = /\b(?:\d[ -]*?){13,19}\b/;
14
+ function detectFindingsFromString(path, value) {
15
+ const findings = [];
16
+ if (EMAIL_PATTERN.test(value))
17
+ findings.push({ path, kind: "email" });
18
+ if (PHONE_PATTERN.test(value))
19
+ findings.push({ path, kind: "phone" });
20
+ if (SSN_PATTERN.test(value))
21
+ findings.push({ path, kind: "ssn" });
22
+ if (CREDIT_CARD_PATTERN.test(value))
23
+ findings.push({ path, kind: "creditCard" });
24
+ return findings;
25
+ }
26
+ function scanValue(path, value, redactSensitiveFields) {
27
+ if (typeof value === "string") {
28
+ const findings = detectFindingsFromString(path, value);
29
+ return {
30
+ findings,
31
+ redactedValue: redactSensitiveFields && findings.length > 0 ? "[REDACTED]" : value,
32
+ };
33
+ }
34
+ if (Array.isArray(value)) {
35
+ const findings = [];
36
+ const redacted = value.map((entry, idx) => {
37
+ const child = scanValue(`${path}[${idx}]`, entry, redactSensitiveFields);
38
+ findings.push(...child.findings);
39
+ return child.redactedValue;
40
+ });
41
+ return { findings, redactedValue: redacted };
42
+ }
43
+ if (value !== null && typeof value === "object") {
44
+ const findings = [];
45
+ const out = {};
46
+ for (const [key, childValue] of Object.entries(value)) {
47
+ const childPath = path ? `${path}.${key}` : key;
48
+ const keyIsSensitive = SENSITIVE_KEY_PATTERN.test(key);
49
+ if (keyIsSensitive) {
50
+ findings.push({ path: childPath, kind: "sensitiveKey" });
51
+ }
52
+ if (keyIsSensitive && redactSensitiveFields) {
53
+ out[key] = "[REDACTED]";
54
+ continue;
55
+ }
56
+ const child = scanValue(childPath, childValue, redactSensitiveFields);
57
+ findings.push(...child.findings);
58
+ out[key] = child.redactedValue;
59
+ }
60
+ return { findings, redactedValue: out };
61
+ }
62
+ return { findings: [], redactedValue: value };
63
+ }
64
+ function makeRecordKey(tenantId, regionId, id) {
65
+ return `${regionId}::${tenantId}::${id}`;
66
+ }
67
+ export class DataGovernanceStore {
68
+ retention;
69
+ records = new Map();
70
+ byPartition = new Map();
71
+ accessLog = [];
72
+ constructor(retention) {
73
+ if (retention.defaultTtlMs <= 0) {
74
+ throw new Error("defaultTtlMs must be > 0");
75
+ }
76
+ this.retention = retention;
77
+ }
78
+ ingest(tenantId, regionId, recordId, payload, options = {}) {
79
+ const nowMs = options.nowMs ?? Date.now();
80
+ const ttlMs = options.ttlMs ?? this.retention.defaultTtlMs;
81
+ const redactSensitiveFields = options.redactSensitiveFields ?? true;
82
+ const actor = options.actor ?? "system";
83
+ const scanned = scanValue("", payload, redactSensitiveFields);
84
+ const envelope = {
85
+ id: recordId,
86
+ tenantId,
87
+ regionId,
88
+ createdAtMs: nowMs,
89
+ expiresAtMs: nowMs + ttlMs,
90
+ legalHold: false,
91
+ piiDetected: scanned.findings.length > 0,
92
+ piiFindings: scanned.findings,
93
+ payload: scanned.redactedValue,
94
+ };
95
+ const key = makeRecordKey(tenantId, regionId, recordId);
96
+ this.records.set(key, envelope);
97
+ const partition = `${regionId}/${tenantId}`;
98
+ let set = this.byPartition.get(partition);
99
+ if (!set) {
100
+ set = new Set();
101
+ this.byPartition.set(partition, set);
102
+ }
103
+ set.add(key);
104
+ this.log({ timestampMs: nowMs, actor, action: "write", tenantId, regionId, recordId });
105
+ return envelope;
106
+ }
107
+ get(tenantId, regionId, recordId, options = {}) {
108
+ const nowMs = options.nowMs ?? Date.now();
109
+ const actor = options.actor ?? "system";
110
+ const key = makeRecordKey(tenantId, regionId, recordId);
111
+ const record = this.records.get(key);
112
+ this.log({ timestampMs: nowMs, actor, action: "read", tenantId, regionId, recordId });
113
+ if (!record)
114
+ return undefined;
115
+ if (!record.legalHold && record.expiresAtMs <= nowMs)
116
+ return undefined;
117
+ return record;
118
+ }
119
+ listByPartition(tenantId, regionId, options = {}) {
120
+ const nowMs = options.nowMs ?? Date.now();
121
+ const actor = options.actor ?? "system";
122
+ this.log({ timestampMs: nowMs, actor, action: "list", tenantId, regionId });
123
+ const partition = `${regionId}/${tenantId}`;
124
+ const keys = this.byPartition.get(partition);
125
+ if (!keys)
126
+ return [];
127
+ const out = [];
128
+ for (const key of keys) {
129
+ const rec = this.records.get(key);
130
+ if (!rec)
131
+ continue;
132
+ if (!rec.legalHold && rec.expiresAtMs <= nowMs)
133
+ continue;
134
+ out.push(rec);
135
+ }
136
+ return out;
137
+ }
138
+ setLegalHold(tenantId, regionId, recordId, options = {}) {
139
+ return this.updateLegalHold(tenantId, regionId, recordId, true, options);
140
+ }
141
+ releaseLegalHold(tenantId, regionId, recordId, options = {}) {
142
+ return this.updateLegalHold(tenantId, regionId, recordId, false, options);
143
+ }
144
+ purgeExpired(nowMs, actor = "system") {
145
+ let purged = 0;
146
+ for (const [key, record] of this.records.entries()) {
147
+ if (record.legalHold || record.expiresAtMs > nowMs)
148
+ continue;
149
+ this.records.delete(key);
150
+ const partition = `${record.regionId}/${record.tenantId}`;
151
+ this.byPartition.get(partition)?.delete(key);
152
+ this.log({
153
+ timestampMs: nowMs,
154
+ actor,
155
+ action: "purge",
156
+ tenantId: record.tenantId,
157
+ regionId: record.regionId,
158
+ recordId: record.id,
159
+ });
160
+ purged += 1;
161
+ }
162
+ return purged;
163
+ }
164
+ getAccessLog() {
165
+ return this.accessLog;
166
+ }
167
+ updateLegalHold(tenantId, regionId, recordId, state, options) {
168
+ const nowMs = options.nowMs ?? Date.now();
169
+ const actor = options.actor ?? "system";
170
+ const key = makeRecordKey(tenantId, regionId, recordId);
171
+ const rec = this.records.get(key);
172
+ if (!rec)
173
+ return false;
174
+ rec.legalHold = state;
175
+ this.log({
176
+ timestampMs: nowMs,
177
+ actor,
178
+ action: state ? "legal_hold_set" : "legal_hold_released",
179
+ tenantId,
180
+ regionId,
181
+ recordId,
182
+ });
183
+ return true;
184
+ }
185
+ log(event) {
186
+ this.accessLog.push(event);
187
+ }
188
+ }