ef823ca9d907f084e190cf1dff092976511e875d
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef823ca9d9 |
fix(#2830): propagate a halted plan to its transitive dependents (#3038)
* test(#2830): add failing regression tests for halted-plan dependent blocking Add tests/fix-2830-halted-plan-dependents.test.cjs covering direct, transitive (2 and 3 hop), and diamond dependents of a halted plan across both independent "which plans are incomplete" readers (phase-plan-index's cmdPhasePlanIndex and findPhaseInternal/searchPhaseInDir), the negative case (an unrelated decoupled plan stays runnable), and a parity check that the two readers agree. Uses only modules that already exist at this commit (gsd-tools.cjs via subprocess, the pre-existing phase-locator.cjs) so the test file loads and runs cleanly on a fresh clone of this exact commit. These fail against current behavior: neither reader has any concept of a halted plan or a blocked_by/runnable view yet. * fix(#2830): a halted plan no longer leaves its dependents on the runnable work list A plan that reaches a designed stop still writes a SUMMARY, so both "which plans are incomplete" readers saw it as an ordinary completion and reported its dependents as ordinary runnable work — never checking whether an upstream plan had halted rather than finished. - New `status: halted` frontmatter value, documented in all four SUMMARY templates alongside the existing `status: complete`. - New shared src/plan-dependency-graph.cts: a single computeHaltPropagation pass that both phase.cts's cmdPhasePlanIndex (wave-grouping) and phase-locator.cts's searchPhaseInDir (the phase-location primitive, ~50 dependent symbols across 5 command routers) now call, so the two-implementation divergence that caused this bug cannot recur. It accepts an optional precomputedOrder so cmdPhasePlanIndex — which already runs Kahn's algorithm in computeDependencyLevels for wave assignment — passes that order straight through instead of a second traversal; searchPhaseInDir (no prior traversal) lets the module derive its own. The two small duplicated predicates each reader would otherwise carry (is this status "halted"?, which summary file matches which plan id?) are centralized in the same module as isHaltedStatus/buildSummaryFileIndex. - Additive fields only: `halted`/`blocked_by`/`runnable` on cmdPhasePlanIndex's plans[] and top level, `halted_plans`/`blocked_by`/ `runnable_plans` on searchPhaseInDir's result. The pre-existing `incomplete`/`incomplete_plans` fields are unchanged in meaning and membership. - execute-phase.md's discover_and_group_plans step now also skips any plan whose `blocked_by` is non-empty, reporting it by name with its blocking chain, in addition to (not instead of) the existing has_summary skip rule. Extends tests/fix-2830-halted-plan-dependents.test.cjs (introduced in the prior commit) with direct unit coverage of computeHaltPropagation (including the precomputedOrder call shape) and a fast-check property test — both only possible once this commit's new module exists. Closes #2830 * fix(#2830): surface the halt-aware view from init execute-phase The adopted work made phase-locator compute halted_plans / blocked_by / runnable_plans, but cmdInitExecutePhase builds its output by explicitly enumerating fields, so all three were computed and then silently dropped at the exact consumer the issue names as regressed. Forwards them additively -- incomplete_plans and incomplete_count keep their name, type and semantics byte-for-byte -- and adds the same three empty defaults to the roadmap-only fallback so the shape is consistent in both branches. Covered by a new test that drives the real CLI end to end rather than the locator function, since the locator already worked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2830): fail closed on dependency cycles and stop the templates inviting the defect Three review findings, all fixed: - BLOCKER (isolated adversarial). Cycle participants never reach indegree 0 in the Kahn pass, so they were excluded from the topological order, never visited by the forward pass, and vanished from blocked_by entirely -- i.e. reported as runnable. The wave-grouping reader hard-fails on a cycle so it never hit this, but the phase-location reader does not, so init execute-phase offered a plan depending directly on a halted plan. Reproduced, then fixed in the shared engine so every consumer is safe regardless of pre-checks: a node absent from the order is now blocked with a deterministic, non-empty named cause. A plan silently missing from both blocked_by and runnable is the exact disappearance this issue exists to prevent. - MAJOR (isolated adversarial). All four summary templates showed the field as an inline comment on the value line. Frontmatter parsing does not strip trailing comments, so an executor copying the templates' own presentation wrote a halt that parsed as a non-halted string, silently reproducing the original bug. Guidance moved off the value line, and the halt predicate now tolerates an unquoted trailing comment. - HARD standards violation. A test regex-matched child-process stderr prose for /cycle/i, which CONTRIBUTING bans. Replaced with the structured failure signal plus a differential assertion (same fixture without the cycle edge must succeed), so it stays cycle-specific without matching prose. Also folds the duplicated read-summary-and-check-halted wrapper out of both readers into the shared module -- centralizing only the predicate left the exact two-copies-that-drift pattern the module exists to prevent -- and commits the artifact-types documentation for the new status value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2830): stop the property generator hanging the whole suite The remote runner did not fail -- it hung. Two containers sat in this file for 31+ minutes, and an earlier attempt ran 9 hours before I killed it. The runner passes --test-timeout=0, so nothing ever reaps it: this would have hung CI indefinitely, not reported a failure. Root cause: the DAG generator built edges by rejection -- from: fc.integer({ min: 0, max: n - 1 }) to: fc.integer({ min: 0, max: n - 1 }) .filter(({ from, to }) => from < to) With n === 1 both integers are forced to 0, so the predicate is unsatisfiable and fast-check retries value generation forever. n is drawn from 1..12 and fast-check biases toward boundary values, so n === 1 is reached almost at once. This also explains why the failing-first run completed normally while the fixed run hung: before the fix the graph module did not exist, so the property test threw on import and never reached generation. It only starts hanging once the code under test works. Generates the DAG by construction instead -- `to` is drawn strictly above `from`, with the degenerate single-node case short-circuited to an empty edge list -- so no rejection sampling is involved. Switches the import to the shared fast-check setup so the seed and run count are pinned per CONTRIBUTING, and adds a bounded regression guard that samples the arbitrary directly, so a future reintroduction fails loudly instead of hanging. Verified: the file now completes in 2 seconds, 29 tests started and 29 finished, zero failures, against an indefinite hang before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2830): restore the depends_on display contract and acknowledge the workflow growth Full-suite run surfaced two things the focused harnesses could not. 1. Regression of a pinned pre-existing contract (#3785). A refactor routed the EMITTED depends_on field through the new dependency resolver, which also consults the canonical-prefix map. The original consulted the plan map only, so a short canonical prefix passed through verbatim -- '24-01' stayed '24-01' rather than becoming '24-01-auth-hardening'. The emitted field is a DISPLAY mapping, not the DAG resolution, and #3785 pins that. Reverted with a comment recording why it must not use the resolver; full resolution is still used for the wave DAG and halt propagation, which is what needs it. 2. The workflow file grew 518 bytes without an acknowledgment, from the halt-aware skip rule and the widened parse contract. Acknowledged. Note on where the acknowledgment landed: the guidance is to add a NEW fragment, but execute-phase.md is already named by an existing fragment and the linter hard-fails when two ack sources name the same path. Appending to the owning fragment, following its own established multi-PR pattern, was the only lint-clean option. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2830): backfill changeset pr number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
67e2ff7b25 |
fix(#2855): scope the phase-locator archived-milestone fallback to the active workstream (#3008)
* 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> |
||
|
|
5f609762ed |
fix(#2237): fail loud on ambiguous bare-number phase directory collision (#2262)
* 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 |
||
|
|
8c3d934a90 |
refactor(#1294): T-final — delete the core.cjs re-export spine (epic #1267 complete) (#1295)
* refactor(#1294): T-final — delete the core.cjs re-export spine (epic #1267 complete) After T0–T6 nothing imports core, so retire the spine and its scaffolding: - delete src/core.cts (and the gitignored gsd-core/bin/lib/core.cjs artifact; remove its .gitignore + eslint-ignore entries) - delete scripts/lint-core-spine-imports.cjs + its allowlist; drop it from the package.json lint:ci chain - regenerate docs/INVENTORY-MANIFEST.json (drops the core.cjs surface) - sweep stale references: CONTEXT.md glossary back-compat clauses (spine retired, callers import the leaf directly), planning-config.md CONFIG_DEFAULTS owner, and false present-tense core.cjs claims in leaf-module docstrings The ADR-857 decomposition is complete: the former Core god-module is fully dissolved into its leaf modules; no re-export spine remains. No behaviour change. Closes #1294 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1294): migrate the computed-path core.cjs importers the literal grep missed bin/install.js used require(path.join(_gsdLibDir, 'core.cjs')) (a computed path, and bin/install.js was never in the convergence lint's scan roots), and ~8 test files referenced core.cjs via path.join/readFileSync/existsSync/FILE_ARG forms the literal-string migration grep missed. Route install.js's symbols to their leaves (RUNTIME_PROFILE_MAP->model-catalog, resolveTierEntry/EFFORT_SET-> model-resolver) and repoint/adjust the test references to the leaves. Recovers the 161 'Cannot find module core.cjs' failures from the spine deletion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
dd81e3d120 |
refactor(#881): extract phase-locator fs-search into phase-locator.cts (#882)
ADR-857 rollout phase 2d. Move the phase-directory search/location functions (searchPhaseInDir, findPhaseInternal, getArchivedPhaseDirs) + their interfaces (PhaseSearchResult, ArchivedPhaseDir) out of core.cts into a new module src/phase-locator.cts. core.cts re-exports all three (callers unchanged). Cycle-free: phase-locator depends only on leaves (phase-id for token/name matching, core-utils for fs-scan/path helpers, planning-workspace for planningDir) — unblocked by the core-utils leaf (2c). This completes the phase-search split: parsing in phase-id (2a), fs-search in phase-locator. New-CLI-module checklist done (.gitignore, eslint, INVENTORY 94->95 + row, manifest, ARCHITECTURE, CONTEXT.md "Phase Locator Module"). Adds tests/phase-locator.test.cjs (37 tests: behavioral + shim-identity + adversarial phase-dir fixtures). Gates: lint, code-review, security-review, codex adversarial-review (0 findings; verbatim move checksum-verified). Mac 4078 pass; clean-build docker 12972 pass, 0 fail. Closes #881 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |