fix(archive): refuse delta files the merge path never reads - #1870
Conversation
validate and archive read a change's deltas only from specs/<capability-path>/spec.md, but the spec-driven artifact graph counts any markdown file under specs/ as written. A delta at specs/user-auth.md was reported done by status and ready by apply, rejected by validate only as having no deltas, and then archived with exit 0 and nothing merged. Name every markdown file under specs/ that carries delta sections but is not a capability's spec.md. validate reports it as an error with the spec.md its requirements belong in, archive runs that validation and refuses the change, and apply lists it in its warnings. --no-validate and changes with no spec files behave as before.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (7)
Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review. 📝 WalkthroughWalkthroughThe change detects delta sections outside capability ChangesUnread delta file handling
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix · Severity of issue fixed: Medium Sequence Diagram(s)sequenceDiagram
participant ApplyInstructions
participant Validator
participant Archive
participant findUnreadDeltaFiles
ApplyInstructions->>findUnreadDeltaFiles: inspect change specs
findUnreadDeltaFiles-->>ApplyInstructions: unread delta paths
ApplyInstructions-->>ApplyInstructions: create move warnings
Validator->>findUnreadDeltaFiles: inspect change specs
findUnreadDeltaFiles-->>Validator: unread delta paths
Validator-->>Validator: emit file-specific errors
Archive->>Validator: validate change
Validator-->>Archive: validation failure
Archive-->>Archive: refuse archive
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The validation, archive, and apply-warning paths consistently handle misplaced delta files and are covered by targeted tests. The PR is ready to merge after normal checks. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation Issue Resolution Update status or its artifact-completion logic to identify delta sections in files that the merge path does not read. Do not report the specs artifact or planning state as complete for those files. Add status tests for flat and nested misplaced delta files, while preserving valid Full details: Docstring CoverageExplanation Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 5 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
The legacy docs/ tree is frozen; the canonical page for the spec-driven delta layout is docs-lab/reference/schemas/spec-driven/index.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Hardening pass: pushed b699b7e, docs(schemas): move delta file placement note to docs-lab. The sentence on where a change's delta must live now sits in docs-lab/reference/schemas/spec-driven/index.md (Delta specs section) instead of the frozen docs/writing-specs.md, which is restored to main. No code changes. @TabishB for the docs-lab wording. Verified on the built CLI: the #1869 repro now fails validate and archive (exit 1) and apply warns. Nested layouts, a README without delta headers beside spec.md, and delta headers inside a fence still validate and archive. New tests fail without the fix (19 of 27; the rest are controls or run against the built CLI). |
alfred-openspec
left a comment
There was a problem hiding this comment.
Misplaced delta files are detected consistently by apply guidance, validation, and archive before anything is silently dropped. The canonical docs describe the enforced layout, and focused tests pass.
Closes #1869.
Why
validateandarchiveread a change's deltas only fromspecs/<capability-path>/spec.md(discoverSpecFiles). The spec-drivenschema declares the specs artifact as
generates: "specs/**/*.md", and bothartifact completion (
status) and the #1783 no-specs warning ininstructions applyresolve that glob. The two readers disagree about anyother markdown file under
specs/.A delta written as
specs/user-auth.md, or as a second file beside acapability's
spec.md, therefore went through every surface without a stop:status: specs done,isPlanningComplete: trueinstructions apply:all_done, no warning (the glob matches the file,so the fix(apply): warn when a change is ready to implement with no specs #1783 warning does not fire)
validate: exit 1, but only the generic "Change must have at least one delta"archive: exit 0. Nothing is merged intoopenspec/specs/, and theoutput never mentions specs.
archive.tsruns delta validation only when it finds delta content it can read,or a root
specs/spec.md(#1385). Otherwise it takes the zero-delta path itsown comment acknowledges ("An UNMARKED zero-delta change still archives with
only non-blocking proposal warnings"). The authored requirement is dropped, and
the change is filed as done.
Verified end to end in a project created by
openspec init+openspec new change, on the published 1.13.0 package and onmain@9d4e597.What Changes
findUnreadDeltaFiles(src/utils/spec-discovery.ts) lists markdownfiles under a change's
specs/that carry delta sections but are not acapability's
spec.md, with thespec.mdtheir requirements belong in:specs/user-auth.md→specs/user-auth/spec.mdspecs/user-auth/more.md→specs/user-auth/spec.md"Carries delta sections" is decided by
parseDeltaSpec, the reader archivemerges with, so headers inside a code fence do not count. The walk follows
discoverSpecFiles: dot entries skipped, symlinked directories not followed,dangling links skipped, other read errors thrown. A root
specs/spec.mdisleft to its existing validate accepts a delta spec.md directly under specs/ that archive silently drops #1385 check.
validatereports each file as an ERROR naming the file and where itsrequirements belong. The generic "No deltas found" is suppressed when this
error already explains the missing deltas, the same way the validate accepts a delta spec.md directly under specs/ that archive silently drops #1385 root error
does.
archivetreats such a file as delta content, the way it treats a rootspecs/spec.md, so it runs delta validation and refuses the change instead ofarchiving it unmerged.
--no-validatekeeps its existing meaning.instructions applylists each file inwarnings, so apply stops beingthe one surface that green-lights the change.
docs-lab/reference/schemas/spec-driven/index.md: one sentence, in the Delta specs section, on where a change's delta must live.Unchanged:
for changes that authored no delta).
specs/with no delta section (a README, review notes) are notreported.
Why not change the schema glob instead. Tightening
schemas/spec-driven/schema.yaml:51tospecs/*/spec.mdwould makestatusagree, but it would also break the supported nested layout
(
specs/<area>/<capability>/spec.md, #1353). A recursivespecs/**/spec.mdwould change what
statusand the instruction loader show, and what completesthe artifact, for projects in mid-flight. Custom schemas keep their own globs
either way. Reporting the unread file where it matters (validate, archive,
apply) closes the gap without changing any schema.
Testing
test/core/misplaced-delta-files.test.ts: 27 tests, written first and runbefore the fix: 20 failed / 7 passed (the 7 are controls). With the fix:
27 passed (27).
Before the fix, the helper tests failed because the helper did not exist, and
every behavioural test failed on its assertion:
validatefound no ERROR for the file;archiveset no exit code and archived the change;applyreturned nowarnings.Edge cases covered:
valid
spec.md; nested area folder;SPEC.md(case); flat and nested validlayouts (not reported); root
spec.md(left to validate accepts a delta spec.md directly under specs/ that archive silently drops #1385); notes without deltasections; delta headers only inside a code fence; dot entries and non-
.mdfiles; sorted output; missing
specs/; dangling symlink (skipped on Windows)found"; a stray file is rejected even beside a valid
spec.md; controls forthe nested layout and for notes beside a valid delta
spec untouched);
--no-validatestill archives; controls for a change with nospec files and for the valid layout (archived and merged)
a stray file; control stays quiet
init,new change, apply, validate,archive) for the flat file and the valid layout
CI parity, in a clean checkout of
9d4e597with this patch, on Node 20.19.0:pnpm run build: successpnpm exec tsc --noEmit: no errorspnpm lint: no errorspnpm test(full suite,VITEST_MAX_WORKERS=4), run in a sandbox sharedwith several other suites:
validation.archive-preflight.test.ts(see Notes for reviewers). It isfixed. That file, this branch's test file,
validation,archiveandapply-instructions-warningspass together: 369 passed.is a 10 s test timeout or
spawnSync npm ETIMEDOUT.store-references,store-remote,store-root-selection,store,worksetandpackage-install-scripts. Each of those filesfails the same way on unpatched
9d4e597under the same load.cli-e2e/basic,cli-e2e/store-lifecycle,cli-e2e/validate-international,artifact-workflowanddeclared-store-fallback. Each passes run alone,both on this branch and on
9d4e597.Changeset
.changeset/reject-unread-delta-files.md(patch). For release notes: a changewhose delta sits outside
specs/<capability-path>/spec.mdused to archivewithout merging it. It now fails validation until the file is moved.
Notes for reviewers
statusstill reports the specs artifact as done for such a file, becausethe schema glob is unchanged (see above).
instructions applynow carries thewarning.
validateChangeDeltaSpecsthe unread-file check runs after the advisorymerge-conflict discovery (validate: MODIFIED/REMOVED/RENAMED-from headers that don't exist in base spec aren't caught until archive (proposal: opt-in cross-change MODIFIED) #1112), not inside it. An error from the advisory
pass then stays an INFO note while strict validation stays valid, which is
what
validation.archive-preflight.test.tspins. The first full-suite runcaught this ordering.
validator.tsandarchive.tsin small, separate hunks. Otheropen fixes that edit those files may need a trivial rebase.
Summary by CodeRabbit
Bug Fixes
--no-validatebehavior is unchanged.Documentation
spec.mdfile.