Repository navigation
Archiving two changes that MODIFY the same requirement silently drops scenarios (distinct from #1112) #1246
Description
Activity
- added 5 commits that reference this issue
on Jun 29, 2026 - added 4 commits that reference this issue
on Jun 29, 2026 - added a commit that references this issue
on Jul 8, 2026 - added a commit that references this issue
on Jul 8, 2026 - added 2 commits that reference this issue
on Jul 11, 2026 Heads-up on the scenario-drift gate that guards this case:
findMissingCurrentScenariosinsrc/core/specs-apply.tshas a duplicate-scenario-name blind spot, so the silent drop this issue describes can still slip through even with the gate in place.The gate stores the incoming (MODIFIED) scenario names in a
Setand flags any current scenario whose name isn't in it:function findMissingCurrentScenarios(current: RequirementBlock, incoming: RequirementBlock): string[] { const incomingScenarioNames = new Set(parseScenarioBlocks(incoming.raw).map((s) => s.name)); return parseScenarioBlocks(current.raw) .filter((s) => !incomingScenarioNames.has(s.name)) // set membership ignores multiplicity .map((s) => s.name); }
Because it's set membership, if a requirement has two scenarios with the same name and a MODIFIED block keeps only one, both current copies still "match" the single incoming name,
missingis empty, and the second copy is dropped with no abort.Minimal repro (single change)
Current canonical spec:
### Requirement: Login The system SHALL authenticate. #### Scenario: Validate - **WHEN** input is empty - **THEN** reject #### Scenario: Validate - **WHEN** input is malformed - **THEN** reject
A change whose
## MODIFIED Requirementskeeps only one#### Scenario: Validate→archivesucceeds and the malformed-input scenario is gone. (Same failure mode as the two-change flow above, just surfaced through the gate's name-set.)Suggested tweak (whichever fix direction you take)
Compare with multiplicity instead of set membership, so duplicates can't cancel out:
function findMissingCurrentScenarios(current, incoming): string[] { const remaining = new Map<string, number>(); for (const s of parseScenarioBlocks(incoming.raw)) remaining.set(s.name, (remaining.get(s.name) ?? 0) + 1); const missing: string[] = []; for (const s of parseScenarioBlocks(current.raw)) { const n = remaining.get(s.name) ?? 0; if (n > 0) remaining.set(s.name, n - 1); else missing.push(s.name); } return missing; }
(Arguably duplicate scenario names within one requirement also deserve a
validatewarning in their own right.)Surfaced while hardening a downstream fork; language-agnostic, reproduces on upstream with English-only specs.
- added a commit that references this issue
on Jul 20, 2026 Closing as fixed by #1391 (commit 470f572). Archive now runs a multiplicity-aware scenario-drift check (
findMissingCurrentScenariosinsrc/core/specs-apply.ts): if the current spec contains scenarios that the incoming MODIFIED block does not, the archive aborts before touching any file.Verified with your exact repro on current main. Archiving change A succeeds; archiving change B fails with:
auth MODIFIED failed for header "### Requirement: Login" - current spec contains scenario(s) not present in the modified block: "S_A". Refresh the change spec before archiving to avoid dropping scenarios. Aborted. No files were changed.and S_A stays in the canonical spec. That is your option A, stop-the-bleeding edition; scenario-level merge (option B) would be its own feature if demand appears. The fix is on main and ships with the next release. Thanks for the unusually precise root-cause analysis.
- added a commit that references this issue
on Sep 7, 2026 - added a commit that references this issue
on Sep 18, 2026 - added a commit that references this issue
on Oct 9, 2026
Related to #1112, but a distinct (opposite) failure mode. #1112 covers
MODIFIEDagainst a header that does not exist in the base spec →openspec archiveaborts loudly (not found). This issue is the inverse: the header does exist and isMODIFIEDby two changes, so archive succeeds but silently overwrites the earlier change's scenarios. #1112's validate-time fix does not address this case.Summary
When two active changes both list the same
### Requirement:under## MODIFIED Requirementsand are archived sequentially, the second archive overwrites the first — the earlier change's scenarios are silently dropped from the canonical spec. No warning, no diff, no conflict marker; archive reports success.Root cause
buildUpdatedSpecinsrc/core/specs-apply.ts(around L296) appliesMODIFIEDas a whole-block replace keyed by requirement name (nameToBlock.set(key, mod)). The delta carries the change author's view of the requirement at authoring time; when applied, it replaces the entire requirement block, discarding any sibling scenarios another change added in between.validateChangeDeltaSpecschecks only intra-change consistency — there is no base-fingerprint / cross-change drift detection, andarchive.tshas no conflict gate.Reproduction
MODIFIEDrequirement R, adds scenarioS_A.MODIFIEDthe same R, adds scenarioS_B.S_A(plus existing scenarios).buildUpdatedSpecwhole-block replaces R with B's version →S_Ais gone, silently.Change B's delta never knew
S_Aexisted, so replacing the whole block drops it.Fix options (RFC drafted)
buildUpdatedSpecto merge scenario lists rather than whole-block replace, so non-overlapping scenario additions commute. Root fix, larger scope + migration.Happy to open a PR in whichever direction you prefer. Full RFC available on request.