Resources
Writing Requirements — A Styleguide
How to write a requirements document that stays true. It covers the file layout in a repo, how one requirement is identified and worded, and the rules that keep a spec from describing something that no longer exists.
This is written for a repository you own, in any language and any stack. It assumes requirements are the artefact you hand to whoever builds the thing — a colleague, a contractor, or an agent. It does not assume a team, a tracker, or a tool.
1. What a requirements document is
A requirements document answers one question: what must be true when this is done?
Not how it is built (that is the plan), not what was built (that is the changelog).
A requirement is, normatively, "a capability or property that a system shall have". That sentence is the capability-only rule: a technology is not a capability, and a structure is not a property.
Three properties matter, in this order:
- Addressable. Every requirement has an ID, so a plan, a pull request, a test and a retro all point at the same thing. Nobody has to quote prose that may have moved.
- Verifiable. Every requirement states how you would know it is satisfied — by a test, a measurement, or an inspection. "Fast" is not a requirement. "First token within 10 s on a quantized 7B model" is. "Nothing leaves the machine" is also, and it is checked by inspection, not by a test.
- Alive. A requirement that changes silently is worse than no requirement. Change goes through the file and the changelog, in the same commit.
A spec is not a wish list and not a design document. If a sentence would be equally at home in an architecture decision record, it belongs in the architecture decision record.
1.1 Where the defects actually are
This is the part most style guides get wrong, so it comes before the rules.
Requirements defects have a measured distribution:
| Error class | Share of defects |
|---|---|
| Incorrect fact | 49% |
| Omission (a requirement never written) | 31% |
| Inconsistency | 13% |
| Ambiguity | 5% |
| Misplaced requirement | 2% |
(Hooks & Farry, reproduced in Ralph R. Young, The Requirements Engineering Handbook, 2004, table 7.3.)
Eighty per cent of requirements defects are wrong facts or missing requirements. Neither is fixed by better wording. The rules in section 5 — one claim per criterion, name the actor, no semicolon — reach only the 18% that is inconsistency plus ambiguity. Ambiguity alone is 5%.
So wording is the third instrument, not the first:
| Instrument | Reaches | Where |
|---|---|---|
| A provenance mark on every requirement | the 49% | section 3.2 |
| A coverage sweep before you finish | the 31% | section 6 |
| Careful wording | the 18% | section 5 |
A cleaner sentence about an unchecked belief is a better-written defect. Write the mark first.
2. The folder contract
Keep all product requirements in docs/requirements/, one file per domain, with a
numeric prefix that says what kind of file it is. The number is the file's address. it
does not change when the content does.
| Range | Holds | Examples |
|---|---|---|
00 | The index. Navigation only, no content of its own | 00_INDEX.md |
01–02 | Core: vision and principles, system context | 01_VISION.md, 02_CONTEXT.md |
10–19 | Feature domains, one per vertical | 10_COLLECTIONS.md, 13_LIBRARY_VIEW.md |
20–29 | Data contracts: the shapes on disk and over the wire | 21_COLLECTION_JSON.md |
30–39 | Architecture and integration extensions, when a repo needs them | 30_EXPORT.md |
90–94 | Global files that cut across every domain | 90_NFRS.md through 94_ASSUMPTIONS.md |
99 | The changelog | 99_CHANGELOG.md |
One number, one file. Two files sharing a prefix means the number no longer identifies anything, and every link that says "see 15" becomes ambiguous. Take the next free number instead.
Why split at all. Most work touches one vertical. One file per domain means a change
touches one requirement file plus the changelog, and two people working on different
domains never conflict. The alternative is one REQUIREMENTS.md that everything appends
to. That turns every change into a merge conflict, and it makes "what does the spec say
about X" a full-file read.
A single monolithic document is frozen, not maintained. If a repo still has one from before the split, it stays for historical reference and says so at the top. Editing both is how the two copies drift apart.
A long well-structured document beats a short unstructured one, because structure lets a reader read selectively. Do not cut a spec because it grew. cut it because a section stopped being true.
3. One requirement
One requirement is one headed block:
### COL-1 - Create collection from PDF files
Status: Done
Priority: P0
Basis: V — owner confirmed 2026-06-03; issue ISSUE-12 section 2
Serves: turn a private document collection into a queryable knowledge base
Verification criteria:
- VC1: Only .pdf files are accepted; a non-PDF selection raises an error that names the file.
- VC2: Duplicate basenames in one request are rejected before any disk write.
- VC3: PDFs are copied to a sources/ directory so the originals can move or be deleted safely.3.1 The ID
A short domain prefix and a stable integer: COL-1, CHAT-3, MM-17, LIC-4. The
prefix is the domain's abbreviation, chosen once and reused in plans, issues, commit
messages and test names. The integer is never reused after a requirement is dropped — a
dead ID is a tombstone, not a vacancy.
3.2 Basis: — the field that carries the most weight
Four values:
| Value | Means | What it must say |
|---|---|---|
| V | Verified against an authoritative source | Cite it: a person and date, a file and section, a standard |
| N | Not yet researched | Name the belief and what would settle it |
| Q | Questionable | Name the doubt |
| I | Inferred by whoever wrote this, not stated by anyone | Say what it was reasoned from |
A mark with no stated reason is not allowed. The mark exists to carry the reason.
Basis: N alone is a to-do list with no item.
I is there because an inference that is not labelled reads exactly like a fact. It is
the commonest failure when requirements are drafted from a conversation. The writer fills
a gap they believe is self-evident, and nobody ever says it out loud.
One escalation rule makes N work: if no authoritative source can be found, write
the rationale instead. A belief with a stated rationale is a decision. A belief with
nothing is a defect waiting for a reviewer.
Basis: is not the same field as Source:. Source: is document provenance — which
file or decision the domain came from, at the top of the file. Basis: is whether anyone
has checked the claim. Collapsing the two looks tidy and does not work. Most Source:
lines in a mature repo record a document, so the mark has nowhere to live. A reader then
cannot tell an unmarked belief from a document reference.
A file-level Source: may legitimately point at a document that has since been split or
renamed. It records where the requirement came from, not what justifies it.
3.3 Serves: — the goal
The goal this requirement is a narrowing of. One line.
It is a deletion instrument rather than an archive. A requirement that cannot name its parent goal is either unnecessary, or it is a goal and belongs in the domain intro. It also supplies the why that a retro will ask for.
Three kinds, and the test is who can be held to it:
- a goal needs more than one party to act — it is a heading, not a requirement.
- a requirement is a goal narrowed until the software alone can be held to it.
- an assumption is a goal narrowed until something outside the software must hold it — a device, an API's behaviour, a file format, a human's habit.
The moment a sentence's subject is the user, the operating system, a remote service or
the filesystem, it is an assumption. It goes in 94_ASSUMPTIONS.md, where it can be
given a break condition. No acceptance criterion can ever verify it from inside the
software, and yet the capability silently depends on it.
3.4 Status:
One of:
| Value | Meaning |
|---|---|
Active | Specified, not built |
Partial | Some verification criteria hold, the rest do not |
Done | Every verification criterion holds |
Proposed | Written ahead of a decision — may never be built |
Deferred | Deliberately postponed, with a named version or trigger |
Rejected | Considered and refused — keep it in place with the reason |
Superseded | Replaced by another requirement — name the replacement and the date |
Obsolete | No longer applicable — say why, and when it stopped being true |
Superseded and Obsolete are not the same thing, and neither is Done. A requirement
whose feature was removed is Obsolete, not Done. leaving it as Done tells the next
reader the behaviour still exists.
Rejected is the state that makes the decision record useful. Keeping a refused idea in
place with its reason is what stops it being re-proposed every few months.
Superseded needs a baseline to mean anything. Without a named replacement and a date it
only says "a newer file exists". The changelog is the version record. Status: is the
state record. They are orthogonal, and a requirement can pass through many versions
without a state change.
Never edit a Done requirement's text in place. Supersede it and add the next, so the
history keeps the reason. The same applies to any requirement a plan or an issue already
cites: its text is an address, and rewriting it silently moves the address.
3.5 Priority:
Four levels, and the definitions are written down once because a level without criteria is a label, not a priority:
| Level | Means |
|---|---|
P0 | The product is not shippable without it |
P1 | The next thing built |
P2 | In scope, not scheduled |
P3 | Nice, deliberately not planned |
Prioritise at one level only. Vision statements always look more important than
capabilities because they subsume several. Criteria always look less important than the
capability they serve. Verification criteria inherit. they do not get a P value.
4. The capability-only rule, and its two licensed exceptions
Requirements do not name a technology, a library, a framework, a database, a file format or a code structure. A requirement states a capability.
Stated that way the rule is unenforceable, because it has no exceptions and every real project has technology pins that are genuinely requirements. There are exactly two homes for a technology statement, and neither is a requirement block.
1. A constraint. Technology imposed from outside. Take any of these:
- the platform must be Windows 10+
- the file must stay readable by another tool
- the licence must verify offline
- a regulator requires an algorithm
- an existing component must be included
A constraint is normatively "a requirement that limits the solution space beyond what is necessary for meeting the given functional requirements and quality requirements".
Write it with the forcing reason. A constraint without a reason is a plan decision wearing a costume.
2. A plan decision. Technology chosen for convenience, taste or speed. It belongs in the plan. When it appears in a requirements file, it is leaking.
The test that catches more than "does it name a technology": requirements models depict the problem. design models document the solution. "The layout is computed headless and the renderer only draws positions" names no technology and is still a design statement.
Six documented reasons the line cannot always be held, so a review can cite a reason instead of relitigating: existing components must be included. external prescriptions fix structure and detail. describing the solution is easier. only the structured system is surveyable. consistency is only clarified by realisation. and the questions about the specification only arise in the design.
5. Verification criteria
Numbered VC1 to VCn, or AC1 to ACn in older files. One criterion is one claim you
can decide true or false without asking the author what they meant.
Verifiable admits three instruments: a test, a measurement, or an inspection procedure. "Observable from outside the code" is too narrow. It assumes tests, which pushes every property toward testability. Inspectable properties then get squeezed out: no outbound socket, no mock code in a production bundle, no secret in the tree.
Four rules carry most of the weight:
- One claim per criterion. "Rejects a non-PDF file and reports the reason" is two criteria. Split it, or only half of it can fail.
- Name the actor. "Non-PDF files are rejected" leaves open who rejects them. "The app rejects a non-PDF file" is testable.
- Keep the modal verb.
mustandshouldare different requirements. Do not promoteshouldtomust, and do not turn a hedge into a fact. - No semicolon. Two clauses joined by a semicolon read as one criterion and test as two.
An adjective is a placeholder for a number, a named standard, or a named checker:
| Written | Means |
|---|---|
| "fast" | within 400 ms |
| "robust" | under no circumstances ends abnormally |
| "portable" | portable by one developer in at most 6 hours |
| "clearly commented" | every module has a header comment per a named standard |
| "platform independent" | all platform-specific code lives in one module |
A criterion that still fails "how would I know this is true" has no threshold, not bad wording. Send it back for a number instead of polishing the sentence.
Six linguistic patterns hide a defect, so run them as a pass over each criterion:
- Passive voice hides who does it. "Data is deleted" — by whom?
- Copula instead of a verb. "The data are consistent" — after what checked them?
- A missing object. "The component reports an error" — to whom?
- An incomplete condition. For an if/then, the else must also be described.
- A universal quantifier. "all users", "every file", "never" — are there restrictions? Are "all users" all users, or the ones present?
- A nominalisation. "Registration" hides who registers where and for what.
6. Sweep for what you did not write
Omission is 31% of defects and invisible from inside the document. You cannot review your way to a requirement that was never written. Five passes, cheapest first.
(a) The door dialogue — five minutes. State the most banal fact about the capability, then ask "and when does that not hold?" along four axes:
| Axis | Ask |
|---|---|
| time | First run? After an update? At midnight? Leap day? |
| actor | New user? Wrong user? Two users at once? No user? |
| failure | Disk full? Network gone? File corrupt? Permission denied? |
| term | What does the word mean at the edge of its range? |
Every answer that changes behaviour is a criterion. This is where the exception criteria nobody writes come from.
(b) Continuity. Most requirements in a mature system are continuity requirements, not change requirements. People describe what displeases them, so a list driven by complaints records the differences and silently omits everything that must stay the same.
Give every domain file a short section:
## Must not change
- Existing library files stay readable; a variant never becomes a second row.
- A scan that only adds files never rewrites an existing record.If you cannot name anything that must not change, say so explicitly. That is a finding, and for anything older than a prototype it is usually wrong.
(c) Negate the verb. For each significant capability, write the failure forms by negating the verb and each qualifier — "message not sent / sent late / sent to the wrong party". Then pick one of six moves and write it as a line:
| Move | What it adds |
|---|---|
| substitute the goal | a different way to achieve the same end |
| substitute the actor | someone or something else does it |
| weaken the goal | a smaller promise that still holds |
| restore after failure | a recovery requirement |
| mitigate | a narrower requirement with its own criteria |
| prevent | a guard, expressed as a requirement |
(d) Walk the quality categories. Six, one line each, most of them "nothing to say": functional suitability, performance efficiency, compatibility, usability, reliability, security. A blank category is a decision, not an oversight — but it has to be a decision.
(e) Read as the tester. For each criterion ask: can I derive a test from this? If not, either the criterion is unverifiable or the requirement is under-specified.
Run the full sweep only where it pays. Classify the capability first:
| Class | When | Passes to run |
|---|---|---|
| H | Data loss, money, irreversible action, security boundary, or a failure that is silent | all five |
| M | Reversible, visible, user-facing | (a) and (b) |
| L | Cosmetic, internal, trivially reversible | none beyond the wording pass |
Most work is M or L. A checklist applied uniformly stops being read. a checklist applied where the risk is is the only kind that survives.
7. Two more checks before you hand it over
7.1 Re-read it in a fresh context
You cannot validate a requirement you just wrote. Not because of bias, but because of silent completion: your prior knowledge fills the gaps while reading, so the gaps are never seen. This is structural. It is worse for a requirement drafted by an agent, which has no memory of hesitating.
There is no second reader here, so use a substitute, cheapest first:
- Fresh-context re-read. Give someone — or a clean session — only the requirement block and the domain file. Ask it to state what it would build and what it would have to guess. Every guess is a gap.
- Transcription. Restate the requirement as a table row, a given/when/then clause, or a data-shape sketch. What cannot be transcribed is the ambiguity.
- Artefact construction. Write one test case, one error message, or one data shape from the requirement. This is the highest-yield move available to one person, and it is not cheap — the cost is what forces the gap into view.
Run at least (1) for every H capability, and for any requirement marked N, Q or I.
7.2 Check a claim against its source, when there is one
A fresh-context re-read finds ambiguity. It does not find a wrong fact, because the re-reader has the same beliefs you do. Only the source settles that.
Basis: V is a claim that a document says something, so it is checkable — but only if the
source is a link. Sort the citations first, because only one kind is worth reading:
| Kind | Example | What settles it |
|---|---|---|
| A download endpoint | a vendor's installer URL | an HTTP status |
| The vendor's own page | their marketing site | nothing: a vendor agrees with itself |
| A specification | a published file format, a standard clause | the page, read properly |
A source list is cheap to check for liveness and expensive to check for meaning. Spend the effort on the specifications, and treat "the vendor's homepage does not say that" as a question rather than a finding.
8. The global files
Five shapes shared across every domain, because each answers a question that does not belong to one vertical.
90_NFRS.md — non-functional requirements, one row each
| ID | Kind | Category | Requirement | How it is measured |
|---|---|---|---|---|
| NFR-1 | Constraint | Security | No document, query or user data leaves the local machine | Inspection: no outbound socket at runtime |
| NFR-3 | Quality | Performance | Collection create for a 100-page PDF completes in under 60 s on a mid-range CPU | Timed, 3 runs, median |Kind is not decoration. "Non-functional requirement" is an alias for two different
things — a quality requirement or a constraint — and they validate differently.
A quality requirement is graded. "First token within 10 s" is met or missed on a scale, and its measurement method is part of the requirement. A constraint is a hard limit. Either it holds or the build is wrong, and you check it by inspection.
Calling both NFR hides which of the two rules applies. It is also how a coding standard ("clean architecture: small focused components") ends up in a requirements file where no criterion can ever verify it.
91_INVARIANTS.md — properties that must never be false
| ID | Constraint | Enforcement Points |
|---|---|---|
| INV-1 | The embedding model is immutable per collection after first indexing | UI disable, hook validation, backend rejection |
| INV-4 | Collection IDs are UUIDs and validated for all file operations | Path-resolution guard |The third column is what separates an invariant from a wish. If nothing enforces it, it is either a requirement or a note, and putting it here makes the table lie.
92_PLANNED_FEATURES.md — the backlog
One line per item, each pointing at the issue or plan that carries the detail. A planned feature is not a requirement until it has verification criteria. until then it is one line and a link.
93_OUT_OF_SCOPE.md — explicit non-goals, with the reason
| Topic | Rationale |
|---|---|
| Cloud or remote backend | Conflicts with the local-first principle |
| Multi-user sharing | The product is a single-user desktop app |
This file exists to end the same conversation twice. A non-goal without a rationale gets re-proposed every few months. with one, the next discussion starts from the answer.
Scope is not the system boundary. They are different axes: a component inside the boundary that must be reused is out of scope. a thing outside the boundary that can be redesigned is in scope. Write each entry as a scope statement, not as a boundary claim.
94_ASSUMPTIONS.md — what the world must hold
| ID | Assumption | Breaks if | Owner |
|---|---|---|---|
| ASM-1 | The data directory stays at the same path between sessions | The directory is moved or mounted elsewhere | user |
| ASM-2 | The export format's importer tolerates absolute file URIs with escaped spaces | A future version normalises paths differently | vendor |Why this is a global file and not a section. A requirement is something the software alone can be held to. The moment a sentence's subject is the user, the operating system, a remote API or the filesystem, no criterion can verify it. Yet the capability silently depends on it. Those sentences are assumptions, they are load-bearing, and without this file they have nowhere to live.
The Breaks if column is the whole value. An assumption nobody can name a break condition
for is decoration: delete it or turn it into a requirement. Review assumptions when a
dependency changes, not on a calendar.
9. The changelog rule
99_CHANGELOG.md is append-only, newest first, one ## YYYY-MM-DD section per change,
each naming the requirement IDs it touched and why.
A behaviour change updates the matching requirement file and the changelog in the same change. Not a follow-up commit, not a cleanup ticket. If the spec is updated later, it is updated wrong, because the person who knew why has moved on.
The changelog is not a git log. It records decisions about requirements — a new requirement, a changed criterion, one retired as obsolete — and it survives the history being rewritten.
Changing a requirement
Two change moments, one gate. Before a requirement is Active, changes flow in
directly. edit the text. After it is Active, they go through impact analysis first.
Impact analysis, in three questions:
- Which other requirements does this touch? (depends-on, conflicts-with, refines)
- Which verification criteria stop holding?
- Which implementation is now wrong?
Then decide — and the decision is the changelog entry plus the status change. There is no board, and one person does not need one.
Four reasons to reject, each written down: too costly for the benefit, contradicts another requirement, would destabilise the system, not covered by the existing scope. The middle two are the ones that get forgotten.
10. The index rule
00_INDEX.md lists every file in the folder with a one-sentence summary, ordered by
number, grouped by the ranges in section 2. It is the only way into the folder. a file that
is not in the index does not exist as far as a reader is concerned.
Pick one index name and delete the other. 00_INDEX.md and 00_Index.md both existing
is a guaranteed disagreement, and it is one of the most common ways a tidy-looking folder
rots.
11. What a script can and cannot check
A script can check the things nobody reliably remembers. Every file has an index entry.
IDs are unique. The changelog was touched. Every referenced ID exists. Every requirement
block has a Basis: mark. A path named in a Source: still exists.
Automate those. They decay at the rate of reviewer attention, and that rate is zero when nobody is reviewing.
A script cannot check whether a requirement is any good. These stay with a person:
- whether a verification criterion is observable by a test, a measurement or an inspection.
- whether a
Basis: Vcites something that actually says that, or whether the belief was merely confident. - whether an NFR number has a measurement method, or is a number someone liked.
- whether a
Kind: Constraintlimit is really imposed from outside, or is a plan decision wearing a costume. - whether an invariant's enforcement column is true, or aspirational.
- whether a
Donestatus still matches the running product.
That gap is deliberate. A requirement nobody can verify is a rule people will route around.
12. The short version
A requirement is a capability the software alone can be held to. Say what must be true when it is done, and how you would know.
Mark each one with how you know: verified, not yet researched, doubtful, or inferred by you. A mark without the reason is worthless. The mark itself is worth more than any amount of careful wording, because 49% of defects are facts nobody checked.
List what must not change before you list what is new.
Sweep for what you did not write — the door dialogue, the continuity section, the negated verb — and sweep hardest where failure is silent, not everywhere.
Then re-read it in a context that does not already believe you, because you cannot see the gap your own knowledge fills.
Everything else is tooling.