@alpaca-software/40kdc-data 0.1.0 → 0.1.2

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 (385) hide show
  1. package/dist/abilities-resolver/index.d.ts +9 -0
  2. package/dist/abilities-resolver/index.d.ts.map +1 -0
  3. package/dist/abilities-resolver/index.js +9 -0
  4. package/dist/abilities-resolver/index.js.map +1 -0
  5. package/dist/abilities-resolver/resolver.d.ts +64 -0
  6. package/dist/abilities-resolver/resolver.d.ts.map +1 -0
  7. package/dist/abilities-resolver/resolver.js +135 -0
  8. package/dist/abilities-resolver/resolver.js.map +1 -0
  9. package/dist/bundle-schemas.d.ts +1 -0
  10. package/dist/bundle-schemas.d.ts.map +1 -0
  11. package/dist/bundle-schemas.js +12 -0
  12. package/dist/bundle-schemas.js.map +1 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +10 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/codegen-data.d.ts +1 -0
  18. package/dist/codegen-data.d.ts.map +1 -0
  19. package/dist/codegen-data.js +2 -0
  20. package/dist/codegen-data.js.map +1 -0
  21. package/dist/commands/import.d.ts +7 -0
  22. package/dist/commands/import.d.ts.map +1 -0
  23. package/dist/commands/import.js +103 -0
  24. package/dist/commands/import.js.map +1 -0
  25. package/dist/commands/translate.d.ts +1 -0
  26. package/dist/commands/translate.d.ts.map +1 -0
  27. package/dist/commands/translate.js +1 -0
  28. package/dist/commands/translate.js.map +1 -0
  29. package/dist/commands/validate-all.d.ts +1 -0
  30. package/dist/commands/validate-all.d.ts.map +1 -0
  31. package/dist/commands/validate-all.js +1 -0
  32. package/dist/commands/validate-all.js.map +1 -0
  33. package/dist/commands/validate-core.d.ts +1 -0
  34. package/dist/commands/validate-core.d.ts.map +1 -0
  35. package/dist/commands/validate-core.js +1 -0
  36. package/dist/commands/validate-core.js.map +1 -0
  37. package/dist/commands/validate-enrichment.d.ts +1 -0
  38. package/dist/commands/validate-enrichment.d.ts.map +1 -0
  39. package/dist/commands/validate-enrichment.js +1 -0
  40. package/dist/commands/validate-enrichment.js.map +1 -0
  41. package/dist/convert-faction.d.ts +1 -0
  42. package/dist/convert-faction.d.ts.map +1 -0
  43. package/dist/convert-faction.js +1 -0
  44. package/dist/convert-faction.js.map +1 -0
  45. package/dist/converters/configs/adepta-sororitas.d.ts +1 -0
  46. package/dist/converters/configs/adepta-sororitas.d.ts.map +1 -0
  47. package/dist/converters/configs/adepta-sororitas.js +1 -0
  48. package/dist/converters/configs/adepta-sororitas.js.map +1 -0
  49. package/dist/converters/configs/adeptus-astartes.d.ts +1 -0
  50. package/dist/converters/configs/adeptus-astartes.d.ts.map +1 -0
  51. package/dist/converters/configs/adeptus-astartes.js +1 -0
  52. package/dist/converters/configs/adeptus-astartes.js.map +1 -0
  53. package/dist/converters/configs/adeptus-custodes.d.ts +1 -0
  54. package/dist/converters/configs/adeptus-custodes.d.ts.map +1 -0
  55. package/dist/converters/configs/adeptus-custodes.js +1 -0
  56. package/dist/converters/configs/adeptus-custodes.js.map +1 -0
  57. package/dist/converters/configs/adeptus-mechanicus.d.ts +1 -0
  58. package/dist/converters/configs/adeptus-mechanicus.d.ts.map +1 -0
  59. package/dist/converters/configs/adeptus-mechanicus.js +1 -0
  60. package/dist/converters/configs/adeptus-mechanicus.js.map +1 -0
  61. package/dist/converters/configs/aeldari.d.ts +1 -0
  62. package/dist/converters/configs/aeldari.d.ts.map +1 -0
  63. package/dist/converters/configs/aeldari.js +1 -0
  64. package/dist/converters/configs/aeldari.js.map +1 -0
  65. package/dist/converters/configs/agents-of-the-imperium.d.ts +1 -0
  66. package/dist/converters/configs/agents-of-the-imperium.d.ts.map +1 -0
  67. package/dist/converters/configs/agents-of-the-imperium.js +1 -0
  68. package/dist/converters/configs/agents-of-the-imperium.js.map +1 -0
  69. package/dist/converters/configs/astra-militarum.d.ts +1 -0
  70. package/dist/converters/configs/astra-militarum.d.ts.map +1 -0
  71. package/dist/converters/configs/astra-militarum.js +1 -0
  72. package/dist/converters/configs/astra-militarum.js.map +1 -0
  73. package/dist/converters/configs/black-templars.d.ts +1 -0
  74. package/dist/converters/configs/black-templars.d.ts.map +1 -0
  75. package/dist/converters/configs/black-templars.js +1 -0
  76. package/dist/converters/configs/black-templars.js.map +1 -0
  77. package/dist/converters/configs/blood-angels.d.ts +1 -0
  78. package/dist/converters/configs/blood-angels.d.ts.map +1 -0
  79. package/dist/converters/configs/blood-angels.js +1 -0
  80. package/dist/converters/configs/blood-angels.js.map +1 -0
  81. package/dist/converters/configs/chaos-daemons.d.ts +1 -0
  82. package/dist/converters/configs/chaos-daemons.d.ts.map +1 -0
  83. package/dist/converters/configs/chaos-daemons.js +1 -0
  84. package/dist/converters/configs/chaos-daemons.js.map +1 -0
  85. package/dist/converters/configs/chaos-knights.d.ts +1 -0
  86. package/dist/converters/configs/chaos-knights.d.ts.map +1 -0
  87. package/dist/converters/configs/chaos-knights.js +1 -0
  88. package/dist/converters/configs/chaos-knights.js.map +1 -0
  89. package/dist/converters/configs/chaos-space-marines.d.ts +1 -0
  90. package/dist/converters/configs/chaos-space-marines.d.ts.map +1 -0
  91. package/dist/converters/configs/chaos-space-marines.js +1 -0
  92. package/dist/converters/configs/chaos-space-marines.js.map +1 -0
  93. package/dist/converters/configs/crimson-fists.d.ts +1 -0
  94. package/dist/converters/configs/crimson-fists.d.ts.map +1 -0
  95. package/dist/converters/configs/crimson-fists.js +1 -0
  96. package/dist/converters/configs/crimson-fists.js.map +1 -0
  97. package/dist/converters/configs/dark-angels.d.ts +1 -0
  98. package/dist/converters/configs/dark-angels.d.ts.map +1 -0
  99. package/dist/converters/configs/dark-angels.js +1 -0
  100. package/dist/converters/configs/dark-angels.js.map +1 -0
  101. package/dist/converters/configs/death-guard.d.ts +1 -0
  102. package/dist/converters/configs/death-guard.d.ts.map +1 -0
  103. package/dist/converters/configs/death-guard.js +1 -0
  104. package/dist/converters/configs/death-guard.js.map +1 -0
  105. package/dist/converters/configs/deathwatch.d.ts +1 -0
  106. package/dist/converters/configs/deathwatch.d.ts.map +1 -0
  107. package/dist/converters/configs/deathwatch.js +1 -0
  108. package/dist/converters/configs/deathwatch.js.map +1 -0
  109. package/dist/converters/configs/drukhari.d.ts +1 -0
  110. package/dist/converters/configs/drukhari.d.ts.map +1 -0
  111. package/dist/converters/configs/drukhari.js +1 -0
  112. package/dist/converters/configs/drukhari.js.map +1 -0
  113. package/dist/converters/configs/emperors-children.d.ts +1 -0
  114. package/dist/converters/configs/emperors-children.d.ts.map +1 -0
  115. package/dist/converters/configs/emperors-children.js +1 -0
  116. package/dist/converters/configs/emperors-children.js.map +1 -0
  117. package/dist/converters/configs/genestealer-cults.d.ts +1 -0
  118. package/dist/converters/configs/genestealer-cults.d.ts.map +1 -0
  119. package/dist/converters/configs/genestealer-cults.js +1 -0
  120. package/dist/converters/configs/genestealer-cults.js.map +1 -0
  121. package/dist/converters/configs/grey-knights.d.ts +1 -0
  122. package/dist/converters/configs/grey-knights.d.ts.map +1 -0
  123. package/dist/converters/configs/grey-knights.js +1 -0
  124. package/dist/converters/configs/grey-knights.js.map +1 -0
  125. package/dist/converters/configs/imperial-fists.d.ts +1 -0
  126. package/dist/converters/configs/imperial-fists.d.ts.map +1 -0
  127. package/dist/converters/configs/imperial-fists.js +1 -0
  128. package/dist/converters/configs/imperial-fists.js.map +1 -0
  129. package/dist/converters/configs/imperial-knights.d.ts +1 -0
  130. package/dist/converters/configs/imperial-knights.d.ts.map +1 -0
  131. package/dist/converters/configs/imperial-knights.js +1 -0
  132. package/dist/converters/configs/imperial-knights.js.map +1 -0
  133. package/dist/converters/configs/iron-hands.d.ts +1 -0
  134. package/dist/converters/configs/iron-hands.d.ts.map +1 -0
  135. package/dist/converters/configs/iron-hands.js +1 -0
  136. package/dist/converters/configs/iron-hands.js.map +1 -0
  137. package/dist/converters/configs/leagues-of-votann.d.ts +1 -0
  138. package/dist/converters/configs/leagues-of-votann.d.ts.map +1 -0
  139. package/dist/converters/configs/leagues-of-votann.js +1 -0
  140. package/dist/converters/configs/leagues-of-votann.js.map +1 -0
  141. package/dist/converters/configs/necrons.d.ts +1 -0
  142. package/dist/converters/configs/necrons.d.ts.map +1 -0
  143. package/dist/converters/configs/necrons.js +1 -0
  144. package/dist/converters/configs/necrons.js.map +1 -0
  145. package/dist/converters/configs/orks.d.ts +1 -0
  146. package/dist/converters/configs/orks.d.ts.map +1 -0
  147. package/dist/converters/configs/orks.js +1 -0
  148. package/dist/converters/configs/orks.js.map +1 -0
  149. package/dist/converters/configs/raven-guard.d.ts +1 -0
  150. package/dist/converters/configs/raven-guard.d.ts.map +1 -0
  151. package/dist/converters/configs/raven-guard.js +1 -0
  152. package/dist/converters/configs/raven-guard.js.map +1 -0
  153. package/dist/converters/configs/salamanders.d.ts +1 -0
  154. package/dist/converters/configs/salamanders.d.ts.map +1 -0
  155. package/dist/converters/configs/salamanders.js +1 -0
  156. package/dist/converters/configs/salamanders.js.map +1 -0
  157. package/dist/converters/configs/space-wolves.d.ts +1 -0
  158. package/dist/converters/configs/space-wolves.d.ts.map +1 -0
  159. package/dist/converters/configs/space-wolves.js +1 -0
  160. package/dist/converters/configs/space-wolves.js.map +1 -0
  161. package/dist/converters/configs/tau-empire.d.ts +1 -0
  162. package/dist/converters/configs/tau-empire.d.ts.map +1 -0
  163. package/dist/converters/configs/tau-empire.js +1 -0
  164. package/dist/converters/configs/tau-empire.js.map +1 -0
  165. package/dist/converters/configs/thousand-sons.d.ts +1 -0
  166. package/dist/converters/configs/thousand-sons.d.ts.map +1 -0
  167. package/dist/converters/configs/thousand-sons.js +1 -0
  168. package/dist/converters/configs/thousand-sons.js.map +1 -0
  169. package/dist/converters/configs/tyranids.d.ts +1 -0
  170. package/dist/converters/configs/tyranids.d.ts.map +1 -0
  171. package/dist/converters/configs/tyranids.js +1 -0
  172. package/dist/converters/configs/tyranids.js.map +1 -0
  173. package/dist/converters/configs/ultramarines.d.ts +1 -0
  174. package/dist/converters/configs/ultramarines.d.ts.map +1 -0
  175. package/dist/converters/configs/ultramarines.js +1 -0
  176. package/dist/converters/configs/ultramarines.js.map +1 -0
  177. package/dist/converters/configs/white-scars.d.ts +1 -0
  178. package/dist/converters/configs/white-scars.d.ts.map +1 -0
  179. package/dist/converters/configs/white-scars.js +1 -0
  180. package/dist/converters/configs/white-scars.js.map +1 -0
  181. package/dist/converters/configs/world-eaters.d.ts +1 -0
  182. package/dist/converters/configs/world-eaters.d.ts.map +1 -0
  183. package/dist/converters/configs/world-eaters.js +1 -0
  184. package/dist/converters/configs/world-eaters.js.map +1 -0
  185. package/dist/converters/faction-config.d.ts +1 -0
  186. package/dist/converters/faction-config.d.ts.map +1 -0
  187. package/dist/converters/faction-config.js +1 -0
  188. package/dist/converters/faction-config.js.map +1 -0
  189. package/dist/converters/id-generator.d.ts +1 -0
  190. package/dist/converters/id-generator.d.ts.map +1 -0
  191. package/dist/converters/id-generator.js +1 -0
  192. package/dist/converters/id-generator.js.map +1 -0
  193. package/dist/converters/keyword-filter.d.ts +1 -0
  194. package/dist/converters/keyword-filter.d.ts.map +1 -0
  195. package/dist/converters/keyword-filter.js +1 -0
  196. package/dist/converters/keyword-filter.js.map +1 -0
  197. package/dist/converters/stat-parser.d.ts +1 -0
  198. package/dist/converters/stat-parser.d.ts.map +1 -0
  199. package/dist/converters/stat-parser.js +1 -0
  200. package/dist/converters/stat-parser.js.map +1 -0
  201. package/dist/converters/view-selector.d.ts +1 -0
  202. package/dist/converters/view-selector.d.ts.map +1 -0
  203. package/dist/converters/view-selector.js +1 -0
  204. package/dist/converters/view-selector.js.map +1 -0
  205. package/dist/converters/weapon-dedup.d.ts +1 -0
  206. package/dist/converters/weapon-dedup.d.ts.map +1 -0
  207. package/dist/converters/weapon-dedup.js +1 -0
  208. package/dist/converters/weapon-dedup.js.map +1 -0
  209. package/dist/cruncher/buffs.d.ts +184 -0
  210. package/dist/cruncher/buffs.d.ts.map +1 -0
  211. package/dist/cruncher/buffs.js +150 -0
  212. package/dist/cruncher/buffs.js.map +1 -0
  213. package/dist/cruncher/engine.d.ts +50 -0
  214. package/dist/cruncher/engine.d.ts.map +1 -0
  215. package/dist/cruncher/engine.js +312 -0
  216. package/dist/cruncher/engine.js.map +1 -0
  217. package/dist/cruncher/from-dsl.d.ts +69 -0
  218. package/dist/cruncher/from-dsl.d.ts.map +1 -0
  219. package/dist/cruncher/from-dsl.js +523 -0
  220. package/dist/cruncher/from-dsl.js.map +1 -0
  221. package/dist/cruncher/from-keyword.d.ts +35 -0
  222. package/dist/cruncher/from-keyword.d.ts.map +1 -0
  223. package/dist/cruncher/from-keyword.js +159 -0
  224. package/dist/cruncher/from-keyword.js.map +1 -0
  225. package/dist/cruncher/get-buffs.d.ts +12 -0
  226. package/dist/cruncher/get-buffs.d.ts.map +1 -0
  227. package/dist/cruncher/get-buffs.js +7 -0
  228. package/dist/cruncher/get-buffs.js.map +1 -0
  229. package/dist/cruncher/index.d.ts +11 -0
  230. package/dist/cruncher/index.d.ts.map +1 -0
  231. package/dist/cruncher/index.js +11 -0
  232. package/dist/cruncher/index.js.map +1 -0
  233. package/dist/data/bundle.generated.d.ts +1 -0
  234. package/dist/data/bundle.generated.d.ts.map +1 -0
  235. package/dist/data/bundle.generated.js +2 -1
  236. package/dist/data/bundle.generated.js.map +1 -0
  237. package/dist/data/collection.d.ts +1 -0
  238. package/dist/data/collection.d.ts.map +1 -0
  239. package/dist/data/collection.js +1 -0
  240. package/dist/data/collection.js.map +1 -0
  241. package/dist/data/dataset.d.ts +54 -2
  242. package/dist/data/dataset.d.ts.map +1 -0
  243. package/dist/data/dataset.js +111 -1
  244. package/dist/data/dataset.js.map +1 -0
  245. package/dist/data/entities.d.ts +70 -2
  246. package/dist/data/entities.d.ts.map +1 -0
  247. package/dist/data/entities.js +122 -0
  248. package/dist/data/entities.js.map +1 -0
  249. package/dist/data/index.d.ts +9 -1
  250. package/dist/data/index.d.ts.map +1 -0
  251. package/dist/data/index.js +14 -1
  252. package/dist/data/index.js.map +1 -0
  253. package/dist/data/normalize.d.ts +1 -0
  254. package/dist/data/normalize.d.ts.map +1 -0
  255. package/dist/data/normalize.js +1 -0
  256. package/dist/data/normalize.js.map +1 -0
  257. package/dist/data/roster-resolve.d.ts +33 -0
  258. package/dist/data/roster-resolve.d.ts.map +1 -0
  259. package/dist/data/roster-resolve.js +36 -0
  260. package/dist/data/roster-resolve.js.map +1 -0
  261. package/dist/data/types.d.ts +4 -1
  262. package/dist/data/types.d.ts.map +1 -0
  263. package/dist/data/types.js +2 -0
  264. package/dist/data/types.js.map +1 -0
  265. package/dist/export/helpers.d.ts +33 -0
  266. package/dist/export/helpers.d.ts.map +1 -0
  267. package/dist/export/helpers.js +57 -0
  268. package/dist/export/helpers.js.map +1 -0
  269. package/dist/export/index.d.ts +21 -0
  270. package/dist/export/index.d.ts.map +1 -0
  271. package/dist/export/index.js +25 -0
  272. package/dist/export/index.js.map +1 -0
  273. package/dist/export/newrecruit-json.d.ts +3 -0
  274. package/dist/export/newrecruit-json.d.ts.map +1 -0
  275. package/dist/export/newrecruit-json.js +140 -0
  276. package/dist/export/newrecruit-json.js.map +1 -0
  277. package/dist/export/newrecruit-simple.d.ts +3 -0
  278. package/dist/export/newrecruit-simple.d.ts.map +1 -0
  279. package/dist/export/newrecruit-simple.js +76 -0
  280. package/dist/export/newrecruit-simple.js.map +1 -0
  281. package/dist/export/newrecruit-wtc.d.ts +4 -0
  282. package/dist/export/newrecruit-wtc.d.ts.map +1 -0
  283. package/dist/export/newrecruit-wtc.js +142 -0
  284. package/dist/export/newrecruit-wtc.js.map +1 -0
  285. package/dist/export/roster-json.d.ts +3 -0
  286. package/dist/export/roster-json.d.ts.map +1 -0
  287. package/dist/export/roster-json.js +8 -0
  288. package/dist/export/roster-json.js.map +1 -0
  289. package/dist/export/serializer.d.ts +27 -0
  290. package/dist/export/serializer.d.ts.map +1 -0
  291. package/dist/export/serializer.js +2 -0
  292. package/dist/export/serializer.js.map +1 -0
  293. package/dist/gen-conformance.d.ts +2 -0
  294. package/dist/gen-conformance.d.ts.map +1 -0
  295. package/dist/gen-conformance.js +131 -0
  296. package/dist/gen-conformance.js.map +1 -0
  297. package/dist/generated.d.ts +194 -118
  298. package/dist/generated.d.ts.map +1 -0
  299. package/dist/generated.js +1 -0
  300. package/dist/generated.js.map +1 -0
  301. package/dist/import/adapter.d.ts +27 -0
  302. package/dist/import/adapter.d.ts.map +1 -0
  303. package/dist/import/adapter.js +10 -0
  304. package/dist/import/adapter.js.map +1 -0
  305. package/dist/import/decode.d.ts +7 -0
  306. package/dist/import/decode.d.ts.map +1 -0
  307. package/dist/import/decode.js +73 -0
  308. package/dist/import/decode.js.map +1 -0
  309. package/dist/import/import-roster.d.ts +35 -0
  310. package/dist/import/import-roster.d.ts.map +1 -0
  311. package/dist/import/import-roster.js +97 -0
  312. package/dist/import/import-roster.js.map +1 -0
  313. package/dist/import/index.d.ts +22 -0
  314. package/dist/import/index.d.ts.map +1 -0
  315. package/dist/import/index.js +19 -0
  316. package/dist/import/index.js.map +1 -0
  317. package/dist/import/listforge.d.ts +24 -0
  318. package/dist/import/listforge.d.ts.map +1 -0
  319. package/dist/import/listforge.js +201 -0
  320. package/dist/import/listforge.js.map +1 -0
  321. package/dist/import/newrecruit-json.d.ts +31 -0
  322. package/dist/import/newrecruit-json.d.ts.map +1 -0
  323. package/dist/import/newrecruit-json.js +224 -0
  324. package/dist/import/newrecruit-json.js.map +1 -0
  325. package/dist/import/newrecruit-simple.d.ts +29 -0
  326. package/dist/import/newrecruit-simple.d.ts.map +1 -0
  327. package/dist/import/newrecruit-simple.js +200 -0
  328. package/dist/import/newrecruit-simple.js.map +1 -0
  329. package/dist/import/newrecruit-text.d.ts +48 -0
  330. package/dist/import/newrecruit-text.d.ts.map +1 -0
  331. package/dist/import/newrecruit-text.js +96 -0
  332. package/dist/import/newrecruit-text.js.map +1 -0
  333. package/dist/import/newrecruit-wtc.d.ts +36 -0
  334. package/dist/import/newrecruit-wtc.d.ts.map +1 -0
  335. package/dist/import/newrecruit-wtc.js +334 -0
  336. package/dist/import/newrecruit-wtc.js.map +1 -0
  337. package/dist/import/resolve.d.ts +20 -0
  338. package/dist/import/resolve.d.ts.map +1 -0
  339. package/dist/import/resolve.js +190 -0
  340. package/dist/import/resolve.js.map +1 -0
  341. package/dist/import/types.d.ts +153 -0
  342. package/dist/import/types.d.ts.map +1 -0
  343. package/dist/import/types.js +20 -0
  344. package/dist/import/types.js.map +1 -0
  345. package/dist/index.d.ts +6 -0
  346. package/dist/index.d.ts.map +1 -0
  347. package/dist/index.js +7 -0
  348. package/dist/index.js.map +1 -0
  349. package/dist/known-support-10e.d.ts +1 -0
  350. package/dist/known-support-10e.d.ts.map +1 -0
  351. package/dist/known-support-10e.js +1 -0
  352. package/dist/known-support-10e.js.map +1 -0
  353. package/dist/link-abilities.d.ts +41 -0
  354. package/dist/link-abilities.d.ts.map +1 -0
  355. package/dist/link-abilities.js +159 -0
  356. package/dist/link-abilities.js.map +1 -0
  357. package/dist/migrations/2026-weapon-keywords.d.ts +2 -0
  358. package/dist/migrations/2026-weapon-keywords.d.ts.map +1 -0
  359. package/dist/migrations/2026-weapon-keywords.js +243 -0
  360. package/dist/migrations/2026-weapon-keywords.js.map +1 -0
  361. package/dist/port-10e-faction.d.ts +1 -0
  362. package/dist/port-10e-faction.d.ts.map +1 -0
  363. package/dist/port-10e-faction.js +1 -0
  364. package/dist/port-10e-faction.js.map +1 -0
  365. package/dist/report.d.ts +1 -0
  366. package/dist/report.d.ts.map +1 -0
  367. package/dist/report.js +1 -0
  368. package/dist/report.js.map +1 -0
  369. package/dist/rube-goldberg.d.ts +3 -0
  370. package/dist/rube-goldberg.d.ts.map +1 -0
  371. package/dist/rube-goldberg.js +109 -0
  372. package/dist/rube-goldberg.js.map +1 -0
  373. package/dist/schema-loader.d.ts +1 -0
  374. package/dist/schema-loader.d.ts.map +1 -0
  375. package/dist/schema-loader.js +1 -0
  376. package/dist/schema-loader.js.map +1 -0
  377. package/dist/validate.d.ts +1 -0
  378. package/dist/validate.d.ts.map +1 -0
  379. package/dist/validate.js +2 -0
  380. package/dist/validate.js.map +1 -0
  381. package/package.json +8 -2
  382. package/schemas/core/roster.schema.json +17 -4
  383. package/schemas/core/weapon-keyword.schema.json +31 -0
  384. package/schemas/core/weapon.schema.json +22 -1
  385. package/schemas/enrichment/ability-dsl/effect.schema.json +23 -1
@@ -0,0 +1,190 @@
1
+ import { normalizeName } from "../data/normalize.js";
2
+ /** The dataset edition/dataslate stamped onto an imported roster. */
3
+ const ROSTER_GAME_VERSION = { edition: "11th", dataslate: "pre-launch-provisional" };
4
+ const MAX_CANDIDATES = 5;
5
+ /** Accumulates warnings and resolved/unresolved tallies during an import. */
6
+ class DiagnosticsBuilder {
7
+ resolved_units = 0;
8
+ unresolved_units = 0;
9
+ resolved_weapons = 0;
10
+ unresolved_weapons = 0;
11
+ warnings = [];
12
+ warn(code, message, raw_name = null) {
13
+ this.warnings.push({ code, message, raw_name });
14
+ }
15
+ build() {
16
+ return {
17
+ resolved_units: this.resolved_units,
18
+ unresolved_units: this.unresolved_units,
19
+ resolved_weapons: this.resolved_weapons,
20
+ unresolved_weapons: this.unresolved_weapons,
21
+ warnings: this.warnings,
22
+ };
23
+ }
24
+ }
25
+ function unresolved(raw_name, candidates = []) {
26
+ return { id: null, raw_name, resolved: false, candidates };
27
+ }
28
+ function resolved(id, raw_name) {
29
+ return { id, raw_name, resolved: true, candidates: [] };
30
+ }
31
+ function toCandidates(records) {
32
+ return records.slice(0, MAX_CANDIDATES).map((r) => ({ id: r.id, name: r.name }));
33
+ }
34
+ /** Map a source battle-size label to the 40kdc enum, if recognisable. */
35
+ function mapBattleSize(raw) {
36
+ if (!raw)
37
+ return null;
38
+ const key = normalizeName(raw);
39
+ if (key.includes("strike force"))
40
+ return "strike-force";
41
+ if (key.includes("incursion"))
42
+ return "incursion";
43
+ return null;
44
+ }
45
+ export function resolve(parsed, ds, format = "listforge") {
46
+ const diag = new DiagnosticsBuilder();
47
+ if (parsed.multi_force) {
48
+ diag.warn("multi-force", "Source list contains more than one faction; the primary faction was used for scoping.");
49
+ }
50
+ // --- Faction (resolved first so other lookups can scope to it). -----------
51
+ let faction_id = null;
52
+ if (parsed.faction_raw_name) {
53
+ const hit = ds.factions.find(parsed.faction_raw_name);
54
+ if (hit) {
55
+ faction_id = hit.id;
56
+ }
57
+ else {
58
+ diag.warn("faction-unresolved", "Faction name did not match any 40kdc faction.", parsed.faction_raw_name);
59
+ }
60
+ }
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);
65
+ const scoped = faction_id
66
+ ? ds.detachments.byFaction(faction_id).find((d) => normalizeName(d.name ?? "") === key)
67
+ : undefined;
68
+ const hit = scoped ?? ds.detachments.find(parsed.detachment_raw_name);
69
+ if (hit) {
70
+ detachment_id = hit.id;
71
+ }
72
+ else {
73
+ diag.warn("detachment-unresolved", "Detachment name did not match any 40kdc detachment.", parsed.detachment_raw_name);
74
+ }
75
+ }
76
+ // --- Battle size. ---------------------------------------------------------
77
+ const battle_size = mapBattleSize(parsed.battle_size_raw);
78
+ if (parsed.battle_size_raw && battle_size === null) {
79
+ diag.warn("battle-size-unmapped", "Battle size label could not be mapped.", parsed.battle_size_raw);
80
+ }
81
+ // --- Units (and their enhancements / wargear). ----------------------------
82
+ const units = parsed.units.map((u) => resolveUnit(u, faction_id, detachment_id, ds, diag));
83
+ // --- Leader attachments (second pass: needs all resolved unit ids). -------
84
+ inferLeaderAttachments(parsed.units, units, ds, diag);
85
+ // --- Points reconciliation (reported vs computed kept distinct). ----------
86
+ if (parsed.total_reported !== null && parsed.total_reported !== parsed.total_computed) {
87
+ diag.warn("points-mismatch", `Source-reported total (${parsed.total_reported}) differs from the sum of cost lines (${parsed.total_computed}).`);
88
+ }
89
+ return {
90
+ name: parsed.name,
91
+ source: { format, generated_by: parsed.generated_by },
92
+ faction_id,
93
+ detachment_id,
94
+ battle_size,
95
+ points: {
96
+ declared_limit: parsed.declared_limit,
97
+ total_reported: parsed.total_reported,
98
+ total_computed: parsed.total_computed,
99
+ },
100
+ units,
101
+ game_version: { ...ROSTER_GAME_VERSION },
102
+ diagnostics: diag.build(),
103
+ };
104
+ }
105
+ function resolveUnit(parsed, faction_id, detachment_id, ds, diag) {
106
+ // Prefer a faction-scoped match (the same unit id recurs across factions),
107
+ // then fall back to a global name lookup.
108
+ const key = normalizeName(parsed.raw_name);
109
+ const scoped = faction_id
110
+ ? ds.units.byFaction(faction_id).find((u) => normalizeName(u.name) === key)
111
+ : undefined;
112
+ const all = ds.units.findAll(parsed.raw_name);
113
+ const hit = scoped ?? all[0];
114
+ let ref;
115
+ if (hit) {
116
+ ref = resolved(hit.id, parsed.raw_name);
117
+ diag.resolved_units += 1;
118
+ }
119
+ else {
120
+ ref = unresolved(parsed.raw_name, toCandidates(all));
121
+ diag.unresolved_units += 1;
122
+ diag.warn("unit-unresolved", "Unit name did not match any 40kdc unit.", parsed.raw_name);
123
+ }
124
+ const enhancement = parsed.enhancement_raw_name
125
+ ? resolveEnhancement(parsed.enhancement_raw_name, detachment_id, ds, diag)
126
+ : null;
127
+ const enhancement_points = enhancement === null ? null : parsed.enhancement_points;
128
+ const wargear = parsed.wargear.map((w) => {
129
+ const hits = ds.weapons.findAll(w.raw_name);
130
+ if (hits[0]) {
131
+ diag.resolved_weapons += 1;
132
+ return { ref: resolved(hits[0].id, w.raw_name), count: w.count };
133
+ }
134
+ diag.unresolved_weapons += 1;
135
+ diag.warn("weapon-unresolved", "Weapon name did not match any 40kdc weapon.", w.raw_name);
136
+ return { ref: unresolved(w.raw_name, toCandidates(hits)), count: w.count };
137
+ });
138
+ return {
139
+ ref,
140
+ model_count: parsed.model_count,
141
+ points: parsed.points,
142
+ is_warlord: parsed.is_warlord,
143
+ enhancement,
144
+ enhancement_points,
145
+ wargear,
146
+ leader_attachment: null,
147
+ };
148
+ }
149
+ function resolveEnhancement(raw_name, detachment_id, ds, diag) {
150
+ 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)
154
+ : undefined;
155
+ const hit = scoped ?? ds.enhancements.find(raw_name);
156
+ if (hit) {
157
+ return resolved(hit.id, raw_name);
158
+ }
159
+ diag.warn("enhancement-unresolved", "Enhancement name did not match any 40kdc enhancement.", raw_name);
160
+ return unresolved(raw_name, toCandidates(ds.enhancements.findAll(raw_name)));
161
+ }
162
+ /**
163
+ * Infer leader→bodyguard attachments. The source format does not encode an
164
+ * unambiguous attachment, so each inferred link is marked provisional: we match
165
+ * a resolved character unit against a resolved non-character unit in the same
166
+ * roster using the dataset's leader-attachment data.
167
+ */
168
+ function inferLeaderAttachments(parsedUnits, units, ds, diag) {
169
+ const bodyguardIds = new Set(units.filter((u, i) => u.ref.id && !parsedUnits[i].is_character).map((u) => u.ref.id));
170
+ units.forEach((unit, i) => {
171
+ if (!unit.ref.id || !parsedUnits[i].is_character)
172
+ return;
173
+ const leaderId = unit.ref.id;
174
+ const attachment = ds.leaderAttachments.find((la) => la.leader_id === leaderId);
175
+ if (!attachment)
176
+ return;
177
+ const bodyguardId = attachment.eligible_bodyguard_ids.find((id) => bodyguardIds.has(id));
178
+ if (!bodyguardId)
179
+ return;
180
+ const bodyguard = units.find((u) => u.ref.id === bodyguardId);
181
+ if (!bodyguard)
182
+ return;
183
+ unit.leader_attachment = {
184
+ bodyguard_ref: resolved(bodyguardId, bodyguard.ref.raw_name),
185
+ provisional: true,
186
+ };
187
+ diag.warn("leader-attachment-inferred", "Leader attachment was inferred from leader-attachment data and is provisional.", unit.ref.raw_name);
188
+ });
189
+ }
190
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
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 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"]}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Types for the army-list importer.
3
+ *
4
+ * Two layers live here:
5
+ * - The **output** types ({@link Roster} and friends) mirror
6
+ * `schemas/core/roster.schema.json` field-for-field. They are hand-authored
7
+ * rather than generated so importer work isn't gated on the Rust→typify codegen
8
+ * round-trip; the AJV validator (against the real schema) is the source of truth
9
+ * for conformance.
10
+ * - The **intermediate** type ({@link ParsedRoster}) is format-agnostic: a parser
11
+ * adapter lowers a source payload to this shape (raw names + counts only, no
12
+ * resolved ids), and {@link resolve} turns it into a {@link Roster}.
13
+ *
14
+ * Nothing here ever carries reproduced rules or ability text — only permitted
15
+ * facts (names, counts, points, keywords, entity ids).
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ /** A 40kdc battle size (mirrors the shared `battle-size` def). */
20
+ export type BattleSize = "incursion" | "strike-force";
21
+ /** Diagnostic warning codes emitted during an import. */
22
+ export type WarningCode = "faction-unresolved" | "unit-unresolved" | "weapon-unresolved" | "enhancement-unresolved" | "detachment-unresolved" | "battle-size-unmapped" | "points-mismatch" | "leader-attachment-inferred" | "multi-force" | "unknown-field";
23
+ /** A near-match suggestion offered when resolution fails. */
24
+ export interface Candidate {
25
+ id: string;
26
+ name: string;
27
+ }
28
+ /**
29
+ * A reference to a 40kdc entity that may or may not have resolved. Retains the
30
+ * source's raw name so the import is lossless even on a miss.
31
+ */
32
+ export interface ResolvedRef {
33
+ /** Resolved entity id, or null when no match was found. */
34
+ id: string | null;
35
+ /** The display name exactly as it appeared in the source payload. */
36
+ raw_name: string;
37
+ /** True iff {@link id} is non-null. */
38
+ resolved: boolean;
39
+ /** Up to 5 best-guess alternatives when resolution failed. */
40
+ candidates: Candidate[];
41
+ }
42
+ /** A weapon/wargear selection on a unit. */
43
+ export interface RosterWargear {
44
+ ref: ResolvedRef;
45
+ count: number;
46
+ }
47
+ /** An inferred, always-provisional leader→bodyguard attachment. */
48
+ export interface RosterLeaderAttachment {
49
+ bodyguard_ref: ResolvedRef;
50
+ provisional: boolean;
51
+ }
52
+ /** One unit entry in a roster. */
53
+ export interface RosterUnit {
54
+ ref: ResolvedRef;
55
+ model_count: number;
56
+ /** Base unit cost (without the enhancement). */
57
+ points: number | null;
58
+ is_warlord: boolean;
59
+ enhancement: ResolvedRef | null;
60
+ /** Points cost of the enhancement when the source reported one; null otherwise. */
61
+ enhancement_points: number | null;
62
+ wargear: RosterWargear[];
63
+ leader_attachment: RosterLeaderAttachment | null;
64
+ }
65
+ /** Identifier for the adapter that produced this roster. New format adapters
66
+ * extend this union; `roster.schema.json` keeps the canonical enum. */
67
+ export type RosterFormat = "listforge" | "newrecruit-json" | "newrecruit-wtc-compact" | "newrecruit-wtc-full" | "newrecruit-simple";
68
+ /** Provenance of the imported list. */
69
+ export interface RosterSource {
70
+ format: RosterFormat;
71
+ generated_by: string | null;
72
+ }
73
+ /** Point totals; reported and computed are kept distinct, never reconciled. */
74
+ export interface RosterPoints {
75
+ declared_limit: number | null;
76
+ total_reported: number | null;
77
+ total_computed: number;
78
+ }
79
+ /** A single diagnostic warning. */
80
+ export interface Warning {
81
+ code: WarningCode;
82
+ message: string;
83
+ raw_name: string | null;
84
+ }
85
+ /** A summary of what resolved and what did not during the import. */
86
+ export interface Diagnostics {
87
+ resolved_units: number;
88
+ unresolved_units: number;
89
+ resolved_weapons: number;
90
+ unresolved_weapons: number;
91
+ warnings: Warning[];
92
+ }
93
+ /** Reference to the game edition + dataslate (mirrors game-version-ref). */
94
+ export interface GameVersionRef {
95
+ edition: string;
96
+ dataslate: string;
97
+ }
98
+ /** A fully-resolved army list. Validates against `roster.schema.json`. */
99
+ export interface Roster {
100
+ name: string;
101
+ source: RosterSource;
102
+ faction_id: string | null;
103
+ detachment_id: string | null;
104
+ battle_size: BattleSize | null;
105
+ points: RosterPoints;
106
+ units: RosterUnit[];
107
+ game_version: GameVersionRef;
108
+ diagnostics: Diagnostics;
109
+ }
110
+ /** A weapon/wargear selection before id resolution. */
111
+ export interface ParsedWargear {
112
+ raw_name: string;
113
+ count: number;
114
+ }
115
+ /** A unit selection before id resolution. */
116
+ export interface ParsedUnit {
117
+ raw_name: string;
118
+ /** True when the source classifies this as a character/leader-capable model. */
119
+ is_character: boolean;
120
+ model_count: number;
121
+ /** Base unit cost (without the enhancement). */
122
+ points: number | null;
123
+ is_warlord: boolean;
124
+ enhancement_raw_name: string | null;
125
+ /** Points cost of the enhancement when the source reported one; null otherwise. */
126
+ enhancement_points: number | null;
127
+ wargear: ParsedWargear[];
128
+ }
129
+ /**
130
+ * The format-agnostic intermediate. A {@link FormatAdapter} produces this from a
131
+ * decoded source payload; {@link resolve} consumes it. Contains only raw display
132
+ * names and counts — never reproduced rules text.
133
+ */
134
+ export interface ParsedRoster {
135
+ name: string;
136
+ generated_by: string | null;
137
+ /** Raw faction name from the source (e.g. "Grey Knights"). */
138
+ faction_raw_name: string | null;
139
+ /** Raw detachment name (e.g. "Banishers"). */
140
+ detachment_raw_name: string | null;
141
+ /** Raw battle-size label (e.g. "2. Strike Force (2000 Point limit)"). */
142
+ battle_size_raw: string | null;
143
+ /** Points limit parsed from the battle-size label, if any. */
144
+ declared_limit: number | null;
145
+ /** Total points reported by the source cost block. */
146
+ total_reported: number | null;
147
+ /** Points summed from every cost line in the source tree. */
148
+ total_computed: number;
149
+ units: ParsedUnit[];
150
+ /** True when the source contained more than one distinct faction. */
151
+ multi_force: boolean;
152
+ }
153
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Types for the army-list importer.
3
+ *
4
+ * Two layers live here:
5
+ * - The **output** types ({@link Roster} and friends) mirror
6
+ * `schemas/core/roster.schema.json` field-for-field. They are hand-authored
7
+ * rather than generated so importer work isn't gated on the Rust→typify codegen
8
+ * round-trip; the AJV validator (against the real schema) is the source of truth
9
+ * for conformance.
10
+ * - The **intermediate** type ({@link ParsedRoster}) is format-agnostic: a parser
11
+ * adapter lowers a source payload to this shape (raw names + counts only, no
12
+ * resolved ids), and {@link resolve} turns it into a {@link Roster}.
13
+ *
14
+ * Nothing here ever carries reproduced rules or ability text — only permitted
15
+ * facts (names, counts, points, keywords, entity ids).
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG","sourcesContent":["/**\n * Types for the army-list importer.\n *\n * Two layers live here:\n * - The **output** types ({@link Roster} and friends) mirror\n * `schemas/core/roster.schema.json` field-for-field. They are hand-authored\n * rather than generated so importer work isn't gated on the Rust→typify codegen\n * round-trip; the AJV validator (against the real schema) is the source of truth\n * for conformance.\n * - The **intermediate** type ({@link ParsedRoster}) is format-agnostic: a parser\n * adapter lowers a source payload to this shape (raw names + counts only, no\n * resolved ids), and {@link resolve} turns it into a {@link Roster}.\n *\n * Nothing here ever carries reproduced rules or ability text — only permitted\n * facts (names, counts, points, keywords, entity ids).\n *\n * @packageDocumentation\n */\n\n/** A 40kdc battle size (mirrors the shared `battle-size` def). */\nexport type BattleSize = \"incursion\" | \"strike-force\";\n\n/** Diagnostic warning codes emitted during an import. */\nexport type WarningCode =\n | \"faction-unresolved\"\n | \"unit-unresolved\"\n | \"weapon-unresolved\"\n | \"enhancement-unresolved\"\n | \"detachment-unresolved\"\n | \"battle-size-unmapped\"\n | \"points-mismatch\"\n | \"leader-attachment-inferred\"\n | \"multi-force\"\n | \"unknown-field\";\n\n// ---------------------------------------------------------------------------\n// Output types (mirror roster.schema.json)\n// ---------------------------------------------------------------------------\n\n/** A near-match suggestion offered when resolution fails. */\nexport interface Candidate {\n id: string;\n name: string;\n}\n\n/**\n * A reference to a 40kdc entity that may or may not have resolved. Retains the\n * source's raw name so the import is lossless even on a miss.\n */\nexport interface ResolvedRef {\n /** Resolved entity id, or null when no match was found. */\n id: string | null;\n /** The display name exactly as it appeared in the source payload. */\n raw_name: string;\n /** True iff {@link id} is non-null. */\n resolved: boolean;\n /** Up to 5 best-guess alternatives when resolution failed. */\n candidates: Candidate[];\n}\n\n/** A weapon/wargear selection on a unit. */\nexport interface RosterWargear {\n ref: ResolvedRef;\n count: number;\n}\n\n/** An inferred, always-provisional leader→bodyguard attachment. */\nexport interface RosterLeaderAttachment {\n bodyguard_ref: ResolvedRef;\n provisional: boolean;\n}\n\n/** One unit entry in a roster. */\nexport interface RosterUnit {\n ref: ResolvedRef;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement: ResolvedRef | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: RosterWargear[];\n leader_attachment: RosterLeaderAttachment | null;\n}\n\n/** Identifier for the adapter that produced this roster. New format adapters\n * extend this union; `roster.schema.json` keeps the canonical enum. */\nexport type RosterFormat =\n | \"listforge\"\n | \"newrecruit-json\"\n | \"newrecruit-wtc-compact\"\n | \"newrecruit-wtc-full\"\n | \"newrecruit-simple\";\n\n/** Provenance of the imported list. */\nexport interface RosterSource {\n format: RosterFormat;\n generated_by: string | null;\n}\n\n/** Point totals; reported and computed are kept distinct, never reconciled. */\nexport interface RosterPoints {\n declared_limit: number | null;\n total_reported: number | null;\n total_computed: number;\n}\n\n/** A single diagnostic warning. */\nexport interface Warning {\n code: WarningCode;\n message: string;\n raw_name: string | null;\n}\n\n/** A summary of what resolved and what did not during the import. */\nexport interface Diagnostics {\n resolved_units: number;\n unresolved_units: number;\n resolved_weapons: number;\n unresolved_weapons: number;\n warnings: Warning[];\n}\n\n/** Reference to the game edition + dataslate (mirrors game-version-ref). */\nexport interface GameVersionRef {\n edition: string;\n dataslate: string;\n}\n\n/** A fully-resolved army list. Validates against `roster.schema.json`. */\nexport interface Roster {\n name: string;\n source: RosterSource;\n faction_id: string | null;\n detachment_id: string | null;\n battle_size: BattleSize | null;\n points: RosterPoints;\n units: RosterUnit[];\n game_version: GameVersionRef;\n diagnostics: Diagnostics;\n}\n\n// ---------------------------------------------------------------------------\n// Intermediate types (format-agnostic; produced by a parser adapter)\n// ---------------------------------------------------------------------------\n\n/** A weapon/wargear selection before id resolution. */\nexport interface ParsedWargear {\n raw_name: string;\n count: number;\n}\n\n/** A unit selection before id resolution. */\nexport interface ParsedUnit {\n raw_name: string;\n /** True when the source classifies this as a character/leader-capable model. */\n is_character: boolean;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: ParsedWargear[];\n}\n\n/**\n * The format-agnostic intermediate. A {@link FormatAdapter} produces this from a\n * decoded source payload; {@link resolve} consumes it. Contains only raw display\n * names and counts — never reproduced rules text.\n */\nexport interface ParsedRoster {\n name: string;\n generated_by: string | null;\n /** Raw faction name from the source (e.g. \"Grey Knights\"). */\n faction_raw_name: string | null;\n /** Raw detachment name (e.g. \"Banishers\"). */\n detachment_raw_name: string | null;\n /** Raw battle-size label (e.g. \"2. Strike Force (2000 Point limit)\"). */\n battle_size_raw: string | null;\n /** Points limit parsed from the battle-size label, if any. */\n declared_limit: number | null;\n /** Total points reported by the source cost block. */\n total_reported: number | null;\n /** Points summed from every cost line in the source tree. */\n total_computed: number;\n units: ParsedUnit[];\n /** True when the source contained more than one distinct faction. */\n multi_force: boolean;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
1
  export * from "./data/index.js";
2
2
  export * from "./generated.js";
3
3
  export { createValidator, findSchemaFiles, listSchemaIds, SCHEMAS_ROOT, } from "./schema-loader.js";
4
+ export { importListForge, importNewRecruit, importRoster, decodeListForge, } from "./import/index.js";
5
+ export { exportRoster, newRecruitJsonSerializer, newRecruitSimpleSerializer, newRecruitWtcCompactSerializer, newRecruitWtcFullSerializer, rosterJsonSerializer, } from "./export/index.js";
6
+ export type { ExportFormat, RosterSerializer } from "./export/index.js";
7
+ export type { FormatAdapter } from "./import/index.js";
8
+ export type { ImportOptions, Roster, RosterUnit, RosterWargear, RosterSource, RosterFormat, RosterPoints, RosterLeaderAttachment, ResolvedRef, Candidate, Diagnostics, Warning, WarningCode, ParsedRoster, ParsedUnit, ParsedWargear, } from "./import/index.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAC;AAGhC,cAAc,gBAAgB,CAAC;AAI/B,OAAO,EACL,eAAe,EACf,eAAe,EACf,aAAa,EACb,YAAY,GACb,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EACV,aAAa,EACb,MAAM,EACN,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,EACP,WAAW,EACX,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -5,3 +5,10 @@ export * from "./generated.js";
5
5
  // Schema access + AJV validation (secondary: this package also validates data
6
6
  // against the canonical JSON Schemas).
7
7
  export { createValidator, findSchemaFiles, listSchemaIds, SCHEMAS_ROOT, } from "./schema-loader.js";
8
+ // Army-list importer (ListForge → resolved 40kdc roster). Types are curated
9
+ // rather than re-exported wholesale to avoid name clashes with generated types
10
+ // (e.g. BattleSize, LeaderAttachment).
11
+ export { importListForge, importNewRecruit, importRoster, decodeListForge, } from "./import/index.js";
12
+ // Army-list exporter (Roster → text or JSON for any of the supported formats).
13
+ export { exportRoster, newRecruitJsonSerializer, newRecruitSimpleSerializer, newRecruitWtcCompactSerializer, newRecruitWtcFullSerializer, rosterJsonSerializer, } from "./export/index.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,cAAc,iBAAiB,CAAC;AAEhC,mDAAmD;AACnD,cAAc,gBAAgB,CAAC;AAE/B,8EAA8E;AAC9E,uCAAuC;AACvC,OAAO,EACL,eAAe,EACf,eAAe,EACf,aAAa,EACb,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,4EAA4E;AAC5E,+EAA+E;AAC/E,uCAAuC;AACvC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,GACrB,MAAM,mBAAmB,CAAC","sourcesContent":["// The linked, typed dataset — the primary entry point.\nexport * from \"./data/index.js\";\n\n// Generated types for every entity in the dataset.\nexport * from \"./generated.js\";\n\n// Schema access + AJV validation (secondary: this package also validates data\n// against the canonical JSON Schemas).\nexport {\n createValidator,\n findSchemaFiles,\n listSchemaIds,\n SCHEMAS_ROOT,\n} from \"./schema-loader.js\";\n\n// Army-list importer (ListForge → resolved 40kdc roster). Types are curated\n// rather than re-exported wholesale to avoid name clashes with generated types\n// (e.g. BattleSize, LeaderAttachment).\nexport {\n importListForge,\n importNewRecruit,\n importRoster,\n decodeListForge,\n} from \"./import/index.js\";\n\n// Army-list exporter (Roster → text or JSON for any of the supported formats).\nexport {\n exportRoster,\n newRecruitJsonSerializer,\n newRecruitSimpleSerializer,\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n rosterJsonSerializer,\n} from \"./export/index.js\";\nexport type { ExportFormat, RosterSerializer } from \"./export/index.js\";\nexport type { FormatAdapter } from \"./import/index.js\";\nexport type {\n ImportOptions,\n Roster,\n RosterUnit,\n RosterWargear,\n RosterSource,\n RosterFormat,\n RosterPoints,\n RosterLeaderAttachment,\n ResolvedRef,\n Candidate,\n Diagnostics,\n Warning,\n WarningCode,\n ParsedRoster,\n ParsedUnit,\n ParsedWargear,\n} from \"./import/index.js\";\n"]}
@@ -29,3 +29,4 @@
29
29
  export declare const KNOWN_SUPPORT_10E: Record<string, readonly string[]>;
30
30
  /** Flatten the registry to faction-prefixed ids for set membership tests. */
31
31
  export declare function knownSupportSet(): Set<string>;
32
+ //# sourceMappingURL=known-support-10e.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-support-10e.d.ts","sourceRoot":"","sources":["../src/known-support-10e.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA0FH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAiB,CAAC;AAElF,6EAA6E;AAC7E,wBAAgB,eAAe,IAAI,GAAG,CAAC,MAAM,CAAC,CAM7C"}
@@ -111,3 +111,4 @@ export function knownSupportSet() {
111
111
  }
112
112
  return set;
113
113
  }
114
+ //# sourceMappingURL=known-support-10e.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-support-10e.js","sourceRoot":"","sources":["../src/known-support-10e.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,+EAA+E;AAC/E,MAAM,oBAAoB,GAAsC;IAC9D,kBAAkB,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,mBAAmB,CAAC;IAE5F,mEAAmE;IACnE,yEAAyE;IACzE,0BAA0B;IAC1B,kBAAkB,EAAE;QAClB,SAAS;QACT,8BAA8B;QAC9B,YAAY;QACZ,qBAAqB;QACrB,oBAAoB;QACpB,WAAW;QACX,eAAe;QACf,iBAAiB;QACjB,uBAAuB;QACvB,YAAY;QACZ,6BAA6B;QAC7B,6BAA6B;QAC7B,mBAAmB;KACpB;IAED,oBAAoB,EAAE,CAAC,uBAAuB,CAAC;IAE/C,SAAS,EAAE,CAAC,gBAAgB,EAAE,aAAa,EAAE,SAAS,CAAC;IAEvD,wBAAwB,EAAE,CAAC,mBAAmB,CAAC;IAE/C,iBAAiB,EAAE,CAAC,uBAAuB,EAAE,mBAAmB,CAAC;IAEjE,qBAAqB,EAAE,CAAC,sBAAsB,CAAC;IAE/C,aAAa,EAAE;QACb,oBAAoB;QACpB,kBAAkB;QAClB,aAAa;QACb,uBAAuB;QACvB,gBAAgB;QAChB,UAAU;KACX;IAED,mBAAmB,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC;IAEhE,SAAS,EAAE;QACT,cAAc;QACd,WAAW;QACX,oBAAoB;QACpB,YAAY;QACZ,cAAc;QACd,cAAc;KACf;IAED,cAAc,EAAE,CAAC,sBAAsB,CAAC;CACzC,CAAC;AAEF,oFAAoF;AACpF,MAAM,cAAc,GAAsC;IACxD,qEAAqE;IACrE,0EAA0E;IAC1E,uEAAuE;IACvE,4EAA4E;IAC5E,0CAA0C;IAC1C,YAAY,EAAE,CAAC,oBAAoB,EAAE,oBAAoB,EAAE,kBAAkB,CAAC;IAE9E,2EAA2E;IAC3E,wEAAwE;IACxE,mEAAmE;IACnE,oEAAoE;IACpE,wEAAwE;IACxE,wBAAwB;IACxB,SAAS,EAAE,CAAC,eAAe,CAAC;CAC7B,CAAC;AAEF,qDAAqD;AACrD,SAAS,WAAW;IAClB,MAAM,MAAM,GAA6B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAClE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;IAC7B,CAAC;IACD,KAAK,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;QAC5D,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;IACzD,CAAC;IACD,2DAA2D;IAC3D,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IAClE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAsC,WAAW,EAAE,CAAC;AAElF,6EAA6E;AAC7E,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC/D,KAAK,MAAM,EAAE,IAAI,GAAG;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Canonical registry of 10e units carrying the \"additional leader\" / \"second\n * leader\" attachment rule — characters that can attach to a unit even when\n * another Leader (e.g. Captain, Chapter Master, Lieutenant) is already\n * attached. In 11e this is formalised as `attachment_role: \"support\"`.\n *\n * The registry has two layers:\n *\n * 1. **`FROM_UPSTREAM_SCRAPE`** — derived deterministically from army-assist\n * `Datasheets.json` by scanning `leader_head` for the canonical phrasing\n * (/already been attached|additional leader|attach this model.*even if/i)\n * and resolving each datasheet name to our kebab-case unit id via the\n * 10e-archive's `data/core/<faction>/units.json` name table.\n * shadowboxing's `assets/Datasheets.json` yields the same 40 names.\n *\n * 2. **`MANUAL_OVERLAY`** — units whose 10e \"additional leader\" rule is\n * *not* captured by either upstream scraper (data-gap entries) plus\n * non-character special cases. Each entry needs a one-line comment\n * naming the SME source so the gap is auditable. This layer is the\n * reason 40kdc-data exists as a canonical upstream.\n *\n * The exported `KNOWN_SUPPORT_10E` is the merged view. To refresh layer 1,\n * re-run the scan recipe documented above. To add a missing unit (layer 2),\n * edit `MANUAL_OVERLAY` with a comment justifying the entry.\n *\n * Treat every entry as a **proposal** until human review confirms. The port\n * emits a warning if a registry entry doesn't match an archive unit.\n */\n\n/** Layer 1 — derived from army-assist (and confirmed against shadowboxing). */\nconst FROM_UPSTREAM_SCRAPE: Record<string, readonly string[]> = {\n \"adepta-sororitas\": [\"dialogus\", \"dogmata\", \"hospitaller\", \"imagifier\", \"ministorum-priest\"],\n\n // Successor chapters share these units via `parent_faction_id`, so\n // chapter-specific variants like `crusade-ancient` (Black Templars) live\n // under adeptus-astartes.\n \"adeptus-astartes\": [\n \"ancient\",\n \"ancient-in-terminator-armour\",\n \"apothecary\",\n \"apothecary-biologis\",\n \"bladeguard-ancient\",\n \"castellan\",\n \"cato-sicarius\",\n \"crusade-ancient\",\n \"imperial-space-marine\",\n \"lieutenant\",\n \"lieutenant-in-phobos-armour\",\n \"lieutenant-in-reiver-armour\",\n \"sanguinary-priest\",\n ],\n\n \"adeptus-mechanicus\": [\"cybernetica-datasmith\"],\n\n \"aeldari\": [\"eldrad-ulthran\", \"the-visarch\", \"warlock\"],\n\n \"agents-of-the-imperium\": [\"ministorum-priest\"],\n\n \"astra-militarum\": [\"death-rider-commissar\", \"ministorum-priest\"],\n\n \"chaos-space-marines\": [\"master-of-executions\"],\n\n \"death-guard\": [\n \"biologus-putrifier\",\n \"foul-blightspawn\",\n \"icon-bearer\",\n \"noxious-blightbringer\",\n \"plague-surgeon\",\n \"tallyman\",\n ],\n\n \"genestealer-cults\": [\"biophagus\", \"clamavus\", \"locus\", \"nexos\"],\n\n \"necrons\": [\n \"chronomancer\",\n \"geomancer\",\n \"orikan-the-diviner\",\n \"plasmancer\",\n \"psychomancer\",\n \"technomancer\",\n ],\n\n \"world-eaters\": [\"master-of-executions\"],\n};\n\n/** Layer 2 — units the upstream scrape misses, plus non-character special cases. */\nconst MANUAL_OVERLAY: Record<string, readonly string[]> = {\n // All three Kroot Shapers carry the additional-leader rule in the GW\n // datasheet, but their `leader_head` in both army-assist and shadowboxing\n // contains only the basic attachment list — the co-attach phrasing was\n // dropped by both community scrapes. (Ethereal is *not* a co-attach Leader;\n // confirmed not missing from the scrape.)\n \"tau-empire\": [\"kroot-flesh-shaper\", \"kroot-trail-shaper\", \"kroot-war-shaper\"],\n\n // Cryptothralls is a non-character bodyguard unit that joins a Cryptek-led\n // unit. In 10e the co-attach Leader rule sits on the Cryptek datasheets\n // (covered by layer 1); cryptothralls is included here for the 11e\n // Support-pattern review since it's the \"joiner\" entity. May need a\n // non-character Support encoding in 11e (`attachment_role` semantically\n // expects a character).\n \"necrons\": [\"cryptothralls\"],\n};\n\n/** Merge layers 1 and 2 into the public registry. */\nfunction mergeLayers(): Record<string, readonly string[]> {\n const merged: Record<string, string[]> = {};\n for (const [faction, ids] of Object.entries(FROM_UPSTREAM_SCRAPE)) {\n merged[faction] = [...ids];\n }\n for (const [faction, ids] of Object.entries(MANUAL_OVERLAY)) {\n merged[faction] = [...(merged[faction] ?? []), ...ids];\n }\n // Sort each list so output order is stable across re-runs.\n for (const faction of Object.keys(merged)) merged[faction].sort();\n return merged;\n}\n\nexport const KNOWN_SUPPORT_10E: Record<string, readonly string[]> = mergeLayers();\n\n/** Flatten the registry to faction-prefixed ids for set membership tests. */\nexport function knownSupportSet(): Set<string> {\n const set = new Set<string>();\n for (const [faction, ids] of Object.entries(KNOWN_SUPPORT_10E)) {\n for (const id of ids) set.add(`${faction}:${id}`);\n }\n return set;\n}\n"]}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Reverse-link enrichment ability `unit_ids` arrays into each unit's
3
+ * `ability_ids` in core data.
4
+ *
5
+ * Each `data/enrichment/<faction>/abilities.json` entry carries a
6
+ * `unit_ids` array naming the units that ability applies to. This script
7
+ * inverts that into `data/core/<faction>/units.json[*].ability_ids`.
8
+ *
9
+ * Additionally layers in `leader-attachments.json` — every `leader_id`
10
+ * gains the `"leader"` core ability.
11
+ *
12
+ * Idempotent and additive: existing `ability_ids` are preserved so
13
+ * manually-curated links (e.g. core abilities like "deep-strike",
14
+ * "deadly-demise-d3" not reachable via the enrichment unit_ids path)
15
+ * survive re-runs.
16
+ *
17
+ * Cross-faction routing: each `(unit_id, ability_id)` pair is bucketed
18
+ * to whichever `data/core/<faction>/units.json` actually contains that
19
+ * `unit_id` — so subfaction enrichment files contribute to the
20
+ * appropriate shared core file (e.g. Blood Angels enrichment routes
21
+ * into `adeptus-astartes/units.json`).
22
+ *
23
+ * Usage:
24
+ * npx tsx tools/src/link-abilities.ts # all factions
25
+ * npx tsx tools/src/link-abilities.ts --faction orks # one faction
26
+ * npx tsx tools/src/link-abilities.ts --dry-run # report only
27
+ */
28
+ export interface LinkOptions {
29
+ rootDir?: string;
30
+ factionFilter?: string;
31
+ dryRun?: boolean;
32
+ }
33
+ export interface LinkSummary {
34
+ factionsScanned: number;
35
+ unitsChanged: number;
36
+ abilityLinksAdded: number;
37
+ unknownUnitIdReferences: string[];
38
+ filesWritten: string[];
39
+ }
40
+ export declare function linkAbilities(opts?: LinkOptions): LinkSummary;
41
+ //# sourceMappingURL=link-abilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-abilities.d.ts","sourceRoot":"","sources":["../src/link-abilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAwBH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAkBD,wBAAgB,aAAa,CAAC,IAAI,GAAE,WAAgB,GAAG,WAAW,CAsFjE"}