@alpaca-software/40kdc-data 0.5.14 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/abilities-resolver/resolver.d.ts.map +1 -1
  2. package/dist/abilities-resolver/resolver.js +6 -2
  3. package/dist/abilities-resolver/resolver.js.map +1 -1
  4. package/dist/audit-coverage.d.ts.map +1 -1
  5. package/dist/audit-coverage.js +90 -1
  6. package/dist/audit-coverage.js.map +1 -1
  7. package/dist/audit-phrasing.d.ts +64 -0
  8. package/dist/audit-phrasing.d.ts.map +1 -0
  9. package/dist/audit-phrasing.js +208 -0
  10. package/dist/audit-phrasing.js.map +1 -0
  11. package/dist/build-share-registry.d.ts +2 -0
  12. package/dist/build-share-registry.d.ts.map +1 -0
  13. package/dist/build-share-registry.js +115 -0
  14. package/dist/build-share-registry.js.map +1 -0
  15. package/dist/cli.js +7 -0
  16. package/dist/cli.js.map +1 -1
  17. package/dist/codegen-data.js +23 -0
  18. package/dist/codegen-data.js.map +1 -1
  19. package/dist/commands/translate.d.ts.map +1 -1
  20. package/dist/commands/translate.js +2 -5
  21. package/dist/commands/translate.js.map +1 -1
  22. package/dist/commands/validate-all.d.ts.map +1 -1
  23. package/dist/commands/validate-all.js +8 -3
  24. package/dist/commands/validate-all.js.map +1 -1
  25. package/dist/data/bundle.generated.js +1 -1
  26. package/dist/data/bundle.generated.js.map +1 -1
  27. package/dist/data/dataset.d.ts +33 -1
  28. package/dist/data/dataset.d.ts.map +1 -1
  29. package/dist/data/dataset.js +107 -1
  30. package/dist/data/dataset.js.map +1 -1
  31. package/dist/data/index.d.ts +4 -0
  32. package/dist/data/index.d.ts.map +1 -1
  33. package/dist/data/index.js +4 -0
  34. package/dist/data/index.js.map +1 -1
  35. package/dist/data/types.d.ts +3 -1
  36. package/dist/data/types.d.ts.map +1 -1
  37. package/dist/data/types.js +1 -0
  38. package/dist/data/types.js.map +1 -1
  39. package/dist/gen-conformance.js +162 -0
  40. package/dist/gen-conformance.js.map +1 -1
  41. package/dist/generated.d.ts +125 -0
  42. package/dist/generated.d.ts.map +1 -1
  43. package/dist/generated.js.map +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/integrity.d.ts +32 -0
  49. package/dist/integrity.d.ts.map +1 -0
  50. package/dist/integrity.js +120 -0
  51. package/dist/integrity.js.map +1 -0
  52. package/dist/report.d.ts +1 -1
  53. package/dist/report.d.ts.map +1 -1
  54. package/dist/report.js +2 -2
  55. package/dist/report.js.map +1 -1
  56. package/dist/runner.d.ts.map +1 -1
  57. package/dist/runner.js +42 -0
  58. package/dist/runner.js.map +1 -1
  59. package/dist/share/codec.d.ts +83 -0
  60. package/dist/share/codec.d.ts.map +1 -0
  61. package/dist/share/codec.js +230 -0
  62. package/dist/share/codec.js.map +1 -0
  63. package/dist/share/index.d.ts +30 -0
  64. package/dist/share/index.d.ts.map +1 -0
  65. package/dist/share/index.js +36 -0
  66. package/dist/share/index.js.map +1 -0
  67. package/dist/share/registry.d.ts +42 -0
  68. package/dist/share/registry.d.ts.map +1 -0
  69. package/dist/share/registry.generated.d.ts +4 -0
  70. package/dist/share/registry.generated.d.ts.map +1 -0
  71. package/dist/share/registry.generated.js +4 -0
  72. package/dist/share/registry.generated.js.map +1 -0
  73. package/dist/share/registry.js +68 -0
  74. package/dist/share/registry.js.map +1 -0
  75. package/dist/terrain/index.d.ts +2 -2
  76. package/dist/terrain/index.d.ts.map +1 -1
  77. package/dist/terrain/index.js +1 -1
  78. package/dist/terrain/index.js.map +1 -1
  79. package/dist/terrain/solve.d.ts +76 -0
  80. package/dist/terrain/solve.d.ts.map +1 -1
  81. package/dist/terrain/solve.js +138 -0
  82. package/dist/terrain/solve.js.map +1 -1
  83. package/dist/translate/condition.d.ts +1 -0
  84. package/dist/translate/condition.d.ts.map +1 -1
  85. package/dist/translate/condition.js +101 -7
  86. package/dist/translate/condition.js.map +1 -1
  87. package/dist/translate/effect.d.ts +28 -20
  88. package/dist/translate/effect.d.ts.map +1 -1
  89. package/dist/translate/effect.js +483 -114
  90. package/dist/translate/effect.js.map +1 -1
  91. package/dist/validate.d.ts.map +1 -1
  92. package/dist/validate.js +1 -0
  93. package/dist/validate.js.map +1 -1
  94. package/package.json +3 -1
  95. package/schemas/core/allied-rule.schema.json +115 -0
  96. package/schemas/core/detachment.schema.json +38 -1
