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

@@ -544,6 +544,13 @@ const {
RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
isAnthropicFlavoredModel: gsdIsAnthropicFlavoredModel,
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
// #4145: shared hash-first recovery for gsd-pristine/ baselines stored at an
// unexpected path (e.g. without the gsd-core/ prefix an earlier release's
// writer dropped). Same module the reapply verifier uses, so the two readers
// cannot drift apart again.
const {
findPristineByHash: gsdFindPristineByHash,
} = require(path.join(_gsdLibDir, 'pristine-baseline.cjs'));
// #2875 Part 2: MODEL_PROFILES + resolveTierEntry are now consumed only by
// install-model-override-resolver.cjs's readGsdRuntimeProfileResolver
// (required below) — this installer no longer needs its own bindings.
@@ -10153,6 +10160,63 @@ function populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathP
return written;
}
/**
* #4145: recover a pristine baseline from a hash-matching orphan stored at an
* unexpected path under gsd-pristine/ (e.g. without the gsd-core/ prefix an
* earlier release's writer dropped).
*
* The preserve-check's strict join (pristineDir + manifest-keyed relPath)
* misses such snapshots, so they were pushed into regeneration from the
* incoming release — and when the file changed upstream, the candidate's hash
* could never satisfy the recorded outgoing hash, leaving the correct
* baseline permanently unconsumed and unpruned (the self-perpetuating state
* #4145 reports). Hash equality with pristine_hashes is the same authority
* the #3657 drift guard trusts, so an exact match cannot be the wrong
* baseline no matter where under gsd-pristine/ it lives.
*
* Recovery = relocation: copy the orphan to the canonical manifest-keyed path
* (hash-verified after the copy) and remove the orphan only once the
* canonical copy is verified in place. Returns true when the canonical path
* ended up holding recorded-hash bytes. Never deletes anything it cannot
* vouch for by hash, and never consumes a path that is the canonical path of
* ANY manifest file (see canonicalSkip below) — only genuine orphans, which
* no strict-join reader ever consults, are eligible for removal.
*/
function recoverOrphanedPristine(pristineDir, relPath, recordedHash, canonicalSkip) {
if (!recordedHash) return false;
let orphanRel;
try {
// canonicalSkip = the normalized manifest keys: a file already sitting at
// any canonical path can never be (re-)adopted through the scan. Without
// this, two modified files sharing byte-identical outgoing content would
// repeatedly "rescue" each other's canonical away (relocate + delete at
// its home path) in alternating updates — bytes identical, state unstable.
// It also keeps drift (#3657) / stale (#3407) territory with the caller.
orphanRel = gsdFindPristineByHash(pristineDir, recordedHash, canonicalSkip);
} catch {
return false;
}
if (!orphanRel) return false;
const outRef = resolveInstallRelativePath(pristineDir, relPath);
if (!outRef) return false;
try {
fs.mkdirSync(path.dirname(outRef.fullPath), { recursive: true });
fs.copyFileSync(path.join(pristineDir, orphanRel), outRef.fullPath);
// Verify the relocated copy before removing the orphan — only a
// hash-matching canonical counts as recovered.
if (fileHash(outRef.fullPath) !== recordedHash) {
try { fs.rmSync(outRef.fullPath, { force: true }); } catch { /* best-effort */ }
return false;
}
// Orphan removal is best-effort: the canonical copy is already verified,
// so a failed unlink leaves a harmless duplicate, never data loss.
try { fs.rmSync(path.join(pristineDir, orphanRel), { force: true }); } catch { /* best-effort */ }
return true;
} catch {
return false;
}
}
/**
* Detect user-modified GSD files by comparing against install manifest.
* Backs up modified files to gsd-local-patches/ for reapply after update.
@@ -10299,6 +10363,15 @@ function saveLocalPatches(configDir, pristineCtx) {
const stalePaths = new Set();
// Track which relPaths were successfully regenerated (from either missing or stale).
const regeneratedPaths = new Set();
// #4145: track which relPaths were recovered by relocating a hash-matching
// orphan (stored at an unexpected path, e.g. without the gsd-core/ prefix).
const rescuedPaths = new Set();
// #4145: the set of paths that are SOME file's canonical pristine path
// (every normalized manifest key). The orphan scan must never consume
// these — see recoverOrphanedPristine.
const canonicalSkip = new Set(
Object.keys(manifest.files || {}).map((k) => normalizeInstallRelativePath(k)).filter(Boolean),
);
const missingPaths = [];
for (const relPath of modified) {
const outRef = resolveInstallRelativePath(pristineDir, relPath);
@@ -10320,6 +10393,17 @@ function saveLocalPatches(configDir, pristineCtx) {
stalePaths.add(relPath);
}
}
// #4145: canonical absent (or just removed as stale) — before falling
// into regeneration, try to recover the baseline from a hash-matching
// orphan elsewhere under gsd-pristine/ and relocate it to the canonical
// path. This is the self-heal for snapshots an earlier release stored
// without the gsd-core/ prefix: without it the state repeats forever
// (regeneration candidates from the incoming release can never satisfy
// the recorded outgoing hash when upstream changed the file).
if (recoverOrphanedPristine(pristineDir, relPath, pristineHashes[relPath], canonicalSkip)) {
rescuedPaths.add(relPath);
continue;
}
// File absent from gsd-pristine/ (or just removed above as stale):
// attempt hash-validated regeneration from new-release source.
missingPaths.push(relPath);
@@ -10362,13 +10446,21 @@ function saveLocalPatches(configDir, pristineCtx) {
}
// `regenerated` = total files successfully regenerated (from missing OR stale).
const regenerated = regeneratedPaths.size;
// `rescued` = files recovered by relocating a hash-matching orphan to its
// canonical path (#4145) — distinct from preservation (canonical already
// correct) and regeneration (bytes re-derived from new-release source).
const rescued = rescuedPaths.size;
// `removed` = stale entries that were deleted and NOT subsequently regenerated.
// Entries that were stale-deleted but then successfully regenerated are counted
// only in `regenerated` — the counts are non-overlapping.
const removed = [...stalePaths].filter(p => !regeneratedPaths.has(p)).length;
// only in `regenerated`; stale-deleted-then-orphan-rescued entries are counted
// only in `rescued` — the counts are non-overlapping.
const removed = [...stalePaths].filter(p => !regeneratedPaths.has(p) && !rescuedPaths.has(p)).length;
if (preserved > 0) {
console.log(' ' + green + '✓' + reset + ' Preserved ' + cyan + 'gsd-pristine/' + reset + ' (' + preserved + ' file(s)) for three-way merge');
}
if (rescued > 0) {
console.log(' ' + green + '✓' + reset + ' Recovered ' + cyan + 'gsd-pristine/' + reset + ' (' + rescued + ' file(s)) by recorded hash from a legacy-path snapshot and relocated them (#4145)');
}
if (regenerated > 0) {
console.log(' ' + green + '✓' + reset + ' Regenerated ' + cyan + 'gsd-pristine/' + reset + ' (' + regenerated + ' file(s)) via hash-validated new-release source');
}