Skip to content

fix: scenario-level merge for MODIFIED requirements#843

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

fix: scenario-level merge for MODIFIED requirements#843
phuongvm wants to merge 6 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

  • New Features

    • Scenario-level merging for fine-grained requirement updates (tagged MODIFIED/REMOVED handling, tag stripping, deduplication).
    • Fenced code block awareness to avoid treating headers inside code fences as structure.
    • First-class Purpose delta support (purpose-only or mixed deltas; purpose replacement/insert).
  • Bug Fixes

    • Refined scenario-count and unmatched-tag warnings; precise removal counting and duplicate-handling.
  • Documentation

    • Updated schema instructions, specs, and guidance for scenario tags and Purpose deltas.
  • Tests

    • Added coverage for scenario merges, fenced-code parsing, and Purpose delta handling.

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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds fenced-codeblock awareness to requirement parsing, implements scenario-level merges for requirement blocks (MODIFIED/REMOVED tags, tag stripping, matched-removed accounting), introduces first-class Purpose delta support, updates validation/schema/docs, and adds comprehensive tests for parsing, merging, and purpose handling.

Changes

Cohort / File(s) Summary
Parsing & scenario APIs
src/core/parsers/requirement-blocks.ts
Adds markdown fence detection (isMarkdownFenceLine) and in-fence state to skip header matching, new scenario types (ScenarioBlock, RequirementBlockWithScenarios), helpers (normalizeScenarioName, extractScenarioTag), and parseScenarios() to extract descriptions and ordered scenarios while preserving fenced content.
Spec apply / merge logic
src/core/specs-apply.ts
Adds scenario-aware flow: hasScenarioTags, mergeScenarios (returns matchedRemovedCount), stripScenarioTag, reconstructRequirementBlock, purpose handling (purposeText) and updates to skeleton/purpose replacement.
Change parsing & schema
src/core/parsers/change-parser.ts, src/core/schemas/change.schema.ts
Extracts ## Purpose into purposeText, emits PURPOSE_MODIFIED deltas, and extends DeltaOperationType enum.
Validation
src/core/validation/validator.ts
Counts purpose presence in delta validation, treats purpose-only deltas as valid, and flags empty ## Purpose as an error.
Tests — parsing & fences
test/core/parsers/requirement-blocks-fence.test.ts
Adds tests ensuring headers inside backtick/tilde fences are ignored and fenced content is preserved in parsed blocks/scenarios.
Tests — archive & scenario merge
test/core/archive.test.ts
Large suite validating scenario-level merge semantics: MODIFIED/REMOVED handling, normalization, duplicate handling, warnings, legacy fallback, and preflight ambiguity errors.
Tests — purpose delta
test/core/purpose-delta.test.ts, test/core/parsers/change-parser.test.ts
Adds end-to-end tests for purpose parsing, validation, and application.
Specs, design & docs
openspec/changes/.../*, openspec/specs/cli-archive/spec.md, openspec/specs/schema-instruction/spec.md, schemas/spec-driven/schema.yaml, docs/concepts.md
Adds/updates proposals, designs, tasks, and specs describing fenced-code parsing, scenario-level merge semantics (tag stripping, matchedRemovedCount, warnings), Purpose delta behavior, and schema/instruction guidance.
Misc & config
.gitignore, openspec/changes/*/.openspec.yaml
Adds .serena/ ignore and multiple change metadata files.

Sequence Diagram

sequenceDiagram
    participant Parser as Requirement Parser
    participant Merger as Scenario Merger
    participant Rebuilder as Reconstructor

    rect rgba(100,150,200,0.5)
        Note over Parser,Merger: Parse & Merge Flow
        Parser->>Parser: splitTopLevelSections(content) with fence tracking
        Parser->>Parser: extractRequirementsSection(section) (skip headers inside fences)
        Parser->>Parser: parseScenarios(block) -> description + scenarios[]
        Parser->>Merger: send main.scenarios[], delta.scenarios[]
        
        Merger->>Merger: build mainByName map (normalized names)
        Merger->>Merger: for each delta scenario: match → replace/remove/append
        Merger->>Merger: track matchedRemovedCount, log unmatched warnings
        Merger->>Rebuilder: return mergedScenarios[], matchedRemovedCount

        Rebuilder->>Rebuilder: stripScenarioTag() on merged items
        Rebuilder->>Rebuilder: reconstructRequirementBlock(description, scenarios, header)
        Rebuilder->>Parser: return merged requirement block
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • TabishB

Poem

🐰 Hop, hop — I parsed the fences tight,

headers safe from code block night.
Scenarios matched, tags trimmed away,
Purpose set true, warnings had their say.
A rabbit's patch — neat merge, bright day!

🚥 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' accurately captures the main change: enabling scenario-level merging for MODIFIED requirement blocks instead of full-block replacement, which was the core issue addressed.
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
📝 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.

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. 🎉

… fixes

- Add MergeResult interface with matchedRemovedCount for precision suppression
- Add mainByName set for accurate REMOVED scenario matching
- Emit warnings for unmatched MODIFIED/REMOVED scenarios (typo detection)
- Revert change-utils.ts and package-lock.json to origin/main state
- Add 5 new tests for unmatched warnings and precision suppression (37 total)
- Update schema.yaml specs instruction with scenario-level tag guidance
- Add new schema-instruction spec with 1 requirement
- Update cli-archive spec with 3 merged scenarios
- Archive: fix-scenario-merge-review, schema-instruction-scenario-tags
@phuongvm
Copy link
Author

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. 🎉

Thanks for the thorough review, Alfred! Really appreciate the detailed feedback. Here's what we've done:

Requested Changes — ✅ Both Fixed

  1. change-utils.ts cosmetic reformatting — Reverted to origin/main. The Serena quote reformatting is gone.

  2. package-lock.json version bump — Reverted to match origin/main state. No version changes in this PR.

Both fixes landed in commit abd9bc9.

Minor Observations — ✅ Both Implemented
We agreed these were worth addressing, so we went ahead and implemented both:

  1. Unmatched MODIFIED scenario warning — If a

(MODIFIED)
-tagged scenario doesn't match any main scenario (typo), we now emit a ⚠️ Warning and append it as new. Same treatment for unmatched

(REMOVED)
tags — warning emitted, scenario ignored. This catches typos early instead of silently doing the wrong thing.

  1. Precision warning suppression — Reworked the logic. Instead of suppressing on any

(REMOVED)
tag, we now track matchedRemovedCount (only counting REMOVED tags that actually matched a main scenario). The expected minimum is calculated as mainCount - matchedRemovedCount. If merged count drops below that, the warning fires. This means: remove 1, accidentally drop 3 others → warning fires as expected.

Both improvements have dedicated tests (37 total in

archive.test.ts
).

Additionally, we updated the spec-driven schema instruction (

schema.yaml
) to document the

(MODIFIED)
and

(REMOVED)
scenario-level tags, so AI agents using openspec instructions will know about them going forward.

Ready for re-review when you get a chance! 🚀

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: 5

🧹 Nitpick comments (1)
test/core/archive.test.ts (1)

838-1485: Optional: extract a scenario-merge test fixture helper to reduce duplication.

The repeated directory/file setup in this block can be centralized (e.g., setupScenarioMergeCase) so each test focuses only on intent + assertions.

