Header comment, the stray staging fixture (now Stray.json) and the build-delivery doc no longer name it. Builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32 KiB
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 <scratch>\playerbuild\Aetheria.exe -logFile <scratch>\unity-player-build.log. No Editor script was added;-buildWindows64Playerneeds none. Exit code 0. It dirtied one tracked file,ProjectSettings/UnityConnectSettings.asset, which was restored to6dc18607; 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 <scratch>\player-run.log, killed at 25s. - Reads of
gamecult-opsinventory, runbooks,nginx/, and its deploy scripts; Idunnsrc/anddocs/;gamecult-site,AetheriaLoreandGameCult-Quartzconfigs and workflows; theCultLib-semverworktree.
No remote host was contacted. No ssh was run.
Rulings (operator, 2026-09-17): F2, the Aetheria site (aetheria.gamecult.org,
repo AetheriaLore) owns the download and changelog pages ("That's the Aetheria site, of
course"); gamecult.org links to them. F1 no (Idunn stays service-only; the artifact
uses the host's existing static path), F3 semver for builds, commit recorded beside it,
F4 hand-written changelog, F5 agent builds, hashes and records while the operator runs
the remote publish, F6 zip, F7 a new aetheria.release document: Self's defaults,
stated to the operator, who did not object. Reopen any on request.
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 (
CS0067onAssets/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-661setsscriptingBackendfor 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
<build>\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 <build>\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.serviceis a plainServiceDeclaration, not anOption. Every deployment unit is a service.Idunn/src/deployment.rs:110-141— aServiceDeclarationrequires anexecutable_artifact, a transport, and a mandatoryhealthdeclaration;:851refuses 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
SystemdTransientandHostActuator(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 HTTProot,try_files, or a web root anywhere insrc/. - 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-50unpacks into/srv/<site>/site/releases/<UTC-stamp>and movescurrentatomically. - A generic TLS publisher:
scripts/publish-static-site-tls-yggdrasil.sh:1-60, parameterised bySITE_HOST/SITE_ROOT, refusing to run unless DNS already resolves to this host (:40-56). certbot/Let's Encrypt throughout. - Transport:
scp ... ygg:thenssh 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.ccin this repo, oneaetheria.releasedocument 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, andActionGameManager.cs:56keeps 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.
- The game must never read or write the release store. It is not added to
- 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 publishverb; none of them writes the feed or the pages directly. - Deletion line: the vacuous catalog guard at
AetheriaStores.cs:26-31is replaced before Cut 1 adds any build code, anddocs/build-delivery-target.md:11-14is 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-deliveryfrom6dc18607. 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 existingAssets/Editor/(whose only current file isEpiphany/EpiphanyEditorBridge.cs) and compiles into the existingAssembly-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:Openrefuses acatalogPaththat does not exist, unlesscatalogWritable(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):BuildPlayerOptionswithscenesread fromEditorBuildSettings.scenesfiltered onenabled— the same two scenes atProjectSettings/EditorBuildSettings.asset:8-13;target = BuildTarget.StandaloneWindows64. SetsPlayerSettings.bundleVersionfrom the passed version, superseding the stale0.1atProjectSettings/ProjectSettings.asset:152. After the build: copyGameData/Aetheria.cc,GameData/Narrative/**next to the exe; deleteAetheria_BurstDebugInformation_DoNotShip/; fail the run on a non-emptyBuildReporterror list.
- Authority map:
- Owner:
AetheriaBuildowns 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.bundleVersionis now set by the build, not authored inProjectSettings.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.
- Owner:
- 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 inAetheriaBuild.cs. - test: a
Aetheria.Shared.Testscase pins thatAetheriaStores.Openthrows on a nonexistent read-only catalog path, and still opens a writable empty one (the seed case atAetheriaStores.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 inServerShared— the game must not link it. Releases/aetheria.releases.cc, tracked in git. Small: a few KB per release.
- An
- 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),ContentHashsha256 (:151),SizeBytes(:156),MimeType(:160),CreatedAtUtc(:164). NoteCultMeshCdnArtifactKinds.Build = "build"atCultMeshCdn.cs:34is 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-11unchanged,ActionGameManager.cs:56unchanged); the site repos; any hand edit. - Deletion line: none — nothing exists to replace.
- Owner: the release store is the authority on what exists
(
- Verification:
- negative:
git grep -n "aetheria.release"must not match anything underAssets/Scripts/ServerShared/. Checked against theaetheria.*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.
- negative:
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 attools/AetherDb/AetherDb.cs:15-32. Areleaseverb group there adds zero build targets and reuses the csproj, theAetheria.Sharedreference, 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 projectverbs, andCHANGELOG.mdat 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 buildshells the Unity entry point from Cut 1 and zips the result, excluding nothing further (Cut 1 already removed the Burst directory).release publishcomputes sha256 and size, reads theCHANGELOG.mdsection for the version, writes theaetheria.releasedocument, and emits the projection.release projectregenerates 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 publishis 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.
- Owner:
- Verification:
- test:
release projectrun twice produces byte-identical output. - negative: nothing outside
tools/AetherDbopensaetheria.releases.cc. - operator: none yet; Cut 5 carries the smoke.
- test:
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/<host>.confmodelled onnginx/erycina.org.conf, keeping the ACME location (:11-14) and the security headers (:7-9), but withroot /srv/aetheria-builds,autoindex off, and notry_filesfallback and noerror_pagerewrite — a missing version must 404.scripts/deploy-aetheria-build-yggdrasil.sh. Modelled onscripts/deploy-erycina-site-yggdrasil.shbut with the rotation removed: the release path is/srv/aetheria-builds/<version>/, it refuses to write into a version directory that already exists, there is nocurrentsymlink, and there is no pruning. Copying:50-56unchanged would delete published downloads and breakdocs/build-delivery-target.md:32.- A runbook alongside
runbooks/erycina-org-site-deploy.md.
- Reuses unchanged:
scripts/publish-static-site-tls-yggdrasil.shfor the vhost install and certbot run — it is already parameterised bySITE_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.orgrecords ingamecult-ops/inventory.mdstill sayA -> 94.130.223.131(:746, :764, :771, :777, :785) while Yggdrasil is at159.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 -Ithe artifact URL returns 200 with the rightContent-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.
- operator:
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.mjsandgenerate-vn-eve-surface.mjswrite JSON intoGameCult/static/..., invoked by the local launcher (scripts/quartz/quartz.ps1:38-52) and not by CI, so the outputs are committed artifacts. Cut 3'srelease projectfills that same role. The site's existing workflow then publishes with no change at all. - Brand. Follow
gamecult-site/docs/brand-design-language.mdand 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.mdand 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 projectleaves 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-opsstatic release path — scp, a root-owned append-only release script, an nginx vhost, certbot — the same authority shape that already serveserycina.organdvelvet.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
serviceoptional (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 atdocs/build-delivery-target.md:44to 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:
AetheriaLoreis 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-39is 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.mjsdemands exactly one major/minor/patch step per release (:39,:62), refuses a version with no changelog entry (:79), and treats0.y.zminor 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 thedocs/semver-policy.mdit cites at:4does 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-57andGhostlight.md:56-57are 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-opsalready 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 aBuildkind. - B: A new
aetheria.releasetype that borrows those field names. - Recommended: B, citing A. The CultLib type is CDN-shaped: its
Chunkslist (: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 needsCommit,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/AetherDbprecisely 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.mdhas not landed and this migration does not wait on it. The build probed in 0.1 is the pre-AddressablesResourcespath, 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
.gitplus a 2.6 GBLibrary/; the ~9 min build in 0.1 was warm, and a coldLibrary/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-14and:44both need correcting once F1 and Cut 1 are ruled; the target currently says builds happen by hand intoBuild/(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.131DNS rows ingamecult-ops/inventory.md:746,764,771,777,785. - Follow-up outside this migration:
CultLib-semver/scripts/check-changelog-semver.mjsis untracked and cites adocs/semver-policy.mdthat does not exist. F3 leans on that work; someone should land it there.