@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,76 @@
1
+ import { displayedUnitPoints, titleCaseId, totalArmyPoints } from "./helpers.js";
2
+ function battleSizeLabel(roster) {
3
+ if (roster.battle_size === "strike-force") {
4
+ return `Strike Force (${roster.points.declared_limit ?? 2000} Point limit)`;
5
+ }
6
+ if (roster.battle_size === "incursion") {
7
+ return `Incursion (${roster.points.declared_limit ?? 1000} Point limit)`;
8
+ }
9
+ return null;
10
+ }
11
+ /** Build the wargear list inline. For homogeneous multi-model units, divides
12
+ * counts by model_count so the per-model render is clean. */
13
+ function wargearText(u, perModelDivisor) {
14
+ const parts = [];
15
+ if (u.enhancement) {
16
+ const ptsTag = u.enhancement_points === null ? "" : ` [${u.enhancement_points} pts]`;
17
+ parts.push(`${u.enhancement.raw_name}${ptsTag}`);
18
+ }
19
+ if (u.is_warlord)
20
+ parts.push("Warlord");
21
+ for (const w of u.wargear) {
22
+ const c = perModelDivisor > 0 ? w.count / perModelDivisor : w.count;
23
+ parts.push(c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name);
24
+ }
25
+ return parts.join(", ");
26
+ }
27
+ function unitText(u) {
28
+ const pts = displayedUnitPoints(u);
29
+ const ptsText = pts === null ? "" : `${pts} pts`;
30
+ if (u.model_count <= 1) {
31
+ return [`${u.ref.raw_name} [${ptsText}]: ${wargearText(u, 1)}`];
32
+ }
33
+ // Multi-model: homogeneous when every weapon count divides cleanly.
34
+ const divisible = u.wargear.every((w) => w.count % u.model_count === 0);
35
+ if (divisible) {
36
+ return [
37
+ `${u.ref.raw_name} [${ptsText}]:`,
38
+ `• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, u.model_count)}`,
39
+ ];
40
+ }
41
+ // Heterogeneous fallback: render as a single bullet with full counts.
42
+ return [
43
+ `${u.ref.raw_name} [${ptsText}]:`,
44
+ `• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, 1)}`,
45
+ ];
46
+ }
47
+ export const newRecruitSimpleSerializer = {
48
+ id: "newrecruit-simple",
49
+ serialize(roster) {
50
+ const faction = titleCaseId(roster.faction_id) ?? "Unknown";
51
+ const detachment = titleCaseId(roster.detachment_id);
52
+ const battle = battleSizeLabel(roster);
53
+ const total = totalArmyPoints(roster);
54
+ const lines = [];
55
+ // First line carries the *declared limit* (the army's points ceiling); the
56
+ // `# ++ Army Roster ++` line carries the *reported total*. They differ
57
+ // when the list isn't filled to the cap.
58
+ const limit = roster.points.declared_limit ?? total;
59
+ lines.push(`${faction} - ${roster.name} - [${limit} pts]`);
60
+ lines.push("");
61
+ lines.push(`# ++ Army Roster ++ [${total} pts]`);
62
+ lines.push("## Configuration");
63
+ if (battle)
64
+ lines.push(`Battle Size: ${battle}`);
65
+ if (detachment)
66
+ lines.push(`Detachment: ${detachment}`);
67
+ lines.push("");
68
+ // The Roster doesn't tag allied vs. battleline per unit; emit one section.
69
+ const sectionTotal = roster.units.reduce((acc, u) => acc + (u.points ?? 0) + (u.enhancement_points ?? 0), 0);
70
+ lines.push(`## Battleline [${sectionTotal} pts]`);
71
+ for (const u of roster.units)
72
+ lines.push(...unitText(u));
73
+ return lines.join("\n") + "\n";
74
+ },
75
+ };
76
+ //# sourceMappingURL=newrecruit-simple.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"newrecruit-simple.js","sourceRoot":"","sources":["../../src/export/newrecruit-simple.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGjF,SAAS,eAAe,CAAC,MAAc;IACrC,IAAI,MAAM,CAAC,WAAW,KAAK,cAAc,EAAE,CAAC;QAC1C,OAAO,iBAAiB,MAAM,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,eAAe,CAAC;IAC9E,CAAC;IACD,IAAI,MAAM,CAAC,WAAW,KAAK,WAAW,EAAE,CAAC;QACvC,OAAO,cAAc,MAAM,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,eAAe,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;6DAC6D;AAC7D,SAAS,WAAW,CAAC,CAAa,EAAE,eAAuB;IACzD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,CAAC,CAAC,kBAAkB,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,kBAAkB,OAAO,CAAC;QACrF,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,QAAQ,GAAG,MAAM,EAAE,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,CAAC,UAAU;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACpE,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,CAAa;IAC7B,MAAM,GAAG,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC;IAEjD,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,oEAAoE;IACpE,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC;IACxE,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI;YACjC,KAAK,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE;SAC1E,CAAC;IACJ,CAAC;IACD,sEAAsE;IACtE,OAAO;QACL,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI;QACjC,KAAK,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;KAC9D,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAqB;IAC1D,EAAE,EAAE,mBAAmB;IAEvB,SAAS,CAAC,MAAc;QACtB,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC;QAC5D,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QAEtC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,2EAA2E;QAC3E,uEAAuE;QACvE,yCAAyC;QACzC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,IAAI,KAAK,CAAC;QACpD,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC;QAC3D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,wBAAwB,KAAK,OAAO,CAAC,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC/B,IAAI,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;QACjD,IAAI,UAAU;YAAE,KAAK,CAAC,IAAI,CAAC,eAAe,UAAU,EAAE,CAAC,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,2EAA2E;QAC3E,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CACtC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC,EAC/D,CAAC,CACF,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,kBAAkB,YAAY,OAAO,CAAC,CAAC;QAClD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAEzD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACjC,CAAC;CACF,CAAC","sourcesContent":["/**\n * NewRecruit \"simple\" markdown-ish text exporter.\n *\n * Shape:\n * ```\n * <faction> - <list name> - [N pts]\n *\n * # ++ Army Roster ++ [N pts]\n * ## Configuration\n * Battle Size: <Label>\n * Detachment: <Name>\n *\n * ## Battleline [N pts]\n * <Unit> [pts]: <wargear, …, EnhName [N pts], …>\n * <Multi-Unit> [pts]:\n * • <Nx> <ModelType>: <wargear>\n * ```\n *\n * Enhancements are inlined as `Name [N pts]` (the only place we re-emit a\n * `[N pts]` bracket on a token).\n *\n * @packageDocumentation\n */\nimport type { Roster, RosterUnit } from \"../import/types.js\";\nimport { displayedUnitPoints, titleCaseId, totalArmyPoints } from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\nfunction battleSizeLabel(roster: Roster): string | null {\n if (roster.battle_size === \"strike-force\") {\n return `Strike Force (${roster.points.declared_limit ?? 2000} Point limit)`;\n }\n if (roster.battle_size === \"incursion\") {\n return `Incursion (${roster.points.declared_limit ?? 1000} Point limit)`;\n }\n return null;\n}\n\n/** Build the wargear list inline. For homogeneous multi-model units, divides\n * counts by model_count so the per-model render is clean. */\nfunction wargearText(u: RosterUnit, perModelDivisor: number): string {\n const parts: string[] = [];\n if (u.enhancement) {\n const ptsTag = u.enhancement_points === null ? \"\" : ` [${u.enhancement_points} pts]`;\n parts.push(`${u.enhancement.raw_name}${ptsTag}`);\n }\n if (u.is_warlord) parts.push(\"Warlord\");\n for (const w of u.wargear) {\n const c = perModelDivisor > 0 ? w.count / perModelDivisor : w.count;\n parts.push(c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name);\n }\n return parts.join(\", \");\n}\n\nfunction unitText(u: RosterUnit): string[] {\n const pts = displayedUnitPoints(u);\n const ptsText = pts === null ? \"\" : `${pts} pts`;\n\n if (u.model_count <= 1) {\n return [`${u.ref.raw_name} [${ptsText}]: ${wargearText(u, 1)}`];\n }\n // Multi-model: homogeneous when every weapon count divides cleanly.\n const divisible = u.wargear.every((w) => w.count % u.model_count === 0);\n if (divisible) {\n return [\n `${u.ref.raw_name} [${ptsText}]:`,\n `• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, u.model_count)}`,\n ];\n }\n // Heterogeneous fallback: render as a single bullet with full counts.\n return [\n `${u.ref.raw_name} [${ptsText}]:`,\n `• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, 1)}`,\n ];\n}\n\nexport const newRecruitSimpleSerializer: RosterSerializer = {\n id: \"newrecruit-simple\",\n\n serialize(roster: Roster): string {\n const faction = titleCaseId(roster.faction_id) ?? \"Unknown\";\n const detachment = titleCaseId(roster.detachment_id);\n const battle = battleSizeLabel(roster);\n const total = totalArmyPoints(roster);\n\n const lines: string[] = [];\n // First line carries the *declared limit* (the army's points ceiling); the\n // `# ++ Army Roster ++` line carries the *reported total*. They differ\n // when the list isn't filled to the cap.\n const limit = roster.points.declared_limit ?? total;\n lines.push(`${faction} - ${roster.name} - [${limit} pts]`);\n lines.push(\"\");\n lines.push(`# ++ Army Roster ++ [${total} pts]`);\n lines.push(\"## Configuration\");\n if (battle) lines.push(`Battle Size: ${battle}`);\n if (detachment) lines.push(`Detachment: ${detachment}`);\n lines.push(\"\");\n\n // The Roster doesn't tag allied vs. battleline per unit; emit one section.\n const sectionTotal = roster.units.reduce(\n (acc, u) => acc + (u.points ?? 0) + (u.enhancement_points ?? 0),\n 0,\n );\n lines.push(`## Battleline [${sectionTotal} pts]`);\n for (const u of roster.units) lines.push(...unitText(u));\n\n return lines.join(\"\\n\") + \"\\n\";\n },\n};\n"]}
@@ -0,0 +1,4 @@
1
+ import type { RosterSerializer } from "./serializer.js";
2
+ export declare const newRecruitWtcCompactSerializer: RosterSerializer;
3
+ export declare const newRecruitWtcFullSerializer: RosterSerializer;
4
+ //# sourceMappingURL=newrecruit-wtc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"newrecruit-wtc.d.ts","sourceRoot":"","sources":["../../src/export/newrecruit-wtc.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AA8DxD,eAAO,MAAM,8BAA8B,EAAE,gBAyB5C,CAAC;AAuBF,eAAO,MAAM,2BAA2B,EAAE,gBAgDzC,CAAC"}
@@ -0,0 +1,142 @@
1
+ import { charSlotAssignment, displayedUnitPoints, titleCaseId, totalArmyPoints, } from "./helpers.js";
2
+ const FENCE = "+++++++++++++++++++++++++++++++++++++++++++++++";
3
+ function wargearListText(unit, includeWarlordTag) {
4
+ const parts = [];
5
+ for (const w of unit.wargear) {
6
+ parts.push(w.count > 1 ? `${w.count}x ${w.ref.raw_name}` : w.ref.raw_name);
7
+ }
8
+ if (includeWarlordTag && unit.is_warlord)
9
+ parts.push("Warlord");
10
+ return parts.join(", ");
11
+ }
12
+ function header(roster, units, charSlots) {
13
+ const faction = titleCaseId(roster.faction_id) ?? "Unknown";
14
+ const detachment = titleCaseId(roster.detachment_id);
15
+ const limit = roster.points.declared_limit ?? totalArmyPoints(roster);
16
+ const total = roster.points.total_reported ?? totalArmyPoints(roster);
17
+ const warlordIdx = units.findIndex((u) => u.is_warlord);
18
+ const warlord = warlordIdx >= 0
19
+ ? `Char${charSlots[warlordIdx]}: ${units[warlordIdx].ref.raw_name}`
20
+ : "—";
21
+ const enhancementIdx = units.findIndex((u) => u.enhancement !== null);
22
+ let enhancement = "—";
23
+ if (enhancementIdx >= 0) {
24
+ const u = units[enhancementIdx];
25
+ enhancement = `${u.enhancement.raw_name} (on Char${charSlots[enhancementIdx]}: ${u.ref.raw_name})`;
26
+ }
27
+ const lines = [
28
+ FENCE,
29
+ `+ LIST NAME: ${roster.name}`,
30
+ `+ FACTION KEYWORD: ${faction}`,
31
+ `+ DETACHMENT: ${detachment ?? "—"}`,
32
+ `+ TOTAL ARMY POINTS: ${total}pts`,
33
+ `+ POINTS LIMIT: ${limit}pts`,
34
+ `+`,
35
+ `+ WARLORD: ${warlord}`,
36
+ `+ ENHANCEMENT: ${enhancement}`,
37
+ `+ NUMBER OF UNITS: ${units.length}`,
38
+ FENCE,
39
+ ];
40
+ return lines.join("\n");
41
+ }
42
+ function isAlliedUnit(u, factionId) {
43
+ // Heuristic: the Roster doesn't tag allied units explicitly, but the
44
+ // multi-force diagnostic + the fact that we only carry the primary faction
45
+ // means non-primary-faction units aren't recognisable. The only fact we *do*
46
+ // have is `leader_attachment` and warlord/enhancement (which mark primary
47
+ // characters). For unit grouping in wtc-full we simply place everything in
48
+ // BATTLELINE unless the Roster's multi-force flag suggests there's an allied
49
+ // detachment. Since the flag is a diagnostic warning, not a per-unit tag,
50
+ // wtc-full export collapses to a single BATTLELINE section.
51
+ void u;
52
+ void factionId;
53
+ return false;
54
+ }
55
+ export const newRecruitWtcCompactSerializer = {
56
+ id: "newrecruit-wtc-compact",
57
+ serialize(roster) {
58
+ const units = roster.units;
59
+ const slots = charSlotAssignment(units);
60
+ const lines = [header(roster, units, slots), ""];
61
+ for (let i = 0; i < units.length; i += 1) {
62
+ const u = units[i];
63
+ const prefix = slots[i] !== null ? `Char${slots[i]}: ` : "";
64
+ const pts = displayedUnitPoints(u);
65
+ const ptsText = pts === null ? "" : `${pts} pts`;
66
+ lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText}): ${wargearListText(u, true)}`);
67
+ if (u.enhancement) {
68
+ const enhText = u.enhancement_points === null
69
+ ? `Enhancement: ${u.enhancement.raw_name}`
70
+ : `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;
71
+ lines.push(enhText);
72
+ }
73
+ }
74
+ return lines.join("\n") + "\n";
75
+ },
76
+ };
77
+ /**
78
+ * For a multi-model unit, render its wargear as `N with <per-model list>` when
79
+ * the wargear divides evenly across models (the natural NewRecruit form).
80
+ * Otherwise emit `1 with <full Nx counts>` so the counts round-trip exactly.
81
+ */
82
+ function multiModelWithLine(u) {
83
+ // Homogeneous when every weapon count divides cleanly by model_count.
84
+ const divisible = u.wargear.every((w) => w.count % u.model_count === 0);
85
+ if (divisible) {
86
+ const perModel = u.wargear
87
+ .map((w) => {
88
+ const c = w.count / u.model_count;
89
+ return c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name;
90
+ })
91
+ .filter((s) => s.length > 0);
92
+ if (u.is_warlord)
93
+ perModel.push("Warlord");
94
+ return `${u.model_count} with ${perModel.join(", ")}`;
95
+ }
96
+ return `1 with ${wargearListText(u, true)}`;
97
+ }
98
+ export const newRecruitWtcFullSerializer = {
99
+ id: "newrecruit-wtc-full",
100
+ serialize(roster) {
101
+ const units = roster.units;
102
+ const slots = charSlotAssignment(units);
103
+ const battlelineIdxs = [];
104
+ const alliedIdxs = [];
105
+ for (let i = 0; i < units.length; i += 1) {
106
+ if (isAlliedUnit(units[i], roster.faction_id))
107
+ alliedIdxs.push(i);
108
+ else
109
+ battlelineIdxs.push(i);
110
+ }
111
+ const lines = [header(roster, units, slots), "", "BATTLELINE", ""];
112
+ const emitUnit = (i) => {
113
+ const u = units[i];
114
+ const prefix = slots[i] !== null ? `Char${slots[i]}: ` : "";
115
+ const pts = displayedUnitPoints(u);
116
+ const ptsText = pts === null ? "" : `${pts} pts`;
117
+ lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText})`);
118
+ if (u.model_count > 1) {
119
+ lines.push(multiModelWithLine(u));
120
+ }
121
+ else {
122
+ lines.push(`1 with ${wargearListText(u, true)}`);
123
+ }
124
+ if (u.enhancement) {
125
+ const enhText = u.enhancement_points === null
126
+ ? `Enhancement: ${u.enhancement.raw_name}`
127
+ : `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;
128
+ lines.push(enhText);
129
+ }
130
+ lines.push("");
131
+ };
132
+ for (const i of battlelineIdxs)
133
+ emitUnit(i);
134
+ if (alliedIdxs.length > 0) {
135
+ lines.push("ALLIED UNITS", "");
136
+ for (const i of alliedIdxs)
137
+ emitUnit(i);
138
+ }
139
+ return lines.join("\n");
140
+ },
141
+ };
142
+ //# sourceMappingURL=newrecruit-wtc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/export/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit wtc-compact and wtc-full text exporters.\n *\n * Both formats lead with a `++++++++` summary header and then list units. The\n * compact body packs each unit onto one line; the full body uses section\n * headers (`BATTLELINE` / `ALLIED UNITS`) and two-line unit blocks with\n * `N with <wargear>` and `• Nx <ModelType>` per-model breakdowns.\n *\n * Faction & detachment display names are reconstructed via\n * {@link titleCaseId}. `CharN:` numbering is re-derived heuristically from\n * `is_warlord || enhancement || leader_attachment` (see\n * {@link charSlotAssignment}). The `+ SECONDARY:` summary line is omitted —\n * tournament secondaries aren't modelled in the Roster.\n *\n * @packageDocumentation\n */\nimport type { Roster, RosterUnit } from \"../import/types.js\";\nimport {\n charSlotAssignment,\n displayedUnitPoints,\n titleCaseId,\n totalArmyPoints,\n} from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\nconst FENCE = \"+++++++++++++++++++++++++++++++++++++++++++++++\";\n\nfunction wargearListText(unit: RosterUnit, includeWarlordTag: boolean): string {\n const parts: string[] = [];\n for (const w of unit.wargear) {\n parts.push(w.count > 1 ? `${w.count}x ${w.ref.raw_name}` : w.ref.raw_name);\n }\n if (includeWarlordTag && unit.is_warlord) parts.push(\"Warlord\");\n return parts.join(\", \");\n}\n\nfunction header(roster: Roster, units: readonly RosterUnit[], charSlots: readonly (number | null)[]): string {\n const faction = titleCaseId(roster.faction_id) ?? \"Unknown\";\n const detachment = titleCaseId(roster.detachment_id);\n const limit = roster.points.declared_limit ?? totalArmyPoints(roster);\n const total = roster.points.total_reported ?? totalArmyPoints(roster);\n\n const warlordIdx = units.findIndex((u) => u.is_warlord);\n const warlord =\n warlordIdx >= 0\n ? `Char${charSlots[warlordIdx]}: ${units[warlordIdx].ref.raw_name}`\n : \"—\";\n\n const enhancementIdx = units.findIndex((u) => u.enhancement !== null);\n let enhancement = \"—\";\n if (enhancementIdx >= 0) {\n const u = units[enhancementIdx];\n enhancement = `${u.enhancement!.raw_name} (on Char${charSlots[enhancementIdx]}: ${u.ref.raw_name})`;\n }\n\n const lines: string[] = [\n FENCE,\n `+ LIST NAME: ${roster.name}`,\n `+ FACTION KEYWORD: ${faction}`,\n `+ DETACHMENT: ${detachment ?? \"—\"}`,\n `+ TOTAL ARMY POINTS: ${total}pts`,\n `+ POINTS LIMIT: ${limit}pts`,\n `+`,\n `+ WARLORD: ${warlord}`,\n `+ ENHANCEMENT: ${enhancement}`,\n `+ NUMBER OF UNITS: ${units.length}`,\n FENCE,\n ];\n return lines.join(\"\\n\");\n}\n\nfunction isAlliedUnit(u: RosterUnit, factionId: string | null): boolean {\n // Heuristic: the Roster doesn't tag allied units explicitly, but the\n // multi-force diagnostic + the fact that we only carry the primary faction\n // means non-primary-faction units aren't recognisable. The only fact we *do*\n // have is `leader_attachment` and warlord/enhancement (which mark primary\n // characters). For unit grouping in wtc-full we simply place everything in\n // BATTLELINE unless the Roster's multi-force flag suggests there's an allied\n // detachment. Since the flag is a diagnostic warning, not a per-unit tag,\n // wtc-full export collapses to a single BATTLELINE section.\n void u;\n void factionId;\n return false;\n}\n\nexport const newRecruitWtcCompactSerializer: RosterSerializer = {\n id: \"newrecruit-wtc-compact\",\n\n serialize(roster: Roster): string {\n const units = roster.units;\n const slots = charSlotAssignment(units);\n const lines: string[] = [header(roster, units, slots), \"\"];\n\n for (let i = 0; i < units.length; i += 1) {\n const u = units[i];\n const prefix = slots[i] !== null ? `Char${slots[i]}: ` : \"\";\n const pts = displayedUnitPoints(u);\n const ptsText = pts === null ? \"\" : `${pts} pts`;\n lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText}): ${wargearListText(u, true)}`);\n if (u.enhancement) {\n const enhText =\n u.enhancement_points === null\n ? `Enhancement: ${u.enhancement.raw_name}`\n : `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;\n lines.push(enhText);\n }\n }\n\n return lines.join(\"\\n\") + \"\\n\";\n },\n};\n\n/**\n * For a multi-model unit, render its wargear as `N with <per-model list>` when\n * the wargear divides evenly across models (the natural NewRecruit form).\n * Otherwise emit `1 with <full Nx counts>` so the counts round-trip exactly.\n */\nfunction multiModelWithLine(u: RosterUnit): string {\n // Homogeneous when every weapon count divides cleanly by model_count.\n const divisible = u.wargear.every((w) => w.count % u.model_count === 0);\n if (divisible) {\n const perModel = u.wargear\n .map((w) => {\n const c = w.count / u.model_count;\n return c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name;\n })\n .filter((s) => s.length > 0);\n if (u.is_warlord) perModel.push(\"Warlord\");\n return `${u.model_count} with ${perModel.join(\", \")}`;\n }\n return `1 with ${wargearListText(u, true)}`;\n}\n\nexport const newRecruitWtcFullSerializer: RosterSerializer = {\n id: \"newrecruit-wtc-full\",\n\n serialize(roster: Roster): string {\n const units = roster.units;\n const slots = charSlotAssignment(units);\n\n const battlelineIdxs: number[] = [];\n const alliedIdxs: number[] = [];\n for (let i = 0; i < units.length; i += 1) {\n if (isAlliedUnit(units[i], roster.faction_id)) alliedIdxs.push(i);\n else battlelineIdxs.push(i);\n }\n\n const lines: string[] = [header(roster, units, slots), \"\", \"BATTLELINE\", \"\"];\n\n const emitUnit = (i: number): void => {\n const u = units[i];\n const prefix = slots[i] !== null ? `Char${slots[i]}: ` : \"\";\n const pts = displayedUnitPoints(u);\n const ptsText = pts === null ? \"\" : `${pts} pts`;\n lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText})`);\n\n if (u.model_count > 1) {\n lines.push(multiModelWithLine(u));\n } else {\n lines.push(`1 with ${wargearListText(u, true)}`);\n }\n\n if (u.enhancement) {\n const enhText =\n u.enhancement_points === null\n ? `Enhancement: ${u.enhancement.raw_name}`\n : `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;\n lines.push(enhText);\n }\n lines.push(\"\");\n };\n\n for (const i of battlelineIdxs) emitUnit(i);\n\n if (alliedIdxs.length > 0) {\n lines.push(\"ALLIED UNITS\", \"\");\n for (const i of alliedIdxs) emitUnit(i);\n }\n\n return lines.join(\"\\n\");\n },\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { RosterSerializer } from "./serializer.js";
2
+ export declare const rosterJsonSerializer: RosterSerializer;
3
+ //# sourceMappingURL=roster-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roster-json.d.ts","sourceRoot":"","sources":["../../src/export/roster-json.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,eAAO,MAAM,oBAAoB,EAAE,gBAKlC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { prettyJson } from "./helpers.js";
2
+ export const rosterJsonSerializer = {
3
+ id: "roster-json",
4
+ serialize(roster) {
5
+ return prettyJson(roster);
6
+ },
7
+ };
8
+ //# sourceMappingURL=roster-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roster-json.js","sourceRoot":"","sources":["../../src/export/roster-json.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,MAAM,CAAC,MAAM,oBAAoB,GAAqB;IACpD,EAAE,EAAE,aAAa;IACjB,SAAS,CAAC,MAAc;QACtB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;CACF,CAAC","sourcesContent":["/**\n * Canonical Roster JSON serializer — emits the {@link Roster} as 2-space JSON,\n * the same shape the importers consume. This is the lossless pivot, so the\n * pretty-printed text is exactly `roster.schema.json` shape.\n *\n * @packageDocumentation\n */\nimport type { Roster } from \"../import/types.js\";\nimport { prettyJson } from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\nexport const rosterJsonSerializer: RosterSerializer = {\n id: \"roster-json\",\n serialize(roster: Roster): string {\n return prettyJson(roster);\n },\n};\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The roster-serializer seam — symmetric counterpart to the
3
+ * {@link FormatAdapter} import seam.
4
+ *
5
+ * Each supported export target implements {@link RosterSerializer}: it takes a
6
+ * fully-resolved {@link Roster} and produces a deterministic string in that
7
+ * format. The seam stays Dataset-free so the TS and Rust mirrors can produce
8
+ * byte-identical output for conformance.
9
+ *
10
+ * Five targets are registered:
11
+ * - `newrecruit-json` — NewRecruit-shaped JSON skeleton (rules-free).
12
+ * - `newrecruit-wtc-compact` — tournament-friendly one-line-per-unit text.
13
+ * - `newrecruit-wtc-full` — tournament-friendly section-and-wargear text.
14
+ * - `newrecruit-simple` — markdown-ish text.
15
+ * - `roster-json` — canonical Roster JSON (the lossless pivot).
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ import type { Roster } from "../import/types.js";
20
+ /** Stable id for an export target. */
21
+ export type ExportFormat = "newrecruit-json" | "newrecruit-wtc-compact" | "newrecruit-wtc-full" | "newrecruit-simple" | "roster-json";
22
+ /** Serializes a {@link Roster} into one specific format. */
23
+ export interface RosterSerializer {
24
+ id: ExportFormat;
25
+ serialize(roster: Roster): string;
26
+ }
27
+ //# sourceMappingURL=serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../src/export/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,sCAAsC;AACtC,MAAM,MAAM,YAAY,GACpB,iBAAiB,GACjB,wBAAwB,GACxB,qBAAqB,GACrB,mBAAmB,GACnB,aAAa,CAAC;AAElB,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,YAAY,CAAC;IACjB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=serializer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.js","sourceRoot":"","sources":["../../src/export/serializer.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The roster-serializer seam — symmetric counterpart to the\n * {@link FormatAdapter} import seam.\n *\n * Each supported export target implements {@link RosterSerializer}: it takes a\n * fully-resolved {@link Roster} and produces a deterministic string in that\n * format. The seam stays Dataset-free so the TS and Rust mirrors can produce\n * byte-identical output for conformance.\n *\n * Five targets are registered:\n * - `newrecruit-json` — NewRecruit-shaped JSON skeleton (rules-free).\n * - `newrecruit-wtc-compact` — tournament-friendly one-line-per-unit text.\n * - `newrecruit-wtc-full` — tournament-friendly section-and-wargear text.\n * - `newrecruit-simple` — markdown-ish text.\n * - `roster-json` — canonical Roster JSON (the lossless pivot).\n *\n * @packageDocumentation\n */\nimport type { Roster } from \"../import/types.js\";\n\n/** Stable id for an export target. */\nexport type ExportFormat =\n | \"newrecruit-json\"\n | \"newrecruit-wtc-compact\"\n | \"newrecruit-wtc-full\"\n | \"newrecruit-simple\"\n | \"roster-json\";\n\n/** Serializes a {@link Roster} into one specific format. */\nexport interface RosterSerializer {\n id: ExportFormat;\n serialize(roster: Roster): string;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gen-conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gen-conformance.d.ts","sourceRoot":"","sources":["../src/gen-conformance.ts"],"names":[],"mappings":""}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Generate the cross-implementation conformance corpus under repo-root
3
+ * `conformance/`. The TypeScript package is the reference implementation, so
4
+ * the goldens it emits are what the Rust crate must reproduce byte-for-byte
5
+ * (structurally). Run via `npm run gen:conformance`; CI regenerates and asserts
6
+ * `git diff --exit-code conformance/` is clean.
7
+ *
8
+ * Outputs:
9
+ * - `conformance/normalize.json` — `[{ input, expected }]` for normalizeName.
10
+ * - `conformance/roster/<case>/expected.roster.json` — the resolved Roster.
11
+ * - `conformance/roster/<case>/expected.<fmt>.{txt,json}` — every export
12
+ * target's golden output. The TS exporter is the oracle; the Rust mirror
13
+ * asserts byte-equal output for the same Roster.
14
+ * - `conformance/roster/<case>/input.newrecruit-{wtc-compact,wtc-full,simple}.txt`
15
+ * — text inputs derived from the seed by the exporter, so a re-import
16
+ * regression in either implementation surfaces immediately.
17
+ *
18
+ * Seeding: each `<case>/` carries one canonical input — either the legacy
19
+ * `input.json` (ListForge) or `input.newrecruit-json.json` (NewRecruit). Other
20
+ * inputs are derived.
21
+ */
22
+ import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
23
+ import { dirname, join } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { Dataset } from "./data/dataset.js";
26
+ import { normalizeName } from "./data/normalize.js";
27
+ import { exportRoster } from "./export/index.js";
28
+ import { importRoster } from "./import/import-roster.js";
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ const REPO_ROOT = join(__dirname, "../..");
31
+ const CONFORMANCE = join(REPO_ROOT, "conformance");
32
+ const NORMALIZE_INPUTS = [
33
+ // NFD diacritic strip
34
+ "Khârn the Betrayer",
35
+ "Brôkhyr",
36
+ "Ûthar",
37
+ "Magnús",
38
+ // apostrophe / quote variants
39
+ "T'au",
40
+ "Be’lakor",
41
+ "Kor’sarro Khan",
42
+ "Aetaos'rau'keres",
43
+ "‘quoted’",
44
+ // whitespace / hyphen collapse + trim
45
+ "Brôkhyr Iron-master",
46
+ " the betrayer ",
47
+ "space--marines",
48
+ // casefold
49
+ "KHÂRN THE BETRAYER",
50
+ // already-normalized (idempotence)
51
+ "kharn the betrayer",
52
+ // distinctness anchors (must NOT collapse together)
53
+ "Khorne",
54
+ "Khârn",
55
+ ];
56
+ function writeJson(path, value) {
57
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
58
+ }
59
+ function writeText(path, value) {
60
+ writeFileSync(path, value);
61
+ }
62
+ function genNormalize() {
63
+ const table = NORMALIZE_INPUTS.map((input) => ({ input, expected: normalizeName(input) }));
64
+ writeJson(join(CONFORMANCE, "normalize.json"), table);
65
+ console.log(`normalize.json: ${table.length} cases`);
66
+ }
67
+ /** Locate the canonical input for a fixture dir: prefer `input.json` (legacy
68
+ * ListForge), fall back to `input.newrecruit-json.json` (NewRecruit). */
69
+ function seedRoster(caseDir, ds) {
70
+ const candidates = ["input.json", "input.newrecruit-json.json"];
71
+ for (const name of candidates) {
72
+ const path = join(caseDir, name);
73
+ if (existsSync(path)) {
74
+ const decoded = JSON.parse(readFileSync(path, "utf8"));
75
+ return importRoster(decoded, { dataset: ds });
76
+ }
77
+ }
78
+ throw new Error(`no canonical input found in ${caseDir}`);
79
+ }
80
+ const TEXT_FORMATS = [
81
+ {
82
+ format: "newrecruit-wtc-compact",
83
+ inputName: "input.newrecruit-wtc-compact.txt",
84
+ goldenName: "expected.newrecruit-wtc-compact.txt",
85
+ },
86
+ {
87
+ format: "newrecruit-wtc-full",
88
+ inputName: "input.newrecruit-wtc-full.txt",
89
+ goldenName: "expected.newrecruit-wtc-full.txt",
90
+ },
91
+ {
92
+ format: "newrecruit-simple",
93
+ inputName: "input.newrecruit-simple.txt",
94
+ goldenName: "expected.newrecruit-simple.txt",
95
+ },
96
+ ];
97
+ function genRosters() {
98
+ const ds = Dataset.embedded();
99
+ const rosterDir = join(CONFORMANCE, "roster");
100
+ for (const entry of readdirSync(rosterDir, { withFileTypes: true })) {
101
+ if (!entry.isDirectory())
102
+ continue;
103
+ const caseDir = join(rosterDir, entry.name);
104
+ const seed = seedRoster(caseDir, ds);
105
+ writeJson(join(caseDir, "expected.roster.json"), seed);
106
+ // JSON export golden — NewRecruit-shaped skeleton.
107
+ const jsonOut = exportRoster(seed, "newrecruit-json");
108
+ writeJson(join(caseDir, "expected.newrecruit-json.json"), JSON.parse(jsonOut));
109
+ // Canonical Roster JSON export — should equal the resolved roster.
110
+ writeJson(join(caseDir, "expected.roster-json.json"), JSON.parse(exportRoster(seed, "roster-json")));
111
+ // Text exports: always write the export golden so the cross-implementation
112
+ // byte-equality check has something to compare against. Only write the
113
+ // `input.*.txt` round-trip seed when the fixture was authored for the
114
+ // NewRecruit pipeline — legacy ListForge fixtures carry decoration
115
+ // (multi-force warnings, leader-attachment inference) that the simple/wtc
116
+ // exporters can't fully preserve, so the round-trip would fail
117
+ // structurally rather than uncover a parser bug.
118
+ const isNewRecruitSeed = existsSync(join(caseDir, "input.newrecruit-json.json"));
119
+ for (const { format, inputName, goldenName } of TEXT_FORMATS) {
120
+ const out = exportRoster(seed, format);
121
+ writeText(join(caseDir, goldenName), out);
122
+ if (isNewRecruitSeed) {
123
+ writeText(join(caseDir, inputName), out);
124
+ }
125
+ }
126
+ console.log(`roster/${entry.name}: ${seed.units.length} units, ${seed.diagnostics.warnings.length} warnings`);
127
+ }
128
+ }
129
+ genNormalize();
130
+ genRosters();
131
+ //# sourceMappingURL=gen-conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gen-conformance.js","sourceRoot":"","sources":["../src/gen-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate the cross-implementation conformance corpus under repo-root\n * `conformance/`. The TypeScript package is the reference implementation, so\n * the goldens it emits are what the Rust crate must reproduce byte-for-byte\n * (structurally). Run via `npm run gen:conformance`; CI regenerates and asserts\n * `git diff --exit-code conformance/` is clean.\n *\n * Outputs:\n * - `conformance/normalize.json` — `[{ input, expected }]` for normalizeName.\n * - `conformance/roster/<case>/expected.roster.json` — the resolved Roster.\n * - `conformance/roster/<case>/expected.<fmt>.{txt,json}` — every export\n * target's golden output. The TS exporter is the oracle; the Rust mirror\n * asserts byte-equal output for the same Roster.\n * - `conformance/roster/<case>/input.newrecruit-{wtc-compact,wtc-full,simple}.txt`\n * — text inputs derived from the seed by the exporter, so a re-import\n * regression in either implementation surfaces immediately.\n *\n * Seeding: each `<case>/` carries one canonical input — either the legacy\n * `input.json` (ListForge) or `input.newrecruit-json.json` (NewRecruit). Other\n * inputs are derived.\n */\nimport { readdirSync, readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Dataset } from \"./data/dataset.js\";\nimport { normalizeName } from \"./data/normalize.js\";\nimport { exportRoster, type ExportFormat } from \"./export/index.js\";\nimport { importRoster } from \"./import/import-roster.js\";\nimport type { Roster } from \"./import/types.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REPO_ROOT = join(__dirname, \"../..\");\nconst CONFORMANCE = join(REPO_ROOT, \"conformance\");\n\nconst NORMALIZE_INPUTS = [\n // NFD diacritic strip\n \"Khârn the Betrayer\",\n \"Brôkhyr\",\n \"Ûthar\",\n \"Magnús\",\n // apostrophe / quote variants\n \"T'au\",\n \"Be’lakor\",\n \"Kor’sarro Khan\",\n \"Aetaos'rau'keres\",\n \"‘quoted’\",\n // whitespace / hyphen collapse + trim\n \"Brôkhyr Iron-master\",\n \" the betrayer \",\n \"space--marines\",\n // casefold\n \"KHÂRN THE BETRAYER\",\n // already-normalized (idempotence)\n \"kharn the betrayer\",\n // distinctness anchors (must NOT collapse together)\n \"Khorne\",\n \"Khârn\",\n];\n\nfunction writeJson(path: string, value: unknown): void {\n writeFileSync(path, `${JSON.stringify(value, null, 2)}\\n`);\n}\n\nfunction writeText(path: string, value: string): void {\n writeFileSync(path, value);\n}\n\nfunction genNormalize(): void {\n const table = NORMALIZE_INPUTS.map((input) => ({ input, expected: normalizeName(input) }));\n writeJson(join(CONFORMANCE, \"normalize.json\"), table);\n console.log(`normalize.json: ${table.length} cases`);\n}\n\n/** Locate the canonical input for a fixture dir: prefer `input.json` (legacy\n * ListForge), fall back to `input.newrecruit-json.json` (NewRecruit). */\nfunction seedRoster(caseDir: string, ds: Dataset): Roster {\n const candidates = [\"input.json\", \"input.newrecruit-json.json\"];\n for (const name of candidates) {\n const path = join(caseDir, name);\n if (existsSync(path)) {\n const decoded = JSON.parse(readFileSync(path, \"utf8\"));\n return importRoster(decoded, { dataset: ds });\n }\n }\n throw new Error(`no canonical input found in ${caseDir}`);\n}\n\nconst TEXT_FORMATS: { format: ExportFormat; inputName: string; goldenName: string }[] = [\n {\n format: \"newrecruit-wtc-compact\",\n inputName: \"input.newrecruit-wtc-compact.txt\",\n goldenName: \"expected.newrecruit-wtc-compact.txt\",\n },\n {\n format: \"newrecruit-wtc-full\",\n inputName: \"input.newrecruit-wtc-full.txt\",\n goldenName: \"expected.newrecruit-wtc-full.txt\",\n },\n {\n format: \"newrecruit-simple\",\n inputName: \"input.newrecruit-simple.txt\",\n goldenName: \"expected.newrecruit-simple.txt\",\n },\n];\n\nfunction genRosters(): void {\n const ds = Dataset.embedded();\n const rosterDir = join(CONFORMANCE, \"roster\");\n for (const entry of readdirSync(rosterDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const caseDir = join(rosterDir, entry.name);\n\n const seed = seedRoster(caseDir, ds);\n writeJson(join(caseDir, \"expected.roster.json\"), seed);\n\n // JSON export golden — NewRecruit-shaped skeleton.\n const jsonOut = exportRoster(seed, \"newrecruit-json\");\n writeJson(join(caseDir, \"expected.newrecruit-json.json\"), JSON.parse(jsonOut));\n\n // Canonical Roster JSON export — should equal the resolved roster.\n writeJson(join(caseDir, \"expected.roster-json.json\"), JSON.parse(exportRoster(seed, \"roster-json\")));\n\n // Text exports: always write the export golden so the cross-implementation\n // byte-equality check has something to compare against. Only write the\n // `input.*.txt` round-trip seed when the fixture was authored for the\n // NewRecruit pipeline — legacy ListForge fixtures carry decoration\n // (multi-force warnings, leader-attachment inference) that the simple/wtc\n // exporters can't fully preserve, so the round-trip would fail\n // structurally rather than uncover a parser bug.\n const isNewRecruitSeed = existsSync(join(caseDir, \"input.newrecruit-json.json\"));\n for (const { format, inputName, goldenName } of TEXT_FORMATS) {\n const out = exportRoster(seed, format);\n writeText(join(caseDir, goldenName), out);\n if (isNewRecruitSeed) {\n writeText(join(caseDir, inputName), out);\n }\n }\n\n console.log(\n `roster/${entry.name}: ${seed.units.length} units, ${seed.diagnostics.warnings.length} warnings`,\n );\n }\n}\n\ngenNormalize();\ngenRosters();\n"]}