* Proposed feature: openspec show --diff to see changed requirements more clearly * Implementation of the --diff feature, which led to some spec changes during implementation. * Update the proposal to be clearer. Thanks coderabbit * Fix a bug identified by coderabbit with excessive trimming, and add a testcase for it. * fix(show): harden --diff for review feedback Keeps `openspec show <change>` without `--diff` a raw proposal passthrough, reports when a change has no delta specs instead of returning silently, preserves the Reason/Migration body of a REMOVED requirement, and resolves main specs through the command's root so `--store <id>` diffs against that store. Text mode and JSON mode now render from one shared collection pass, the CLI tests drive argv arrays from a mkdtemp project instead of interpolated shell strings, `--diff` is registered for shell completions, and the stray package-lock.json is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * build(deps): declare the diff dependency and refresh the flake hash Adds the `diff` runtime dependency that requirement-diff.ts imports, updates pnpm-lock.yaml, and regenerates the flake's pnpmDeps hash so `nix build` matches the new lockfile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(show): diff nested capabilities too collectSpecDiffs enumerated only top-level directories under the change's specs/, so a nested capability (specs/<area>/<id>/spec.md) was skipped: text mode printed nothing for it and its MODIFIED deltas came back from --json with no diff. It now uses the same discoverSpecFiles() helper ChangeParser uses, so the capability ids match the `spec` field of the JSON deltas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(show): report header mismatches instead of hiding them Two cases where --diff quietly showed something misleading: A MODIFIED requirement whose capability has no main spec was rendered as all-additions, which reads like a new capability. It is an authoring error archive will reject, so it now prints the raw text with a warning naming the missing spec. A header that differs from the main spec only in case or interior spacing found no match at all under exact lookup, or matched under a lowercase-only comparison that let a real mismatch through silently. Lookup is now exact first, then the shared foldRequirementName fallback, and a folded match prints the diff the author meant alongside a warning that archive matches names exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(show): name the main spec as such in collectSpecDiffs Comment and locals still called the main spec the "base" spec, and the no-main-spec comment described the old all-additions behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(deps-dev): bump the development-dependencies group with 2 updates Bumps the development-dependencies group with 2 updates: [smol-toml](https://github.com/squirrelchat/smol-toml) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint). Updates `smol-toml` from 1.7.1 to 1.8.0 - [Release notes](https://github.com/squirrelchat/smol-toml/releases) - [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.7.1...v1.8.0) Updates `typescript-eslint` from 8.66.0 to 8.67.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/typescript-eslint) --- updated-dependencies: - dependency-name: smol-toml dependency-version: 1.8.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies - dependency-name: typescript-eslint dependency-version: 8.67.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> * chore(nix): invalidate pnpm dependency hash * fix(nix): update pnpm dependency hash * fix(show): harden requirement diff output * build(nix): pin combined dependency hash * test(show): assert proposal precedes diffs * docs(show): clarify JSON diff diagnostics * fix(show): retain unified diff hunk headers --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4.5 KiB
Why
When proposing a change, delta spec files under openspec/changes/<name>/specs/ duplicate large portions of existing specs in openspec/specs/. A MODIFIED requirement must include the entire requirement block (all scenarios), making it hard to see what actually changed versus what was copied verbatim. This friction slows review and increases the risk of errors.
What Changes
Add a --diff flag to openspec show (change type) that renders each delta spec as a unified diff against the corresponding main spec in openspec/specs/. This is the smallest viable improvement: it doesn't change the storage format or workflow, just adds a new way to view the deltas.
Approaches considered
Three approaches were evaluated:
-
Stop storing delta specs; edit main specs on the branch directly. This would eliminate duplication entirely but conflicts with the spec-driven workflow where changes are proposed, reviewed, and archived as discrete artifacts before the main specs are updated. Deferred — would require rethinking the change lifecycle.
-
Store deltas as diffs instead of full specs. The
specs/<cap>/spec.mdfiles inside a change would contain unified diffs (or a structured delta format) rather than full requirement text. This eliminates duplication at the source but complicates authoring (AI and humans must produce correct diffs), parsing, validation, and the archive/apply step that merges deltas into main specs. Promising for a future change, but high complexity. -
Add
openspec show --diffto render deltas against main specs. (Chosen.) Leave the storage format unchanged. When displaying a change, compute the diff on the fly by comparing each delta spec file against its matching main spec. This gives reviewers the view they need with minimal code changes and zero workflow disruption.
What this change delivers
- A
--diffflag onopenspec show <change>(andopenspec change show <change>) that outputs a human-readable unified diff per delta spec
JSON mode (--json --diff): The existing JSON structure ({ id, title, deltaCount, deltas }) is preserved. Only deltas with operation: "MODIFIED" include a "diff" field containing unified-diff text. Deltas with operation: "ADDED", "REMOVED", or "RENAMED" do not include a "diff" field (it is absent from the object). When no matching requirement block is found for a MODIFIED delta — no match in the main spec, or no main spec for that capability at all — the delta includes a "warning" field (string) instead of "diff", describing the mismatch. A header that matches only after folding case and interior whitespace carries both: the "diff" the author meant and a "warning" that archive matches names exactly.
Text mode (--diff without --json): The proposal markdown is printed first, followed by a "Specifications Changed (diffs)" section. MODIFIED deltas show colorized unified diffs (additions in green, removals in red); ADDED deltas show the full requirement text as all-additions in green; REMOVED deltas show the authored removal block, Reason and Migration included, in red; RENAMED deltas show old and new names. When a MODIFIED delta has no matching requirement block — or its capability has no main spec — the raw requirement text is printed with a warning instead of a diff. Without --diff, openspec show <change> prints the proposal and nothing else, exactly as before.
Capabilities
New Capabilities
None.
Modified Capabilities
cli-show: Add--diffflag support for change display, computing unified diffs of delta specs against their main specs
Non-goals
- Changing the delta spec storage format (approach 2 above — future work)
- Changing when or how main specs are updated (approach 1 above — future work)
- Diffing non-spec artifacts (proposal, design, tasks)
- Git-aware diffing (this compares files on disk, not git history)
Impact
src/commands/show.ts— pass--diffflag through to change displaysrc/commands/change.ts— implement diff rendering inshow()for text and JSON modessrc/cli/index.ts— register--diffoption on the show and change show commandssrc/utils/requirement-diff.ts— new: pull one requirement block out of a spec and diff it against the delta block (diffpackage)src/core/parsers/requirement-blocks.ts— expose the raw REMOVED blocks so a removal's Reason/Migration text survives into the outputsrc/core/completions/command-registry.ts— offer--diffin shell completions