@alpaca-software/40kdc-data 0.5.5 → 0.5.12

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 (126) hide show
  1. package/README.md +10 -0
  2. package/dist/codegen-data.js +2 -0
  3. package/dist/codegen-data.js.map +1 -1
  4. package/dist/commands/import.d.ts.map +1 -1
  5. package/dist/commands/import.js +3 -1
  6. package/dist/commands/import.js.map +1 -1
  7. package/dist/compare.d.ts +167 -0
  8. package/dist/compare.d.ts.map +1 -0
  9. package/dist/compare.js +302 -0
  10. package/dist/compare.js.map +1 -0
  11. package/dist/convert-faction.d.ts.map +1 -1
  12. package/dist/convert-faction.js +22 -2
  13. package/dist/convert-faction.js.map +1 -1
  14. package/dist/cruncher/buffs.d.ts +13 -0
  15. package/dist/cruncher/buffs.d.ts.map +1 -1
  16. package/dist/cruncher/buffs.js +7 -0
  17. package/dist/cruncher/buffs.js.map +1 -1
  18. package/dist/cruncher/engine.d.ts.map +1 -1
  19. package/dist/cruncher/engine.js +14 -10
  20. package/dist/cruncher/engine.js.map +1 -1
  21. package/dist/data/bundle.generated.js +1 -1
  22. package/dist/data/bundle.generated.js.map +1 -1
  23. package/dist/data/dataset.d.ts +3 -1
  24. package/dist/data/dataset.d.ts.map +1 -1
  25. package/dist/data/dataset.js +4 -0
  26. package/dist/data/dataset.js.map +1 -1
  27. package/dist/data/entities.d.ts +22 -0
  28. package/dist/data/entities.d.ts.map +1 -1
  29. package/dist/data/entities.js +45 -1
  30. package/dist/data/entities.js.map +1 -1
  31. package/dist/data/index.d.ts +4 -0
  32. package/dist/data/index.d.ts.map +1 -1
  33. package/dist/data/index.js +4 -0
  34. package/dist/data/index.js.map +1 -1
  35. package/dist/data/types.d.ts +5 -1
  36. package/dist/data/types.d.ts.map +1 -1
  37. package/dist/data/types.js +2 -0
  38. package/dist/data/types.js.map +1 -1
  39. package/dist/export/newrecruit-json.d.ts.map +1 -1
  40. package/dist/export/newrecruit-json.js +4 -3
  41. package/dist/export/newrecruit-json.js.map +1 -1
  42. package/dist/export/newrecruit-simple.js +2 -2
  43. package/dist/export/newrecruit-simple.js.map +1 -1
  44. package/dist/export/newrecruit-wtc.d.ts.map +1 -1
  45. package/dist/export/newrecruit-wtc.js +3 -1
  46. package/dist/export/newrecruit-wtc.js.map +1 -1
  47. package/dist/export/rosterizer.d.ts.map +1 -1
  48. package/dist/export/rosterizer.js +6 -7
  49. package/dist/export/rosterizer.js.map +1 -1
  50. package/dist/extract-faction-pack.d.ts +56 -0
  51. package/dist/extract-faction-pack.d.ts.map +1 -0
  52. package/dist/extract-faction-pack.js +374 -0
  53. package/dist/extract-faction-pack.js.map +1 -0
  54. package/dist/gen-conformance.js +37 -4
  55. package/dist/gen-conformance.js.map +1 -1
  56. package/dist/generated.d.ts +66 -2
  57. package/dist/generated.d.ts.map +1 -1
  58. package/dist/generated.js.map +1 -1
  59. package/dist/import/gw.js +1 -1
  60. package/dist/import/gw.js.map +1 -1
  61. package/dist/import/import-roster.d.ts.map +1 -1
  62. package/dist/import/import-roster.js +5 -0
  63. package/dist/import/import-roster.js.map +1 -1
  64. package/dist/import/index.d.ts +1 -0
  65. package/dist/import/index.d.ts.map +1 -1
  66. package/dist/import/index.js +1 -0
  67. package/dist/import/index.js.map +1 -1
  68. package/dist/import/listforge-text.js +1 -1
  69. package/dist/import/listforge-text.js.map +1 -1
  70. package/dist/import/listforge.d.ts.map +1 -1
  71. package/dist/import/listforge.js +18 -3
  72. package/dist/import/listforge.js.map +1 -1
  73. package/dist/import/newrecruit-json.d.ts.map +1 -1
  74. package/dist/import/newrecruit-json.js +21 -3
  75. package/dist/import/newrecruit-json.js.map +1 -1
  76. package/dist/import/newrecruit-simple.js +1 -1
  77. package/dist/import/newrecruit-simple.js.map +1 -1
  78. package/dist/import/newrecruit-wtc.js +1 -1
  79. package/dist/import/newrecruit-wtc.js.map +1 -1
  80. package/dist/import/resolve.d.ts.map +1 -1
  81. package/dist/import/resolve.js +46 -18
  82. package/dist/import/resolve.js.map +1 -1
  83. package/dist/import/roster-json.d.ts +32 -0
  84. package/dist/import/roster-json.d.ts.map +1 -0
  85. package/dist/import/roster-json.js +92 -0
  86. package/dist/import/roster-json.js.map +1 -0
  87. package/dist/import/rosterizer.js +3 -3
  88. package/dist/import/rosterizer.js.map +1 -1
  89. package/dist/import/types.d.ts +16 -5
  90. package/dist/import/types.d.ts.map +1 -1
  91. package/dist/import/types.js.map +1 -1
  92. package/dist/index.d.ts +2 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +8 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/migrate-keystone-twins.d.ts +23 -0
  97. package/dist/migrate-keystone-twins.d.ts.map +1 -0
  98. package/dist/migrate-keystone-twins.js +184 -0
  99. package/dist/migrate-keystone-twins.js.map +1 -0
  100. package/dist/rube-goldberg.js +1 -1
  101. package/dist/rube-goldberg.js.map +1 -1
  102. package/dist/runner.d.ts.map +1 -1
  103. package/dist/runner.js +94 -1
  104. package/dist/runner.js.map +1 -1
  105. package/dist/scope.d.ts +31 -0
  106. package/dist/scope.d.ts.map +1 -0
  107. package/dist/scope.js +34 -0
  108. package/dist/scope.js.map +1 -0
  109. package/dist/translate/effect.d.ts +17 -3
  110. package/dist/translate/effect.d.ts.map +1 -1
  111. package/dist/translate/effect.js +23 -4
  112. package/dist/translate/effect.js.map +1 -1
  113. package/dist/translate/index.d.ts +1 -1
  114. package/dist/translate/index.d.ts.map +1 -1
  115. package/dist/translate/index.js +1 -1
  116. package/dist/translate/index.js.map +1 -1
  117. package/dist/validate.d.ts.map +1 -1
  118. package/dist/validate.js +2 -0
  119. package/dist/validate.js.map +1 -1
  120. package/package.json +2 -1
  121. package/schemas/core/hull-shape.schema.json +30 -0
  122. package/schemas/core/roster.schema.json +26 -9
  123. package/schemas/core/stratagem.schema.json +2 -2
  124. package/schemas/core/target-profile.schema.json +30 -0
  125. package/schemas/core/unit-composition.schema.json +7 -0
  126. package/schemas/enrichment/ability-dsl/ability.schema.json +14 -0
