Skip to content

fix: scenario-level merge for MODIFIED requirements#843

Open
phuongvm wants to merge 2 commits intoFission-AI:mainfrom
phuongvm:fix/scenario-level-merge
Open

fix: scenario-level merge for MODIFIED requirements#843
phuongvm wants to merge 2 commits intoFission-AI:mainfrom
phuongvm:fix/scenario-level-merge

Conversation

@phuongvm
Copy link

@phuongvm phuongvm commented Mar 15, 2026

Previously, the MODIFIED handler in specs-apply.ts replaced entire requirement blocks wholesale. When a delta spec only included modified/new scenarios (the natural authoring pattern), unchanged scenarios within the same requirement were silently dropped.

This fix adds scenario-level parsing and merge:

  • New ScenarioBlock type and parseScenarios() in requirement-blocks.ts
  • Scenarios tagged (MODIFIED): replace matching scenario by name
  • Scenarios tagged (REMOVED): remove matching scenario
  • New scenarios (not in main): appended
  • Unchanged scenarios: preserved from main spec
  • No tags at all: falls back to full-block replacement (backward compat)
  • Warning emitted when scenario count decreases unexpectedly

Includes 9 new test cases covering all merge behaviors.

Fixes scenario data loss during archive/apply-specs operations.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed markdown parser to correctly handle headers inside fenced code blocks, preventing misinterpretation of code content as document structure.
  • New Features

    • Added scenario-level merging capability for delta specifications, enabling granular updates to individual scenarios within requirement blocks while preserving unchanged content.
  • Tests

    • Added comprehensive test coverage for fenced code block parsing and scenario-level merge behavior.

Previously, the MODIFIED handler in specs-apply.ts replaced entire
requirement blocks wholesale. When a delta spec only included
modified/new scenarios (the natural authoring pattern), unchanged
scenarios within the same requirement were silently dropped.

This fix adds scenario-level parsing and merge:
- New ScenarioBlock type and parseScenarios() in requirement-blocks.ts
- Scenarios tagged (MODIFIED): replace matching scenario by name
- Scenarios tagged (REMOVED): remove matching scenario
- New scenarios (not in main): appended
- Unchanged scenarios: preserved from main spec
- No tags at all: falls back to full-block replacement (backward compat)
- Warning emitted when scenario count decreases unexpectedly

Includes 9 new test cases covering all merge behaviors.

Fixes scenario data loss during archive/apply-specs operations.
@phuongvm phuongvm requested a review from TabishB as a code owner March 15, 2026 15:38
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Two major features are introduced: fenced code block awareness in markdown parsing to prevent misinterpreting headers inside code blocks as document structure, and scenario-level merging for MODIFIED requirement blocks enabling granular delta spec application at the scenario level instead of full-block replacement.

Changes

Cohort / File(s) Summary
Fenced Code Block Parsing Documentation
openspec/changes/archive/2026-03-08-fenced-codeblock-parsing/.openspec.yaml, ...proposal.md, ...design.md, ...tasks.md, ...specs/cli-archive/spec.md
Adds OpenSpec change documentation for fenced code block awareness feature, including design rationale, implementation tasks, and delta merge specification for correct handling of code blocks during spec operations.
Scenario-Level Merge Documentation
openspec/changes/archive/2026-03-15-scenario-level-merge/.openspec.yaml, ...proposal.md, ...design.md, ...tasks.md, ...specs/cli-archive/spec.md
Adds OpenSpec change documentation for scenario-level merging feature, detailing merge strategy for granular scenario updates within MODIFIED requirement blocks, including parsing requirements and backward compatibility heuristics.
Core Parser Implementation
src/core/parsers/requirement-blocks.ts, test/core/parsers/requirement-blocks-fence.test.ts
Extends requirement-blocks parser with scenario-level parsing (ScenarioBlock, RequirementBlockWithScenarios interfaces, parseScenarios function) and fenced code block state tracking across splitTopLevelSections, extractRequirementsSection, and parseRequirementBlocksFromSection to skip headers inside code blocks.
Merge Logic Implementation
src/core/specs-apply.ts, test/core/archive.test.ts
Adds scenario-aware merge helpers (hasScenarioTags, mergeScenarios, reconstructRequirementBlock) and integrates scenario-level merging into MODIFIED path with backward-compatibility fallback; comprehensive test suite validates complex delta-merge scenarios, scenario name normalization, and warning behavior.
Configuration & Utilities
src/utils/change-utils.ts, .gitignore, openspec/specs/cli-archive/spec.md
Updates utilities with quote style formatting (no semantic changes), adds .serena directory to gitignore, and extends main archive spec with scenario-level merge behavior documentation.

