Byspec
Documentation

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.

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: byspec verify specs/failing.spec.md --provider mock
    expect_exit: 1

Fields: 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 criterion blocks is a valid spec with zero criteria; verify reports nothing to do and exits 0. lint warns.
  • Duplicate id values across blocks in one file are a SPEC_SCHEMA error.
  • source.file and source.line are 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 in verfiy must 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):

markdown
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.

PatternShapeDetection regex (case-insensitive, leading whitespace tolerated)
ubiquitousThe system shall X.^the .+ shall and none of the below
eventWhen T, the system shall X.^when .+, the .+ shall
stateWhile S, the system shall X.^while .+, the .+ shall
unwantedIf C, then the system shall X.^if .+, (then )?the .+ shall
optionalWhere 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.

RuleSeverityDeductionCondition
L01_IDerror20id missing, malformed, or duplicate
L02_EARS_PRESENTerror20ears missing or empty
L03_PATTERN_MATCHerror15detected pattern differs from declared pattern, or no pattern detected
L04_SHALLerror15ears does not contain the word "shall"
L05_VAGUEwarning5 per term, max 20ears contains a term from the vague list (Section 7.2)
L06_VERIFY_PRESENTwarning15verify is empty
L07_SINGLE_SHALLwarning10more than one "shall" (compound requirement; split it)
L08_MEASURABLEwarning10ears contains a comparative ("within", "less than", "at most", "at least", "no more than", "faster than") with no number adjacent
L09_JUDGE_ONLYinfo5verify contains only judge entries and no deterministic check
L10_RUBRIC_PRESENTerror15a 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:

json
{
  "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