Skip to content

Commit 126c5d6

Browse files
kitimarkclaudeclay-good
authored
fix(validate): report a Purpose left as the archive placeholder (#1671)
* docs(openspec): propose warn-on-purpose-placeholder When a delta introduces a capability with no usable `## Purpose`, archive writes `TBD - created by archiving change <name>. Update Purpose after archive.` into the new main spec. Three places already tell authors to replace it -- the `specs` instruction ("including a leftover `TBD` placeholder"), the sync-specs summary step ("so it gets written now rather than lingering"), and the cli-archive contract -- but nothing reports that it is still there. `--strict` cannot reach it. The check meant to catch a Purpose nobody wrote is a 50-character floor and the placeholder is 91 characters, so the one rule that exists to catch a thin Purpose is satisfied by the exact text meaning nobody wrote one: a Purpose reading "Does stuff." fails --strict today, while one saying nothing at all passes. Proposes reporting it as a warning against the spec's Purpose -- silent by default, failing under --strict, so a project already carrying placeholders keeps validating until it opts into the stricter gate. Detection is narrow: the generated sentence wherever it appears, and otherwise only a `TBD` opening the Purpose, so prose raising an open question is left alone. Planning artifacts only; no source changes. Refs #369 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(validate): report a Purpose left as the archive placeholder When a delta introduces a capability with no usable `## Purpose`, archive writes a placeholder into the new main spec. Nothing read it afterwards, so the capability kept a to-do in it while every command reported success. `--strict` could not reach it. The check that exists to catch a Purpose nobody wrote is a 50-character floor, and the placeholder clears it: a spec whose Purpose read "Does stuff." failed --strict, while a spec whose Purpose said nothing at all passed. #369 reported agents leaving the placeholder behind and stayed open seven months; every remedy since has been an instruction, which is the mechanism that report described as unreliable. validate now reports it as a warning against the Purpose, naming the line and saying to edit the main spec directly -- a delta's `## Purpose` is read only when the capability is created, so it cannot replace an existing one. Warning rather than error, because strict mode already means "warnings fail": a project carrying placeholders keeps validating by default and only --strict fails. Archive is untouched -- it validates rebuilt specs without --strict, so a spec archive writes still passes the validation it would have passed before, and the text archive writes is byte-identical. The placeholder is recognised through the same constants the writer composes it from, so the check cannot drift from the sentence it looks for -- the failure mode of a second, hand-copied spelling being a check that matches nothing and looks exactly like a check that found nothing. The one case that cannot be a lookup is an agent-written placeholder, kept to a `TBD` opening the Purpose: "the retry budget is TBD pending benchmarks" is authored prose and is left alone. Verified: 209 archive tests pass unchanged (the placeholder text is asserted literally, so the output is provably identical); full suite 138 files / 3993 tests; 36/36 strict spec validations; build, lint and typecheck clean. Against a project carrying four real placeholders, default mode still exits 0 and --strict fails exactly those four. Cross-platform CI is not yet confirmed -- it needs a pushed branch. Line endings are covered by tests asserting a CRLF spec and an LF spec produce identical findings, and the module does no path handling. Refs #369 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(validate): make every placeholder guard load-bearing A mutation pass over the seven guards -- revert one, see which tests die -- found two that no test held. The prefix/suffix test did not exercise the guard it named. Its Purpose read "Explains what happens when archiving change my-change runs twice", which contains neither half of the generated sentence, so it passed whether or not the suffix was required. Matching on the prefix alone killed nothing. The Purpose now embeds the real prefix constant and asserts the suffix is absent, so the case is the one the name claims; the mutation kills it. The empty-Purpose early return was genuinely dead. Neither rule matches empty text, so removing the branch changed no behaviour and failed no test. Rather than keep a guard nothing can hold, the branch is gone and the comment says why an empty Purpose still yields null. The tests asserting that behaviour are unchanged and still pass. Every guard now dies under mutation: whole check removed from applySpecRules ......... 6 tests brevity no longer suppressed (else -> if) ....... 1 word boundary dropped from the TBD marker ....... 1 generated placeholder matched on prefix alone ... 1 line-ending normalisation removed ............... 2 section-boundary guard removed from locator ..... 1 Full suite 138 files / 3993 tests, lint and typecheck clean. Refs #1670 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(openspec): record the mutation pass in the task list The mutation work changed the implementation -- a test rewritten and a dead branch removed -- but no task covered it, so the plan claimed less work than was done. Added as group 6, marked complete, with why it was not planned. 5.4 now says what blocks it. It needs a pushed branch for the cross-platform matrix, and the note records that line endings are covered locally by tests asserting a CRLF spec and an LF spec produce identical findings, so a reader can tell the difference between unverified and unverifiable-from-here. The specs, proposal and design are unchanged and were checked: the delta's empty-Purpose clause constrains behaviour, not structure, and that behaviour is the same -- the redundant branch went, the rule did not. 26 of 27 tasks complete; the change still validates --strict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(openspec): close 5.4 on a green cross-platform matrix CI dispatched on the fork against this branch: lint & typecheck, and the test suite on linux-bash, macos-bash and windows-pwsh -- all green. The Windows job installed, built and ran the suite rather than short-circuiting, which is the part 5.4 existed to check, since the placeholder locator counts lines in files that may carry either ending. Recorded as a workflow_dispatch run on the fork, not the upstream pull-request run, because those are not the same gate and the note should not let a reader assume otherwise. Nix Flake Validation and Validate Release Tracking skipped: this branch touches neither the flake nor release tracking. 27 of 27 tasks complete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(validate): name the placeholder's line, not the prose above it The warning tells you which line to fix, and named the wrong one when the generated sentence did not open the Purpose: 3 ## Purpose 4 Handles widget retries. <- warning pointed here 5 6 TBD - created by archiving ... <- placeholder is here The locator asked "what is the first non-blank line after ## Purpose?" rather than "where is the placeholder?". Those are the same line in five of the six shapes a placeholder can take -- a leading TBD marker is the first non-blank line by definition, and archive writes the generated sentence as the section's only content -- so the two questions only diverge when a human types prose above a leftover placeholder. Pointing at that prose is worse than pointing nowhere: the reader sees a sentence that is plainly fine and concludes the check is broken. design.md already said a wrong line number is worse than none, and the delta already required naming the line the placeholder is on, so this is the implementation meeting a contract that was already written, not a change of contract. The locator is now told which rule matched. A leading marker keeps the first-non-blank behaviour, because that is where it sits; the generated sentence is located by its own text. When both match the leading marker wins, being the earlier of the two. Found by CodeRabbit on #1671. The finding was real despite its own "Addressed" marker, which only tracked the file changing in a later commit. Two test gaps let it through. The case that covered this input asserted only that something was reported, never which line -- so it now asserts the line, and a table pins every position a placeholder can occupy, each case first checking that the line it expects really carries the placeholder. The mutation pass could not have caught it either: mutation proves a test dies when a guard is broken, and cannot invent an assertion nobody wrote. Reverting the branch fails exactly the three new expectations. Full suite 4000 tests / 138 files, lint and typecheck clean. Refs #1670 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(validate): set the placeholder line unconditionally ValidationIssue.line is optional and the project does not enable exactOptionalPropertyTypes, so a plain assignment typechecks and JSON output is unchanged - JSON.stringify drops undefined values. findPurposePlaceholderIssue already returns the key unconditionally, and the neighbouring push sites assign line plainly, so the conditional spread was the odd one out. * fix(validate): widen the placeholder check to TODO and read fences as quoted #1670 left two questions open. Both are answered here, against how OpenSpec already reads a spec. A `TODO` opening the Purpose now reports as the same finding as a `TBD`. Nothing OpenSpec writes produces one, but the marker an author leaves behind is whichever word they reached for, and a Purpose reading `TODO: fill this in` is as unwritten as one reading `TBD`. Only the opening position counts, as before, so `TODOs are tracked in the linked issue` is still authored prose. Fenced code inside a Purpose is now read as quoted material rather than as the Purpose speaking, through the `buildCodeFenceMask` the requirement and structure parsers already share. Without it a spec documenting the sentence archive writes is reported as carrying it, which is the check failing the one document that explains it - and a warning that fires on the docs teaches people to ignore the warning. Fenced lines are skipped when locating the placeholder too, so a `## Purpose` or `## Requirements` quoted in a fence can neither be mistaken for the section header nor end the section early. The message now names both what archive writes and a marker left in its place, since one message covers both. Severity is unchanged: still a warning, so a project carrying placeholders keeps validating and only --strict fails. Every new guard is mutation-checked: dropping `TODO` kills 3 tests, unmasking detection kills 2, unmasking the line locator kills 3, unmasking the header search kills 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): read the marker boundary in any script, not just ASCII Review found `\b` reading `TODOé` and `TBD١` as a marker followed by punctuation, because `\b` only knows ASCII word characters. A Purpose is prose and prose is not always Latin script, so the rule that a longer word beginning with those letters is not a marker has to hold in any script. The lookahead rejects letters, digits, combining marks and `_`, and nothing else, so `TODO:`, `TBD -` and `TODO(owner):` are still the marker they look like. Held in both directions: loosening it back to `\b` kills 1 test, tightening it to reject punctuation kills 4. Also reworded a task line that opened with `#1670`, which markdownlint reads as a heading missing its space. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(validate): locate the matched purpose placeholder * docs(validate): remove trailing task whitespace --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Clay Good <hi@claygood.com>
1 parent a7353ae commit 126c5d6

12 files changed

Lines changed: 1185 additions & 3 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@fission-ai/openspec": patch
3+
---
4+
5+
`openspec validate` now reports a `## Purpose` that is still the placeholder archive writes for a new capability, instead of passing it. The placeholder is longer than the 50-character brevity floor, so until now the one check meant to catch a Purpose nobody wrote was satisfied by the exact text saying nobody wrote one — a spec whose Purpose read `Does stuff.` failed `--strict` while a spec whose Purpose said nothing at all passed. A capability could carry the placeholder indefinitely while every command reported success.
6+
7+
It is a warning, so a project that already has placeholders on disk keeps validating by default and only `--strict` fails. The message says to edit the main spec directly, since a `## Purpose` in a delta is read only when the capability is created and cannot replace an existing one.
8+
9+
Detection is narrow. The placeholder archive generates is recognised through the same definition that writes it, wherever it appears in the Purpose. Otherwise only a `TBD` or `TODO` opening the Purpose counts, so `The retry budget is TBD pending benchmarks` is still a valid Purpose and a word like `TBDs` is not a marker. Fenced code inside the Purpose is quoted material rather than the Purpose speaking, so a spec that documents the placeholder keeps passing. An empty Purpose is unchanged, and a Purpose reported as a placeholder is no longer also reported as too brief, so a bare `TBD` yields one finding rather than two.
10+
11+
`openspec archive` is unaffected: it validates rebuilt specs without `--strict`, so a spec archive writes still passes the validation it would have passed before, and the text archive writes is unchanged.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-08-15
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
## Context
2+
3+
See proposal.md — Why. What shapes the approach here is where the two ends
4+
already sit:
5+
6+
- `buildSpecSkeleton` composes the placeholder inline, interpolating the change
7+
name. It is generated text with no name of its own.
8+
- `applySpecRules` is the single place both spec entry points converge —
9+
`validateSpec` (a file) and `validateSpecContent` (a rebuilt spec, called by
10+
archive). A rule added there reaches the CLI and archive at once, so the
11+
blast radius on archive has to be answered rather than assumed.
12+
- Strict mode is already defined as "warnings fail": `createReport` treats a
13+
warning as invalid only when `strictMode` is set. Severity is therefore a
14+
choice between two existing behaviors, not a new mechanism.
15+
- `task-numbering.ts` establishes the shape for a check like this: a pure module
16+
under `src/core/validation/` returning findings, mapped to issues at one call
17+
site in the validator.
18+
19+
## Goals / Non-Goals
20+
21+
**Goals:**
22+
23+
- Report the placeholder without changing what any command does today by default.
24+
- Recognise placeholders already on disk, including ones written by earlier
25+
versions, since those are the ones that have lingered longest.
26+
- Keep the rule quiet on authored prose, so the warning stays worth reading.
27+
28+
**Non-Goals:**
29+
30+
- Changing what archive writes. The placeholder is a useful marker at the moment
31+
it is written; this change is about reporting it afterwards.
32+
- Reporting a `## Purpose` in a delta spec. Delta Purposes are only read when a
33+
capability is created, and archive already warns when it ignores one.
34+
- Filling the Purpose in automatically. Only the author knows what the capability
35+
is for.
36+
37+
## Decisions
38+
39+
### Severity is a warning, not an error
40+
41+
Strict mode already means "warnings are failures", so a warning gives both
42+
behaviors from one severity: silent by default, failing under `--strict`.
43+
44+
*Alternative — error:* every project with a placeholder on disk starts failing
45+
`openspec validate` on upgrade. On the evidence that these linger for months,
46+
that is a large and involuntary blast radius for a documentation defect.
47+
48+
*Alternative — a dedicated opt-in flag:* adds a surface to learn and to document,
49+
and duplicates what `--strict` is for. Rejected as a second mechanism for an
50+
existing one.
51+
52+
### The check lives in validation, not in archive
53+
54+
Placed as a pure module beside `task-numbering.ts` and called from
55+
`applySpecRules`, so it applies to every path that validates a main spec.
56+
57+
*Alternative — report at archive time, when the placeholder is written:* archive
58+
already prints at that moment, and a line in a terminal is exactly what did not
59+
survive. The defect is what persists on disk, so the check belongs where disk
60+
state is inspected, and it must keep working for a spec archived a year ago by a
61+
version that no longer runs.
62+
63+
### The generated sentence is recognised through a shared constant
64+
65+
The placeholder is text this tool generates, so it gets a name: the template
66+
moves into a constant that `buildSpecSkeleton` composes from and the check
67+
recognises through. Detection is then anchored to the thing itself rather than to
68+
a second, hand-copied spelling of it that can drift from the writer.
69+
70+
The change name is interpolated, so recognition matches the constant's fixed
71+
segments around it rather than the whole string.
72+
73+
*Alternative — spell the sentence out in the detector:* two independent copies of
74+
one string, and the check silently stops matching the day the writer is reworded
75+
— the failure mode being a check that reports nothing and looks healthy.
76+
77+
### A second, narrow marker rule covers what the constant cannot
78+
79+
A placeholder is not always the generated one. The `specs` instruction tells
80+
agents to write "a brief TBD placeholder" when a delta has none, and an agent
81+
writes its own wording. So a `TBD` **opening** the Purpose is also reported.
82+
83+
The rule is deliberately positional rather than a search: a Purpose that opens
84+
with `TBD` is announcing it was not written, while "the retry budget is TBD
85+
pending benchmarks" is a real Purpose with an open question in it. Reporting the
86+
second would train people to ignore the warning, which costs more than the
87+
findings it would add. A word that merely starts with those letters (`TBDs`) is
88+
excluded for the same reason.
89+
90+
This is the one place the change cannot use an explicit lookup — the text is
91+
written by agents and authors, not generated here, so there is no list to consult.
92+
It is kept to a single anchored marker at a known position precisely to stay as
93+
close to a lookup as the input allows.
94+
95+
It also covers a case the constant match cannot. A markdown formatter that
96+
rewraps the generated sentence across two lines breaks the constant lookup, and
97+
the marker rule still catches it, because every spelling of the placeholder opens
98+
with `TBD`. So the fallback is not only for agent-written placeholders — it is
99+
what keeps detection working when the generated one is reformatted.
100+
101+
### The placeholder finding replaces the brevity finding
102+
103+
A bare `TBD` is both a placeholder and under the length floor. Reporting both puts
104+
two findings on one line where only one is actionable: "you left the placeholder
105+
in" tells the author what to do, "your Purpose is under 50 characters" does not.
106+
The placeholder check therefore runs first and the brevity check runs only when it
107+
does not fire.
108+
109+
### Locating the line follows the rule that matched
110+
111+
The warning names the line carrying the placeholder, and which line that is
112+
depends on which rule fired. A leading `TBD` is the section's first non-blank line
113+
by definition. The generated sentence is not: it can sit below prose somebody
114+
wrote, so it is located by its own text.
115+
116+
Naming the first non-blank line in that second case points at the authored prose —
117+
a line the reader can see is fine, which reads as the check being wrong rather
118+
than the Purpose being unwritten. When both rules match the leading marker wins,
119+
because it is the earlier of the two.
120+
121+
When the placeholder cannot be located — no section header, or a generated
122+
sentence no single line carries — the finding is reported without a line rather
123+
than with a guessed one, since a wrong line number is worse than none.
124+
125+
Line endings are normalised before counting, so a spec saved on Windows reports
126+
the same line number as the same spec saved on macOS or Linux.
127+
128+
## Risks / Trade-offs
129+
130+
- **A project running `--strict` in CI starts failing on upgrade** → that is the
131+
intended effect and the reason severity is not an error: the failure is opt-in,
132+
arrives only where a stricter gate was already requested, and is fixed by
133+
writing one sentence. The message names the file to edit.
134+
135+
- **A legitimate Purpose that opens with "TBD" is reported** → accepted. A Purpose
136+
whose first word is `TBD` is stating it was not written; reporting it is the
137+
feature, not a false positive.
138+
139+
- **The marker rule is a positional match on authored prose, against the project's
140+
preference for explicit lookups** → confined to the one case where no list can
141+
exist, and anchored at a single position so its behavior is enumerable. The
142+
generated sentence, which *can* be looked up, is looked up.
143+
144+
- **Wording of the generated placeholder changes later and old specs stop being
145+
recognised by the constant** → the marker rule still catches them, since every
146+
spelling used so far opens with `TBD`.
147+
148+
- **Archive behavior changes unintentionally** → archive constructs its validators
149+
without strict mode, so a warning cannot flip a rebuilt spec to invalid. Covered
150+
by a test asserting the exact call archive makes.
151+
152+
## Migration Plan
153+
154+
None. No data, config, or spec files change. A project sees the new warning the
155+
first time it validates after upgrading, and fixes it by writing the Purpose in
156+
the main spec.
157+
158+
## Open Questions
159+
160+
- Should a `TODO` marker be treated the same as `TBD`? No tool or instruction
161+
produces one today, so it is left out; adding it later is a one-line widening
162+
that changes no scenario already written here.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
## Why
2+
3+
When a delta introduces a capability without a usable `## Purpose`, archive writes
4+
`TBD - created by archiving change <name>. Update Purpose after archive.` into the
5+
new main spec. Three places already tell authors to replace it — the `specs`
6+
instruction ("including a leftover `TBD` placeholder — edit the main spec
7+
directly"), the sync-specs summary step ("so it gets written now rather than
8+
lingering"), and the archive contract itself — but nothing reports that it is
9+
still there.
10+
11+
`--strict` cannot reach it. The check meant to catch a Purpose nobody wrote is a
12+
50-character floor, and the placeholder is 91 characters, so the one rule that
13+
exists to catch a thin Purpose is satisfied by the exact text meaning "nobody
14+
wrote one". A spec whose Purpose reads `Does stuff.` fails `--strict` today; a
15+
spec whose Purpose says nothing at all passes.
16+
17+
The result is a capability that carries a to-do indefinitely while every command
18+
reports success, and a silent pass is indistinguishable from a clean run.
19+
[#369](https://github.com/Fission-AI/OpenSpec/issues/369) reported agents leaving
20+
the placeholder behind and stayed open for seven months; the remedies since have
21+
been instructions, which is the mechanism that report described as unreliable.
22+
23+
## What Changes
24+
25+
- `openspec validate` reports a `## Purpose` that is still the archive
26+
placeholder, as a warning on the spec's Purpose, naming the line to replace.
27+
- The message says to edit the main spec directly, because a `## Purpose` in a
28+
delta is read only when a capability is created and cannot replace an existing
29+
one.
30+
- Detection stays narrow: the sentence archive itself writes counts wherever it
31+
appears in the Purpose, and otherwise only a `TBD` or `TODO` opening the
32+
Purpose counts. A marker inside a sentence is authored prose and is left alone,
33+
and so is anything inside a fenced code block, which is a Purpose quoting the
34+
placeholder rather than carrying it.
35+
- A Purpose reported as a placeholder is no longer also reported as too brief, so
36+
a bare `TBD` yields one finding rather than two.
37+
- Not breaking: the finding is a warning, so a project that already carries
38+
placeholders keeps validating by default and only `--strict` fails. `openspec
39+
archive` is unaffected — it validates rebuilt specs without `--strict`, so a
40+
spec archive writes still passes the validation it would have passed before.
41+
42+
## Capabilities
43+
44+
### New Capabilities
45+
46+
None.
47+
48+
### Modified Capabilities
49+
50+
- `cli-validate`: adds a requirement that spec validation report a Purpose left as
51+
the archive placeholder, with the severity, detection boundary, and precedence
52+
over the existing brevity warning stated as contract.
53+
54+
## Impact
55+
56+
- **Affected behavior**: `openspec validate` on main specs — `validate <spec>`,
57+
`validate --specs`, and the bulk/interactive paths that share it. A project
58+
carrying a placeholder sees a new warning; under `--strict` that project now
59+
fails until the Purpose is written.
60+
- **Unaffected**: `openspec archive`, which validates rebuilt specs non-strictly;
61+
delta spec validation, which does not read a main spec's Purpose; and any spec
62+
whose Purpose is authored prose.
63+
- **Docs**: none required — the message carries its own remediation, and the
64+
`specs` instruction already tells authors to edit the main spec directly.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Spec validation SHALL report a Purpose left as the archive placeholder
4+
5+
The `validate` command SHALL report, as a warning against the spec's Purpose, a
6+
`## Purpose` that is still a placeholder rather than a Purpose someone wrote:
7+
the sentence `openspec archive` writes for a new capability, or a marker left in
8+
its place. The report SHALL name the line the placeholder is on when it can be
9+
located, and SHALL omit the line rather than point at the wrong text when it
10+
cannot.
11+
12+
The remediation SHALL say to edit the main spec directly, because a `## Purpose`
13+
in a delta is read only when a capability is created and therefore cannot replace
14+
one that already exists.
15+
16+
The finding SHALL be a warning. A project that already carries placeholders
17+
therefore keeps validating by default, and only `--strict` fails — the
18+
placeholder is worth keeping at the moment archive writes it, and worth reporting
19+
once it has outlived that moment.
20+
21+
Detection SHALL be narrow, because a Purpose is prose and prose that raises an
22+
open question is not a placeholder:
23+
24+
- the sentence archive itself writes SHALL be reported wherever it appears in the
25+
Purpose, since nobody writes it by accident;
26+
- otherwise only a `TBD` or `TODO` marker opening the Purpose SHALL be reported.
27+
The two words SHALL be read the same way, because which one got typed says
28+
nothing about whether the Purpose was written;
29+
- a marker appearing inside a sentence SHALL NOT be reported;
30+
- a longer word that merely begins with those letters SHALL NOT be reported,
31+
in any script.
32+
33+
Text inside a fenced code block SHALL NOT be read as the Purpose speaking, for
34+
either rule. A Purpose that quotes the placeholder is documenting it rather than
35+
carrying it, and a check that fails the document explaining the placeholder
36+
teaches its readers to ignore the warning.
37+
38+
An empty Purpose SHALL NOT be reported by this requirement, which the
39+
empty-Purpose error already covers. A Purpose reported as a placeholder SHALL NOT
40+
also be reported as too brief, so a bare `TBD` yields one finding and not two.
41+
42+
Validation performed inside `openspec archive` SHALL be unaffected, because
43+
archive validates a rebuilt spec without `--strict` and a warning does not change
44+
that verdict: a spec archive writes SHALL still pass the validation it would have
45+
passed before this requirement existed.
46+
47+
#### Scenario: The placeholder passes by default and fails under strict
48+
49+
- **GIVEN** a main spec whose Purpose is the placeholder archive wrote
50+
- **WHEN** `openspec validate --specs` runs
51+
- **THEN** report a warning against the Purpose, naming the line it is on and
52+
saying to edit the main spec directly
53+
- **AND** the spec is reported valid
54+
55+
#### Scenario: Strict validation fails on the placeholder
56+
57+
- **GIVEN** the same main spec
58+
- **WHEN** `openspec validate --specs --strict` runs
59+
- **THEN** the spec is reported invalid
60+
61+
#### Scenario: An authored Purpose raising an open question is not reported
62+
63+
- **GIVEN** a Purpose reading "Bounds how often a failed delivery is retried. The exact budget is TBD pending load tests."
64+
- **WHEN** `openspec validate --specs --strict` runs
65+
- **THEN** report no placeholder warning, because the marker does not open the Purpose
66+
- **AND** the spec is reported valid
67+
68+
#### Scenario: A Purpose left as a TODO is reported like a TBD
69+
70+
- **GIVEN** a Purpose consisting only of "TODO"
71+
- **WHEN** `openspec validate --specs --strict` runs
72+
- **THEN** report the placeholder warning, the same finding a bare "TBD" reports
73+
74+
#### Scenario: A Purpose quoting the placeholder inside a fence is not reported
75+
76+
- **GIVEN** a Purpose that explains the placeholder and shows it inside a fenced
77+
code block
78+
- **WHEN** `openspec validate --specs --strict` runs
79+
- **THEN** report no placeholder warning
80+
- **AND** the spec is reported valid
81+
82+
#### Scenario: A word beginning with the marker is not reported
83+
84+
- **GIVEN** a Purpose opening "TBDs raised during design review are tracked in the linked issue.", or the same sentence opening with "TODOs"
85+
- **WHEN** `openspec validate --specs --strict` runs
86+
- **THEN** report no placeholder warning
87+
88+
#### Scenario: A bare TBD is reported once
89+
90+
- **GIVEN** a Purpose consisting only of "TBD"
91+
- **WHEN** `openspec validate --specs --strict` runs
92+
- **THEN** report exactly one finding against the Purpose, the placeholder warning
93+
rather than the too-brief warning
94+
95+
#### Scenario: A terse but authored Purpose still reports as too brief
96+
97+
- **GIVEN** a Purpose reading "Does stuff."
98+
- **WHEN** `openspec validate --specs --strict` runs
99+
- **THEN** report the too-brief warning and no placeholder warning
100+
101+
#### Scenario: Archive still writes the spec it would have written
102+
103+
- **GIVEN** a change whose delta introduces a capability with no usable `## Purpose`
104+
- **WHEN** `openspec archive` validates the rebuilt spec before writing it
105+
- **THEN** that spec is reported valid and archive completes exactly as before
106+
107+
#### Scenario: Line endings do not change what is reported
108+
109+
- **GIVEN** two main specs with the same placeholder Purpose, one saved with LF
110+
line endings and one with CRLF
111+
- **WHEN** `openspec validate --specs` runs on each
112+
- **THEN** both report the same warning against the same line number

0 commit comments

Comments
 (0)