bug(verification): --project-dir is ignored by verification.fingerprint and the staleness recompute (both re-derive root from phaseDir)
Summary
--project-dir is validated and honored by the dispatcher (#3881: it sets cwd and skips the ancestor walk-up), but verification.fingerprint and the staleness recompute in readVerificationStatus never consult it. Both re-derive the project root from the phase directory alone via findProjectRoot(phaseDir) (src/verification.cts, the fingerprint verb and the computeCoveredDigest(findProjectRoot(phaseDir), …) call in the status path).
This is acceptance criterion (4) from the #4815 triage brief — "--project-dir is either honored or consistently documented behavior for this path" — which #4819 deliberately does not address.
Reproduction
.planning symlinked to an external git-managed store (the layout from #4815), phase directory passed by its real store path — e.g. after a caller has resolved the symlink:
T=$(mktemp -d); mkdir -p $T/proj/src $T/proj/.git $T/store/phases/01-x $T/store/.git
echo hi > $T/proj/src/a.txt; echo '{}' > $T/store/config.json
ln -s $T/store $T/proj/.planning
G="node gsd-core/bin/gsd-tools.cjs"
# via the symlink path: works (with the #4819 fix)
(cd $T/proj && $G verification.fingerprint .planning/phases/01-x src/a.txt)
# via the real store path, no flag: fails (walk-up from the store never reaches proj)
(cd $T/proj && $G verification.fingerprint $T/store/phases/01-x src/a.txt)
# Error: could not compute fingerprint — a covered file is missing, unreadable, or escapes the project root
# via the real store path WITH --project-dir: still fails — flag ignored
(cd $T && $G verification.fingerprint $T/store/phases/01-x src/a.txt --project-dir $T/proj)
# Error: could not compute fingerprint — (same)Why it can't be fixed in one place
Honoring the flag in cmdVerificationFingerprint alone (one site; cwd is already the explicit root there) would emit a digest computed under the explicit root while readVerificationStatus recomputes under findProjectRoot(phaseDir) — so verification.status / phase.complete would still read stale. Both must change together, or neither.
The status side is reached through readVerificationStatus ← isPhaseComplete and ~9 callers (roadmap, init, state, planning-inspect, phase, planning-snapshot, uat-predicate, workstream-inventory).
Possible approaches
- Thread an optional
projectRootthroughreadVerificationStatus/isPhaseComplete, populated only when--project-dirwas explicit; fingerprint usescwdin that case. Non-explicit behavior unchanged. - Document only: state that verification derives its root from the phase directory and does not consult
--project-dir. Satisfies "consistently documented" but leaves the flag a trap for realpath callers.
Must not weaken covered-file containment or the fail-closed staleness contract (out of scope per the #4815 brief).
Happy to send a PR for (1) once a direction is confirmed. Follow-up to #4815 / #4819.
Source: open-gsd/gsd-core