@alpaca-software/40kdc-data 0.2.0 → 0.3.0

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 (41) hide show
  1. package/dist/author-input.d.ts +20 -1
  2. package/dist/author-input.d.ts.map +1 -1
  3. package/dist/author-input.js +64 -8
  4. package/dist/author-input.js.map +1 -1
  5. package/dist/author-seed.d.ts +62 -0
  6. package/dist/author-seed.d.ts.map +1 -0
  7. package/dist/author-seed.js +194 -0
  8. package/dist/author-seed.js.map +1 -0
  9. package/dist/commands/translate.d.ts.map +1 -1
  10. package/dist/commands/translate.js +6 -68
  11. package/dist/commands/translate.js.map +1 -1
  12. package/dist/data/bundle.generated.js +1 -1
  13. package/dist/data/bundle.generated.js.map +1 -1
  14. package/dist/gen-conformance.js +22 -1
  15. package/dist/gen-conformance.js.map +1 -1
  16. package/dist/generated.d.ts +181 -146
  17. package/dist/generated.d.ts.map +1 -1
  18. package/dist/generated.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/runner.d.ts.map +1 -1
  24. package/dist/runner.js +16 -0
  25. package/dist/runner.js.map +1 -1
  26. package/dist/translate/condition.d.ts +26 -0
  27. package/dist/translate/condition.d.ts.map +1 -0
  28. package/dist/translate/condition.js +171 -0
  29. package/dist/translate/condition.js.map +1 -0
  30. package/dist/translate/index.d.ts +9 -0
  31. package/dist/translate/index.d.ts.map +1 -0
  32. package/dist/translate/index.js +9 -0
  33. package/dist/translate/index.js.map +1 -0
  34. package/dist/translate/scoring.d.ts +38 -0
  35. package/dist/translate/scoring.d.ts.map +1 -0
  36. package/dist/translate/scoring.js +80 -0
  37. package/dist/translate/scoring.js.map +1 -0
  38. package/package.json +2 -1
  39. package/schemas/core/secondary-card.schema.json +50 -28
  40. package/schemas/enrichment/ability-dsl/condition.schema.json +5 -2
  41. package/schemas/enrichment/ability-dsl/effect.schema.json +2 -1
@@ -19,11 +19,12 @@
19
19
  * `input.json` (ListForge) or `input.newrecruit-json.json` (NewRecruit). Other
20
20
  * inputs are derived.
21
21
  */
22
- import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
22
+ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
23
23
  import { dirname, join } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
25
  import { Dataset } from "./data/dataset.js";
26
26
  import { normalizeName } from "./data/normalize.js";
27
+ import { describeScoringCard } from "./translate/index.js";
27
28
  import { exportRoster } from "./export/index.js";
28
29
  import { importRoster, REGISTERED_ADAPTERS } from "./import/import-roster.js";
29
30
  import { selectAdapter } from "./import/adapter.js";
@@ -325,8 +326,28 @@ function genAttribution() {
325
326
  writeJson(join(CONFORMANCE, "attribution", "cases.json"), serialised);
326
327
  console.log(`attribution/cases.json: ${cases.length} cases`);
327
328
  }
329
+ /**
330
+ * Scoring-card translation corpus: humanize each primary mission card's
331
+ * `awards` into plain English. The TS translator is the oracle; the Rust port
332
+ * must reproduce every string byte-for-byte (the differ compares structurally,
333
+ * no tolerance). Only `card_type: "primary"` cards are pinned — the 14-card
334
+ * secondary deck isn't revealed yet. Cases are sorted by id for stability, and
335
+ * the `awards` array order within each card is load-bearing.
336
+ */
337
+ function genScoringTranslation() {
338
+ const ds = Dataset.embedded();
339
+ mkdirSync(join(CONFORMANCE, "scoring-translation"), { recursive: true });
340
+ const cases = ds.secondaryCards.all
341
+ .filter((c) => c.card_type === "primary")
342
+ .slice()
343
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
344
+ .map((card) => ({ cardId: card.id, expected: { awards: describeScoringCard(card) } }));
345
+ writeJson(join(CONFORMANCE, "scoring-translation", "cases.json"), cases);
346
+ console.log(`scoring-translation/cases.json: ${cases.length} cases`);
347
+ }
328
348
  genNormalize();
329
349
  genRosters();
330
350
  genLinkedApi();
331
351
  genAttribution();
352
+ genScoringTranslation();
332
353
  //# sourceMappingURL=gen-conformance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gen-conformance.js","sourceRoot":"","sources":["../src/gen-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate the cross-implementation conformance corpus under repo-root\n * `conformance/`. The TypeScript package is the reference implementation, so\n * the goldens it emits are what the Rust crate must reproduce byte-for-byte\n * (structurally). Run via `npm run gen:conformance`; CI regenerates and asserts\n * `git diff --exit-code conformance/` is clean.\n *\n * Outputs:\n * - `conformance/normalize.json` — `[{ input, expected }]` for normalizeName.\n * - `conformance/roster/<case>/expected.roster.json` — the resolved Roster.\n * - `conformance/roster/<case>/expected.<fmt>.{txt,json}` — every export\n * target's golden output. The TS exporter is the oracle; the Rust mirror\n * asserts byte-equal output for the same Roster.\n * - `conformance/roster/<case>/input.newrecruit-{wtc-compact,wtc-full,simple}.txt`\n * — text inputs derived from the seed by the exporter, so a re-import\n * regression in either implementation surfaces immediately.\n *\n * Seeding: each `<case>/` carries one canonical input — either the legacy\n * `input.json` (ListForge) or `input.newrecruit-json.json` (NewRecruit). Other\n * inputs are derived.\n */\nimport { readdirSync, readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Dataset } from \"./data/dataset.js\";\nimport { normalizeName } from \"./data/normalize.js\";\nimport { exportRoster, type ExportFormat } from \"./export/index.js\";\nimport { importRoster, REGISTERED_ADAPTERS } from \"./import/import-roster.js\";\nimport { selectAdapter } from \"./import/adapter.js\";\nimport type { ParsedRoster, Roster } from \"./import/types.js\";\nimport { attributeStages } from \"./cruncher/attribution.js\";\nimport type { EngineInput } from \"./cruncher/index.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REPO_ROOT = join(__dirname, \"../..\");\nconst CONFORMANCE = join(REPO_ROOT, \"conformance\");\n\nconst NORMALIZE_INPUTS = [\n // NFD diacritic strip\n \"Khârn the Betrayer\",\n \"Brôkhyr\",\n \"Ûthar\",\n \"Magnús\",\n // apostrophe / quote variants\n \"T'au\",\n \"Be’lakor\",\n \"Kor’sarro Khan\",\n \"Aetaos'rau'keres\",\n \"‘quoted’\",\n // whitespace / hyphen collapse + trim\n \"Brôkhyr Iron-master\",\n \" the betrayer \",\n \"space--marines\",\n // casefold\n \"KHÂRN THE BETRAYER\",\n // already-normalized (idempotence)\n \"kharn the betrayer\",\n // distinctness anchors (must NOT collapse together)\n \"Khorne\",\n \"Khârn\",\n // Unicode whitespace beyond ASCII — every Unicode whitespace must collapse\n // identically across implementations or `find(\"Khorne Lord\")` and\n // `find(\"Khorne Lord\")` will silently disagree across ports.\n \"Khorne Lord\",\n \"Khorne Lord\",\n // Turkish dotted-I: NFD decomposes to `I` + combining dot above; the dot is\n // stripped, then locale-independent lowercase yields `i`. The case pins that\n // no implementation introduces locale-aware casefolding (which would map\n // `I` → `ı` under Turkish locale and break ASCII-text search).\n \"İmperial Fists\",\n // Zero-width joiner: passes through every step today. Pinned so behavior\n // does not silently change — if a future commit strips Cf-category chars,\n // this golden updates in the same PR.\n \"Khorne‍Lord\",\n];\n\nfunction writeJson(path: string, value: unknown): void {\n writeFileSync(path, `${JSON.stringify(value, null, 2)}\\n`);\n}\n\nfunction writeText(path: string, value: string): void {\n writeFileSync(path, value);\n}\n\nfunction genNormalize(): void {\n const table = NORMALIZE_INPUTS.map((input) => ({ input, expected: normalizeName(input) }));\n writeJson(join(CONFORMANCE, \"normalize.json\"), table);\n console.log(`normalize.json: ${table.length} cases`);\n}\n\n/** Locate the canonical input for a fixture dir: prefer `input.json` (legacy\n * ListForge), then `input.newrecruit-json.json` (NewRecruit), then the\n * text-only `input.gw.txt` (GW app export — import-only, like ListForge). */\nfunction seedRoster(caseDir: string, ds: Dataset): Roster {\n const decoded = decodeCanonicalSeed(caseDir);\n return importRoster(decoded, { dataset: ds });\n}\n\n/** Return the decoded payload for the canonical seed — the same value the\n * import pipeline would dispatch on. JSON seeds come back parsed; text seeds\n * come back as the raw string. */\nfunction decodeCanonicalSeed(caseDir: string): unknown {\n const jsonSeed = join(caseDir, \"input.json\");\n if (existsSync(jsonSeed)) {\n return JSON.parse(readFileSync(jsonSeed, \"utf8\"));\n }\n const nrSeed = join(caseDir, \"input.newrecruit-json.json\");\n if (existsSync(nrSeed)) {\n return JSON.parse(readFileSync(nrSeed, \"utf8\"));\n }\n const gwSeed = join(caseDir, \"input.gw.txt\");\n if (existsSync(gwSeed)) {\n return readFileSync(gwSeed, \"utf8\");\n }\n throw new Error(`no canonical input found in ${caseDir}`);\n}\n\n/** Run a decoded payload through the adapter pipeline up to (but not past)\n * resolution. The result is the format-agnostic ParsedRoster — the same\n * intermediate the resolver consumes. Pinning this layer surfaces parser\n * regressions even when resolution masks them. */\nfunction parsedFromCanonicalSeed(caseDir: string): ParsedRoster {\n const decoded = decodeCanonicalSeed(caseDir);\n const adapter = selectAdapter(decoded, [...REGISTERED_ADAPTERS]);\n return adapter.parse(decoded);\n}\n\nconst TEXT_FORMATS: { format: ExportFormat; inputName: string; goldenName: string }[] = [\n {\n format: \"newrecruit-wtc-compact\",\n inputName: \"input.newrecruit-wtc-compact.txt\",\n goldenName: \"expected.newrecruit-wtc-compact.txt\",\n },\n {\n format: \"newrecruit-wtc-full\",\n inputName: \"input.newrecruit-wtc-full.txt\",\n goldenName: \"expected.newrecruit-wtc-full.txt\",\n },\n {\n format: \"newrecruit-simple\",\n inputName: \"input.newrecruit-simple.txt\",\n goldenName: \"expected.newrecruit-simple.txt\",\n },\n];\n\nfunction genRosters(): void {\n const ds = Dataset.embedded();\n const rosterDir = join(CONFORMANCE, \"roster\");\n for (const entry of readdirSync(rosterDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const caseDir = join(rosterDir, entry.name);\n\n const seed = seedRoster(caseDir, ds);\n writeJson(join(caseDir, \"expected.roster.json\"), seed);\n\n // Parsed-stage golden — the intermediate ParsedRoster produced by the\n // adapter for the canonical seed, before resolution. Catches parser bugs\n // that resolution would otherwise mask (e.g. wrong unit count from a\n // duplicate cost line that resolves to the same unit twice).\n writeJson(join(caseDir, \"expected.parsed.json\"), parsedFromCanonicalSeed(caseDir));\n\n // JSON export golden — NewRecruit-shaped skeleton.\n const jsonOut = exportRoster(seed, \"newrecruit-json\");\n writeJson(join(caseDir, \"expected.newrecruit-json.json\"), JSON.parse(jsonOut));\n\n // Canonical Roster JSON export — should equal the resolved roster.\n writeJson(join(caseDir, \"expected.roster-json.json\"), JSON.parse(exportRoster(seed, \"roster-json\")));\n\n // Text exports: always write the export golden so every fixture exercises\n // the cross-implementation byte-equality check. Only write the\n // `input.*.txt` round-trip seed when the fixture was authored for the\n // NewRecruit pipeline — legacy ListForge fixtures carry decoration\n // (multi-force warnings, leader-attachment inference) that the simple/wtc\n // exporters can't fully preserve, so the round-trip would fail\n // structurally rather than uncover a parser bug.\n const isNewRecruitSeed = existsSync(join(caseDir, \"input.newrecruit-json.json\"));\n for (const { format, inputName, goldenName } of TEXT_FORMATS) {\n const out = exportRoster(seed, format);\n writeText(join(caseDir, goldenName), out);\n if (isNewRecruitSeed) {\n writeText(join(caseDir, inputName), out);\n }\n }\n\n // Rosterizer JSON export + a derived round-trip input. The exporter is\n // deterministic and round-trips through the adapter, so emitting it as\n // both `expected.rosterizer.json` and `input.rosterizer.json` pins the\n // cross-implementation goldens and the importer regression at the same\n // time. Same NewRecruit-seed gate as the text formats — multi-force\n // ListForge fixtures lose their provisional leader-attachment under\n // round-trip, so they only get the export golden, not the derived input.\n const rosterizerOut = exportRoster(seed, \"rosterizer\");\n writeJson(join(caseDir, \"expected.rosterizer.json\"), JSON.parse(rosterizerOut));\n if (isNewRecruitSeed) {\n writeJson(join(caseDir, \"input.rosterizer.json\"), JSON.parse(rosterizerOut));\n }\n\n console.log(\n `roster/${entry.name}: ${seed.units.length} units, ${seed.diagnostics.warnings.length} warnings`,\n );\n }\n}\n\n/**\n * Linked-API query cases. Each descriptor names a query method on Dataset, the\n * args to call it with, and how the result should be compared.\n *\n * `comparison: \"ordered\"` pins the result order — used for queries that iterate\n * a data-driven array (`unit.ability_ids`, `unit.weapon_ids`) where order is\n * encoded in the data and both implementations iterate it the same way.\n *\n * `comparison: \"set\"` pins only the set of ids — used for queries that walk an\n * index (faction → abilities, ability → phases) where iteration order depends\n * on dataset bundler internals and is incidental. Ids are sorted before\n * comparison.\n *\n * `comparison: \"scalar\"` pins a single id-or-null result (find_* and\n * faction_of(unit)).\n */\ntype LinkedApiQuery =\n | { name: string; query: \"find_unit\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"find_weapon\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"find_faction\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"find_ability\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"abilities_of\"; args: { unitId: string }; comparison: \"ordered\" }\n | { name: string; query: \"weapons_of\"; args: { unitId: string }; comparison: \"ordered\" }\n | { name: string; query: \"phases_of\"; args: { abilityId: string }; comparison: \"set\" }\n | { name: string; query: \"faction_of\"; args: { unitId: string }; comparison: \"scalar\" }\n | { name: string; query: \"abilities_of_faction\"; args: { factionId: string }; comparison: \"set\" }\n | { name: string; query: \"weapons_of_faction\"; args: { factionId: string }; comparison: \"set\" };\n\nconst LINKED_API_QUERIES: LinkedApiQuery[] = [\n // find_unit: diacritic-insensitive lookup, miss returns null.\n { name: \"find_unit by diacritic name\", query: \"find_unit\", args: { query: \"Kharn\" }, comparison: \"scalar\" },\n { name: \"find_unit miss returns null\", query: \"find_unit\", args: { query: \"not-a-real-unit-xyz\" }, comparison: \"scalar\" },\n // find_weapon: hyphen + space tolerance.\n { name: \"find_weapon by name\", query: \"find_weapon\", args: { query: \"bolt rifle\" }, comparison: \"scalar\" },\n // find_faction: punctuation/diacritic tolerance.\n { name: \"find_faction by display name\", query: \"find_faction\", args: { query: \"World Eaters\" }, comparison: \"scalar\" },\n // find_ability: ability name lookup.\n { name: \"find_ability by name\", query: \"find_ability\", args: { query: \"Berzerker Frenzy\" }, comparison: \"scalar\" },\n // abilities_of(unit): ordered, iterates unit.ability_ids array.\n { name: \"abilities_of intercessor-squad\", query: \"abilities_of\", args: { unitId: \"intercessor-squad\" }, comparison: \"ordered\" },\n { name: \"abilities_of kharn-the-betrayer\", query: \"abilities_of\", args: { unitId: \"kharn-the-betrayer\" }, comparison: \"ordered\" },\n // weapons_of(unit): ordered, iterates unit.weapon_ids array.\n { name: \"weapons_of intercessor-squad\", query: \"weapons_of\", args: { unitId: \"intercessor-squad\" }, comparison: \"ordered\" },\n { name: \"weapons_of kharn-the-betrayer\", query: \"weapons_of\", args: { unitId: \"kharn-the-betrayer\" }, comparison: \"ordered\" },\n // phases_of(ability): compared as set (phase index iteration order is incidental).\n { name: \"phases_of berzerker-frenzy\", query: \"phases_of\", args: { abilityId: \"berzerker-frenzy\" }, comparison: \"set\" },\n // faction_of(unit): scalar id or null.\n { name: \"faction_of intercessor-squad\", query: \"faction_of\", args: { unitId: \"intercessor-squad\" }, comparison: \"scalar\" },\n // abilities_of_faction: compared as set (collection-index order is incidental).\n { name: \"abilities_of_faction world-eaters\", query: \"abilities_of_faction\", args: { factionId: \"world-eaters\" }, comparison: \"set\" },\n // weapons_of_faction: compared as set.\n { name: \"weapons_of_faction world-eaters\", query: \"weapons_of_faction\", args: { factionId: \"world-eaters\" }, comparison: \"set\" },\n];\n\nfunction genLinkedApi(): void {\n const ds = Dataset.embedded();\n const cases = LINKED_API_QUERIES.map((q) => {\n const expected = runLinkedQuery(ds, q);\n return { ...q, expected };\n });\n writeJson(join(CONFORMANCE, \"linked-api\", \"cases.json\"), cases);\n console.log(`linked-api/cases.json: ${cases.length} cases`);\n}\n\nfunction runLinkedQuery(ds: Dataset, q: LinkedApiQuery): string | null | string[] {\n switch (q.query) {\n case \"find_unit\":\n return ds.units.find(q.args.query)?.id ?? null;\n case \"find_weapon\":\n return ds.weapons.find(q.args.query)?.id ?? null;\n case \"find_faction\":\n return ds.factions.find(q.args.query)?.id ?? null;\n case \"find_ability\":\n return ds.abilities.find(q.args.query)?.id ?? null;\n case \"abilities_of\": {\n const u = ds.units.get(q.args.unitId);\n if (!u) throw new Error(`abilities_of: unknown unit ${q.args.unitId}`);\n return u.abilities.map((a) => a.id);\n }\n case \"weapons_of\": {\n const u = ds.units.get(q.args.unitId);\n if (!u) throw new Error(`weapons_of: unknown unit ${q.args.unitId}`);\n return u.weapons.map((w) => w.id);\n }\n case \"phases_of\": {\n const a = ds.abilities.get(q.args.abilityId);\n if (!a) throw new Error(`phases_of: unknown ability ${q.args.abilityId}`);\n return [...a.phases].sort();\n }\n case \"faction_of\": {\n const u = ds.units.get(q.args.unitId);\n if (!u) throw new Error(`faction_of: unknown unit ${q.args.unitId}`);\n return u.faction?.id ?? null;\n }\n case \"abilities_of_faction\":\n return ds.abilities.byFaction(q.args.factionId).map((a) => a.id).sort();\n case \"weapons_of_faction\": {\n // Mirrors Rust `weapons_of_faction`: aggregate weapons across the\n // faction's units and dedupe by id. The collection-level\n // `weapons.byFaction()` is a different operation (it looks up weapons\n // whose own `faction_id` is set, which is empty for most factions).\n const f = ds.factions.get(q.args.factionId);\n if (!f) throw new Error(`weapons_of_faction: unknown faction ${q.args.factionId}`);\n return f.weapons.map((w) => w.id).sort();\n }\n }\n}\n\n/**\n * Attribution corpus: reuses the existing cruncher inputs from the cases that\n * carry at least one groupable buff (ability or manual). The expected shape\n * is the AttributedStage array produced by attributeStages; both\n * implementations of the leave-one-out decomposition must reproduce it\n * within the per-stage float tolerance.\n */\nconst ATTRIBUTION_CASE_FILES = [\n \"05-anti-infantry-vs-cultist.json\",\n \"07-twin-linked-heavy-stationary-vs-knight.json\",\n];\n\ninterface CruncherCaseInput {\n name: string;\n attacker: { weaponId: string; profileIndex: number };\n modelsFiring: number;\n target: { unitId: string; profileIndex: number; modelCount?: number };\n context: EngineInput[\"context\"];\n buffs: EngineInput[\"buffs\"];\n}\n\nfunction loadAttributionInput(ds: Dataset, filename: string): {\n name: string;\n input: EngineInput;\n} {\n const path = join(CONFORMANCE, \"cruncher\", filename);\n const c = JSON.parse(readFileSync(path, \"utf8\")) as CruncherCaseInput;\n const weapon = ds.weapons.get(c.attacker.weaponId);\n const unit = ds.units.get(c.target.unitId);\n if (!weapon) throw new Error(`attribution: unknown weapon ${c.attacker.weaponId}`);\n if (!unit) throw new Error(`attribution: unknown unit ${c.target.unitId}`);\n return {\n name: c.name,\n input: {\n attacker: { weapon: weapon.raw, profileIndex: c.attacker.profileIndex },\n target: {\n unit: unit.raw,\n profileIndex: c.target.profileIndex,\n ...(c.target.modelCount !== undefined ? { modelCount: c.target.modelCount } : {}),\n },\n modelsFiring: c.modelsFiring,\n buffs: c.buffs,\n context: c.context,\n },\n };\n}\n\nfunction genAttribution(): void {\n const ds = Dataset.embedded();\n const cases = ATTRIBUTION_CASE_FILES.map((filename, idx) => {\n const { name, input } = loadAttributionInput(ds, filename);\n const stages = attributeStages(input, ds);\n return {\n // Persist the input by file reference so the corpus stays a single\n // source of truth — the cruncher case file already pins the EngineInput.\n name,\n cruncher_case: filename,\n expected: stages.map((s) => ({\n name: s.name,\n expected: s.expected,\n baseline: s.baseline,\n lifts: s.lifts.map((l) => ({ source: l.source, delta: l.delta })),\n residual: s.residual,\n intrinsics: s.intrinsics,\n })),\n // Stable ordering of cases in the corpus file.\n _order: idx,\n };\n });\n // Sort by _order and strip the helper before writing.\n cases.sort((a, b) => a._order - b._order);\n const serialised = cases.map(({ _order: _o, ...rest }) => rest);\n writeJson(join(CONFORMANCE, \"attribution\", \"cases.json\"), serialised);\n console.log(`attribution/cases.json: ${cases.length} cases`);\n}\n\ngenNormalize();\ngenRosters();\ngenLinkedApi();\ngenAttribution();\n"]}
