From 2c2b92579f558fb511ef3de47feaef74a3dd35d5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 17 Sep 2026 22:00:45 +0200 Subject: [PATCH] Map build delivery, and narrow the Idunn invariant to services Co-Authored-By: Claude Opus 5 --- docs/build-delivery-cut.md | 542 ++++++++++++++++++++++++++++++++++ docs/build-delivery-target.md | 7 +- 2 files changed, 547 insertions(+), 2 deletions(-) create mode 100644 docs/build-delivery-cut.md diff --git a/docs/build-delivery-cut.md b/docs/build-delivery-cut.md new file mode 100644 index 000000000..9e63cadbd --- /dev/null +++ b/docs/build-delivery-cut.md @@ -0,0 +1,542 @@ +# Build Delivery: Cut Map + +Date: 2026-09-17 + +Status: Imagination pass, cut map. Nothing here has landed. Anchors are against +`codex/item-provenance` HEAD `6dc18607`. Ends are owned by +`docs/build-delivery-target.md`; this document owns the means. + +Claims marked **(probe)** were run, not read off names. What was run: + +- A real batchmode player build of the main tree, Unity closed, output to a + scratch directory outside the repo: + `Unity.exe -batchmode -quit -nographics -silent-crashes -projectPath F:\Projects\Aetheria -buildWindows64Player \playerbuild\Aetheria.exe -logFile \unity-player-build.log`. + No Editor script was added; `-buildWindows64Player` needs none. Exit code 0. + It dirtied one tracked file, `ProjectSettings/UnityConnectSettings.asset`, + which was restored to `6dc18607`; the other 16 modified files in the worktree + predate this pass. Build output and the zip were deleted afterwards. +- A headless run of the produced player: + `Aetheria.exe -batchmode -nographics -logFile \player-run.log`, killed + at 25s. +- Reads of `gamecult-ops` inventory, runbooks, `nginx/`, and its deploy scripts; + Idunn `src/` and `docs/`; `gamecult-site`, `AetheriaLore` and `GameCult-Quartz` + configs and workflows; the `CultLib-semver` worktree. + +No remote host was contacted. No `ssh` was run. + +## 0. What the probes changed about the target + +Four findings move the design. Read these before the cuts. + +### 0.1 A player build already works **(probe)** + +The target says Aetheria "has no player build pipeline" +(`docs/build-delivery-target.md:11`). Half true: there is no build *code* — zero +`BuildPipeline`/`BuildPlayer`/`BuildTarget` references in any tracked `.cs`, and +`Build/` holds only Eve asset bundles and logs, never a player. But the project +itself builds clean today with the stock command-line switch. + +- Scripts compiled for StandaloneWindows64 with warnings only (`CS0067` on + `Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs:44-46`). +- ~9 minutes wall on Starfire with a warm `Library/`. +- Output 212 files, **352.4 MB** on disk; **189.8 MB** as a zip + (`Compress-Archive -CompressionLevel Optimal`, 35s). +- Scenes come from `ProjectSettings/EditorBuildSettings.asset:8-13`: + `Assets/Scenes/Main Menu.unity`, `Assets/Scenes/ARPG.unity`, both enabled. +- Scripting backend is Mono, not IL2CPP: `ProjectSettings/ProjectSettings.asset:660-661` + sets `scriptingBackend` for Android only, so Standalone takes the default. +- `Aetheria_BurstDebugInformation_DoNotShip/` is emitted beside the exe and must + not ship. + +So Cut 1 is not "make a build possible". It is "make a build repeatable, +identified, and complete", which is a much smaller cut. + +### 0.2 The catalog gap is silent, not loud **(probe)** + +`ActionGameManager.cs:38-41` resolves `GameDataDirectory` as +`new DirectoryInfo(Application.dataPath).Parent.CreateSubdirectory("GameData")`, +and `:45` builds `CatalogPath` from it. In a player that is +`\GameData\Aetheria.cc`. Nothing in the repo copies the catalog there. + +The dangerous part is what happens next. `AetheriaStores.cs:13-15` promises the +open "Throws when a `[CultGlobal]` type routed to the catalog has no record", and +`:26-31` implements exactly that. But none of the seven catalog types at +`AetheriaStores.cs:9` is `[CultGlobal]`, so the guard selects nothing and the +open succeeds over a catalog file that does not exist. + +Proven, not inferred: the built player launched, reached Main Menu, ran for 25s, +and wrote `\GameData\player.cc` (1,964 bytes) — while `Aetheria.cc` was +absent the whole time. No exception, no log line about the catalog. The only +errors were `Stardust` compute-shader nulls, which are `-nographics` artefacts. + +A player shipped without its catalog therefore does not refuse. It boots to a +menu and fails later, in the ARPG scene, with an empty world. That is worse than +a crash, and Cut 1 has to close it. + +### 0.3 Idunn structurally cannot carry a file artifact + +The target invariant reads "Idunn owns deployment to Yggdrasil" +(`docs/build-delivery-target.md:44`). Idunn cannot own this one. + +- `Idunn/src/deployment.rs:33` — `TargetDeclaration.service` is a plain + `ServiceDeclaration`, not an `Option`. Every deployment unit is a service. +- `Idunn/src/deployment.rs:110-141` — a `ServiceDeclaration` requires an + `executable_artifact`, a transport, and a mandatory `health` declaration; + `:851` refuses a plan whose service executable is not a declared artifact. +- Admission does not complete until the workload publishes its own signed + presence and health (`Idunn/docs/deployment-authority.md:84-92`). A zip cannot + sign presence. +- The only workload drivers are `SystemdTransient` and `HostActuator` + (`Idunn/src/deployment.rs:442-450`) — both process supervisors. There is no + oneshot path. +- The only route drivers are `NginxStreamTcp`/`NginxStreamUdp` + (`Idunn/src/deployment.rs:698-701`), writing stream fragments to + `/etc/nginx/idunn-stream-routes/`. Idunn has no concept of an HTTP `root`, + `try_files`, or a web root anywhere in `src/`. +- Idunn has no push transport at all. It pulls: `GitSourceDriver` + (`Idunn/src/drivers.rs:682,714`) fetches the admitted ref on the host. Even the + remote Windows actuator fetches its own source + (`Idunn/docs/host-actuator.md:27-47`). Bytes never travel *from* Idunn. + +`ArtifactOutput` (`Idunn/src/deployment.rs:91-101`) can carry an arbitrary blob +with an `expected_sha256`, and `required_adjacent_artifacts` (`:115`) ships +non-executable files — but only alongside a supervised executable. There is no +unit shaped like "a file at a URL". + +Meanwhile Yggdrasil already serves static files, twice, through a different and +proven authority (see 0.4). The target invariant should be narrowed to "Idunn +owns *service* deployment to Yggdrasil"; static bytes already have an owner, and +it is not a second deployment authority invented for this migration. See **F1**. + +### 0.4 The site is GitHub Pages; only the bytes can live on Yggdrasil + +`gamecult.org` is GitHub Pages out of `GameCult/gamecult-site` +(`gamecult-ops/inventory.md:728-738`, `runbooks/gamecult-pages-cutover.md:1-19`), +built by the shared engine `GameCult/GameCult-Quartz` through a reusable workflow +(`gamecult-site/.github/workflows/deploy-quartz.yml`, +`GameCult-Quartz/.github/workflows/quartz-pages.yml:24-72`, ending in +`actions/deploy-pages@v4`). `aetheria.gamecult.org` is the same mechanism out of +`GameCult/AetheriaLore` (`AetheriaLore/site/quartz.config.ts:66`, +`gamecult-ops/inventory.md:801-806`). + +A 190 MB zip cannot go in either repo: GitHub blocks files over 100 MB, and +Pages has its own soft limits. So the split is forced and is exactly what the +operator asked for — the *page* is on the site, the *bytes* are on Yggdrasil. + +What Yggdrasil already has: + +- nginx with real static vhosts: `gamecult-ops/nginx/erycina.org.conf:4` + (`root /srv/erycina-site/site/current`), `nginx/velvet.erycina.org.conf:4`. +- An immutable-release store pattern: + `scripts/deploy-erycina-site-yggdrasil.sh:33-50` unpacks into + `/srv//site/releases/` and moves `current` atomically. +- A generic TLS publisher: `scripts/publish-static-site-tls-yggdrasil.sh:1-60`, + parameterised by `SITE_HOST`/`SITE_ROOT`, refusing to run unless DNS already + resolves to this host (`:40-56`). certbot/Let's Encrypt throughout. +- Transport: `scp ... ygg:` then `ssh ygg 'sudo -n bash /tmp/...'` + (`runbooks/erycina-org-site-deploy.md:78-81`). + +One thing does **not** transfer. The site-release pattern rotates: a `current` +symlink plus `KEEP_RELEASES=5` pruning +(`scripts/deploy-erycina-site-yggdrasil.sh:50-56`). The target requires the +opposite — "A published version stays published" +(`docs/build-delivery-target.md:32`). The artifact store is append-only, with no +`current` symlink and no pruning. Cut 4 says so explicitly, because copying the +erycina script wholesale would silently delete published downloads. + +## 1. Authority map (whole migration) + +- **Owner of the release record:** a new CultCache store + `Releases/aetheria.releases.cc` in this repo, one `aetheria.release` document + per published build. The Aetheria repo owns the facts about Aetheria builds. +- **Inputs:** the built artifact's bytes (size, sha256), the commit it was built + from, the platform, the build host, the version, and a hand-authored changelog + entry. +- **Outputs:** the artifact zip; the release document; a derived feed and derived + changelog pages in the site repo. +- **Derived state, never authored:** the browser-facing JSON feed, the download + page, and every changelog page. All are projections of the release store, + regenerated wholesale. Editing them by hand is the failure the target names at + `docs/build-delivery-target.md:30-31`. +- **Forbidden writers:** + - The *game* must never read or write the release store. It is not added to + `AetheriaStores.cs:9-11`, and `ActionGameManager.cs:56` keeps opening exactly + three stores. A release record is publishing state, not game state. + - Idunn does not write the artifact, the feed, or the pages (0.3). + - The site repos never author release facts; their content files for downloads + and changelog are generated and overwritten. + - `Build/` stays gitignored (`.gitignore:8`) and never becomes an artifact + store. +- **Shared paths:** there is exactly one publish primitive. A manual publish, a + re-publish, and any future automated publish all go through the same + `release publish` verb; none of them writes the feed or the pages directly. +- **Deletion line:** the vacuous catalog guard at `AetheriaStores.cs:26-31` is + replaced before Cut 1 adds any build code, and + `docs/build-delivery-target.md:11-14` is corrected once Cut 1 lands. + +**Atomicity, honestly.** The target asks that publishing "publishes the artifact, +the feed entry and the changelog entry together, or it publishes nothing" +(`:28-29`). Three destinations — a remote filesystem, a git-tracked store, and a +second git repo — admit no real two-phase commit. The invariant that actually +protects a visitor is narrower and is the one to enforce: **no feed entry exists +without verified bytes already fetchable at its URL.** Order is artifact → verify +by re-downloading and re-hashing → release document → projection → site push. +A failure before the release document leaves unreferenced bytes, which are inert +and which the next publish of the same version may overwrite only when the hash +matches. A failure after it leaves a pending page, which the next site build +fixes. Neither produces a download link that 404s. + +## 2. Cuts + +### Cut 1. A player build that is repeatable, identified, and carries its catalog + +- **Repo/branch:** Aetheria, `codex/build-delivery` from `6dc18607`. Depends on + nothing. +- **First:** keep the probe numbers in 0.1 as the baseline — 212 files, + 352.4 MB, 189.8 MB zipped, ~9 min warm. A first landed build that differs + wildly means the build options diverged from the stock switch. +- **Deletes first:** + - `Assets/Scripts/ServerShared/AetheriaStores.cs:13-15` (the comment promising + a guard that does not fire) and the guard body at `:26-31`, replaced rather + than supplemented. +- **Adds:** + - `Assets/Editor/AetheriaBuild.cs` — one static entry point for + `-executeMethod`, taking version and output root from the command line. It + goes under the existing `Assets/Editor/` (whose only current file is + `Epiphany/EpiphanyEditorBridge.cs`) and compiles into the existing + `Assembly-CSharp-Editor`; no new asmdef, no new assembly. + - A catalog staging step in that entry point. +- **Per-file changes** (against `6dc18607`): + - `Assets/Scripts/ServerShared/AetheriaStores.cs:16-32`: `Open` refuses a + `catalogPath` that does not exist, unless `catalogWritable` (the seed case + already exempted at `:24-25`). Keep the existing global check as a second, + narrower guard; it is not wrong, it is merely inert for today's type set, and + it will bite the moment a catalog type is marked `[CultGlobal]`. The new + check is the one that protects a shipped player. + - `Assets/Editor/AetheriaBuild.cs` (new): `BuildPlayerOptions` with + `scenes` read from `EditorBuildSettings.scenes` filtered on `enabled` — the + same two scenes at `ProjectSettings/EditorBuildSettings.asset:8-13`; + `target = BuildTarget.StandaloneWindows64`. Sets + `PlayerSettings.bundleVersion` from the passed version, superseding the + stale `0.1` at `ProjectSettings/ProjectSettings.asset:152`. After the build: + copy `GameData/Aetheria.cc`, `GameData/Narrative/**` and + `GameData/SoundbanksInfo.json` next to the exe; delete + `Aetheria_BurstDebugInformation_DoNotShip/`; fail the run on a non-empty + `BuildReport` error list. +- **Authority map:** + - Owner: `AetheriaBuild` owns what a player build contains. + - Inputs: `EditorBuildSettings.scenes`, `GameData/`, a version string, an + output root. + - Outputs: a complete player directory, and the build report. + - Derived: `PlayerSettings.bundleVersion` is now set by the build, not authored + in `ProjectSettings.asset`. + - Forbidden writers: nothing else sets `bundleVersion`; nothing else decides + the scene list. + - Deletion line: the inert-guard comment goes before the entry point is added. +- **Verification:** + - builds: the entry point produces a player; size within ~10% of 352.4 MB. + - negative: `git grep -n "Aetheria_BurstDebugInformation"` finds only the + deletion in `AetheriaBuild.cs`. + - test: a `Aetheria.Shared.Tests` case pins that `AetheriaStores.Open` throws + on a nonexistent read-only catalog path, and still opens a writable empty one + (the seed case at `AetheriaStores.cs:24-25`). This is the regression that 0.2 + proves is live today. + - operator: launch the staged build by double-click, reach Main Menu, start a + game, confirm the world is populated. Then rename `GameData/` beside the exe + and relaunch: it must refuse loudly, not boot to a menu. + +### Cut 2. The release record + +- **Repo/branch:** Aetheria, same branch. Depends on Cut 1 for the fields it + records. +- **Adds:** + - An `aetheria.release` `[CultDocument]` type, declared in the tools assembly, + not in `ServerShared` — the game must not link it. + - `Releases/aetheria.releases.cc`, tracked in git. Small: a few KB per release. +- **Shape.** Model the fields on the one existing GameCult type that already + describes a versioned build artifact, + `CultLib/src/GameCult.Mesh/CultMeshCdn.cs:130-171` + (`gamecult.mesh.cdn_artifact_manifest`): `Version` (`:145`), `ContentHash` + sha256 (`:151`), `SizeBytes` (`:156`), `MimeType` (`:160`), `CreatedAtUtc` + (`:164`). Note `CultMeshCdnArtifactKinds.Build = "build"` at `CultMeshCdn.cs:34` + is documented as "Executable build or install/update payload" — this exact case. + Add what that type lacks and the target requires: `Commit`, `Platform`, + `BuildHost`, `Url`, and the changelog body. See **F7** on reuse vs a new type. +- **Authority map:** + - Owner: the release store is the authority on what exists + (`docs/build-delivery-target.md:30`). + - Inputs: Cut 3's publish verb only. + - Derived: everything a browser sees. + - Forbidden writers: the game (`AetheriaStores.cs:9-11` unchanged, + `ActionGameManager.cs:56` unchanged); the site repos; any hand edit. + - Deletion line: none — nothing exists to replace. +- **Verification:** + - negative: `git grep -n "aetheria.release"` must not match anything under + `Assets/Scripts/ServerShared/`. Checked against the `aetheria.*` schema names + already in that tree so it cannot collide. + - test: reopening the store yields the same documents; a published version is + refused on re-publish with different bytes. + +### Cut 3. One publish primitive + +- **Repo/branch:** Aetheria, same branch. Depends on Cuts 1 and 2. +- **Keeps and reuses:** `tools/AetherDb` — the existing headless .NET console over + Aetheria's CultCache stores, with store-path resolution already written at + `tools/AetherDb/AetherDb.cs:15-32`. A `release` verb group there adds **zero** + build targets and reuses the csproj, the `Aetheria.Shared` reference, and the + path walk. A second console would add a target for nothing. See **F5** on who + runs it. +- **Adds:** `release build`, `release publish`, `release project` verbs, and + `CHANGELOG.md` at the repo root in the Keep-a-Changelog shape CultLib just + adopted (`CultLib-semver/unity/org.gamecult.cultlib/CHANGELOG.md:1`). +- **Per-file changes:** + - `tools/AetherDb/Program.cs`: new verb dispatch. `release build` shells the + Unity entry point from Cut 1 and zips the result, excluding nothing further + (Cut 1 already removed the Burst directory). `release publish` computes + sha256 and size, reads the `CHANGELOG.md` section for the version, writes the + `aetheria.release` document, and emits the projection. `release project` + regenerates the projection alone from the store, so the pages can be rebuilt + without a build. + - Follow the deterministic-packaging precedent in + `CultLib-semver/scripts/build-unity-package.ps1:41-52`, which clears + intermediates and disables source-revision stamping so a rebuild is + byte-comparable. The target asks for reproducibility from a commit + (`docs/build-delivery-target.md:36-37`); say plainly in the doc what Unity + does *not* guarantee there rather than claim byte-identity that Cut 1 has not + demonstrated. +- **Authority map:** + - Owner: `release publish` is the only writer of the release store. + - Derived: the feed JSON and the markdown pages, regenerated wholesale, never + merged into. + - Shared paths: a first publish and a re-publish use the same verb. + - Forbidden writers: no script writes the feed or pages directly. +- **Verification:** + - test: `release project` run twice produces byte-identical output. + - negative: nothing outside `tools/AetherDb` opens `aetheria.releases.cc`. + - operator: none yet; Cut 5 carries the smoke. + +### Cut 4. The artifact host on Yggdrasil + +- **Repo/branch:** `gamecult-ops`, its own branch. Depends on Cut 3 producing a + zip. Independent of Cuts 1-3 landing. +- **Adds:** + - `nginx/.conf` modelled on `nginx/erycina.org.conf`, keeping the ACME + location (`:11-14`) and the security headers (`:7-9`), but with + `root /srv/aetheria-builds`, `autoindex off`, and **no** `try_files` + fallback and **no** `error_page` rewrite — a missing version must 404. + - `scripts/deploy-aetheria-build-yggdrasil.sh`. Modelled on + `scripts/deploy-erycina-site-yggdrasil.sh` but with the rotation removed: + the release path is `/srv/aetheria-builds//`, it **refuses** to + write into a version directory that already exists, there is **no** `current` + symlink, and there is **no** pruning. Copying `:50-56` unchanged would delete + published downloads and break + `docs/build-delivery-target.md:32`. + - A runbook alongside `runbooks/erycina-org-site-deploy.md`. +- **Reuses unchanged:** `scripts/publish-static-site-tls-yggdrasil.sh` for the + vhost install and certbot run — it is already parameterised by + `SITE_HOST`/`SITE_ROOT` (`:1-60`) and already refuses to run before DNS + resolves to this host (`:40-56`). +- **Hazard to carry into the runbook:** five existing `*.gamecult.org` records in + `gamecult-ops/inventory.md` still say `A -> 94.130.223.131` + (`:746, :764, :771, :777, :785`) while Yggdrasil is at `159.195.114.249` + (`:363, :387`). The new record must use the current address, and the stale rows + are a follow-up outside this migration. +- **Authority map:** + - Owner: the release directory tree on Yggdrasil is append-only and root-owned. + - Inputs: a zip and its sha256, scp'd by the operator. + - Forbidden writers: nothing overwrites a published version directory; Idunn + does not touch this tree (0.3); no pruning job exists. + - Deletion line: the rotation and pruning logic is removed when the script is + adapted, not left in and disabled. +- **Verification:** + - operator: `curl -I` the artifact URL returns 200 with the right + `Content-Length`; TLS valid; the parent directory does not list; + a nonexistent version returns 404. + - negative: re-running the deploy script for an existing version exits nonzero + and changes nothing. + - budget: ~190 MB per release on a ~2 TB root + (`gamecult-ops/inventory.md:355-392`). Ten releases is ~1.9 GB. Retention + owner is the operator; append-only is the default and nothing prunes. + +### Cut 5. The download page and the changelog pages + +- **Repo/branch:** the chosen site repo (see **F2**), its own branch. Depends on + Cut 3's projection and Cut 4's URL. +- **Adds:** generated content files under the site's content dir — one downloads + page, one page per release — plus the feed JSON under `static/`. +- **Mechanism.** Do not write a Quartz emitter plugin. The established precedent + in these repos is pre-build generation into committed files: + `gamecult-site/scripts/generate-vn-repo-doc-tree.mjs` and + `generate-vn-eve-surface.mjs` write JSON into `GameCult/static/...`, invoked by + the local launcher (`scripts/quartz/quartz.ps1:38-52`) and **not** by CI, so the + outputs are committed artifacts. Cut 3's `release project` fills that same role. + The site's existing workflow then publishes with no change at all. +- **Brand.** Follow `gamecult-site/docs/brand-design-language.md` and the config + it describes, not invention: Montserrat 100-300 for titles, Ubuntu 300 body, + IBM Plex Mono uppercase and letter-spaced for the structural labels a download + page is mostly made of (version, date, platform, size, hash). Ground `#07111a`, + panels `#16212c`, body `#b7c7d9`, headings `#eef5ff`, accent `#ff8a2a`, links + `#59b7ff`. Reuse the existing component classes rather than adding a page-type + variant: `.gamecult-repo-facts` / `-fact-label` / `-fact-value` + (`gamecult-site/site/quartz/styles/custom.scss:953-1008`) is already a + label/value fact table, and `.gamecult-action` + (`custom.scss:2246-2360+`) is already the button. Note the brand defines no + success/warning/error colour (`brand-design-language.md:135-138`); a download + page needs none. +- **Authority map:** + - Owner: Cut 3's projector owns every byte of these files. + - Derived: all of them. A hand edit is overwritten on the next projection and + that is the intended behaviour. + - Forbidden writers: no human edits the generated pages; changelog prose is + authored in the Aetheria repo's `CHANGELOG.md` and flows through Cut 3. + - Shared paths: the site's existing Pages workflow is untouched. +- **Verification:** + - builds: the site's Quartz build succeeds with the generated pages present. + - negative: re-running `release project` leaves the working tree clean. + - **operator, the real check:** from a machine that has never held the repo — + open the download page on the public site, click the link, let it download, + verify the sha256 shown on the page against the file, unzip it, run + `Aetheria.exe`, start a game, confirm the world is populated. Then confirm + the changelog page for that version says what actually changed. This is the + only check that proves the whole path; every other verification above is a + component test. + +## 3. Operator forks + +**F1. Does Idunn carry the artifact?** *Most blocking — it decides Cut 4.* +- **A:** Extend Idunn with a static-artifact deployment unit (optional `service`, + a file workload driver, an HTTP root route driver). +- **B:** Use the existing `gamecult-ops` static release path — scp, a root-owned + append-only release script, an nginx vhost, certbot — the same authority shape + that already serves `erycina.org` and `velvet.erycina.org`. +- **Recommended: B.** A is a large change in Rust to the one daemon whose whole + design premise is that it depends on no managed target and supervises signed, + health-reporting processes; it would mean making `service` optional + (`Idunn/src/deployment.rs:33`) and inventing an HTTP-root route driver, to make + Idunn do the thing it is deliberately not shaped for. B is not a second + deployment authority — it is the *existing* file-serving authority on that + host. This does mean narrowing the target invariant at + `docs/build-delivery-target.md:44` to name service deployment; that edit + belongs to whoever lands Cut 4. + +**F2. Which site owns the download page?** +- **A:** `gamecult.org` (`gamecult-site`) — what the target's invariant names + (`docs/build-delivery-target.md:46`). +- **B:** `aetheria.gamecult.org` (`AetheriaLore`) — the game's own live site, + same engine, same brand family. +- **Recommended: B, with A linking to it.** A download for Aetheria belongs on + Aetheria's site, and per-release changelog pages will accumulate; parking + dozens of them in the studio site's tree is noise there and natural there. The + honest counter: `AetheriaLore` is a *lore vault* (`Aetheria/Lore`, `Fiction`, + `Worldbuilding`) and release notes are not lore, so B needs a new top-level + section and a masthead entry + (`AetheriaLore/site/quartz/components/AetheriaMasthead.tsx:19-39` is a + hardcoded route list). This one is close; the target's wording points at A and + only the operator can say whether that wording was deliberate. + +**F3. Versioning scheme for game builds.** +- **A:** semver, next is `0.2.0`, commit recorded as a separate field. +- **B:** CalVer, `2026.09.17`. +- **C:** commit-derived, `0.1.0+g6dc1860`. +- **Recommended: A.** It reuses the policy work already in flight: + `CultLib-semver/scripts/check-changelog-semver.mjs` demands exactly one + major/minor/patch step per release (`:39,:62`), refuses a version with no + changelog entry (`:79`), and treats `0.y.z` minor as the breaking lane + (`:98,:139`) — which is exactly right for a game whose saves will break. + Caveat the operator should know: that script is **untracked** on its branch, + and the `docs/semver-policy.md` it cites at `:4` does not exist in any CultLib + worktree. It is package-only and says nothing about builds, so adopting it here + is a decision, not an inheritance. B tells a player nothing about whether their + save survives. C puts a commit in the version string where the target already + wants the commit as its own field (`docs/build-delivery-target.md:26-27`). + +**F4. Hand-written or commit-derived changelog?** +- **A:** Hand-written per release in `CHANGELOG.md`, ingested at publish. +- **B:** Derived from commit subjects between tags. +- **Recommended: A.** B produces a build log, not release notes — and GameCult + already has that experiment running by hand: + `gamecult-site/GameCult/Projects/VoidBot.md:53-57` and `Ghostlight.md:56-57` + are dated commit-subject lists, and they read like machine output. A also + matches what CultLib just adopted for its own packages. The cost is that + publishing blocks on someone writing prose, which is the correct thing for it + to block on. + +**F5. Agent-run or operator-run publishing?** +- **A:** Fully agent-run, including the Yggdrasil push. +- **B:** Agent builds, hashes, writes the record and the projection, and opens + the PRs; the operator runs the two remote scripts. +- **Recommended: B.** `gamecult-ops` already draws this exact line, in a comment + worth quoting: "putting a name on the public internet is a separate decision + from having bytes on disk" + (`scripts/deploy-erycina-site-yggdrasil.sh:9-11`). A published version is + permanent by the target's own rule, so the irreversible step is the one a human + should take. Everything expensive and mechanical stays automated. + +**F6. Zip or installer?** +- **A:** Zip. **B:** An installer (NSIS/Inno/MSI). +- **Recommended: A.** Measured at 189.8 MB **(probe)**. No installer toolchain + exists anywhere in GameCult, and B drags in code signing, an uninstaller, and a + new build dependency for no player benefit while launchers and auto-update are + already out of scope (`docs/build-delivery-target.md:55`). Revisit when there + is a reason, not before. + +**F7. Reuse `gamecult.mesh.cdn_artifact_manifest` or declare `aetheria.release`?** +- **A:** Reuse the CultLib type (`CultLib/src/GameCult.Mesh/CultMeshCdn.cs:130`), + which already has version, sha256, size, mime, created-at, and a `Build` kind. +- **B:** A new `aetheria.release` type that borrows those field names. +- **Recommended: B, citing A.** The CultLib type is CDN-shaped: its `Chunks` list + (`:168`) is the point of it, and content-addressed chunking + (`CultMeshCdn.cs:96-120`) is machinery this migration does not use and should + not fake. A release also needs `Commit`, `BuildHost`, and changelog prose, + which do not belong bolted onto a CDN manifest consumed elsewhere. Borrow the + field shape, not the type. If a CultMesh CDN path ever carries these builds, A + becomes the transport and B stays the record. + +## 4. Subtraction and build budget + +This migration is net additive and cannot honestly be made otherwise: it buys a +capability that does not exist today — nobody outside Starfire can play the game. +What it must not do is buy it twice. + +| Cut | Removes | Adds | Targets | +|---|---|---|---| +| 1 | inert guard + stale comment, `AetheriaStores.cs:13-15,26-31`; the shipped Burst debug dir | one Editor file under the existing `Assets/Editor/` | +0 assemblies (no asmdef; joins `Assembly-CSharp-Editor`) | +| 2 | — | one document type, one tracked `.cc` | +0 | +| 3 | — | verbs inside the existing `tools/AetherDb`; one root `CHANGELOG.md` | **+0 executable targets** | +| 4 | the rotation and pruning logic while adapting the erycina script | one nginx conf, one shell script, one runbook | +0 (no compilation in `gamecult-ops`) | +| 5 | — | generated markdown/JSON in a site repo | +0 (no workflow change) | + +Explicit budget statements: + +- **No new executable target anywhere.** Cut 3 reuses `tools/AetherDb` precisely + so the repo does not gain a second console. If that reuse is rejected, the + budget changes and the fork should be raised. +- **No new Unity assembly**, no new package dependency, no Addressables + dependency — `docs/addressables-cut.md` has not landed and this migration does + not wait on it. The build probed in 0.1 is the pre-Addressables `Resources` + path, and it works. +- **Build host is Starfire, Windows, Unity 6000.3.24f1** + (`ProjectSettings/ProjectVersion.txt:1`). The serving host is Debian 13. The + artifact is Windows-only and produced only on Windows; nothing about Cut 4 + proves anything about the player, and nothing about Cut 1 proves anything about + the server. +- **Disk.** ~190 MB per release on Yggdrasil, append-only, no pruning. The + operator owns retention. +- **A fresh clone is not required per release.** The checkout is 1.78 GiB of + `.git` plus a 2.6 GB `Library/`; the ~9 min build in 0.1 was warm, and a cold + `Library/` import is far worse. Publishing builds from the working Starfire + checkout is the assumption; if that ever moves to a clean builder, the import + cost has to be budgeted then, not assumed away now. + +## 5. Open + +- Every fork in section 3 is unruled. +- `docs/build-delivery-target.md:11-14` and `:44` both need correcting once F1 + and Cut 1 are ruled; the target currently says builds happen by hand into + `Build/` (they do not — nothing produces a player) and that Idunn owns this + deployment (it structurally cannot). +- Follow-up outside this migration: the five stale `A -> 94.130.223.131` DNS rows + in `gamecult-ops/inventory.md:746,764,771,777,785`. +- Follow-up outside this migration: `CultLib-semver/scripts/check-changelog-semver.mjs` + is untracked and cites a `docs/semver-policy.md` that does not exist. F3 leans + on that work; someone should land it there. diff --git a/docs/build-delivery-target.md b/docs/build-delivery-target.md index fdea850b2..95c7437f9 100644 --- a/docs/build-delivery-target.md +++ b/docs/build-delivery-target.md @@ -41,8 +41,11 @@ artifact: Idunn deploys services to Yggdrasil, not files. - **State is CultCache, not JSON** (`F:\Projects\CLAUDE.md`). The release record and the changelog are typed documents. Anything a browser eats is a projection at the xenos boundary, and is derived, never authored. -- **Idunn owns deployment to Yggdrasil.** Publishing does not invent a second - deployment authority, and does not require Idunn to be up in order to build. +- **Idunn owns *service* deployment to Yggdrasil**, and only that. Probed + 2026-09-17 (`docs/build-delivery-cut.md`): every Idunn unit is a supervised + process that must publish signed health, and Idunn pulls rather than pushes, so + it cannot carry a file artifact. Publishing uses the host's existing static-file + path and does not invent a second *service* deployment authority. - **The site's brand is the site's** (`F:\Projects\gamecult-site`), and the download and changelog pages follow it. - **The build host is Windows.** Unity 6000.3.24f1 builds on Starfire; the