The spec format
A Byspec spec is any Markdown file. The parser extracts every fenced code block whose language tag is criterion and ignores everything else, so a PRD, a design document, or a ticket can carry its acceptance criteria next to the prose that motivates them.
id: EXM-001
title: Nonzero exit on any FAIL
ears: When any criterion in scope has verdict FAIL, the CLI shall exit with code 1.
pattern: event
priority: must
verify:
- type: command
run: byspec verify specs/failing.spec.md --provider mock
expect_exit: 1Fields: id (^[A-Z]{2,5}-\d{3}$, unique across the spec set), title, ears (the requirement sentence), pattern (one of the five EARS patterns below), priority (must, should, may), an optional scope list of globs used by --changed, and verify, an ordered list of checks. Unknown keys are an error by design: a typo like verfiy must not silently turn a criterion into an unverifiable one.
Rules of the format#
An BSF file is any Markdown document. The parser walks the mdast tree and extracts every fenced code block whose language tag is exactly criterion. The body of each block is YAML conforming to the Criterion schema. All other Markdown is ignored, which means product specs, PRDs, and this document can carry criteria inline next to the prose that motivates them.
Rules:
- A file with zero
criterionblocks is a valid spec with zero criteria;verifyreports nothing to do and exits 0.lintwarns. - Duplicate
idvalues across blocks in one file are aSPEC_SCHEMAerror. source.fileandsource.lineare populated by the parser from the mdast position; if present in the YAML they are overwritten.- Unknown top-level keys in a criterion are a schema error (zod
.strict()). This is deliberate; typos inverfiymust not silently become unverifiable criteria. - Multiple spec files MAY be passed to
verify; criteria IDs must be unique across the set.
Example (shown inside a four-backtick fence so this illustration is a single markdown code node and the parser does not extract it; only top-level criterion code nodes count):
The CLI must be safe to run in CI.
```criterion
id: EXM-001
title: Nonzero exit on any FAIL
ears: When any criterion in scope has verdict FAIL, the CLI shall exit with code 1.
pattern: event
priority: must
verify:
- type: command
run: pnpm byspec verify fixtures/specs/one-failing.spec.md --repo fixtures/repos/failing-repo --provider mock
expect_exit: 1
```The parser lives in packages/core/src/spec/parse.ts and exposes parseSpecFile({ path, root }) and parseSpecString({ content, sourceFile }).
Source: docs/byspec-implementation-spec.md § 6. The Byspec Spec Format (BSF)
EARS patterns and the clarity gate#
EARS (Easy Approach to Requirements Syntax) has five patterns. Byspec uses them because they are testable by construction and because a criterion that cannot be fitted into one of them is usually two criteria or an opinion.
| Pattern | Shape | Detection regex (case-insensitive, leading whitespace tolerated) |
|---|---|---|
ubiquitous | The system shall X. | ^the .+ shall and none of the below |
event | When T, the system shall X. | ^when .+, the .+ shall |
state | While S, the system shall X. | ^while .+, the .+ shall |
unwanted | If C, then the system shall X. | ^if .+, (then )?the .+ shall |
optional | Where F, the system shall X. | ^where .+, the .+ shall |
Lint rules#
byspec lint evaluates each criterion against the rules below and produces a per-criterion score and a document score (mean of criterion scores, floored). Each rule has a severity and a deduction.
| Rule | Severity | Deduction | Condition |
|---|---|---|---|
L01_ID | error | 20 | id missing, malformed, or duplicate |
L02_EARS_PRESENT | error | 20 | ears missing or empty |
L03_PATTERN_MATCH | error | 15 | detected pattern differs from declared pattern, or no pattern detected |
L04_SHALL | error | 15 | ears does not contain the word "shall" |
L05_VAGUE | warning | 5 per term, max 20 | ears contains a term from the vague list (Section 7.2) |
L06_VERIFY_PRESENT | warning | 15 | verify is empty |
L07_SINGLE_SHALL | warning | 10 | more than one "shall" (compound requirement; split it) |
L08_MEASURABLE | warning | 10 | ears contains a comparative ("within", "less than", "at most", "at least", "no more than", "faster than") with no number adjacent |
L09_JUDGE_ONLY | info | 5 | verify contains only judge entries and no deterministic check |
L10_RUBRIC_PRESENT | error | 15 | a judge entry has no rubric |
Score = max(0, 100 minus deductions). lint --min-score N exits 1 when the document score is below N (default from config, 80). Errors always exit 1 regardless of score.
Vague terms#
Default list, overridable in config: fast, quickly, quick, easy, easily, simple, user-friendly, intuitive, appropriate, appropriately, reasonable, reasonably, robust, efficient, efficiently, seamless, seamlessly, as needed, when necessary, etc, and so on, properly, correctly, good, better, best, optimal, adequate, sufficient, minimal, various, some, many, several. Matching is whole-word, case-insensitive. "correctly" is on the list on purpose: "the system shall correctly parse" is not a requirement.
Rewrite (--rewrite)#
When --rewrite is passed, for every criterion with at least one error or warning, the linter asks the provider for a rewrite. The prompt (Section 14.4) contains the original criterion, the rule violations, the EARS table, and instructions to (a) rewrite into exactly one EARS pattern, (b) split compound criteria into multiple criteria with suffixed IDs (ABC-001a, ABC-001b), (c) replace vague terms with measurable ones or mark them [NEEDS NUMBER], and (d) propose at least one deterministic check where obvious. Output is written to a sibling file whose trailing .md is replaced with .rewrite.md (vague.spec.md becomes vague.spec.rewrite.md), containing only criterion blocks and a one-line note per block explaining the change. The original file is never modified. Suffixed IDs like ABC-001a are valid only in rewrite files; the human renumbers before adopting them, and the parser rejects them elsewhere via L01_ID.
Lint output#
--format json prints one document:
{
"document_score": 74,
"exit_code": 2,
"totals": { "errors": 1, "warnings": 3, "info": 0 },
"criteria": [
{
"id": "ABC-001",
"score": 65,
"violations": [
{ "rule": "L05_VAGUE", "severity": "warning", "message": "vague term: quickly" }
]
}
]
}Text output prints one line per violation (ABC-001 warning L05_VAGUE vague term: quickly) and a closing score line.
Source: docs/byspec-implementation-spec.md § 7. EARS and the clarity gate