From 1e0c16399939b91916559484cd66c73afd7f664e Mon Sep 17 00:00:00 2001 From: Ryan de Melo Date: Wed, 23 Sep 2026 22:04:45 +0800 Subject: [PATCH 1/2] docs(openspec): propose reporting requirements two active changes both claim A change proposal for the behavior #1698 implements, written after the fact because that PR opened before CONTRIBUTING.md asked for proposals ahead of feature code. Bulk validate would report each requirement that two or more active changes claim, with the operation each applies and whether the main spec holds it today. It is advisory only and never moves the exit code. The design records why it reports claims without ranking them: a ranking needs the applicability rules in specs-apply.ts, and a second copy of those rules disagreed with archive when tested. Refs #1669 --- .../.openspec.yaml | 2 + .../report-cross-change-overlap/design.md | 90 +++++++++++++++++++ .../report-cross-change-overlap/proposal.md | 31 +++++++ .../specs/cli-validate/spec.md | 89 ++++++++++++++++++ .../report-cross-change-overlap/tasks.md | 27 ++++++ 5 files changed, 239 insertions(+) create mode 100644 openspec/changes/report-cross-change-overlap/.openspec.yaml create mode 100644 openspec/changes/report-cross-change-overlap/design.md create mode 100644 openspec/changes/report-cross-change-overlap/proposal.md create mode 100644 openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md create mode 100644 openspec/changes/report-cross-change-overlap/tasks.md diff --git a/openspec/changes/report-cross-change-overlap/.openspec.yaml b/openspec/changes/report-cross-change-overlap/.openspec.yaml new file mode 100644 index 0000000000..265da3d94e --- /dev/null +++ b/openspec/changes/report-cross-change-overlap/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-23 diff --git a/openspec/changes/report-cross-change-overlap/design.md b/openspec/changes/report-cross-change-overlap/design.md new file mode 100644 index 0000000000..807fd81fcb --- /dev/null +++ b/openspec/changes/report-cross-change-overlap/design.md @@ -0,0 +1,90 @@ +## Context + +See `proposal.md` for the failure this addresses. Every existing check in `validate` and `archive` takes one change and one base, the main specs as they are now. Nothing reads two active changes side by side, so nothing can see them converge. + +The repository's parallel merge plan (`openspec-parallel-merge-plan.md`) calls this Phase 1: make overlap visible before building anything that acts on it. The open `add-change-stacking-awareness` change plans overlap warnings from a `touches` field that authors fill in by hand. This proposal needs no metadata. It reads the claims from the delta files each change already has, at requirement level, so it cannot drift from what archive will actually apply. The two fit together: declared intent from metadata, observed claims from deltas. + +## Goals / Non-Goals + +**Goals:** + +- Show every requirement that more than one active change claims, before any of them archives. +- Give the author enough to judge the overlap without opening files: which changes, which operations, and whether the requirement exists today. +- Read exactly the files archive will apply, from the root the run selected. +- Stay out of the way: no new failure, no exit code change, nothing printed when there is nothing to report. + +**Non-Goals:** + +- Ranking overlaps by severity, or saying which archive order works. See decision 1. +- Detecting overlap at scenario level, or between changes and archived history. +- Changing any existing check, the findings report, or targeted `validate ` output. +- Deciding whether an overlap is intended. Overlap is often deliberate, such as a stacked pair or sequenced work, and nothing in a delta records intent. + +## Decisions + +### 1. Report claims, never a verdict + +The obvious next step is to rank each overlap: this pair cannot both archive, this pair only works in one order. An earlier version of #1698 did that, and it was removed. + +Knowing whether a given archive order aborts means reproducing the preconditions in `specs-apply.ts`, including the cases it treats as already synced rather than as collisions: an ADD of a requirement already present word for word, a rename whose source is gone but whose target is present, a REMOVED whose target is already absent. A second copy of those rules would be free to disagree with the code that does the writing. Tested against real archive runs, the ranked version did disagree. It reported conflicts for pairs that archive cleanly in either order, and in one case recommended the order that actually fails. + +A wrong verdict is worse than none, because it tells an author to rewrite a change that would have archived fine. Ranking needs one applicability check that `archive` and `validate` both call. #1112 shows the same split from the other side, with `validate` passing a MODIFIED whose target does not exist and `archive` refusing it later. That shared check is future work. This proposal stays on the side of the line that cannot be wrong. + +The one piece of context it does add is `inMainSpec`. Two changes editing shared text is a different situation from two changes each proposing a requirement that does not exist yet, and that fact is read directly from the main spec rather than predicted. + +### 2. Advisory, and never a failure + +Overlap never moves the exit code. Any error inside the scan is swallowed and the scan reports nothing, because every delta it reads is also read by the validation of each change, which reports unreadable or malformed files on its own path. Advisory output must never be the thing that fails a run. + +### 3. Read what archive reads + +Delta files are enumerated with `discoverSpecFiles()`, the same walk `archive` and `specs-apply` use. Nested capability ids such as `platform/session-layout` are included, and nothing is read that archive would ignore. Requirement names are matched with `normalizeRequirementName()`, the function the validator and archive already share. Both paths come from the resolved root, `root.changesDir` and `root.specsDir`, so a `--store` run scans the store it selected rather than a path rebuilt from the project root. + +A requirement is identified by spec id plus normalized name. The same name in two different specs is not an overlap. + +### 4. Where the report appears + +The scan runs only for bulk validation with changes in scope, and is skipped below two changes, since one change cannot overlap anything. Targeted validation of a single item does not scan. + +In human output the section follows the existing details and prints only when there is at least one overlap: + +```text +⚠ 2 requirements are claimed by more than one active change: + colors: Widget colors (not in the main spec yet) + adds-focus ADDED, adds-hover ADDED + widgets: Widget state (in the main spec) + adds-focus MODIFIED, adds-hover MODIFIED +Whichever of these archives second lands on a spec the first one changed; re-read it before archiving. +``` + +In JSON, `overlaps` sits beside `items` and `summary` in the full v1 document: + +```json +"overlaps": [ + { + "specId": "widgets", + "requirement": "Widget state", + "inMainSpec": true, + "claimants": [ + { "changeId": "adds-focus", "operation": "MODIFIED", "requirement": "Widget state" }, + { "changeId": "adds-hover", "operation": "MODIFIED", "requirement": "Widget state" } + ] + } +] +``` + +The field is present, possibly empty, whenever changes are in scope, including an empty scope, so a consumer sees one shape on every path. It is absent for `--specs` and `--archived`. The findings document from `--report findings` carries item findings only by design, so the scan is skipped on that path rather than computed and discarded. + +### 5. Stable order + +Overlaps are sorted by spec id, then requirement name, and claimants by change id, then operation. Sorting compares code units rather than using `localeCompare`, so the order does not change with the ICU locale of the machine and output can be diffed in CI. + +### 6. Which changes count as active + +The scan takes the list of change ids the run already resolved and never works out on its own what "active" means. If change state moves into metadata, as #1683 and the `status` discussion on #1813 suggest, only that one call site in `validate` changes. + +## Risks / Tradeoffs + +- **Noise from deliberate overlap.** A stacked pair will be reported every run. That is the cost of refusing to guess intent. It is one short section, it never fails anything, and it disappears when either change archives. +- **Requirement level only.** Two changes touching different scenarios of one requirement are reported. That matches how archive applies a MODIFIED block, which replaces the whole requirement. +- **Cost.** One extra read of each change's delta files and of the main specs those deltas name. Only specs some change claims are read. diff --git a/openspec/changes/report-cross-change-overlap/proposal.md b/openspec/changes/report-cross-change-overlap/proposal.md new file mode 100644 index 0000000000..a15dd2bddc --- /dev/null +++ b/openspec/changes/report-cross-change-overlap/proposal.md @@ -0,0 +1,31 @@ +## Why + +Every check `validate` runs compares one change against the main specs as they are today. When two active changes edit the same requirement, each one is consistent with a spec that neither has landed in yet, so both pass and `validate --changes` reports nothing. The collision only shows up when the first change archives and the second starts failing. By then the second author has built against a base that moved, over a requirement they may never have touched. + +The scenario loss guard stopped this from losing data: archive now refuses a MODIFIED block that would drop scenarios. What is left is timing. The refusal lands late, on the wrong person, after the work is done. #1246 describes the failure, #1669 asks for the overlap to be visible before either change archives, and #1387 reports a working tool built for the same gap outside the CLI. + +## What Changes + +- When changes are in scope (`--changes` or `--all`), bulk `validate` reports each requirement that two or more active changes claim. +- Each entry names the spec, the requirement, every claiming change with the operation it applies (`ADDED`, `MODIFIED`, `REMOVED`, `RENAMED_FROM`, `RENAMED_TO`), and whether the main spec holds that requirement today. +- A RENAMED delta claims both of its names. The old name collides with anyone editing it, and the new name collides with anyone adding it. +- Human output gets one advisory section after the existing details. JSON output gets an `overlaps` array, present and possibly empty whenever changes are in scope, and absent otherwise. +- The report is advisory. It never changes the exit code, never fails a run, and draws no conclusion about which change is wrong or which should archive first. +- `--report findings` keeps its own document unchanged and carries no overlaps. + +## Capabilities + +### New Capabilities + +_None._ + +### Modified Capabilities + +- `cli-validate`: bulk validation reports requirements that more than one active change claims. + +## Impact + +- **Public CLI:** new output on `validate --changes` and `validate --all` only when an overlap exists. No new flag, no default behavior change, no exit code change. +- **JSON consumers:** one additive top level field, `overlaps`, on the full v1 document. The `add-validation-findings-report` design names exactly this field as needing an explicit contract decision before it is added, and this proposal is that decision. The findings document is untouched. +- **Implementation:** a new read only module under `src/core/`, a call site in `src/commands/validate.ts`, docs in `docs-lab/reference/cli.md` and `docs/agent-contract.md` §4.3, tests, and a minor changeset. No new dependency. +- **Existing implementation:** #1698 implements this proposal as written and is kept rebased on `main`. diff --git a/openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md b/openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md new file mode 100644 index 0000000000..a96c213415 --- /dev/null +++ b/openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md @@ -0,0 +1,89 @@ +## ADDED Requirements + +### Requirement: Bulk validation SHALL report requirements that more than one active change claims + +When active changes are in scope of a bulk validation (`--changes` or `--all`), the `validate` command SHALL report each requirement that two or more active changes claim in their delta specs. A requirement is identified by its spec id and its normalized name. The report SHALL be advisory: it SHALL NOT change the exit code, SHALL NOT fail the run, and SHALL NOT state which change is wrong or which archive order is correct. + +#### Scenario: Two changes modify the same requirement + +- **WHEN** two active changes each have a MODIFIED block for the same requirement in the same spec +- **AND** that requirement exists in the main spec +- **THEN** human output SHALL include one advisory entry naming the spec, the requirement, and `(in the main spec)` +- **AND** the entry SHALL list both changes with the operation each applies + +#### Scenario: Two changes add the same requirement + +- **WHEN** two active changes each ADD a requirement with the same name to the same spec +- **AND** the main spec does not hold that requirement +- **THEN** the entry SHALL say `(not in the main spec yet)` + +#### Scenario: A rename claims both of its names + +- **WHEN** one active change RENAMES a requirement from an old name to a new name +- **AND** a second active change claims the old name, and a third claims the new name +- **THEN** the report SHALL include an entry for the old name listing `RENAMED_FROM` beside the second change's operation +- **AND** an entry for the new name listing `RENAMED_TO` beside the third change's operation + +#### Scenario: One change never overlaps itself + +- **WHEN** a single change is the only claimant of a requirement, including a change whose RENAMED pair names it twice +- **THEN** no overlap SHALL be reported for that requirement + +#### Scenario: The same name in different specs is not an overlap + +- **WHEN** two active changes claim requirements with the same name in two different specs +- **THEN** no overlap SHALL be reported + +#### Scenario: Overlap never changes the exit code + +- **WHEN** every change in scope is valid and an overlap exists +- **THEN** validation SHALL exit 0 +- **AND WHEN** a change in scope is invalid and an overlap exists +- **THEN** validation SHALL exit 1 exactly as it would without the overlap + +#### Scenario: Nothing is printed when nothing overlaps + +- **WHEN** no requirement is claimed by more than one active change +- **THEN** human output SHALL be unchanged from validation without this report + +#### Scenario: JSON output carries an overlaps array whenever changes are in scope + +- **WHEN** a user runs `validate --changes --json` or `validate --all --json` +- **THEN** the full v1 document SHALL include an `overlaps` array, empty when nothing overlaps and also when the scope holds no changes +- **AND** each entry SHALL carry `specId`, `requirement`, `inMainSpec`, and `claimants`, where each claimant carries `changeId`, `operation`, and `requirement` +- **AND** `operation` SHALL be one of `ADDED`, `MODIFIED`, `REMOVED`, `RENAMED_FROM`, `RENAMED_TO` + +#### Scenario: No overlaps field without changes in scope + +- **WHEN** a user runs `validate --specs --json` or `validate --archived --json` +- **THEN** the document SHALL NOT include an `overlaps` field + +#### Scenario: The findings report is unchanged + +- **WHEN** a user runs bulk validation with `--report findings` +- **THEN** the findings document SHALL NOT include overlaps +- **AND** the overlap scan SHALL NOT run + +#### Scenario: Targeted validation does not scan + +- **WHEN** a user validates a single named change +- **THEN** no overlap report SHALL be produced + +#### Scenario: The scan reads what archive applies + +- **WHEN** a change stores delta specs under nested capability paths +- **THEN** the scan SHALL find them with the same discovery archive uses and report their nested spec ids +- **AND WHEN** a run selects a store +- **THEN** the scan SHALL read that store's changes and main specs + +#### Scenario: A scan failure reports nothing + +- **WHEN** a change's delta files cannot be read during the scan +- **THEN** the scan SHALL skip that change without an error of its own +- **AND** the change's own validation SHALL report the problem as it does today + +#### Scenario: Output order is stable + +- **WHEN** overlaps are reported +- **THEN** they SHALL be ordered by spec id and then requirement name, with claimants ordered by change id and then operation +- **AND** the order SHALL NOT depend on the locale of the machine diff --git a/openspec/changes/report-cross-change-overlap/tasks.md b/openspec/changes/report-cross-change-overlap/tasks.md new file mode 100644 index 0000000000..1f328be282 --- /dev/null +++ b/openspec/changes/report-cross-change-overlap/tasks.md @@ -0,0 +1,27 @@ +## 1. Overlap detection + +- [ ] 1.1 Add a read only module under `src/core/` that turns each delta file into claims (spec id, normalized requirement name, operation), with RENAMED contributing a `RENAMED_FROM` and a `RENAMED_TO` claim, and a repeated claim inside one change dropped +- [ ] 1.2 Enumerate delta files with `discoverSpecFiles()` and match names with `normalizeRequirementName()`, so the scan sees exactly what archive applies +- [ ] 1.3 Group claims by spec id and normalized name, keep groups claimed by two or more changes, and set `inMainSpec` from the main spec's current requirements +- [ ] 1.4 Sort overlaps and claimants by code unit, never by locale +- [ ] 1.5 Skip an unreadable change or spec instead of throwing + +## 2. Validate integration + +- [ ] 2.1 Run the scan only for bulk validation with changes in scope, only with two or more changes, and never for `--report findings` +- [ ] 2.2 Pass `root.changesDir`, `root.specsDir`, and the change ids the run already resolved, so store runs scan the selected store +- [ ] 2.3 Print the advisory section after the existing details only when an overlap exists +- [ ] 2.4 Add `overlaps` to the full v1 JSON document whenever changes are in scope, including an empty scope, and leave it out otherwise +- [ ] 2.5 Swallow any scan error and leave the exit code exactly as validation sets it + +## 3. Documentation and release tracking + +- [ ] 3.1 Document the section, the JSON fields, and the advisory status in `docs-lab/reference/cli.md` +- [ ] 3.2 Add `overlaps` to the `validate --json` shape in `docs/agent-contract.md` §4.3 +- [ ] 3.3 Add a minor changeset + +## 4. Verification + +- [ ] 4.1 Unit tests for claims, grouping, rename at both ends, same name in different specs, nested spec ids, three claimants on one requirement, and stable order +- [ ] 4.2 End to end tests through the CLI for human and JSON output, `--specs`, empty and single change scopes, `--report findings`, a selected store, and an unchanged exit code when an overlap exists +- [ ] 4.3 Run build, lint, type checks, the full suite, and `openspec validate report-cross-change-overlap --strict` From 7fd1a5f8c83cbf067580153b29996f1403615c4e Mon Sep 17 00:00:00 2001 From: Ryan de Melo Date: Wed, 23 Sep 2026 22:17:01 +0800 Subject: [PATCH 2/2] docs(openspec): define what an overlap scan failure reports An unreadable change or main spec only removes itself from the scan, and overlaps among the remaining changes are still reported. Any other error empties the report for that run. The design, spec and tasks now say the same thing, matching #1698. --- openspec/changes/report-cross-change-overlap/design.md | 2 +- .../report-cross-change-overlap/specs/cli-validate/spec.md | 3 ++- openspec/changes/report-cross-change-overlap/tasks.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openspec/changes/report-cross-change-overlap/design.md b/openspec/changes/report-cross-change-overlap/design.md index 807fd81fcb..96b5c235f7 100644 --- a/openspec/changes/report-cross-change-overlap/design.md +++ b/openspec/changes/report-cross-change-overlap/design.md @@ -34,7 +34,7 @@ The one piece of context it does add is `inMainSpec`. Two changes editing shared ### 2. Advisory, and never a failure -Overlap never moves the exit code. Any error inside the scan is swallowed and the scan reports nothing, because every delta it reads is also read by the validation of each change, which reports unreadable or malformed files on its own path. Advisory output must never be the thing that fails a run. +Overlap never moves the exit code, and the scan fails in two layers. An unreadable input only removes itself: a change whose delta files cannot be discovered or read is skipped, a main spec that cannot be read counts as holding no requirements, and overlaps among the remaining changes are still reported. Any other error inside the scan is swallowed and the report is empty for that run. Neither case loses information, because every delta the scan reads is also read by the validation of each change, which reports unreadable or malformed files on its own path. Advisory output must never be the thing that fails a run. ### 3. Read what archive reads diff --git a/openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md b/openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md index a96c213415..3756a03b0b 100644 --- a/openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md +++ b/openspec/changes/report-cross-change-overlap/specs/cli-validate/spec.md @@ -76,10 +76,11 @@ When active changes are in scope of a bulk validation (`--changes` or `--all`), - **AND WHEN** a run selects a store - **THEN** the scan SHALL read that store's changes and main specs -#### Scenario: A scan failure reports nothing +#### Scenario: An unreadable change is skipped - **WHEN** a change's delta files cannot be read during the scan - **THEN** the scan SHALL skip that change without an error of its own +- **AND** overlaps among the remaining changes SHALL still be reported - **AND** the change's own validation SHALL report the problem as it does today #### Scenario: Output order is stable diff --git a/openspec/changes/report-cross-change-overlap/tasks.md b/openspec/changes/report-cross-change-overlap/tasks.md index 1f328be282..2782fce8f7 100644 --- a/openspec/changes/report-cross-change-overlap/tasks.md +++ b/openspec/changes/report-cross-change-overlap/tasks.md @@ -4,7 +4,7 @@ - [ ] 1.2 Enumerate delta files with `discoverSpecFiles()` and match names with `normalizeRequirementName()`, so the scan sees exactly what archive applies - [ ] 1.3 Group claims by spec id and normalized name, keep groups claimed by two or more changes, and set `inMainSpec` from the main spec's current requirements - [ ] 1.4 Sort overlaps and claimants by code unit, never by locale -- [ ] 1.5 Skip an unreadable change or spec instead of throwing +- [ ] 1.5 Skip an unreadable change, and treat an unreadable main spec as holding no requirements, so overlaps among the remaining changes are still reported ## 2. Validate integration