How does a module release know which topdata it shipped against?
Part of #54. #58 found that publish-module-release.sh:59 records hak_release — the paired hak tag — but there is no topdata_release field. fetch-hak-manifest.sh:48-49 prepends sow_top by name only, deliberately, so module/topdata parity stays deploy-pinned by sow-platform.
That was survivable while nothing at release time needed the topdata digest. Now sow-module CI assembles the manifest, and a manifest must cover sow_top.hak and sow_tlk.tlk with their digests. So the release has to resolve topdata somehow.
Two shapes:
Pin a tag at release, next to hak_release, written by the same preflight that writes .cache/hak-release.txt. Consistent with how haks are already handled, and it closes a real hole that exists today independently of NWSync — a module release currently does not record which topdata it shipped against at all.
Read the topdata current channel at build time and do not record it. Simpler, but the manifest then describes a content set the release itself cannot name afterwards, and two builds of the same tag could differ.
Settle which, and whether changing this conflicts with the deploy-pinned parity rule sow-platform owns — pinning a topdata tag in the module release may or may not be the same thing as resolving a topdata channel, and that distinction is what fetch-hak-manifest.sh:48-49 was protecting.
## Question
How does a module release know which topdata it shipped against?
Part of #54. #58 found that `publish-module-release.sh:59` records `hak_release` — the paired hak tag — but there is **no `topdata_release` field**. `fetch-hak-manifest.sh:48-49` prepends `sow_top` by name only, deliberately, so module/topdata parity stays deploy-pinned by sow-platform.
That was survivable while nothing at release time needed the topdata digest. Now sow-module CI assembles the manifest, and a manifest must cover `sow_top.hak` and `sow_tlk.tlk` with their digests. So the release has to resolve topdata somehow.
Two shapes:
- **Pin a tag at release**, next to `hak_release`, written by the same preflight that writes `.cache/hak-release.txt`. Consistent with how haks are already handled, and it closes a real hole that exists today independently of NWSync — a module release currently does not record which topdata it shipped against at all.
- **Read the topdata `current` channel at build time** and do not record it. Simpler, but the manifest then describes a content set the release itself cannot name afterwards, and two builds of the same tag could differ.
Settle which, and whether changing this conflicts with the deploy-pinned parity rule sow-platform owns — pinning a topdata tag in the module release may or may not be the same thing as resolving a topdata channel, and that distinction is what `fetch-hak-manifest.sh:48-49` was protecting.
Pin a tag at release — but as a record, not a binding. The module build must fetch a topdata catalog anyway to get sow_top.hak and sow_tlk.tlk digests for the NWSync manifest; once it has, the tag falls out for free, so it gets written down.
The framing that settles it
The ticket asked whether pinning a topdata tag conflicts with the deploy-pinned parity rule fetch-hak-manifest.sh:48-49 was protecting. It does not, because that rule does not exist in the form the comment describes — and the record does not attempt it anyway.
sow-platform/releases/prod.yml says so in writing: "Game-tier artifacts (module, topdata, HAKs, and the future nwserver stack) release through Bunny channel catalogs in their owning repos and are never pinned here", and netcup-game "resolves its game-tier artifacts from Bunny channel catalogs and pins nothing here". Platform cannot pin game-tier artifacts; it follows channels (current, testing). No game-tier resolver exists in platform yet.
So topdata_release is provenance: it says "this .mod was built and manifested against topdata v0.4.7". Nothing reads it to decide what to install. Deploy still resolves topdata from its own channel and may legitimately run a different one. The comment on fetch-hak-manifest.sh:48-49 was guarding against binding module to topdata, which this is not.
This is not parity enforcement. Nothing checks whether a module built against last week's codebase is valid — no gate, no refusal, no automation. That judgment is human, made by choosing when to promote a channel and when to push a tag, and the scenarios vary (code unchanged, only haks added, and so on). The record is a receipt written after the fact, not a rule applied before it.
Version numbers across the four repos are expected to be uneven — v0.1.5 module pairing with v0.4.7 topdata is normal, because a repo with no changes does not bump. Nothing in the design compares version numbers: the channel indirects through channels.json and topdata_release is a free-form tag string copied verbatim.
Scope: topdata only, not four repos
At module build time the four repos divide like this:
repo
what the module build takes
channel variable
sow-assets-manifest
hak list + digests
SOW_MODULE_HAK_CHANNEL (exists — this is the assets-manifest channel; it is named for the content, not the repo)
sow-topdata
sow_top.hak + sow_tlk.tlk digests
SOW_MODULE_TOPDATA_CHANNEL (new — this ticket)
sow-codebase
nothing
n/a
sow-module
itself
the pushed tag
sow-codebase is the .NET/Anvil server code shipped as the nwserver container image (promote-nwserver.sh, a Gitea generic package whose channels.json values are 40-char commit SHAs, not version tags). It is not an input to crucible module build, contributes no resource that could appear in an NWSync manifest, and is a runtime peer the deploy resolves. Recording it would mean CI copying a SHA it never consumed — a guess, not a record. Codebase parity stays deploy-time, exactly as today.
Decisions
topdata_release is a record, not a binding. Provenance only; nothing reads it to deploy.
Two derived channel variables. Existing SOW_MODULE_HAK_CHANNEL plus new SOW_MODULE_TOPDATA_CHANNEL, both set from one tag-shape case arm in .gitea/workflows/release.yml:36-37 (v*-* → testing, v* → current). Pushing a tag stays the only input — no operator ever types a channel. Two variables rather than one because assets and topdata release independently and "rc haks, stable topdata" is a legitimate state to reproduce locally while debugging; CI sets both from the same arm, so a release can never diverge by accident.
Topdata only. Nothing recorded about sow-codebase (see above).
fetch-hak-manifest.sh → fetch-upstream-manifests.sh, extended to resolve the topdata channel as well. The script already owns "resolve upstream channels for this build" and already prepends sow_top; a second script would duplicate the same catalog shape, CDN, and fail-closed rules. Resolving at publish time was rejected outright — the build needs topdata resolved before it, or the two halves could observe different channel states.
Coordinated rename flip. The script name is a hardcoded path convention, not config: sow-tools/internal/app/app.go:388 runs <root>/scripts/fetch-hak-manifest, and crucible.sh:58 resolves the latest crucible release unpinned. Both sides flip together and the broken window is accepted; the failure is loud and unmistakable (required project script is missing). Smallest gap is merging the sow-module rename and cutting the crucible release back to back. Transitional symlink and Go-side fallback were both considered and rejected as cruft.
Keep the catalogs.fetch-upstream-manifests.sh writes .cache/hak-catalog.json and .cache/topdata-catalog.json verbatim, alongside .cache/hak-release.txt and .cache/topdata-release.txt. Assemble reads them locally and fetches nothing. Build and publish run in the same job (release.yml), so .cache survives between steps. This is also where the hak digests #58 found ("one deleted jq map away") come from — one change answers both sides. Re-fetching per tag would have been safe (per-tag catalogs are immutable) but needs a second fetch implementation and buys nothing.
Field and schema.topdata_release, a bare tag string, beside hak_release in releases/module/<tag>/haks.json. schema stays 1: the change is additive, and sow-depot-proxy/internal/server/downloads.go:234 decodes the catalog into a struct that ignores unknown fields and never reads schema. A bump would signal a break that is not there.
Hard fail on all three topdata failure modes — CDN unreachable, channel unset in channels.json, catalog present-but-malformed — matching the hak side (fetch-hak-manifest.sh:34,37,42,45), in CI and on developer machines alike. promote-topdata.sh:67-70 seeds testing to current when unset, so the realistic failure is a genuine outage or a genuinely unpublished channel, both of which should stop a build. A local-only soft fail was rejected: it produces a build that succeeds locally and fails in CI for the same tree.
sow_top stays hardcoded in the module_haks prepend (fetch-hak-manifest.sh:48). Catalogs supply digests only, so module_haks output is byte-identical and tests/fetch-hak-manifest-contract.sh needs no change beyond the rename.
No version suffixes on sow_top.hak / sow_tlk.tlk. Considered and rejected. Version identity already lives in two places that cannot drift — the release tag now in topdata_release, and the sha256 in the catalog; a versioned filename would be a third copy and the only one nothing verifies. It buys NWSync nothing: manifests list resources, never hak filenames, and blob names are the SHA-1 of each uncompressed resource, so client-side dedup is already per-resource. The costs are real — every topdata release would rewrite Mod_HakList, making the .mod binary itself differ by topdata tag (reintroducing exactly the lockstep this ticket keeps human); manual non-NWSync players would swap files each release and accumulate old ones; nwn-tool.yaml's hak_order and docs would need lockstep updates; and NWN's Mod_Hak / Mod_CustomTlkCExoString length budget is tight for a name plus semver suffix. If "which topdata is this, offline, from the file itself" is ever wanted, the shape is a small version resource insidesow_top.hak, not a rename — held as an idea, not adopted.
Implementation
Rides with #65 (wiring the consumer repos), not a new ticket. Touches: sow-module/scripts/fetch-hak-manifest.sh (rename + topdata resolution), publish-module-release.sh (read .cache/topdata-release.txt, emit topdata_release), sow-tools/internal/app/app.go:388, the two contract tests under sow-module/tests/, plus the stale name in .gitignore, flake.nix, nwn-tool.yaml and release.yml comments. The fetch-hak-manifest.sh:48-49 comment about parity being "deploy-pinned by sow-platform" should be corrected while there — platform pins nothing game-tier.
Follow-up filed
sow-depot-proxy#22 — resolve topdata_release into paired_topdata on the downloads page, mirroring paired_haks. Kept separate: it is a downloads-page feature, not NWSync publishing.
## Resolution
**Pin a tag at release — but as a record, not a binding.** The module build must fetch a topdata catalog anyway to get `sow_top.hak` and `sow_tlk.tlk` digests for the NWSync manifest; once it has, the tag falls out for free, so it gets written down.
### The framing that settles it
The ticket asked whether pinning a topdata tag conflicts with the deploy-pinned parity rule `fetch-hak-manifest.sh:48-49` was protecting. It does not, because that rule does not exist in the form the comment describes — and the record does not attempt it anyway.
`sow-platform/releases/prod.yml` says so in writing: *"Game-tier artifacts (module, topdata, HAKs, and the future nwserver stack) release through Bunny channel catalogs in their owning repos and are never pinned here"*, and netcup-game "resolves its game-tier artifacts from Bunny channel catalogs and pins nothing here". Platform cannot pin game-tier artifacts; it follows channels (`current`, `testing`). No game-tier resolver exists in platform yet.
So `topdata_release` is **provenance**: it says "this `.mod` was built and manifested against topdata `v0.4.7`". Nothing reads it to decide what to install. Deploy still resolves topdata from its own channel and may legitimately run a different one. The comment on `fetch-hak-manifest.sh:48-49` was guarding against *binding* module to topdata, which this is not.
**This is not parity enforcement.** Nothing checks whether a module built against last week's codebase is valid — no gate, no refusal, no automation. That judgment is human, made by choosing when to promote a channel and when to push a tag, and the scenarios vary (code unchanged, only haks added, and so on). The record is a receipt written after the fact, not a rule applied before it.
Version numbers across the four repos are expected to be uneven — `v0.1.5` module pairing with `v0.4.7` topdata is normal, because a repo with no changes does not bump. Nothing in the design compares version numbers: the channel indirects through `channels.json` and `topdata_release` is a free-form tag string copied verbatim.
### Scope: topdata only, not four repos
At module build time the four repos divide like this:
| repo | what the module build takes | channel variable |
|---|---|---|
| sow-assets-manifest | hak list + digests | `SOW_MODULE_HAK_CHANNEL` (exists — this **is** the assets-manifest channel; it is named for the content, not the repo) |
| sow-topdata | `sow_top.hak` + `sow_tlk.tlk` digests | `SOW_MODULE_TOPDATA_CHANNEL` (new — this ticket) |
| sow-codebase | nothing | n/a |
| sow-module | itself | the pushed tag |
sow-codebase is the .NET/Anvil server code shipped as the **nwserver container image** (`promote-nwserver.sh`, a Gitea generic package whose `channels.json` values are 40-char commit SHAs, not version tags). It is not an input to `crucible module build`, contributes no resource that could appear in an NWSync manifest, and is a runtime peer the deploy resolves. Recording it would mean CI copying a SHA it never consumed — a guess, not a record. Codebase parity stays deploy-time, exactly as today.
### Decisions
1. **`topdata_release` is a record, not a binding.** Provenance only; nothing reads it to deploy.
2. **Two derived channel variables.** Existing `SOW_MODULE_HAK_CHANNEL` plus new `SOW_MODULE_TOPDATA_CHANNEL`, both set from one tag-shape `case` arm in `.gitea/workflows/release.yml:36-37` (`v*-*` → `testing`, `v*` → `current`). Pushing a tag stays the only input — no operator ever types a channel. Two variables rather than one because assets and topdata release independently and "rc haks, stable topdata" is a legitimate state to reproduce locally while debugging; CI sets both from the same arm, so a release can never diverge by accident.
3. **Topdata only.** Nothing recorded about sow-codebase (see above).
4. **`fetch-hak-manifest.sh` → `fetch-upstream-manifests.sh`**, extended to resolve the topdata channel as well. The script already owns "resolve upstream channels for this build" and already prepends `sow_top`; a second script would duplicate the same catalog shape, CDN, and fail-closed rules. Resolving at publish time was rejected outright — the build needs topdata resolved before it, or the two halves could observe different channel states.
5. **Coordinated rename flip.** The script name is a hardcoded path convention, not config: `sow-tools/internal/app/app.go:388` runs `<root>/scripts/fetch-hak-manifest`, and `crucible.sh:58` resolves the *latest* crucible release unpinned. Both sides flip together and the broken window is accepted; the failure is loud and unmistakable (`required project script is missing`). Smallest gap is merging the sow-module rename and cutting the crucible release back to back. Transitional symlink and Go-side fallback were both considered and rejected as cruft.
6. **Keep the catalogs.** `fetch-upstream-manifests.sh` writes `.cache/hak-catalog.json` and `.cache/topdata-catalog.json` verbatim, alongside `.cache/hak-release.txt` and `.cache/topdata-release.txt`. Assemble reads them locally and fetches nothing. Build and publish run in the same job (`release.yml`), so `.cache` survives between steps. This is also where the hak digests #58 found ("one deleted `jq` map away") come from — one change answers both sides. Re-fetching per tag would have been safe (per-tag catalogs are immutable) but needs a second fetch implementation and buys nothing.
7. **Field and schema.** `topdata_release`, a bare tag string, beside `hak_release` in `releases/module/<tag>/haks.json`. `schema` stays `1`: the change is additive, and `sow-depot-proxy/internal/server/downloads.go:234` decodes the catalog into a struct that ignores unknown fields and never reads `schema`. A bump would signal a break that is not there.
8. **Hard fail on all three topdata failure modes** — CDN unreachable, channel unset in `channels.json`, catalog present-but-malformed — matching the hak side (`fetch-hak-manifest.sh:34,37,42,45`), in CI and on developer machines alike. `promote-topdata.sh:67-70` seeds `testing` to `current` when unset, so the realistic failure is a genuine outage or a genuinely unpublished channel, both of which should stop a build. A local-only soft fail was rejected: it produces a build that succeeds locally and fails in CI for the same tree.
9. **`sow_top` stays hardcoded** in the `module_haks` prepend (`fetch-hak-manifest.sh:48`). Catalogs supply digests only, so `module_haks` output is byte-identical and `tests/fetch-hak-manifest-contract.sh` needs no change beyond the rename.
10. **No version suffixes on `sow_top.hak` / `sow_tlk.tlk`.** Considered and rejected. Version identity already lives in two places that cannot drift — the release tag now in `topdata_release`, and the `sha256` in the catalog; a versioned filename would be a third copy and the only one nothing verifies. It buys NWSync nothing: manifests list **resources**, never hak filenames, and blob names are the SHA-1 of each uncompressed resource, so client-side dedup is already per-resource. The costs are real — every topdata release would rewrite `Mod_HakList`, making the `.mod` binary itself differ by topdata tag (reintroducing exactly the lockstep this ticket keeps human); manual non-NWSync players would swap files each release and accumulate old ones; `nwn-tool.yaml`'s `hak_order` and docs would need lockstep updates; and NWN's `Mod_Hak` / `Mod_CustomTlk` `CExoString` length budget is tight for a name plus semver suffix. If "which topdata is this, offline, from the file itself" is ever wanted, the shape is a small version resource *inside* `sow_top.hak`, not a rename — held as an idea, not adopted.
### Implementation
Rides with #65 (wiring the consumer repos), not a new ticket. Touches: `sow-module/scripts/fetch-hak-manifest.sh` (rename + topdata resolution), `publish-module-release.sh` (read `.cache/topdata-release.txt`, emit `topdata_release`), `sow-tools/internal/app/app.go:388`, the two contract tests under `sow-module/tests/`, plus the stale name in `.gitignore`, `flake.nix`, `nwn-tool.yaml` and `release.yml` comments. The `fetch-hak-manifest.sh:48-49` comment about parity being "deploy-pinned by sow-platform" should be corrected while there — platform pins nothing game-tier.
### Follow-up filed
[sow-depot-proxy#22](https://git.westgate.pw/ShadowsOverWestgate/sow-depot-proxy/issues/22) — resolve `topdata_release` into `paired_topdata` on the downloads page, mirroring `paired_haks`. Kept separate: it is a downloads-page feature, not NWSync publishing.
Decision 5 (Coordinated rename flip), crucible half: #84 flips internal/app/app.go:388 to scripts/fetch-upstream-manifests, matching the sow-module rename in ShadowsOverWestgate/sow-module#57. Tracked as #83.
Implemented as decided — no transitional symlink, no Go-side fallback, so the broken window is real: sow-module release CI runs nix flake update sow-tools unpinned, so every crucible module build there fails from the moment sow-module#57 merges until a crucible release carrying #84 is cut. Merging and releasing back to back keeps that gap short.
Decision 5 (Coordinated rename flip), crucible half: #84 flips `internal/app/app.go:388` to `scripts/fetch-upstream-manifests`, matching the sow-module rename in ShadowsOverWestgate/sow-module#57. Tracked as #83.
Implemented as decided — no transitional symlink, no Go-side fallback, so the broken window is real: sow-module release CI runs `nix flake update sow-tools` unpinned, so every crucible module build there fails from the moment sow-module#57 merges until a crucible release carrying #84 is cut. Merging and releasing back to back keeps that gap short.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Question
How does a module release know which topdata it shipped against?
Part of #54. #58 found that
publish-module-release.sh:59recordshak_release— the paired hak tag — but there is notopdata_releasefield.fetch-hak-manifest.sh:48-49prependssow_topby name only, deliberately, so module/topdata parity stays deploy-pinned by sow-platform.That was survivable while nothing at release time needed the topdata digest. Now sow-module CI assembles the manifest, and a manifest must cover
sow_top.hakandsow_tlk.tlkwith their digests. So the release has to resolve topdata somehow.Two shapes:
hak_release, written by the same preflight that writes.cache/hak-release.txt. Consistent with how haks are already handled, and it closes a real hole that exists today independently of NWSync — a module release currently does not record which topdata it shipped against at all.currentchannel at build time and do not record it. Simpler, but the manifest then describes a content set the release itself cannot name afterwards, and two builds of the same tag could differ.Settle which, and whether changing this conflicts with the deploy-pinned parity rule sow-platform owns — pinning a topdata tag in the module release may or may not be the same thing as resolving a topdata channel, and that distinction is what
fetch-hak-manifest.sh:48-49was protecting.Resolution
Pin a tag at release — but as a record, not a binding. The module build must fetch a topdata catalog anyway to get
sow_top.hakandsow_tlk.tlkdigests for the NWSync manifest; once it has, the tag falls out for free, so it gets written down.The framing that settles it
The ticket asked whether pinning a topdata tag conflicts with the deploy-pinned parity rule
fetch-hak-manifest.sh:48-49was protecting. It does not, because that rule does not exist in the form the comment describes — and the record does not attempt it anyway.sow-platform/releases/prod.ymlsays so in writing: "Game-tier artifacts (module, topdata, HAKs, and the future nwserver stack) release through Bunny channel catalogs in their owning repos and are never pinned here", and netcup-game "resolves its game-tier artifacts from Bunny channel catalogs and pins nothing here". Platform cannot pin game-tier artifacts; it follows channels (current,testing). No game-tier resolver exists in platform yet.So
topdata_releaseis provenance: it says "this.modwas built and manifested against topdatav0.4.7". Nothing reads it to decide what to install. Deploy still resolves topdata from its own channel and may legitimately run a different one. The comment onfetch-hak-manifest.sh:48-49was guarding against binding module to topdata, which this is not.This is not parity enforcement. Nothing checks whether a module built against last week's codebase is valid — no gate, no refusal, no automation. That judgment is human, made by choosing when to promote a channel and when to push a tag, and the scenarios vary (code unchanged, only haks added, and so on). The record is a receipt written after the fact, not a rule applied before it.
Version numbers across the four repos are expected to be uneven —
v0.1.5module pairing withv0.4.7topdata is normal, because a repo with no changes does not bump. Nothing in the design compares version numbers: the channel indirects throughchannels.jsonandtopdata_releaseis a free-form tag string copied verbatim.Scope: topdata only, not four repos
At module build time the four repos divide like this:
SOW_MODULE_HAK_CHANNEL(exists — this is the assets-manifest channel; it is named for the content, not the repo)sow_top.hak+sow_tlk.tlkdigestsSOW_MODULE_TOPDATA_CHANNEL(new — this ticket)sow-codebase is the .NET/Anvil server code shipped as the nwserver container image (
promote-nwserver.sh, a Gitea generic package whosechannels.jsonvalues are 40-char commit SHAs, not version tags). It is not an input tocrucible module build, contributes no resource that could appear in an NWSync manifest, and is a runtime peer the deploy resolves. Recording it would mean CI copying a SHA it never consumed — a guess, not a record. Codebase parity stays deploy-time, exactly as today.Decisions
topdata_releaseis a record, not a binding. Provenance only; nothing reads it to deploy.SOW_MODULE_HAK_CHANNELplus newSOW_MODULE_TOPDATA_CHANNEL, both set from one tag-shapecasearm in.gitea/workflows/release.yml:36-37(v*-*→testing,v*→current). Pushing a tag stays the only input — no operator ever types a channel. Two variables rather than one because assets and topdata release independently and "rc haks, stable topdata" is a legitimate state to reproduce locally while debugging; CI sets both from the same arm, so a release can never diverge by accident.fetch-hak-manifest.sh→fetch-upstream-manifests.sh, extended to resolve the topdata channel as well. The script already owns "resolve upstream channels for this build" and already prependssow_top; a second script would duplicate the same catalog shape, CDN, and fail-closed rules. Resolving at publish time was rejected outright — the build needs topdata resolved before it, or the two halves could observe different channel states.sow-tools/internal/app/app.go:388runs<root>/scripts/fetch-hak-manifest, andcrucible.sh:58resolves the latest crucible release unpinned. Both sides flip together and the broken window is accepted; the failure is loud and unmistakable (required project script is missing). Smallest gap is merging the sow-module rename and cutting the crucible release back to back. Transitional symlink and Go-side fallback were both considered and rejected as cruft.fetch-upstream-manifests.shwrites.cache/hak-catalog.jsonand.cache/topdata-catalog.jsonverbatim, alongside.cache/hak-release.txtand.cache/topdata-release.txt. Assemble reads them locally and fetches nothing. Build and publish run in the same job (release.yml), so.cachesurvives between steps. This is also where the hak digests #58 found ("one deletedjqmap away") come from — one change answers both sides. Re-fetching per tag would have been safe (per-tag catalogs are immutable) but needs a second fetch implementation and buys nothing.topdata_release, a bare tag string, besidehak_releaseinreleases/module/<tag>/haks.json.schemastays1: the change is additive, andsow-depot-proxy/internal/server/downloads.go:234decodes the catalog into a struct that ignores unknown fields and never readsschema. A bump would signal a break that is not there.channels.json, catalog present-but-malformed — matching the hak side (fetch-hak-manifest.sh:34,37,42,45), in CI and on developer machines alike.promote-topdata.sh:67-70seedstestingtocurrentwhen unset, so the realistic failure is a genuine outage or a genuinely unpublished channel, both of which should stop a build. A local-only soft fail was rejected: it produces a build that succeeds locally and fails in CI for the same tree.sow_topstays hardcoded in themodule_haksprepend (fetch-hak-manifest.sh:48). Catalogs supply digests only, somodule_haksoutput is byte-identical andtests/fetch-hak-manifest-contract.shneeds no change beyond the rename.sow_top.hak/sow_tlk.tlk. Considered and rejected. Version identity already lives in two places that cannot drift — the release tag now intopdata_release, and thesha256in the catalog; a versioned filename would be a third copy and the only one nothing verifies. It buys NWSync nothing: manifests list resources, never hak filenames, and blob names are the SHA-1 of each uncompressed resource, so client-side dedup is already per-resource. The costs are real — every topdata release would rewriteMod_HakList, making the.modbinary itself differ by topdata tag (reintroducing exactly the lockstep this ticket keeps human); manual non-NWSync players would swap files each release and accumulate old ones;nwn-tool.yaml'shak_orderand docs would need lockstep updates; and NWN'sMod_Hak/Mod_CustomTlkCExoStringlength budget is tight for a name plus semver suffix. If "which topdata is this, offline, from the file itself" is ever wanted, the shape is a small version resource insidesow_top.hak, not a rename — held as an idea, not adopted.Implementation
Rides with #65 (wiring the consumer repos), not a new ticket. Touches:
sow-module/scripts/fetch-hak-manifest.sh(rename + topdata resolution),publish-module-release.sh(read.cache/topdata-release.txt, emittopdata_release),sow-tools/internal/app/app.go:388, the two contract tests undersow-module/tests/, plus the stale name in.gitignore,flake.nix,nwn-tool.yamlandrelease.ymlcomments. Thefetch-hak-manifest.sh:48-49comment about parity being "deploy-pinned by sow-platform" should be corrected while there — platform pins nothing game-tier.Follow-up filed
sow-depot-proxy#22 — resolve
topdata_releaseintopaired_topdataon the downloads page, mirroringpaired_haks. Kept separate: it is a downloads-page feature, not NWSync publishing.Decision 5 (Coordinated rename flip), crucible half: #84 flips
internal/app/app.go:388toscripts/fetch-upstream-manifests, matching the sow-module rename in ShadowsOverWestgate/sow-module#57. Tracked as #83.Implemented as decided — no transitional symlink, no Go-side fallback, so the broken window is real: sow-module release CI runs
nix flake update sow-toolsunpinned, so every crucible module build there fails from the moment sow-module#57 merges until a crucible release carrying #84 is cut. Merging and releasing back to back keeps that gap short.