Skip to content

fix(validate): reject a delta spec at the change's specs/ root#1392

Merged
clay-good merged 2 commits into
mainfrom
fix/validate-root-delta-spec
Jul 20, 2026
Merged

fix(validate): reject a delta spec at the change's specs/ root#1392
clay-good merged 2 commits into
mainfrom
fix/validate-root-delta-spec

Conversation

@clay-good

@clay-good clay-good commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1385.

Status: LGTM — reproduced on main, fixed, regression-tested.

What was wrong

If you wrote a change's delta spec at openspec/changes/<change>/specs/spec.md — no capability folder — OpenSpec told you it was fine and then threw it away.

openspec validate <change> --strict passed and counted the deltas. But everything that actually merges specs (show, apply, archive) only reads specs/<capability>/spec.md, so it saw zero deltas. The change archived successfully and the requirement never reached openspec/specs/. The only signal was a non-blocking warning that scrolled by.

Two walkers disagreed: the validator had its own recursive scan, while the merge path used the shared discoverSpecFiles helper.

How it's fixed

  • The validator now uses discoverSpecFiles — the same helper the merge path uses — so it can never accept a layout the merge silently skips.
  • A file at specs/spec.md is reported as an error that names the fix: Delta spec found at specs/spec.md. Delta specs must live in a capability folder (e.g. specs/<capability>/spec.md) — a file at the specs/ root is ignored when the change is applied or archived.
  • archive trips its validation gate on that file simply existing, so it blocks instead of archiving a change whose content will be dropped. Existence, not delta headers: a root-level file written in main-spec shape (## Requirements) is dropped just the same.
  • A directory named specs/spec.md is left alone — that is an ordinary capability folder the merge path reads normally.
  • The generic "No deltas found" error is suppressed when the precise error fires, so the output doesn't claim there are no deltas in the file it just named.

Move the file into a capability folder and everything behaves as before. No other layout changes.

Proof

Before (main), from the issue's repro:

$ openspec validate add-metrics --strict
Change 'add-metrics' is valid
$ openspec archive add-metrics --yes
Change 'add-metrics' archived as '2026-07-20-add-metrics'.
$ grep -r "Request metrics" openspec/specs/
(no match — the requirement is gone)

After:

$ openspec archive add-metrics --yes
Validation errors in change delta specs:
  ✗ Delta spec found at specs/spec.md. Delta specs must live in a capability folder
    (e.g. specs/<capability>/spec.md) — a file at the specs/ root is ignored when the
    change is applied or archived.
Validation failed. Please fix the errors before archiving.
(exit 1, change still active)

$ mkdir -p specs/metrics && mv specs/spec.md specs/metrics/spec.md
$ openspec archive add-metrics --yes
  metrics: create
Change 'add-metrics' archived as '2026-07-20-add-metrics'.
$ grep -rl "Request metrics" openspec/specs/
openspec/specs/metrics/spec.md

Behavior matrix, each row exercised against the built CLI:

Layout validate archive
specs/spec.md (delta shape) error blocked
specs/spec.md (main-spec shape) error blocked
specs/spec.md plus a valid capability delta error blocked
specs/<capability>/spec.md pass archives, merges
specs/<area>/<capability>/spec.md pass archives, merges
specs/spec.md/spec.md (folder named spec.md) pass archives, merges

Five regression tests added across validation.test.ts and archive.test.ts. Full suite: 2,015 passing; the only failures are the 17 known environment-only zsh-installer cases that also fail on unmodified main.

Notes

  • Consolidating on discoverSpecFiles also makes the validator skip dot-directories, matching the merge path — a delta hidden in a dot-directory was never merged anyway.
  • Direction picked from the two the issue offered: reject the layout rather than teach the merge to accept it, since a root-level file has no capability id to merge into.
  • Out of scope, worth a separate issue: delta content in a file not named spec.md (e.g. specs/billing.md) is invisible to both walkers, so it is dropped the same way without any warning. That predates this change and affects a different code path; happy to file it.
  • --no-validate still archives without these checks. That is the documented opt-out and already warns that it may archive invalid specs.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed delta spec discovery so misplaced specs/spec.md directly under a change’s specs/ folder is now detected during validation.
    • Validation now reports the issue with guidance to place delta specs under a capability folder (e.g., specs/<capability>/spec.md).
    • Archive operations are now correctly blocked when such misplaced delta specs are found.
  • Tests

    • Added regression coverage ensuring blocked archive behavior and improved validator edge-case handling for both invalid root layouts and valid nested capability layouts.

@clay-good
clay-good requested a review from TabishB as a code owner July 20, 2026 16:49
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Validation now uses shared spec discovery and rejects delta specs at specs/spec.md. Archive detects this layout, invokes validation, and blocks archival. Tests cover invalid root-level and valid nested capability layouts.

Changes

Delta spec layout enforcement

Layer / File(s) Summary
Validator discovery and layout checks
src/core/validation/validator.ts, test/core/validation.test.ts
validateChangeDeltaSpecs now uses shared discovery, reports root-level specs/spec.md as an error, removes the recursive walker, and tests nested capability layouts.
Archive validation gate
src/core/archive.ts, test/core/archive.test.ts, .changeset/root-level-delta-spec.md
Archive includes root-level spec.md in delta candidates, blocks invalid archives, and documents the corrected behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: tabishb, mc856

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR matches #1385 by rejecting root-level specs/spec.md and preserving valid nested capability layouts.
Out of Scope Changes check ✅ Passed All code and tests focus on the validation/archive delta-spec discovery fix; no unrelated changes stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: rejecting a delta spec at the change's specs/ root.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/validate-root-delta-spec

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.

A `spec.md` written directly under a change's `specs/` directory was
accepted by `validate` — including `--strict` — but skipped by the
apply/archive merge, which only reads capability folders. The change
validated clean, archived successfully, and its requirements never
reached `openspec/specs/`.

Point the validator at the shared `discoverSpecFiles` helper so it applies
exactly the merge path's rules, and report a root-level `specs/spec.md` as
an error naming the capability-folder convention. Archive's delta-detection
gate now also sees that file, so validation runs and blocks the archive
instead of completing with the delta dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good
clay-good force-pushed the fix/validate-root-delta-spec branch from 805169b to 61d6a2b Compare July 20, 2026 16:54
Follow-up to the same divergence class as the parent commit, found while
re-reviewing it.

Archive's gate only ran validation when a candidate file carried delta
headers, so a root-level `specs/spec.md` written in main-spec shape
(`## Requirements`) still archived with exit 0 while `validate` reported an
error — the two commands disagreed again. The file is never merged whatever
its shape, so existence alone now trips the gate.

Also stop reporting a *directory* named `specs/spec.md` as misplaced: that
is an ordinary capability folder the merge path reads normally, and
`fileExists` matched it. Both sites now require a regular file.

Finally, suppress the generic "No deltas found" error when the root-level
error already fired: it contradicted the precise message by claiming there
were no deltas in the very file just named.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Using the shared discovery helper removes the validator/merge walker divergence, while the explicit root-file check correctly blocks archive regardless of file shape and preserves a directory named spec.md as a valid capability. The focused validation/archive suite passed 97/97 at this exact head, and the full cross-platform CI matrix is green.

@clay-good
clay-good added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit a13abea Jul 20, 2026
14 checks passed
@clay-good
clay-good deleted the fix/validate-root-delta-spec branch July 20, 2026 17:31
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 accepts a delta spec.md directly under specs/ that archive silently drops

2 participants