@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.
Files changed (96) hide show
  1. package/dist/abilities-resolver/resolver.d.ts.map +1 -1
  2. package/dist/abilities-resolver/resolver.js +6 -2
  3. package/dist/abilities-resolver/resolver.js.map +1 -1
  4. package/dist/audit-coverage.d.ts.map +1 -1
  5. package/dist/audit-coverage.js +90 -1
  6. package/dist/audit-coverage.js.map +1 -1
  7. package/dist/audit-phrasing.d.ts +64 -0
  8. package/dist/audit-phrasing.d.ts.map +1 -0
  9. package/dist/audit-phrasing.js +208 -0
  10. package/dist/audit-phrasing.js.map +1 -0
  11. package/dist/build-share-registry.d.ts +2 -0
  12. package/dist/build-share-registry.d.ts.map +1 -0
  13. package/dist/build-share-registry.js +115 -0
  14. package/dist/build-share-registry.js.map +1 -0
  15. package/dist/cli.js +7 -0
  16. package/dist/cli.js.map +1 -1
  17. package/dist/codegen-data.js +23 -0
  18. package/dist/codegen-data.js.map +1 -1
  19. package/dist/commands/translate.d.ts.map +1 -1
  20. package/dist/commands/translate.js +2 -5
  21. package/dist/commands/translate.js.map +1 -1
  22. package/dist/commands/validate-all.d.ts.map +1 -1
  23. package/dist/commands/validate-all.js +8 -3
  24. package/dist/commands/validate-all.js.map +1 -1
  25. package/dist/data/bundle.generated.js +1 -1
  26. package/dist/data/bundle.generated.js.map +1 -1
  27. package/dist/data/dataset.d.ts +33 -1
  28. package/dist/data/dataset.d.ts.map +1 -1
  29. package/dist/data/dataset.js +107 -1
  30. package/dist/data/dataset.js.map +1 -1
  31. package/dist/data/index.d.ts +4 -0
  32. package/dist/data/index.d.ts.map +1 -1
  33. package/dist/data/index.js +4 -0
  34. package/dist/data/index.js.map +1 -1
  35. package/dist/data/types.d.ts +3 -1
  36. package/dist/data/types.d.ts.map +1 -1
  37. package/dist/data/types.js +1 -0
  38. package/dist/data/types.js.map +1 -1
  39. package/dist/gen-conformance.js +162 -0
  40. package/dist/gen-conformance.js.map +1 -1
  41. package/dist/generated.d.ts +125 -0
  42. package/dist/generated.d.ts.map +1 -1
  43. package/dist/generated.js.map +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/integrity.d.ts +32 -0
  49. package/dist/integrity.d.ts.map +1 -0
  50. package/dist/integrity.js +120 -0
  51. package/dist/integrity.js.map +1 -0
  52. package/dist/report.d.ts +1 -1
  53. package/dist/report.d.ts.map +1 -1
  54. package/dist/report.js +2 -2
  55. package/dist/report.js.map +1 -1
  56. package/dist/runner.d.ts.map +1 -1
  57. package/dist/runner.js +42 -0
  58. package/dist/runner.js.map +1 -1
  59. package/dist/share/codec.d.ts +83 -0
  60. package/dist/share/codec.d.ts.map +1 -0
  61. package/dist/share/codec.js +230 -0
  62. package/dist/share/codec.js.map +1 -0
  63. package/dist/share/index.d.ts +30 -0
  64. package/dist/share/index.d.ts.map +1 -0
  65. package/dist/share/index.js +36 -0
  66. package/dist/share/index.js.map +1 -0
  67. package/dist/share/registry.d.ts +42 -0
  68. package/dist/share/registry.d.ts.map +1 -0
  69. package/dist/share/registry.generated.d.ts +4 -0
  70. package/dist/share/registry.generated.d.ts.map +1 -0
  71. package/dist/share/registry.generated.js +4 -0
  72. package/dist/share/registry.generated.js.map +1 -0
  73. package/dist/share/registry.js +68 -0
  74. package/dist/share/registry.js.map +1 -0
  75. package/dist/terrain/index.d.ts +2 -2
  76. package/dist/terrain/index.d.ts.map +1 -1
  77. package/dist/terrain/index.js +1 -1
  78. package/dist/terrain/index.js.map +1 -1
  79. package/dist/terrain/solve.d.ts +76 -0
  80. package/dist/terrain/solve.d.ts.map +1 -1
  81. package/dist/terrain/solve.js +138 -0
  82. package/dist/terrain/solve.js.map +1 -1
  83. package/dist/translate/condition.d.ts +1 -0
  84. package/dist/translate/condition.d.ts.map +1 -1
  85. package/dist/translate/condition.js +101 -7
  86. package/dist/translate/condition.js.map +1 -1
  87. package/dist/translate/effect.d.ts +28 -20
  88. package/dist/translate/effect.d.ts.map +1 -1
  89. package/dist/translate/effect.js +483 -114
  90. package/dist/translate/effect.js.map +1 -1
  91. package/dist/validate.d.ts.map +1 -1
  92. package/dist/validate.js +1 -0
  93. package/dist/validate.js.map +1 -1
  94. package/package.json +3 -1
  95. package/schemas/core/allied-rule.schema.json +115 -0
  96. package/schemas/core/detachment.schema.json +38 -1
