@its-not-rocket-science/ananke 0.1.64 → 0.1.66
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 +48 -0
- package/dist/src/content-pack.d.ts +68 -1
- package/dist/src/content-pack.js +158 -0
- package/dist/src/sim/ai/perception.d.ts +1 -1
- package/dist/src/sim/testing.d.ts +1 -1
- package/dist/tools/pack-cli.js +9 -0
- package/docs/versioning.md +64 -0
- package/package.json +3 -2
- package/schema/pack.schema.json +45 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,54 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.66] — 2026-04-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **PM-7 — API Deprecation Framework (complete):**
|
|
14
|
+
- `tools/audit-deprecations.ts` (new): scans all `src/` TypeScript files for `@deprecated` JSDoc tags and outputs a structured table of `{ symbol, file, line, since, removeAfter, replacement, overdue }`.
|
|
15
|
+
- `--json` flag: machine-readable JSON output with timestamp, engine version, and full entry list.
|
|
16
|
+
- `--check` flag: exits 1 if any symbol's `removeAfter` version ≤ current engine version (overdue).
|
|
17
|
+
- `prepublishOnly` now includes `npm run audit-deprecations -- --check`: `npm publish` fails if any symbol is overdue for removal.
|
|
18
|
+
- npm script: `audit-deprecations`.
|
|
19
|
+
- Structured `@deprecated` convention defined: `@deprecated since {version} — use {replacement} instead. Removes at {removeAfter}.`
|
|
20
|
+
- All three existing `@deprecated` tags in `src/` updated to the new structured format:
|
|
21
|
+
- `anankeVersion` in `content-pack.ts`: since 0.1.65, removes at 0.3.0
|
|
22
|
+
- `Perception` type alias in `sim/ai/perception.ts`: since 0.1.0, removes at 0.3.0
|
|
23
|
+
- `mkWorld(seed, loadout)` overload in `sim/testing.ts`: since 0.1.0, removes at 0.2.0
|
|
24
|
+
- `docs/versioning.md`: new "Deprecation lifecycle" section documenting the three-phase pattern (mark → migration window → remove), the required tag format, and the audit checklist.
|
|
25
|
+
- `docs/module-index.md`: new "Deprecated exports" table surfacing all known deprecated symbols with since/removeAfter/replacement.
|
|
26
|
+
- 0 new tests (5,593 total). Coverage: 97.11%/88.07%/95.83%/97.11%. Build: clean.
|
|
27
|
+
|
|
28
|
+
### Deprecated
|
|
29
|
+
|
|
30
|
+
- `AnankePackManifest.anankeVersion` — since 0.1.65, use `registry.compatRange` instead. Removes at 0.3.0.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## [0.1.65] — 2026-04-01
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **PM-6 — Content-Pack Registry Format (complete):**
|
|
39
|
+
- `PackRegistryMeta` interface (new): optional `registry` block in `AnankePackManifest` with fields:
|
|
40
|
+
- `compatRange` (string): semver range enforced at runtime by `validatePack` — rejects packs incompatible with the running engine version.
|
|
41
|
+
- `stabilityTier` (`"stable"` | `"experimental"` | `"internal"`): controls listing in a public registry.
|
|
42
|
+
- `requiredExports` (string[]): subpath exports the pack's content depends on — informational.
|
|
43
|
+
- `checksum` (string): SHA-256 hex digest of the pack JSON — computed by `npx ananke pack bundle`, verified by the host.
|
|
44
|
+
- `license` (string): SPDX identifier.
|
|
45
|
+
- `provenance` (object[]): dataset / paper references for empirically grounded content.
|
|
46
|
+
- `PackStabilityTier` and `PackProvenanceRef` types (new, exported from `"./content-pack"`).
|
|
47
|
+
- `ANANKE_ENGINE_VERSION = "0.1.65"` constant (new, exported from `"./content-pack"`): current engine version used in `compatRange` evaluation.
|
|
48
|
+
- `semverSatisfies(version, range)` (new, exported): lightweight semver range evaluator — supports `>=`, `>`, `<=`, `<`, `=`, `^` (caret), `~` (tilde), bare version, and compound space-separated ranges. No external dependencies.
|
|
49
|
+
- `validatePack` extended to validate all registry sub-fields and reject incompatible `compatRange`.
|
|
50
|
+
- `tools/pack-cli.ts` `bundle` command: automatically computes SHA-256 checksum and embeds it in `registry.checksum` before writing the bundle.
|
|
51
|
+
- `schema/pack.schema.json`: `registry` block with full JSON Schema definition for all sub-fields.
|
|
52
|
+
- `docs/pack-registry-spec.md` (new): full specification — field reference, checksum algorithm, runtime enforcement table, future online registry design.
|
|
53
|
+
- 24 new tests (5,593 total). Coverage: 97.11%/88.07%/95.83%/97.11%. Build: clean.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
9
57
|
## [0.1.64] — 2026-04-01
|
|
10
58
|
|
|
11
59
|
### Added
|
|
@@ -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.66";
|
|
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 since 0.1.65 — use `registry.compatRange` instead. Removes at 0.3.0.
|
|
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.66";
|
|
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])) {
|
|
@@ -7,6 +7,6 @@ export interface LocalPerception {
|
|
|
7
7
|
enemies: Entity[];
|
|
8
8
|
allies: Entity[];
|
|
9
9
|
}
|
|
10
|
-
/** @deprecated
|
|
10
|
+
/** @deprecated since 0.1.0 — use `LocalPerception` instead. Removes at 0.3.0. */
|
|
11
11
|
export type Perception = LocalPerception;
|
|
12
12
|
export declare function perceiveLocal(world: WorldState | undefined, self: Entity, index: WorldIndex, spatial: SpatialIndex, radius_m: number, maxCount?: number, env?: SensoryEnvironment): LocalPerception;
|
|
@@ -8,6 +8,6 @@ import { ImpactEvent } from "./events.js";
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function mkHumanoidEntity(id: number, teamId: number, x_m: number, y_m: number, z_m?: number): Entity;
|
|
10
10
|
export declare function mkWorld(seed: number, entities: Entity[]): WorldState;
|
|
11
|
-
/** @deprecated
|
|
11
|
+
/** @deprecated since 0.1.0 — use `mkWorld(seed, entities[])` instead. Removes at 0.2.0. */
|
|
12
12
|
export declare function mkWorld(seed: number, loadoutA: Loadout): WorldState;
|
|
13
13
|
export declare function mkImpactEvent(attackerId: number, targetId: number, region?: string, energy_J?: number, protectedByArmour?: boolean, blocked?: boolean, parried?: boolean, weaponId?: string, wpn?: Weapon, hitQuality?: number, shieldBlocked?: boolean): ImpactEvent;
|
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/docs/versioning.md
CHANGED
|
@@ -179,3 +179,67 @@ body plan hooks):
|
|
|
179
179
|
3. Keep an `UPSTREAM.md` at your fork root noting your base version and a diff summary
|
|
180
180
|
4. Periodically rebase onto upstream Tier 3 commits to collect non-breaking improvements;
|
|
181
181
|
treat Tier 1/2 commits as explicit migration tasks to schedule
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Deprecation lifecycle
|
|
186
|
+
|
|
187
|
+
Symbols are deprecated rather than removed immediately so downstream projects have time
|
|
188
|
+
to migrate. The lifecycle follows a three-phase pattern:
|
|
189
|
+
|
|
190
|
+
### 1 — Mark deprecated (current version)
|
|
191
|
+
|
|
192
|
+
Add a structured JSDoc tag to the symbol:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
/**
|
|
196
|
+
* @deprecated since 0.1.50 — use `newFunction` instead. Removes at 0.3.0.
|
|
197
|
+
*/
|
|
198
|
+
export function oldFunction() { … }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The **required format** is:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
@deprecated since {version} — use {replacement} instead. Removes at {removeAfter}.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
| Field | Meaning |
|
|
208
|
+
|-------|---------|
|
|
209
|
+
| `since` | Version in which the deprecation was introduced |
|
|
210
|
+
| `replacement` | Short description or code reference of what to use instead |
|
|
211
|
+
| `removeAfter` | Version at which the symbol will be deleted — must be a future version |
|
|
212
|
+
|
|
213
|
+
`removeAfter` must satisfy `> current` at publish time; `npm publish` runs
|
|
214
|
+
`audit-deprecations --check` and fails if any symbol is overdue.
|
|
215
|
+
|
|
216
|
+
### 2 — Migration window
|
|
217
|
+
|
|
218
|
+
During the migration window the symbol still works but emits a TypeScript deprecation
|
|
219
|
+
warning in IDEs (the `@deprecated` tag triggers the strikethrough).
|
|
220
|
+
|
|
221
|
+
The migration window is at least one **minor** version for Tier 2 symbols
|
|
222
|
+
and at least one **major** version for Tier 1 (Stable) symbols.
|
|
223
|
+
|
|
224
|
+
### 3 — Remove at removeAfter
|
|
225
|
+
|
|
226
|
+
When the engine reaches `removeAfter`, the symbol is deleted and a CHANGELOG entry is
|
|
227
|
+
added under a `### Removed` heading. The `since` version and the replacement are
|
|
228
|
+
included in the removal note so the changelog is self-contained.
|
|
229
|
+
|
|
230
|
+
### Auditing
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm run audit-deprecations # human-readable table
|
|
234
|
+
npm run audit-deprecations -- --json # machine-readable JSON
|
|
235
|
+
npm run audit-deprecations -- --check # exit 1 if any overdue
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The `--check` flag is run automatically by `npm run prepublishOnly`.
|
|
239
|
+
|
|
240
|
+
### Adding a new deprecation (checklist)
|
|
241
|
+
|
|
242
|
+
- [ ] Add the structured `@deprecated` JSDoc tag with `since`, replacement, and `removeAfter`.
|
|
243
|
+
- [ ] Run `npm run audit-deprecations` to confirm the tag is detected and not overdue.
|
|
244
|
+
- [ ] Add a CHANGELOG entry under `### Deprecated`.
|
|
245
|
+
- [ ] Surface the replacement in the relevant `docs/` file or `STABLE_API.md`.
|
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.66",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"game-engine"
|
|
248
248
|
],
|
|
249
249
|
"scripts": {
|
|
250
|
-
"prepublishOnly": "npm run build && npm run test:coverage",
|
|
250
|
+
"prepublishOnly": "npm run build && npm run test:coverage && npm run audit-deprecations -- --check",
|
|
251
251
|
"build": "tsc -p tsconfig.build.json",
|
|
252
252
|
"test": "vitest run",
|
|
253
253
|
"test:watch": "vitest",
|
|
@@ -298,6 +298,7 @@
|
|
|
298
298
|
"benchmark-check": "node dist/tools/benchmark-check.js",
|
|
299
299
|
"benchmark-check:strict": "node dist/tools/benchmark-check.js --threshold=0.10",
|
|
300
300
|
"benchmark-check:update": "node dist/tools/benchmark-check.js --update-baseline",
|
|
301
|
+
"audit-deprecations": "node dist/tools/audit-deprecations.js",
|
|
301
302
|
"benchmark:guide": "node dist/tools/benchmark-guide.js",
|
|
302
303
|
"benchmark:parallel": "node dist/tools/benchmark-parallel.js",
|
|
303
304
|
"run:renderer-bridge": "node dist/tools/renderer-bridge.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",
|