@@ -28,6 +28,7 @@ import { describeScoringCard, describeAbility } from "./translate/index.js";
28
28
  import { awardsOf } from "./scoring/index.js";
29
29
  import { createRunnerState, dispatch } from "./runner.js";
30
30
  import { exportRoster } from "./export/index.js";
31
+ import { decodeShareToken, encodeShareToken, shareRegistryVersion, } from "./share/index.js";
31
32
  import { importRoster, REGISTERED_ADAPTERS } from "./import/import-roster.js";
32
33
  import { selectAdapter } from "./import/adapter.js";
33
34
  import { encodeBase } from "./runner.js";
@@ -235,6 +236,18 @@ const LINKED_API_QUERIES = [
235
236
  { name: "base_size_of windriders (draft flying base)", query: "base_size_of", args: { unitId: "windriders" }, comparison: "scalar" },
236
237
  // model_bases_of(unit): ordered per-model bases; jakhals mixes 28.5mm bodies with a 40mm Dishonoured.
237
238
  { name: "model_bases_of jakhals (mixed)", query: "model_bases_of", args: { unitId: "jakhals" }, comparison: "ordered" },
239
+ // units_with_keyword: case-insensitive over keywords ∪ faction_keywords; compared as set.
240
+ { name: "units_with_keyword Khorne", query: "units_with_keyword", args: { keyword: "Khorne" }, comparison: "set" },
241
+ { name: "units_with_keyword damned (lowercase)", query: "units_with_keyword", args: { keyword: "damned" }, comparison: "set" },
242
+ // allies_for: two-gate offer; ordered (allied-rules data-file order, locale-independent).
243
+ { name: "allies_for chaos-knights (no detachment)", query: "allies_for", args: { factionId: "chaos-knights" }, comparison: "ordered" },
244
+ { name: "allies_for chaos-knights with iconoclast-fiefdom", query: "allies_for", args: { factionId: "chaos-knights", detachmentIds: ["iconoclast-fiefdom"] }, comparison: "ordered" },
245
+ { name: "allies_for world-eaters (two pools)", query: "allies_for", args: { factionId: "world-eaters" }, comparison: "ordered" },
246
+ { name: "allies_for genestealer-cults with final-day", query: "allies_for", args: { factionId: "genestealer-cults", detachmentIds: ["final-day"] }, comparison: "ordered" },
247
+ // ally_units_for: resolved pool; compared as set (accessor sorts by name, locale-dependent).
248
+ { name: "ally_units_for iconoclast-fiefdom-damned", query: "ally_units_for", args: { ruleId: "iconoclast-fiefdom-damned" }, comparison: "set" },
249
+ { name: "ally_units_for world-eaters-khorne-daemons", query: "ally_units_for", args: { ruleId: "world-eaters-khorne-daemons" }, comparison: "set" },
250
+ { name: "ally_units_for star-childrens-blessings (excludes broodlord/genestealers)", query: "ally_units_for", args: { ruleId: "star-childrens-blessings" }, comparison: "set" },
238
251
  ];
