Skip to content

Spec Template

Rafael Queiroz edited this page Aug 12, 2026 · 1 revision

SPEC-NNN — Spec title

Status Draft
PRD source Fx (FR-x.1–x.n)
Depends on SPEC-NNN, SPEC-NNN
Required by SPEC-NNN
Milestone Mn

Description

Business rules

ID Rule
BR-NNN-01
BR-NNN-02

Acceptance Criteria

  • [ ]
  • [ ]

Out of Scope

Decision Logs

# Decision Alternatives considered Rationale
DL-NNN-01

Conventions for this project

Not part of the template — delete this section when copying. It records how the existing sixteen specs are written so new ones stay consistent with them.

Numbering

Prefix Meaning Scope
SPEC-NNN Spec, zero-padded to three digits Project
BR-NNN-nn Business rule; NNN matches the spec Spec
DL-NNN-nn Decision; NNN matches the spec Spec
FR-x.y PRD functional/non-functional requirement PRD
CR-n PRD calculation rule PRD §6
DM-n PRD data-model constraint PRD §7.3

IDs are stable identifiers, not a reading order. A rule added during review takes the next free number rather than triggering a renumber, so references from other specs stay valid.

Header table

Status, PRD source and Milestone are required. Depends on and Required by are included where they exist — they are what generates the build order in README.md, so keep them accurate when adding a spec.

Cite the PRD by section link plus the requirement range it covers. Coverage is verified mechanically against these ranges (see below), so an inaccurate header shows up as a false gap or a missed one.

Traceability

Every PRD requirement must be covered by at least one spec. This is checkable rather than assumed — the check that found the missing platform-qualities spec was:

python3 - <<'EOF'
import re, glob
covered = set()
for f in glob.glob('docs/specs/SPEC-*.md'):
    txt = open(f, encoding='utf-8').read()
    for line in txt.split('\n'):
        if '**PRD source**' in line:
            for m in re.finditer(r'FR-(\d+)\.(\d+)[–-](?:\d+\.)?(\d+)', line):
                for n in range(int(m.group(2)), int(m.group(3)) + 1):
                    covered.add(f'FR-{m.group(1)}.{n}')
    covered.update(re.findall(r'FR-\d+\.\d+', txt))
prd = open('docs/PRD.md', encoding='utf-8').read()
allfr = set(re.findall(r'^\| (FR-\d+\.\d+)', prd, re.M)) | set(re.findall(r'\*\*(FR-\d+\.\d+)\*\*', prd))
missing = sorted(allfr - covered, key=lambda x: [int(p) for p in x[3:].split('.')])
print(f'PRD requirements: {len(allfr)}  covered: {len(allfr & covered)}  UNCOVERED: {len(missing)}')
print(missing)
EOF

Run it from the repo root after adding or editing a spec. A non-empty UNCOVERED list means either a spec header needs the requirement added, or a spec is missing.

Template compliance is worth checking too — all five sections, spelled exactly:

for f in docs/specs/SPEC-*.md; do n=$(grep -cE '^# (Description|Business rules|Acceptance Criteria|Out of Scope|Decision Logs)$' "$f"); [ "$n" -eq 5 ] || echo "FAIL $f ($n/5)"; done

When adding a spec

  1. Copy this file to SPEC-NNN-kebab-case-title.md, delete this Conventions section.
  2. Fill the header, including Depends on — dependencies drive build order.
  3. Add it to the table and the build-order diagram in README.md.
  4. Run both checks above.
  5. If it introduces an invariant asserted in more than one spec, add it to the cross-spec invariants list in README.md.

Things the existing specs do that are worth copying

  • State the constraint that shaped the design, up front in Description. SPEC-005 opens with B3's APIs being B2B-only because nothing in that spec makes sense otherwise.
  • Record what breaks under the alternative. "Chose average cost" is not a decision log; "FIFO would produce figures contradicting the user's own tax return" is.
  • Name the asymmetries. Where a rule deliberately differs from a neighbouring one — SPEC-010's sales reducing allocations proportionally while purchases wait for a decision — say why in the rule itself, so it doesn't read as an oversight.
  • Make cross-report and cross-spec agreement an acceptance criterion, not an assumption. Most of the invariants in README.md exist because a total reconciling between two features is the cheapest way to catch an aggregation bug.

Clone this wiki locally