package/dist/scope.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Resolve which roster units an ability (typically a detachment rule) benefits,
3
+ * from its curated `applies_to` keyword filter. This is the roster-highlighting
4
+ * contract consumers replicate: a unit matches iff it carries every
5
+ * `required_keywords` entry and none of the `excluded_keywords`, compared
6
+ * against the union of its `keywords` and `faction_keywords`. Matching is
7
+ * exact-string and case-sensitive — keywords are authored in datasheet Title
8
+ * Case on both sides.
9
+ *
10
+ * Two distinct "no constraint" forms, both deliberate:
11
+ * - a `null`/absent filter has no resolvable scope → matches nothing (the app
12
+ * renders no highlight rather than guess);
13
+ * - a present filter object with neither keyword list constrains nothing →
14
+ * matches every unit (vacuous required/excluded sets).
15
+ *
16
+ * Pinned byte-for-byte across the TS/Rust/Python ports by the
17
+ * `conformance/applies-to` corpus (bump `conformance/SPEC_VERSION` on change).
18
+ */
19
+ /** True when `unit` falls within `filter`'s scope. See module docs for semantics. */
20
+ export function unitMatchesAppliesTo(filter, unit) {
21
+ if (filter == null)
22
+ return false;
23
+ const owned = new Set([...(unit.keywords ?? []), ...(unit.faction_keywords ?? [])]);
24
+ for (const kw of filter.required_keywords ?? []) {
25
+ if (!owned.has(kw))
26
+ return false;
27
+ }
28
+ for (const kw of filter.excluded_keywords ?? []) {
29
+ if (owned.has(kw))
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAcH,qFAAqF;AACrF,MAAM,UAAU,oBAAoB,CAClC,MAA0C,EAC1C,IAAuB;IAEvB,IAAI,MAAM,IAAI,IAAI;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5F,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,iBAAiB,IAAI,EAAE,EAAE,CAAC;QAChD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IACD,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,iBAAiB,IAAI,EAAE,EAAE,CAAC;QAChD,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;IAClC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * Resolve which roster units an ability (typically a detachment rule) benefits,\n * from its curated `applies_to` keyword filter. This is the roster-highlighting\n * contract consumers replicate: a unit matches iff it carries every\n * `required_keywords` entry and none of the `excluded_keywords`, compared\n * against the union of its `keywords` and `faction_keywords`. Matching is\n * exact-string and case-sensitive — keywords are authored in datasheet Title\n * Case on both sides.\n *\n * Two distinct \"no constraint\" forms, both deliberate:\n * - a `null`/absent filter has no resolvable scope → matches nothing (the app\n * renders no highlight rather than guess);\n * - a present filter object with neither keyword list constrains nothing →\n * matches every unit (vacuous required/excluded sets).\n *\n * Pinned byte-for-byte across the TS/Rust/Python ports by the\n * `conformance/applies-to` corpus (bump `conformance/SPEC_VERSION` on change).\n */\n\n/** The curated keyword filter, as carried on `Ability.applies_to`. */\nexport interface AppliesToFilter {\n required_keywords?: string[];\n excluded_keywords?: string[];\n}\n\n/** The keyword-bearing fields of a unit a filter is matched against. */\nexport interface UnitKeywordSource {\n keywords?: string[];\n faction_keywords?: string[];\n}\n\n/** True when `unit` falls within `filter`'s scope. See module docs for semantics. */\nexport function unitMatchesAppliesTo(\n filter: AppliesToFilter | null | undefined,\n unit: UnitKeywordSource,\n): boolean {\n if (filter == null) return false;\n const owned = new Set<string>([...(unit.keywords ?? []), ...(unit.faction_keywords ?? [])]);\n for (const kw of filter.required_keywords ?? []) {\n if (!owned.has(kw)) return false;\n }\n for (const kw of filter.excluded_keywords ?? []) {\n if (owned.has(kw)) return false;\n }\n return true;\n}\n"]}
@@ -48,11 +48,17 @@ export interface AbilityScope {
48
48
  duration?: string;
49
49
  range_inches?: number;
50
50
  }
51
+ /** Curated keyword filter naming which units an ability benefits. */
52
+ export interface AbilityAppliesTo {
53
+ required_keywords?: string[];
54
+ excluded_keywords?: string[];
55
+ }
51
56
  /** Minimal ability view for `describeAbility`. */
52
57
  export interface AbilityLike {
53
58
  name?: string;
54
59
  effect?: Effect;
55
60
  scope?: AbilityScope;
61
+ applies_to?: AbilityAppliesTo | null;
56
62
  }
57
63
  /** Single-clause translation for leaf effects (and inline container forms). */
58
64
  export declare function describeEffectInline(e: Effect): string;
@@ -64,9 +70,17 @@ export declare function describeEffect(e: Effect, depth?: number): string;
64
70
  /** `Scope: aura (6"). Duration: phase.` — empty string when absent. */
65
71
  export declare function describeScope(s?: AbilityScope): string;
66
72
  /**
67
- * Full generated text for an ability: the effect tree plus a trailing scope
68
- * line. This is the `ability.print()` consumers render when the dataset
69
- * carries no rules prose.
73
+ * `Applies to: units with Possessed.` the roster-highlighting audience named
74
+ * by a curated `applies_to` filter. Empty string when the filter is absent or
75
+ * carries no keywords (nothing to say). `required_keywords` reads as an AND set;
76
+ * `excluded_keywords` render as a trailing `(excluding …)`.
77
+ */
78
+ export declare function describeAppliesTo(a?: AbilityAppliesTo | null): string;
79
+ /**
80
+ * Full generated text for an ability: the effect tree, a trailing scope line,
81
+ * and a trailing `Applies to:` line when the ability carries a curated
82
+ * `applies_to` filter. This is the `ability.print()` consumers render when the
83
+ * dataset carries no rules prose.
70
84
  */
71
85
  export declare function describeAbility(a: AbilityLike): string;
72
86
  //# sourceMappingURL=effect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../../src/translate/effect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,MAAM,GAAG;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACvC,CAAC,EAAE,CAAC;IACL,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,kDAAkD;AAClD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAoCD,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAmItD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,MAAM,CAmCnE;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAMtD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAItD"}
