@alpaca-software/40kdc-data 0.5.14 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abilities-resolver/resolver.d.ts.map +1 -1
- package/dist/abilities-resolver/resolver.js +6 -2
- package/dist/abilities-resolver/resolver.js.map +1 -1
- package/dist/audit-coverage.d.ts.map +1 -1
- package/dist/audit-coverage.js +90 -1
- package/dist/audit-coverage.js.map +1 -1
- package/dist/audit-phrasing.d.ts +64 -0
- package/dist/audit-phrasing.d.ts.map +1 -0
- package/dist/audit-phrasing.js +208 -0
- package/dist/audit-phrasing.js.map +1 -0
- package/dist/build-share-registry.d.ts +2 -0
- package/dist/build-share-registry.d.ts.map +1 -0
- package/dist/build-share-registry.js +115 -0
- package/dist/build-share-registry.js.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/codegen-data.js +23 -0
- package/dist/codegen-data.js.map +1 -1
- package/dist/commands/translate.d.ts.map +1 -1
- package/dist/commands/translate.js +2 -5
- package/dist/commands/translate.js.map +1 -1
- package/dist/commands/validate-all.d.ts.map +1 -1
- package/dist/commands/validate-all.js +8 -3
- package/dist/commands/validate-all.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/dataset.d.ts +33 -1
- package/dist/data/dataset.d.ts.map +1 -1
- package/dist/data/dataset.js +107 -1
- package/dist/data/dataset.js.map +1 -1
- package/dist/data/index.d.ts +4 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +4 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/types.d.ts +3 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/data/types.js +1 -0
- package/dist/data/types.js.map +1 -1
- package/dist/gen-conformance.js +162 -0
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +125 -0
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrity.d.ts +32 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +120 -0
- package/dist/integrity.js.map +1 -0
- package/dist/report.d.ts +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +2 -2
- package/dist/report.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +42 -0
- package/dist/runner.js.map +1 -1
- package/dist/share/codec.d.ts +83 -0
- package/dist/share/codec.d.ts.map +1 -0
- package/dist/share/codec.js +230 -0
- package/dist/share/codec.js.map +1 -0
- package/dist/share/index.d.ts +30 -0
- package/dist/share/index.d.ts.map +1 -0
- package/dist/share/index.js +36 -0
- package/dist/share/index.js.map +1 -0
- package/dist/share/registry.d.ts +42 -0
- package/dist/share/registry.d.ts.map +1 -0
- package/dist/share/registry.generated.d.ts +4 -0
- package/dist/share/registry.generated.d.ts.map +1 -0
- package/dist/share/registry.generated.js +4 -0
- package/dist/share/registry.generated.js.map +1 -0
- package/dist/share/registry.js +68 -0
- package/dist/share/registry.js.map +1 -0
- package/dist/terrain/index.d.ts +2 -2
- package/dist/terrain/index.d.ts.map +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/index.js.map +1 -1
- package/dist/terrain/solve.d.ts +76 -0
- package/dist/terrain/solve.d.ts.map +1 -1
- package/dist/terrain/solve.js +138 -0
- package/dist/terrain/solve.js.map +1 -1
- package/dist/translate/condition.d.ts +1 -0
- package/dist/translate/condition.d.ts.map +1 -1
- package/dist/translate/condition.js +101 -7
- package/dist/translate/condition.js.map +1 -1
- package/dist/translate/effect.d.ts +28 -20
- package/dist/translate/effect.d.ts.map +1 -1
- package/dist/translate/effect.js +483 -114
- package/dist/translate/effect.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +1 -0
- package/dist/validate.js.map +1 -1
- package/package.json +3 -1
- package/schemas/core/allied-rule.schema.json +115 -0
- package/schemas/core/detachment.schema.json +38 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phrasing audit: catalogue the generated English text for *every* authored
|
|
3
|
+
* ability and flag mechanical phrasing defects, so an across-the-board prose
|
|
4
|
+
* cleanup has a worklist.
|
|
5
|
+
*
|
|
6
|
+
* The DSL→English describer (`describeAbility`, a.k.a. `ability.print()`) is the
|
|
7
|
+
* user-facing rules readout in downstream consumers. This tool runs that *real*
|
|
8
|
+
* describer over each `data/enrichment/<faction>/abilities.json` entry and runs
|
|
9
|
+
* a set of regex heuristics over the output, flagging the defect classes a
|
|
10
|
+
* manual audit turned up: leftover `?` placeholders (modifier field-name
|
|
11
|
+
* mismatches), unmapped `[type]` fallbacks, raw `snake_case` identifiers that
|
|
12
|
+
* leaked into prose, doubled prepositions ("at on model destroyed"), `0"` noise
|
|
13
|
+
* on movement grants, `(s)` pluralization stubs, and empty output.
|
|
14
|
+
*
|
|
15
|
+
* This is a TS-only analysis utility — it *consumes* the conformance-pinned
|
|
16
|
+
* describer, it does not change it — so it needs no SPEC_VERSION bump and no
|
|
17
|
+
* Rust/Python port (mirrors `audit-coverage.ts`).
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* npx tsx tools/src/audit-phrasing.ts (pretty report to stdout)
|
|
21
|
+
* npx tsx tools/src/audit-phrasing.ts --json (machine-readable)
|
|
22
|
+
* npx tsx tools/src/audit-phrasing.ts --write (also emit data/_audit/phrasing.*)
|
|
23
|
+
*
|
|
24
|
+
* Wired as `40kdc-validate audit-phrasing` and `npm run audit:phrasing`.
|
|
25
|
+
*/
|
|
26
|
+
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
27
|
+
import { resolve, basename } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import chalk from "chalk";
|
|
30
|
+
import { describeAbility } from "./translate/index.js";
|
|
31
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
32
|
+
const DATA_ROOT = resolve(__dirname, "../../data");
|
|
33
|
+
const ENRICHMENT_ROOT = resolve(DATA_ROOT, "enrichment");
|
|
34
|
+
const AUDIT_DIR = resolve(DATA_ROOT, "_audit");
|
|
35
|
+
const PREP = "(?:at|on|in|of|to|by|for|from|with)";
|
|
36
|
+
const DOUBLED_PREP = new RegExp(`\\b${PREP}\\s+${PREP}\\b`, "i");
|
|
37
|
+
// The unknown-effect-type fallback emits a lowercase kebab token in brackets
|
|
38
|
+
// (`[some-type]`); intentional GW weapon keywords are bracketed UPPERCASE
|
|
39
|
+
// (`[LETHAL HITS]`) and must NOT be flagged.
|
|
40
|
+
const TYPE_FALLBACK = /\[[a-z][a-z0-9-]*\]/;
|
|
41
|
+
const SNAKE_CASE = /[a-z]+_[a-z]+/;
|
|
42
|
+
const ZERO_INCH = /\b0"/;
|
|
43
|
+
/**
|
|
44
|
+
* Classify one piece of generated text. Pure and order-stable so it is directly
|
|
45
|
+
* unit-testable. The order of the returned flags follows the {@link PhrasingFlag}
|
|
46
|
+
* union for deterministic output.
|
|
47
|
+
*/
|
|
48
|
+
export function classifyPhrasing(text) {
|
|
49
|
+
const flags = [];
|
|
50
|
+
if (text.trim() === "") {
|
|
51
|
+
flags.push("empty");
|
|
52
|
+
return flags;
|
|
53
|
+
}
|
|
54
|
+
if (text.includes("?"))
|
|
55
|
+
flags.push("placeholder");
|
|
56
|
+
if (TYPE_FALLBACK.test(text))
|
|
57
|
+
flags.push("type_fallback");
|
|
58
|
+
if (SNAKE_CASE.test(text))
|
|
59
|
+
flags.push("snake_case_leftover");
|
|
60
|
+
if (DOUBLED_PREP.test(text))
|
|
61
|
+
flags.push("doubled_preposition");
|
|
62
|
+
if (ZERO_INCH.test(text))
|
|
63
|
+
flags.push("zero_inch");
|
|
64
|
+
if (text.includes("(s)"))
|
|
65
|
+
flags.push("paren_plural");
|
|
66
|
+
return flags;
|
|
67
|
+
}
|
|
68
|
+
/** Compute the phrasing report for a set of factions. Pure — IO lives in the command. */
|
|
69
|
+
export function auditPhrasing(input) {
|
|
70
|
+
const rows = [];
|
|
71
|
+
const byFaction = [];
|
|
72
|
+
const flagCounts = new Map();
|
|
73
|
+
for (const { faction, abilities } of input) {
|
|
74
|
+
let factionFlagged = 0;
|
|
75
|
+
for (const a of abilities) {
|
|
76
|
+
const text = describeAbility({
|
|
77
|
+
...(a.effect ? { effect: a.effect } : {}),
|
|
78
|
+
...(a.scope ? { scope: a.scope } : {}),
|
|
79
|
+
...(a.applies_to ? { applies_to: a.applies_to } : {}),
|
|
80
|
+
});
|
|
81
|
+
const flags = classifyPhrasing(text);
|
|
82
|
+
if (flags.length > 0) {
|
|
83
|
+
factionFlagged++;
|
|
84
|
+
for (const f of flags)
|
|
85
|
+
flagCounts.set(f, (flagCounts.get(f) ?? 0) + 1);
|
|
86
|
+
}
|
|
87
|
+
rows.push({
|
|
88
|
+
faction,
|
|
89
|
+
ability_id: a.ability_id,
|
|
90
|
+
name: a.name ?? a.ability_id,
|
|
91
|
+
unit_ids: a.unit_ids ?? [],
|
|
92
|
+
text,
|
|
93
|
+
flags,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
byFaction.push({ faction, total: abilities.length, flagged: factionFlagged });
|
|
97
|
+
}
|
|
98
|
+
byFaction.sort((a, b) => a.faction.localeCompare(b.faction));
|
|
99
|
+
rows.sort((a, b) => a.faction.localeCompare(b.faction) || a.ability_id.localeCompare(b.ability_id));
|
|
100
|
+
const byFlag = [...flagCounts.entries()]
|
|
101
|
+
.map(([flag, count]) => ({ flag, count }))
|
|
102
|
+
.sort((a, b) => b.count - a.count || a.flag.localeCompare(b.flag));
|
|
103
|
+
return {
|
|
104
|
+
rows,
|
|
105
|
+
total: rows.length,
|
|
106
|
+
flagged: rows.filter((r) => r.flags.length > 0).length,
|
|
107
|
+
byFlag,
|
|
108
|
+
byFaction,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Read every `data/enrichment/<faction>/abilities.json` (skips `_`-prefixed dirs). */
|
|
112
|
+
export function loadFactions() {
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const entry of readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })) {
|
|
115
|
+
if (!entry.isDirectory() || entry.name.startsWith("_"))
|
|
116
|
+
continue;
|
|
117
|
+
const file = resolve(ENRICHMENT_ROOT, entry.name, "abilities.json");
|
|
118
|
+
let abilities;
|
|
119
|
+
try {
|
|
120
|
+
abilities = JSON.parse(readFileSync(file, "utf-8"));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue; // no abilities.json in this folder
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(abilities))
|
|
126
|
+
continue;
|
|
127
|
+
out.push({ faction: entry.name, abilities });
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
const pct = (n, d) => (d === 0 ? "—" : `${Math.round((100 * n) / d)}%`);
|
|
132
|
+
/** CSV-escape one field (RFC-4180: wrap in quotes, double internal quotes). */
|
|
133
|
+
function csvField(v) {
|
|
134
|
+
return `"${v.replace(/"/g, '""')}"`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* One row per ability. The multi-line describer output is flattened to ` / ` so
|
|
138
|
+
* each catalogued ability stays on a single CSV line (greppable); the JSON
|
|
139
|
+
* artifact keeps the original newlines.
|
|
140
|
+
*/
|
|
141
|
+
function csvReport(r) {
|
|
142
|
+
const lines = ["faction,ability_id,name,unit_ids,flags,text"];
|
|
143
|
+
for (const row of r.rows) {
|
|
144
|
+
lines.push([
|
|
145
|
+
csvField(row.faction),
|
|
146
|
+
csvField(row.ability_id),
|
|
147
|
+
csvField(row.name),
|
|
148
|
+
csvField(row.unit_ids.join(";")),
|
|
149
|
+
csvField(row.flags.join(";")),
|
|
150
|
+
csvField(row.text.replace(/\n/g, " / ")),
|
|
151
|
+
].join(","));
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n") + "\n";
|
|
154
|
+
}
|
|
155
|
+
function prettyReport(r) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(chalk.bold("40kdc Ability Phrasing Audit"));
|
|
159
|
+
lines.push(chalk.gray("─".repeat(60)));
|
|
160
|
+
lines.push(`Abilities catalogued: ${chalk.cyan(String(r.total))}`);
|
|
161
|
+
lines.push(`Flagged for review: ${chalk.yellow(String(r.flagged))} (${pct(r.flagged, r.total)})`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push(chalk.bold("Defect flags:"));
|
|
164
|
+
if (r.byFlag.length === 0) {
|
|
165
|
+
lines.push(chalk.green(" none — every ability's generated text is clean"));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
for (const { flag, count } of r.byFlag) {
|
|
169
|
+
lines.push(` ${String(count).padStart(5)} ${chalk.gray(flag)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push(chalk.bold("Most-flagged factions:"));
|
|
174
|
+
const worst = [...r.byFaction].filter((f) => f.flagged > 0).sort((a, b) => b.flagged - a.flagged);
|
|
175
|
+
for (const f of worst.slice(0, 15)) {
|
|
176
|
+
lines.push(` ${String(f.flagged).padStart(4)}/${String(f.total).padEnd(4)} ${chalk.gray(f.faction)}`);
|
|
177
|
+
}
|
|
178
|
+
lines.push("");
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
181
|
+
/** Commander action: run the phrasing audit over the repo's enrichment data. */
|
|
182
|
+
export function auditPhrasingCommand(opts = {}) {
|
|
183
|
+
const report = auditPhrasing(loadFactions());
|
|
184
|
+
if (opts.write) {
|
|
185
|
+
mkdirSync(AUDIT_DIR, { recursive: true });
|
|
186
|
+
writeFileSync(resolve(AUDIT_DIR, "phrasing.json"), JSON.stringify(report, null, 2) + "\n");
|
|
187
|
+
writeFileSync(resolve(AUDIT_DIR, "phrasing.csv"), csvReport(report));
|
|
188
|
+
}
|
|
189
|
+
if (opts.reporter === "json") {
|
|
190
|
+
console.log(JSON.stringify(report, null, 2));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
console.log(prettyReport(report));
|
|
194
|
+
if (opts.write)
|
|
195
|
+
console.log(chalk.gray(`Wrote ${basename(AUDIT_DIR)}/phrasing.csv + phrasing.json`));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Direct-invocation entry point (`npx tsx tools/src/audit-phrasing.ts [--json] [--write]`).
|
|
199
|
+
const isMain = process.argv[1] &&
|
|
200
|
+
resolve(process.argv[1]).replace(/\.\w+$/, "") === fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
|
|
201
|
+
if (isMain) {
|
|
202
|
+
const argv = process.argv.slice(2);
|
|
203
|
+
auditPhrasingCommand({
|
|
204
|
+
reporter: argv.includes("--json") ? "json" : "pretty",
|
|
205
|
+
write: argv.includes("--write"),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=audit-phrasing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-phrasing.js","sourceRoot":"","sources":["../src/audit-phrasing.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Phrasing audit: catalogue the generated English text for *every* authored\n * ability and flag mechanical phrasing defects, so an across-the-board prose\n * cleanup has a worklist.\n *\n * The DSL→English describer (`describeAbility`, a.k.a. `ability.print()`) is the\n * user-facing rules readout in downstream consumers. This tool runs that *real*\n * describer over each `data/enrichment/<faction>/abilities.json` entry and runs\n * a set of regex heuristics over the output, flagging the defect classes a\n * manual audit turned up: leftover `?` placeholders (modifier field-name\n * mismatches), unmapped `[type]` fallbacks, raw `snake_case` identifiers that\n * leaked into prose, doubled prepositions (\"at on model destroyed\"), `0\"` noise\n * on movement grants, `(s)` pluralization stubs, and empty output.\n *\n * This is a TS-only analysis utility — it *consumes* the conformance-pinned\n * describer, it does not change it — so it needs no SPEC_VERSION bump and no\n * Rust/Python port (mirrors `audit-coverage.ts`).\n *\n * Usage:\n * npx tsx tools/src/audit-phrasing.ts (pretty report to stdout)\n * npx tsx tools/src/audit-phrasing.ts --json (machine-readable)\n * npx tsx tools/src/audit-phrasing.ts --write (also emit data/_audit/phrasing.*)\n *\n * Wired as `40kdc-validate audit-phrasing` and `npm run audit:phrasing`.\n */\nimport { readdirSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport { describeAbility, type Effect, type AbilityScope, type AbilityAppliesTo } from \"./translate/index.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\nconst ENRICHMENT_ROOT = resolve(DATA_ROOT, \"enrichment\");\nconst AUDIT_DIR = resolve(DATA_ROOT, \"_audit\");\n\n/** Shape of an authored ability entry (only the fields the describer reads). */\ninterface AbilityEntry {\n ability_id: string;\n name?: string;\n effect?: Effect;\n scope?: AbilityScope;\n applies_to?: AbilityAppliesTo | null;\n /** Units that carry this ability — surfaced so a worklist row points at the datasheet. */\n unit_ids?: string[];\n}\n\n/** One catalogued ability: its identity, the generated text, and any defect flags. */\nexport interface PhrasingRow {\n faction: string;\n ability_id: string;\n name: string;\n /** Slugs of the units carrying this ability (for datasheet/Wahapedia lookup). */\n unit_ids: string[];\n /** Full multi-line `describeAbility` output (effect + scope + applies). */\n text: string;\n flags: PhrasingFlag[];\n}\n\nexport interface PhrasingReport {\n rows: PhrasingRow[];\n total: number;\n /** Rows carrying ≥1 flag. */\n flagged: number;\n /** Per-flag counts, descending. */\n byFlag: { flag: PhrasingFlag; count: number }[];\n /** Per-faction totals + flagged counts, faction-sorted. */\n byFaction: { faction: string; total: number; flagged: number }[];\n}\n\nexport type PhrasingFlag =\n | \"placeholder\"\n | \"type_fallback\"\n | \"snake_case_leftover\"\n | \"doubled_preposition\"\n | \"zero_inch\"\n | \"paren_plural\"\n | \"empty\";\n\nconst PREP = \"(?:at|on|in|of|to|by|for|from|with)\";\nconst DOUBLED_PREP = new RegExp(`\\\\b${PREP}\\\\s+${PREP}\\\\b`, \"i\");\n// The unknown-effect-type fallback emits a lowercase kebab token in brackets\n// (`[some-type]`); intentional GW weapon keywords are bracketed UPPERCASE\n// (`[LETHAL HITS]`) and must NOT be flagged.\nconst TYPE_FALLBACK = /\\[[a-z][a-z0-9-]*\\]/;\nconst SNAKE_CASE = /[a-z]+_[a-z]+/;\nconst ZERO_INCH = /\\b0\"/;\n\n/**\n * Classify one piece of generated text. Pure and order-stable so it is directly\n * unit-testable. The order of the returned flags follows the {@link PhrasingFlag}\n * union for deterministic output.\n */\nexport function classifyPhrasing(text: string): PhrasingFlag[] {\n const flags: PhrasingFlag[] = [];\n if (text.trim() === \"\") {\n flags.push(\"empty\");\n return flags;\n }\n if (text.includes(\"?\")) flags.push(\"placeholder\");\n if (TYPE_FALLBACK.test(text)) flags.push(\"type_fallback\");\n if (SNAKE_CASE.test(text)) flags.push(\"snake_case_leftover\");\n if (DOUBLED_PREP.test(text)) flags.push(\"doubled_preposition\");\n if (ZERO_INCH.test(text)) flags.push(\"zero_inch\");\n if (text.includes(\"(s)\")) flags.push(\"paren_plural\");\n return flags;\n}\n\n/** Compute the phrasing report for a set of factions. Pure — IO lives in the command. */\nexport function auditPhrasing(\n input: { faction: string; abilities: AbilityEntry[] }[],\n): PhrasingReport {\n const rows: PhrasingRow[] = [];\n const byFaction: { faction: string; total: number; flagged: number }[] = [];\n const flagCounts = new Map<PhrasingFlag, number>();\n\n for (const { faction, abilities } of input) {\n let factionFlagged = 0;\n for (const a of abilities) {\n const text = describeAbility({\n ...(a.effect ? { effect: a.effect } : {}),\n ...(a.scope ? { scope: a.scope } : {}),\n ...(a.applies_to ? { applies_to: a.applies_to } : {}),\n });\n const flags = classifyPhrasing(text);\n if (flags.length > 0) {\n factionFlagged++;\n for (const f of flags) flagCounts.set(f, (flagCounts.get(f) ?? 0) + 1);\n }\n rows.push({\n faction,\n ability_id: a.ability_id,\n name: a.name ?? a.ability_id,\n unit_ids: a.unit_ids ?? [],\n text,\n flags,\n });\n }\n byFaction.push({ faction, total: abilities.length, flagged: factionFlagged });\n }\n\n byFaction.sort((a, b) => a.faction.localeCompare(b.faction));\n rows.sort(\n (a, b) => a.faction.localeCompare(b.faction) || a.ability_id.localeCompare(b.ability_id),\n );\n\n const byFlag = [...flagCounts.entries()]\n .map(([flag, count]) => ({ flag, count }))\n .sort((a, b) => b.count - a.count || a.flag.localeCompare(b.flag));\n\n return {\n rows,\n total: rows.length,\n flagged: rows.filter((r) => r.flags.length > 0).length,\n byFlag,\n byFaction,\n };\n}\n\n/** Read every `data/enrichment/<faction>/abilities.json` (skips `_`-prefixed dirs). */\nexport function loadFactions(): { faction: string; abilities: AbilityEntry[] }[] {\n const out: { faction: string; abilities: AbilityEntry[] }[] = [];\n for (const entry of readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })) {\n if (!entry.isDirectory() || entry.name.startsWith(\"_\")) continue;\n const file = resolve(ENRICHMENT_ROOT, entry.name, \"abilities.json\");\n let abilities: AbilityEntry[];\n try {\n abilities = JSON.parse(readFileSync(file, \"utf-8\"));\n } catch {\n continue; // no abilities.json in this folder\n }\n if (!Array.isArray(abilities)) continue;\n out.push({ faction: entry.name, abilities });\n }\n return out;\n}\n\nconst pct = (n: number, d: number): string => (d === 0 ? \"—\" : `${Math.round((100 * n) / d)}%`);\n\n/** CSV-escape one field (RFC-4180: wrap in quotes, double internal quotes). */\nfunction csvField(v: string): string {\n return `\"${v.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * One row per ability. The multi-line describer output is flattened to ` / ` so\n * each catalogued ability stays on a single CSV line (greppable); the JSON\n * artifact keeps the original newlines.\n */\nfunction csvReport(r: PhrasingReport): string {\n const lines = [\"faction,ability_id,name,unit_ids,flags,text\"];\n for (const row of r.rows) {\n lines.push(\n [\n csvField(row.faction),\n csvField(row.ability_id),\n csvField(row.name),\n csvField(row.unit_ids.join(\";\")),\n csvField(row.flags.join(\";\")),\n csvField(row.text.replace(/\\n/g, \" / \")),\n ].join(\",\"),\n );\n }\n return lines.join(\"\\n\") + \"\\n\";\n}\n\nfunction prettyReport(r: PhrasingReport): string {\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(\"40kdc Ability Phrasing Audit\"));\n lines.push(chalk.gray(\"─\".repeat(60)));\n lines.push(`Abilities catalogued: ${chalk.cyan(String(r.total))}`);\n lines.push(`Flagged for review: ${chalk.yellow(String(r.flagged))} (${pct(r.flagged, r.total)})`);\n lines.push(\"\");\n lines.push(chalk.bold(\"Defect flags:\"));\n if (r.byFlag.length === 0) {\n lines.push(chalk.green(\" none — every ability's generated text is clean\"));\n } else {\n for (const { flag, count } of r.byFlag) {\n lines.push(` ${String(count).padStart(5)} ${chalk.gray(flag)}`);\n }\n }\n lines.push(\"\");\n lines.push(chalk.bold(\"Most-flagged factions:\"));\n const worst = [...r.byFaction].filter((f) => f.flagged > 0).sort((a, b) => b.flagged - a.flagged);\n for (const f of worst.slice(0, 15)) {\n lines.push(` ${String(f.flagged).padStart(4)}/${String(f.total).padEnd(4)} ${chalk.gray(f.faction)}`);\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nexport interface AuditPhrasingOptions {\n reporter?: \"pretty\" | \"json\";\n write?: boolean;\n}\n\n/** Commander action: run the phrasing audit over the repo's enrichment data. */\nexport function auditPhrasingCommand(opts: AuditPhrasingOptions = {}): void {\n const report = auditPhrasing(loadFactions());\n\n if (opts.write) {\n mkdirSync(AUDIT_DIR, { recursive: true });\n writeFileSync(resolve(AUDIT_DIR, \"phrasing.json\"), JSON.stringify(report, null, 2) + \"\\n\");\n writeFileSync(resolve(AUDIT_DIR, \"phrasing.csv\"), csvReport(report));\n }\n\n if (opts.reporter === \"json\") {\n console.log(JSON.stringify(report, null, 2));\n } else {\n console.log(prettyReport(report));\n if (opts.write) console.log(chalk.gray(`Wrote ${basename(AUDIT_DIR)}/phrasing.csv + phrasing.json`));\n }\n}\n\n// Direct-invocation entry point (`npx tsx tools/src/audit-phrasing.ts [--json] [--write]`).\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") === fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\nif (isMain) {\n const argv = process.argv.slice(2);\n auditPhrasingCommand({\n reporter: argv.includes(\"--json\") ? \"json\" : \"pretty\",\n write: argv.includes(\"--write\"),\n });\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-share-registry.d.ts","sourceRoot":"","sources":["../src/build-share-registry.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerates the share-token id registry (`data/share-registry.json`).
|
|
3
|
+
*
|
|
4
|
+
* The registry assigns every shareable entity id a stable integer index so the
|
|
5
|
+
* list-builder share codec can encode a roster as compact varints instead of
|
|
6
|
+
* full id strings (see `src/share/` and `docs/share-token.md`).
|
|
7
|
+
*
|
|
8
|
+
* **Append-only.** This tool never reorders or drops a slot: existing ids keep
|
|
9
|
+
* their index forever, genuinely-new ids are appended (sorted) at the end, and
|
|
10
|
+
* an id that has vanished from the dataset keeps its slot and is recorded in
|
|
11
|
+
* `tombstones`. Renames are author-curated: move the old id from `tombstones`
|
|
12
|
+
* into `aliases` ({ "old-id": "new-id" }) by hand. These invariants guarantee a
|
|
13
|
+
* newer registry can always decode an older token; only the reverse (an old
|
|
14
|
+
* package meeting a newer token) can fail, and it fails safe (stale-registry).
|
|
15
|
+
*
|
|
16
|
+
* Run intentionally via `npm run registry:build` and commit the diff — this is
|
|
17
|
+
* a migration step, not a per-build codegen. `codegen:data` separately inlines
|
|
18
|
+
* the committed JSON into `src/share/registry.generated.ts`.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, resolve } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { alliedRules, detachments, enhancements, factions, forceDispositions, units, wargear, weapons, } from "./data/index.js";
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const REGISTRY_PATH = resolve(__dirname, "../../data/share-registry.json");
|
|
26
|
+
/** Registry kinds, in a fixed order. The wire format references each by name. */
|
|
27
|
+
const KINDS = [
|
|
28
|
+
"faction",
|
|
29
|
+
"detachment",
|
|
30
|
+
"unit",
|
|
31
|
+
"wargear",
|
|
32
|
+
"enhancement",
|
|
33
|
+
"ally_rule",
|
|
34
|
+
"disposition",
|
|
35
|
+
];
|
|
36
|
+
/** Distinct ids currently in the dataset for each kind, sorted lexically. */
|
|
37
|
+
function currentIds() {
|
|
38
|
+
const distinct = (ids) => new Set(ids);
|
|
39
|
+
return {
|
|
40
|
+
faction: distinct(factions.all.map((f) => f.id)),
|
|
41
|
+
detachment: distinct(detachments.all.map((d) => d.id)),
|
|
42
|
+
unit: distinct(units.all.map((u) => u.id)),
|
|
43
|
+
// Loadout refs are either weapons or non-weapon wargear; they share one id
|
|
44
|
+
// space in the token, so the registry merges both collections.
|
|
45
|
+
wargear: distinct([...weapons.all.map((w) => w.id), ...wargear.all.map((w) => w.id)]),
|
|
46
|
+
enhancement: distinct(enhancements.all.map((e) => e.id)),
|
|
47
|
+
ally_rule: distinct(alliedRules.all.map((r) => r.id)),
|
|
48
|
+
disposition: distinct(forceDispositions.all.map((d) => d.id)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function emptyRegistry() {
|
|
52
|
+
return {
|
|
53
|
+
version: 0,
|
|
54
|
+
kinds: Object.fromEntries(KINDS.map((k) => [k, []])),
|
|
55
|
+
aliases: {},
|
|
56
|
+
tombstones: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function loadRegistry() {
|
|
60
|
+
if (!existsSync(REGISTRY_PATH))
|
|
61
|
+
return emptyRegistry();
|
|
62
|
+
const parsed = JSON.parse(readFileSync(REGISTRY_PATH, "utf-8"));
|
|
63
|
+
// Tolerate a registry authored before a kind existed.
|
|
64
|
+
for (const kind of KINDS)
|
|
65
|
+
parsed.kinds[kind] ??= [];
|
|
66
|
+
parsed.aliases ??= {};
|
|
67
|
+
parsed.tombstones ??= [];
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
function main() {
|
|
71
|
+
const prev = loadRegistry();
|
|
72
|
+
const current = currentIds();
|
|
73
|
+
const kinds = {};
|
|
74
|
+
const newlyRemoved = [];
|
|
75
|
+
let appended = 0;
|
|
76
|
+
for (const kind of KINDS) {
|
|
77
|
+
const existing = prev.kinds[kind] ?? [];
|
|
78
|
+
const have = new Set(existing);
|
|
79
|
+
// Append-only: new ids (sorted) after the preserved existing order.
|
|
80
|
+
const added = [...current[kind]].filter((id) => !have.has(id)).sort();
|
|
81
|
+
kinds[kind] = [...existing, ...added];
|
|
82
|
+
appended += added.length;
|
|
83
|
+
// A previously-allocated id absent from the dataset becomes a tombstone
|
|
84
|
+
// (its slot is retained above, so existing indices never shift).
|
|
85
|
+
for (const id of existing) {
|
|
86
|
+
if (!current[kind].has(id))
|
|
87
|
+
newlyRemoved.push(id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Tombstone anything newly removed that the author hasn't already mapped to a
|
|
91
|
+
// replacement via `aliases`. Preserve prior tombstones and aliases verbatim.
|
|
92
|
+
const tombstones = new Set(prev.tombstones);
|
|
93
|
+
for (const id of newlyRemoved) {
|
|
94
|
+
if (!(id in prev.aliases))
|
|
95
|
+
tombstones.add(id);
|
|
96
|
+
}
|
|
97
|
+
const changed = appended > 0 || tombstones.size !== prev.tombstones.length;
|
|
98
|
+
const next = {
|
|
99
|
+
version: changed ? prev.version + 1 : prev.version,
|
|
100
|
+
kinds,
|
|
101
|
+
aliases: prev.aliases,
|
|
102
|
+
tombstones: [...tombstones].sort(),
|
|
103
|
+
};
|
|
104
|
+
writeFileSync(REGISTRY_PATH, JSON.stringify(next, null, 2) + "\n");
|
|
105
|
+
const counts = KINDS.map((k) => `${k}=${kinds[k].length}`).join(", ");
|
|
106
|
+
if (changed) {
|
|
107
|
+
console.log(`Wrote ${REGISTRY_PATH}\n version ${prev.version} → ${next.version} ` +
|
|
108
|
+
`(+${appended} ids, ${newlyRemoved.length} tombstoned)\n ${counts}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(`No change: ${REGISTRY_PATH} already at version ${next.version}\n ${counts}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
main();
|
|
115
|
+
//# sourceMappingURL=build-share-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-share-registry.js","sourceRoot":"","sources":["../src/build-share-registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Regenerates the share-token id registry (`data/share-registry.json`).\n *\n * The registry assigns every shareable entity id a stable integer index so the\n * list-builder share codec can encode a roster as compact varints instead of\n * full id strings (see `src/share/` and `docs/share-token.md`).\n *\n * **Append-only.** This tool never reorders or drops a slot: existing ids keep\n * their index forever, genuinely-new ids are appended (sorted) at the end, and\n * an id that has vanished from the dataset keeps its slot and is recorded in\n * `tombstones`. Renames are author-curated: move the old id from `tombstones`\n * into `aliases` ({ \"old-id\": \"new-id\" }) by hand. These invariants guarantee a\n * newer registry can always decode an older token; only the reverse (an old\n * package meeting a newer token) can fail, and it fails safe (stale-registry).\n *\n * Run intentionally via `npm run registry:build` and commit the diff — this is\n * a migration step, not a per-build codegen. `codegen:data` separately inlines\n * the committed JSON into `src/share/registry.generated.ts`.\n */\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n alliedRules,\n detachments,\n enhancements,\n factions,\n forceDispositions,\n units,\n wargear,\n weapons,\n} from \"./data/index.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REGISTRY_PATH = resolve(__dirname, \"../../data/share-registry.json\");\n\n/** Registry kinds, in a fixed order. The wire format references each by name. */\nconst KINDS = [\n \"faction\",\n \"detachment\",\n \"unit\",\n \"wargear\",\n \"enhancement\",\n \"ally_rule\",\n \"disposition\",\n] as const;\ntype KindName = (typeof KINDS)[number];\n\n/** The committed registry artifact. */\ninterface ShareRegistry {\n version: number;\n kinds: Record<KindName, string[]>;\n /** Renames applied on decode: a tombstoned id → its current replacement. */\n aliases: Record<string, string>;\n /** Ids removed from the dataset but kept as slots so old indices stay valid. */\n tombstones: string[];\n}\n\n/** Distinct ids currently in the dataset for each kind, sorted lexically. */\nfunction currentIds(): Record<KindName, Set<string>> {\n const distinct = (ids: Iterable<string>): Set<string> => new Set(ids);\n return {\n faction: distinct(factions.all.map((f) => f.id)),\n detachment: distinct(detachments.all.map((d) => d.id)),\n unit: distinct(units.all.map((u) => u.id)),\n // Loadout refs are either weapons or non-weapon wargear; they share one id\n // space in the token, so the registry merges both collections.\n wargear: distinct([...weapons.all.map((w) => w.id), ...wargear.all.map((w) => w.id)]),\n enhancement: distinct(enhancements.all.map((e) => e.id)),\n ally_rule: distinct(alliedRules.all.map((r) => r.id)),\n disposition: distinct(forceDispositions.all.map((d) => d.id)),\n };\n}\n\nfunction emptyRegistry(): ShareRegistry {\n return {\n version: 0,\n kinds: Object.fromEntries(KINDS.map((k) => [k, [] as string[]])) as Record<\n KindName,\n string[]\n >,\n aliases: {},\n tombstones: [],\n };\n}\n\nfunction loadRegistry(): ShareRegistry {\n if (!existsSync(REGISTRY_PATH)) return emptyRegistry();\n const parsed = JSON.parse(readFileSync(REGISTRY_PATH, \"utf-8\")) as ShareRegistry;\n // Tolerate a registry authored before a kind existed.\n for (const kind of KINDS) parsed.kinds[kind] ??= [];\n parsed.aliases ??= {};\n parsed.tombstones ??= [];\n return parsed;\n}\n\nfunction main(): void {\n const prev = loadRegistry();\n const current = currentIds();\n\n const kinds = {} as Record<KindName, string[]>;\n const newlyRemoved: string[] = [];\n let appended = 0;\n\n for (const kind of KINDS) {\n const existing = prev.kinds[kind] ?? [];\n const have = new Set(existing);\n // Append-only: new ids (sorted) after the preserved existing order.\n const added = [...current[kind]].filter((id) => !have.has(id)).sort();\n kinds[kind] = [...existing, ...added];\n appended += added.length;\n // A previously-allocated id absent from the dataset becomes a tombstone\n // (its slot is retained above, so existing indices never shift).\n for (const id of existing) {\n if (!current[kind].has(id)) newlyRemoved.push(id);\n }\n }\n\n // Tombstone anything newly removed that the author hasn't already mapped to a\n // replacement via `aliases`. Preserve prior tombstones and aliases verbatim.\n const tombstones = new Set(prev.tombstones);\n for (const id of newlyRemoved) {\n if (!(id in prev.aliases)) tombstones.add(id);\n }\n\n const changed = appended > 0 || tombstones.size !== prev.tombstones.length;\n const next: ShareRegistry = {\n version: changed ? prev.version + 1 : prev.version,\n kinds,\n aliases: prev.aliases,\n tombstones: [...tombstones].sort(),\n };\n\n writeFileSync(REGISTRY_PATH, JSON.stringify(next, null, 2) + \"\\n\");\n\n const counts = KINDS.map((k) => `${k}=${kinds[k].length}`).join(\", \");\n if (changed) {\n console.log(\n `Wrote ${REGISTRY_PATH}\\n version ${prev.version} → ${next.version} ` +\n `(+${appended} ids, ${newlyRemoved.length} tombstoned)\\n ${counts}`,\n );\n } else {\n console.log(`No change: ${REGISTRY_PATH} already at version ${next.version}\\n ${counts}`);\n }\n}\n\nmain();\n"]}
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { validateAllCommand } from "./commands/validate-all.js";
|
|
|
6
6
|
import { translateCommand } from "./commands/translate.js";
|
|
7
7
|
import { importCommand } from "./commands/import.js";
|
|
8
8
|
import { auditCoverageCommand } from "./audit-coverage.js";
|
|
9
|
+
import { auditPhrasingCommand } from "./audit-phrasing.js";
|
|
9
10
|
import { populateBaseSizesCommand } from "./commands/populate-base-sizes.js";
|
|
10
11
|
const program = new Command();
|
|
11
12
|
program
|
|
@@ -38,6 +39,12 @@ program
|
|
|
38
39
|
.option("--reporter <mode>", "Output format: pretty or json", "pretty")
|
|
39
40
|
.option("--write", "Also write data/_audit/coverage.json + summary.md", false)
|
|
40
41
|
.action((opts) => auditCoverageCommand({ reporter: opts.reporter, write: opts.write }));
|
|
42
|
+
program
|
|
43
|
+
.command("audit-phrasing")
|
|
44
|
+
.description("Catalogue every ability's generated English text and flag phrasing defects, per faction")
|
|
45
|
+
.option("--reporter <mode>", "Output format: pretty or json", "pretty")
|
|
46
|
+
.option("--write", "Also write data/_audit/phrasing.csv + phrasing.json", false)
|
|
47
|
+
.action((opts) => auditPhrasingCommand({ reporter: opts.reporter, write: opts.write }));
|
|
41
48
|
program
|
|
42
49
|
.command("import")
|
|
43
50
|
.description("Import a ListForge army-list export into a 40kdc roster")
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAE7E,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,gBAAgB,CAAC;KACtB,WAAW,CAAC,2CAA2C,CAAC;KACxD,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,eAAe,CAAC;KACxB,WAAW,CAAC,0BAA0B,CAAC;KACvC,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAE/B,OAAO;KACJ,OAAO,CAAC,qBAAqB,CAAC;KAC9B,WAAW,CAAC,gCAAgC,CAAC;KAC7C,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAErC,OAAO;KACJ,OAAO,CAAC,cAAc,CAAC;KACvB,WAAW,CAAC,yBAAyB,CAAC;KACtC,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,kBAAkB,CAAC,CAAC;AAE9B,OAAO;KACJ,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,wCAAwC,CAAC;KACrD,QAAQ,CAAC,QAAQ,EAAE,6BAA6B,CAAC;KACjD,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAE5B,OAAO;KACJ,OAAO,CAAC,gBAAgB,CAAC;KACzB,WAAW,CAAC,yEAAyE,CAAC;KACtF,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,SAAS,EAAE,mDAAmD,EAAE,KAAK,CAAC;KAC7E,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,oBAAoB,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAE1F,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,yDAAyD,CAAC;KACtE,QAAQ,CAAC,SAAS,EAAE,wEAAwE,CAAC;KAC7F,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,MAAM,CAAC;KACpE,MAAM,CAAC,cAAc,EAAE,+CAA+C,CAAC;KACvE,MAAM,CAAC,aAAa,CAAC,CAAC;AAEzB,OAAO;KACJ,OAAO,CAAC,qBAAqB,CAAC;KAC9B,WAAW,CAAC,mGAAmG,CAAC;KAChH,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAEpC,OAAO,CAAC,KAAK,EAAE,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { validateCoreCommand } from \"./commands/validate-core.js\";\nimport { validateEnrichmentCommand } from \"./commands/validate-enrichment.js\";\nimport { validateAllCommand } from \"./commands/validate-all.js\";\nimport { translateCommand } from \"./commands/translate.js\";\nimport { importCommand } from \"./commands/import.js\";\nimport { auditCoverageCommand } from \"./audit-coverage.js\";\nimport { populateBaseSizesCommand } from \"./commands/populate-base-sizes.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"40kdc-validate\")\n .description(\"Validate 40kdc data files against schemas\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"validate-core\")\n .description(\"Validate core data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateCoreCommand);\n\nprogram\n .command(\"validate-enrichment\")\n .description(\"Validate enrichment data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateEnrichmentCommand);\n\nprogram\n .command(\"validate-all\")\n .description(\"Validate all data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateAllCommand);\n\nprogram\n .command(\"translate\")\n .description(\"Translate ability DSL to plain English\")\n .argument(\"[path]\", \"Path to abilities.json file\")\n .action(translateCommand);\n\nprogram\n .command(\"audit-coverage\")\n .description(\"Audit how much ability data translates into cruncher buffs, per faction\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .option(\"--write\", \"Also write data/_audit/coverage.json + summary.md\", false)\n .action((opts) => auditCoverageCommand({ reporter: opts.reporter, write: opts.write }));\n\nprogram\n .command(\"import\")\n .description(\"Import a ListForge army-list export into a 40kdc roster\")\n .argument(\"[input]\", \"ListForge URL, base64 segment, JSON, or file path (omit/'-' for stdin)\")\n .option(\"--reporter <mode>\", \"Output format: json or pretty\", \"json\")\n .option(\"--out <file>\", \"Write roster JSON to a file instead of stdout\")\n .action(importCommand);\n\nprogram\n .command(\"populate-base-sizes\")\n .description(\"Populate base_size_mm on units + composition models from the GW base-size guide (+ bevy fallback)\")\n .action(populateBaseSizesCommand);\n\nprogram.parse();\n"]}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { validateCoreCommand } from \"./commands/validate-core.js\";\nimport { validateEnrichmentCommand } from \"./commands/validate-enrichment.js\";\nimport { validateAllCommand } from \"./commands/validate-all.js\";\nimport { translateCommand } from \"./commands/translate.js\";\nimport { importCommand } from \"./commands/import.js\";\nimport { auditCoverageCommand } from \"./audit-coverage.js\";\nimport { auditPhrasingCommand } from \"./audit-phrasing.js\";\nimport { populateBaseSizesCommand } from \"./commands/populate-base-sizes.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"40kdc-validate\")\n .description(\"Validate 40kdc data files against schemas\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"validate-core\")\n .description(\"Validate core data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateCoreCommand);\n\nprogram\n .command(\"validate-enrichment\")\n .description(\"Validate enrichment data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateEnrichmentCommand);\n\nprogram\n .command(\"validate-all\")\n .description(\"Validate all data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateAllCommand);\n\nprogram\n .command(\"translate\")\n .description(\"Translate ability DSL to plain English\")\n .argument(\"[path]\", \"Path to abilities.json file\")\n .action(translateCommand);\n\nprogram\n .command(\"audit-coverage\")\n .description(\"Audit how much ability data translates into cruncher buffs, per faction\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .option(\"--write\", \"Also write data/_audit/coverage.json + summary.md\", false)\n .action((opts) => auditCoverageCommand({ reporter: opts.reporter, write: opts.write }));\n\nprogram\n .command(\"audit-phrasing\")\n .description(\"Catalogue every ability's generated English text and flag phrasing defects, per faction\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .option(\"--write\", \"Also write data/_audit/phrasing.csv + phrasing.json\", false)\n .action((opts) => auditPhrasingCommand({ reporter: opts.reporter, write: opts.write }));\n\nprogram\n .command(\"import\")\n .description(\"Import a ListForge army-list export into a 40kdc roster\")\n .argument(\"[input]\", \"ListForge URL, base64 segment, JSON, or file path (omit/'-' for stdin)\")\n .option(\"--reporter <mode>\", \"Output format: json or pretty\", \"json\")\n .option(\"--out <file>\", \"Write roster JSON to a file instead of stdout\")\n .action(importCommand);\n\nprogram\n .command(\"populate-base-sizes\")\n .description(\"Populate base_size_mm on units + composition models from the GW base-size guide (+ bevy fallback)\")\n .action(populateBaseSizesCommand);\n\nprogram.parse();\n"]}
|
package/dist/codegen-data.js
CHANGED
|
@@ -19,6 +19,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
19
19
|
const REPO_ROOT = resolve(__dirname, "../..");
|
|
20
20
|
const DATA_ROOTS = [join(REPO_ROOT, "data", "core"), join(REPO_ROOT, "data", "enrichment")];
|
|
21
21
|
const OUT_FILE = join(__dirname, "data", "bundle.generated.ts");
|
|
22
|
+
/** Committed share-token id registry (authored by `npm run registry:build`). */
|
|
23
|
+
const REGISTRY_IN = join(REPO_ROOT, "data", "share-registry.json");
|
|
24
|
+
const REGISTRY_OUT = join(__dirname, "share", "registry.generated.ts");
|
|
22
25
|
/** Directory names that hold examples/scratch data and must never be bundled. */
|
|
23
26
|
const EXCLUDED_DIRS = new Set(["_example", "_port-audit"]);
|
|
24
27
|
/** Map a data file's base name (sans `.json`) to its `RawData` collection key. */
|
|
@@ -31,6 +34,7 @@ const FILE_TO_COLLECTION = {
|
|
|
31
34
|
abilities: "abilities",
|
|
32
35
|
"phase-mappings": "phaseMappings",
|
|
33
36
|
detachments: "detachments",
|
|
37
|
+
allies: "alliedRules",
|
|
34
38
|
stratagems: "stratagems",
|
|
35
39
|
enhancements: "enhancements",
|
|
36
40
|
"leader-attachments": "leaderAttachments",
|
|
@@ -122,14 +126,33 @@ const JSON_TEXT = ${literal};
|
|
|
122
126
|
export const RAW_DATA: RawData = JSON.parse(JSON_TEXT) as RawData;
|
|
123
127
|
`;
|
|
124
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Inline the committed share-token registry as a parsed-once module, mirroring
|
|
131
|
+
* the dataset bundle so the codec stays filesystem-free. The registry itself is
|
|
132
|
+
* regenerated only by `npm run registry:build`; here we just embed it verbatim.
|
|
133
|
+
*/
|
|
134
|
+
function emitRegistry() {
|
|
135
|
+
const jsonText = readFileSync(REGISTRY_IN, "utf-8");
|
|
136
|
+
const literal = JSON.stringify(jsonText);
|
|
137
|
+
return `/* GENERATED by 'npm run codegen:data' from data/share-registry.json. DO NOT EDIT BY HAND. */
|
|
138
|
+
import type { ShareRegistry } from "./registry.js";
|
|
139
|
+
|
|
140
|
+
const JSON_TEXT = ${literal};
|
|
141
|
+
|
|
142
|
+
/** The committed share-token id registry, embedded at build time. */
|
|
143
|
+
export const SHARE_REGISTRY: ShareRegistry = JSON.parse(JSON_TEXT) as ShareRegistry;
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
125
146
|
function main() {
|
|
126
147
|
const data = build();
|
|
127
148
|
reportDuplicateIds(data);
|
|
128
149
|
writeFileSync(OUT_FILE, emit(data));
|
|
150
|
+
writeFileSync(REGISTRY_OUT, emitRegistry());
|
|
129
151
|
const counts = Object.keys(data)
|
|
130
152
|
.map((k) => `${k}=${data[k].length}`)
|
|
131
153
|
.join(", ");
|
|
132
154
|
console.log(`Wrote ${OUT_FILE}\n ${counts}`);
|
|
155
|
+
console.log(`Wrote ${REGISTRY_OUT}`);
|
|
133
156
|
}
|
|
134
157
|
main();
|
|
135
158
|
//# sourceMappingURL=codegen-data.js.map
|
package/dist/codegen-data.js.map
CHANGED
|
@@ -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 \"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
|
+
{"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/** Committed share-token id registry (authored by `npm run registry:build`). */\nconst REGISTRY_IN = join(REPO_ROOT, \"data\", \"share-registry.json\");\nconst REGISTRY_OUT = join(__dirname, \"share\", \"registry.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 allies: \"alliedRules\",\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\n/**\n * Inline the committed share-token registry as a parsed-once module, mirroring\n * the dataset bundle so the codec stays filesystem-free. The registry itself is\n * regenerated only by `npm run registry:build`; here we just embed it verbatim.\n */\nfunction emitRegistry(): string {\n const jsonText = readFileSync(REGISTRY_IN, \"utf-8\");\n const literal = JSON.stringify(jsonText);\n return `/* GENERATED by 'npm run codegen:data' from data/share-registry.json. DO NOT EDIT BY HAND. */\nimport type { ShareRegistry } from \"./registry.js\";\n\nconst JSON_TEXT = ${literal};\n\n/** The committed share-token id registry, embedded at build time. */\nexport const SHARE_REGISTRY: ShareRegistry = JSON.parse(JSON_TEXT) as ShareRegistry;\n`;\n}\n\nfunction main(): void {\n const data = build();\n reportDuplicateIds(data);\n writeFileSync(OUT_FILE, emit(data));\n writeFileSync(REGISTRY_OUT, emitRegistry());\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 console.log(`Wrote ${REGISTRY_OUT}`);\n}\n\nmain();\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/commands/translate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,wBAAsB,gBAAgB,CACpC,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/commands/translate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,wBAAsB,gBAAgB,CACpC,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { describeAbility } from "../translate/effect.js";
|
|
11
11
|
export async function translateCommand(path) {
|
|
12
12
|
const filePath = resolve(process.cwd(), path ?? "../data/enrichment/world-eaters/abilities.json");
|
|
13
13
|
const abilities = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
@@ -24,10 +24,7 @@ export async function translateCommand(path) {
|
|
|
24
24
|
console.log(`\n═══ ${a.name} [${a.ability_id}] ═══`);
|
|
25
25
|
if (meta.length)
|
|
26
26
|
console.log(` ${meta.join(" | ")}`);
|
|
27
|
-
console.log(
|
|
28
|
-
const scope = describeScope(a.scope);
|
|
29
|
-
if (scope)
|
|
30
|
-
console.log(scope);
|
|
27
|
+
console.log(describeAbility({ effect: a.effect, scope: a.scope }));
|
|
31
28
|
}
|
|
32
29
|
console.log(`\n── ${abilities.length} abilities translated ──`);
|
|
33
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/commands/translate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/commands/translate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAkC,MAAM,wBAAwB,CAAC;AAczF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAa;IAEb,MAAM,QAAQ,GAAG,OAAO,CACtB,OAAO,CAAC,GAAG,EAAE,EACb,IAAI,IAAI,gDAAgD,CACzD,CAAC;IACF,MAAM,SAAS,GAAc,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAEzE,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,IAAI,CAAC,CAAC,YAAY;YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;QAC9C,IAAI,CAAC,CAAC,QAAQ;YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,CAAC,CAAC,aAAa;YAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,CAAC,QAAQ,EAAE,MAAM;YAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAErE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,UAAU,OAAO,CAAC,CAAC;QACrD,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxD,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,QAAQ,SAAS,CAAC,MAAM,0BAA0B,CAAC,CAAC;AAClE,CAAC","sourcesContent":["/**\n * Translates ability DSL entries into plain English descriptions.\n *\n * Thin CLI shell over the shared `translate/effect.ts` describer (the\n * conformance-pinned `ability.print()`); this file only handles file loading\n * and per-ability presentation.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nimport { describeAbility, type Effect, type AbilityScope } from \"../translate/effect.js\";\n\ninterface Ability {\n ability_id: string;\n name: string;\n ability_type?: string;\n behavior?: string;\n detachment_id?: string | null;\n faction_id?: string | null;\n unit_ids?: string[];\n effect: Effect;\n scope?: AbilityScope;\n}\n\nexport async function translateCommand(\n path?: string\n): Promise<void> {\n const filePath = resolve(\n process.cwd(),\n path ?? \"../data/enrichment/world-eaters/abilities.json\"\n );\n const abilities: Ability[] = JSON.parse(readFileSync(filePath, \"utf-8\"));\n\n for (const a of abilities) {\n const meta: string[] = [];\n if (a.ability_type) meta.push(a.ability_type);\n if (a.behavior) meta.push(a.behavior);\n if (a.detachment_id) meta.push(`detachment: ${a.detachment_id}`);\n if (a.unit_ids?.length) meta.push(`units: ${a.unit_ids.join(\", \")}`);\n\n console.log(`\\n═══ ${a.name} [${a.ability_id}] ═══`);\n if (meta.length) console.log(` ${meta.join(\" | \")}`);\n console.log(describeAbility({ effect: a.effect, scope: a.scope }));\n }\n\n console.log(`\\n── ${abilities.length} abilities translated ──`);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-all.d.ts","sourceRoot":"","sources":["../../src/commands/validate-all.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate-all.d.ts","sourceRoot":"","sources":["../../src/commands/validate-all.ts"],"names":[],"mappings":"AAMA,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBlF"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createValidator } from "../schema-loader.js";
|
|
2
2
|
import { validateFiles } from "../validate.js";
|
|
3
|
+
import { checkReferentialIntegrity } from "../integrity.js";
|
|
3
4
|
import { formatReport } from "../report.js";
|
|
4
5
|
export async function validateAllCommand(opts) {
|
|
5
6
|
const ajv = createValidator();
|
|
7
|
+
const mode = opts.reporter;
|
|
6
8
|
const coreResult = await validateFiles(ajv, "core/**/*.json");
|
|
7
9
|
const enrichmentResult = await validateFiles(ajv, "enrichment/**/*.json");
|
|
8
10
|
const combined = {
|
|
@@ -12,9 +14,12 @@ export async function validateAllCommand(opts) {
|
|
|
12
14
|
failed: coreResult.failed + enrichmentResult.failed,
|
|
13
15
|
errors: [...coreResult.errors, ...enrichmentResult.errors],
|
|
14
16
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
console.log(formatReport(combined, mode));
|
|
18
|
+
// Cross-entity referential integrity (ability-ref resolution, faction_keyword
|
|
19
|
+
// membership) — checks that per-file JSON Schema validation cannot express.
|
|
20
|
+
const integrity = await checkReferentialIntegrity();
|
|
21
|
+
console.log(formatReport(integrity, mode, "40kdc Referential Integrity Report"));
|
|
22
|
+
if (combined.failed > 0 || integrity.failed > 0) {
|
|
18
23
|
process.exit(1);
|
|
19
24
|
}
|
|
20
25
|
}
|