1
+ {"version":3,"file":"gen-conformance.js","sourceRoot":"","sources":["../src/gen-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate the cross-implementation conformance corpus under repo-root\n * `conformance/`. The TypeScript package is the reference implementation, so\n * the goldens it emits are what the Rust crate must reproduce byte-for-byte\n * (structurally). Run via `npm run gen:conformance`; CI regenerates and asserts\n * `git diff --exit-code conformance/` is clean.\n *\n * Outputs:\n * - `conformance/normalize.json` — `[{ input, expected }]` for normalizeName.\n * - `conformance/roster/<case>/expected.roster.json` — the resolved Roster.\n * - `conformance/roster/<case>/expected.<fmt>.{txt,json}` — every export\n * target's golden output. The TS exporter is the oracle; the Rust mirror\n * asserts byte-equal output for the same Roster.\n * - `conformance/roster/<case>/input.newrecruit-{wtc-compact,wtc-full,simple}.txt`\n * — text inputs derived from the seed by the exporter, so a re-import\n * regression in either implementation surfaces immediately.\n *\n * Seeding: each `<case>/` carries one canonical input — either the legacy\n * `input.json` (ListForge) or `input.newrecruit-json.json` (NewRecruit). Other\n * inputs are derived.\n */\nimport { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Dataset } from \"./data/dataset.js\";\nimport { normalizeName } from \"./data/normalize.js\";\nimport { describeScoringCard } from \"./translate/index.js\";\nimport { exportRoster, type ExportFormat } from \"./export/index.js\";\nimport { importRoster, REGISTERED_ADAPTERS } from \"./import/import-roster.js\";\nimport { selectAdapter } from \"./import/adapter.js\";\nimport type { ParsedRoster, Roster } from \"./import/types.js\";\nimport { attributeStages } from \"./cruncher/attribution.js\";\nimport type { EngineInput } from \"./cruncher/index.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REPO_ROOT = join(__dirname, \"../..\");\nconst CONFORMANCE = join(REPO_ROOT, \"conformance\");\n\nconst NORMALIZE_INPUTS = [\n // NFD diacritic strip\n \"Khârn the Betrayer\",\n \"Brôkhyr\",\n \"Ûthar\",\n \"Magnús\",\n // apostrophe / quote variants\n \"T'au\",\n \"Be’lakor\",\n \"Kor’sarro Khan\",\n \"Aetaos'rau'keres\",\n \"‘quoted’\",\n // whitespace / hyphen collapse + trim\n \"Brôkhyr Iron-master\",\n \" the betrayer \",\n \"space--marines\",\n // casefold\n \"KHÂRN THE BETRAYER\",\n // already-normalized (idempotence)\n \"kharn the betrayer\",\n // distinctness anchors (must NOT collapse together)\n \"Khorne\",\n \"Khârn\",\n // Unicode whitespace beyond ASCII — every Unicode whitespace must collapse\n // identically across implementations or `find(\"Khorne Lord\")` and\n // `find(\"Khorne Lord\")` will silently disagree across ports.\n \"Khorne Lord\",\n \"Khorne Lord\",\n // Turkish dotted-I: NFD decomposes to `I` + combining dot above; the dot is\n // stripped, then locale-independent lowercase yields `i`. The case pins that\n // no implementation introduces locale-aware casefolding (which would map\n // `I` → `ı` under Turkish locale and break ASCII-text search).\n \"İmperial Fists\",\n // Zero-width joiner: passes through every step today. Pinned so behavior\n // does not silently change — if a future commit strips Cf-category chars,\n // this golden updates in the same PR.\n \"Khorne‍Lord\",\n];\n\nfunction writeJson(path: string, value: unknown): void {\n writeFileSync(path, `${JSON.stringify(value, null, 2)}\\n`);\n}\n\nfunction writeText(path: string, value: string): void {\n writeFileSync(path, value);\n}\n\nfunction genNormalize(): void {\n const table = NORMALIZE_INPUTS.map((input) => ({ input, expected: normalizeName(input) }));\n writeJson(join(CONFORMANCE, \"normalize.json\"), table);\n console.log(`normalize.json: ${table.length} cases`);\n}\n\n/** Locate the canonical input for a fixture dir: prefer `input.json` (legacy\n * ListForge), then `input.newrecruit-json.json` (NewRecruit), then the\n * text-only `input.gw.txt` (GW app export — import-only, like ListForge). */\nfunction seedRoster(caseDir: string, ds: Dataset): Roster {\n const decoded = decodeCanonicalSeed(caseDir);\n return importRoster(decoded, { dataset: ds });\n}\n\n/** Return the decoded payload for the canonical seed — the same value the\n * import pipeline would dispatch on. JSON seeds come back parsed; text seeds\n * come back as the raw string. */\nfunction decodeCanonicalSeed(caseDir: string): unknown {\n const jsonSeed = join(caseDir, \"input.json\");\n if (existsSync(jsonSeed)) {\n return JSON.parse(readFileSync(jsonSeed, \"utf8\"));\n }\n const nrSeed = join(caseDir, \"input.newrecruit-json.json\");\n if (existsSync(nrSeed)) {\n return JSON.parse(readFileSync(nrSeed, \"utf8\"));\n }\n const gwSeed = join(caseDir, \"input.gw.txt\");\n if (existsSync(gwSeed)) {\n return readFileSync(gwSeed, \"utf8\");\n }\n throw new Error(`no canonical input found in ${caseDir}`);\n}\n\n/** Run a decoded payload through the adapter pipeline up to (but not past)\n * resolution. The result is the format-agnostic ParsedRoster — the same\n * intermediate the resolver consumes. Pinning this layer surfaces parser\n * regressions even when resolution masks them. */\nfunction parsedFromCanonicalSeed(caseDir: string): ParsedRoster {\n const decoded = decodeCanonicalSeed(caseDir);\n const adapter = selectAdapter(decoded, [...REGISTERED_ADAPTERS]);\n return adapter.parse(decoded);\n}\n\nconst TEXT_FORMATS: { format: ExportFormat; inputName: string; goldenName: string }[] = [\n {\n format: \"newrecruit-wtc-compact\",\n inputName: \"input.newrecruit-wtc-compact.txt\",\n goldenName: \"expected.newrecruit-wtc-compact.txt\",\n },\n {\n format: \"newrecruit-wtc-full\",\n inputName: \"input.newrecruit-wtc-full.txt\",\n goldenName: \"expected.newrecruit-wtc-full.txt\",\n },\n {\n format: \"newrecruit-simple\",\n inputName: \"input.newrecruit-simple.txt\",\n goldenName: \"expected.newrecruit-simple.txt\",\n },\n];\n\nfunction genRosters(): void {\n const ds = Dataset.embedded();\n const rosterDir = join(CONFORMANCE, \"roster\");\n for (const entry of readdirSync(rosterDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const caseDir = join(rosterDir, entry.name);\n\n const seed = seedRoster(caseDir, ds);\n writeJson(join(caseDir, \"expected.roster.json\"), seed);\n\n // Parsed-stage golden — the intermediate ParsedRoster produced by the\n // adapter for the canonical seed, before resolution. Catches parser bugs\n // that resolution would otherwise mask (e.g. wrong unit count from a\n // duplicate cost line that resolves to the same unit twice).\n writeJson(join(caseDir, \"expected.parsed.json\"), parsedFromCanonicalSeed(caseDir));\n\n // JSON export golden — NewRecruit-shaped skeleton.\n const jsonOut = exportRoster(seed, \"newrecruit-json\");\n writeJson(join(caseDir, \"expected.newrecruit-json.json\"), JSON.parse(jsonOut));\n\n // Canonical Roster JSON export — should equal the resolved roster.\n writeJson(join(caseDir, \"expected.roster-json.json\"), JSON.parse(exportRoster(seed, \"roster-json\")));\n\n // Text exports: always write the export golden so every fixture exercises\n // the cross-implementation byte-equality check. Only write the\n // `input.*.txt` round-trip seed when the fixture was authored for the\n // NewRecruit pipeline — legacy ListForge fixtures carry decoration\n // (multi-force warnings, leader-attachment inference) that the simple/wtc\n // exporters can't fully preserve, so the round-trip would fail\n // structurally rather than uncover a parser bug.\n const isNewRecruitSeed = existsSync(join(caseDir, \"input.newrecruit-json.json\"));\n for (const { format, inputName, goldenName } of TEXT_FORMATS) {\n const out = exportRoster(seed, format);\n writeText(join(caseDir, goldenName), out);\n if (isNewRecruitSeed) {\n writeText(join(caseDir, inputName), out);\n }\n }\n\n // Rosterizer JSON export + a derived round-trip input. The exporter is\n // deterministic and round-trips through the adapter, so emitting it as\n // both `expected.rosterizer.json` and `input.rosterizer.json` pins the\n // cross-implementation goldens and the importer regression at the same\n // time. Same NewRecruit-seed gate as the text formats — multi-force\n // ListForge fixtures lose their provisional leader-attachment under\n // round-trip, so they only get the export golden, not the derived input.\n const rosterizerOut = exportRoster(seed, \"rosterizer\");\n writeJson(join(caseDir, \"expected.rosterizer.json\"), JSON.parse(rosterizerOut));\n if (isNewRecruitSeed) {\n writeJson(join(caseDir, \"input.rosterizer.json\"), JSON.parse(rosterizerOut));\n }\n\n console.log(\n `roster/${entry.name}: ${seed.units.length} units, ${seed.diagnostics.warnings.length} warnings`,\n );\n }\n}\n\n/**\n * Linked-API query cases. Each descriptor names a query method on Dataset, the\n * args to call it with, and how the result should be compared.\n *\n * `comparison: \"ordered\"` pins the result order — used for queries that iterate\n * a data-driven array (`unit.ability_ids`, `unit.weapon_ids`) where order is\n * encoded in the data and both implementations iterate it the same way.\n *\n * `comparison: \"set\"` pins only the set of ids — used for queries that walk an\n * index (faction → abilities, ability → phases) where iteration order depends\n * on dataset bundler internals and is incidental. Ids are sorted before\n * comparison.\n *\n * `comparison: \"scalar\"` pins a single id-or-null result (find_* and\n * faction_of(unit)).\n */\ntype LinkedApiQuery =\n | { name: string; query: \"find_unit\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"find_weapon\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"find_faction\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"find_ability\"; args: { query: string }; comparison: \"scalar\" }\n | { name: string; query: \"abilities_of\"; args: { unitId: string }; comparison: \"ordered\" }\n | { name: string; query: \"weapons_of\"; args: { unitId: string }; comparison: \"ordered\" }\n | { name: string; query: \"phases_of\"; args: { abilityId: string }; comparison: \"set\" }\n | { name: string; query: \"faction_of\"; args: { unitId: string }; comparison: \"scalar\" }\n | { name: string; query: \"abilities_of_faction\"; args: { factionId: string }; comparison: \"set\" }\n | { name: string; query: \"weapons_of_faction\"; args: { factionId: string }; comparison: \"set\" };\n\nconst LINKED_API_QUERIES: LinkedApiQuery[] = [\n // find_unit: diacritic-insensitive lookup, miss returns null.\n { name: \"find_unit by diacritic name\", query: \"find_unit\", args: { query: \"Kharn\" }, comparison: \"scalar\" },\n { name: \"find_unit miss returns null\", query: \"find_unit\", args: { query: \"not-a-real-unit-xyz\" }, comparison: \"scalar\" },\n // find_weapon: hyphen + space tolerance.\n { name: \"find_weapon by name\", query: \"find_weapon\", args: { query: \"bolt rifle\" }, comparison: \"scalar\" },\n // find_faction: punctuation/diacritic tolerance.\n { name: \"find_faction by display name\", query: \"find_faction\", args: { query: \"World Eaters\" }, comparison: \"scalar\" },\n // find_ability: ability name lookup.\n { name: \"find_ability by name\", query: \"find_ability\", args: { query: \"Berzerker Frenzy\" }, comparison: \"scalar\" },\n // abilities_of(unit): ordered, iterates unit.ability_ids array.\n { name: \"abilities_of intercessor-squad\", query: \"abilities_of\", args: { unitId: \"intercessor-squad\" }, comparison: \"ordered\" },\n { name: \"abilities_of kharn-the-betrayer\", query: \"abilities_of\", args: { unitId: \"kharn-the-betrayer\" }, comparison: \"ordered\" },\n // weapons_of(unit): ordered, iterates unit.weapon_ids array.\n { name: \"weapons_of intercessor-squad\", query: \"weapons_of\", args: { unitId: \"intercessor-squad\" }, comparison: \"ordered\" },\n { name: \"weapons_of kharn-the-betrayer\", query: \"weapons_of\", args: { unitId: \"kharn-the-betrayer\" }, comparison: \"ordered\" },\n // phases_of(ability): compared as set (phase index iteration order is incidental).\n { name: \"phases_of berzerker-frenzy\", query: \"phases_of\", args: { abilityId: \"berzerker-frenzy\" }, comparison: \"set\" },\n // faction_of(unit): scalar id or null.\n { name: \"faction_of intercessor-squad\", query: \"faction_of\", args: { unitId: \"intercessor-squad\" }, comparison: \"scalar\" },\n // abilities_of_faction: compared as set (collection-index order is incidental).\n { name: \"abilities_of_faction world-eaters\", query: \"abilities_of_faction\", args: { factionId: \"world-eaters\" }, comparison: \"set\" },\n // weapons_of_faction: compared as set.\n { name: \"weapons_of_faction world-eaters\", query: \"weapons_of_faction\", args: { factionId: \"world-eaters\" }, comparison: \"set\" },\n];\n\nfunction genLinkedApi(): void {\n const ds = Dataset.embedded();\n const cases = LINKED_API_QUERIES.map((q) => {\n const expected = runLinkedQuery(ds, q);\n return { ...q, expected };\n });\n writeJson(join(CONFORMANCE, \"linked-api\", \"cases.json\"), cases);\n console.log(`linked-api/cases.json: ${cases.length} cases`);\n}\n\nfunction runLinkedQuery(ds: Dataset, q: LinkedApiQuery): string | null | string[] {\n switch (q.query) {\n case \"find_unit\":\n return ds.units.find(q.args.query)?.id ?? null;\n case \"find_weapon\":\n return ds.weapons.find(q.args.query)?.id ?? null;\n case \"find_faction\":\n return ds.factions.find(q.args.query)?.id ?? null;\n case \"find_ability\":\n return ds.abilities.find(q.args.query)?.id ?? null;\n case \"abilities_of\": {\n const u = ds.units.get(q.args.unitId);\n if (!u) throw new Error(`abilities_of: unknown unit ${q.args.unitId}`);\n return u.abilities.map((a) => a.id);\n }\n case \"weapons_of\": {\n const u = ds.units.get(q.args.unitId);\n if (!u) throw new Error(`weapons_of: unknown unit ${q.args.unitId}`);\n return u.weapons.map((w) => w.id);\n }\n case \"phases_of\": {\n const a = ds.abilities.get(q.args.abilityId);\n if (!a) throw new Error(`phases_of: unknown ability ${q.args.abilityId}`);\n return [...a.phases].sort();\n }\n case \"faction_of\": {\n const u = ds.units.get(q.args.unitId);\n if (!u) throw new Error(`faction_of: unknown unit ${q.args.unitId}`);\n return u.faction?.id ?? null;\n }\n case \"abilities_of_faction\":\n return ds.abilities.byFaction(q.args.factionId).map((a) => a.id).sort();\n case \"weapons_of_faction\": {\n // Mirrors Rust `weapons_of_faction`: aggregate weapons across the\n // faction's units and dedupe by id. The collection-level\n // `weapons.byFaction()` is a different operation (it looks up weapons\n // whose own `faction_id` is set, which is empty for most factions).\n const f = ds.factions.get(q.args.factionId);\n if (!f) throw new Error(`weapons_of_faction: unknown faction ${q.args.factionId}`);\n return f.weapons.map((w) => w.id).sort();\n }\n }\n}\n\n/**\n * Attribution corpus: reuses the existing cruncher inputs from the cases that\n * carry at least one groupable buff (ability or manual). The expected shape\n * is the AttributedStage array produced by attributeStages; both\n * implementations of the leave-one-out decomposition must reproduce it\n * within the per-stage float tolerance.\n */\nconst ATTRIBUTION_CASE_FILES = [\n \"05-anti-infantry-vs-cultist.json\",\n \"07-twin-linked-heavy-stationary-vs-knight.json\",\n];\n\ninterface CruncherCaseInput {\n name: string;\n attacker: { weaponId: string; profileIndex: number };\n modelsFiring: number;\n target: { unitId: string; profileIndex: number; modelCount?: number };\n context: EngineInput[\"context\"];\n buffs: EngineInput[\"buffs\"];\n}\n\nfunction loadAttributionInput(ds: Dataset, filename: string): {\n name: string;\n input: EngineInput;\n} {\n const path = join(CONFORMANCE, \"cruncher\", filename);\n const c = JSON.parse(readFileSync(path, \"utf8\")) as CruncherCaseInput;\n const weapon = ds.weapons.get(c.attacker.weaponId);\n const unit = ds.units.get(c.target.unitId);\n if (!weapon) throw new Error(`attribution: unknown weapon ${c.attacker.weaponId}`);\n if (!unit) throw new Error(`attribution: unknown unit ${c.target.unitId}`);\n return {\n name: c.name,\n input: {\n attacker: { weapon: weapon.raw, profileIndex: c.attacker.profileIndex },\n target: {\n unit: unit.raw,\n profileIndex: c.target.profileIndex,\n ...(c.target.modelCount !== undefined ? { modelCount: c.target.modelCount } : {}),\n },\n modelsFiring: c.modelsFiring,\n buffs: c.buffs,\n context: c.context,\n },\n };\n}\n\nfunction genAttribution(): void {\n const ds = Dataset.embedded();\n const cases = ATTRIBUTION_CASE_FILES.map((filename, idx) => {\n const { name, input } = loadAttributionInput(ds, filename);\n const stages = attributeStages(input, ds);\n return {\n // Persist the input by file reference so the corpus stays a single\n // source of truth — the cruncher case file already pins the EngineInput.\n name,\n cruncher_case: filename,\n expected: stages.map((s) => ({\n name: s.name,\n expected: s.expected,\n baseline: s.baseline,\n lifts: s.lifts.map((l) => ({ source: l.source, delta: l.delta })),\n residual: s.residual,\n intrinsics: s.intrinsics,\n })),\n // Stable ordering of cases in the corpus file.\n _order: idx,\n };\n });\n // Sort by _order and strip the helper before writing.\n cases.sort((a, b) => a._order - b._order);\n const serialised = cases.map(({ _order: _o, ...rest }) => rest);\n writeJson(join(CONFORMANCE, \"attribution\", \"cases.json\"), serialised);\n console.log(`attribution/cases.json: ${cases.length} cases`);\n}\n\n/**\n * Scoring-card translation corpus: humanize each primary mission card's\n * `awards` into plain English. The TS translator is the oracle; the Rust port\n * must reproduce every string byte-for-byte (the differ compares structurally,\n * no tolerance). Only `card_type: \"primary\"` cards are pinned — the 14-card\n * secondary deck isn't revealed yet. Cases are sorted by id for stability, and\n * the `awards` array order within each card is load-bearing.\n */\nfunction genScoringTranslation(): void {\n const ds = Dataset.embedded();\n mkdirSync(join(CONFORMANCE, \"scoring-translation\"), { recursive: true });\n const cases = ds.secondaryCards.all\n .filter((c) => c.card_type === \"primary\")\n .slice()\n .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n .map((card) => ({ cardId: card.id, expected: { awards: describeScoringCard(card) } }));\n writeJson(join(CONFORMANCE, \"scoring-translation\", \"cases.json\"), cases);\n console.log(`scoring-translation/cases.json: ${cases.length} cases`);\n}\n\ngenNormalize();\ngenRosters();\ngenLinkedApi();\ngenAttribution();\ngenScoringTranslation();\n"]}
@@ -123,68 +123,43 @@ export type ConditionNode = SimpleCondition | CompoundCondition;
123
123
  */