1
+ {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../../src/translate/effect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,MAAM,GAAG;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACvC,CAAC,EAAE,CAAC;IACL,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,kDAAkD;AAClD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACtC;AAoCD,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAmItD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,MAAM,CAmCnE;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAMtD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,CAAC,EAAE,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAQrE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAKtD"}
@@ -229,13 +229,32 @@ export function describeScope(s) {
229
229
  return `Scope: ${range}${inches}. Duration: ${duration}.`;
230
230
  }
231
231
  /**
232
- * Full generated text for an ability: the effect tree plus a trailing scope
233
- * line. This is the `ability.print()` consumers render when the dataset
234
- * carries no rules prose.
232
+ * `Applies to: units with Possessed.` the roster-highlighting audience named
233
+ * by a curated `applies_to` filter. Empty string when the filter is absent or
234
+ * carries no keywords (nothing to say). `required_keywords` reads as an AND set;
235
+ * `excluded_keywords` render as a trailing `(excluding …)`.
236
+ */
237
+ export function describeAppliesTo(a) {
238
+ if (!a)
239
+ return "";
240
+ const required = a.required_keywords ?? [];
241
+ const excluded = a.excluded_keywords ?? [];
242
+ if (required.length === 0 && excluded.length === 0)
243
+ return "";
244
+ const base = required.length ? `units with ${required.join(", ")}` : "all units";
245
+ const exc = excluded.length ? ` (excluding ${excluded.join(", ")})` : "";
246
+ return `Applies to: ${base}${exc}.`;
247
+ }
248
+ /**
249
+ * Full generated text for an ability: the effect tree, a trailing scope line,
250
+ * and a trailing `Applies to:` line when the ability carries a curated
251
+ * `applies_to` filter. This is the `ability.print()` consumers render when the
252
+ * dataset carries no rules prose.
235
253
  */
236
254
  export function describeAbility(a) {
237
255
  const effect = a.effect ? describeEffect(a.effect) : "";
238
256
  const scope = describeScope(a.scope);
239
- return scope ? (effect ? `${effect}\n${scope}` : scope) : effect;
257
+ const applies = describeAppliesTo(a.applies_to);
258
+ return [effect, scope, applies].filter(Boolean).join("\n");
240
259
  }
241
260
  //# sourceMappingURL=effect.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"effect.js","sourceRoot":"","sources":["../../src/translate/effect.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Humanize an Ability-DSL `effect` tree into plain English — the\n * `ability.print()` of the dataset. Output is an *approximation* generated\n * purely from the structured data (no external rules text), ASCII-only, with\n * a fixed clause order: it is pinned byte-for-byte across the TS and Rust\n * ports by the `conformance/effect-translation` corpus, so any phrasing\n * change here is a semantic corpus change (bump `conformance/SPEC_VERSION`).\n *\n * Container nodes (`sequence`, `conditional`, `choice`, `dice-gated`,\n * `dice-pool-allocation`) render block-style with two-space indentation and\n * an ASCII `-> ` arrow; leaves render as single clauses. Unknown leaf types\n * and unrecognized modifier shapes degrade to a deterministic bracketed form\n * (`[the-type]`) rather than failing — coverage improves as authoring does.\n */\n\nimport { describeCondition, dekebab, type Condition } from \"./condition.js\";\n\n/**\n * Minimal structural view of an effect node. Matches the ability-dsl effect\n * schema: a single effect carries `type` + `target` + `modifier`; containers\n * carry their own shape (`steps`, `options`, `condition`/`effect`, dice\n * fields).\n */\nexport interface Effect {\n type?: string;\n target?: string;\n modifier?: Record<string, unknown>;\n condition?: Condition;\n effect?: Effect;\n steps?: Effect[];\n options?: (Effect & {\n name?: string;\n requirement?: Record<string, unknown>;\n })[];\n choice_label?: string;\n dice?: string;\n threshold?: number | string;\n comparison?: string;\n on_success?: Effect | null;\n on_fail?: Effect | null;\n pool?: { count: number; die: string };\n max_activations?: number;\n}\n\n/** Ability scope, as carried on enrichment ability entries. */\nexport interface AbilityScope {\n range?: string;\n duration?: string;\n range_inches?: number;\n}\n\n/** Minimal ability view for `describeAbility`. */\nexport interface AbilityLike {\n name?: string;\n effect?: Effect;\n scope?: AbilityScope;\n}\n\n/** JS-template stringification (numbers print without trailing `.0`). */\nfunction jstr(v: unknown): string {\n if (v == null) return \"?\";\n if (Array.isArray(v)) return v.map(jstr).join(\", \");\n return String(v);\n}\n\nfunction formatTarget(t?: string): string {\n return t ? dekebab(t) : \"target\";\n}\n\nfunction signed(operation: unknown, value: unknown): string {\n const op = operation === \"add\" || operation === \"improve\" ? \"+\" : \"-\";\n return `${op}${jstr(value)}`;\n}\n\nfunction formatComparison(comp: string, threshold: unknown): string {\n const th = jstr(threshold);\n switch (comp) {\n case \"gte\":\n return `${th}+`;\n case \"lte\":\n return `${th} or less`;\n case \"gt\":\n return `greater than ${th}`;\n case \"lt\":\n return `less than ${th}`;\n case \"eq\":\n return `exactly ${th}`;\n default:\n return `${th}+`;\n }\n}\n\n/** Single-clause translation for leaf effects (and inline container forms). */\nexport function describeEffectInline(e: Effect): string {\n const m = e.modifier ?? {};\n const target = formatTarget(e.target);\n\n switch (e.type) {\n case \"stat-modifier\": {\n const scope = m.attack_type ? ` (${jstr(m.attack_type)})` : \"\";\n if (m.stat == null) return `modify stats for ${target}`;\n if (m.operation === \"set\") return `set ${jstr(m.stat)} to ${jstr(m.value)}${scope} for ${target}`;\n return `${signed(m.operation, m.value)} ${jstr(m.stat)}${scope} for ${target}`;\n }\n case \"roll-modifier\": {\n const ctx = m.context ? ` (${jstr(m.context)})` : \"\";\n if (m.value == null) return `${dekebab(jstr(m.operation))} ${jstr(m.roll)} rolls${ctx} for ${target}`;\n return `${signed(m.operation, m.value)} to ${jstr(m.roll)} rolls${ctx} for ${target}`;\n }\n case \"re-roll\": {\n const subset = m.subset ? ` (${dekebab(jstr(m.subset))})` : \"\";\n const atk = m.attack_type ? ` (${jstr(m.attack_type)})` : \"\";\n return `re-roll ${jstr(m.roll)} rolls${subset}${atk} for ${target}`;\n }\n case \"mortal-wounds\": {\n const amount = m.count ?? m.amount ?? (m.amount_table ? \"variable\" : \"?\");\n const range = m.range ?? m.range_inches;\n const within = range != null ? ` (within ${jstr(range)}\")` : \"\";\n return `deal ${jstr(amount)} mortal wounds to ${target}${within}`;\n }\n case \"feel-no-pain\":\n return `${target} gains Feel No Pain ${jstr(m.threshold)}+`;\n case \"ward\":\n return `${target} gains Ward ${jstr(m.threshold ?? m.value)}+`;\n case \"invulnerable-save\":\n return `${target} gains a ${jstr(m.value)}+ invulnerable save`;\n case \"keyword-grant\": {\n const kw = Array.isArray(m.keywords) ? m.keywords.map(jstr).join(\", \") : jstr(m.keyword ?? \"keywords\");\n if (m.weapon_name != null) return `${target}'s ${jstr(m.weapon_name)} gains ${kw}`;\n if (m.weapon_type != null) return `${target}'s ${jstr(m.weapon_type)} weapons gain ${kw}`;\n return `${target}'s weapons gain ${kw}`;\n }\n case \"ability-grant\": {\n const grant = m.grant_type ?? m.ability_id;\n const cap = m.capacity != null ? ` (${jstr(m.capacity)})` : \"\";\n return `${target} gains ${grant != null ? dekebab(jstr(grant)) : \"an ability\"}${cap}`;\n }\n case \"movement-modifier\": {\n const kind = m.move_type ?? m.type;\n const dist = m.distance ?? m.value;\n const inches = dist != null ? ` ${jstr(dist)}\"` : \"\";\n return `${target} gains ${kind != null ? dekebab(jstr(kind)) : \"a movement effect\"}${inches}`;\n }\n case \"damage-reduction\":\n return `reduce incoming damage to ${target} by ${jstr(m.amount ?? m.value)}`;\n case \"resurrection\":\n return `return ${jstr(m.count ?? 1)} model(s) to ${target} with ${jstr(m.wounds_remaining ?? \"full\")} wounds`;\n case \"model-destruction\":\n return `destroy ${jstr(m.count)} non-leader model(s) from ${target}`;\n case \"cp-gain\":\n return `gain ${jstr(m.amount)} CP`;\n case \"cp-refund\":\n return `refund ${jstr(m.amount)} CP`;\n case \"resource-gain\":\n return `gain ${jstr(m.amount)} to ${jstr(m.pool_id)}`;\n case \"resource-spend\":\n return `spend ${jstr(m.amount)} from ${jstr(m.pool_id)}`;\n case \"leadership-modifier\": {\n if (m.test != null && m.operation == null) return `force a ${dekebab(jstr(m.test))} test on ${target}`;\n if (m.test != null) return `${dekebab(jstr(m.operation))} ${dekebab(jstr(m.test))} tests for ${target}`;\n if (m.operation != null) return `${signed(m.operation, m.value)} Leadership for ${target}`;\n return `modify Leadership for ${target}`;\n }\n case \"fight-first\":\n return `${target} fights first`;\n case \"fight-last\":\n return `${target} fights last`;\n case \"fight-on-death\":\n return `${target} fights on death`;\n case \"shoot-on-death\":\n return `${target} shoots on death`;\n case \"deep-strike\":\n return `${target} can deep strike`;\n case \"fallback-and-act\":\n return `${target} can fall back and act`;\n case \"attack-restriction\": {\n const what = m.restriction ?? m.restriction_type;\n const range = m.range != null ? ` (within ${jstr(m.range)}\")` : \"\";\n const max = m.max_models != null ? ` (max ${jstr(m.max_models)} models)` : \"\";\n return `${target}: ${what != null ? dekebab(jstr(what)) : \"attack restriction\"}${range}${max}`;\n }\n case \"objective-control-modifier\": {\n if (m.operation != null) return `${signed(m.operation, m.value)} OC for ${target}`;\n return `modify OC of ${target} by ${jstr(m.value)}`;\n }\n case \"bs-modifier\":\n return `${signed(m.operation, m.value)} BS for ${target}`;\n case \"charge-roll-modifier\":\n return `${signed(m.operation, m.value)} to charge rolls for ${target}`;\n case \"engagement-passthrough\":\n return `${target} can move through engagement range`;\n case \"terrain-area-tag\":\n return `tag the terrain area as ${dekebab(jstr(m.tag))}`;\n case \"objective-tag\":\n return `tag the objective as ${dekebab(jstr(m.tag))}`;\n case \"unit-tag\":\n return `tag ${target} as ${dekebab(jstr(m.tag))}`;\n\n // Container types — inline forms.\n case \"conditional\":\n return `if ${describeCondition(e.condition ?? {})}: ${describeEffectInline(e.effect ?? {})}`;\n case \"sequence\":\n return (e.steps ?? []).map(describeEffectInline).join(\"; \");\n case \"choice\": {\n const label = e.choice_label ? ` (${e.choice_label})` : \"\";\n return `choose one${label}: ${(e.options ?? []).map(describeEffectInline).join(\" / \")}`;\n }\n case \"dice-gated\": {\n const comp = formatComparison(e.comparison ?? \"gte\", e.threshold);\n const success = e.on_success ? describeEffectInline(e.on_success) : \"nothing\";\n const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : \"\";\n return `roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;\n }\n case \"dice-pool-allocation\": {\n const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : \"?\";\n const opts = (e.options ?? [])\n .map((o) => `${jstr(o.name)} (${jstr(o.requirement?.min_value)}+): ${describeEffectInline(o.effect ?? {})}`)\n .join(\" / \");\n return `roll ${pool}: ${opts}`;\n }\n\n default:\n return `[${e.type ?? \"unknown\"}]`;\n }\n}\n\n/**\n * Block translation of an effect tree. Containers expand over multiple lines\n * with two-space indentation; leaves delegate to `describeEffectInline`.\n */\nexport function describeEffect(e: Effect, depth: number = 0): string {\n const indent = \" \".repeat(depth);\n const arrow = depth > 0 ? \"-> \" : \"\";\n\n switch (e.type) {\n case \"conditional\":\n return `${indent}If ${describeCondition(e.condition ?? {})}:\\n` + describeEffect(e.effect ?? {}, depth + 1);\n case \"sequence\":\n return (e.steps ?? []).map((s) => describeEffect(s, depth)).join(\"\\n\");\n case \"choice\": {\n const label = e.choice_label ? ` (${e.choice_label})` : \"\";\n return (\n `${indent}${arrow}Choose one${label}:\\n` +\n (e.options ?? []).map((o, i) => `${indent} ${i + 1}. ${describeEffectInline(o)}`).join(\"\\n\")\n );\n }\n case \"dice-gated\": {\n const comp = formatComparison(e.comparison ?? \"gte\", e.threshold);\n const success = e.on_success ? describeEffectInline(e.on_success) : \"nothing\";\n const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : \"\";\n return `${indent}${arrow}Roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;\n }\n case \"dice-pool-allocation\": {\n const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : \"?\";\n const lines = [`${indent}${arrow}Roll ${pool} (max ${jstr(e.max_activations)} activations):`];\n for (const opt of e.options ?? []) {\n lines.push(\n `${indent} - ${jstr(opt.name)}: need ${jstr(opt.requirement?.type)} of ${jstr(opt.requirement?.min_value)}+ -> ${describeEffectInline(opt.effect ?? {})}`\n );\n }\n return lines.join(\"\\n\");\n }\n default:\n return `${indent}${arrow}${describeEffectInline(e)}`;\n }\n}\n\n/** `Scope: aura (6\"). Duration: phase.` — empty string when absent. */\nexport function describeScope(s?: AbilityScope): string {\n if (!s || (!s.range && !s.duration)) return \"\";\n const range = dekebab(s.range ?? \"\");\n const inches = s.range_inches != null ? ` (${jstr(s.range_inches)}\")` : \"\";\n const duration = dekebab(s.duration ?? \"\");\n return `Scope: ${range}${inches}. Duration: ${duration}.`;\n}\n\n/**\n * Full generated text for an ability: the effect tree plus a trailing scope\n * line. This is the `ability.print()` consumers render when the dataset\n * carries no rules prose.\n */\nexport function describeAbility(a: AbilityLike): string {\n const effect = a.effect ? describeEffect(a.effect) : \"\";\n const scope = describeScope(a.scope);\n return scope ? (effect ? `${effect}\\n${scope}` : scope) : effect;\n}\n"]}
1
+ {"version":3,"file":"effect.js","sourceRoot":"","sources":["../../src/translate/effect.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Humanize an Ability-DSL `effect` tree into plain English — the\n * `ability.print()` of the dataset. Output is an *approximation* generated\n * purely from the structured data (no external rules text), ASCII-only, with\n * a fixed clause order: it is pinned byte-for-byte across the TS and Rust\n * ports by the `conformance/effect-translation` corpus, so any phrasing\n * change here is a semantic corpus change (bump `conformance/SPEC_VERSION`).\n *\n * Container nodes (`sequence`, `conditional`, `choice`, `dice-gated`,\n * `dice-pool-allocation`) render block-style with two-space indentation and\n * an ASCII `-> ` arrow; leaves render as single clauses. Unknown leaf types\n * and unrecognized modifier shapes degrade to a deterministic bracketed form\n * (`[the-type]`) rather than failing — coverage improves as authoring does.\n */\n\nimport { describeCondition, dekebab, type Condition } from \"./condition.js\";\n\n/**\n * Minimal structural view of an effect node. Matches the ability-dsl effect\n * schema: a single effect carries `type` + `target` + `modifier`; containers\n * carry their own shape (`steps`, `options`, `condition`/`effect`, dice\n * fields).\n */\nexport interface Effect {\n type?: string;\n target?: string;\n modifier?: Record<string, unknown>;\n condition?: Condition;\n effect?: Effect;\n steps?: Effect[];\n options?: (Effect & {\n name?: string;\n requirement?: Record<string, unknown>;\n })[];\n choice_label?: string;\n dice?: string;\n threshold?: number | string;\n comparison?: string;\n on_success?: Effect | null;\n on_fail?: Effect | null;\n pool?: { count: number; die: string };\n max_activations?: number;\n}\n\n/** Ability scope, as carried on enrichment ability entries. */\nexport interface AbilityScope {\n range?: string;\n duration?: string;\n range_inches?: number;\n}\n\n/** Curated keyword filter naming which units an ability benefits. */\nexport interface AbilityAppliesTo {\n required_keywords?: string[];\n excluded_keywords?: string[];\n}\n\n/** Minimal ability view for `describeAbility`. */\nexport interface AbilityLike {\n name?: string;\n effect?: Effect;\n scope?: AbilityScope;\n applies_to?: AbilityAppliesTo | null;\n}\n\n/** JS-template stringification (numbers print without trailing `.0`). */\nfunction jstr(v: unknown): string {\n if (v == null) return \"?\";\n if (Array.isArray(v)) return v.map(jstr).join(\", \");\n return String(v);\n}\n\nfunction formatTarget(t?: string): string {\n return t ? dekebab(t) : \"target\";\n}\n\nfunction signed(operation: unknown, value: unknown): string {\n const op = operation === \"add\" || operation === \"improve\" ? \"+\" : \"-\";\n return `${op}${jstr(value)}`;\n}\n\nfunction formatComparison(comp: string, threshold: unknown): string {\n const th = jstr(threshold);\n switch (comp) {\n case \"gte\":\n return `${th}+`;\n case \"lte\":\n return `${th} or less`;\n case \"gt\":\n return `greater than ${th}`;\n case \"lt\":\n return `less than ${th}`;\n case \"eq\":\n return `exactly ${th}`;\n default:\n return `${th}+`;\n }\n}\n\n/** Single-clause translation for leaf effects (and inline container forms). */\nexport function describeEffectInline(e: Effect): string {\n const m = e.modifier ?? {};\n const target = formatTarget(e.target);\n\n switch (e.type) {\n case \"stat-modifier\": {\n const scope = m.attack_type ? ` (${jstr(m.attack_type)})` : \"\";\n if (m.stat == null) return `modify stats for ${target}`;\n if (m.operation === \"set\") return `set ${jstr(m.stat)} to ${jstr(m.value)}${scope} for ${target}`;\n return `${signed(m.operation, m.value)} ${jstr(m.stat)}${scope} for ${target}`;\n }\n case \"roll-modifier\": {\n const ctx = m.context ? ` (${jstr(m.context)})` : \"\";\n if (m.value == null) return `${dekebab(jstr(m.operation))} ${jstr(m.roll)} rolls${ctx} for ${target}`;\n return `${signed(m.operation, m.value)} to ${jstr(m.roll)} rolls${ctx} for ${target}`;\n }\n case \"re-roll\": {\n const subset = m.subset ? ` (${dekebab(jstr(m.subset))})` : \"\";\n const atk = m.attack_type ? ` (${jstr(m.attack_type)})` : \"\";\n return `re-roll ${jstr(m.roll)} rolls${subset}${atk} for ${target}`;\n }\n case \"mortal-wounds\": {\n const amount = m.count ?? m.amount ?? (m.amount_table ? \"variable\" : \"?\");\n const range = m.range ?? m.range_inches;\n const within = range != null ? ` (within ${jstr(range)}\")` : \"\";\n return `deal ${jstr(amount)} mortal wounds to ${target}${within}`;\n }\n case \"feel-no-pain\":\n return `${target} gains Feel No Pain ${jstr(m.threshold)}+`;\n case \"ward\":\n return `${target} gains Ward ${jstr(m.threshold ?? m.value)}+`;\n case \"invulnerable-save\":\n return `${target} gains a ${jstr(m.value)}+ invulnerable save`;\n case \"keyword-grant\": {\n const kw = Array.isArray(m.keywords) ? m.keywords.map(jstr).join(\", \") : jstr(m.keyword ?? \"keywords\");\n if (m.weapon_name != null) return `${target}'s ${jstr(m.weapon_name)} gains ${kw}`;\n if (m.weapon_type != null) return `${target}'s ${jstr(m.weapon_type)} weapons gain ${kw}`;\n return `${target}'s weapons gain ${kw}`;\n }\n case \"ability-grant\": {\n const grant = m.grant_type ?? m.ability_id;\n const cap = m.capacity != null ? ` (${jstr(m.capacity)})` : \"\";\n return `${target} gains ${grant != null ? dekebab(jstr(grant)) : \"an ability\"}${cap}`;\n }\n case \"movement-modifier\": {\n const kind = m.move_type ?? m.type;\n const dist = m.distance ?? m.value;\n const inches = dist != null ? ` ${jstr(dist)}\"` : \"\";\n return `${target} gains ${kind != null ? dekebab(jstr(kind)) : \"a movement effect\"}${inches}`;\n }\n case \"damage-reduction\":\n return `reduce incoming damage to ${target} by ${jstr(m.amount ?? m.value)}`;\n case \"resurrection\":\n return `return ${jstr(m.count ?? 1)} model(s) to ${target} with ${jstr(m.wounds_remaining ?? \"full\")} wounds`;\n case \"model-destruction\":\n return `destroy ${jstr(m.count)} non-leader model(s) from ${target}`;\n case \"cp-gain\":\n return `gain ${jstr(m.amount)} CP`;\n case \"cp-refund\":\n return `refund ${jstr(m.amount)} CP`;\n case \"resource-gain\":\n return `gain ${jstr(m.amount)} to ${jstr(m.pool_id)}`;\n case \"resource-spend\":\n return `spend ${jstr(m.amount)} from ${jstr(m.pool_id)}`;\n case \"leadership-modifier\": {\n if (m.test != null && m.operation == null) return `force a ${dekebab(jstr(m.test))} test on ${target}`;\n if (m.test != null) return `${dekebab(jstr(m.operation))} ${dekebab(jstr(m.test))} tests for ${target}`;\n if (m.operation != null) return `${signed(m.operation, m.value)} Leadership for ${target}`;\n return `modify Leadership for ${target}`;\n }\n case \"fight-first\":\n return `${target} fights first`;\n case \"fight-last\":\n return `${target} fights last`;\n case \"fight-on-death\":\n return `${target} fights on death`;\n case \"shoot-on-death\":\n return `${target} shoots on death`;\n case \"deep-strike\":\n return `${target} can deep strike`;\n case \"fallback-and-act\":\n return `${target} can fall back and act`;\n case \"attack-restriction\": {\n const what = m.restriction ?? m.restriction_type;\n const range = m.range != null ? ` (within ${jstr(m.range)}\")` : \"\";\n const max = m.max_models != null ? ` (max ${jstr(m.max_models)} models)` : \"\";\n return `${target}: ${what != null ? dekebab(jstr(what)) : \"attack restriction\"}${range}${max}`;\n }\n case \"objective-control-modifier\": {\n if (m.operation != null) return `${signed(m.operation, m.value)} OC for ${target}`;\n return `modify OC of ${target} by ${jstr(m.value)}`;\n }\n case \"bs-modifier\":\n return `${signed(m.operation, m.value)} BS for ${target}`;\n case \"charge-roll-modifier\":\n return `${signed(m.operation, m.value)} to charge rolls for ${target}`;\n case \"engagement-passthrough\":\n return `${target} can move through engagement range`;\n case \"terrain-area-tag\":\n return `tag the terrain area as ${dekebab(jstr(m.tag))}`;\n case \"objective-tag\":\n return `tag the objective as ${dekebab(jstr(m.tag))}`;\n case \"unit-tag\":\n return `tag ${target} as ${dekebab(jstr(m.tag))}`;\n\n // Container types — inline forms.\n case \"conditional\":\n return `if ${describeCondition(e.condition ?? {})}: ${describeEffectInline(e.effect ?? {})}`;\n case \"sequence\":\n return (e.steps ?? []).map(describeEffectInline).join(\"; \");\n case \"choice\": {\n const label = e.choice_label ? ` (${e.choice_label})` : \"\";\n return `choose one${label}: ${(e.options ?? []).map(describeEffectInline).join(\" / \")}`;\n }\n case \"dice-gated\": {\n const comp = formatComparison(e.comparison ?? \"gte\", e.threshold);\n const success = e.on_success ? describeEffectInline(e.on_success) : \"nothing\";\n const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : \"\";\n return `roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;\n }\n case \"dice-pool-allocation\": {\n const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : \"?\";\n const opts = (e.options ?? [])\n .map((o) => `${jstr(o.name)} (${jstr(o.requirement?.min_value)}+): ${describeEffectInline(o.effect ?? {})}`)\n .join(\" / \");\n return `roll ${pool}: ${opts}`;\n }\n\n default:\n return `[${e.type ?? \"unknown\"}]`;\n }\n}\n\n/**\n * Block translation of an effect tree. Containers expand over multiple lines\n * with two-space indentation; leaves delegate to `describeEffectInline`.\n */\nexport function describeEffect(e: Effect, depth: number = 0): string {\n const indent = \" \".repeat(depth);\n const arrow = depth > 0 ? \"-> \" : \"\";\n\n switch (e.type) {\n case \"conditional\":\n return `${indent}If ${describeCondition(e.condition ?? {})}:\\n` + describeEffect(e.effect ?? {}, depth + 1);\n case \"sequence\":\n return (e.steps ?? []).map((s) => describeEffect(s, depth)).join(\"\\n\");\n case \"choice\": {\n const label = e.choice_label ? ` (${e.choice_label})` : \"\";\n return (\n `${indent}${arrow}Choose one${label}:\\n` +\n (e.options ?? []).map((o, i) => `${indent} ${i + 1}. ${describeEffectInline(o)}`).join(\"\\n\")\n );\n }\n case \"dice-gated\": {\n const comp = formatComparison(e.comparison ?? \"gte\", e.threshold);\n const success = e.on_success ? describeEffectInline(e.on_success) : \"nothing\";\n const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : \"\";\n return `${indent}${arrow}Roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;\n }\n case \"dice-pool-allocation\": {\n const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : \"?\";\n const lines = [`${indent}${arrow}Roll ${pool} (max ${jstr(e.max_activations)} activations):`];\n for (const opt of e.options ?? []) {\n lines.push(\n `${indent} - ${jstr(opt.name)}: need ${jstr(opt.requirement?.type)} of ${jstr(opt.requirement?.min_value)}+ -> ${describeEffectInline(opt.effect ?? {})}`\n );\n }\n return lines.join(\"\\n\");\n }\n default:\n return `${indent}${arrow}${describeEffectInline(e)}`;\n }\n}\n\n/** `Scope: aura (6\"). Duration: phase.` — empty string when absent. */\nexport function describeScope(s?: AbilityScope): string {\n if (!s || (!s.range && !s.duration)) return \"\";\n const range = dekebab(s.range ?? \"\");\n const inches = s.range_inches != null ? ` (${jstr(s.range_inches)}\")` : \"\";\n const duration = dekebab(s.duration ?? \"\");\n return `Scope: ${range}${inches}. Duration: ${duration}.`;\n}\n\n/**\n * `Applies to: units with Possessed.` — the roster-highlighting audience named\n * by a curated `applies_to` filter. Empty string when the filter is absent or\n * carries no keywords (nothing to say). `required_keywords` reads as an AND set;\n * `excluded_keywords` render as a trailing `(excluding …)`.\n */\nexport function describeAppliesTo(a?: AbilityAppliesTo | null): string {\n if (!a) return \"\";\n const required = a.required_keywords ?? [];\n const excluded = a.excluded_keywords ?? [];\n if (required.length === 0 && excluded.length === 0) return \"\";\n const base = required.length ? `units with ${required.join(\", \")}` : \"all units\";\n const exc = excluded.length ? ` (excluding ${excluded.join(\", \")})` : \"\";\n return `Applies to: ${base}${exc}.`;\n}\n\n/**\n * Full generated text for an ability: the effect tree, a trailing scope line,\n * and a trailing `Applies to:` line when the ability carries a curated\n * `applies_to` filter. This is the `ability.print()` consumers render when the\n * dataset carries no rules prose.\n */\nexport function describeAbility(a: AbilityLike): string {\n const effect = a.effect ? describeEffect(a.effect) : \"\";\n const scope = describeScope(a.scope);\n const applies = describeAppliesTo(a.applies_to);\n return [effect, scope, applies].filter(Boolean).join(\"\\n\");\n}\n"]}
@@ -8,5 +8,5 @@
8
8
  */
9
9
  export { describeCondition, dekebab, type Condition } from "./condition.js";
10
10
  export { describeTrigger, describeAward, describeScoringCard, type ScoringTrigger, type ScoringAward, type ScoringMode, } from "./scoring.js";
11
- export { describeEffect, describeEffectInline, describeAbility, describeScope, type Effect, type AbilityScope, type AbilityLike, } from "./effect.js";
11
+ export { describeEffect, describeEffectInline, describeAbility, describeAppliesTo, describeScope, type Effect, type AbilityScope, type AbilityAppliesTo, type AbilityLike, } from "./effect.js";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC"}
@@ -8,5 +8,5 @@
8
8
  */
9
9
  export { describeCondition, dekebab } from "./condition.js";
10
10
  export { describeTrigger, describeAward, describeScoringCard, } from "./scoring.js";
11
- export { describeEffect, describeEffectInline, describeAbility, describeScope, } from "./effect.js";
11
+ export { describeEffect, describeEffectInline, describeAbility, describeAppliesTo, describeScope, } from "./effect.js";
12
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,GAIpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,aAAa,GAId,MAAM,aAAa,CAAC","sourcesContent":["/**\n * Plain-English translation of structured game data — the `secondary-card`\n * scoring `awards` (mission \"how to play\" readouts), the shared Ability-DSL\n * condition humanizer, and the Ability-DSL effect describer (\"ability.print()\").\n * Output is ASCII-only and pinned across language ports by the\n * `conformance/scoring-translation` and `conformance/effect-translation`\n * corpora.\n */\nexport { describeCondition, dekebab, type Condition } from \"./condition.js\";\nexport {\n describeTrigger,\n describeAward,\n describeScoringCard,\n type ScoringTrigger,\n type ScoringAward,\n type ScoringMode,\n} from \"./scoring.js\";\nexport {\n describeEffect,\n describeEffectInline,\n describeAbility,\n describeScope,\n type Effect,\n type AbilityScope,\n type AbilityLike,\n} from \"./effect.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,GAIpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,aAAa,GAKd,MAAM,aAAa,CAAC","sourcesContent":["/**\n * Plain-English translation of structured game data — the `secondary-card`\n * scoring `awards` (mission \"how to play\" readouts), the shared Ability-DSL\n * condition humanizer, and the Ability-DSL effect describer (\"ability.print()\").\n * Output is ASCII-only and pinned across language ports by the\n * `conformance/scoring-translation` and `conformance/effect-translation`\n * corpora.\n */\nexport { describeCondition, dekebab, type Condition } from \"./condition.js\";\nexport {\n describeTrigger,\n describeAward,\n describeScoringCard,\n type ScoringTrigger,\n type ScoringAward,\n type ScoringMode,\n} from \"./scoring.js\";\nexport {\n describeEffect,\n describeEffectInline,\n describeAbility,\n describeAppliesTo,\n describeScope,\n type Effect,\n type AbilityScope,\n type AbilityAppliesTo,\n type AbilityLike,\n} from \"./effect.js\";\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAgDD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CAkF3B"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAkDD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CAkF3B"}
package/dist/validate.js CHANGED
@@ -10,6 +10,7 @@ const DATA_ROOT = resolve(__dirname, "../../data");
10
10
  const SCHEMA_MAP = {
11
11
  factions: "https://40kdc.dev/schemas/core/faction.schema.json",
12
12
  units: "https://40kdc.dev/schemas/core/unit.schema.json",
13
+ "target-profiles": "https://40kdc.dev/schemas/core/target-profile.schema.json",
13
14
  weapons: "https://40kdc.dev/schemas/core/weapon.schema.json",
14
15
  "weapon-keywords": "https://40kdc.dev/schemas/core/weapon-keyword.schema.json",
15
16
  "game-versions": "https://40kdc.dev/schemas/core/game-version.schema.json",
@@ -27,6 +28,7 @@ const SCHEMA_MAP = {
27
28
  "secondary-cards": "https://40kdc.dev/schemas/core/secondary-card.schema.json",
28
29
  "terrain-templates": "https://40kdc.dev/schemas/core/terrain-template.schema.json",
29
30
  "terrain-layouts": "https://40kdc.dev/schemas/core/terrain-layout.schema.json",
31
+ "hull-shapes": "https://40kdc.dev/schemas/core/hull-shape.schema.json",
30
32
  "phase-mappings": "https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json",
31
33
  "timing-flags": "https://40kdc.dev/schemas/enrichment/timing-flag.schema.json",
32
34
  "interaction-flags": "https://40kdc.dev/schemas/enrichment/interaction-flag.schema.json",
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type Ajv from \"ajv\";\nimport { readFileSync } from \"node:fs\";\nimport { glob } from \"glob\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\n\nexport interface ValidationError {\n file: string;\n index: number;\n errors: Array<{ path: string; message: string }>;\n}\n\nexport interface ValidationResult {\n totalFiles: number;\n totalItems: number;\n passed: number;\n failed: number;\n errors: ValidationError[];\n}\n\n/**\n * Map from data file base-name prefix to schema $id.\n */\nconst SCHEMA_MAP: Record<string, string> = {\n factions: \"https://40kdc.dev/schemas/core/faction.schema.json\",\n units: \"https://40kdc.dev/schemas/core/unit.schema.json\",\n weapons: \"https://40kdc.dev/schemas/core/weapon.schema.json\",\n \"weapon-keywords\": \"https://40kdc.dev/schemas/core/weapon-keyword.schema.json\",\n \"game-versions\": \"https://40kdc.dev/schemas/core/game-version.schema.json\",\n detachments: \"https://40kdc.dev/schemas/core/detachment.schema.json\",\n enhancements: \"https://40kdc.dev/schemas/core/enhancement.schema.json\",\n stratagems: \"https://40kdc.dev/schemas/core/stratagem.schema.json\",\n \"wargear-options\": \"https://40kdc.dev/schemas/core/wargear-option.schema.json\",\n wargear: \"https://40kdc.dev/schemas/core/wargear.schema.json\",\n \"leader-attachments\": \"https://40kdc.dev/schemas/core/leader-attachment.schema.json\",\n \"unit-compositions\": \"https://40kdc.dev/schemas/core/unit-composition.schema.json\",\n \"force-dispositions\": \"https://40kdc.dev/schemas/core/force-disposition.schema.json\",\n \"deployment-patterns\": \"https://40kdc.dev/schemas/core/deployment-pattern.schema.json\",\n \"mission-matchups\": \"https://40kdc.dev/schemas/core/mission-matchup.schema.json\",\n missions: \"https://40kdc.dev/schemas/core/mission.schema.json\",\n \"secondary-cards\": \"https://40kdc.dev/schemas/core/secondary-card.schema.json\",\n \"terrain-templates\": \"https://40kdc.dev/schemas/core/terrain-template.schema.json\",\n \"terrain-layouts\": \"https://40kdc.dev/schemas/core/terrain-layout.schema.json\",\n \"phase-mappings\": \"https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json\",\n \"timing-flags\": \"https://40kdc.dev/schemas/enrichment/timing-flag.schema.json\",\n \"interaction-flags\": \"https://40kdc.dev/schemas/enrichment/interaction-flag.schema.json\",\n abilities: \"https://40kdc.dev/schemas/enrichment/ability-dsl/ability.schema.json\",\n \"resource-pools\": \"https://40kdc.dev/schemas/enrichment/resource-pool.schema.json\",\n};\n\n/**\n * Determine which schema $id to use for a given data file path.\n * Convention: the file's base name starts with a SCHEMA_MAP prefix (real data is\n * `<prefix>.json`; test fixtures are `<prefix>-good.json` / `<prefix>-bad.json`).\n * Prefixes are tried longest-first so `wargear-options.json` resolves to the\n * wargear-option schema rather than the shorter `wargear` key (distinct entities).\n */\nconst SCHEMA_PREFIXES = Object.keys(SCHEMA_MAP).sort((a, b) => b.length - a.length);\nfunction resolveSchemaId(filePath: string): string | null {\n const base = basename(filePath);\n for (const prefix of SCHEMA_PREFIXES) {\n if (base.startsWith(prefix)) return SCHEMA_MAP[prefix];\n }\n return null;\n}\n\n/**\n * Validate all data files matching the given glob pattern.\n * Each data file is expected to be a JSON array; each element is validated individually.\n */\nexport async function validateFiles(\n ajv: Ajv,\n pattern: string,\n cwd?: string,\n): Promise<ValidationResult> {\n const root = cwd ?? DATA_ROOT;\n const files = await glob(pattern, { cwd: root, absolute: true });\n\n const result: ValidationResult = {\n totalFiles: files.length,\n totalItems: 0,\n passed: 0,\n failed: 0,\n errors: [],\n };\n\n for (const file of files) {\n // Underscore-prefixed files are scratch/reports (e.g. the converter's\n // `_wargear-options.unparsed.json`), not dataset entities — skip them.\n if (basename(file).startsWith(\"_\")) continue;\n const schemaId = resolveSchemaId(file);\n if (!schemaId) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `No schema mapping found for file: ${basename(file)}` }],\n });\n result.failed++;\n continue;\n }\n\n const validate = ajv.getSchema(schemaId);\n if (!validate) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Schema not found: ${schemaId}` }],\n });\n result.failed++;\n continue;\n }\n\n let data: unknown;\n try {\n const raw = readFileSync(file, \"utf-8\");\n data = JSON.parse(raw);\n } catch (err) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Failed to parse JSON: ${(err as Error).message}` }],\n });\n result.failed++;\n continue;\n }\n\n if (!Array.isArray(data)) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: \"Data file must be a JSON array\" }],\n });\n result.failed++;\n continue;\n }\n\n for (let i = 0; i < data.length; i++) {\n result.totalItems++;\n const valid = validate(data[i]);\n if (valid) {\n result.passed++;\n } else {\n result.failed++;\n result.errors.push({\n file,\n index: i,\n errors: (validate.errors ?? []).map((e) => ({\n path: e.instancePath || \"/\",\n message: e.message ?? \"Unknown validation error\",\n })),\n });\n }\n }\n }\n\n return result;\n}\n"]}
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type Ajv from \"ajv\";\nimport { readFileSync } from \"node:fs\";\nimport { glob } from \"glob\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\n\nexport interface ValidationError {\n file: string;\n index: number;\n errors: Array<{ path: string; message: string }>;\n}\n\nexport interface ValidationResult {\n totalFiles: number;\n totalItems: number;\n passed: number;\n failed: number;\n errors: ValidationError[];\n}\n\n/**\n * Map from data file base-name prefix to schema $id.\n */\nconst SCHEMA_MAP: Record<string, string> = {\n factions: \"https://40kdc.dev/schemas/core/faction.schema.json\",\n units: \"https://40kdc.dev/schemas/core/unit.schema.json\",\n \"target-profiles\": \"https://40kdc.dev/schemas/core/target-profile.schema.json\",\n weapons: \"https://40kdc.dev/schemas/core/weapon.schema.json\",\n \"weapon-keywords\": \"https://40kdc.dev/schemas/core/weapon-keyword.schema.json\",\n \"game-versions\": \"https://40kdc.dev/schemas/core/game-version.schema.json\",\n detachments: \"https://40kdc.dev/schemas/core/detachment.schema.json\",\n enhancements: \"https://40kdc.dev/schemas/core/enhancement.schema.json\",\n stratagems: \"https://40kdc.dev/schemas/core/stratagem.schema.json\",\n \"wargear-options\": \"https://40kdc.dev/schemas/core/wargear-option.schema.json\",\n wargear: \"https://40kdc.dev/schemas/core/wargear.schema.json\",\n \"leader-attachments\": \"https://40kdc.dev/schemas/core/leader-attachment.schema.json\",\n \"unit-compositions\": \"https://40kdc.dev/schemas/core/unit-composition.schema.json\",\n \"force-dispositions\": \"https://40kdc.dev/schemas/core/force-disposition.schema.json\",\n \"deployment-patterns\": \"https://40kdc.dev/schemas/core/deployment-pattern.schema.json\",\n \"mission-matchups\": \"https://40kdc.dev/schemas/core/mission-matchup.schema.json\",\n missions: \"https://40kdc.dev/schemas/core/mission.schema.json\",\n \"secondary-cards\": \"https://40kdc.dev/schemas/core/secondary-card.schema.json\",\n \"terrain-templates\": \"https://40kdc.dev/schemas/core/terrain-template.schema.json\",\n \"terrain-layouts\": \"https://40kdc.dev/schemas/core/terrain-layout.schema.json\",\n \"hull-shapes\": \"https://40kdc.dev/schemas/core/hull-shape.schema.json\",\n \"phase-mappings\": \"https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json\",\n \"timing-flags\": \"https://40kdc.dev/schemas/enrichment/timing-flag.schema.json\",\n \"interaction-flags\": \"https://40kdc.dev/schemas/enrichment/interaction-flag.schema.json\",\n abilities: \"https://40kdc.dev/schemas/enrichment/ability-dsl/ability.schema.json\",\n \"resource-pools\": \"https://40kdc.dev/schemas/enrichment/resource-pool.schema.json\",\n};\n\n/**\n * Determine which schema $id to use for a given data file path.\n * Convention: the file's base name starts with a SCHEMA_MAP prefix (real data is\n * `<prefix>.json`; test fixtures are `<prefix>-good.json` / `<prefix>-bad.json`).\n * Prefixes are tried longest-first so `wargear-options.json` resolves to the\n * wargear-option schema rather than the shorter `wargear` key (distinct entities).\n */\nconst SCHEMA_PREFIXES = Object.keys(SCHEMA_MAP).sort((a, b) => b.length - a.length);\nfunction resolveSchemaId(filePath: string): string | null {\n const base = basename(filePath);\n for (const prefix of SCHEMA_PREFIXES) {\n if (base.startsWith(prefix)) return SCHEMA_MAP[prefix];\n }\n return null;\n}\n\n/**\n * Validate all data files matching the given glob pattern.\n * Each data file is expected to be a JSON array; each element is validated individually.\n */\nexport async function validateFiles(\n ajv: Ajv,\n pattern: string,\n cwd?: string,\n): Promise<ValidationResult> {\n const root = cwd ?? DATA_ROOT;\n const files = await glob(pattern, { cwd: root, absolute: true });\n\n const result: ValidationResult = {\n totalFiles: files.length,\n totalItems: 0,\n passed: 0,\n failed: 0,\n errors: [],\n };\n\n for (const file of files) {\n // Underscore-prefixed files are scratch/reports (e.g. the converter's\n // `_wargear-options.unparsed.json`), not dataset entities — skip them.\n if (basename(file).startsWith(\"_\")) continue;\n const schemaId = resolveSchemaId(file);\n if (!schemaId) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `No schema mapping found for file: ${basename(file)}` }],\n });\n result.failed++;\n continue;\n }\n\n const validate = ajv.getSchema(schemaId);\n if (!validate) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Schema not found: ${schemaId}` }],\n });\n result.failed++;\n continue;\n }\n\n let data: unknown;\n try {\n const raw = readFileSync(file, \"utf-8\");\n data = JSON.parse(raw);\n } catch (err) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Failed to parse JSON: ${(err as Error).message}` }],\n });\n result.failed++;\n continue;\n }\n\n if (!Array.isArray(data)) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: \"Data file must be a JSON array\" }],\n });\n result.failed++;\n continue;\n }\n\n for (let i = 0; i < data.length; i++) {\n result.totalItems++;\n const valid = validate(data[i]);\n if (valid) {\n result.passed++;\n } else {\n result.failed++;\n result.errors.push({\n file,\n index: i,\n errors: (validate.errors ?? []).map((e) => ({\n path: e.instancePath || \"/\",\n message: e.message ?? \"Unknown validation error\",\n })),\n });\n }\n }\n }\n\n return result;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpaca-software/40kdc-data",
3
- "version": "0.5.5",
3
+ "version": "0.5.12",
4
4
  "type": "module",
5
5
  "description": "The 40kdc Warhammer 40K dataset behind a linked, typed API — find units, follow them to their weapons, abilities, phases, and factions. Also validates data against the canonical JSON Schemas.",
6
6
  "keywords": [
@@ -64,6 +64,7 @@
64
64
  "translate": "tsx src/cli.ts translate",
65
65
  "audit:coverage": "tsx src/cli.ts audit-coverage --write",
66
66
  "scrub:defensive-flag": "tsx src/scrub-defensive-flag.ts",
67
+ "extract:faction-pack": "tsx src/extract-faction-pack.ts",
67
68
  "author:input": "tsx src/author-input.ts",
68
69
  "author:seed": "tsx src/author-seed.ts",
69
70
  "author:propose": "tsx src/author-batch.ts propose",
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/hull-shape.schema.json",
4
+ "title": "Hull Shape",
5
+ "description": "A model's 2D collision footprint as an explicit polygon, used in place of a circular/oval base for vehicles and other hull-based models. Points are authored in local inches (y-down); a consumer re-centers the polygon on its area centroid before placement, so the local origin does not affect where the model lands — only its shape matters (mirrors the terrain-template footprint convention). A hull shape is faction-agnostic and reusable: one outline (e.g. a Rhino chassis) is authored once and referenced by `hull_shape_id` from every model that shares that hull, across factions. This entity stores geometry only — never an image, image URL, or any source-asset metadata.",
6
+ "type": "object",
7
+ "properties": {
8
+ "id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
9
+ "name": { "type": "string", "minLength": 1, "maxLength": 128 },
10
+ "points": {
11
+ "type": "array",
12
+ "description": "Polygon vertices in local inches (y-down), in order around the outline. A hull is always a polygon (never a rectangle/right-triangle), so the points are given directly rather than via the shared `footprint` oneOf.",
13
+ "items": { "$ref": "../defs/common.schema.json#/$defs/vec2" },
14
+ "minItems": 3
15
+ },
16
+ "bounds_width_in": {
17
+ "type": "number",
18
+ "exclusiveMinimum": 0,
19
+ "description": "Cached axis-aligned bounding-box width in inches (max x − min x). Derived from `points`; recorded so consumers can size/scale without recomputing."
20
+ },
21
+ "bounds_height_in": {
22
+ "type": "number",
23
+ "exclusiveMinimum": 0,
24
+ "description": "Cached axis-aligned bounding-box height in inches (max y − min y). Derived from `points`."
25
+ },
26
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
27
+ },
28
+ "required": ["id", "name", "points", "bounds_width_in", "bounds_height_in", "game_version"],
29
+ "additionalProperties": false
30
+ }
@@ -20,7 +20,8 @@
20
20
  "newrecruit-wtc-full",
21
21
  "newrecruit-simple",
22
22
  "rosterizer",
23
- "gw"
23
+ "gw",
24
+ "roster-json"
24
25
  ]
25
26
  },
