# TODO State: the library reads and the MCP answers questions about it. 10,988 units parse with no failures, all of them carry MegaMek's computed figures, and six tools expose the Meks over MCP. What is weak is what can be *asked*: the filters describe a unit's paperwork — tonnage, era, role — and say almost nothing about how it fights. Battle value and construction are still borrowed from the Java bridge. The epics these belong to are in headquarters: `plan/unit-library.md`, `plan/unit-search.md`, `plan/unit-rules.md`. Several items below were prompted by reading [MekBay](https://github.com/MegaMek/mekbay), MegaMek's own web client, for what it lets a person filter on. It is GPL-3.0 and helm is not, so what was taken is the vocabulary and the shape of the problem, never code. ## What to do next, in order Twenty things, ranked. The tiers are the argument: the first finishes what `plan/unit-rules.md` already promises, the second widens what helm can score at all, and the third is what makes any of it reach a player. Everything below appears again in its own section with the detail; this is the order to take them in and the reason for it. **Finish what unit-rules promises.** The epic's exit criterion is a force's battle value computed the same way in the browser and on the server, agreeing with MegaMek except where a deviation is registered. 1. **Read C3 networks from a `.mul` and score them.** The arithmetic is written; nothing reads which unit is whose master, so `helm force` scores every unit as though it were alone and a networked lance is reported five percent light. 2. **Double verification across the wire.** Named in the epic: the same crate compiled twice, agreeing. The smoke test checks a few designs; the proof is the whole library scored in Node and natively with no disagreement. It is the reason `helm-bv` does no I/O, and it is currently unproven. 3. **Battle value for a varied unit.** `Condition` models damage and not variation. A player swapping an LRM bin for inferno rounds, or changing an omni pod, is exactly what the epic says players will do - and it is the largest gap between what helm computes and what the product needs. 4. **Pin the rounding.** Also named in the epic. Battle value is full of half-point steps and nothing proves the two builds round alike, or that a total does not depend on the order floats were added in. 5. **Settle the two compensating explosive errors.** B-Pods against CASE II ammunition: both sides are wrong and the fresh figure is right by luck. Wants two oracle designs before either can be corrected. **Widen what helm can score.** 6. **Vehicle battle value.** `CombatVehicleBVCalculator` upstream. Scenarios already carry tanks - `MercBrawl`, `example.mul` - and `helm check` answers "cannot be scored" for every one of them. 7. **Battle armour and infantry.** The same argument with different calculators, and the reason `LegAttack` and `SwarmMek` are synthesised. 8. **C-bill cost.** The second of the three figures borrowed from the bridge, and what anything economic needs. 9. **The Alpha Strike conversion.** The third. It already has a registered deviation, so the shape of the work is known. 10. **Retire `bridge/` for battle value.** Once 8 and 9 exist the JDK container leaves the build, which is `plan/unit-library.md`'s exit criterion. 11. **The equipment MegaMek synthesises while loading.** 5,534 units differ from `MekSummary.equipmentNames` only because of it. It blocks equipment filtering conformance and probably hides battle value bugs. 12. **The nine registered disagreements.** Now that they are named they can be taken one at a time; the Super-Griffin and the Jabberwocky are the two most likely to be a rule rather than a broken file. **Make it reach a player.** These belong to epics in headquarters as much as to helm. 13. **Decide how a design reaches the page, and build it.** The one open decision, and everything in `plan/unit-search.md` waits behind it. 14. **Compile `helm-facet` to wasm and filter with it in the browser.** The epic says it outright: run the same predicate, do not reimplement. Two filter implementations is how "filtering means the same thing everywhere" dies. 15. **The redistribution question.** MegaMek's data is CC BY-NC-SA and serving an index of it to browsers is redistribution. It needs a decision and an attribution mechanism *before* 13 ships. 16. **Provenance on every record.** Which MegaMek, which producer, which `RULES_VERSION`. The constants exist and nothing writes them into the database or into an ATProto record, so a stored figure cannot be told from one computed against a different MegaMek. 17. **The attribution and the repair list on a match page.** `plan/after-action.md`. The pieces are built and cross the wasm boundary; what is missing is the screen that reads them. 18. **Damage that persists between matches.** `plan/campaign.md`. The mechanical half exists; storing a force's condition as a record and reading it back is the rest, and the construct-write-read round trip supports it. 19. **Formation validation.** `list_formations` and `validate_formation` against the Campaign Operations blueprints - "is this a legal Fire Lance". Validation before generation, and it needs no faction data: it is role and count, both of which helm holds. 20. **Ingest `data/forcegenerator/`.** 40 era files of per-faction availability weights, `factions.xml`, and 61 faction rulesets that are a small DSL of their own. This is the foundation for force generation, faction filtering and era-correct scenarios, and nothing above it should be attempted until it exists. ## Derived combat metrics The highest-value gap. These are what make a filter describe a *fighting machine* rather than a spec sheet, and every one is computable from data helm already holds: `unit_equipment` joined to the `equipment` catalogue, which carries heat, damage and ranges per weapon. - [x] **`firepower`** — total weapon damage, physical attacks excluded. The one-number answer to "how hard does it hit". - [x] **`max_range`** — longest weapon's long range. Separates a brawler from a fire-support unit far better than `role` does, because `role` is a hand-assigned label and this is a fact about the loadout. - [x] **`heat`** — heat generated firing everything at once. - [x] **`dissipation`** — heat sinking capacity, from the `heat sinks:` line. Doubles count twice. - [x] **`heat_efficiency`** — dissipation against generation. A Mek that alphas into shutdown is a different unit from one that does not, and nothing in the current facet set can tell them apart. - [x] **`armor_pct`** — armour as a fraction of the maximum for its tonnage. Normalises across weight classes, so "well armoured for its size" becomes expressible. An Atlas AS7-D carries 304 points and a Locust LCT-1V 64, which says the Atlas is the armoured one; as a percentage they are 99 and 93, which is the truth. A Wasp WSP-1A is 70 on the same twenty tons as that Locust. The maximum is two points a location for every point of internal structure, except the head, which takes nine whatever it is bolted to and twelve on a superheavy — so it is not twice the structure total. Front and rear plate share a torso's allowance rather than each having one. The figure is in no unit file, so it cannot be conformed against MegaMek the way a battle value can. What holds it is that no design may exceed it: over all 4,294 Meks none does and 815 sit at exactly 100, which is the check both ways round — a maximum set too low would put designs over it, and one set too high would leave nothing at the top. This is declared data rather than derived from the catalogue, so unlike the five above it the column is filled whether or not a build had one. It is null for everything that is not a Mek, the table being a Mek's. - [x] **Expose all of the above as MCP range filters and in `list_facets`.** Verified against designs whose loadouts are well known: an Atlas AS7-D comes out at 60 firepower and 29 heat, an Awesome AWS-8Q at 33 and 31. The discrimination is real — a Kodiak carries 102 firepower at -28 heat efficiency and cannot sustain it, where an Annihilator C 'Gausszilla' manages 75 at +15, which is what the nickname is about. ## Filters the data already supports Cheap: the columns exist and the MCP does not offer them. - [x] **`rules_level`** — Introductory, Standard, Advanced, Experimental, from the `tech_level` column. The `rules level:` line in a `.mtf` is a bare digit and is not this. - [x] **`cost`** range, in C-bills. Distinct from battle value, and a test holds them to selecting different sets so neither can quietly stop being applied. - [x] **`quirks`** — all-of rather than any-of, since asking for two means wanting both. `list_facets` names the twenty commonest, and an unknown one comes back with near misses. - [x] **`config`** — Biped, Quad, Tripod. **Not `motion_type`**, which this list originally said: that field is populated for exactly zero Meks, being a `.blk` concept. The `OmniMek` suffix is stripped, so `config: Biped` finds OmniMeks too and `omni` asks that separately. - [x] **`run_mp`**. - [x] **`mul_id` lookup** — go straight to a unit from a MUL record id. 4,126 Meks carry one. - [x] **`source`** — which book a unit comes from. The column is a comma-joined list (`TR:3039,TR:SW`), so it is split into one and matched as an any-of list rather than with a substring test that would find `TR:303` inside `TR:3039`. `list_facets` names the twenty commonest, since a caller cannot ask for a book without knowing how it is spelled. ## Equipment filtering - [x] **Weapon class, not just weapon name.** All 27 of MegaMek's `AdvancedSearchEquipmentClass` values, ported with its matching rules rather than reinvented — substring tests on the lowercased internal name, composites defined in terms of each other, ammunition excluded before anything else. `Gauss` as a class finds 456 Meks where the named `ISGaussRifle` finds 317, because the class also catches the Clan, light, heavy and AP variants. Two upstream behaviours are reproduced on purpose and pinned by tests so nobody tidies them away. `Ultra` matches `ultraa` with two As, which is not a typo: it is matching `ISUltraAC5`, where "ultra" runs into the "AC". And a class leaf **sums quantities across matching entries** where a name leaf does not, so two medium lasers and a large laser satisfy "at least 3 energy weapons" while no name leaf asking for 3 would. - [ ] **The loadout is still not MegaMek's.** 2,935 of 10,988 units match `MekSummary.equipmentNames` exactly. Of the rest, 5,534 differ *only* because MegaMek has entries no file declares — it synthesises them while loading: `BAJumpJet` from a suit's jump MP, `LegAttack` and `SwarmMek` because battle armour can swarm, `Standard Armor` and `Heat Sink` on a Mek from header lines. Closing this is modelling MegaMek's loader, so it belongs with the domain model rather than with the parser. Recorded on `UnitFacets::loadout`. ## Damage at range - [x] **`damage_at_range` and `max_single_damage_at_range`**, following MegaMek's `FormationType.damageAtRangeStats`, because the formation rules are written against it. Stored for the six bands those rules ask about: totals at 6, 7, 9 and 18, and the hardest single hit at 15 and 18. This is **maximum** damage where `firepower` is average, and the two are not interchangeable: a full LRM 20 salvo is 20 here and 12 there. The first version scored every ammunition-fed weapon at zero and nothing looked broken - an Atlas came out at 20 instead of 72, because a `.mtf` weapons block lists no ammunition and the lookup therefore never matched. Ammunition is now found in the critical slots, where a `.mtf` actually puts it. It moved the count of Meks meeting the Assault Lance criteria from 786 to 1,765, so it was not a rounding error. Two more of the same shape came out of the formation oracle. Artillery was scored at zero, because a weapon was recognised by having an average damage and artillery has a sentinel instead of one - MegaMek asks `instanceof WeaponType`, and so does `EquipmentEntry::is_weapon` now. And an omni's ammunition was invisible: a `.mtf` writes `(OMNIPOD)` into the slot itself, so the name in the slot is not the name of the thing in it. `helm_core::strip_slot_markers` is that cleaning, shared with `helm_bv::loadout` rather than written twice. - [x] **A Clan design's weapons are scored as the Clan versions.** The loadout is read from the `Weapons:` block, which spells a weapon the way a person would - `Large Pulse Laser` - and two weapons answer to that name: the Inner Sphere one reaches ten hexes, the Clan one twenty. `Catalogue::resolve_for` takes the design's tech base and picks, which is what MegaMek does by reading the critical slots instead. Two more of the same shape came with it. A weapon whose damage is a sentinel rather than a number - a Snub-Nose PPC does ten, eight and five by band - was counted as nothing, so `EquipmentEntry::damage_at` reads the bands the way `WeaponType.getDamage` does. And a one-shot launcher arrives loaded: its rounds are part of the weapon rather than a bin in a slot, so MegaMek makes the ammunition while it reads the design and helm found none, which scored a Firestarter FS9-C's six rocket launchers at zero. Measured by the formation oracle: 179 damage disagreements of 42,453 requirement checks, now 5. - [x] **Walking MP is what the design manages, not what it declares.** A Black Knight BLK-NT-3A carrying a medium shield declares 5 and walks 4, and a Banshee BNC-11X wearing modular armour loses a point to that. `helm_core::walking_mp` applies both, and the facets and the database carry the figure MegaMek reports rather than the line in the file. Of 4,294 Meks the declared line is wrong for 18; one is left, and it is the chain drape already recorded as a known deviation. Deliberately not hardened armour, which costs a Mek a point in the battle value calculation and nothing in what MegaMek reports about the design. The two figures are not the same figure. - [x] **The loadout is read from the slots.** A `.mtf` says what it carries twice: the `Weapons:` block spells a weapon the way a person would and the critical slots spell it the way MegaMek looks it up. Only the second settles which of two weapons sharing a display name is meant, which the tech base only usually does - a Yinghuochong YHC-3E is an Inner Sphere design carrying a Clan ER PPC. `helm_core::mounted_items` reads the slots and the weapons block is the fallback for a `.blk`, which has no slots. That took the formation oracle's damage disagreements from 5 to 0, and the test no longer forgives any: 42,453 requirement checks over 477 Meks, and every figure helm computes for them is MegaMek's. - [x] **Jumping MP is the jets, not the line.** `helm_core::jumping_mp` counts them, with the partial wing's lift, the mechanical jump booster, the shields and the modular armour that cost a point, and nought for a design with UMUs - which swims rather than jumps and declares a figure it has no jets for. Of 4,294 Meks the declared line is wrong for 57 and this is wrong for none. Deliberately not the same figure `helm_bv` works out. Battle value runs under a setting that says `noModularArmor`, so a Koshi (Mist Lynx) Z is scored jumping six there and reported jumping five here. Two figures, both right, and a test each. ## Force value - [x] **A C3 network is worth something, and helm can say how much.** The third of the four scopes, and the only one a unit cannot work out about itself: every member is paid five percent of the network's combined base value, seven for a boosted set, and a set with nobody to talk to is worth nothing. `helm_bv::c3_kind` reads what a design carries - 244 standard sets in the library, 178 C3i, 38 boosted and 29 Nova - and `helm_force::c3_bonuses` says what a force of them is worth. Nova is counted among its own kind rather than on a network, which is how MegaMek pays it. A `.mul` records the network as pointers rather than as a list - a slave names its master's `c3UUID`, and a C3i unit names every other member in `` elements - so `helm_force::c3_bonuses_linked` turns the pointers into networks and pays each one for its own members. A force can hold more than one: two lances with their own masters are two networks, and a chain of masters under a master is one. `helm force` reports the share in a column of its own. A Gunslinger GUN-2ERD and a Malice MAL-YZ wired together are 2,423 and 2,982 apart and +270 each on the network, which is five percent of both. A Peacekeeper carrying a slave set with no master in the force is paid nothing, because a set with nobody to talk to is not a network. - [x] **Pilot skill changes battle value.** `helm-force` ports MegaMek's 9x9 `bvMultipliers`, indexed gunnery-then-piloting and clamped the way `bvSkillMultiplier` clamps. A regular 4/5 crew multiplies by exactly 1, which is what makes every battle value quoted without a pilot mean "as flown by a regular". The table is transcribed rather than derived - there is no formula behind it - and reading it the wrong way round is easy: `[3][4]` is 1.32, not the 1.44 at `[3][3]`. My own first test expectations were wrong that way and the implementation was right. - [x] **Force totals**, rounded per unit as MegaMek rounds them rather than on the total, which differs by a point or two. Base and skill adjustment are reported separately: a force over budget because of its units is a different problem from one over budget because of its pilots. The same four Meks are 4,809 with regular crews and 6,348 with veterans. - [x] **`force_value` MCP tool**, taking chassis and model per unit with optional gunnery and piloting. - [x] **Search returns chassis and model separately.** A `.mul` identifies a unit by that pair, so a force record cannot be built from results that only carry a display name - and a Clan design's display name contains a parenthesised second name that is in neither field. ## Battle value The long pole, and the reason `helm-core` does no I/O. Tracked as `unit-rules` in headquarters' `plan/`. - [x] **The oracle, before the arithmetic.** `helm-bv` exists, scores nothing, and is measured against MegaMek's own answer for all 4,294 Meks in the library. `helm bv-report` prints the comparison and exits non-zero while anything disagrees; `crates/helm-bv/conformance.txt` is that report, committed, and the test fails when a run stops matching it. So an improvement shows up as its own diff and a regression cannot be merged quietly. It reads 0 / 4,294 today, which is the honest number. - [x] **The design model battle value reads.** Engine (rating and type), heat sinks, gyro, cockpit, internal structure and armour type, all typed in `helm-core`'s `design` module against the vocabulary the 4,294 Meks actually use rather than a remembered list. Every enum has an `Other` arm, so a component a future MegaMek adds makes a design unscoreable instead of quietly scoring as the wrong component. Two things fell out. The tech-base marker is spelled in three places - `XL (Clan) Engine`, `XL Engine(IS)`, `IS Endo Steel` - and reading only the first misses 116 designs. And patchwork armour writes its type in front of the points, `LT armor:Clan Standard(Clan):26`, which the parser rejected as non-numeric: all five patchwork designs were reading as almost unarmoured, a Zeus-X at 28 points where MegaMek says 247. Total armour now agrees with MegaMek on all 10,749 designs it reports one for. - [x] **Battle value for a unit somebody varied.** A `.mtf` says what a design left the factory with; a `.mul` says what is in every critical slot now, and MegaMek reads those back - a magazine whose `type` names acid rounds is loaded with acid rounds and scored as such. That is how a player changes a unit without designing one, and helm read the design's own slots and answered as though nothing had been touched. A Shadow Hawk SHD-2H is 1,064 as built and 1,069 with acid rounds in its SRM bin. Both figures are MegaMek's, and both are now helm's. `bridge/DumpMul.java` is the oracle, and it is the first one that reads a file rather than shooting a design itself - which makes it the harness for anything expressed in a `.mul` rather than derived from a `.mtf`. The fixtures in `crates/helm-bv/tests/mul/` are this repository's own, because MegaMek ships no file that varies a unit. An unknown name in a slot is ignored rather than treated as an empty one: a newer MegaMek's spelling should cost nothing, and the design's own answer is a better guess than a hole. An omni pod swap - different equipment rather than different rounds - should ride the same mechanism and has not been checked. - [x] **The rounding is pinned, and the designs it decides are named.** Battle value is built from half points - two and a half a point of armour, one and a half a point of structure - so a total landing exactly on a half is ordinary rather than freakish. 53 designs of 4,262 do, and for those the figure is not decided by the arithmetic at all but by which way a half goes. Java's `Math.round` sends a half up; Rust's `f64::round` sends it away from zero. Those are the same thing until a figure is negative, and every figure rounded today is positive - so the rule is written down once in `helm_core::round_half_up` rather than assumed at eleven call sites, and a term that comes out negative later cannot quietly disagree by a point. The test measures rather than asserts a limit: how many designs sit within a millionth of a boundary, how many exactly on one, and whether any of those is a *point* away from MegaMek - which is what a rounding disagreement looks like. None is. The only near-boundary design that differs is the Uni ATAE-70, and it is 43 out because its file allocates one Thumper's slots to two Thumpers. - [ ] **The equipment MegaMek synthesises while loading** rather than reading from the file, which battle value counts. Recorded on `UnitFacets::loadout`. Half done. `helm_core::equipment_list` models the part a Mek's header lines imply - one structure entry, one armour entry, every heat sink including the ones inside the engine, and a Clan design's structural CASE - and designs whose list is MegaMek's went from 1,883 of 10,988 to 4,860. `crates/helm-facet/tests/loadout.rs` holds the figure and reports what makes up the rest, which is now infantry weapons, battle armour gear and aerospace bays rather than anything Mek-shaped. Battle armour too: the jets are declared in ``, which nothing read, and the four anti-Mek attacks are invented for a suit that can climb a Mek. Which can is `BattleArmor.canMakeAntiMekAttacks`: lighter than a heavy suit, not a quad, and with two hands free - two armoured gloves or two basic manipulators, or one battle claw, or a magnetic clamp. Suits carrying MegaMek's list went from 276 of 1,188 to 1,184. Infantry is untouched: a platoon's weapons are declared in a way of their own, and all 1,792 differ. Not yet wired to `UnitFacets::loadout` or the database. A synthesised entry has no location and the equipment table stores one row per mount with one, so switching the facet over is a change to what a search returns and wants its own evidence. - [x] **The oracle says where, not just whether.** `bridge/DumpBv.java` runs MegaMek's own calculator and records what it held afterwards - the defensive and offensive ratings, movement points, heat sum - into `bv.jsonl`. Without it every part of the calculation has to exist before any of it can be measured, which is the long blind stretch the oracle was built to avoid. It costs a second pass over the library because the calculator needs a real Entity rather than the summary cache. - [~] **Defensive battle rating.** 4,235 of 4,294 Meks match MegaMek exactly. Armour at 2.5 a point and internal structure at 1.5, each through its own type multiplier, plus the gyro at half a ton, defensive equipment at catalogue value, minus 15 a slot for ammunition that can cook off, all through a movement factor of 1 + TMM/10. Every multiplier was fitted against MegaMek across the library rather than transcribed. Three came out worth commenting: an Inner Sphere XL engine is 0.5 where a Clan XL is 0.75, because the Inner Sphere version dies with either side torso; Ferro-Fibrous is 1.0 like standard plate, since it buys more *points* and those are already counted; and CASE is better modelled as "carries it anywhere" than "carries it in that location", because a design that fits CASE fits it where the ammunition is. What is left is a long tail of defensive equipment - 1,655 designs, most within 20%. Counting equipment per critical slot rather than per item was worth 200 designs on its own: a large shield fills seven slots and was being paid for seven times. - [~] **Offensive battle rating.** 4,193 of 4,294 match. Weapons at catalogue value, halved once the design runs out of heat, plus ammunition capped by the weapons that eat it, plus the Mek's own mass, all through the speed factor. The weapon ordering is the part worth knowing: cold weapons first whatever they are worth, and only then by descending battle value. A machine gun costs nothing to fire, so it never competes for the heat budget, and sorting on value alone charges it as though it did. - [~] **Battle value end to end.** 4,166 of 4,294 designs match MegaMek exactly. `helm bv-report --clusters` groups what is left by what the designs have in common, which is how each of the remaining rules was found - the ranked list of worst offenders only ever shows whatever is rarest. The two ratings are summed and the total rounded once, which is the last place a half point can change an answer. - [x] **Battle value for combat vehicles.** 1,215 of 1,216 tanks, hovercraft, VTOLs and naval vessels agree with MegaMek, and the one that does not is recorded as a known deviation. The whole report is now 5,467 of 5,510 designs, both ratings included. It is one calculation and not two. MegaMek has a `BVCalculator` and a subclass per unit type, and `CombatVehicleBVCalculator` changes five things: the plate is worth less for being on a vehicle, the frame is a tenth of the tonnage in each location that wears armour, the tonnage counts for half, there is no heat budget to run out of, and only the front hull has a say in which way the machine faces. So `Mek` became `Machine`, it carries a `Chassis`, and the five places that differ ask it. What the oracle found that reading the rules would not have: a trailer with a stealth system is not hard to hit, because a machine that cannot move gets no movement modifier at all; a superheavy hull numbers its locations differently and MegaMek's facing rule is written against the numbers rather than the locations, which is in `crates/helm-bv/UPSTREAM.md`; and a vehicle's ammunition carries no explosive penalty - none of the 1,256 reports has the line. - [ ] **Support vehicles.** 225 designs, declined by type. They use the same calculator, but a support vehicle has a BAR rating on its armour, skips the amphibious modifiers, and has a chassis-and-fuel weight model of its own. Reading them as combat vehicles would be wrong rather than approximately right. - [ ] **The clusters still outstanding**, from `helm bv-report --clusters`. Each is self-contained and none of them blocks the others: - **LAMs** (23 designs) are declined rather than scored, by decision: lance.blue will not field them. They need their own movement modes - airmech and fighter - feeding both the speed factor and a heat efficiency three points higher, and a LAM scored as a Mek is wrong rather than approximately right. Reinstating them is deleting the guard in `battle_value` and writing those modes. - [x] **Battle value in a browser.** `crates/helm-wasm` is a `cdylib` with plain `extern "C"` exports and no bindgen - the boundary is four numbers wide, and the benchmark had already shown wasm runs the arithmetic at 1.02x native. A page loads the equipment catalogue once, then hands over a design's `.mtf` text and gets an `i64` back; errors are negative, so a caller that ignores them gets an obviously wrong figure rather than a plausible one. `crates/helm-wasm/smoke.mjs` drives it from Node, which is the only thing that exercises the interface a browser actually calls through: cargo build -p helm-wasm --target wasm32-unknown-unknown --release node crates/helm-wasm/smoke.mjs An Atlas AS7-D comes back 1897 fresh, 1564 with its torsos stripped, 2504 flown by a veteran and 1631 by a green crew. - [x] **Battle value for a machine that has been shot at.** `Condition` carries what it has lost - armour, internal structure, the critical slots a hit took out and the magazines that are empty - and every one of those changes the figure, because MegaMek reads what the Mek still has rather than what the sheet says. `bridge/DumpDamaged.java` is the oracle, because a `.mtf` describes a design as it leaves the factory and nothing else. Six designs in six states each, and the rows are worth reading rather than only passing: | what was done to it | Atlas AS7-D | Locust LCT-1V | |---|---|---| | nothing | 1897 | 432 | | plate stripped | 985 | 208 | | one torso opened | 1714 | 390 | | half a torso's frame gone | 1877 | 426 | | right arm shot off | 1719 | 402 | | magazines run dry | 1890 | **451** | Running a Locust dry *raises* its battle value: an empty bin no longer explodes, and the defensive penalty that removes is worth more than the rounds were. Reasoning from the rules would have got that backwards, and two of these six states found real bugs - a heat sink shot out of an arm is cooling the `heat sinks:` line still counts, and a hit anywhere in a multi-slot weapon destroys the whole weapon rather than the slot. **Widened, and it found three more.** Nine designs in fourteen states - 126 rows - covering the systems whose loss changes a figure: engine, gyro, heat sinks, jump jets, arm actuators, MASC, and the leg joints. Six states were added at once and three of them disagreed straight away: - **An engine critical is five points of heat a turn**, and MegaMek takes it off the heat efficiency after the movement heat rather than through it: `mekHeatEfficiency -= getEngineHits() * 5`. A standard fusion engine fills six centre torso slots and all six count. helm charged nothing, and an Atlas with its engine hit was 1897 where MegaMek says 1631. - **Jets that have been shot off are not jets the design never had.** The `jump mp:` line is the fallback for a design carrying no jets this crate can resolve, and a Catapult whose jets were destroyed fell through to it and was scored as still jumping four. - **A hatchet is never dropped for heat.** MegaMek builds its heat list from `getTotalWeaponList`, which a physical weapon is not in - it is scored with the equipment at full value. A vibroblade *is* in it, added by name with the heat its motor makes. helm halved every melee weapon once a design ran out of budget, which no fresh design in the library is far enough over to show. One of the six was the oracle's own fault rather than helm's: destroying "every heat sink" also destroyed the ones inside the engine, which fill no critical slot, cannot be shot out in the game, and have no way to be written into a `.mul`. An oracle for a state helm can never be asked about is not an oracle. It now damages only what a critical hit could reach. 126 of 126 rows agree, and the undamaged library is unchanged at 4,252. **Widened again, this time across designs.** `bridge/DumpDamaged.java` takes `every:N` and samples the library rather than naming designs, which is how the shapes and construction a hand-picked list never covers get asked about. At `every:40` that is 1,638 states, and it found two more: - **A weapon too big for one location** has the rest of itself in the next one, and it is one weapon. A Victor VTR-9Ka's autocannon fills nine slots of an arm and one of the torso beside it; blowing the arm off left helm counting a whole autocannon that was no longer there, worth 465 points of offence. The halves are now paired - but only when there are exactly two of them and they account for the whole item. A Chameleon Light Polarization Shield is spread over half a design a slot at a time *by construction*, and pairing on the name alone took a Scarecrow's shield out with its arm. - **An engine only runs hot when it is hit if it is a fusion or fission plant.** An internal combustion engine takes the critical and makes no extra heat, so a SecurityMech with its engine shot out is worth what it was. - [ ] **What the sampled damage corpus still disagrees about.** Four kinds, all found by `every:40` and none of them yet understood. Reproduce with `DumpDamaged.java out.jsonl every:40`; the curated list is what the committed test runs, and it passes at 154 of 154. - **A blown-off arm holding a big gun**, +8 to +9 defensive on a Cerberus MR-V3, an Ebony MEB-12, a Huron Warrior and a Thunder Hawk TDK-7S. All four carry a Gauss Rifle or a TSEMP Cannon in the arm, which suggests an explosive penalty MegaMek keeps charging for a weapon that is gone. - **Modular armour on a stripped design**, +97.5 on an Enforcer III ENF-7D - exactly 39 points of plate at 2.5. MegaMek counts modular armour as part of the location's armour, so stripping the location takes it too; helm adds it on top of a location it has already zeroed. - **A Devastator DVS-X10 MUSE EARTH's movement gear**, hundreds of points out on a hip or leg actuator hit and a few points out on a gyro hit. An XTR one-off, so it is likely a movement system helm reads and MegaMek recomputes differently once the legs are damaged. - **A Scarecrow UCU-F4 run dry**, 3 points of defence. The smallest of them and probably an explosive penalty on a magazine one of us empties and the other does not. **Three more, all the same mistake in different clothes: reading what a design was built with where MegaMek reads what is left, or the reverse.** - A **torso-mounted cockpit** counts its centre torso's plate a second time, for keeping the pilot alive rather than the Mek - and it has to be the plate that is still there. A stripped Enforcer III ENF-7D was being paid twice for armour it no longer had, 97.5 points of it. - An **armoured system slot** is worth something, and stays worth it when the slot is shot out: the plating is on the machine whether or not what it was protecting still works. helm stopped counting, so a Banshee BNC-11X with its arm blown off came out 36 light. - **Modular armour** goes the same way, which is the opposite of what it looks like: MegaMek pays for the pack whether or not the location holding it is still attached. And one that was not a rule at all: a Banshee writes `Hip (ARMORED)`, and the leg-damage rule matched the bare name. The design walked as though its hip were whole and came out three hundred points heavy. - [x] **A catalogue small enough to ship.** `helm catalogue` writes the fields the rules actually read and drops the rest: the bridge dumps a `namesVector`, a `techAdvancement` and several static tables on every one of four thousand rows, and helm-bv looks at about twenty fields. Same format and same reader, so nothing downstream changes: | | bridge dump | shipped | |---|---|---| | raw | 6.0 MB | 1.7 MB | | gzipped | 0.32 MB | 0.09 MB | | parsed in a browser | 216 ms | 88 ms | Checked by scoring the whole library from it: 4,252 of 4,294 designs match MegaMek either way, which is the same figure to the design. That check is a test now rather than something done once. The saving is only safe while the fields it drops are fields the rules never read, and "never read" is a claim about every rule in the crate rather than the ones somebody remembered - so every design is scored twice, once from each catalogue, and the two have to agree. The list of kept fields moved to `helm-bridge` beside the reader that makes it true. - [x] **Read a force from a `.mul`.** MegaMek's own record of units in play, written and read by MegaMek, MegaMekLab and MekHQ. It carries three of the four scopes a battle value is worked out in - what each machine has lost, who is flying it, which force it is in - and none of the first, because an entity names a design by chassis and model and says nothing about what is in it. The library is still what holds designs. `helm force ` scores one. Two files MegaMek itself wrote: | | fresh | from the file | |---|---|---| | Shadow Hawk SHD-2H | 1064 | 738 | | Griffin GRF-1N | 1272 | 694 | | Orion ON1-VA (untouched) | 1328 | 1328 | Parsed by hand like the other MegaMek formats. Three things the format does that a reader has to know: slots are numbered from one where everything else here counts from zero, `points` is sometimes a word rather than a number - `Destroyed` is none left and `N/A` is a location that never had any - and an ejected MekWarrior is written as another entity in the same file, so a force has to tell people from machines. - [x] **A ratchet for speed, not just for answers.** `performance.txt` records what each operation costs and the test fails anything a fifth worse. `conformance.txt` catches a wrong answer; nothing caught a dependency swapped for a slower one, a lookup that became a scan, or a parser that started allocating per line. Two things make the figures steady enough to assert on. Every task is timed in each round rather than one task to completion, so a busy minute costs all of them a round instead of making one look slow. And each keeps its fastest round, because a shared machine has a floor and no ceiling. Run-to-run drift is then under a tenth, against a gate of a fifth. Nine operations now, not five: the repair list, the upload check, and both ways of wearing a design down. The two searches are the expensive ones by a long way - wearing to a figure is 21 times a design read and a battle is 37, because each is dozens of scorings rather than one - which is worth having recorded before anything tries to run one per frame. Held only in release: an unoptimised build is several times slower and the two sets of figures are not comparable, so a debug run prints them and checks nothing. `HELM_PERF_BLESS=1` re-records, and a commit that moves a figure should say why. ## How a design reaches the page The one integration question still open, and the only thing on this list that wants a decision rather than work. A `.mul` names a design by chassis and model and carries nothing about what is in it, so the page needs the designs from somewhere regardless. Measured rather than guessed. About three quarters of a `.mtf` is prose - `overview:`, `history:`, `deployment:` - and stripping it leaves every one of the 4,294 battle values identical: | how | per design | whole library | |---|---|---| | `.mtf` as shipped | 8.9 KB median | 31.9 MB zip | | `.mtf` with the prose stripped | 2.6 KB median | **0.56 MB gzipped** | | a minimal JSON of the fields the rules read | 2.0 KB median | 0.45 MB gzipped | Which makes "ship the whole library and look up by chassis and model" the option that retires the question instead of answering it: 0.56 MB beside the 0.09 MB catalogue is under 0.7 MB for everything a page could ask about, and it can be fetched in the background while somebody browses. The JSON form saves 0.6 KB a design and costs a format: a new encoder in `helm`, a new decoder in `helm-wasm`, and both able to drift from the `.mtf` parser that conformance actually tests. Not worth it at that price. - [ ] **Decide, then build it.** Either a lean-library command beside `helm catalogue`, or a per-design fetch. helm's database already stores `unit_criticals` and `unit_armor`, so headquarters can reconstruct a design without a `.mtf` if that turns out to suit it better. ## What MegaMek keeps units in Four readers, and that is the whole set - `megamek/common/loaders` in 0.51.0: | format | what it is | count | |---|---|---| | `.mtf` | one Mek design, line-oriented text | 4,294 | | `.blk` | every other unit type, from tanks to warships | 6,694 | | `.mul` | a *force in a state*, XML - not a design format | interchange | | `units.cache` | a Java-serialised index: name, tonnage, BV, cost | 1 | Older MegaMek had HeavyMetal Pro importers (`.hmp`, `.hmv`); they are gone. MegaMekLab reads and writes `.mtf` and `.blk` and has no format of its own, so it is not a fifth thing to support. ## What the web UI needs from a .mul The format lance.blue works in, so these are the jobs it has to do. Written down because the first of them changes what the writer has to be, and that is not obvious from any one of them. - [x] **A rewrite must not lose what it did not understand.** Was the blocker for most of the rest. `write_mul` builds a file from what this crate models, and a `.mul` MegaMek wrote carries 36 attributes where we model 13: a round trip through lance.blue would drop `externalId`, `camoCategory`, `camoFileName`, `edge`, `deployment`, `gender`, `nick` and sixteen more. Storing a player's file and handing it back would have been data loss with no error and no way to notice. Each unit now keeps the element it was read from, and writing starts there and changes only what the model says - so a camo, a portrait, an `externalId` and any tag a later MegaMek adds all survive. A location says `Destroyed` rather than a nought this crate invented, and a slot still says which equipment is in it. - [x] **Take a `.mul` a player uploaded**, and say what is wrong with it before anything else touches it - designs not in the library, a crew outside 0-8, a location a shape does not have. `helm_bv::check` reports all of it at once and refuses nothing; `helm check ` prints it and exits non-zero. Most of the checks are about what the *reader* forgave. A location index it cannot place is skipped, an unparseable figure is left out, more structure than a design has is clamped when it is scored - all right for a reader, since a force that is nine tenths readable is worth reading, and all wrong for an upload, where the nine tenths get stored and nobody is told about the last tenth. Two of the checks can only be answered from the document, because by the time it is a model the evidence is gone. Three more since: six wounds is a dead MekWarrior rather than a hurt one, a slot marked as an empty magazine that holds a laser is a file describing something that cannot be, and a slot shot out that was empty to begin with is one written against a different design. The magazine one matters most because it is silent - the reader drops the slot either way, so the design comes out missing a weapon nobody shot. A complaint says whether it is about the file or about us. A force of tanks is a good file helm cannot score yet, and refusing it would be charging a player for our own gap - so only the file's own faults fail the command. The guard against the obvious failure - a check that refuses good files - is every `.mul` the install ships: 99 machines, no complaint about any of them. Four name a design MegaMek does not ship, which is a fault in MegaMek's own `example.mul` and is written up in `UPSTREAM.md`. Not reachable from a page yet: the check needs the design to compare against, and how a design gets to the page is the decision still open below. Everything here is `helm-core` and `helm-unitfile` only, so it crosses when that is settled. - [ ] **Store one on an ATProto record and read it back.** The record holds the `.mul` itself, with metadata beside it. The file now keeps the version that wrote it - MekBay stamps its own build where MegaMek writes `0.51.00`, and rewriting one as the other destroyed the only record of where a force came from. A rewrite is an *edit* of the document that arrived, never a rebuild of one from the model. Four separate losses came from getting that wrong - dropped attributes, dropped `` elements, a rewritten version string, reordered locations - and each was silent. The conformance test now compares the whole document element for element and attribute for attribute across every `.mul` the install ships, because comparing the *model* only proves the parts helm understands survive, which is the easy half. What the metadata has to carry, so a stored figure can be told from one computed against something else: `helm_bv::CONFORMED_TO_MEGAMEK`, the MegaMek these rules agree with, and `helm_bv::RULES_VERSION`, the build that produced the figure. A conformance test now refuses to let either drift from what `conformance.txt` was actually measured against. - [ ] **Edit a force in the page, with the value following.** Held designs already make this cheap: a re-crew is 1us and a re-score 200us, so a lance follows a slider inside a frame. - [ ] **Set a camo per unit.** Two attributes on the entity, and nothing to do with battle value - but it is the case that proves the round trip, since a lossy writer silently reverts it. - [x] **Damage a design until it is worth a target.** A search over conditions rather than a new rule: `helm_bv::wear_to` scores, compares and damages further, and `helm wear "Atlas AS7-D" --to 1500` prints what it did. For fitting a machine that has already been in a fight into a scenario's budget, which is a better answer than swapping the Atlas for something lighter. It bisects on plate and then on the frame under it, both spread evenly across the locations, because armour is most of the defensive rating and is the one term the figure moves smoothly with. It touches no magazine and shoots nothing out: an empty bin stops being worth anything *and* stops exploding, so that lever does not have a direction and a search cannot bisect on it. One point of frame is left everywhere - at nought a location is off the machine and takes its weapons with it. The bisection moves every location at once, so it lands within a step that is worth thirty points on a large design; a bounded pass taking single points off the thickest location closes that. Over a sample of the library at three targets each: 67 of 243 reachable targets hit exactly and the worst miss is 5, against 28 and 15 before the refinement. A point of armour is worth five to eight points of battle value, so a target between two achievable figures cannot be hit at all - `missed_by` says so rather than leaving the caller to compare. Of 321 targets tried, 78 were below what stripping a machine can reach. Most of a Mek's battle value is what it carries rather than what protects it, so "half of what it is worth" is not always a condition that exists. **Battle damage, rather than an even shave.** `helm wear ... --battle` damages a design the way a fight does: fire lands where MegaMek's own 2d6 hit table sends it, so the torsos take most of it and the head one roll in thirty-six; it clusters, because a gunner with the range keeps shooting the same spot; it arrives in the sizes weapons throw, a machine gun's 2 and a medium laser's 5 and an autocannon/20's 20 rather than a smooth distribution; and when it gets through the plate it takes equipment with it on MegaMek's critical roll - nothing on 7 or less, one component on 8-9, two on 10-11, three on 12. This is not a combat simulation and does not want to be. There is no attacker, no range and no to-hit roll: what is borrowed is the shape of damage, which is what makes two machines at the same figure look like two machines rather than two spreadsheet rows. Nothing it produces is a mission kill. No location is destroyed - the frame stops at one point, so no limb is ever blown off - the head is left alone below its plate, the engine takes at most one hit of the three that destroy it and the gyro one of the two, and magazines are never hit, that being an explosion and a dead crew without CASE. 180 battles across the library are checked against every one of those. Guns *do* come off, and should: shooting one off is often the shortest way down to a figure, and an Atlas that lost its autocannon and kept its lasers is a more interesting unit than one with evenly thinned plate. What is refused is the end of that - nothing left to fire, or less than a quarter of what it could throw. A leg joint turned out to be the sharpest lever of the lot, and the least obvious one. A hip has no battle value of its own, so shooting one off looked free - and it is not free at all, because it halves what the Mek walks and movement feeds the defensive factor through the target movement modifier and the offensive factor through the speed factor. helm was not modelling that: a wrecked hip changed nothing. MegaMek takes an Atlas AS7-D from 1897 to 1630 for one hip, and to 1289 for the three actuators below it. The rule is now written and the damaged oracle carries two more scenarios to hold it: a destroyed hip halves the walk rounding up, two hips stop the Mek, every other leg actuator costs a point, and a leg with a wrecked hip does not also pay for its remaining actuators. 40 of 40 oracle rows agree. A location is only ever damaged behind plate that was breached. The pass that puts armour back when a hit overshoots was patching up locations it had already been shot through - 62 of 1,760 breached locations, a Javelin with one point of frame left behind eight points of armour. It now skips any location with a bent frame or a wrecked component, and a test over the library holds it there. The hits land in the sizes they land in, so the figure steps past the target as often as onto it. The even search's single-point pass finishes the job, in both directions: plate off the thickest location when the machine is still worth too much, and back onto the least damaged one when a hit overshot. Every seed tried lands within a point of armour's worth. - [x] **Turn a post-match `.mul` into a repair list.** The difference between the design and its condition, per location: armour points, structure, the slots that were shot out, and the magazines to fill. `helm_bv::repairs` reports it and `helm repairs ` prints it, with `--why` adding what the damage cost in battle value. A slot names what was in it, because "slot 3" is not a repair order and "PPC" is. Nothing is costed. What a repair takes in time, parts and C-bills is a campaign's question; what is missing is a fact about two files. Writing it found a real bug in the `.mul` writer. The key for rear plate was built as `RT` plus the location's first letter, which is right for the three torsos and sends a left *arm* to `RTL` - the left torso's rear. Reading was harmless, since MegaMek writes no rear plate on a limb. Writing was not: a force with a damaged torso came back with rear armour tags on two limbs that never had any, and `Bot_North.mul` gained a whole `` it did not have. The round trip over the shipped files missed it because none of them has rear damage. The test that holds the mapping is exact: strip a design to nothing and the repair list has to come to the armour and structure it was built with. Over all 4,294 Meks, every one does - which is what says the keys are right for a quad's four legs as well as a biped's arms. - [x] **Say where a unit's value went.** Which term lost it - plate, frame, a weapon, cooling. `helm_bv::attribute` takes two conditions and reports the terms that moved between them, worst loss first, in battle value points rather than in their own units. `helm force --why` prints it and `Design#attribution` is the same thing in a browser. The terms do not add up to the figure by themselves - each half is a sum through a factor, and damage moves both - so the split needs a convention: an additive term is valued at the factor the machine ended up with, and a factor against the subtotal it started from. That is the one split with no cross term left over, and it is what lets a page draw a bar per term and have the bars sum to the drop beside them. A Griffin GRF-1N that came out of a match at 694 from 1272 reports -310 to its weapons, -257 to its plate and -12 to its frame, which is a repair list in the order it matters. Two things the sweep found. Wrecking a torso or a leg moves the *speed* factor and not the movement factor, because what it destroys is jump jets: 1,159 designs of 4,294 against 306. And a test over the damaged oracle rows alone never exercised either, so a split that valued a factor at the wrong moment passed it - the sweep over the library is what closes that, and both wrong splits were tried on purpose to see the guard fire. - [ ] **Verify a post-match file and show it back.** The upload check is written and is the same check either way; what is left is showing it, which wants the attribution beside it - what the file says went wrong, and what it cost. - [x] **Checked against the published rules, not only against MegaMek.** The TechManual's own three worked examples, run through helm term by term: | | the book | helm | |---|---|---| | MON-66 Mongoose | DBR 433.3, OBR 325.08, BV 758 | 433.3, 325.1, 758 | | TDR-10SE Thunderbolt | DBR 954.85, OBR 1052.98, BV 2008 | 954.9, 1053.0, 2008 | | WI-DM DemolitionMech | DBR 328.2 | 328.2 | Armour, structure, gyro, defensive equipment, the explosive penalty, both defensive factors and both speed factors agree. That is the rules, MegaMek and this crate arriving at the same numbers by three routes. Two places the book and MegaMek genuinely differ, where helm follows MegaMek because that is what conformance means: the book gives a cockpit modifier only for the small cockpit, where MegaMek also has torso-mounted, small command console, drone, interface and VRRP; and the book describes a design's *armament* where MegaMek scores everything with a battle value, so a DemolitionMech's wrecking ball and rock cutter count. The one real disagreement is written up in `crates/helm-bv/UPSTREAM.md`. - [ ] **Verification still worth having.** The structural round trip and the generated documents cover the losses found so far. Four more, in the order they would earn their keep: - **Injection into real files.** Put an unknown attribute, element, comment and text run at every position of every real `.mul` and assert each survives. The generator makes plausible documents; this proves the files that actually exist tolerate whatever a later MegaMek adds. - **A named list of what the writer may touch.** A test asserting it only ever modifies `points`, `index`, `isDestroyed`, `isHit`, `shots`, `gunnery`, `piloting`, `hits`, `chassis`, `model`, `type` and `version`. That turns "do not lose data" from a habit into a check. - **Deliberately break the guards.** The performance gate was proved to fire by making something slower on purpose; nothing has proved the losslessness guards fail when they should. - [x] **Construct, write, read.** Every round trip started from a file somebody else wrote. `helm wear ... --write worn.mul` builds a `Mul` in code and writes it, which is the case a player hits when helm hands them a damaged machine to load into MegaMek - and a test wears eleven designs down, writes them out, reads them back and checks each is worth what it was worn to. - [ ] **Generate more forces from MekBay when a reader changes.** is MegaMek's own web client and its force generator writes `.mul` files, so it is the only source of documents this repository did not write. It found the one interop bug nothing in the corpus could: MekBay folds a Clan design's second name into the chassis - `Loki (Hellbringer)` - where MegaMek keeps `chassis:Loki` and `clanname:Hellbringer` apart, so every Clan unit in a MekBay force matched nothing. It also cross-checked three battle values against a third implementation. - [ ] **Two compensating errors in the explosive penalty.** Found chasing a Scarecrow UCU-F4 that is worth 1,472 fresh - agreeing with MegaMek to the point - and 1,472 run dry where MegaMek says 1,475. Its fresh penalty is -2 either side, and the -2 is not the same -2. helm charges two B-Pods, one in each leg, and charges nothing for the machine gun ammunition because CASE II protects it. MegaMek charges the ammunition and nothing for the B-Pods - which is consistent with its own `countAsOffensiveWeapon`, where a B-Pod is excluded by name. Emptying the bin then removes the whole of MegaMek's penalty and none of helm's. Two changes are needed and neither can be made alone: the fresh figure is right by luck, and correcting one side without the other breaks four thousand designs that currently agree. What would settle it is oracle coverage of each separately - a design with B-Pods and no ammunition, and one with CASE II ammunition and no pods. - [~] **Zero unregistered disagreements.** Ten designs of 4,294 still differ, and here they are by name. None is a line unit; all are one-offs, joke designs or IndustrialMeks, which is why they have survived this long - but a disagreement nobody has written down is one nobody can pick up. | design | out by | what it carries | |---|---|---| | Uni ATAE-70 ArtilleryMech | off -43.0 | a Thumper | | Super-Griffin GRF-2N-X | off +41.2 | LRM 10, experimental | | Jabberwocky JAW-67 MilitiaMech | off +30.8 | LRM 5, industrial | | UrbanKnight UM-DKX | off +15.5 | industrial | | Tessen TSN-X4R 'Rapunzel' | off -11.7 | one-off | | Vulpes VLP-1DX 'Beast' | off -6.5 | one-off | | Atlas AS7-00 (Jurn) | def -6.0 | a named character's | | Hybrid Rifleman RFL-3N (Sneede) | def -3.6 | one-off, two variants | | Hercules HRC-LS-9004 | def +1.3 | Ultra AC/10 and an AMS | The ArtilleryMech is settled, and it is not our arithmetic. Its file declares two Thumpers and allocates fifteen critical slots between them, which is one Thumper's worth: the design cannot be built as written. MegaMek scores two and helm reads the criticals and finds one, which is the 43 points exactly. Written up in `crates/helm-bv/UPSTREAM.md`. - [x] **What a recompute costs.** `helm bv-report --bench`, because "can the browser do this as somebody drags a slider" needed an answer before the wasm work is designed around it. The three things a player does are not the same work at all: | what changed | ns per unit | a lance | |---|---|---| | the pilot | 2 | free | | armour or damage | 73,000 | 0.29 ms | | the whole loadout | 216,000 | 0.87 ms | Reseating a pilot recomputes *nothing* - battle value is the design's figure times a number from a 9x9 table - so the interactive case that sounded most expensive is the one that costs nothing at all. Repairing changes the armour but not the loadout, so the expensive half is skipped. Measured in wasm too, running the identical work under Node 24 from a `cdylib` with plain `extern "C"` exports and no bindgen: the arithmetic is **1.02x** native and reading a design is **1.31x**, the difference being string work. So a lance re-scores in about 0.3 ms in a browser and a company in under a millisecond, which is inside a frame either way. Measured again through the finished boundary, from Node against the real module rather than a sketch of one: | what a page does | cost | |---|---| | instantiate the module | 3 ms | | load the shipped catalogue | 88 ms, once | | score a design from its `.mtf` text | 253 us | | re-score a *held* design, damaged | 326 us | | re-crew a *held* design | **1 us** | Holding the design is what makes the last row true: reading the `.mtf` is most of what scoring costs, and a crew only scales a base that has already been worked out. Without a handle the same re-crew was 432us, which is the promise of the native benchmark being thrown away at the boundary. A lance re-crews in 4us and re-scores for damage in 1.3ms. Three things came out of writing it. Asking "is this location exposed" per item rather than per location made the cheapest rating cost more than reading the design; the loadout's mountings were rebuilt on every call when both ratings ask several times each; and `has_flag` walked every mounting's flag list for a question with eight distinct answers. Together those were 3x. - [x] **The rules agree with themselves across the wire.** The reason this crate does no I/O is that the same code runs in a browser while somebody edits a force and again on the server that believes the answer. Nothing checked it: the smoke test crosses the boundary with one design and thirty damaged states, which proves the boundary works rather than that the two builds agree. `helm wire-check` writes what a page would be handed - the catalogue, every design as text, and this build's answer for each - and `crates/helm-wasm/wire.mjs` scores all of them through the boundary and compares. Every design twice, fresh and in one condition that carries plate, frame, a wrecked slot and an empty magazine, since a `Condition` crosses as JSON and is its own chance to disagree. 4,262 designs, 8,524 figures, none of them differing. Eight seconds for the check and forty to build the 39 MB bundle, which is derived and committed nowhere. - [ ] **Retire `bridge/` for battle value.** `helm build` takes it from `helm-bv`; the bridge keeps cost and the Alpha Strike conversion until those are computed too. ## Force generation - [ ] **Budget is a range, not a target.** `{min, max}`. Generating *to* a number produces contorted forces; generating *into a band* does not. Unit count the same. - [ ] **Pilot skill is an input, because it changes battle value.** `BVCalculator.bvSkillMultiplier(gunnery, piloting)` — so "a 5,000 BV force" is ambiguous until the crews' skill is stated. Take gunnery and piloting, with a spread and a maximum delta across the force. - [ ] **Locked units.** Generate *around* units already chosen, so a force can be built up rather than re-rolled. This is the shape an agent wants: pick the command Mek, then fill the lance. - [ ] **No duplicate chassis**, unless asked for. Otherwise a lance is four Atlases. - [ ] **`generate_force` MCP tool**, seeded like `random_unit` and reporting the seed, the budget it landed on and why it stopped. ## Formations - [x] **Formation types as a target shape.** MegaMek carries 41 of them in `FormationType`, from Campaign Operations - Assault, Fire, Recon, Command, Pursuit, Horde and the rest - each with an ideal role, weight bounds and requirements like "at least three units with the Sniper or Missile Boat role". `bridge/DumpFormations.java` reads them off the shelf, `helm_force::Blueprint` holds one and `Blueprint::evaluate` says whether a force is one. The requirements themselves do not cross the bridge: each is a Java lambda with a display label beside it, and only the label survives. So `Rule::parse` reads the label back into a predicate and MegaMek's own per-unit answers are the oracle - 42,453 checks over 477 Meks, and every requirement helm claims to read means what MegaMek means by it. A label it cannot read becomes `Rule::Unreadable`, a force carrying one gets `Verdict::Unknown`, and nothing guesses. Two labels do not describe their requirement and one blueprint cannot be answered at all; all three are in `crates/helm-force/UPSTREAM.md`. - [ ] **The six requirements about equipment.** `Indirect fire weapon`, `Artillery`, `AC weapon`, `AC, SRM, or LRM`, `Probe, ECM, TAG` and `Standard AC, LBX, Artillery weapon, Anti-Air targeting quirk`. The shapes are known - the last is `ACWeapon`, `LBXACWeapon`, `UACWeapon`, `SRMWeapon` or `LRMWeapon` by class, and the quirk is a unit quirk rather than equipment - and `helm_force::Carried` is the vocabulary they need. What is missing is the half that reads them off a design, which wants the catalogue and so belongs in `helm-core`. Until then a `Member` carries `carries: None` and any blueprint asking gets no verdict. - [ ] **`list_formations` and `validate_formation` in the MCP**, and a `helm formations` beside them. The rules are written and measured; what is left is a `formations` table filled by `helm build` and the two calls over it. Validation is worth having before generation: it says whether a force somebody assembled is a legal Fire Lance, which is also the check a generator has to satisfy. ## Faction and era availability - [ ] **Ingest `data/forcegenerator/`.** 41 era files (2398-3160), a `factions.xml` naming 296 factions, and 61 per-faction rule files, about 22MB. Availability is per chassis and per model, as a weight per faction: `MOC:3,HL:2,FS:5,LA:5,DC:5`. Read off the shelf like everything else; nothing committed. - [ ] **Faction and era as filters**, with the availability weight exposed so "common in the Draconis Combine" is answerable rather than just "legal". - [ ] **Faction force structure.** `faction_rules/` is what makes a generated force a *lance* of four, a *star* of five or a Level II of six, instead of a generic pile of N units. ## Beyond Meks - [ ] **Generic art defaults for anything that is not a Mek.** `helm art` leaves 365 designs with no picture — vehicles, infantry, gun emplacements, aerospace. MegaMek always draws something: `genericFor` falls back by motion type and tonnage against thresholds this repo has not read, and inventing them would put a plausible wrong answer in a column meant to be checkable. Read `MekTileset` and finish the stage. - [ ] **Vehicles and infantry.** The MCP is scoped to Meks and the plumbing is general — `unit_type` is an ordinary facet — so this is a widening rather than a rewrite. What it needs is unit-type-specific facets: bays, doors, troop space, squad size. ## Housekeeping - [x] **Auto-detect the MegaMek version** from the install rather than trusting `--megamek-version`. Read from `docs/history.txt`, whose first release line begins with it. The flag still overrides. This mattered sooner than expected: a conformance report blessed against one MegaMek and compared against another is a diff full of changes nobody made. - [ ] **Report the MegaMek defects in `crates/helm-bv/UPSTREAM.md`.** One unit file so far, found by conforming against the bridge. Worth sending upstream rather than only carrying. - [x] **`helm-mcp` has no place to report a stale database.** It has one now: pass the install beside the database - `helm-mcp helm.sqlite `, or `HELM_MEGAMEK` - and a mismatch is warned about on startup and carried in `list_facets` as `stale`, where an agent reading the library will see it. Without the install it says nothing rather than guessing. A database built from one MegaMek and queried about another answers every question and answers them about a library that is not there: designs since added are missing and figures since corrected are the old ones. The version was already recorded in both places; the comparison just had nowhere to be made.