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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +10 -0
  2. package/dist/codegen-data.js +2 -0
  3. package/dist/codegen-data.js.map +1 -1
  4. package/dist/commands/import.d.ts.map +1 -1
  5. package/dist/commands/import.js +3 -1
  6. package/dist/commands/import.js.map +1 -1
  7. package/dist/compare.d.ts +167 -0
  8. package/dist/compare.d.ts.map +1 -0
  9. package/dist/compare.js +302 -0
  10. package/dist/compare.js.map +1 -0
  11. package/dist/convert-faction.d.ts.map +1 -1
  12. package/dist/convert-faction.js +22 -2
  13. package/dist/convert-faction.js.map +1 -1
  14. package/dist/cruncher/buffs.d.ts +13 -0
  15. package/dist/cruncher/buffs.d.ts.map +1 -1
  16. package/dist/cruncher/buffs.js +7 -0
  17. package/dist/cruncher/buffs.js.map +1 -1
  18. package/dist/cruncher/engine.d.ts.map +1 -1
  19. package/dist/cruncher/engine.js +14 -10
  20. package/dist/cruncher/engine.js.map +1 -1
  21. package/dist/data/bundle.generated.js +1 -1
  22. package/dist/data/bundle.generated.js.map +1 -1
  23. package/dist/data/dataset.d.ts +3 -1
  24. package/dist/data/dataset.d.ts.map +1 -1
  25. package/dist/data/dataset.js +4 -0
  26. package/dist/data/dataset.js.map +1 -1
  27. package/dist/data/entities.d.ts +22 -0
  28. package/dist/data/entities.d.ts.map +1 -1
  29. package/dist/data/entities.js +45 -1
  30. package/dist/data/entities.js.map +1 -1
  31. package/dist/data/index.d.ts +4 -0
  32. package/dist/data/index.d.ts.map +1 -1
  33. package/dist/data/index.js +4 -0
  34. package/dist/data/index.js.map +1 -1
  35. package/dist/data/types.d.ts +5 -1
  36. package/dist/data/types.d.ts.map +1 -1
  37. package/dist/data/types.js +2 -0
  38. package/dist/data/types.js.map +1 -1
  39. package/dist/export/newrecruit-json.d.ts.map +1 -1
  40. package/dist/export/newrecruit-json.js +4 -3
  41. package/dist/export/newrecruit-json.js.map +1 -1
  42. package/dist/export/newrecruit-simple.js +2 -2
  43. package/dist/export/newrecruit-simple.js.map +1 -1
  44. package/dist/export/newrecruit-wtc.d.ts.map +1 -1
  45. package/dist/export/newrecruit-wtc.js +3 -1
  46. package/dist/export/newrecruit-wtc.js.map +1 -1
  47. package/dist/export/rosterizer.d.ts.map +1 -1
  48. package/dist/export/rosterizer.js +6 -7
  49. package/dist/export/rosterizer.js.map +1 -1
  50. package/dist/extract-faction-pack.d.ts +56 -0
  51. package/dist/extract-faction-pack.d.ts.map +1 -0
  52. package/dist/extract-faction-pack.js +374 -0
  53. package/dist/extract-faction-pack.js.map +1 -0
  54. package/dist/gen-conformance.js +37 -4
  55. package/dist/gen-conformance.js.map +1 -1
  56. package/dist/generated.d.ts +66 -2
  57. package/dist/generated.d.ts.map +1 -1
  58. package/dist/generated.js.map +1 -1
  59. package/dist/import/gw.js +1 -1
  60. package/dist/import/gw.js.map +1 -1
  61. package/dist/import/import-roster.d.ts.map +1 -1
  62. package/dist/import/import-roster.js +5 -0
  63. package/dist/import/import-roster.js.map +1 -1
  64. package/dist/import/index.d.ts +1 -0
  65. package/dist/import/index.d.ts.map +1 -1
  66. package/dist/import/index.js +1 -0
  67. package/dist/import/index.js.map +1 -1
  68. package/dist/import/listforge-text.js +1 -1
  69. package/dist/import/listforge-text.js.map +1 -1
  70. package/dist/import/listforge.d.ts.map +1 -1
  71. package/dist/import/listforge.js +18 -3
  72. package/dist/import/listforge.js.map +1 -1
  73. package/dist/import/newrecruit-json.d.ts.map +1 -1
  74. package/dist/import/newrecruit-json.js +21 -3
  75. package/dist/import/newrecruit-json.js.map +1 -1
  76. package/dist/import/newrecruit-simple.js +1 -1
  77. package/dist/import/newrecruit-simple.js.map +1 -1
  78. package/dist/import/newrecruit-wtc.js +1 -1
  79. package/dist/import/newrecruit-wtc.js.map +1 -1
  80. package/dist/import/resolve.d.ts.map +1 -1
  81. package/dist/import/resolve.js +46 -18
  82. package/dist/import/resolve.js.map +1 -1
  83. package/dist/import/roster-json.d.ts +32 -0
  84. package/dist/import/roster-json.d.ts.map +1 -0
  85. package/dist/import/roster-json.js +92 -0
  86. package/dist/import/roster-json.js.map +1 -0
  87. package/dist/import/rosterizer.js +3 -3
  88. package/dist/import/rosterizer.js.map +1 -1
  89. package/dist/import/types.d.ts +16 -5
  90. package/dist/import/types.d.ts.map +1 -1
  91. package/dist/import/types.js.map +1 -1
  92. package/dist/index.d.ts +2 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +8 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/migrate-keystone-twins.d.ts +23 -0
  97. package/dist/migrate-keystone-twins.d.ts.map +1 -0
  98. package/dist/migrate-keystone-twins.js +184 -0
  99. package/dist/migrate-keystone-twins.js.map +1 -0
  100. package/dist/rube-goldberg.js +1 -1
  101. package/dist/rube-goldberg.js.map +1 -1
  102. package/dist/runner.d.ts.map +1 -1
  103. package/dist/runner.js +94 -1
  104. package/dist/runner.js.map +1 -1
  105. package/dist/scope.d.ts +31 -0
  106. package/dist/scope.d.ts.map +1 -0
  107. package/dist/scope.js +34 -0
  108. package/dist/scope.js.map +1 -0
  109. package/dist/translate/effect.d.ts +17 -3
  110. package/dist/translate/effect.d.ts.map +1 -1
  111. package/dist/translate/effect.js +23 -4
  112. package/dist/translate/effect.js.map +1 -1
  113. package/dist/translate/index.d.ts +1 -1
  114. package/dist/translate/index.d.ts.map +1 -1
  115. package/dist/translate/index.js +1 -1
  116. package/dist/translate/index.js.map +1 -1
  117. package/dist/validate.d.ts.map +1 -1
  118. package/dist/validate.js +2 -0
  119. package/dist/validate.js.map +1 -1
  120. package/package.json +2 -1
  121. package/schemas/core/hull-shape.schema.json +30 -0
  122. package/schemas/core/roster.schema.json +26 -9
  123. package/schemas/core/stratagem.schema.json +2 -2
  124. package/schemas/core/target-profile.schema.json +30 -0
  125. package/schemas/core/unit-composition.schema.json +7 -0
  126. package/schemas/enrichment/ability-dsl/ability.schema.json +14 -0
@@ -1 +1 @@
1
- {"version":3,"file":"rosterizer.js","sourceRoot":"","sources":["../../src/import/rosterizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Rosterizer adapter: lower a Rosterizer roster JSON payload to a\n * {@link ParsedRoster}.\n *\n * Rosterizer (https://rosterizer.com) stores a roster as a `Roster` envelope\n * with a recursive `Asset` tree under `snapshot` (or `history.present.roster`\n * as a fallback). Every entity — faction, detachment, unit, weapon, ability,\n * enhancement — is an `Asset` keyed by `Classification§Designation` (e.g.\n * `\"Unit§Tactical Squad\"`). Children sit under `assets.included` (game pieces)\n * and `assets.traits` (modifiers, abilities, markers).\n *\n * The schema is rulebook-agnostic, so the actual `Classification` strings come\n * from whichever Rosterizer rulebook authored the roster. The constants below\n * encode the 40K convention used by the 40kdc reference rulebook; tune\n * them here without touching parser logic if a real export disagrees.\n *\n * **IP safety**: the walk reads an ALLOWLIST — `item`, `designation`, `name`,\n * `classification`, `quantity`, `meta.points`, `stats.Points.value`,\n * `aspects.Visibility`, and the recursive `assets.included`/`assets.traits`\n * children. Prose-bearing fields — `text`, `description`, `rules`, ability\n * `stats`, `_layers`, `lineage`, `processed`, `classIdentity`, `bareResourceKey`\n * — are never touched, so the importer's output is free of copyrighted prose\n * by construction.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\n// --- 40K rulebook Classification§Designation conventions. -------------------\n// These pin the strings the adapter looks for. Tune them in one place if a\n// real Rosterizer export uses different labels.\n\nconst CLS_ROSTER = \"Roster\";\nconst CLS_FACTION = \"Faction\";\nconst CLS_DETACHMENT = \"Detachment\";\nconst CLS_UNIT = \"Unit\";\nconst CLS_SQUAD = \"Squad\"; // alternative unit class some rulebooks use\nconst CLS_WEAPON = \"Weapon\";\nconst CLS_ENHANCEMENT = \"Enhancement\";\nconst CLS_BATTLE_SIZE = \"Battle Size\";\nconst CLS_TRAIT = \"Trait\";\nconst DSG_WARLORD = \"Warlord\";\nconst CHAR_CLASSIFICATIONS = new Set([\"Character\", \"Epic Hero\"]);\n\nconst POINTS_STAT_KEYS = [\"Points\", \"Pts\"];\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\n\n// --- Structural views ------------------------------------------------------\n\ninterface RawStat {\n value?: unknown;\n}\ninterface RawAspects {\n Visibility?: unknown;\n}\ninterface RawAssetChildren {\n included?: unknown;\n traits?: unknown;\n}\ninterface RawAsset {\n item?: unknown;\n name?: unknown;\n designation?: unknown;\n classification?: unknown;\n quantity?: unknown;\n aspects?: unknown;\n assets?: unknown;\n meta?: unknown;\n stats?: unknown;\n keywords?: unknown;\n}\ninterface RawRulebook {\n name?: unknown;\n game?: unknown;\n publisher?: unknown;\n url?: unknown;\n}\ninterface RawHistoryItem {\n roster?: unknown;\n note?: unknown;\n}\ninterface RawHistory {\n present?: unknown;\n}\ninterface RawEnvelope {\n rulebook?: unknown;\n snapshot?: unknown;\n history?: unknown;\n slug?: unknown;\n authors?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asObject(value: unknown): Record<string, unknown> | null {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction asNumber(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const n = Number.parseFloat(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/** Split `Classification§Designation` into its two halves. Falls back to the\n * raw `classification`/`designation` fields when `item` is absent. */\nfunction splitItem(asset: RawAsset): { classification: string; designation: string } {\n const item = asString(asset.item);\n if (item !== null) {\n const idx = item.indexOf(\"§\"); // §\n if (idx >= 0) {\n return {\n classification: item.slice(0, idx),\n designation: item.slice(idx + 1),\n };\n }\n }\n return {\n classification: asString(asset.classification) ?? \"\",\n designation: asString(asset.designation) ?? \"\",\n };\n}\n\n/** A user-facing display name for an asset: `name` override beats the\n * designation parsed out of the `item` key. */\nfunction displayName(asset: RawAsset): string {\n return asString(asset.name) ?? splitItem(asset).designation;\n}\n\nfunction quantity(asset: RawAsset): number {\n const n = asNumber(asset.quantity);\n return n !== null && n > 0 ? Math.trunc(n) : 1;\n}\n\nfunction included(asset: RawAsset): RawAsset[] {\n const a = asset.assets as RawAssetChildren | undefined;\n return asArray(a?.included) as RawAsset[];\n}\n\nfunction traits(asset: RawAsset): RawAsset[] {\n const a = asset.assets as RawAssetChildren | undefined;\n return asArray(a?.traits) as RawAsset[];\n}\n\n/** Points cost from `stats.Points.value` (or aliases) / `meta.points`, or null. */\nfunction pointsOf(asset: RawAsset): number | null {\n const stats = asObject(asset.stats);\n if (stats) {\n for (const key of POINTS_STAT_KEYS) {\n const stat = asObject(stats[key]) as RawStat | null;\n if (stat) {\n const v = asNumber(stat.value);\n if (v !== null) return Math.trunc(v);\n }\n }\n }\n const meta = asObject(asset.meta);\n if (meta) {\n const v = asNumber(meta.points);\n if (v !== null) return Math.trunc(v);\n }\n return null;\n}\n\n/** Depth-first visit of an asset and every included/trait descendant. */\nfunction walk(asset: RawAsset, visit: (a: RawAsset) => void): void {\n visit(asset);\n for (const child of included(asset)) walk(child, visit);\n for (const child of traits(asset)) walk(child, visit);\n}\n\nfunction classOf(asset: RawAsset): string {\n return splitItem(asset).classification;\n}\n\nfunction isUnitAsset(asset: RawAsset): boolean {\n const cls = classOf(asset);\n return cls === CLS_UNIT || cls === CLS_SQUAD;\n}\n\nfunction isWeaponAsset(asset: RawAsset): boolean {\n const cls = classOf(asset);\n // Match exact \"Weapon\", or any \"<X> Weapon\" classification (e.g. \"Ranged Weapon\").\n return cls === CLS_WEAPON || cls.endsWith(` ${CLS_WEAPON}`);\n}\n\nfunction isEnhancementAsset(asset: RawAsset): boolean {\n return classOf(asset) === CLS_ENHANCEMENT;\n}\n\nfunction isCharacterAsset(asset: RawAsset): boolean {\n const keywords = asObject(asset.keywords);\n if (keywords) {\n for (const list of Object.values(keywords)) {\n for (const kw of asArray(list)) {\n if (typeof kw === \"string\" && CHAR_CLASSIFICATIONS.has(kw)) return true;\n }\n }\n }\n // Any nested trait classified as Character also flags the unit.\n for (const t of traits(asset)) {\n if (CHAR_CLASSIFICATIONS.has(classOf(t))) return true;\n const dsg = displayName(t);\n if (CHAR_CLASSIFICATIONS.has(dsg)) return true;\n }\n return false;\n}\n\nfunction isWarlordTrait(asset: RawAsset): boolean {\n const { classification, designation } = splitItem(asset);\n if (designation === DSG_WARLORD) return true;\n return classification === CLS_TRAIT && designation === DSG_WARLORD;\n}\n\n/** Sum every Weapon/Enhancement-bearing leaf quantity to derive the unit's\n * model count. Falls back to `quantity(unit)` when the tree has no per-model\n * markers (single-model entries). */\nfunction modelCount(unit: RawAsset): number {\n // Rosterizer doesn't carve \"model\" out separately from \"unit\" the way\n // BattleScribe does; the unit's own quantity is the model count for\n // squads. For multi-model squads with explicit per-model children, each\n // child unit-class asset's quantity contributes.\n let nested = 0;\n for (const child of included(unit)) {\n if (isUnitAsset(child)) nested += quantity(child);\n }\n return nested > 0 ? nested : quantity(unit);\n}\n\nfunction parseUnit(unit: RawAsset): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const child of included(unit)) {\n walk(child, (a) => {\n if (isEnhancementAsset(a)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = displayName(a);\n enhancement_points = pointsOf(a);\n }\n return;\n }\n if (isWeaponAsset(a)) {\n wargear.push({ raw_name: displayName(a), count: quantity(a) });\n }\n });\n }\n for (const t of traits(unit)) {\n walk(t, (a) => {\n if (isWarlordTrait(a)) is_warlord = true;\n });\n }\n\n return {\n raw_name: displayName(unit),\n is_character: isCharacterAsset(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\n/** Resolve the snapshot Asset tree from an envelope, preferring the explicit\n * `snapshot` field but falling through to the history-present roster. */\nfunction snapshotOf(env: RawEnvelope): RawAsset | null {\n const snap = asObject(env.snapshot);\n if (snap) return snap as RawAsset;\n const history = asObject(env.history) as RawHistory | null;\n const present = history && asObject(history.present);\n if (present) {\n const present_roster = asObject((present as RawHistoryItem).roster);\n if (present_roster) return present_roster as RawAsset;\n }\n return null;\n}\n\nfunction isRosterizerEnvelope(decoded: unknown): decoded is RawEnvelope {\n const env = asObject(decoded) as RawEnvelope | null;\n if (!env) return false;\n if (!asObject(env.rulebook)) return false;\n return snapshotOf(env) !== null;\n}\n\n/** Find the first child Asset with the given classification, if any. */\nfunction findChildByClass(asset: RawAsset, cls: string): RawAsset | null {\n for (const c of included(asset)) {\n if (classOf(c) === cls) return c;\n }\n return null;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\nexport const rosterizerAdapter: FormatAdapter = {\n id: \"rosterizer\",\n\n matches(decoded: unknown): boolean {\n return isRosterizerEnvelope(decoded);\n },\n\n parse(decoded: unknown): ParsedRoster {\n if (!isRosterizerEnvelope(decoded)) {\n throw new Error(\"rosterizer: payload is not a Rosterizer roster envelope\");\n }\n const snapshot = snapshotOf(decoded);\n if (snapshot === null) {\n throw new Error(\"rosterizer: envelope has no snapshot or history.present.roster\");\n }\n\n // Treat the snapshot as the roster root regardless of its `item` value —\n // some exports root at `Roster§Roster`, others at the faction itself.\n const root = snapshot;\n\n // Roster-level metadata children. Faction and detachment come from the\n // first child Asset of their respective classification; battle size the\n // same way. Walk the whole tree (rather than just root.assets.included)\n // so nested-force shapes still pick up the markers.\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const factions: string[] = [];\n walk(root, (a) => {\n const cls = classOf(a);\n if (cls === CLS_FACTION) {\n const name = displayName(a);\n if (!factions.includes(name)) factions.push(name);\n faction_raw_name ??= name;\n } else if (cls === CLS_DETACHMENT) {\n detachment_raw_name ??= displayName(a);\n } else if (cls === CLS_BATTLE_SIZE) {\n battle_size_raw ??= displayName(a);\n }\n });\n\n // Allow the rulebook envelope to carry a battle-size override (e.g.\n // `rulebook.notes: \"2000 Point limit\"`) — strictly optional.\n if (battle_size_raw === null) {\n const rulebook = asObject((decoded as RawEnvelope).rulebook) as RawRulebook | null;\n // Intentionally not reading rulebook.notes — it may carry prose.\n void rulebook;\n }\n\n // Collect units: any Unit/Squad asset anywhere in the tree, excluding\n // ones nested under another unit (those are attached leaders we'll fold\n // into leader_attachment via the resolver — but ParsedUnit doesn't model\n // them yet, so for v1 every Unit asset becomes a top-level ParsedUnit).\n const units: ParsedUnit[] = [];\n const collectUnits = (a: RawAsset, underUnit: boolean): void => {\n if (isUnitAsset(a) && !underUnit) {\n units.push(parseUnit(a));\n for (const c of included(a)) collectUnits(c, true);\n for (const c of traits(a)) collectUnits(c, true);\n return;\n }\n if (isUnitAsset(a) && underUnit) {\n // Nested unit (leader on a body, body on a leader, etc.) — emit it as\n // its own top-level ParsedUnit so the resolver can match its id and\n // the leader-attachment inference pass can link the two.\n units.push(parseUnit(a));\n return;\n }\n for (const c of included(a)) collectUnits(c, underUnit);\n for (const c of traits(a)) collectUnits(c, underUnit);\n };\n collectUnits(root, false);\n\n // Roster-level total: prefer an explicit Points stat on the root, else\n // sum every unit's (base + enhancement) contribution.\n const total_reported = pointsOf(root);\n let total_computed = 0;\n for (const u of units) {\n total_computed += u.points ?? 0;\n total_computed += u.enhancement_points ?? 0;\n }\n\n const env = decoded as RawEnvelope;\n const rulebook = asObject(env.rulebook) as RawRulebook | null;\n const generated_by = rulebook ? asString(rulebook.name) ?? asString(rulebook.url) : null;\n const name = displayName(root) || asString(rulebook?.name) || \"Imported roster\";\n\n return {\n name,\n generated_by,\n faction_raw_name,\n detachment_raw_name,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n\n// Internals re-exported for the symmetric exporter and unit tests.\nexport const _internals = {\n CLS_ROSTER,\n CLS_FACTION,\n CLS_DETACHMENT,\n CLS_UNIT,\n CLS_WEAPON,\n CLS_ENHANCEMENT,\n CLS_BATTLE_SIZE,\n CLS_TRAIT,\n DSG_WARLORD,\n POINTS_STAT_KEYS,\n splitItem,\n displayName,\n classOf,\n findChildByClass,\n};\n"]}
1
+ {"version":3,"file":"rosterizer.js","sourceRoot":"","sources":["../../src/import/rosterizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Rosterizer adapter: lower a Rosterizer roster JSON payload to a\n * {@link ParsedRoster}.\n *\n * Rosterizer (https://rosterizer.com) stores a roster as a `Roster` envelope\n * with a recursive `Asset` tree under `snapshot` (or `history.present.roster`\n * as a fallback). Every entity — faction, detachment, unit, weapon, ability,\n * enhancement — is an `Asset` keyed by `Classification§Designation` (e.g.\n * `\"Unit§Tactical Squad\"`). Children sit under `assets.included` (game pieces)\n * and `assets.traits` (modifiers, abilities, markers).\n *\n * The schema is rulebook-agnostic, so the actual `Classification` strings come\n * from whichever Rosterizer rulebook authored the roster. The constants below\n * encode the 40K convention used by the 40kdc reference rulebook; tune\n * them here without touching parser logic if a real export disagrees.\n *\n * **IP safety**: the walk reads an ALLOWLIST — `item`, `designation`, `name`,\n * `classification`, `quantity`, `meta.points`, `stats.Points.value`,\n * `aspects.Visibility`, and the recursive `assets.included`/`assets.traits`\n * children. Prose-bearing fields — `text`, `description`, `rules`, ability\n * `stats`, `_layers`, `lineage`, `processed`, `classIdentity`, `bareResourceKey`\n * — are never touched, so the importer's output is free of copyrighted prose\n * by construction.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\n// --- 40K rulebook Classification§Designation conventions. -------------------\n// These pin the strings the adapter looks for. Tune them in one place if a\n// real Rosterizer export uses different labels.\n\nconst CLS_ROSTER = \"Roster\";\nconst CLS_FACTION = \"Faction\";\nconst CLS_DETACHMENT = \"Detachment\";\nconst CLS_UNIT = \"Unit\";\nconst CLS_SQUAD = \"Squad\"; // alternative unit class some rulebooks use\nconst CLS_WEAPON = \"Weapon\";\nconst CLS_ENHANCEMENT = \"Enhancement\";\nconst CLS_BATTLE_SIZE = \"Battle Size\";\nconst CLS_TRAIT = \"Trait\";\nconst DSG_WARLORD = \"Warlord\";\nconst CHAR_CLASSIFICATIONS = new Set([\"Character\", \"Epic Hero\"]);\n\nconst POINTS_STAT_KEYS = [\"Points\", \"Pts\"];\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\n\n// --- Structural views ------------------------------------------------------\n\ninterface RawStat {\n value?: unknown;\n}\ninterface RawAspects {\n Visibility?: unknown;\n}\ninterface RawAssetChildren {\n included?: unknown;\n traits?: unknown;\n}\ninterface RawAsset {\n item?: unknown;\n name?: unknown;\n designation?: unknown;\n classification?: unknown;\n quantity?: unknown;\n aspects?: unknown;\n assets?: unknown;\n meta?: unknown;\n stats?: unknown;\n keywords?: unknown;\n}\ninterface RawRulebook {\n name?: unknown;\n game?: unknown;\n publisher?: unknown;\n url?: unknown;\n}\ninterface RawHistoryItem {\n roster?: unknown;\n note?: unknown;\n}\ninterface RawHistory {\n present?: unknown;\n}\ninterface RawEnvelope {\n rulebook?: unknown;\n snapshot?: unknown;\n history?: unknown;\n slug?: unknown;\n authors?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asObject(value: unknown): Record<string, unknown> | null {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction asNumber(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const n = Number.parseFloat(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/** Split `Classification§Designation` into its two halves. Falls back to the\n * raw `classification`/`designation` fields when `item` is absent. */\nfunction splitItem(asset: RawAsset): { classification: string; designation: string } {\n const item = asString(asset.item);\n if (item !== null) {\n const idx = item.indexOf(\"§\"); // §\n if (idx >= 0) {\n return {\n classification: item.slice(0, idx),\n designation: item.slice(idx + 1),\n };\n }\n }\n return {\n classification: asString(asset.classification) ?? \"\",\n designation: asString(asset.designation) ?? \"\",\n };\n}\n\n/** A user-facing display name for an asset: `name` override beats the\n * designation parsed out of the `item` key. */\nfunction displayName(asset: RawAsset): string {\n return asString(asset.name) ?? splitItem(asset).designation;\n}\n\nfunction quantity(asset: RawAsset): number {\n const n = asNumber(asset.quantity);\n return n !== null && n > 0 ? Math.trunc(n) : 1;\n}\n\nfunction included(asset: RawAsset): RawAsset[] {\n const a = asset.assets as RawAssetChildren | undefined;\n return asArray(a?.included) as RawAsset[];\n}\n\nfunction traits(asset: RawAsset): RawAsset[] {\n const a = asset.assets as RawAssetChildren | undefined;\n return asArray(a?.traits) as RawAsset[];\n}\n\n/** Points cost from `stats.Points.value` (or aliases) / `meta.points`, or null. */\nfunction pointsOf(asset: RawAsset): number | null {\n const stats = asObject(asset.stats);\n if (stats) {\n for (const key of POINTS_STAT_KEYS) {\n const stat = asObject(stats[key]) as RawStat | null;\n if (stat) {\n const v = asNumber(stat.value);\n if (v !== null) return Math.trunc(v);\n }\n }\n }\n const meta = asObject(asset.meta);\n if (meta) {\n const v = asNumber(meta.points);\n if (v !== null) return Math.trunc(v);\n }\n return null;\n}\n\n/** Depth-first visit of an asset and every included/trait descendant. */\nfunction walk(asset: RawAsset, visit: (a: RawAsset) => void): void {\n visit(asset);\n for (const child of included(asset)) walk(child, visit);\n for (const child of traits(asset)) walk(child, visit);\n}\n\nfunction classOf(asset: RawAsset): string {\n return splitItem(asset).classification;\n}\n\nfunction isUnitAsset(asset: RawAsset): boolean {\n const cls = classOf(asset);\n return cls === CLS_UNIT || cls === CLS_SQUAD;\n}\n\nfunction isWeaponAsset(asset: RawAsset): boolean {\n const cls = classOf(asset);\n // Match exact \"Weapon\", or any \"<X> Weapon\" classification (e.g. \"Ranged Weapon\").\n return cls === CLS_WEAPON || cls.endsWith(` ${CLS_WEAPON}`);\n}\n\nfunction isEnhancementAsset(asset: RawAsset): boolean {\n return classOf(asset) === CLS_ENHANCEMENT;\n}\n\nfunction isCharacterAsset(asset: RawAsset): boolean {\n const keywords = asObject(asset.keywords);\n if (keywords) {\n for (const list of Object.values(keywords)) {\n for (const kw of asArray(list)) {\n if (typeof kw === \"string\" && CHAR_CLASSIFICATIONS.has(kw)) return true;\n }\n }\n }\n // Any nested trait classified as Character also flags the unit.\n for (const t of traits(asset)) {\n if (CHAR_CLASSIFICATIONS.has(classOf(t))) return true;\n const dsg = displayName(t);\n if (CHAR_CLASSIFICATIONS.has(dsg)) return true;\n }\n return false;\n}\n\nfunction isWarlordTrait(asset: RawAsset): boolean {\n const { classification, designation } = splitItem(asset);\n if (designation === DSG_WARLORD) return true;\n return classification === CLS_TRAIT && designation === DSG_WARLORD;\n}\n\n/** Sum every Weapon/Enhancement-bearing leaf quantity to derive the unit's\n * model count. Falls back to `quantity(unit)` when the tree has no per-model\n * markers (single-model entries). */\nfunction modelCount(unit: RawAsset): number {\n // Rosterizer doesn't carve \"model\" out separately from \"unit\" the way\n // BattleScribe does; the unit's own quantity is the model count for\n // squads. For multi-model squads with explicit per-model children, each\n // child unit-class asset's quantity contributes.\n let nested = 0;\n for (const child of included(unit)) {\n if (isUnitAsset(child)) nested += quantity(child);\n }\n return nested > 0 ? nested : quantity(unit);\n}\n\nfunction parseUnit(unit: RawAsset): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const child of included(unit)) {\n walk(child, (a) => {\n if (isEnhancementAsset(a)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = displayName(a);\n enhancement_points = pointsOf(a);\n }\n return;\n }\n if (isWeaponAsset(a)) {\n wargear.push({ raw_name: displayName(a), count: quantity(a) });\n }\n });\n }\n for (const t of traits(unit)) {\n walk(t, (a) => {\n if (isWarlordTrait(a)) is_warlord = true;\n });\n }\n\n return {\n raw_name: displayName(unit),\n is_character: isCharacterAsset(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\n/** Resolve the snapshot Asset tree from an envelope, preferring the explicit\n * `snapshot` field but falling through to the history-present roster. */\nfunction snapshotOf(env: RawEnvelope): RawAsset | null {\n const snap = asObject(env.snapshot);\n if (snap) return snap as RawAsset;\n const history = asObject(env.history) as RawHistory | null;\n const present = history && asObject(history.present);\n if (present) {\n const present_roster = asObject((present as RawHistoryItem).roster);\n if (present_roster) return present_roster as RawAsset;\n }\n return null;\n}\n\nfunction isRosterizerEnvelope(decoded: unknown): decoded is RawEnvelope {\n const env = asObject(decoded) as RawEnvelope | null;\n if (!env) return false;\n if (!asObject(env.rulebook)) return false;\n return snapshotOf(env) !== null;\n}\n\n/** Find the first child Asset with the given classification, if any. */\nfunction findChildByClass(asset: RawAsset, cls: string): RawAsset | null {\n for (const c of included(asset)) {\n if (classOf(c) === cls) return c;\n }\n return null;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\nexport const rosterizerAdapter: FormatAdapter = {\n id: \"rosterizer\",\n\n matches(decoded: unknown): boolean {\n return isRosterizerEnvelope(decoded);\n },\n\n parse(decoded: unknown): ParsedRoster {\n if (!isRosterizerEnvelope(decoded)) {\n throw new Error(\"rosterizer: payload is not a Rosterizer roster envelope\");\n }\n const snapshot = snapshotOf(decoded);\n if (snapshot === null) {\n throw new Error(\"rosterizer: envelope has no snapshot or history.present.roster\");\n }\n\n // Treat the snapshot as the roster root regardless of its `item` value —\n // some exports root at `Roster§Roster`, others at the faction itself.\n const root = snapshot;\n\n // Roster-level metadata children. Faction and detachment come from the\n // first child Asset of their respective classification; battle size the\n // same way. Walk the whole tree (rather than just root.assets.included)\n // so nested-force shapes still pick up the markers.\n let faction_raw_name: string | null = null;\n const detachment_raw_names: string[] = [];\n let battle_size_raw: string | null = null;\n const factions: string[] = [];\n walk(root, (a) => {\n const cls = classOf(a);\n if (cls === CLS_FACTION) {\n const name = displayName(a);\n if (!factions.includes(name)) factions.push(name);\n faction_raw_name ??= name;\n } else if (cls === CLS_DETACHMENT) {\n detachment_raw_names.push(displayName(a));\n } else if (cls === CLS_BATTLE_SIZE) {\n battle_size_raw ??= displayName(a);\n }\n });\n\n // Allow the rulebook envelope to carry a battle-size override (e.g.\n // `rulebook.notes: \"2000 Point limit\"`) — strictly optional.\n if (battle_size_raw === null) {\n const rulebook = asObject((decoded as RawEnvelope).rulebook) as RawRulebook | null;\n // Intentionally not reading rulebook.notes — it may carry prose.\n void rulebook;\n }\n\n // Collect units: any Unit/Squad asset anywhere in the tree, excluding\n // ones nested under another unit (those are attached leaders we'll fold\n // into leader_attachment via the resolver — but ParsedUnit doesn't model\n // them yet, so for v1 every Unit asset becomes a top-level ParsedUnit).\n const units: ParsedUnit[] = [];\n const collectUnits = (a: RawAsset, underUnit: boolean): void => {\n if (isUnitAsset(a) && !underUnit) {\n units.push(parseUnit(a));\n for (const c of included(a)) collectUnits(c, true);\n for (const c of traits(a)) collectUnits(c, true);\n return;\n }\n if (isUnitAsset(a) && underUnit) {\n // Nested unit (leader on a body, body on a leader, etc.) — emit it as\n // its own top-level ParsedUnit so the resolver can match its id and\n // the leader-attachment inference pass can link the two.\n units.push(parseUnit(a));\n return;\n }\n for (const c of included(a)) collectUnits(c, underUnit);\n for (const c of traits(a)) collectUnits(c, underUnit);\n };\n collectUnits(root, false);\n\n // Roster-level total: prefer an explicit Points stat on the root, else\n // sum every unit's (base + enhancement) contribution.\n const total_reported = pointsOf(root);\n let total_computed = 0;\n for (const u of units) {\n total_computed += u.points ?? 0;\n total_computed += u.enhancement_points ?? 0;\n }\n\n const env = decoded as RawEnvelope;\n const rulebook = asObject(env.rulebook) as RawRulebook | null;\n const generated_by = rulebook ? asString(rulebook.name) ?? asString(rulebook.url) : null;\n const name = displayName(root) || asString(rulebook?.name) || \"Imported roster\";\n\n return {\n name,\n generated_by,\n faction_raw_name,\n detachment_raw_names,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n\n// Internals re-exported for the symmetric exporter and unit tests.\nexport const _internals = {\n CLS_ROSTER,\n CLS_FACTION,\n CLS_DETACHMENT,\n CLS_UNIT,\n CLS_WEAPON,\n CLS_ENHANCEMENT,\n CLS_BATTLE_SIZE,\n CLS_TRAIT,\n DSG_WARLORD,\n POINTS_STAT_KEYS,\n splitItem,\n displayName,\n classOf,\n findChildByClass,\n};\n"]}
@@ -19,7 +19,7 @@
19
19
  /** A 40kdc battle size (mirrors the shared `battle-size` def). */
