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

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 4364
---
**`/gsd-update --reapply` no longer reports no_baseline when a hash-matching gsd-pristine/ snapshot is stored without the gsd-core/ prefix** — the verifier and the installer now resolve the baseline by the recorded SHA-256 and relocate the orphaned snapshot to its canonical path on the next update, so the correct baseline is finally consumed instead of sitting unusable forever. (#4145)

2
.gitignore vendored
View File

@@ -138,6 +138,8 @@ build/
/gsd-core/bin/lib/ui-consideration-probe.cjs /gsd-core/bin/lib/ui-consideration-probe.cjs
/gsd-core/bin/lib/config-types.cjs /gsd-core/bin/lib/config-types.cjs
/gsd-core/bin/lib/cli-exit.cjs /gsd-core/bin/lib/cli-exit.cjs
# #4145: emitted artifact of src/pristine-baseline.cts — never edited.
/gsd-core/bin/lib/pristine-baseline.cjs
/gsd-core/bin/lib/code-review-flags.cjs /gsd-core/bin/lib/code-review-flags.cjs
/gsd-core/bin/lib/code-review-depth.cjs /gsd-core/bin/lib/code-review-depth.cjs
/gsd-core/bin/lib/context-utilization.cjs /gsd-core/bin/lib/context-utilization.cjs

View File

@@ -544,6 +544,13 @@ const {
RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP, RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
isAnthropicFlavoredModel: gsdIsAnthropicFlavoredModel, isAnthropicFlavoredModel: gsdIsAnthropicFlavoredModel,
} = require(path.join(_gsdLibDir, 'model-catalog.cjs')); } = 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 // #2875 Part 2: MODEL_PROFILES + resolveTierEntry are now consumed only by
// install-model-override-resolver.cjs's readGsdRuntimeProfileResolver // install-model-override-resolver.cjs's readGsdRuntimeProfileResolver
// (required below) — this installer no longer needs its own bindings. // (required below) — this installer no longer needs its own bindings.
@@ -10153,6 +10160,63 @@ function populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathP
return written; 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. * Detect user-modified GSD files by comparing against install manifest.
* Backs up modified files to gsd-local-patches/ for reapply after update. * Backs up modified files to gsd-local-patches/ for reapply after update.
@@ -10299,6 +10363,15 @@ function saveLocalPatches(configDir, pristineCtx) {
const stalePaths = new Set(); const stalePaths = new Set();
// Track which relPaths were successfully regenerated (from either missing or stale). // Track which relPaths were successfully regenerated (from either missing or stale).
const regeneratedPaths = new Set(); 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 = []; const missingPaths = [];
for (const relPath of modified) { for (const relPath of modified) {
const outRef = resolveInstallRelativePath(pristineDir, relPath); const outRef = resolveInstallRelativePath(pristineDir, relPath);
@@ -10320,6 +10393,17 @@ function saveLocalPatches(configDir, pristineCtx) {
stalePaths.add(relPath); 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): // File absent from gsd-pristine/ (or just removed above as stale):
// attempt hash-validated regeneration from new-release source. // attempt hash-validated regeneration from new-release source.
missingPaths.push(relPath); missingPaths.push(relPath);
@@ -10362,13 +10446,21 @@ function saveLocalPatches(configDir, pristineCtx) {
} }
// `regenerated` = total files successfully regenerated (from missing OR stale). // `regenerated` = total files successfully regenerated (from missing OR stale).
const regenerated = regeneratedPaths.size; 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. // `removed` = stale entries that were deleted and NOT subsequently regenerated.
// Entries that were stale-deleted but then successfully regenerated are counted // Entries that were stale-deleted but then successfully regenerated are counted
// only in `regenerated` — the counts are non-overlapping. // only in `regenerated`; stale-deleted-then-orphan-rescued entries are counted
const removed = [...stalePaths].filter(p => !regeneratedPaths.has(p)).length; // only in `rescued` — the counts are non-overlapping.
const removed = [...stalePaths].filter(p => !regeneratedPaths.has(p) && !rescuedPaths.has(p)).length;
if (preserved > 0) { if (preserved > 0) {
console.log(' ' + green + '✓' + reset + ' Preserved ' + cyan + 'gsd-pristine/' + reset + ' (' + preserved + ' file(s)) for three-way merge'); 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) { if (regenerated > 0) {
console.log(' ' + green + '✓' + reset + ' Regenerated ' + cyan + 'gsd-pristine/' + reset + ' (' + regenerated + ' file(s)) via hash-validated new-release source'); console.log(' ' + green + '✓' + reset + ' Regenerated ' + cyan + 'gsd-pristine/' + reset + ' (' + regenerated + ' file(s)) via hash-validated new-release source');
} }

View File

@@ -475,6 +475,7 @@
"planning-scope.cjs", "planning-scope.cjs",
"planning-snapshot.cjs", "planning-snapshot.cjs",
"planning-workspace.cjs", "planning-workspace.cjs",
"pristine-baseline.cjs",
"probe-core.cjs", "probe-core.cjs",
"profile-output.cjs", "profile-output.cjs",
"profile-pipeline-command-router.cjs", "profile-pipeline-command-router.cjs",

View File

@@ -603,6 +603,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `planning-scope.cjs` | Frozen `SCOPE` discriminator (`COMPLETE`/`TRUNCATED`/`UNSCOPED`/`UNREADABLE`) distinguishing a genuinely-empty derivation from one computed over a truncated or unscoped input, so callers can branch on the difference instead of reading a plausible zero (ADR-3180) | | `planning-scope.cjs` | Frozen `SCOPE` discriminator (`COMPLETE`/`TRUNCATED`/`UNSCOPED`/`UNREADABLE`) distinguishing a genuinely-empty derivation from one computed over a truncated or unscoped input, so callers can branch on the difference instead of reading a plausible zero (ADR-3180) |
| `planning-snapshot.cjs` | Parsed projection of `.planning/` composed exclusively from the ADR-3180 §7 owners (milestone identity, phase enumeration, phase completion, plan/summary counting, STATE.md current-phase) — exposes only scope-carrying parsed values, never raw document text, so a diagnostic rule cannot re-derive a field's location (ADR-3180 §8.1) | | `planning-snapshot.cjs` | Parsed projection of `.planning/` composed exclusively from the ADR-3180 §7 owners (milestone identity, phase enumeration, phase completion, plan/summary counting, STATE.md current-phase) — exposes only scope-carrying parsed values, never raw document text, so a diagnostic rule cannot re-derive a field's location (ADR-3180 §8.1) |
| `planning-workspace.cjs` | Planning path/workstream seam (`planningDir`, `planningPaths`, active-workstream routing, `.planning/.lock` orchestration) | | `planning-workspace.cjs` | Planning path/workstream seam (`planningDir`, `planningPaths`, active-workstream routing, `.planning/.lock` orchestration) |
| `pristine-baseline.cjs` | Hash-first recovery for `gsd-pristine/` baselines stored at an unexpected path (compiled from `src/pristine-baseline.cts`, gitignored; #4145) — `findPristineByHash(pristineDir, recordedHash, skip?)` walks `gsd-pristine/` in deterministic sorted order, skips symlinks, and returns the first file whose SHA-256 equals the recorded `backup-meta.json.pristine_hashes` entry (the same authority the #3657 drift guard trusts); the `skip` set excludes canonical manifest-keyed paths so a relocation never consumes another file's canonical baseline. Shared by `verify-reapply-patches.cjs`'s `verifyFile` (read-only adoption when the strict join misses) and `install.js`'s `saveLocalPatches` (orphan relocation self-heal) so the two readers cannot drift apart again |
| `project-root.cjs` | Resolves a project root from a starting directory using four heuristics (own `.planning/` guard, `sub_repos` config, `multiRepo` flag, `.git` heuristic) | | `project-root.cjs` | Resolves a project root from a starting directory using four heuristics (own `.planning/` guard, `sub_repos` config, `multiRepo` flag, `.git` heuristic) |
| `profile-output.cjs` | Profile rendering, USER-PROFILE.md and dev-preferences.md generation | | `profile-output.cjs` | Profile rendering, USER-PROFILE.md and dev-preferences.md generation |
| `profile-pipeline-command-router.cjs` | ADR-959 capability command router for the profile-pipeline command family — dispatches scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase); phase 6 cutover | | `profile-pipeline-command-router.cjs` | ADR-959 capability command router for the profile-pipeline command family — dispatches scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase); phase 6 cutover |

View File

@@ -126,6 +126,8 @@ export default tseslint.config(
'gsd-core/bin/lib/prohibition-enforcement.cjs', 'gsd-core/bin/lib/prohibition-enforcement.cjs',
// #3770: tsc-generated runtime artifact — lint the src/tdd-red-evidence.cts source. // #3770: tsc-generated runtime artifact — lint the src/tdd-red-evidence.cts source.
'gsd-core/bin/lib/tdd-red-evidence.cjs', 'gsd-core/bin/lib/tdd-red-evidence.cjs',
// #4145: tsc-generated runtime artifact — lint the src/pristine-baseline.cts source.
'gsd-core/bin/lib/pristine-baseline.cjs',
'gsd-core/bin/lib/ui-consideration-probe.cjs', 'gsd-core/bin/lib/ui-consideration-probe.cjs',
'gsd-core/bin/lib/code-review-flags.cjs', 'gsd-core/bin/lib/code-review-flags.cjs',
'gsd-core/bin/lib/code-review-depth.cjs', 'gsd-core/bin/lib/code-review-depth.cjs',

View File

@@ -33,6 +33,11 @@ const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const { ExitError, runMain } = require('./lib/cli-exit.cjs'); const { ExitError, runMain } = require('./lib/cli-exit.cjs');
// #4145: shared hash-first recovery for baselines stored at an unexpected
// path under gsd-pristine/ (e.g. without the gsd-core/ prefix an earlier
// release's writer dropped). Same module the installer's preserve-check uses,
// so the two readers cannot drift apart again.
const { findPristineByHash } = require('./lib/pristine-baseline.cjs');
const SIGNIFICANT_MIN_CHARS = 12; const SIGNIFICANT_MIN_CHARS = 12;
const GSD_HOOK_VERSION_LINE_RE = /^(?:\/\/|#)\s*gsd-hook-version:\s*\S+\s*$/i; const GSD_HOOK_VERSION_LINE_RE = /^(?:\/\/|#)\s*gsd-hook-version:\s*\S+\s*$/i;
@@ -344,8 +349,29 @@ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashe
// pristinePathExists stays false. // pristinePathExists stays false.
} }
// Bug #934: recordedHash is present (modern installer) but the pristine // Bug #4145: the canonical join missed, but the recorded hash is the
// path does not exist on disk at all (stat threw above). This means // baseline authority the #3657 drift guard already trusts. Before
// reporting OK_NO_BASELINE, scan gsd-pristine/ for byte-identical content
// (an earlier release may have stored the snapshot without the gsd-core/
// prefix). An exact sha-256 match cannot be the wrong baseline, and
// gsd-pristine/ holds only backed-up files, so the scan is small. The
// canonical path itself is excluded — a mismatching file at the joined
// path is drift (#3657), never re-adopted through the scan.
if (!pristinePathExists && recordedHash) {
try {
const recoveredRel = findPristineByHash(pristineDir, recordedHash, hashKey);
if (recoveredRel) {
pristineContent = fs.readFileSync(path.join(pristineDir, recoveredRel), 'utf8');
pristinePathExists = true;
}
} catch {
// scan or read failure — fall through to the OK_NO_BASELINE posture
}
}
// Bug #934: recordedHash is present (modern installer) but no hash-matching
// pristine exists anywhere under gsd-pristine/ (stat threw above AND the
// #4145 scan found nothing). This means
// saveLocalPatches recorded a hash but could not write the corresponding // saveLocalPatches recorded a hash but could not write the corresponding
// gsd-pristine/ file (the only candidate was discarded because it was from // gsd-pristine/ file (the only candidate was discarded because it was from
// a newer release). Falling to over-broad mode here would treat every // a newer release). Falling to over-broad mode here would treat every
@@ -353,8 +379,9 @@ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashe
// false FAIL_USER_LINES_MISSING for each upstream removal. Since we // false FAIL_USER_LINES_MISSING for each upstream removal. Since we
// cannot reason correctly without a baseline, the safe answer is advisory/ // cannot reason correctly without a baseline, the safe answer is advisory/
// non-blocking: return OK_NO_BASELINE and let the caller decide. // non-blocking: return OK_NO_BASELINE and let the caller decide.
// NOTE: this guard fires ONLY when stat threw (path absent), not when the // NOTE: this guard fires ONLY when the baseline path is absent (stat threw
// path is present but non-file — in that case over-broad mode is safer. // and nothing matched by hash), not when the path is present but non-file —
// in that case over-broad mode is safer.
if (!pristinePathExists && recordedHash) { if (!pristinePathExists && recordedHash) {
result.reason = REASON.OK_NO_BASELINE; result.reason = REASON.OK_NO_BASELINE;
return result; return result;

View File

@@ -169,7 +169,7 @@ Check if a `gsd-pristine/` directory exists alongside `gsd-local-patches/`:
```bash ```bash
PRISTINE_DIR="$CONFIG_DIR/gsd-pristine" PRISTINE_DIR="$CONFIG_DIR/gsd-pristine"
``` ```
If it exists, the installer saved pristine copies at install time. Use these as the baseline. If it exists, the installer saved pristine copies at install time. Use these as the baseline. Both the deterministic verifier and the installer's preserve-check resolve each file's snapshot at its canonical path first and, when that misses, by the SHA-256 recorded in `pristine_hashes` — so a snapshot stored at a legacy path (for example, without the `gsd-core/` prefix an earlier release dropped) is still found and, on the next update, relocated to its canonical path (#4145).
### Option C: No baseline available (two-way fallback) ### Option C: No baseline available (two-way fallback)
If neither git history nor pristine snapshots are available, fall back to two-way comparison — but with **strengthened heuristics** (see Step 3). If neither git history nor pristine snapshots are available, fall back to two-way comparison — but with **strengthened heuristics** (see Step 3).

94
src/pristine-baseline.cts Normal file
View File

@@ -0,0 +1,94 @@
/**
* #4145: hash-first recovery for gsd-pristine/ baselines stored at an
* unexpected path.
*
* Some installs hold a pristine snapshot whose SHA-256 equals the hash recorded
* in backup-meta.json.pristine_hashes for a manifest-keyed file, but at a path
* that is not `path.join(pristineDir, relPath)` — e.g. stored without the
* `gsd-core/` top-level segment by an earlier release's writer. Both readers
* (verify-reapply-patches.cjs verifyFile and install.js saveLocalPatches)
* resolved strictly by that join, missed the snapshot, and reported
* ok_no_baseline / fell into regeneration that can never satisfy the recorded
* outgoing hash — a self-perpetuating gap.
*
* Hash equality with the recorded pristine_hashes entry is the same authority
* the #3657 drift guard already trusts, so a match cannot be the wrong
* baseline regardless of which release wrote it or where under gsd-pristine/
* it lives. This module owns the shared scan so the two readers cannot drift
* apart again (two private strict joins drifting is exactly the bug class).
*
* ADR-457: runtime module in src/*.cts, compiled to
* gsd-core/bin/lib/pristine-baseline.cjs.
*/
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
/**
* SHA-256 hex digest of a file's raw bytes. Byte-for-byte the same digest
* install.js fileHash() records into manifests and backup-meta.json.
*/
export function sha256File(absPath: string): string {
return crypto.createHash('sha256').update(fs.readFileSync(absPath)).digest('hex');
}
function walkSorted(dir: string, relPrefix: string, results: string[]): void {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return; // absent or unreadable — nothing to scan here
}
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
for (const entry of entries) {
// Never follow symlinks: gsd-pristine/ is installer-authored plain files;
// a link here is not a baseline and must not redirect the walk out of the
// tree (same posture as migration 004's walker).
if (entry.isSymbolicLink()) continue;
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
walkSorted(path.join(dir, entry.name), rel, results);
} else if (entry.isFile()) {
results.push(rel);
}
}
}
/**
* Find the first file under `pristineDir` (deterministic sorted walk) whose
* SHA-256 equals `recordedHash`, as a pristineDir-relative POSIX path.
*
* - `skip` is never returned — a single POSIX relPath string or a Set of them.
* Callers pass the canonical path(s) they (or other files in the same run)
* already own, so a file sitting at a canonical path is never adopted
* through the scan. For the installer's relocation this is what prevents a
* byte-identical canonical belonging to ANOTHER modified file from being
* "rescued" away (relocated and deleted at its home path).
* - Multiple matches are byte-identical by sha-256 authority; sorted order
* makes the choice deterministic.
* - Returns null when pristineDir is absent/unreadable or nothing matches.
*/
export function findPristineByHash(
pristineDir: string,
recordedHash: string,
skip?: string | ReadonlySet<string>,
): string | null {
if (!pristineDir || typeof recordedHash !== 'string' || recordedHash.length === 0) {
return null;
}
const skipSet = skip instanceof Set ? skip : new Set(skip !== undefined ? [skip] : []);
const rels: string[] = [];
walkSorted(pristineDir, '', rels);
for (const rel of rels) {
if (skipSet.has(rel)) continue;
try {
if (sha256File(path.join(pristineDir, rel)) === recordedHash) {
return rel;
}
} catch {
// unreadable candidate — keep scanning
}
}
return null;
}

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');
});
});
});
}

