Shared Weapon-Catalog Drift Audit & Architecture Recommendation
Before extracting a shared per-weapon catalog, this pass proves where weapon definitions overlap across the four faction data files and whether any shared weapon's values DISAGREE, then recommends the catalog architecture. Weapons used by both nations (Lend-Lease generics, command/logistics kit) are currently hand-authored in two prototype copies that swap player vs enemy faction — a structural drift risk. The audit script-parses weaponSpecificStats / weaponAmmo / weaponShortNames by weapon name across all four files and classifies every shared name. No prototype js/ changes are made in this pass.
1. Overview / why this audit
The fire-timing display arc (rngMin → rof → TOT) is complete, so we now hand-author per-weapon firing values (rof, layTime, rngMin). The tactical-battle prototype ships as two deliberately separate copies that role-swap the player:
- deployment-plan-first — UK 11th Armoured is the player (
data.js); Soviet 2nd Guards is the enemy (enemy-data.js). - deployment-plan-soviet — Soviet is the player (
data.js); UK is the enemy (enemy-data.js).
Inside each copy, data.js declares const weaponSpecificStats = {…} (plus weaponAmmo, weaponShortNames) and enemy-data.js appends the opposing nation's weapons by Object.assign(weaponSpecificStats, {…}) onto that same shared object (top-level consts share lexical scope across classic <script> tags). The net effect: every weapon ends up authored twice — once per copy — and weapons shared by both nations (e.g. Lend-Lease .50 cal/.30 cal, the Bombs air strike, mines, and all command/logistics "weapons") are authored in both player files. Two hand-edited homes for one logical value is a drift risk. This audit measures whether that risk has already materialized.
All findings are produced by a reproducible parser run against live source and spot-checked against file:line. Audit scripts: scratchpad/audit_weapon_catalog.js (parser + classifier) and scratchpad/audit_faction_overlap.js (faction reach + mirror check).
2. Method (scriptable, reproducible)
- For each of the four files, locate the three table blocks by opener —
const NAME = {orObject.assign(NAME, {— and walk to the matching close brace with a string/comment/brace-aware scanner. - Tokenize every top-level
"key": valuepair inside each block. This is required because theenemy-dataweaponShortNamesblocks pack several pairs per physical line (e.g.soviet/enemy-data.js:184); a naïve line parser under-counts and reports false drift. Each pair records the line its key sits on. - Normalize each value by
eval→ key-sorted canonical string (a value-hash), so key-order differences are not mistaken for drift. - Build
name → [{file, line, hash}]per table. Classify each name: authored-once (1 file), duplicated-identical (N files, 1 distinct hash), drifted (N files, ≥2 distinct hashes). Citefile:linefor every drift. - Faction overlay: tag files UK = {
first/data,soviet/enemy}, SOV = {first/enemy,soviet/data}. A name in both a UK and a SOV file is cross-nation. Cross-check the two mirror pairs (same faction, opposite copy) for value diffs and coverage gaps.
3. Drift results — the headline
Value drift is essentially nil. Of 385 shared (multi-file) definitions across the three tables, exactly one disagrees — and it is a cosmetic display label, not a mechanical stat. (397 distinct names total = 385 shared + 12 authored-once.)
| Table | Distinct names | Authored-once | Duplicated-identical | Drifted |
|---|---|---|---|---|
weaponSpecificStatsrng/rof/layTime/grd/air — the firing values | 135 | 3 | 132 | 0 |
weaponAmmorounds/caliber/kind — magazine + display label | 119 | 3 | 115 | 1 |
weaponShortNameslayout-chip abbreviations | 143 | 6 | 137 | 0 |
Entry counts per file (authoring volume that feeds the duplication burden): weaponSpecificStats — first/data 93, first/enemy 40, soviet/data 62, soviet/enemy 72 (267 authorings for 135 distinct ≈ 2× duplication). The duplication is real and large; the disagreement is not.
4. The one value drift — weaponAmmo["Bombs"]
"Bombs" is the generic air-strike munition shared by both nations' aircraft. Its firing profile (weaponSpecificStats) is byte-identical across copies; only the cosmetic caliber display string in weaponAmmo disagrees:
| Field | first/data.js (UK player) | soviet/data.js (SOV player) | Verdict |
|---|---|---|---|
weaponSpecificStats["Bombs"] | first/data.js:472 | soviet/data.js:369 | identical (rng 500, rngMin 300, he 6, ap 8, blast 40…) |
weaponAmmo["Bombs"].caliber | "500 pounds"first/data.js:572 | "FAB"soviet/data.js:438 | DRIFT — display label only (rounds:2, kind:"bomb" identical) |
weaponShortNames["Bombs"] | "Bombs" first/data.js:321 | "Bombs" soviet/data.js:243 | identical |
Is it a bug? Borderline. The two labels read as deliberate national flavour — British "500 pounds" vs Soviet FAB (фугасная авиабомба). But because "Bombs" is one generic weapon name shared by both decks, the label a player sees depends on which copy they open rather than on the firing unit's nation — the air-strike on a UK enemy aircraft in the Soviet copy would read "FAB". This is exactly the inconsistency a catalog removes. Resolution at extraction (Step 2): either (a) pick one canonical caliber for the shared "Bombs" entry, or (b) if per-nation flavour is wanted, split into nation-specific weapon names ("Typhoon bombs" already exists at first/data:573 with "500 pounds"; a Soviet "FAB bombs" could carry "FAB") and let the generic "Bombs" resolve to one default.
5. Cross-nation shared weapons — the de-nationalization surface
These weapon names appear in both a UK and a SOV table — the true shared surface a catalog would author once. All are duplicated-identical except "Bombs" (§4). 20 cross-nation weaponSpecificStats entries:
| Group | Cross-nation names | Drift? |
|---|---|---|
| Lend-Lease / light | .30 cal MG, Quad .50 cal AA | identical |
| Engineering munitions | AT mines, AP mines | identical |
| Air strike | Bombs | caliber drift (§4) |
| Recon / utility kit | Binoculars, Observation gear, Engineer tools, Repair gear, Carrier mount | identical |
| Logistics (statless) | Supplies, Munitions, Petrol tank, Bailey bridge equipment, Workshops | identical |
| Command (rng:"CMD") | Radio net, Fire direction, Infantry staging, Air control party, Operations staff | identical |
Where they live (the asymmetry): all 20 cross-nation entries are authored in the two player files (first/data.js and soviet/data.js) and in neither enemy overlay. That is by design — within a copy, the enemy units reference these generics from the host data.js object that already declared them. So the duplication for generics is "once per copy" (2×), and the "Bombs" drift is the consequence of those two independent hands. weaponShortNames has 22 cross-nation entries (adds Tow gear, Tow hitch, Troop benches, Troop compartment); weaponAmmo has 5 (.30 cal MG, AP/AT mines, Bombs, Quad .50 cal AA).
6. Mirror-pair integrity + coverage gaps
The two mirror pairs are perfectly value-consistent. Comparing the same faction across copies — UK: first/data vs soviet/enemy; SOV: first/enemy vs soviet/data — yields value-diff = 0 in all three tables for both pairs. Whatever weapon a faction has in one copy, it has with the same values in the other. Good discipline; it also means the byte-shareable surface is essentially the whole table.
Coverage gaps (secondary finding — not value drift). The 12 "authored-once" names are weapons present in only one of the four files. Most are benign (the referencing unit simply does not exist in the mirror copy). Three are minor coverage gaps where a unit references a weapon the file's tables do not define, so the engine falls back to a generic profile/label:
| Weapon | Table | Defined at | Referenced but undefined in | Effect |
|---|---|---|---|---|
No.4 Mk I (T) (UK sniper) | weaponSpecificStats | first/data.js:410 | soviet copy (referenced by UK-enemy unit via soviet/enemy-data.js; host soviet/data.js never defines it) | sniper uses generic rifle fallback stats in the Soviet copy |
No.77 WP grenades | weaponShortNames | soviet/enemy-data.js:177 | first/data.js (Sappers reference it; no short name there) | layout chip shows fallback label in the first copy |
Motorcycle mounts | weaponShortNames | soviet/data.js:257 | first/enemy-data.js (SOV recon references it; no short name there) | layout chip shows fallback label in the first copy |
Benign authored-once (referenced only where defined): Mosin-Nagant M91/30 PU and PTRD-41 (both soviet/data.js only; the SOV enemy detachment in the first copy doesn't field them). These three gaps are also dissolved by a shared catalog — a single union table makes "referenced but undefined" impossible. They are flagged here so Step 2 can confirm/clean them; this pass makes no edits.
7. Architecture options & recommendation
The data says: drift risk is real structurally (2× hand-authoring) but has produced only one cosmetic disagreement so far. So the choice is between eliminating the duplication (a catalog) and guarding it (a verifier). Three candidates, all offline file://-safe constraints respected (plain <script>, no import/fetch/build step — see memory/prototype-fileurl-constraint.md).
Option A — Shared js/weapon-catalog.js (extract authored-once union) recommended for Step 2
Add one new module js/weapon-catalog.js that declares the full union of the three tables (UK + SOV + generics) once. Load it before data.js in each index.html (the current order is data.js → enemy-data.js → … at index.html:148). data.js/enemy-data.js stop declaring weapon stats and only reference the catalog (or layer thin per-faction overrides). De-nationalizes weapon stats entirely.
Pros: single source of truth — 267 authorings collapse to 135; drift becomes impossible by construction; the "Bombs" caliber and all three coverage gaps resolve once; 4-file scatter → 1 catalog file per copy.
Cons: requires a real refactor — the const weaponSpecificStats = {…} declarations in both data.js must change to consume the catalog (can't re-const a name the catalog already declared); two index.html edits (one <script> each); the catalog file itself is still a per-copy artifact that must stay byte-identical across the two copies (the copies are deliberately self-contained — see memory/deployment-plan-soviet-copy.md), so the two-copy mirror burden shrinks from "every weapon in 4 files" to "one catalog file in 2 copies."
Option B — Keep the Object.assign overlay + add a drift verifier
Change nothing structural; commit scratchpad/audit_weapon_catalog.js as a maintained verifier (e.g. tools/verify-weapon-catalog.js) that exits non-zero on any cross-file value drift, and run it as part of the existing validation gate.
Pros: near-zero change now; preserves the working overlay; the verifier already exists and passes; cheap insurance against the next drift.
Cons: does not remove the 2× authoring burden — you still hand-edit each weapon in two places; it catches drift after it's typed, not at the source; no CI runs automatically for an offline prototype, so it relies on discipline to run; the "Bombs" drift and coverage gaps remain unless separately fixed.
Option C — Hybrid: extract only the cross-nation set, verifier for the rest
Extract just the 20 cross-nation generics (§5) into a small shared js/weapon-catalog.js; leave nation-specific combat weapons in their faction files; keep a thin verifier on the faction mirror pairs.
Pros: smallest shared surface that kills the only observed drift (Bombs) and the worst duplication (generics authored in both player files); minimal refactor.
Cons: two mechanisms to reason about (catalog + overlay + verifier); nation-specific weapons keep their 2× mirror burden and still need the verifier; partial solution to a problem worth solving once.
| Criterion | A — full catalog | B — overlay + verifier | C — hybrid |
|---|---|---|---|
| Removes 2× duplication | fully | no | partial (generics only) |
| Drift possible afterwards | no (by construction) | only if verifier run | nation weapons still can |
| Fixes Bombs + 3 coverage gaps | yes, once | no (separate fix) | Bombs yes; gaps partial |
| Refactor cost now | medium (2 data.js + 2 index.html) | tiny | small–medium |
Offline file://-safe | yes | yes | yes |
| Two-copy mirror burden | 1 catalog file ×2 copies | unchanged (4 files) | 1 small file ×2 + faction files |
memory/deployment-plan-handoff.md). As a cheap interim guard adopted now, promote this audit's parser to a committed verifier (Option B's mechanism) so any drift introduced before Step 2 lands is caught. Option C is the fallback if the full data.js refactor is judged too large for one step — but the refactor is bounded (two declaration sites + two <script> lines) and worth doing once.8. Approval Gates
js/weapon-catalog.js as the Step 2 target?
"Bombs" caliber drift by picking one canonical caliber for the shared generic?
js/ changes to gated Step 2?
Compile Responses
Answer any gate questions and/or add section feedback, then compile YAML for the agent. Complete approval requires all required gates answered with no unresolved negative feedback.