@alpaca-software/40kdc-data 0.5.11 → 0.5.14

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 (131) hide show
  1. package/README.md +10 -0
  2. package/dist/author-input-pack.d.ts +27 -0
  3. package/dist/author-input-pack.d.ts.map +1 -0
  4. package/dist/author-input-pack.js +198 -0
  5. package/dist/author-input-pack.js.map +1 -0
  6. package/dist/codegen-data.js +1 -0
  7. package/dist/codegen-data.js.map +1 -1
  8. package/dist/commands/import.d.ts.map +1 -1
  9. package/dist/commands/import.js +3 -1
  10. package/dist/commands/import.js.map +1 -1
  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/data/bundle.generated.js +1 -1
  15. package/dist/data/bundle.generated.js.map +1 -1
  16. package/dist/data/dataset.d.ts +2 -1
  17. package/dist/data/dataset.d.ts.map +1 -1
  18. package/dist/data/dataset.js +2 -0
  19. package/dist/data/dataset.js.map +1 -1
  20. package/dist/data/entities.d.ts +22 -0
  21. package/dist/data/entities.d.ts.map +1 -1
  22. package/dist/data/entities.js +28 -0
  23. package/dist/data/entities.js.map +1 -1
  24. package/dist/data/index.d.ts +3 -1
  25. package/dist/data/index.d.ts.map +1 -1
  26. package/dist/data/index.js +3 -1
  27. package/dist/data/index.js.map +1 -1
  28. package/dist/data/roster-resolve.d.ts +16 -1
  29. package/dist/data/roster-resolve.d.ts.map +1 -1
  30. package/dist/data/roster-resolve.js +19 -0
  31. package/dist/data/roster-resolve.js.map +1 -1
  32. package/dist/data/types.d.ts +3 -1
  33. package/dist/data/types.d.ts.map +1 -1
  34. package/dist/data/types.js +1 -0
  35. package/dist/data/types.js.map +1 -1
  36. package/dist/export/newrecruit-json.d.ts.map +1 -1
  37. package/dist/export/newrecruit-json.js +4 -3
  38. package/dist/export/newrecruit-json.js.map +1 -1
  39. package/dist/export/newrecruit-simple.js +2 -2
  40. package/dist/export/newrecruit-simple.js.map +1 -1
  41. package/dist/export/newrecruit-wtc.d.ts.map +1 -1
  42. package/dist/export/newrecruit-wtc.js +3 -1
  43. package/dist/export/newrecruit-wtc.js.map +1 -1
  44. package/dist/export/rosterizer.d.ts.map +1 -1
  45. package/dist/export/rosterizer.js +6 -7
  46. package/dist/export/rosterizer.js.map +1 -1
  47. package/dist/extract-faction-pack.d.ts +61 -0
  48. package/dist/extract-faction-pack.d.ts.map +1 -0
  49. package/dist/extract-faction-pack.js +318 -0
  50. package/dist/extract-faction-pack.js.map +1 -0
  51. package/dist/gen-conformance.js +37 -4
  52. package/dist/gen-conformance.js.map +1 -1
  53. package/dist/generated.d.ts +46 -2
  54. package/dist/generated.d.ts.map +1 -1
  55. package/dist/generated.js.map +1 -1
  56. package/dist/import/gw-headerless.d.ts +70 -0
  57. package/dist/import/gw-headerless.d.ts.map +1 -0
  58. package/dist/import/gw-headerless.js +343 -0
  59. package/dist/import/gw-headerless.js.map +1 -0
  60. package/dist/import/gw.js +1 -1
  61. package/dist/import/gw.js.map +1 -1
  62. package/dist/import/import-roster.d.ts.map +1 -1
  63. package/dist/import/import-roster.js +8 -0
  64. package/dist/import/import-roster.js.map +1 -1
  65. package/dist/import/listforge-text.js +1 -1
  66. package/dist/import/listforge-text.js.map +1 -1
  67. package/dist/import/listforge.d.ts.map +1 -1
  68. package/dist/import/listforge.js +18 -3
  69. package/dist/import/listforge.js.map +1 -1
  70. package/dist/import/newrecruit-json.d.ts.map +1 -1
  71. package/dist/import/newrecruit-json.js +21 -3
  72. package/dist/import/newrecruit-json.js.map +1 -1
  73. package/dist/import/newrecruit-simple.js +1 -1
  74. package/dist/import/newrecruit-simple.js.map +1 -1
  75. package/dist/import/newrecruit-wtc.js +1 -1
  76. package/dist/import/newrecruit-wtc.js.map +1 -1
  77. package/dist/import/resolve.d.ts.map +1 -1
  78. package/dist/import/resolve.js +46 -18
  79. package/dist/import/resolve.js.map +1 -1
  80. package/dist/import/roster-json.d.ts +5 -3
  81. package/dist/import/roster-json.d.ts.map +1 -1
  82. package/dist/import/roster-json.js +8 -4
  83. package/dist/import/roster-json.js.map +1 -1
  84. package/dist/import/rosterizer.js +3 -3
  85. package/dist/import/rosterizer.js.map +1 -1
  86. package/dist/import/types.d.ts +15 -4
  87. package/dist/import/types.d.ts.map +1 -1
  88. package/dist/import/types.js.map +1 -1
  89. package/dist/index.d.ts +1 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +4 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/merge-faction-pack.d.ts +2 -0
  94. package/dist/merge-faction-pack.d.ts.map +1 -0
  95. package/dist/merge-faction-pack.js +157 -0
  96. package/dist/merge-faction-pack.js.map +1 -0
  97. package/dist/pack-blocks.d.ts +32 -0
  98. package/dist/pack-blocks.d.ts.map +1 -0
  99. package/dist/pack-blocks.js +99 -0
  100. package/dist/pack-blocks.js.map +1 -0
  101. package/dist/rube-goldberg.js +1 -1
  102. package/dist/rube-goldberg.js.map +1 -1
  103. package/dist/runner.d.ts.map +1 -1
  104. package/dist/runner.js +33 -1
  105. package/dist/runner.js.map +1 -1
  106. package/dist/scope.d.ts +31 -0
  107. package/dist/scope.d.ts.map +1 -0
  108. package/dist/scope.js +34 -0
  109. package/dist/scope.js.map +1 -0
  110. package/dist/seed-detachment-abilities.d.ts +2 -0
  111. package/dist/seed-detachment-abilities.d.ts.map +1 -0
  112. package/dist/seed-detachment-abilities.js +137 -0
  113. package/dist/seed-detachment-abilities.js.map +1 -0
  114. package/dist/translate/effect.d.ts +17 -3
  115. package/dist/translate/effect.d.ts.map +1 -1
  116. package/dist/translate/effect.js +23 -4
  117. package/dist/translate/effect.js.map +1 -1
  118. package/dist/translate/index.d.ts +1 -1
  119. package/dist/translate/index.d.ts.map +1 -1
  120. package/dist/translate/index.js +1 -1
  121. package/dist/translate/index.js.map +1 -1
  122. package/dist/validate.d.ts.map +1 -1
  123. package/dist/validate.js +1 -0
  124. package/dist/validate.js.map +1 -1
  125. package/package.json +3 -1
  126. package/schemas/core/detachment.schema.json +6 -0
  127. package/schemas/core/hull-shape.schema.json +30 -0
  128. package/schemas/core/roster.schema.json +24 -8
  129. package/schemas/core/stratagem.schema.json +2 -2
  130. package/schemas/core/unit-composition.schema.json +7 -0
  131. package/schemas/enrichment/ability-dsl/ability.schema.json +14 -0
package/README.md CHANGED
@@ -61,6 +61,16 @@ as `normalizeName` so you can reproduce it in your own search UI.
61
61
 
62
62
  Auto-generated from the source: [`docs/api/`](docs/api/README.md).
