Report-only drift audit · research/architecture pass

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.

Audit date2026-06-27
Statusreview
Product pathunthinkable
Topicweapon-catalog de-nationalization
Scopereport-only; extraction deferred to gated Step 2
Files auditeddeployment-plan-first & deployment-plan-soviet — js/{data,enemy-data}.js

1. Overview / why this audit

The fire-timing display arc (rngMinrofTOT) 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:

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.

What "drift" means here: two files define the same weapon name with different serialized values. We distinguish drift (true disagreement → a bug surface) from duplicated-identical (same value in N files → safe but redundant maintenance burden) and authored-once (name in a single file).

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)

  1. For each of the four files, locate the three table blocks by opener — const NAME = { or Object.assign(NAME, { — and walk to the matching close brace with a string/comment/brace-aware scanner.
  2. Tokenize every top-level "key": value pair inside each block. This is required because the enemy-data weaponShortNames blocks 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.
  3. Normalize each value by eval → key-sorted canonical string (a value-hash), so key-order differences are not mistaken for drift.
  4. 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). Cite file:line for every drift.
  5. 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.)

1 true value drift (total)
384 duplicated-identical
12 authored-once
0 firing-stat drift
TableDistinct namesAuthored-onceDuplicated-identicalDrifted
weaponSpecificStats
rng/rof/layTime/grd/air — the firing values
13531320
weaponAmmo
rounds/caliber/kind — magazine + display label
11931151
weaponShortNames
layout-chip abbreviations
14361370

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:

Fieldfirst/data.js (UK player)soviet/data.js (SOV player)Verdict
weaponSpecificStats["Bombs"]first/data.js:472soviet/data.js:369identical (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:243identical

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:

GroupCross-nation namesDrift?
Lend-Lease / light.30 cal MG, Quad .50 cal AAidentical
Engineering munitionsAT mines, AP minesidentical
Air strikeBombscaliber drift (§4)
Recon / utility kitBinoculars, Observation gear, Engineer tools, Repair gear, Carrier mountidentical
Logistics (statless)Supplies, Munitions, Petrol tank, Bailey bridge equipment, Workshopsidentical
Command (rng:"CMD")Radio net, Fire direction, Infantry staging, Air control party, Operations staffidentical

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:

WeaponTableDefined atReferenced but undefined inEffect
No.4 Mk I (T) (UK sniper)weaponSpecificStatsfirst/data.js:410soviet 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 grenadesweaponShortNamessoviet/enemy-data.js:177first/data.js (Sappers reference it; no short name there)layout chip shows fallback label in the first copy
Motorcycle mountsweaponShortNamessoviet/data.js:257first/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.jsenemy-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.

CriterionA — full catalogB — overlay + verifierC — hybrid
Removes 2× duplicationfullynopartial (generics only)
Drift possible afterwardsno (by construction)only if verifier runnation weapons still can
Fixes Bombs + 3 coverage gapsyes, onceno (separate fix)Bombs yes; gaps partial
Refactor cost nowmedium (2 data.js + 2 index.html)tinysmall–medium
Offline file://-safeyesyesyes
Two-copy mirror burden1 catalog file ×2 copiesunchanged (4 files)1 small file ×2 + faction files
Recommendation. Adopt Option A for Step 2 as the clean end-state — it dissolves the duplication, the one drift, and the three coverage gaps together, and aligns with the "data is single source of truth" direction (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

1. Adopt Option A — shared js/weapon-catalog.js as the Step 2 target?
2. Resolve the "Bombs" caliber drift by picking one canonical caliber for the shared generic?
3. Promote the audit parser to a committed drift verifier as an interim guard before Step 2?
4. Fold the 3 coverage gaps (§6) into the Step 2 extraction?
5. Confirm this pass is report-only — defer all prototype 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.