124
124
  export type AbilityCondition1 = SimpleCondition | CompoundCondition;
125
125
  /**
126
- * A terrain piece's 2D footprint, relative to the piece's `position`. Axis-aligned rectangle, right triangle (right angle at the local origin, legs along +x/+y), or an explicit polygon. GW's standard templates (e.g. 7"×11.5" rectangles, 8"×11.5" right triangles, 6"×4" rectangles, 10"×2.5" and 6"×2" lines) are all expressible here; lines are thin rectangles.
127
- *
128
- * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
129
- * via the `definition` "footprint".
130
- */
131
- export type Footprint = {
132
- type: "rectangle";
133
- width: number;
134
- height: number;
135
- } | {
136
- type: "right-triangle";
137
- width: number;
138
- height: number;
139
- } | {
140
- type: "polygon";
141
- /**
142
- * @minItems 3
143
- */
144
- points: [Vec2, Vec2, Vec2, ...Vec2[]];
145
- };
146
- /**
147
- * An 11e terrain-area keyword. Confirmed launch set; extend as further keywords publish on dataslate.
148
- *
149
- * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
150
- * via the `definition` "terrain-area-keyword".
126
+ * Effect applied when the action completes (e.g. terrain-area-tag, objective-tag, or unit-tag to mark transient state).
151
127
  */
