diff --git a/docs/adr/2264-golden-parity-redesign.md b/docs/adr/2264-golden-parity-redesign.md index 0702f09c9..c4dcfa6ff 100644 --- a/docs/adr/2264-golden-parity-redesign.md +++ b/docs/adr/2264-golden-parity-redesign.md @@ -90,4 +90,6 @@ Implementing Phase 2 began with an empirical spike classifying every emitted ins 1. **File-set snapshot** — `tests/golden-install-tree.test.cjs` + `tests/fixtures/install-tree/*.json`: a per-runtime sorted list of emitted paths (no hashes), reusing the single-source exclusions via `buildInstallTree` (= sorted keys of `buildParityManifest`). Cheap structural coverage; changes only when the file *set* changes, giving a clean reviewable diff instead of hash noise. 2. **Anti-staleness CI selection** — `scripts/ci-test-scope.cjs` gains a rule selecting `golden-install-parity` + the file-set snapshot whenever an installed-source path changes: the four `gsd-core/` content subtrees (`workflows`, `templates`, `references`, `contexts`) + `gsd-core/bin/shared/*.json`, plus `hooks/**`, `commands/**`, `agents/**`, `skills/**`, and the shipped `scripts/*` files (`scripts/changeset/`, `scripts/lib/`, and three named generators) — not just `src/`/installer paths. `gsd-core/bin/**` (tsc-compiled) is deliberately excluded (already covered by the installer rule). This makes the #2266 silent-staleness class impossible: a source edit that changes emitted output re-verifies the fixtures at PR time. +3. **Regeneration convenience** (Phase 3, #2268) — an `npm run gen:golden` one-command regenerator (both fixture sets, each now self-building `hooks/dist`) that the golden-parity/tree failure messages point at, so drift caught by (2) is a one-command fix. The full CI-auto-comment (auto-posting the fixture patch on drift, jest `--ci` posture) is deferred: it needs a write-token workflow running on PR code (an injection surface) that cannot be verified locally. + **Deferred as an optional future refinement:** the §3-style content-dedup of verbatim copies. The *safe* subset (structurally raw-copied non-`.md`/`.js` data files) is modest, and the broad form is unsound — a `.md` file that is byte-equal today transforms the moment a self-reference is added, producing false property-test failures. Since the anti-staleness rule already removes the root pain (silent stale merges) and the file-set snapshot adds structural coverage, the content-hash golden is retained as-is; churn on *transformed* output is legitimate (you should review transformed output when you change its source). diff --git a/package.json b/package.json index e9770428c..95acd9e1c 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write", "gen:capability-registry": "node scripts/gen-capability-registry.cjs --write", "gen:registry": "node scripts/gen-registry.cjs --write", + "gen:golden": "node scripts/gen-golden-install-parity-zcode.cjs && node scripts/gen-install-tree-fixtures.cjs", "validate:registry": "node scripts/validate-registry.cjs", "prepack": "npm run build:lib", "prepare": "npm run build:lib", diff --git a/tests/golden-install-parity.test.cjs b/tests/golden-install-parity.test.cjs index e14d3d09b..df6a84b25 100644 --- a/tests/golden-install-parity.test.cjs +++ b/tests/golden-install-parity.test.cjs @@ -90,7 +90,7 @@ for (const runtime of runtimes) { if (!fs.existsSync(fixturePath)) { assert.fail( `Golden fixture missing for runtime '${runtime}': ${fixturePath}\n` + - 'Run UPDATE_GOLDEN=1 node --test tests/golden-install-parity.test.cjs to capture.' + 'Run "npm run gen:golden" to generate the fixtures (or, for humans, UPDATE_GOLDEN=1 node --test tests/golden-install-parity.test.cjs).' ); } @@ -108,7 +108,7 @@ for (const runtime of runtimes) { if (added.length) lines.push(` added (${added.length}): ${added.join(', ')}`); if (removed.length) lines.push(` removed (${removed.length}): ${removed.join(', ')}`); if (changed.length) lines.push(` changed (${changed.length}): ${changed.join(', ')}`); - lines.push('Run UPDATE_GOLDEN=1 to recapture if the change is intentional.'); + lines.push('If the change is intentional, run "npm run gen:golden" to regenerate the fixtures (works without node --test; humans may also use UPDATE_GOLDEN=1 node --test).'); assert.deepEqual(actual, golden, lines.join('\n')); } }); @@ -146,7 +146,7 @@ test('golden parity — claude (local legacy layout)', async (t) => { if (!fs.existsSync(fixturePath)) { assert.fail( `Golden fixture missing for claude-local: ${fixturePath}\n` + - 'Run UPDATE_GOLDEN=1 node --test tests/golden-install-parity.test.cjs to capture.', + 'Run "npm run gen:golden" to generate the fixtures (or, for humans, UPDATE_GOLDEN=1 node --test tests/golden-install-parity.test.cjs).', ); } @@ -159,7 +159,7 @@ test('golden parity — claude (local legacy layout)', async (t) => { if (added.length) lines.push(` added (${added.length}): ${added.join(', ')}`); if (removed.length) lines.push(` removed (${removed.length}): ${removed.join(', ')}`); if (changed.length) lines.push(` changed (${changed.length}): ${changed.join(', ')}`); - lines.push('Run UPDATE_GOLDEN=1 to recapture if the change is intentional.'); + lines.push('If the change is intentional, run "npm run gen:golden" to regenerate the fixtures (works without node --test; humans may also use UPDATE_GOLDEN=1 node --test).'); assert.deepEqual(actual, golden, lines.join('\n')); } }); diff --git a/tests/golden-install-tree.test.cjs b/tests/golden-install-tree.test.cjs index 5ea28e564..d18080aaf 100644 --- a/tests/golden-install-tree.test.cjs +++ b/tests/golden-install-tree.test.cjs @@ -86,7 +86,7 @@ for (const runtime of runtimes) { if (!fs.existsSync(fixturePath)) { assert.fail( `Install-tree fixture missing for runtime '${runtime}': ${fixturePath}\n` + - 'Run UPDATE_INSTALL_TREE=1 node --test tests/golden-install-tree.test.cjs to capture.' + 'Run "npm run gen:golden" to generate the fixtures (or, for humans, UPDATE_INSTALL_TREE=1 node --test tests/golden-install-tree.test.cjs).' ); } @@ -98,7 +98,7 @@ for (const runtime of runtimes) { const lines = [`Install-tree mismatch for runtime '${runtime}':`]; if (added.length) lines.push(` added (${added.length}): ${added.join(', ')}`); if (removed.length) lines.push(` removed (${removed.length}): ${removed.join(', ')}`); - lines.push('Run UPDATE_INSTALL_TREE=1 to recapture if the change is intentional.'); + lines.push('If the change is intentional, run "npm run gen:golden" to regenerate the fixtures (works without node --test; humans may also use UPDATE_INSTALL_TREE=1 node --test).'); assert.deepEqual(actual, fixture, lines.join('\n')); } }); @@ -132,7 +132,7 @@ test('install tree — claude (local legacy layout)', async (t) => { if (!fs.existsSync(fixturePath)) { assert.fail( `Install-tree fixture missing for claude-local: ${fixturePath}\n` + - 'Run UPDATE_INSTALL_TREE=1 node --test tests/golden-install-tree.test.cjs to capture.', + 'Run "npm run gen:golden" to generate the fixtures (or, for humans, UPDATE_INSTALL_TREE=1 node --test tests/golden-install-tree.test.cjs).', ); } @@ -142,7 +142,7 @@ test('install tree — claude (local legacy layout)', async (t) => { const lines = ['Install-tree mismatch for claude-local:']; if (added.length) lines.push(` added (${added.length}): ${added.join(', ')}`); if (removed.length) lines.push(` removed (${removed.length}): ${removed.join(', ')}`); - lines.push('Run UPDATE_INSTALL_TREE=1 to recapture if the change is intentional.'); + lines.push('If the change is intentional, run "npm run gen:golden" to regenerate the fixtures (works without node --test; humans may also use UPDATE_INSTALL_TREE=1 node --test).'); assert.deepEqual(actual, fixture, lines.join('\n')); } });