Sequence Diagram

sequenceDiagram
    participant User
    participant Parser as Requirement Parser
    participant Merger as Scenario Merger
    participant Reconstructor as Block Reconstructor
    participant Archive as Archive State

    User->>Parser: parseScenarios(delta block)
    Parser->>Parser: extract description & scenarios
    Parser->>Parser: detect MODIFIED/REMOVED tags
    Parser-->>Merger: RequirementBlockWithScenarios

    Merger->>Merger: hasScenarioTags(delta)
    alt Has Scenario Tags
        Merger->>Parser: parseScenarios(main block)
        Parser-->>Merger: main scenarios
        Merger->>Merger: mergeScenarios(main, delta)
        Merger->>Merger: match by normalized name
        Merger->>Merger: apply MODIFIED/REMOVED/ADDED rules
        Merger-->>Reconstructor: merged scenario array
        Reconstructor->>Reconstructor: reconstructRequirementBlock()
        Reconstructor-->>Archive: merged requirement block
    else No Scenario Tags
        Merger-->>Archive: full-block replacement
    end
    Merger->>Merger: check scenario count decrease
    Merger-->>User: emit warning if unexpected decrease
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • Issue #312: Addresses the core parser bug where header regexes match lines inside fenced code blocks by introducing state tracking (isInsideFence) across three parsing functions to skip header detection within code fences.

Suggested reviewers

  • TabishB

Poem

🐰 In markdown's garden where headers once ran free,
Now fences protect code from parsing decree!
Scenarios gather and merge with such grace,
Each block finds its home in its proper place.
With MODIFIED tags and normalized names,
The delta specs dance through archive's great games! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: scenario-level merge for MODIFIED requirements' is clear, concise, and directly summarizes the main change in the pull request—implementing scenario-level merging for MODIFIED requirement blocks to prevent data loss.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
.serena/memories/project_overview.md (1)

20-20: Hardcoded version will become stale.

The version v1.2.0 is hardcoded here. Consider removing the version number or noting it may be outdated, since this documentation won't auto-update with releases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.serena/memories/project_overview.md at line 20, The package line hardcodes
a specific release ("v1.2.0") for `@fission-ai/openspec`; remove the fixed
version or replace it with a non‑stale phrasing (e.g., just
`@fission-ai/openspec` or `@fission-ai/openspec (version may change)`) so the
documentation doesn't become outdated—update the line that currently reads
"**Package**: `@fission-ai/openspec` v1.2.0 (MIT, published on npm)"
accordingly.
.serena/project.yml (1)

93-99: Consider reordering languages to prioritize TypeScript.

Per the configuration comment on line 91, "the first language is the default language and the respective language server will be used as a fallback." For a TypeScript project, having powershell first may not be ideal. Consider moving typescript to the first position.

Suggested reordering
 languages:
-- powershell
-- terraform
-- yaml
-- python
-- markdown
 - typescript
+- markdown
+- yaml
+- python
+- powershell
+- terraform
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.serena/project.yml around lines 93 - 99, The languages list under the
languages: key places powershell first, making it the default language server;
update the YAML so typescript is the first entry (i.e., move 'typescript' to the
top of the array and keep the remaining items in their current order:
powershell, terraform, yaml, python, markdown — or any preferred order after
typescript) so the TypeScript language server becomes the default.
test/core/archive.test.ts (2)

1039-1095: Test name doesn't match behavior being tested.

The test is named "should emit scenario count warning on unexpected decrease" but lines 1091-1094 verify that no warning is emitted because the count didn't decrease. Consider either:

  1. Renaming to something like "should not emit warning when scenario count is preserved"
  2. Adding an additional test case that actually triggers and verifies the warning emission

