M4 iteration 1: the module engine - #48
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a declaration Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parts + hull metadata (6) and the sim rewiring (7) now precede the Mockingbird carve (8), and shipclass.load survives as a shim until the sim resolves fits. Previously the build was red from task 3 through task 8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows were 10 chars (must be a multiple of 3, one block per tile) and put the helm glyph in the tile centre, where it is not a console -- h is an edge glyph. Caught by the task 3 implementer's test run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ranches (#M4) Addresses the task 3 review: load_all's duplicate-id rejection and three of four validate branches (duplicate slot id, out-of-range digit, non-positive mass) were implemented but unverified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d patch shapes (#M4) Addresses the task 4 review: load_all's catch-all Error(_) skip silently discarded a genuine per-hull directory that failed to read. is_directory now separates a stray file (skip) from a real read failure (propagate). Adds load_all coverage for both registries plus true-ragged, negative-origin and zero-size patch rejection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…test (#M4) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e reason vocabulary (#M4) The eight-of-nine block copy had zero coverage: every existing test still passed against a stamp that wrote only the tile centre. `h` is an EDGE glyph, so the one behaviour the module engine exists to provide was the one behaviour untested. - a_module_supplies_its_own_wall_console_test: a hull with no helm of its own plus a module drawing `h` on a bay tile's east wall; the resolved class has the console, and the same hull with a blank module has none. - the_stamp_writes_edges_in_the_right_orientation_test: an asymmetric patch (door on north only) pins the row/column mapping of copy_tile. - stamp_owns_the_ne_color_and_the_hull_owns_the_sw_slot_test: the NE colour digit is module-owned, the SW slot digit hull-owned. Ratify mount_bad_size / part_bad_size / patch_bad_deck as CONTENT errors rather than folding them into player-facing refusals — a broken data file should not read as a legal refit refusal. resolve now documents the complete reason vocabulary split into refusals and content errors, with one test per content reason pinning the exact string. Also: distinctive cargo fallback (7) so derived-vs-fallback capacity is discriminating; tests for a patch running off the grid edge and for zero_mass; check_tags sorts tags so a multi-deficit message names a deterministic one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The task 5 review found mount_bad_size/part_bad_size/patch_bad_deck emitted but absent from the plan's refit_result list. Ratified rather than folded: they mean a data file is wrong, not that the player asked for something illegal, and disguising a content bug as a refit refusal is worse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ata (#M4) Adds the two Rijay engine parts and the Mockingbird's (and the harness fixture's) hull metadata (mass/provides/requires/mounts/default_loadout) so loadout.resolve has something to resolve ahead of the M4 sim rewiring. The uncarved Mockingbird resolves with zero modules at exactly the pre-M4 flight constants (accel 40.0, turn_rate 180.0); swapping in the stock engine trades turn for thrust (55.0/160.0). Extends ship_class.schema.json (additive only) to describe these fields, which hull.gleam has decoded since Task 5 but no real document had used yet.
Addresses the task 6 review: nothing verified the fixture hull every pytest test spawns from actually resolves at the flight constants test_m1_flight.py depends on. A typo there would have surfaced only as a mystifying harness failure after the sim rewiring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (#M4) Every ship now carries its own `loadout.Fit`, resolved at spawn from the hull/module/part registries the server loads at boot. `sim.State` holds `fits: List(#(Int, loadout.Fit))` keyed by ship id; every consumer of the old world-wide `state.class` looks its ship's fit up through `fit_for` and skips the ship when there is none, so a missing fit can never crash a tick. The docked-ship composite stitches each ship's OWN resolved plan. `ship.step` takes the ship's resolved `shipclass.Flight`; the `main_accel` and `turn_rate` globals are gone. The Task 3 shim (`shipclass.load`, `load_with`, `default_shim_flight`) is deleted — a `ShipClass` is now only ever produced by `loadout.resolve` (via `from_plan`) or decoded from the wire, which makes "a ship is its resolved fit" true rather than aspirational. DH_SHIP_CLASS keeps its name but now means "one extra hull document to load into the registry and spawn from", which is what the pytest harness wants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (#M4) Addresses the task 7 review. Replacing shipclass_test wholesale dropped 17 cases; most were obsolete or covered elsewhere, but five guarded live rules with no other test: unknown cargo.handling, a missing cargo block, and dock_standoff read/default on hull documents, plus both branches of the decoder's own copy of the pallet-derive rule (independent of from_plan's). Also drops sim_test's duplicate of fit.sim_args and corrects handle_trade's missing-fit reason from ship_not_docked to no_fit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#M4) Her hand-authored three-deck interior is now a 96.0-tonne hull plus five authored overlay modules that stamp back exactly what was there before — `mockingbird_test` compares the resolved default fit against the frozen pre-M4 map tile for tile, and 143 of her tiles now come from modules. Slots (SW-corner digit): cockpit, forward_crew, commons, aft_crew on the Upper deck; hold on the Lower. Fixed hull structure keeps no digit: every corridor, both stair columns and their landings, the whole Mezzanine with its two Q docking ports, the Lower bow-ramp band, the master's cabin off the mess and the aft engineering space. Emptying a slot strips its partitions, decor, colour and wall consoles and reduces its shell to plain wall, so the helm exists because the cockpit module draws `h` and the 60-unit hold capacity because the break-bulk module draws 60 `p`. Perimeter edges belonging to HULL tiles are left as authored — canon has the hull own the corridors and guarantee connectivity by authoring, and moving them would move a door. Two alternates ship alongside: passenger staterooms in `forward_crew` and a bunkerage hold that clears the forward pallets, dropping derived capacity to 27. Mass bookkeeping is unchanged where it matters: 96.0 + 24.0 of default modules + a massless Consol patch = the same 120.0, so she still flies at exactly 40.0 / 180.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…of the module test The task 8 review adjudicated the perimeter question: both collision and rendering OR the two facing edges, so the hull's side is a floor on what a module can do, never a ceiling -- and hull-side OPEN is strictly more permissive than a hull-side door, which still forbids a doorless opening. Records why this could not be retrofitted to the Mockingbird and that her forward_crew doors are permanently fixed as a result, plus the consequence that a slot's consoles belong to whichever module draws them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…panicking (#M4) `rebuild_space` asserts on `composite.build`, so a refit that made the station un-stitchable would drop every connected player. The previous justification — that a stamp only writes into the hull's own authored rows, so a refit cannot change the footprint — was wrong: `place_ships` derives a ship's offset from `plan.spawn_deck` / `plan.spawn_tile`, and those are re-derived from the STAMPED map. A module drawing a docking port or an `s` spawn glyph inside its own slot resolves cleanly, relocates the mooring tile and shifts the whole hull relative to its berth. Nothing enforces "no module carries a spawn glyph"; it is a content invariant that happens to hold. `handle_refit` now pre-flights the composite against the prospective fits and routes a build failure through the same `fail` closure as every other refusal, so the verb stays total and atomicity is preserved by construction. `rebuild_space` and the dock/undock paths are untouched. Also corrects the `refit_result` doc block: `no_fit` is an internal condition, not a loadout refusal, so it moves to CONTENT ERRORS; `unknown_hull` carries an id suffix; the "all but the first N verbatim" claim was wrong for `unknown_hull:<id>` too, since `resolve` takes an already-looked-up hull. Tests: a refit that would break the composite is refused and leaves the fit untouched, driven by a testbed hull under `test/fixtures/` rather than a spawn-glyph module in real content. The cargo test now asserts a second refusal after the transfer lands, so both terms of the capacity sum are pinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four end-to-end cases over the real wire: welcome carries flight stats, a refit installs a module and the served deck actually changes (the diff is confined to the patched tile's 3x3 block minus the hull-owned SW corner), refit while flying is refused, and an unknown module is refused. Adds one slot to the harness fixture hull and a zero-mass bunkroom module so the fixture's pinned 40/180 flight behaviour is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t shipped (#M4) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…icking (#M4) `rebuild_space` asserted on `composite.build`, justified by a comment claiming world validation and berth spacing make failure impossible for authored layouts. That justification died with M4: before this branch every ship shared one `ShipClass`, so a station's build outcome was uniform, but a fit is now DURABLE and per-ship. `handle_refit` pre-flights the build against the station the ship is docked at — and that guarantee expires the moment the docked set changes. A hull refitted with the berth line to herself, or flown to another station entirely, reaches `rebuild_space` wearing a footprint nothing ever pre-flighted there, and the assertion drops every connected player. `rebuild_space` splits in two. `try_rebuild_space` returns the composite's own reason; the paths that ADD a mooring — dock and login — take the `Result` and refuse with it (`dock_result` already publishes `berth_blocked`, and a login already refuses by reason). Undock, despawn and the pre-flighted refit commit only ever REMOVE a mooring, so they cannot create a collision and keep the `rebuild_space` wrapper, which holds the old space on the failure that cannot happen to them. The stale comment is replaced by what is actually true. The test drives a fixture station whose two berths sit one tile apart — far tighter than any station in `worlds/` — and a fixture hull whose `shell` slot plates over her fore and aft rows: Bo undocks, Ada refits alone (legal, and the pre-flight passes), and Bo's return is refused with `berth_blocked` rather than taking the actor down. Berth assignment is seed-random, so the fixture is symmetric by construction: the plated hull overlaps her neighbour from either side. A second test pins the sim's `unknown_hull:<id>` on the login path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…re (#M4) `from_plan` and `ship_class_decoder` each spelled out "pallet count if the map draws any, else the authored fallback". Two copies of one rule that must agree: change one and `encode`/`decode` silently stops round-tripping, and since the decoder has no production caller nothing would notice at runtime. Extracted to `effective_capacity`, called by both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`loadout_test` pins reason strings by exact literal so they cannot drift, but seven of the vocabulary published in `protocol.gleam`'s wire block and `docs/modules.md` were never asserted anywhere: `loadout_wrong_hull`, `module_wrong_slot`, `mount_not_on_hull`, `unknown_part`, `duplicate_mount`, `mount_wrong_kind` and `invalid_hull_plan`. Each gets a test in the existing style; the wrong-kind case uses a part small enough to fit the mount, so it cannot pass as `mount_too_small` by accident, and the wrong-hull-plan case proves the AUTHORED rows are what failed to parse, not the stamped ones. Also makes `every_shipped_module_resolves_in_its_slot` non-vacuous: it builds the swapped loadout with `list.map`, which only REPLACES, so a module whose slot has no default entry would resolve the default fit and pass without ever being installed. All five Mockingbird slots have defaults today; the assertion says so out loud instead of relying on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`decode.float` rejects `4` and accepts only `4.0`, and the schemas cannot make up the difference: they type these fields as `number`, and JSON Schema draft-06 defines `integer` as any number with a zero fractional part, so `4.0` is an integer to a validator and "float-spelled only" is not expressible. The schemas' "must be written with a decimal point" warnings were promising an enforcement that does not exist, so the decoders meet the schemas instead. `hull.number_decoder` takes either spelling and is shared by hull, module and part — the same way `tags_decoder` already is — and the schema descriptions now say what is true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…one (#M4) The schema validates `server/shipclasses/*.json`, which since M4 are HULL documents decoded by `hull.gleam` — but its title said "Ship class document" and its description pointed authors at `shipclass.gleam`, whose decoder requires a `flight` block a hull document must never carry. Renamed to `hull.schema.json`, with the title, `$id` and description naming `hull_decoder` and spelling out how a hull differs from the resolved class the bake produces. The `slot` definition now says that digit and id uniqueness is enforced by `hull.validate` rather than by the schema, since draft-06 cannot express uniqueness across array items. `data_schema_test`'s constants and test names follow. Its handling-value case was asserting on an M2-era document that `additionalProperties` would have rejected anyway; it now uses a minimal hull document and a passing control, so the refusal is about `handling`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…acity (#M4) Two small things that misled a reader. `deckplan-format.md` claimed "any future hold module must redraw that `c`, or the ship will not resolve". False: `shipclass.validate` requires only a HELM console. A hold module that forgets its `c` resolves fine and simply leaves the crew with nowhere to work cargo, which is what `docs/modules.md` already says. `mockingbird.json` authored `cargo.capacity: 40` while her default fit resolves to 60 from pallet glyphs. The field is now a fallback that only fires for a hull whose plan draws no pallets, so 40 was a number no code path would ever read and every reader would take for the truth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…solved An engine with mass 0.0 was the one part on the ship where mass demonstrably did not matter -- and it is the part the design uses to explain that mass matters. The Consol patch now masses 8.0 and the heavier Rijay original 12.0, so the swap trades weight as well as thrust. Neither the old 120.0 total nor the old 40.0/180.0 feel was a tuned number; both were back-solved from ship.gleam's pre-M4 constants. So nothing is back-solved now: masses say what each thing is, engines say what they push with, and accel = thrust/mass falls out (39.06 u/s^2 and 171.9 deg/s on the default fit, 53.03 and 151.5 with the original). Retuning is four numbers in two JSON files. Flight assertions move to a tolerance -- comparing a derived quotient with float equality only worked because the old divisors were powers of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Slot membership hides in each tile's SW corner, which is unreadable in the raw JSON. This paints the digit into each tile so the modular regions read against the ship's own walls and doors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Iteration 1 of M4: the module engine, whole. A ship's interior is now a hull plus swappable modules, with exterior parts on hull mount points — and the Mockingbird's current deck is expressed as her default loadout, unchanged tile for tile.
Design:
docs/modules.md. Plan:docs/superpowers/plans/2026-07-24-m4-module-engine.md.The central idea
The bake works on text, not parsed cells. Resolving a loadout splices each module's authored character-blocks into the hull's authored rows and re-runs the ordinary deck-plan parse. That is what keeps "the map is the single source of truth" true after a refit: consoles, the mooring tile, docking ports and hold capacity all keep falling out of glyphs with no derivation logic of the bake's own. Install a cockpit and the helm console exists because the module drew the glyph.
loadout.resolveis 38 lines with no map logic in it.What landed
hull.gleam/module.gleam/part.gleam— the authored documents and their registries. A hull keeps its deck rows as raw text so a refit re-stamps from the authored map rather than trying to un-stamp the previous fit.loadout.gleam— the overlay stamp (void= passthrough), the pooled-tag validator (sum(provides) >= sum(requires), plus one module per slot, every non-void cell on that slot's digit, mount kind/size), and the bake. No reachability or geometry analysis anywhere.ShipClassis gone; all 17 formerstate.classsites take their own ship's fit.refitverb — docked-only and free this iteration, withrefit_resultandship_fitpushes and a station composite rebuild.docs/modules.mdandDESIGN.mdbrought in line with what shipped.Testing
327 Gleam tests, 29 harness tests. The ones worth knowing about:
default_loadout_reproduces_the_authored_deck_testcompares whole parsedDeckPlans against a frozen pre-M4 map. The frozen fixture's blob hash is the pre-carve hull's, so there is no hand-touched reference.a_module_supplies_its_own_wall_console_testuses an edge console glyph — the version of the test that fails if the stamp only writes tile centres.Two things to look at
The slot perimeter rule. A slot's perimeter is a double edge shared with hull structure, and collision and rendering both OR the two facing edges — so the hull's side is a floor on what a module can do, never a ceiling. The rule is: author the hull side open and draw the perimeter's walls and doors in the module. The Mockingbird could not follow it, because blanking a corridor's slot-facing wall changes that hull tile's own authored edge and fails the golden test. Consequence: her corridor-side walls are permanent and her
forward_crewdoors are fixed at the two positions the corridor already opens. Written up indocs/deckplan-format.md; the Sparrow and Finch should be authored to the rule instead.Reason strings come in two groups.
loadout.resolvereturns loadout refusals (the player asked for an illegal fit) and content errors (a data file is wrong). Keeping them distinct matters: foldingmount_bad_sizeintomount_too_smallwould tell a player "that engine is too big for that mount" when the truth is someone typed"size": "xl"in a JSON file.Follow-ups (not blocking)
rebuild_space's panic path is now fallible for dock and refit, but undock/despawn still keep the old space on failure — fine, since they only remove moorings.shipclass.decodehas no production caller; retained as the encode/decode contract that pins thewelcome/ship_fitwire shape.server/modules/test_fixture/ships in the content tree becausemodule_dirhas no env override — wantsDH_MODULE_DIR.mass: 0.0, back-solved so her default fit reproduces the pre-M4 constants. Worth a rebalance in iteration 2 — the one part where mass demonstrably doesn't matter is the engine, which is the part the design uses to explain that mass matters.docs/M3.1-RESULTS.mdstill callsberth_blockedunreachable.Not in this iteration
Iteration 2: the Sparrow and Finch hulls, hull mount geometry, standalone exterior part sprite exports, client-side sprite layering, the engineering-bay module.
Iteration 3: the refit loop — shipyard stations, a refit console you walk to, per-station catalogs and prices, charging the wallet, the Godot refit UI.
🤖 Generated with Claude Code