fix(#3518): pin uat_path to the phase's own UAT artifact via the shared phase-pinned resolver (#3525)
* fix(#3518): pin uat_path to the phase's own UAT artifact via the shared phase-pinned resolver Both uat_path projectors in src/init.cts picked the phase's UAT file with a bare .find() over unsorted readdir order — no phase-membership check, no ordering — so a stray cross-phase 04-UAT.md in phase 03's directory could become phase 03's uat_path, filesystem-dependently (creation order on APFS, hash order on ext4/XFS): two machines on the same commit could emit different uat_path values for the same phase. Route both sites through a new resolveUatFile in src/verification.cts, the UAT counterpart of #3357/#3492's resolveVerificationFile, sharing the exact selection rule via one extracted core (resolvePhaseArtifactFile): the phase's own <token>-UAT.md always wins; otherwise the alphabetically-first dashed candidate (deterministic everywhere); a bare UAT.md only via allowBare when no dashed candidate exists. resolveVerificationFile now delegates to the same core — behavior byte-identical. Guarded by: two end-to-end repro tests in tests/init.test.cjs (plan-phase and phase-op, red on the pre-fix readdir pick), resolveUatFile contract anchors and a src/-wide call-site guard in tests/verification-status.test.cjs. * chore(#3518): add changeset fragment for PR #3525 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
28
src/init.cts
28
src/init.cts
@@ -103,7 +103,7 @@ const {
|
||||
|
||||
const { determinePhaseStatus } = commandsMod;
|
||||
const { extractFrontmatter } = frontmatterMod;
|
||||
const { isPhaseComplete, resolveVerificationFile } = verificationMod;
|
||||
const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
|
||||
const { evaluateUatPassed } = uatPredicateMod;
|
||||
const { resolveLoopHooks } = loopResolverMod;
|
||||
const { loadRegistry } = capabilityLoaderMod;
|
||||
@@ -1151,14 +1151,23 @@ function cmdInitPlanPhase(
|
||||
// #3492: pin selection to THIS phase's own token so a stray cross-phase
|
||||
// or sentinel-numbered canonically-shaped file cannot outrank this
|
||||
// phase's own (possibly non-canonical) report.
|
||||
const phaseToken = extractPhaseToken(path.basename(phaseDirFull));
|
||||
const verificationFile = resolveVerificationFile(files, {
|
||||
allowBare: true,
|
||||
phaseToken: extractPhaseToken(path.basename(phaseDirFull)),
|
||||
phaseToken,
|
||||
});
|
||||
if (verificationFile) {
|
||||
result['verification_path'] = toPosixPath(path.join(phaseDirFull, verificationFile));
|
||||
}
|
||||
const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md');
|
||||
// #3518: routed through the shared UAT resolver — the prior hand-rolled
|
||||
// `.find()` over unsorted readdir order had no phase check and no
|
||||
// ordering, so a stray cross-phase 02-UAT.md could become this phase's
|
||||
// uat_path, filesystem-dependently. Pinned to this phase's own token
|
||||
// (same rule as verification_path above).
|
||||
const uatFile = resolveUatFile(files, {
|
||||
allowBare: true,
|
||||
phaseToken,
|
||||
});
|
||||
if (uatFile) {
|
||||
result['uat_path'] = toPosixPath(path.join(phaseDirFull, uatFile));
|
||||
}
|
||||
@@ -1984,14 +1993,23 @@ function cmdInitPhaseOp(cwd: string, phase: string, raw: boolean): void {
|
||||
// #3492: pin selection to THIS phase's own token so a stray cross-phase
|
||||
// or sentinel-numbered canonically-shaped file cannot outrank this
|
||||
// phase's own (possibly non-canonical) report.
|
||||
const phaseToken = extractPhaseToken(path.basename(phaseDirFull));
|
||||
const verificationFile = resolveVerificationFile(files, {
|
||||
allowBare: true,
|
||||
phaseToken: extractPhaseToken(path.basename(phaseDirFull)),
|
||||
phaseToken,
|
||||
});
|
||||
if (verificationFile) {
|
||||
result['verification_path'] = toPosixPath(path.join(phaseDirFull, verificationFile));
|
||||
}
|
||||
const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md');
|
||||
// #3518: routed through the shared UAT resolver — the prior hand-rolled
|
||||
// `.find()` over unsorted readdir order had no phase check and no
|
||||
// ordering, so a stray cross-phase 02-UAT.md could become this phase's
|
||||
// uat_path, filesystem-dependently. Pinned to this phase's own token
|
||||
// (same rule as verification_path above).
|
||||
const uatFile = resolveUatFile(files, {
|
||||
allowBare: true,
|
||||
phaseToken,
|
||||
});
|
||||
if (uatFile) {
|
||||
result['uat_path'] = toPosixPath(path.join(phaseDirFull, uatFile));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user