Merge branch 'next' into fix/2665-test-env-base-config-location-vars
This commit is contained in:
@@ -184,6 +184,39 @@ node gsd-tools.cjs roadmap analyze
|
||||
node gsd-tools.cjs roadmap update-plan-progress <N>
|
||||
```
|
||||
|
||||
### Milestone window scope (`roadmap analyze`)
|
||||
|
||||
`roadmap analyze` scopes its phase list to the current milestone's section of
|
||||
`ROADMAP.md`. Its JSON output carries a `scope` field describing how much of the
|
||||
intended input that scoping actually saw:
|
||||
|
||||
| `scope` | Meaning |
|
||||
|---|---|
|
||||
| `complete` | The window was computed over the whole intended input. `phase_count: 0` here is a **real** answer — a freshly-declared milestone genuinely has no phases yet. |
|
||||
| `truncated` | The milestone's heading was found, but its window closed before reaching the document's phase region — typically because a closed-milestone heading sits between the active milestone and its `### Phase N:` sections. `phase_count: 0` here is a **non**-answer. |
|
||||
| `unscoped` | No milestone version could be resolved (or its section is absent) on a ROADMAP that does use versioned milestones, so the result is not milestone-scoped. |
|
||||
| `unreadable` | `ROADMAP.md` could not be read. |
|
||||
|
||||
Before this field existed, all four cases produced the same well-formed
|
||||
`phase_count: 0` with no error, so a consumer could not tell a genuinely empty
|
||||
milestone from a scoping failure. Branch on `scope`, not on `phase_count` alone.
|
||||
|
||||
A ROADMAP with no versioned milestone headings at all (the free-form legacy
|
||||
shape) reports `complete`: the whole document *is* the milestone there.
|
||||
|
||||
### `milestone complete` refuses an untrustworthy window
|
||||
|
||||
`milestone complete` archives `ROADMAP.md`/`REQUIREMENTS.md` and **moves phase
|
||||
directories** — a one-way door. When the milestone window's `scope` is
|
||||
`truncated` — the milestone heading was found but its section closes before
|
||||
reaching any phase entries, even though the ROADMAP has phase entries
|
||||
elsewhere — phase scoping cannot be trusted, and the command now refuses
|
||||
rather than falling back to an over-inclusive filter that would archive every
|
||||
phase directory in the project. `unreadable` (no ROADMAP.md at all) and
|
||||
`unscoped` (no section for this version) are pre-existing, legitimately
|
||||
handled states and are not refused here. Pass `--force` to override, the same
|
||||
affordance the unstarted-phase guard uses.
|
||||
|
||||
---
|
||||
|
||||
## Config Commands
|
||||
|
||||
@@ -472,6 +472,8 @@ If any category is non-empty you are prompted with `[R] Resolve` / `[A] Acknowle
|
||||
|
||||
> **Note:** the `deferred-items.md` category is the per-phase SCOPE BOUNDARY log a phase agent writes when it finds a defect it should not fix. It is a different artifact from the `## Deferred Items` section `[A]` writes into `STATE.md`, which records what you acknowledged at close.
|
||||
|
||||
> **Truncated-window guard.** Archiving also refuses when the milestone's ROADMAP window is truncated — `Cannot mark milestone complete: the ROADMAP window for "<version>" is truncated`. This is the case where the milestone's heading is found but its section closes before reaching the roadmap's `### Phase N:` region (typically a closed-milestone heading sitting in between), which previously degraded to an over-inclusive filter and archived *every* phase directory in the project rather than the milestone's own. An unreadable ROADMAP.md or a version with no matching section at all are pre-existing, legitimately-handled states and are not refused here. Same override as below: `gsd-tools milestone complete <version> --force`. A window that is genuinely empty — a freshly-declared milestone with no phases yet — is *not* affected and still completes normally.
|
||||
|
||||
> **Unstarted-phase guard.** Archiving refuses if the milestone's ROADMAP still lists a phase with no phase directory on disk — `Cannot mark milestone complete: ROADMAP lists N unstarted phase(s)`. If a phase was intentionally deferred or merged without a directory, run `gsd-tools milestone complete <version> --force` (the `/gsd-complete-milestone` workflow runs the underlying command without `--force`, so use the CLI directly to override). A `STATE.md` `milestone:` value that does not match `<version>` prints a WARNING and still runs the guard (#2946).
|
||||
|
||||
---
|
||||
|
||||
@@ -295,3 +295,59 @@ plan report as a naming violation — the diagnostic reads non-membership as a d
|
||||
`allPlanFiles`. The general rule: a **diagnostic about file naming** wants the physical set; only a
|
||||
question about outstanding *work* wants the live set. Later phases must make that choice explicitly
|
||||
per call site rather than swapping in `planFiles` mechanically.
|
||||
|
||||
### Amendment 2 — Phase 2 (#3184) validation: the contract held; the copy count was low again
|
||||
|
||||
Decision 2's contract needed **no change** for its second consumer: `SCOPE`'s four values covered
|
||||
every row of the windowing derivation's behavior table, including the two rows the epic's text does
|
||||
not distinguish (a free-form legacy ROADMAP with no versioned milestones is `COMPLETE`, not
|
||||
`UNSCOPED` — whole-document genuinely *is* the milestone there). Phase 2 adds no member and changes
|
||||
no semantics. `src/planning-scope.cts` needed no edit, so Phase 2 carries no `.cts` six-gate ripple.
|
||||
|
||||
**What Phase 2 found.** The epic and this ADR both scope milestone windowing at **three** copies, all
|
||||
inside `roadmap-parser.cts`. Building the Decision 4(a) whole-repo guard found **two more**, in a
|
||||
different module and one function down: `state.cts` `buildStateFrontmatter` and `syncStateFrontmatter`
|
||||
each hand-roll `^#{1,3}\s+(?!Phase\s+\S).*${escapeRegex(version)}` to answer "is this milestone
|
||||
bounded to a versioned ROADMAP heading" — the heading-location half of the derivation, byte-identical
|
||||
to each other. This is Phase 1's finding repeating with a different derivation: **the epic's copy
|
||||
counts are a lower bound derived from the reported issues, and the whole-repo guard is what makes them
|
||||
real.** Both sites now call the owner's `isMilestoneBoundedInRoadmap`, which is a straight
|
||||
consolidation of the two identical `state.cts` regexes onto `locateMilestoneHeadings` with **no
|
||||
behavior change** — which is all it should ever have been.
|
||||
|
||||
**A boundary tightening was tried and reverted.** A first pass at `locateMilestoneHeadings` swapped its
|
||||
`\b` version-token boundary for the stricter `(?![\w.-])` used by `isMilestoneShippedInRoadmap`
|
||||
(#2562), reasoning that `v2.0` should not match inside `v2.0.1` anywhere windowing happens. That broke
|
||||
`extractCurrentMilestoneScoped`'s #730 contract: a milestone STATE of `v8.0` legitimately selects the
|
||||
`## v8.0-B …` active sub-milestone heading over a closed `v8.0-A` sibling (`0` is a word character, `-`
|
||||
is not, so `\b` matches; `(?![\w.-])` does not, because `-` is in its excluded set). `\b` is restored in
|
||||
`locateMilestoneHeadings`; the stricter boundary stays local to `isMilestoneShippedInRoadmap` and to the
|
||||
#730 `detailsVersionBoundary`, which answer a narrower question ("is exactly this milestone shipped" /
|
||||
"which Phase Details section is exactly this one's version token's") than "which heading does this
|
||||
milestone STATE select." The consolidation itself (three `roadmap-parser.cts` copies plus the two
|
||||
`state.cts` copies onto one owner) is behavior-preserving.
|
||||
|
||||
**A composition-level re-derivation, caught in review of this phase's own diff.** Decision 4(c)
|
||||
anticipated a consumer post-*filtering* an owner's result. The shape that actually appeared is its
|
||||
mirror: two sites re-*assembling* a window out of the owner's primitives —
|
||||
`locateMilestoneHeadings` → pick a heading → `computeMilestoneSectionEnd` → slice — in
|
||||
`getMilestonePhaseFilter`'s `versionOverride` branch and in `milestone.cts`'s unstarted-phase guard.
|
||||
Both call the canonical owner at every step, so the drift guard and an owner-level identity test are
|
||||
both green, and the two compositions had **already diverged** on whether to skip a closed milestone
|
||||
heading. Decision 4(c) is therefore read to cover **assembly as well as post-processing**: where a
|
||||
derivation has a composition, the composition is itself an owner. Added as
|
||||
`sliceMilestoneWindow`; both sites route through it.
|
||||
|
||||
**Decision 3's Tier-2 table, re-derived for Phase 2** per its own contingency clause. The row this
|
||||
ADR predicted lands as written, plus two the prediction did not contain:
|
||||
|
||||
| Command surface | Output change |
|
||||
|---|---|
|
||||
| `roadmap analyze` | gains a `scope` field. `phase_count: 0` is still emitted verbatim — what changes is that a sibling field now says whether that zero is an answer. Stated precisely because the first draft of this row claimed the count itself changed, which is not what shipped |
|
||||
| `/gsd:progress --next` Route 0 | `gsd-core/workflows/next.md` treats a non-`complete` scope as scan-failed (warn + fall through to the prior-phase check) instead of looping a phase list the scan could not populate. Without this the new field would be a diagnostic no consumer reads, and #3165's actual symptom — the resume invariant reporting clean because it could not run — would still reproduce |
|
||||
| `milestone complete` | refuses (unless `--force`) when the window's scope is `TRUNCATED` — the milestone heading was found but its section closes before reaching any phase entries, even though the ROADMAP has phase entries elsewhere — instead of pass-all archiving every phase directory on disk (#3166). `UNREADABLE` and `UNSCOPED` are pre-existing, legitimately-handled states (`missingExplicitVersion` errors where that matters; a missing ROADMAP.md has its own documented graceful path) and are not refused here. |
|
||||
| `milestone complete` unstarted-phase guard — **not predicted** | the guard scoped its window by STATE.md's `milestone:` field while the filter beside it scoped by the `version` argument; the two could disagree, and the guard under-detected unstarted phases on the destructive path. Both now use the `version` argument. |
|
||||
|
||||
**Scope note.** Phase 3 (enumeration) inherits a window layer that is now single-owner and
|
||||
scope-carrying; its own guard starts from a green windowing baseline, exactly as Phase 1 left plan
|
||||
counting clean for Phase 3.
|
||||
|
||||
Reference in New Issue
Block a user