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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +10 -0
  2. package/dist/author-input-pack.d.ts +27 -0
  3. package/dist/author-input-pack.d.ts.map +1 -0
  4. package/dist/author-input-pack.js +198 -0
  5. package/dist/author-input-pack.js.map +1 -0
  6. package/dist/codegen-data.js +1 -0
  7. package/dist/codegen-data.js.map +1 -1
  8. package/dist/commands/import.d.ts.map +1 -1
  9. package/dist/commands/import.js +3 -1
  10. package/dist/commands/import.js.map +1 -1
  11. package/dist/convert-faction.d.ts.map +1 -1
  12. package/dist/convert-faction.js +22 -2
  13. package/dist/convert-faction.js.map +1 -1
  14. package/dist/data/bundle.generated.js +1 -1
  15. package/dist/data/bundle.generated.js.map +1 -1
  16. package/dist/data/dataset.d.ts +2 -1
  17. package/dist/data/dataset.d.ts.map +1 -1
  18. package/dist/data/dataset.js +2 -0
  19. package/dist/data/dataset.js.map +1 -1
  20. package/dist/data/entities.d.ts +22 -0
  21. package/dist/data/entities.d.ts.map +1 -1
  22. package/dist/data/entities.js +28 -0
  23. package/dist/data/entities.js.map +1 -1
  24. package/dist/data/index.d.ts +3 -1
  25. package/dist/data/index.d.ts.map +1 -1
  26. package/dist/data/index.js +3 -1
  27. package/dist/data/index.js.map +1 -1
  28. package/dist/data/roster-resolve.d.ts +16 -1
  29. package/dist/data/roster-resolve.d.ts.map +1 -1
  30. package/dist/data/roster-resolve.js +19 -0
  31. package/dist/data/roster-resolve.js.map +1 -1
  32. package/dist/data/types.d.ts +3 -1
  33. package/dist/data/types.d.ts.map +1 -1
  34. package/dist/data/types.js +1 -0
  35. package/dist/data/types.js.map +1 -1
  36. package/dist/export/newrecruit-json.d.ts.map +1 -1
  37. package/dist/export/newrecruit-json.js +4 -3
  38. package/dist/export/newrecruit-json.js.map +1 -1
  39. package/dist/export/newrecruit-simple.js +2 -2
  40. package/dist/export/newrecruit-simple.js.map +1 -1
  41. package/dist/export/newrecruit-wtc.d.ts.map +1 -1
  42. package/dist/export/newrecruit-wtc.js +3 -1
  43. package/dist/export/newrecruit-wtc.js.map +1 -1
  44. package/dist/export/rosterizer.d.ts.map +1 -1
  45. package/dist/export/rosterizer.js +6 -7
  46. package/dist/export/rosterizer.js.map +1 -1
  47. package/dist/extract-faction-pack.d.ts +61 -0
  48. package/dist/extract-faction-pack.d.ts.map +1 -0
  49. package/dist/extract-faction-pack.js +318 -0
  50. package/dist/extract-faction-pack.js.map +1 -0
  51. package/dist/gen-conformance.js +37 -4
  52. package/dist/gen-conformance.js.map +1 -1
  53. package/dist/generated.d.ts +46 -2
  54. package/dist/generated.d.ts.map +1 -1
  55. package/dist/generated.js.map +1 -1
  56. package/dist/import/gw-headerless.d.ts +70 -0
  57. package/dist/import/gw-headerless.d.ts.map +1 -0
  58. package/dist/import/gw-headerless.js +343 -0
  59. package/dist/import/gw-headerless.js.map +1 -0
  60. package/dist/import/gw.js +1 -1
  61. package/dist/import/gw.js.map +1 -1
  62. package/dist/import/import-roster.d.ts.map +1 -1
  63. package/dist/import/import-roster.js +8 -0
  64. package/dist/import/import-roster.js.map +1 -1
  65. package/dist/import/listforge-text.js +1 -1
  66. package/dist/import/listforge-text.js.map +1 -1
  67. package/dist/import/listforge.d.ts.map +1 -1
  68. package/dist/import/listforge.js +18 -3
  69. package/dist/import/listforge.js.map +1 -1
  70. package/dist/import/newrecruit-json.d.ts.map +1 -1
  71. package/dist/import/newrecruit-json.js +21 -3
  72. package/dist/import/newrecruit-json.js.map +1 -1
  73. package/dist/import/newrecruit-simple.js +1 -1
  74. package/dist/import/newrecruit-simple.js.map +1 -1
  75. package/dist/import/newrecruit-wtc.js +1 -1
  76. package/dist/import/newrecruit-wtc.js.map +1 -1
  77. package/dist/import/resolve.d.ts.map +1 -1
  78. package/dist/import/resolve.js +46 -18
  79. package/dist/import/resolve.js.map +1 -1
  80. package/dist/import/roster-json.d.ts +5 -3
  81. package/dist/import/roster-json.d.ts.map +1 -1
  82. package/dist/import/roster-json.js +8 -4
  83. package/dist/import/roster-json.js.map +1 -1
  84. package/dist/import/rosterizer.js +3 -3
  85. package/dist/import/rosterizer.js.map +1 -1
  86. package/dist/import/types.d.ts +15 -4
  87. package/dist/import/types.d.ts.map +1 -1
  88. package/dist/import/types.js.map +1 -1
  89. package/dist/index.d.ts +1 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +4 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/merge-faction-pack.d.ts +2 -0
  94. package/dist/merge-faction-pack.d.ts.map +1 -0
  95. package/dist/merge-faction-pack.js +157 -0
  96. package/dist/merge-faction-pack.js.map +1 -0
  97. package/dist/pack-blocks.d.ts +32 -0
  98. package/dist/pack-blocks.d.ts.map +1 -0
  99. package/dist/pack-blocks.js +99 -0
  100. package/dist/pack-blocks.js.map +1 -0
  101. package/dist/rube-goldberg.js +1 -1
  102. package/dist/rube-goldberg.js.map +1 -1
  103. package/dist/runner.d.ts.map +1 -1
  104. package/dist/runner.js +33 -1
  105. package/dist/runner.js.map +1 -1
  106. package/dist/scope.d.ts +31 -0
  107. package/dist/scope.d.ts.map +1 -0
  108. package/dist/scope.js +34 -0
  109. package/dist/scope.js.map +1 -0
  110. package/dist/seed-detachment-abilities.d.ts +2 -0
  111. package/dist/seed-detachment-abilities.d.ts.map +1 -0
  112. package/dist/seed-detachment-abilities.js +137 -0
  113. package/dist/seed-detachment-abilities.js.map +1 -0
  114. package/dist/translate/effect.d.ts +17 -3
  115. package/dist/translate/effect.d.ts.map +1 -1
  116. package/dist/translate/effect.js +23 -4
  117. package/dist/translate/effect.js.map +1 -1
  118. package/dist/translate/index.d.ts +1 -1
  119. package/dist/translate/index.d.ts.map +1 -1
  120. package/dist/translate/index.js +1 -1
  121. package/dist/translate/index.js.map +1 -1
  122. package/dist/validate.d.ts.map +1 -1
  123. package/dist/validate.js +1 -0
  124. package/dist/validate.js.map +1 -1
  125. package/package.json +3 -1
  126. package/schemas/core/detachment.schema.json +6 -0
  127. package/schemas/core/hull-shape.schema.json +30 -0
  128. package/schemas/core/roster.schema.json +24 -8
  129. package/schemas/core/stratagem.schema.json +2 -2
  130. package/schemas/core/unit-composition.schema.json +7 -0
  131. package/schemas/enrichment/ability-dsl/ability.schema.json +14 -0
@@ -1 +1 @@
1
- {"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"wtc-compact\" and \"wtc-full\" text adapters.\n *\n * Both formats open with a `++++++++` summary header carrying FACTION KEYWORD,\n * DETACHMENT, TOTAL ARMY POINTS, WARLORD, ENHANCEMENT(s), NUMBER OF UNITS, and\n * SECONDARY tournament-objective shorthand. The body diverges:\n *\n * - **wtc-compact** — one unit per line:\n * `[CharN: ]Nx <Unit> (P pts): <comma-separated wargear>`\n * followed optionally by `Enhancement: <Name> (+P pts)` on the next line.\n *\n * - **wtc-full** — uppercase section headers (`BATTLELINE`, `ALLIED UNITS`),\n * two-line unit blocks (`[CharN: ]Nx <Unit> (P pts)` then `N with <wargear>`),\n * `Enhancement: <Name> (+P pts)` on its own line, and per-model-type\n * breakdowns with `• Nx <ModelType>` + indented `N with <wargear>` lines.\n * Real exports also mix in compact-style lines: single-model units arrive as\n * `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line, and a model-type\n * bullet may inline its loadout after a colon (`• 1x Champion: Chainblades`,\n * `• 5x Eightbound: 5 with Chainblades`). The full-body parser accepts all\n * of these.\n *\n * The {@link Roster} pivot stores units at unit granularity — per-model-type\n * wargear breakdowns and `CharN:` slot numbers aren't modelled, so this adapter\n * collapses them: the parsed unit's `model_count` is summed from the breakdown\n * and its `wargear` is the union of every loadout under it. The `WARLORD` /\n * `Houndpack Lance Character` tokens are stripped from the wargear list (and\n * set `is_warlord`/`is_character` instead) so resolution doesn't try to look\n * them up as weapons. Round-trips are at Roster level, not byte-for-byte.\n *\n * Enhancement points (`+15 pts`) are subtracted from the displayed unit total\n * so `ParsedUnit.points` is the *base* unit cost — matching the ListForge\n * convention where the unit's own cost line is base and the enhancement is a\n * sibling cost line. `total_computed` walks every cost line just like ListForge\n * (base unit pts + each enhancement pts).\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport {\n classifyWargearList,\n factionFromKeyword,\n inferBattleSizeRaw,\n splitWargearList,\n stripParenthetical,\n} from \"./newrecruit-text.js\";\n\nconst WTC_HEADER_PREFIX = \"+ FACTION KEYWORD:\";\n\n// --- header parsing ---------------------------------------------------------\n\ninterface WtcHeader {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n declared_limit: number | null;\n total_reported: number | null;\n battle_size_raw: string | null;\n}\n\nconst HEADER_FIELDS = {\n faction: /^\\+\\s*FACTION KEYWORD:\\s*(.+?)\\s*$/i,\n detachment: /^\\+\\s*DETACHMENT:\\s*(.+?)\\s*$/i,\n totalPoints: /^\\+\\s*TOTAL ARMY POINTS:\\s*(\\d+)\\s*pts?\\s*$/i,\n pointsLimit: /^\\+\\s*POINTS LIMIT:\\s*(\\d+)\\s*pts?\\s*$/i,\n listName: /^\\+\\s*LIST NAME:\\s*(.+?)\\s*$/i,\n} as const;\n\n/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */\nfunction parseWtcHeader(text: string): { header: WtcHeader; bodyStart: number } | null {\n const lines = text.split(/\\r?\\n/);\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let totalReported: number | null = null;\n let pointsLimit: number | null = null;\n let listName: string | null = null;\n\n // Two `+++++…` fence lines wrap the header. Find them.\n const fenceIndices: number[] = [];\n for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {\n if (/^\\++\\s*$/.test(lines[i])) fenceIndices.push(i);\n }\n let sawFactionKeyword = false;\n for (const line of lines) {\n if (!line.startsWith(\"+\")) continue;\n const factionMatch = HEADER_FIELDS.faction.exec(line);\n if (factionMatch) {\n faction_raw_name = factionFromKeyword(factionMatch[1]);\n sawFactionKeyword = true;\n continue;\n }\n const detMatch = HEADER_FIELDS.detachment.exec(line);\n if (detMatch) {\n detachment_raw_name = stripParenthetical(detMatch[1]);\n continue;\n }\n const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);\n if (ptsMatch) {\n totalReported = Number.parseInt(ptsMatch[1], 10);\n continue;\n }\n const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);\n if (limitMatch) {\n pointsLimit = Number.parseInt(limitMatch[1], 10);\n continue;\n }\n const nameMatch = HEADER_FIELDS.listName.exec(line);\n if (nameMatch) {\n listName = nameMatch[1];\n }\n }\n\n if (!sawFactionKeyword) return null;\n\n const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;\n // POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —\n // is the army's points ceiling. When the source carries only a single\n // figure (the tournament default), fall back to it.\n const declared_limit = pointsLimit ?? totalReported;\n const battle_size_raw = inferBattleSizeRaw(declared_limit);\n\n return {\n header: {\n name: listName ?? \"Imported roster\",\n faction_raw_name,\n detachment_raw_name,\n declared_limit,\n total_reported: totalReported,\n battle_size_raw,\n },\n bodyStart,\n };\n}\n\n// --- shared body helpers ----------------------------------------------------\n\nconst UNIT_HEADER_COMPACT =\n /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*:\\s*(.*)$/i;\nconst UNIT_HEADER_FULL = /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst ENHANCEMENT_LINE =\n /^Enhancement:\\s*(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst WITH_PREFIX = /^(\\d+)\\s+with\\s+(.*)$/i;\n// Optional trailing `: <wargear>` — NewRecruit inlines a model group's loadout\n// after the model type (`• 1x Champion: Chainblades`, `• 5x Eightbound: 5 with\n// Chainblades`) instead of always breaking it onto `N with` continuation lines.\nconst MODEL_BREAKDOWN =\n /^\\s*•\\s*(\\d+)x\\s+([^:]+?)(?:\\s*\\[[^\\]]*\\])?\\s*(?::\\s*(.+))?$/u;\nconst SECTION_HEADER = /^[A-Z][A-Z0-9 \\-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.\nconst HEADER_LINE = /^\\+/;\n\n/**\n * `N with X, Y, Z` means each of `N` models carries the same list — the weapon\n * counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when\n * the line has no `with` prefix.\n */\nfunction parseWithGroup(text: string): { multiplier: number; list: string } {\n const m = WITH_PREFIX.exec(text);\n if (m) {\n const n = Number.parseInt(m[1], 10);\n return { multiplier: n > 0 ? n : 1, list: m[2] };\n }\n return { multiplier: 1, list: text };\n}\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Total displayed pts from the header line; base computed once an enhancement is known. */\n displayed_pts: number | null;\n enhancement_pts: number;\n model_count: number;\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number, leading_count: number, is_character_prefix: boolean): UnitBuilder {\n return {\n raw_name: name,\n is_character: is_character_prefix,\n is_warlord: false,\n enhancement_raw_name: null,\n displayed_pts,\n enhancement_pts: 0,\n model_count: leading_count > 0 ? leading_count : 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyWithGroup(unit: UnitBuilder, listText: string): void {\n const { multiplier, list } = parseWithGroup(listText);\n const tokens = splitWargearList(list);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n // wtc never inlines the enhancement points in the wargear list (that's the\n // simple format) but classifyWargearList silently absorbs it if it shows up;\n // wtc's enhancement is always parsed off the explicit \"Enhancement:\" line.\n const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const displayed = unit.displayed_pts;\n const points = displayed === null ? null : displayed - unit.enhancement_pts;\n const wargear: ParsedWargear[] = [];\n for (const [raw_name, count] of unit.wargear) {\n wargear.push({ raw_name, count });\n }\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear,\n };\n}\n\n/** Compute total_computed by walking every parsed unit cost line. */\nfunction computeTotal(units: ParsedUnit[], enhancementPtsByIndex: number[]): number {\n let total = 0;\n for (let i = 0; i < units.length; i += 1) {\n total += units[i].points ?? 0;\n total += enhancementPtsByIndex[i] ?? 0;\n }\n return total;\n}\n\nfunction attachEnhancement(unit: UnitBuilder, raw_name: string, pts: number): void {\n unit.enhancement_raw_name = raw_name.trim();\n unit.enhancement_pts = pts;\n}\n\n// --- compact body parser ----------------------------------------------------\n\nfunction parseCompactBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n // Emit immediately so subsequent unit lines start fresh.\n finalize();\n continue;\n }\n\n const unitMatch = UNIT_HEADER_COMPACT.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, unitMatch[4]);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- full body parser -------------------------------------------------------\n\nfunction parseFullBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n let breakdownModels = 0;\n\n const finalize = (): void => {\n if (current) {\n if (breakdownModels > 0) current.model_count = breakdownModels;\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n breakdownModels = 0;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {\n finalize();\n continue;\n }\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n continue;\n }\n\n const unitMatch = UNIT_HEADER_FULL.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n continue;\n }\n\n // Single-model units (characters, vehicles) appear compact-style even in\n // full exports: `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line.\n // Without this branch they fall through every matcher and vanish.\n const compactMatch = UNIT_HEADER_COMPACT.exec(line);\n if (compactMatch) {\n finalize();\n const leading_count = Number.parseInt(compactMatch[1], 10);\n const name = compactMatch[2].trim();\n const pts = Number.parseInt(compactMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, compactMatch[4]);\n continue;\n }\n\n const breakdown = MODEL_BREAKDOWN.exec(raw);\n if (breakdown && current) {\n breakdownModels += Number.parseInt(breakdown[1], 10);\n // Inline loadout after the model type; `N with` continuation lines for\n // the same group still arrive separately and are handled below.\n if (breakdown[3] !== undefined) applyWithGroup(current, breakdown[3]);\n continue;\n }\n\n if (WITH_PREFIX.test(line) && current) {\n applyWithGroup(current, line);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- multi-force detection --------------------------------------------------\n\n/** Heuristic for `multi_force`: are there units with \"ALLIED\" decorating\n * the body? wtc-full has an explicit `ALLIED UNITS` section header; compact\n * has no section markers but the user-facing summary header counts every unit\n * together, so detect from explicit section presence. */\nfunction detectMultiForce(text: string, format: \"wtc-compact\" | \"wtc-full\"): boolean {\n if (format === \"wtc-full\") {\n return /^ALLIED UNITS\\s*$/im.test(text);\n }\n // wtc-compact has no section header. Multi-force surfaces only via the\n // primary-faction summary; assume single-force unless we add a richer marker.\n return false;\n}\n\n// --- adapters ---------------------------------------------------------------\n\nfunction isWtcText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n // Both wtc formats begin with the FACTION KEYWORD header line (possibly\n // after some leading whitespace/fence characters).\n if (!decoded.includes(WTC_HEADER_PREFIX)) return null;\n return decoded;\n}\n\n/** Distinguishes wtc-full from wtc-compact: full has a line starting with\n * `\\d+ with ` at the start of a body line (compact only puts `N with` after\n * `:` on the same line as the unit header). */\nfunction isFullFormat(text: string): boolean {\n return /^[\\t ]*\\d+\\s+with\\b/m.test(text);\n}\n\n/** `•`-prefixed body lines. wtc-full uses them for per-model breakdowns; the GW\n * app format uses them for every wargear entry. wtc-compact never emits them,\n * so it's the one matcher that must exclude them to stay disjoint from GW. */\nfunction hasBullets(text: string): boolean {\n return /^[\\t ]*•/mu.test(text);\n}\n\nfunction parseWith(text: string, format: \"wtc-compact\" | \"wtc-full\"): ParsedRoster {\n const parsed = parseWtcHeader(text);\n if (!parsed) {\n throw new Error(`${format}: missing \"+ FACTION KEYWORD:\" header`);\n }\n const { header, bodyStart } = parsed;\n const body = text.split(/\\r?\\n/).slice(bodyStart).join(\"\\n\");\n const { units, enhancementPts } =\n format === \"wtc-full\" ? parseFullBody(body) : parseCompactBody(body);\n\n return {\n name: header.name,\n generated_by: null,\n faction_raw_name: header.faction_raw_name,\n detachment_raw_name: header.detachment_raw_name,\n battle_size_raw: header.battle_size_raw,\n declared_limit: header.declared_limit,\n total_reported: header.total_reported,\n total_computed: computeTotal(units, enhancementPts),\n units,\n multi_force: detectMultiForce(text, format),\n };\n}\n\nexport const newRecruitWtcCompactAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-compact\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n // wtc-compact has no `N with` lines (that's wtc-full) and no `•` bullets\n // (that's the GW app format) — excluding both keeps the matcher disjoint.\n return !isFullFormat(text) && !hasBullets(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-compact: input is not a string\");\n return parseWith(text, \"wtc-compact\");\n },\n};\n\nexport const newRecruitWtcFullAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-full\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n return isFullFormat(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-full: input is not a string\");\n return parseWith(text, \"wtc-full\");\n },\n};\n"]}
1
+ {"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"wtc-compact\" and \"wtc-full\" text adapters.\n *\n * Both formats open with a `++++++++` summary header carrying FACTION KEYWORD,\n * DETACHMENT, TOTAL ARMY POINTS, WARLORD, ENHANCEMENT(s), NUMBER OF UNITS, and\n * SECONDARY tournament-objective shorthand. The body diverges:\n *\n * - **wtc-compact** — one unit per line:\n * `[CharN: ]Nx <Unit> (P pts): <comma-separated wargear>`\n * followed optionally by `Enhancement: <Name> (+P pts)` on the next line.\n *\n * - **wtc-full** — uppercase section headers (`BATTLELINE`, `ALLIED UNITS`),\n * two-line unit blocks (`[CharN: ]Nx <Unit> (P pts)` then `N with <wargear>`),\n * `Enhancement: <Name> (+P pts)` on its own line, and per-model-type\n * breakdowns with `• Nx <ModelType>` + indented `N with <wargear>` lines.\n * Real exports also mix in compact-style lines: single-model units arrive as\n * `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line, and a model-type\n * bullet may inline its loadout after a colon (`• 1x Champion: Chainblades`,\n * `• 5x Eightbound: 5 with Chainblades`). The full-body parser accepts all\n * of these.\n *\n * The {@link Roster} pivot stores units at unit granularity — per-model-type\n * wargear breakdowns and `CharN:` slot numbers aren't modelled, so this adapter\n * collapses them: the parsed unit's `model_count` is summed from the breakdown\n * and its `wargear` is the union of every loadout under it. The `WARLORD` /\n * `Houndpack Lance Character` tokens are stripped from the wargear list (and\n * set `is_warlord`/`is_character` instead) so resolution doesn't try to look\n * them up as weapons. Round-trips are at Roster level, not byte-for-byte.\n *\n * Enhancement points (`+15 pts`) are subtracted from the displayed unit total\n * so `ParsedUnit.points` is the *base* unit cost — matching the ListForge\n * convention where the unit's own cost line is base and the enhancement is a\n * sibling cost line. `total_computed` walks every cost line just like ListForge\n * (base unit pts + each enhancement pts).\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport {\n classifyWargearList,\n factionFromKeyword,\n inferBattleSizeRaw,\n splitWargearList,\n stripParenthetical,\n} from \"./newrecruit-text.js\";\n\nconst WTC_HEADER_PREFIX = \"+ FACTION KEYWORD:\";\n\n// --- header parsing ---------------------------------------------------------\n\ninterface WtcHeader {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n declared_limit: number | null;\n total_reported: number | null;\n battle_size_raw: string | null;\n}\n\nconst HEADER_FIELDS = {\n faction: /^\\+\\s*FACTION KEYWORD:\\s*(.+?)\\s*$/i,\n detachment: /^\\+\\s*DETACHMENT:\\s*(.+?)\\s*$/i,\n totalPoints: /^\\+\\s*TOTAL ARMY POINTS:\\s*(\\d+)\\s*pts?\\s*$/i,\n pointsLimit: /^\\+\\s*POINTS LIMIT:\\s*(\\d+)\\s*pts?\\s*$/i,\n listName: /^\\+\\s*LIST NAME:\\s*(.+?)\\s*$/i,\n} as const;\n\n/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */\nfunction parseWtcHeader(text: string): { header: WtcHeader; bodyStart: number } | null {\n const lines = text.split(/\\r?\\n/);\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let totalReported: number | null = null;\n let pointsLimit: number | null = null;\n let listName: string | null = null;\n\n // Two `+++++…` fence lines wrap the header. Find them.\n const fenceIndices: number[] = [];\n for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {\n if (/^\\++\\s*$/.test(lines[i])) fenceIndices.push(i);\n }\n let sawFactionKeyword = false;\n for (const line of lines) {\n if (!line.startsWith(\"+\")) continue;\n const factionMatch = HEADER_FIELDS.faction.exec(line);\n if (factionMatch) {\n faction_raw_name = factionFromKeyword(factionMatch[1]);\n sawFactionKeyword = true;\n continue;\n }\n const detMatch = HEADER_FIELDS.detachment.exec(line);\n if (detMatch) {\n detachment_raw_name = stripParenthetical(detMatch[1]);\n continue;\n }\n const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);\n if (ptsMatch) {\n totalReported = Number.parseInt(ptsMatch[1], 10);\n continue;\n }\n const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);\n if (limitMatch) {\n pointsLimit = Number.parseInt(limitMatch[1], 10);\n continue;\n }\n const nameMatch = HEADER_FIELDS.listName.exec(line);\n if (nameMatch) {\n listName = nameMatch[1];\n }\n }\n\n if (!sawFactionKeyword) return null;\n\n const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;\n // POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —\n // is the army's points ceiling. When the source carries only a single\n // figure (the tournament default), fall back to it.\n const declared_limit = pointsLimit ?? totalReported;\n const battle_size_raw = inferBattleSizeRaw(declared_limit);\n\n return {\n header: {\n name: listName ?? \"Imported roster\",\n faction_raw_name,\n detachment_raw_name,\n declared_limit,\n total_reported: totalReported,\n battle_size_raw,\n },\n bodyStart,\n };\n}\n\n// --- shared body helpers ----------------------------------------------------\n\nconst UNIT_HEADER_COMPACT =\n /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*:\\s*(.*)$/i;\nconst UNIT_HEADER_FULL = /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst ENHANCEMENT_LINE =\n /^Enhancement:\\s*(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst WITH_PREFIX = /^(\\d+)\\s+with\\s+(.*)$/i;\n// Optional trailing `: <wargear>` — NewRecruit inlines a model group's loadout\n// after the model type (`• 1x Champion: Chainblades`, `• 5x Eightbound: 5 with\n// Chainblades`) instead of always breaking it onto `N with` continuation lines.\nconst MODEL_BREAKDOWN =\n /^\\s*•\\s*(\\d+)x\\s+([^:]+?)(?:\\s*\\[[^\\]]*\\])?\\s*(?::\\s*(.+))?$/u;\nconst SECTION_HEADER = /^[A-Z][A-Z0-9 \\-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.\nconst HEADER_LINE = /^\\+/;\n\n/**\n * `N with X, Y, Z` means each of `N` models carries the same list — the weapon\n * counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when\n * the line has no `with` prefix.\n */\nfunction parseWithGroup(text: string): { multiplier: number; list: string } {\n const m = WITH_PREFIX.exec(text);\n if (m) {\n const n = Number.parseInt(m[1], 10);\n return { multiplier: n > 0 ? n : 1, list: m[2] };\n }\n return { multiplier: 1, list: text };\n}\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Total displayed pts from the header line; base computed once an enhancement is known. */\n displayed_pts: number | null;\n enhancement_pts: number;\n model_count: number;\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number, leading_count: number, is_character_prefix: boolean): UnitBuilder {\n return {\n raw_name: name,\n is_character: is_character_prefix,\n is_warlord: false,\n enhancement_raw_name: null,\n displayed_pts,\n enhancement_pts: 0,\n model_count: leading_count > 0 ? leading_count : 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyWithGroup(unit: UnitBuilder, listText: string): void {\n const { multiplier, list } = parseWithGroup(listText);\n const tokens = splitWargearList(list);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n // wtc never inlines the enhancement points in the wargear list (that's the\n // simple format) but classifyWargearList silently absorbs it if it shows up;\n // wtc's enhancement is always parsed off the explicit \"Enhancement:\" line.\n const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const displayed = unit.displayed_pts;\n const points = displayed === null ? null : displayed - unit.enhancement_pts;\n const wargear: ParsedWargear[] = [];\n for (const [raw_name, count] of unit.wargear) {\n wargear.push({ raw_name, count });\n }\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear,\n };\n}\n\n/** Compute total_computed by walking every parsed unit cost line. */\nfunction computeTotal(units: ParsedUnit[], enhancementPtsByIndex: number[]): number {\n let total = 0;\n for (let i = 0; i < units.length; i += 1) {\n total += units[i].points ?? 0;\n total += enhancementPtsByIndex[i] ?? 0;\n }\n return total;\n}\n\nfunction attachEnhancement(unit: UnitBuilder, raw_name: string, pts: number): void {\n unit.enhancement_raw_name = raw_name.trim();\n unit.enhancement_pts = pts;\n}\n\n// --- compact body parser ----------------------------------------------------\n\nfunction parseCompactBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n // Emit immediately so subsequent unit lines start fresh.\n finalize();\n continue;\n }\n\n const unitMatch = UNIT_HEADER_COMPACT.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, unitMatch[4]);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- full body parser -------------------------------------------------------\n\nfunction parseFullBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n let breakdownModels = 0;\n\n const finalize = (): void => {\n if (current) {\n if (breakdownModels > 0) current.model_count = breakdownModels;\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n breakdownModels = 0;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {\n finalize();\n continue;\n }\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n continue;\n }\n\n const unitMatch = UNIT_HEADER_FULL.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n continue;\n }\n\n // Single-model units (characters, vehicles) appear compact-style even in\n // full exports: `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line.\n // Without this branch they fall through every matcher and vanish.\n const compactMatch = UNIT_HEADER_COMPACT.exec(line);\n if (compactMatch) {\n finalize();\n const leading_count = Number.parseInt(compactMatch[1], 10);\n const name = compactMatch[2].trim();\n const pts = Number.parseInt(compactMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, compactMatch[4]);\n continue;\n }\n\n const breakdown = MODEL_BREAKDOWN.exec(raw);\n if (breakdown && current) {\n breakdownModels += Number.parseInt(breakdown[1], 10);\n // Inline loadout after the model type; `N with` continuation lines for\n // the same group still arrive separately and are handled below.\n if (breakdown[3] !== undefined) applyWithGroup(current, breakdown[3]);\n continue;\n }\n\n if (WITH_PREFIX.test(line) && current) {\n applyWithGroup(current, line);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- multi-force detection --------------------------------------------------\n\n/** Heuristic for `multi_force`: are there units with \"ALLIED\" decorating\n * the body? wtc-full has an explicit `ALLIED UNITS` section header; compact\n * has no section markers but the user-facing summary header counts every unit\n * together, so detect from explicit section presence. */\nfunction detectMultiForce(text: string, format: \"wtc-compact\" | \"wtc-full\"): boolean {\n if (format === \"wtc-full\") {\n return /^ALLIED UNITS\\s*$/im.test(text);\n }\n // wtc-compact has no section header. Multi-force surfaces only via the\n // primary-faction summary; assume single-force unless we add a richer marker.\n return false;\n}\n\n// --- adapters ---------------------------------------------------------------\n\nfunction isWtcText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n // Both wtc formats begin with the FACTION KEYWORD header line (possibly\n // after some leading whitespace/fence characters).\n if (!decoded.includes(WTC_HEADER_PREFIX)) return null;\n return decoded;\n}\n\n/** Distinguishes wtc-full from wtc-compact: full has a line starting with\n * `\\d+ with ` at the start of a body line (compact only puts `N with` after\n * `:` on the same line as the unit header). */\nfunction isFullFormat(text: string): boolean {\n return /^[\\t ]*\\d+\\s+with\\b/m.test(text);\n}\n\n/** `•`-prefixed body lines. wtc-full uses them for per-model breakdowns; the GW\n * app format uses them for every wargear entry. wtc-compact never emits them,\n * so it's the one matcher that must exclude them to stay disjoint from GW. */\nfunction hasBullets(text: string): boolean {\n return /^[\\t ]*•/mu.test(text);\n}\n\nfunction parseWith(text: string, format: \"wtc-compact\" | \"wtc-full\"): ParsedRoster {\n const parsed = parseWtcHeader(text);\n if (!parsed) {\n throw new Error(`${format}: missing \"+ FACTION KEYWORD:\" header`);\n }\n const { header, bodyStart } = parsed;\n const body = text.split(/\\r?\\n/).slice(bodyStart).join(\"\\n\");\n const { units, enhancementPts } =\n format === \"wtc-full\" ? parseFullBody(body) : parseCompactBody(body);\n\n return {\n name: header.name,\n generated_by: null,\n faction_raw_name: header.faction_raw_name,\n detachment_raw_names: header.detachment_raw_name ? [header.detachment_raw_name] : [],\n battle_size_raw: header.battle_size_raw,\n declared_limit: header.declared_limit,\n total_reported: header.total_reported,\n total_computed: computeTotal(units, enhancementPts),\n units,\n multi_force: detectMultiForce(text, format),\n };\n}\n\nexport const newRecruitWtcCompactAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-compact\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n // wtc-compact has no `N with` lines (that's wtc-full) and no `•` bullets\n // (that's the GW app format) — excluding both keeps the matcher disjoint.\n return !isFullFormat(text) && !hasBullets(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-compact: input is not a string\");\n return parseWith(text, \"wtc-compact\");\n },\n};\n\nexport const newRecruitWtcFullAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-full\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n return isFullFormat(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-full: input is not a string\");\n return parseWith(text, \"wtc-full\");\n },\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/import/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,KAAK,EAIV,YAAY,EAGZ,MAAM,EACN,YAAY,EAIb,MAAM,YAAY,CAAC;AAwDpB,wBAAgB,OAAO,CACrB,MAAM,EAAE,YAAY,EACpB,EAAE,EAAE,OAAO,EACX,MAAM,GAAE,YAA0B,GACjC,MAAM,CAuER"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/import/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,KAAK,EAIV,YAAY,EAGZ,MAAM,EAEN,YAAY,EAIb,MAAM,YAAY,CAAC;AAoEpB,wBAAgB,OAAO,CACrB,MAAM,EAAE,YAAY,EACpB,EAAE,EAAE,OAAO,EACX,MAAM,GAAE,YAA0B,GACjC,MAAM,CA0FR"}
@@ -42,6 +42,17 @@ function mapBattleSize(raw) {
42
42
  return "incursion";
43
43
  return null;
44
44
  }
45
+ /** 11e detachment-point budget for a battle size; null when the size is unknown. */
46
+ function detachmentCap(battle_size) {
47
+ switch (battle_size) {
48
+ case "strike-force":
49
+ return 3;
50
+ case "incursion":
51
+ return 2;
52
+ default:
53
+ return null;
54
+ }
55
+ }
45
56
  export function resolve(parsed, ds, format = "listforge") {
46
57
  const diag = new DiagnosticsBuilder();
47
58
  if (parsed.multi_force) {
@@ -58,28 +69,41 @@ export function resolve(parsed, ds, format = "listforge") {
58
69
  diag.warn("faction-unresolved", "Faction name did not match any 40kdc faction.", parsed.faction_raw_name);
59
70
  }
60
71
  }
61
- // --- Detachment (scoped to faction, then global fallback). ----------------
62
- let detachment_id = null;
63
- if (parsed.detachment_raw_name) {
64
- const key = normalizeName(parsed.detachment_raw_name);
72
+ // --- Detachments (each scoped to faction, then global fallback). ----------
73
+ // 11e lists may field several detachments under a detachment-point cap; the
74
+ // list preserves source order. `dp_cost` is looked up from the resolved
75
+ // detachment entity (no source format reports it).
76
+ const detachments = parsed.detachment_raw_names.map((raw_name) => {
77
+ const key = normalizeName(raw_name);
65
78
  const scoped = faction_id
66
79
  ? ds.detachments.byFaction(faction_id).find((d) => normalizeName(d.name ?? "") === key)
67
80
  : undefined;
68
- const hit = scoped ?? ds.detachments.find(parsed.detachment_raw_name);
81
+ const hit = scoped ?? ds.detachments.find(raw_name);
69
82
  if (hit) {
70
- detachment_id = hit.id;
83
+ return { ref: resolved(hit.id, raw_name), dp_cost: hit.detachment_points ?? null };
71
84
  }
72
- else {
73
- diag.warn("detachment-unresolved", "Detachment name did not match any 40kdc detachment.", parsed.detachment_raw_name);
74
- }
75
- }
85
+ diag.warn("detachment-unresolved", "Detachment name did not match any 40kdc detachment.", raw_name);
86
+ return {
87
+ ref: unresolved(raw_name, toCandidates(ds.detachments.findAll(raw_name))),
88
+ dp_cost: null,
89
+ };
90
+ });
91
+ const detachmentIds = detachments.map((d) => d.ref.id).filter((id) => id !== null);
76
92
  // --- Battle size. ---------------------------------------------------------
77
93
  const battle_size = mapBattleSize(parsed.battle_size_raw);
78
94
  if (parsed.battle_size_raw && battle_size === null) {
79
95
  diag.warn("battle-size-unmapped", "Battle size label could not be mapped.", parsed.battle_size_raw);
80
96
  }
97
+ const detachment_cap = detachmentCap(battle_size);
98
+ // --- Detachment-point cap check (only when cap and every cost are known). --
99
+ if (detachment_cap !== null && detachments.length > 0 && detachments.every((d) => d.dp_cost !== null)) {
100
+ const spent = detachments.reduce((sum, d) => sum + (d.dp_cost ?? 0), 0);
101
+ if (spent > detachment_cap) {
102
+ diag.warn("detachment-points-exceeded", `Detachments cost ${spent} detachment points but the ${battle_size} budget is ${detachment_cap}.`);
103
+ }
104
+ }
81
105
  // --- Units (and their enhancements / wargear). ----------------------------
82
- const units = parsed.units.map((u) => resolveUnit(u, faction_id, detachment_id, ds, diag));
106
+ const units = parsed.units.map((u) => resolveUnit(u, faction_id, detachmentIds, ds, diag));
83
107
  // --- Leader attachments (second pass: needs all resolved unit ids). -------
84
108
  inferLeaderAttachments(parsed.units, units, ds, diag);
85
109
  // --- Points reconciliation (reported vs computed kept distinct). ----------
@@ -90,10 +114,11 @@ export function resolve(parsed, ds, format = "listforge") {
90
114
  name: parsed.name,
91
115
  source: { format, generated_by: parsed.generated_by },
92
116
  faction_id,
93
- detachment_id,
117
+ detachments,
94
118
  battle_size,
95
119
  points: {
96
120
  declared_limit: parsed.declared_limit,
121
+ detachment_cap,
97
122
  total_reported: parsed.total_reported,
98
123
  total_computed: parsed.total_computed,
99
124
  },
@@ -102,7 +127,7 @@ export function resolve(parsed, ds, format = "listforge") {
102
127
  diagnostics: diag.build(),
103
128
  };
104
129
  }
105
- function resolveUnit(parsed, faction_id, detachment_id, ds, diag) {
130
+ function resolveUnit(parsed, faction_id, detachmentIds, ds, diag) {
106
131
  // Prefer a faction-scoped match (the same unit id recurs across factions),
107
132
  // then fall back to a global name lookup.
108
133
  const key = normalizeName(parsed.raw_name);
@@ -122,7 +147,7 @@ function resolveUnit(parsed, faction_id, detachment_id, ds, diag) {
122
147
  diag.warn("unit-unresolved", "Unit name did not match any 40kdc unit.", parsed.raw_name);
123
148
  }
124
149
  const enhancement = parsed.enhancement_raw_name
125
- ? resolveEnhancement(parsed.enhancement_raw_name, detachment_id, ds, diag)
150
+ ? resolveEnhancement(parsed.enhancement_raw_name, detachmentIds, ds, diag)
126
151
  : null;
127
152
  const enhancement_points = enhancement === null ? null : parsed.enhancement_points;
128
153
  const wargear = parsed.wargear.map((w) => {
@@ -146,11 +171,14 @@ function resolveUnit(parsed, faction_id, detachment_id, ds, diag) {
146
171
  leader_attachment: null,
147
172
  };
148
173
  }
149
- function resolveEnhancement(raw_name, detachment_id, ds, diag) {
174
+ function resolveEnhancement(raw_name, detachmentIds, ds, diag) {
150
175
  const key = normalizeName(raw_name);
151
- // Enhancements belong to a detachment, not a faction — scope by detachment_id.
152
- const scoped = detachment_id
153
- ? ds.enhancements.all.find((e) => e.detachment_id === detachment_id && normalizeName(e.name ?? "") === key)
176
+ // Enhancements belong to a detachment, not a faction — scope to any of the
177
+ // roster's resolved detachments.
178
+ const scoped = detachmentIds.length > 0
179
+ ? ds.enhancements.all.find((e) => e.detachment_id != null &&
180
+ detachmentIds.includes(e.detachment_id) &&
181
+ normalizeName(e.name ?? "") === key)
154
182
  : undefined;
155
183
  const hit = scoped ?? ds.enhancements.find(raw_name);
156
184
  if (hit) {
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/import/resolve.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAerD,qEAAqE;AACrE,MAAM,mBAAmB,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,wBAAwB,EAAE,CAAC;AAErF,MAAM,cAAc,GAAG,CAAC,CAAC;AAOzB,6EAA6E;AAC7E,MAAM,kBAAkB;IACtB,cAAc,GAAG,CAAC,CAAC;IACnB,gBAAgB,GAAG,CAAC,CAAC;IACrB,gBAAgB,GAAG,CAAC,CAAC;IACrB,kBAAkB,GAAG,CAAC,CAAC;IACd,QAAQ,GAAc,EAAE,CAAC;IAElC,IAAI,CAAC,IAAiB,EAAE,OAAe,EAAE,WAA0B,IAAI;QACrE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,KAAK;QACH,OAAO;YACL,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;YACvC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;YACvC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;IACJ,CAAC;CACF;AAED,SAAS,UAAU,CAAC,QAAgB,EAAE,aAA0B,EAAE;IAChE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC7D,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU,EAAE,QAAgB;IAC5C,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;AAC1D,CAAC;AAED,SAAS,YAAY,CAAC,OAA+B;IACnD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACnF,CAAC;AAED,yEAAyE;AACzE,SAAS,aAAa,CAAC,GAAkB;IACvC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,cAAc,CAAC;IACxD,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IAClD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,OAAO,CACrB,MAAoB,EACpB,EAAW,EACX,SAAuB,WAAW;IAElC,MAAM,IAAI,GAAG,IAAI,kBAAkB,EAAE,CAAC;IAEtC,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,IAAI,CAAC,IAAI,CACP,aAAa,EACb,uFAAuF,CACxF,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACtD,IAAI,GAAG,EAAE,CAAC;YACR,UAAU,GAAG,GAAG,CAAC,EAAE,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,+CAA+C,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAC5G,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,IAAI,aAAa,GAAkB,IAAI,CAAC;IACxC,IAAI,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACtD,MAAM,MAAM,GAAG,UAAU;YACvB,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,KAAK,GAAG,CAAC;YACvF,CAAC,CAAC,SAAS,CAAC;QACd,MAAM,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACtE,IAAI,GAAG,EAAE,CAAC;YACR,aAAa,GAAG,GAAG,CAAC,EAAE,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,uBAAuB,EAAE,qDAAqD,EAAE,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACxH,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAC1D,IAAI,MAAM,CAAC,eAAe,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACnD,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,wCAAwC,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;IACtG,CAAC;IAED,6EAA6E;IAC7E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;IAE3F,6EAA6E;IAC7E,sBAAsB,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IAEtD,6EAA6E;IAC7E,IAAI,MAAM,CAAC,cAAc,KAAK,IAAI,IAAI,MAAM,CAAC,cAAc,KAAK,MAAM,CAAC,cAAc,EAAE,CAAC;QACtF,IAAI,CAAC,IAAI,CACP,iBAAiB,EACjB,0BAA0B,MAAM,CAAC,cAAc,yCAAyC,MAAM,CAAC,cAAc,IAAI,CAClH,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE;QACrD,UAAU;QACV,aAAa;QACb,WAAW;QACX,MAAM,EAAE;YACN,cAAc,EAAE,MAAM,CAAC,cAAc;YACrC,cAAc,EAAE,MAAM,CAAC,cAAc;YACrC,cAAc,EAAE,MAAM,CAAC,cAAc;SACtC;QACD,KAAK;QACL,YAAY,EAAE,EAAE,GAAG,mBAAmB,EAAE;QACxC,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE;KAC1B,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAClB,MAAkB,EAClB,UAAyB,EACzB,aAA4B,EAC5B,EAAW,EACX,IAAwB;IAExB,2EAA2E;IAC3E,0CAA0C;IAC1C,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,UAAU;QACvB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC;QAC3E,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IAE7B,IAAI,GAAgB,CAAC;IACrB,IAAI,GAAG,EAAE,CAAC;QACR,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;IAC3B,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;QACrD,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,yCAAyC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3F,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,oBAAoB;QAC7C,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,oBAAoB,EAAE,aAAa,EAAE,EAAE,EAAE,IAAI,CAAC;QAC1E,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,kBAAkB,GAAG,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC;IAEnF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACvC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACZ,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC;YAC3B,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACnE,CAAC;QACD,IAAI,CAAC,kBAAkB,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,6CAA6C,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC1F,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAC7E,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,GAAG;QACH,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,WAAW;QACX,kBAAkB;QAClB,OAAO;QACP,iBAAiB,EAAE,IAAI;KACxB,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CACzB,QAAgB,EAChB,aAA4B,EAC5B,EAAW,EACX,IAAwB;IAExB,MAAM,GAAG,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACpC,+EAA+E;IAC/E,MAAM,MAAM,GAAG,aAAa;QAC1B,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,aAAa,IAAI,aAAa,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,KAAK,GAAG,CAAC;QAC3G,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,wBAAwB,EAAE,uDAAuD,EAAE,QAAQ,CAAC,CAAC;IACvG,OAAO,UAAU,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAkB,CAAC,CAAC,CAAC;AAChG,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAC7B,WAAyB,EACzB,KAAmB,EACnB,EAAW,EACX,IAAwB;IAExB,MAAM,YAAY,GAAG,IAAI,GAAG,CAC1B,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAY,CAAC,CAChG,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QACxB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,YAAY;YAAE,OAAO;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC;QAChF,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,MAAM,WAAW,GAAG,UAAU,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACzF,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzB,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;QAC9D,IAAI,CAAC,SAAS;YAAE,OAAO;QAEvB,IAAI,CAAC,iBAAiB,GAAG;YACvB,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC5D,WAAW,EAAE,IAAI;SAClB,CAAC;QACF,IAAI,CAAC,IAAI,CACP,4BAA4B,EAC5B,gFAAgF,EAChF,IAAI,CAAC,GAAG,CAAC,QAAQ,CAClB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Resolve a {@link ParsedRoster} onto 40kdc entity ids, producing a {@link Roster}.\n *\n * Resolution is lenient: a name that doesn't match a 40kdc entity yields a\n * {@link ResolvedRef} with `id: null`, `resolved: false`, and up to five\n * candidate suggestions — the roster is never dropped or rejected. Everything\n * that didn't resolve cleanly is summarised in the {@link Diagnostics} block.\n *\n * Matching reuses the dataset's own lookups ({@link Collection.find},\n * {@link Collection.findAll}, {@link Collection.byFaction}) and\n * {@link normalizeName}; there is no bespoke fuzzy matcher. Faction is resolved\n * first so unit/detachment/enhancement lookups can be scoped to it — the same\n * unit id can appear under several factions, so scoping disambiguates.\n *\n * @packageDocumentation\n */\nimport type { Dataset } from \"../data/dataset.js\";\nimport { normalizeName } from \"../data/normalize.js\";\nimport type {\n BattleSize,\n Candidate,\n Diagnostics,\n ParsedRoster,\n ParsedUnit,\n ResolvedRef,\n Roster,\n RosterFormat,\n RosterUnit,\n Warning,\n WarningCode,\n} from \"./types.js\";\n\n/** The dataset edition/dataslate stamped onto an imported roster. */\nconst ROSTER_GAME_VERSION = { edition: \"11th\", dataslate: \"pre-launch-provisional\" };\n\nconst MAX_CANDIDATES = 5;\n\ninterface NamedRecord {\n id: string;\n name: string;\n}\n\n/** Accumulates warnings and resolved/unresolved tallies during an import. */\nclass DiagnosticsBuilder {\n resolved_units = 0;\n unresolved_units = 0;\n resolved_weapons = 0;\n unresolved_weapons = 0;\n readonly warnings: Warning[] = [];\n\n warn(code: WarningCode, message: string, raw_name: string | null = null): void {\n this.warnings.push({ code, message, raw_name });\n }\n\n build(): Diagnostics {\n return {\n resolved_units: this.resolved_units,\n unresolved_units: this.unresolved_units,\n resolved_weapons: this.resolved_weapons,\n unresolved_weapons: this.unresolved_weapons,\n warnings: this.warnings,\n };\n }\n}\n\nfunction unresolved(raw_name: string, candidates: Candidate[] = []): ResolvedRef {\n return { id: null, raw_name, resolved: false, candidates };\n}\n\nfunction resolved(id: string, raw_name: string): ResolvedRef {\n return { id, raw_name, resolved: true, candidates: [] };\n}\n\nfunction toCandidates(records: readonly NamedRecord[]): Candidate[] {\n return records.slice(0, MAX_CANDIDATES).map((r) => ({ id: r.id, name: r.name }));\n}\n\n/** Map a source battle-size label to the 40kdc enum, if recognisable. */\nfunction mapBattleSize(raw: string | null): BattleSize | null {\n if (!raw) return null;\n const key = normalizeName(raw);\n if (key.includes(\"strike force\")) return \"strike-force\";\n if (key.includes(\"incursion\")) return \"incursion\";\n return null;\n}\n\nexport function resolve(\n parsed: ParsedRoster,\n ds: Dataset,\n format: RosterFormat = \"listforge\",\n): Roster {\n const diag = new DiagnosticsBuilder();\n\n if (parsed.multi_force) {\n diag.warn(\n \"multi-force\",\n \"Source list contains more than one faction; the primary faction was used for scoping.\",\n );\n }\n\n // --- Faction (resolved first so other lookups can scope to it). -----------\n let faction_id: string | null = null;\n if (parsed.faction_raw_name) {\n const hit = ds.factions.find(parsed.faction_raw_name);\n if (hit) {\n faction_id = hit.id;\n } else {\n diag.warn(\"faction-unresolved\", \"Faction name did not match any 40kdc faction.\", parsed.faction_raw_name);\n }\n }\n\n // --- Detachment (scoped to faction, then global fallback). ----------------\n let detachment_id: string | null = null;\n if (parsed.detachment_raw_name) {\n const key = normalizeName(parsed.detachment_raw_name);\n const scoped = faction_id\n ? ds.detachments.byFaction(faction_id).find((d) => normalizeName(d.name ?? \"\") === key)\n : undefined;\n const hit = scoped ?? ds.detachments.find(parsed.detachment_raw_name);\n if (hit) {\n detachment_id = hit.id;\n } else {\n diag.warn(\"detachment-unresolved\", \"Detachment name did not match any 40kdc detachment.\", parsed.detachment_raw_name);\n }\n }\n\n // --- Battle size. ---------------------------------------------------------\n const battle_size = mapBattleSize(parsed.battle_size_raw);\n if (parsed.battle_size_raw && battle_size === null) {\n diag.warn(\"battle-size-unmapped\", \"Battle size label could not be mapped.\", parsed.battle_size_raw);\n }\n\n // --- Units (and their enhancements / wargear). ----------------------------\n const units = parsed.units.map((u) => resolveUnit(u, faction_id, detachment_id, ds, diag));\n\n // --- Leader attachments (second pass: needs all resolved unit ids). -------\n inferLeaderAttachments(parsed.units, units, ds, diag);\n\n // --- Points reconciliation (reported vs computed kept distinct). ----------\n if (parsed.total_reported !== null && parsed.total_reported !== parsed.total_computed) {\n diag.warn(\n \"points-mismatch\",\n `Source-reported total (${parsed.total_reported}) differs from the sum of cost lines (${parsed.total_computed}).`,\n );\n }\n\n return {\n name: parsed.name,\n source: { format, generated_by: parsed.generated_by },\n faction_id,\n detachment_id,\n battle_size,\n points: {\n declared_limit: parsed.declared_limit,\n total_reported: parsed.total_reported,\n total_computed: parsed.total_computed,\n },\n units,\n game_version: { ...ROSTER_GAME_VERSION },\n diagnostics: diag.build(),\n };\n}\n\nfunction resolveUnit(\n parsed: ParsedUnit,\n faction_id: string | null,\n detachment_id: string | null,\n ds: Dataset,\n diag: DiagnosticsBuilder,\n): RosterUnit {\n // Prefer a faction-scoped match (the same unit id recurs across factions),\n // then fall back to a global name lookup.\n const key = normalizeName(parsed.raw_name);\n const scoped = faction_id\n ? ds.units.byFaction(faction_id).find((u) => normalizeName(u.name) === key)\n : undefined;\n const all = ds.units.findAll(parsed.raw_name);\n const hit = scoped ?? all[0];\n\n let ref: ResolvedRef;\n if (hit) {\n ref = resolved(hit.id, parsed.raw_name);\n diag.resolved_units += 1;\n } else {\n ref = unresolved(parsed.raw_name, toCandidates(all));\n diag.unresolved_units += 1;\n diag.warn(\"unit-unresolved\", \"Unit name did not match any 40kdc unit.\", parsed.raw_name);\n }\n\n const enhancement = parsed.enhancement_raw_name\n ? resolveEnhancement(parsed.enhancement_raw_name, detachment_id, ds, diag)\n : null;\n const enhancement_points = enhancement === null ? null : parsed.enhancement_points;\n\n const wargear = parsed.wargear.map((w) => {\n const hits = ds.weapons.findAll(w.raw_name);\n if (hits[0]) {\n diag.resolved_weapons += 1;\n return { ref: resolved(hits[0].id, w.raw_name), count: w.count };\n }\n diag.unresolved_weapons += 1;\n diag.warn(\"weapon-unresolved\", \"Weapon name did not match any 40kdc weapon.\", w.raw_name);\n return { ref: unresolved(w.raw_name, toCandidates(hits)), count: w.count };\n });\n\n return {\n ref,\n model_count: parsed.model_count,\n points: parsed.points,\n is_warlord: parsed.is_warlord,\n enhancement,\n enhancement_points,\n wargear,\n leader_attachment: null,\n };\n}\n\nfunction resolveEnhancement(\n raw_name: string,\n detachment_id: string | null,\n ds: Dataset,\n diag: DiagnosticsBuilder,\n): ResolvedRef {\n const key = normalizeName(raw_name);\n // Enhancements belong to a detachment, not a faction — scope by detachment_id.\n const scoped = detachment_id\n ? ds.enhancements.all.find((e) => e.detachment_id === detachment_id && normalizeName(e.name ?? \"\") === key)\n : undefined;\n const hit = scoped ?? ds.enhancements.find(raw_name);\n if (hit) {\n return resolved(hit.id, raw_name);\n }\n diag.warn(\"enhancement-unresolved\", \"Enhancement name did not match any 40kdc enhancement.\", raw_name);\n return unresolved(raw_name, toCandidates(ds.enhancements.findAll(raw_name) as NamedRecord[]));\n}\n\n/**\n * Infer leader→bodyguard attachments. The source format does not encode an\n * unambiguous attachment, so each inferred link is marked provisional: we match\n * a resolved character unit against a resolved non-character unit in the same\n * roster using the dataset's leader-attachment data.\n */\nfunction inferLeaderAttachments(\n parsedUnits: ParsedUnit[],\n units: RosterUnit[],\n ds: Dataset,\n diag: DiagnosticsBuilder,\n): void {\n const bodyguardIds = new Set(\n units.filter((u, i) => u.ref.id && !parsedUnits[i].is_character).map((u) => u.ref.id as string),\n );\n\n units.forEach((unit, i) => {\n if (!unit.ref.id || !parsedUnits[i].is_character) return;\n const leaderId = unit.ref.id;\n const attachment = ds.leaderAttachments.find((la) => la.leader_id === leaderId);\n if (!attachment) return;\n const bodyguardId = attachment.eligible_bodyguard_ids.find((id) => bodyguardIds.has(id));\n if (!bodyguardId) return;\n\n const bodyguard = units.find((u) => u.ref.id === bodyguardId);\n if (!bodyguard) return;\n\n unit.leader_attachment = {\n bodyguard_ref: resolved(bodyguardId, bodyguard.ref.raw_name),\n provisional: true,\n };\n diag.warn(\n \"leader-attachment-inferred\",\n \"Leader attachment was inferred from leader-attachment data and is provisional.\",\n unit.ref.raw_name,\n );\n });\n}\n"]}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/import/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve a {@link ParsedRoster} onto 40kdc entity ids, producing a {@link Roster}.\n *\n * Resolution is lenient: a name that doesn't match a 40kdc entity yields a\n * {@link ResolvedRef} with `id: null`, `resolved: false`, and up to five\n * candidate suggestions — the roster is never dropped or rejected. Everything\n * that didn't resolve cleanly is summarised in the {@link Diagnostics} block.\n *\n * Matching reuses the dataset's own lookups ({@link Collection.find},\n * {@link Collection.findAll}, {@link Collection.byFaction}) and\n * {@link normalizeName}; there is no bespoke fuzzy matcher. Faction is resolved\n * first so unit/detachment/enhancement lookups can be scoped to it — the same\n * unit id can appear under several factions, so scoping disambiguates.\n *\n * @packageDocumentation\n */\nimport type { Dataset } from \"../data/dataset.js\";\nimport { normalizeName } from \"../data/normalize.js\";\nimport type {\n BattleSize,\n Candidate,\n Diagnostics,\n ParsedRoster,\n ParsedUnit,\n ResolvedRef,\n Roster,\n RosterDetachment,\n RosterFormat,\n RosterUnit,\n Warning,\n WarningCode,\n} from \"./types.js\";\n\n/** The dataset edition/dataslate stamped onto an imported roster. */\nconst ROSTER_GAME_VERSION = { edition: \"11th\", dataslate: \"pre-launch-provisional\" };\n\nconst MAX_CANDIDATES = 5;\n\ninterface NamedRecord {\n id: string;\n name: string;\n}\n\n/** Accumulates warnings and resolved/unresolved tallies during an import. */\nclass DiagnosticsBuilder {\n resolved_units = 0;\n unresolved_units = 0;\n resolved_weapons = 0;\n unresolved_weapons = 0;\n readonly warnings: Warning[] = [];\n\n warn(code: WarningCode, message: string, raw_name: string | null = null): void {\n this.warnings.push({ code, message, raw_name });\n }\n\n build(): Diagnostics {\n return {\n resolved_units: this.resolved_units,\n unresolved_units: this.unresolved_units,\n resolved_weapons: this.resolved_weapons,\n unresolved_weapons: this.unresolved_weapons,\n warnings: this.warnings,\n };\n }\n}\n\nfunction unresolved(raw_name: string, candidates: Candidate[] = []): ResolvedRef {\n return { id: null, raw_name, resolved: false, candidates };\n}\n\nfunction resolved(id: string, raw_name: string): ResolvedRef {\n return { id, raw_name, resolved: true, candidates: [] };\n}\n\nfunction toCandidates(records: readonly NamedRecord[]): Candidate[] {\n return records.slice(0, MAX_CANDIDATES).map((r) => ({ id: r.id, name: r.name }));\n}\n\n/** Map a source battle-size label to the 40kdc enum, if recognisable. */\nfunction mapBattleSize(raw: string | null): BattleSize | null {\n if (!raw) return null;\n const key = normalizeName(raw);\n if (key.includes(\"strike force\")) return \"strike-force\";\n if (key.includes(\"incursion\")) return \"incursion\";\n return null;\n}\n\n/** 11e detachment-point budget for a battle size; null when the size is unknown. */\nfunction detachmentCap(battle_size: BattleSize | null): number | null {\n switch (battle_size) {\n case \"strike-force\":\n return 3;\n case \"incursion\":\n return 2;\n default:\n return null;\n }\n}\n\nexport function resolve(\n parsed: ParsedRoster,\n ds: Dataset,\n format: RosterFormat = \"listforge\",\n): Roster {\n const diag = new DiagnosticsBuilder();\n\n if (parsed.multi_force) {\n diag.warn(\n \"multi-force\",\n \"Source list contains more than one faction; the primary faction was used for scoping.\",\n );\n }\n\n // --- Faction (resolved first so other lookups can scope to it). -----------\n let faction_id: string | null = null;\n if (parsed.faction_raw_name) {\n const hit = ds.factions.find(parsed.faction_raw_name);\n if (hit) {\n faction_id = hit.id;\n } else {\n diag.warn(\"faction-unresolved\", \"Faction name did not match any 40kdc faction.\", parsed.faction_raw_name);\n }\n }\n\n // --- Detachments (each scoped to faction, then global fallback). ----------\n // 11e lists may field several detachments under a detachment-point cap; the\n // list preserves source order. `dp_cost` is looked up from the resolved\n // detachment entity (no source format reports it).\n const detachments: RosterDetachment[] = parsed.detachment_raw_names.map((raw_name) => {\n const key = normalizeName(raw_name);\n const scoped = faction_id\n ? ds.detachments.byFaction(faction_id).find((d) => normalizeName(d.name ?? \"\") === key)\n : undefined;\n const hit = scoped ?? ds.detachments.find(raw_name);\n if (hit) {\n return { ref: resolved(hit.id, raw_name), dp_cost: hit.detachment_points ?? null };\n }\n diag.warn(\"detachment-unresolved\", \"Detachment name did not match any 40kdc detachment.\", raw_name);\n return {\n ref: unresolved(raw_name, toCandidates(ds.detachments.findAll(raw_name) as NamedRecord[])),\n dp_cost: null,\n };\n });\n const detachmentIds = detachments.map((d) => d.ref.id).filter((id): id is string => id !== null);\n\n // --- Battle size. ---------------------------------------------------------\n const battle_size = mapBattleSize(parsed.battle_size_raw);\n if (parsed.battle_size_raw && battle_size === null) {\n diag.warn(\"battle-size-unmapped\", \"Battle size label could not be mapped.\", parsed.battle_size_raw);\n }\n const detachment_cap = detachmentCap(battle_size);\n\n // --- Detachment-point cap check (only when cap and every cost are known). --\n if (detachment_cap !== null && detachments.length > 0 && detachments.every((d) => d.dp_cost !== null)) {\n const spent = detachments.reduce((sum, d) => sum + (d.dp_cost ?? 0), 0);\n if (spent > detachment_cap) {\n diag.warn(\n \"detachment-points-exceeded\",\n `Detachments cost ${spent} detachment points but the ${battle_size} budget is ${detachment_cap}.`,\n );\n }\n }\n\n // --- Units (and their enhancements / wargear). ----------------------------\n const units = parsed.units.map((u) => resolveUnit(u, faction_id, detachmentIds, ds, diag));\n\n // --- Leader attachments (second pass: needs all resolved unit ids). -------\n inferLeaderAttachments(parsed.units, units, ds, diag);\n\n // --- Points reconciliation (reported vs computed kept distinct). ----------\n if (parsed.total_reported !== null && parsed.total_reported !== parsed.total_computed) {\n diag.warn(\n \"points-mismatch\",\n `Source-reported total (${parsed.total_reported}) differs from the sum of cost lines (${parsed.total_computed}).`,\n );\n }\n\n return {\n name: parsed.name,\n source: { format, generated_by: parsed.generated_by },\n faction_id,\n detachments,\n battle_size,\n points: {\n declared_limit: parsed.declared_limit,\n detachment_cap,\n total_reported: parsed.total_reported,\n total_computed: parsed.total_computed,\n },\n units,\n game_version: { ...ROSTER_GAME_VERSION },\n diagnostics: diag.build(),\n };\n}\n\nfunction resolveUnit(\n parsed: ParsedUnit,\n faction_id: string | null,\n detachmentIds: string[],\n ds: Dataset,\n diag: DiagnosticsBuilder,\n): RosterUnit {\n // Prefer a faction-scoped match (the same unit id recurs across factions),\n // then fall back to a global name lookup.\n const key = normalizeName(parsed.raw_name);\n const scoped = faction_id\n ? ds.units.byFaction(faction_id).find((u) => normalizeName(u.name) === key)\n : undefined;\n const all = ds.units.findAll(parsed.raw_name);\n const hit = scoped ?? all[0];\n\n let ref: ResolvedRef;\n if (hit) {\n ref = resolved(hit.id, parsed.raw_name);\n diag.resolved_units += 1;\n } else {\n ref = unresolved(parsed.raw_name, toCandidates(all));\n diag.unresolved_units += 1;\n diag.warn(\"unit-unresolved\", \"Unit name did not match any 40kdc unit.\", parsed.raw_name);\n }\n\n const enhancement = parsed.enhancement_raw_name\n ? resolveEnhancement(parsed.enhancement_raw_name, detachmentIds, ds, diag)\n : null;\n const enhancement_points = enhancement === null ? null : parsed.enhancement_points;\n\n const wargear = parsed.wargear.map((w) => {\n const hits = ds.weapons.findAll(w.raw_name);\n if (hits[0]) {\n diag.resolved_weapons += 1;\n return { ref: resolved(hits[0].id, w.raw_name), count: w.count };\n }\n diag.unresolved_weapons += 1;\n diag.warn(\"weapon-unresolved\", \"Weapon name did not match any 40kdc weapon.\", w.raw_name);\n return { ref: unresolved(w.raw_name, toCandidates(hits)), count: w.count };\n });\n\n return {\n ref,\n model_count: parsed.model_count,\n points: parsed.points,\n is_warlord: parsed.is_warlord,\n enhancement,\n enhancement_points,\n wargear,\n leader_attachment: null,\n };\n}\n\nfunction resolveEnhancement(\n raw_name: string,\n detachmentIds: string[],\n ds: Dataset,\n diag: DiagnosticsBuilder,\n): ResolvedRef {\n const key = normalizeName(raw_name);\n // Enhancements belong to a detachment, not a faction — scope to any of the\n // roster's resolved detachments.\n const scoped =\n detachmentIds.length > 0\n ? ds.enhancements.all.find(\n (e) =>\n e.detachment_id != null &&\n detachmentIds.includes(e.detachment_id) &&\n normalizeName(e.name ?? \"\") === key,\n )\n : undefined;\n const hit = scoped ?? ds.enhancements.find(raw_name);\n if (hit) {\n return resolved(hit.id, raw_name);\n }\n diag.warn(\"enhancement-unresolved\", \"Enhancement name did not match any 40kdc enhancement.\", raw_name);\n return unresolved(raw_name, toCandidates(ds.enhancements.findAll(raw_name) as NamedRecord[]));\n}\n\n/**\n * Infer leader→bodyguard attachments. The source format does not encode an\n * unambiguous attachment, so each inferred link is marked provisional: we match\n * a resolved character unit against a resolved non-character unit in the same\n * roster using the dataset's leader-attachment data.\n */\nfunction inferLeaderAttachments(\n parsedUnits: ParsedUnit[],\n units: RosterUnit[],\n ds: Dataset,\n diag: DiagnosticsBuilder,\n): void {\n const bodyguardIds = new Set(\n units.filter((u, i) => u.ref.id && !parsedUnits[i].is_character).map((u) => u.ref.id as string),\n );\n\n units.forEach((unit, i) => {\n if (!unit.ref.id || !parsedUnits[i].is_character) return;\n const leaderId = unit.ref.id;\n const attachment = ds.leaderAttachments.find((la) => la.leader_id === leaderId);\n if (!attachment) return;\n const bodyguardId = attachment.eligible_bodyguard_ids.find((id) => bodyguardIds.has(id));\n if (!bodyguardId) return;\n\n const bodyguard = units.find((u) => u.ref.id === bodyguardId);\n if (!bodyguard) return;\n\n unit.leader_attachment = {\n bodyguard_ref: resolved(bodyguardId, bodyguard.ref.raw_name),\n provisional: true,\n };\n diag.warn(\n \"leader-attachment-inferred\",\n \"Leader attachment was inferred from leader-attachment data and is provisional.\",\n unit.ref.raw_name,\n );\n });\n}\n"]}
@@ -12,9 +12,11 @@
12
12
  * Lowering notes:
13
13
  * - Unit/wargear/enhancement rows lower to their `ref.raw_name` — the same
14
14
  * raw-display-name path every other adapter takes.
15
- * - `faction_id`/`detachment_id` have no raw name in the canonical shape, so
16
- * the id slug is passed through as the raw name; collection lookup does an
17
- * exact-id match before any name lookup, so resolution is exact.
15
+ * - `faction_id` has no raw name in the canonical shape, so the id slug is
16
+ * passed through as the raw name; collection lookup does an exact-id match
17
+ * before any name lookup, so resolution is exact. Detachments do carry a
18
+ * `ref.raw_name`, so (like units/enhancements) that lowers directly and
19
+ * round-trips the display name.
18
20
  * - `is_character` isn't stored on the canonical shape (it's an inference
19
21
  * input, not an output). It lowers as `leader_attachment != null`, which
20
22
  * reproduces the original (deterministic) attachment inference on
@@ -1 +1 @@
1
- {"version":3,"file":"roster-json.d.ts","sourceRoot":"","sources":["../../src/import/roster-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAalD,eAAO,MAAM,iBAAiB,EAAE,aAyD/B,CAAC"}
1
+ {"version":3,"file":"roster-json.d.ts","sourceRoot":"","sources":["../../src/import/roster-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAalD,eAAO,MAAM,iBAAiB,EAAE,aA2D/B,CAAC"}
@@ -12,9 +12,11 @@
12
12
  * Lowering notes:
13
13
  * - Unit/wargear/enhancement rows lower to their `ref.raw_name` — the same
14
14
  * raw-display-name path every other adapter takes.
15
- * - `faction_id`/`detachment_id` have no raw name in the canonical shape, so
16
- * the id slug is passed through as the raw name; collection lookup does an
17
- * exact-id match before any name lookup, so resolution is exact.
15
+ * - `faction_id` has no raw name in the canonical shape, so the id slug is
16
+ * passed through as the raw name; collection lookup does an exact-id match
17
+ * before any name lookup, so resolution is exact. Detachments do carry a
18
+ * `ref.raw_name`, so (like units/enhancements) that lowers directly and
19
+ * round-trips the display name.
18
20
  * - `is_character` isn't stored on the canonical shape (it's an inference
19
21
  * input, not an output). It lowers as `leader_attachment != null`, which
20
22
  * reproduces the original (deterministic) attachment inference on
@@ -74,7 +76,9 @@ export const rosterJsonAdapter = {
74
76
  // Id slugs pass through as raw names — collection lookup id-matches
75
77
  // exactly before any name lookup.
76
78
  faction_raw_name: roster.faction_id,
77
- detachment_raw_name: roster.detachment_id,
79
+ // Each detachment carries its own raw name (like units/enhancements), so
80
+ // lower that — it round-trips the display name and re-resolves exactly.
81
+ detachment_raw_names: roster.detachments.map((d) => d.ref.raw_name),
78
82
  battle_size_raw: roster.battle_size != null ? (BATTLE_SIZE_LABELS[roster.battle_size] ?? null) : null,
79
83
  declared_limit: roster.points.declared_limit,
80
84
  total_reported: roster.points.total_reported,
@@ -1 +1 @@
1
- {"version":3,"file":"roster-json.js","sourceRoot":"","sources":["../../src/import/roster-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,0DAA0D;AAC1D,MAAM,kBAAkB,GAA2B;IACjD,SAAS,EAAE,WAAW;IACtB,cAAc,EAAE,cAAc;CAC/B,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC9C,EAAE,EAAE,aAAa;IAEjB;;;;OAIG;IACH,OAAO,CAAC,OAAgB;QACtB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,MAAM,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;QAC5C,OAAO,CACL,QAAQ,CAAC,MAAM,CAAC;YAChB,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAQ;YACpC,QAAQ,CAAC,WAAW,CAAC;YACrB,OAAO,WAAW,CAAC,SAAS,CAAC,KAAK,QAAQ;YAC1C,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;YAChC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAChC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAgB;QACpB,kEAAkE;QAClE,MAAM,MAAM,GAAG,OAA4B,CAAC;QAE5C,MAAM,KAAK,GAAiB,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnD,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ;YACxB,yEAAyE;YACzE,kEAAkE;YAClE,gCAAgC;YAChC,YAAY,EAAE,CAAC,CAAC,iBAAiB,IAAI,IAAI;YACzC,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,oBAAoB,EAAE,CAAC,CAAC,WAAW,EAAE,QAAQ,IAAI,IAAI;YACrD,kBAAkB,EAAE,CAAC,CAAC,kBAAkB;YACxC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;SAC9E,CAAC,CAAC,CAAC;QAEJ,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY;YACxC,oEAAoE;YACpE,kCAAkC;YAClC,gBAAgB,EAAE,MAAM,CAAC,UAAU;YACnC,mBAAmB,EAAE,MAAM,CAAC,aAAa;YACzC,eAAe,EACb,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;YACtF,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc;YAC5C,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc;YAC5C,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc;YAC5C,KAAK;YACL,wDAAwD;YACxD,WAAW,EAAE,KAAK;SACnB,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["/**\n * Canonical roster-json adapter: re-import a 40kdc {@link Roster} export.\n *\n * The exporter's `roster-json` format (see `export/roster-json.ts`) is the\n * lossless pivot — exactly `roster.schema.json` shape. This adapter closes the\n * loop so a 40kdc-native export round-trips through the normal\n * `tryImportRoster` pipeline: validate the canonical envelope, lower it to the\n * format-agnostic {@link ParsedRoster}, and let `resolve` re-derive ids\n * against the *current* dataset (so a stored export keeps resolving across\n * dataset releases, and stale ids self-heal through name resolution).\n *\n * Lowering notes:\n * - Unit/wargear/enhancement rows lower to their `ref.raw_name` — the same\n * raw-display-name path every other adapter takes.\n * - `faction_id`/`detachment_id` have no raw name in the canonical shape, so\n * the id slug is passed through as the raw name; collection lookup does an\n * exact-id match before any name lookup, so resolution is exact.\n * - `is_character` isn't stored on the canonical shape (it's an inference\n * input, not an output). It lowers as `leader_attachment != null`, which\n * reproduces the original (deterministic) attachment inference on\n * re-import. Attachments are always provisional either way.\n *\n * **IP safety**: the canonical document carries only permitted facts (names,\n * counts, points, ids); no prose fields exist to read.\n *\n * TS mirror of `crates/wh40kdc/src/import/roster_json.rs`.\n */\n\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, Roster } from \"./types.js\";\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\n/** Battle-size labels `resolve` maps back to the enum. */\nconst BATTLE_SIZE_LABELS: Record<string, string> = {\n incursion: \"Incursion\",\n \"strike-force\": \"Strike Force\",\n};\n\nexport const rosterJsonAdapter: FormatAdapter = {\n id: \"roster-json\",\n\n /**\n * The canonical shape is unmistakable: a `source.format` discriminator plus\n * the `game_version` + `diagnostics` envelope no external builder emits.\n * All three are required by `roster.schema.json`.\n */\n matches(decoded: unknown): boolean {\n if (!isRecord(decoded)) return false;\n const source = decoded[\"source\"];\n const gameVersion = decoded[\"game_version\"];\n return (\n isRecord(source) &&\n typeof source[\"format\"] === \"string\" &&\n isRecord(gameVersion) &&\n typeof gameVersion[\"edition\"] === \"string\" &&\n isRecord(decoded[\"diagnostics\"]) &&\n Array.isArray(decoded[\"units\"])\n );\n },\n\n parse(decoded: unknown): ParsedRoster {\n // matches() vetted the envelope; the unit rows are schema-shaped.\n const roster = decoded as unknown as Roster;\n\n const units: ParsedUnit[] = roster.units.map((u) => ({\n raw_name: u.ref.raw_name,\n // Not stored canonically; attached units were characters, and re-running\n // the (deterministic) inference over them reproduces the exported\n // attachments. See module docs.\n is_character: u.leader_attachment != null,\n model_count: u.model_count,\n points: u.points,\n is_warlord: u.is_warlord,\n enhancement_raw_name: u.enhancement?.raw_name ?? null,\n enhancement_points: u.enhancement_points,\n wargear: u.wargear.map((w) => ({ raw_name: w.ref.raw_name, count: w.count })),\n }));\n\n return {\n name: roster.name,\n generated_by: roster.source.generated_by,\n // Id slugs pass through as raw names — collection lookup id-matches\n // exactly before any name lookup.\n faction_raw_name: roster.faction_id,\n detachment_raw_name: roster.detachment_id,\n battle_size_raw:\n roster.battle_size != null ? (BATTLE_SIZE_LABELS[roster.battle_size] ?? null) : null,\n declared_limit: roster.points.declared_limit,\n total_reported: roster.points.total_reported,\n total_computed: roster.points.total_computed,\n units,\n // The canonical shape carries a single primary faction.\n multi_force: false,\n };\n },\n};\n"]}
1
+ {"version":3,"file":"roster-json.js","sourceRoot":"","sources":["../../src/import/roster-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,0DAA0D;AAC1D,MAAM,kBAAkB,GAA2B;IACjD,SAAS,EAAE,WAAW;IACtB,cAAc,EAAE,cAAc;CAC/B,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC9C,EAAE,EAAE,aAAa;IAEjB;;;;OAIG;IACH,OAAO,CAAC,OAAgB;QACtB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,MAAM,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;QAC5C,OAAO,CACL,QAAQ,CAAC,MAAM,CAAC;YAChB,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAQ;YACpC,QAAQ,CAAC,WAAW,CAAC;YACrB,OAAO,WAAW,CAAC,SAAS,CAAC,KAAK,QAAQ;YAC1C,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;YAChC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAChC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAgB;QACpB,kEAAkE;QAClE,MAAM,MAAM,GAAG,OAA4B,CAAC;QAE5C,MAAM,KAAK,GAAiB,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnD,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ;YACxB,yEAAyE;YACzE,kEAAkE;YAClE,gCAAgC;YAChC,YAAY,EAAE,CAAC,CAAC,iBAAiB,IAAI,IAAI;YACzC,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,oBAAoB,EAAE,CAAC,CAAC,WAAW,EAAE,QAAQ,IAAI,IAAI;YACrD,kBAAkB,EAAE,CAAC,CAAC,kBAAkB;YACxC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;SAC9E,CAAC,CAAC,CAAC;QAEJ,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY;YACxC,oEAAoE;YACpE,kCAAkC;YAClC,gBAAgB,EAAE,MAAM,CAAC,UAAU;YACnC,yEAAyE;YACzE,wEAAwE;YACxE,oBAAoB,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;YACnE,eAAe,EACb,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;YACtF,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc;YAC5C,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc;YAC5C,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc;YAC5C,KAAK;YACL,wDAAwD;YACxD,WAAW,EAAE,KAAK;SACnB,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["/**\n * Canonical roster-json adapter: re-import a 40kdc {@link Roster} export.\n *\n * The exporter's `roster-json` format (see `export/roster-json.ts`) is the\n * lossless pivot — exactly `roster.schema.json` shape. This adapter closes the\n * loop so a 40kdc-native export round-trips through the normal\n * `tryImportRoster` pipeline: validate the canonical envelope, lower it to the\n * format-agnostic {@link ParsedRoster}, and let `resolve` re-derive ids\n * against the *current* dataset (so a stored export keeps resolving across\n * dataset releases, and stale ids self-heal through name resolution).\n *\n * Lowering notes:\n * - Unit/wargear/enhancement rows lower to their `ref.raw_name` — the same\n * raw-display-name path every other adapter takes.\n * - `faction_id` has no raw name in the canonical shape, so the id slug is\n * passed through as the raw name; collection lookup does an exact-id match\n * before any name lookup, so resolution is exact. Detachments do carry a\n * `ref.raw_name`, so (like units/enhancements) that lowers directly and\n * round-trips the display name.\n * - `is_character` isn't stored on the canonical shape (it's an inference\n * input, not an output). It lowers as `leader_attachment != null`, which\n * reproduces the original (deterministic) attachment inference on\n * re-import. Attachments are always provisional either way.\n *\n * **IP safety**: the canonical document carries only permitted facts (names,\n * counts, points, ids); no prose fields exist to read.\n *\n * TS mirror of `crates/wh40kdc/src/import/roster_json.rs`.\n */\n\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, Roster } from \"./types.js\";\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\n/** Battle-size labels `resolve` maps back to the enum. */\nconst BATTLE_SIZE_LABELS: Record<string, string> = {\n incursion: \"Incursion\",\n \"strike-force\": \"Strike Force\",\n};\n\nexport const rosterJsonAdapter: FormatAdapter = {\n id: \"roster-json\",\n\n /**\n * The canonical shape is unmistakable: a `source.format` discriminator plus\n * the `game_version` + `diagnostics` envelope no external builder emits.\n * All three are required by `roster.schema.json`.\n */\n matches(decoded: unknown): boolean {\n if (!isRecord(decoded)) return false;\n const source = decoded[\"source\"];\n const gameVersion = decoded[\"game_version\"];\n return (\n isRecord(source) &&\n typeof source[\"format\"] === \"string\" &&\n isRecord(gameVersion) &&\n typeof gameVersion[\"edition\"] === \"string\" &&\n isRecord(decoded[\"diagnostics\"]) &&\n Array.isArray(decoded[\"units\"])\n );\n },\n\n parse(decoded: unknown): ParsedRoster {\n // matches() vetted the envelope; the unit rows are schema-shaped.\n const roster = decoded as unknown as Roster;\n\n const units: ParsedUnit[] = roster.units.map((u) => ({\n raw_name: u.ref.raw_name,\n // Not stored canonically; attached units were characters, and re-running\n // the (deterministic) inference over them reproduces the exported\n // attachments. See module docs.\n is_character: u.leader_attachment != null,\n model_count: u.model_count,\n points: u.points,\n is_warlord: u.is_warlord,\n enhancement_raw_name: u.enhancement?.raw_name ?? null,\n enhancement_points: u.enhancement_points,\n wargear: u.wargear.map((w) => ({ raw_name: w.ref.raw_name, count: w.count })),\n }));\n\n return {\n name: roster.name,\n generated_by: roster.source.generated_by,\n // Id slugs pass through as raw names — collection lookup id-matches\n // exactly before any name lookup.\n faction_raw_name: roster.faction_id,\n // Each detachment carries its own raw name (like units/enhancements), so\n // lower that — it round-trips the display name and re-resolves exactly.\n detachment_raw_names: roster.detachments.map((d) => d.ref.raw_name),\n battle_size_raw:\n roster.battle_size != null ? (BATTLE_SIZE_LABELS[roster.battle_size] ?? null) : null,\n declared_limit: roster.points.declared_limit,\n total_reported: roster.points.total_reported,\n total_computed: roster.points.total_computed,\n units,\n // The canonical shape carries a single primary faction.\n multi_force: false,\n };\n },\n};\n"]}
@@ -250,7 +250,7 @@ export const rosterizerAdapter = {
250
250
  // same way. Walk the whole tree (rather than just root.assets.included)
251
251
  // so nested-force shapes still pick up the markers.
252
252
  let faction_raw_name = null;
253
- let detachment_raw_name = null;
253
+ const detachment_raw_names = [];
254
254
  let battle_size_raw = null;
255
255
  const factions = [];
256
256
  walk(root, (a) => {
@@ -262,7 +262,7 @@ export const rosterizerAdapter = {
262
262
  faction_raw_name ??= name;
263
263
  }
264
264
  else if (cls === CLS_DETACHMENT) {
265
- detachment_raw_name ??= displayName(a);
265
+ detachment_raw_names.push(displayName(a));
266
266
  }
267
267
  else if (cls === CLS_BATTLE_SIZE) {
268
268
  battle_size_raw ??= displayName(a);
@@ -318,7 +318,7 @@ export const rosterizerAdapter = {
318
318
  name,
319
319
  generated_by,
320
320
  faction_raw_name,
321
- detachment_raw_name,
321
+ detachment_raw_names,
322
322
  battle_size_raw,
323
323
  declared_limit: parseLimit(battle_size_raw),
324
324
  total_reported,
@@ -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"]}