Skip to content

feat(validate): accept zero-delta changes that declare skip_specs - #1399

Merged
clay-good merged 1 commit into
mainfrom
fix/propose-guidance-zero-deltas
Jul 22, 2026
Merged

feat(validate): accept zero-delta changes that declare skip_specs#1399
clay-good merged 1 commit into
mainfrom
fix/propose-guidance-zero-deltas

Conversation

@clay-good

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

Copy link
Copy Markdown
Collaborator

Fixes #1384.

Status: ready for re-review (round 5 feedback addressed: the marker now requires the full loadable-metadata contract).

What was missing: the propose guidance said "leave Modified Capabilities empty if no requirement changes," but openspec validate unconditionally rejects a change with zero deltas (CHANGE_NO_DELTAS). A pure refactor — which Recipe 5 explicitly documents as having no spec delta — followed the guidance and then failed at validate time, after proposal/design/tasks were already written. There was no way to say "this change intentionally touches no specs."

What it does: a change can now declare skip_specs: true in its .openspec.yaml:

  • openspec validate accepts zero deltas for such a change (INFO note instead of ERROR). The marker is honored only when the metadata would load for status/instructions: the file parses under the shared ChangeMetadataSchema, the schema name is a known schema, and the schema itself loads via resolveSchema. Anything status/instructions would reject (missing schema, unknown name, a schema.yaml that exists but does not parse, a metadata file that exists but cannot be read) yields an explicit error instead of a silently-honored marker.
  • Marker + any spec file under specs/ is an explicit conflict error, never a silent drop.
  • The artifact graph counts specs-producing artifacts as complete for such changes (unblocking tasks), tracked separately so openspec status renders them as [~] skipped, not [x] done.
  • openspec instructions specs on such a change emits only a warning naming the marker (no creation directive or template), and the --json payload carries the same skipped/warning fields; instructions apply counts skipped artifacts as present, so custom schemas whose apply phase requires the specs artifact don't dead-end.
  • The conflict fires on any file under specs/ (root spec.md, headerless files, stray notes, symlinks — the artifact graph’s globs follow them, so they count as content), and openspec archive runs the same check, so validate and archive always agree. A metadata file that mentions skip_specs but isn't valid YAML gets the explicit invalid-metadata error rather than a misleading zero-delta hint.
  • Agent-facing surfaces updated coherently: continue/update skill templates and docs/agent-contract.md document the new skipped status and instructions fields (skills regenerated, golden hashes updated); docs/cli.md documents the marker and the [~] rendering.
  • The CHANGE_NO_DELTAS guidance, propose/specs schema instructions, and proposal template now point at the marker ("do not invent a requirement just to satisfy validation"), and Recipe 5 shows it.
  • A forgotten specs phase is still caught: without the marker, zero deltas remain a hard error, unchanged.

Proof (real CLI output):

$ openspec validate refactor-payment-module --type change   # no marker
✗ [ERROR] file: Change must have at least one delta. ... set "skip_specs: true" ...

$ cat .openspec.yaml   # adversarial: marker without required schema field
skip_specs: true
$ openspec validate refactor-payment-module --type change
✗ [ERROR] .openspec.yaml: skip_specs is set but .openspec.yaml is not valid change
metadata, so the marker is not honored. Fix the metadata (schema: Invalid input: ...)
✗ [ERROR] file: Change must have at least one delta. ...

$ openspec validate refactor-payment-module --type change   # valid metadata + marker
Change 'refactor-payment-module' is valid
$ openspec status --change refactor-payment-module
Progress: 1/3 artifacts complete (1 skipped)
[x] proposal
[ ] design
[~] specs (skipped: change declares skip_specs)
[-] tasks (blocked by: design)