Refactor sketch
+async function setupScenarioMergeCase(args: {
+  tempDir: string;
+  changeName: string;
+  capability?: string;
+  mainSpec: string;
+  deltaSpec: string;
+}) {
+  const capability = args.capability ?? 'multi';
+  const changeDir = path.join(args.tempDir, 'openspec', 'changes', args.changeName);
+  const changeSpecDir = path.join(changeDir, 'specs', capability);
+  const mainSpecDir = path.join(args.tempDir, 'openspec', 'specs', capability);
+  await fs.mkdir(changeSpecDir, { recursive: true });
+  await fs.mkdir(mainSpecDir, { recursive: true });
+  await fs.writeFile(path.join(mainSpecDir, 'spec.md'), args.mainSpec);
+  await fs.writeFile(path.join(changeSpecDir, 'spec.md'), args.deltaSpec);
+  return { changeDir, mainSpecDir };
+}
🤖 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 838 - 1485, Extract the repeated test
setup into a helper (e.g., setupScenarioMergeCase) that creates the change and
main spec directories, writes main spec content (or multiScenarioMainSpec()) and
delta content, and returns paths/values needed by the tests so each it-block
only calls setupScenarioMergeCase(changeName, { mainContent?, deltaContent? })
then invokes archiveCommand.execute(changeName, { yes: true, noValidate: true })
and reads the updated spec; update tests to call the helper instead of repeating
fs.mkdir/fs.writeFile and path.join logic to reduce duplication while keeping
existing assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/tasks.md`:
- Around line 13-23: The fenced code block containing the "## MODIFIED
Requirements" snippet is missing a language identifier and triggers MD040; fix
it by adding a language after the opening fence (e.g., change the opening "```"
to "```md") for the block that includes "## MODIFIED Requirements" / "###
Requirement: User Authentication" / "#### Scenario: Login with MFA (MODIFIED)";
keep the closing "```" as-is so the block is properly fenced and markdown-lint
compliant.

In `@openspec/specs/cli-archive/spec.md`:
- Around line 114-119: Update the scenario text to match the new expected-count
logic: remove the outdated "no (REMOVED) tags" condition and change the
WHEN/THEN wording to compute expected_count as main_count - matchedRemovedCount
and emit a warning only when merged_count < expected_count; update the warning
message to `"⚠️ Warning: {specName} requirement '{name}': scenario count is
{merged_count}, expected at least {expected_count}"` (or similar phrasing using
merged_count and expected_count) so it references expected_count, main_count,
matchedRemovedCount, and merged_count.

In `@openspec/specs/schema-instruction/spec.md`:
- Line 4: Replace the placeholder "TBD - created by archiving change
schema-instruction-scenario-tags. Update Purpose after archive." on the Purpose
line with the finalized Purpose statement for this spec: locate the Purpose
header/section (the line currently containing "TBD" and the reference to
schema-instruction-scenario-tags) and update it to a clear, concise description
of the spec's intent and scope, ensuring it no longer contains "TBD" or archival
notes and accurately reflects the finalized purpose of schema-instruction.

In `@src/core/specs-apply.ts`:
- Around line 121-126: The loop over delta.scenarios increments
matchedRemovedCount per REMOVED entry which double-counts duplicates; instead,
track unique removed scenario names using the existing normalizeScenarioName and
removedNames set (or create one) and only increment matchedRemovedCount when a
normalized name is newly added to removedNames and exists in mainByName. Update
the same logic where matchedRemovedCount is adjusted around the later block
referenced (lines ~446-447) to use unique names rather than raw entries so
expectedMinCount and warning suppression reflect unique removals.
- Around line 157-167: Appended scenarios from delta can be duplicated because
the normalized name isn't added to usedDeltaNames after pushing; in the loop
over delta.scenarios (the block using normalizeScenarioName(s.name) and
result.push(stripScenarioTag(s))), add usedDeltaNames.add(name) immediately
after result.push(...) so subsequent iterations skip duplicates, preserving the
existing MODIFIED/REMOVED checks and behavior.

---

Nitpick comments:
In `@test/core/archive.test.ts`:
- Around line 838-1485: Extract the repeated test setup into a helper (e.g.,
setupScenarioMergeCase) that creates the change and main spec directories,
writes main spec content (or multiScenarioMainSpec()) and delta content, and
returns paths/values needed by the tests so each it-block only calls
setupScenarioMergeCase(changeName, { mainContent?, deltaContent? }) then invokes
archiveCommand.execute(changeName, { yes: true, noValidate: true }) and reads
the updated spec; update tests to call the helper instead of repeating
fs.mkdir/fs.writeFile and path.join logic to reduce duplication while keeping
existing assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ea2f510-ac9d-40b4-a8cc-76c5f02c36bf

📥 Commits

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

