@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.
- package/dist/abilities-resolver/resolver.d.ts.map +1 -1
- package/dist/abilities-resolver/resolver.js +6 -2
- package/dist/abilities-resolver/resolver.js.map +1 -1
- package/dist/audit-coverage.d.ts.map +1 -1
- package/dist/audit-coverage.js +90 -1
- package/dist/audit-coverage.js.map +1 -1
- package/dist/audit-phrasing.d.ts +64 -0
- package/dist/audit-phrasing.d.ts.map +1 -0
- package/dist/audit-phrasing.js +208 -0
- package/dist/audit-phrasing.js.map +1 -0
- package/dist/build-share-registry.d.ts +2 -0
- package/dist/build-share-registry.d.ts.map +1 -0
- package/dist/build-share-registry.js +115 -0
- package/dist/build-share-registry.js.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/codegen-data.js +23 -0
- package/dist/codegen-data.js.map +1 -1
- package/dist/commands/translate.d.ts.map +1 -1
- package/dist/commands/translate.js +2 -5
- package/dist/commands/translate.js.map +1 -1
- package/dist/commands/validate-all.d.ts.map +1 -1
- package/dist/commands/validate-all.js +8 -3
- package/dist/commands/validate-all.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/dataset.d.ts +33 -1
- package/dist/data/dataset.d.ts.map +1 -1
- package/dist/data/dataset.js +107 -1
- package/dist/data/dataset.js.map +1 -1
- package/dist/data/index.d.ts +4 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +4 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/types.d.ts +3 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/data/types.js +1 -0
- package/dist/data/types.js.map +1 -1
- package/dist/gen-conformance.js +162 -0
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +125 -0
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrity.d.ts +32 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +120 -0
- package/dist/integrity.js.map +1 -0
- package/dist/report.d.ts +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +2 -2
- package/dist/report.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +42 -0
- package/dist/runner.js.map +1 -1
- package/dist/share/codec.d.ts +83 -0
- package/dist/share/codec.d.ts.map +1 -0
- package/dist/share/codec.js +230 -0
- package/dist/share/codec.js.map +1 -0
- package/dist/share/index.d.ts +30 -0
- package/dist/share/index.d.ts.map +1 -0
- package/dist/share/index.js +36 -0
- package/dist/share/index.js.map +1 -0
- package/dist/share/registry.d.ts +42 -0
- package/dist/share/registry.d.ts.map +1 -0
- package/dist/share/registry.generated.d.ts +4 -0
- package/dist/share/registry.generated.d.ts.map +1 -0
- package/dist/share/registry.generated.js +4 -0
- package/dist/share/registry.generated.js.map +1 -0
- package/dist/share/registry.js +68 -0
- package/dist/share/registry.js.map +1 -0
- package/dist/terrain/index.d.ts +2 -2
- package/dist/terrain/index.d.ts.map +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/index.js.map +1 -1
- package/dist/terrain/solve.d.ts +76 -0
- package/dist/terrain/solve.d.ts.map +1 -1
- package/dist/terrain/solve.js +138 -0
- package/dist/terrain/solve.js.map +1 -1
- package/dist/translate/condition.d.ts +1 -0
- package/dist/translate/condition.d.ts.map +1 -1
- package/dist/translate/condition.js +101 -7
- package/dist/translate/condition.js.map +1 -1
- package/dist/translate/effect.d.ts +28 -20
- package/dist/translate/effect.d.ts.map +1 -1
- package/dist/translate/effect.js +483 -114
- package/dist/translate/effect.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +1 -0
- package/dist/validate.js.map +1 -1
- package/package.json +3 -1
- package/schemas/core/allied-rule.schema.json +115 -0
- package/schemas/core/detachment.schema.json +38 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/abilities-resolver/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,KAAK,EAAa,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,yEAAyE;IACzE,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB,CAAC;AAEF,gFAAgF;AAChF,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,KAAK,GACX,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/abilities-resolver/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,KAAK,EAAa,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,yEAAyE;IACzE,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB,CAAC;AAEF,gFAAgF;AAChF,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,KAAK,GACX,eAAe,EAAE,CA2GnB"}
|
|
@@ -34,8 +34,12 @@ export function resolveEligibleAbilities(dataset, input, phase) {
|
|
|
34
34
|
phases: intersect(ability.phases, phase),
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
// 3. Detachment stratagems.
|
|
38
|
-
|
|
37
|
+
// 3. Detachment stratagems. A generic Codex detachment id is shared across
|
|
38
|
+
// every faction's view, so prefer the copy in the resolving faction (its
|
|
39
|
+
// stratagem list is identical across copies, but this keeps lookups
|
|
40
|
+
// faction-correct); fall back to the first registered copy.
|
|
41
|
+
const detachment = dataset.detachments.getInFaction(input.detachmentId, factionId) ??
|
|
42
|
+
dataset.detachments.get(input.detachmentId);
|
|
39
43
|
if (detachment) {
|
|
40
44
|
for (const stratId of detachment.stratagem_ids ?? []) {
|
|
41
45
|
const stratagem = dataset.stratagems.get(stratId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../src/abilities-resolver/resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Walks the dataset for every ability that could apply to a chosen unit in a\n * chosen phase. The SPA passes the result through to the buff layer (each\n * {@link EligibleAbility} carries `.getBuffs()`, the source is pre-tagged).\n *\n * Resolution order — stable for snapshot tests:\n *\n * 1. **army** — faction-scoped abilities whose `ability_type` is `\"faction\"`\n * and whose `faction_id` matches the input.\n * 2. **detachment** — abilities authored against the detachment\n * (`ability_type` is `\"detachment\"`, `detachment_id` matches).\n * 3. **detachment-stratagem** — stratagems on the detachment, each yielding\n * the ability referenced by `stratagem.ability_id` (if any).\n * 4. **unit** — abilities listed in `unit.ability_ids`.\n * 5. **attached** — abilities of each attached member. A leader + bodyguard\n * (and, in 11th, support) form one combined unit, so every member's\n * abilities apply to the whole unit — pulled in full (not aura-filtered),\n * whichever member is the selected/firing unit.\n * 6. **support** — abilities on supporting units whose scope range is an\n * aura (not `self` / `unit`).\n *\n * Each step phase-filters via the existing `Dataset.phasesFor` index. The\n * resolver collects abilities first, *then* filters by phase, so the SPA can\n * also ask \"what abilities are eligible across all phases?\" by passing every\n * phase (today the API requires a single phase; if the SPA wants the wide\n * view it can call the resolver once per phase).\n */\nimport type { Phase, Stratagem } from \"../generated.js\";\nimport type { Dataset } from \"../data/dataset.js\";\nimport type { AbilityView } from \"../data/entities.js\";\n\nexport type EligibleAbilitySource =\n | { kind: \"army\" }\n | { kind: \"detachment\"; detachmentId: string }\n | { kind: \"detachment-stratagem\"; stratagemId: string; cpCost: number }\n | { kind: \"unit\"; unitId: string }\n | { kind: \"attached\"; unitId: string }\n | { kind: \"support\"; sourceUnitId: string };\n\nexport type EligibilityInput = {\n unitId: string;\n /** Overrides the unit's own `faction_id` when given (for inheritance cases). */\n factionId?: string;\n detachmentId?: string;\n /**\n * Other members of the combined (\"attached\") unit — the attached leader, its\n * bodyguard, or (11th) support attachments — whichever is *not* the selected\n * `unitId`. Their abilities are pooled onto the combined unit. A list so\n * multi-member attachments need no shape change; order is preserved.\n */\n attachedUnitIds?: string[];\n /** Friendly units whose auras could apply (M2 walks only their aura-ranged abilities). */\n supportingUnitIds?: string[];\n};\n\nexport type EligibleAbility = {\n ability: AbilityView;\n source: EligibleAbilitySource;\n /** The subset of `ability.phases` that intersect the requested phase. */\n phases: Phase[];\n};\n\n/** Compute the sorted-by-source eligible-ability list for one (unit, phase). */\nexport function resolveEligibleAbilities(\n dataset: Dataset,\n input: EligibilityInput,\n phase: Phase,\n): EligibleAbility[] {\n // Resolve units within the faction when one is known. Unit ids are shared\n // across factions (a shared chassis like `chaos-land-raider` lives under\n // several Chaos factions), so a faction-blind `get` can return the wrong\n // faction's copy — and with it the wrong intrinsic abilities/keywords.\n const resolveUnit = (id: string, fid: string | undefined) =>\n (fid ? dataset.units.getInFaction(id, fid) : undefined) ?? dataset.units.get(id);\n\n const unit = resolveUnit(input.unitId, input.factionId);\n if (!unit) return [];\n const factionId = input.factionId ?? unit.raw.faction_id;\n const seen = new Set<string>();\n const out: EligibleAbility[] = [];\n\n // 1. Army — faction-scoped abilities (faction rule + any other faction-typed).\n for (const ability of dataset.abilities.byFaction(factionId)) {\n if (ability.raw.ability_type !== \"faction\") continue;\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, { ability, source: { kind: \"army\" }, phases: intersect(ability.phases, phase) });\n }\n\n // 2. Detachment abilities — abilities whose detachment_id matches.\n if (input.detachmentId) {\n for (const ability of dataset.abilities) {\n if (ability.raw.ability_type !== \"detachment\") continue;\n if (ability.raw.detachment_id !== input.detachmentId) continue;\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"detachment\", detachmentId: input.detachmentId },\n phases: intersect(ability.phases, phase),\n });\n }\n\n // 3. Detachment stratagems.\n const detachment = dataset.detachments.get(input.detachmentId);\n if (detachment) {\n for (const stratId of detachment.stratagem_ids ?? []) {\n const stratagem = dataset.stratagems.get(stratId);\n if (!stratagem) continue;\n if (!stratagemPhaseMatches(stratagem, phase)) continue;\n const ability =\n stratagem.ability_id !== null && stratagem.ability_id !== undefined\n ? dataset.abilities.get(stratagem.ability_id)\n : undefined;\n if (!ability) continue;\n pushUnique(out, seen, {\n ability,\n source: {\n kind: \"detachment-stratagem\",\n stratagemId: stratagem.id,\n cpCost: stratagem.cp_cost,\n },\n phases: [phase], // the stratagem's printed phase governs eligibility.\n });\n }\n }\n }\n\n // 4. Unit's own abilities.\n for (const ability of unit.abilities) {\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"unit\", unitId: input.unitId },\n phases: intersect(ability.phases, phase),\n });\n }\n\n // 5. Attached members — the combined unit pools every member's abilities,\n // pulled in full (not aura-filtered like step 6), regardless of which member\n // is the selected/firing unit.\n for (const memberId of input.attachedUnitIds ?? []) {\n const member = resolveUnit(memberId, factionId);\n if (!member) continue;\n for (const ability of member.abilities) {\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"attached\", unitId: memberId },\n phases: intersect(ability.phases, phase),\n });\n }\n }\n\n // 6. Supporting units — only aura-scoped abilities (otherwise the buff\n // would describe a self-target effect that doesn't reach the input unit).\n for (const supportId of input.supportingUnitIds ?? []) {\n const supporter = resolveUnit(supportId, factionId);\n if (!supporter) continue;\n for (const ability of supporter.abilities) {\n if (!phaseMatches(ability, phase)) continue;\n if (!isAuraScope(ability.raw.scope?.range)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"support\", sourceUnitId: supportId },\n phases: intersect(ability.phases, phase),\n });\n }\n }\n\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction phaseMatches(ability: AbilityView, phase: Phase): boolean {\n const phases = ability.phases;\n // An ability with no phase-mapping is permissive — surface it everywhere so\n // the SPA can decide. M2's translator already gates conditional-on-phase\n // effects internally, so this stays generous on purpose.\n if (phases.length === 0) return true;\n return phases.includes(phase);\n}\n\nfunction stratagemPhaseMatches(stratagem: Stratagem, phase: Phase): boolean {\n if (!stratagem.phases || stratagem.phases.length === 0) return false;\n return (stratagem.phases as Phase[]).includes(phase);\n}\n\nfunction intersect(phases: Phase[], phase: Phase): Phase[] {\n return phases.includes(phase) ? [phase] : phases;\n}\n\nfunction isAuraScope(range: unknown): boolean {\n if (typeof range !== \"string\") return false;\n return range.startsWith(\"aura-\") || range === \"any-on-battlefield\" || range === \"any-visible\";\n}\n\nfunction pushUnique(\n out: EligibleAbility[],\n seen: Set<string>,\n entry: EligibleAbility,\n): void {\n const key = `${entry.source.kind}::${entry.ability.id}`;\n if (seen.has(key)) return;\n seen.add(key);\n out.push(entry);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../src/abilities-resolver/resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Walks the dataset for every ability that could apply to a chosen unit in a\n * chosen phase. The SPA passes the result through to the buff layer (each\n * {@link EligibleAbility} carries `.getBuffs()`, the source is pre-tagged).\n *\n * Resolution order — stable for snapshot tests:\n *\n * 1. **army** — faction-scoped abilities whose `ability_type` is `\"faction\"`\n * and whose `faction_id` matches the input.\n * 2. **detachment** — abilities authored against the detachment\n * (`ability_type` is `\"detachment\"`, `detachment_id` matches).\n * 3. **detachment-stratagem** — stratagems on the detachment, each yielding\n * the ability referenced by `stratagem.ability_id` (if any).\n * 4. **unit** — abilities listed in `unit.ability_ids`.\n * 5. **attached** — abilities of each attached member. A leader + bodyguard\n * (and, in 11th, support) form one combined unit, so every member's\n * abilities apply to the whole unit — pulled in full (not aura-filtered),\n * whichever member is the selected/firing unit.\n * 6. **support** — abilities on supporting units whose scope range is an\n * aura (not `self` / `unit`).\n *\n * Each step phase-filters via the existing `Dataset.phasesFor` index. The\n * resolver collects abilities first, *then* filters by phase, so the SPA can\n * also ask \"what abilities are eligible across all phases?\" by passing every\n * phase (today the API requires a single phase; if the SPA wants the wide\n * view it can call the resolver once per phase).\n */\nimport type { Phase, Stratagem } from \"../generated.js\";\nimport type { Dataset } from \"../data/dataset.js\";\nimport type { AbilityView } from \"../data/entities.js\";\n\nexport type EligibleAbilitySource =\n | { kind: \"army\" }\n | { kind: \"detachment\"; detachmentId: string }\n | { kind: \"detachment-stratagem\"; stratagemId: string; cpCost: number }\n | { kind: \"unit\"; unitId: string }\n | { kind: \"attached\"; unitId: string }\n | { kind: \"support\"; sourceUnitId: string };\n\nexport type EligibilityInput = {\n unitId: string;\n /** Overrides the unit's own `faction_id` when given (for inheritance cases). */\n factionId?: string;\n detachmentId?: string;\n /**\n * Other members of the combined (\"attached\") unit — the attached leader, its\n * bodyguard, or (11th) support attachments — whichever is *not* the selected\n * `unitId`. Their abilities are pooled onto the combined unit. A list so\n * multi-member attachments need no shape change; order is preserved.\n */\n attachedUnitIds?: string[];\n /** Friendly units whose auras could apply (M2 walks only their aura-ranged abilities). */\n supportingUnitIds?: string[];\n};\n\nexport type EligibleAbility = {\n ability: AbilityView;\n source: EligibleAbilitySource;\n /** The subset of `ability.phases` that intersect the requested phase. */\n phases: Phase[];\n};\n\n/** Compute the sorted-by-source eligible-ability list for one (unit, phase). */\nexport function resolveEligibleAbilities(\n dataset: Dataset,\n input: EligibilityInput,\n phase: Phase,\n): EligibleAbility[] {\n // Resolve units within the faction when one is known. Unit ids are shared\n // across factions (a shared chassis like `chaos-land-raider` lives under\n // several Chaos factions), so a faction-blind `get` can return the wrong\n // faction's copy — and with it the wrong intrinsic abilities/keywords.\n const resolveUnit = (id: string, fid: string | undefined) =>\n (fid ? dataset.units.getInFaction(id, fid) : undefined) ?? dataset.units.get(id);\n\n const unit = resolveUnit(input.unitId, input.factionId);\n if (!unit) return [];\n const factionId = input.factionId ?? unit.raw.faction_id;\n const seen = new Set<string>();\n const out: EligibleAbility[] = [];\n\n // 1. Army — faction-scoped abilities (faction rule + any other faction-typed).\n for (const ability of dataset.abilities.byFaction(factionId)) {\n if (ability.raw.ability_type !== \"faction\") continue;\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, { ability, source: { kind: \"army\" }, phases: intersect(ability.phases, phase) });\n }\n\n // 2. Detachment abilities — abilities whose detachment_id matches.\n if (input.detachmentId) {\n for (const ability of dataset.abilities) {\n if (ability.raw.ability_type !== \"detachment\") continue;\n if (ability.raw.detachment_id !== input.detachmentId) continue;\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"detachment\", detachmentId: input.detachmentId },\n phases: intersect(ability.phases, phase),\n });\n }\n\n // 3. Detachment stratagems. A generic Codex detachment id is shared across\n // every faction's view, so prefer the copy in the resolving faction (its\n // stratagem list is identical across copies, but this keeps lookups\n // faction-correct); fall back to the first registered copy.\n const detachment =\n dataset.detachments.getInFaction(input.detachmentId, factionId) ??\n dataset.detachments.get(input.detachmentId);\n if (detachment) {\n for (const stratId of detachment.stratagem_ids ?? []) {\n const stratagem = dataset.stratagems.get(stratId);\n if (!stratagem) continue;\n if (!stratagemPhaseMatches(stratagem, phase)) continue;\n const ability =\n stratagem.ability_id !== null && stratagem.ability_id !== undefined\n ? dataset.abilities.get(stratagem.ability_id)\n : undefined;\n if (!ability) continue;\n pushUnique(out, seen, {\n ability,\n source: {\n kind: \"detachment-stratagem\",\n stratagemId: stratagem.id,\n cpCost: stratagem.cp_cost,\n },\n phases: [phase], // the stratagem's printed phase governs eligibility.\n });\n }\n }\n }\n\n // 4. Unit's own abilities.\n for (const ability of unit.abilities) {\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"unit\", unitId: input.unitId },\n phases: intersect(ability.phases, phase),\n });\n }\n\n // 5. Attached members — the combined unit pools every member's abilities,\n // pulled in full (not aura-filtered like step 6), regardless of which member\n // is the selected/firing unit.\n for (const memberId of input.attachedUnitIds ?? []) {\n const member = resolveUnit(memberId, factionId);\n if (!member) continue;\n for (const ability of member.abilities) {\n if (!phaseMatches(ability, phase)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"attached\", unitId: memberId },\n phases: intersect(ability.phases, phase),\n });\n }\n }\n\n // 6. Supporting units — only aura-scoped abilities (otherwise the buff\n // would describe a self-target effect that doesn't reach the input unit).\n for (const supportId of input.supportingUnitIds ?? []) {\n const supporter = resolveUnit(supportId, factionId);\n if (!supporter) continue;\n for (const ability of supporter.abilities) {\n if (!phaseMatches(ability, phase)) continue;\n if (!isAuraScope(ability.raw.scope?.range)) continue;\n pushUnique(out, seen, {\n ability,\n source: { kind: \"support\", sourceUnitId: supportId },\n phases: intersect(ability.phases, phase),\n });\n }\n }\n\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction phaseMatches(ability: AbilityView, phase: Phase): boolean {\n const phases = ability.phases;\n // An ability with no phase-mapping is permissive — surface it everywhere so\n // the SPA can decide. M2's translator already gates conditional-on-phase\n // effects internally, so this stays generous on purpose.\n if (phases.length === 0) return true;\n return phases.includes(phase);\n}\n\nfunction stratagemPhaseMatches(stratagem: Stratagem, phase: Phase): boolean {\n if (!stratagem.phases || stratagem.phases.length === 0) return false;\n return (stratagem.phases as Phase[]).includes(phase);\n}\n\nfunction intersect(phases: Phase[], phase: Phase): Phase[] {\n return phases.includes(phase) ? [phase] : phases;\n}\n\nfunction isAuraScope(range: unknown): boolean {\n if (typeof range !== \"string\") return false;\n return range.startsWith(\"aura-\") || range === \"any-on-battlefield\" || range === \"any-visible\";\n}\n\nfunction pushUnique(\n out: EligibleAbility[],\n seen: Set<string>,\n entry: EligibleAbility,\n): void {\n const key = `${entry.source.kind}::${entry.ability.id}`;\n if (seen.has(key)) return;\n seen.add(key);\n out.push(entry);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-coverage.d.ts","sourceRoot":"","sources":["../src/audit-coverage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audit-coverage.d.ts","sourceRoot":"","sources":["../src/audit-coverage.ts"],"names":[],"mappings":"AA6CA,6EAA6E;AAC7E,UAAU,YAAY;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,2EAA2E;IAC3E,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IACzC,6DAA6D;IAC7D,kBAAkB,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxD,uDAAuD;IACvD,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAuFD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAiBvD;AAWD,8EAA8E;AAC9E,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,YAAY,EAAE,CAAA;CAAE,EAAE,GACtD,cAAc,CA2EhB;AA2MD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,oBAAyB,GAAG,IAAI,CAsB1E"}
|
package/dist/audit-coverage.js
CHANGED
|
@@ -34,6 +34,9 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
|
34
34
|
const DATA_ROOT = resolve(__dirname, "../../data");
|
|
35
35
|
const ENRICHMENT_ROOT = resolve(DATA_ROOT, "enrichment");
|
|
36
36
|
const AUDIT_DIR = resolve(DATA_ROOT, "_audit");
|
|
37
|
+
const CONTRIBUTING_PATH = resolve(__dirname, "../../CONTRIBUTING.md");
|
|
38
|
+
const COVERAGE_START = "<!-- coverage:start -->";
|
|
39
|
+
const COVERAGE_END = "<!-- coverage:end -->";
|
|
37
40
|
const PHASES = ["command", "movement", "shooting", "charge", "fight"];
|
|
38
41
|
/**
|
|
39
42
|
* Build a permissive context for `phase` — every situational flag set so that
|
|
@@ -308,6 +311,88 @@ function markdownReport(r) {
|
|
|
308
311
|
lines.push("");
|
|
309
312
|
return lines.join("\n");
|
|
310
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* The marker-bounded coverage digest embedded in CONTRIBUTING.md: a compact
|
|
316
|
+
* per-faction *repair* table — the work still outstanding, not total coverage.
|
|
317
|
+
* Sorted by authoring need (structural stubs first) so a contributor sees at a
|
|
318
|
+
* glance which factions need the most DSL work. The full total+repair breakdown
|
|
319
|
+
* and unsupported-effect histogram stay in `data/_audit/summary.md`; the
|
|
320
|
+
* enumerated stub worklist (ability ids) is in `data/_audit/worklist.md`.
|
|
321
|
+
*/
|
|
322
|
+
function coverageDigest(r) {
|
|
323
|
+
const rows = [...r.factions].sort((a, b) => b.stubStructural - a.stubStructural ||
|
|
324
|
+
b.stub - a.stub ||
|
|
325
|
+
b.gwTextLeak - a.gwTextLeak ||
|
|
326
|
+
b.defensiveSkipped - a.defensiveSkipped ||
|
|
327
|
+
a.faction.localeCompare(b.faction));
|
|
328
|
+
const lines = [
|
|
329
|
+
COVERAGE_START,
|
|
330
|
+
"<!-- Generated by `npm run audit:coverage` — do not edit by hand. -->",
|
|
331
|
+
"",
|
|
332
|
+
"Repair worklist by faction — abilities still needing DSL work (not total coverage).",
|
|
333
|
+
"",
|
|
334
|
+
"| faction | stub\\* | notes-stub | gw-leak | def-skipped |",
|
|
335
|
+
"|---|--:|--:|--:|--:|",
|
|
336
|
+
];
|
|
337
|
+
for (const f of rows) {
|
|
338
|
+
lines.push(`| ${f.faction} | ${f.stubStructural} | ${f.stub} | ${f.gwTextLeak} | ${f.defensiveSkipped} |`);
|
|
339
|
+
}
|
|
340
|
+
const t = r.totals;
|
|
341
|
+
lines.push(`| **TOTAL** | **${t.stubStructural}** | **${t.stub}** | **${t.gwTextLeak}** | **${t.defensiveSkipped}** |`, "", "`stub*` = empty-modifier placeholder nodes (highest need first) · `notes-stub` = flagged in " +
|
|
342
|
+
"`community_notes` · `gw-leak` = suspected verbatim GW text · `def-skipped` = defensive abilities " +
|
|
343
|
+
"skipped by the buff walk. Enumerated stub ids: [`data/_audit/worklist.md`](data/_audit/worklist.md). " +
|
|
344
|
+
"Full total+repair breakdown: [`data/_audit/summary.md`](data/_audit/summary.md).", COVERAGE_END);
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The enumerated stub worklist (`data/_audit/worklist.md`): one row per
|
|
349
|
+
* structural stub, naming the ability and the gap that needs authoring. The
|
|
350
|
+
* actionable counterpart to the per-faction `stub*` counts.
|
|
351
|
+
*/
|
|
352
|
+
function worklistReport(r) {
|
|
353
|
+
const stubs = r.worklist
|
|
354
|
+
.filter((w) => w.stub)
|
|
355
|
+
.sort((a, b) => a.faction.localeCompare(b.faction) || a.ability_id.localeCompare(b.ability_id));
|
|
356
|
+
const lines = [
|
|
357
|
+
"# Ability repair worklist",
|
|
358
|
+
"",
|
|
359
|
+
"Generated by `tools/src/audit-coverage.ts` (`npm run audit:coverage`). Each row is a",
|
|
360
|
+
"structural stub — an effect carrying an empty-modifier placeholder node that still needs",
|
|
361
|
+
"DSL authored. This is the actionable counterpart to the per-faction `stub*` counts in",
|
|
362
|
+
"[`CONTRIBUTING.md`](../../CONTRIBUTING.md) and [`summary.md`](summary.md).",
|
|
363
|
+
"",
|
|
364
|
+
`Total: ${stubs.length} stub abilities.`,
|
|
365
|
+
"",
|
|
366
|
+
"| faction | ability | shape | needs |",
|
|
367
|
+
"|---|---|---|---|",
|
|
368
|
+
];
|
|
369
|
+
for (const w of stubs) {
|
|
370
|
+
lines.push(`| ${w.faction} | ${w.ability_id} | ${w.shape ?? "—"} | ${w.gap ?? "empty modifier"} |`);
|
|
371
|
+
}
|
|
372
|
+
lines.push("");
|
|
373
|
+
return lines.join("\n");
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Splice the live coverage digest into CONTRIBUTING.md between the sentinel
|
|
377
|
+
* markers, leaving the rest untouched. No-op (not an error) when the markers
|
|
378
|
+
* are absent, so the audit never depends on the doc.
|
|
379
|
+
*/
|
|
380
|
+
function injectCoverageIntoContributing(report) {
|
|
381
|
+
let md;
|
|
382
|
+
try {
|
|
383
|
+
md = readFileSync(CONTRIBUTING_PATH, "utf-8");
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const start = md.indexOf(COVERAGE_START);
|
|
389
|
+
const end = md.indexOf(COVERAGE_END);
|
|
390
|
+
if (start === -1 || end === -1 || end < start)
|
|
391
|
+
return;
|
|
392
|
+
const next = md.slice(0, start) + coverageDigest(report) + md.slice(end + COVERAGE_END.length);
|
|
393
|
+
if (next !== md)
|
|
394
|
+
writeFileSync(CONTRIBUTING_PATH, next);
|
|
395
|
+
}
|
|
311
396
|
/** Commander action: run the audit over the repo's enrichment data. */
|
|
312
397
|
export function auditCoverageCommand(opts = {}) {
|
|
313
398
|
const report = computeCoverage(loadFactions());
|
|
@@ -316,8 +401,12 @@ export function auditCoverageCommand(opts = {}) {
|
|
|
316
401
|
writeFileSync(resolve(AUDIT_DIR, "coverage.json"), JSON.stringify(report, null, 2) + "\n");
|
|
317
402
|
writeFileSync(resolve(AUDIT_DIR, "summary.md"), markdownReport(report));
|
|
318
403
|
// The named-gap worklist is the authoring artifact — split out so it's
|
|
319
|
-
// diffable on its own and consumable without the histograms.
|
|
404
|
+
// diffable on its own and consumable without the histograms. Emit both the
|
|
405
|
+
// machine-readable JSON and a human-readable stub table.
|
|
320
406
|
writeFileSync(resolve(AUDIT_DIR, "worklist.json"), JSON.stringify(report.worklist, null, 2) + "\n");
|
|
407
|
+
writeFileSync(resolve(AUDIT_DIR, "worklist.md"), worklistReport(report));
|
|
408
|
+
// Keep the CONTRIBUTING.md coverage snapshot current (markers permitting).
|
|
409
|
+
injectCoverageIntoContributing(report);
|
|
321
410
|
}
|
|
322
411
|
if (opts.reporter === "json") {
|
|
323
412
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-coverage.js","sourceRoot":"","sources":["../src/audit-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ability-coverage audit: measure how much of the community-authored ability\n * data actually translates into cruncher buffs, per faction.\n *\n * The buff layer only interprets a subset of the ability DSL (see\n * `cruncher/from-dsl.ts`). An ability that doesn't translate is inert in Salvo —\n * the projection silently falls back to raw-statline math. This tool runs the\n * *real* translator (`effectToBuffs`) over every authored ability, under both\n * the attacker and target perspectives and across every phase, and classifies\n * each entry as:\n *\n * - **offensive** — yields ≥1 attacker-side buff (auto-applied or activatable)\n * - **defensive** — yields ≥1 target-side buff\n * - **inert** — neither; the effect is `unsupported` and/or a stub\n *\n * It also tallies the verbatim-GW-text leak (`community_notes` carrying an\n * `\"Original:\"` dump) and the explicit `\"skipped for damage calc\"` defensive\n * entries, and histograms the `unsupported.reason` strings so the output is a\n * directly-actionable authoring worklist.\n *\n * Usage:\n * npx tsx tools/src/audit-coverage.ts (pretty report to stdout)\n * npx tsx tools/src/audit-coverage.ts --json (machine-readable)\n * npx tsx tools/src/audit-coverage.ts --write (also emit data/_audit/*)\n *\n * Wired as `40kdc-validate audit-coverage` and `npm run audit:coverage`.\n */\nimport { readdirSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport { effectToBuffs } from \"./cruncher/from-dsl.js\";\nimport type { BuffSource, EngineContext } from \"./cruncher/buffs.js\";\nimport type { Phase } from \"./generated.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\nconst ENRICHMENT_ROOT = resolve(DATA_ROOT, \"enrichment\");\nconst AUDIT_DIR = resolve(DATA_ROOT, \"_audit\");\n\nconst PHASES: Phase[] = [\"command\", \"movement\", \"shooting\", \"charge\", \"fight\"];\n\n/** Shape of an authored ability entry (only the fields this audit reads). */\ninterface AbilityEntry {\n ability_id: string;\n name?: string;\n ability_type?: string;\n community_notes?: string;\n effect?: unknown;\n}\n\nexport interface FactionCoverage {\n faction: string;\n total: number;\n offensive: number;\n defensive: number;\n /** Produces neither an offensive nor a defensive buff. */\n inert: number;\n /** `community_notes` flags it an auto-generated stub / partial. */\n stub: number;\n /**\n * Structurally a placeholder: the effect tree contains a modifier-bearing\n * node with an empty `modifier: {}` (the original pass's untyped stub, e.g.\n * `stat-modifier {}`). This — not `inert` — is the authoring worklist: an\n * inert-but-correctly-typed ability (movement, objective control) is *done*;\n * an empty-modifier node is a gap regardless of who consumes it.\n */\n stubStructural: number;\n /** `community_notes` carries a verbatim GW `\"Original:\"` text dump (IP leak). */\n gwTextLeak: number;\n /** Explicitly tagged `\"defensive ability (skipped for damage calc)\"`. */\n defensiveSkipped: number;\n}\n\n/**\n * One named gap. Carries the ability's identity and current shape so the gap is\n * self-describing on the consumer end (the fan-out joins the source rule; the\n * editor / downstream tools can list \"what's unauthored\" without re-deriving).\n */\nexport interface WorklistEntry {\n faction: string;\n ability_id: string;\n name: string;\n /** Top-level effect type as authored today (the \"shape\"), or `null` if absent. */\n shape: string | null;\n /** Has an empty-modifier placeholder node somewhere in its effect tree. */\n stub: boolean;\n offensive: boolean;\n defensive: boolean;\n /** Most-informative unsupported reason from the attacker walk, if any. */\n gap: string | null;\n}\n\nexport interface CoverageReport {\n factions: FactionCoverage[];\n totals: Omit<FactionCoverage, \"faction\">;\n /** `unsupported.reason` (normalized) → count, descending. */\n unsupportedReasons: { reason: string; count: number }[];\n /** Per-ability named gaps — the authoring worklist. */\n worklist: WorklistEntry[];\n}\n\n/**\n * Build a permissive context for `phase` — every situational flag set so that\n * conditionals gated on stationary/charged/half-range/attachment can fire. We\n * want \"could this ability *ever* produce a buff\", not \"does it fire right now\".\n * Keyword-specific conditions (`target-has-keyword`) still evaluate `\"unknown\"`\n * since no concrete target exists; those surface in the reason histogram.\n */\nfunction permissiveContext(phase: Phase): EngineContext {\n return {\n phase,\n attackerStationary: true,\n attackerCharged: true,\n withinHalfRange: true,\n attackerInCover: true,\n targetInCover: true,\n attackerAttached: true,\n attackerKeywords: [],\n targetKeywords: [],\n };\n}\n\n/** Map an `ability_type` to the buff-source kind (only labels; not load-bearing here). */\nfunction sourceKind(abilityType: string | undefined): BuffSource {\n const kind =\n abilityType === \"faction\"\n ? \"army\"\n : abilityType === \"detachment\"\n ? \"detachment\"\n : abilityType === \"stratagem\"\n ? \"detachment-stratagem\"\n : \"unit\";\n return { kind: \"ability\", abilityId: \"audit\", abilityKind: kind };\n}\n\n/**\n * Keep reasons verbatim — the quoted specifics (which effect type, which\n * operation) ARE the worklist signal. We only collapse the `subset \"…\"` half of\n * the re-roll reason, which is high-cardinality noise, while keeping the roll.\n */\nfunction normalizeReason(reason: string): string {\n return reason.replace(/\\(subset \"[^\"]*\"\\)/g, \"(subset …)\");\n}\n\n/**\n * Translate one ability under one perspective across all phases; return whether\n * it produced any buff, accumulating unsupported reasons into `reasonCounts`.\n */\nfunction producesBuff(\n effect: unknown,\n source: BuffSource,\n perspective: \"attacker\" | \"target\",\n reasonCounts: Map<string, number>,\n): boolean {\n let produced = false;\n for (const phase of PHASES) {\n const t = effectToBuffs(effect, source, permissiveContext(phase), perspective);\n if (t.applied.length > 0 || t.activatable.length > 0) produced = true;\n // Only attacker-perspective reasons feed the histogram — the target pass\n // re-walks the same tree and would double-count every offensive-only branch\n // as \"unsupported on defense\", which is noise for the worklist.\n if (perspective === \"attacker\") {\n for (const u of t.unsupported) {\n const key = normalizeReason(u.reason);\n reasonCounts.set(key, (reasonCounts.get(key) ?? 0) + 1);\n }\n }\n }\n return produced;\n}\n\n/**\n * Effect types that are pure flags — they carry no parameters, so an empty\n * `modifier: {}` is *correct*, not a stub (per `translate.ts`, these read no\n * modifier fields). Excluding them keeps the worklist from crying wolf on\n * legitimately-authored abilities like Deep Strike.\n */\nconst PARAMETERLESS_EFFECTS = new Set([\n \"deep-strike\",\n \"fallback-and-act\",\n \"fight-first\",\n \"fight-last\",\n \"fight-on-death\",\n \"shoot-on-death\",\n]);\n\n/**\n * True if any node in the effect tree is a *parameter-requiring* leaf left with\n * an empty `modifier: {}` — the original pass's untyped placeholder. Pure-flag\n * effects (see {@link PARAMETERLESS_EFFECTS}) are exempt.\n */\nexport function hasEmptyModifier(node: unknown): boolean {\n if (Array.isArray(node)) return node.some(hasEmptyModifier);\n if (typeof node !== \"object\" || node === null) return false;\n const rec = node as Record<string, unknown>;\n const mod = rec.modifier;\n if (\n typeof rec.type === \"string\" &&\n !PARAMETERLESS_EFFECTS.has(rec.type) &&\n mod !== undefined &&\n typeof mod === \"object\" &&\n mod !== null &&\n !Array.isArray(mod) &&\n Object.keys(mod).length === 0\n ) {\n return true;\n }\n return Object.values(rec).some(hasEmptyModifier);\n}\n\n/** Top-level effect type (the \"shape\") as authored today. */\nfunction shapeOf(effect: unknown): string | null {\n if (typeof effect === \"object\" && effect !== null && !Array.isArray(effect)) {\n const t = (effect as Record<string, unknown>).type;\n if (typeof t === \"string\") return t;\n }\n return null;\n}\n\n/** Compute coverage for a set of factions. Pure — IO lives in the command. */\nexport function computeCoverage(\n input: { faction: string; abilities: AbilityEntry[] }[],\n): CoverageReport {\n const reasonCounts = new Map<string, number>();\n const factions: FactionCoverage[] = [];\n const worklist: WorklistEntry[] = [];\n\n for (const { faction, abilities } of input) {\n const fc: FactionCoverage = {\n faction,\n total: abilities.length,\n offensive: 0,\n defensive: 0,\n inert: 0,\n stub: 0,\n stubStructural: 0,\n gwTextLeak: 0,\n defensiveSkipped: 0,\n };\n for (const a of abilities) {\n const notes = a.community_notes ?? \"\";\n if (/Original:/.test(notes)) fc.gwTextLeak++;\n if (/stub|partial/i.test(notes)) fc.stub++;\n if (/skipped for damage calc/i.test(notes)) fc.defensiveSkipped++;\n\n const source = sourceKind(a.ability_type);\n // De-dupe reasons within a single ability so a 5-phase walk doesn't count\n // the same unsupported branch five times.\n const perAbility = new Map<string, number>();\n const off = producesBuff(a.effect, source, \"attacker\", perAbility);\n const def = producesBuff(a.effect, source, \"target\", new Map());\n for (const [reason] of perAbility) {\n reasonCounts.set(reason, (reasonCounts.get(reason) ?? 0) + 1);\n }\n if (off) fc.offensive++;\n if (def) fc.defensive++;\n if (!off && !def) fc.inert++;\n\n const isStub = hasEmptyModifier(a.effect);\n if (isStub) fc.stubStructural++;\n\n worklist.push({\n faction,\n ability_id: a.ability_id,\n name: a.name ?? a.ability_id,\n shape: shapeOf(a.effect),\n stub: isStub,\n offensive: off,\n defensive: def,\n gap: perAbility.size > 0 ? [...perAbility.keys()][0] : null,\n });\n }\n factions.push(fc);\n }\n\n factions.sort((a, b) => a.faction.localeCompare(b.faction));\n\n const totals = factions.reduce(\n (acc, f) => {\n acc.total += f.total;\n acc.offensive += f.offensive;\n acc.defensive += f.defensive;\n acc.inert += f.inert;\n acc.stub += f.stub;\n acc.stubStructural += f.stubStructural;\n acc.gwTextLeak += f.gwTextLeak;\n acc.defensiveSkipped += f.defensiveSkipped;\n return acc;\n },\n { total: 0, offensive: 0, defensive: 0, inert: 0, stub: 0, stubStructural: 0, gwTextLeak: 0, defensiveSkipped: 0 },\n );\n\n const unsupportedReasons = [...reasonCounts.entries()]\n .map(([reason, count]) => ({ reason, count }))\n .sort((a, b) => b.count - a.count);\n\n return { factions, totals, unsupportedReasons, worklist };\n}\n\n/** Read every `data/enrichment/<faction>/abilities.json` (skips `_example`). */\nfunction loadFactions(): { faction: string; abilities: AbilityEntry[] }[] {\n const out: { faction: string; abilities: AbilityEntry[] }[] = [];\n for (const entry of readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })) {\n if (!entry.isDirectory() || entry.name === \"_example\") continue;\n const file = resolve(ENRICHMENT_ROOT, entry.name, \"abilities.json\");\n let abilities: AbilityEntry[];\n try {\n abilities = JSON.parse(readFileSync(file, \"utf-8\"));\n } catch {\n continue; // no abilities.json in this folder\n }\n if (!Array.isArray(abilities)) continue;\n out.push({ faction: entry.name, abilities });\n }\n return out;\n}\n\nconst pct = (n: number, d: number): string => (d === 0 ? \"—\" : `${Math.round((100 * n) / d)}%`);\n\nfunction prettyReport(r: CoverageReport): string {\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(\"40kdc Ability Coverage Audit\"));\n lines.push(chalk.gray(\"─\".repeat(78)));\n lines.push(\n chalk.gray(\n \"faction\".padEnd(24) +\n \"total\".padStart(7) +\n \"off\".padStart(7) +\n \"def\".padStart(7) +\n \"inert\".padStart(7) +\n \"stub\".padStart(7) +\n \"leak\".padStart(7),\n ),\n );\n for (const f of r.factions) {\n const stubWarn = f.stubStructural > 0;\n const row =\n f.faction.padEnd(24) +\n String(f.total).padStart(7) +\n `${f.offensive}`.padStart(7) +\n `${f.defensive}`.padStart(7) +\n `${f.inert}`.padStart(7) +\n `${f.stubStructural}`.padStart(7) +\n `${f.gwTextLeak}`.padStart(7);\n lines.push(stubWarn ? chalk.yellow(row) : row);\n }\n lines.push(chalk.gray(\"─\".repeat(78)));\n const t = r.totals;\n lines.push(\n chalk.bold(\n \"TOTAL\".padEnd(24) +\n String(t.total).padStart(7) +\n `${t.offensive}`.padStart(7) +\n `${t.defensive}`.padStart(7) +\n `${t.inert}`.padStart(7) +\n `${t.stubStructural}`.padStart(7) +\n `${t.gwTextLeak}`.padStart(7),\n ),\n );\n lines.push(\"\");\n lines.push(\n `Offensive coverage: ${chalk.cyan(pct(t.offensive, t.total))} ` +\n `Defensive coverage: ${chalk.cyan(pct(t.defensive, t.total))} ` +\n `Inert: ${chalk.yellow(pct(t.inert, t.total))}`,\n );\n if (t.gwTextLeak > 0) {\n lines.push(chalk.red(`⚠ ${t.gwTextLeak} entries leak verbatim GW text (community_notes \"Original:\") — IP scrub needed (#20).`));\n }\n lines.push(chalk.gray(`${t.defensiveSkipped} entries tagged \"skipped for damage calc\" (defensive worklist, #23).`));\n lines.push(\"\");\n lines.push(chalk.bold(\"Top unsupported-effect reasons (offensive walk):\"));\n for (const { reason, count } of r.unsupportedReasons.slice(0, 15)) {\n lines.push(` ${String(count).padStart(5)} ${chalk.gray(reason)}`);\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nfunction markdownReport(r: CoverageReport): string {\n const lines: string[] = [\n \"# Ability coverage audit\",\n \"\",\n \"Generated by `tools/src/audit-coverage.ts` (`npm run audit:coverage`). Counts\",\n \"abilities that translate into cruncher buffs via the real `effectToBuffs`\",\n \"(attacker + target perspective, all phases). `inert` = produces neither.\",\n \"\",\n \"| faction | total | offensive | defensive | inert | stub* | notes-stub | gw-leak | def-skipped |\",\n \"|---|--:|--:|--:|--:|--:|--:|--:|--:|\",\n ];\n for (const f of r.factions) {\n lines.push(\n `| ${f.faction} | ${f.total} | ${f.offensive} | ${f.defensive} | ${f.inert} | ${f.stubStructural} | ${f.stub} | ${f.gwTextLeak} | ${f.defensiveSkipped} |`,\n );\n }\n const t = r.totals;\n lines.push(\n `| **TOTAL** | **${t.total}** | **${t.offensive}** | **${t.defensive}** | **${t.inert}** | **${t.stubStructural}** | **${t.stub}** | **${t.gwTextLeak}** | **${t.defensiveSkipped}** |`,\n );\n lines.push(\"\", \"`stub*` = structural (empty-modifier placeholder node) — the authoring worklist. `notes-stub` = flagged in community_notes.\");\n lines.push(\"\", \"## Unsupported-effect reasons (offensive walk)\", \"\");\n for (const { reason, count } of r.unsupportedReasons) {\n lines.push(`- \\`${count}\\` — ${reason}`);\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nexport interface AuditCoverageOptions {\n reporter?: \"pretty\" | \"json\";\n write?: boolean;\n}\n\n/** Commander action: run the audit over the repo's enrichment data. */\nexport function auditCoverageCommand(opts: AuditCoverageOptions = {}): void {\n const report = computeCoverage(loadFactions());\n\n if (opts.write) {\n mkdirSync(AUDIT_DIR, { recursive: true });\n writeFileSync(resolve(AUDIT_DIR, \"coverage.json\"), JSON.stringify(report, null, 2) + \"\\n\");\n writeFileSync(resolve(AUDIT_DIR, \"summary.md\"), markdownReport(report));\n // The named-gap worklist is the authoring artifact — split out so it's\n // diffable on its own and consumable without the histograms.\n writeFileSync(resolve(AUDIT_DIR, \"worklist.json\"), JSON.stringify(report.worklist, null, 2) + \"\\n\");\n }\n\n if (opts.reporter === \"json\") {\n console.log(JSON.stringify(report, null, 2));\n } else {\n console.log(prettyReport(report));\n if (opts.write) console.log(chalk.gray(`Wrote ${basename(AUDIT_DIR)}/coverage.json + summary.md`));\n }\n}\n\n// Direct-invocation entry point (`npx tsx tools/src/audit-coverage.ts [--json] [--write]`).\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") === fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\nif (isMain) {\n const argv = process.argv.slice(2);\n auditCoverageCommand({\n reporter: argv.includes(\"--json\") ? \"json\" : \"pretty\",\n write: argv.includes(\"--write\"),\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audit-coverage.js","sourceRoot":"","sources":["../src/audit-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ability-coverage audit: measure how much of the community-authored ability\n * data actually translates into cruncher buffs, per faction.\n *\n * The buff layer only interprets a subset of the ability DSL (see\n * `cruncher/from-dsl.ts`). An ability that doesn't translate is inert in Salvo —\n * the projection silently falls back to raw-statline math. This tool runs the\n * *real* translator (`effectToBuffs`) over every authored ability, under both\n * the attacker and target perspectives and across every phase, and classifies\n * each entry as:\n *\n * - **offensive** — yields ≥1 attacker-side buff (auto-applied or activatable)\n * - **defensive** — yields ≥1 target-side buff\n * - **inert** — neither; the effect is `unsupported` and/or a stub\n *\n * It also tallies the verbatim-GW-text leak (`community_notes` carrying an\n * `\"Original:\"` dump) and the explicit `\"skipped for damage calc\"` defensive\n * entries, and histograms the `unsupported.reason` strings so the output is a\n * directly-actionable authoring worklist.\n *\n * Usage:\n * npx tsx tools/src/audit-coverage.ts (pretty report to stdout)\n * npx tsx tools/src/audit-coverage.ts --json (machine-readable)\n * npx tsx tools/src/audit-coverage.ts --write (also emit data/_audit/*)\n *\n * Wired as `40kdc-validate audit-coverage` and `npm run audit:coverage`.\n */\nimport { readdirSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport { effectToBuffs } from \"./cruncher/from-dsl.js\";\nimport type { BuffSource, EngineContext } from \"./cruncher/buffs.js\";\nimport type { Phase } from \"./generated.js\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\nconst ENRICHMENT_ROOT = resolve(DATA_ROOT, \"enrichment\");\nconst AUDIT_DIR = resolve(DATA_ROOT, \"_audit\");\nconst CONTRIBUTING_PATH = resolve(__dirname, \"../../CONTRIBUTING.md\");\nconst COVERAGE_START = \"<!-- coverage:start -->\";\nconst COVERAGE_END = \"<!-- coverage:end -->\";\n\nconst PHASES: Phase[] = [\"command\", \"movement\", \"shooting\", \"charge\", \"fight\"];\n\n/** Shape of an authored ability entry (only the fields this audit reads). */\ninterface AbilityEntry {\n ability_id: string;\n name?: string;\n ability_type?: string;\n community_notes?: string;\n effect?: unknown;\n}\n\nexport interface FactionCoverage {\n faction: string;\n total: number;\n offensive: number;\n defensive: number;\n /** Produces neither an offensive nor a defensive buff. */\n inert: number;\n /** `community_notes` flags it an auto-generated stub / partial. */\n stub: number;\n /**\n * Structurally a placeholder: the effect tree contains a modifier-bearing\n * node with an empty `modifier: {}` (the original pass's untyped stub, e.g.\n * `stat-modifier {}`). This — not `inert` — is the authoring worklist: an\n * inert-but-correctly-typed ability (movement, objective control) is *done*;\n * an empty-modifier node is a gap regardless of who consumes it.\n */\n stubStructural: number;\n /** `community_notes` carries a verbatim GW `\"Original:\"` text dump (IP leak). */\n gwTextLeak: number;\n /** Explicitly tagged `\"defensive ability (skipped for damage calc)\"`. */\n defensiveSkipped: number;\n}\n\n/**\n * One named gap. Carries the ability's identity and current shape so the gap is\n * self-describing on the consumer end (the fan-out joins the source rule; the\n * editor / downstream tools can list \"what's unauthored\" without re-deriving).\n */\nexport interface WorklistEntry {\n faction: string;\n ability_id: string;\n name: string;\n /** Top-level effect type as authored today (the \"shape\"), or `null` if absent. */\n shape: string | null;\n /** Has an empty-modifier placeholder node somewhere in its effect tree. */\n stub: boolean;\n offensive: boolean;\n defensive: boolean;\n /** Most-informative unsupported reason from the attacker walk, if any. */\n gap: string | null;\n}\n\nexport interface CoverageReport {\n factions: FactionCoverage[];\n totals: Omit<FactionCoverage, \"faction\">;\n /** `unsupported.reason` (normalized) → count, descending. */\n unsupportedReasons: { reason: string; count: number }[];\n /** Per-ability named gaps — the authoring worklist. */\n worklist: WorklistEntry[];\n}\n\n/**\n * Build a permissive context for `phase` — every situational flag set so that\n * conditionals gated on stationary/charged/half-range/attachment can fire. We\n * want \"could this ability *ever* produce a buff\", not \"does it fire right now\".\n * Keyword-specific conditions (`target-has-keyword`) still evaluate `\"unknown\"`\n * since no concrete target exists; those surface in the reason histogram.\n */\nfunction permissiveContext(phase: Phase): EngineContext {\n return {\n phase,\n attackerStationary: true,\n attackerCharged: true,\n withinHalfRange: true,\n attackerInCover: true,\n targetInCover: true,\n attackerAttached: true,\n attackerKeywords: [],\n targetKeywords: [],\n };\n}\n\n/** Map an `ability_type` to the buff-source kind (only labels; not load-bearing here). */\nfunction sourceKind(abilityType: string | undefined): BuffSource {\n const kind =\n abilityType === \"faction\"\n ? \"army\"\n : abilityType === \"detachment\"\n ? \"detachment\"\n : abilityType === \"stratagem\"\n ? \"detachment-stratagem\"\n : \"unit\";\n return { kind: \"ability\", abilityId: \"audit\", abilityKind: kind };\n}\n\n/**\n * Keep reasons verbatim — the quoted specifics (which effect type, which\n * operation) ARE the worklist signal. We only collapse the `subset \"…\"` half of\n * the re-roll reason, which is high-cardinality noise, while keeping the roll.\n */\nfunction normalizeReason(reason: string): string {\n return reason.replace(/\\(subset \"[^\"]*\"\\)/g, \"(subset …)\");\n}\n\n/**\n * Translate one ability under one perspective across all phases; return whether\n * it produced any buff, accumulating unsupported reasons into `reasonCounts`.\n */\nfunction producesBuff(\n effect: unknown,\n source: BuffSource,\n perspective: \"attacker\" | \"target\",\n reasonCounts: Map<string, number>,\n): boolean {\n let produced = false;\n for (const phase of PHASES) {\n const t = effectToBuffs(effect, source, permissiveContext(phase), perspective);\n if (t.applied.length > 0 || t.activatable.length > 0) produced = true;\n // Only attacker-perspective reasons feed the histogram — the target pass\n // re-walks the same tree and would double-count every offensive-only branch\n // as \"unsupported on defense\", which is noise for the worklist.\n if (perspective === \"attacker\") {\n for (const u of t.unsupported) {\n const key = normalizeReason(u.reason);\n reasonCounts.set(key, (reasonCounts.get(key) ?? 0) + 1);\n }\n }\n }\n return produced;\n}\n\n/**\n * Effect types that are pure flags — they carry no parameters, so an empty\n * `modifier: {}` is *correct*, not a stub (per `translate.ts`, these read no\n * modifier fields). Excluding them keeps the worklist from crying wolf on\n * legitimately-authored abilities like Deep Strike.\n */\nconst PARAMETERLESS_EFFECTS = new Set([\n \"deep-strike\",\n \"fallback-and-act\",\n \"fight-first\",\n \"fight-last\",\n \"fight-on-death\",\n \"shoot-on-death\",\n]);\n\n/**\n * True if any node in the effect tree is a *parameter-requiring* leaf left with\n * an empty `modifier: {}` — the original pass's untyped placeholder. Pure-flag\n * effects (see {@link PARAMETERLESS_EFFECTS}) are exempt.\n */\nexport function hasEmptyModifier(node: unknown): boolean {\n if (Array.isArray(node)) return node.some(hasEmptyModifier);\n if (typeof node !== \"object\" || node === null) return false;\n const rec = node as Record<string, unknown>;\n const mod = rec.modifier;\n if (\n typeof rec.type === \"string\" &&\n !PARAMETERLESS_EFFECTS.has(rec.type) &&\n mod !== undefined &&\n typeof mod === \"object\" &&\n mod !== null &&\n !Array.isArray(mod) &&\n Object.keys(mod).length === 0\n ) {\n return true;\n }\n return Object.values(rec).some(hasEmptyModifier);\n}\n\n/** Top-level effect type (the \"shape\") as authored today. */\nfunction shapeOf(effect: unknown): string | null {\n if (typeof effect === \"object\" && effect !== null && !Array.isArray(effect)) {\n const t = (effect as Record<string, unknown>).type;\n if (typeof t === \"string\") return t;\n }\n return null;\n}\n\n/** Compute coverage for a set of factions. Pure — IO lives in the command. */\nexport function computeCoverage(\n input: { faction: string; abilities: AbilityEntry[] }[],\n): CoverageReport {\n const reasonCounts = new Map<string, number>();\n const factions: FactionCoverage[] = [];\n const worklist: WorklistEntry[] = [];\n\n for (const { faction, abilities } of input) {\n const fc: FactionCoverage = {\n faction,\n total: abilities.length,\n offensive: 0,\n defensive: 0,\n inert: 0,\n stub: 0,\n stubStructural: 0,\n gwTextLeak: 0,\n defensiveSkipped: 0,\n };\n for (const a of abilities) {\n const notes = a.community_notes ?? \"\";\n if (/Original:/.test(notes)) fc.gwTextLeak++;\n if (/stub|partial/i.test(notes)) fc.stub++;\n if (/skipped for damage calc/i.test(notes)) fc.defensiveSkipped++;\n\n const source = sourceKind(a.ability_type);\n // De-dupe reasons within a single ability so a 5-phase walk doesn't count\n // the same unsupported branch five times.\n const perAbility = new Map<string, number>();\n const off = producesBuff(a.effect, source, \"attacker\", perAbility);\n const def = producesBuff(a.effect, source, \"target\", new Map());\n for (const [reason] of perAbility) {\n reasonCounts.set(reason, (reasonCounts.get(reason) ?? 0) + 1);\n }\n if (off) fc.offensive++;\n if (def) fc.defensive++;\n if (!off && !def) fc.inert++;\n\n const isStub = hasEmptyModifier(a.effect);\n if (isStub) fc.stubStructural++;\n\n worklist.push({\n faction,\n ability_id: a.ability_id,\n name: a.name ?? a.ability_id,\n shape: shapeOf(a.effect),\n stub: isStub,\n offensive: off,\n defensive: def,\n gap: perAbility.size > 0 ? [...perAbility.keys()][0] : null,\n });\n }\n factions.push(fc);\n }\n\n factions.sort((a, b) => a.faction.localeCompare(b.faction));\n\n const totals = factions.reduce(\n (acc, f) => {\n acc.total += f.total;\n acc.offensive += f.offensive;\n acc.defensive += f.defensive;\n acc.inert += f.inert;\n acc.stub += f.stub;\n acc.stubStructural += f.stubStructural;\n acc.gwTextLeak += f.gwTextLeak;\n acc.defensiveSkipped += f.defensiveSkipped;\n return acc;\n },\n { total: 0, offensive: 0, defensive: 0, inert: 0, stub: 0, stubStructural: 0, gwTextLeak: 0, defensiveSkipped: 0 },\n );\n\n const unsupportedReasons = [...reasonCounts.entries()]\n .map(([reason, count]) => ({ reason, count }))\n .sort((a, b) => b.count - a.count);\n\n return { factions, totals, unsupportedReasons, worklist };\n}\n\n/** Read every `data/enrichment/<faction>/abilities.json` (skips `_example`). */\nfunction loadFactions(): { faction: string; abilities: AbilityEntry[] }[] {\n const out: { faction: string; abilities: AbilityEntry[] }[] = [];\n for (const entry of readdirSync(ENRICHMENT_ROOT, { withFileTypes: true })) {\n if (!entry.isDirectory() || entry.name === \"_example\") continue;\n const file = resolve(ENRICHMENT_ROOT, entry.name, \"abilities.json\");\n let abilities: AbilityEntry[];\n try {\n abilities = JSON.parse(readFileSync(file, \"utf-8\"));\n } catch {\n continue; // no abilities.json in this folder\n }\n if (!Array.isArray(abilities)) continue;\n out.push({ faction: entry.name, abilities });\n }\n return out;\n}\n\nconst pct = (n: number, d: number): string => (d === 0 ? \"—\" : `${Math.round((100 * n) / d)}%`);\n\nfunction prettyReport(r: CoverageReport): string {\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(\"40kdc Ability Coverage Audit\"));\n lines.push(chalk.gray(\"─\".repeat(78)));\n lines.push(\n chalk.gray(\n \"faction\".padEnd(24) +\n \"total\".padStart(7) +\n \"off\".padStart(7) +\n \"def\".padStart(7) +\n \"inert\".padStart(7) +\n \"stub\".padStart(7) +\n \"leak\".padStart(7),\n ),\n );\n for (const f of r.factions) {\n const stubWarn = f.stubStructural > 0;\n const row =\n f.faction.padEnd(24) +\n String(f.total).padStart(7) +\n `${f.offensive}`.padStart(7) +\n `${f.defensive}`.padStart(7) +\n `${f.inert}`.padStart(7) +\n `${f.stubStructural}`.padStart(7) +\n `${f.gwTextLeak}`.padStart(7);\n lines.push(stubWarn ? chalk.yellow(row) : row);\n }\n lines.push(chalk.gray(\"─\".repeat(78)));\n const t = r.totals;\n lines.push(\n chalk.bold(\n \"TOTAL\".padEnd(24) +\n String(t.total).padStart(7) +\n `${t.offensive}`.padStart(7) +\n `${t.defensive}`.padStart(7) +\n `${t.inert}`.padStart(7) +\n `${t.stubStructural}`.padStart(7) +\n `${t.gwTextLeak}`.padStart(7),\n ),\n );\n lines.push(\"\");\n lines.push(\n `Offensive coverage: ${chalk.cyan(pct(t.offensive, t.total))} ` +\n `Defensive coverage: ${chalk.cyan(pct(t.defensive, t.total))} ` +\n `Inert: ${chalk.yellow(pct(t.inert, t.total))}`,\n );\n if (t.gwTextLeak > 0) {\n lines.push(chalk.red(`⚠ ${t.gwTextLeak} entries leak verbatim GW text (community_notes \"Original:\") — IP scrub needed (#20).`));\n }\n lines.push(chalk.gray(`${t.defensiveSkipped} entries tagged \"skipped for damage calc\" (defensive worklist, #23).`));\n lines.push(\"\");\n lines.push(chalk.bold(\"Top unsupported-effect reasons (offensive walk):\"));\n for (const { reason, count } of r.unsupportedReasons.slice(0, 15)) {\n lines.push(` ${String(count).padStart(5)} ${chalk.gray(reason)}`);\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nfunction markdownReport(r: CoverageReport): string {\n const lines: string[] = [\n \"# Ability coverage audit\",\n \"\",\n \"Generated by `tools/src/audit-coverage.ts` (`npm run audit:coverage`). Counts\",\n \"abilities that translate into cruncher buffs via the real `effectToBuffs`\",\n \"(attacker + target perspective, all phases). `inert` = produces neither.\",\n \"\",\n \"| faction | total | offensive | defensive | inert | stub* | notes-stub | gw-leak | def-skipped |\",\n \"|---|--:|--:|--:|--:|--:|--:|--:|--:|\",\n ];\n for (const f of r.factions) {\n lines.push(\n `| ${f.faction} | ${f.total} | ${f.offensive} | ${f.defensive} | ${f.inert} | ${f.stubStructural} | ${f.stub} | ${f.gwTextLeak} | ${f.defensiveSkipped} |`,\n );\n }\n const t = r.totals;\n lines.push(\n `| **TOTAL** | **${t.total}** | **${t.offensive}** | **${t.defensive}** | **${t.inert}** | **${t.stubStructural}** | **${t.stub}** | **${t.gwTextLeak}** | **${t.defensiveSkipped}** |`,\n );\n lines.push(\"\", \"`stub*` = structural (empty-modifier placeholder node) — the authoring worklist. `notes-stub` = flagged in community_notes.\");\n lines.push(\"\", \"## Unsupported-effect reasons (offensive walk)\", \"\");\n for (const { reason, count } of r.unsupportedReasons) {\n lines.push(`- \\`${count}\\` — ${reason}`);\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/**\n * The marker-bounded coverage digest embedded in CONTRIBUTING.md: a compact\n * per-faction *repair* table — the work still outstanding, not total coverage.\n * Sorted by authoring need (structural stubs first) so a contributor sees at a\n * glance which factions need the most DSL work. The full total+repair breakdown\n * and unsupported-effect histogram stay in `data/_audit/summary.md`; the\n * enumerated stub worklist (ability ids) is in `data/_audit/worklist.md`.\n */\nfunction coverageDigest(r: CoverageReport): string {\n const rows = [...r.factions].sort(\n (a, b) =>\n b.stubStructural - a.stubStructural ||\n b.stub - a.stub ||\n b.gwTextLeak - a.gwTextLeak ||\n b.defensiveSkipped - a.defensiveSkipped ||\n a.faction.localeCompare(b.faction),\n );\n const lines: string[] = [\n COVERAGE_START,\n \"<!-- Generated by `npm run audit:coverage` — do not edit by hand. -->\",\n \"\",\n \"Repair worklist by faction — abilities still needing DSL work (not total coverage).\",\n \"\",\n \"| faction | stub\\\\* | notes-stub | gw-leak | def-skipped |\",\n \"|---|--:|--:|--:|--:|\",\n ];\n for (const f of rows) {\n lines.push(\n `| ${f.faction} | ${f.stubStructural} | ${f.stub} | ${f.gwTextLeak} | ${f.defensiveSkipped} |`,\n );\n }\n const t = r.totals;\n lines.push(\n `| **TOTAL** | **${t.stubStructural}** | **${t.stub}** | **${t.gwTextLeak}** | **${t.defensiveSkipped}** |`,\n \"\",\n \"`stub*` = empty-modifier placeholder nodes (highest need first) · `notes-stub` = flagged in \" +\n \"`community_notes` · `gw-leak` = suspected verbatim GW text · `def-skipped` = defensive abilities \" +\n \"skipped by the buff walk. Enumerated stub ids: [`data/_audit/worklist.md`](data/_audit/worklist.md). \" +\n \"Full total+repair breakdown: [`data/_audit/summary.md`](data/_audit/summary.md).\",\n COVERAGE_END,\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * The enumerated stub worklist (`data/_audit/worklist.md`): one row per\n * structural stub, naming the ability and the gap that needs authoring. The\n * actionable counterpart to the per-faction `stub*` counts.\n */\nfunction worklistReport(r: CoverageReport): string {\n const stubs = r.worklist\n .filter((w) => w.stub)\n .sort((a, b) => a.faction.localeCompare(b.faction) || a.ability_id.localeCompare(b.ability_id));\n const lines: string[] = [\n \"# Ability repair worklist\",\n \"\",\n \"Generated by `tools/src/audit-coverage.ts` (`npm run audit:coverage`). Each row is a\",\n \"structural stub — an effect carrying an empty-modifier placeholder node that still needs\",\n \"DSL authored. This is the actionable counterpart to the per-faction `stub*` counts in\",\n \"[`CONTRIBUTING.md`](../../CONTRIBUTING.md) and [`summary.md`](summary.md).\",\n \"\",\n `Total: ${stubs.length} stub abilities.`,\n \"\",\n \"| faction | ability | shape | needs |\",\n \"|---|---|---|---|\",\n ];\n for (const w of stubs) {\n lines.push(`| ${w.faction} | ${w.ability_id} | ${w.shape ?? \"—\"} | ${w.gap ?? \"empty modifier\"} |`);\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/**\n * Splice the live coverage digest into CONTRIBUTING.md between the sentinel\n * markers, leaving the rest untouched. No-op (not an error) when the markers\n * are absent, so the audit never depends on the doc.\n */\nfunction injectCoverageIntoContributing(report: CoverageReport): void {\n let md: string;\n try {\n md = readFileSync(CONTRIBUTING_PATH, \"utf-8\");\n } catch {\n return;\n }\n const start = md.indexOf(COVERAGE_START);\n const end = md.indexOf(COVERAGE_END);\n if (start === -1 || end === -1 || end < start) return;\n const next = md.slice(0, start) + coverageDigest(report) + md.slice(end + COVERAGE_END.length);\n if (next !== md) writeFileSync(CONTRIBUTING_PATH, next);\n}\n\nexport interface AuditCoverageOptions {\n reporter?: \"pretty\" | \"json\";\n write?: boolean;\n}\n\n/** Commander action: run the audit over the repo's enrichment data. */\nexport function auditCoverageCommand(opts: AuditCoverageOptions = {}): void {\n const report = computeCoverage(loadFactions());\n\n if (opts.write) {\n mkdirSync(AUDIT_DIR, { recursive: true });\n writeFileSync(resolve(AUDIT_DIR, \"coverage.json\"), JSON.stringify(report, null, 2) + \"\\n\");\n writeFileSync(resolve(AUDIT_DIR, \"summary.md\"), markdownReport(report));\n // The named-gap worklist is the authoring artifact — split out so it's\n // diffable on its own and consumable without the histograms. Emit both the\n // machine-readable JSON and a human-readable stub table.\n writeFileSync(resolve(AUDIT_DIR, \"worklist.json\"), JSON.stringify(report.worklist, null, 2) + \"\\n\");\n writeFileSync(resolve(AUDIT_DIR, \"worklist.md\"), worklistReport(report));\n // Keep the CONTRIBUTING.md coverage snapshot current (markers permitting).\n injectCoverageIntoContributing(report);\n }\n\n if (opts.reporter === \"json\") {\n console.log(JSON.stringify(report, null, 2));\n } else {\n console.log(prettyReport(report));\n if (opts.write) console.log(chalk.gray(`Wrote ${basename(AUDIT_DIR)}/coverage.json + summary.md`));\n }\n}\n\n// Direct-invocation entry point (`npx tsx tools/src/audit-coverage.ts [--json] [--write]`).\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") === fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\nif (isMain) {\n const argv = process.argv.slice(2);\n auditCoverageCommand({\n reporter: argv.includes(\"--json\") ? \"json\" : \"pretty\",\n write: argv.includes(\"--write\"),\n });\n}\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Effect, type AbilityScope, type AbilityAppliesTo } from "./translate/index.js";
|
|
2
|
+
/** Shape of an authored ability entry (only the fields the describer reads). */
|
|
3
|
+
interface AbilityEntry {
|
|
4
|
+
ability_id: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
effect?: Effect;
|
|
7
|
+
scope?: AbilityScope;
|
|
8
|
+
applies_to?: AbilityAppliesTo | null;
|
|
9
|
+
/** Units that carry this ability — surfaced so a worklist row points at the datasheet. */
|
|
10
|
+
unit_ids?: string[];
|
|
11
|
+
}
|
|
12
|
+
/** One catalogued ability: its identity, the generated text, and any defect flags. */
|
|
13
|
+
export interface PhrasingRow {
|
|
14
|
+
faction: string;
|
|
15
|
+
ability_id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
/** Slugs of the units carrying this ability (for datasheet/Wahapedia lookup). */
|
|
18
|
+
unit_ids: string[];
|
|
19
|
+
/** Full multi-line `describeAbility` output (effect + scope + applies). */
|
|
20
|
+
text: string;
|
|
21
|
+
flags: PhrasingFlag[];
|
|
22
|
+
}
|
|
23
|
+
export interface PhrasingReport {
|
|
24
|
+
rows: PhrasingRow[];
|
|
25
|
+
total: number;
|
|
26
|
+
/** Rows carrying ≥1 flag. */
|
|
27
|
+
flagged: number;
|
|
28
|
+
/** Per-flag counts, descending. */
|
|
29
|
+
byFlag: {
|
|
30
|
+
flag: PhrasingFlag;
|
|
31
|
+
count: number;
|
|
32
|
+
}[];
|
|
33
|
+
/** Per-faction totals + flagged counts, faction-sorted. */
|
|
34
|
+
byFaction: {
|
|
35
|
+
faction: string;
|
|
36
|
+
total: number;
|
|
37
|
+
flagged: number;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
export type PhrasingFlag = "placeholder" | "type_fallback" | "snake_case_leftover" | "doubled_preposition" | "zero_inch" | "paren_plural" | "empty";
|
|
41
|
+
/**
|
|
42
|
+
* Classify one piece of generated text. Pure and order-stable so it is directly
|
|
43
|
+
* unit-testable. The order of the returned flags follows the {@link PhrasingFlag}
|
|
44
|
+
* union for deterministic output.
|
|
45
|
+
*/
|
|
46
|
+
export declare function classifyPhrasing(text: string): PhrasingFlag[];
|
|
47
|
+
/** Compute the phrasing report for a set of factions. Pure — IO lives in the command. */
|
|
48
|
+
export declare function auditPhrasing(input: {
|
|
49
|
+
faction: string;
|
|
50
|
+
abilities: AbilityEntry[];
|
|
51
|
+
}[]): PhrasingReport;
|
|
52
|
+
/** Read every `data/enrichment/<faction>/abilities.json` (skips `_`-prefixed dirs). */
|
|
53
|
+
export declare function loadFactions(): {
|
|
54
|
+
faction: string;
|
|
55
|
+
abilities: AbilityEntry[];
|
|
56
|
+
}[];
|
|
57
|
+
export interface AuditPhrasingOptions {
|
|
58
|
+
reporter?: "pretty" | "json";
|
|
59
|
+
write?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Commander action: run the phrasing audit over the repo's enrichment data. */
|
|
62
|
+
export declare function auditPhrasingCommand(opts?: AuditPhrasingOptions): void;
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=audit-phrasing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-phrasing.d.ts","sourceRoot":"","sources":["../src/audit-phrasing.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAmB,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAO9G,gFAAgF;AAChF,UAAU,YAAY;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACrC,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,MAAM,EAAE;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChD,2DAA2D;IAC3D,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAClE;AAED,MAAM,MAAM,YAAY,GACpB,aAAa,GACb,eAAe,GACf,qBAAqB,GACrB,qBAAqB,GACrB,WAAW,GACX,cAAc,GACd,OAAO,CAAC;AAWZ;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAa7D;AAED,yFAAyF;AACzF,wBAAgB,aAAa,CAC3B,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,YAAY,EAAE,CAAA;CAAE,EAAE,GACtD,cAAc,CA8ChB;AAED,uFAAuF;AACvF,wBAAgB,YAAY,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,YAAY,EAAE,CAAA;CAAE,EAAE,CAe/E;AAyDD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,oBAAyB,GAAG,IAAI,CAe1E"}
|