Tests cover accept/reject/conflict/malformed-metadata/adversarial-metadata paths (test/core/validation.skip-specs.test.ts), the graph unblock and skipped status (instruction-loader.test.ts), and the metadata schema. Full suite green except the 17 known environment-only zsh-installer failures (#1321).

Notes: per-change and explicit by design — unlike the project-wide requireSpecDeltas config proposed in #977, the marker is a reviewable statement in the change itself. Archive enforces the marker (conflict and unhonorable-metadata states block), and its behavior-critical paths are mutation-tested: reverting any of the 12 key behaviors kills at least one test. Deliberately out of scope: an unmarked zero-delta change still archives with only non-blocking warnings — that gap predates this PR (pinned by the existing archive tests) and closing it would change behavior for changes that never opted into anything; the archive code comment states the boundary explicitly.

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

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

The spec-driven workflow now supports explicit skip_specs: true metadata for zero-delta changes. Validation accepts those changes, rejects conflicts with spec files, and marks spec-producing artifacts as skipped and complete.

Changes

Explicit zero-delta workflow

Layer / File(s) Summary
Zero-delta guidance and metadata contract
schemas/spec-driven/..., src/core/change-metadata/schema.ts, docs/examples.md, .changeset/*
Guidance and examples document skip_specs: true, while change metadata validates it as an optional boolean.
skip_specs validation behavior
src/core/validation/*, test/core/validation.skip-specs.test.ts, test/utils/change-metadata.test.ts
Validation accepts explicit zero-delta changes, rejects spec-file conflicts and malformed metadata, and adds coverage for these cases.
Workflow completion and status handling
src/core/artifact-graph/instruction-loader.ts, src/core/change-status-policy.ts, src/commands/workflow/*, test/core/artifact-graph/instruction-loader.test.ts
Spec-generating artifacts are marked skipped and complete when specs are explicitly skipped, with updated status indicators, progress output, instructions, types, and tests.

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

Possibly related PRs

Suggested reviewers: tabishb, tabishb

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested skip_specs escape hatch and resolves the validate-vs-guidance contradiction in #1384.
Out of Scope Changes check ✅ Passed The changes stay focused on skip_specs validation, workflow handling, docs, and tests, with no unrelated features added.
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 summarizes the main change: validation now accepts zero-delta changes when skip_specs is declared.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/propose-guidance-zero-deltas

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@schemas/spec-driven/templates/proposal.md`:
- Around line 18-20: Update the guidance comment in the proposal template to
include the pure-refactor requirement for a testable non-functional property,
matching the corresponding instructions in schema.yaml. Preserve the existing
capability/delta guidance and clarify that pure refactors must still define at
least one verifiable non-functional outcome.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4c2c50c-57ad-47d7-9a5a-362952053446

📥 Commits

Reviewing files that changed from the base of the PR and between 60f720c and 149b95e.

📒 Files selected for processing (2)
  • schemas/spec-driven/schema.yaml
  • schemas/spec-driven/templates/proposal.md

Comment thread schemas/spec-driven/templates/proposal.md Outdated

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

This trades the late validation failure for a worse contract: pure refactors are now told to invent a changed requirement, while docs/examples.md still says they have no delta and docs/concepts.md says implementation-only changes do not belong in specs. Please model true no-behavior work as an explicit skipped/not-applicable specs state, or make the hard-minimum policy a coordinated product and documentation change rather than updating only the schema prompt.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying openspec-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: e3c8d34
Status: ✅  Deploy successful!
Preview URL: https://857ff3cb.openspec-docs.pages.dev
Branch Preview URL: https://fix-propose-guidance-zero-de.openspec-docs.pages.dev

View logs

@clay-good clay-good changed the title fix(schemas): align propose guidance with validate's one-delta minimum feat(validate): accept zero-delta changes that declare skip_specs Jul 21, 2026
@clay-good

Copy link
Copy Markdown
Collaborator Author

Reworked per the review: the previous revision kept the hard one-delta minimum and told pure refactors to name a changed guarantee, which contradicted docs/examples.md Recipe 5 and concepts.md. This revision takes the suggested first option — an explicit not-applicable specs state. skip_specs: true in the change's .openspec.yaml makes validate accept zero deltas (and error if the marker coexists with delta files), unblocks the artifact graph, and the guidance/docs now describe that contract instead of asking authors to invent requirements. Without the marker, zero deltas still hard-fail, so the late-discovery footgun from #1384 stays fixed for changes that simply forgot their specs.

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

The new direction fixes the fake-requirement contradiction, but changeDeclaresSkipSpecs() trusts raw skip_specs: true without validating the metadata shape: I reproduced a .openspec.yaml containing only that field, with the required schema missing, passing as valid while status/instructions reject the same metadata. Please validate the marker through the shared metadata schema (and add the adversarial case), and surface specs as explicitly skipped/not-applicable in status rather than collapsing them to [x] complete.

@clay-good

Copy link
Copy Markdown
Collaborator Author

Both round-2 points addressed in 215494a:

  1. Marker validated through the shared schema. readSkipSpecsMarker now runs the file through ChangeMetadataSchema; the marker only counts when the metadata parses. Your repro — .openspec.yaml containing only skip_specs: true — now fails validate with an explicit .openspec.yaml error naming the missing field, plus the unchanged zero-delta error, matching what status/instructions reject. Adversarial tests added for both validateChangeDeltaSpecs and validateChange.

  2. Skipped is now a first-class status. loadChangeContext tracks synthesized completions in skippedArtifacts (kept in completed so dependents unblock), ArtifactStatus gained 'skipped', and openspec status renders [~] specs (skipped: change declares skip_specs) with the skipped artifact excluded from the progress denominator instead of [x] complete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/core/validation.skip-specs.test.ts`:
- Around line 109-119: Update Validator.validateChange so the CHANGE_NO_DELTAS
issue is filtered only when readSkipSpecsMarker(changeDir) reports valid
metadata, not merely when the marker is declared; preserve the existing
filtering behavior for valid skip_specs metadata and retain the no-deltas error
for invalid metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1c00c9d8-d600-4b32-bd61-254cb5087582

📥 Commits

Reviewing files that changed from the base of the PR and between 25bb2fe and 215494a.

📒 Files selected for processing (10)
  • .changeset/skip-specs-explicit-zero-delta.md
  • docs/examples.md
  • src/commands/workflow/shared.ts
  • src/commands/workflow/status.ts
  • src/core/artifact-graph/instruction-loader.ts
  • src/core/change-status-policy.ts
  • src/core/validation/constants.ts
  • src/core/validation/validator.ts
  • test/core/artifact-graph/instruction-loader.test.ts
  • test/core/validation.skip-specs.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • .changeset/skip-specs-explicit-zero-delta.md
  • test/core/artifact-graph/instruction-loader.test.ts
  • src/core/validation/constants.ts
  • src/core/validation/validator.ts

Comment thread test/core/validation.skip-specs.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/workflow/instructions.ts`:
- Around line 192-199: Update printInstructionsText so when instructions.skipped
is true, it stops before emitting the creation directive and output path, or
otherwise makes the remaining instructions explicitly non-actionable. Preserve
the existing warning text and normal creation instructions for non-skipped
artifacts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f66235ae-0361-4f11-82bf-21c3fcc9161e

📥 Commits

Reviewing files that changed from the base of the PR and between 215494a and 8814ccc.

📒 Files selected for processing (3)
  • src/commands/workflow/instructions.ts
  • src/core/artifact-graph/instruction-loader.ts
  • test/core/artifact-graph/instruction-loader.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/core/artifact-graph/instruction-loader.test.ts
  • src/core/artifact-graph/instruction-loader.ts

Comment thread src/commands/workflow/instructions.ts Outdated

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

One remaining cross-surface trust gap: readSkipSpecsMarker validates only the Zod shape, not whether schema actually resolves. I reproduced .openspec.yaml with schema: does-not-exist and skip_specs: true: openspec validate refactor reports valid with exit 0, while openspec status rejects the same metadata; archive can likewise bypass delta validation when specs/ is empty. Please route the marker through the same resolvable-metadata contract as status and instructions, then add this adversarial case.

@clay-good

Copy link
Copy Markdown
Collaborator Author

Fixed in b805f4b + d7ceca9. The marker is now routed through the full loadable-metadata contract, in two steps that mirror status/instructions exactly:

  1. b805f4breadSkipSpecsMarker requires the schema name to pass readChangeMetadata's listSchemas membership check (same project-root derivation as resolveSchemaForChange, so project-local and store-scoped schemas resolve identically to status). Your repro — schema: does-not-exist + skip_specs: true — now fails validate with exit 1 (skip_specs is set but .openspec.yaml is not valid change metadata (schema: unknown schema 'does-not-exist')) and blocks archive with the same message.

  2. d7ceca9 — an adversarial pass on that fix found the next gap in the same class: listSchemas only checks that schema.yaml exists, so a project-local schema file that exists but fails to parse still passed validate while status errored one step later in resolveSchema. The marker now also requires resolveSchema to succeed, and its error text becomes the diagnostic (e.g. (Failed to parse schema at '…/schemas/custom/schema.yaml': …)), so validate and status report the same failure. Same pass also closed: symlinked spec files evading the specs/ conflict probe, metadata files that exist but can't be read (EACCES/EISDIR) degrading to "unmarked" so archive proceeded while status errored (now fail closed as unhonorable), an explicit skip_specs: false dragging unrelated metadata errors into validate, and ./specs/… generates paths missing the skip set.

Adversarial cases pinned in tests: unknown schema (validate both paths + archive), exists-but-unparseable schema, parses-but-invalid schema, extension alias spec-driven.yaml (rejected — resolveSchema alone would normalize it, readChangeMetadata doesn't), unreadable metadata (validate + archive), symlinked spec conflict, skip_specs: false, and a loadable project-local schema as the positive control. 2069 tests pass locally.

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

Re-review at 349c4e6: the marker now requires the same loadable-schema contract as status and instructions, and the reproduced unknown-schema case fails validate and blocks archive instead of bypassing delta validation. The added unloadable-schema, unreadable-metadata, symlink, false-marker, project-schema, and normalized-path cases are sound; 165 focused tests, clean skill regeneration, build, and the full exact-head CI and CodeQL matrix pass.

Squashed for rebase; see PR #1399 for the full commit history.

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

Re-reviewed the exact squashed head. The skip_specs marker now uses the same loadable-metadata contract as status/instructions, conflicts fail closed across validate/archive, skipped artifacts compose correctly with the transitive proposal flow, and all 2,148 local tests plus required CI pass.

@clay-good
clay-good added this pull request to the merge queue Jul 22, 2026
Merged via the queue into main with commit 27b22ab Jul 22, 2026
15 checks passed
@clay-good
clay-good deleted the fix/propose-guidance-zero-deltas branch July 22, 2026 16:01
@TabishB

TabishB commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@clay-good How does skip-specs work for schemas that don't have spec defined artifacts? i.e what happens if schema is not spec-driven?

seeems like a workflow specific flag than a generalized flag

@clay-good

Copy link
Copy Markdown
Collaborator Author

@clay-good How does skip-specs work for schemas that don't have spec defined artifacts? i.e what happens if schema is not spec-driven?

seeems like a workflow specific flag than a generalized flag

It works for any resolvable schema, not just spec-driven ones: validate demands a spec delta from every change regardless of schema, so skip_specs is the escape hatch that lets a non-spec-driven change pass, and the artifact-completion step is a harmless no-op there since it only touches artifacts whose generates path lives under specs/, but you're right that a cleaner generalized form is a schema-level "requires specs" declaration rather than a per-change marker.

@alfred-openspec

Copy link
Copy Markdown
Collaborator

Since this is already merged, I’d keep skip_specs as the explicit per-change N/A override for spec-driven, then move the implicit global delta requirement into schema validation in a follow-up; schemas without a declared delta-spec policy should never need the flag. I’d avoid a bare requires_specs boolean and declare the delta artifact/policy instead, because a path under specs/ is not a durable semantic contract.

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.

Feedback: propose phase's 'leave empty if no requirement changes' guidance conflicts with validate's hard CHANGE_NO_DELTAS >=1 constraint

3 participants