239
252
  function genLinkedApi() {
240
253
  const ds = Dataset.embedded();
@@ -304,6 +317,13 @@ function runLinkedQuery(ds, q) {
304
317
  const comp = ds.unitCompositions.find((c) => c.unit_id === q.args.unitId);
305
318
  return (comp?.models ?? []).map((m) => `${m.name}=${encodeBase(m.base_size_mm) ?? "none"}`);
306
319
  }
320
+ case "units_with_keyword":
321
+ return ds.unitsWithKeyword(q.args.keyword).map((u) => u.id).sort();
322
+ case "allies_for":
323
+ // Ordered: allied-rules data-file order (deterministic across impls).
324
+ return ds.alliesFor(q.args.factionId, q.args.detachmentIds ?? []).map((r) => r.id);
325
+ case "ally_units_for":
326
+ return ds.allyUnitsFor(q.args.ruleId).map((u) => u.id).sort();
307
327
  }
308
328
  }
309
329
  /**
@@ -971,6 +991,147 @@ function genEffectTranslation() {
971
991
  writeJson(join(CONFORMANCE, "effect-translation", "cases.json"), cases);
972
992
  console.log(`effect-translation/cases.json: ${cases.length} cases (${seen.size} node types)`);
973
993
  }
994
+ /** Unsigned LEB128 of a small non-negative integer (for hand-built tokens). */
995
+ function leb128(value) {
996
+ const out = [];
997
+ let v = value;
998
+ while (v >= 0x80) {
999
+ out.push((v & 0x7f) | 0x80);
1000
+ v = Math.floor(v / 0x80);
1001
+ }
1002
+ out.push(v);
1003
+ return out;
1004
+ }
1005
+ /** base64url (no padding) of a raw byte array — for negative decode goldens. */
1006
+ function bytesToBase64url(bytes) {
1007
+ return Buffer.from(bytes)
1008
+ .toString("base64")
1009
+ .replace(/\+/g, "-")
1010
+ .replace(/\//g, "_")
1011
+ .replace(/=+$/, "");
1012
+ }
1013
+ /**
1014
+ * The share-token corpus. Each positive case pins `encodeShareToken(list)` to an
1015
+ * exact `token` and asserts `decodeShareToken(token)` round-trips to `list`;
1016
+ * negative cases pin the decoder's `malformed` / `stale-registry` verdicts. The
1017
+ * input lists are derived from the embedded dataset deterministically (sorted,
1018
+ * first-N) so the goldens are stable and self-maintaining. Rust and Python must
1019
+ * reproduce every `token` byte-for-byte and every decode verdict.
1020
+ */
1021
+ function genShare() {
1022
+ mkdirSync(join(CONFORMANCE, "share"), { recursive: true });
1023
+ const ds = Dataset.embedded();
1024
+ const f = "adeptus-astartes";
1025
+ const dets = ds.detachments.all
1026
+ .filter((d) => d.faction_id === f)
1027
+ .map((d) => d.id)
1028
+ .sort();
1029
+ const units = ds.units
1030
+ .byFaction(f)
1031
+ .map((u) => u.id)
1032
+ .sort();
1033
+ const weapons = ds.weapons.all.map((w) => w.id).sort();
1034
+ const enhancement = ds.enhancements.all.map((e) => e.id).sort()[0];
1035
+ const disposition = ds.forceDispositions.all.map((d) => d.id).sort()[0];
1036
+ const allyFactionId = "chaos-daemons";
1037
+ const allyRuleId = ds.alliedRules.all.map((r) => r.id).sort()[0];
1038
+ const plainUnit = (id) => ({
1039
+ datasheetId: id,
1040
+ modelCount: 1,
1041
+ isWarlord: false,
1042
+ enhancementId: null,
1043
+ allyFactionId: null,
1044
+ allyRuleId: null,
1045
+ attachedToOrdinal: null,
1046
+ grants: [],
1047
+ loadout: [],
1048
+ });
1049
+ const lists = [
1050
+ {
1051
+ name: "minimal",
1052
+ list: {
1053
+ name: "",
1054
+ factionId: f,
1055
+ detachmentIds: [],
1056
+ battleSize: "strike-force",
1057
+ disposition: null,
1058
+ units: [],
1059
+ },
1060
+ },
1061
+ {
1062
+ name: "warlord-enhancement-grants",
1063
+ list: {
1064
+ name: "Strîke Force 🔨",
1065
+ factionId: f,
1066
+ detachmentIds: [dets[0]],
1067
+ battleSize: "strike-force",
1068
+ disposition,
1069
+ units: [
1070
+ {
1071
+ ...plainUnit(units[0]),
1072
+ modelCount: 5,
1073
+ isWarlord: true,
1074
+ enhancementId: enhancement,
1075
+ grants: ["Character"],
1076
+ loadout: [
1077
+ [weapons[0], 2],
1078
+ [weapons[1], 1],
1079
+ ],
1080
+ },
1081
+ { ...plainUnit(units[1]), modelCount: 10, loadout: [[weapons[2], 10]] },
1082
+ ],
1083
+ },
1084
+ },
1085
+ {
1086
+ name: "allies-attachment",
1087
+ list: {
1088
+ name: "Soup",
1089
+ factionId: f,
1090
+ detachmentIds: [dets[0], dets[1]],
1091
+ battleSize: "incursion",
1092
+ disposition,
1093
+ units: [
1094
+ { ...plainUnit(units[0]), isWarlord: true, attachedToOrdinal: 1 }, // leader → bodyguard
1095
+ { ...plainUnit(units[1]), modelCount: 5 },
1096
+ {
1097
+ ...plainUnit(units[2]),
1098
+ allyFactionId,
1099
+ allyRuleId,
1100
+ grants: ["Battleline"],
1101
+ loadout: [[weapons[3], 1]],
1102
+ },
1103
+ ],
1104
+ },
1105
+ },
1106
+ ];
1107
+ const cases = lists.map(({ name, list }) => {
1108
+ const token = encodeShareToken(list);
1109
+ const decoded = decodeShareToken(token);
1110
+ if (!decoded.ok)
1111
+ throw new Error(`share case ${name} failed to round-trip: ${decoded.reason}`);
1112
+ // Sanity: the generator's own round-trip must reproduce the input exactly.
1113
+ if (JSON.stringify(decoded.list) !== JSON.stringify(list)) {
1114
+ throw new Error(`share case ${name} did not round-trip losslessly`);
1115
+ }
1116
+ return { name, list, token };
1117
+ });
1118
+ // Negative decode cases (deterministic hand-built tokens).
1119
+ cases.push({
1120
+ name: "malformed-bad-format-byte",
1121
+ decode_token: bytesToBase64url([0x00]),
1122
+ expected_decode: { ok: false, reason: "malformed" },
1123
+ });
1124
+ cases.push({
1125
+ name: "stale-future-index",
1126
+ // Valid header (format 1, registry version, empty name) then a faction
1127
+ // index no committed registry will ever hold → stale-registry.
1128
+ decode_token: bytesToBase64url([0x01, ...leb128(shareRegistryVersion), 0x00, ...leb128(2_000_000)]),
1129
+ expected_decode: { ok: false, reason: "stale-registry" },
1130
+ });
1131
+ writeJson(join(CONFORMANCE, "share", "cases.json"), cases);
1132
+ console.log(`share/cases.json: ${cases.length} cases (registry v${shareRegistryVersion}, ` +
1133
+ `${cases.filter((c) => c.token).length} round-trip + ${cases.filter((c) => c.decode_token).length} negative)`);
1134
+ }
974
1135
  genNormalize();
975
1136
  genRosters();
976
1137
  genLinkedApi();
@@ -980,4 +1141,5 @@ genEffectTranslation();
980
1141
  genScoring();
981
1142
  genTerrainResolver();
982
1143
  genTerrainKeystones();
1144
+ genShare();
983
1145
  //# sourceMappingURL=gen-conformance.js.map