26
27
  "generated_by": {
@@ -38,12 +39,10 @@
38
39
  ],
39
40
  "description": "Resolved primary faction; null when the source faction could not be matched."
40
41
  },
41
- "detachment_id": {
42
- "oneOf": [
43
- { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
44
- { "type": "null" }
45
- ],
46
- "description": "Resolved detachment; null when absent or unmatched."
42
+ "detachments": {
43
+ "type": "array",
44
+ "description": "Resolved detachments. 11e lists may field several detachments under a detachment-point cap; pre-11e lists carry zero or one. Order is load-bearing (source/parse order). Empty when the source declared no detachment.",
45
+ "items": { "$ref": "#/$defs/roster-detachment" }
47
46
  },
48
47
  "battle_size": {
49
48
  "oneOf": [
@@ -60,6 +59,10 @@
60
59
  "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }],
61
60
  "description": "Points ceiling parsed from the battle size (e.g. 2000), if known."
62
61
  },
62
+ "detachment_cap": {
63
+ "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }],
64
+ "description": "11e detachment-point budget derived from the battle size (strike-force 3, incursion 2); null when the battle size is unknown. The sum of each detachment's `dp_cost` is meant to fit within this cap."
65
+ },
63
66
  "total_reported": {
64
67
  "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }],
65
68
  "description": "Total points as reported by the source roster's cost block."
@@ -70,7 +73,7 @@
70
73
  "description": "Sum of this roster's per-unit point values."
71
74
  }
72
75
  },
73
- "required": ["declared_limit", "total_reported", "total_computed"],
76
+ "required": ["declared_limit", "detachment_cap", "total_reported", "total_computed"],
74
77
  "additionalProperties": false
75
78
  },
76
79
  "units": {
@@ -80,7 +83,7 @@
80
83
  "game_version": { "$ref": "../defs/game-version-ref.schema.json" },
81
84
  "diagnostics": { "$ref": "#/$defs/roster-diagnostics" }
82
85
  },
83
- "required": ["name", "source", "faction_id", "detachment_id", "battle_size", "points", "units", "game_version", "diagnostics"],
86
+ "required": ["name", "source", "faction_id", "detachments", "battle_size", "points", "units", "game_version", "diagnostics"],
84
87
  "additionalProperties": false,
85
88
  "$defs": {
86
89
  "roster-resolved-ref": {
@@ -121,6 +124,19 @@
121
124
  "required": ["id", "raw_name", "resolved", "candidates"],
122
125
  "additionalProperties": false
123
126
  },
127
+ "roster-detachment": {
128
+ "type": "object",
129
+ "description": "One detachment selected on the roster, paired with its resolved detachment-point cost.",
130
+ "properties": {
131
+ "ref": { "$ref": "#/$defs/roster-resolved-ref" },
132
+ "dp_cost": {
133
+ "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }],
134
+ "description": "Detachment-point cost (1–3) looked up from the resolved detachment entity's `detachment_points`. Null when the detachment is unresolved or the entity records no cost."
135
+ }
136
+ },
137
+ "required": ["ref", "dp_cost"],
138
+ "additionalProperties": false
139
+ },
124
140
  "roster-unit": {
125
141
  "type": "object",
126
142
  "properties": {
@@ -192,6 +208,7 @@
192
208
  "weapon-unresolved",
193
209
  "enhancement-unresolved",
194
210
  "detachment-unresolved",
211
+ "detachment-points-exceeded",
195
212
  "battle-size-unmapped",
196
213
  "points-mismatch",
197
214
  "leader-attachment-inferred",
@@ -15,7 +15,7 @@
15
15
  "type": {
16
16
  "type": "string",
17
17
  "enum": ["battle-tactic", "strategic-ploy", "epic-deed", "wargear"],
18
- "description": "GW-printed stratagem category from the card"
18
+ "description": "GW-printed stratagem category from the card. Optional: 11e faction packs omit it for newly introduced detachments, and the category has no in-game effect; absent when the source does not state one."
19
19
  },
20
20
  "detachment_id": {
21
21
  "oneOf": [
@@ -53,6 +53,6 @@
53
53
  },
54
54
  "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
55
55
  },
56
- "required": ["id", "name", "category", "type", "cp_cost", "phases", "player_turn", "timing", "game_version"],
56
+ "required": ["id", "name", "category", "cp_cost", "phases", "player_turn", "timing", "game_version"],
57
57
  "additionalProperties": false
58
58
  }
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/target-profile.schema.json",
4
+ "title": "TargetProfile",
5
+ "description": "A named target archetype for damage comparison. References a real dataset unit (faction_id + unit_id) rather than copying its stat line, so the profile stays in sync with dataset updates. Stats, keywords, and defensive abilities are resolved from the referenced unit at use time.",
6
+ "type": "object",
7
+ "properties": {
8
+ "id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
9
+ "name": { "type": "string", "minLength": 1, "maxLength": 128 },
10
+ "description": { "type": "string" },
11
+ "faction_id": {
12
+ "$ref": "../defs/common.schema.json#/$defs/entity-id",
13
+ "description": "Scopes the unit lookup. Required because shared units (Rhino, Forgefiend, Predator) reuse one unit id across factions; resolution must be faction-scoped."
14
+ },
15
+ "unit_id": {
16
+ "$ref": "../defs/common.schema.json#/$defs/entity-id",
17
+ "description": "The dataset unit this archetype stands in for, resolved within faction_id. Its profile[0] supplies T/Sv/invuln/W; its keywords and defensive abilities are read at use time."
18
+ },
19
+ "model_count_override": {
20
+ "oneOf": [
21
+ { "type": "integer", "minimum": 1 },
22
+ { "type": "null" }
23
+ ],
24
+ "description": "Optional non-default squad size for the comparison. When null/absent, the referenced unit's model_count.min is used."
25
+ },
26
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
27
+ },
28
+ "required": ["id", "name", "faction_id", "unit_id", "game_version"],
29
+ "additionalProperties": false
30
+ }
@@ -28,6 +28,13 @@
28
28
  "base_size_mm": {
29
29
  "description": "This model's base. Absent when no base could be resolved for the model.",
30
30
  "$ref": "../defs/common.schema.json#/$defs/base-size"
31
+ },
32
+ "hull_shape_id": {
33
+ "description": "Optional reference to a hull-shape entity giving this model's 2D collision polygon, used instead of the circular/oval base footprint. By convention a model carrying this should set `base_size_mm.shape` to \"hull\".",
34
+ "oneOf": [
35
+ { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
36
+ { "type": "null" }
37
+ ]
31
38
  }
32
39
  },
33
40
  "required": ["name", "min", "max"],
@@ -39,6 +39,20 @@
39
39
  },
40
40
  "effect": { "$ref": "effect.schema.json" },
41
41
  "scope": { "$ref": "scope.schema.json" },
42
+ "applies_to": {
43
+ "description": "Static, human-curated keyword filter naming which datasheet units this ability benefits, for roster-side highlighting. A unit matches when it carries every keyword in `required_keywords` (across its `keywords` + `faction_keywords`) and none in `excluded_keywords`. This is a denormalized projection distinct from the runtime `effect` condition tree (which mixes static class, runtime-granted markers, and timing gates and must not be scraped for scope). Absent/null means no resolvable unit scope — consumers render no highlight rather than guess.",
44
+ "oneOf": [
45
+ {
46
+ "type": "object",
47
+ "properties": {
48
+ "required_keywords": { "$ref": "../../defs/common.schema.json#/$defs/keyword-list" },
49
+ "excluded_keywords": { "$ref": "../../defs/common.schema.json#/$defs/keyword-list" }
50
+ },
51
+ "additionalProperties": false
52
+ },
53
+ { "type": "null" }
54
+ ]
55
+ },
42
56
  "interactions": {
43
57
  "type": "array",
44
58
  "items": {