152
- export type TerrainAreaKeyword = "obscuring" | "hidden" | "plunging-fire";
153
- export type AbilityEffect1 = SingleEffect | ChoiceEffect | SequenceEffect | DiceGatedEffect | ConditionalEffect | DicePoolAllocationEffect;
128
+ export type AbilityEffect = SingleEffect | ChoiceEffect | SequenceEffect | DiceGatedEffect | ConditionalEffect | DicePoolAllocationEffect;
154
129
  /**
155
130
  * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
156
131
  * via the `definition` "single-effect".
157
132
  */
158
133
  export type SingleEffect = unknown & {
159
- type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "bs-modifier" | "engagement-passthrough";
134
+ type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "objective-tag" | "unit-tag" | "bs-modifier" | "engagement-passthrough";
160
135
  target: "self" | "bearer" | "unit" | "attached-unit" | "attacker" | "defender" | "friendly-within-aura" | "enemy-within-aura" | "all-friendly" | "all-enemy";
161
136
  modifier?: {
162
137
  [k: string]: unknown;
163
138
  };
164
139
  [k: string]: unknown;
165
140
  } & {
166
- type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "bs-modifier" | "engagement-passthrough";
141
+ type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "objective-tag" | "unit-tag" | "bs-modifier" | "engagement-passthrough";
167
142
  target: "self" | "bearer" | "unit" | "attached-unit" | "attacker" | "defender" | "friendly-within-aura" | "enemy-within-aura" | "all-friendly" | "all-enemy";
168
143
  modifier?: {
169
144
  [k: string]: unknown;
170
145
  };
171
146
  [k: string]: unknown;
172
147
  } & {
173
- type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "bs-modifier" | "engagement-passthrough";
148
+ type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "objective-tag" | "unit-tag" | "bs-modifier" | "engagement-passthrough";
174
149
  target: "self" | "bearer" | "unit" | "attached-unit" | "attacker" | "defender" | "friendly-within-aura" | "enemy-within-aura" | "all-friendly" | "all-enemy";
175
150
  modifier?: {
176
151
  [k: string]: unknown;
177
152
  };
178
153
  [k: string]: unknown;
179
154
  } & {
180
- type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "bs-modifier" | "engagement-passthrough";
155
+ type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "objective-tag" | "unit-tag" | "bs-modifier" | "engagement-passthrough";
181
156
  target: "self" | "bearer" | "unit" | "attached-unit" | "attacker" | "defender" | "friendly-within-aura" | "enemy-within-aura" | "all-friendly" | "all-enemy";
182
157
  modifier?: {
183
158
  [k: string]: unknown;
184
159
  };
185
160
  [k: string]: unknown;
186
161
  } & {
187
- type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "bs-modifier" | "engagement-passthrough";
162
+ type: "stat-modifier" | "roll-modifier" | "re-roll" | "mortal-wounds" | "feel-no-pain" | "invulnerable-save" | "ward" | "keyword-grant" | "movement-modifier" | "deep-strike" | "fallback-and-act" | "fight-first" | "fight-last" | "shoot-on-death" | "fight-on-death" | "objective-control-modifier" | "leadership-modifier" | "damage-reduction" | "attack-restriction" | "ability-grant" | "cp-gain" | "cp-refund" | "model-destruction" | "resurrection" | "resource-gain" | "resource-spend" | "charge-roll-modifier" | "terrain-area-tag" | "objective-tag" | "unit-tag" | "bs-modifier" | "engagement-passthrough";
188
163
  target: "self" | "bearer" | "unit" | "attached-unit" | "attacker" | "defender" | "friendly-within-aura" | "enemy-within-aura" | "all-friendly" | "all-enemy";
189
164
  modifier?: {
190
165
  [k: string]: unknown;
@@ -197,6 +172,35 @@ export type SingleEffect = unknown & {
197
172
  */
198
173
  export type EffectNode = SingleEffect | ChoiceEffect | SequenceEffect | DiceGatedEffect | ConditionalEffect | DicePoolAllocationEffect;
199
174
  export type AbilityCondition2 = SimpleCondition | CompoundCondition;
175
+ /**
176
+ * A terrain piece's 2D footprint, relative to the piece's `position`. Axis-aligned rectangle, right triangle (right angle at the local origin, legs along +x/+y), or an explicit polygon. GW's standard templates (e.g. 7"×11.5" rectangles, 8"×11.5" right triangles, 6"×4" rectangles, 10"×2.5" and 6"×2" lines) are all expressible here; lines are thin rectangles.
177
+ *
178
+ * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
179
+ * via the `definition` "footprint".
180
+ */
181
+ export type Footprint = {
182
+ type: "rectangle";
183
+ width: number;
184
+ height: number;
185
+ } | {
186
+ type: "right-triangle";
187
+ width: number;
188
+ height: number;
189
+ } | {
190
+ type: "polygon";
191
+ /**
192
+ * @minItems 3
193
+ */
194
+ points: [Vec2, Vec2, Vec2, ...Vec2[]];
195
+ };
196
+ /**
197
+ * An 11e terrain-area keyword. Confirmed launch set; extend as further keywords publish on dataslate.
198
+ *
199
+ * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
200
+ * via the `definition` "terrain-area-keyword".
201
+ */
202
+ export type TerrainAreaKeyword = "obscuring" | "hidden" | "plunging-fire";
203
+ export type AbilityEffect1 = unknown | ChoiceEffect | SequenceEffect | DiceGatedEffect | ConditionalEffect | DicePoolAllocationEffect;
200
204
  /**
201
205
  * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
202
206
  * via the `definition` "condition".
@@ -519,24 +523,58 @@ export interface SecondaryCard {
519
523
  condition?: ArmyCompositionPredicate1;
520
524
  };
521
525
  /**
522
- * Optional player action the card enables.
526
+ * Optional player actions the card enables. Most cards have a single action; a few (e.g. Observe Enemy, with separate Baited-removal and Spotted actions) have two distinct actions on the same card.
527
+ *
528
+ * @minItems 1
523
529
  */
524
- action?: {
525
- /**
526
- * The five official game phases. Unchanged between 10th and 11th edition — 11e reorders Pile In timing within the Fight phase but adds no top-level phase.
527
- */
528
- starts?: "command" | "movement" | "shooting" | "charge" | "fight";
529
- player_turn?: PlayerTurn;
530
- units?: AbilityCondition;
531
- /**
532
- * Maximum number of times the action may be performed.
533
- */
534
- use_limit?: number;
535
- completes?: AbilityCondition1;
536
- effect?: unknown;
537
- };
530
+ actions?: [
531
+ {
532
+ /**
533
+ * Optional kebab-case identifier used to reference this action from `action-completed` conditions in `awards[].when`.
534
+ */
535
+ action_id?: string;
536
+ /**
537
+ * The five official game phases. Unchanged between 10th and 11th edition — 11e reorders Pile In timing within the Fight phase but adds no top-level phase.
538
+ */
539
+ starts?: "command" | "movement" | "shooting" | "charge" | "fight";
540
+ player_turn?: PlayerTurn;
541
+ units?: AbilityCondition;
542
+ /**
543
+ * Maximum number of times the action may be performed (per turn unless `use_limit_scope` says otherwise).
544
+ */
545
+ use_limit?: number;
546
+ /**
547
+ * Whether `use_limit` is enforced per turn or once per game (e.g. Recover the Relics / Find and Deny 'Overwhelming Force' is once per game).
548
+ */
549
+ use_limit_scope?: "per-turn" | "per-game";
550
+ completes?: AbilityCondition1;
551
+ effect?: AbilityEffect;
552
+ },
553
+ ...{
554
+ /**
555
+ * Optional kebab-case identifier used to reference this action from `action-completed` conditions in `awards[].when`.
556
+ */
557
+ action_id?: string;
558
+ /**
559
+ * The five official game phases. Unchanged between 10th and 11th edition — 11e reorders Pile In timing within the Fight phase but adds no top-level phase.
560
+ */
561
+ starts?: "command" | "movement" | "shooting" | "charge" | "fight";
562
+ player_turn?: PlayerTurn;
563
+ units?: AbilityCondition;
564
+ /**
565
+ * Maximum number of times the action may be performed (per turn unless `use_limit_scope` says otherwise).
566
+ */
567
+ use_limit?: number;
568
+ /**
569
+ * Whether `use_limit` is enforced per turn or once per game (e.g. Recover the Relics / Find and Deny 'Overwhelming Force' is once per game).
570
+ */
571
+ use_limit_scope?: "per-turn" | "per-game";
572
+ completes?: AbilityCondition1;
573
+ effect?: AbilityEffect;
574
+ }[]
575
+ ];
538
576
  /**
539
- * VP-award blocks: each scores when `trigger` fires and the optional `when` condition holds. An award scores either a flat `vp` or a count-scaled `vp_per` (VP per instance of the thing named by `per`). Awards accrue independently and sum; a card's '+ ... CUMULATIVE' rows are modelled as separate awards flagged `cumulative` for faithful round-trip.
577
+ * VP-award blocks: each scores when `trigger` fires and the optional `when` condition holds. An award scores either a flat `vp` or a count-scaled `vp_per` (VP per instance of the thing named by `per`). Awards accrue independently and sum; a card's '+ ... CUMULATIVE' rows are modelled as separate awards flagged `cumulative` for faithful round-trip. Awards sharing the same `exclusive_group` value within a card resolve as the highest-scoring single award fires (the card's literal 'OR' rows between tier breakpoints, e.g. Record-Breaking Mission's 3-Fronts vs 4-Fronts).
540
578
  *
541
579
  * @minItems 1
542
580
  */
@@ -585,7 +623,7 @@ export interface ArmyCompositionPredicate1 {
585
623
  * via the `definition` "simple-condition".
586
624
  */
587
625
  export interface SimpleCondition {
588
- type: "phase-is" | "timing-is" | "player-turn-is" | "unit-below-starting-strength" | "unit-below-half-strength" | "unit-has-keyword" | "unit-within-range-of" | "model-is-leader" | "target-has-keyword" | "charged-this-turn" | "advanced-this-turn" | "remained-stationary" | "is-battle-shocked" | "has-lost-wounds" | "opponent-unit-within-range" | "within-range-of-objective" | "attack-is-type" | "has-fought-this-phase" | "destroyed-by-attack-type" | "controls-objective" | "is-attached" | "terrain-area-control" | "engagement-state" | "territory-control" | "fights-first" | "disposition-matches" | "units-destroyed" | "units-destroyed-comparison" | "objective-majority";
626
+ type: "phase-is" | "timing-is" | "player-turn-is" | "unit-below-starting-strength" | "unit-below-half-strength" | "unit-has-keyword" | "unit-within-range-of" | "model-is-leader" | "target-has-keyword" | "charged-this-turn" | "advanced-this-turn" | "remained-stationary" | "is-battle-shocked" | "has-lost-wounds" | "opponent-unit-within-range" | "within-range-of-objective" | "attack-is-type" | "has-fought-this-phase" | "destroyed-by-attack-type" | "controls-objective" | "is-attached" | "terrain-area-control" | "engagement-state" | "territory-control" | "fights-first" | "disposition-matches" | "units-destroyed" | "units-destroyed-comparison" | "objective-majority" | "action-completed" | "objective-has-tag" | "unit-has-tag" | "terrain-has-tag" | "new-objective-controlled" | "engagement-fronts" | "destroyed-while-on-objective";
589
627
  parameters?: {
590
628
  [k: string]: unknown;
591
629
  };
@@ -604,6 +642,99 @@ export interface CompoundCondition {
604
642
  operands: [ConditionNode, ...ConditionNode[]];
605
643
  [k: string]: unknown;
606
644
  }
645
+ /**
646
+ * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
647
+ * via the `definition` "choice-effect".
648
+ */
649
+ export interface ChoiceEffect {
650
+ type: "choice";
651
+ /**
652
+ * @minItems 2
653
+ */
654
+ options: [EffectNode, EffectNode, ...EffectNode[]];
655
+ choice_label?: string;
656
+ [k: string]: unknown;
657
+ }
658
+ /**
659
+ * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
660
+ * via the `definition` "sequence-effect".
661
+ */
662
+ export interface SequenceEffect {
663
+ type: "sequence";
664
+ /**
665
+ * @minItems 1
666
+ */
667
+ steps: [EffectNode, ...EffectNode[]];
668
+ [k: string]: unknown;
669
+ }
670
+ /**
671
+ * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
672
+ * via the `definition` "dice-gated-effect".
673
+ */
674
+ export interface DiceGatedEffect {
675
+ type: "dice-gated";
676
+ /**
677
+ * Dice expression, e.g. 'D6', '2D6'
678
+ */
679
+ dice: string;
680
+ /**
681
+ * Fixed threshold or model characteristic to compare against
682
+ */
683
+ threshold: number | ("leadership" | "toughness" | "save");
684
+ comparison?: "gte" | "lte" | "gt" | "lt" | "eq";
685
+ on_success?: EffectNode | null;
686
+ on_fail?: EffectNode | null;
687
+ [k: string]: unknown;
688
+ }
689
+ /**
690
+ * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
691
+ * via the `definition` "conditional-effect".
692
+ */
693
+ export interface ConditionalEffect {
694
+ type: "conditional";
695
+ condition: AbilityCondition2;
696
+ effect: EffectNode;
697
+ [k: string]: unknown;
698
+ }
699
+ /**
700
+ * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
701
+ * via the `definition` "dice-pool-allocation-effect".
702
+ */
703
+ export interface DicePoolAllocationEffect {
704
+ type: "dice-pool-allocation";
705
+ pool: {
706
+ count: number;
707
+ die: string;
708
+ [k: string]: unknown;
709
+ };
710
+ max_activations: number;
711
+ /**
712
+ * @minItems 1
713
+ */
714
+ options: [
715
+ {
716
+ name: string;
717
+ requirement: {
718
+ type: "pair" | "triple" | "single" | "run";
719
+ min_value: number;
720
+ [k: string]: unknown;
721
+ };
722
+ effect: EffectNode;
723
+ [k: string]: unknown;
724
+ },
725
+ ...{
726
+ name: string;
727
+ requirement: {
728
+ type: "pair" | "triple" | "single" | "run";
729
+ min_value: number;
730
+ [k: string]: unknown;
731
+ };
732
+ effect: EffectNode;
733
+ [k: string]: unknown;
734
+ }[]
735
+ ];
736
+ [k: string]: unknown;
737
+ }
607
738
  /**
608
739
  * A CP-costed ability usable during specific game phases.
609
740
  *
@@ -880,105 +1011,9 @@ export interface WeaponKeyword {
880
1011
  "value" | "target_keyword" | "threshold",
881
1012
  "value" | "target_keyword" | "threshold"
882
1013
  ];
883
- /**
884
- * Mechanical effect of this keyword. Null when the behaviour is faction-specific flavour not yet expressible in the DSL — engines treat such references as no-op buffs and may surface them as 'cannot auto-apply'.
885
- */
886
- effect: AbilityEffect1 | null;
1014
+ effect: unknown;
887
1015
  game_version: GameVersionReference;
888
1016
  }
889
- /**
890
- * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
891
- * via the `definition` "choice-effect".
892
- */
893
- export interface ChoiceEffect {
894
- type: "choice";
895
- /**
896
- * @minItems 2
897
- */
898
- options: [EffectNode, EffectNode, ...EffectNode[]];
899
- choice_label?: string;
900
- [k: string]: unknown;
901
- }
902
- /**
903
- * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
904
- * via the `definition` "sequence-effect".
905
- */
906
- export interface SequenceEffect {
907
- type: "sequence";
908
- /**
909
- * @minItems 1
910
- */
911
- steps: [EffectNode, ...EffectNode[]];
912
- [k: string]: unknown;
913
- }
914
- /**
915
- * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
916
- * via the `definition` "dice-gated-effect".
917
- */
918
- export interface DiceGatedEffect {
919
- type: "dice-gated";
920
- /**
921
- * Dice expression, e.g. 'D6', '2D6'
922
- */
923
- dice: string;
924
- /**
925
- * Fixed threshold or model characteristic to compare against
926
- */
927
- threshold: number | ("leadership" | "toughness" | "save");
928
- comparison?: "gte" | "lte" | "gt" | "lt" | "eq";
929
- on_success?: EffectNode | null;
930
- on_fail?: EffectNode | null;
931
- [k: string]: unknown;
932
- }
933
- /**
934
- * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
935
- * via the `definition` "conditional-effect".
936
- */
937
- export interface ConditionalEffect {
938
- type: "conditional";
939
- condition: AbilityCondition2;
940
- effect: EffectNode;
941
- [k: string]: unknown;
942
- }
943
- /**
944
- * This interface was referenced by `0KdcBundledSchemas`'s JSON-Schema
945
- * via the `definition` "dice-pool-allocation-effect".
946
- */
947
- export interface DicePoolAllocationEffect {
948
- type: "dice-pool-allocation";
949
- pool: {
950
- count: number;
951
- die: string;
952
- [k: string]: unknown;
953
- };
954
- max_activations: number;
955
- /**
956
- * @minItems 1
957
- */
958
- options: [
959
- {
960
- name: string;
961
- requirement: {
962
- type: "pair" | "triple" | "single" | "run";
963
- min_value: number;
964
- [k: string]: unknown;
965
- };
966
- effect: EffectNode;
967
- [k: string]: unknown;
968
- },
969
- ...{
970
- name: string;
971
- requirement: {
972
- type: "pair" | "triple" | "single" | "run";
973
- min_value: number;
974
- [k: string]: unknown;
975
- };
976
- effect: EffectNode;
977
- [k: string]: unknown;
978
- }[]
979
- ];
980
- [k: string]: unknown;
981
- }
982
1017
  /**
983
1018
  * A weapon entry with one or more stat profiles (e.g., standard and overcharge modes).
984
1019
  *