Specs, Deltas and Validation
The capabilities contract in action, the three user stories as testable requirements, the article contract, one requirement traced end to end, and what the validator refuses.
The proposal named four capabilities. This page is what that obliged, what the specifications actually say, and how the validator keeps them honest.
10.1 The capabilities contract
FIGURE W4 — One section of the proposal decides which spec files must exist. Get it wrong here and the specs phase writes the wrong files.
Four capabilities named, four delta specification files obliged. Every capability listed under ## Capabilities must have exactly one file beneath specs/, and a change with no delta at all is rejected by validation unless .openspec.yaml explicitly sets skip_specs: true.
Four rather than one is a judgement, not a rule. They were split this way because each can be tested on its own: a list of new items is useful before anything drafts an article, and a draft is useful before anything files it. That maps directly onto the three user stories, which is not a coincidence — the stories were what suggested the split.
10.2 The three user stories, as specifications
The stories that drove this design are the same three used across all three companion sections:
- US1 — sources are watched without anyone remembering to look.
- US2 — a finished draft, not a summary.
- US3 — it arrives where the team already works.
Those stories carry nine numbered functional requirements, FR-001 to FR-009, and the identifiers are worth keeping because they are how the three companion sections refer to the same rules. Two are referenced by name in the figures on this page: FR-008 — "System MUST NOT make any article publicly visible" — and FR-009 — "System MUST notify a person when a run fails". In OpenSpec they are not a numbered list in a document; each becomes a ### Requirement: inside the capability that owns it, with at least one scenario against it.
Here is what US1 looks like once it is a specification rather than a sentence. This is changes/add-content-pipeline/specs/source-collection/spec.md, abridged:
## Purpose
Finds items published at each configured source since the previous run,
and discards anything already collected, so that nothing is missed
because a person was busy and nothing is reviewed twice.
## ADDED Requirements
### Requirement: Configured sources are watched
The system SHALL check every configured source on a fixed schedule
without human initiation.
#### Scenario: A source publishes a new item
- **WHEN** a source has published an item since the last run
- **THEN** that item is collected
#### Scenario: A source is unreachable
- **WHEN** a source cannot be reached during a run
- **THEN** the run continues with the remaining sources
- **AND** the failure is recorded
### Requirement: Items already seen are discarded
The system SHALL discard an item that was collected in an earlier run,
identified by the canonical URL its publisher declares.
#### Scenario: The same item appears twice
- **WHEN** an item's canonical URL matches one collected before
- **THEN** it is not collected again
#### Scenario: Two sources syndicate one story
- **WHEN** two sources publish the same canonical URL
- **THEN** it is collected onceThree things about that file are worth pointing at.
Every scenario is something you could write a test from. "WHEN a source cannot be reached, THEN the run continues" is checkable without knowing anything about how it was built.
The ## Purpose section is there because this is a new capability. It seeds the Purpose of the main specification that archiving will create. On a delta modifying an existing capability it would be ignored, because that specification already has one.
Nothing in it names a technology. Not RSS, not Python, not a library. The decision to prefer feeds over page-fetching lives in design.md, where it can be argued with — not in the specification, where it would be mistaken for a requirement.
10.3 The article contract
The one artifact worth reading closely is the contract US2 depends on. The writing step returns a structured result rather than prose, and that structure is a genuine contract — the thing the writer must produce and the thing the filing step consumes:
{
"headline": { "type": "string", "maxLength": 120 },
"body": { "type": "string", "minLength": 1200 },
"tags": { "type": "array", "items": { "type": "string" },
"minItems": 1, "maxItems": 6 },
"sources": { "type": "array", "items": { "type": "string" },
"minItems": 1 }
}"sources" having minItems: 1 is the attribution rule expressed as something a machine can enforce. An article the writer cannot attribute fails the parser rather than reaching an editor. That is the difference between a policy and a control.
The corresponding requirement in specs/article-drafting/spec.md:
### Requirement: Every article carries its sources
The system SHALL record the source URLs each article was derived from.
An article the system cannot attribute MUST NOT be created.
#### Scenario: A claim cannot be attributed
- **WHEN** the writer cannot attribute a claim to a collected source
- **THEN** that claim is omitted from the article
#### Scenario: No sources at all
- **WHEN** the produced article has an empty sources list
- **THEN** it fails validation and no article is filed10.4 The design, and what it rejected
design.md is where technology finally enters, and where the coding agent has to defend it. The section that earns its keep is ## Decisions, because each one carries the alternatives it beat:
## Decisions
### Decision: Read published feeds in preference to fetching pages
A feed is a published, stable interface. Fetching and parsing a page is
more work to build, more fragile in operation, and more likely to draw
an objection from the source.
Alternatives considered: fetch every source as a page (rejected —
needless fragility where a feed exists); a third-party aggregation API
(rejected — an external dependency and a recurring cost for something a
well-established library already does).
### Decision: De-duplicate on the publisher's canonical URL
Stable across re-publication, and present for every item from both
feeds and fetched pages.
Alternatives considered: title (rejected — edited after publication);
feed GUID (rejected — not universal, and absent from fetched pages);
content hash (rejected — a one-character edit defeats it).
### Decision: A dedicated WordPress account with an application password
Revocable independently of any human user, scoped to one integration,
and accepted by the REST API without registering an OAuth application.
Alternatives considered: OAuth2 (rejected — more setup for no gain
here); an existing editor's account (rejected — attributes machine-
written drafts to a person, and widens what a leak reaches).
Risk noted: some WordPress security plugins disable application
passwords. To be confirmed against the client's installation.Note — the last line of that block is the kind of thing a design document exists to hold. It is not a decision, it is an unresolved risk with a named owner and a named check. It sits in the artifact where somebody will find it, rather than in a message somebody will not.
10.5 One requirement, traced
FIGURE W5 — "Why does that file exist?" Follow the chain back. Five links, and every one of them is a line in a file somebody can open, in your own repository.
The chain in that figure is the one an auditor walks, and nothing in it is generated, inferred, or reconstructed afterwards from commit messages. A user story in the proposal, a requirement in the specification, its scenario, the numbered task, and the module that satisfies it.
The requirement in the middle is the one this whole section keeps returning to — that nothing becomes publicly visible. It is not enforced by a setting or a code-review convention. It is a requirement with a scenario against it, which is why both the validator and the archive step refuse a change that would quietly remove it.
10.6 What the validator refuses
FIGURE W6 — What the validator checks, and what it simply refuses. This is the closest thing OpenSpec has to a coverage report — and unlike a report, a failure here stops the change.
Four errors fail regardless of flags, and each describes a way a specification can look complete while quietly meaning less than it appears to:
Requirement must contain SHALL or MUST keyword— and it must be in the requirement body, not only in the header. If SHALL appears only in the title, the message says so explicitly and tells you to move it to the line beneath.Requirement must have at least one scenario— usually because a scenario was written at three hashes rather than four, which fails silently: the scenario is simply not seen.MODIFIED omits scenario(s) the current spec still has— a MODIFIED block replaces the whole requirement, so archiving refuses to let it drop one. The error names the missing scenario.Change must have at least one delta— unless.openspec.yamlsetsskip_specs: true, and setting that marker while delta files exist is itself an error.
Five more become failures only under --strict: no SHALL or MUST at all, a ## Purpose under 50 characters, a ## Why over 1000, a requirement body over 500, and more than ten deltas in one change.
Avoid: treating a clean validation run as evidence the specification is right. It is evidence the specification is well-formed. A perfectly valid specification can describe the wrong system, and only a person reading it catches that — which is why the review in FIGURE 3 is a lane and not a checkbox.
How is this guide?
Last updated on
The Worked Example
One real automation, from a fuzzy idea to a change folder on disk — the publishing pipeline, its four settled questions, and the proposal that came out.
Applying, Archiving and Day Two
Why an agent's output can be trusted, the five checks that must all pass before anything runs on a schedule, what archiving does, and the change you will actually make six months later.