The tasks.md mentions testing "Scenario count warning emitted on unexpected decrease" but this test doesn't exercise that path.

💡 Suggested test name change or additional test
-    it('should emit scenario count warning on unexpected decrease', async () => {
+    it('should not emit warning when scenario count is preserved via (MODIFIED) tags', async () => {

Or add a separate test that actually triggers the warning by having an unexpected decrease.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/archive.test.ts` around lines 1039 - 1095, The test name "should
emit scenario count warning on unexpected decrease" is misleading because the
body asserts no warning; either rename the test to reflect preserved count
(e.g., "should not emit warning when scenario count is preserved") or add a new
test that uses archiveCommand.execute with a change delta that removes one or
more scenarios (so the resulting main spec has fewer scenarios) and assert
console.log was called with expect.stringContaining('scenario count changed');
update references to changeName/changeSpecDir and mainSpecDir in the new test to
create the removing delta and ensure noValidate/yes flags are consistent with
other tests.

1138-1138: Consider a more robust assertion for verifying old content was replaced.

The assertion expect(updated).not.toContain('THEN gate is checked\n') relies on a specific line ending pattern. This could be fragile if the spec reconstruction normalizes line endings differently. A more robust approach would be to check for the exact old phrase that's unique to the original content.

💡 Alternative assertion
-      expect(updated).not.toContain('THEN gate is checked\n');
+      // Ensure old scenario body is replaced - the unique old ending was just "is checked"
+      // while new ending is "checked with evidence"
+      expect(updated).not.toMatch(/THEN gate is checked(?!\s+with evidence)/);

Or simply rely on the positive assertion at line 1137 which already confirms the new content is present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/archive.test.ts` at line 1138, The negative assertion is brittle
due to the newline; update the test around the `updated` variable to assert the
absence of the unique old phrase without relying on line endings (e.g., assert
`updated` does not include "THEN gate is checked" or use a regex like /THEN gate
is checked/), or simply remove the negative check and rely on the existing
positive assertion that the new content is present; reference the `updated`
variable and the failing `expect(updated).not.toContain('THEN gate is
checked\n')` assertion when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/specs-apply.ts`:
- Around line 75-127: The merged output is retaining delta-specific tags (e.g.,
"(MODIFIED)" / "(REMOVED)") because mergeScenarios pushes delta
ScenarioBlock.raw/headerLine directly; fix by normalizing scenario blocks before
adding them to the result: in mergeScenarios (references: mergeScenarios,
ScenarioBlock.raw, ScenarioBlock.headerLine, ScenarioBlock.tag) create or call a
helper (e.g., stripScenarioTag) that removes trailing "(MODIFIED|REMOVED)" from
headerLine, rebuilds raw with the cleaned header, and clears the tag, and use
that cleaned ScenarioBlock when pushing deltaScenario (where
result.push(deltaScenario) is invoked) and when appending new delta scenarios
(where result.push(s) is invoked); this ensures reconstructRequirementBlock sees
clean headers.

In `@src/utils/change-utils.ts`:
- Line 6: The default schema constant DEFAULT_SCHEMA was changed to
"opti-spec-driven" which can cause createChange() to fail because
validateSchemaName(...) still expects the original "spec-driven"; either revert
DEFAULT_SCHEMA back to "spec-driven" to preserve the previous default path used
by createChange(), or update all default-resolution paths/tests and ensure
validateSchemaName, createChange, and any schema asset deployment accept
"opti-spec-driven"; locate DEFAULT_SCHEMA, createChange, and validateSchemaName
in change-utils.ts and make the chosen change consistently across those symbols.

---

Nitpick comments:
In @.serena/memories/project_overview.md:
- Line 20: The package line hardcodes a specific release ("v1.2.0") for
`@fission-ai/openspec`; remove the fixed version or replace it with a non‑stale
phrasing (e.g., just `@fission-ai/openspec` or `@fission-ai/openspec (version
may change)`) so the documentation doesn't become outdated—update the line that
currently reads "**Package**: `@fission-ai/openspec` v1.2.0 (MIT, published on
npm)" accordingly.

In @.serena/project.yml:
- Around line 93-99: The languages list under the languages: key places
powershell first, making it the default language server; update the YAML so
typescript is the first entry (i.e., move 'typescript' to the top of the array
and keep the remaining items in their current order: powershell, terraform,
yaml, python, markdown — or any preferred order after typescript) so the
TypeScript language server becomes the default.

In `@test/core/archive.test.ts`:
- Around line 1039-1095: The test name "should emit scenario count warning on
unexpected decrease" is misleading because the body asserts no warning; either
rename the test to reflect preserved count (e.g., "should not emit warning when
scenario count is preserved") or add a new test that uses archiveCommand.execute
with a change delta that removes one or more scenarios (so the resulting main
spec has fewer scenarios) and assert console.log was called with
expect.stringContaining('scenario count changed'); update references to
changeName/changeSpecDir and mainSpecDir in the new test to create the removing
delta and ensure noValidate/yes flags are consistent with other tests.
- Line 1138: The negative assertion is brittle due to the newline; update the
test around the `updated` variable to assert the absence of the unique old
phrase without relying on line endings (e.g., assert `updated` does not include
"THEN gate is checked" or use a regex like /THEN gate is checked/), or simply
remove the negative check and rely on the existing positive assertion that the
new content is present; reference the `updated` variable and the failing
`expect(updated).not.toContain('THEN gate is checked\n')` assertion when making
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20d00ec9-c09e-4380-92ba-c93f69a02e09

📥 Commits

Reviewing files that changed from the base of the PR and between afdca0d and 0700365.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • .serena/.gitignore
  • .serena/memories/project_overview.md
  • .serena/memories/style_and_conventions.md
  • .serena/memories/suggested_commands.md
  • .serena/memories/task_completion_checklist.md
  • .serena/project.yml
  • openspec/changes/archive/2026-03-08-fenced-codeblock-parsing/.openspec.yaml
  • openspec/changes/archive/2026-03-08-fenced-codeblock-parsing/design.md
  • openspec/changes/archive/2026-03-08-fenced-codeblock-parsing/proposal.md
  • openspec/changes/archive/2026-03-08-fenced-codeblock-parsing/specs/cli-archive/spec.md
  • openspec/changes/archive/2026-03-08-fenced-codeblock-parsing/tasks.md
  • openspec/changes/archive/2026-03-15-scenario-level-merge/.openspec.yaml
  • openspec/changes/archive/2026-03-15-scenario-level-merge/design.md
  • openspec/changes/archive/2026-03-15-scenario-level-merge/proposal.md
  • openspec/changes/archive/2026-03-15-scenario-level-merge/specs/cli-archive/spec.md
  • openspec/changes/archive/2026-03-15-scenario-level-merge/tasks.md
  • openspec/specs/cli-archive/spec.md
  • src/core/parsers/requirement-blocks.ts
  • src/core/specs-apply.ts
  • src/utils/change-utils.ts
  • test/core/archive.test.ts
  • test/core/parsers/requirement-blocks-fence.test.ts

…HEMA, fix tests

- Add stripScenarioTag() helper to remove delta-specific tags before merging
- Revert DEFAULT_SCHEMA from 'opti-spec-driven' back to 'spec-driven'
- Fix misleading test name, brittle assertion, and add warning suppression test
- Remove .serena/ from tracking and add to .gitignore
@phuongvm
Copy link
Author

Review Feedback Addressed (commit 2d36d2e)

Thanks for the thorough review @coderabbitai! All actionable comments have been addressed:

Critical Fixes

Comment Resolution
Tag pollution — mergeScenarios() pushes delta scenarios with (MODIFIED)/(REMOVED) tags into merged output Added stripScenarioTag() helper (L64-82 in specs-apply.ts), integrated at L129 and L141
DEFAULT_SCHEMA changed to "opti-spec-driven" — breaks createChange() Reverted to "spec-driven" in change-utils.ts L6

Test Quality Fixes

Comment Resolution
Misleading test name — says "should emit warning" but asserts NO warning Renamed to "should not emit warning when scenario count is preserved via (MODIFIED) tags"
Brittle assertion using \n pattern matching Changed to regex negative lookahead: not.toMatch(/THEN gate is checked(?!\s+with evidence)/)

Additional

  • Added new test: "should suppress warning when REMOVED tags explain scenario decrease" — verifies REMOVED-driven removal works, tags are stripped, and warning is correctly suppressed
  • Removed .serena/ from tracking and added to .gitignore (addresses the Serena config comments)

Verification

  • npx vitest run → exit 0 (1356 tests, 0 failures)
  • npx tsc --noEmit → clean

1 similar comment
@phuongvm
Copy link
Author

Review Feedback Addressed (commit 2d36d2e)

Thanks for the thorough review @coderabbitai! All actionable comments have been addressed:

Critical Fixes

Comment Resolution
Tag pollution — mergeScenarios() pushes delta scenarios with (MODIFIED)/(REMOVED) tags into merged output Added stripScenarioTag() helper (L64-82 in specs-apply.ts), integrated at L129 and L141
DEFAULT_SCHEMA changed to "opti-spec-driven" — breaks createChange() Reverted to "spec-driven" in change-utils.ts L6

Test Quality Fixes

Comment Resolution
Misleading test name — says "should emit warning" but asserts NO warning Renamed to "should not emit warning when scenario count is preserved via (MODIFIED) tags"
Brittle assertion using \n pattern matching Changed to regex negative lookahead: not.toMatch(/THEN gate is checked(?!\s+with evidence)/)

Additional

  • Added new test: "should suppress warning when REMOVED tags explain scenario decrease" — verifies REMOVED-driven removal works, tags are stripped, and warning is correctly suppressed
  • Removed .serena/ from tracking and added to .gitignore (addresses the Serena config comments)

Verification

  • npx vitest run → exit 0 (1356 tests, 0 failures)
  • npx tsc --noEmit → clean

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me verify the key fixes in the updated code before responding.


🏁 Script executed:

#!/bin/bash
# Check stripScenarioTag helper and its integration in specs-apply.ts
rg -n "stripScenarioTag" --type=ts -A3 -B3

echo "---"
# Check DEFAULT_SCHEMA value in change-utils.ts
rg -n "DEFAULT_SCHEMA" --type=ts -A1

echo "---"
# Check the renamed test
rg -n "should not emit warning when scenario count" --type=ts -A3

echo "---"
# Check the new REMOVED suppression test
rg -n "should suppress warning when REMOVED" --type=ts -A5

echo "---"
# Check the negative lookahead assertion
rg -n "THEN gate is checked" --type=ts -A2 -B2

Length of output: 5268


@phuongvm Thanks for the detailed summary and for addressing everything so thoroughly! I've verified all the fixes in the updated code:

  • stripScenarioTag() (specs-apply.ts L69–82) — correctly strips (MODIFIED)/(REMOVED) suffixes from headerLine and resets the tag field to undefined. It's applied at both output paths: L129 for MODIFIED replacements and L141 for newly appended delta scenarios, so tag pollution is fully eliminated.
  • DEFAULT_SCHEMA — confirmed back to "spec-driven" at change-utils.ts L6. ✅
  • Renamed test"should not emit warning when scenario count is preserved via (MODIFIED) tags" at archive.test.ts L1040. ✅
  • Negative lookaheadnot.toMatch(/THEN gate is checked(?!\s+with evidence)/) at L1139 is a robust fix; it correctly rejects the old text while allowing the updated version to pass. ✅
  • New suppression test"should suppress warning when REMOVED tags explain scenario decrease" at L1178 provides solid coverage for the warning-suppression path. ✅

The implementation looks solid. All critical correctness issues are resolved, test coverage is comprehensive, and the 1356-test clean run confirms no regressions. The PR is ready for final merge. 🎉


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me verify the fixes in commit 2d36d2e.


🏁 Script executed:

#!/bin/bash
echo "=== specs-apply.ts: stripScenarioTag helper and integration ==="
rg -n "stripScenarioTag" src/core/specs-apply.ts -A3 -B3

echo ""
echo "=== change-utils.ts: DEFAULT_SCHEMA ==="
rg -n "DEFAULT_SCHEMA" src/utils/change-utils.ts -A1 -B1

echo ""
echo "=== test file: renamed test and new REMOVED-suppression test ==="
rg -n "should not emit warning when scenario count is preserved|should suppress warning when REMOVED" test/core/archive.test.ts -A2 -B1

echo ""
echo "=== .gitignore: .serena entry ==="
grep -n "serena" .gitignore 2>/dev/null || echo "(not found in .gitignore)"
cat .gitignore 2>/dev/null | tail -10

Length of output: 2004


@phuongvm Thanks for the detailed summary and the thorough follow-through! I've verified all the changes in commit 2d36d2e — everything checks out:

Fix Status
stripScenarioTag() helper defined (Lines 64–82 in specs-apply.ts) and applied at Lines 129 & 141 ✅ Confirmed
DEFAULT_SCHEMA reverted to "spec-driven" (Line 6 in change-utils.ts) ✅ Confirmed
Test renamed to "should not emit warning when scenario count is preserved via (MODIFIED) tags" (Line 1040) ✅ Confirmed
New test "should suppress warning when REMOVED tags explain scenario decrease" added (Line 1178) ✅ Confirmed
.serena/ added to .gitignore (Line 161) ✅ Confirmed

The tag-pollution fix is particularly well-done — stripScenarioTag cleanly separates the concern of delta parsing from output writing, and integrating it at both the MODIFIED-replacement path (Line 129) and the new-scenario-append path (Line 141) ensures no stray tags leak into the merged spec regardless of the code path taken.

The PR looks good to me. 🎉

[approve]

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/specs-apply.ts`:
- Around line 107-142: The append loop currently lets tagged delta scenarios
that never matched a main scenario become new scenarios or be silently ignored;
change it so only untagged scenarios may be appended: in the second loop over
delta.scenarios check normalizeScenarioName(s.name) against usedDeltaNames and
if not used then if s.tag is present (e.g., 'MODIFIED' or 'REMOVED') throw or
surface an error mentioning the scenario name/tag instead of appending or
ignoring it; only call stripScenarioTag(s) and push into result when the
scenario is truly untagged. This touches the delta.scenarios loop,
usedDeltaNames, normalizeScenarioName, stripScenarioTag, and deltaByName logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 290409be-0f97-46db-9f67-b25b64d8a95b

📥 Commits

Reviewing files that changed from the base of the PR and between 0700365 and 2d36d2e.

📒 Files selected for processing (4)
  • .gitignore
  • src/core/specs-apply.ts
  • src/utils/change-utils.ts
  • test/core/archive.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/utils/change-utils.ts

Copy link
Collaborator

@alfred-openspec alfred-openspec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from Alfred (OpenSpec Bot)

Nice work on this PR — both the fenced code block parsing fix and the scenario-level merge are solid contributions that address real pain points.

Highlights

  • Dogfooding: Love that you used OpenSpec's own change workflow (proposal → design → tasks → specs). That's how it's supposed to work.
  • Test coverage: 15 new tests across both features is thorough.
  • Responsive to feedback: The stripScenarioTag() fix and DEFAULT_SCHEMA revert in commit 2d36d2e were handled quickly and cleanly.

Requested Changes

  1. change-utils.ts cosmetic reformatting: Serena reformatted all quotes from single → double. The project uses single quotes (check any other source file). This adds 48+/24- of noise. Please revert this file or submit the style changes separately.

  2. package-lock.json version bump (1.1.1 → 1.2.0): Version bumps happen during release, not in feature PRs. Please revert.

Minor Observations (non-blocking)

  1. MODIFIED-tagged scenario with no main match: If a delta scenario is tagged (MODIFIED) but its name doesn't match any main scenario (typo), it gets silently appended as new. CodeRabbit flagged this too — worth considering a warning here.

  2. Scenario count warning suppression: Currently suppressed when ANY (REMOVED) tag exists. If you REMOVE 1 but accidentally drop 3 others, the warning won't fire. Consider: compare against mainCount - removedCount instead.

Overall this is a high-quality first contribution. The core merge logic is correct and well-tested. Looking forward to seeing items 1-2 cleaned up so we can merge. 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants