* 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:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user