From 01a2e7d05f8e5981b0de3a3a28506475228e0a21 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 12 Sep 2026 16:14:20 -0400 Subject: [PATCH 1/2] fix(ci): register release-notes with moon and fix its manifest path spread (RIG-3731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/release-notes carried a moon.yml with typecheck/test/ci tasks but was absent from .moon/workspace.yml. moon discovers projects only from that explicit map, so the project was invisible — `moon project release-notes` returned project_graph::unknown_id, and its 22 tests and typecheck had NEVER run in CI, while the release lane depends on it to generate the Release body appendix and the nix-outputs.json manifest. This is the same hazard the macos-bundle entry already warns about in situ: an unregistered tool is silently inert and ships untested. Registering it immediately surfaced a real defect the missing gate had hidden: index.ts(312,49): error TS2783: 'path' is specified more than once, so this usage will be overwritten. In the keyed form of `nix path-info --json`, the object KEY is the authoritative store path, but it was spread BEFORE the value, so a `path` field inside the value would overwrite it and record a wrong store path in the manifest. Spread the value first and let the key win. Not a type silencer — it corrects which store path the manifest reports. Audited the whole repo: release-notes was the only unregistered tool project, so this gap is isolated rather than systemic. Verified: `moon project release-notes` resolves (was unknown_id); `moon run release-notes:ci` is green — 22 pass / 0 fail, typecheck clean (was exit 2); the generator's --dry-run emits 8 named nix outputs each with a narHash and real /nix/store paths; biome clean; sea-ref-gate and orion-ref-gate clean. Ledger-impact: none — CI project registration and a local bug fix. Refs RIG-3731 --- .moon/workspace.yml | 7 +++++++ tools/release-notes/index.ts | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.moon/workspace.yml b/.moon/workspace.yml index a718eec44..a6191a5d8 100644 --- a/.moon/workspace.yml +++ b/.moon/workspace.yml @@ -164,6 +164,13 @@ projects: # (runInCI:false) — the shell compile itself is covered by the gtk4 e2e + # darwin CI lanes. compass-app-dev: 'tools/compass-app-dev' + # The release-notes generator (compass-distribution §18): assembles the + # Release body appendix + the machine-readable nix-outputs.json manifest from + # the built asset names, the resolved container image and the pinned nix + # toolchain store paths. Registered so its typecheck + unit tests ride the + # moon-driven CI sweep — it was unregistered until RIG-3731, so the release + # lane depended on a tool whose 22 tests never ran in CI. + release-notes: 'tools/release-notes' # File hashing uses moon's in-process native walker (the v2.5+ default), NOT the # git shell-out. This is load-bearing: the git hasher (`git ls-files` then `git diff --git a/tools/release-notes/index.ts b/tools/release-notes/index.ts index a58a36be4..1bca24086 100644 --- a/tools/release-notes/index.ts +++ b/tools/release-notes/index.ts @@ -306,10 +306,13 @@ async function gatherNixOutputs(): Promise { const info = JSON.parse(infoJson) as | PathInfoEntry[] | Record; - // nix path-info emits an array (newer nix) or an object keyed by path. + // nix path-info emits an array (newer nix) or an object keyed by path. In + // the keyed form the KEY is the authoritative store path, so it is spread + // last — the value may carry its own `path` field, which would otherwise + // overwrite the key (TS2783). const entries: PathInfoEntry[] = Array.isArray(info) ? info - : Object.entries(info).map(([path, v]) => ({ path, ...v })); + : Object.entries(info).map(([path, v]) => ({ ...v, path })); // A single-store-path query returns exactly one entry; anything else means // `store` did not resolve to one output path and picking [0] would record // an arbitrary identity — fail loud rather than ship a wrong manifest entry. From 17842706e150888354a57a2fb67d0c470d066073 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 12 Sep 2026 16:55:22 -0400 Subject: [PATCH 2/2] refactor(release-notes): model the two nix path-info shapes distinctly (RIG-3731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up, and a correction to the parent commit's claim. The parent said reordering the spread to `{ ...v, path }` "corrects which store path the manifest reports". That is WRONG, and I verified it against real nix rather than reasoning about the type: `nix path-info --json` (2.34.7, the CI nix) emits the keyed form, and its values carry ca, deriver, narHash, narSize, references, registrationTime, signatures, storeDir, ultimate, version with NO `path` key. So both spread orders already produced the authoritative key, and the shipped manifest was never wrong. The reorder was a TS2783 silencer that happened to also be correct in principle. The actual root cause was the type model: PathInfoEntry was used for BOTH the array element (which does carry `path`) and the keyed-form value (which does not, because the path IS the key). Declaring `path: string` on the keyed value made TS believe `v.path` exists, which is what produced the duplicate-key error. Model the two shapes distinctly — `KeyedPathInfo = Omit` in the Record position — so the type matches what nix actually emits and the spread order stops mattering at all. Deliberately NOT adding a regression test for the duplicate-`path` divergence: it is unreachable with real nix, so a test would assert behaviour on a shape nix never emits. That is padding, not coverage. Verified: `moon run release-notes:ci` green — 22 pass / 0 fail, tsc rc=0; probed `nix path-info --json` directly to confirm the keyed value has no `path` field. Ledger-impact: none — type model only, no behaviour change. Refs RIG-3731 --- tools/release-notes/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/release-notes/index.ts b/tools/release-notes/index.ts index 1bca24086..597e20f8d 100644 --- a/tools/release-notes/index.ts +++ b/tools/release-notes/index.ts @@ -282,8 +282,14 @@ async function gatherImage(sha: string): Promise { }); } -/** The `nix path-info --json` record shape (the fields the manifest reads). */ +/** + * The `nix path-info --json` record shape (the fields the manifest reads). + * The array form carries its own `path`; the keyed form does not — there the + * store path IS the object key, so it is modelled separately rather than + * pretending the value carries one. + */ type PathInfoEntry = { path: string; narHash?: string }; +type KeyedPathInfo = Omit; /** * Resolve the toolchain `langs` set to store paths and run `nix path-info` over @@ -305,11 +311,9 @@ async function gatherNixOutputs(): Promise { const infoJson = await $`nix path-info --json ${store}`.quiet().text(); const info = JSON.parse(infoJson) as | PathInfoEntry[] - | Record; - // nix path-info emits an array (newer nix) or an object keyed by path. In - // the keyed form the KEY is the authoritative store path, so it is spread - // last — the value may carry its own `path` field, which would otherwise - // overwrite the key (TS2783). + | Record; + // nix path-info emits an array (newer nix) or an object keyed by path; in + // the keyed form the key is the authoritative store path. const entries: PathInfoEntry[] = Array.isArray(info) ? info : Object.entries(info).map(([path, v]) => ({ ...v, path }));