Skip to content

feat(validate): check MODIFIED headers against canonical base; --accept-cross-change-base opt-in (#1112)#1113

Open
luigileap wants to merge 1 commit into
Fission-AI:mainfrom
luigileap:feat/validate-modified-base-check
Open

feat(validate): check MODIFIED headers against canonical base; --accept-cross-change-base opt-in (#1112)#1113
luigileap wants to merge 1 commit into
Fission-AI:mainfrom
luigileap:feat/validate-modified-base-check

Conversation

@luigileap
Copy link
Copy Markdown

@luigileap luigileap commented May 21, 2026

Closes #1112.

Summary

openspec validate <change> now runs the same MODIFIED/REMOVED/RENAMED-from base-header check that openspec archive already enforces — so authoring bugs surface at write time instead of weeks later at archive time.

A new opt-in flag --accept-cross-change-base widens the lookup to include sister-pending changes (the one legitimate cross-change pattern). Archive remains strict regardless.

Behavior

Path Today After this PR
openspec validate <change> (default) "Change is valid" even when MODIFIED title is absent from base ERROR with actionable message naming the missing canonical path + the opt-in flag
openspec validate <change> --accept-cross-change-base n/a Same check, but passes if the header is found in any openspec/changes/<other>/specs/<cap>/spec.md (sister-pending)
openspec archive <change> Aborts mid-archive with "MODIFIED failed for header X - not found" Unchanged — still strict, no override
new Validator(true) (legacy boolean constructor) Works Works (overload preserved; defaults to acceptCrossChangeBase: false)

Why opt-in instead of strict-everywhere

There's a legitimate cross-change pattern (sister change in flight; you're extending its requirements before either change archives). Forcing every MODIFIED to require a fully-archived canonical base would make that pattern impossible. The opt-in encodes "I know I'm writing against a sister-pending base; I'll archive the sister or fold this in before archive."

Archive stays strict because the canonical record matters once shipped. The two checks have different jobs:

  • Validate = "is this delta authored correctly at write time"
  • Archive = "is this delta fileable into the canonical spec right now"

Implementation

  • src/core/validation/validator.ts — new ValidatorOptions shape (strictMode, acceptCrossChangeBase); new helper checkDeltaAgainstCanonicalBase invoked from inside the existing per-spec loop in validateChangeDeltaSpecs. ~140 lines added.
  • src/commands/validate.ts — plumb acceptCrossChangeBase through ExecuteOptionsvalidateByType / runBulkValidation. ~15 lines.
  • src/cli/index.ts — register --accept-cross-change-base on the top-level validate command. 2 lines.
  • Legacy new Validator(true) boolean constructor preserved via type overload — no breaking change.

Tests

9 new tests in test/core/validation.test.ts:

  • MODIFIED header absent from canonical → default error.
  • Canonical spec doesn't exist (CREATE shape) + MODIFIED → error.
  • MODIFIED matches canonical exactly → pass.
  • Cross-change MODIFIED, default → error.
  • Cross-change MODIFIED, acceptCrossChangeBase=true → pass.
  • acceptCrossChangeBase=true + non-matching name in both canonical AND sister → still errors (regression guard).
  • REMOVED header not found in canonical → error.
  • RENAMED-from header not found in canonical → error.
  • Legacy new Validator(true) boolean constructor backward compat.

Existing test suite: 1520/1520 pass (was 1511; +9 from new tests, no regressions).

Naming

--accept-cross-change-base is descriptive but long. Happy to rename to whatever the project prefers — --allow-pending-base, --allow-cross-change, --accept-sister-deltas, etc. I went with the most literal name because the help text fits the design ("accept a base that lives in another in-flight change").

Summary by CodeRabbit

Release Notes

  • New Features
    • Added --accept-cross-change-base flag to the openspec validate command to optionally include sister-pending changes during validation.
    • Enhanced delta spec validation to verify that MODIFIED, REMOVED, and RENAMED references match existing requirement headers in the canonical base specification.

Review Change Stack

…anonical base; add --accept-cross-change-base opt-in

Fixes Fission-AI#1112.

`Validator.validateChangeDeltaSpecs` now mirrors `openspec archive`'s
existing strict check: for every MODIFIED / REMOVED / RENAMED-from
entry in a change delta, look up `openspec/specs/<cap>/spec.md` and
verify the `### Requirement: <name>` header exists there (whitespace-
insensitive). Without this, authoring bugs only surfaced at archive
time — typically days or weeks after the implementing PR shipped.

Opt-in `--accept-cross-change-base` (also exposed via the
`Validator({ acceptCrossChangeBase: true })` constructor option) also
checks sister-pending changes at `openspec/changes/<other>/specs/
<cap>/spec.md` and passes if the header is found there. Covers the
legitimate cross-change pattern (sister change in flight; you're
extending its requirements before either change archives).

`openspec archive` remains strict regardless of the new flag — the
flag affects ONLY write-time validate semantics. Users opting in at
write time commit to either archiving the sister change first OR
folding this change into it before this change's own archive.

Implementation notes:
- New helper `Validator.checkDeltaAgainstCanonicalBase` invoked from
  inside the existing per-spec loop in `validateChangeDeltaSpecs`. Adds
  per-target ERROR-level issues with actionable guidance (which flag
  to use, what archive will refuse, how to switch to ADDED).
- New `Validator(options: ValidatorOptions)` constructor signature.
  Legacy `new Validator(true)` boolean constructor preserved via type
  overload — backward compatible.
- CLI flag `--accept-cross-change-base` added to `openspec validate`.
  Plumbed through `ExecuteOptions` → `validateByType` / `runBulkValidation`.

Tests:
- 9 new tests covering: MODIFIED-vs-missing-canonical default error,
  CREATE-shape (no canonical exists), MODIFIED matches canonical pass,
  cross-change MODIFIED default error vs flag-pass, sister-pending
  with non-matching name still errors (regression guard), REMOVED-not-
  found, RENAMED-from-not-found, legacy boolean-constructor backward
  compat.
- Full existing suite (1511 tests) still green.
@luigileap luigileap requested a review from TabishB as a code owner May 21, 2026 14:19
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 679f5c7e-0c12-443d-a63b-94ec09d94a5d

📥 Commits

Reviewing files that changed from the base of the PR and between 79303b5 and ee64aba.

📒 Files selected for processing (4)
  • src/cli/index.ts
  • src/commands/validate.ts
  • src/core/validation/validator.ts
  • test/core/validation.test.ts

📝 Walkthrough

Walkthrough

The PR adds early validation of delta spec references at write time, catching MODIFIED/REMOVED/RENAMED-from headers that don't exist in the canonical base spec. A new --accept-cross-change-base CLI flag enables optional sister-change scanning for cross-change MODIFIED workflows while archive-time validation remains strictly canonical-only.

Changes

Delta spec canonical-base cross-reference validation

Layer / File(s) Summary
CLI flag and command option threading
src/cli/index.ts, src/commands/validate.ts
Adds --accept-cross-change-base flag and extends ExecuteOptions, then threads the option through execute(), bulk/interactive/direct validation paths, and into validateByType() and runBulkValidation() so it reaches the Validator constructor.
Validator API expansion and canonical-base checking
src/core/validation/validator.ts
Introduces ValidatorOptions interface supporting strictMode and acceptCrossChangeBase; updates Validator constructor to accept legacy boolean or options object; integrates canonical-base cross-reference check into validateChangeDeltaSpecs loop; adds helpers to extract normalized ### Requirement: headers and validate delta targets against canonical (plus optional sister-pending changes when flag enabled).
Canonical-base cross-reference validation test suite
test/core/validation.test.ts
Comprehensive tests covering MODIFIED/REMOVED/RENAMED failure when headers missing from canonical, success when matching, sister-pending change behavior (blocked by default, allowed with flag), error when missing everywhere, and backward compatibility of legacy new Validator(true) constructor.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI validate
  participant Execute as ValidateCommand.execute
  participant ValidateByType as validateByType
  participant Validator as Validator
  participant Helper as checkDeltaAgainstCanonicalBase
  CLI->>Execute: {acceptCrossChangeBase: true}
  Execute->>ValidateByType: pass acceptCrossChangeBase in opts
  ValidateByType->>Validator: new Validator({strictMode, acceptCrossChangeBase})
  Validator->>Validator: validateChangeDeltaSpecs()
  loop for each delta spec
    Validator->>Helper: check MODIFIED/REMOVED/RENAMED targets
    Helper->>Helper: extractRequirementHeaderNames from canonical
    alt acceptCrossChangeBase true
      Helper->>Helper: also scan sister-pending changes
    end
    Helper->>Validator: push ERROR if target missing
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #1112: This PR directly implements the proposal to catch MODIFIED/REMOVED/RENAMED delta references that don't exist in the canonical spec at validation time (instead of only at archive time), with opt-in sister-change support via --accept-cross-change-base.

Poem

🐰 Headers once slipped past the validation gate,
But now we check the canonical fate!
Sister changes dance with a flag's gentle grace—
Cross-change MODIFIED finds its rightful place. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding canonical base header validation and an opt-in flag for cross-change modifications.
Linked Issues check ✅ Passed All objectives from #1112 are met: header-existence check moved to validate-time, opt-in --accept-cross-change-base flag implemented, archive remains strict, backwards compatibility preserved, and comprehensive tests added.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #1112 requirements: validator enhancement, CLI flag addition, command plumbing, and test coverage for the new validation behavior.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown
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.

Thanks, this is the right fix shape and it closes a real validate/archive trust gap.

One scope concern before approval: --accept-cross-change-base currently accepts REMOVED and RENAMED-from targets from sister pending changes too, but the issue rationale and tests only justify the cross-change case for MODIFIED. Please either narrow the opt-in to MODIFIED only, or add explicit rationale and tests for why cross-change REMOVED and RENAMED-from should be supported.

I checked the diff against the validation path and ran pnpm exec vitest run test/core/validation.test.ts locally, which passed.

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.

validate: MODIFIED/REMOVED/RENAMED-from headers that don't exist in base spec aren't caught until archive (proposal: opt-in cross-change MODIFIED)

3 participants