📒 Files selected for processing (15)
  • openspec/changes/archive/2026-03-18-fix-scenario-merge-review/.openspec.yaml
  • openspec/changes/archive/2026-03-18-fix-scenario-merge-review/design.md
  • openspec/changes/archive/2026-03-18-fix-scenario-merge-review/proposal.md
  • openspec/changes/archive/2026-03-18-fix-scenario-merge-review/specs/cli-archive/spec.md
  • openspec/changes/archive/2026-03-18-fix-scenario-merge-review/tasks.md
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/.openspec.yaml
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/design.md
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/proposal.md
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/specs/schema-instruction/spec.md
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/tasks.md
  • openspec/specs/cli-archive/spec.md
  • openspec/specs/schema-instruction/spec.md
  • schemas/spec-driven/schema.yaml
  • src/core/specs-apply.ts
  • test/core/archive.test.ts
✅ Files skipped from review due to trivial changes (5)
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/proposal.md
  • openspec/changes/archive/2026-03-18-fix-scenario-merge-review/design.md
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/design.md
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/.openspec.yaml
  • openspec/changes/archive/2026-03-18-fix-scenario-merge-review/.openspec.yaml

- Add PURPOSE_MODIFIED to DeltaOperationType enum
- Parser extracts ## Purpose section from delta specs
- Validator accepts purpose-only delta files as valid
- ChangeParser emits PURPOSE_MODIFIED in parsed output
- Apply/archive replaces main spec Purpose on merge
- Rewrite schema-instruction delta as true Purpose delta
- Add delta specs for openspec-conventions, cli-validate, specs-sync-skill
- Update docs/concepts.md and schema.yaml with Purpose delta semantics
- Add 9 new tests (8 purpose-delta + 1 change-parser regression)
- Fix CodeRabbit C1-C5, N1 findings (mergeScenarios bugs, spec text, lint)

Closes: PR Fission-AI#843 second-round review findings
Reviewed-by: Reviewer-Collab (6 rounds, APPROVED v6)
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/core/parsers/requirement-blocks.ts (2)

42-55: ⚠️ Potential issue | 🟠 Major

Fence tracking still misses valid Markdown fence forms.

All of these scanners toggle inFence only on startsWith('```'). Markdown fences can be indented and can use ~~~, so headings inside those blocks will still be treated as real ##/###/#### markers and split specs incorrectly. Please centralize fence detection behind a Markdown-compatible helper and reuse it everywhere, including replacePurposeSection() in src/core/specs-apply.ts.