20
20
  export type BattleSize = "incursion" | "strike-force";
21
21
  /** Diagnostic warning codes emitted during an import. */
22
- export type WarningCode = "faction-unresolved" | "unit-unresolved" | "weapon-unresolved" | "enhancement-unresolved" | "detachment-unresolved" | "battle-size-unmapped" | "points-mismatch" | "leader-attachment-inferred" | "multi-force" | "unknown-field";
22
+ export type WarningCode = "faction-unresolved" | "unit-unresolved" | "weapon-unresolved" | "enhancement-unresolved" | "detachment-unresolved" | "detachment-points-exceeded" | "battle-size-unmapped" | "points-mismatch" | "leader-attachment-inferred" | "multi-force" | "unknown-field";
23
23
  /** A near-match suggestion offered when resolution fails. */
24
24
  export interface Candidate {
25
25
  id: string;
@@ -44,6 +44,12 @@ export interface RosterWargear {
44
44
  ref: ResolvedRef;
45
45
  count: number;
46
46
  }
47
+ /** One detachment on the roster, paired with its resolved DP cost. */
48
+ export interface RosterDetachment {
49
+ ref: ResolvedRef;
50
+ /** DP cost (1–3) from the resolved detachment entity; null when unresolved or unrecorded. */
51
+ dp_cost: number | null;
52
+ }
47
53
  /** An inferred, always-provisional leader→bodyguard attachment. */
48
54
  export interface RosterLeaderAttachment {
49
55
  bodyguard_ref: ResolvedRef;
@@ -64,7 +70,7 @@ export interface RosterUnit {
64
70
  }
65
71
  /** Identifier for the adapter that produced this roster. New format adapters
66
72
  * extend this union; `roster.schema.json` keeps the canonical enum. */
67
- export type RosterFormat = "listforge" | "listforge-text" | "newrecruit-json" | "newrecruit-wtc-compact" | "newrecruit-wtc-full" | "newrecruit-simple" | "rosterizer" | "gw";
73
+ export type RosterFormat = "listforge" | "listforge-text" | "newrecruit-json" | "newrecruit-wtc-compact" | "newrecruit-wtc-full" | "newrecruit-simple" | "rosterizer" | "gw" | "roster-json";
68
74
  /** Provenance of the imported list. */
69
75
  export interface RosterSource {
70
76
  format: RosterFormat;
@@ -73,6 +79,8 @@ export interface RosterSource {
73
79
  /** Point totals; reported and computed are kept distinct, never reconciled. */
74
80
  export interface RosterPoints {
75
81
  declared_limit: number | null;
82
+ /** 11e detachment-point budget from the battle size (strike-force 3, incursion 2); null when unknown. */
83
+ detachment_cap: number | null;
76
84
  total_reported: number | null;
77
85
  total_computed: number;
78
86
  }
@@ -100,7 +108,7 @@ export interface Roster {
100
108
  name: string;
101
109
  source: RosterSource;
102
110
  faction_id: string | null;
103
- detachment_id: string | null;
111
+ detachments: RosterDetachment[];
104
112
  battle_size: BattleSize | null;
105
113
  points: RosterPoints;
106
114
  units: RosterUnit[];
@@ -136,8 +144,11 @@ export interface ParsedRoster {
136
144
  generated_by: string | null;
137
145
  /** Raw faction name from the source (e.g. "Grey Knights"). */
138
146
  faction_raw_name: string | null;
139
- /** Raw detachment name (e.g. "Banishers"). */
140
- detachment_raw_name: string | null;
147
+ /**
148
+ * Raw detachment names in source order (e.g. ["Gladius Task Force"]). 11e lists may
149
+ * carry several; most formats and pre-11e lists carry zero or one.
150
+ */
151
+ detachment_raw_names: string[];
141
152
  /** Raw battle-size label (e.g. "2. Strike Force (2000 Point limit)"). */
142
153
  battle_size_raw: string | null;
143
154
  /** Points limit parsed from the battle-size label, if any. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG","sourcesContent":["/**\n * Types for the army-list importer.\n *\n * Two layers live here:\n * - The **output** types ({@link Roster} and friends) mirror\n * `schemas/core/roster.schema.json` field-for-field. They are hand-authored\n * rather than generated so importer work isn't gated on the Rust→typify codegen\n * round-trip; the AJV validator (against the real schema) is the source of truth\n * for conformance.\n * - The **intermediate** type ({@link ParsedRoster}) is format-agnostic: a parser\n * adapter lowers a source payload to this shape (raw names + counts only, no\n * resolved ids), and {@link resolve} turns it into a {@link Roster}.\n *\n * Nothing here ever carries reproduced rules or ability text — only permitted\n * facts (names, counts, points, keywords, entity ids).\n *\n * @packageDocumentation\n */\n\n/** A 40kdc battle size (mirrors the shared `battle-size` def). */\nexport type BattleSize = \"incursion\" | \"strike-force\";\n\n/** Diagnostic warning codes emitted during an import. */\nexport type WarningCode =\n | \"faction-unresolved\"\n | \"unit-unresolved\"\n | \"weapon-unresolved\"\n | \"enhancement-unresolved\"\n | \"detachment-unresolved\"\n | \"battle-size-unmapped\"\n | \"points-mismatch\"\n | \"leader-attachment-inferred\"\n | \"multi-force\"\n | \"unknown-field\";\n\n// ---------------------------------------------------------------------------\n// Output types (mirror roster.schema.json)\n// ---------------------------------------------------------------------------\n\n/** A near-match suggestion offered when resolution fails. */\nexport interface Candidate {\n id: string;\n name: string;\n}\n\n/**\n * A reference to a 40kdc entity that may or may not have resolved. Retains the\n * source's raw name so the import is lossless even on a miss.\n */\nexport interface ResolvedRef {\n /** Resolved entity id, or null when no match was found. */\n id: string | null;\n /** The display name exactly as it appeared in the source payload. */\n raw_name: string;\n /** True iff {@link id} is non-null. */\n resolved: boolean;\n /** Up to 5 best-guess alternatives when resolution failed. */\n candidates: Candidate[];\n}\n\n/** A weapon/wargear selection on a unit. */\nexport interface RosterWargear {\n ref: ResolvedRef;\n count: number;\n}\n\n/** An inferred, always-provisional leader→bodyguard attachment. */\nexport interface RosterLeaderAttachment {\n bodyguard_ref: ResolvedRef;\n provisional: boolean;\n}\n\n/** One unit entry in a roster. */\nexport interface RosterUnit {\n ref: ResolvedRef;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement: ResolvedRef | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: RosterWargear[];\n leader_attachment: RosterLeaderAttachment | null;\n}\n\n/** Identifier for the adapter that produced this roster. New format adapters\n * extend this union; `roster.schema.json` keeps the canonical enum. */\nexport type RosterFormat =\n | \"listforge\"\n | \"listforge-text\"\n | \"newrecruit-json\"\n | \"newrecruit-wtc-compact\"\n | \"newrecruit-wtc-full\"\n | \"newrecruit-simple\"\n | \"rosterizer\"\n | \"gw\";\n\n/** Provenance of the imported list. */\nexport interface RosterSource {\n format: RosterFormat;\n generated_by: string | null;\n}\n\n/** Point totals; reported and computed are kept distinct, never reconciled. */\nexport interface RosterPoints {\n declared_limit: number | null;\n total_reported: number | null;\n total_computed: number;\n}\n\n/** A single diagnostic warning. */\nexport interface Warning {\n code: WarningCode;\n message: string;\n raw_name: string | null;\n}\n\n/** A summary of what resolved and what did not during the import. */\nexport interface Diagnostics {\n resolved_units: number;\n unresolved_units: number;\n resolved_weapons: number;\n unresolved_weapons: number;\n warnings: Warning[];\n}\n\n/** Reference to the game edition + dataslate (mirrors game-version-ref). */\nexport interface GameVersionRef {\n edition: string;\n dataslate: string;\n}\n\n/** A fully-resolved army list. Validates against `roster.schema.json`. */\nexport interface Roster {\n name: string;\n source: RosterSource;\n faction_id: string | null;\n detachment_id: string | null;\n battle_size: BattleSize | null;\n points: RosterPoints;\n units: RosterUnit[];\n game_version: GameVersionRef;\n diagnostics: Diagnostics;\n}\n\n// ---------------------------------------------------------------------------\n// Intermediate types (format-agnostic; produced by a parser adapter)\n// ---------------------------------------------------------------------------\n\n/** A weapon/wargear selection before id resolution. */\nexport interface ParsedWargear {\n raw_name: string;\n count: number;\n}\n\n/** A unit selection before id resolution. */\nexport interface ParsedUnit {\n raw_name: string;\n /** True when the source classifies this as a character/leader-capable model. */\n is_character: boolean;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: ParsedWargear[];\n}\n\n/**\n * The format-agnostic intermediate. A {@link FormatAdapter} produces this from a\n * decoded source payload; {@link resolve} consumes it. Contains only raw display\n * names and counts — never reproduced rules text.\n */\nexport interface ParsedRoster {\n name: string;\n generated_by: string | null;\n /** Raw faction name from the source (e.g. \"Grey Knights\"). */\n faction_raw_name: string | null;\n /** Raw detachment name (e.g. \"Banishers\"). */\n detachment_raw_name: string | null;\n /** Raw battle-size label (e.g. \"2. Strike Force (2000 Point limit)\"). */\n battle_size_raw: string | null;\n /** Points limit parsed from the battle-size label, if any. */\n declared_limit: number | null;\n /** Total points reported by the source cost block. */\n total_reported: number | null;\n /** Points summed from every cost line in the source tree. */\n total_computed: number;\n units: ParsedUnit[];\n /** True when the source contained more than one distinct faction. */\n multi_force: boolean;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG","sourcesContent":["/**\n * Types for the army-list importer.\n *\n * Two layers live here:\n * - The **output** types ({@link Roster} and friends) mirror\n * `schemas/core/roster.schema.json` field-for-field. They are hand-authored\n * rather than generated so importer work isn't gated on the Rust→typify codegen\n * round-trip; the AJV validator (against the real schema) is the source of truth\n * for conformance.\n * - The **intermediate** type ({@link ParsedRoster}) is format-agnostic: a parser\n * adapter lowers a source payload to this shape (raw names + counts only, no\n * resolved ids), and {@link resolve} turns it into a {@link Roster}.\n *\n * Nothing here ever carries reproduced rules or ability text — only permitted\n * facts (names, counts, points, keywords, entity ids).\n *\n * @packageDocumentation\n */\n\n/** A 40kdc battle size (mirrors the shared `battle-size` def). */\nexport type BattleSize = \"incursion\" | \"strike-force\";\n\n/** Diagnostic warning codes emitted during an import. */\nexport type WarningCode =\n | \"faction-unresolved\"\n | \"unit-unresolved\"\n | \"weapon-unresolved\"\n | \"enhancement-unresolved\"\n | \"detachment-unresolved\"\n | \"detachment-points-exceeded\"\n | \"battle-size-unmapped\"\n | \"points-mismatch\"\n | \"leader-attachment-inferred\"\n | \"multi-force\"\n | \"unknown-field\";\n\n// ---------------------------------------------------------------------------\n// Output types (mirror roster.schema.json)\n// ---------------------------------------------------------------------------\n\n/** A near-match suggestion offered when resolution fails. */\nexport interface Candidate {\n id: string;\n name: string;\n}\n\n/**\n * A reference to a 40kdc entity that may or may not have resolved. Retains the\n * source's raw name so the import is lossless even on a miss.\n */\nexport interface ResolvedRef {\n /** Resolved entity id, or null when no match was found. */\n id: string | null;\n /** The display name exactly as it appeared in the source payload. */\n raw_name: string;\n /** True iff {@link id} is non-null. */\n resolved: boolean;\n /** Up to 5 best-guess alternatives when resolution failed. */\n candidates: Candidate[];\n}\n\n/** A weapon/wargear selection on a unit. */\nexport interface RosterWargear {\n ref: ResolvedRef;\n count: number;\n}\n\n/** One detachment on the roster, paired with its resolved DP cost. */\nexport interface RosterDetachment {\n ref: ResolvedRef;\n /** DP cost (1–3) from the resolved detachment entity; null when unresolved or unrecorded. */\n dp_cost: number | null;\n}\n\n/** An inferred, always-provisional leader→bodyguard attachment. */\nexport interface RosterLeaderAttachment {\n bodyguard_ref: ResolvedRef;\n provisional: boolean;\n}\n\n/** One unit entry in a roster. */\nexport interface RosterUnit {\n ref: ResolvedRef;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement: ResolvedRef | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: RosterWargear[];\n leader_attachment: RosterLeaderAttachment | null;\n}\n\n/** Identifier for the adapter that produced this roster. New format adapters\n * extend this union; `roster.schema.json` keeps the canonical enum. */\nexport type RosterFormat =\n | \"listforge\"\n | \"listforge-text\"\n | \"newrecruit-json\"\n | \"newrecruit-wtc-compact\"\n | \"newrecruit-wtc-full\"\n | \"newrecruit-simple\"\n | \"rosterizer\"\n | \"gw\"\n | \"roster-json\";\n\n/** Provenance of the imported list. */\nexport interface RosterSource {\n format: RosterFormat;\n generated_by: string | null;\n}\n\n/** Point totals; reported and computed are kept distinct, never reconciled. */\nexport interface RosterPoints {\n declared_limit: number | null;\n /** 11e detachment-point budget from the battle size (strike-force 3, incursion 2); null when unknown. */\n detachment_cap: number | null;\n total_reported: number | null;\n total_computed: number;\n}\n\n/** A single diagnostic warning. */\nexport interface Warning {\n code: WarningCode;\n message: string;\n raw_name: string | null;\n}\n\n/** A summary of what resolved and what did not during the import. */\nexport interface Diagnostics {\n resolved_units: number;\n unresolved_units: number;\n resolved_weapons: number;\n unresolved_weapons: number;\n warnings: Warning[];\n}\n\n/** Reference to the game edition + dataslate (mirrors game-version-ref). */\nexport interface GameVersionRef {\n edition: string;\n dataslate: string;\n}\n\n/** A fully-resolved army list. Validates against `roster.schema.json`. */\nexport interface Roster {\n name: string;\n source: RosterSource;\n faction_id: string | null;\n detachments: RosterDetachment[];\n battle_size: BattleSize | null;\n points: RosterPoints;\n units: RosterUnit[];\n game_version: GameVersionRef;\n diagnostics: Diagnostics;\n}\n\n// ---------------------------------------------------------------------------\n// Intermediate types (format-agnostic; produced by a parser adapter)\n// ---------------------------------------------------------------------------\n\n/** A weapon/wargear selection before id resolution. */\nexport interface ParsedWargear {\n raw_name: string;\n count: number;\n}\n\n/** A unit selection before id resolution. */\nexport interface ParsedUnit {\n raw_name: string;\n /** True when the source classifies this as a character/leader-capable model. */\n is_character: boolean;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: ParsedWargear[];\n}\n\n/**\n * The format-agnostic intermediate. A {@link FormatAdapter} produces this from a\n * decoded source payload; {@link resolve} consumes it. Contains only raw display\n * names and counts — never reproduced rules text.\n */\nexport interface ParsedRoster {\n name: string;\n generated_by: string | null;\n /** Raw faction name from the source (e.g. \"Grey Knights\"). */\n faction_raw_name: string | null;\n /**\n * Raw detachment names in source order (e.g. [\"Gladius Task Force\"]). 11e lists may\n * carry several; most formats and pre-11e lists carry zero or one.\n */\n detachment_raw_names: string[];\n /** Raw battle-size label (e.g. \"2. Strike Force (2000 Point limit)\"). */\n battle_size_raw: string | null;\n /** Points limit parsed from the battle-size label, if any. */\n declared_limit: number | null;\n /** Total points reported by the source cost block. */\n total_reported: number | null;\n /** Points summed from every cost line in the source tree. */\n total_computed: number;\n units: ParsedUnit[];\n /** True when the source contained more than one distinct faction. */\n multi_force: boolean;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export { resolveLayout, polygonCentroid, footprintVertices, orientedOffsets, Ter
7
7
  export type { ResolvedPiece, ResolvedVec2, BoardEdge, FeatureRef, Keystone, KeystoneMeasurement, DimensionLine, SolveInput, TriangulationLine, TriangulateInput, AttachLine, AttachPiece, AttachInput, } from "./terrain/index.js";
8
8
  export * from "./scoring/index.js";
9
9
  export * from "./cruncher/index.js";
10
+ export * from "./compare.js";
11
+ export * from "./scope.js";
10
12
  export { importListForge, importNewRecruit, importRoster, tryImportRoster, decodeListForge, } from "./import/index.js";
11
13
  export { exportRoster, newRecruitJsonSerializer, newRecruitSimpleSerializer, newRecruitWtcCompactSerializer, newRecruitWtcFullSerializer, rosterJsonSerializer, rosterizerSerializer, } from "./export/index.js";
12
14
  export type { ExportFormat, RosterSerializer } from "./export/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAC;AAGhC,cAAc,gBAAgB,CAAC;AAI/B,cAAc,sBAAsB,CAAC;AAKrC,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKnD,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,SAAS,EACT,UAAU,EACV,QAAQ,EACR,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAK5B,cAAc,oBAAoB,CAAC;AAKnC,cAAc,qBAAqB,CAAC;AAUpC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EACV,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,MAAM,EACN,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,EACP,WAAW,EACX,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAC;AAGhC,cAAc,gBAAgB,CAAC;AAI/B,cAAc,sBAAsB,CAAC;AAKrC,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKnD,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,SAAS,EACT,UAAU,EACV,QAAQ,EACR,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAK5B,cAAc,oBAAoB,CAAC;AAKnC,cAAc,qBAAqB,CAAC;AAKpC,cAAc,cAAc,CAAC;AAK7B,cAAc,YAAY,CAAC;AAU3B,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EACV,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,MAAM,EACN,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,EACP,WAAW,EACX,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -17,6 +17,14 @@ export * from "./scoring/index.js";
17
17
  // Mirrored by the Rust `wh40kdc::cruncher` module and pinned by the
18
18
  // `conformance/cruncher` corpus. Pure functions — universal (Node + browser).
19
19
  export * from "./cruncher/index.js";
20
+ // Fleet comparison (attacker set × target-profile set → expected-kills matrix),
21
+ // built on the cruncher. Mirrors `python/src/wh40kdc/compare.py`; the per-cell
22
+ // math is pinned against Python by the `conformance/compare` corpus.
23
+ export * from "./compare.js";
24
+ // Roster-highlighting scope: resolve which units an ability's curated
25
+ // `applies_to` keyword filter benefits. Mirrors `wh40kdc::scope` (Rust) and
26
+ // `wh40kdc.scope` (Python); pinned by the `conformance/applies-to` corpus.
27
+ export * from "./scope.js";
20
28
  // Schema access + AJV validation lives behind the `./validate` subpath export
21
29
  // (`@alpaca-software/40kdc-data/validate`), NOT the root barrel: it reads
22
30
  // schema files from disk at module load (node:fs/node:url), which breaks
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,cAAc,iBAAiB,CAAC;AAEhC,mDAAmD;AACnD,cAAc,gBAAgB,CAAC;AAE/B,0EAA0E;AAC1E,6EAA6E;AAC7E,cAAc,sBAAsB,CAAC;AAWrC,+EAA+E;AAC/E,2EAA2E;AAC3E,8EAA8E;AAC9E,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAiB5B,6EAA6E;AAC7E,+EAA+E;AAC/E,yDAAyD;AACzD,cAAc,oBAAoB,CAAC;AAEnC,4EAA4E;AAC5E,oEAAoE;AACpE,8EAA8E;AAC9E,cAAc,qBAAqB,CAAC;AAEpC,8EAA8E;AAC9E,0EAA0E;AAC1E,yEAAyE;AACzE,qEAAqE;AAErE,4EAA4E;AAC5E,+EAA+E;AAC/E,uCAAuC;AACvC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC","sourcesContent":["// The linked, typed dataset — the primary entry point.\nexport * from \"./data/index.js\";\n\n// Generated types for every entity in the dataset.\nexport * from \"./generated.js\";\n\n// Plain-English translation of structured data (scoring-card awards + the\n// shared Ability-DSL condition humanizer). Cross-impl pinned by conformance.\nexport * from \"./translate/index.js\";\n\n// `ScoringTrigger` is emitted by both ./generated.js (schema-derived) and\n// ./translate (hand-authored). They are structurally identical; disambiguate the\n// two wildcard re-exports in favour of the generated, schema-canonical type.\nexport type { ScoringTrigger } from \"./generated.js\";\n\n// `AbilityScope` likewise: ./translate/effect.ts hand-authors a looser view\n// for the describer; prefer the generated, schema-canonical type.\nexport type { AbilityScope } from \"./generated.js\";\n\n// Terrain geometry resolver (template-anchored layout → board-space vertices).\n// Curated (not wildcard) so its internal type aliases don't clash with the\n// generated Footprint/TerrainTemplate/TerrainLayout types. Cross-impl pinned.\nexport {\n resolveLayout,\n polygonCentroid,\n footprintVertices,\n orientedOffsets,\n TerrainResolveError,\n solveCentroid,\n solveCentroidTriangulated,\n solveCentroidAttached,\n TerrainSolveError,\n keystoneMeasurements,\n BOARD_INCHES,\n TerrainKeystoneError,\n} from \"./terrain/index.js\";\nexport type {\n ResolvedPiece,\n ResolvedVec2,\n BoardEdge,\n FeatureRef,\n Keystone,\n KeystoneMeasurement,\n DimensionLine,\n SolveInput,\n TriangulationLine,\n TriangulateInput,\n AttachLine,\n AttachPiece,\n AttachInput,\n} from \"./terrain/index.js\";\n\n// Card-driven secondary-mission scoring: compute VP from asserted awards and\n// track per-round, per-player scoring. Mirrored by the Rust `wh40kdc::scoring`\n// module and pinned by the `conformance/scoring` corpus.\nexport * from \"./scoring/index.js\";\n\n// Damage-projection engine (expected-value math over weapon/unit profiles).\n// Mirrored by the Rust `wh40kdc::cruncher` module and pinned by the\n// `conformance/cruncher` corpus. Pure functions — universal (Node + browser).\nexport * from \"./cruncher/index.js\";\n\n// Schema access + AJV validation lives behind the `./validate` subpath export\n// (`@alpaca-software/40kdc-data/validate`), NOT the root barrel: it reads\n// schema files from disk at module load (node:fs/node:url), which breaks\n// browser bundles. The root barrel stays universal (Node + browser).\n\n// Army-list importer (ListForge → resolved 40kdc roster). Types are curated\n// rather than re-exported wholesale to avoid name clashes with generated types\n// (e.g. BattleSize, LeaderAttachment).\nexport {\n importListForge,\n importNewRecruit,\n importRoster,\n tryImportRoster,\n decodeListForge,\n} from \"./import/index.js\";\n\n// Army-list exporter (Roster → text or JSON for any of the supported formats).\nexport {\n exportRoster,\n newRecruitJsonSerializer,\n newRecruitSimpleSerializer,\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n rosterJsonSerializer,\n rosterizerSerializer,\n} from \"./export/index.js\";\nexport type { ExportFormat, RosterSerializer } from \"./export/index.js\";\nexport type { FormatAdapter } from \"./import/index.js\";\nexport type {\n ImportOptions,\n ImportResult,\n ImportFailureReason,\n AdapterTrial,\n Roster,\n RosterUnit,\n RosterWargear,\n RosterSource,\n RosterFormat,\n RosterPoints,\n RosterLeaderAttachment,\n ResolvedRef,\n Candidate,\n Diagnostics,\n Warning,\n WarningCode,\n ParsedRoster,\n ParsedUnit,\n ParsedWargear,\n} from \"./import/index.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,cAAc,iBAAiB,CAAC;AAEhC,mDAAmD;AACnD,cAAc,gBAAgB,CAAC;AAE/B,0EAA0E;AAC1E,6EAA6E;AAC7E,cAAc,sBAAsB,CAAC;AAWrC,+EAA+E;AAC/E,2EAA2E;AAC3E,8EAA8E;AAC9E,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAiB5B,6EAA6E;AAC7E,+EAA+E;AAC/E,yDAAyD;AACzD,cAAc,oBAAoB,CAAC;AAEnC,4EAA4E;AAC5E,oEAAoE;AACpE,8EAA8E;AAC9E,cAAc,qBAAqB,CAAC;AAEpC,gFAAgF;AAChF,+EAA+E;AAC/E,qEAAqE;AACrE,cAAc,cAAc,CAAC;AAE7B,sEAAsE;AACtE,4EAA4E;AAC5E,2EAA2E;AAC3E,cAAc,YAAY,CAAC;AAE3B,8EAA8E;AAC9E,0EAA0E;AAC1E,yEAAyE;AACzE,qEAAqE;AAErE,4EAA4E;AAC5E,+EAA+E;AAC/E,uCAAuC;AACvC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC","sourcesContent":["// The linked, typed dataset — the primary entry point.\nexport * from \"./data/index.js\";\n\n// Generated types for every entity in the dataset.\nexport * from \"./generated.js\";\n\n// Plain-English translation of structured data (scoring-card awards + the\n// shared Ability-DSL condition humanizer). Cross-impl pinned by conformance.\nexport * from \"./translate/index.js\";\n\n// `ScoringTrigger` is emitted by both ./generated.js (schema-derived) and\n// ./translate (hand-authored). They are structurally identical; disambiguate the\n// two wildcard re-exports in favour of the generated, schema-canonical type.\nexport type { ScoringTrigger } from \"./generated.js\";\n\n// `AbilityScope` likewise: ./translate/effect.ts hand-authors a looser view\n// for the describer; prefer the generated, schema-canonical type.\nexport type { AbilityScope } from \"./generated.js\";\n\n// Terrain geometry resolver (template-anchored layout → board-space vertices).\n// Curated (not wildcard) so its internal type aliases don't clash with the\n// generated Footprint/TerrainTemplate/TerrainLayout types. Cross-impl pinned.\nexport {\n resolveLayout,\n polygonCentroid,\n footprintVertices,\n orientedOffsets,\n TerrainResolveError,\n solveCentroid,\n solveCentroidTriangulated,\n solveCentroidAttached,\n TerrainSolveError,\n keystoneMeasurements,\n BOARD_INCHES,\n TerrainKeystoneError,\n} from \"./terrain/index.js\";\nexport type {\n ResolvedPiece,\n ResolvedVec2,\n BoardEdge,\n FeatureRef,\n Keystone,\n KeystoneMeasurement,\n DimensionLine,\n SolveInput,\n TriangulationLine,\n TriangulateInput,\n AttachLine,\n AttachPiece,\n AttachInput,\n} from \"./terrain/index.js\";\n\n// Card-driven secondary-mission scoring: compute VP from asserted awards and\n// track per-round, per-player scoring. Mirrored by the Rust `wh40kdc::scoring`\n// module and pinned by the `conformance/scoring` corpus.\nexport * from \"./scoring/index.js\";\n\n// Damage-projection engine (expected-value math over weapon/unit profiles).\n// Mirrored by the Rust `wh40kdc::cruncher` module and pinned by the\n// `conformance/cruncher` corpus. Pure functions — universal (Node + browser).\nexport * from \"./cruncher/index.js\";\n\n// Fleet comparison (attacker set × target-profile set → expected-kills matrix),\n// built on the cruncher. Mirrors `python/src/wh40kdc/compare.py`; the per-cell\n// math is pinned against Python by the `conformance/compare` corpus.\nexport * from \"./compare.js\";\n\n// Roster-highlighting scope: resolve which units an ability's curated\n// `applies_to` keyword filter benefits. Mirrors `wh40kdc::scope` (Rust) and\n// `wh40kdc.scope` (Python); pinned by the `conformance/applies-to` corpus.\nexport * from \"./scope.js\";\n\n// Schema access + AJV validation lives behind the `./validate` subpath export\n// (`@alpaca-software/40kdc-data/validate`), NOT the root barrel: it reads\n// schema files from disk at module load (node:fs/node:url), which breaks\n// browser bundles. The root barrel stays universal (Node + browser).\n\n// Army-list importer (ListForge → resolved 40kdc roster). Types are curated\n// rather than re-exported wholesale to avoid name clashes with generated types\n// (e.g. BattleSize, LeaderAttachment).\nexport {\n importListForge,\n importNewRecruit,\n importRoster,\n tryImportRoster,\n decodeListForge,\n} from \"./import/index.js\";\n\n// Army-list exporter (Roster → text or JSON for any of the supported formats).\nexport {\n exportRoster,\n newRecruitJsonSerializer,\n newRecruitSimpleSerializer,\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n rosterJsonSerializer,\n rosterizerSerializer,\n} from \"./export/index.js\";\nexport type { ExportFormat, RosterSerializer } from \"./export/index.js\";\nexport type { FormatAdapter } from \"./import/index.js\";\nexport type {\n ImportOptions,\n ImportResult,\n ImportFailureReason,\n AdapterTrial,\n Roster,\n RosterUnit,\n RosterWargear,\n RosterSource,\n RosterFormat,\n RosterPoints,\n RosterLeaderAttachment,\n ResolvedRef,\n Candidate,\n Diagnostics,\n Warning,\n WarningCode,\n ParsedRoster,\n ParsedUnit,\n ParsedWargear,\n} from \"./import/index.js\";\n"]}
@@ -0,0 +1,23 @@
1
+ import { type Keystone, type TerrainLayout, type TerrainTemplate } from "./terrain/resolve.js";
2
+ export interface PairingAddition {
3
+ layoutId: string;
4
+ /** The piece RECEIVING the mirrored keystone. */
5
+ pieceId: string;
6
+ /** The piece whose keystone was mirrored. */
7
+ fromPieceId: string;
8
+ keystone: Keystone;
9
+ }
10
+ export interface PairingReport {
11
+ /** Mirrors that are missing (check mode) / were added (write mode). */
12
+ additions: PairingAddition[];
13
+ /** Keystone-bearing pieces we could not pair or mirror, with the reason. */
14
+ warnings: string[];
15
+ }
16
+ /**
17
+ * Scan every layout for keystones whose symmetry twin lacks the mirror.
18
+ * `apply` mutates the layout objects in place (appending the mirrors);
19
+ * check mode leaves them untouched. Either way the report lists every
20
+ * addition and every piece that could not be handled.
21
+ */
22
+ export declare function pairKeystones(layouts: TerrainLayout[], templates: TerrainTemplate[], apply: boolean): PairingReport;
23
+ //# sourceMappingURL=migrate-keystone-twins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-keystone-twins.d.ts","sourceRoot":"","sources":["../src/migrate-keystone-twins.ts"],"names":[],"mappings":"AA0BA,OAAO,EAIL,KAAK,QAAQ,EAEb,KAAK,aAAa,EAClB,KAAK,eAAe,EAErB,MAAM,sBAAsB,CAAC;AAqE9B,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EAAE,EACxB,SAAS,EAAE,eAAe,EAAE,EAC5B,KAAK,EAAE,OAAO,GACb,aAAa,CAqEf"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Keystone twin pairing for `data/core/terrain-layouts.json`: every authored
3
+ * measurement keystone should have its point-reflected counterpart on the
4
+ * piece's 180°-symmetry twin, so a layout card prints dimension lines for
5
+ * BOTH players' halves (the layout editor now maintains this invariant on
6
+ * add/remove; this script back-fills the existing data and re-audits it).
7
+ *
8
+ * Twins are not stored — they are recovered the way the editor's
9
+ * `autoPairTwins` does: same template, unparented, centroid within 0.75″ of
10
+ * the point reflection through the board centre. The mirrored keystone flips
11
+ * its board edge (left↔right, top↔bottom); a face ref flips its side, and a
12
+ * vertex ref is resolved GEOMETRICALLY — reflect the source vertex through
13
+ * the centre and take the twin's nearest vertex. Index arithmetic is not
14
+ * safe: many layouts store the twin at the same angle (not θ+180), so vertex
15
+ * i on one side need not be vertex i on the other. A reflection that lands
16
+ * more than 0.25″ from every twin vertex is skipped with a warning rather
17
+ * than guessed.
18
+ *
19
+ * Usage (from `tools/`):
20
+ * npx tsx src/migrate-keystone-twins.ts # write missing mirrors
21
+ * npx tsx src/migrate-keystone-twins.ts --check # report only; exit 1 if any are missing
22
+ */
23
+ import { readFileSync, writeFileSync } from "node:fs";
24
+ import { join, resolve } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { orientedOffsets, } from "./terrain/resolve.js";
27
+ const BOARD = { width: 60, height: 44 };
28
+ /** Twin pairing tolerance on the point-reflected centroid (matches the editor). */
29
+ const POS_TOL = 0.75;
30
+ /** A piece this close to the board centre is self-symmetric and has no twin. */
31
+ const CENTRE_TOL = 0.3;
32
+ /** How close (inches) the reflected vertex must land to a twin vertex. */
33
+ const VERT_EPS = 0.25;
34
+ const REPO_ROOT = join(new URL("../..", import.meta.url).pathname);
35
+ const LAYOUTS_PATH = join(REPO_ROOT, "data", "core", "terrain-layouts.json");
36
+ const TEMPLATES_PATH = join(REPO_ROOT, "data", "core", "terrain-templates.json");
37
+ // ---- geometry --------------------------------------------------------------
38
+ const flipEdge = (e) => e === "left" ? "right" : e === "right" ? "left" : e === "top" ? "bottom" : "top";
39
+ const flipSide = (s) => s === "min-x" ? "max-x" : s === "max-x" ? "min-x" : s === "min-y" ? "max-y" : "min-y";
40
+ const sameRef = (a, b) => a.kind === "vertex" && b.kind === "vertex"
41
+ ? a.index === b.index
42
+ : a.kind === "face" && b.kind === "face"
43
+ ? a.side === b.side
44
+ : false;
45
+ const hasKeystone = (p, k) => (p.keystones ?? []).some((e) => e.edge === k.edge && sameRef(e.ref, k.ref));
46
+ /** Board-space vertices of an UNPARENTED piece (centroid + oriented offsets). */
47
+ function boardVertices(p, templates) {
48
+ const fp = p.footprint ?? (p.template ? templates.get(p.template)?.footprint : undefined);
49
+ if (!fp)
50
+ return null;
51
+ const offsets = orientedOffsets(fp, p.rotation_degrees ?? 0, p.mirror ?? "none");
52
+ return offsets.map((o) => ({ x: p.position.x + o.x, y: p.position.y + o.y }));
53
+ }
54
+ /**
55
+ * The keystone `k` mirrored from `primary` onto `twin`, or null (with a
56
+ * reason) when the vertex reflection has no close-enough twin vertex.
57
+ */
58
+ function mirrorKeystone(k, primaryVerts, twinVerts) {
59
+ const edge = flipEdge(k.edge);
60
+ if (k.ref.kind === "face")
61
+ return { keystone: { edge, ref: { kind: "face", side: flipSide(k.ref.side) } } };
62
+ const anchor = primaryVerts[k.ref.index];
63
+ if (!anchor)
64
+ return { error: `vertex index ${k.ref.index} out of range` };
65
+ const reflected = { x: BOARD.width - anchor.x, y: BOARD.height - anchor.y };
66
+ let bestIndex = -1;
67
+ let best = Infinity;
68
+ twinVerts.forEach((v, i) => {
69
+ const d = Math.hypot(v.x - reflected.x, v.y - reflected.y);
70
+ if (d < best) {
71
+ best = d;
72
+ bestIndex = i;
73
+ }
74
+ });
75
+ if (bestIndex < 0 || best > VERT_EPS) {
76
+ return { error: `reflected vertex lands ${best.toFixed(2)}″ from the nearest twin vertex (> ${VERT_EPS}″)` };
77
+ }
78
+ return { keystone: { edge, ref: { kind: "vertex", index: bestIndex } } };
79
+ }
80
+ /**
81
+ * Scan every layout for keystones whose symmetry twin lacks the mirror.
82
+ * `apply` mutates the layout objects in place (appending the mirrors);
83
+ * check mode leaves them untouched. Either way the report lists every
84
+ * addition and every piece that could not be handled.
85
+ */
86
+ export function pairKeystones(layouts, templates, apply) {
87
+ const byId = new Map(templates.map((t) => [t.id, t]));
88
+ const additions = [];
89
+ const warnings = [];
90
+ for (const layout of layouts) {
91
+ const pieces = layout.pieces ?? [];
92
+ // Twin pairing pass (unparented, off-centre, same template, reflected centroid).
93
+ const twin = new Map();
94
+ for (const p of pieces) {
95
+ if (twin.has(p) || p.parent_area_id)
96
+ continue;
97
+ const onCentre = Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&
98
+ Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;
99
+ if (onCentre)
100
+ continue;
101
+ const want = { x: BOARD.width - p.position.x, y: BOARD.height - p.position.y };
102
+ const match = pieces.find((q) => q !== p &&
103
+ !twin.has(q) &&
104
+ !q.parent_area_id &&
105
+ q.template === p.template &&
106
+ Math.hypot(q.position.x - want.x, q.position.y - want.y) <= POS_TOL);
107
+ if (match) {
108
+ twin.set(p, match);
109
+ twin.set(match, p);
110
+ }
111
+ }
112
+ for (const p of pieces) {
113
+ if (!p.keystones?.length)
114
+ continue;
115
+ const label = `${layout.id}/${p.id ?? p.name ?? "?"}`;
116
+ if (p.parent_area_id) {
117
+ warnings.push(`${label}: keystones on a parented feature — pair by hand`);
118
+ continue;
119
+ }
120
+ const t = twin.get(p);
121
+ if (!t) {
122
+ const onCentre = Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&
123
+ Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;
124
+ if (!onCentre)
125
+ warnings.push(`${label}: no symmetry twin found — pair by hand`);
126
+ continue; // centre pieces are their own mirror; nothing to do
127
+ }
128
+ const pv = boardVertices(p, byId);
129
+ const tv = boardVertices(t, byId);
130
+ if (!pv || !tv) {
131
+ warnings.push(`${label}: missing footprint on the pair — pair by hand`);
132
+ continue;
133
+ }
134
+ for (const k of p.keystones) {
135
+ const m = mirrorKeystone(k, pv, tv);
136
+ if ("error" in m) {
137
+ warnings.push(`${label} [${k.edge}/${JSON.stringify(k.ref)}]: ${m.error}`);
138
+ continue;
139
+ }
140
+ if (hasKeystone(t, m.keystone))
141
+ continue;
142
+ additions.push({
143
+ layoutId: layout.id,
144
+ pieceId: t.id ?? "?",
145
+ fromPieceId: p.id ?? "?",
146
+ keystone: m.keystone,
147
+ });
148
+ if (apply)
149
+ t.keystones = [...(t.keystones ?? []), m.keystone];
150
+ }
151
+ }
152
+ }
153
+ return { additions, warnings };
154
+ }
155
+ // ---- CLI -------------------------------------------------------------------
156
+ function main() {
157
+ const check = process.argv.includes("--check");
158
+ const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, "utf8"));
159
+ const templates = JSON.parse(readFileSync(TEMPLATES_PATH, "utf8"));
160
+ const report = pairKeystones(layouts, templates, !check);
161
+ for (const w of report.warnings)
162
+ console.warn(`warn: ${w}`);
163
+ const byLayout = new Map();
164
+ for (const a of report.additions)
165
+ byLayout.set(a.layoutId, (byLayout.get(a.layoutId) ?? 0) + 1);
166
+ for (const [id, n] of byLayout)
167
+ console.log(`${check ? "missing" : "added"}: ${id} — ${n} keystone(s)`);
168
+ console.log(`${report.additions.length} mirrored keystone(s) ${check ? "missing" : "added"} across ${byLayout.size} layout(s); ${report.warnings.length} warning(s)`);
169
+ if (check) {
170
+ if (report.additions.length > 0)
171
+ process.exit(1);
172
+ return;
173
+ }
174
+ if (report.additions.length > 0) {
175
+ writeFileSync(LAYOUTS_PATH, JSON.stringify(layouts, null, 2) + "\n");
176
+ console.log(`wrote ${LAYOUTS_PATH}`);
177
+ }
178
+ }
179
+ // Direct-invocation entry point (`npx tsx src/migrate-keystone-twins.ts [--check]`).
180
+ const isMain = process.argv[1] &&
181
+ resolve(process.argv[1]).replace(/\.\w+$/, "") === fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
182
+ if (isMain)
183
+ main();
184
+ //# sourceMappingURL=migrate-keystone-twins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-keystone-twins.js","sourceRoot":"","sources":["../src/migrate-keystone-twins.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Keystone twin pairing for `data/core/terrain-layouts.json`: every authored\n * measurement keystone should have its point-reflected counterpart on the\n * piece's 180°-symmetry twin, so a layout card prints dimension lines for\n * BOTH players' halves (the layout editor now maintains this invariant on\n * add/remove; this script back-fills the existing data and re-audits it).\n *\n * Twins are not stored — they are recovered the way the editor's\n * `autoPairTwins` does: same template, unparented, centroid within 0.75″ of\n * the point reflection through the board centre. The mirrored keystone flips\n * its board edge (left↔right, top↔bottom); a face ref flips its side, and a\n * vertex ref is resolved GEOMETRICALLY — reflect the source vertex through\n * the centre and take the twin's nearest vertex. Index arithmetic is not\n * safe: many layouts store the twin at the same angle (not θ+180), so vertex\n * i on one side need not be vertex i on the other. A reflection that lands\n * more than 0.25″ from every twin vertex is skipped with a warning rather\n * than guessed.\n *\n * Usage (from `tools/`):\n * npx tsx src/migrate-keystone-twins.ts # write missing mirrors\n * npx tsx src/migrate-keystone-twins.ts --check # report only; exit 1 if any are missing\n */\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n orientedOffsets,\n type BoardEdge,\n type Footprint,\n type Keystone,\n type LayoutPiece,\n type TerrainLayout,\n type TerrainTemplate,\n type Vec2,\n} from \"./terrain/resolve.js\";\n\nconst BOARD = { width: 60, height: 44 } as const;\n/** Twin pairing tolerance on the point-reflected centroid (matches the editor). */\nconst POS_TOL = 0.75;\n/** A piece this close to the board centre is self-symmetric and has no twin. */\nconst CENTRE_TOL = 0.3;\n/** How close (inches) the reflected vertex must land to a twin vertex. */\nconst VERT_EPS = 0.25;\n\nconst REPO_ROOT = join(new URL(\"../..\", import.meta.url).pathname);\nconst LAYOUTS_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-layouts.json\");\nconst TEMPLATES_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-templates.json\");\n\n// ---- geometry --------------------------------------------------------------\n\nconst flipEdge = (e: BoardEdge): BoardEdge =>\n e === \"left\" ? \"right\" : e === \"right\" ? \"left\" : e === \"top\" ? \"bottom\" : \"top\";\nconst flipSide = (s: \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\"): \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\" =>\n s === \"min-x\" ? \"max-x\" : s === \"max-x\" ? \"min-x\" : s === \"min-y\" ? \"max-y\" : \"min-y\";\n\nconst sameRef = (a: Keystone[\"ref\"], b: Keystone[\"ref\"]): boolean =>\n a.kind === \"vertex\" && b.kind === \"vertex\"\n ? a.index === b.index\n : a.kind === \"face\" && b.kind === \"face\"\n ? a.side === b.side\n : false;\nconst hasKeystone = (p: LayoutPiece, k: Keystone): boolean =>\n (p.keystones ?? []).some((e) => e.edge === k.edge && sameRef(e.ref, k.ref));\n\n/** Board-space vertices of an UNPARENTED piece (centroid + oriented offsets). */\nfunction boardVertices(p: LayoutPiece, templates: Map<string, TerrainTemplate>): Vec2[] | null {\n const fp: Footprint | undefined = p.footprint ?? (p.template ? templates.get(p.template)?.footprint : undefined);\n if (!fp) return null;\n const offsets = orientedOffsets(fp, p.rotation_degrees ?? 0, p.mirror ?? \"none\");\n return offsets.map((o) => ({ x: p.position.x + o.x, y: p.position.y + o.y }));\n}\n\n/**\n * The keystone `k` mirrored from `primary` onto `twin`, or null (with a\n * reason) when the vertex reflection has no close-enough twin vertex.\n */\nfunction mirrorKeystone(\n k: Keystone,\n primaryVerts: Vec2[],\n twinVerts: Vec2[],\n): { keystone: Keystone } | { error: string } {\n const edge = flipEdge(k.edge);\n if (k.ref.kind === \"face\") return { keystone: { edge, ref: { kind: \"face\", side: flipSide(k.ref.side) } } };\n const anchor = primaryVerts[k.ref.index];\n if (!anchor) return { error: `vertex index ${k.ref.index} out of range` };\n const reflected = { x: BOARD.width - anchor.x, y: BOARD.height - anchor.y };\n let bestIndex = -1;\n let best = Infinity;\n twinVerts.forEach((v, i) => {\n const d = Math.hypot(v.x - reflected.x, v.y - reflected.y);\n if (d < best) {\n best = d;\n bestIndex = i;\n }\n });\n if (bestIndex < 0 || best > VERT_EPS) {\n return { error: `reflected vertex lands ${best.toFixed(2)}″ from the nearest twin vertex (> ${VERT_EPS}″)` };\n }\n return { keystone: { edge, ref: { kind: \"vertex\", index: bestIndex } } };\n}\n\n// ---- pairing + audit -------------------------------------------------------\n\nexport interface PairingAddition {\n layoutId: string;\n /** The piece RECEIVING the mirrored keystone. */\n pieceId: string;\n /** The piece whose keystone was mirrored. */\n fromPieceId: string;\n keystone: Keystone;\n}\n\nexport interface PairingReport {\n /** Mirrors that are missing (check mode) / were added (write mode). */\n additions: PairingAddition[];\n /** Keystone-bearing pieces we could not pair or mirror, with the reason. */\n warnings: string[];\n}\n\n/**\n * Scan every layout for keystones whose symmetry twin lacks the mirror.\n * `apply` mutates the layout objects in place (appending the mirrors);\n * check mode leaves them untouched. Either way the report lists every\n * addition and every piece that could not be handled.\n */\nexport function pairKeystones(\n layouts: TerrainLayout[],\n templates: TerrainTemplate[],\n apply: boolean,\n): PairingReport {\n const byId = new Map(templates.map((t) => [t.id, t]));\n const additions: PairingAddition[] = [];\n const warnings: string[] = [];\n\n for (const layout of layouts) {\n const pieces = layout.pieces ?? [];\n // Twin pairing pass (unparented, off-centre, same template, reflected centroid).\n const twin = new Map<LayoutPiece, LayoutPiece>();\n for (const p of pieces) {\n if (twin.has(p) || p.parent_area_id) continue;\n const onCentre =\n Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&\n Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;\n if (onCentre) continue;\n const want = { x: BOARD.width - p.position.x, y: BOARD.height - p.position.y };\n const match = pieces.find(\n (q) =>\n q !== p &&\n !twin.has(q) &&\n !q.parent_area_id &&\n q.template === p.template &&\n Math.hypot(q.position.x - want.x, q.position.y - want.y) <= POS_TOL,\n );\n if (match) {\n twin.set(p, match);\n twin.set(match, p);\n }\n }\n\n for (const p of pieces) {\n if (!p.keystones?.length) continue;\n const label = `${layout.id}/${p.id ?? p.name ?? \"?\"}`;\n if (p.parent_area_id) {\n warnings.push(`${label}: keystones on a parented feature — pair by hand`);\n continue;\n }\n const t = twin.get(p);\n if (!t) {\n const onCentre =\n Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&\n Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;\n if (!onCentre) warnings.push(`${label}: no symmetry twin found — pair by hand`);\n continue; // centre pieces are their own mirror; nothing to do\n }\n const pv = boardVertices(p, byId);\n const tv = boardVertices(t, byId);\n if (!pv || !tv) {\n warnings.push(`${label}: missing footprint on the pair — pair by hand`);\n continue;\n }\n for (const k of p.keystones) {\n const m = mirrorKeystone(k, pv, tv);\n if (\"error\" in m) {\n warnings.push(`${label} [${k.edge}/${JSON.stringify(k.ref)}]: ${m.error}`);\n continue;\n }\n if (hasKeystone(t, m.keystone)) continue;\n additions.push({\n layoutId: layout.id,\n pieceId: t.id ?? \"?\",\n fromPieceId: p.id ?? \"?\",\n keystone: m.keystone,\n });\n if (apply) t.keystones = [...(t.keystones ?? []), m.keystone];\n }\n }\n }\n return { additions, warnings };\n}\n\n// ---- CLI -------------------------------------------------------------------\n\nfunction main(): void {\n const check = process.argv.includes(\"--check\");\n const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, \"utf8\")) as TerrainLayout[];\n const templates = JSON.parse(readFileSync(TEMPLATES_PATH, \"utf8\")) as TerrainTemplate[];\n\n const report = pairKeystones(layouts, templates, !check);\n\n for (const w of report.warnings) console.warn(`warn: ${w}`);\n const byLayout = new Map<string, number>();\n for (const a of report.additions) byLayout.set(a.layoutId, (byLayout.get(a.layoutId) ?? 0) + 1);\n for (const [id, n] of byLayout) console.log(`${check ? \"missing\" : \"added\"}: ${id} — ${n} keystone(s)`);\n console.log(\n `${report.additions.length} mirrored keystone(s) ${check ? \"missing\" : \"added\"} across ${byLayout.size} layout(s); ${report.warnings.length} warning(s)`,\n );\n\n if (check) {\n if (report.additions.length > 0) process.exit(1);\n return;\n }\n if (report.additions.length > 0) {\n writeFileSync(LAYOUTS_PATH, JSON.stringify(layouts, null, 2) + \"\\n\");\n console.log(`wrote ${LAYOUTS_PATH}`);\n }\n}\n\n// Direct-invocation entry point (`npx tsx src/migrate-keystone-twins.ts [--check]`).\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") === fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\nif (isMain) main();\n"]}
@@ -51,7 +51,7 @@ function walkLinkedApi(label, roster, ds) {
51
51
  rule(`${label}: walking the linked Dataset API`);
52
52
  console.log(`Roster: ${roster.name} (${roster.units.length} units)`);
53
53
  console.log(` faction_id = ${roster.faction_id ?? "—"}`);
54
- console.log(` detachment_id = ${roster.detachment_id ?? "—"}`);
54
+ console.log(` detachments = ${roster.detachments.map((d) => d.ref.id ?? d.ref.raw_name).join(", ") || "—"}`);
55
55
  console.log(` total_computed = ${roster.points.total_computed}`);
56
56
  for (const u of roster.units) {
57
57
  if (!u.ref.id) {