63
63
 
64
+ ## See it in action
65
+
66
+ Open-source example apps built on this package:
67
+
68
+ - [List Builder](https://list-builder.alpacasoft.dev) — build an army list and export to ListForge / NewRecruit.
69
+ - [Salvo](https://salvo.alpacasoft.dev) — damage calculator over the cruncher and abilities-resolver.
70
+ - [Mission Matrix](https://mission-matrix.alpacasoft.dev) — the 11th-edition Force Disposition matchup grid.
71
+ - [Layout Editor](https://layout-editor.alpacasoft.dev) — author terrain layouts on a board; exports terrain-layout JSON.
72
+ - [Hull Tracer](https://hull-tracer.alpacasoft.dev) — trace a model's collision hull from a top-down photo; exports geometry-only hull-shape JSON.
73
+
64
74
  ## Also: schema validation
65
75
 
66
76
  This package also ships the canonical JSON Schemas and an AJV-based validator
@@ -0,0 +1,27 @@
1
+ import { type Block } from "./pack-blocks.js";
2
+ import type { AuthorInputEntry } from "./author-input.js";
3
+ /** The start of one card within a detachment: its id, name, column, and top y. */
4
+ export interface CardAnchor {
5
+ id: string;
6
+ name: string;
7
+ kind: "detachment" | "stratagem" | "enhancement";
8
+ x: number;
9
+ topGy: number;
10
+ }
11
+ /**
12
+ * Locate every card-start anchor in a detachment's blocks: the detachment rule
13
+ * (under a DETACHMENT RULE(S) header), each stratagem (its name block above a
14
+ * "<NAME> – <TYPE> STRATAGEM" label), and each enhancement (a caps header with a
15
+ * prose body below). Anchors bound each other's bodies by same-column y order.
16
+ */
17
+ export declare function findAnchors(blocks: Block[]): CardAnchor[];
18
+ /**
19
+ * Capture a card's rule body: every same-column block strictly below the anchor's
20
+ * name/header, up to the next same-column anchor (the next card's start). Joined
21
+ * in reading order. Column-scoping is what keeps the adjacent column's text out —
22
+ * the whole reason bbox beats `-layout` on these 2-column pages.
23
+ */
24
+ export declare function captureBody(blocks: Block[], anchor: CardAnchor, anchors: CardAnchor[]): string;
25
+ /** Build author-input entries for one faction's new-detachment stubs from its pack. */
26
+ export declare function buildFactionFromPack(faction: string, pdf: string): AuthorInputEntry[];
27
+ //# sourceMappingURL=author-input-pack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"author-input-pack.d.ts","sourceRoot":"","sources":["../src/author-input-pack.ts"],"names":[],"mappings":"AA8BA,OAAO,EACL,KAAK,KAAK,EAOX,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAW1D,kFAAkF;AAClF,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,GAAG,WAAW,GAAG,aAAa,CAAC;IACjD,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACf;AAkBD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,UAAU,EAAE,CAgDzD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAa9F;AAID,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAmDrF"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Build the DSL-authoring input for *new 11e detachments* from a Faction Pack PDF.
3
+ *
4
+ * The stock `author-input.ts` resolves each empty-modifier ability stub to its
5
+ * source rule in the **10e army-assist archive** (unit→datasheet→ability chain).
6
+ * That archive predates the 11e Faction Packs, so the new detachments' rules /
7
+ * stratagems / enhancements aren't in it — the chain returns nothing and the
8
+ * authoring pipeline has no text to classify.
9
+ *
10
+ * This sibling tool fills the gap from the *primary* 11e source: the pack PDF.
11
+ * It captures each card's rule body by bbox coordinate (column-scoped, so the
12
+ * packs' 2-column stratagem layout never interleaves — the failure mode that
13
+ * killed the `-layout` approach) and writes the identical `AuthorInputEntry`
14
+ * shape that `author-batch.ts propose` consumes. Nothing downstream changes.
15
+ *
16
+ * IP firewall: rule prose is captured ONLY into the gitignored authoring worklist
17
+ * `data/_audit/author-input/<faction>.json` (.gitignore). It is never written to
18
+ * the committed `faction-pack-input/` staging (that stays names + metadata only).
19
+ *
20
+ * Scope: only empty-modifier stubs that belong to a detachment (`detachment_id`
21
+ * set) — i.e. the new-detachment rules/stratagems/enhancements the pack covers.
22
+ * Unit-scoped stubs (`detachment_id` null) remain the 10e-archive path's job.
23
+ *
24
+ * Requires `pdftotext` (poppler) on PATH — authoring-time only, not CI.
25
+ *
26
+ * Usage: npx tsx tools/src/author-input-pack.ts --pdf <pack.pdf> --faction <id>
27
+ */
28
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
29
+ import { resolve } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import { isCapsHeader, sameColumn, stripCp, slug, titleCase, normCaps, } from "./pack-blocks.js";
32
+ import { detachmentSegments } from "./extract-faction-pack.js";
33
+ import { hasEmptyModifier } from "./audit-coverage.js";
34
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
35
+ const DATA_ROOT = resolve(__dirname, "../../data");
36
+ const ENRICHMENT_ROOT = resolve(DATA_ROOT, "enrichment");
37
+ const OUT_DIR = resolve(DATA_ROOT, "_audit", "author-input");
38
+ const readJSON = (p) => JSON.parse(readFileSync(p, "utf-8"));
39
+ const STRAT_LABEL = /\bSTRATAGEM$/;
40
+ const STRAT_SECTION = /^STRATAGEMS?$/;
41
+ /** Table headers that masquerade as caps "enhancement" names (mirror of extractor). */
42
+ const NOISE = new Set([
43
+ "RANGED WEAPONS", "MELEE WEAPONS", "RANGE", "WARGEAR OPTIONS", "UNIT COMPOSITION",
44
+ "KEYWORDS", "LEADER", "TRANSPORT", "DAMAGED", "SUPREME COMMANDER",
45
+ ]);
46
+ /** True when the nearest same-column block below `b` is mixed-case prose. */
47
+ function hasProseBelow(blocks, b) {
48
+ return blocks
49
+ .filter((o) => sameColumn(o.x, b.x) && o.gy > b.gy)
50
+ .sort((p, q) => p.gy - q.gy)
51
+ .some((o, i) => i === 0 && /[a-z]/.test(o.text));
52
+ }
53
+ /**
54
+ * Locate every card-start anchor in a detachment's blocks: the detachment rule
55
+ * (under a DETACHMENT RULE(S) header), each stratagem (its name block above a
56
+ * "<NAME> – <TYPE> STRATAGEM" label), and each enhancement (a caps header with a
57
+ * prose body below). Anchors bound each other's bodies by same-column y order.
58
+ */
59
+ export function findAnchors(blocks) {
60
+ const anchors = [];
61
+ const seen = new Set();
62
+ const push = (a) => {
63
+ if (a.id && !seen.has(a.id)) {
64
+ seen.add(a.id);
65
+ anchors.push(a);
66
+ }
67
+ };
68
+ // detachment rule: first caps header beneath the DETACHMENT RULE(S) marker.
69
+ const ruleHeader = blocks.filter((b) => /^DETACHMENT RULES?$/.test(b.text)).sort((a, b) => a.gy - b.gy)[0];
70
+ let ruleSlug = "";
71
+ if (ruleHeader) {
72
+ const rn = blocks
73
+ .filter((b) => sameColumn(b.x, ruleHeader.x) && b.gy > ruleHeader.gy && isCapsHeader(b.text))
74
+ .sort((a, b) => a.gy - b.gy)[0];
75
+ if (rn) {
76
+ ruleSlug = slug(rn.text);
77
+ push({ id: ruleSlug, name: titleCase(rn.text), kind: "detachment", x: rn.x, topGy: rn.gy });
78
+ }
79
+ }
80
+ // stratagems: name block directly above each "… STRATAGEM" label, same column.
81
+ const stratIds = new Set();
82
+ for (const label of blocks.filter((b) => STRAT_LABEL.test(b.text) && !STRAT_SECTION.test(b.text))) {
83
+ const nameBlock = blocks
84
+ .filter((b) => sameColumn(b.x, label.x) && b.gy < label.gy && isCapsHeader(stripCp(b.text).name))
85
+ .sort((a, b) => b.gy - a.gy)[0];
86
+ if (!nameBlock)
87
+ continue;
88
+ const { name } = stripCp(nameBlock.text);
89
+ const id = slug(name);
90
+ stratIds.add(id);
91
+ push({ id, name: titleCase(name), kind: "stratagem", x: nameBlock.x, topGy: nameBlock.gy });
92
+ }
93
+ // enhancements: remaining caps headers with a prose body below (subtractive,
94
+ // mirroring the extractor) — not stratagem names, the rule, or table noise.
95
+ for (const b of blocks) {
96
+ if (!isCapsHeader(b.text) || NOISE.has(normCaps(b.text)))
97
+ continue;
98
+ const id = slug(b.text.replace(/ UPGRADE$/, "").trim());
99
+ if (!id || id === ruleSlug || stratIds.has(id) || seen.has(id))
100
+ continue;
101
+ if (!hasProseBelow(blocks, b))
102
+ continue;
103
+ const name = titleCase(b.text.replace(/ UPGRADE$/, "").trim());
104
+ push({ id, name, kind: "enhancement", x: b.x, topGy: b.gy });
105
+ }
106
+ return anchors;
107
+ }
108
+ /**
109
+ * Capture a card's rule body: every same-column block strictly below the anchor's
110
+ * name/header, up to the next same-column anchor (the next card's start). Joined
111
+ * in reading order. Column-scoping is what keeps the adjacent column's text out —
112
+ * the whole reason bbox beats `-layout` on these 2-column pages.
113
+ */
114
+ export function captureBody(blocks, anchor, anchors) {
115
+ const nextTop = Math.min(...anchors
116
+ .filter((a) => a !== anchor && sameColumn(a.x, anchor.x) && a.topGy > anchor.topGy)
117
+ .map((a) => a.topGy), Infinity);
118
+ return blocks
119
+ .filter((b) => sameColumn(b.x, anchor.x) && b.gy > anchor.topGy && b.gy < nextTop)
120
+ .sort((a, b) => a.gy - b.gy)
121
+ .map((b) => b.text)
122
+ .join("\n")
123
+ .trim();
124
+ }
125
+ const normName = (s) => normCaps(s);
126
+ /** Build author-input entries for one faction's new-detachment stubs from its pack. */
127
+ export function buildFactionFromPack(faction, pdf) {
128
+ const abilitiesPath = resolve(ENRICHMENT_ROOT, faction, "abilities.json");
129
+ if (!existsSync(abilitiesPath))
130
+ return [];
131
+ const abilities = readJSON(abilitiesPath);
132
+ // In scope: empty-modifier stubs that belong to a detachment (the new-detachment
133
+ // rules/stratagems/enhancements). Unit-scoped stubs stay the 10e-archive path's.
134
+ const stubs = abilities.filter((a) => a.detachment_id && hasEmptyModifier(a.effect));
135
+ const segments = detachmentSegments(pdf);
136
+ const segById = new Map(segments.map((s) => [s.id, s]));
137
+ const anchorsBySeg = new Map();
138
+ const out = [];
139
+ for (const a of stubs) {
140
+ const base = {
141
+ faction,
142
+ ability_id: a.ability_id,
143
+ name: a.name,
144
+ unit_ids: (a.unit_ids ?? []),
145
+ target: (a.effect && typeof a.effect === "object" && a.effect.target) || null,
146
+ scope: a.scope ?? null,
147
+ faction_id: a.faction_id ?? null,
148
+ ability_type: a.ability_type ?? null,
149
+ };
150
+ const seg = segById.get(a.detachment_id);
151
+ if (!seg) {
152
+ out.push({ ...base, resolved: false, reason: `detachment "${a.detachment_id}" not found in pack` });
153
+ continue;
154
+ }
155
+ let anchors = anchorsBySeg.get(seg.id);
156
+ if (!anchors)
157
+ anchorsBySeg.set(seg.id, (anchors = findAnchors(seg.blocks)));
158
+ const anchor = anchors.find((c) => c.id === a.ability_id) ??
159
+ anchors.find((c) => normName(c.name) === normName(a.name));
160
+ if (!anchor) {
161
+ out.push({ ...base, resolved: false, reason: `no card matched "${a.name}" in "${a.detachment_id}"` });
162
+ continue;
163
+ }
164
+ const description = captureBody(seg.blocks, anchor, anchors);
165
+ if (!description) {
166
+ out.push({ ...base, resolved: false, reason: `empty body for "${a.name}"` });
167
+ continue;
168
+ }
169
+ out.push({
170
+ ...base,
171
+ resolved: true,
172
+ src: { datasheet_id: "", src_type: null, parameter: null, phases: null, description },
173
+ });
174
+ }
175
+ return out;
176
+ }
177
+ function main() {
178
+ const argv = process.argv.slice(2);
179
+ const pdf = argv[argv.indexOf("--pdf") + 1];
180
+ const faction = argv[argv.indexOf("--faction") + 1];
181
+ if (!argv.includes("--pdf") || !argv.includes("--faction") || !pdf || !faction) {
182
+ console.error("Usage: npx tsx tools/src/author-input-pack.ts --pdf <pack.pdf> --faction <id>");
183
+ process.exit(1);
184
+ }
185
+ const entries = buildFactionFromPack(faction, resolve(pdf));
186
+ mkdirSync(OUT_DIR, { recursive: true });
187
+ const outPath = resolve(OUT_DIR, `${faction}.json`);
188
+ writeFileSync(outPath, JSON.stringify(entries, null, 2) + "\n");
189
+ const resolved = entries.filter((e) => e.resolved).length;
190
+ console.log(` ${faction}: ${entries.length} stubs, ${resolved} source-resolved → ${outPath}`);
191
+ for (const e of entries.filter((e) => !e.resolved))
192
+ console.log(` unresolved ${e.ability_id}: ${e.reason}`);
193
+ }
194
+ const isMain = process.argv[1] &&
195
+ resolve(process.argv[1]).replace(/\.\w+$/, "") === fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
196
+ if (isMain)
197
+ main();
198
+ //# sourceMappingURL=author-input-pack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"author-input-pack.js","sourceRoot":"","sources":["../src/author-input-pack.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Build the DSL-authoring input for *new 11e detachments* from a Faction Pack PDF.\n *\n * The stock `author-input.ts` resolves each empty-modifier ability stub to its\n * source rule in the **10e army-assist archive** (unit→datasheet→ability chain).\n * That archive predates the 11e Faction Packs, so the new detachments' rules /\n * stratagems / enhancements aren't in it — the chain returns nothing and the\n * authoring pipeline has no text to classify.\n *\n * This sibling tool fills the gap from the *primary* 11e source: the pack PDF.\n * It captures each card's rule body by bbox coordinate (column-scoped, so the\n * packs' 2-column stratagem layout never interleaves — the failure mode that\n * killed the `-layout` approach) and writes the identical `AuthorInputEntry`\n * shape that `author-batch.ts propose` consumes. Nothing downstream changes.\n *\n * IP firewall: rule prose is captured ONLY into the gitignored authoring worklist\n * `data/_audit/author-input/<faction>.json` (.gitignore). It is never written to\n * the committed `faction-pack-input/` staging (that stays names + metadata only).\n *\n * Scope: only empty-modifier stubs that belong to a detachment (`detachment_id`\n * set) — i.e. the new-detachment rules/stratagems/enhancements the pack covers.\n * Unit-scoped stubs (`detachment_id` null) remain the 10e-archive path's job.\n *\n * Requires `pdftotext` (poppler) on PATH — authoring-time only, not CI.\n *\n * Usage: npx tsx tools/src/author-input-pack.ts --pdf <pack.pdf> --faction <id>\n */\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n type Block,\n isCapsHeader,\n sameColumn,\n stripCp,\n slug,\n titleCase,\n normCaps,\n} from \"./pack-blocks.js\";\nimport { detachmentSegments } from \"./extract-faction-pack.js\";\nimport { hasEmptyModifier } from \"./audit-coverage.js\";\nimport type { AuthorInputEntry } from \"./author-input.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\nconst ENRICHMENT_ROOT = resolve(DATA_ROOT, \"enrichment\");\nconst OUT_DIR = resolve(DATA_ROOT, \"_audit\", \"author-input\");\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Json = any;\nconst readJSON = (p: string): Json => JSON.parse(readFileSync(p, \"utf-8\"));\n\n/** The start of one card within a detachment: its id, name, column, and top y. */\nexport interface CardAnchor {\n id: string;\n name: string;\n kind: \"detachment\" | \"stratagem\" | \"enhancement\";\n x: number;\n topGy: number;\n}\n\nconst STRAT_LABEL = /\\bSTRATAGEM$/;\nconst STRAT_SECTION = /^STRATAGEMS?$/;\n/** Table headers that masquerade as caps \"enhancement\" names (mirror of extractor). */\nconst NOISE = new Set([\n \"RANGED WEAPONS\", \"MELEE WEAPONS\", \"RANGE\", \"WARGEAR OPTIONS\", \"UNIT COMPOSITION\",\n \"KEYWORDS\", \"LEADER\", \"TRANSPORT\", \"DAMAGED\", \"SUPREME COMMANDER\",\n]);\n\n/** True when the nearest same-column block below `b` is mixed-case prose. */\nfunction hasProseBelow(blocks: Block[], b: Block): boolean {\n return blocks\n .filter((o) => sameColumn(o.x, b.x) && o.gy > b.gy)\n .sort((p, q) => p.gy - q.gy)\n .some((o, i) => i === 0 && /[a-z]/.test(o.text));\n}\n\n/**\n * Locate every card-start anchor in a detachment's blocks: the detachment rule\n * (under a DETACHMENT RULE(S) header), each stratagem (its name block above a\n * \"<NAME> – <TYPE> STRATAGEM\" label), and each enhancement (a caps header with a\n * prose body below). Anchors bound each other's bodies by same-column y order.\n */\nexport function findAnchors(blocks: Block[]): CardAnchor[] {\n const anchors: CardAnchor[] = [];\n const seen = new Set<string>();\n const push = (a: CardAnchor): void => {\n if (a.id && !seen.has(a.id)) {\n seen.add(a.id);\n anchors.push(a);\n }\n };\n\n // detachment rule: first caps header beneath the DETACHMENT RULE(S) marker.\n const ruleHeader = blocks.filter((b) => /^DETACHMENT RULES?$/.test(b.text)).sort((a, b) => a.gy - b.gy)[0];\n let ruleSlug = \"\";\n if (ruleHeader) {\n const rn = blocks\n .filter((b) => sameColumn(b.x, ruleHeader.x) && b.gy > ruleHeader.gy && isCapsHeader(b.text))\n .sort((a, b) => a.gy - b.gy)[0];\n if (rn) {\n ruleSlug = slug(rn.text);\n push({ id: ruleSlug, name: titleCase(rn.text), kind: \"detachment\", x: rn.x, topGy: rn.gy });\n }\n }\n\n // stratagems: name block directly above each \"… STRATAGEM\" label, same column.\n const stratIds = new Set<string>();\n for (const label of blocks.filter((b) => STRAT_LABEL.test(b.text) && !STRAT_SECTION.test(b.text))) {\n const nameBlock = blocks\n .filter((b) => sameColumn(b.x, label.x) && b.gy < label.gy && isCapsHeader(stripCp(b.text).name))\n .sort((a, b) => b.gy - a.gy)[0];\n if (!nameBlock) continue;\n const { name } = stripCp(nameBlock.text);\n const id = slug(name);\n stratIds.add(id);\n push({ id, name: titleCase(name), kind: \"stratagem\", x: nameBlock.x, topGy: nameBlock.gy });\n }\n\n // enhancements: remaining caps headers with a prose body below (subtractive,\n // mirroring the extractor) — not stratagem names, the rule, or table noise.\n for (const b of blocks) {\n if (!isCapsHeader(b.text) || NOISE.has(normCaps(b.text))) continue;\n const id = slug(b.text.replace(/ UPGRADE$/, \"\").trim());\n if (!id || id === ruleSlug || stratIds.has(id) || seen.has(id)) continue;\n if (!hasProseBelow(blocks, b)) continue;\n const name = titleCase(b.text.replace(/ UPGRADE$/, \"\").trim());\n push({ id, name, kind: \"enhancement\", x: b.x, topGy: b.gy });\n }\n\n return anchors;\n}\n\n/**\n * Capture a card's rule body: every same-column block strictly below the anchor's\n * name/header, up to the next same-column anchor (the next card's start). Joined\n * in reading order. Column-scoping is what keeps the adjacent column's text out —\n * the whole reason bbox beats `-layout` on these 2-column pages.\n */\nexport function captureBody(blocks: Block[], anchor: CardAnchor, anchors: CardAnchor[]): string {\n const nextTop = Math.min(\n ...anchors\n .filter((a) => a !== anchor && sameColumn(a.x, anchor.x) && a.topGy > anchor.topGy)\n .map((a) => a.topGy),\n Infinity,\n );\n return blocks\n .filter((b) => sameColumn(b.x, anchor.x) && b.gy > anchor.topGy && b.gy < nextTop)\n .sort((a, b) => a.gy - b.gy)\n .map((b) => b.text)\n .join(\"\\n\")\n .trim();\n}\n\nconst normName = (s: string): string => normCaps(s);\n\n/** Build author-input entries for one faction's new-detachment stubs from its pack. */\nexport function buildFactionFromPack(faction: string, pdf: string): AuthorInputEntry[] {\n const abilitiesPath = resolve(ENRICHMENT_ROOT, faction, \"abilities.json\");\n if (!existsSync(abilitiesPath)) return [];\n const abilities: Json[] = readJSON(abilitiesPath);\n\n // In scope: empty-modifier stubs that belong to a detachment (the new-detachment\n // rules/stratagems/enhancements). Unit-scoped stubs stay the 10e-archive path's.\n const stubs = abilities.filter((a) => a.detachment_id && hasEmptyModifier(a.effect));\n\n const segments = detachmentSegments(pdf);\n const segById = new Map(segments.map((s) => [s.id, s]));\n const anchorsBySeg = new Map<string, CardAnchor[]>();\n\n const out: AuthorInputEntry[] = [];\n for (const a of stubs) {\n const base = {\n faction,\n ability_id: a.ability_id as string,\n name: a.name as string,\n unit_ids: (a.unit_ids ?? []) as string[],\n target: (a.effect && typeof a.effect === \"object\" && a.effect.target) || null,\n scope: a.scope ?? null,\n faction_id: a.faction_id ?? null,\n ability_type: a.ability_type ?? null,\n };\n const seg = segById.get(a.detachment_id);\n if (!seg) {\n out.push({ ...base, resolved: false, reason: `detachment \"${a.detachment_id}\" not found in pack` });\n continue;\n }\n let anchors = anchorsBySeg.get(seg.id);\n if (!anchors) anchorsBySeg.set(seg.id, (anchors = findAnchors(seg.blocks)));\n const anchor =\n anchors.find((c) => c.id === a.ability_id) ??\n anchors.find((c) => normName(c.name) === normName(a.name));\n if (!anchor) {\n out.push({ ...base, resolved: false, reason: `no card matched \"${a.name}\" in \"${a.detachment_id}\"` });\n continue;\n }\n const description = captureBody(seg.blocks, anchor, anchors);\n if (!description) {\n out.push({ ...base, resolved: false, reason: `empty body for \"${a.name}\"` });\n continue;\n }\n out.push({\n ...base,\n resolved: true,\n src: { datasheet_id: \"\", src_type: null, parameter: null, phases: null, description },\n });\n }\n return out;\n}\n\nfunction main(): void {\n const argv = process.argv.slice(2);\n const pdf = argv[argv.indexOf(\"--pdf\") + 1];\n const faction = argv[argv.indexOf(\"--faction\") + 1];\n if (!argv.includes(\"--pdf\") || !argv.includes(\"--faction\") || !pdf || !faction) {\n console.error(\"Usage: npx tsx tools/src/author-input-pack.ts --pdf <pack.pdf> --faction <id>\");\n process.exit(1);\n }\n\n const entries = buildFactionFromPack(faction, resolve(pdf));\n mkdirSync(OUT_DIR, { recursive: true });\n const outPath = resolve(OUT_DIR, `${faction}.json`);\n writeFileSync(outPath, JSON.stringify(entries, null, 2) + \"\\n\");\n const resolved = entries.filter((e) => e.resolved).length;\n console.log(` ${faction}: ${entries.length} stubs, ${resolved} source-resolved → ${outPath}`);\n for (const e of entries.filter((e) => !e.resolved)) console.log(` unresolved ${e.ability_id}: ${e.reason}`);\n}\n\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") === fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\nif (isMain) main();\n"]}
@@ -45,6 +45,7 @@ const FILE_TO_COLLECTION = {
45
45
  "force-dispositions": "forceDispositions",
46
46
  "terrain-templates": "terrainTemplates",
47
47
  "terrain-layouts": "terrainLayouts",
48
+ "hull-shapes": "hullShapes",
48
49
  "resource-pools": "resourcePools",
49
50
  "timing-flags": "timingFlags",
50
51
  "interaction-flags": "interactionFlags",
@@ -1 +1 @@
1
- {"version":3,"file":"codegen-data.js","sourceRoot":"","sources":["../src/codegen-data.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bundles every authored data file under `data/` into a single embedded module,\n * `src/data/bundle.generated.ts`.\n *\n * The bundle is inlined as an escaped JSON *string* that is `JSON.parse`d at load\n * time (mirroring the Rust crate's `include_str!`): tsc typechecks it instantly\n * (it is just a string), it parses once at import, and it compiles into `dist`\n * with no runtime filesystem access — so the published package works in Node,\n * bundlers, and browsers alike, where `data/` is not shipped.\n *\n * Run via `npm run codegen:data`. The output is gitignored and regenerated on\n * build/test/pack.\n */\nimport { readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { emptyRawData, type RawData } from \"./data/types.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REPO_ROOT = resolve(__dirname, \"../..\");\nconst DATA_ROOTS = [join(REPO_ROOT, \"data\", \"core\"), join(REPO_ROOT, \"data\", \"enrichment\")];\nconst OUT_FILE = join(__dirname, \"data\", \"bundle.generated.ts\");\n\n/** Directory names that hold examples/scratch data and must never be bundled. */\nconst EXCLUDED_DIRS = new Set([\"_example\", \"_port-audit\"]);\n\n/** Map a data file's base name (sans `.json`) to its `RawData` collection key. */\nconst FILE_TO_COLLECTION: Record<string, keyof RawData> = {\n units: \"units\",\n \"target-profiles\": \"targetProfiles\",\n weapons: \"weapons\",\n \"weapon-keywords\": \"weaponKeywords\",\n factions: \"factions\",\n abilities: \"abilities\",\n \"phase-mappings\": \"phaseMappings\",\n detachments: \"detachments\",\n stratagems: \"stratagems\",\n enhancements: \"enhancements\",\n \"leader-attachments\": \"leaderAttachments\",\n \"unit-compositions\": \"unitCompositions\",\n \"wargear-options\": \"wargearOptions\",\n wargear: \"wargear\",\n \"game-versions\": \"gameVersions\",\n missions: \"missions\",\n \"mission-matchups\": \"missionMatchups\",\n \"secondary-cards\": \"missionCards\",\n \"deployment-patterns\": \"deploymentPatterns\",\n \"force-dispositions\": \"forceDispositions\",\n \"terrain-templates\": \"terrainTemplates\",\n \"terrain-layouts\": \"terrainLayouts\",\n \"resource-pools\": \"resourcePools\",\n \"timing-flags\": \"timingFlags\",\n \"interaction-flags\": \"interactionFlags\",\n};\n\n/** The id-bearing key for a collection, used only for duplicate-id reporting. */\nconst ID_KEY: Partial<Record<keyof RawData, string>> = {\n abilities: \"ability_id\",\n};\n\n/** Recursively collect bundleable `.json` files, skipping excluded dirs/examples. */\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n for (const entry of readdirSync(dir)) {\n if (EXCLUDED_DIRS.has(entry)) continue;\n const full = join(dir, entry);\n if (statSync(full).isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.endsWith(\".json\") && !entry.endsWith(\".example.json\")) {\n out.push(full);\n }\n }\n return out;\n}\n\nfunction baseName(file: string): string {\n return file.slice(file.lastIndexOf(\"/\") + 1, -\".json\".length);\n}\n\nfunction build(): RawData {\n const data = emptyRawData();\n for (const root of DATA_ROOTS) {\n for (const file of collectFiles(root)) {\n const collection = FILE_TO_COLLECTION[baseName(file)];\n if (!collection) continue; // schema/scratch json we don't bundle\n const parsed = JSON.parse(readFileSync(file, \"utf-8\")) as unknown;\n if (!Array.isArray(parsed)) {\n throw new Error(`expected a JSON array in ${file}`);\n }\n (data[collection] as unknown[]).push(...parsed);\n }\n }\n return data;\n}\n\n/** Warn (do not fail) on duplicate primary ids — a data-hygiene signal. */\nfunction reportDuplicateIds(data: RawData): void {\n for (const [collection, key] of Object.entries(ID_KEY) as [keyof RawData, string][]) {\n const seen = new Set<string>();\n const dupes = new Set<string>();\n for (const item of data[collection] as Record<string, unknown>[]) {\n const id = item[key] as string | undefined;\n if (id === undefined) continue;\n if (seen.has(id)) dupes.add(id);\n else seen.add(id);\n }\n if (dupes.size > 0) {\n console.warn(` ⚠ ${collection}: ${dupes.size} duplicate ${key}(s), e.g. ${[...dupes].slice(0, 3).join(\", \")}`);\n }\n }\n}\n\nfunction emit(data: RawData): string {\n // JSON.stringify of the JSON text yields a valid, fully-escaped JS string\n // literal — safe to drop straight into the generated source.\n const jsonText = JSON.stringify(data);\n const literal = JSON.stringify(jsonText);\n return `/* GENERATED by 'npm run codegen:data' from the repository's data/ tree. DO NOT EDIT BY HAND. */\nimport type { RawData } from \"./types.js\";\n\nconst JSON_TEXT = ${literal};\n\n/** The full 40kdc dataset, embedded at build time and parsed once at load. */\nexport const RAW_DATA: RawData = JSON.parse(JSON_TEXT) as RawData;\n`;\n}\n\nfunction main(): void {\n const data = build();\n reportDuplicateIds(data);\n writeFileSync(OUT_FILE, emit(data));\n const counts = (Object.keys(data) as (keyof RawData)[])\n .map((k) => `${k}=${data[k].length}`)\n .join(\", \");\n console.log(`Wrote ${OUT_FILE}\\n ${counts}`);\n}\n\nmain();\n"]}
1
+ {"version":3,"file":"codegen-data.js","sourceRoot":"","sources":["../src/codegen-data.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bundles every authored data file under `data/` into a single embedded module,\n * `src/data/bundle.generated.ts`.\n *\n * The bundle is inlined as an escaped JSON *string* that is `JSON.parse`d at load\n * time (mirroring the Rust crate's `include_str!`): tsc typechecks it instantly\n * (it is just a string), it parses once at import, and it compiles into `dist`\n * with no runtime filesystem access — so the published package works in Node,\n * bundlers, and browsers alike, where `data/` is not shipped.\n *\n * Run via `npm run codegen:data`. The output is gitignored and regenerated on\n * build/test/pack.\n */\nimport { readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { emptyRawData, type RawData } from \"./data/types.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REPO_ROOT = resolve(__dirname, \"../..\");\nconst DATA_ROOTS = [join(REPO_ROOT, \"data\", \"core\"), join(REPO_ROOT, \"data\", \"enrichment\")];\nconst OUT_FILE = join(__dirname, \"data\", \"bundle.generated.ts\");\n\n/** Directory names that hold examples/scratch data and must never be bundled. */\nconst EXCLUDED_DIRS = new Set([\"_example\", \"_port-audit\"]);\n\n/** Map a data file's base name (sans `.json`) to its `RawData` collection key. */\nconst FILE_TO_COLLECTION: Record<string, keyof RawData> = {\n units: \"units\",\n \"target-profiles\": \"targetProfiles\",\n weapons: \"weapons\",\n \"weapon-keywords\": \"weaponKeywords\",\n factions: \"factions\",\n abilities: \"abilities\",\n \"phase-mappings\": \"phaseMappings\",\n detachments: \"detachments\",\n stratagems: \"stratagems\",\n enhancements: \"enhancements\",\n \"leader-attachments\": \"leaderAttachments\",\n \"unit-compositions\": \"unitCompositions\",\n \"wargear-options\": \"wargearOptions\",\n wargear: \"wargear\",\n \"game-versions\": \"gameVersions\",\n missions: \"missions\",\n \"mission-matchups\": \"missionMatchups\",\n \"secondary-cards\": \"missionCards\",\n \"deployment-patterns\": \"deploymentPatterns\",\n \"force-dispositions\": \"forceDispositions\",\n \"terrain-templates\": \"terrainTemplates\",\n \"terrain-layouts\": \"terrainLayouts\",\n \"hull-shapes\": \"hullShapes\",\n \"resource-pools\": \"resourcePools\",\n \"timing-flags\": \"timingFlags\",\n \"interaction-flags\": \"interactionFlags\",\n};\n\n/** The id-bearing key for a collection, used only for duplicate-id reporting. */\nconst ID_KEY: Partial<Record<keyof RawData, string>> = {\n abilities: \"ability_id\",\n};\n\n/** Recursively collect bundleable `.json` files, skipping excluded dirs/examples. */\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n for (const entry of readdirSync(dir)) {\n if (EXCLUDED_DIRS.has(entry)) continue;\n const full = join(dir, entry);\n if (statSync(full).isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.endsWith(\".json\") && !entry.endsWith(\".example.json\")) {\n out.push(full);\n }\n }\n return out;\n}\n\nfunction baseName(file: string): string {\n return file.slice(file.lastIndexOf(\"/\") + 1, -\".json\".length);\n}\n\nfunction build(): RawData {\n const data = emptyRawData();\n for (const root of DATA_ROOTS) {\n for (const file of collectFiles(root)) {\n const collection = FILE_TO_COLLECTION[baseName(file)];\n if (!collection) continue; // schema/scratch json we don't bundle\n const parsed = JSON.parse(readFileSync(file, \"utf-8\")) as unknown;\n if (!Array.isArray(parsed)) {\n throw new Error(`expected a JSON array in ${file}`);\n }\n (data[collection] as unknown[]).push(...parsed);\n }\n }\n return data;\n}\n\n/** Warn (do not fail) on duplicate primary ids — a data-hygiene signal. */\nfunction reportDuplicateIds(data: RawData): void {\n for (const [collection, key] of Object.entries(ID_KEY) as [keyof RawData, string][]) {\n const seen = new Set<string>();\n const dupes = new Set<string>();\n for (const item of data[collection] as Record<string, unknown>[]) {\n const id = item[key] as string | undefined;\n if (id === undefined) continue;\n if (seen.has(id)) dupes.add(id);\n else seen.add(id);\n }\n if (dupes.size > 0) {\n console.warn(` ⚠ ${collection}: ${dupes.size} duplicate ${key}(s), e.g. ${[...dupes].slice(0, 3).join(\", \")}`);\n }\n }\n}\n\nfunction emit(data: RawData): string {\n // JSON.stringify of the JSON text yields a valid, fully-escaped JS string\n // literal — safe to drop straight into the generated source.\n const jsonText = JSON.stringify(data);\n const literal = JSON.stringify(jsonText);\n return `/* GENERATED by 'npm run codegen:data' from the repository's data/ tree. DO NOT EDIT BY HAND. */\nimport type { RawData } from \"./types.js\";\n\nconst JSON_TEXT = ${literal};\n\n/** The full 40kdc dataset, embedded at build time and parsed once at load. */\nexport const RAW_DATA: RawData = JSON.parse(JSON_TEXT) as RawData;\n`;\n}\n\nfunction main(): void {\n const data = build();\n reportDuplicateIds(data);\n writeFileSync(OUT_FILE, emit(data));\n const counts = (Object.keys(data) as (keyof RawData)[])\n .map((k) => `${k}=${data[k].length}`)\n .join(\", \");\n console.log(`Wrote ${OUT_FILE}\\n ${counts}`);\n}\n\nmain();\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/commands/import.ts"],"names":[],"mappings":"AAkBA,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAqDD,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,IAAI,CAAC,CAsCf"}
1
+ {"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/commands/import.ts"],"names":[],"mappings":"AAkBA,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AA2DD,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,IAAI,CAAC,CAsCf"}
@@ -34,7 +34,9 @@ function formatPretty(roster) {
34
34
  const lines = [];
35
35
  lines.push(`Roster: ${roster.name}`);
36
36
  lines.push(` Faction: ${roster.faction_id ?? "(unresolved)"}`);
37
- lines.push(` Detachment: ${roster.detachment_id ?? "(none/unresolved)"}`);
37
+ lines.push(` Detachments: ${roster.detachments
38
+ .map((d) => `${d.ref.id ?? d.ref.raw_name}${d.dp_cost != null ? ` (${d.dp_cost} DP)` : ""}`)
39
+ .join(", ") || "(none/unresolved)"}`);
38
40
  lines.push(` Battle size: ${roster.battle_size ?? "(unmapped)"}`);
39
41
  lines.push(` Points: computed ${roster.points.total_computed}` +
40
42
  (roster.points.total_reported !== null ? `, reported ${roster.points.total_reported}` : "") +
@@ -1 +1 @@
1
- {"version":3,"file":"import.js","sourceRoot":"","sources":["../../src/commands/import.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `import` command: turn a ListForge army-list export into a 40kdc roster.\n *\n * Input may be a ListForge URL, a bare base64 segment, an already-decoded JSON\n * string, or a path to a file containing any of those (or `-`/omitted for stdin).\n * The resolved roster is validated against `roster.schema.json` before output —\n * a guard that the importer only ever emits schema-valid rosters.\n *\n * The import is lenient: unresolved entries do not fail the command (exit 0).\n * Only a decode failure or schema-invalid output is fatal (exit 1).\n */\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { importListForge } from \"../import/index.js\";\nimport { createValidator } from \"../schema-loader.js\";\nimport type { Roster } from \"../import/index.js\";\n\nconst ROSTER_SCHEMA_ID = \"https://40kdc.dev/schemas/core/roster.schema.json\";\n\ninterface ImportOpts {\n reporter: string;\n out?: string;\n}\n\nfunction readStdin(): string {\n try {\n return readFileSync(0, \"utf8\");\n } catch {\n return \"\";\n }\n}\n\n/** Resolve the command's input source to a raw payload string. */\nfunction resolveInput(input: string | undefined): string {\n if (!input || input === \"-\") return readStdin().trim();\n if (existsSync(input)) return readFileSync(input, \"utf8\").trim();\n return input;\n}\n\nfunction formatPretty(roster: Roster): string {\n const d = roster.diagnostics;\n const lines: string[] = [];\n lines.push(`Roster: ${roster.name}`);\n lines.push(` Faction: ${roster.faction_id ?? \"(unresolved)\"}`);\n lines.push(` Detachment: ${roster.detachment_id ?? \"(none/unresolved)\"}`);\n lines.push(` Battle size: ${roster.battle_size ?? \"(unmapped)\"}`);\n lines.push(\n ` Points: computed ${roster.points.total_computed}` +\n (roster.points.total_reported !== null ? `, reported ${roster.points.total_reported}` : \"\") +\n (roster.points.declared_limit !== null ? `, limit ${roster.points.declared_limit}` : \"\"),\n );\n lines.push(` Units (${roster.units.length}):`);\n for (const u of roster.units) {\n const mark = u.ref.resolved ? \"✓\" : \"✗\";\n const id = u.ref.resolved ? u.ref.id : `${u.ref.raw_name} → unresolved`;\n const extras: string[] = [];\n if (u.is_warlord) extras.push(\"warlord\");\n if (u.enhancement) extras.push(`enh:${u.enhancement.resolved ? u.enhancement.id : \"?\"}`);\n if (u.leader_attachment) extras.push(`leads:${u.leader_attachment.bodyguard_ref.id}?`);\n const suffix = extras.length ? ` [${extras.join(\", \")}]` : \"\";\n lines.push(` ${mark} ${id} ×${u.model_count}${u.points !== null ? ` (${u.points}pts)` : \"\"}${suffix}`);\n }\n lines.push(\n ` Resolved: ${d.resolved_units} units / ${d.resolved_weapons} weapons; ` +\n `unresolved: ${d.unresolved_units} units / ${d.unresolved_weapons} weapons`,\n );\n if (d.warnings.length) {\n lines.push(` Warnings (${d.warnings.length}):`);\n for (const w of d.warnings) {\n lines.push(` - [${w.code}]${w.raw_name ? ` \"${w.raw_name}\":` : \"\"} ${w.message}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\nexport async function importCommand(\n input: string | undefined,\n opts: ImportOpts,\n): Promise<void> {\n const payload = resolveInput(input);\n if (!payload) {\n console.error(\"import: no input (provide a URL/base64/JSON argument, a file path, or pipe via stdin)\");\n process.exit(1);\n }\n\n let roster: Roster;\n try {\n roster = importListForge(payload);\n } catch (err) {\n console.error(`import: failed to decode/parse payload: ${(err as Error).message}`);\n process.exit(1);\n }\n\n // Guard: our own output must be schema-valid.\n const validate = createValidator().getSchema(ROSTER_SCHEMA_ID);\n if (!validate) {\n console.error(`import: roster schema not found (${ROSTER_SCHEMA_ID})`);\n process.exit(1);\n }\n if (!validate(roster)) {\n console.error(\"import: produced roster failed schema validation:\");\n console.error(JSON.stringify(validate.errors, null, 2));\n process.exit(1);\n }\n\n const json = JSON.stringify(roster, null, 2);\n if (opts.out) {\n writeFileSync(opts.out, json + \"\\n\", \"utf8\");\n console.error(`Wrote roster → ${opts.out}`);\n }\n\n if (opts.reporter === \"pretty\") {\n console.log(formatPretty(roster));\n } else if (!opts.out) {\n console.log(json);\n }\n}\n"]}
1
+ {"version":3,"file":"import.js","sourceRoot":"","sources":["../../src/commands/import.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `import` command: turn a ListForge army-list export into a 40kdc roster.\n *\n * Input may be a ListForge URL, a bare base64 segment, an already-decoded JSON\n * string, or a path to a file containing any of those (or `-`/omitted for stdin).\n * The resolved roster is validated against `roster.schema.json` before output —\n * a guard that the importer only ever emits schema-valid rosters.\n *\n * The import is lenient: unresolved entries do not fail the command (exit 0).\n * Only a decode failure or schema-invalid output is fatal (exit 1).\n */\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { importListForge } from \"../import/index.js\";\nimport { createValidator } from \"../schema-loader.js\";\nimport type { Roster } from \"../import/index.js\";\n\nconst ROSTER_SCHEMA_ID = \"https://40kdc.dev/schemas/core/roster.schema.json\";\n\ninterface ImportOpts {\n reporter: string;\n out?: string;\n}\n\nfunction readStdin(): string {\n try {\n return readFileSync(0, \"utf8\");\n } catch {\n return \"\";\n }\n}\n\n/** Resolve the command's input source to a raw payload string. */\nfunction resolveInput(input: string | undefined): string {\n if (!input || input === \"-\") return readStdin().trim();\n if (existsSync(input)) return readFileSync(input, \"utf8\").trim();\n return input;\n}\n\nfunction formatPretty(roster: Roster): string {\n const d = roster.diagnostics;\n const lines: string[] = [];\n lines.push(`Roster: ${roster.name}`);\n lines.push(` Faction: ${roster.faction_id ?? \"(unresolved)\"}`);\n lines.push(\n ` Detachments: ${\n roster.detachments\n .map((d) => `${d.ref.id ?? d.ref.raw_name}${d.dp_cost != null ? ` (${d.dp_cost} DP)` : \"\"}`)\n .join(\", \") || \"(none/unresolved)\"\n }`,\n );\n lines.push(` Battle size: ${roster.battle_size ?? \"(unmapped)\"}`);\n lines.push(\n ` Points: computed ${roster.points.total_computed}` +\n (roster.points.total_reported !== null ? `, reported ${roster.points.total_reported}` : \"\") +\n (roster.points.declared_limit !== null ? `, limit ${roster.points.declared_limit}` : \"\"),\n );\n lines.push(` Units (${roster.units.length}):`);\n for (const u of roster.units) {\n const mark = u.ref.resolved ? \"✓\" : \"✗\";\n const id = u.ref.resolved ? u.ref.id : `${u.ref.raw_name} → unresolved`;\n const extras: string[] = [];\n if (u.is_warlord) extras.push(\"warlord\");\n if (u.enhancement) extras.push(`enh:${u.enhancement.resolved ? u.enhancement.id : \"?\"}`);\n if (u.leader_attachment) extras.push(`leads:${u.leader_attachment.bodyguard_ref.id}?`);\n const suffix = extras.length ? ` [${extras.join(\", \")}]` : \"\";\n lines.push(` ${mark} ${id} ×${u.model_count}${u.points !== null ? ` (${u.points}pts)` : \"\"}${suffix}`);\n }\n lines.push(\n ` Resolved: ${d.resolved_units} units / ${d.resolved_weapons} weapons; ` +\n `unresolved: ${d.unresolved_units} units / ${d.unresolved_weapons} weapons`,\n );\n if (d.warnings.length) {\n lines.push(` Warnings (${d.warnings.length}):`);\n for (const w of d.warnings) {\n lines.push(` - [${w.code}]${w.raw_name ? ` \"${w.raw_name}\":` : \"\"} ${w.message}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\nexport async function importCommand(\n input: string | undefined,\n opts: ImportOpts,\n): Promise<void> {\n const payload = resolveInput(input);\n if (!payload) {\n console.error(\"import: no input (provide a URL/base64/JSON argument, a file path, or pipe via stdin)\");\n process.exit(1);\n }\n\n let roster: Roster;\n try {\n roster = importListForge(payload);\n } catch (err) {\n console.error(`import: failed to decode/parse payload: ${(err as Error).message}`);\n process.exit(1);\n }\n\n // Guard: our own output must be schema-valid.\n const validate = createValidator().getSchema(ROSTER_SCHEMA_ID);\n if (!validate) {\n console.error(`import: roster schema not found (${ROSTER_SCHEMA_ID})`);\n process.exit(1);\n }\n if (!validate(roster)) {\n console.error(\"import: produced roster failed schema validation:\");\n console.error(JSON.stringify(validate.errors, null, 2));\n process.exit(1);\n }\n\n const json = JSON.stringify(roster, null, 2);\n if (opts.out) {\n writeFileSync(opts.out, json + \"\\n\", \"utf8\");\n console.error(`Wrote roster → ${opts.out}`);\n }\n\n if (opts.reporter === \"pretty\") {\n console.log(formatPretty(roster));\n } else if (!opts.out) {\n console.log(json);\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"convert-faction.d.ts","sourceRoot":"","sources":["../src/convert-faction.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,OAAO,EAAE,KAAK,aAAa,EAAkC,MAAM,gCAAgC,CAAC;AAGpG,OAAO,sCAAsC,CAAC;AAC9C,OAAO,2CAA2C,CAAC;AACnD,OAAO,uCAAuC,CAAC;AAC/C,OAAO,0CAA0C,CAAC;AAClD,OAAO,2CAA2C,CAAC;AACnD,OAAO,kCAAkC,CAAC;AAC1C,OAAO,2CAA2C,CAAC;AACnD,OAAO,sCAAsC,CAAC;AAC9C,OAAO,uCAAuC,CAAC;AAC/C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,0CAA0C,CAAC;AAClD,OAAO,0CAA0C,CAAC;AAClD,OAAO,gDAAgD,CAAC;AACxD,OAAO,4CAA4C,CAAC;AACpD,OAAO,oCAAoC,CAAC;AAC5C,OAAO,kCAAkC,CAAC;AAC1C,OAAO,iCAAiC,CAAC;AACzC,OAAO,uCAAuC,CAAC;AAC/C,OAAO,8BAA8B,CAAC;AACtC,OAAO,iCAAiC,CAAC;AACzC,OAAO,6CAA6C,CAAC;AACrD,OAAO,yCAAyC,CAAC;AACjD,OAAO,0CAA0C,CAAC;AAClD,OAAO,sCAAsC,CAAC;AAC9C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,sCAAsC,CAAC;AAC9C,OAAO,wCAAwC,CAAC;AAChD,OAAO,oCAAoC,CAAC;AAC5C,OAAO,sCAAsC,CAAC;AAC9C,OAAO,wCAAwC,CAAC;AAChD,OAAO,uCAAuC,CAAC;AAC/C,OAAO,oCAAoC,CAAC;AAC5C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qCAAqC,CAAC;AAqJ7C,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,IAAI,CA6dN"}
1
+ {"version":3,"file":"convert-faction.d.ts","sourceRoot":"","sources":["../src/convert-faction.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,OAAO,EAAE,KAAK,aAAa,EAAkC,MAAM,gCAAgC,CAAC;AAGpG,OAAO,sCAAsC,CAAC;AAC9C,OAAO,2CAA2C,CAAC;AACnD,OAAO,uCAAuC,CAAC;AAC/C,OAAO,0CAA0C,CAAC;AAClD,OAAO,2CAA2C,CAAC;AACnD,OAAO,kCAAkC,CAAC;AAC1C,OAAO,2CAA2C,CAAC;AACnD,OAAO,sCAAsC,CAAC;AAC9C,OAAO,uCAAuC,CAAC;AAC/C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,0CAA0C,CAAC;AAClD,OAAO,0CAA0C,CAAC;AAClD,OAAO,gDAAgD,CAAC;AACxD,OAAO,4CAA4C,CAAC;AACpD,OAAO,oCAAoC,CAAC;AAC5C,OAAO,kCAAkC,CAAC;AAC1C,OAAO,iCAAiC,CAAC;AACzC,OAAO,uCAAuC,CAAC;AAC/C,OAAO,8BAA8B,CAAC;AACtC,OAAO,iCAAiC,CAAC;AACzC,OAAO,6CAA6C,CAAC;AACrD,OAAO,yCAAyC,CAAC;AACjD,OAAO,0CAA0C,CAAC;AAClD,OAAO,sCAAsC,CAAC;AAC9C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,sCAAsC,CAAC;AAC9C,OAAO,wCAAwC,CAAC;AAChD,OAAO,oCAAoC,CAAC;AAC5C,OAAO,sCAAsC,CAAC;AAC9C,OAAO,wCAAwC,CAAC;AAChD,OAAO,uCAAuC,CAAC;AAC/C,OAAO,oCAAoC,CAAC;AAC5C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qCAAqC,CAAC;AAqJ7C,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,IAAI,CAmfN"}
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Faction configs are registered via side-effect imports from ./converters/configs/.
8
8
  */
9
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
10
10
  import { resolve, dirname } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { nameToId, parseStratagemType, parsePlayerTurn, mapPhases } from "./converters/id-generator.js";
@@ -265,6 +265,18 @@ export function convertFaction(config, options = {}) {
265
265
  factionEnhancements = factionEnhancements.filter((e) => allowedDetachments.has(e.detachment));
266
266
  factionStratagems = factionStratagems.filter((s) => allowedDetachments.has(s.detachment));
267
267
  }
268
+ // `detachment_points` (11e DP cost) and `force_dispositions` are maintained
269
+ // by hand — army-assist doesn't carry them — so a re-run must NOT drop them.
270
+ // Carry both forward by id from the existing on-disk file.
271
+ const existingDetPath = resolve(ROOT, `data/core/${factionId}/detachments.json`);
272
+ const preservedById = new Map();
273
+ if (existsSync(existingDetPath)) {
274
+ const existing = JSON.parse(readFileSync(existingDetPath, "utf-8"));
275
+ for (const d of existing) {
276
+ if (typeof d.id === "string")
277
+ preservedById.set(d.id, d);
278
+ }
279
+ }
268
280
  const detachments = detachmentNames.map((detName) => {
269
281
  const detId = nameToId(detName);
270
282
  const detEnhIds = factionEnhancements
@@ -273,7 +285,7 @@ export function convertFaction(config, options = {}) {
273
285
  const detStratIds = factionStratagems
274
286
  .filter((s) => s.detachment === detName)
275
287
  .map((s) => nameToId(s.name));
276
- return {
288
+ const record = {
277
289
  id: detId,
278
290
  name: detName,
279
291
  faction_id: factionId,
@@ -282,6 +294,14 @@ export function convertFaction(config, options = {}) {
282
294
  stratagem_ids: detStratIds,
283
295
  game_version: GAME_VERSION,
284
296
  };
297
+ const preserved = preservedById.get(detId);
298
+ if (preserved && "detachment_points" in preserved) {
299
+ record.detachment_points = preserved.detachment_points;
300
+ }
301
+ if (preserved && "force_dispositions" in preserved) {
302
+ record.force_dispositions = preserved.force_dispositions;
303
+ }
304
+ return record;
285
305
  });
286
306
  // ─── Build enhancements ───
287
307
  console.log("Converting enhancements...");