@alpaca-software/40kdc-data 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/author-input.d.ts +20 -1
- package/dist/author-input.d.ts.map +1 -1
- package/dist/author-input.js +64 -8
- package/dist/author-input.js.map +1 -1
- package/dist/author-seed.d.ts +62 -0
- package/dist/author-seed.d.ts.map +1 -0
- package/dist/author-seed.js +194 -0
- package/dist/author-seed.js.map +1 -0
- package/dist/commands/translate.d.ts.map +1 -1
- package/dist/commands/translate.js +6 -68
- package/dist/commands/translate.js.map +1 -1
- package/dist/cruncher/buffs.d.ts +57 -1
- package/dist/cruncher/buffs.d.ts.map +1 -1
- package/dist/cruncher/buffs.js +32 -3
- package/dist/cruncher/buffs.js.map +1 -1
- package/dist/cruncher/engine.d.ts.map +1 -1
- package/dist/cruncher/engine.js +50 -15
- package/dist/cruncher/engine.js.map +1 -1
- package/dist/cruncher/from-dsl.d.ts.map +1 -1
- package/dist/cruncher/from-dsl.js +121 -6
- package/dist/cruncher/from-dsl.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/normalize.d.ts.map +1 -1
- package/dist/data/normalize.js +8 -1
- package/dist/data/normalize.js.map +1 -1
- package/dist/gen-conformance.js +22 -1
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +181 -146
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +85 -24
- package/dist/runner.js.map +1 -1
- package/dist/scrub-defensive-flag.d.ts +23 -0
- package/dist/scrub-defensive-flag.d.ts.map +1 -0
- package/dist/scrub-defensive-flag.js +149 -0
- package/dist/scrub-defensive-flag.js.map +1 -0
- package/dist/translate/condition.d.ts +26 -0
- package/dist/translate/condition.d.ts.map +1 -0
- package/dist/translate/condition.js +171 -0
- package/dist/translate/condition.js.map +1 -0
- package/dist/translate/index.d.ts +9 -0
- package/dist/translate/index.d.ts.map +1 -0
- package/dist/translate/index.js +9 -0
- package/dist/translate/index.js.map +1 -0
- package/dist/translate/scoring.d.ts +38 -0
- package/dist/translate/scoring.d.ts.map +1 -0
- package/dist/translate/scoring.js +80 -0
- package/dist/translate/scoring.js.map +1 -0
- package/package.json +3 -1
- package/schemas/core/secondary-card.schema.json +50 -28
- package/schemas/enrichment/ability-dsl/condition.schema.json +5 -2
- package/schemas/enrichment/ability-dsl/effect.schema.json +2 -1
package/dist/author-input.d.ts
CHANGED
|
@@ -20,14 +20,33 @@ export interface AuthorInputEntry {
|
|
|
20
20
|
reason?: string;
|
|
21
21
|
src?: SourceRule;
|
|
22
22
|
}
|
|
23
|
-
interface ArchiveIndex {
|
|
23
|
+
export interface ArchiveIndex {
|
|
24
24
|
/** kebab faction id → archive faction code (e.g. "adepta-sororitas" → "AS"). */
|
|
25
25
|
factionCode: (kebab: string) => string | undefined;
|
|
26
26
|
/** `${unitName}|${factionCode}` (lower) → datasheet ids. */
|
|
27
27
|
datasheetsFor: (unitName: string, code: string) => string[];
|
|
28
28
|
/** datasheet_id → (abilityName lower → rule). */
|
|
29
29
|
ruleFor: (datasheetId: string, abilityName: string) => Omit<SourceRule, "datasheet_id"> | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* datasheet_id → every ability on that datasheet, as `[name, rule]` pairs.
|
|
32
|
+
* `name` is the archive's original (non-normalized) ability name. Used by the
|
|
33
|
+
* seed tool to enumerate a zero-ability unit's datasheet abilities, where
|
|
34
|
+
* {@link ruleFor} only answers a name we already know.
|
|
35
|
+
*/
|
|
36
|
+
abilitiesFor: (datasheetId: string) => Array<[string, Omit<SourceRule, "datasheet_id">]>;
|
|
37
|
+
/**
|
|
38
|
+
* Fallback for faction- and detachment-scoped rules that live in the archive's
|
|
39
|
+
* `Abilities.json` / `Detachment_abilities.json`, NOT on any unit datasheet
|
|
40
|
+
* (e.g. "Battle Focus", "Sagas", "Mission Tactics"). The unit→datasheet chain
|
|
41
|
+
* can't reach these, so {@link resolveSource} falls back here. Matches by
|
|
42
|
+
* normalized name (allowing the archive's faction-prefixed form, e.g.
|
|
43
|
+
* "Deathwatch Mission Tactics" for "Mission Tactics"); prefers a faction-code
|
|
44
|
+
* match, else accepts a globally-unique name match (so cross-faction filing —
|
|
45
|
+
* a Deathwatch unit under `adeptus-astartes` — still resolves safely).
|
|
46
|
+
*/
|
|
47
|
+
factionRuleFor: (code: string | undefined, abilityName: string) => Omit<SourceRule, "datasheet_id"> | undefined;
|
|
30
48
|
}
|
|
49
|
+
export declare function loadArchive(): ArchiveIndex;
|
|
31
50
|
/** Resolve the source rule for one stub via the unit→datasheet→ability chain. */
|
|
32
51
|
export declare function resolveSource(archive: ArchiveIndex, factionCode: string | undefined, unitNames: string[], abilityName: string): {
|
|
33
52
|
src?: SourceRule;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"author-input.d.ts","sourceRoot":"","sources":["../src/author-input.ts"],"names":[],"mappings":"AAoCA,KAAK,IAAI,GAAG,GAAG,CAAC;AAKhB,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,IAAI,CAAC;IACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAED,
|
|
1
|
+
{"version":3,"file":"author-input.d.ts","sourceRoot":"","sources":["../src/author-input.ts"],"names":[],"mappings":"AAoCA,KAAK,IAAI,GAAG,GAAG,CAAC;AAKhB,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,IAAI,CAAC;IACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACnD,4DAA4D;IAC5D,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC5D,iDAAiD;IACjD,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,GAAG,SAAS,CAAC;IACpG;;;;;OAKG;IACH,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,KAAK,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACzF;;;;;;;;;OASG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,GAAG,SAAS,CAAC;CACjH;AAYD,wBAAgB,WAAW,IAAI,YAAY,CAiF1C;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,YAAY,EACrB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EAAE,EACnB,WAAW,EAAE,MAAM,GAClB;IAAE,GAAG,CAAC,EAAE,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBvC"}
|
package/dist/author-input.js
CHANGED
|
@@ -42,7 +42,7 @@ const FACTION_CODE_ALIAS = {
|
|
|
42
42
|
"tau-empire": "TAU", // archive "T'au Empire"
|
|
43
43
|
"leagues-of-votann": "LoV", // archive "Votann"
|
|
44
44
|
};
|
|
45
|
-
function loadArchive() {
|
|
45
|
+
export function loadArchive() {
|
|
46
46
|
const factions = readJSON(resolve(ARCHIVE, "Factions.json"));
|
|
47
47
|
const codeByName = new Map();
|
|
48
48
|
for (const f of factions)
|
|
@@ -63,25 +63,76 @@ function loadArchive() {
|
|
|
63
63
|
byDatasheet.set(a.datasheet_id, (m = new Map()));
|
|
64
64
|
if (!m.has(norm(a.name))) {
|
|
65
65
|
m.set(norm(a.name), {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
name: a.name,
|
|
67
|
+
rule: {
|
|
68
|
+
src_type: a.type ?? null,
|
|
69
|
+
parameter: a.parameter ?? null,
|
|
70
|
+
phases: a.phases ?? null,
|
|
71
|
+
description: a.description ?? "",
|
|
72
|
+
},
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
}
|
|
76
|
+
const factionRules = [];
|
|
77
|
+
const pushFactionRules = (file, srcType) => {
|
|
78
|
+
let rows;
|
|
79
|
+
try {
|
|
80
|
+
rows = readJSON(resolve(ARCHIVE, file));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const r of rows) {
|
|
86
|
+
if (!r.name)
|
|
87
|
+
continue;
|
|
88
|
+
factionRules.push({
|
|
89
|
+
norm: norm(r.name),
|
|
90
|
+
code: r.faction_id ?? "",
|
|
91
|
+
rule: { src_type: srcType, parameter: r.parameter ?? null, phases: r.phases ?? null, description: r.description ?? "" },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
pushFactionRules("Abilities.json", "Faction");
|
|
96
|
+
pushFactionRules("Detachment_abilities.json", "Detachment");
|
|
97
|
+
// Match by exact normalized name OR the archive's faction-prefixed form
|
|
98
|
+
// ("deathwatch mission tactics" matches "mission tactics").
|
|
99
|
+
const nameMatches = (archiveNorm, queryNorm) => archiveNorm === queryNorm || archiveNorm.endsWith(` ${queryNorm}`);
|
|
73
100
|
return {
|
|
74
101
|
factionCode: (kebab) => FACTION_CODE_ALIAS[kebab] ?? codeByName.get(kebab.replace(/-/g, " ")),
|
|
75
102
|
datasheetsFor: (unitName, code) => dsByNameCode.get(`${norm(unitName)}|${code}`) ?? [],
|
|
76
|
-
ruleFor: (dsId, abilityName) => byDatasheet.get(dsId)?.get(norm(abilityName)),
|
|
103
|
+
ruleFor: (dsId, abilityName) => byDatasheet.get(dsId)?.get(norm(abilityName))?.rule,
|
|
104
|
+
abilitiesFor: (dsId) => Array.from(byDatasheet.get(dsId)?.values() ?? []).map((e) => [e.name, e.rule]),
|
|
105
|
+
factionRuleFor: (code, abilityName) => {
|
|
106
|
+
const q = norm(abilityName);
|
|
107
|
+
// A rule with no description is useless for authoring — skip it so we
|
|
108
|
+
// never hand the model empty source text (better to report unresolved).
|
|
109
|
+
const hits = factionRules.filter((r) => nameMatches(r.norm, q) && r.rule.description.trim() !== "");
|
|
110
|
+
if (hits.length === 0)
|
|
111
|
+
return undefined;
|
|
112
|
+
// Prefer a faction-code match; else only accept a globally-unique name so
|
|
113
|
+
// we never silently bind to the wrong faction's same-named rule.
|
|
114
|
+
const byCode = code ? hits.filter((r) => r.code === code) : [];
|
|
115
|
+
if (byCode.length > 0)
|
|
116
|
+
return byCode[0].rule;
|
|
117
|
+
const exact = hits.filter((r) => r.norm === q);
|
|
118
|
+
if (exact.length === 1)
|
|
119
|
+
return exact[0].rule;
|
|
120
|
+
return hits.length === 1 ? hits[0].rule : undefined;
|
|
121
|
+
},
|
|
77
122
|
};
|
|
78
123
|
}
|
|
79
124
|
/** Resolve the source rule for one stub via the unit→datasheet→ability chain. */
|
|
80
125
|
export function resolveSource(archive, factionCode, unitNames, abilityName) {
|
|
81
126
|
if (!factionCode)
|
|
82
127
|
return { reason: "no archive faction code for this faction" };
|
|
83
|
-
|
|
84
|
-
|
|
128
|
+
// Faction/detachment-scoped rules have no unit to chain through — go straight
|
|
129
|
+
// to the faction-rule fallback (Abilities.json / Detachment_abilities.json).
|
|
130
|
+
if (unitNames.length === 0) {
|
|
131
|
+
const fr = archive.factionRuleFor(factionCode, abilityName);
|
|
132
|
+
if (fr)
|
|
133
|
+
return { src: { datasheet_id: "", ...fr } };
|
|
134
|
+
return { reason: "ability has no unit_ids and no matching faction/detachment rule" };
|
|
135
|
+
}
|
|
85
136
|
const tried = [];
|
|
86
137
|
for (const unitName of unitNames) {
|
|
87
138
|
const dsIds = archive.datasheetsFor(unitName, factionCode);
|
|
@@ -93,6 +144,11 @@ export function resolveSource(archive, factionCode, unitNames, abilityName) {
|
|
|
93
144
|
return { src: { datasheet_id: dsId, ...rule } };
|
|
94
145
|
}
|
|
95
146
|
}
|
|
147
|
+
// Datasheet join found nothing — the ability may be a faction/detachment rule
|
|
148
|
+
// surfaced on the unit (e.g. "Mission Tactics" on a Deathwatch kill team).
|
|
149
|
+
const fr = archive.factionRuleFor(factionCode, abilityName);
|
|
150
|
+
if (fr)
|
|
151
|
+
return { src: { datasheet_id: "", ...fr } };
|
|
96
152
|
return { reason: `no matching ability on faction datasheets (tried: ${tried.join(", ") || unitNames.join(", ")})` };
|
|
97
153
|
}
|
|
98
154
|
function buildFaction(faction, archive) {
|
package/dist/author-input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"author-input.js","sourceRoot":"","sources":["../src/author-input.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Build the authoring input for the DSL stub fan-out (#21): join each\n * empty-modifier stub to its *correct* source rule in the 10e archive.\n *\n * The naive `ability.name → Datasheets_abilities.name` join is unsafe — ability\n * names collide across datasheets and factions (e.g. \"Simulacrum Imperialis\"\n * exists on a Sororitas *and* an Agents-of-the-Imperium \"Sanctifiers\" datasheet\n * with different rules). We disambiguate by chaining through the unit that\n * carries the ability and the faction it belongs to:\n *\n * ability.unit_ids → core unit.name → archive Datasheet (name + faction code)\n * → datasheet_id → Datasheets_abilities (datasheet_id + ability name)\n *\n * Output: data/_audit/author-input/<faction>.json — one entry per stub with the\n * resolved source rule (or `resolved:false` + a reason when the chain breaks),\n * ready to feed the classify→assemble→verify workflow.\n *\n * The archive lives outside the repo; point `ARMY_ASSIST_JSON` at it or rely on\n * the default `~/army-assist/src/assets/json`.\n *\n * Usage: npx tsx tools/src/author-input.ts [faction|--all]\n */\nimport { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\nimport { hasEmptyModifier } from \"./audit-coverage.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\nconst ENRICHMENT_ROOT = resolve(DATA_ROOT, \"enrichment\");\nconst CORE_ROOT = resolve(DATA_ROOT, \"core\");\nconst OUT_DIR = resolve(DATA_ROOT, \"_audit\", \"author-input\");\nconst ARCHIVE = process.env.ARMY_ASSIST_JSON ?? resolve(homedir(), \"army-assist/src/assets/json\");\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Json = any;\n\nconst readJSON = (p: string): Json => JSON.parse(readFileSync(p, \"utf-8\"));\nconst norm = (s: string): string => s.toLowerCase().trim();\n\nexport interface SourceRule {\n datasheet_id: string;\n src_type: string | null;\n parameter: string | null;\n phases: string[] | null;\n description: string;\n}\n\nexport interface AuthorInputEntry {\n faction: string;\n ability_id: string;\n name: string;\n unit_ids: string[];\n target: string | null;\n scope: Json;\n faction_id: string | null;\n ability_type: string | null;\n resolved: boolean;\n /** Why the source rule couldn't be resolved (when `resolved` is false). */\n reason?: string;\n src?: SourceRule;\n}\n\ninterface ArchiveIndex {\n /** kebab faction id → archive faction code (e.g. \"adepta-sororitas\" → \"AS\"). */\n factionCode: (kebab: string) => string | undefined;\n /** `${unitName}|${factionCode}` (lower) → datasheet ids. */\n datasheetsFor: (unitName: string, code: string) => string[];\n /** datasheet_id → (abilityName lower → rule). */\n ruleFor: (datasheetId: string, abilityName: string) => Omit<SourceRule, \"datasheet_id\"> | undefined;\n}\n\n/**\n * Kebab faction id → archive code, for the cases name-normalization can't bridge\n * (apostrophes the kebab drops, or an archive name that uses different words).\n */\nconst FACTION_CODE_ALIAS: Record<string, string> = {\n \"emperors-children\": \"EC\", // archive \"Emperor's Children\"\n \"tau-empire\": \"TAU\", // archive \"T'au Empire\"\n \"leagues-of-votann\": \"LoV\", // archive \"Votann\"\n};\n\nfunction loadArchive(): ArchiveIndex {\n const factions: Json[] = readJSON(resolve(ARCHIVE, \"Factions.json\"));\n const codeByName = new Map<string, string>();\n for (const f of factions) codeByName.set(norm(f.name), f.id);\n\n const datasheets: Json[] = readJSON(resolve(ARCHIVE, \"Datasheets.json\"));\n const dsByNameCode = new Map<string, string[]>();\n for (const d of datasheets) {\n const key = `${norm(d.name)}|${d.faction_id}`;\n (dsByNameCode.get(key) ?? dsByNameCode.set(key, []).get(key)!).push(d.id);\n }\n\n const dsAbilities: Json[] = readJSON(resolve(ARCHIVE, \"Datasheets_abilities.json\"));\n const byDatasheet = new Map<string, Map<string, Omit<SourceRule, \"datasheet_id\">>>();\n for (const a of dsAbilities) {\n if (!a.datasheet_id || !a.name) continue;\n let m = byDatasheet.get(a.datasheet_id);\n if (!m) byDatasheet.set(a.datasheet_id, (m = new Map()));\n if (!m.has(norm(a.name))) {\n m.set(norm(a.name), {\n src_type: a.type ?? null,\n parameter: a.parameter ?? null,\n phases: a.phases ?? null,\n description: a.description ?? \"\",\n });\n }\n }\n\n return {\n factionCode: (kebab) => FACTION_CODE_ALIAS[kebab] ?? codeByName.get(kebab.replace(/-/g, \" \")),\n datasheetsFor: (unitName, code) => dsByNameCode.get(`${norm(unitName)}|${code}`) ?? [],\n ruleFor: (dsId, abilityName) => byDatasheet.get(dsId)?.get(norm(abilityName)),\n };\n}\n\n/** Resolve the source rule for one stub via the unit→datasheet→ability chain. */\nexport function resolveSource(\n archive: ArchiveIndex,\n factionCode: string | undefined,\n unitNames: string[],\n abilityName: string,\n): { src?: SourceRule; reason?: string } {\n if (!factionCode) return { reason: \"no archive faction code for this faction\" };\n if (unitNames.length === 0) return { reason: \"ability has no unit_ids (faction/detachment scope) — datasheet join needs a unit\" };\n const tried: string[] = [];\n for (const unitName of unitNames) {\n const dsIds = archive.datasheetsFor(unitName, factionCode);\n if (dsIds.length === 0) tried.push(`${unitName}:no-datasheet`);\n for (const dsId of dsIds) {\n const rule = archive.ruleFor(dsId, abilityName);\n if (rule) return { src: { datasheet_id: dsId, ...rule } };\n }\n }\n return { reason: `no matching ability on faction datasheets (tried: ${tried.join(\", \") || unitNames.join(\", \")})` };\n}\n\nfunction buildFaction(faction: string, archive: ArchiveIndex): AuthorInputEntry[] {\n const code = archive.factionCode(faction);\n const unitsPath = resolve(CORE_ROOT, faction, \"units.json\");\n const unitName = new Map<string, string>();\n if (existsSync(unitsPath)) for (const u of readJSON(unitsPath) as Json[]) unitName.set(u.id, u.name);\n\n const abilitiesPath = resolve(ENRICHMENT_ROOT, faction, \"abilities.json\");\n if (!existsSync(abilitiesPath)) return [];\n const abilities: Json[] = readJSON(abilitiesPath);\n\n const out: AuthorInputEntry[] = [];\n for (const a of abilities) {\n if (!hasEmptyModifier(a.effect)) continue;\n const unitIds: string[] = a.unit_ids ?? [];\n const unitNames = unitIds.map((id) => unitName.get(id)).filter((n): n is string => !!n);\n const { src, reason } = resolveSource(archive, code, unitNames, a.name);\n out.push({\n faction,\n ability_id: a.ability_id,\n name: a.name,\n unit_ids: unitIds,\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 resolved: !!src,\n ...(src ? { src } : { reason }),\n });\n }\n return out;\n}\n\nfunction main(): void {\n const arg = process.argv[2];\n if (!arg) {\n console.error(\"Usage: npx tsx tools/src/author-input.ts <faction|--all>\");\n process.exit(1);\n }\n const archive = loadArchive();\n const factions =\n arg === \"--all\"\n ? readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })\n .filter((e) => e.isDirectory() && e.name !== \"_example\")\n .map((e) => e.name)\n : [arg];\n\n mkdirSync(OUT_DIR, { recursive: true });\n let totalStubs = 0;\n let totalResolved = 0;\n for (const faction of factions) {\n const entries = buildFaction(faction, archive);\n if (entries.length === 0) continue;\n const resolved = entries.filter((e) => e.resolved).length;\n totalStubs += entries.length;\n totalResolved += resolved;\n writeFileSync(resolve(OUT_DIR, `${faction}.json`), JSON.stringify(entries, null, 2) + \"\\n\");\n console.log(` ${faction}: ${entries.length} stubs, ${resolved} source-resolved`);\n }\n console.log(`\\n${totalStubs} stubs total, ${totalResolved} resolved (${Math.round((100 * totalResolved) / Math.max(1, totalStubs))}%). → ${OUT_DIR}`);\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"]}
|
|
1
|
+
{"version":3,"file":"author-input.js","sourceRoot":"","sources":["../src/author-input.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Build the authoring input for the DSL stub fan-out (#21): join each\n * empty-modifier stub to its *correct* source rule in the 10e archive.\n *\n * The naive `ability.name → Datasheets_abilities.name` join is unsafe — ability\n * names collide across datasheets and factions (e.g. \"Simulacrum Imperialis\"\n * exists on a Sororitas *and* an Agents-of-the-Imperium \"Sanctifiers\" datasheet\n * with different rules). We disambiguate by chaining through the unit that\n * carries the ability and the faction it belongs to:\n *\n * ability.unit_ids → core unit.name → archive Datasheet (name + faction code)\n * → datasheet_id → Datasheets_abilities (datasheet_id + ability name)\n *\n * Output: data/_audit/author-input/<faction>.json — one entry per stub with the\n * resolved source rule (or `resolved:false` + a reason when the chain breaks),\n * ready to feed the classify→assemble→verify workflow.\n *\n * The archive lives outside the repo; point `ARMY_ASSIST_JSON` at it or rely on\n * the default `~/army-assist/src/assets/json`.\n *\n * Usage: npx tsx tools/src/author-input.ts [faction|--all]\n */\nimport { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\nimport { hasEmptyModifier } from \"./audit-coverage.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\nconst ENRICHMENT_ROOT = resolve(DATA_ROOT, \"enrichment\");\nconst CORE_ROOT = resolve(DATA_ROOT, \"core\");\nconst OUT_DIR = resolve(DATA_ROOT, \"_audit\", \"author-input\");\nconst ARCHIVE = process.env.ARMY_ASSIST_JSON ?? resolve(homedir(), \"army-assist/src/assets/json\");\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Json = any;\n\nconst readJSON = (p: string): Json => JSON.parse(readFileSync(p, \"utf-8\"));\nconst norm = (s: string): string => s.toLowerCase().trim();\n\nexport interface SourceRule {\n datasheet_id: string;\n src_type: string | null;\n parameter: string | null;\n phases: string[] | null;\n description: string;\n}\n\nexport interface AuthorInputEntry {\n faction: string;\n ability_id: string;\n name: string;\n unit_ids: string[];\n target: string | null;\n scope: Json;\n faction_id: string | null;\n ability_type: string | null;\n resolved: boolean;\n /** Why the source rule couldn't be resolved (when `resolved` is false). */\n reason?: string;\n src?: SourceRule;\n}\n\nexport interface ArchiveIndex {\n /** kebab faction id → archive faction code (e.g. \"adepta-sororitas\" → \"AS\"). */\n factionCode: (kebab: string) => string | undefined;\n /** `${unitName}|${factionCode}` (lower) → datasheet ids. */\n datasheetsFor: (unitName: string, code: string) => string[];\n /** datasheet_id → (abilityName lower → rule). */\n ruleFor: (datasheetId: string, abilityName: string) => Omit<SourceRule, \"datasheet_id\"> | undefined;\n /**\n * datasheet_id → every ability on that datasheet, as `[name, rule]` pairs.\n * `name` is the archive's original (non-normalized) ability name. Used by the\n * seed tool to enumerate a zero-ability unit's datasheet abilities, where\n * {@link ruleFor} only answers a name we already know.\n */\n abilitiesFor: (datasheetId: string) => Array<[string, Omit<SourceRule, \"datasheet_id\">]>;\n /**\n * Fallback for faction- and detachment-scoped rules that live in the archive's\n * `Abilities.json` / `Detachment_abilities.json`, NOT on any unit datasheet\n * (e.g. \"Battle Focus\", \"Sagas\", \"Mission Tactics\"). The unit→datasheet chain\n * can't reach these, so {@link resolveSource} falls back here. Matches by\n * normalized name (allowing the archive's faction-prefixed form, e.g.\n * \"Deathwatch Mission Tactics\" for \"Mission Tactics\"); prefers a faction-code\n * match, else accepts a globally-unique name match (so cross-faction filing —\n * a Deathwatch unit under `adeptus-astartes` — still resolves safely).\n */\n factionRuleFor: (code: string | undefined, abilityName: string) => Omit<SourceRule, \"datasheet_id\"> | undefined;\n}\n\n/**\n * Kebab faction id → archive code, for the cases name-normalization can't bridge\n * (apostrophes the kebab drops, or an archive name that uses different words).\n */\nconst FACTION_CODE_ALIAS: Record<string, string> = {\n \"emperors-children\": \"EC\", // archive \"Emperor's Children\"\n \"tau-empire\": \"TAU\", // archive \"T'au Empire\"\n \"leagues-of-votann\": \"LoV\", // archive \"Votann\"\n};\n\nexport function loadArchive(): ArchiveIndex {\n const factions: Json[] = readJSON(resolve(ARCHIVE, \"Factions.json\"));\n const codeByName = new Map<string, string>();\n for (const f of factions) codeByName.set(norm(f.name), f.id);\n\n const datasheets: Json[] = readJSON(resolve(ARCHIVE, \"Datasheets.json\"));\n const dsByNameCode = new Map<string, string[]>();\n for (const d of datasheets) {\n const key = `${norm(d.name)}|${d.faction_id}`;\n (dsByNameCode.get(key) ?? dsByNameCode.set(key, []).get(key)!).push(d.id);\n }\n\n const dsAbilities: Json[] = readJSON(resolve(ARCHIVE, \"Datasheets_abilities.json\"));\n // datasheet_id → (normalized name → { name: original, rule }). The original\n // name is retained so the seed tool can author from it; ruleFor keys on the\n // normalized name to match a name the caller already holds.\n type Entry = { name: string; rule: Omit<SourceRule, \"datasheet_id\"> };\n const byDatasheet = new Map<string, Map<string, Entry>>();\n for (const a of dsAbilities) {\n if (!a.datasheet_id || !a.name) continue;\n let m = byDatasheet.get(a.datasheet_id);\n if (!m) byDatasheet.set(a.datasheet_id, (m = new Map()));\n if (!m.has(norm(a.name))) {\n m.set(norm(a.name), {\n name: a.name,\n rule: {\n src_type: a.type ?? null,\n parameter: a.parameter ?? null,\n phases: a.phases ?? null,\n description: a.description ?? \"\",\n },\n });\n }\n }\n\n // Faction- and detachment-scoped rules (no datasheet). Each carries a\n // normalized name, faction code, and the source rule. `srcType` distinguishes\n // the two archive files for provenance.\n type FactionRule = { norm: string; code: string; rule: Omit<SourceRule, \"datasheet_id\"> };\n const factionRules: FactionRule[] = [];\n const pushFactionRules = (file: string, srcType: string): void => {\n let rows: Json[];\n try { rows = readJSON(resolve(ARCHIVE, file)); } catch { return; }\n for (const r of rows) {\n if (!r.name) continue;\n factionRules.push({\n norm: norm(r.name),\n code: r.faction_id ?? \"\",\n rule: { src_type: srcType, parameter: r.parameter ?? null, phases: r.phases ?? null, description: r.description ?? \"\" },\n });\n }\n };\n pushFactionRules(\"Abilities.json\", \"Faction\");\n pushFactionRules(\"Detachment_abilities.json\", \"Detachment\");\n\n // Match by exact normalized name OR the archive's faction-prefixed form\n // (\"deathwatch mission tactics\" matches \"mission tactics\").\n const nameMatches = (archiveNorm: string, queryNorm: string): boolean =>\n archiveNorm === queryNorm || archiveNorm.endsWith(` ${queryNorm}`);\n\n return {\n factionCode: (kebab) => FACTION_CODE_ALIAS[kebab] ?? codeByName.get(kebab.replace(/-/g, \" \")),\n datasheetsFor: (unitName, code) => dsByNameCode.get(`${norm(unitName)}|${code}`) ?? [],\n ruleFor: (dsId, abilityName) => byDatasheet.get(dsId)?.get(norm(abilityName))?.rule,\n abilitiesFor: (dsId) =>\n Array.from(byDatasheet.get(dsId)?.values() ?? []).map((e) => [e.name, e.rule]),\n factionRuleFor: (code, abilityName) => {\n const q = norm(abilityName);\n // A rule with no description is useless for authoring — skip it so we\n // never hand the model empty source text (better to report unresolved).\n const hits = factionRules.filter((r) => nameMatches(r.norm, q) && r.rule.description.trim() !== \"\");\n if (hits.length === 0) return undefined;\n // Prefer a faction-code match; else only accept a globally-unique name so\n // we never silently bind to the wrong faction's same-named rule.\n const byCode = code ? hits.filter((r) => r.code === code) : [];\n if (byCode.length > 0) return byCode[0].rule;\n const exact = hits.filter((r) => r.norm === q);\n if (exact.length === 1) return exact[0].rule;\n return hits.length === 1 ? hits[0].rule : undefined;\n },\n };\n}\n\n/** Resolve the source rule for one stub via the unit→datasheet→ability chain. */\nexport function resolveSource(\n archive: ArchiveIndex,\n factionCode: string | undefined,\n unitNames: string[],\n abilityName: string,\n): { src?: SourceRule; reason?: string } {\n if (!factionCode) return { reason: \"no archive faction code for this faction\" };\n // Faction/detachment-scoped rules have no unit to chain through — go straight\n // to the faction-rule fallback (Abilities.json / Detachment_abilities.json).\n if (unitNames.length === 0) {\n const fr = archive.factionRuleFor(factionCode, abilityName);\n if (fr) return { src: { datasheet_id: \"\", ...fr } };\n return { reason: \"ability has no unit_ids and no matching faction/detachment rule\" };\n }\n const tried: string[] = [];\n for (const unitName of unitNames) {\n const dsIds = archive.datasheetsFor(unitName, factionCode);\n if (dsIds.length === 0) tried.push(`${unitName}:no-datasheet`);\n for (const dsId of dsIds) {\n const rule = archive.ruleFor(dsId, abilityName);\n if (rule) return { src: { datasheet_id: dsId, ...rule } };\n }\n }\n // Datasheet join found nothing — the ability may be a faction/detachment rule\n // surfaced on the unit (e.g. \"Mission Tactics\" on a Deathwatch kill team).\n const fr = archive.factionRuleFor(factionCode, abilityName);\n if (fr) return { src: { datasheet_id: \"\", ...fr } };\n return { reason: `no matching ability on faction datasheets (tried: ${tried.join(\", \") || unitNames.join(\", \")})` };\n}\n\nfunction buildFaction(faction: string, archive: ArchiveIndex): AuthorInputEntry[] {\n const code = archive.factionCode(faction);\n const unitsPath = resolve(CORE_ROOT, faction, \"units.json\");\n const unitName = new Map<string, string>();\n if (existsSync(unitsPath)) for (const u of readJSON(unitsPath) as Json[]) unitName.set(u.id, u.name);\n\n const abilitiesPath = resolve(ENRICHMENT_ROOT, faction, \"abilities.json\");\n if (!existsSync(abilitiesPath)) return [];\n const abilities: Json[] = readJSON(abilitiesPath);\n\n const out: AuthorInputEntry[] = [];\n for (const a of abilities) {\n if (!hasEmptyModifier(a.effect)) continue;\n const unitIds: string[] = a.unit_ids ?? [];\n const unitNames = unitIds.map((id) => unitName.get(id)).filter((n): n is string => !!n);\n const { src, reason } = resolveSource(archive, code, unitNames, a.name);\n out.push({\n faction,\n ability_id: a.ability_id,\n name: a.name,\n unit_ids: unitIds,\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 resolved: !!src,\n ...(src ? { src } : { reason }),\n });\n }\n return out;\n}\n\nfunction main(): void {\n const arg = process.argv[2];\n if (!arg) {\n console.error(\"Usage: npx tsx tools/src/author-input.ts <faction|--all>\");\n process.exit(1);\n }\n const archive = loadArchive();\n const factions =\n arg === \"--all\"\n ? readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })\n .filter((e) => e.isDirectory() && e.name !== \"_example\")\n .map((e) => e.name)\n : [arg];\n\n mkdirSync(OUT_DIR, { recursive: true });\n let totalStubs = 0;\n let totalResolved = 0;\n for (const faction of factions) {\n const entries = buildFaction(faction, archive);\n if (entries.length === 0) continue;\n const resolved = entries.filter((e) => e.resolved).length;\n totalStubs += entries.length;\n totalResolved += resolved;\n writeFileSync(resolve(OUT_DIR, `${faction}.json`), JSON.stringify(entries, null, 2) + \"\\n\");\n console.log(` ${faction}: ${entries.length} stubs, ${resolved} source-resolved`);\n }\n console.log(`\\n${totalStubs} stubs total, ${totalResolved} resolved (${Math.round((100 * totalResolved) / Math.max(1, totalStubs))}%). → ${OUT_DIR}`);\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"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ArchiveIndex } from "./author-input.js";
|
|
2
|
+
type Json = any;
|
|
3
|
+
/** Minimal enrichment ability entry the seed tool authors. */
|
|
4
|
+
export interface SeededAbility {
|
|
5
|
+
ability_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
authored_by: string;
|
|
8
|
+
game_version: {
|
|
9
|
+
edition: string;
|
|
10
|
+
dataslate: string;
|
|
11
|
+
};
|
|
12
|
+
version: string;
|
|
13
|
+
effect: {
|
|
14
|
+
type: "stat-modifier";
|
|
15
|
+
target: "unit";
|
|
16
|
+
modifier: Record<string, never>;
|
|
17
|
+
};
|
|
18
|
+
scope: {
|
|
19
|
+
range: "unit";
|
|
20
|
+
duration: "permanent";
|
|
21
|
+
};
|
|
22
|
+
unit_ids: string[];
|
|
23
|
+
ability_type: "unit";
|
|
24
|
+
behavior: "passive";
|
|
25
|
+
}
|
|
26
|
+
export interface UnresolvedEntry {
|
|
27
|
+
faction: string;
|
|
28
|
+
unit_id: string;
|
|
29
|
+
unit_name: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
/** kebab-case slug matching the entity-id pattern `^[a-z0-9][a-z0-9-]*[a-z0-9]$`. */
|
|
33
|
+
export declare function kebab(name: string): string;
|
|
34
|
+
export interface SeedResult {
|
|
35
|
+
/** The faction's abilities array after seeding (live entries + new stubs). */
|
|
36
|
+
abilities: Json[];
|
|
37
|
+
/** Count of stub entries newly created. */
|
|
38
|
+
created: number;
|
|
39
|
+
/** Count of units merged into an existing entry's unit_ids. */
|
|
40
|
+
merged: number;
|
|
41
|
+
/**
|
|
42
|
+
* Merges into an already-authored (non-stub) entry — surfaced for review so a
|
|
43
|
+
* rare same-id/different-rule collision can be caught. Purely informational;
|
|
44
|
+
* the merge still happened (additive unit_ids).
|
|
45
|
+
*/
|
|
46
|
+
mergedIntoAuthored: {
|
|
47
|
+
ability_id: string;
|
|
48
|
+
unit_id: string;
|
|
49
|
+
}[];
|
|
50
|
+
unresolved: UnresolvedEntry[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Pure core: given a faction's existing abilities + its empty units, return the
|
|
54
|
+
* seeded abilities array plus a report. No I/O — the unit test drives this with
|
|
55
|
+
* a fake {@link ArchiveIndex}.
|
|
56
|
+
*/
|
|
57
|
+
export declare function seedFaction(faction: string, archive: ArchiveIndex, emptyUnits: {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
}[], existing: Json[]): SeedResult;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=author-seed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"author-seed.d.ts","sourceRoot":"","sources":["../src/author-seed.ts"],"names":[],"mappings":"AAyCA,OAAO,EAAe,KAAK,YAAY,EAAmB,MAAM,mBAAmB,CAAC;AAUpF,KAAK,IAAI,GAAG,GAAG,CAAC;AAOhB,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;KAAE,CAAC;IACnF,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,CAAC;IAChD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qFAAqF;AACrF,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1C;AAiBD,MAAM,WAAW,UAAU;IACzB,8EAA8E;IAC9E,SAAS,EAAE,IAAI,EAAE,CAAC;IAClB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,kBAAkB,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,EAC1C,QAAQ,EAAE,IAAI,EAAE,GACf,UAAU,CA2DZ"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed empty-modifier ability stubs for units that have *zero* enrichment
|
|
3
|
+
* abilities (#NN). The DSL authoring pipeline (`author-input` → `author-batch`)
|
|
4
|
+
* only fleshes stubs that already exist — it iterates `hasEmptyModifier`
|
|
5
|
+
* entries and never invents new ones. So a unit with `ability_ids: []` is
|
|
6
|
+
* invisible to it: no stub, nothing to flesh, stays a bare statline forever.
|
|
7
|
+
*
|
|
8
|
+
* This tool closes that gap. For every unit whose core `ability_ids` is empty,
|
|
9
|
+
* it walks the same archive join `author-input` uses —
|
|
10
|
+
*
|
|
11
|
+
* core unit.name → archive Datasheet (name + faction code) → datasheet_id
|
|
12
|
+
* → every ability on that datasheet (Datasheets_abilities)
|
|
13
|
+
*
|
|
14
|
+
* — and writes one empty-modifier stub per resolved ability into
|
|
15
|
+
* `data/enrichment/<faction>/abilities.json`. The stubs then flow through the
|
|
16
|
+
* normal `author-input → author-batch propose/apply` workflow like any other.
|
|
17
|
+
*
|
|
18
|
+
* IP posture: the archive's rule `description` is GW text and is **never**
|
|
19
|
+
* written to the repo. Only the ability *name* (a factual label, as the
|
|
20
|
+
* existing dataset already stores) and an empty-modifier placeholder effect are
|
|
21
|
+
* emitted; the real mechanic is authored downstream by the classify step, which
|
|
22
|
+
* reads the description transiently and emits structured DSL (no prose).
|
|
23
|
+
*
|
|
24
|
+
* Dedup is conservative — it never rewrites an authored ability:
|
|
25
|
+
* Dedup keys on `ability_id` (the project's own ability identity — that's how
|
|
26
|
+
* `link-abilities` reverse-links). Within a single faction file a matching
|
|
27
|
+
* `ability_id` is the same game ability, so we add the unit to that entry's
|
|
28
|
+
* `unit_ids` (purely additive — never touches the effect). Merges into an
|
|
29
|
+
* already-authored entry are flagged in the report for review, since a rare
|
|
30
|
+
* same-name/different-rule collision would surface there.
|
|
31
|
+
*
|
|
32
|
+
* Unresolved units/abilities (no datasheet match — selectable-power pools like
|
|
33
|
+
* the C'tan, name mismatches, Legends not in the archive) are reported to
|
|
34
|
+
* `data/_audit/seed-unresolved.json` for hand-authoring, never silently dropped.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
* npx tsx tools/src/author-seed.ts <faction|--all> [--dry-run]
|
|
38
|
+
*/
|
|
39
|
+
import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
40
|
+
import { resolve } from "node:path";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
import { loadArchive } from "./author-input.js";
|
|
43
|
+
import { hasEmptyModifier } from "./audit-coverage.js";
|
|
44
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
45
|
+
const DATA_ROOT = resolve(__dirname, "../../data");
|
|
46
|
+
const ENRICHMENT_ROOT = resolve(DATA_ROOT, "enrichment");
|
|
47
|
+
const CORE_ROOT = resolve(DATA_ROOT, "core");
|
|
48
|
+
const UNRESOLVED_PATH = resolve(DATA_ROOT, "_audit", "seed-unresolved.json");
|
|
49
|
+
const readJSON = (p) => JSON.parse(readFileSync(p, "utf-8"));
|
|
50
|
+
const STUB_AUTHORED_BY = "40kdc-community";
|
|
51
|
+
const STUB_VERSION = "2025-q3";
|
|
52
|
+
const STUB_GAME_VERSION = { edition: "11th", dataslate: "pre-launch-provisional" };
|
|
53
|
+
/** kebab-case slug matching the entity-id pattern `^[a-z0-9][a-z0-9-]*[a-z0-9]$`. */
|
|
54
|
+
export function kebab(name) {
|
|
55
|
+
return name
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/['’]/g, "")
|
|
58
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
59
|
+
.replace(/^-+|-+$/g, "");
|
|
60
|
+
}
|
|
61
|
+
function newStub(abilityId, name, unitId) {
|
|
62
|
+
return {
|
|
63
|
+
ability_id: abilityId,
|
|
64
|
+
name,
|
|
65
|
+
authored_by: STUB_AUTHORED_BY,
|
|
66
|
+
game_version: { ...STUB_GAME_VERSION },
|
|
67
|
+
version: STUB_VERSION,
|
|
68
|
+
effect: { type: "stat-modifier", target: "unit", modifier: {} },
|
|
69
|
+
scope: { range: "unit", duration: "permanent" },
|
|
70
|
+
unit_ids: [unitId],
|
|
71
|
+
ability_type: "unit",
|
|
72
|
+
behavior: "passive",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Pure core: given a faction's existing abilities + its empty units, return the
|
|
77
|
+
* seeded abilities array plus a report. No I/O — the unit test drives this with
|
|
78
|
+
* a fake {@link ArchiveIndex}.
|
|
79
|
+
*/
|
|
80
|
+
export function seedFaction(faction, archive, emptyUnits, existing) {
|
|
81
|
+
const code = archive.factionCode(faction);
|
|
82
|
+
const abilities = existing.map((a) => ({ ...a, unit_ids: [...(a.unit_ids ?? [])] }));
|
|
83
|
+
const byId = new Map(abilities.map((a) => [a.ability_id, a]));
|
|
84
|
+
const result = { abilities, created: 0, merged: 0, mergedIntoAuthored: [], unresolved: [] };
|
|
85
|
+
const addUnit = (entry, unitId) => {
|
|
86
|
+
if (!entry.unit_ids.includes(unitId)) {
|
|
87
|
+
entry.unit_ids.push(unitId);
|
|
88
|
+
result.merged++;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
for (const unit of emptyUnits) {
|
|
92
|
+
if (!code) {
|
|
93
|
+
result.unresolved.push({ faction, unit_id: unit.id, unit_name: unit.name, reason: "no archive faction code" });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const dsIds = archive.datasheetsFor(unit.name, code);
|
|
97
|
+
if (dsIds.length === 0) {
|
|
98
|
+
result.unresolved.push({ faction, unit_id: unit.id, unit_name: unit.name, reason: "no matching datasheet in archive" });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Collect this unit's datasheet abilities, deduped by normalized name.
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const abilitiesForUnit = [];
|
|
104
|
+
for (const dsId of dsIds) {
|
|
105
|
+
for (const [name, rule] of archive.abilitiesFor(dsId)) {
|
|
106
|
+
const k = name.toLowerCase().trim();
|
|
107
|
+
if (seen.has(k))
|
|
108
|
+
continue;
|
|
109
|
+
seen.add(k);
|
|
110
|
+
abilitiesForUnit.push([name, rule]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (abilitiesForUnit.length === 0) {
|
|
114
|
+
result.unresolved.push({ faction, unit_id: unit.id, unit_name: unit.name, reason: "datasheet carries no abilities in archive" });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
for (const [name] of abilitiesForUnit) {
|
|
118
|
+
const id = kebab(name);
|
|
119
|
+
if (!id)
|
|
120
|
+
continue; // unsluggable name (e.g. all punctuation) — skip
|
|
121
|
+
const existingEntry = byId.get(id);
|
|
122
|
+
if (existingEntry) {
|
|
123
|
+
const before = existingEntry.unit_ids.includes(unit.id);
|
|
124
|
+
addUnit(existingEntry, unit.id);
|
|
125
|
+
// Flag merges into authored entries (not stubs) for review.
|
|
126
|
+
if (!before && !hasEmptyModifier(existingEntry.effect)) {
|
|
127
|
+
result.mergedIntoAuthored.push({ ability_id: id, unit_id: unit.id });
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const stub = newStub(id, name, unit.id);
|
|
132
|
+
abilities.push(stub);
|
|
133
|
+
byId.set(id, stub);
|
|
134
|
+
result.created++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function listEmptyUnits(faction) {
|
|
140
|
+
const unitsPath = resolve(CORE_ROOT, faction, "units.json");
|
|
141
|
+
if (!existsSync(unitsPath))
|
|
142
|
+
return [];
|
|
143
|
+
return readJSON(unitsPath)
|
|
144
|
+
.filter((u) => Array.isArray(u.ability_ids) && u.ability_ids.length === 0)
|
|
145
|
+
.map((u) => ({ id: u.id, name: u.name }));
|
|
146
|
+
}
|
|
147
|
+
function main() {
|
|
148
|
+
const args = process.argv.slice(2);
|
|
149
|
+
const dryRun = args.includes("--dry-run");
|
|
150
|
+
// The faction selector is either the literal "--all" or a bare faction name.
|
|
151
|
+
const arg = args.includes("--all") ? "--all" : args.find((a) => !a.startsWith("--"));
|
|
152
|
+
if (!arg) {
|
|
153
|
+
console.error("Usage: npx tsx tools/src/author-seed.ts <faction|--all> [--dry-run]");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
const archive = loadArchive();
|
|
157
|
+
const factions = arg === "--all"
|
|
158
|
+
? readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })
|
|
159
|
+
.filter((e) => e.isDirectory() && e.name !== "_example")
|
|
160
|
+
.map((e) => e.name)
|
|
161
|
+
: [arg];
|
|
162
|
+
const allUnresolved = [];
|
|
163
|
+
const allReview = [];
|
|
164
|
+
let totalCreated = 0;
|
|
165
|
+
let totalMerged = 0;
|
|
166
|
+
for (const faction of factions) {
|
|
167
|
+
const emptyUnits = listEmptyUnits(faction);
|
|
168
|
+
if (emptyUnits.length === 0)
|
|
169
|
+
continue;
|
|
170
|
+
const abilitiesPath = resolve(ENRICHMENT_ROOT, faction, "abilities.json");
|
|
171
|
+
const existing = existsSync(abilitiesPath) ? readJSON(abilitiesPath) : [];
|
|
172
|
+
const r = seedFaction(faction, archive, emptyUnits, existing);
|
|
173
|
+
totalCreated += r.created;
|
|
174
|
+
totalMerged += r.merged;
|
|
175
|
+
allUnresolved.push(...r.unresolved);
|
|
176
|
+
allReview.push(...r.mergedIntoAuthored.map((m) => ({ faction, ...m })));
|
|
177
|
+
console.log(` ${faction}: ${emptyUnits.length} empty units → +${r.created} stubs, ${r.merged} merged (${r.mergedIntoAuthored.length} into authored), ${r.unresolved.length} unresolved`);
|
|
178
|
+
if (!dryRun && (r.created > 0 || r.merged > 0)) {
|
|
179
|
+
writeFileSync(abilitiesPath, JSON.stringify(r.abilities, null, 2) + "\n");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!dryRun) {
|
|
183
|
+
mkdirSync(resolve(DATA_ROOT, "_audit"), { recursive: true });
|
|
184
|
+
writeFileSync(UNRESOLVED_PATH, JSON.stringify({ unresolved: allUnresolved, mergedIntoAuthored: allReview }, null, 2) + "\n");
|
|
185
|
+
}
|
|
186
|
+
console.log(`\n${totalCreated} stubs created, ${totalMerged} unit links merged ` +
|
|
187
|
+
`(${allReview.length} into authored — review), ${allUnresolved.length} unresolved.` +
|
|
188
|
+
(dryRun ? " (dry run — nothing written)" : ` Report → ${UNRESOLVED_PATH}`));
|
|
189
|
+
}
|
|
190
|
+
const isMain = process.argv[1] &&
|
|
191
|
+
resolve(process.argv[1]).replace(/\.\w+$/, "") === fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
|
|
192
|
+
if (isMain)
|
|
193
|
+
main();
|
|
194
|
+
//# sourceMappingURL=author-seed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"author-seed.js","sourceRoot":"","sources":["../src/author-seed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,WAAW,EAAsC,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AACnD,MAAM,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AACzD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAC7C,MAAM,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE,sBAAsB,CAAC,CAAC;AAI7E,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAQ,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3E,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;AAC3C,MAAM,YAAY,GAAG,SAAS,CAAC;AAC/B,MAAM,iBAAiB,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,wBAAwB,EAAE,CAAC;AAuBnF,qFAAqF;AACrF,MAAM,UAAU,KAAK,CAAC,IAAY;IAChC,OAAO,IAAI;SACR,WAAW,EAAE;SACb,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;SACpB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,OAAO,CAAC,SAAiB,EAAE,IAAY,EAAE,MAAc;IAC9D,OAAO;QACL,UAAU,EAAE,SAAS;QACrB,IAAI;QACJ,WAAW,EAAE,gBAAgB;QAC7B,YAAY,EAAE,EAAE,GAAG,iBAAiB,EAAE;QACtC,OAAO,EAAE,YAAY;QACrB,MAAM,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC/D,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE;QAC/C,QAAQ,EAAE,CAAC,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM;QACpB,QAAQ,EAAE,SAAS;KACpB,CAAC;AACJ,CAAC;AAkBD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,OAAe,EACf,OAAqB,EACrB,UAA0C,EAC1C,QAAgB;IAEhB,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,SAAS,GAAW,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7F,MAAM,IAAI,GAAG,IAAI,GAAG,CAAe,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAe,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;IAExG,MAAM,OAAO,GAAG,CAAC,KAAW,EAAE,MAAc,EAAQ,EAAE;QACpD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACrC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC5B,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,yBAAyB,EAAE,CAAC,CAAC;YAC/G,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACrD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,kCAAkC,EAAE,CAAC,CAAC;YACxH,SAAS;QACX,CAAC;QACD,uEAAuE;QACvE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,MAAM,gBAAgB,GAAsD,EAAE,CAAC;QAC/E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtD,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;gBACpC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;oBAAE,SAAS;gBAC1B,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACZ,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QACD,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,2CAA2C,EAAE,CAAC,CAAC;YACjI,SAAS;QACX,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,gBAAgB,EAAE,CAAC;YACtC,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YACvB,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,iDAAiD;YACpE,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACnC,IAAI,aAAa,EAAE,CAAC;gBAClB,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACxD,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;gBAChC,4DAA4D;gBAC5D,IAAI,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;oBACvD,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvE,CAAC;gBACD,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;YACxC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrB,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACnB,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,cAAc,CAAC,OAAe;IACrC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;IAC5D,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,OAAQ,QAAQ,CAAC,SAAS,CAAY;SACnC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC;SACzE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,IAAI;IACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1C,6EAA6E;IAC7E,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACrF,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;QACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,MAAM,QAAQ,GACZ,GAAG,KAAK,OAAO;QACb,CAAC,CAAC,WAAW,CAAC,eAAe,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAClD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;aACvD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACvB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAEZ,MAAM,aAAa,GAAsB,EAAE,CAAC;IAC5C,MAAM,SAAS,GAA+D,EAAE,CAAC;IACjF,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACtC,MAAM,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC;QAC1E,MAAM,QAAQ,GAAW,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,MAAM,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC9D,YAAY,IAAI,CAAC,CAAC,OAAO,CAAC;QAC1B,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC;QACxB,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;QACpC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACxE,OAAO,CAAC,GAAG,CACT,KAAK,OAAO,KAAK,UAAU,CAAC,MAAM,mBAAmB,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,YAAY,CAAC,CAAC,kBAAkB,CAAC,MAAM,oBAAoB,CAAC,CAAC,UAAU,CAAC,MAAM,aAAa,CAC7K,CAAC;QACF,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;YAC/C,aAAa,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IAED,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,aAAa,CACX,eAAe,EACf,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,aAAa,EAAE,kBAAkB,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAC7F,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,GAAG,CACT,KAAK,YAAY,mBAAmB,WAAW,qBAAqB;QAClE,IAAI,SAAS,CAAC,MAAM,6BAA6B,aAAa,CAAC,MAAM,cAAc;QACnF,CAAC,MAAM,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,aAAa,eAAe,EAAE,CAAC,CAC7E,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACf,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAC1G,IAAI,MAAM;IAAE,IAAI,EAAE,CAAC","sourcesContent":["/**\n * Seed empty-modifier ability stubs for units that have *zero* enrichment\n * abilities (#NN). The DSL authoring pipeline (`author-input` → `author-batch`)\n * only fleshes stubs that already exist — it iterates `hasEmptyModifier`\n * entries and never invents new ones. So a unit with `ability_ids: []` is\n * invisible to it: no stub, nothing to flesh, stays a bare statline forever.\n *\n * This tool closes that gap. For every unit whose core `ability_ids` is empty,\n * it walks the same archive join `author-input` uses —\n *\n * core unit.name → archive Datasheet (name + faction code) → datasheet_id\n * → every ability on that datasheet (Datasheets_abilities)\n *\n * — and writes one empty-modifier stub per resolved ability into\n * `data/enrichment/<faction>/abilities.json`. The stubs then flow through the\n * normal `author-input → author-batch propose/apply` workflow like any other.\n *\n * IP posture: the archive's rule `description` is GW text and is **never**\n * written to the repo. Only the ability *name* (a factual label, as the\n * existing dataset already stores) and an empty-modifier placeholder effect are\n * emitted; the real mechanic is authored downstream by the classify step, which\n * reads the description transiently and emits structured DSL (no prose).\n *\n * Dedup is conservative — it never rewrites an authored ability:\n * Dedup keys on `ability_id` (the project's own ability identity — that's how\n * `link-abilities` reverse-links). Within a single faction file a matching\n * `ability_id` is the same game ability, so we add the unit to that entry's\n * `unit_ids` (purely additive — never touches the effect). Merges into an\n * already-authored entry are flagged in the report for review, since a rare\n * same-name/different-rule collision would surface there.\n *\n * Unresolved units/abilities (no datasheet match — selectable-power pools like\n * the C'tan, name mismatches, Legends not in the archive) are reported to\n * `data/_audit/seed-unresolved.json` for hand-authoring, never silently dropped.\n *\n * Usage:\n * npx tsx tools/src/author-seed.ts <faction|--all> [--dry-run]\n */\nimport { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { loadArchive, type ArchiveIndex, type SourceRule } from \"./author-input.js\";\nimport { hasEmptyModifier } from \"./audit-coverage.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\nconst ENRICHMENT_ROOT = resolve(DATA_ROOT, \"enrichment\");\nconst CORE_ROOT = resolve(DATA_ROOT, \"core\");\nconst UNRESOLVED_PATH = resolve(DATA_ROOT, \"_audit\", \"seed-unresolved.json\");\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\nconst STUB_AUTHORED_BY = \"40kdc-community\";\nconst STUB_VERSION = \"2025-q3\";\nconst STUB_GAME_VERSION = { edition: \"11th\", dataslate: \"pre-launch-provisional\" };\n\n/** Minimal enrichment ability entry the seed tool authors. */\nexport interface SeededAbility {\n ability_id: string;\n name: string;\n authored_by: string;\n game_version: { edition: string; dataslate: string };\n version: string;\n effect: { type: \"stat-modifier\"; target: \"unit\"; modifier: Record<string, never> };\n scope: { range: \"unit\"; duration: \"permanent\" };\n unit_ids: string[];\n ability_type: \"unit\";\n behavior: \"passive\";\n}\n\nexport interface UnresolvedEntry {\n faction: string;\n unit_id: string;\n unit_name: string;\n reason: string;\n}\n\n/** kebab-case slug matching the entity-id pattern `^[a-z0-9][a-z0-9-]*[a-z0-9]$`. */\nexport function kebab(name: string): string {\n return name\n .toLowerCase()\n .replace(/['’]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\nfunction newStub(abilityId: string, name: string, unitId: string): SeededAbility {\n return {\n ability_id: abilityId,\n name,\n authored_by: STUB_AUTHORED_BY,\n game_version: { ...STUB_GAME_VERSION },\n version: STUB_VERSION,\n effect: { type: \"stat-modifier\", target: \"unit\", modifier: {} },\n scope: { range: \"unit\", duration: \"permanent\" },\n unit_ids: [unitId],\n ability_type: \"unit\",\n behavior: \"passive\",\n };\n}\n\nexport interface SeedResult {\n /** The faction's abilities array after seeding (live entries + new stubs). */\n abilities: Json[];\n /** Count of stub entries newly created. */\n created: number;\n /** Count of units merged into an existing entry's unit_ids. */\n merged: number;\n /**\n * Merges into an already-authored (non-stub) entry — surfaced for review so a\n * rare same-id/different-rule collision can be caught. Purely informational;\n * the merge still happened (additive unit_ids).\n */\n mergedIntoAuthored: { ability_id: string; unit_id: string }[];\n unresolved: UnresolvedEntry[];\n}\n\n/**\n * Pure core: given a faction's existing abilities + its empty units, return the\n * seeded abilities array plus a report. No I/O — the unit test drives this with\n * a fake {@link ArchiveIndex}.\n */\nexport function seedFaction(\n faction: string,\n archive: ArchiveIndex,\n emptyUnits: { id: string; name: string }[],\n existing: Json[],\n): SeedResult {\n const code = archive.factionCode(faction);\n const abilities: Json[] = existing.map((a) => ({ ...a, unit_ids: [...(a.unit_ids ?? [])] }));\n const byId = new Map<string, Json>(abilities.map((a) => [a.ability_id, a]));\n const result: SeedResult = { abilities, created: 0, merged: 0, mergedIntoAuthored: [], unresolved: [] };\n\n const addUnit = (entry: Json, unitId: string): void => {\n if (!entry.unit_ids.includes(unitId)) {\n entry.unit_ids.push(unitId);\n result.merged++;\n }\n };\n\n for (const unit of emptyUnits) {\n if (!code) {\n result.unresolved.push({ faction, unit_id: unit.id, unit_name: unit.name, reason: \"no archive faction code\" });\n continue;\n }\n const dsIds = archive.datasheetsFor(unit.name, code);\n if (dsIds.length === 0) {\n result.unresolved.push({ faction, unit_id: unit.id, unit_name: unit.name, reason: \"no matching datasheet in archive\" });\n continue;\n }\n // Collect this unit's datasheet abilities, deduped by normalized name.\n const seen = new Set<string>();\n const abilitiesForUnit: Array<[string, Omit<SourceRule, \"datasheet_id\">]> = [];\n for (const dsId of dsIds) {\n for (const [name, rule] of archive.abilitiesFor(dsId)) {\n const k = name.toLowerCase().trim();\n if (seen.has(k)) continue;\n seen.add(k);\n abilitiesForUnit.push([name, rule]);\n }\n }\n if (abilitiesForUnit.length === 0) {\n result.unresolved.push({ faction, unit_id: unit.id, unit_name: unit.name, reason: \"datasheet carries no abilities in archive\" });\n continue;\n }\n\n for (const [name] of abilitiesForUnit) {\n const id = kebab(name);\n if (!id) continue; // unsluggable name (e.g. all punctuation) — skip\n const existingEntry = byId.get(id);\n if (existingEntry) {\n const before = existingEntry.unit_ids.includes(unit.id);\n addUnit(existingEntry, unit.id);\n // Flag merges into authored entries (not stubs) for review.\n if (!before && !hasEmptyModifier(existingEntry.effect)) {\n result.mergedIntoAuthored.push({ ability_id: id, unit_id: unit.id });\n }\n continue;\n }\n const stub = newStub(id, name, unit.id);\n abilities.push(stub);\n byId.set(id, stub);\n result.created++;\n }\n }\n return result;\n}\n\nfunction listEmptyUnits(faction: string): { id: string; name: string }[] {\n const unitsPath = resolve(CORE_ROOT, faction, \"units.json\");\n if (!existsSync(unitsPath)) return [];\n return (readJSON(unitsPath) as Json[])\n .filter((u) => Array.isArray(u.ability_ids) && u.ability_ids.length === 0)\n .map((u) => ({ id: u.id, name: u.name }));\n}\n\nfunction main(): void {\n const args = process.argv.slice(2);\n const dryRun = args.includes(\"--dry-run\");\n // The faction selector is either the literal \"--all\" or a bare faction name.\n const arg = args.includes(\"--all\") ? \"--all\" : args.find((a) => !a.startsWith(\"--\"));\n if (!arg) {\n console.error(\"Usage: npx tsx tools/src/author-seed.ts <faction|--all> [--dry-run]\");\n process.exit(1);\n }\n const archive = loadArchive();\n const factions =\n arg === \"--all\"\n ? readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })\n .filter((e) => e.isDirectory() && e.name !== \"_example\")\n .map((e) => e.name)\n : [arg];\n\n const allUnresolved: UnresolvedEntry[] = [];\n const allReview: { faction: string; ability_id: string; unit_id: string }[] = [];\n let totalCreated = 0;\n let totalMerged = 0;\n for (const faction of factions) {\n const emptyUnits = listEmptyUnits(faction);\n if (emptyUnits.length === 0) continue;\n const abilitiesPath = resolve(ENRICHMENT_ROOT, faction, \"abilities.json\");\n const existing: Json[] = existsSync(abilitiesPath) ? readJSON(abilitiesPath) : [];\n const r = seedFaction(faction, archive, emptyUnits, existing);\n totalCreated += r.created;\n totalMerged += r.merged;\n allUnresolved.push(...r.unresolved);\n allReview.push(...r.mergedIntoAuthored.map((m) => ({ faction, ...m })));\n console.log(\n ` ${faction}: ${emptyUnits.length} empty units → +${r.created} stubs, ${r.merged} merged (${r.mergedIntoAuthored.length} into authored), ${r.unresolved.length} unresolved`,\n );\n if (!dryRun && (r.created > 0 || r.merged > 0)) {\n writeFileSync(abilitiesPath, JSON.stringify(r.abilities, null, 2) + \"\\n\");\n }\n }\n\n if (!dryRun) {\n mkdirSync(resolve(DATA_ROOT, \"_audit\"), { recursive: true });\n writeFileSync(\n UNRESOLVED_PATH,\n JSON.stringify({ unresolved: allUnresolved, mergedIntoAuthored: allReview }, null, 2) + \"\\n\",\n );\n }\n console.log(\n `\\n${totalCreated} stubs created, ${totalMerged} unit links merged ` +\n `(${allReview.length} into authored — review), ${allUnresolved.length} unresolved.` +\n (dryRun ? \" (dry run — nothing written)\" : ` Report → ${UNRESOLVED_PATH}`),\n );\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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/commands/translate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/commands/translate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkNH,wBAAsB,gBAAgB,CACpC,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAsBf"}
|