@its-not-rocket-science/ananke 0.1.64 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/src/content-pack.d.ts +68 -1
- package/dist/src/content-pack.js +158 -0
- package/dist/tools/pack-cli.js +9 -0
- package/package.json +1 -1
- package/schema/pack.schema.json +45 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.65] — 2026-04-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **PM-6 — Content-Pack Registry Format (complete):**
|
|
14
|
+
- `PackRegistryMeta` interface (new): optional `registry` block in `AnankePackManifest` with fields:
|
|
15
|
+
- `compatRange` (string): semver range enforced at runtime by `validatePack` — rejects packs incompatible with the running engine version.
|
|
16
|
+
- `stabilityTier` (`"stable"` | `"experimental"` | `"internal"`): controls listing in a public registry.
|
|
17
|
+
- `requiredExports` (string[]): subpath exports the pack's content depends on — informational.
|
|
18
|
+
- `checksum` (string): SHA-256 hex digest of the pack JSON — computed by `npx ananke pack bundle`, verified by the host.
|
|
19
|
+
- `license` (string): SPDX identifier.
|
|
20
|
+
- `provenance` (object[]): dataset / paper references for empirically grounded content.
|
|
21
|
+
- `PackStabilityTier` and `PackProvenanceRef` types (new, exported from `"./content-pack"`).
|
|
22
|
+
- `ANANKE_ENGINE_VERSION = "0.1.65"` constant (new, exported from `"./content-pack"`): current engine version used in `compatRange` evaluation.
|
|
23
|
+
- `semverSatisfies(version, range)` (new, exported): lightweight semver range evaluator — supports `>=`, `>`, `<=`, `<`, `=`, `^` (caret), `~` (tilde), bare version, and compound space-separated ranges. No external dependencies.
|
|
24
|
+
- `validatePack` extended to validate all registry sub-fields and reject incompatible `compatRange`.
|
|
25
|
+
- `tools/pack-cli.ts` `bundle` command: automatically computes SHA-256 checksum and embeds it in `registry.checksum` before writing the bundle.
|
|
26
|
+
- `schema/pack.schema.json`: `registry` block with full JSON Schema definition for all sub-fields.
|
|
27
|
+
- `docs/pack-registry-spec.md` (new): full specification — field reference, checksum algorithm, runtime enforcement table, future online registry design.
|
|
28
|
+
- 24 new tests (5,593 total). Coverage: 97.11%/88.07%/95.83%/97.11%. Build: clean.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
9
32
|
## [0.1.64] — 2026-04-01
|
|
10
33
|
|
|
11
34
|
### 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.65";
|
|
2
4
|
/** A single actionable validation failure from `validatePack`. */
|
|
3
5
|
export interface PackValidationError {
|
|
4
6
|
/** JSONPath-style location, e.g. `"$.weapons[2].mass_kg"`. */
|
|
@@ -6,6 +8,61 @@ export interface PackValidationError {
|
|
|
6
8
|
/** Human-readable explanation of what is wrong. */
|
|
7
9
|
message: string;
|
|
8
10
|
}
|
|
11
|
+
/** Stability tier for a content pack — controls how it is listed in a registry. */
|
|
12
|
+
export type PackStabilityTier = "stable" | "experimental" | "internal";
|
|
13
|
+
/** Dataset or paper reference for empirically grounded pack content. */
|
|
14
|
+
export interface PackProvenanceRef {
|
|
15
|
+
/** Short description of the source. */
|
|
16
|
+
title: string;
|
|
17
|
+
/** URL of the source, if available. */
|
|
18
|
+
url?: string;
|
|
19
|
+
/** DOI of the source, if applicable. */
|
|
20
|
+
doi?: string;
|
|
21
|
+
/** Free-text notes about what this source grounds. */
|
|
22
|
+
notes?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Registry metadata block — optional top-level section of a pack manifest.
|
|
26
|
+
*
|
|
27
|
+
* Including a `registry` block enables:
|
|
28
|
+
* - Runtime compatibility checking via `compatRange`
|
|
29
|
+
* - Deterministic integrity verification via `checksum` (SHA-256)
|
|
30
|
+
* - Licensing and provenance attestation for empirical content
|
|
31
|
+
*
|
|
32
|
+
* Generate the checksum with:
|
|
33
|
+
* `npx ananke pack bundle <dir>` (embeds it automatically)
|
|
34
|
+
*
|
|
35
|
+
* or manually with `computePackChecksum(manifest)` from `@ananke/content-pack`.
|
|
36
|
+
*/
|
|
37
|
+
export interface PackRegistryMeta {
|
|
38
|
+
/**
|
|
39
|
+
* Semver range of Ananke engine versions this pack targets.
|
|
40
|
+
* Examples: `">=0.1.50"`, `">=0.1 <0.2"`, `"^0.1.60"`.
|
|
41
|
+
* `validatePack` rejects packs whose `compatRange` excludes the running version.
|
|
42
|
+
*/
|
|
43
|
+
compatRange?: string;
|
|
44
|
+
/** Stability guarantee — governs how the pack appears in a public registry. */
|
|
45
|
+
stabilityTier?: PackStabilityTier;
|
|
46
|
+
/**
|
|
47
|
+
* Subpath exports from `@its-not-rocket-science/ananke` that this pack's
|
|
48
|
+
* content references, e.g. `["./combat", "./catalog"]`.
|
|
49
|
+
* Informational only — not enforced at runtime.
|
|
50
|
+
*/
|
|
51
|
+
requiredExports?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* SHA-256 hex digest of the pack JSON (with `registry.checksum` set to `""`
|
|
54
|
+
* before hashing, so the field is present but blank).
|
|
55
|
+
* Compute with `npx ananke pack bundle` or `computePackChecksum`.
|
|
56
|
+
*/
|
|
57
|
+
checksum?: string;
|
|
58
|
+
/** SPDX license identifier, e.g. `"MIT"`, `"CC-BY-4.0"`. */
|
|
59
|
+
license?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Dataset or paper references for empirically grounded pack content.
|
|
62
|
+
* Include when your pack derives parameters from research data.
|
|
63
|
+
*/
|
|
64
|
+
provenance?: PackProvenanceRef[];
|
|
65
|
+
}
|
|
9
66
|
/**
|
|
10
67
|
* The `.ananke-pack` manifest schema.
|
|
11
68
|
*
|
|
@@ -24,9 +81,14 @@ export interface AnankePackManifest {
|
|
|
24
81
|
description?: string;
|
|
25
82
|
/**
|
|
26
83
|
* Minimum Ananke version required, as a semver range string.
|
|
27
|
-
*
|
|
84
|
+
* @deprecated Use `registry.compatRange` instead — this field is informational only.
|
|
28
85
|
*/
|
|
29
86
|
anankeVersion?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Registry metadata — compatibility, checksum, license, and provenance.
|
|
89
|
+
* `registry.compatRange` is enforced at runtime by `validatePack`.
|
|
90
|
+
*/
|
|
91
|
+
registry?: PackRegistryMeta;
|
|
30
92
|
/** Weapon definitions — each passed to `registerWeapon`. */
|
|
31
93
|
weapons?: unknown[];
|
|
32
94
|
/** Armour definitions — each passed to `registerArmour`. */
|
|
@@ -59,6 +121,11 @@ export interface LoadPackResult {
|
|
|
59
121
|
/** Validation and registration errors. Empty on full success. */
|
|
60
122
|
errors: PackValidationError[];
|
|
61
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Test whether `version` satisfies `range`.
|
|
126
|
+
* Returns `false` if the range string is unparseable.
|
|
127
|
+
*/
|
|
128
|
+
export declare function semverSatisfies(version: string, range: string): boolean;
|
|
62
129
|
/**
|
|
63
130
|
* Validate a pack manifest for structural conformance without loading it.
|
|
64
131
|
*
|
package/dist/src/content-pack.js
CHANGED
|
@@ -13,6 +13,95 @@ import { registerWeapon, registerArmour, registerArchetype } from "./catalog.js"
|
|
|
13
13
|
import { validateScenario, loadScenario } from "./scenario.js";
|
|
14
14
|
import { hashMod } from "./modding.js";
|
|
15
15
|
import { registerWorldArchetype, registerWorldItem } from "./world-factory.js";
|
|
16
|
+
// ── Version constant ──────────────────────────────────────────────────────────
|
|
17
|
+
// Must be kept in sync with package.json "version" field.
|
|
18
|
+
/** Current Ananke engine version — used to evaluate pack compatRange at runtime. */
|
|
19
|
+
export const ANANKE_ENGINE_VERSION = "0.1.65";
|
|
20
|
+
// ── Semver utilities ──────────────────────────────────────────────────────────
|
|
21
|
+
// Lightweight range evaluator — no external dependencies.
|
|
22
|
+
// Supports: >=X.Y.Z >X.Y.Z <=X.Y.Z <X.Y.Z =X.Y.Z ^X.Y.Z ~X.Y.Z
|
|
23
|
+
// Short forms X.Y and X treated as X.Y.0 and X.0.0 respectively.
|
|
24
|
+
// Compound ranges (space-separated) require all constraints to match.
|
|
25
|
+
function parseSemverTuple(v) {
|
|
26
|
+
const parts = v.replace(/^v/, "").split(".").map(Number);
|
|
27
|
+
if (parts.some(isNaN))
|
|
28
|
+
return null;
|
|
29
|
+
const [major = 0, minor = 0, patch = 0] = parts;
|
|
30
|
+
return [major, minor, patch];
|
|
31
|
+
}
|
|
32
|
+
function cmpSemver(a, b) {
|
|
33
|
+
if (a[0] !== b[0])
|
|
34
|
+
return a[0] - b[0];
|
|
35
|
+
if (a[1] !== b[1])
|
|
36
|
+
return a[1] - b[1];
|
|
37
|
+
return a[2] - b[2];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Test whether `version` satisfies `range`.
|
|
41
|
+
* Returns `false` if the range string is unparseable.
|
|
42
|
+
*/
|
|
43
|
+
export function semverSatisfies(version, range) {
|
|
44
|
+
const ver = parseSemverTuple(version);
|
|
45
|
+
if (!ver)
|
|
46
|
+
return false;
|
|
47
|
+
const constraints = range.trim().split(/\s+/);
|
|
48
|
+
for (const constraint of constraints) {
|
|
49
|
+
if (!evalConstraint(ver, constraint.trim()))
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function evalConstraint(ver, c) {
|
|
55
|
+
// Caret: ^X.Y.Z — compatible within the leftmost non-zero component.
|
|
56
|
+
// ^1.2.3 → >=1.2.3 <2.0.0 (major locked when major > 0)
|
|
57
|
+
// ^0.2.3 → >=0.2.3 <0.3.0 (minor locked when major == 0, minor > 0)
|
|
58
|
+
// ^0.0.3 → >=0.0.3 <0.0.4 (patch locked when both major and minor == 0)
|
|
59
|
+
if (c.startsWith("^")) {
|
|
60
|
+
const lo = parseSemverTuple(c.slice(1));
|
|
61
|
+
if (!lo)
|
|
62
|
+
return false;
|
|
63
|
+
let hi;
|
|
64
|
+
if (lo[0] > 0)
|
|
65
|
+
hi = [lo[0] + 1, 0, 0];
|
|
66
|
+
else if (lo[1] > 0)
|
|
67
|
+
hi = [0, lo[1] + 1, 0];
|
|
68
|
+
else
|
|
69
|
+
hi = [0, 0, lo[2] + 1];
|
|
70
|
+
return cmpSemver(ver, lo) >= 0 && cmpSemver(ver, hi) < 0;
|
|
71
|
+
}
|
|
72
|
+
// Tilde: ~X.Y.Z → >=X.Y.Z <X.(Y+1).0
|
|
73
|
+
if (c.startsWith("~")) {
|
|
74
|
+
const lo = parseSemverTuple(c.slice(1));
|
|
75
|
+
if (!lo)
|
|
76
|
+
return false;
|
|
77
|
+
const hi = [lo[0], lo[1] + 1, 0];
|
|
78
|
+
return cmpSemver(ver, lo) >= 0 && cmpSemver(ver, hi) < 0;
|
|
79
|
+
}
|
|
80
|
+
// Comparators
|
|
81
|
+
if (c.startsWith(">=")) {
|
|
82
|
+
const t = parseSemverTuple(c.slice(2));
|
|
83
|
+
return t !== null && cmpSemver(ver, t) >= 0;
|
|
84
|
+
}
|
|
85
|
+
if (c.startsWith(">")) {
|
|
86
|
+
const t = parseSemverTuple(c.slice(1));
|
|
87
|
+
return t !== null && cmpSemver(ver, t) > 0;
|
|
88
|
+
}
|
|
89
|
+
if (c.startsWith("<=")) {
|
|
90
|
+
const t = parseSemverTuple(c.slice(2));
|
|
91
|
+
return t !== null && cmpSemver(ver, t) <= 0;
|
|
92
|
+
}
|
|
93
|
+
if (c.startsWith("<")) {
|
|
94
|
+
const t = parseSemverTuple(c.slice(1));
|
|
95
|
+
return t !== null && cmpSemver(ver, t) < 0;
|
|
96
|
+
}
|
|
97
|
+
if (c.startsWith("=")) {
|
|
98
|
+
const t = parseSemverTuple(c.slice(1));
|
|
99
|
+
return t !== null && cmpSemver(ver, t) === 0;
|
|
100
|
+
}
|
|
101
|
+
// Bare version: exact match
|
|
102
|
+
const t = parseSemverTuple(c);
|
|
103
|
+
return t !== null && cmpSemver(ver, t) === 0;
|
|
104
|
+
}
|
|
16
105
|
const _packs = new Map();
|
|
17
106
|
// ── Validation ────────────────────────────────────────────────────────────────
|
|
18
107
|
/**
|
|
@@ -39,6 +128,75 @@ export function validatePack(manifest) {
|
|
|
39
128
|
!/^\d+\.\d+(\.\d+)?$/.test(m["version"])) {
|
|
40
129
|
errors.push({ path: "$.version", message: 'must be a semver string like "1.0.0" or "1.0"' });
|
|
41
130
|
}
|
|
131
|
+
// Optional: registry block
|
|
132
|
+
if (m["registry"] !== undefined) {
|
|
133
|
+
if (typeof m["registry"] !== "object" || m["registry"] === null || Array.isArray(m["registry"])) {
|
|
134
|
+
errors.push({ path: "$.registry", message: "must be a plain object if present" });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const reg = m["registry"];
|
|
138
|
+
// compatRange — semver range string; must include the running engine version
|
|
139
|
+
if (reg["compatRange"] !== undefined) {
|
|
140
|
+
if (typeof reg["compatRange"] !== "string") {
|
|
141
|
+
errors.push({ path: "$.registry.compatRange", message: "must be a string" });
|
|
142
|
+
}
|
|
143
|
+
else if (!semverSatisfies(ANANKE_ENGINE_VERSION, reg["compatRange"])) {
|
|
144
|
+
errors.push({
|
|
145
|
+
path: "$.registry.compatRange",
|
|
146
|
+
message: `engine version ${ANANKE_ENGINE_VERSION} does not satisfy range "${reg["compatRange"]}"`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// stabilityTier — must be one of the known tiers
|
|
151
|
+
const TIERS = ["stable", "experimental", "internal"];
|
|
152
|
+
if (reg["stabilityTier"] !== undefined && !TIERS.includes(reg["stabilityTier"])) {
|
|
153
|
+
errors.push({
|
|
154
|
+
path: "$.registry.stabilityTier",
|
|
155
|
+
message: `must be one of: ${TIERS.join(", ")}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// requiredExports — must be array of strings
|
|
159
|
+
if (reg["requiredExports"] !== undefined) {
|
|
160
|
+
if (!Array.isArray(reg["requiredExports"])) {
|
|
161
|
+
errors.push({ path: "$.registry.requiredExports", message: "must be an array" });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
for (let i = 0; i < reg["requiredExports"].length; i++) {
|
|
165
|
+
if (typeof reg["requiredExports"][i] !== "string") {
|
|
166
|
+
errors.push({ path: `$.registry.requiredExports[${i}]`, message: "must be a string" });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// checksum — must be a 64-char hex string (SHA-256) if present
|
|
172
|
+
if (reg["checksum"] !== undefined) {
|
|
173
|
+
if (typeof reg["checksum"] !== "string" || !/^[0-9a-f]{64}$/.test(reg["checksum"])) {
|
|
174
|
+
errors.push({ path: "$.registry.checksum", message: "must be a 64-character lowercase hex string (SHA-256)" });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// license — must be a non-empty string
|
|
178
|
+
if (reg["license"] !== undefined && (typeof reg["license"] !== "string" || reg["license"].trim() === "")) {
|
|
179
|
+
errors.push({ path: "$.registry.license", message: "must be a non-empty SPDX identifier string" });
|
|
180
|
+
}
|
|
181
|
+
// provenance — must be array of objects with at least a title
|
|
182
|
+
if (reg["provenance"] !== undefined) {
|
|
183
|
+
if (!Array.isArray(reg["provenance"])) {
|
|
184
|
+
errors.push({ path: "$.registry.provenance", message: "must be an array" });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
for (let i = 0; i < reg["provenance"].length; i++) {
|
|
188
|
+
const ref = reg["provenance"][i];
|
|
189
|
+
if (typeof ref !== "object" || ref === null) {
|
|
190
|
+
errors.push({ path: `$.registry.provenance[${i}]`, message: "must be an object" });
|
|
191
|
+
}
|
|
192
|
+
else if (typeof ref["title"] !== "string") {
|
|
193
|
+
errors.push({ path: `$.registry.provenance[${i}].title`, message: "must be a string" });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
42
200
|
// Optional arrays — must be arrays if present
|
|
43
201
|
for (const key of ["weapons", "armour", "archetypes", "scenarios"]) {
|
|
44
202
|
if (m[key] !== undefined && !Array.isArray(m[key])) {
|
package/dist/tools/pack-cli.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// npx ananke replay diff <a.json> <b.json>
|
|
13
13
|
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join, resolve, extname, basename } from "node:path";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
15
16
|
import { validatePack, loadPack } from "../src/content-pack.js";
|
|
16
17
|
import { diffReplayJson } from "../src/netcode.js";
|
|
17
18
|
import { q } from "../src/units.js";
|
|
@@ -97,6 +98,13 @@ function cmdBundle(args) {
|
|
|
97
98
|
if (!bundle.name && typeof partial.name === "string")
|
|
98
99
|
bundle.name = partial.name;
|
|
99
100
|
}
|
|
101
|
+
// Compute SHA-256 checksum: serialise with checksum="" (placeholder), then hash.
|
|
102
|
+
// Store in registry block so consumers can verify integrity.
|
|
103
|
+
if (!bundle.registry)
|
|
104
|
+
bundle.registry = {};
|
|
105
|
+
bundle.registry.checksum = ""; // placeholder — field present but blank for hashing
|
|
106
|
+
const checksumInput = JSON.stringify(bundle, null, 2);
|
|
107
|
+
bundle.registry.checksum = createHash("sha256").update(checksumInput).digest("hex");
|
|
100
108
|
// Pre-validate before writing
|
|
101
109
|
const errors = validatePack(bundle);
|
|
102
110
|
if (errors.length > 0) {
|
|
@@ -107,6 +115,7 @@ function cmdBundle(args) {
|
|
|
107
115
|
writeFileSync(outFile, json, "utf8");
|
|
108
116
|
console.log(`✓ Bundle written to ${outFile}`);
|
|
109
117
|
console.log(` weapons: ${bundle.weapons.length}, armour: ${bundle.armour.length}, archetypes: ${bundle.archetypes.length}, scenarios: ${bundle.scenarios.length}`);
|
|
118
|
+
console.log(` checksum: ${bundle.registry.checksum}`);
|
|
110
119
|
}
|
|
111
120
|
function cmdLoad(args) {
|
|
112
121
|
const filePath = args[0];
|
package/package.json
CHANGED
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",
|