From 112dd2453614baec4e2b6cc1b77df460c32a22a4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 18 Oct 2026 18:20:07 +0200 Subject: [PATCH] aetheria-release map: boss hull derived r4 (fixed Loadouts), stand-in hulls (SI) probe and cut sequence Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-release-map.md | 445 ++++++++++++++++++++++++++++------- 1 file changed, 360 insertions(+), 85 deletions(-) diff --git a/docs/aetheria-release-map.md b/docs/aetheria-release-map.md index b3a9f1c42..54699054e 100644 --- a/docs/aetheria-release-map.md +++ b/docs/aetheria-release-map.md @@ -10786,110 +10786,192 @@ worktree C:\aeth-wt\ag6-quality-readers at 70c7a497. Catalog: GameData/Aetheria. ## Boss hull derived -Probes and source reads by imagination-ag6-boss-hull (session self-2026-10-18-ag6), 2026-10-18, -in the detached worktree `C:\aeth-wt\ag6-boss-hull` at `origin/master` `eb27ced1`. `origin/master` -moved to `ac835a8a` during the pass. That commit adds only -`docs/research/2026-10-18-agent-built-hull-art-prior-art.md`, so every anchor below is the same -at both. Read by `cut-boss-hull-derived.r3`. This section supersedes the anchors of "Demo hulls, -the Pirates and the boss" H7 (`@2a5fc00b`) and of Products PL6. Its design rationale -("The boss hull is derived, not named") stands, with the changes recorded under Rationale below. +r3's probes and source reads were made by imagination-ag6-boss-hull (session self-2026-10-18-ag6), +2026-10-18, in the detached worktree `C:\aeth-wt\ag6-boss-hull` at `origin/master` `eb27ced1` +(BH1-BH11). r4's probes were made by imagination-ag6-boss-hull-r4 in the same worktree at the +branch head `095baf2f`, which is r3's h1 (BH12-BH17). Read by `cut-boss-hull-derived.r4`. This +section supersedes the anchors of "Demo hulls, the Pirates and the boss" H7 (`@2a5fc00b`) and of +Products PL6. Under Rationale below, r4 replaces r3's boss-loadout path. Ruling +`boss-gear-is-a-loadout` revokes that path; it replaced `boss-gear-is-fixed`. ### Body facts - **BH1. What the cut waits on is on master.** boss-gate is merged at `39c7508e` (cut-boss-gate r2, verdict s3). faction-play-1 is merged at `d7f30ed1` (r3, h6), and its catalog - replay is at `aa12cbac`. minor-power-factions is merged at `c486b06d`; it is the remap of - maker-only-faction. pirates-record is merged at `2c065937` and package-product at `3535660c`. - No cut that edits Faction or the main galaxy's faction draw is still in flight ahead of this - one. + replay is at `aa12cbac`. minor-power-factions is merged at `c486b06d`. pirates-record is merged + at `2c065937` and package-product at `3535660c`. - **BH2. Faction keys at `eb27ced1`** (`Corporations.cs`): 1-5, 7, 8, a retired-key comment for 9 - (`:39`), `BossHull` Key(10) at `:42-43`, then 11-15, 17 (Doctrine), 19 (Assembly) and 21 (Places). -- **BH3. HullData keys** (`ItemData.cs:531-556`): 23, 24, 25, retired 26 (`:542`), 27, 28, 29 and - 32 (`Visual`, `:555-556`). Key 33 is free on HullData. Fuse's Key(32) at `:526` is on a sibling - type. -- **BH4. Every BossHull reader at `eb27ced1`** (`git grep BossHull`): - `Galaxy.cs:293` (`PlaceFactionsMain` picks boss factions by `BossHull.IsSet()`, `:291-295`); - `LoadoutGenerator.cs:212-230` (`GenerateBossLoadout`); - `tools/AetherDb/Program.cs:586` (dangling comment) and `:709-712` (the factions boss column and - its `broken` count, the method's only use of `broken`, `:700-716`); - `RunGoalTests.cs:74-115` (two tests) and `:199-202` (Exit_open_only_in_the_exit_zone sets one). - No writer exists: no AetherDb command, no Pirates or minor-power command, no package path. - `GameData/Aetheria.cc` holds the string `bossHull`, which is its schema table. + (`:39`), `BossHull` Key(10) at `:42-43`, then 11-15, 17 (Doctrine), 19 (Assembly) and 21 + (Places). h1 deleted `BossHull` and retired key 10. +- **BH3. HullData keys** (`ItemData.cs:531-556` at `eb27ced1`): 23, 24, 25, retired 26, 27, 28, + 29 and 32 (`Visual`). h1 added `Boss` at Key(33) (`ItemData.cs:557-560` at `095baf2f`). +- **BH4. Every BossHull reader at `eb27ced1`** was `Galaxy.cs:293`, + `LoadoutGenerator.cs:212-230`, `tools/AetherDb/Program.cs:586` and `:709-712`, and + `RunGoalTests.cs:74-115` and `:199-202`. h1 removed every one. - **BH5. Retiring a key with stored values needs no catalog rewrite.** Precedents: `1d689adf` - retired Faction key 9 and `be7f2b07` retired HullData key 26, and neither touched - `GameData/Aetheria.cc`. The suite loads the shipped catalog in `RunStartTests`' constructor. - This is a precedent read; no decode was run in this pass. -- **BH6. Every generation hull draw goes through one availability rule.** `RandomHull` - (`LoadoutGenerator.cs:165-170`) calls `RandomProduct`, then `RandomProducts` (`:176-185`), then - `AvailableProducts` (`:188-210`). Station stock (`:126-127`) and the boss fallback (`:226`) also - go through `AvailableProducts`. Its `candidates` filter (`:191-198`) runs before the - any-manufacturer fallback (`:201-206`). An exclusion placed there therefore holds on the - fallback too. `ChooseProduct` (`:262-273`) has its own `Where` and serves presets that name a - design. -- **BH7. Explicit hull draws.** `ScenarioStage.Generated` (`ScenarioStage.cs:90-97`) and AetherDb - `loadout` (`Program.cs:805-818`, which runs every hull through `GenerateShipLoadout(candidate - => candidate == hull)`) both reach `AvailableProducts`. Many tests call - `GenerateShipLoadout(h => h == hull)` on shipped hulls (for example `RestoredHullsTests.cs:606`). - None of those hulls is a Boss hull. -- **BH8. The boss path.** `ZoneGenerator.cs:398-412` calls `GenerateBossLoadout` for every - `BossZones` entry in the zone and marks the result `Boss`. `Galaxy.cs:150` sets the prelude and - demo antagonist's boss zone without reading any hull. `Galaxy` holds `_cache` (`:36`, set at - `:51`, `:102` and `:169`), so `PlaceFactionsMain` can call a static derivation over the catalog. -- **BH9. The package writer.** `ShipAuthoringCommands.cs` (157 lines): `Usage` at `:11`, `inspect` - at `:29-35`, `price` at `:79-93` (it judges before it writes, and a refusal leaves the file - byte-identical), and `product` at `:94-125`. `ShipProductTests.ProductAndPriceCommands` - (`:259-300`) pins the price and product commands with `RefusedAndUntouched`. Compose copies a - package's HullData verbatim, so a member set in the package reaches the composed catalog. - `ComposeCopiesEveryProductAndEachMakerIsOfferedTheHull` (`:80-100`) byte-compares products. -- **BH10. Zhestokost makes no ship hull.** Products PL6 still holds: its only hull product is the - Turret. Until a Zhestokost Boss hull exists, the demo boss is the logged fallback (the priciest - ordinary ship hull Zhestokost can be offered). -- **BH11. Which hulls can be bosses.** Ruling `hull-programmer-art-stopgap` (2026-10-18) leaves - open whether boss hulls come from the operator's packages or an agent-built stopgap. The Eyes - facts file at `ac835a8a` places stopgap art on the package path. Nothing in the derivation - reads `Prefab`, `Visual` or a model, so a hull from either source, or with neither, can be a - boss. + (Faction key 9) and `be7f2b07` (HullData key 26). +- **BH6. Every generation hull draw goes through one availability rule**, `AvailableProducts` + (`LoadoutGenerator.cs:196-219` at `095baf2f`). Its candidate filter carries h1's + `!(entry.design is HullData { Boss: true })` (`:204`) ahead of the any-manufacturer fallback. + `ChooseProduct` (`:278-289`) has its own `Where` and does not exclude Boss hulls. r4 never asks + it for a hull, because `BossOf` returns only a Loadout whose `HullProduct` is set. +- **BH7. Explicit hull draws.** `ScenarioStage.Generated` (`ScenarioStage.cs:93`) and AetherDb + `loadout` reach `AvailableProducts`. Many tests call `GenerateShipLoadout(h => h == hull)` on + shipped hulls, and none of those hulls is a Boss hull. +- **BH8. The boss path at `095baf2f`.** `ZoneGenerator.cs:401-411` calls + `GetLoadoutGenerator(bossFaction).GenerateBossLoadout()` for every `BossZones` entry in the zone. + It logs "No boss could be generated" on null and otherwise marks the pack `Boss` and the zone + `BossSpawned`. `:414-415` logs exit-without-boss. `GetLoadoutGenerator` (`:282-287`) caches one + generator per faction over the zone's `random`. `GenerateBossLoadout` (`LoadoutGenerator.cs:221-233`) + builds `BossHullsOf[0]` through `ShipLoadout(product)` (`:55-66`: instance, Ship, + `OutfitEntity`, `EquipPickup`, `StockConsumables`, Pack). Otherwise it logs "makes no boss + hull" and generates the priciest ship hull on offer. Its readers are only `ZoneGenerator.cs:403` + and AetherDb `loadout` (`Program.cs:815`). +- **BH9. The package writer.** `ShipAuthoringCommands.cs`: `Usage` at `:11`; h1 added `boss + on|off`. Compose copies a package's HullData verbatim. + `ShipProductTests.ABossPackageIsItsMakersBossAfterCompose` passes at `095baf2f`. +- **BH10. Zhestokost makes no ship hull, and the shipped catalog has no Boss hull.** Products PL6 + still holds. Today's demo boss is the generated fallback: the priciest ordinary ship hull + Zhestokost can be offered. Under r4 there is no fallback, so the demo exit has no boss until a + Zhestokost Boss hull and its Loadout exist (question `demo-boss-before-its-hull`). +- **BH11. Which hulls can be bosses.** Ruling `hull-programmer-art-reads-as-stand-in` (it + supersedes `hull-programmer-art-stopgap`) leaves the stopgap mechanism open. The Eyes facts file + at `ac835a8a` places stopgap art on the package path. Nothing in either derivation reads + `Prefab`, `Visual` or a model. +- **BH12. The three failures in h1 are a fixture defect.** Probe p1 ran ygg-verify at `095baf2f` + with the filter of the three tests and the detailed console logger. All three throw + `InvalidOperationException: Sequence contains no matching element` at + `RunStartTests.FactionNamed` (`RunGoalTests.cs:75`), called with `"Lucent"` at `:130`, `:149` + and `:185`. The shipped faction is `"Lucent Media"`, and `"Lucent"` is its ShortName + (`DemoTerminusTests.cs:23`, `CatalogRoleTests.cs:368`). Probes p2 and p3 used a scratch clone + with only that string changed (`f683baa4`). All three passed: `Passed + A_boss_hull_two_makers_sell_is_a_boss_of_each`, `Passed Boss_hulls_are_never_stock_or_traffic` + and `Passed Another_factions_boss_hull_is_never_used`. The h1 job hid the messages because its + grep kept only `Assert|Expected|Actual` lines. Verdicts: + - `Boss_hulls_are_never_stock_or_traffic` is a real fixture defect. Its rule (ruling + `boss-hulls-exclusive`) holds once the name is fixed, and the test survives unchanged + otherwise. + - `Another_factions_boss_hull_is_never_used` is a real fixture defect. Its fallback assertions + die with the revoked path. Its rule, that another maker's Boss hull is not this faction's, + survives as a `NoBossHull` refusal. + - `A_boss_hull_two_makers_sell_is_a_boss_of_each` is a real fixture defect. Its `BossHullsOf` + rule survives. Its last assertion (the boss flies "Shared Boss") becomes "flies its Loadout". +- **BH13. The Loadout record already carries the key from a boss hull to its gear.** `Loadout` + (`Loadout.cs:14-22`, schema `aetheria.loadout` v1) has `Name` (Key 0), `Hull` (Key 1), + `Slots` (Key 2: `Position`, `Rotation`, `Design`, optional `Product`), `WeaponGroups` (Key 3) + and `HullProduct` (Key 4, optional). `HullProduct` names the product, and therefore the maker, + of the hull. So "the Loadout whose `HullProduct` is the faction's boss hull product" needs no new + field on `Loadout`, `Faction`, `FactionProductData` or `HullData`. It also tells apart two makers + of one shared Boss hull. +- **BH14. `Loadouts.Materialize` is already the builder a fixed boss needs.** It is all or + nothing, pins a set product as named, and builds a design-only slot through the passed chooser. + Its doc says a game spawner passes `LoadoutGenerator.ChooseProduct`. It builds a Ship from a ship + hull with the given zone, null included, as `ShipLoadout` did, and pads to `WeaponGroupCount`. + It adds no pickup, consumables or outfit. Its callers today are `ScenarioStage.cs:179` (fits) + and the capture round trip in tests. +- **BH15. A catalog Loadout cannot name a package hull.** `Loadouts.Commit` (`Loadout.cs:78-98`) + is the only writer of Loadout records, through the in-game capture command + (`ActionGameManager.cs:643`). It refuses any loadout whose hull or slot design is a mod key. + Package hulls and products are keyed `mod-hull:`, `mod-product:` and `mod-ship:` + (`ShipModCatalog.cs:23-30`), and "the shipped catalog ... never reference[s] one". A package + carries only its HullData, its ShipAuthoring and its products (`ShipModCatalog.Package`, + `:13-21`). So a boss Loadout for a package Boss hull has to ride in the package. That is the + next cut line below, not r4. +- **BH16. Boss-dependent tests.** `RunGoalTests.cs` holds 18 tests. Those that read the demo's + fallback boss are `Boss_spawns_once_in_boss_zone`, `Boss_spawns_in_a_zone_that_is_not_ambient`, + `Exit_sealed_until_boss_dies`, `Boss_mark_survives_continue` and + `Exit_open_after_save_of_a_slain_boss`. `Exit_sealed_when_no_ship_hull_is_on_offer` pins the + fallback's null branch. No other test file references `IsBoss`, `BossSpawned` or `ExitOpen`. +- **BH17. CultCache variants are not available to Aetheria yet.** CultLib `src` has variant + records (`CultCache.cs:59-60`, `CultVariantDelta`, "a variant record (slot 4): its base and + overrides"). Aetheria pins CultLib `45c2f4006f5f` (`Directory.Build.props:6`), and + `git grep CultVariantDelta 45c2f40` finds nothing. An interim Boss hull copied from a shipped + hull would be a plain copy until the pin moves. "Variant" is that record kind, never a loadout + (ruling `boss-gear-is-a-loadout`). -### Rationale +### Rationale (r4) -- **One owner of which hull is a faction's boss:** `LoadoutGenerator.BossHullsOf(CultCache, - Faction)`. It returns the faction's products whose design is a `HullType.Ship` HullData with - `Boss` set, ordered by hull Price descending, then hull Name ordinal. The flag is authored on - the hull, and the faction comes from the product's maker. Nothing stores the pairing. - `GenerateBossLoadout`, `Galaxy.PlaceFactionsMain` and AetherDb `factions` all call it. -- **One owner of "a Boss hull is never on offer":** `AvailableProducts`' candidate filter (BH6). - r2 put the exclusion on `RandomHull` and the stock filter, and gave `RandomHull` a boss bool. - That made two sites and one mode flag, and it missed the fallback's own - `AvailableProducts` call. r3 uses one clause instead. The boss is built straight from its - product through a new `ShipLoadout(FactionProductData)` builder, which is the body of - `GenerateShipLoadout` after its draw. So the boss path never asks the availability rule to - make an exception. -- **The "named but unsold" branch goes.** It existed because BossHull named a design. A - derivation that starts from products cannot name an unsold hull, so its log line and its test - (`Boss_hull_nobody_sells_falls_back`) are deleted. They are not rewritten. -- **A boss is not a market draw.** `ShipLoadout` from the boss product does not apply - `AvailableProducts`' `Price > 0` rule. A price-0 Boss hull is still a boss. -- **Kept:** a sold Boss hull's hardpoints still count in `SoldHulls`, so gear only it can mount has - a home. That gear is loot. `ScenarioStage.Generated("")` fails like any unavailable - hull. `ChooseProduct` is untouched. -- **Prior art (from memory, medium confidence; no Eyes pass this round):** Starsector tags unique - and boss hulls so markets never sell them. They are spawned by script, outside a faction's - known-ships draw. EVE's officer spawns fly hulls that no market sells. Both match the exclusive - reading. Question `boss-hull-exclusive` asks the operator to confirm it. +- **One owner of what a boss flies:** `Loadouts.BossOf(CultCache, Faction, out BossRefusal)`, in + `Loadout.cs` beside the other owners of Loadout records. It takes `BossHullsOf(items, faction)[0]` + and returns the Loadout whose `HullProduct` key is that product's key, first by Name ordinal. + When there is none, it returns null with `NoBossHull` or `NoLoadout`. It reads no `Random`, no + model and no availability rule. `ZoneGenerator`'s boss block, `Galaxy.PlaceFactionsMain` and + AetherDb `factions` all call it. `LoadoutGenerator.BossHullsOf` stays the one owner of which hull + is a boss. +- **Keyed on the product, not the hull** (BH13). A Boss hull two makers sell is a boss of each, + and each maker's boss is the Loadout on its own product. No field on Faction or the product + points at a Loadout, so no second record can disagree with the Loadout. +- **No generator on the boss path.** `GenerateBossLoadout`, its priciest-hull fallback and + `ShipLoadout(product)` are deleted. `GenerateShipLoadout` gets back its single body, because the + boss was `ShipLoadout`'s only other caller. The boss is built by `Loadouts.Materialize`, the one + builder from a Loadout (BH14). It carries exactly its cells: no tractor pickup, no consumables + and no outfit pass. +- **The chooser is not a draw of gear.** Materialize needs a chooser only for a design-only slot. + Ruling `loadout-product-optional-context-chooses` says that slot's product is chosen by context, + and `ChooseProduct` is that rule's owner. It never picks a design, a cell or the hull. The + negative check forbids every generator draw on the boss path (`GenerateShipLoadout`, + `RandomHull`, `RandomProduct(s)`, `OutfitEntity`, `EquipPickup`, `StockConsumables`). An agent + who wants the brand fixed pins the slot's product. +- **A typed refusal, never a fallback.** `BossRefusal { None, NoBossHull, NoLoadout }`. + `Loadouts.Describe` names the missing field (`HullData.Boss`, `Loadout.HullProduct`) and echoes + no product name or record key. The zone logs ` has no boss: it .`, spawns + nothing and leaves `BossSpawned` false, so the exit gate fails closed through boss-gate's + existing rule. A Loadout that does not fit is logged with Materialize's failures and also spawns + nothing. +- **Galaxy picks factions that have a boss to fly.** In the main galaxy, a faction with a Boss + hull but no Loadout gets no boss zone. That keeps a placed boss zone from being empty by + construction. +- **Authoring** (ruling `boss-loadouts-agent-authored`). AetherDb `loadout-commit [,,,[@]]... [--group i,j]... [--replace]` writes one + Loadout through `Loadouts.Commit`, which stays the only writer. The command first resolves every + name exactly and test-materializes the Loadout, so a typo or a misfit refuses before anything is + written. The operator adjusts the record in Studio afterwards. The command is argument-driven, + not a coded table like `product-lines tiers`. A coded table replayed at merges would fight her + Studio edits. The record in the catalog is the authority, and the commit message records the + command line for a merge replay. +- **Vocabulary.** `Loadout.cs` says "Loadout" throughout. "Preset" and "variant" go from its + comments and messages. The wider sweep (the `capturepreset` console command, `ScenarioStage`, + `Provenance`, `LoadoutGenerator` and `ActionGameManager` comments) is follow-up + `loadout-vocabulary-sweep`. +- **Kept from r3 (h1, landed):** Faction.BossHull is deleted with key 10 retired, `HullData.Boss` + Key(33) stays, `BossHullsOf` stays, the exclusive clause in `AvailableProducts` stays (ruling + `boss-hulls-exclusive`), and `ship-authoring boss`, the compose test and the no-authored-model + test stay (ruling `hull-programmer-art-reads-as-stand-in`). +- **Revoked from r3:** `GenerateBossLoadout` through `BossHullsOf` and `ShipLoadout(product)`, the + AetherDb `loadout` boss branch, `Boss_without_a_boss_hull_falls_back`, and the "makes no boss + hull" log. +- **Prior art (from memory, medium confidence; no Eyes pass this round):** Starsector's unique + bosses (for example the Ziggurat and the Omega flagships) fly hand-authored variant files + attached to a scripted fleet, never the faction's autofit. EVE's officer and faction-commander + spawns have fixed fittings per NPC type. Both match a fixed record keyed to the boss, with no + generator. ### Model page rows -Replace the two rows in "Demo scope, Model page rows" (`Boss hull`, `Faction.BossHull`) with: +Replace the rows in "Demo scope, Model page rows" (`Boss hull`, `Faction.BossHull`), and add the +boss Loadout row: | Kind | Identity | Lifecycle | Authority | |---|---|---|---| -| Boss hull | `HullData.Boss` (key 33, bool) on any HullData, whether shipped, package, or agent-built stopgap | Set by `ship-authoring boss on\|off` on packages, or in Studio's inspector on shipped hulls. Derived per generation; stored nowhere | The hull's author sets the flag. `LoadoutGenerator.BossHullsOf` derives the faction from the product's maker | +| Boss hull | `HullData.Boss` (key 33, bool) on any HullData: shipped, package, or agent-built stopgap | Set by `ship-authoring boss on\|off` on packages, or in Studio's inspector on catalog hulls. Derived per generation; stored nowhere | The hull's author sets the flag. `LoadoutGenerator.BossHullsOf` derives the faction from the product's maker | +| Boss Loadout | A `Loadout` record (`aetheria.loadout` v1) whose `HullProduct` is a faction's Boss hull product, at `Loadouts.KeyOf(name)` | Written by AetherDb `loadout-commit` (agents) or the in-game capture, both through `Loadouts.Commit`. Studio edits after that. Read at zone generation by `Loadouts.BossOf` | `Loadouts.BossOf` alone decides what a boss flies, and `Loadouts.Materialize` builds it. No generator draw | | `Faction.BossHull` (key 10) | Retired; never reuse | Deleted by `boss-hull-derived`. The shipped records keep `''` in the slot (BH5) | none | ### Cut sequence -Remove the `boss-hull-derived` line. Change the `demo-cast-hulls` line's "boss-hull-derived" in -"Waits on" to "boss-hull-derived (spec cut-boss-hull-derived.r3)" until it merges. +- `boss-hull-derived` (spec `cut-boss-hull-derived.r4`, branch at `095baf2f`). Its merge waits on + question `demo-boss-before-its-hull`. +- If `interim-catalog-boss` is chosen: `zhestokost-interim-boss`, a sibling content cut that lands + with r4. Through AetherDb it writes a catalog Boss hull copied from the hull today's demo boss + flies (BH10), plus its Zhestokost product and its Loadout (`loadout-commit`). It is replayed at + merges and replaced when the real Zhestokost Boss hull lands. +- `package-boss-loadout`: a package carries its boss Loadout, as it carries its product (ruling + `package-carries-product`, BH15). `ship.cc` gains Loadout records, Compose writes them under a + `mod-loadout:` key that `IsModKey` knows, and `ship-authoring loadout` writes them. It is needed + before the first package Boss hull can be a boss. `BossOf` already matches it unchanged, by + `HullProduct` key in the composed cache. +- `demo-cast-hulls`'s "Waits on" reads "boss-hull-derived (spec cut-boss-hull-derived.r4)" until it + merges. ## Product tiers @@ -11186,3 +11268,196 @@ with the wider scope taken when the FusedRounds premise is fixed. **Old saves.** A pre-cut save reads `Seed == 0` and continues with name-only drops. Under ruling `no-save-compatibility-before-players` the cut may refuse it, but a refusal gate would be code written for nobody. + +## Stand-in hulls (SI) + +Imagination pass `imagination-ag6-hull-art`, session `self-2026-10-18-ag6`, 2026-10-18. Pinned to Aetheria +`origin/master` 6d10331c (worktree `C:\aeth-wt\ag6-hull-art`). Serves ruling +`hull-programmer-art-reads-as-stand-in`, which supersedes `hull-programmer-art-stopgap`. +Research input: `docs/research/2026-10-18-agent-built-hull-art-prior-art.md`. That file surveys making agent-built +art look good or deliberate. This ruling asks for the opposite, so the research's §B3 (low poly that reads as +deliberate) is evidence about what to avoid. The greybox "dev texture" convention below comes from general practice, +not from that file (one of the gaps listed at the end). + +Scratch root `S` = `C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\1b477360-19e3-41e2-bde4-8932ace0b8fd\scratchpad\imagination-ag6-hull-art`. + +### Renders for the operator + +- `S\probe\out\longinus_top_standin_vs_existing.png`: top view, stand-in (left) and the shipped `Longinus.fbx` (right), each at its own scale. +- `S\probe\out\longinus_iso_standin_vs_existing.png`: three-quarter view, same pair. +- `S\probe\out\longinus_combat_zoom_proxy.png`: both top views at ship lengths of 48, 80 and 128 px. +- `S\probe\out\longinus_top_same_metres.png`: both in one 40 m frame. The stand-in uses 2 m cells and the shipped mesh about 1 m per cell (SI-B5). + +All four come from one scratch rasterizer (`S\probe\render.py`) with the same camera and light. The stand-in side is +read back from the `ship.glb` the package path accepted, so it is not a separate preview. + +### What the game reads (body facts) + +- **SI-B1. Package shape.** A mod hull is `GameData/Mods//ship.cc` plus `ship.glb`. The `.cc` holds `HullData` + (`mod-hull:`) and `ShipAuthoring` (`mod-ship:`; `ModelAsset`, `Anchors`, `SchematicLines`, `Joints`). + Source: `ShipAuthoring.cs:12-21`, `docs/moddable-ship-authoring.md:160-200`. +- **SI-B2. Anchors the game needs.** Exactly one each of `map-icon`, `hull-collider`, `shield` and `tractor` + (`ShipAuthoring.cs:288-290`). Each visible hardpoint has a mount anchor whose id is its mount id: `thruster-emitter`, + `radiator-mesh` or `weapon-mount`. Each weapon needs at least one `weapon-muzzle` whose ParentId is the mount + (`:325-356`). These nodes must carry a mesh: thruster-emitter, radiator-mesh, map-icon and hull-collider. The + shield node needs three positive scale radii (`ShipModCatalog.cs:256-262`). Weapon mounts and the tractor may be + empties. Internal hardpoints have no anchor. Every hardpoint needs a non-empty mount id. +- **SI-B3. Frame.** Blender Ship Root has the nose toward -Y, starboard toward -X and Z up. Cell (x, y) is centred at + `origin - 2(x, y)`, schematic +x is starboard and +y is forward, so row 0 is the stern + (`tools/blender/aetheria_ships/hull_grid.py:3-20`). Package exports with `export_yup` (`__init__.py:791`), so the + glTF nose is +Z. Hardpoint `Rotation` gives the facing: 0 nose, 1 port, 2 aft, 3 starboard + (`hull_grid.py:141-145`). A footprint shape is stored as placed, and validation reads it at `Position` without + rotating it (`ShipAuthoring.cs:310-320`). +- **SI-B4. Longinus HullData** (decoded from `GameData/Aetheria.cc` with cultcache-py, `S\grid.py`). It is 6 x 17 + cells, symmetric, with 13 hardpoints. Visible: Th.L and Th.R (2x2, rows 0-1, aft), Th.CW and Th.CCW (1x2, row 14, + port and starboard), En.L and En.R (1x2, row 8), La.L and La.R (1x3, flanks, rows 5-7), Ra.L and Ra.R (1x2, + row 2). Internal: Reactor, ControlModule, and Sensors, which has a **null mount id**. Because of that null id, a + package copy of the shipped Longinus is refused until the Sensors hardpoint is given an id (`ship_cc.replace_layout` + raised "needs 7 typed fields and a stable mount ID"). The probe gave it the id `Sensors`. +- **SI-B5. Scale.** The add-on authors at 2 m per cell (`hull_grid.py:11`, "Settings.SchematicCellSize"; New Ship + sizes "this many 2 m cells"). The shipped `Longinus.fbx` body is 5.2 x 16.8 m for 6 x 17 cells, about 1 m per cell + (`S\fbx.py`, mesh `Cube.043`). Ruling `schematic-and-model-are-separate-spaces` already allows the two to differ. + The stand-in uses the add-on's 2 m. +- **SI-B6. Materials today.** `ShipModVisual` imports the GLB with glTFast and keeps its glTF materials. The only + material it adds is the schematic line material (`ShipModVisual.cs:55-64`). `Aetheria/Hull` and the UV0 livery mask + belong to `ships-hull-material`, which has a spec but has not landed. So a stand-in's look today is whatever its + glTF materials say. +- **SI-B7. `ship-authoring create --like`** copies a shipped hull's stats with an empty shape and no hardpoints + (`ShipAuthoringCommands.cs:57-75`, `HullLike` clears `Shape` and `Hardpoints`). The layout comes from Blender or + from `ship_cc.replace_layout`. +- **SI-B8. OpenSCAD over LevelDsl, decided from source.** The Geometry.Csg `LevelDsl` has box, cut box, oriented box, + cylinder_z, dome_cap_z and floret_arm, with no hull or free-axis cone (research, Internal fit), and it needs a + cargo build. OpenSCAD nightly with Manifold is installed, and brush-tree's `openscad-target` already rules + OpenSCAD-shaped text. LevelDsl was not probed. + +### The probe (Longinus) + +The stand-in source has three files (`S\probe\`): +- `standin.scad`: the vocabulary. Glyph per kind, kind colours, cell frame, part dispatch. +- `longinus.layout.scad`: generated from HullData by `gen_layout.py`, and never edited by hand. It holds the visible + mounts: id, kind, position, placed footprint and rotation. +- `longinus.standin.scad`: agent-authored, 25 lines. Four `hull()` segments make the body (stern block, flared + weapons section, spine, nose), plus a `deck()` height function. + +`build.py` renders each part separately with `openscad.com --backend Manifold --render all -D part="…"` (one process +at a time, about 22 runs). It then writes `ship.glb`: a flat-faceted mesh per part, UV0 as a top-down projection in +cell units, and nodes tagged `extras.aetheria.id`. It also writes `anchors.json`. + +- **SI-P1. Measurements** (`S\probe\build.py` output): + - The body has 104 triangles. 0 of its 60 vertices lie outside the hull cells, and the worst miss of the mirror + test is 0.0000 m (the body is symmetric). + - The GLB is 84 KB, with 28 nodes and 18 anchors. + - All 10 visible mounts project into their own footprint: weapons and radiators at the footprint centre, thrusters + on the footprint edge they face. + - The probe checked vertices, not edges, against the cells. Each segment is convex inside a rectangular run of + cells, which is why the vertex check suffices here; a stepped outline needs an edge check. +- **SI-P2. Package path, on Yggdrasil.** `ygg-verify` ran on a scratch clone at `2329782244d3`. That clone is + 6d10331c plus `standin-probe/`; its commits are throwaway and were never pushed to origin. Image + `mcr.microsoft.com/dotnet/sdk:10.0`, with pins `/pins/cultlib-git-45c2f40` and `/pins/cultlib-9c1b06cd` and the + `/lfs` catalog. Log: `S\ygg-standin-6.log`, GREEN. The steps were: + 1. `ship-authoring create … --like Longinus`. + 2. `ship_cc.replace_layout` with Longinus' shape and hardpoints, then `ship_cc.replace_visual` with the 18 anchors + (the add-on's own Python). + 3. `validate`, which reported "13 hardpoints, 18 anchors, 0 lines (records and model)", meaning + `ShipModCatalog.ReadPackage` judged the GLB. + 4. `compose`, which reported "Composed 1 mod ships", with no exclusions. + + Runs 1-5 were red on job plumbing: LFS pointer, CultLib pins, empty layout from `--like`, and the null Sensors id. +- **SI-P3. Not proven.** The Unity side was not run: `ShipModPreview.Smoke -assemble` and `ShipModPlaySmoke.Run` + (thrusters, weapons with muzzles, collider, map icon). The probe's GLB writer is scratch Python, not the add-on's + Package Ship. Blender was not in the loop, because no current Blender is on Starfire (only 2.77 and 2.78 under + `D:\Applications`). Glyph proportions were checked only in the proxy renders, never at the real combat camera. +- **SI-P4. What the renders show.** + - From above, the stand-in states where every gun, launcher, engine and radiator is, by colour. At 48 px the + shipped mesh reads only as a silhouette. + - The body reads as an unfinished blockout: slab facets, a cell grid, no surface detail. Nobody would take it for + the shipped hull. + - The grid is texture noise at 48 px. In engine, mips average it away at distance. + - The first radiator fins were edge-on from above and vanished. They became panels tilted 30° outward. Energy + barrels were thickened from 0.14 to 0.28 m for the same reason. + +### Visual grammar (proposal; the look is question `standin-visual-mark`) + +1. **Body.** A few convex `hull()` segments that follow the hull's outline and its concept (Concept Mill image or + description). Flat facets at `$fn = 8`. No bevels, panel lines, greebles or invented surface detail. One neutral + grey, carrying a dark line every 2 m cell on UV0, which is projected top-down in cell units. The grid is the + greybox mark, and because the cells are the schematic's, it also states size class. +2. **Mount glyphs by kind,** each in one saturated colour, on its footprint, facing its Rotation: + - energy: a puck and one long barrel; + - ballistic: a puck and two short thick barrels; + - launcher: a box with tube mouths on its facing face; + - thruster: a bell opening toward its facing, whose mouth disc is the `thruster-emitter` mesh; + - radiator: a panel tilted outward, which is itself the `radiator-mesh`. + + Internal hardpoints are not drawn, because the schematic UI owns them. +3. **Size.** Model metres are cells x 2 m (the add-on's rule). +4. **Faction.** None in the mesh. Once `ships-hull-material` lands, the body's UV0 is where the livery mask goes + (primary channel on the body). Until then the body is neutral grey. + +### Model (SKILL step 0b): rows for the persistent kinds this adds + +| Kind | Identity | Lifecycle | Authority | +|---|---|---|---| +| Stand-in source, `GameData/Mods//standin.scad` | The package directory, which equals `ShipAuthoring.Id`, names it. One per package, at a fixed name. Injective because package ids are. | Created by an agent pass for a hull with no operator art. Revised by agents. Deleted in the same change that lands operator art for that package. | The agent pass (Imagination for content, Hands for the cut) authors the body. The operator decides when it dies, by packaging her art. Forbidden writer: nothing hand-edits the layout. | +| Layout, generated | Derived from that package's HullData. | Regenerated on every build. Not committed, never edited. | HullData. Owner only under `standin-layout-follows-schematic` = follow-schematic. Under free-like-real-art this row disappears and the source holds mount placement. | +| Shared vocabulary, `tools/blender/aetheria_ships/standin/standin.scad` | Single file. | Revised by cuts; a revision re-renders every stand-in at its next build. | The ship add-on (Aetheria). | +| `ship.glb` | `ShipAuthoring.ModelAsset`: one slot per package. | While `standin.scad` exists, a build output of it. Afterwards, the output of the operator's Package. | Exactly one writer at a time. The slot holds one mesh, so two meshes can never both decide. | +| "This hull is a stand-in" | The presence of `standin.scad` in the package. **No new field.** | Ends when the file is deleted. | Derived. Build Stand-in refuses a package without the file, so it can never overwrite operator art. Any "which hulls are still stand-ins" view is a directory listing. | +| Anchors | `ShipAuthoring.Anchors`, ids = mount ids. | Rewritten by Package on each build, stand-in or art. Ids are stable across the swap. | Package Ship (`ship_cc.replace_visual`). The same anchor ids keep loadouts and saves valid across the swap. | + +Replacement when real art arrives: the operator packages her collection into the same package. Package writes +`ship.glb` and the anchors, and the same change deletes `standin.scad`. That is one act and one owner. +Open choice inside the cut, not an operator fork: whether Package deletes `standin.scad` itself or refuses until it is +deleted. Recommendation: Package deletes it when the bound collection was not built by Build Stand-in, and says so. + +`standin.scad` ships inside the mod directory, so it may reach player builds. That is harmless, and it doubles as an +example for modders. The alternative is `tools/standin/hulls/.scad`, which breaks "presence in the package is the +status". + +### Rationale + +- **Why not Tripo or "deliberate low poly".** The ruling's test is "never mistaken for final art". Every element of + the grammar is chosen to fail the polish test on purpose: the grid, flat facets, kind colours, and no surface + detail. Each element also carries information (scale, mount kind, facing), so the look is utility. +- **Why the layout is generated.** + - Readability: the mounts the player sees agree with the schematic UI. + - Upkeep: a schematic edit re-renders the stand-in with no agent pass. + - Opus's weak spot: spatial placement is the failure mode the prior art documents most (LL3M §5; CAD + valid-but-wrong), and generation leaves only the body to judgment. + + This is model geometry derived from cells, against `schematic-and-model-are-separate-spaces`, hence question + `standin-layout-follows-schematic`. +- **Why Blender for the target, not the probe's Python.** Package Ship already writes `ship.glb` and the anchors, + and runs validate. A second GLB writer would be a second authority over the same file. The cut imports OpenSCAD + part meshes into the bound collection with role tags, then calls Package. Brush-tree's `bake-only` and + `lowered-hull-through-package` point the same way. + +### Cut sequence (one line each; no spec admitted, because `standin-build` waits on both questions) + +1. `standin-build`. Add-on operator Build Stand-in (headless-capable): checks that `standin.scad` exists, generates + the layout from the bound `ship.cc`, runs `openscad.com` per part, imports the parts into the bound collection with + `aetheria.role`/`aetheria.id` tags, and calls Package Ship. It carries the vocabulary file and Longinus as its test + hull, gives the shipped Longinus Sensors hardpoint a mount id (SI-B4), and adds a `smoke_standin.py` beside + `smoke_package.py`. Verification: + - `ShipModPreview.Smoke -assemble` and `ShipModPlaySmoke.Run` on the package; + - the in-cells, mirror and in-footprint checks from SI-P1 (the last only under follow-schematic). + + Deploy owner: OpenSCAD in whatever image runs Blender smokes on Yggdrasil. Not established, because the Blender + smoke's host was not found. +2. `standin-package-retire`. Package Ship, on a package that still has `standin.scad`, deletes the file when the + collection is the operator's (model row above). It can fold into cut 1 if small. +3. `standin-role-hulls` (content). Stand-ins for the demo role hulls as their HullData land (follow-up + `demo-role-hulls`), and for the cast flyables until her art arrives. Each body is authored from the hull's + Concept Mill keeper or description (follow-up `hulls-through-concept-mill`). +4. After `ships-hull-material`: the stand-in mask, with primary channel = body, so faction liveries colour stand-ins. + A line in that spec's next revision, not a cut of its own. + +### Gaps + +- Combat camera distance and angle were not read, so the zoom panel is a proxy. +- The greybox dev-texture convention is general practice, not evidence in the Eyes file. +- Where a current Blender runs headless for agents is not established on Starfire. The Package smoke's host was + not checked. +- Settings.SchematicCellSize = 2 m is cited from `hull_grid.py`, not read from `Settings.asset`. +- Scope reading, not a ruling: stand-ins are for hulls without operator art (demo role hulls; cast flyables until + hers land). Shipped legacy hulls keep their meshes.