View File

@@ -1170,3 +1170,272 @@ describe('Bug #4086: verifyFile resolves skills entries at the runtime skills ro
}); });
}); });
} }
// ────────────────────────────────────────────────────────────────────────
// Folded regression block — #4145 (a hash-matching gsd-pristine/ baseline
// stored without the gsd-core/ prefix is never resolved). verifyFile() joined
// the manifest-keyed path strictly; when stat missed and a hash was recorded
// it reported OK_NO_BASELINE even though byte-correct content sat elsewhere
// under gsd-pristine/. The fix consults the recorded pristine_hashes — the
// same authority the #3657 drift guard trusts — and adopts an exact-hash
// match found anywhere in the tree.
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe('folded:bug-4145-pristine-prefix-resolution', () => {
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const crypto = require('node:crypto');
const { cleanup } = require('./helpers.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const ROOT = path.join(__dirname, '..');
const SCRIPT = path.join(ROOT, 'gsd-core', 'bin', 'verify-reapply-patches.cjs');
const { REASON } = require(SCRIPT);
const { findPristineByHash } = require(
path.join(ROOT, 'gsd-core', 'bin', 'lib', 'pristine-baseline.cjs'),
);
let tmpRoot;
let patchesDir;
let configDir;
let pristineDir;
function sha256(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
function writeFile(absPath, content) {
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, content);
}
function writeBackupMeta(pristine_hashes) {
writeFile(path.join(patchesDir, 'backup-meta.json'), JSON.stringify({ pristine_hashes }, null, 2));
}
function resetFixture() {
for (const dir of [patchesDir, configDir, pristineDir]) {
cleanup(dir);
}
fs.mkdirSync(patchesDir);
fs.mkdirSync(configDir);
fs.mkdirSync(pristineDir);
}
function runVerifier() {
const r = runNode([
SCRIPT,
'--patches-dir', patchesDir,
'--config-dir', configDir,
'--pristine-dir', pristineDir,
'--json',
], { timeoutMs: 30_000 });
return {
status: r.exitCode,
report: r.stdout && r.stdout.length ? JSON.parse(r.stdout) : null,
};
}
before(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-'));
patchesDir = path.join(tmpRoot, 'patches');
configDir = path.join(tmpRoot, 'installed');
pristineDir = path.join(tmpRoot, 'pristine');
resetFixture();
});
after(() => {
cleanup(tmpRoot);
});
describe('Bug #4145: hash-matching prefix-less pristine baseline is resolved', () => {
/**
* Core regression. The manifest key is `gsd-core/bin/lib/frontmatter.cjs`
* but the snapshot sits at `bin/lib/frontmatter.cjs` — one segment away
* from the joined path. Its SHA-256 equals the recorded pristine_hashes
* entry. The upstream release replaced the file wholesale and only the
* user's line survived the merge, so a verifier that recovered the
* baseline computes exactly one user-added line (present → exit 0,
* no_baseline 0), while the pre-fix run reported ok_no_baseline.
*/
test('#4145: resolves a hash-matching prefix-less pristine baseline instead of reporting ok_no_baseline', () => {
resetFixture();
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
const OLD_PRISTINE =
'outgoing pristine stock line one with substantial content\n' +
'outgoing pristine stock line two also substantial content\n';
const USER_LINE = 'user customization line that must survive the reapply merge';
const backupContent = OLD_PRISTINE + USER_LINE + '\n';
const installedContent =
'incoming upstream replacement line with substantial content\n' + USER_LINE + '\n';
writeBackupMeta({ [FILE]: sha256(OLD_PRISTINE) });
writeFile(path.join(patchesDir, FILE), backupContent);
writeFile(path.join(configDir, FILE), installedContent);
// The orphan: same bytes, stored WITHOUT the gsd-core/ prefix.
writeFile(path.join(pristineDir, 'bin', 'lib', 'frontmatter.cjs'), OLD_PRISTINE);
const { status, report } = runVerifier();
assert.equal(status, 0, `expected exit 0; report=${JSON.stringify(report)}`);
assert.equal(report.no_baseline, 0, 'a hash-matching baseline was on disk — it must be resolved');
assert.deepEqual(report.no_baseline_files, []);
assert.equal(report.failures, 0);
const r0 = report.results[0];
assert.equal(r0.status, 'ok');
assert.notEqual(r0.reason, REASON.OK_NO_BASELINE);
});
/** Negative space: nothing anywhere under gsd-pristine/ matches the record. */
test('#4145: still reports ok_no_baseline when the recorded hash matches nothing under gsd-pristine', () => {
resetFixture();
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
const backupContent =
'upstream line present in the backup of the outgoing release\n' +
'model: sonnet — the user customisation line in the backup file\n';
const installedContent =
'replacement upstream line in the newer release version\n' +
'model: sonnet — the user customisation line in the backup file\n';
writeBackupMeta({ [FILE]: 'deadbeef00000000000000000000000000000000000000000000000000000001' });
writeFile(path.join(patchesDir, FILE), backupContent);
writeFile(path.join(configDir, FILE), installedContent);
// No pristine file anywhere.
const { status, report } = runVerifier();
assert.equal(status, 0, 'no-baseline is advisory, never a failure');
assert.equal(report.no_baseline, 1);
assert.equal(report.results[0].reason, REASON.OK_NO_BASELINE);
});
/**
* Negative space: an orphan whose bytes do NOT hash to the record is never
* adopted — only exact recorded-hash matches are accepted.
*/
test('#4145: never adopts a hash-mismatching orphan — only exact recorded-hash matches', () => {
resetFixture();
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
const OLD_PRISTINE = 'outgoing pristine bytes that the record hashes\n';
const OTHER_CONTENT = 'some other release snapshot with different bytes\n';
writeBackupMeta({ [FILE]: sha256(OLD_PRISTINE) });
writeFile(path.join(patchesDir, FILE), 'outgoing pristine bytes that the record hashes\nuser line\n');
writeFile(path.join(configDir, FILE), 'user line\n');
// Orphan exists but hashes to something else.
writeFile(path.join(pristineDir, 'bin', 'lib', 'frontmatter.cjs'), OTHER_CONTENT);
const { status, report } = runVerifier();
assert.equal(status, 0);
assert.equal(report.no_baseline, 1, 'a mismatching orphan is not a baseline');
assert.equal(report.results[0].reason, REASON.OK_NO_BASELINE);
});
/**
* Precedence lock: the canonical prefixed path still resolves exactly as
* today even when an identical-content orphan also exists — the strict join
* stays first, and the verifier (read-only) leaves the orphan untouched.
*/
test('#4145: prefixed canonical baseline resolves exactly as today when an identical orphan also exists', () => {
resetFixture();
const FILE = 'gsd-core/workflows/execute-phase.md';
const pristineContent = 'stock workflow line long enough to pass the significance threshold\n';
const droppedLine = 'user workflow customisation that was lost in the merge operation';
const backupContent = pristineContent + droppedLine + '\n';
const installedContent = pristineContent; // user line dropped — real failure
writeBackupMeta({ [FILE]: sha256(pristineContent) });
writeFile(path.join(patchesDir, FILE), backupContent);
writeFile(path.join(configDir, FILE), installedContent);
writeFile(path.join(pristineDir, FILE), pristineContent);
const orphanPath = path.join(pristineDir, 'workflows', 'execute-phase.md');
writeFile(orphanPath, pristineContent);
const { status, report } = runVerifier();
assert.equal(status, 1, 'the dropped user line must still be caught via the canonical baseline');
const r0 = report.results[0];
assert.equal(r0.status, 'fail');
assert.equal(r0.reason, REASON.FAIL_USER_LINES_MISSING);
assert.ok(r0.missing.includes(droppedLine));
// Read-only verifier: the orphan is never relocated or pruned by a verify run.
assert.equal(fs.existsSync(orphanPath), true, 'verifier must not mutate gsd-pristine/');
});
/**
* Drift-path lock: a hash-MISMATCHING canonical snapshot still reports
* OK_PRISTINE_DRIFT_DETECTED (#3657) — the recovery scan must not reach the
* drift case.
*/
test('#4145: canonical-path drift still reports ok_pristine_drift_detected even when a hash-matching orphan exists', () => {
resetFixture();
const FILE = 'gsd-core/agents/gsd-executor.md';
const oldPristine = 'old pristine line that was present when backup was captured\n';
const newPristine = 'refreshed upstream line in the newer pristine snapshot\n';
const userLine = 'user customisation line that should be preserved across updates';
writeBackupMeta({ [FILE]: sha256(oldPristine) });
writeFile(path.join(patchesDir, FILE), oldPristine + userLine + '\n');
writeFile(path.join(configDir, FILE), newPristine + userLine + '\n');
writeFile(path.join(pristineDir, FILE), newPristine); // canonical drifted
// A hash-matching orphan exists elsewhere — drift must still win.
writeFile(path.join(pristineDir, 'agents', 'gsd-executor.md'), oldPristine);
const { status, report } = runVerifier();
assert.equal(status, 0);
const r0 = report.results[0];
assert.equal(r0.reason, REASON.OK_PRISTINE_DRIFT_DETECTED,
`expected the untouched #3657 drift posture; got ${r0.reason}`);
assert.equal(report.drifted, 1);
});
/** Module unit: deterministic sorted-first match, symlink skip, absent dir. */
test('#4145: findPristineByHash returns the sorted-first match, skips symlinks, and null on an absent dir', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-unit-'));
const symRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-sym-'));
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-out-'));
try {
const contentA = 'identical bytes that two snapshots happen to share\n';
const hashA = sha256(contentA);
writeFile(path.join(root, 'zz-dir', 'late-match.md'), contentA);
writeFile(path.join(root, 'aa.txt'), contentA);
assert.equal(findPristineByHash(root, hashA), 'aa.txt',
'sorted-first match wins deterministically');
assert.equal(findPristineByHash(root, hashA, 'aa.txt'), 'zz-dir/late-match.md',
'skipRel is never returned');
assert.equal(findPristineByHash(root, hashA, new Set(['aa.txt', 'zz-dir/late-match.md'])), null,
'every member of a skip Set is excluded (canonical-path protection)');
assert.equal(findPristineByHash(root, sha256('no such content anywhere here\n')), null,
'no match resolves to null');
assert.equal(findPristineByHash(path.join(root, 'absent'), hashA), null,
'absent dir resolves to null');
// A symlink is never followed, even when its target would hash-match.
// The target lives OUTSIDE symRoot so the only hashable entry inside the
// scanned tree is the symlink itself.
const outsideTarget = path.join(outsideRoot, 'outside-target.md');
fs.writeFileSync(outsideTarget, contentA);
fs.symlinkSync(outsideTarget, path.join(symRoot, 'sym.md'));
assert.equal(findPristineByHash(symRoot, hashA), null,
'symlinked candidates are skipped, not followed');
} finally {
cleanup(root);
cleanup(symRoot);
cleanup(outsideRoot);
}
});
});
});
}