OpenSpec v1.10.0 and v1.11.0 shipped in quick succession (August 2026) — both incremental, no breaking changes. The centerpiece of v1.11.0 is openspec show --diff, which answers the most practical pain point in SDD practice: a MODIFIED requirement must restate every scenario it keeps, so reviewing a delta means reading mostly text identical to the main spec. This article walks the full post-upgrade workflow (Explore → Propose → Apply → Archive) through a real change — “cart query and item removal” (cart-query-and-remove) — and records the changes in both releases that affect how we practice.
Before this upgrade (v1.9.0), three rough edges in our workflow were each addressed by v1.10/v1.11:
Friction 1: MODIFIED reviews are verbose
A modified requirement restates every kept scenario -> >90% of a review
is unchanged text. Real changes drown in restatement.
Friction 2: explore's write boundary is fuzzy
Is "create the proposal file" thinking or implementing? No explicit
confirmation point before explore writes. Answering a clarifying
question can read as consent to write.
Friction 3: status checks are per-change
With several active changes you run status one by one — awkward for
dashboards and CI.
openspec init --language supports non-English artifacts (we already maintain Chinese specs by hand; keeping SHALL/MUST in English is enough to validate, so we don’t need it).openspec show <change> --diff (centerpiece): renders a colorized unified diff per requirement — ADDED shows full text, MODIFIED shows only the lines that actually change, REMOVED shows Reason/Migration, RENAMED shows FROM/TO; --json --diff brings the same detail to pipelines.openspec status --all: one command covers every active change; the JSON envelope is sorted and stable, and a change that fails to load contributes a diagnostic in place instead of stopping the sweep.openspec validate catches an unwritten Purpose (warning by default, fails under --strict) — a natural follow-up to v1.9.0’s Overview→Purpose migration.sync command (openspec sync does not exist); openspec archive merges deltas → updates main specs → archives in one step and prints merge stats (+1 added / ~1 modified). The agent-driven /opsx:sync remains for “sync only, don’t archive”.Exploring the dual implementations (Node.js + Python) against the specs surfaced a two-way gap in cart capabilities:
| Capability | Spec declares | Node.js | Python |
|---|---|---|---|
| Add item | Yes (3 scenarios) | ✅ | ✅ |
| Remove item | Yes (1 scenario) | ❌ no code | ❌ no code |
| View cart | No requirement | service only, no route | service only, no route |
“Remove” is under-implemented (the spec says it, code doesn’t have it); “query” is inverted (code exists, but neither spec nor HTTP layer exposes it). This change closes the loop — add → view → remove → order — and naturally produces both ADDED and MODIFIED deltas, exercising both renderings of show --diff.
openspec new change "cart-query-and-remove"
Schema-driven generation produced four artifacts:
cart-management (ADDED “购物车查询” + MODIFIED “购物车商品移除”); API contract: GET /api/cart/items and DELETE /api/cart/items/{productId} (removing a missing item → CART_ITEM_NOT_FOUND 404)productId rather than the internal item id; explicit 404 for a missing item instead of idempotent success; both endpoints return the full Cart; isomorphic error channels (Node Error(code) / Python ValueError(code))Validate caught an editing trap: renaming the scenario “移除购物车商品” to “移除购物车中的商品” inside MODIFIED failed validation — a MODIFIED block replaces the whole requirement, and archive refuses to silently drop a scenario the main spec still has. There is no scenario-level RENAMED mechanism (RENAMED exists only at the requirement level); the fix is to keep the scenario name and change only its content:
✗ [ERROR] MODIFIED "购物车商品移除" omits scenario(s) the current spec still has:
"移除购物车商品". Copy them into the MODIFIED block ...
-> restoring the original scenario name passes validation
Reviewing the delta takes a single command:
openspec show cart-query-and-remove --diff
The key experience: ADDED “购物车查询” renders as full text (a new requirement has no baseline to diff against); MODIFIED “购物车商品移除” renders only the lines that actually changed — the old 15-line requirement was replaced wholesale by 30 lines, yet the diff shows just: the reworded description and Rationale, the refined When/Then clauses, and the 2 new scenarios. The review surface shrinks from “reading 45 lines of restatement” to “looking at 15 lines of change.”
Service-layer implementations stay isomorphic:
// Node.js - cart.js
removeItem(userId, productId) {
const cart = this.getCart(userId)
const index = cart.items.findIndex(i => i.productId === productId)
if (index === -1) throw new Error('CART_ITEM_NOT_FOUND')
cart.items.splice(index, 1)
this.cartRepo.save(cart)
return cart
}
# Python - cart.py
def remove_item(self, user_id: str, product_id: str) -> Cart:
cart = self.get_cart(user_id)
item = next((i for i in cart.items if i.product_id == product_id), None)
if not item:
raise ValueError("CART_ITEM_NOT_FOUND")
cart.items.remove(item)
self.repo.save(user_id, cart)
return cart
HTTP layer: the Node dev server keeps its fixed-user_dev mock pattern; Python follows its request-carried userId convention (query parameter on GET/DELETE). Route error mapping gains CART_ITEM_NOT_FOUND → 404. As in the two previous practices, the separately maintained reduced route table in server.prod.js stays out of scope.
Tests: Node unit +3 (successful removal / missing item throws / multi-item isolation), integration E2E +1 (empty-cart query → add → query → remove → repeat remove 404); Python smoke +1 (same sequence with an isolated userId). Results: Node 14/14, Python 5/5 green.
v1.11.0’s openspec archive embeds the spec merge (no standalone sync command in the CLI):
$ openspec archive cart-query-and-remove --yes
Specs to update:
cart-management: update
Applying changes to openspec/specs/cart-management/spec.md:
+ 1 added
~ 1 modified
Totals: + 1, ~ 1, - 0, → 0
Specs updated successfully.
Change 'cart-query-and-remove' archived as '2026-09-02-cart-query-and-remove'.
Merge fidelity spot-checked: MODIFIED replaces only the target requirement block, ADDED appends, and untouched requirements (e.g. “购物车数量限制”) keep their original content and position. Full openspec validate: all 7 specs pass, no active changes left behind.
Reviewing a delta used to mean opening the file and reading restated text; now a single show --diff call exposes every real change. The more scenarios a MODIFIED requirement keeps, the bigger the win — this is the compensating mechanism for the design cost of “restate everything you keep.”
show --diff turns “write delta → review immediately” into a zero-friction move; semantic drift can be caught as early as the propose phase.After v1.9.0’s ## Purpose migration, v1.11.0’s validate warns on unwritten Purpose placeholders (default warning) — completing a “format migration → content guard” loop.
This article is based on the cart-query-and-remove practice in the OpenSpec Practise repository (2026-09-02); full artifacts live under openspec/changes/archive/2026-09-02-cart-query-and-remove/.