Files
msd-core/.changeset/3518-uat-path-phase-pinned.md
Tom Boucher 3893d1ff69 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>
2026-08-14 23:00:43 -04:00

994 B

type, pr
type pr
Fixed 3525

uat_path is now pinned to the phase's own UAT artifact instead of being picked by unsorted directory-listing order — both uat_path projections (init plan-phase and init phase-op) selected the phase's *-UAT.md with a bare first-match .find() that had no phase-membership check and no ordering, so a stray or cross-phase 04-UAT.md sitting in phase 03's directory could become phase 03's uat_path, and which file won was filesystem-dependent (creation order on APFS, hash order on ext4/XFS) — meaning two machines on the same commit could emit different uat_path values for the same phase, sending downstream workflows to read another phase's UAT state. Both sites now route through a shared phase-pinned resolver (resolveUatFile, sibling of the resolveVerificationFile rule from #3357/#3492): the phase's own <token>-UAT.md always wins, otherwise the alphabetically-first dashed candidate, deterministically on every machine. (#3518)