🛠️ Suggested helper
+const FENCE_DELIMITER_REGEX = /^\s*(```|~~~)/;
+
+function isFenceDelimiter(line: string): boolean {
+  return FENCE_DELIMITER_REGEX.test(line);
+}
@@
-    if (lines[i].startsWith('```')) {
+    if (isFenceDelimiter(lines[i])) {
       inFence = !inFence;
       continue;
     }

Also applies to: 67-78, 95-104, 170-181, 210-241, 339-378

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

In `@src/core/parsers/requirement-blocks.ts` around lines 42 - 55, The scanners
toggle fence state only on lines starting with "```", which misses indented
fences and "~~~" variants; add a single Markdown-compatible helper (e.g.,
FENCE_DELIMITER_REGEX and isFenceDelimiter(line: string)) and replace all direct
startsWith('```') checks in requirement-blocks.ts (the fence logic loop using
inFence/inFenceEnd) and the other locations mentioned plus
replacePurposeSection() in src/core/specs-apply.ts so they call
isFenceDelimiter(line) to toggle the inFence flag; ensure the helper recognizes
optional leading indentation and both backtick and tilde fences so headings
inside fenced blocks are ignored consistently.

121-160: ⚠️ Potential issue | 🟠 Major

Keep Purpose-section presence separate from Purpose text.

purposeText: purposeText || undefined erases an empty ## Purpose section, and sectionPresence has no purpose flag. Downstream callers then cannot distinguish “Purpose header present but prose missing” from “no Purpose section at all”, which makes the new Purpose-specific validation path impossible to implement cleanly.

🛠️ Suggested shape change
 export interface DeltaPlan {
   added: RequirementBlock[];
   modified: RequirementBlock[];
   removed: string[]; // requirement names
   renamed: Array<{ from: string; to: string }>;
   purposeText?: string; // delta Purpose text, if present
   sectionPresence: {
     added: boolean;
     modified: boolean;
     removed: boolean;
     renamed: boolean;
+    purpose: boolean;
   };
 }
@@
-  const purposeText = purposeLookup.found ? purposeLookup.body.trim() : undefined;
+  const purposeText = purposeLookup.found ? purposeLookup.body.trim() : undefined;
   return {
@@
-    purposeText: purposeText || undefined,
+    purposeText,
     sectionPresence: {
       added: addedLookup.found,
       modified: modifiedLookup.found,
       removed: removedLookup.found,
       renamed: renamedLookup.found,
+      purpose: purposeLookup.found,
     },
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/parsers/requirement-blocks.ts` around lines 121 - 160, The
parseDeltaSpec currently loses the fact that a Purpose header existed when its
body is empty because it sets purposeText to undefined; update parseDeltaSpec so
sectionPresence includes a purpose boolean (e.g., sectionPresence.purpose)
derived from purposeLookup.found, and set purposeText to purposeLookup.found ?
purposeLookup.body.trim() : undefined (so an empty Purpose section yields an
empty string rather than being treated as absent). Make sure to reference the
existing symbols parseDeltaSpec, purposeLookup, purposeText, and sectionPresence
when implementing the change so callers can distinguish “Purpose header present
but empty” from “no Purpose section”.
🧹 Nitpick comments (2)
src/core/parsers/change-parser.ts (1)

147-156: LGTM!

The PURPOSE_MODIFIED delta parsing correctly:

  • Finds the ## Purpose section
  • Skips empty sections
  • Creates a delta with truncated description (80 chars max)
💡 Optional: Minor efficiency improvement

The trim() call is repeated 3 times. Consider caching it:

     // Parse Purpose delta
     const purposeSection = this.findSection(sections, 'Purpose');
-    if (purposeSection && purposeSection.content.trim()) {
+    const purposeContent = purposeSection?.content.trim();
+    if (purposeContent) {
       deltas.push({
         spec: specName,
         operation: 'PURPOSE_MODIFIED' as DeltaOperation,
-        description: `Update Purpose: ${purposeSection.content.trim().substring(0, 80)}${purposeSection.content.trim().length > 80 ? '...' : ''}`,
+        description: `Update Purpose: ${purposeContent.substring(0, 80)}${purposeContent.length > 80 ? '...' : ''}`,
       });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/parsers/change-parser.ts` around lines 147 - 156, The purpose
parsing repeats purposeSection.content.trim() multiple times—cache the trimmed
string to avoid redundant work: inside the block where purposeSection is
non-null, assign const trimmed = purposeSection.content.trim(); then use trimmed
for the truthy check, for building the description and for computing
length/substring; update the block that pushes to deltas (referencing
purposeSection, specName, deltas, and DeltaOperation) to use trimmed instead of
calling trim() repeatedly.
test/core/purpose-delta.test.ts (1)

171-297: Add one apply test for specs that lack ## Purpose.

Both apply tests only exercise replacement of an existing Purpose section. The new fallback in replacePurposeSection() that inserts ## Purpose after the title is still untested, so a regression in the “missing Purpose” path would slip through.

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

In `@test/core/purpose-delta.test.ts` around lines 171 - 297, The tests only cover
replacing an existing "## Purpose" section; add a new test that exercises the
fallback path in replacePurposeSection() by providing a target spec that has no
"## Purpose" header and a source delta that contains a Purpose section, then
call buildUpdatedSpec(update, 'test-change') and assert the rebuilt output now
includes the injected "## Purpose" and the new purpose text while preserving
requirements; reference SpecUpdate, buildUpdatedSpec, and replacePurposeSection
in the test so the case where Purpose is missing is covered and counts reflect
no requirement removals (and any added requirements if present).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@openspec/changes/pr843-coderabbit-review-fixes/specs/specs-sync-skill/spec.md`:
- Around line 13-16: The spec requires that an "## ADDED Requirements" delta
should overwrite an existing requirement with the same name, but the ADDED
branch in src/core/specs-apply.ts currently throws "ADDED failed ... already
exists"; change the ADDED handler so when a requirement name collision is found
it performs an update/replace of the existing requirement (reuse the existing
update logic or call the same function used for CHANGED/UPDATE flows) instead of
throwing, making sure to preserve any required identifiers and run existing
validation paths; update or add tests to cover the "ADDED requirement already
exists" scenario.

In `@src/core/specs-apply.ts`:
- Around line 103-107: When building deltaByName from delta.scenarios using
normalizeScenarioName, detect and reject duplicate normalized names instead of
overwriting: iterate delta.scenarios, compute key =
normalizeScenarioName(s.name), if deltaByName.has(key) throw a descriptive error
(or aggregate conflicts and throw) naming both conflicting scenarios and their
operations; apply the same guard to the other map-building code paths that
normalize and set scenario names (e.g., any baseByName or similar maps in the
later block) so conflicting normalized names (e.g., "Foo (MODIFIED)" vs "Foo
(REMOVED)" or repeated entries) fail fast.

---

Outside diff comments:
In `@src/core/parsers/requirement-blocks.ts`:
- Around line 42-55: The scanners toggle fence state only on lines starting with
"```", which misses indented fences and "~~~" variants; add a single
Markdown-compatible helper (e.g., FENCE_DELIMITER_REGEX and
isFenceDelimiter(line: string)) and replace all direct startsWith('```') checks
in requirement-blocks.ts (the fence logic loop using inFence/inFenceEnd) and the
other locations mentioned plus replacePurposeSection() in
src/core/specs-apply.ts so they call isFenceDelimiter(line) to toggle the
inFence flag; ensure the helper recognizes optional leading indentation and both
backtick and tilde fences so headings inside fenced blocks are ignored
consistently.
- Around line 121-160: The parseDeltaSpec currently loses the fact that a
Purpose header existed when its body is empty because it sets purposeText to
undefined; update parseDeltaSpec so sectionPresence includes a purpose boolean
(e.g., sectionPresence.purpose) derived from purposeLookup.found, and set
purposeText to purposeLookup.found ? purposeLookup.body.trim() : undefined (so
an empty Purpose section yields an empty string rather than being treated as
absent). Make sure to reference the existing symbols parseDeltaSpec,
purposeLookup, purposeText, and sectionPresence when implementing the change so
callers can distinguish “Purpose header present but empty” from “no Purpose
section”.

---

Nitpick comments:
In `@src/core/parsers/change-parser.ts`:
- Around line 147-156: The purpose parsing repeats purposeSection.content.trim()
multiple times—cache the trimmed string to avoid redundant work: inside the
block where purposeSection is non-null, assign const trimmed =
purposeSection.content.trim(); then use trimmed for the truthy check, for
building the description and for computing length/substring; update the block
that pushes to deltas (referencing purposeSection, specName, deltas, and
DeltaOperation) to use trimmed instead of calling trim() repeatedly.

In `@test/core/purpose-delta.test.ts`:
- Around line 171-297: The tests only cover replacing an existing "## Purpose"
section; add a new test that exercises the fallback path in
replacePurposeSection() by providing a target spec that has no "## Purpose"
header and a source delta that contains a Purpose section, then call
buildUpdatedSpec(update, 'test-change') and assert the rebuilt output now
includes the injected "## Purpose" and the new purpose text while preserving
requirements; reference SpecUpdate, buildUpdatedSpec, and replacePurposeSection
in the test so the case where Purpose is missing is covered and counts reflect
no requirement removals (and any added requirements if present).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d105c79a-e9c0-47f4-8d5b-f152b1580b8e

📥 Commits

Reviewing files that changed from the base of the PR and between abd9bc9 and db09c44.

📒 Files selected for processing (22)
  • docs/concepts.md
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/tasks.md
  • openspec/changes/pr843-coderabbit-review-fixes/.openspec.yaml
  • openspec/changes/pr843-coderabbit-review-fixes/design.md
  • openspec/changes/pr843-coderabbit-review-fixes/proposal.md
  • openspec/changes/pr843-coderabbit-review-fixes/specs/cli-archive/spec.md
  • openspec/changes/pr843-coderabbit-review-fixes/specs/cli-validate/spec.md
  • openspec/changes/pr843-coderabbit-review-fixes/specs/openspec-conventions/spec.md
  • openspec/changes/pr843-coderabbit-review-fixes/specs/schema-instruction/spec.md
  • openspec/changes/pr843-coderabbit-review-fixes/specs/specs-sync-skill/spec.md
  • openspec/changes/pr843-coderabbit-review-fixes/tasks.md
  • openspec/specs/cli-archive/spec.md
  • openspec/specs/schema-instruction/spec.md
  • schemas/spec-driven/schema.yaml
  • src/core/parsers/change-parser.ts
  • src/core/parsers/requirement-blocks.ts
  • src/core/schemas/change.schema.ts
  • src/core/specs-apply.ts
  • src/core/validation/validator.ts
  • test/core/archive.test.ts
  • test/core/parsers/change-parser.test.ts
  • test/core/purpose-delta.test.ts
✅ Files skipped from review due to trivial changes (9)
  • openspec/changes/pr843-coderabbit-review-fixes/.openspec.yaml
  • openspec/changes/pr843-coderabbit-review-fixes/specs/schema-instruction/spec.md
  • schemas/spec-driven/schema.yaml
  • openspec/changes/archive/2026-03-18-schema-instruction-scenario-tags/tasks.md
  • openspec/changes/pr843-coderabbit-review-fixes/tasks.md
  • openspec/changes/pr843-coderabbit-review-fixes/specs/openspec-conventions/spec.md
  • openspec/changes/pr843-coderabbit-review-fixes/proposal.md
  • openspec/changes/pr843-coderabbit-review-fixes/design.md
  • openspec/specs/schema-instruction/spec.md

@phuonglambert
Copy link

Commit db09c44 — First-Class Purpose Delta Support

This commit addresses all CodeRabbit second-round findings (C1-C5, N1) and adds first-class Purpose delta support to OpenSpec:

Engine Changes (5 files)

  • PURPOSE_MODIFIED added to DeltaOperationType enum
  • Parser extracts ## Purpose section from delta specs
  • Validator accepts purpose-only delta files as valid
  • ChangeParser emits PURPOSE_MODIFIED in parsed output
  • Apply/archive replaces main spec Purpose on merge via replacePurposeSection()

Source-of-Truth Updates (7 files)

  • 4 delta specs: openspec-conventions, cli-validate, specs-sync-skill, schema-instruction
  • docs/concepts.md — Purpose row in Delta Sections table + Glossary
  • schemas/spec-driven/schema.yaml — Purpose in delta operations instruction

Test Coverage

  • 50/50 tests passing (39 archive + 8 purpose-delta + 3 change-parser)
  • openspec validate ✅ | tsc --noEmit ✅ | npm run build

Review History

Reviewed through 6 rounds by Reviewer-Collab — APPROVED v6.

Phase 4 of PR Fission-AI#843 — fixes all 5 findings from CodeRabbit review on db09c44:

- F1: ADDED dup scenario → abort with guidance (spec alignment)
- F2: mergeScenarios() preflight validates duplicate scenario names
- F3: shared isMarkdownFenceLine() replaces all inline fence checks
- F4: empty Purpose section classified correctly (no spurious CHANGE_NO_DELTAS)
- N2: Purpose insertion test for target spec without ## Purpose

Tests: 64/64 passing | Build: clean | openspec validate: clean
Reviewer-Collab: APPROVED (v10)
- Archived change to openspec/changes/archive/2026-03-20-pr843-coderabbit-review-fixes/
- Synced 5 delta specs to main specs:
  cli-archive, cli-validate, openspec-conventions, schema-instruction, specs-sync-skill
- Updated tasks.md: marked 8.6 (Reviewer APPROVED) and 8.7 (commit+push) complete
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.

3 participants