@alpaca-software/40kdc-data 0.1.1 → 0.1.3
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.
- package/README.md +2 -0
- package/dist/abilities-resolver/index.d.ts +9 -0
- package/dist/abilities-resolver/index.d.ts.map +1 -0
- package/dist/abilities-resolver/index.js +9 -0
- package/dist/abilities-resolver/index.js.map +1 -0
- package/dist/abilities-resolver/resolver.d.ts +73 -0
- package/dist/abilities-resolver/resolver.d.ts.map +1 -0
- package/dist/abilities-resolver/resolver.js +142 -0
- package/dist/abilities-resolver/resolver.js.map +1 -0
- package/dist/audit-coverage.d.ts +78 -0
- package/dist/audit-coverage.d.ts.map +1 -0
- package/dist/audit-coverage.js +341 -0
- package/dist/audit-coverage.js.map +1 -0
- package/dist/author-batch.d.ts +147 -0
- package/dist/author-batch.d.ts.map +1 -0
- package/dist/author-batch.js +675 -0
- package/dist/author-batch.js.map +1 -0
- package/dist/author-input.d.ts +37 -0
- package/dist/author-input.d.ts.map +1 -0
- package/dist/author-input.js +162 -0
- package/dist/author-input.js.map +1 -0
- package/dist/bundle-schemas.d.ts +1 -0
- package/dist/bundle-schemas.d.ts.map +1 -0
- package/dist/bundle-schemas.js +1 -0
- package/dist/bundle-schemas.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen-data.d.ts +1 -0
- package/dist/codegen-data.d.ts.map +1 -0
- package/dist/codegen-data.js +2 -0
- package/dist/codegen-data.js.map +1 -0
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +1 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/translate.d.ts +1 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +10 -4
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/validate-all.d.ts +1 -0
- package/dist/commands/validate-all.d.ts.map +1 -0
- package/dist/commands/validate-all.js +1 -0
- package/dist/commands/validate-all.js.map +1 -0
- package/dist/commands/validate-core.d.ts +1 -0
- package/dist/commands/validate-core.d.ts.map +1 -0
- package/dist/commands/validate-core.js +1 -0
- package/dist/commands/validate-core.js.map +1 -0
- package/dist/commands/validate-enrichment.d.ts +1 -0
- package/dist/commands/validate-enrichment.d.ts.map +1 -0
- package/dist/commands/validate-enrichment.js +1 -0
- package/dist/commands/validate-enrichment.js.map +1 -0
- package/dist/convert-faction.d.ts +1 -0
- package/dist/convert-faction.d.ts.map +1 -0
- package/dist/convert-faction.js +1 -0
- package/dist/convert-faction.js.map +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts.map +1 -0
- package/dist/converters/configs/adepta-sororitas.js +1 -0
- package/dist/converters/configs/adepta-sororitas.js.map +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-astartes.js +1 -0
- package/dist/converters/configs/adeptus-astartes.js.map +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-custodes.js +1 -0
- package/dist/converters/configs/adeptus-custodes.js.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js.map +1 -0
- package/dist/converters/configs/aeldari.d.ts +1 -0
- package/dist/converters/configs/aeldari.d.ts.map +1 -0
- package/dist/converters/configs/aeldari.js +1 -0
- package/dist/converters/configs/aeldari.js.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js.map +1 -0
- package/dist/converters/configs/astra-militarum.d.ts +1 -0
- package/dist/converters/configs/astra-militarum.d.ts.map +1 -0
- package/dist/converters/configs/astra-militarum.js +1 -0
- package/dist/converters/configs/astra-militarum.js.map +1 -0
- package/dist/converters/configs/black-templars.d.ts +1 -0
- package/dist/converters/configs/black-templars.d.ts.map +1 -0
- package/dist/converters/configs/black-templars.js +1 -0
- package/dist/converters/configs/black-templars.js.map +1 -0
- package/dist/converters/configs/blood-angels.d.ts +1 -0
- package/dist/converters/configs/blood-angels.d.ts.map +1 -0
- package/dist/converters/configs/blood-angels.js +1 -0
- package/dist/converters/configs/blood-angels.js.map +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts.map +1 -0
- package/dist/converters/configs/chaos-daemons.js +1 -0
- package/dist/converters/configs/chaos-daemons.js.map +1 -0
- package/dist/converters/configs/chaos-knights.d.ts +1 -0
- package/dist/converters/configs/chaos-knights.d.ts.map +1 -0
- package/dist/converters/configs/chaos-knights.js +1 -0
- package/dist/converters/configs/chaos-knights.js.map +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts.map +1 -0
- package/dist/converters/configs/chaos-space-marines.js +1 -0
- package/dist/converters/configs/chaos-space-marines.js.map +1 -0
- package/dist/converters/configs/crimson-fists.d.ts +1 -0
- package/dist/converters/configs/crimson-fists.d.ts.map +1 -0
- package/dist/converters/configs/crimson-fists.js +1 -0
- package/dist/converters/configs/crimson-fists.js.map +1 -0
- package/dist/converters/configs/dark-angels.d.ts +1 -0
- package/dist/converters/configs/dark-angels.d.ts.map +1 -0
- package/dist/converters/configs/dark-angels.js +1 -0
- package/dist/converters/configs/dark-angels.js.map +1 -0
- package/dist/converters/configs/death-guard.d.ts +1 -0
- package/dist/converters/configs/death-guard.d.ts.map +1 -0
- package/dist/converters/configs/death-guard.js +1 -0
- package/dist/converters/configs/death-guard.js.map +1 -0
- package/dist/converters/configs/deathwatch.d.ts +1 -0
- package/dist/converters/configs/deathwatch.d.ts.map +1 -0
- package/dist/converters/configs/deathwatch.js +1 -0
- package/dist/converters/configs/deathwatch.js.map +1 -0
- package/dist/converters/configs/drukhari.d.ts +1 -0
- package/dist/converters/configs/drukhari.d.ts.map +1 -0
- package/dist/converters/configs/drukhari.js +1 -0
- package/dist/converters/configs/drukhari.js.map +1 -0
- package/dist/converters/configs/emperors-children.d.ts +1 -0
- package/dist/converters/configs/emperors-children.d.ts.map +1 -0
- package/dist/converters/configs/emperors-children.js +1 -0
- package/dist/converters/configs/emperors-children.js.map +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts.map +1 -0
- package/dist/converters/configs/genestealer-cults.js +1 -0
- package/dist/converters/configs/genestealer-cults.js.map +1 -0
- package/dist/converters/configs/grey-knights.d.ts +1 -0
- package/dist/converters/configs/grey-knights.d.ts.map +1 -0
- package/dist/converters/configs/grey-knights.js +1 -0
- package/dist/converters/configs/grey-knights.js.map +1 -0
- package/dist/converters/configs/imperial-fists.d.ts +1 -0
- package/dist/converters/configs/imperial-fists.d.ts.map +1 -0
- package/dist/converters/configs/imperial-fists.js +1 -0
- package/dist/converters/configs/imperial-fists.js.map +1 -0
- package/dist/converters/configs/imperial-knights.d.ts +1 -0
- package/dist/converters/configs/imperial-knights.d.ts.map +1 -0
- package/dist/converters/configs/imperial-knights.js +1 -0
- package/dist/converters/configs/imperial-knights.js.map +1 -0
- package/dist/converters/configs/iron-hands.d.ts +1 -0
- package/dist/converters/configs/iron-hands.d.ts.map +1 -0
- package/dist/converters/configs/iron-hands.js +1 -0
- package/dist/converters/configs/iron-hands.js.map +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts.map +1 -0
- package/dist/converters/configs/leagues-of-votann.js +1 -0
- package/dist/converters/configs/leagues-of-votann.js.map +1 -0
- package/dist/converters/configs/necrons.d.ts +1 -0
- package/dist/converters/configs/necrons.d.ts.map +1 -0
- package/dist/converters/configs/necrons.js +1 -0
- package/dist/converters/configs/necrons.js.map +1 -0
- package/dist/converters/configs/orks.d.ts +1 -0
- package/dist/converters/configs/orks.d.ts.map +1 -0
- package/dist/converters/configs/orks.js +1 -0
- package/dist/converters/configs/orks.js.map +1 -0
- package/dist/converters/configs/raven-guard.d.ts +1 -0
- package/dist/converters/configs/raven-guard.d.ts.map +1 -0
- package/dist/converters/configs/raven-guard.js +1 -0
- package/dist/converters/configs/raven-guard.js.map +1 -0
- package/dist/converters/configs/salamanders.d.ts +1 -0
- package/dist/converters/configs/salamanders.d.ts.map +1 -0
- package/dist/converters/configs/salamanders.js +1 -0
- package/dist/converters/configs/salamanders.js.map +1 -0
- package/dist/converters/configs/space-wolves.d.ts +1 -0
- package/dist/converters/configs/space-wolves.d.ts.map +1 -0
- package/dist/converters/configs/space-wolves.js +1 -0
- package/dist/converters/configs/space-wolves.js.map +1 -0
- package/dist/converters/configs/tau-empire.d.ts +1 -0
- package/dist/converters/configs/tau-empire.d.ts.map +1 -0
- package/dist/converters/configs/tau-empire.js +1 -0
- package/dist/converters/configs/tau-empire.js.map +1 -0
- package/dist/converters/configs/thousand-sons.d.ts +1 -0
- package/dist/converters/configs/thousand-sons.d.ts.map +1 -0
- package/dist/converters/configs/thousand-sons.js +1 -0
- package/dist/converters/configs/thousand-sons.js.map +1 -0
- package/dist/converters/configs/tyranids.d.ts +1 -0
- package/dist/converters/configs/tyranids.d.ts.map +1 -0
- package/dist/converters/configs/tyranids.js +1 -0
- package/dist/converters/configs/tyranids.js.map +1 -0
- package/dist/converters/configs/ultramarines.d.ts +1 -0
- package/dist/converters/configs/ultramarines.d.ts.map +1 -0
- package/dist/converters/configs/ultramarines.js +1 -0
- package/dist/converters/configs/ultramarines.js.map +1 -0
- package/dist/converters/configs/white-scars.d.ts +1 -0
- package/dist/converters/configs/white-scars.d.ts.map +1 -0
- package/dist/converters/configs/white-scars.js +1 -0
- package/dist/converters/configs/white-scars.js.map +1 -0
- package/dist/converters/configs/world-eaters.d.ts +1 -0
- package/dist/converters/configs/world-eaters.d.ts.map +1 -0
- package/dist/converters/configs/world-eaters.js +1 -0
- package/dist/converters/configs/world-eaters.js.map +1 -0
- package/dist/converters/faction-config.d.ts +1 -0
- package/dist/converters/faction-config.d.ts.map +1 -0
- package/dist/converters/faction-config.js +1 -0
- package/dist/converters/faction-config.js.map +1 -0
- package/dist/converters/id-generator.d.ts +1 -0
- package/dist/converters/id-generator.d.ts.map +1 -0
- package/dist/converters/id-generator.js +1 -0
- package/dist/converters/id-generator.js.map +1 -0
- package/dist/converters/keyword-filter.d.ts +1 -0
- package/dist/converters/keyword-filter.d.ts.map +1 -0
- package/dist/converters/keyword-filter.js +1 -0
- package/dist/converters/keyword-filter.js.map +1 -0
- package/dist/converters/stat-parser.d.ts +1 -0
- package/dist/converters/stat-parser.d.ts.map +1 -0
- package/dist/converters/stat-parser.js +1 -0
- package/dist/converters/stat-parser.js.map +1 -0
- package/dist/converters/view-selector.d.ts +1 -0
- package/dist/converters/view-selector.d.ts.map +1 -0
- package/dist/converters/view-selector.js +1 -0
- package/dist/converters/view-selector.js.map +1 -0
- package/dist/converters/weapon-dedup.d.ts +1 -0
- package/dist/converters/weapon-dedup.d.ts.map +1 -0
- package/dist/converters/weapon-dedup.js +1 -0
- package/dist/converters/weapon-dedup.js.map +1 -0
- package/dist/cruncher/attribution.d.ts +66 -0
- package/dist/cruncher/attribution.d.ts.map +1 -0
- package/dist/cruncher/attribution.js +88 -0
- package/dist/cruncher/attribution.js.map +1 -0
- package/dist/cruncher/buffs.d.ts +206 -0
- package/dist/cruncher/buffs.d.ts.map +1 -0
- package/dist/cruncher/buffs.js +150 -0
- package/dist/cruncher/buffs.js.map +1 -0
- package/dist/cruncher/engine.d.ts +50 -0
- package/dist/cruncher/engine.d.ts.map +1 -0
- package/dist/cruncher/engine.js +312 -0
- package/dist/cruncher/engine.js.map +1 -0
- package/dist/cruncher/from-dsl.d.ts +101 -0
- package/dist/cruncher/from-dsl.d.ts.map +1 -0
- package/dist/cruncher/from-dsl.js +968 -0
- package/dist/cruncher/from-dsl.js.map +1 -0
- package/dist/cruncher/from-keyword.d.ts +35 -0
- package/dist/cruncher/from-keyword.d.ts.map +1 -0
- package/dist/cruncher/from-keyword.js +159 -0
- package/dist/cruncher/from-keyword.js.map +1 -0
- package/dist/cruncher/get-buffs.d.ts +12 -0
- package/dist/cruncher/get-buffs.d.ts.map +1 -0
- package/dist/cruncher/get-buffs.js +7 -0
- package/dist/cruncher/get-buffs.js.map +1 -0
- package/dist/cruncher/index.d.ts +12 -0
- package/dist/cruncher/index.d.ts.map +1 -0
- package/dist/cruncher/index.js +12 -0
- package/dist/cruncher/index.js.map +1 -0
- package/dist/data/bundle.generated.d.ts +1 -0
- package/dist/data/bundle.generated.d.ts.map +1 -0
- package/dist/data/bundle.generated.js +2 -1
- package/dist/data/bundle.generated.js.map +1 -0
- package/dist/data/collection.d.ts +10 -0
- package/dist/data/collection.d.ts.map +1 -0
- package/dist/data/collection.js +15 -0
- package/dist/data/collection.js.map +1 -0
- package/dist/data/dataset.d.ts +132 -2
- package/dist/data/dataset.d.ts.map +1 -0
- package/dist/data/dataset.js +248 -1
- package/dist/data/dataset.js.map +1 -0
- package/dist/data/entities.d.ts +67 -2
- package/dist/data/entities.d.ts.map +1 -0
- package/dist/data/entities.js +122 -0
- package/dist/data/entities.js.map +1 -0
- package/dist/data/index.d.ts +10 -1
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +14 -1
- package/dist/data/index.js.map +1 -0
- package/dist/data/normalize.d.ts +1 -0
- package/dist/data/normalize.d.ts.map +1 -0
- package/dist/data/normalize.js +1 -0
- package/dist/data/normalize.js.map +1 -0
- package/dist/data/roster-resolve.d.ts +58 -0
- package/dist/data/roster-resolve.d.ts.map +1 -0
- package/dist/data/roster-resolve.js +82 -0
- package/dist/data/roster-resolve.js.map +1 -0
- package/dist/data/types.d.ts +4 -1
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +2 -0
- package/dist/data/types.js.map +1 -0
- package/dist/export/helpers.d.ts +33 -0
- package/dist/export/helpers.d.ts.map +1 -0
- package/dist/export/helpers.js +57 -0
- package/dist/export/helpers.js.map +1 -0
- package/dist/export/index.d.ts +22 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +28 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/newrecruit-json.d.ts +3 -0
- package/dist/export/newrecruit-json.d.ts.map +1 -0
- package/dist/export/newrecruit-json.js +140 -0
- package/dist/export/newrecruit-json.js.map +1 -0
- package/dist/export/newrecruit-simple.d.ts +3 -0
- package/dist/export/newrecruit-simple.d.ts.map +1 -0
- package/dist/export/newrecruit-simple.js +76 -0
- package/dist/export/newrecruit-simple.js.map +1 -0
- package/dist/export/newrecruit-wtc.d.ts +4 -0
- package/dist/export/newrecruit-wtc.d.ts.map +1 -0
- package/dist/export/newrecruit-wtc.js +142 -0
- package/dist/export/newrecruit-wtc.js.map +1 -0
- package/dist/export/roster-json.d.ts +3 -0
- package/dist/export/roster-json.d.ts.map +1 -0
- package/dist/export/roster-json.js +8 -0
- package/dist/export/roster-json.js.map +1 -0
- package/dist/export/rosterizer.d.ts +3 -0
- package/dist/export/rosterizer.d.ts.map +1 -0
- package/dist/export/rosterizer.js +144 -0
- package/dist/export/rosterizer.js.map +1 -0
- package/dist/export/serializer.d.ts +27 -0
- package/dist/export/serializer.d.ts.map +1 -0
- package/dist/export/serializer.js +2 -0
- package/dist/export/serializer.js.map +1 -0
- package/dist/gen-conformance.d.ts +1 -0
- package/dist/gen-conformance.d.ts.map +1 -0
- package/dist/gen-conformance.js +274 -12
- package/dist/gen-conformance.js.map +1 -0
- package/dist/generated.d.ts +194 -118
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +1 -0
- package/dist/generated.js.map +1 -0
- package/dist/import/adapter.d.ts +4 -3
- package/dist/import/adapter.d.ts.map +1 -0
- package/dist/import/adapter.js +1 -0
- package/dist/import/adapter.js.map +1 -0
- package/dist/import/decode.d.ts +1 -0
- package/dist/import/decode.d.ts.map +1 -0
- package/dist/import/decode.js +1 -0
- package/dist/import/decode.js.map +1 -0
- package/dist/import/gw.d.ts +69 -0
- package/dist/import/gw.d.ts.map +1 -0
- package/dist/import/gw.js +245 -0
- package/dist/import/gw.js.map +1 -0
- package/dist/import/import-roster.d.ts +84 -0
- package/dist/import/import-roster.d.ts.map +1 -0
- package/dist/import/import-roster.js +207 -0
- package/dist/import/import-roster.js.map +1 -0
- package/dist/import/index.d.ts +7 -3
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +5 -1
- package/dist/import/index.js.map +1 -0
- package/dist/import/listforge.d.ts +1 -0
- package/dist/import/listforge.d.ts.map +1 -0
- package/dist/import/listforge.js +22 -2
- package/dist/import/listforge.js.map +1 -0
- package/dist/import/newrecruit-json.d.ts +31 -0
- package/dist/import/newrecruit-json.d.ts.map +1 -0
- package/dist/import/newrecruit-json.js +224 -0
- package/dist/import/newrecruit-json.js.map +1 -0
- package/dist/import/newrecruit-simple.d.ts +29 -0
- package/dist/import/newrecruit-simple.d.ts.map +1 -0
- package/dist/import/newrecruit-simple.js +200 -0
- package/dist/import/newrecruit-simple.js.map +1 -0
- package/dist/import/newrecruit-text.d.ts +51 -0
- package/dist/import/newrecruit-text.d.ts.map +1 -0
- package/dist/import/newrecruit-text.js +102 -0
- package/dist/import/newrecruit-text.js.map +1 -0
- package/dist/import/newrecruit-wtc.d.ts +36 -0
- package/dist/import/newrecruit-wtc.d.ts.map +1 -0
- package/dist/import/newrecruit-wtc.js +337 -0
- package/dist/import/newrecruit-wtc.js.map +1 -0
- package/dist/import/resolve.d.ts +3 -2
- package/dist/import/resolve.d.ts.map +1 -0
- package/dist/import/resolve.js +5 -2
- package/dist/import/resolve.js.map +1 -0
- package/dist/import/rosterizer.d.ts +70 -0
- package/dist/import/rosterizer.d.ts.map +1 -0
- package/dist/import/rosterizer.js +348 -0
- package/dist/import/rosterizer.js.map +1 -0
- package/dist/import/types.d.ts +11 -1
- package/dist/import/types.d.ts.map +1 -0
- package/dist/import/types.js +1 -0
- package/dist/import/types.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -0
- package/dist/known-support-10e.d.ts +1 -0
- package/dist/known-support-10e.d.ts.map +1 -0
- package/dist/known-support-10e.js +1 -0
- package/dist/known-support-10e.js.map +1 -0
- package/dist/link-abilities.d.ts +41 -0
- package/dist/link-abilities.d.ts.map +1 -0
- package/dist/link-abilities.js +159 -0
- package/dist/link-abilities.js.map +1 -0
- package/dist/migrations/2026-weapon-keywords.d.ts +2 -0
- package/dist/migrations/2026-weapon-keywords.d.ts.map +1 -0
- package/dist/migrations/2026-weapon-keywords.js +247 -0
- package/dist/migrations/2026-weapon-keywords.js.map +1 -0
- package/dist/port-10e-faction.d.ts +1 -0
- package/dist/port-10e-faction.d.ts.map +1 -0
- package/dist/port-10e-faction.js +1 -0
- package/dist/port-10e-faction.js.map +1 -0
- package/dist/report.d.ts +1 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +1 -0
- package/dist/report.js.map +1 -0
- package/dist/rube-goldberg.d.ts +3 -0
- package/dist/rube-goldberg.d.ts.map +1 -0
- package/dist/rube-goldberg.js +109 -0
- package/dist/rube-goldberg.js.map +1 -0
- package/dist/runner.d.ts +38 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +492 -0
- package/dist/runner.js.map +1 -0
- package/dist/schema-loader.d.ts +1 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/schema-loader.js +1 -0
- package/dist/schema-loader.js.map +1 -0
- package/dist/scrub-ip.d.ts +14 -0
- package/dist/scrub-ip.d.ts.map +1 -0
- package/dist/scrub-ip.js +88 -0
- package/dist/scrub-ip.js.map +1 -0
- package/dist/validate.d.ts +1 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +2 -0
- package/dist/validate.js.map +1 -0
- package/package.json +15 -3
- package/schemas/core/roster.schema.json +19 -4
- package/schemas/core/weapon-keyword.schema.json +31 -0
- package/schemas/core/weapon.schema.json +22 -1
- package/schemas/enrichment/ability-dsl/effect.schema.json +23 -1
- package/dist/import/import-listforge.d.ts +0 -23
- package/dist/import/import-listforge.js +0 -32
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"rosterizer.d.ts","sourceRoot":"","sources":["../../src/export/rosterizer.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAqIxD,eAAO,MAAM,oBAAoB,EAAE,gBAuClC,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { prettyJson, titleCaseId, totalArmyPoints } from "./helpers.js";
|
|
2
|
+
// Mirror the importer's constants (kept inline rather than imported so the
|
|
3
|
+
// exporter stays decoupled — the seams are the `item` keys themselves).
|
|
4
|
+
const CLS_ROSTER = "Roster";
|
|
5
|
+
const CLS_FACTION = "Faction";
|
|
6
|
+
const CLS_DETACHMENT = "Detachment";
|
|
7
|
+
const CLS_UNIT = "Unit";
|
|
8
|
+
const CLS_WEAPON = "Weapon";
|
|
9
|
+
const CLS_ENHANCEMENT = "Enhancement";
|
|
10
|
+
const CLS_BATTLE_SIZE = "Battle Size";
|
|
11
|
+
const CLS_TRAIT = "Trait";
|
|
12
|
+
const DSG_WARLORD = "Warlord";
|
|
13
|
+
const RULEBOOK_NAME = "40kdc";
|
|
14
|
+
const RULEBOOK_GAME = "Warhammer 40,000";
|
|
15
|
+
const RULEBOOK_PUBLISHER = "Tabletop Developer Consortium";
|
|
16
|
+
const RULEBOOK_URL = "https://40kdc.dev";
|
|
17
|
+
const RULEBOOK_GENRE = "wargame";
|
|
18
|
+
function key(classification, designation) {
|
|
19
|
+
return `${classification}§${designation}`; // §
|
|
20
|
+
}
|
|
21
|
+
function pointsStat(value) {
|
|
22
|
+
if (value === null || value === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
return { Points: { value } };
|
|
25
|
+
}
|
|
26
|
+
function wargearAsset(w) {
|
|
27
|
+
return {
|
|
28
|
+
item: key(CLS_WEAPON, w.ref.raw_name),
|
|
29
|
+
name: w.ref.raw_name,
|
|
30
|
+
quantity: w.count,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function enhancementAsset(u) {
|
|
34
|
+
if (!u.enhancement)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
item: key(CLS_ENHANCEMENT, u.enhancement.raw_name),
|
|
38
|
+
name: u.enhancement.raw_name,
|
|
39
|
+
quantity: 1,
|
|
40
|
+
...(u.enhancement_points !== null
|
|
41
|
+
? { stats: pointsStat(u.enhancement_points) }
|
|
42
|
+
: {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function warlordTraitAsset() {
|
|
46
|
+
return {
|
|
47
|
+
item: key(CLS_TRAIT, DSG_WARLORD),
|
|
48
|
+
name: DSG_WARLORD,
|
|
49
|
+
quantity: 1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function unitAsset(u) {
|
|
53
|
+
const included = [];
|
|
54
|
+
const enh = enhancementAsset(u);
|
|
55
|
+
if (enh !== null)
|
|
56
|
+
included.push(enh);
|
|
57
|
+
for (const w of u.wargear)
|
|
58
|
+
included.push(wargearAsset(w));
|
|
59
|
+
const traits = [];
|
|
60
|
+
if (u.is_warlord)
|
|
61
|
+
traits.push(warlordTraitAsset());
|
|
62
|
+
const asset = {
|
|
63
|
+
item: key(CLS_UNIT, u.ref.raw_name),
|
|
64
|
+
name: u.ref.raw_name,
|
|
65
|
+
quantity: u.model_count,
|
|
66
|
+
};
|
|
67
|
+
const stats = pointsStat(u.points);
|
|
68
|
+
if (stats !== undefined)
|
|
69
|
+
asset.stats = stats;
|
|
70
|
+
if (included.length > 0 || traits.length > 0) {
|
|
71
|
+
asset.assets = {};
|
|
72
|
+
if (included.length > 0)
|
|
73
|
+
asset.assets.included = included;
|
|
74
|
+
if (traits.length > 0)
|
|
75
|
+
asset.assets.traits = traits;
|
|
76
|
+
}
|
|
77
|
+
return asset;
|
|
78
|
+
}
|
|
79
|
+
function factionAsset(roster) {
|
|
80
|
+
const display = titleCaseId(roster.faction_id);
|
|
81
|
+
if (display === null)
|
|
82
|
+
return null;
|
|
83
|
+
return { item: key(CLS_FACTION, display), name: display, quantity: 1 };
|
|
84
|
+
}
|
|
85
|
+
function detachmentAsset(roster) {
|
|
86
|
+
const display = titleCaseId(roster.detachment_id);
|
|
87
|
+
if (display === null)
|
|
88
|
+
return null;
|
|
89
|
+
return { item: key(CLS_DETACHMENT, display), name: display, quantity: 1 };
|
|
90
|
+
}
|
|
91
|
+
function battleSizeAsset(roster) {
|
|
92
|
+
if (roster.battle_size === "strike-force") {
|
|
93
|
+
const limit = roster.points.declared_limit ?? 2000;
|
|
94
|
+
const label = `Strike Force (${limit} Point limit)`;
|
|
95
|
+
return { item: key(CLS_BATTLE_SIZE, label), name: label, quantity: 1 };
|
|
96
|
+
}
|
|
97
|
+
if (roster.battle_size === "incursion") {
|
|
98
|
+
const limit = roster.points.declared_limit ?? 1000;
|
|
99
|
+
const label = `Incursion (${limit} Point limit)`;
|
|
100
|
+
return { item: key(CLS_BATTLE_SIZE, label), name: label, quantity: 1 };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
export const rosterizerSerializer = {
|
|
105
|
+
id: "rosterizer",
|
|
106
|
+
serialize(roster) {
|
|
107
|
+
const included = [];
|
|
108
|
+
const faction = factionAsset(roster);
|
|
109
|
+
if (faction)
|
|
110
|
+
included.push(faction);
|
|
111
|
+
const detachment = detachmentAsset(roster);
|
|
112
|
+
if (detachment)
|
|
113
|
+
included.push(detachment);
|
|
114
|
+
const battleSize = battleSizeAsset(roster);
|
|
115
|
+
if (battleSize)
|
|
116
|
+
included.push(battleSize);
|
|
117
|
+
for (const u of roster.units)
|
|
118
|
+
included.push(unitAsset(u));
|
|
119
|
+
const total = totalArmyPoints(roster);
|
|
120
|
+
const snapshot = {
|
|
121
|
+
item: key(CLS_ROSTER, CLS_ROSTER),
|
|
122
|
+
name: roster.name,
|
|
123
|
+
quantity: 1,
|
|
124
|
+
...(total > 0 ? { stats: pointsStat(total) } : {}),
|
|
125
|
+
assets: { included },
|
|
126
|
+
};
|
|
127
|
+
const envelope = {
|
|
128
|
+
slug: "",
|
|
129
|
+
key: "",
|
|
130
|
+
visible: "hidden",
|
|
131
|
+
locked: false,
|
|
132
|
+
rulebook: {
|
|
133
|
+
name: RULEBOOK_NAME,
|
|
134
|
+
game: RULEBOOK_GAME,
|
|
135
|
+
publisher: RULEBOOK_PUBLISHER,
|
|
136
|
+
url: RULEBOOK_URL,
|
|
137
|
+
genre: RULEBOOK_GENRE,
|
|
138
|
+
},
|
|
139
|
+
snapshot,
|
|
140
|
+
};
|
|
141
|
+
return prettyJson(envelope);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=rosterizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rosterizer.js","sourceRoot":"","sources":["../../src/export/rosterizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Rosterizer serializer — emits a Rosterizer-shaped roster JSON skeleton that\n * round-trips through {@link rosterizerAdapter}.\n *\n * The shape carries only fields the importer reads: `rulebook` (envelope),\n * `snapshot` (an `Asset` tree rooted at `Roster§Roster`), and per-unit\n * `item`/`name`/`quantity`/`stats.Points.value`/`assets.included`/`assets.traits`.\n * No `text`, `description`, `rules`, `lineage`, `_layers`, `classIdentity`,\n * `processed`, or `bareResourceKey` ever appear — they aren't stored in the\n * Roster and emitting them could leak prose.\n *\n * Faction and detachment display names come from {@link titleCaseId} — the\n * Roster doesn't carry the source's raw faction name, so we reconstruct it\n * from the kebab-case id. Same lossy hop as the NewRecruit JSON serializer.\n *\n * @packageDocumentation\n */\nimport type { Roster, RosterUnit, RosterWargear } from \"../import/types.js\";\nimport { prettyJson, titleCaseId, totalArmyPoints } from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\n// Mirror the importer's constants (kept inline rather than imported so the\n// exporter stays decoupled — the seams are the `item` keys themselves).\nconst CLS_ROSTER = \"Roster\";\nconst CLS_FACTION = \"Faction\";\nconst CLS_DETACHMENT = \"Detachment\";\nconst CLS_UNIT = \"Unit\";\nconst CLS_WEAPON = \"Weapon\";\nconst CLS_ENHANCEMENT = \"Enhancement\";\nconst CLS_BATTLE_SIZE = \"Battle Size\";\nconst CLS_TRAIT = \"Trait\";\nconst DSG_WARLORD = \"Warlord\";\n\nconst RULEBOOK_NAME = \"40kdc\";\nconst RULEBOOK_GAME = \"Warhammer 40,000\";\nconst RULEBOOK_PUBLISHER = \"Tabletop Developer Consortium\";\nconst RULEBOOK_URL = \"https://40kdc.dev\";\nconst RULEBOOK_GENRE = \"wargame\";\n\ninterface Asset {\n item: string;\n name?: string;\n quantity?: number;\n stats?: Record<string, { value: number }>;\n assets?: {\n included?: Asset[];\n traits?: Asset[];\n };\n}\n\ninterface Envelope {\n slug: string;\n key: string;\n visible: \"hidden\" | \"public\" | \"friends\";\n locked: boolean;\n rulebook: {\n name: string;\n game: string;\n publisher: string;\n url: string;\n genre: string;\n };\n snapshot: Asset;\n}\n\nfunction key(classification: string, designation: string): string {\n return `${classification}§${designation}`; // §\n}\n\nfunction pointsStat(value: number | null | undefined): Record<string, { value: number }> | undefined {\n if (value === null || value === undefined) return undefined;\n return { Points: { value } };\n}\n\nfunction wargearAsset(w: RosterWargear): Asset {\n return {\n item: key(CLS_WEAPON, w.ref.raw_name),\n name: w.ref.raw_name,\n quantity: w.count,\n };\n}\n\nfunction enhancementAsset(u: RosterUnit): Asset | null {\n if (!u.enhancement) return null;\n return {\n item: key(CLS_ENHANCEMENT, u.enhancement.raw_name),\n name: u.enhancement.raw_name,\n quantity: 1,\n ...(u.enhancement_points !== null\n ? { stats: pointsStat(u.enhancement_points) }\n : {}),\n };\n}\n\nfunction warlordTraitAsset(): Asset {\n return {\n item: key(CLS_TRAIT, DSG_WARLORD),\n name: DSG_WARLORD,\n quantity: 1,\n };\n}\n\nfunction unitAsset(u: RosterUnit): Asset {\n const included: Asset[] = [];\n const enh = enhancementAsset(u);\n if (enh !== null) included.push(enh);\n for (const w of u.wargear) included.push(wargearAsset(w));\n\n const traits: Asset[] = [];\n if (u.is_warlord) traits.push(warlordTraitAsset());\n\n const asset: Asset = {\n item: key(CLS_UNIT, u.ref.raw_name),\n name: u.ref.raw_name,\n quantity: u.model_count,\n };\n const stats = pointsStat(u.points);\n if (stats !== undefined) asset.stats = stats;\n if (included.length > 0 || traits.length > 0) {\n asset.assets = {};\n if (included.length > 0) asset.assets.included = included;\n if (traits.length > 0) asset.assets.traits = traits;\n }\n return asset;\n}\n\nfunction factionAsset(roster: Roster): Asset | null {\n const display = titleCaseId(roster.faction_id);\n if (display === null) return null;\n return { item: key(CLS_FACTION, display), name: display, quantity: 1 };\n}\n\nfunction detachmentAsset(roster: Roster): Asset | null {\n const display = titleCaseId(roster.detachment_id);\n if (display === null) return null;\n return { item: key(CLS_DETACHMENT, display), name: display, quantity: 1 };\n}\n\nfunction battleSizeAsset(roster: Roster): Asset | null {\n if (roster.battle_size === \"strike-force\") {\n const limit = roster.points.declared_limit ?? 2000;\n const label = `Strike Force (${limit} Point limit)`;\n return { item: key(CLS_BATTLE_SIZE, label), name: label, quantity: 1 };\n }\n if (roster.battle_size === \"incursion\") {\n const limit = roster.points.declared_limit ?? 1000;\n const label = `Incursion (${limit} Point limit)`;\n return { item: key(CLS_BATTLE_SIZE, label), name: label, quantity: 1 };\n }\n return null;\n}\n\nexport const rosterizerSerializer: RosterSerializer = {\n id: \"rosterizer\",\n\n serialize(roster: Roster): string {\n const included: Asset[] = [];\n const faction = factionAsset(roster);\n if (faction) included.push(faction);\n const detachment = detachmentAsset(roster);\n if (detachment) included.push(detachment);\n const battleSize = battleSizeAsset(roster);\n if (battleSize) included.push(battleSize);\n for (const u of roster.units) included.push(unitAsset(u));\n\n const total = totalArmyPoints(roster);\n const snapshot: Asset = {\n item: key(CLS_ROSTER, CLS_ROSTER),\n name: roster.name,\n quantity: 1,\n ...(total > 0 ? { stats: pointsStat(total) } : {}),\n assets: { included },\n };\n\n const envelope: Envelope = {\n slug: \"\",\n key: \"\",\n visible: \"hidden\",\n locked: false,\n rulebook: {\n name: RULEBOOK_NAME,\n game: RULEBOOK_GAME,\n publisher: RULEBOOK_PUBLISHER,\n url: RULEBOOK_URL,\n genre: RULEBOOK_GENRE,\n },\n snapshot,\n };\n\n return prettyJson(envelope);\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" | "rosterizer";
|
|
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,GACb,YAAY,CAAC;AAEjB,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,YAAY,CAAC;IACjB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC"}
|
|
@@ -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 | \"rosterizer\";\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 @@
|
|
|
1
|
+
{"version":3,"file":"gen-conformance.d.ts","sourceRoot":"","sources":["../src/gen-conformance.ts"],"names":[],"mappings":""}
|
package/dist/gen-conformance.js
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate the cross-implementation conformance corpus under repo-root
|
|
3
|
-
* `conformance/`. The TypeScript package is the reference implementation, so
|
|
4
|
-
* goldens it emits are what the Rust crate must reproduce byte-for-byte
|
|
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
5
|
* (structurally). Run via `npm run gen:conformance`; CI regenerates and asserts
|
|
6
6
|
* `git diff --exit-code conformance/` is clean.
|
|
7
7
|
*
|
|
8
8
|
* Outputs:
|
|
9
9
|
* - `conformance/normalize.json` — `[{ input, expected }]` for normalizeName.
|
|
10
|
-
* - `conformance/roster/<case>/expected.roster.json` — the resolved Roster
|
|
11
|
-
*
|
|
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.
|
|
12
21
|
*/
|
|
13
|
-
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
14
23
|
import { dirname, join } from "node:path";
|
|
15
24
|
import { fileURLToPath } from "node:url";
|
|
16
25
|
import { Dataset } from "./data/dataset.js";
|
|
17
26
|
import { normalizeName } from "./data/normalize.js";
|
|
18
|
-
import {
|
|
27
|
+
import { exportRoster } from "./export/index.js";
|
|
28
|
+
import { importRoster, REGISTERED_ADAPTERS } from "./import/import-roster.js";
|
|
29
|
+
import { selectAdapter } from "./import/adapter.js";
|
|
30
|
+
import { attributeStages } from "./cruncher/attribution.js";
|
|
19
31
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
32
|
const REPO_ROOT = join(__dirname, "../..");
|
|
21
33
|
const CONFORMANCE = join(REPO_ROOT, "conformance");
|
|
22
|
-
/** Inputs for the normalize table — every rule, plus real accented/quoted names. */
|
|
23
34
|
const NORMALIZE_INPUTS = [
|
|
24
35
|
// NFD diacritic strip
|
|
25
36
|
"Khârn the Betrayer",
|
|
@@ -43,16 +54,83 @@ const NORMALIZE_INPUTS = [
|
|
|
43
54
|
// distinctness anchors (must NOT collapse together)
|
|
44
55
|
"Khorne",
|
|
45
56
|
"Khârn",
|
|
57
|
+
// Unicode whitespace beyond ASCII — every Unicode whitespace must collapse
|
|
58
|
+
// identically across implementations or `find("Khorne Lord")` and
|
|
59
|
+
// `find("Khorne Lord")` will silently disagree across ports.
|
|
60
|
+
"Khorne Lord",
|
|
61
|
+
"Khorne Lord",
|
|
62
|
+
// Turkish dotted-I: NFD decomposes to `I` + combining dot above; the dot is
|
|
63
|
+
// stripped, then locale-independent lowercase yields `i`. The case pins that
|
|
64
|
+
// no implementation introduces locale-aware casefolding (which would map
|
|
65
|
+
// `I` → `ı` under Turkish locale and break ASCII-text search).
|
|
66
|
+
"İmperial Fists",
|
|
67
|
+
// Zero-width joiner: passes through every step today. Pinned so behavior
|
|
68
|
+
// does not silently change — if a future commit strips Cf-category chars,
|
|
69
|
+
// this golden updates in the same PR.
|
|
70
|
+
"KhorneLord",
|
|
46
71
|
];
|
|
47
|
-
/** Pretty JSON with a trailing newline (matches the repo's 2-space convention). */
|
|
48
72
|
function writeJson(path, value) {
|
|
49
73
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
50
74
|
}
|
|
75
|
+
function writeText(path, value) {
|
|
76
|
+
writeFileSync(path, value);
|
|
77
|
+
}
|
|
51
78
|
function genNormalize() {
|
|
52
79
|
const table = NORMALIZE_INPUTS.map((input) => ({ input, expected: normalizeName(input) }));
|
|
53
80
|
writeJson(join(CONFORMANCE, "normalize.json"), table);
|
|
54
81
|
console.log(`normalize.json: ${table.length} cases`);
|
|
55
82
|
}
|
|
83
|
+
/** Locate the canonical input for a fixture dir: prefer `input.json` (legacy
|
|
84
|
+
* ListForge), then `input.newrecruit-json.json` (NewRecruit), then the
|
|
85
|
+
* text-only `input.gw.txt` (GW app export — import-only, like ListForge). */
|
|
86
|
+
function seedRoster(caseDir, ds) {
|
|
87
|
+
const decoded = decodeCanonicalSeed(caseDir);
|
|
88
|
+
return importRoster(decoded, { dataset: ds });
|
|
89
|
+
}
|
|
90
|
+
/** Return the decoded payload for the canonical seed — the same value the
|
|
91
|
+
* import pipeline would dispatch on. JSON seeds come back parsed; text seeds
|
|
92
|
+
* come back as the raw string. */
|
|
93
|
+
function decodeCanonicalSeed(caseDir) {
|
|
94
|
+
const jsonSeed = join(caseDir, "input.json");
|
|
95
|
+
if (existsSync(jsonSeed)) {
|
|
96
|
+
return JSON.parse(readFileSync(jsonSeed, "utf8"));
|
|
97
|
+
}
|
|
98
|
+
const nrSeed = join(caseDir, "input.newrecruit-json.json");
|
|
99
|
+
if (existsSync(nrSeed)) {
|
|
100
|
+
return JSON.parse(readFileSync(nrSeed, "utf8"));
|
|
101
|
+
}
|
|
102
|
+
const gwSeed = join(caseDir, "input.gw.txt");
|
|
103
|
+
if (existsSync(gwSeed)) {
|
|
104
|
+
return readFileSync(gwSeed, "utf8");
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`no canonical input found in ${caseDir}`);
|
|
107
|
+
}
|
|
108
|
+
/** Run a decoded payload through the adapter pipeline up to (but not past)
|
|
109
|
+
* resolution. The result is the format-agnostic ParsedRoster — the same
|
|
110
|
+
* intermediate the resolver consumes. Pinning this layer surfaces parser
|
|
111
|
+
* regressions even when resolution masks them. */
|
|
112
|
+
function parsedFromCanonicalSeed(caseDir) {
|
|
113
|
+
const decoded = decodeCanonicalSeed(caseDir);
|
|
114
|
+
const adapter = selectAdapter(decoded, [...REGISTERED_ADAPTERS]);
|
|
115
|
+
return adapter.parse(decoded);
|
|
116
|
+
}
|
|
117
|
+
const TEXT_FORMATS = [
|
|
118
|
+
{
|
|
119
|
+
format: "newrecruit-wtc-compact",
|
|
120
|
+
inputName: "input.newrecruit-wtc-compact.txt",
|
|
121
|
+
goldenName: "expected.newrecruit-wtc-compact.txt",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
format: "newrecruit-wtc-full",
|
|
125
|
+
inputName: "input.newrecruit-wtc-full.txt",
|
|
126
|
+
goldenName: "expected.newrecruit-wtc-full.txt",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
format: "newrecruit-simple",
|
|
130
|
+
inputName: "input.newrecruit-simple.txt",
|
|
131
|
+
goldenName: "expected.newrecruit-simple.txt",
|
|
132
|
+
},
|
|
133
|
+
];
|
|
56
134
|
function genRosters() {
|
|
57
135
|
const ds = Dataset.embedded();
|
|
58
136
|
const rosterDir = join(CONFORMANCE, "roster");
|
|
@@ -60,11 +138,195 @@ function genRosters() {
|
|
|
60
138
|
if (!entry.isDirectory())
|
|
61
139
|
continue;
|
|
62
140
|
const caseDir = join(rosterDir, entry.name);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
141
|
+
const seed = seedRoster(caseDir, ds);
|
|
142
|
+
writeJson(join(caseDir, "expected.roster.json"), seed);
|
|
143
|
+
// Parsed-stage golden — the intermediate ParsedRoster produced by the
|
|
144
|
+
// adapter for the canonical seed, before resolution. Catches parser bugs
|
|
145
|
+
// that resolution would otherwise mask (e.g. wrong unit count from a
|
|
146
|
+
// duplicate cost line that resolves to the same unit twice).
|
|
147
|
+
writeJson(join(caseDir, "expected.parsed.json"), parsedFromCanonicalSeed(caseDir));
|
|
148
|
+
// JSON export golden — NewRecruit-shaped skeleton.
|
|
149
|
+
const jsonOut = exportRoster(seed, "newrecruit-json");
|
|
150
|
+
writeJson(join(caseDir, "expected.newrecruit-json.json"), JSON.parse(jsonOut));
|
|
151
|
+
// Canonical Roster JSON export — should equal the resolved roster.
|
|
152
|
+
writeJson(join(caseDir, "expected.roster-json.json"), JSON.parse(exportRoster(seed, "roster-json")));
|
|
153
|
+
// Text exports: always write the export golden so every fixture exercises
|
|
154
|
+
// the cross-implementation byte-equality check. Only write the
|
|
155
|
+
// `input.*.txt` round-trip seed when the fixture was authored for the
|
|
156
|
+
// NewRecruit pipeline — legacy ListForge fixtures carry decoration
|
|
157
|
+
// (multi-force warnings, leader-attachment inference) that the simple/wtc
|
|
158
|
+
// exporters can't fully preserve, so the round-trip would fail
|
|
159
|
+
// structurally rather than uncover a parser bug.
|
|
160
|
+
const isNewRecruitSeed = existsSync(join(caseDir, "input.newrecruit-json.json"));
|
|
161
|
+
for (const { format, inputName, goldenName } of TEXT_FORMATS) {
|
|
162
|
+
const out = exportRoster(seed, format);
|
|
163
|
+
writeText(join(caseDir, goldenName), out);
|
|
164
|
+
if (isNewRecruitSeed) {
|
|
165
|
+
writeText(join(caseDir, inputName), out);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Rosterizer JSON export + a derived round-trip input. The exporter is
|
|
169
|
+
// deterministic and round-trips through the adapter, so emitting it as
|
|
170
|
+
// both `expected.rosterizer.json` and `input.rosterizer.json` pins the
|
|
171
|
+
// cross-implementation goldens and the importer regression at the same
|
|
172
|
+
// time. Same NewRecruit-seed gate as the text formats — multi-force
|
|
173
|
+
// ListForge fixtures lose their provisional leader-attachment under
|
|
174
|
+
// round-trip, so they only get the export golden, not the derived input.
|
|
175
|
+
const rosterizerOut = exportRoster(seed, "rosterizer");
|
|
176
|
+
writeJson(join(caseDir, "expected.rosterizer.json"), JSON.parse(rosterizerOut));
|
|
177
|
+
if (isNewRecruitSeed) {
|
|
178
|
+
writeJson(join(caseDir, "input.rosterizer.json"), JSON.parse(rosterizerOut));
|
|
179
|
+
}
|
|
180
|
+
console.log(`roster/${entry.name}: ${seed.units.length} units, ${seed.diagnostics.warnings.length} warnings`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const LINKED_API_QUERIES = [
|
|
184
|
+
// find_unit: diacritic-insensitive lookup, miss returns null.
|
|
185
|
+
{ name: "find_unit by diacritic name", query: "find_unit", args: { query: "Kharn" }, comparison: "scalar" },
|
|
186
|
+
{ name: "find_unit miss returns null", query: "find_unit", args: { query: "not-a-real-unit-xyz" }, comparison: "scalar" },
|
|
187
|
+
// find_weapon: hyphen + space tolerance.
|
|
188
|
+
{ name: "find_weapon by name", query: "find_weapon", args: { query: "bolt rifle" }, comparison: "scalar" },
|
|
189
|
+
// find_faction: punctuation/diacritic tolerance.
|
|
190
|
+
{ name: "find_faction by display name", query: "find_faction", args: { query: "World Eaters" }, comparison: "scalar" },
|
|
191
|
+
// find_ability: ability name lookup.
|
|
192
|
+
{ name: "find_ability by name", query: "find_ability", args: { query: "Berzerker Frenzy" }, comparison: "scalar" },
|
|
193
|
+
// abilities_of(unit): ordered, iterates unit.ability_ids array.
|
|
194
|
+
{ name: "abilities_of intercessor-squad", query: "abilities_of", args: { unitId: "intercessor-squad" }, comparison: "ordered" },
|
|
195
|
+
{ name: "abilities_of kharn-the-betrayer", query: "abilities_of", args: { unitId: "kharn-the-betrayer" }, comparison: "ordered" },
|
|
196
|
+
// weapons_of(unit): ordered, iterates unit.weapon_ids array.
|
|
197
|
+
{ name: "weapons_of intercessor-squad", query: "weapons_of", args: { unitId: "intercessor-squad" }, comparison: "ordered" },
|
|
198
|
+
{ name: "weapons_of kharn-the-betrayer", query: "weapons_of", args: { unitId: "kharn-the-betrayer" }, comparison: "ordered" },
|
|
199
|
+
// phases_of(ability): compared as set (phase index iteration order is incidental).
|
|
200
|
+
{ name: "phases_of berzerker-frenzy", query: "phases_of", args: { abilityId: "berzerker-frenzy" }, comparison: "set" },
|
|
201
|
+
// faction_of(unit): scalar id or null.
|
|
202
|
+
{ name: "faction_of intercessor-squad", query: "faction_of", args: { unitId: "intercessor-squad" }, comparison: "scalar" },
|
|
203
|
+
// abilities_of_faction: compared as set (collection-index order is incidental).
|
|
204
|
+
{ name: "abilities_of_faction world-eaters", query: "abilities_of_faction", args: { factionId: "world-eaters" }, comparison: "set" },
|
|
205
|
+
// weapons_of_faction: compared as set.
|
|
206
|
+
{ name: "weapons_of_faction world-eaters", query: "weapons_of_faction", args: { factionId: "world-eaters" }, comparison: "set" },
|
|
207
|
+
];
|
|
208
|
+
function genLinkedApi() {
|
|
209
|
+
const ds = Dataset.embedded();
|
|
210
|
+
const cases = LINKED_API_QUERIES.map((q) => {
|
|
211
|
+
const expected = runLinkedQuery(ds, q);
|
|
212
|
+
return { ...q, expected };
|
|
213
|
+
});
|
|
214
|
+
writeJson(join(CONFORMANCE, "linked-api", "cases.json"), cases);
|
|
215
|
+
console.log(`linked-api/cases.json: ${cases.length} cases`);
|
|
216
|
+
}
|
|
217
|
+
function runLinkedQuery(ds, q) {
|
|
218
|
+
switch (q.query) {
|
|
219
|
+
case "find_unit":
|
|
220
|
+
return ds.units.find(q.args.query)?.id ?? null;
|
|
221
|
+
case "find_weapon":
|
|
222
|
+
return ds.weapons.find(q.args.query)?.id ?? null;
|
|
223
|
+
case "find_faction":
|
|
224
|
+
return ds.factions.find(q.args.query)?.id ?? null;
|
|
225
|
+
case "find_ability":
|
|
226
|
+
return ds.abilities.find(q.args.query)?.id ?? null;
|
|
227
|
+
case "abilities_of": {
|
|
228
|
+
const u = ds.units.get(q.args.unitId);
|
|
229
|
+
if (!u)
|
|
230
|
+
throw new Error(`abilities_of: unknown unit ${q.args.unitId}`);
|
|
231
|
+
return u.abilities.map((a) => a.id);
|
|
232
|
+
}
|
|
233
|
+
case "weapons_of": {
|
|
234
|
+
const u = ds.units.get(q.args.unitId);
|
|
235
|
+
if (!u)
|
|
236
|
+
throw new Error(`weapons_of: unknown unit ${q.args.unitId}`);
|
|
237
|
+
return u.weapons.map((w) => w.id);
|
|
238
|
+
}
|
|
239
|
+
case "phases_of": {
|
|
240
|
+
const a = ds.abilities.get(q.args.abilityId);
|
|
241
|
+
if (!a)
|
|
242
|
+
throw new Error(`phases_of: unknown ability ${q.args.abilityId}`);
|
|
243
|
+
return [...a.phases].sort();
|
|
244
|
+
}
|
|
245
|
+
case "faction_of": {
|
|
246
|
+
const u = ds.units.get(q.args.unitId);
|
|
247
|
+
if (!u)
|
|
248
|
+
throw new Error(`faction_of: unknown unit ${q.args.unitId}`);
|
|
249
|
+
return u.faction?.id ?? null;
|
|
250
|
+
}
|
|
251
|
+
case "abilities_of_faction":
|
|
252
|
+
return ds.abilities.byFaction(q.args.factionId).map((a) => a.id).sort();
|
|
253
|
+
case "weapons_of_faction": {
|
|
254
|
+
// Mirrors Rust `weapons_of_faction`: aggregate weapons across the
|
|
255
|
+
// faction's units and dedupe by id. The collection-level
|
|
256
|
+
// `weapons.byFaction()` is a different operation (it looks up weapons
|
|
257
|
+
// whose own `faction_id` is set, which is empty for most factions).
|
|
258
|
+
const f = ds.factions.get(q.args.factionId);
|
|
259
|
+
if (!f)
|
|
260
|
+
throw new Error(`weapons_of_faction: unknown faction ${q.args.factionId}`);
|
|
261
|
+
return f.weapons.map((w) => w.id).sort();
|
|
262
|
+
}
|
|
67
263
|
}
|
|
68
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Attribution corpus: reuses the existing cruncher inputs from the cases that
|
|
267
|
+
* carry at least one groupable buff (ability or manual). The expected shape
|
|
268
|
+
* is the AttributedStage array produced by attributeStages; both
|
|
269
|
+
* implementations of the leave-one-out decomposition must reproduce it
|
|
270
|
+
* within the per-stage float tolerance.
|
|
271
|
+
*/
|
|
272
|
+
const ATTRIBUTION_CASE_FILES = [
|
|
273
|
+
"05-anti-infantry-vs-cultist.json",
|
|
274
|
+
"07-twin-linked-heavy-stationary-vs-knight.json",
|
|
275
|
+
];
|
|
276
|
+
function loadAttributionInput(ds, filename) {
|
|
277
|
+
const path = join(CONFORMANCE, "cruncher", filename);
|
|
278
|
+
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
279
|
+
const weapon = ds.weapons.get(c.attacker.weaponId);
|
|
280
|
+
const unit = ds.units.get(c.target.unitId);
|
|
281
|
+
if (!weapon)
|
|
282
|
+
throw new Error(`attribution: unknown weapon ${c.attacker.weaponId}`);
|
|
283
|
+
if (!unit)
|
|
284
|
+
throw new Error(`attribution: unknown unit ${c.target.unitId}`);
|
|
285
|
+
return {
|
|
286
|
+
name: c.name,
|
|
287
|
+
input: {
|
|
288
|
+
attacker: { weapon: weapon.raw, profileIndex: c.attacker.profileIndex },
|
|
289
|
+
target: {
|
|
290
|
+
unit: unit.raw,
|
|
291
|
+
profileIndex: c.target.profileIndex,
|
|
292
|
+
...(c.target.modelCount !== undefined ? { modelCount: c.target.modelCount } : {}),
|
|
293
|
+
},
|
|
294
|
+
modelsFiring: c.modelsFiring,
|
|
295
|
+
buffs: c.buffs,
|
|
296
|
+
context: c.context,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function genAttribution() {
|
|
301
|
+
const ds = Dataset.embedded();
|
|
302
|
+
const cases = ATTRIBUTION_CASE_FILES.map((filename, idx) => {
|
|
303
|
+
const { name, input } = loadAttributionInput(ds, filename);
|
|
304
|
+
const stages = attributeStages(input, ds);
|
|
305
|
+
return {
|
|
306
|
+
// Persist the input by file reference so the corpus stays a single
|
|
307
|
+
// source of truth — the cruncher case file already pins the EngineInput.
|
|
308
|
+
name,
|
|
309
|
+
cruncher_case: filename,
|
|
310
|
+
expected: stages.map((s) => ({
|
|
311
|
+
name: s.name,
|
|
312
|
+
expected: s.expected,
|
|
313
|
+
baseline: s.baseline,
|
|
314
|
+
lifts: s.lifts.map((l) => ({ source: l.source, delta: l.delta })),
|
|
315
|
+
residual: s.residual,
|
|
316
|
+
intrinsics: s.intrinsics,
|
|
317
|
+
})),
|
|
318
|
+
// Stable ordering of cases in the corpus file.
|
|
319
|
+
_order: idx,
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
// Sort by _order and strip the helper before writing.
|
|
323
|
+
cases.sort((a, b) => a._order - b._order);
|
|
324
|
+
const serialised = cases.map(({ _order: _o, ...rest }) => rest);
|
|
325
|
+
writeJson(join(CONFORMANCE, "attribution", "cases.json"), serialised);
|
|
326
|
+
console.log(`attribution/cases.json: ${cases.length} cases`);
|
|
327
|
+
}
|
|
69
328
|
genNormalize();
|
|
70
329
|
genRosters();
|
|
330
|
+
genLinkedApi();
|
|
331
|
+
genAttribution();
|
|
332
|
+
//# sourceMappingURL=gen-conformance.js.map
|