Requirements
Requirements answer the question: what does the system need to do?
Before you build, requirements capture what you’re trying to achieve. After you build, they serve as a reference for what the system is supposed to do — useful for onboarding, debugging, and deciding whether a change is a bug fix or a new feature.
What to Capture
Not every detail needs to be a requirement. A good rule of thumb: if it’s important enough to validate, it’s worth capturing as a requirement.
Requirements work well for:
- User-facing behavior (“when a user does X, Y happens”)
- Business rules (“orders over $100 get free shipping”)
- Edge cases that matter (“expired links show an error, not a blank page”)
Requirements are less useful for:
- Implementation details (“use a hash map for lookups”)
- Obvious behavior (“the page loads”)
- Things that will change frequently during development
Structure
A requirement block has two parts: a header and children.
LOGIN-1: A registered user, Jamie, can log in to their account
0. → When Jamie provides valid credentials, they are authenticated
1. → When Jamie provides invalid credentials, they see an error messageThe header (LOGIN-1: A registered user...) corresponds to the “story” or “use case” level. It contains:
- A unique key (
LOGIN-1) — used to reference this requirement in tests - A summary — describes the overall behavior
Children (the indented lines) correspond to the “acceptance criteria” or “example” level. They break down the requirement into specific criteria:
- A position (
0.,1.) — defines the hierarchy. Because these are primarily used as a programmatic reference, they are visible in markdown, but not in the web composer. - An optional label (like
Important,Given,When,Then) - Content — the specific criterion or example
Children can be nested:
CHECKOUT-1: A customer, Alex, can complete a purchase
0. Given → Alex has items in their cart
1. When → Alex submits payment with a valid card
2. Then → The order is confirmed
2.0. And → Alex sees a confirmation page with an order number
2.1. And → Alex receives a confirmation emailThe Markdown Format
Requirements are written in fenced code blocks with the dotrequirements language identifier:
```dotrequirements
KEY: Summary text
0. Label → Content
1. → Content without label
1.0. → Nested content
```This format is:
- Human-readable — easy to scan and understand
- Machine-parseable — tools can extract and validate requirements
- Editor-friendly — works in any text editor, renders on GitHub
Labels
Labels are optional. They appear before the arrow delimiter (→ or ->):
REFUND-1: A customer can request a refund
0. Given → The customer purchased an item within 30 days
1. When → The customer submits a refund request
2. Then → The refund is processed within 5 business daysCommon labels include Given, When, Then, And, and AC (acceptance criteria), but you can use any label that makes sense for your team. Labels are not required — unlabeled children work fine:
SEARCH-1: A user can search for products
0. → Entering a search term shows matching results
1. → Searching for a nonexistent term shows "no results" message
2. → Search results can be filtered by categoryKeys and Paths
Every requirement has a key that uniquely identifies it within your project.
Key format: DOMAIN-N where DOMAIN is a short prefix and N is sequential.
LOGIN-1,LOGIN-2,LOGIN-3CHECKOUT-1,CHECKOUT-2AUTHN-1,AUTHZ-1
Paths reference specific parts of a requirement:
| Path | What it references |
|---|---|
LOGIN-1 | The requirement header |
LOGIN-1.0 | First child (by position) |
LOGIN-1.1 | Second child |
LOGIN-1.2.0 | First child of third child (nested) |
LOGIN-1.important | First child labeled “Important” |
LOGIN-1.then.and | First “And” under first “Then” |
Label paths are case-insensitive and zero-indexed. If you have multiple children with the same label, use #N to disambiguate: LOGIN-1.important#1 for the second “Important”.
Referencing in Tests
Use the requirement() function to reference requirements in your test descriptions:
import { requirement } from '@popoverai/dotrequirements/test'
describe(requirement('LOGIN-1'), () => {
// Set up valid and invalid test credentials
it(requirement('LOGIN-1.0'), async () => {
// Test: valid credentials → authenticated
})
it(requirement('LOGIN-1.1'), async () => {
// Test: invalid credentials → error message
})
})The requirement() function returns the requirement text, so your test output shows meaningful descriptions instead of cryptic IDs (both in the CLI test runner and in the IDE test explorer). It also stays up to date if a requirement’s language is updated.
Next Steps
- See requirements in context — Documents explains how requirements fit into documents
- Track what’s tested — Coverage Tracking shows how to see which requirements have tests