@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,109 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Rube Goldberg machine — round-trips a NewRecruit list through every
|
|
4
|
+
* supported format and walks the linked Dataset API at each end.
|
|
5
|
+
*
|
|
6
|
+
* Loop:
|
|
7
|
+
* ```
|
|
8
|
+
* seed (newrecruit-json) → import → Roster₀
|
|
9
|
+
* → export wtc-compact → import
|
|
10
|
+
* → export wtc-full → import
|
|
11
|
+
* → export simple → import
|
|
12
|
+
* → export newrecruit-json → import → Rosterₙ
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Asserts `Roster₀ == Rosterₙ` at the Roster level (after stripping
|
|
16
|
+
* `source` + `diagnostics`, which legitimately change across hops). Exits
|
|
17
|
+
* non-zero on divergence. Walks `unit.faction.units` / `weapon.units` /
|
|
18
|
+
* `ability.phases` at each end to demonstrate the cross-reference loops.
|
|
19
|
+
*
|
|
20
|
+
* Strict IP rule: only ids, names, and counts may be printed. No ability
|
|
21
|
+
* description text, no rules text — none of that is stored in the
|
|
22
|
+
* dataset, so we can't accidentally leak it, but be careful adding fields.
|
|
23
|
+
*
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { Dataset } from "./data/dataset.js";
|
|
30
|
+
import { exportRoster } from "./export/index.js";
|
|
31
|
+
import { importRoster } from "./import/import-roster.js";
|
|
32
|
+
const SEED_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "conformance", "roster", "chaos-knights-houndpack", "input.newrecruit-json.json");
|
|
33
|
+
const HOPS = [
|
|
34
|
+
"newrecruit-wtc-compact",
|
|
35
|
+
"newrecruit-wtc-full",
|
|
36
|
+
"newrecruit-simple",
|
|
37
|
+
"newrecruit-json",
|
|
38
|
+
];
|
|
39
|
+
/** Strip the fields that legitimately change across format hops so the
|
|
40
|
+
* fixed-point comparison can focus on the structural Roster shape. */
|
|
41
|
+
function stable(r) {
|
|
42
|
+
const x = JSON.parse(JSON.stringify(r));
|
|
43
|
+
delete x.source;
|
|
44
|
+
delete x.diagnostics;
|
|
45
|
+
return x;
|
|
46
|
+
}
|
|
47
|
+
function rule(title) {
|
|
48
|
+
console.log(`\n── ${title} ${"─".repeat(Math.max(0, 60 - title.length))}`);
|
|
49
|
+
}
|
|
50
|
+
function walkLinkedApi(label, roster, ds) {
|
|
51
|
+
rule(`${label}: walking the linked Dataset API`);
|
|
52
|
+
console.log(`Roster: ${roster.name} (${roster.units.length} units)`);
|
|
53
|
+
console.log(` faction_id = ${roster.faction_id ?? "—"}`);
|
|
54
|
+
console.log(` detachment_id = ${roster.detachment_id ?? "—"}`);
|
|
55
|
+
console.log(` total_computed = ${roster.points.total_computed}`);
|
|
56
|
+
for (const u of roster.units) {
|
|
57
|
+
if (!u.ref.id) {
|
|
58
|
+
console.log(` · ${u.ref.raw_name} [unresolved]`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const unit = ds.units.get(u.ref.id);
|
|
62
|
+
if (!unit) {
|
|
63
|
+
console.log(` · ${u.ref.id} [not in dataset]`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const fac = unit.faction;
|
|
67
|
+
const facUnits = fac?.units.length ?? 0;
|
|
68
|
+
console.log(` · ${unit.id} → faction=${fac?.id ?? "—"} (faction has ${facUnits} units)`);
|
|
69
|
+
for (const w of unit.weapons) {
|
|
70
|
+
const carriers = w.units.length;
|
|
71
|
+
console.log(` weapon=${w.id} carried by ${carriers} units`);
|
|
72
|
+
}
|
|
73
|
+
for (const a of unit.abilities) {
|
|
74
|
+
const phaseIds = a.phases.join(",") || "—";
|
|
75
|
+
const carriers = a.units.length;
|
|
76
|
+
console.log(` ability=${a.id} phases=[${phaseIds}] on ${carriers} units`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function main() {
|
|
81
|
+
const ds = Dataset.embedded();
|
|
82
|
+
const seedJson = JSON.parse(readFileSync(SEED_PATH, "utf8"));
|
|
83
|
+
rule("seed");
|
|
84
|
+
console.log(`Loading: conformance/roster/chaos-knights-houndpack/input.newrecruit-json.json`);
|
|
85
|
+
const roster0 = importRoster(seedJson, { dataset: ds });
|
|
86
|
+
walkLinkedApi("Roster₀", roster0, ds);
|
|
87
|
+
let current = roster0;
|
|
88
|
+
for (const [i, fmt] of HOPS.entries()) {
|
|
89
|
+
const text = exportRoster(current, fmt);
|
|
90
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
91
|
+
const isJson = fmt === "newrecruit-json";
|
|
92
|
+
const reimported = importRoster(isJson ? JSON.parse(text) : text, { dataset: ds });
|
|
93
|
+
console.log(`[hop ${i + 1}/${HOPS.length}] ${fmt} → ${bytes.toLocaleString()} bytes → reimport: ${reimported.units.length} units (computed ${reimported.points.total_computed} pts)`);
|
|
94
|
+
current = reimported;
|
|
95
|
+
}
|
|
96
|
+
walkLinkedApi("Rosterₙ", current, ds);
|
|
97
|
+
rule("fixed-point check");
|
|
98
|
+
const before = stable(roster0);
|
|
99
|
+
const after = stable(current);
|
|
100
|
+
const equal = JSON.stringify(before) === JSON.stringify(after);
|
|
101
|
+
if (equal) {
|
|
102
|
+
console.log(`Roster₀ == Rosterₙ after ${HOPS.length} format hops (source/diagnostics excluded).`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
console.error(`Roster₀ ≠ Rosterₙ — round-trip diverged.`);
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
process.exit(main());
|
|
109
|
+
//# sourceMappingURL=rube-goldberg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rube-goldberg.js","sourceRoot":"","sources":["../src/rube-goldberg.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env tsx\n/**\n * Rube Goldberg machine — round-trips a NewRecruit list through every\n * supported format and walks the linked Dataset API at each end.\n *\n * Loop:\n * ```\n * seed (newrecruit-json) → import → Roster₀\n * → export wtc-compact → import\n * → export wtc-full → import\n * → export simple → import\n * → export newrecruit-json → import → Rosterₙ\n * ```\n *\n * Asserts `Roster₀ == Rosterₙ` at the Roster level (after stripping\n * `source` + `diagnostics`, which legitimately change across hops). Exits\n * non-zero on divergence. Walks `unit.faction.units` / `weapon.units` /\n * `ability.phases` at each end to demonstrate the cross-reference loops.\n *\n * Strict IP rule: only ids, names, and counts may be printed. No ability\n * description text, no rules text — none of that is stored in the\n * dataset, so we can't accidentally leak it, but be careful adding fields.\n *\n * @packageDocumentation\n */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Dataset } from \"./data/dataset.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 SEED_PATH = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"..\",\n \"conformance\",\n \"roster\",\n \"chaos-knights-houndpack\",\n \"input.newrecruit-json.json\",\n);\n\nconst HOPS: ExportFormat[] = [\n \"newrecruit-wtc-compact\",\n \"newrecruit-wtc-full\",\n \"newrecruit-simple\",\n \"newrecruit-json\",\n];\n\n/** Strip the fields that legitimately change across format hops so the\n * fixed-point comparison can focus on the structural Roster shape. */\nfunction stable(r: Roster): Record<string, unknown> {\n const x = JSON.parse(JSON.stringify(r)) as Record<string, unknown>;\n delete x.source;\n delete x.diagnostics;\n return x;\n}\n\nfunction rule(title: string): void {\n console.log(`\\n── ${title} ${\"─\".repeat(Math.max(0, 60 - title.length))}`);\n}\n\nfunction walkLinkedApi(label: string, roster: Roster, ds: Dataset): void {\n rule(`${label}: walking the linked Dataset API`);\n console.log(`Roster: ${roster.name} (${roster.units.length} units)`);\n console.log(` faction_id = ${roster.faction_id ?? \"—\"}`);\n console.log(` detachment_id = ${roster.detachment_id ?? \"—\"}`);\n console.log(` total_computed = ${roster.points.total_computed}`);\n\n for (const u of roster.units) {\n if (!u.ref.id) {\n console.log(` · ${u.ref.raw_name} [unresolved]`);\n continue;\n }\n const unit = ds.units.get(u.ref.id);\n if (!unit) {\n console.log(` · ${u.ref.id} [not in dataset]`);\n continue;\n }\n const fac = unit.faction;\n const facUnits = fac?.units.length ?? 0;\n console.log(` · ${unit.id} → faction=${fac?.id ?? \"—\"} (faction has ${facUnits} units)`);\n\n for (const w of unit.weapons) {\n const carriers = w.units.length;\n console.log(` weapon=${w.id} carried by ${carriers} units`);\n }\n for (const a of unit.abilities) {\n const phaseIds = a.phases.join(\",\") || \"—\";\n const carriers = a.units.length;\n console.log(` ability=${a.id} phases=[${phaseIds}] on ${carriers} units`);\n }\n }\n}\n\nfunction main(): number {\n const ds = Dataset.embedded();\n const seedJson = JSON.parse(readFileSync(SEED_PATH, \"utf8\"));\n\n rule(\"seed\");\n console.log(`Loading: conformance/roster/chaos-knights-houndpack/input.newrecruit-json.json`);\n\n const roster0 = importRoster(seedJson, { dataset: ds });\n walkLinkedApi(\"Roster₀\", roster0, ds);\n\n let current: Roster = roster0;\n for (const [i, fmt] of HOPS.entries()) {\n const text = exportRoster(current, fmt);\n const bytes = Buffer.byteLength(text, \"utf8\");\n const isJson = fmt === \"newrecruit-json\";\n const reimported = importRoster(isJson ? JSON.parse(text) : text, { dataset: ds });\n console.log(\n `[hop ${i + 1}/${HOPS.length}] ${fmt} → ${bytes.toLocaleString()} bytes → reimport: ${reimported.units.length} units (computed ${reimported.points.total_computed} pts)`,\n );\n current = reimported;\n }\n\n walkLinkedApi(\"Rosterₙ\", current, ds);\n\n rule(\"fixed-point check\");\n const before = stable(roster0);\n const after = stable(current);\n const equal = JSON.stringify(before) === JSON.stringify(after);\n if (equal) {\n console.log(`Roster₀ == Rosterₙ after ${HOPS.length} format hops (source/diagnostics excluded).`);\n return 0;\n }\n console.error(`Roster₀ ≠ Rosterₙ — round-trip diverged.`);\n return 1;\n}\n\nprocess.exit(main());\n"]}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Dataset } from "./data/dataset.js";
|
|
3
|
+
import type Ajv from "ajv";
|
|
4
|
+
export type RunnerResponse = {
|
|
5
|
+
ok: true;
|
|
6
|
+
value: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
error_kind: ErrorKind;
|
|
10
|
+
error_payload?: unknown;
|
|
11
|
+
};
|
|
12
|
+
declare const ERROR_KINDS: readonly ["INVALID_INPUT", "UNKNOWN_OP", "UNKNOWN_ENTITY", "IMPORT_FAILED", "EXPORT_FAILED", "VALIDATION_ERROR", "CRUNCH_ERROR", "INTERNAL_ERROR"];
|
|
13
|
+
type ErrorKind = (typeof ERROR_KINDS)[number];
|
|
14
|
+
export interface RunnerState {
|
|
15
|
+
initialized: boolean;
|
|
16
|
+
locale: string;
|
|
17
|
+
tz: string;
|
|
18
|
+
seed: number;
|
|
19
|
+
dataset?: Dataset;
|
|
20
|
+
validator?: Ajv;
|
|
21
|
+
}
|
|
22
|
+
export declare function createRunnerState(): RunnerState;
|
|
23
|
+
/**
|
|
24
|
+
* Apply one decoded request to the runner state and return the response. Used
|
|
25
|
+
* directly by tests; the CLI loop wraps it with line parsing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function dispatch(state: RunnerState, req: {
|
|
28
|
+
op: string;
|
|
29
|
+
args?: unknown;
|
|
30
|
+
}): RunnerResponse;
|
|
31
|
+
/**
|
|
32
|
+
* Process one line of stdin (one NDJSON request) and return the line that
|
|
33
|
+
* should be written to stdout (one NDJSON response). Returns `null` only on
|
|
34
|
+
* fully-empty input lines, which should be silently ignored.
|
|
35
|
+
*/
|
|
36
|
+
export declare function processRequest(state: RunnerState, line: string): string | null;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAO5C,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AA8C3B,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,QAAA,MAAM,WAAW,oJASP,CAAC;AACX,KAAK,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAgB9C,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C;AAoTD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CA4BhG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9E"}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NDJSON conformance runner — the TypeScript implementation of the wire
|
|
4
|
+
* protocol in `conformance/RUNNER_PROTOCOL.md`. Each line on stdin is a JSON
|
|
5
|
+
* request `{op, args?}`; each line on stdout is a JSON response
|
|
6
|
+
* `{ok: true, value}` or `{ok: false, error_kind, error_payload?}`.
|
|
7
|
+
*
|
|
8
|
+
* This file exports {@link processRequest} so vitest can drive the runner
|
|
9
|
+
* in-process without spawning a child; the bottom `if (isCliMain())` block
|
|
10
|
+
* wires the same dispatcher to real stdin/stdout for the cross-impl differ.
|
|
11
|
+
*
|
|
12
|
+
* The runner is a *thin* wrapper over the existing public API — it is not the
|
|
13
|
+
* canonical way to use the package. Library consumers should import the
|
|
14
|
+
* functions directly; the runner exists to give the cross-implementation
|
|
15
|
+
* differ a uniform interface across language ports.
|
|
16
|
+
*/
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import process from "node:process";
|
|
22
|
+
import { Dataset } from "./data/dataset.js";
|
|
23
|
+
import { normalizeName } from "./data/normalize.js";
|
|
24
|
+
import { exportRoster } from "./export/index.js";
|
|
25
|
+
import { importRoster, tryImportRoster } from "./import/import-roster.js";
|
|
26
|
+
import { createValidator } from "./schema-loader.js";
|
|
27
|
+
import { crunch } from "./cruncher/index.js";
|
|
28
|
+
// -----------------------------------------------------------------------------
|
|
29
|
+
// Constants — spec version and implementation identity.
|
|
30
|
+
// -----------------------------------------------------------------------------
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
function loadSpecVersion() {
|
|
33
|
+
// The corpus lives at repo-root/conformance/. Walk up from tools/src/ until
|
|
34
|
+
// we find it so the runner works both from source (tsx) and from dist/.
|
|
35
|
+
for (const candidate of [
|
|
36
|
+
join(__dirname, "../../conformance/SPEC_VERSION"),
|
|
37
|
+
join(__dirname, "../../../conformance/SPEC_VERSION"),
|
|
38
|
+
]) {
|
|
39
|
+
try {
|
|
40
|
+
return Number.parseInt(readFileSync(candidate, "utf8").trim(), 10);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// try next
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error("could not locate conformance/SPEC_VERSION");
|
|
47
|
+
}
|
|
48
|
+
function loadImplVersion() {
|
|
49
|
+
for (const candidate of [
|
|
50
|
+
join(__dirname, "../package.json"),
|
|
51
|
+
join(__dirname, "../../package.json"),
|
|
52
|
+
]) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(candidate, "utf8")).version;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// try next
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return "unknown";
|
|
61
|
+
}
|
|
62
|
+
const SPEC_VERSION = loadSpecVersion();
|
|
63
|
+
const IMPL_VERSION = loadImplVersion();
|
|
64
|
+
const IMPL_NAME = "ts";
|
|
65
|
+
const ERROR_KINDS = [
|
|
66
|
+
"INVALID_INPUT",
|
|
67
|
+
"UNKNOWN_OP",
|
|
68
|
+
"UNKNOWN_ENTITY",
|
|
69
|
+
"IMPORT_FAILED",
|
|
70
|
+
"EXPORT_FAILED",
|
|
71
|
+
"VALIDATION_ERROR",
|
|
72
|
+
"CRUNCH_ERROR",
|
|
73
|
+
"INTERNAL_ERROR",
|
|
74
|
+
];
|
|
75
|
+
function ok(value) {
|
|
76
|
+
return { ok: true, value };
|
|
77
|
+
}
|
|
78
|
+
function err(kind, payload) {
|
|
79
|
+
return payload === undefined
|
|
80
|
+
? { ok: false, error_kind: kind }
|
|
81
|
+
: { ok: false, error_kind: kind, error_payload: payload };
|
|
82
|
+
}
|
|
83
|
+
export function createRunnerState() {
|
|
84
|
+
return { initialized: false, locale: "C", tz: "UTC", seed: 0 };
|
|
85
|
+
}
|
|
86
|
+
function getDataset(state) {
|
|
87
|
+
if (!state.dataset)
|
|
88
|
+
state.dataset = Dataset.embedded();
|
|
89
|
+
return state.dataset;
|
|
90
|
+
}
|
|
91
|
+
function getValidator(state) {
|
|
92
|
+
if (!state.validator)
|
|
93
|
+
state.validator = createValidator();
|
|
94
|
+
return state.validator;
|
|
95
|
+
}
|
|
96
|
+
// -----------------------------------------------------------------------------
|
|
97
|
+
// Op handlers.
|
|
98
|
+
// -----------------------------------------------------------------------------
|
|
99
|
+
function handleInit(state, args) {
|
|
100
|
+
if (state.initialized) {
|
|
101
|
+
return err("INVALID_INPUT", { detail: "init called twice" });
|
|
102
|
+
}
|
|
103
|
+
if (typeof args !== "object" || args === null) {
|
|
104
|
+
return err("INVALID_INPUT", { detail: "init args must be an object" });
|
|
105
|
+
}
|
|
106
|
+
const a = args;
|
|
107
|
+
if (a.spec_version !== SPEC_VERSION) {
|
|
108
|
+
return err("INVALID_INPUT", {
|
|
109
|
+
detail: `spec_version mismatch: runner=${SPEC_VERSION}, request=${String(a.spec_version)}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (a.locale !== "C") {
|
|
113
|
+
return err("INVALID_INPUT", { detail: `unsupported locale: ${String(a.locale)} (only "C")` });
|
|
114
|
+
}
|
|
115
|
+
if (a.tz !== "UTC") {
|
|
116
|
+
return err("INVALID_INPUT", { detail: `unsupported tz: ${String(a.tz)} (only "UTC")` });
|
|
117
|
+
}
|
|
118
|
+
if (typeof a.seed !== "number") {
|
|
119
|
+
return err("INVALID_INPUT", { detail: "seed must be a number" });
|
|
120
|
+
}
|
|
121
|
+
state.initialized = true;
|
|
122
|
+
state.locale = a.locale;
|
|
123
|
+
state.tz = a.tz;
|
|
124
|
+
state.seed = a.seed;
|
|
125
|
+
return ok({ impl: IMPL_NAME, spec_version: SPEC_VERSION, impl_version: IMPL_VERSION });
|
|
126
|
+
}
|
|
127
|
+
function handleNormalize(args) {
|
|
128
|
+
if (typeof args !== "object" || args === null) {
|
|
129
|
+
return err("INVALID_INPUT", { detail: "normalize args must be an object" });
|
|
130
|
+
}
|
|
131
|
+
const a = args;
|
|
132
|
+
if (typeof a.input !== "string") {
|
|
133
|
+
return err("INVALID_INPUT", { detail: "normalize.input must be a string" });
|
|
134
|
+
}
|
|
135
|
+
return ok(normalizeName(a.input));
|
|
136
|
+
}
|
|
137
|
+
function handleImport(state, args) {
|
|
138
|
+
if (typeof args !== "object" || args === null) {
|
|
139
|
+
return err("INVALID_INPUT", { detail: "import args must be an object" });
|
|
140
|
+
}
|
|
141
|
+
const a = args;
|
|
142
|
+
if (typeof a.input !== "string") {
|
|
143
|
+
return err("INVALID_INPUT", { detail: "import.input must be a string" });
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
// The wire protocol carries every input as a string: JSON payloads come
|
|
147
|
+
// through as the JSON text, text payloads come through as-is. The import
|
|
148
|
+
// pipeline decides which by attempting to parse JSON first.
|
|
149
|
+
const trimmed = a.input.trimStart();
|
|
150
|
+
let decoded;
|
|
151
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
152
|
+
try {
|
|
153
|
+
decoded = JSON.parse(a.input);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
decoded = a.input;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
decoded = a.input;
|
|
161
|
+
}
|
|
162
|
+
const roster = importRoster(decoded, { dataset: getDataset(state) });
|
|
163
|
+
return ok(roster);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
return err("IMPORT_FAILED", { detail: e.message, format: a.format ?? null });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function handleTryImport(state, args) {
|
|
170
|
+
if (typeof args !== "object" || args === null) {
|
|
171
|
+
return err("INVALID_INPUT", { detail: "try_import args must be an object" });
|
|
172
|
+
}
|
|
173
|
+
const a = args;
|
|
174
|
+
if (typeof a.input !== "string") {
|
|
175
|
+
return err("INVALID_INPUT", { detail: "try_import.input must be a string" });
|
|
176
|
+
}
|
|
177
|
+
const result = tryImportRoster(a.input, { dataset: getDataset(state) });
|
|
178
|
+
if (!result.ok) {
|
|
179
|
+
return err("IMPORT_FAILED", { reason: result.reason, message: result.message });
|
|
180
|
+
}
|
|
181
|
+
return ok({ format: result.format, roster: result.roster });
|
|
182
|
+
}
|
|
183
|
+
const EXPORT_FORMATS = [
|
|
184
|
+
"newrecruit-json",
|
|
185
|
+
"newrecruit-wtc-compact",
|
|
186
|
+
"newrecruit-wtc-full",
|
|
187
|
+
"newrecruit-simple",
|
|
188
|
+
"roster-json",
|
|
189
|
+
"rosterizer",
|
|
190
|
+
];
|
|
191
|
+
function handleExport(args) {
|
|
192
|
+
if (typeof args !== "object" || args === null) {
|
|
193
|
+
return err("INVALID_INPUT", { detail: "export args must be an object" });
|
|
194
|
+
}
|
|
195
|
+
const a = args;
|
|
196
|
+
if (typeof a.format !== "string" || !EXPORT_FORMATS.includes(a.format)) {
|
|
197
|
+
return err("INVALID_INPUT", { detail: `unknown export format: ${String(a.format)}` });
|
|
198
|
+
}
|
|
199
|
+
if (typeof a.roster !== "object" || a.roster === null) {
|
|
200
|
+
return err("INVALID_INPUT", { detail: "export.roster must be an object" });
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
// We assume the caller passes the canonical resolved Roster shape; if they
|
|
204
|
+
// pass something the serializer can't handle, this throws.
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
206
|
+
return ok(exportRoster(a.roster, a.format));
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
return err("EXPORT_FAILED", { detail: e.message });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function handleLinkedQuery(state, args) {
|
|
213
|
+
if (typeof args !== "object" || args === null) {
|
|
214
|
+
return err("INVALID_INPUT", { detail: "linked_query args must be an object" });
|
|
215
|
+
}
|
|
216
|
+
const a = args;
|
|
217
|
+
if (typeof a.query !== "string") {
|
|
218
|
+
return err("INVALID_INPUT", { detail: "linked_query.query must be a string" });
|
|
219
|
+
}
|
|
220
|
+
const ds = getDataset(state);
|
|
221
|
+
const input = (a.input ?? {});
|
|
222
|
+
try {
|
|
223
|
+
switch (a.query) {
|
|
224
|
+
case "find_unit":
|
|
225
|
+
return ok(ds.units.find(input.query ?? "")?.id ?? null);
|
|
226
|
+
case "find_weapon":
|
|
227
|
+
return ok(ds.weapons.find(input.query ?? "")?.id ?? null);
|
|
228
|
+
case "find_faction":
|
|
229
|
+
return ok(ds.factions.find(input.query ?? "")?.id ?? null);
|
|
230
|
+
case "find_ability":
|
|
231
|
+
return ok(ds.abilities.find(input.query ?? "")?.id ?? null);
|
|
232
|
+
case "abilities_of": {
|
|
233
|
+
const u = ds.units.get(input.unitId);
|
|
234
|
+
if (!u)
|
|
235
|
+
return err("UNKNOWN_ENTITY", { kind: "unit", id: input.unitId });
|
|
236
|
+
return ok(u.abilities.map((x) => x.id));
|
|
237
|
+
}
|
|
238
|
+
case "weapons_of": {
|
|
239
|
+
const u = ds.units.get(input.unitId);
|
|
240
|
+
if (!u)
|
|
241
|
+
return err("UNKNOWN_ENTITY", { kind: "unit", id: input.unitId });
|
|
242
|
+
return ok(u.weapons.map((x) => x.id));
|
|
243
|
+
}
|
|
244
|
+
case "phases_of": {
|
|
245
|
+
const ab = ds.abilities.get(input.abilityId);
|
|
246
|
+
if (!ab)
|
|
247
|
+
return err("UNKNOWN_ENTITY", { kind: "ability", id: input.abilityId });
|
|
248
|
+
return ok([...ab.phases]);
|
|
249
|
+
}
|
|
250
|
+
case "faction_of": {
|
|
251
|
+
const u = ds.units.get(input.unitId);
|
|
252
|
+
if (!u)
|
|
253
|
+
return err("UNKNOWN_ENTITY", { kind: "unit", id: input.unitId });
|
|
254
|
+
return ok(u.faction?.id ?? null);
|
|
255
|
+
}
|
|
256
|
+
case "abilities_of_faction":
|
|
257
|
+
return ok(ds.abilities.byFaction(input.factionId).map((x) => x.id));
|
|
258
|
+
case "weapons_of_faction": {
|
|
259
|
+
const f = ds.factions.get(input.factionId);
|
|
260
|
+
if (!f)
|
|
261
|
+
return err("UNKNOWN_ENTITY", { kind: "faction", id: input.factionId });
|
|
262
|
+
return ok(f.weapons.map((x) => x.id));
|
|
263
|
+
}
|
|
264
|
+
default:
|
|
265
|
+
return err("INVALID_INPUT", { detail: `unknown linked_query: ${a.query}` });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
return err("INTERNAL_ERROR", { detail: e.message });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const VALIDATOR_TARGETS = {
|
|
273
|
+
unit: "https://40kdc.dev/schemas/core/unit.schema.json",
|
|
274
|
+
weapon: "https://40kdc.dev/schemas/core/weapon.schema.json",
|
|
275
|
+
faction: "https://40kdc.dev/schemas/core/faction.schema.json",
|
|
276
|
+
ability: "https://40kdc.dev/schemas/enrichment/ability-dsl/ability.schema.json",
|
|
277
|
+
};
|
|
278
|
+
function ajvKeywordToCode(keyword) {
|
|
279
|
+
switch (keyword) {
|
|
280
|
+
case "required":
|
|
281
|
+
return "REQUIRED_MISSING";
|
|
282
|
+
case "type":
|
|
283
|
+
return "TYPE_MISMATCH";
|
|
284
|
+
case "enum":
|
|
285
|
+
return "ENUM_VIOLATION";
|
|
286
|
+
case "pattern":
|
|
287
|
+
case "format":
|
|
288
|
+
return "PATTERN_MISMATCH";
|
|
289
|
+
case "minimum":
|
|
290
|
+
case "maximum":
|
|
291
|
+
case "exclusiveMinimum":
|
|
292
|
+
case "exclusiveMaximum":
|
|
293
|
+
case "minLength":
|
|
294
|
+
case "maxLength":
|
|
295
|
+
case "minItems":
|
|
296
|
+
case "maxItems":
|
|
297
|
+
return "RANGE_VIOLATION";
|
|
298
|
+
case "additionalProperties":
|
|
299
|
+
return "ADDITIONAL_PROPERTY";
|
|
300
|
+
case "uniqueItems":
|
|
301
|
+
return "UNIQUE_VIOLATION";
|
|
302
|
+
default:
|
|
303
|
+
return `UNMAPPED:${keyword}`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function handleValidate(state, args) {
|
|
307
|
+
if (typeof args !== "object" || args === null) {
|
|
308
|
+
return err("INVALID_INPUT", { detail: "validate args must be an object" });
|
|
309
|
+
}
|
|
310
|
+
const a = args;
|
|
311
|
+
if (typeof a.target !== "string" || !(a.target in VALIDATOR_TARGETS)) {
|
|
312
|
+
return err("INVALID_INPUT", { detail: `unknown validator target: ${String(a.target)}` });
|
|
313
|
+
}
|
|
314
|
+
let validate;
|
|
315
|
+
try {
|
|
316
|
+
validate = getValidator(state).getSchema(VALIDATOR_TARGETS[a.target]);
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
return err("VALIDATION_ERROR", { detail: e.message });
|
|
320
|
+
}
|
|
321
|
+
if (!validate) {
|
|
322
|
+
return err("VALIDATION_ERROR", { detail: `schema not loaded: ${a.target}` });
|
|
323
|
+
}
|
|
324
|
+
validate(a.value);
|
|
325
|
+
const raw = validate.errors ?? [];
|
|
326
|
+
const seen = new Set();
|
|
327
|
+
const out = [];
|
|
328
|
+
for (const e of raw) {
|
|
329
|
+
const code = ajvKeywordToCode(e.keyword);
|
|
330
|
+
if (code.startsWith("UNMAPPED:"))
|
|
331
|
+
continue;
|
|
332
|
+
const path = e.keyword === "required"
|
|
333
|
+
? `${e.instancePath}/${e.params.missingProperty ?? ""}`
|
|
334
|
+
: e.instancePath;
|
|
335
|
+
const key = `${path}|${code}`;
|
|
336
|
+
if (seen.has(key))
|
|
337
|
+
continue;
|
|
338
|
+
seen.add(key);
|
|
339
|
+
out.push({ path, code });
|
|
340
|
+
}
|
|
341
|
+
return ok(out);
|
|
342
|
+
}
|
|
343
|
+
function handleCrunch(state, args) {
|
|
344
|
+
if (typeof args !== "object" || args === null) {
|
|
345
|
+
return err("INVALID_INPUT", { detail: "crunch args must be an object" });
|
|
346
|
+
}
|
|
347
|
+
const a = args;
|
|
348
|
+
if (!a.attacker?.weaponId || typeof a.attacker.profileIndex !== "number") {
|
|
349
|
+
return err("INVALID_INPUT", { detail: "crunch.attacker.weaponId/profileIndex required" });
|
|
350
|
+
}
|
|
351
|
+
if (!a.target?.unitId || typeof a.target.profileIndex !== "number") {
|
|
352
|
+
return err("INVALID_INPUT", { detail: "crunch.target.unitId/profileIndex required" });
|
|
353
|
+
}
|
|
354
|
+
if (typeof a.modelsFiring !== "number") {
|
|
355
|
+
return err("INVALID_INPUT", { detail: "crunch.modelsFiring required" });
|
|
356
|
+
}
|
|
357
|
+
if (!a.context) {
|
|
358
|
+
return err("INVALID_INPUT", { detail: "crunch.context required" });
|
|
359
|
+
}
|
|
360
|
+
const ds = getDataset(state);
|
|
361
|
+
const weapon = ds.weapons.get(a.attacker.weaponId);
|
|
362
|
+
if (!weapon)
|
|
363
|
+
return err("UNKNOWN_ENTITY", { kind: "weapon", id: a.attacker.weaponId });
|
|
364
|
+
const unit = ds.units.get(a.target.unitId);
|
|
365
|
+
if (!unit)
|
|
366
|
+
return err("UNKNOWN_ENTITY", { kind: "unit", id: a.target.unitId });
|
|
367
|
+
try {
|
|
368
|
+
const input = {
|
|
369
|
+
attacker: { weapon: weapon.raw, profileIndex: a.attacker.profileIndex },
|
|
370
|
+
target: {
|
|
371
|
+
unit: unit.raw,
|
|
372
|
+
profileIndex: a.target.profileIndex,
|
|
373
|
+
...(a.target.modelCount !== undefined ? { modelCount: a.target.modelCount } : {}),
|
|
374
|
+
},
|
|
375
|
+
modelsFiring: a.modelsFiring,
|
|
376
|
+
buffs: a.buffs ?? [],
|
|
377
|
+
context: a.context,
|
|
378
|
+
};
|
|
379
|
+
return ok(crunch(input, ds));
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
return err("CRUNCH_ERROR", { detail: e.message });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// -----------------------------------------------------------------------------
|
|
386
|
+
// Dispatcher and per-line entry point.
|
|
387
|
+
// -----------------------------------------------------------------------------
|
|
388
|
+
/**
|
|
389
|
+
* Apply one decoded request to the runner state and return the response. Used
|
|
390
|
+
* directly by tests; the CLI loop wraps it with line parsing.
|
|
391
|
+
*/
|
|
392
|
+
export function dispatch(state, req) {
|
|
393
|
+
if (!state.initialized && req.op !== "init") {
|
|
394
|
+
return err("INVALID_INPUT", { detail: "must init before any other op" });
|
|
395
|
+
}
|
|
396
|
+
switch (req.op) {
|
|
397
|
+
case "init":
|
|
398
|
+
return handleInit(state, req.args);
|
|
399
|
+
case "version":
|
|
400
|
+
return ok({ impl: IMPL_NAME, spec_version: SPEC_VERSION, impl_version: IMPL_VERSION });
|
|
401
|
+
case "normalize":
|
|
402
|
+
return handleNormalize(req.args);
|
|
403
|
+
case "import":
|
|
404
|
+
return handleImport(state, req.args);
|
|
405
|
+
case "try_import":
|
|
406
|
+
return handleTryImport(state, req.args);
|
|
407
|
+
case "export":
|
|
408
|
+
return handleExport(req.args);
|
|
409
|
+
case "linked_query":
|
|
410
|
+
return handleLinkedQuery(state, req.args);
|
|
411
|
+
case "validate":
|
|
412
|
+
return handleValidate(state, req.args);
|
|
413
|
+
case "crunch":
|
|
414
|
+
return handleCrunch(state, req.args);
|
|
415
|
+
case "shutdown":
|
|
416
|
+
return ok(null);
|
|
417
|
+
default:
|
|
418
|
+
return err("UNKNOWN_OP", { op: req.op });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Process one line of stdin (one NDJSON request) and return the line that
|
|
423
|
+
* should be written to stdout (one NDJSON response). Returns `null` only on
|
|
424
|
+
* fully-empty input lines, which should be silently ignored.
|
|
425
|
+
*/
|
|
426
|
+
export function processRequest(state, line) {
|
|
427
|
+
const trimmed = line.trim();
|
|
428
|
+
if (trimmed === "")
|
|
429
|
+
return null;
|
|
430
|
+
let req;
|
|
431
|
+
try {
|
|
432
|
+
req = JSON.parse(trimmed);
|
|
433
|
+
}
|
|
434
|
+
catch (e) {
|
|
435
|
+
return JSON.stringify(err("INVALID_INPUT", { detail: `not valid JSON: ${e.message}` }));
|
|
436
|
+
}
|
|
437
|
+
if (typeof req.op !== "string") {
|
|
438
|
+
return JSON.stringify(err("INVALID_INPUT", { detail: "request must have a string `op` field" }));
|
|
439
|
+
}
|
|
440
|
+
const response = dispatch(state, { op: req.op, args: req.args });
|
|
441
|
+
return JSON.stringify(response);
|
|
442
|
+
}
|
|
443
|
+
// -----------------------------------------------------------------------------
|
|
444
|
+
// CLI: wire stdin/stdout. Only runs when this file is the process entry point.
|
|
445
|
+
// -----------------------------------------------------------------------------
|
|
446
|
+
function isCliMain() {
|
|
447
|
+
// process.argv[1] is the path the runtime invoked. With tsx and node both,
|
|
448
|
+
// it points at this file (or its compiled .js form). Comparing absolute
|
|
449
|
+
// paths is the most robust portable check.
|
|
450
|
+
if (!process.argv[1])
|
|
451
|
+
return false;
|
|
452
|
+
const entry = process.argv[1];
|
|
453
|
+
const here = fileURLToPath(import.meta.url);
|
|
454
|
+
return entry === here || entry.endsWith("/runner.js") || entry.endsWith("/runner.ts");
|
|
455
|
+
}
|
|
456
|
+
async function runCli() {
|
|
457
|
+
const state = createRunnerState();
|
|
458
|
+
const rl = createInterface({ input: process.stdin });
|
|
459
|
+
// We must respond on the same tick the request arrives — the differ
|
|
460
|
+
// pipelines requests and expects responses in order. readline preserves
|
|
461
|
+
// line ordering, so processing inside `line` handler is sufficient.
|
|
462
|
+
for await (const line of rl) {
|
|
463
|
+
const out = processRequest(state, line);
|
|
464
|
+
if (out !== null) {
|
|
465
|
+
process.stdout.write(out + "\n");
|
|
466
|
+
}
|
|
467
|
+
// `shutdown` returns `ok(null)`; honor it by exiting after the response
|
|
468
|
+
// is flushed.
|
|
469
|
+
try {
|
|
470
|
+
const req = JSON.parse(line);
|
|
471
|
+
if (req && req.op === "shutdown") {
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// already handled above; not a shutdown
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (isCliMain()) {
|
|
481
|
+
runCli().catch((e) => {
|
|
482
|
+
// Last-ditch INTERNAL_ERROR so the differ sees a typed failure rather
|
|
483
|
+
// than an opaque crash.
|
|
484
|
+
process.stdout.write(JSON.stringify({
|
|
485
|
+
ok: false,
|
|
486
|
+
error_kind: "INTERNAL_ERROR",
|
|
487
|
+
error_payload: { detail: e.message },
|
|
488
|
+
}) + "\n");
|
|
489
|
+
process.exit(1);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=runner.js.map
|