@@ -0,0 +1,120 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { glob } from "glob";
3
+ import { resolve, basename, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
6
+ const DATA_ROOT = resolve(__dirname, "../../data");
7
+ /**
8
+ * Factions whose units carry exactly one bare legion/faction faction_keyword.
9
+ *
10
+ * These are the shared-datasheet factions where contamination has been observed:
11
+ * a unit materialized from a parent template keeps the parent's faction keyword
12
+ * (e.g. a World Eaters Chaos Rhino left carrying `Emperor's Children`, or a Chaos
13
+ * Space Marines unit carrying the full multi-legion union line). For a faction in
14
+ * this map, every unit's `faction_keywords` must be a subset of `{home}`.
15
+ *
16
+ * Factions absent from this map (e.g. Space Marine chapters, which legitimately
17
+ * carry several faction_keywords) are not subject to the membership check, so the
18
+ * guard never produces a false positive on them. Extend this map as other
19
+ * single-token factions adopt the convention.
20
+ */
21
+ export const FACTION_HOME_KEYWORD = {
22
+ "chaos-space-marines": "Heretic Astartes",
23
+ "world-eaters": "World Eaters",
24
+ "death-guard": "Death Guard",
25
+ "thousand-sons": "Thousand Sons",
26
+ "emperors-children": "Emperor’s Children",
27
+ };
28
+ function readArray(file) {
29
+ return JSON.parse(readFileSync(file, "utf-8"));
30
+ }
31
+ function loadAbilityIds(file, into) {
32
+ try {
33
+ for (const a of readArray(file)) {
34
+ if (a.ability_id)
35
+ into.add(a.ability_id);
36
+ }
37
+ }
38
+ catch {
39
+ // file absent — faction has no enrichment abilities, or no shared core pool
40
+ }
41
+ }
42
+ /**
43
+ * Cross-entity referential integrity that per-file JSON Schema validation cannot
44
+ * express:
45
+ *
46
+ * - every unit `ability_id` must resolve to an ability defined in that faction's
47
+ * `enrichment/<faction>/abilities.json` (or the shared `enrichment/_core` pool).
48
+ * Same-faction scoping is deliberate — a union check would pass shared-unit
49
+ * contaminants because they happen to be defined in some *other* faction's
50
+ * enrichment.
51
+ * - every unit `faction_keywords` entry must be permitted for the unit's faction
52
+ * (see {@link FACTION_HOME_KEYWORD}).
53
+ *
54
+ * Results reuse {@link ValidationResult} so the CLI reporter can render them.
55
+ */
56
+ export async function checkReferentialIntegrity(dataRoot) {
57
+ const root = dataRoot ?? DATA_ROOT;
58
+ const result = {
59
+ totalFiles: 0,
60
+ totalItems: 0,
61
+ passed: 0,
62
+ failed: 0,
63
+ errors: [],
64
+ };
65
+ // Shared core ability pool, available to every faction (optional).
66
+ const coreAbilities = new Set();
67
+ loadAbilityIds(resolve(root, "enrichment/_core/abilities.json"), coreAbilities);
68
+ const unitFiles = await glob("core/*/units.json", { cwd: root, absolute: true });
69
+ unitFiles.sort();
70
+ for (const file of unitFiles) {
71
+ const faction = basename(dirname(file));
72
+ if (faction.startsWith("_"))
73
+ continue; // scratch/example/report dirs
74
+ let units;
75
+ try {
76
+ units = readArray(file);
77
+ }
78
+ catch {
79
+ continue; // structural problems are the AJV pass's job
80
+ }
81
+ if (!Array.isArray(units))
82
+ continue;
83
+ const defined = new Set(coreAbilities);
84
+ loadAbilityIds(resolve(root, `enrichment/${faction}/abilities.json`), defined);
85
+ const home = FACTION_HOME_KEYWORD[faction];
86
+ result.totalFiles++;
87
+ for (let i = 0; i < units.length; i++) {
88
+ const u = units[i];
89
+ result.totalItems++;
90
+ const errs = [];
91
+ for (const aid of u.ability_ids ?? []) {
92
+ if (!defined.has(aid)) {
93
+ errs.push({
94
+ path: `/${i}/ability_ids`,
95
+ message: `unit "${u.id}": ability_id "${aid}" is not defined in ${faction} enrichment`,
96
+ });
97
+ }
98
+ }
99
+ if (home !== undefined) {
100
+ for (const fk of u.faction_keywords ?? []) {
101
+ if (fk !== home) {
102
+ errs.push({
103
+ path: `/${i}/faction_keywords`,
104
+ message: `unit "${u.id}": faction_keyword "${fk}" is not permitted for ${faction} (expected only "${home}")`,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ if (errs.length > 0) {
110
+ result.failed++;
111
+ result.errors.push({ file, index: i, errors: errs });
112
+ }
113
+ else {
114
+ result.passed++;
115
+ }
116
+ }
117
+ }
118
+ return result;
119
+ }
120
+ //# sourceMappingURL=integrity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrity.js","sourceRoot":"","sources":["../src/integrity.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { glob } from \"glob\";\nimport { resolve, basename, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ValidationResult } from \"./validate.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\n\n/**\n * Factions whose units carry exactly one bare legion/faction faction_keyword.\n *\n * These are the shared-datasheet factions where contamination has been observed:\n * a unit materialized from a parent template keeps the parent's faction keyword\n * (e.g. a World Eaters Chaos Rhino left carrying `Emperor's Children`, or a Chaos\n * Space Marines unit carrying the full multi-legion union line). For a faction in\n * this map, every unit's `faction_keywords` must be a subset of `{home}`.\n *\n * Factions absent from this map (e.g. Space Marine chapters, which legitimately\n * carry several faction_keywords) are not subject to the membership check, so the\n * guard never produces a false positive on them. Extend this map as other\n * single-token factions adopt the convention.\n */\nexport const FACTION_HOME_KEYWORD: Record<string, string> = {\n \"chaos-space-marines\": \"Heretic Astartes\",\n \"world-eaters\": \"World Eaters\",\n \"death-guard\": \"Death Guard\",\n \"thousand-sons\": \"Thousand Sons\",\n \"emperors-children\": \"Emperor’s Children\",\n};\n\ninterface UnitLike {\n id?: string;\n ability_ids?: string[];\n faction_keywords?: string[];\n}\ninterface AbilityLike {\n ability_id?: string;\n}\n\nfunction readArray<T>(file: string): T[] {\n return JSON.parse(readFileSync(file, \"utf-8\")) as T[];\n}\n\nfunction loadAbilityIds(file: string, into: Set<string>): void {\n try {\n for (const a of readArray<AbilityLike>(file)) {\n if (a.ability_id) into.add(a.ability_id);\n }\n } catch {\n // file absent — faction has no enrichment abilities, or no shared core pool\n }\n}\n\n/**\n * Cross-entity referential integrity that per-file JSON Schema validation cannot\n * express:\n *\n * - every unit `ability_id` must resolve to an ability defined in that faction's\n * `enrichment/<faction>/abilities.json` (or the shared `enrichment/_core` pool).\n * Same-faction scoping is deliberate — a union check would pass shared-unit\n * contaminants because they happen to be defined in some *other* faction's\n * enrichment.\n * - every unit `faction_keywords` entry must be permitted for the unit's faction\n * (see {@link FACTION_HOME_KEYWORD}).\n *\n * Results reuse {@link ValidationResult} so the CLI reporter can render them.\n */\nexport async function checkReferentialIntegrity(dataRoot?: string): Promise<ValidationResult> {\n const root = dataRoot ?? DATA_ROOT;\n const result: ValidationResult = {\n totalFiles: 0,\n totalItems: 0,\n passed: 0,\n failed: 0,\n errors: [],\n };\n\n // Shared core ability pool, available to every faction (optional).\n const coreAbilities = new Set<string>();\n loadAbilityIds(resolve(root, \"enrichment/_core/abilities.json\"), coreAbilities);\n\n const unitFiles = await glob(\"core/*/units.json\", { cwd: root, absolute: true });\n unitFiles.sort();\n\n for (const file of unitFiles) {\n const faction = basename(dirname(file));\n if (faction.startsWith(\"_\")) continue; // scratch/example/report dirs\n\n let units: UnitLike[];\n try {\n units = readArray<UnitLike>(file);\n } catch {\n continue; // structural problems are the AJV pass's job\n }\n if (!Array.isArray(units)) continue;\n\n const defined = new Set<string>(coreAbilities);\n loadAbilityIds(resolve(root, `enrichment/${faction}/abilities.json`), defined);\n const home = FACTION_HOME_KEYWORD[faction];\n\n result.totalFiles++;\n for (let i = 0; i < units.length; i++) {\n const u = units[i];\n result.totalItems++;\n const errs: Array<{ path: string; message: string }> = [];\n\n for (const aid of u.ability_ids ?? []) {\n if (!defined.has(aid)) {\n errs.push({\n path: `/${i}/ability_ids`,\n message: `unit \"${u.id}\": ability_id \"${aid}\" is not defined in ${faction} enrichment`,\n });\n }\n }\n\n if (home !== undefined) {\n for (const fk of u.faction_keywords ?? []) {\n if (fk !== home) {\n errs.push({\n path: `/${i}/faction_keywords`,\n message: `unit \"${u.id}\": faction_keyword \"${fk}\" is not permitted for ${faction} (expected only \"${home}\")`,\n });\n }\n }\n }\n\n if (errs.length > 0) {\n result.failed++;\n result.errors.push({ file, index: i, errors: errs });\n } else {\n result.passed++;\n }\n }\n }\n\n return result;\n}\n"]}
package/dist/report.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { ValidationResult } from "./validate.js";
2
2
  export type ReporterMode = "pretty" | "json";
3
- export declare function formatReport(result: ValidationResult, mode: ReporterMode): string;
3
+ export declare function formatReport(result: ValidationResult, mode: ReporterMode, title?: string): string;
4
4
  //# sourceMappingURL=report.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7C,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAgCjF"}
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7C,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,YAAY,EAClB,KAAK,SAAiC,GACrC,MAAM,CAgCR"}
package/dist/report.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import chalk from "chalk";
2
- export function formatReport(result, mode) {
2
+ export function formatReport(result, mode, title = "40kdc Data Validation Report") {
3
3
  if (mode === "json") {
4
4
  return JSON.stringify(result, null, 2);
5
5
  }
6
6
  const lines = [];
7
7
  lines.push("");
8
- lines.push(chalk.bold("40kdc Data Validation Report"));
8
+ lines.push(chalk.bold(title));
9
9
  lines.push(chalk.gray("─".repeat(40)));
10
10
  lines.push(`Files scanned: ${result.totalFiles}`);
11
11
  lines.push(`Items validated: ${result.totalItems}`);
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,UAAU,YAAY,CAAC,MAAwB,EAAE,IAAkB;IACvE,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC,CAAC;IACvD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,KAAK,CAAC,IAAI,CAAC,mBAAmB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACnD,KAAK,CAAC,IAAI,CAAC,oBAAoB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACpD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAEpD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;YAC/C,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;gBAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["import chalk from \"chalk\";\nimport type { ValidationResult } from \"./validate.js\";\n\nexport type ReporterMode = \"pretty\" | \"json\";\n\nexport function formatReport(result: ValidationResult, mode: ReporterMode): string {\n if (mode === \"json\") {\n return JSON.stringify(result, null, 2);\n }\n\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(\"40kdc Data Validation Report\"));\n lines.push(chalk.gray(\"─\".repeat(40)));\n lines.push(`Files scanned: ${result.totalFiles}`);\n lines.push(`Items validated: ${result.totalItems}`);\n lines.push(chalk.green(`Passed: ${result.passed}`));\n\n if (result.failed > 0) {\n lines.push(chalk.red(`Failed: ${result.failed}`));\n lines.push(\"\");\n\n for (const err of result.errors) {\n const loc = err.index >= 0 ? `[${err.index}]` : \"\";\n lines.push(chalk.red(` ✗ ${err.file}${loc}`));\n for (const e of err.errors) {\n lines.push(chalk.yellow(` ${e.path}: ${e.message}`));\n }\n }\n } else {\n lines.push(chalk.green(`Failed: 0`));\n lines.push(\"\");\n lines.push(chalk.green(\"All validations passed.\"));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,UAAU,YAAY,CAC1B,MAAwB,EACxB,IAAkB,EAClB,KAAK,GAAG,8BAA8B;IAEtC,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,KAAK,CAAC,IAAI,CAAC,mBAAmB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACnD,KAAK,CAAC,IAAI,CAAC,oBAAoB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACpD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAEpD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;YAC/C,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;gBAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["import chalk from \"chalk\";\nimport type { ValidationResult } from \"./validate.js\";\n\nexport type ReporterMode = \"pretty\" | \"json\";\n\nexport function formatReport(\n result: ValidationResult,\n mode: ReporterMode,\n title = \"40kdc Data Validation Report\",\n): string {\n if (mode === \"json\") {\n return JSON.stringify(result, null, 2);\n }\n\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(title));\n lines.push(chalk.gray(\"─\".repeat(40)));\n lines.push(`Files scanned: ${result.totalFiles}`);\n lines.push(`Items validated: ${result.totalItems}`);\n lines.push(chalk.green(`Passed: ${result.passed}`));\n\n if (result.failed > 0) {\n lines.push(chalk.red(`Failed: ${result.failed}`));\n lines.push(\"\");\n\n for (const err of result.errors) {\n const loc = err.index >= 0 ? `[${err.index}]` : \"\";\n lines.push(chalk.red(` ✗ ${err.file}${loc}`));\n for (const e of err.errors) {\n lines.push(chalk.yellow(` ${e.path}: ${e.message}`));\n }\n }\n } else {\n lines.push(chalk.green(`Failed: 0`));\n lines.push(\"\");\n lines.push(chalk.green(\"All validations passed.\"));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAsC5C,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AA8C3B,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,QAAA,MAAM,WAAW,oJASP,CAAC;AACX,KAAK,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAgB9C,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C;AAmID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,CAAC,EACG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrG,IAAI,GACJ,SAAS,GACZ,MAAM,GAAG,IAAI,CAQf;AA0mBD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAkDhG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9E"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAuC5C,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AA8C3B,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,QAAA,MAAM,WAAW,oJASP,CAAC;AACX,KAAK,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAgB9C,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C;AAgKD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,CAAC,EACG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrG,IAAI,GACJ,SAAS,GACZ,MAAM,GAAG,IAAI,CAQf;AAmnBD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAsDhG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9E"}
package/dist/runner.js CHANGED
@@ -23,6 +23,7 @@ import { Dataset } from "./data/dataset.js";
23
23
  import { normalizeName } from "./data/normalize.js";
24
24
  import { maximalLoadout } from "./data/loadout.js";
25
25
  import { exportRoster } from "./export/index.js";
26
+ import { decodeShareToken, encodeShareToken } from "./share/index.js";
26
27
  import { importRoster, tryImportRoster } from "./import/import-roster.js";
27
28
  import { createValidator } from "./schema-loader.js";
28
29
  import { attributeStages, crunch } from "./cruncher/index.js";
@@ -215,6 +216,34 @@ function handleExport(args) {
215
216
  return err("EXPORT_FAILED", { detail: e.message });
216
217
  }
217
218
  }
219
+ function handleShareEncode(args) {
220
+ if (typeof args !== "object" || args === null) {
221
+ return err("INVALID_INPUT", { detail: "share_encode args must be an object" });
222
+ }
223
+ const a = args;
224
+ if (typeof a.list !== "object" || a.list === null) {
225
+ return err("INVALID_INPUT", { detail: "share_encode.list must be an object" });
226
+ }
227
+ try {
228
+ return ok(encodeShareToken(a.list));
229
+ }
230
+ catch (e) {
231
+ // An id absent from the embedded registry is the only expected throw.
232
+ return err("INVALID_INPUT", { detail: e.message });
233
+ }
234
+ }
235
+ function handleShareDecode(args) {
236
+ if (typeof args !== "object" || args === null) {
237
+ return err("INVALID_INPUT", { detail: "share_decode args must be an object" });
238
+ }
239
+ const a = args;
240
+ if (typeof a.token !== "string") {
241
+ return err("INVALID_INPUT", { detail: "share_decode.token must be a string" });
242
+ }
243
+ // The DecodeResult ({ ok, list } | { ok, reason }) is the compared value;
244
+ // a malformed/stale token is a normal result, not a protocol error.
245
+ return ok(decodeShareToken(a.token));
246
+ }
218
247
  /**
219
248
  * Canonical string encoding of a base size for cross-impl comparison (mirrors the
220
249
  * string-encoding used by `maximal_loadout`). Returns null for an absent base.
@@ -318,6 +347,15 @@ function handleLinkedQuery(state, args) {
318
347
  return err("UNKNOWN_ENTITY", { kind: "faction", id: input.factionId });
319
348
  return ok(f.weapons.map((x) => x.id));
320
349
  }
350
+ case "units_with_keyword":
351
+ return ok(ds.unitsWithKeyword(input.keyword ?? "").map((u) => u.id));
352
+ case "allies_for": {
353
+ const raw = input.detachmentIds;
354
+ const detachmentIds = Array.isArray(raw) ? raw : [];
355
+ return ok(ds.alliesFor(input.factionId ?? "", detachmentIds).map((r) => r.id));
356
+ }
357
+ case "ally_units_for":
358
+ return ok(ds.allyUnitsFor(input.ruleId ?? "").map((u) => u.id));
321
359
  default:
322
360
  return err("INVALID_INPUT", { detail: `unknown linked_query: ${a.query}` });
323
361
  }
@@ -856,6 +894,10 @@ export function dispatch(state, req) {
856
894
  return handleResolveTerrain(req.args);
857
895
  case "keystones":
858
896
  return handleKeystones(req.args);
897
+ case "share_encode":
898
+ return handleShareEncode(req.args);
899
+ case "share_decode":
900
+ return handleShareDecode(req.args);
859
901
  case "shutdown":
860
902
  return ok(null);
861
903
  default: