* test(#3335): fold the workflow-content & phase-lifecycle fix-* cluster — Wave 3
Folds 13 legacy fix-*.test.cjs regression files (131 test() blocks) into
their module's main suite, per H3 (#3315) of the test-hygiene epic (#3053):
- 6 files with no prior target coverage: renamed (git mv) into new suites
(spike-manifest-scoping, ship-note, add-todo, workflow-jq-dependency,
resolve-execution-dynamic-routing, clock)
- 7 files merged into 5 pre-existing suites (worktree-base-ref x2,
model-resolver, phase-locator x2, frontmatter, verification-status),
deduplicated against existing coverage
Zero net test-coverage loss: every source assertion preserved or verified
as a genuine pre-existing duplicate. No production code changed.
Last fix-* wave (Wave 1 #3341, Wave 2 #3342 already merged); 4 issue-*
waves remain in #3315.
* test(#3335): fix orthogonal-review findings — Wave 3 fold
Standards-axis review + Memtrace graph pass found real defects in the
just-folded suites, all fixed here:
- phase-locator.test.cjs: pinned an unseeded fast-check property test
(CONTRIBUTING.md determinism requirement), matching the sibling test's
seed:7 convention.
- phase-locator.test.cjs: added assert.ok() presence guards after 9
data.plans.find() calls that were dereferenced unguarded, inconsistent
with 5 sibling tests in the same file that already guard correctly.
Latent robustness gap — an omitted plan would throw an opaque TypeError
instead of a clear assertion failure.
- Standardized the fold-wrapper convention (block-scoped __foldDescribe)
across worktree-base-ref.test.cjs, verification-status.test.cjs, and
phase-locator.test.cjs to match the pattern already established in
frontmatter.test.cjs and model-resolver.test.cjs from earlier folds.
- worktree-base-ref.test.cjs: moved a mid-file require to the top-of-file
require block.
- Eliminated duplicated env-isolation helpers: model-resolver.test.cjs and
phase-locator.test.cjs each reimplemented GSD_WORKSTREAM/GSD_PROJECT
save-restore independently; factored a shared isolateWorkstreamEnv()/
restoreWorkstreamEnv() into tests/helpers.cjs and pointed both call
sites at it.
No test() count changed in any file. No production code touched.
---------
Co-authored-by: sim <sim@local>
* test(#2855): add failing-first regression test for cross-workstream archive leak
Covers findPhaseInternal/getArchivedPhaseDirs in src/phase-locator.cts
resolving a pending workstream phase to an unrelated workstream's (or
flat-mode's) archived phase because the archive fallback hardcodes the
project-root .planning/milestones/ tree. Fails against the current
implementation; the fix lands in a follow-up commit.
* fix(#2855): scope phase-locator archived-milestone fallback to the active workstream
findPhaseInternal and getArchivedPhaseDirs in src/phase-locator.cts hardcoded
the project-root .planning/milestones/ tree when falling back to search
archived phases, ignoring GSD_WORKSTREAM. A pending phase in one workstream
whose own phases/ directory didn't exist yet would silently resolve to a
same-numbered phase archived under an unrelated workstream's (or flat-mode's)
history, complete with stale plan/summary counts and an archived status.
Route the archive fallback through planningDir(cwd) instead — the same
workstream-aware helper the active-phase search (three lines above) and the
archive-write path (archivePhaseDirectories in milestone.cts) already use.
Flat/non-workstream projects are unaffected: planningDir(cwd) with no
GSD_WORKSTREAM resolves to the same root .planning path as before.
Also switch the reported relBase/basePath from a hardcoded
'.planning/milestones/...' literal to path.relative(cwd, archivePath), so the
paths returned to callers stay consistent with wherever the archive actually
resolved to (root or workstream-scoped).
* chore(#2855): add changeset for phase-locator workstream archive fix
* fix(#2855): normalize getArchivedPhaseDirs basePath to posix separators
Orthogonal code-review finding: findPhaseInternal's relBase/directory field
was explicitly toPosixPath-normalized, but getArchivedPhaseDirs's basePath
used a bare path.relative() call, leaving it native-separator on Windows —
an inconsistency between two sibling "relative path from cwd" report fields
introduced by the same #2855 fix. Wrap basePath in toPosixPath to match, and
update the two existing assertions that compared basePath against path.join
output (which would break on Windows now that the field is guaranteed posix)
to compare against forward-slash literals instead, matching how the sibling
`directory` field is already asserted elsewhere in this suite.
* refactor(#2855): share archive-directory resolution between findPhaseInternal and getArchivedPhaseDirs
Orthogonal code-review finding: the two functions carried independent copies
of the same resolve-milestonesDir-then-enumerate-archive-dirs logic — the
exact shape that let the original #2855 bug (hardcoded root path) exist in
one copy while the workstream-aware active-phase search sat three lines
above it. Extract listArchiveVersionDirs(cwd) as the single seam both
functions now consume, so a future change to how the archive tree is located
only needs to happen once. Byte-for-behaviour preserved: readSubdirectories
and searchPhaseInDir already self-contain their own try/catch and never
throw, so moving the iteration outside the old inline try block changes
nothing observable (verified via manual repro scripts covering leak
prevention, positive resolution, flat-mode parity, and multi-milestone
reverse-sort ordering).
* test(#2855): demonstrate ROADMAP.md presence does not affect the archive-leak guard
Orthogonal code-review (spec axis) finding: issue #2855's AC1 states the
guard must hold "regardless of whether workstream A's roadmap already lists
the phase and when it doesn't yet" — an explicit two-value dimension that
had no direct test coverage; it was only inferable by reading
findPhaseInternal's source and confirming it never touches ROADMAP.md.
Add a parametrized test creating the workstream's ROADMAP.md with and
without a matching Phase heading, asserting the archive-leak guard resolves
identically (null) either way.
* chore(#2855): backfill changeset PR number to 3008
---------
Co-authored-by: sim <sim@local>
* fix(#2237): fail loud on ambiguous bare-number phase directory collision
When two unrelated projects share a .planning/phases/ tree, a bare phase
number silently resolved to the first 0N-* directory found — risking
cross-project file writes. The fix detects multiple matches for the same
phase number and surfaces an ambiguous_matches result instead of silently
taking the first.
Changes:
- src/phase-locator.cts: searchPhaseInDir uses filter() + ambiguity check
- src/phase.cts: cmdFindPhase same pattern
- src/init.cts: cmdInitPhaseOp surfaces ambiguous_matches in the result
- tests/phase-locator.test.cjs: 3 regression tests
* docs: backfill changeset PR number (#2262)
* merge: keep up to date with next
* fix: regenerate stale capability-registry after next merge
The convergence lint only scanned src/ + gsd-core/bin, so ~35 test files
still imported core.cjs. Repoint all 33 behaviour importers to the leaf
modules directly (same symbol->leaf map as the src migration; leaves are the
objects core re-exported by reference), delete the now-meaningless
shim-identity describe blocks in the 8 leaf tests, and delete tests/core.test.cjs
(forwarded-behaviour coverage now lives at the leaves; resolveWorktreeRoot
test relocated to worktree-safety in T0) and tests/lint-core-spine-imports.test.cjs
(the lint is removed in T-final). Dropped the stale core.test.cjs entries from
the allow-test-rule-refs allowlist; eslint-rules RuleTester fixture path
pointed at io.cjs.
After T6: ZERO test imports core.cjs. core.cts still builds (now fully unused);
T-final deletes it. No behaviour change.
Closes#1291
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>