@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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse-link enrichment ability `unit_ids` arrays into each unit's
|
|
3
|
+
* `ability_ids` in core data.
|
|
4
|
+
*
|
|
5
|
+
* Each `data/enrichment/<faction>/abilities.json` entry carries a
|
|
6
|
+
* `unit_ids` array naming the units that ability applies to. This script
|
|
7
|
+
* inverts that into `data/core/<faction>/units.json[*].ability_ids`.
|
|
8
|
+
*
|
|
9
|
+
* Additionally layers in `leader-attachments.json` — every `leader_id`
|
|
10
|
+
* gains the `"leader"` core ability.
|
|
11
|
+
*
|
|
12
|
+
* Idempotent and additive: existing `ability_ids` are preserved so
|
|
13
|
+
* manually-curated links (e.g. core abilities like "deep-strike",
|
|
14
|
+
* "deadly-demise-d3" not reachable via the enrichment unit_ids path)
|
|
15
|
+
* survive re-runs.
|
|
16
|
+
*
|
|
17
|
+
* Cross-faction routing: each `(unit_id, ability_id)` pair is bucketed
|
|
18
|
+
* to whichever `data/core/<faction>/units.json` actually contains that
|
|
19
|
+
* `unit_id` — so subfaction enrichment files contribute to the
|
|
20
|
+
* appropriate shared core file (e.g. Blood Angels enrichment routes
|
|
21
|
+
* into `adeptus-astartes/units.json`).
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* npx tsx tools/src/link-abilities.ts # all factions
|
|
25
|
+
* npx tsx tools/src/link-abilities.ts --faction orks # one faction
|
|
26
|
+
* npx tsx tools/src/link-abilities.ts --dry-run # report only
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
29
|
+
import { resolve, dirname, join } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const DEFAULT_ROOT = resolve(__dirname, "../..");
|
|
33
|
+
function readJSON(path) {
|
|
34
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
35
|
+
}
|
|
36
|
+
function writeJSON(path, data) {
|
|
37
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
38
|
+
}
|
|
39
|
+
function listFactionDirs(dir) {
|
|
40
|
+
if (!existsSync(dir))
|
|
41
|
+
return [];
|
|
42
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
43
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("_"))
|
|
44
|
+
.map((e) => e.name)
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
47
|
+
export function linkAbilities(opts = {}) {
|
|
48
|
+
const root = opts.rootDir ?? DEFAULT_ROOT;
|
|
49
|
+
const coreDir = resolve(root, "data/core");
|
|
50
|
+
const enrichmentDir = resolve(root, "data/enrichment");
|
|
51
|
+
// Collect (unit_id → set<ability_id>) from every enrichment file.
|
|
52
|
+
const wanted = new Map();
|
|
53
|
+
for (const faction of listFactionDirs(enrichmentDir)) {
|
|
54
|
+
const path = join(enrichmentDir, faction, "abilities.json");
|
|
55
|
+
if (!existsSync(path))
|
|
56
|
+
continue;
|
|
57
|
+
const abilities = readJSON(path);
|
|
58
|
+
for (const a of abilities) {
|
|
59
|
+
if (!a.ability_id || !a.unit_ids)
|
|
60
|
+
continue;
|
|
61
|
+
for (const unitId of a.unit_ids) {
|
|
62
|
+
let set = wanted.get(unitId);
|
|
63
|
+
if (!set)
|
|
64
|
+
wanted.set(unitId, (set = new Set()));
|
|
65
|
+
set.add(a.ability_id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Layer in leader-attachments → "leader".
|
|
70
|
+
for (const faction of listFactionDirs(coreDir)) {
|
|
71
|
+
const path = join(coreDir, faction, "leader-attachments.json");
|
|
72
|
+
if (!existsSync(path))
|
|
73
|
+
continue;
|
|
74
|
+
const leaders = readJSON(path);
|
|
75
|
+
for (const l of leaders) {
|
|
76
|
+
let set = wanted.get(l.leader_id);
|
|
77
|
+
if (!set)
|
|
78
|
+
wanted.set(l.leader_id, (set = new Set()));
|
|
79
|
+
set.add("leader");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Route each (unit_id, ability_id) pair to its owning core/<faction>/units.json.
|
|
83
|
+
const factionsWithUnits = listFactionDirs(coreDir).filter((f) => existsSync(join(coreDir, f, "units.json")));
|
|
84
|
+
let unitsChanged = 0;
|
|
85
|
+
let linksAdded = 0;
|
|
86
|
+
const handled = new Set();
|
|
87
|
+
const filesWritten = [];
|
|
88
|
+
for (const faction of factionsWithUnits) {
|
|
89
|
+
if (opts.factionFilter && opts.factionFilter !== faction)
|
|
90
|
+
continue;
|
|
91
|
+
const unitsPath = join(coreDir, faction, "units.json");
|
|
92
|
+
const units = readJSON(unitsPath);
|
|
93
|
+
let mutated = false;
|
|
94
|
+
for (const unit of units) {
|
|
95
|
+
const incoming = wanted.get(unit.id);
|
|
96
|
+
if (!incoming)
|
|
97
|
+
continue;
|
|
98
|
+
handled.add(unit.id);
|
|
99
|
+
const before = new Set(unit.ability_ids ?? []);
|
|
100
|
+
const beforeCount = before.size;
|
|
101
|
+
const merged = new Set([...before, ...incoming]);
|
|
102
|
+
if (merged.size === beforeCount)
|
|
103
|
+
continue;
|
|
104
|
+
linksAdded += merged.size - beforeCount;
|
|
105
|
+
unitsChanged += 1;
|
|
106
|
+
unit.ability_ids = [...merged].sort();
|
|
107
|
+
mutated = true;
|
|
108
|
+
}
|
|
109
|
+
if (mutated && !opts.dryRun) {
|
|
110
|
+
writeJSON(unitsPath, units);
|
|
111
|
+
filesWritten.push(unitsPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Surface unit_ids that no core file claims.
|
|
115
|
+
const unknown = [];
|
|
116
|
+
for (const unitId of wanted.keys()) {
|
|
117
|
+
if (!handled.has(unitId))
|
|
118
|
+
unknown.push(unitId);
|
|
119
|
+
}
|
|
120
|
+
unknown.sort();
|
|
121
|
+
return {
|
|
122
|
+
factionsScanned: factionsWithUnits.length,
|
|
123
|
+
unitsChanged,
|
|
124
|
+
abilityLinksAdded: linksAdded,
|
|
125
|
+
unknownUnitIdReferences: unknown,
|
|
126
|
+
filesWritten,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ─── CLI ────────────────────────────────────────────────────────────────
|
|
130
|
+
const isMain = process.argv[1] &&
|
|
131
|
+
resolve(process.argv[1]).replace(/\.\w+$/, "") ===
|
|
132
|
+
fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
|
|
133
|
+
if (isMain) {
|
|
134
|
+
const args = process.argv.slice(2);
|
|
135
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
136
|
+
console.log("Usage: npx tsx tools/src/link-abilities.ts [--faction <id>] [--dry-run]");
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
const dryRun = args.includes("--dry-run");
|
|
140
|
+
const factionFlag = args.indexOf("--faction");
|
|
141
|
+
const factionFilter = factionFlag !== -1 ? args[factionFlag + 1] : undefined;
|
|
142
|
+
const summary = linkAbilities({ dryRun, factionFilter });
|
|
143
|
+
console.log(`Factions scanned: ${summary.factionsScanned}`);
|
|
144
|
+
console.log(`Units changed: ${summary.unitsChanged}`);
|
|
145
|
+
console.log(`Ability links added: ${summary.abilityLinksAdded}`);
|
|
146
|
+
console.log(`Files written: ${summary.filesWritten.length}`);
|
|
147
|
+
if (summary.unknownUnitIdReferences.length > 0) {
|
|
148
|
+
const preview = summary.unknownUnitIdReferences.slice(0, 20);
|
|
149
|
+
console.log(`\nWarning: ${summary.unknownUnitIdReferences.length} unit_ids in enrichment files do not match any core unit:`);
|
|
150
|
+
for (const id of preview)
|
|
151
|
+
console.log(` ${id}`);
|
|
152
|
+
if (summary.unknownUnitIdReferences.length > preview.length) {
|
|
153
|
+
console.log(` … and ${summary.unknownUnitIdReferences.length - preview.length} more`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (dryRun)
|
|
157
|
+
console.log("\n(dry-run; no files written)");
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=link-abilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-abilities.js","sourceRoot":"","sources":["../src/link-abilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reverse-link enrichment ability `unit_ids` arrays into each unit's\n * `ability_ids` in core data.\n *\n * Each `data/enrichment/<faction>/abilities.json` entry carries a\n * `unit_ids` array naming the units that ability applies to. This script\n * inverts that into `data/core/<faction>/units.json[*].ability_ids`.\n *\n * Additionally layers in `leader-attachments.json` — every `leader_id`\n * gains the `\"leader\"` core ability.\n *\n * Idempotent and additive: existing `ability_ids` are preserved so\n * manually-curated links (e.g. core abilities like \"deep-strike\",\n * \"deadly-demise-d3\" not reachable via the enrichment unit_ids path)\n * survive re-runs.\n *\n * Cross-faction routing: each `(unit_id, ability_id)` pair is bucketed\n * to whichever `data/core/<faction>/units.json` actually contains that\n * `unit_id` — so subfaction enrichment files contribute to the\n * appropriate shared core file (e.g. Blood Angels enrichment routes\n * into `adeptus-astartes/units.json`).\n *\n * Usage:\n * npx tsx tools/src/link-abilities.ts # all factions\n * npx tsx tools/src/link-abilities.ts --faction orks # one faction\n * npx tsx tools/src/link-abilities.ts --dry-run # report only\n */\n\nimport { readFileSync, writeFileSync, existsSync, readdirSync } from \"node:fs\";\nimport { resolve, dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst DEFAULT_ROOT = resolve(__dirname, \"../..\");\n\ninterface EnrichmentAbility {\n ability_id: string | null;\n unit_ids: string[] | null;\n}\n\ninterface CoreUnit {\n id: string;\n ability_ids?: string[];\n [k: string]: unknown;\n}\n\ninterface LeaderAttachment {\n leader_id: string;\n}\n\nexport interface LinkOptions {\n rootDir?: string;\n factionFilter?: string;\n dryRun?: boolean;\n}\n\nexport interface LinkSummary {\n factionsScanned: number;\n unitsChanged: number;\n abilityLinksAdded: number;\n unknownUnitIdReferences: string[];\n filesWritten: string[];\n}\n\nfunction readJSON<T>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\n\nfunction writeJSON(path: string, data: unknown): void {\n writeFileSync(path, JSON.stringify(data, null, 2) + \"\\n\");\n}\n\nfunction listFactionDirs(dir: string): string[] {\n if (!existsSync(dir)) return [];\n return readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() && !e.name.startsWith(\"_\"))\n .map((e) => e.name)\n .sort();\n}\n\nexport function linkAbilities(opts: LinkOptions = {}): LinkSummary {\n const root = opts.rootDir ?? DEFAULT_ROOT;\n const coreDir = resolve(root, \"data/core\");\n const enrichmentDir = resolve(root, \"data/enrichment\");\n\n // Collect (unit_id → set<ability_id>) from every enrichment file.\n const wanted = new Map<string, Set<string>>();\n\n for (const faction of listFactionDirs(enrichmentDir)) {\n const path = join(enrichmentDir, faction, \"abilities.json\");\n if (!existsSync(path)) continue;\n const abilities = readJSON<EnrichmentAbility[]>(path);\n for (const a of abilities) {\n if (!a.ability_id || !a.unit_ids) continue;\n for (const unitId of a.unit_ids) {\n let set = wanted.get(unitId);\n if (!set) wanted.set(unitId, (set = new Set()));\n set.add(a.ability_id);\n }\n }\n }\n\n // Layer in leader-attachments → \"leader\".\n for (const faction of listFactionDirs(coreDir)) {\n const path = join(coreDir, faction, \"leader-attachments.json\");\n if (!existsSync(path)) continue;\n const leaders = readJSON<LeaderAttachment[]>(path);\n for (const l of leaders) {\n let set = wanted.get(l.leader_id);\n if (!set) wanted.set(l.leader_id, (set = new Set()));\n set.add(\"leader\");\n }\n }\n\n // Route each (unit_id, ability_id) pair to its owning core/<faction>/units.json.\n const factionsWithUnits = listFactionDirs(coreDir).filter((f) =>\n existsSync(join(coreDir, f, \"units.json\")),\n );\n\n let unitsChanged = 0;\n let linksAdded = 0;\n const handled = new Set<string>();\n const filesWritten: string[] = [];\n\n for (const faction of factionsWithUnits) {\n if (opts.factionFilter && opts.factionFilter !== faction) continue;\n const unitsPath = join(coreDir, faction, \"units.json\");\n const units = readJSON<CoreUnit[]>(unitsPath);\n let mutated = false;\n\n for (const unit of units) {\n const incoming = wanted.get(unit.id);\n if (!incoming) continue;\n handled.add(unit.id);\n\n const before = new Set(unit.ability_ids ?? []);\n const beforeCount = before.size;\n const merged = new Set([...before, ...incoming]);\n if (merged.size === beforeCount) continue;\n\n linksAdded += merged.size - beforeCount;\n unitsChanged += 1;\n unit.ability_ids = [...merged].sort();\n mutated = true;\n }\n\n if (mutated && !opts.dryRun) {\n writeJSON(unitsPath, units);\n filesWritten.push(unitsPath);\n }\n }\n\n // Surface unit_ids that no core file claims.\n const unknown: string[] = [];\n for (const unitId of wanted.keys()) {\n if (!handled.has(unitId)) unknown.push(unitId);\n }\n unknown.sort();\n\n return {\n factionsScanned: factionsWithUnits.length,\n unitsChanged,\n abilityLinksAdded: linksAdded,\n unknownUnitIdReferences: unknown,\n filesWritten,\n };\n}\n\n// ─── CLI ────────────────────────────────────────────────────────────────\n\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") ===\n fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\n\nif (isMain) {\n const args = process.argv.slice(2);\n if (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(\n \"Usage: npx tsx tools/src/link-abilities.ts [--faction <id>] [--dry-run]\",\n );\n process.exit(0);\n }\n\n const dryRun = args.includes(\"--dry-run\");\n const factionFlag = args.indexOf(\"--faction\");\n const factionFilter =\n factionFlag !== -1 ? args[factionFlag + 1] : undefined;\n\n const summary = linkAbilities({ dryRun, factionFilter });\n\n console.log(`Factions scanned: ${summary.factionsScanned}`);\n console.log(`Units changed: ${summary.unitsChanged}`);\n console.log(`Ability links added: ${summary.abilityLinksAdded}`);\n console.log(`Files written: ${summary.filesWritten.length}`);\n\n if (summary.unknownUnitIdReferences.length > 0) {\n const preview = summary.unknownUnitIdReferences.slice(0, 20);\n console.log(\n `\\nWarning: ${summary.unknownUnitIdReferences.length} unit_ids in enrichment files do not match any core unit:`,\n );\n for (const id of preview) console.log(` ${id}`);\n if (summary.unknownUnitIdReferences.length > preview.length) {\n console.log(\n ` … and ${summary.unknownUnitIdReferences.length - preview.length} more`,\n );\n }\n }\n\n if (dryRun) console.log(\"\\n(dry-run; no files written)\");\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"2026-weapon-keywords.d.ts","sourceRoot":"","sources":["../../src/migrations/2026-weapon-keywords.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot migration to:
|
|
3
|
+
* 1. Turn weapon-profile `keywords` strings into typed catalog references.
|
|
4
|
+
* Each `"Sustained Hits 1"` (and case variants) becomes
|
|
5
|
+
* `{ keyword_id: "sustained-hits", parameters: { value: 1 } }`.
|
|
6
|
+
* Strings whose pattern does not match any catalog entry are surfaced
|
|
7
|
+
* as diagnostics; the run leaves the original string untouched in that
|
|
8
|
+
* slot so a human can fix it before re-running.
|
|
9
|
+
*
|
|
10
|
+
* 2. Normalise every `type: "re-roll"` DSL effect's modifier so the dice
|
|
11
|
+
* subset is explicit:
|
|
12
|
+
* - `condition: "any-fail"` → `subset: "all-failures"`
|
|
13
|
+
* - `condition: "natural-1"` → `subset: "ones"`
|
|
14
|
+
* - no `condition` field → `subset: "all-failures"`
|
|
15
|
+
* The `condition` field is removed in all cases; other modifier
|
|
16
|
+
* properties (`attack_type`, `uses`, `max_rerolls`, `context`, `roll`)
|
|
17
|
+
* are preserved.
|
|
18
|
+
*
|
|
19
|
+
* The script is idempotent: weapons already in the new shape (objects, not
|
|
20
|
+
* strings) are left alone; re-roll modifiers with an explicit `subset` are
|
|
21
|
+
* left alone.
|
|
22
|
+
*
|
|
23
|
+
* Run:
|
|
24
|
+
* npx tsx tools/src/migrations/2026-weapon-keywords.ts
|
|
25
|
+
*
|
|
26
|
+
* Outputs a final summary of `(file, keyword string, diagnostic)` tuples for
|
|
27
|
+
* any unmatched keywords. Exit status is non-zero only if a JSON parse fails;
|
|
28
|
+
* unmatched keywords are warnings, not errors, so the operator can iterate.
|
|
29
|
+
*/
|
|
30
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
31
|
+
import { join, resolve } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
34
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
35
|
+
const CATALOG_PATH = join(REPO_ROOT, "data/core/weapon-keywords.json");
|
|
36
|
+
const DATA_ROOTS = [join(REPO_ROOT, "data/core"), join(REPO_ROOT, "data/enrichment")];
|
|
37
|
+
/** Build the set of (display_name lower-cased → entry) lookups for the catalog. */
|
|
38
|
+
function loadCatalog() {
|
|
39
|
+
const raw = JSON.parse(readFileSync(CATALOG_PATH, "utf-8"));
|
|
40
|
+
const byName = new Map();
|
|
41
|
+
const ids = new Set();
|
|
42
|
+
for (const entry of raw) {
|
|
43
|
+
byName.set(entry.name.toLowerCase(), entry);
|
|
44
|
+
ids.add(entry.id);
|
|
45
|
+
}
|
|
46
|
+
return { byName, ids };
|
|
47
|
+
}
|
|
48
|
+
/** Title-case a multi-word string ("epic hero" → "Epic Hero"). */
|
|
49
|
+
function titleCase(s) {
|
|
50
|
+
return s
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/(^|[\s-])([a-z])/g, (_, sep, ch) => sep + ch.toUpperCase());
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse a single keyword string into a typed reference, or `null` when no
|
|
56
|
+
* catalog entry matches. The function recognises the canonical 11e parameter
|
|
57
|
+
* shapes:
|
|
58
|
+
*
|
|
59
|
+
* - bare: `"Lethal Hits"` → `{ keyword_id: "lethal-hits" }`
|
|
60
|
+
* - value (number or dice): `"Sustained Hits D3"` → `{ ..., parameters: { value: "D3" } }`
|
|
61
|
+
* - anti-X N+: `"Anti-INFANTRY 4+"` → `{ ..., parameters: { target_keyword: "INFANTRY", threshold: 4 } }`
|
|
62
|
+
*
|
|
63
|
+
* Returns `null` for unrecognised strings so the caller can surface them as a
|
|
64
|
+
* diagnostic instead of silently dropping them.
|
|
65
|
+
*/
|
|
66
|
+
function parseKeywordString(raw, catalog) {
|
|
67
|
+
const trimmed = raw.trim();
|
|
68
|
+
const lower = trimmed.toLowerCase();
|
|
69
|
+
// Anti-X N+ — special case: the target keyword and threshold are embedded.
|
|
70
|
+
// Title-case the keyword to match unit-keyword data convention ("Infantry",
|
|
71
|
+
// "Epic Hero" — never "INFANTRY" or "infantry").
|
|
72
|
+
const antiMatch = /^anti-([a-z][a-z\s'-]*)\s+([2-6])\+$/i.exec(trimmed);
|
|
73
|
+
if (antiMatch) {
|
|
74
|
+
return {
|
|
75
|
+
keyword_id: "anti",
|
|
76
|
+
parameters: {
|
|
77
|
+
target_keyword: titleCase(antiMatch[1]),
|
|
78
|
+
threshold: Number(antiMatch[2]),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Try each catalog entry's display name as a prefix; the suffix carries the
|
|
83
|
+
// value parameter if the entry takes one.
|
|
84
|
+
for (const [name, entry] of catalog.byName) {
|
|
85
|
+
if (lower === name) {
|
|
86
|
+
// Exact match — must be parameterless.
|
|
87
|
+
if (entry.required_parameters.length === 0) {
|
|
88
|
+
return { keyword_id: entry.id };
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (lower.startsWith(name + " ")) {
|
|
93
|
+
const rest = trimmed.slice(name.length + 1).trim();
|
|
94
|
+
if (entry.required_parameters.length === 1 && entry.required_parameters[0] === "value") {
|
|
95
|
+
// Numeric (e.g. "5") or dice expression (e.g. "D3", "D6+3").
|
|
96
|
+
const numericMatch = /^\d+$/.exec(rest);
|
|
97
|
+
const diceMatch = /^\d*[Dd]\d+(\+\d+)?$/.exec(rest);
|
|
98
|
+
if (numericMatch) {
|
|
99
|
+
return { keyword_id: entry.id, parameters: { value: Number(rest) } };
|
|
100
|
+
}
|
|
101
|
+
if (diceMatch) {
|
|
102
|
+
return { keyword_id: entry.id, parameters: { value: rest.toUpperCase() } };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Convert one weapon's `profiles[].keywords` array from legacy string form to
|
|
111
|
+
* the new ref form, in place. Mutates `weapon`.
|
|
112
|
+
*/
|
|
113
|
+
function migrateWeapon(weapon, catalog, file, diagnostics) {
|
|
114
|
+
for (const profile of weapon.profiles ?? []) {
|
|
115
|
+
const kws = profile.keywords;
|
|
116
|
+
if (!Array.isArray(kws))
|
|
117
|
+
continue;
|
|
118
|
+
const next = [];
|
|
119
|
+
for (const item of kws) {
|
|
120
|
+
if (typeof item === "object" && item !== null && "keyword_id" in item) {
|
|
121
|
+
// Already migrated.
|
|
122
|
+
next.push(item);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (typeof item !== "string") {
|
|
126
|
+
diagnostics.push({ file, raw: String(item), reason: "non-string, non-object keyword entry" });
|
|
127
|
+
next.push(item);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const ref = parseKeywordString(item, catalog);
|
|
131
|
+
if (ref) {
|
|
132
|
+
next.push(ref);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
diagnostics.push({ file, raw: item, reason: "no catalog entry matches" });
|
|
136
|
+
// Keep the original string so the file still reads cleanly; a follow-up
|
|
137
|
+
// pass can address it after the operator updates the catalog.
|
|
138
|
+
next.push(item);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
profile.keywords = next;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Normalise a `re-roll` modifier in place: condition → subset, drop condition. */
|
|
145
|
+
function migrateRerollModifier(mod) {
|
|
146
|
+
if (typeof mod.subset === "string")
|
|
147
|
+
return false; // already migrated
|
|
148
|
+
const cond = mod.condition;
|
|
149
|
+
if (cond === "natural-1")
|
|
150
|
+
mod.subset = "ones";
|
|
151
|
+
else if (cond === "any-fail")
|
|
152
|
+
mod.subset = "all-failures";
|
|
153
|
+
// Some pre-migration data encoded "re-roll 1s" as `value: 1` rather than a
|
|
154
|
+
// `condition`. Honor that signal before defaulting, or the intent is lost.
|
|
155
|
+
else if (typeof mod.value === "number" && mod.value === 1)
|
|
156
|
+
mod.subset = "ones";
|
|
157
|
+
else
|
|
158
|
+
mod.subset = "all-failures";
|
|
159
|
+
if ("condition" in mod)
|
|
160
|
+
delete mod.condition;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/** Walk a JSON value and migrate any `type: "re-roll"` single-effect modifiers. */
|
|
164
|
+
function migrateRerollsIn(node) {
|
|
165
|
+
if (Array.isArray(node)) {
|
|
166
|
+
let n = 0;
|
|
167
|
+
for (const child of node)
|
|
168
|
+
n += migrateRerollsIn(child);
|
|
169
|
+
return n;
|
|
170
|
+
}
|
|
171
|
+
if (node && typeof node === "object") {
|
|
172
|
+
const obj = node;
|
|
173
|
+
let n = 0;
|
|
174
|
+
if (obj.type === "re-roll" && obj.modifier && typeof obj.modifier === "object") {
|
|
175
|
+
if (migrateRerollModifier(obj.modifier))
|
|
176
|
+
n += 1;
|
|
177
|
+
}
|
|
178
|
+
for (const value of Object.values(obj))
|
|
179
|
+
n += migrateRerollsIn(value);
|
|
180
|
+
return n;
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
function findJsonFiles(root, predicate) {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const entry of readdirSync(root)) {
|
|
187
|
+
const full = join(root, entry);
|
|
188
|
+
if (statSync(full).isDirectory()) {
|
|
189
|
+
out.push(...findJsonFiles(full, predicate));
|
|
190
|
+
}
|
|
191
|
+
else if (entry.endsWith(".json") && predicate(entry)) {
|
|
192
|
+
out.push(full);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function writeJsonPreservingTrailingNewline(file, value) {
|
|
198
|
+
writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
|
|
199
|
+
}
|
|
200
|
+
function main() {
|
|
201
|
+
const catalog = loadCatalog();
|
|
202
|
+
const diagnostics = [];
|
|
203
|
+
let weaponFiles = 0;
|
|
204
|
+
let rerollFiles = 0;
|
|
205
|
+
let rerollEffects = 0;
|
|
206
|
+
// 1. Weapons.
|
|
207
|
+
for (const root of DATA_ROOTS) {
|
|
208
|
+
for (const file of findJsonFiles(root, (n) => n === "weapons.json" || n === "weapons.example.json")) {
|
|
209
|
+
const data = JSON.parse(readFileSync(file, "utf-8"));
|
|
210
|
+
if (!Array.isArray(data))
|
|
211
|
+
continue;
|
|
212
|
+
for (const weapon of data)
|
|
213
|
+
migrateWeapon(weapon, catalog, file, diagnostics);
|
|
214
|
+
writeJsonPreservingTrailingNewline(file, data);
|
|
215
|
+
weaponFiles += 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// 2. Re-roll DSL effects — abilities + any other file that carries effect trees.
|
|
219
|
+
for (const root of DATA_ROOTS) {
|
|
220
|
+
for (const file of findJsonFiles(root, (n) => n.endsWith(".json") && n !== "weapons.json")) {
|
|
221
|
+
const raw = readFileSync(file, "utf-8");
|
|
222
|
+
const before = raw;
|
|
223
|
+
const data = JSON.parse(raw);
|
|
224
|
+
const n = migrateRerollsIn(data);
|
|
225
|
+
if (n > 0) {
|
|
226
|
+
const after = JSON.stringify(data, null, 2) + "\n";
|
|
227
|
+
if (after !== before) {
|
|
228
|
+
writeFileSync(file, after);
|
|
229
|
+
rerollFiles += 1;
|
|
230
|
+
rerollEffects += n;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log(`Weapon files rewritten: ${weaponFiles}`);
|
|
236
|
+
console.log(`Re-roll effects normalised: ${rerollEffects} across ${rerollFiles} files`);
|
|
237
|
+
if (diagnostics.length > 0) {
|
|
238
|
+
console.log(`\nDiagnostics — ${diagnostics.length} unmatched keyword strings:`);
|
|
239
|
+
for (const d of diagnostics.slice(0, 30)) {
|
|
240
|
+
console.log(` ${d.file.replace(REPO_ROOT + "/", "")} ${JSON.stringify(d.raw)} (${d.reason})`);
|
|
241
|
+
}
|
|
242
|
+
if (diagnostics.length > 30)
|
|
243
|
+
console.log(` ... and ${diagnostics.length - 30} more`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
main();
|
|
247
|
+
//# sourceMappingURL=2026-weapon-keywords.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"2026-weapon-keywords.js","sourceRoot":"","sources":["../../src/migrations/2026-weapon-keywords.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One-shot migration to:\n * 1. Turn weapon-profile `keywords` strings into typed catalog references.\n * Each `\"Sustained Hits 1\"` (and case variants) becomes\n * `{ keyword_id: \"sustained-hits\", parameters: { value: 1 } }`.\n * Strings whose pattern does not match any catalog entry are surfaced\n * as diagnostics; the run leaves the original string untouched in that\n * slot so a human can fix it before re-running.\n *\n * 2. Normalise every `type: \"re-roll\"` DSL effect's modifier so the dice\n * subset is explicit:\n * - `condition: \"any-fail\"` → `subset: \"all-failures\"`\n * - `condition: \"natural-1\"` → `subset: \"ones\"`\n * - no `condition` field → `subset: \"all-failures\"`\n * The `condition` field is removed in all cases; other modifier\n * properties (`attack_type`, `uses`, `max_rerolls`, `context`, `roll`)\n * are preserved.\n *\n * The script is idempotent: weapons already in the new shape (objects, not\n * strings) are left alone; re-roll modifiers with an explicit `subset` are\n * left alone.\n *\n * Run:\n * npx tsx tools/src/migrations/2026-weapon-keywords.ts\n *\n * Outputs a final summary of `(file, keyword string, diagnostic)` tuples for\n * any unmatched keywords. Exit status is non-zero only if a JSON parse fails;\n * unmatched keywords are warnings, not errors, so the operator can iterate.\n */\nimport { readFileSync, readdirSync, statSync, writeFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst REPO_ROOT = resolve(__dirname, \"../../..\");\nconst CATALOG_PATH = join(REPO_ROOT, \"data/core/weapon-keywords.json\");\nconst DATA_ROOTS = [join(REPO_ROOT, \"data/core\"), join(REPO_ROOT, \"data/enrichment\")];\n\ninterface CatalogEntry {\n id: string;\n name: string;\n required_parameters: (\"value\" | \"target_keyword\" | \"threshold\")[];\n}\n\ntype Diagnostic = { file: string; raw: string; reason: string };\n\n/** Build the set of (display_name lower-cased → entry) lookups for the catalog. */\nfunction loadCatalog(): { byName: Map<string, CatalogEntry>; ids: Set<string> } {\n const raw = JSON.parse(readFileSync(CATALOG_PATH, \"utf-8\")) as CatalogEntry[];\n const byName = new Map<string, CatalogEntry>();\n const ids = new Set<string>();\n for (const entry of raw) {\n byName.set(entry.name.toLowerCase(), entry);\n ids.add(entry.id);\n }\n return { byName, ids };\n}\n\ninterface KeywordRef {\n keyword_id: string;\n parameters?: { value?: number | string; target_keyword?: string; threshold?: number };\n}\n\n/** Title-case a multi-word string (\"epic hero\" → \"Epic Hero\"). */\nfunction titleCase(s: string): string {\n return s\n .toLowerCase()\n .replace(/(^|[\\s-])([a-z])/g, (_, sep: string, ch: string) => sep + ch.toUpperCase());\n}\n\n/**\n * Parse a single keyword string into a typed reference, or `null` when no\n * catalog entry matches. The function recognises the canonical 11e parameter\n * shapes:\n *\n * - bare: `\"Lethal Hits\"` → `{ keyword_id: \"lethal-hits\" }`\n * - value (number or dice): `\"Sustained Hits D3\"` → `{ ..., parameters: { value: \"D3\" } }`\n * - anti-X N+: `\"Anti-INFANTRY 4+\"` → `{ ..., parameters: { target_keyword: \"INFANTRY\", threshold: 4 } }`\n *\n * Returns `null` for unrecognised strings so the caller can surface them as a\n * diagnostic instead of silently dropping them.\n */\nfunction parseKeywordString(\n raw: string,\n catalog: { byName: Map<string, CatalogEntry>; ids: Set<string> },\n): KeywordRef | null {\n const trimmed = raw.trim();\n const lower = trimmed.toLowerCase();\n\n // Anti-X N+ — special case: the target keyword and threshold are embedded.\n // Title-case the keyword to match unit-keyword data convention (\"Infantry\",\n // \"Epic Hero\" — never \"INFANTRY\" or \"infantry\").\n const antiMatch = /^anti-([a-z][a-z\\s'-]*)\\s+([2-6])\\+$/i.exec(trimmed);\n if (antiMatch) {\n return {\n keyword_id: \"anti\",\n parameters: {\n target_keyword: titleCase(antiMatch[1]),\n threshold: Number(antiMatch[2]),\n },\n };\n }\n\n // Try each catalog entry's display name as a prefix; the suffix carries the\n // value parameter if the entry takes one.\n for (const [name, entry] of catalog.byName) {\n if (lower === name) {\n // Exact match — must be parameterless.\n if (entry.required_parameters.length === 0) {\n return { keyword_id: entry.id };\n }\n continue;\n }\n if (lower.startsWith(name + \" \")) {\n const rest = trimmed.slice(name.length + 1).trim();\n if (entry.required_parameters.length === 1 && entry.required_parameters[0] === \"value\") {\n // Numeric (e.g. \"5\") or dice expression (e.g. \"D3\", \"D6+3\").\n const numericMatch = /^\\d+$/.exec(rest);\n const diceMatch = /^\\d*[Dd]\\d+(\\+\\d+)?$/.exec(rest);\n if (numericMatch) {\n return { keyword_id: entry.id, parameters: { value: Number(rest) } };\n }\n if (diceMatch) {\n return { keyword_id: entry.id, parameters: { value: rest.toUpperCase() } };\n }\n }\n }\n }\n\n return null;\n}\n\n/**\n * Convert one weapon's `profiles[].keywords` array from legacy string form to\n * the new ref form, in place. Mutates `weapon`.\n */\nfunction migrateWeapon(\n weapon: { profiles?: { keywords?: unknown }[] },\n catalog: { byName: Map<string, CatalogEntry>; ids: Set<string> },\n file: string,\n diagnostics: Diagnostic[],\n): void {\n for (const profile of weapon.profiles ?? []) {\n const kws = profile.keywords;\n if (!Array.isArray(kws)) continue;\n const next: (KeywordRef | string)[] = [];\n for (const item of kws) {\n if (typeof item === \"object\" && item !== null && \"keyword_id\" in (item as object)) {\n // Already migrated.\n next.push(item as KeywordRef);\n continue;\n }\n if (typeof item !== \"string\") {\n diagnostics.push({ file, raw: String(item), reason: \"non-string, non-object keyword entry\" });\n next.push(item as unknown as string);\n continue;\n }\n const ref = parseKeywordString(item, catalog);\n if (ref) {\n next.push(ref);\n } else {\n diagnostics.push({ file, raw: item, reason: \"no catalog entry matches\" });\n // Keep the original string so the file still reads cleanly; a follow-up\n // pass can address it after the operator updates the catalog.\n next.push(item);\n }\n }\n profile.keywords = next;\n }\n}\n\n/** Normalise a `re-roll` modifier in place: condition → subset, drop condition. */\nfunction migrateRerollModifier(mod: Record<string, unknown>): boolean {\n if (typeof mod.subset === \"string\") return false; // already migrated\n const cond = mod.condition;\n if (cond === \"natural-1\") mod.subset = \"ones\";\n else if (cond === \"any-fail\") mod.subset = \"all-failures\";\n // Some pre-migration data encoded \"re-roll 1s\" as `value: 1` rather than a\n // `condition`. Honor that signal before defaulting, or the intent is lost.\n else if (typeof mod.value === \"number\" && mod.value === 1) mod.subset = \"ones\";\n else mod.subset = \"all-failures\";\n if (\"condition\" in mod) delete mod.condition;\n return true;\n}\n\n/** Walk a JSON value and migrate any `type: \"re-roll\"` single-effect modifiers. */\nfunction migrateRerollsIn(node: unknown): number {\n if (Array.isArray(node)) {\n let n = 0;\n for (const child of node) n += migrateRerollsIn(child);\n return n;\n }\n if (node && typeof node === \"object\") {\n const obj = node as Record<string, unknown>;\n let n = 0;\n if (obj.type === \"re-roll\" && obj.modifier && typeof obj.modifier === \"object\") {\n if (migrateRerollModifier(obj.modifier as Record<string, unknown>)) n += 1;\n }\n for (const value of Object.values(obj)) n += migrateRerollsIn(value);\n return n;\n }\n return 0;\n}\n\nfunction findJsonFiles(root: string, predicate: (name: string) => boolean): string[] {\n const out: string[] = [];\n for (const entry of readdirSync(root)) {\n const full = join(root, entry);\n if (statSync(full).isDirectory()) {\n out.push(...findJsonFiles(full, predicate));\n } else if (entry.endsWith(\".json\") && predicate(entry)) {\n out.push(full);\n }\n }\n return out;\n}\n\nfunction writeJsonPreservingTrailingNewline(file: string, value: unknown): void {\n writeFileSync(file, JSON.stringify(value, null, 2) + \"\\n\");\n}\n\nfunction main(): void {\n const catalog = loadCatalog();\n const diagnostics: Diagnostic[] = [];\n let weaponFiles = 0;\n let rerollFiles = 0;\n let rerollEffects = 0;\n\n // 1. Weapons.\n for (const root of DATA_ROOTS) {\n for (const file of findJsonFiles(root, (n) => n === \"weapons.json\" || n === \"weapons.example.json\")) {\n const data = JSON.parse(readFileSync(file, \"utf-8\")) as unknown;\n if (!Array.isArray(data)) continue;\n for (const weapon of data) migrateWeapon(weapon, catalog, file, diagnostics);\n writeJsonPreservingTrailingNewline(file, data);\n weaponFiles += 1;\n }\n }\n\n // 2. Re-roll DSL effects — abilities + any other file that carries effect trees.\n for (const root of DATA_ROOTS) {\n for (const file of findJsonFiles(root, (n) => n.endsWith(\".json\") && n !== \"weapons.json\")) {\n const raw = readFileSync(file, \"utf-8\");\n const before = raw;\n const data = JSON.parse(raw) as unknown;\n const n = migrateRerollsIn(data);\n if (n > 0) {\n const after = JSON.stringify(data, null, 2) + \"\\n\";\n if (after !== before) {\n writeFileSync(file, after);\n rerollFiles += 1;\n rerollEffects += n;\n }\n }\n }\n }\n\n console.log(`Weapon files rewritten: ${weaponFiles}`);\n console.log(`Re-roll effects normalised: ${rerollEffects} across ${rerollFiles} files`);\n if (diagnostics.length > 0) {\n console.log(`\\nDiagnostics — ${diagnostics.length} unmatched keyword strings:`);\n for (const d of diagnostics.slice(0, 30)) {\n console.log(` ${d.file.replace(REPO_ROOT + \"/\", \"\")} ${JSON.stringify(d.raw)} (${d.reason})`);\n }\n if (diagnostics.length > 30) console.log(` ... and ${diagnostics.length - 30} more`);\n }\n}\n\nmain();\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-10e-faction.d.ts","sourceRoot":"","sources":["../src/port-10e-faction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAeH,yEAAyE;AACzE,eAAO,MAAM,mBAAmB;;;CAA2D,CAAC;AAK5F,KAAK,IAAI,GAAG,GAAG,CAAC;AAEhB,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAYD,2EAA2E;AAC3E,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAO1C;AAoCD,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM,EAAE,CAelD;AAED,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM,EAAE,CAIxD;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAcvD;AAID,6DAA6D;AAC7D,wBAAgB,IAAI,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,CAEvC;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAS1F;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI,CAM/C;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI,CAK9C;AAED,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,IAAI,GAAG,IAAI,CAInD"}
|
package/dist/port-10e-faction.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-10e-faction.js","sourceRoot":"","sources":["../src/port-10e-faction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Port 10e archive faction data forward as an 11e provisional seed (Section 6).\n *\n * Usage:\n * npx tsx tools/src/port-10e-faction.ts <faction-id>\n * npx tsx tools/src/port-10e-faction.ts --all\n *\n * Reads data/{core,enrichment}/<faction>/*.json from the `10e-archive` ref,\n * applies the per-entity 11e transforms, writes the result into the working\n * tree, emits a per-faction audit under data/_port-audit/, and validates the\n * output against the (bumped) schemas. Exits non-zero on validation failure.\n *\n * Distinct from convert-faction.ts (the army-assist bootstrap); this script is\n * purpose-built for the archive→11e port and reads only from the git archive.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { writeFileSync, mkdirSync, readdirSync, readFileSync } from \"node:fs\";\nimport { resolve, dirname, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createValidator } from \"./schema-loader.js\";\nimport { validateFiles } from \"./validate.js\";\nimport { KNOWN_SUPPORT_10E } from \"./known-support-10e.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst ROOT = resolve(__dirname, \"../..\");\nconst ARCHIVE_REF = \"10e-archive\";\nconst AUDIT_DIR = \"data/_port-audit\";\n\n/** The provisional 11e dataslate every ported entity is stamped with. */\nexport const TARGET_GAME_VERSION = { edition: \"11th\", dataslate: \"pre-launch-provisional\" };\n\nconst ENTITY_ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Json = any;\n\nexport interface AuditItem {\n category: string;\n entity_type: string;\n entity_id: string;\n detail: string;\n}\n\n// ─── git archive access ──────────────────────────────────────────────\n\nfunction git(args: string[]): string {\n return execFileSync(\"git\", args, {\n cwd: ROOT,\n encoding: \"utf-8\",\n maxBuffer: 256 * 1024 * 1024,\n });\n}\n\n/** Faction directory names present under data/core/ on the archive ref. */\nexport function archiveFactions(): string[] {\n const out = git([\"ls-tree\", \"-d\", \"--name-only\", ARCHIVE_REF, \"data/core/\"]);\n return out\n .split(\"\\n\")\n .map((l) => basename(l.trim()))\n .filter((f) => f && f !== \"_example\")\n .sort();\n}\n\n/** JSON files present for a faction in a data area on the archive ref. */\nfunction archiveFiles(area: \"core\" | \"enrichment\", faction: string): string[] {\n let out: string;\n try {\n out = git([\"ls-tree\", \"-r\", \"--name-only\", ARCHIVE_REF, `data/${area}/${faction}`]);\n } catch {\n return [];\n }\n return out\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((p) => p.endsWith(\".json\"));\n}\n\nfunction readArchiveJSON(path: string): Json {\n return JSON.parse(git([\"show\", `${ARCHIVE_REF}:${path}`]));\n}\n\nfunction archiveSha(): string {\n return git([\"rev-parse\", ARCHIVE_REF]).trim();\n}\n\n// ─── DSL inspection helpers ──────────────────────────────────────────\n\n/** Collect every `type` string found anywhere in a DSL subtree. */\nfunction collectTypes(node: Json, acc: string[]): void {\n if (Array.isArray(node)) {\n for (const v of node) collectTypes(v, acc);\n } else if (node && typeof node === \"object\") {\n if (typeof node.type === \"string\") acc.push(node.type);\n for (const v of Object.values(node)) collectTypes(v, acc);\n }\n}\n\n/** Ability ids granted via `ability-grant` whose id mentions cover. */\nexport function coverGrants(effect: Json): string[] {\n const hits: string[] = [];\n const walk = (node: Json): void => {\n if (Array.isArray(node)) {\n for (const v of node) walk(v);\n } else if (node && typeof node === \"object\") {\n if (node.type === \"ability-grant\") {\n const aid = String(node.modifier?.ability_id ?? \"\");\n if (aid.toLowerCase().includes(\"cover\")) hits.push(aid);\n }\n for (const v of Object.values(node)) walk(v);\n }\n };\n walk(effect);\n return [...new Set(hits)];\n}\n\n/** Charge/Fights-First effect types 11e may make redundant. */\nexport function chargeTimingTypes(effect: Json): string[] {\n const types: string[] = [];\n collectTypes(effect, types);\n return [...new Set(types.filter((t) => t === \"fight-first\" || t === \"charged-this-turn\"))];\n}\n\n/** True if any string value equals the 10e engagement-range constant (1\"). */\nexport function referencesOneInch(entity: Json): boolean {\n let found = false;\n const walk = (node: Json): void => {\n if (found) return;\n if (Array.isArray(node)) {\n for (const v of node) walk(v);\n } else if (node && typeof node === \"object\") {\n for (const v of Object.values(node)) walk(v);\n } else if (typeof node === \"string\" && node.trim() === '1\"') {\n found = true;\n }\n };\n walk(entity);\n return found;\n}\n\n// ─── per-entity transforms (pure) ────────────────────────────────────\n\n/** Stamp the provisional 11e game_version onto an entity. */\nexport function bump(entity: Json): Json {\n return { ...entity, game_version: { ...TARGET_GAME_VERSION } };\n}\n\n/**\n * units.json: bump, mark points provisional, set attachment_role.\n *\n * Precedence: a unit in `supportIds` (curated registry) becomes `\"support\"`,\n * even if it isn't a leader_id (the cryptothralls case — non-character joiner).\n * Otherwise, a `leader_id` becomes `\"leader\"`. Non-attaching units get no role.\n */\nexport function portUnit(unit: Json, leaderIds: Set<string>, supportIds: Set<string>): Json {\n const out = bump(unit);\n out.points_provisional = true;\n if (supportIds.has(unit.id)) {\n out.attachment_role = \"support\";\n } else if (leaderIds.has(unit.id)) {\n out.attachment_role = \"leader\";\n }\n return out;\n}\n\n/** enhancements.json: bump, mark provisional, default-fill upgrade_tag/max_targets. */\nexport function portEnhancement(enh: Json): Json {\n const out = bump(enh);\n out.points_provisional = true;\n if (out.upgrade_tag === undefined) out.upgrade_tag = false;\n if (out.max_targets === undefined) out.max_targets = 1;\n return out;\n}\n\n/** detachments.json: bump, default-fill detachment_points/force_dispositions. */\nexport function portDetachment(det: Json): Json {\n const out = bump(det);\n if (out.detachment_points === undefined) out.detachment_points = null;\n if (out.force_dispositions === undefined) out.force_dispositions = [];\n return out;\n}\n\n/** leader-attachments.json: drop the retired max_leaders_per_unit, then bump. */\nexport function portLeaderAttachment(la: Json): Json {\n const { max_leaders_per_unit: _drop, ...rest } = la;\n void _drop;\n return bump(rest);\n}\n\n// ─── faction port ────────────────────────────────────────────────────\n\ninterface PortResult {\n faction: string;\n written: string[];\n summary: Record<string, number>;\n}\n\nfunction transformCore(name: string, entities: Json[], leaderIds: Set<string>, supportIds: Set<string>): Json[] {\n switch (name) {\n case \"units.json\":\n return entities.map((u) => portUnit(u, leaderIds, supportIds));\n case \"enhancements.json\":\n return entities.map(portEnhancement);\n case \"detachments.json\":\n return entities.map(portDetachment);\n case \"leader-attachments.json\":\n return entities.map(portLeaderAttachment);\n default:\n // factions, weapons, unit-compositions, stratagems — bump only\n return entities.map(bump);\n }\n}\n\nfunction writeJSON(rel: string, data: Json): void {\n const out = resolve(ROOT, rel);\n mkdirSync(dirname(out), { recursive: true });\n writeFileSync(out, JSON.stringify(data, null, 2) + \"\\n\");\n}\n\nfunction portFaction(faction: string): PortResult {\n if (!ENTITY_ID_RE.test(faction)) {\n throw new Error(`Invalid faction id: ${JSON.stringify(faction)}`);\n }\n\n const core: Record<string, Json[]> = {};\n for (const path of archiveFiles(\"core\", faction)) core[basename(path)] = readArchiveJSON(path);\n if (Object.keys(core).length === 0) {\n throw new Error(`No core data for faction '${faction}' on ${ARCHIVE_REF}`);\n }\n const enrichment: Record<string, Json[]> = {};\n for (const path of archiveFiles(\"enrichment\", faction)) {\n enrichment[basename(path)] = readArchiveJSON(path);\n }\n\n const leaderIds = new Set<string>((core[\"leader-attachments.json\"] ?? []).map((e) => e.leader_id));\n const supportIds = new Set<string>(KNOWN_SUPPORT_10E[faction] ?? []);\n\n const items: AuditItem[] = [];\n const written: string[] = [];\n\n // Warn before the port runs if a registry entry doesn't match an archive\n // unit — silent typos would otherwise mean the role is never applied.\n const unitIds = new Set<string>((core[\"units.json\"] ?? []).map((u) => u.id));\n for (const candidateId of supportIds) {\n if (!unitIds.has(candidateId)) {\n console.warn(` ⚠ ${faction}: support registry lists '${candidateId}' but no such unit in archive`);\n }\n }\n\n // Core files.\n for (const [name, entities] of Object.entries(core)) {\n const out = transformCore(name, entities, leaderIds, supportIds);\n const rel = `data/core/${faction}/${name}`;\n writeJSON(rel, out);\n written.push(rel);\n }\n\n // Record an audit entry per unit the registry assigned `attachment_role:\n // \"support\"` to — both as a visible roster of the call and so summary.md\n // surfaces the list for review.\n for (const candidateId of supportIds) {\n if (!unitIds.has(candidateId)) continue;\n items.push({\n category: \"support-assigned\",\n entity_type: \"unit\",\n entity_id: candidateId,\n detail: `Assigned attachment_role: \"support\" from the curated registry. To revert, remove from tools/src/known-support-10e.ts and re-run the port.`,\n });\n }\n\n // Enrichment files + ability audits.\n for (const [name, entities] of Object.entries(enrichment)) {\n if (name === \"abilities.json\") {\n for (const a of entities) {\n const cover = coverGrants(a.effect ?? {});\n if (cover.length) {\n items.push({\n category: \"cover-ability\",\n entity_type: \"ability\",\n entity_id: a.ability_id,\n detail: `Grants ${cover.join(\", \")}; 11e cover is −1 BS (not +1 Sv) — re-check the granted ability's definition.`,\n });\n }\n const charge = chargeTimingTypes(a.effect ?? {});\n if (charge.length) {\n items.push({\n category: \"charge-timing\",\n entity_type: \"ability\",\n entity_id: a.ability_id,\n detail: `Uses ${charge.join(\", \")}; 11e charging grants Fights First by default — check for redundancy.`,\n });\n }\n if (referencesOneInch(a)) {\n items.push({\n category: \"engagement-range\",\n entity_type: \"ability\",\n entity_id: a.ability_id,\n detail: `References 1\"; 11e engagement range is 2\" — confirm intent.`,\n });\n }\n }\n }\n const rel = `data/enrichment/${faction}/${name}`;\n writeJSON(rel, entities.map(bump));\n written.push(rel);\n }\n\n // Bulk-review categories: every detachment / stratagem needs human work later.\n const bulkReview: Json = {};\n if (core[\"detachments.json\"]) {\n bulkReview[\"detachment-disposition\"] = core[\"detachments.json\"].map((d) => d.id);\n }\n if (core[\"stratagems.json\"]) {\n bulkReview[\"stratagem-type\"] = core[\"stratagems.json\"].length;\n }\n\n const summary: Record<string, number> = {};\n for (const it of items) summary[it.category] = (summary[it.category] ?? 0) + 1;\n\n writeJSON(`${AUDIT_DIR}/${faction}.json`, {\n faction,\n ported_from_ref: archiveSha(),\n summary,\n items,\n bulk_review: bulkReview,\n });\n\n return { faction, written, summary };\n}\n\n/** Rebuild summary.md from every per-faction audit json currently on disk. */\nfunction regenerateSummary(): void {\n const dir = resolve(ROOT, AUDIT_DIR);\n const audits = readdirSync(dir)\n .filter((f) => f.endsWith(\".json\"))\n .map((f) => JSON.parse(readFileSync(resolve(dir, f), \"utf-8\")))\n .sort((a, b) => a.faction.localeCompare(b.faction));\n\n const itemsOf = (a: Json, category: string): AuditItem[] =>\n (a.items as AuditItem[]).filter((i) => i.category === category);\n\n const lines: string[] = [\n \"# 10e → 11e port — audit queue\",\n \"\",\n \"Generated by `tools/src/port-10e-faction.ts`. Each item below needs human\",\n \"review before the provisional seed is treated as confirmed 11e data.\",\n \"\",\n ];\n\n // Flat roster of units the port assigned attachment_role: \"support\".\n lines.push(\"## Support — assigned by the port\");\n lines.push(\"\");\n lines.push(\n \"The units below were written with `attachment_role: \\\"support\\\"` by the port,\",\n \"sourced from the curated 10e \\\"additional leader\\\" registry\",\n \"(`tools/src/known-support-10e.ts`).\",\n \"\",\n \"To **revert** any entry: remove the id from the registry and re-run\",\n \"`npx tsx tools/src/port-10e-faction.ts --all`. The port is the durable\",\n \"source of truth — don't hand-edit `attachment_role` in faction units.json.\",\n \"\",\n );\n const assigned = audits.flatMap((a) =>\n itemsOf(a, \"support-assigned\").map((i) => ({ faction: a.faction, id: i.entity_id })),\n );\n for (const c of assigned) {\n lines.push(`- \\`${c.faction}\\` / **${c.id}**`);\n }\n lines.push(\"\", `_${assigned.length} units assigned._`, \"\");\n\n // Per-faction detail with names for the other discretionary flags.\n lines.push(\"## Per-faction detail\", \"\");\n for (const a of audits) {\n const cover = itemsOf(a, \"cover-ability\").map((i) => i.entity_id);\n const charge = itemsOf(a, \"charge-timing\").map((i) => i.entity_id);\n const engage = itemsOf(a, \"engagement-range\").map((i) => i.entity_id);\n const support = itemsOf(a, \"support-assigned\").map((i) => i.entity_id);\n const dets = a.bulk_review[\"detachment-disposition\"] ?? [];\n const strats = a.bulk_review[\"stratagem-type\"] ?? 0;\n if (!cover.length && !charge.length && !engage.length && !support.length && !dets.length && !strats) {\n continue;\n }\n lines.push(`### ${a.faction}`, \"\");\n if (support.length) lines.push(`- Support candidates: ${fmtIds(support)}`);\n if (cover.length) lines.push(`- Cover abilities (rework \\`benefit-of-cover\\` → −1 BS): ${fmtIds(cover)}`);\n if (charge.length) lines.push(`- Charge-timing / Fights First (check redundancy): ${fmtIds(charge)}`);\n if (engage.length) lines.push(`- Engagement-range (1\" → 2\"): ${fmtIds(engage)}`);\n if (dets.length) lines.push(`- Detachments needing DP + Force Disposition assignment (${dets.length}): ${fmtIds(dets)}`);\n if (strats) lines.push(`- Stratagems pending 11e type-enum reconciliation: ${strats}`);\n lines.push(\"\");\n }\n\n const out = resolve(ROOT, `${AUDIT_DIR}/summary.md`);\n mkdirSync(dirname(out), { recursive: true });\n writeFileSync(out, lines.join(\"\\n\") + \"\\n\");\n}\n\n/** Render a list of ids as inline-code, comma-separated. */\nfunction fmtIds(ids: string[]): string {\n return ids.map((i) => `\\`${i}\\``).join(\", \");\n}\n\nasync function validateFaction(faction: string): Promise<number> {\n const ajv = createValidator();\n const core = await validateFiles(ajv, `core/${faction}/**/*.json`);\n const enrich = await validateFiles(ajv, `enrichment/${faction}/**/*.json`);\n const failed = core.failed + enrich.failed;\n if (failed) {\n for (const e of [...core.errors, ...enrich.errors]) {\n const where = e.index >= 0 ? `[${e.index}]` : \"\";\n console.error(` ✗ ${basename(e.file)}${where}: ${e.errors.map((x) => `${x.path} ${x.message}`).join(\"; \")}`);\n }\n }\n return failed;\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args.length === 0 || args[0] === \"--help\") {\n console.log(\"Usage: npx tsx tools/src/port-10e-faction.ts <faction-id>\");\n console.log(\" npx tsx tools/src/port-10e-faction.ts --all\");\n console.log(\" npx tsx tools/src/port-10e-faction.ts --summary (rebuild audit summary only)\");\n process.exit(args[0] === \"--help\" ? 0 : 1);\n }\n\n if (args[0] === \"--summary\") {\n regenerateSummary();\n console.log(`Rebuilt ${AUDIT_DIR}/summary.md`);\n return;\n }\n\n const factions = args[0] === \"--all\" ? archiveFactions() : [args[0]];\n let totalFailed = 0;\n\n for (const faction of factions) {\n console.log(`\\n▶ porting ${faction}`);\n const res = portFaction(faction);\n for (const w of res.written) console.log(` ✓ ${w}`);\n const failed = await validateFaction(faction);\n if (failed) {\n console.error(` ✗ ${faction}: ${failed} validation failure(s)`);\n totalFailed += failed;\n } else {\n const cats = Object.entries(res.summary).map(([k, v]) => `${k}=${v}`).join(\", \");\n console.log(` ✓ ${faction} validates (audit: ${cats || \"none\"})`);\n }\n }\n\n regenerateSummary();\n console.log(`\\n${factions.length} faction(s) ported. Audit queue: ${AUDIT_DIR}/summary.md`);\n if (totalFailed) process.exit(1);\n}\n\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") === fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\n\nif (isMain) {\n main().catch((err) => {\n console.error(err);\n process.exit(1);\n });\n}\n"]}
|
package/dist/report.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7C,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAgCjF"}
|
package/dist/report.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,UAAU,YAAY,CAAC,MAAwB,EAAE,IAAkB;IACvE,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC,CAAC;IACvD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,KAAK,CAAC,IAAI,CAAC,mBAAmB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACnD,KAAK,CAAC,IAAI,CAAC,oBAAoB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACpD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAEpD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;YAC/C,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;gBAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["import chalk from \"chalk\";\nimport type { ValidationResult } from \"./validate.js\";\n\nexport type ReporterMode = \"pretty\" | \"json\";\n\nexport function formatReport(result: ValidationResult, mode: ReporterMode): string {\n if (mode === \"json\") {\n return JSON.stringify(result, null, 2);\n }\n\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(\"40kdc Data Validation Report\"));\n lines.push(chalk.gray(\"─\".repeat(40)));\n lines.push(`Files scanned: ${result.totalFiles}`);\n lines.push(`Items validated: ${result.totalItems}`);\n lines.push(chalk.green(`Passed: ${result.passed}`));\n\n if (result.failed > 0) {\n lines.push(chalk.red(`Failed: ${result.failed}`));\n lines.push(\"\");\n\n for (const err of result.errors) {\n const loc = err.index >= 0 ? `[${err.index}]` : \"\";\n lines.push(chalk.red(` ✗ ${err.file}${loc}`));\n for (const e of err.errors) {\n lines.push(chalk.yellow(` ${e.path}: ${e.message}`));\n }\n }\n } else {\n lines.push(chalk.green(`Failed: 0`));\n lines.push(\"\");\n lines.push(chalk.green(\"All validations passed.\"));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rube-goldberg.d.ts","sourceRoot":"","sources":["../src/rube-goldberg.ts"],"names":[],"mappings":""}
|