fix(#4145): resolve gsd-pristine/ baselines by recorded hash, relocate orphans (#4364)

* test(#4145): regression rows for hash-matching prefix-less pristine baselines

RED skeleton: src/pristine-baseline.cts exports findPristineByHash as a
null-returning stub so the new rows fail behaviorally, not at require time.
Failing-first rows: verifier resolution (no_baseline must drop to 0 when an
exact-hash orphan exists), findPristineByHash unit row, and the two
saveLocalPatches relocation rows. Negative-space rows pin today's behavior:
missing baselines still report ok_no_baseline, mismatching orphans are never
adopted or deleted, canonical precedence and the #3657 drift posture are
untouched.

* fix(#4145): resolve gsd-pristine/ baselines by recorded hash, relocate orphans

Both pristine readers joined the manifest-keyed path strictly, so a snapshot
stored without the gsd-core/ prefix (an earlier release's writer) was reported
as ok_no_baseline by the verifier and pushed into regeneration by
saveLocalPatches — where incoming-release candidates can never satisfy the
recorded outgoing hash, leaving the correct baseline permanently unconsumed.

- src/pristine-baseline.cts (new, ADR-457): shared findPristineByHash —
  deterministic sorted scan of gsd-pristine/, exact sha-256 equality with the
  recorded pristine_hashes entry (the same authority the #3657 drift guard
  trusts), symlink-skipping, canonical path excluded via skipRel.
- verify-reapply-patches.cjs verifyFile(): on canonical miss with a recorded
  hash, adopt byte-identical content found anywhere under gsd-pristine/ before
  reporting OK_NO_BASELINE. Drift posture (#3657), canonical precedence, and
  the frozen REASON/report shapes are untouched; the verifier stays read-only.
- install.js saveLocalPatches(): preserve-check rescue — relocate a
  hash-matching orphan to the canonical path (copy, hash-verify, then remove
  the orphan) so the state self-heals on the next update instead of repeating
  forever. Honest accounting: new non-overlapping rescued counter.
- Workflow doc: one-sentence note on hash-based snapshot resolution.
- Derived ripples: INVENTORY-MANIFEST.json regen, eslint ignore + .gitignore
  entries for the compiled artifact, seedFixture mkdir fix in the new rows.

Emitted-Drift-Ack-Growth: reapply-patches.md — one-sentence note on hash-based pristine snapshot resolution (#4145)

* fix(#4145): review follow-up — orphan scan never consumes a canonical path

Adversarial review finding: with two modified files sharing byte-identical
outgoing content, recoverOrphanedPristine could adopt the OTHER file's
canonical pristine as its rescue source — relocating it (copy + delete at
its home path) and ping-ponging the single baseline between the two files
across updates. findPristineByHash's skip parameter now accepts a Set, and
saveLocalPatches passes the normalized manifest keys so every canonical
path is excluded; only genuine non-canonical orphans are eligible for
removal (no strict-join reader ever consults those). Adds the
canonical-theft regression row, a Set-skip unit assertion, and tightens the
workflow doc sentence the same pass flagged as overstated.

* fix(#4145): INVENTORY roster row + symlink-fixture correction

Two leftovers from the ab17b7a1e5 bench run, both root-caused:
- docs/INVENTORY.md roster row for cli_modules/pristine-baseline.cjs
  (#3762 gate: every manifest entry carries a row).
- The findPristineByHash symlink unit fixture placed its symlink target
  INSIDE the scanned root, so the walk legitimately matched the real target
  file. The implementation skips the symlink itself; the fixture now keeps
  the target outside the scanned tree so the assertion tests what it claims.

* changeset(#4145): fixed fragment for pristine baseline hash resolution

---------

Co-authored-by: gsd-agent <agent@gsd.local>
This commit is contained in:
Tom Boucher
2026-09-06 02:04:18 -04:00
committed by GitHub
parent d5a85da8ab
commit 6adf3098ac
11 changed files with 724 additions and 7 deletions

View File

@@ -4013,3 +4013,227 @@ describe('Bug #4086: saveLocalPatches resolves skills/ manifest keys at the runt
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded regression block — #4145 (self-heal side). saveLocalPatches'
// preserve-check resolved gsd-pristine/ entries strictly by the manifest-keyed
// path, so a hash-matching snapshot stored without the gsd-core/ prefix was
// pushed into regeneration from the incoming release; when upstream changed
// the file, the candidate hash-mismatched and was discarded. The correct
// baseline was never consumed and never pruned — the state repeated on every
// future update. The fix rescues exact-recorded-hash orphans by relocating
// them to the canonical path.
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe('folded:bug-4145-saveLocalPatches-orphan-rescue', () => {
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe, beforeEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const crypto = require('node:crypto');
const ROOT = path.join(__dirname, '..');
const INSTALL = require(path.join(ROOT, 'bin', 'install.js'));
const { cleanup } = require('./helpers.cjs');
const MANIFEST_NAME = 'gsd-file-manifest.json';
function sha256(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
describe('Bug #4145: saveLocalPatches rescues hash-matching orphaned pristine snapshots', () => {
let tmpDir;
let configDir;
let newSrcDir;
let pristineDir;
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
const OLD_PRISTINE = '# Old Release Content\nThis is the outgoing pristine.\n';
const NEW_RELEASE = '# New Release Content\nUpstream rewrote this file wholesale in v2.\n';
const USER_MODIFIED = OLD_PRISTINE + '## User addition\nUser customization here.\n';
beforeEach((t) => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-slp-'));
configDir = path.join(tmpDir, 'config');
newSrcDir = path.join(tmpDir, 'new-release-src');
pristineDir = path.join(configDir, 'gsd-pristine');
fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(newSrcDir, { recursive: true });
t.after(() => {
cleanup(tmpDir);
});
});
function seedFixture({ orphanRel, orphanContent, canonicalContent, newReleaseContent }) {
fs.mkdirSync(path.dirname(path.join(configDir, FILE)), { recursive: true });
fs.writeFileSync(path.join(configDir, FILE), USER_MODIFIED);
fs.writeFileSync(
path.join(configDir, MANIFEST_NAME),
JSON.stringify({ version: '1.0.0', files: { [FILE]: sha256(OLD_PRISTINE) } }, null, 2),
);
if (canonicalContent !== undefined) {
fs.mkdirSync(path.dirname(path.join(pristineDir, FILE)), { recursive: true });
fs.writeFileSync(path.join(pristineDir, FILE), canonicalContent);
}
if (orphanRel !== undefined) {
fs.mkdirSync(path.dirname(path.join(pristineDir, orphanRel)), { recursive: true });
fs.writeFileSync(path.join(pristineDir, orphanRel), orphanContent);
}
fs.mkdirSync(path.dirname(path.join(newSrcDir, FILE)), { recursive: true });
fs.writeFileSync(path.join(newSrcDir, FILE), newReleaseContent);
}
/**
* Core regression (self-heal): the hash-matching snapshot sits at
* bin/lib/frontmatter.cjs — without the gsd-core/ segment. The new release
* changed the file upstream, so regeneration candidates are discarded.
* After the fix the orphan is relocated to the canonical manifest-keyed
* path and the unprefixed copy no longer lingers.
*/
test('#4145: saveLocalPatches relocates a hash-matching unprefixed orphan to the canonical pristine path', () => {
const orphanRel = 'bin/lib/frontmatter.cjs';
seedFixture({ orphanRel, orphanContent: OLD_PRISTINE, newReleaseContent: NEW_RELEASE });
INSTALL.saveLocalPatches(configDir, {
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
});
const canonical = path.join(pristineDir, FILE);
assert.ok(fs.existsSync(canonical), 'canonical prefixed pristine must exist after the update');
assert.equal(sha256(fs.readFileSync(canonical, 'utf8')), sha256(OLD_PRISTINE),
'relocated baseline must carry the outgoing (recorded-hash) bytes, not new-release bytes');
assert.equal(fs.existsSync(path.join(pristineDir, orphanRel)), false,
'the unprefixed orphan must not linger once relocated');
});
/**
* Stale canonical (new-release bytes) + hash-matching orphan elsewhere:
* the stale entry is removed by the #3407 path and then rescued from the
* orphan — the file must not end in no-baseline limbo.
*/
test('#4145: rescues after stale-canonical removal when a hash-matching orphan exists', () => {
seedFixture({
orphanRel: 'legacy/frontmatter.cjs',
orphanContent: OLD_PRISTINE,
canonicalContent: NEW_RELEASE, // stale — hash mismatch
newReleaseContent: NEW_RELEASE,
});
INSTALL.saveLocalPatches(configDir, {
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
});
const canonical = path.join(pristineDir, FILE);
assert.ok(fs.existsSync(canonical), 'canonical pristine must exist after stale removal + rescue');
assert.equal(sha256(fs.readFileSync(canonical, 'utf8')), sha256(OLD_PRISTINE),
'rescued baseline must carry the recorded-hash bytes');
assert.equal(fs.existsSync(path.join(pristineDir, 'legacy', 'frontmatter.cjs')), false,
'the orphan must be consumed by the relocation');
});
/** Negative space: no orphan, upstream changed — regeneration discard (#3407) is unchanged. */
test('#4145: leaves the baseline absent when no orphan exists and upstream changed', () => {
seedFixture({ newReleaseContent: NEW_RELEASE });
INSTALL.saveLocalPatches(configDir, {
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
});
assert.equal(fs.existsSync(path.join(pristineDir, FILE)), false,
'no hash-matching source exists — the baseline must stay absent (over-broad/no-baseline fallback)');
});
/** Negative space: a mismatching orphan is neither adopted nor deleted. */
test('#4145: never adopts nor deletes a hash-mismatching orphan', () => {
const orphanRel = 'bin/lib/frontmatter.cjs';
seedFixture({ orphanRel, orphanContent: NEW_RELEASE, newReleaseContent: NEW_RELEASE });
INSTALL.saveLocalPatches(configDir, {
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
});
assert.equal(fs.existsSync(path.join(pristineDir, FILE)), false,
'mismatching bytes must not be written to the canonical pristine path');
assert.equal(fs.existsSync(path.join(pristineDir, orphanRel)), true,
'pruning files the recorded hashes do not vouch for is not this fix\'s job');
});
/**
* Review finding (fix follow-up): two modified files sharing byte-identical
* outgoing content. The orphan scan must never consume a path that is
* another manifest file's canonical pristine path — otherwise the rescue
* would relocate a correct canonical away from its owner and the two files
* would ping-pong it between updates. Only genuine non-canonical orphans
* are eligible.
*/
test('#4145: does not steal a byte-identical canonical belonging to another modified file', () => {
const FILE_B = 'gsd-core/bin/lib/other-file.cjs';
const SHARED_OLD = '# Shared Old Content\nByte-identical across two manifest files.\n';
// A and B are both user-modified on top of byte-identical outgoing stock,
// so both manifest records carry the SAME pristine hash.
fs.mkdirSync(path.dirname(path.join(configDir, FILE_B)), { recursive: true });
fs.writeFileSync(path.join(configDir, FILE), SHARED_OLD + '## User addition A\nCustom A.\n');
fs.writeFileSync(path.join(configDir, FILE_B), SHARED_OLD + '## User addition B\nCustom B.\n');
fs.writeFileSync(
path.join(configDir, MANIFEST_NAME),
JSON.stringify({
version: '1.0.0',
files: { [FILE]: sha256(SHARED_OLD), [FILE_B]: sha256(SHARED_OLD) },
}, null, 2),
);
// A (processed first) has the ALREADY-correct canonical holding the shared
// old bytes. B has no canonical and no orphan — B's only possible hash
// match is A's canonical. Without the canonical skip set, B's rescue would
// copy A's canonical to B's path and then DELETE A's canonical.
fs.mkdirSync(path.dirname(path.join(pristineDir, FILE)), { recursive: true });
fs.writeFileSync(path.join(pristineDir, FILE), SHARED_OLD);
fs.mkdirSync(path.dirname(path.join(newSrcDir, FILE)), { recursive: true });
fs.writeFileSync(path.join(newSrcDir, FILE), NEW_RELEASE);
fs.mkdirSync(path.dirname(path.join(newSrcDir, FILE_B)), { recursive: true });
fs.writeFileSync(path.join(newSrcDir, FILE_B), NEW_RELEASE);
INSTALL.saveLocalPatches(configDir, {
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
});
// A's canonical must survive untouched — never stolen to become B's.
assert.ok(fs.existsSync(path.join(pristineDir, FILE)),
'the byte-identical canonical of the earlier-processed file must survive');
assert.equal(sha256(fs.readFileSync(path.join(pristineDir, FILE), 'utf8')), sha256(SHARED_OLD));
// B gains no baseline from A's canonical (falls to regeneration instead).
assert.equal(fs.existsSync(path.join(pristineDir, FILE_B)), false,
'a canonical path of another file must never be relocated as the rescue source');
});
/** Preserve-path lock: an already-correct canonical stays put; the preserve loop ignores the orphan. */
test('#4145: preserves an already-correct canonical and leaves a coexisting identical orphan in place', () => {
const orphanRel = 'bin/lib/frontmatter.cjs';
seedFixture({
orphanRel,
orphanContent: OLD_PRISTINE,
canonicalContent: OLD_PRISTINE, // already correct
newReleaseContent: NEW_RELEASE,
});
INSTALL.saveLocalPatches(configDir, {
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
});
const canonical = path.join(pristineDir, FILE);
assert.ok(fs.existsSync(canonical));
assert.equal(sha256(fs.readFileSync(canonical, 'utf8')), sha256(OLD_PRISTINE),
'preserved canonical must be byte-identical to before the run');
assert.equal(fs.existsSync(path.join(pristineDir, orphanRel)), true,
'the preserve path must not disturb unrelated files');
});
});
});
}