Requirements to Code
On a safety-critical vehicle you cannot merely write software that works — you must be able to demonstrate, mechanically and repeatably, that every line exists because something asked for it and that something tests it. This page is a complete worked tutorial of the two-tier requirement scheme that makes that possible: HLSR (high-level software requirements) describing what the software must do, LLSR (low-level software requirements) describing how a specific function does it, the annotations that bind LLSRs to code, the tests that discharge them, and the pipeline scripts that fail the build when any link in that chain is broken.
Worked throughout on an invented example — a free-inertial drift monitor for a generic launch vehicle. Everything here is a pattern, not a product: identifiers, wording and directory names are chosen to be copied into a new repository. Related reading: Rocket navigation for the system this kind of monitor lives inside, and the drift test for the physics the example requirements describe.
1 · Why two tiers at all
The instinct is to write one list of requirements. That fails for a reason worth understanding, because it explains every design choice that follows.
Requirements serve two audiences who need incompatible things. A systems engineer, a customer or a regulator needs statements about observable vehicle behaviour, stable across redesigns, reviewable without reading code: "the vehicle shall detect a degraded inertial sensor before committing to flight." A developer needs statements precise enough to implement and test a single function: "this function shall return FAULT when the computed radial error rate exceeds the configured threshold." Force those into one document and you get either requirements too vague to test or a "requirements" document that is really source code in prose, and which must change every time you refactor.
| HLSR — high-level | LLSR — low-level | |
|---|---|---|
| Answers | What must the software do? | How does this function do it? |
| Audience | Systems, safety, certification, customer | Developers, reviewers, test authors |
| Lives in | A controlled specification document | The code itself, as annotations |
| Granularity | One observable behaviour | One branch of one function |
| Changes when | The mission or interface changes | The implementation changes |
| Verified by | Test, analysis, inspection, demonstration | Almost always a unit test |
| Owned by | Systems engineering; change is controlled | The developer who wrote the function |
The split pays for itself the first time you refactor. Splitting one function into three changes three LLSRs and touches no HLSR, no customer document, and no certification argument — because the observable behaviour did not change. That containment is the entire point.
2 · The chain, end to end
Six artefacts, each with exactly one job. Read this diagram first; the rest of the page is one section per arrow.
3 · The example we will build
A small, self-contained capability with enough structure to exercise every part of the scheme: a monitor that decides whether the inertial sensor is healthy enough to fly on, by comparing the vehicle's own free-inertial solution against a surveyed truth while it sits on the pad.
| Element | Value in this tutorial |
|---|---|
| Repository | drift_monitor |
| Specification id | SPEC-4000 (stands in for a document number) |
| Module / prefix | drift_monitor |
| Class | DriftMonitor |
| Behaviour | Accumulate radial position error against truth; declare a fault when its
rate exceeds a configured limit; latch the verdict; report never-evaluated as
UNKNOWN. |
The layout — worth copying verbatim into a new repository, because the scripts assume it:
4 · Step 1 — write the HLSRs
An HLSR is a structured record, not a sentence. The prose matters, but so do the fields around it: they are what make the requirement reviewable, traceable and verifiable.
4.1 Start from the operational need
Begin with what somebody actually wants, in their words. For our example, three statements from a concept-of-operations review:
| # | Operational need | Where it comes from |
|---|---|---|
| N1 | The vehicle must not begin flight relying on a degraded inertial sensor. | Flight safety |
| N2 | Operators need the health verdict early enough to recycle the count. | Launch operations |
| N3 | A verdict, once given, must not silently reverse itself. | Safety review |
None of these is testable yet. "Degraded" is undefined, "early enough" is not a number, and "silently" is a judgement. Turning them into requirements is the systems engineering work.
4.2 Write them in the mandated form
Use one sentence pattern and never deviate, because tooling and reviewers both rely on it:
| Word | Meaning — this is a convention, enforce it |
|---|---|
| shall | A requirement. Binding, verifiable, traceable. The only word that creates an obligation. |
| should | A goal. Never put this in a requirement — it cannot be failed, so it cannot be verified. |
| will | A statement of fact about something else (the environment, another subsystem). Context, not obligation. |
| may | Permission. Implementation latitude, deliberately granted. |
Applying the pattern to N1–N3 gives three HLSRs:
4.3 What each field is for
| Field | Purpose | Failure mode if you skip it |
|---|---|---|
id | Immutable numeric handle. Everything else points at this. | Renaming a requirement silently breaks every trace to it |
contents | The requirement text, in the mandated pattern. | Untestable requirements discovered at verification time, when they are expensive |
rationale | Why this requirement exists, and why the number is that number. | Nobody dares change it later; dead requirements accumulate forever |
verificationMethod | Test · Analysis · Inspection · Demonstration. | Verification planning cannot be scoped or scheduled |
verificationExpectations | What evidence would convince a reviewer. | A test exists, passes, and proves something adjacent to the requirement |
satisfies | Upward link to the parent (vehicle or system) requirement. | Orphan requirement — no one can say why it is in the build |
derived | True when no parent asked for this; it follows from the design. | Derived requirements escape the extra safety review they are owed |
tags | Applicability and classification (variant, safety). | Cannot tell which requirements apply to which product variant |
derived: true is a flag that asks for scrutiny, not a shortcut. It means "nothing above
asked for this — the design needs it." That is legitimate: 4003 exists because we chose to have an
initial state, which no operator requested. But because no parent requirement constrains it, a derived
requirement cannot be validated by appeal to the level above; it must be justified on its own and, in most
safety processes, reviewed by someone who can say whether it introduces hazard. Marking a requirement
derived to avoid finding its parent is the single most common abuse of this field.
4.4 Six tests for a bad requirement
| Smell | Bad | Better |
|---|---|---|
| Unmeasurable | "shall respond quickly" | "shall respond within 2.0 s" |
| Two requirements in one | "shall detect and annunciate and log" | Three requirements, separately verifiable |
| Prescribes design | "shall use a Kalman filter to…" | "shall estimate … to within X" — let design be design |
| Unfalsifiable | "shall be robust to sensor noise" | "shall hold the verdict for input noise up to X" |
| Passive voice, no subject | "the fault shall be declared" | "the Drift Monitor shall declare" — by whom matters |
| Hidden condition | "shall declare a fault" | "WHEN in PREFLIGHT, shall declare a fault" — when? |
5 · Step 2 — decompose into LLSRs
Now the developer's turn. The HLSR says the software must declare a fault within 2 s; the LLSRs say which function does what, branch by branch.
5.1 The identifier scheme
Every LLSR has a structured id. Structure is what lets a script check it, so the format is rigid:
| Part | Rule | Why a script can check it |
|---|---|---|
| module | Matches the directory and the trace filename | Locates the CSV that must contain this id |
| class | Matches the enclosing class or file | Catches copy-paste between modules — the commonest error |
| function | Matches the declaration the annotation sits above | Catches annotations left behind after a rename |
| MAIN / ALT | Nominal versus off-nominal path | Makes "did we test the error paths?" answerable by counting |
| suffix | The HLSR id when it maps to exactly one; else a counter | A human reading code sees the requirement without opening the spec |
MAIN-4001 knows the behaviour is externally required, while
MAIN-2 is an internal decomposition step. When one function serves several HLSRs — or an LLSR
is pure implementation structure — use a counter and let the trace CSV carry the mapping.
5.2 Declare them in the header
LLSRs go immediately above the declaration, in the header, where every reviewer of the function will see
them. @post states what is true after the call — a postcondition, which is exactly what a
requirement about behaviour is.
evaluate() has three: the nominal detection, the
latch, and the input guard. Each is separately testable and separately meaningful. If you cannot write a
test that fails when one specific LLSR is broken and passes otherwise, the decomposition is wrong — usually
too coarse.
5.3 Implement, marking the paths
The implementation carries a short comment naming the LLSR each branch discharges. This is the line a reviewer checks against the header, and the one that makes a stale annotation obvious.
Note the ordering constraint in the first guard, and that it is load-bearing: if the latch check came
first a NaN would be evaluated against the limit, and NaN > limit is false, so a broken
sensor would read HEALTHY. That is precisely the kind of detail an LLSR should capture — it is behaviour,
not style, and the test below exists to keep it true.
6 · Step 3 — generate the trace
The trace is a flat CSV linking every LLSR to the HLSR it serves. It is generated from the annotations; hand-editing it is how traces rot.
| Column | Meaning |
|---|---|
LLR | The LLSR id, exactly as written in the @post |
HLR | The numeric HLSR id it contributes to |
Derived | true when no HLSR directly asked for this LLSR |
Rationale | One line: why this LLSR serves that HLSR |
The third row is the interesting one. The input guard is derived: true — no requirement asked
for a NaN check — but it still traces to 4001, because its reason for existing is to keep 4001
honest. That is the pattern for defensive code: derived, but not orphaned.
7 · Step 4 — verify, and say what you verified
A test that exercises a requirement but does not say so is invisible to the process. One macro call fixes that: it records, at runtime, that this test claims coverage of that LLSR.
VERIFIES records a claim
— "this test covers that LLSR" — which lets CI compute coverage across the whole specification. It proves
nothing by itself. A test that calls VERIFIES and then asserts something trivially true is
worse than no test, because it makes an uncovered requirement look covered. This is what review is
for, and it is the one part of the chain no script can check.
Two conventions that keep coverage honest as the code grows:
- Several LLSRs per test is fine when one scenario genuinely exercises them together — call
VERIFIESonce per LLSR. Prefer separate tests when the failures would be distinct. - Verified-by-parent needs recording. When a caller's test necessarily exercises a callee's LLSR, say so in a comment block listing those ids, so the coverage script can credit them rather than reporting a gap that is not real.
8 · Step 5 — make CI enforce all of it
Everything above is a convention, and conventions decay unless something mechanical objects. Four scripts, each answering one question, each able to fail the pipeline.
| Script | Question | Typical failure it catches |
|---|---|---|
check_llsr_format.py |
Is every LLSR id well-formed, unique, and in the right place? | Copy-pasted annotation still naming the old class; two functions with the same id |
gen_trace.py --check |
Does the committed CSV match what the annotations generate? | Added an @post and forgot to regenerate; hand-edited CSV |
check_trace.py |
Does every link resolve, in both directions? | LLSR citing a deleted HLSR; an HLSR that no LLSR implements |
check_verification.py |
Is every LLSR named by at least one test? | New branch added with an LLSR but no test |
The --fail-under 100 deserves a comment, because it looks unreasonable and is not. Statement
coverage of 100% is often impractical; requirement coverage of 100% is simply the statement that
every behaviour you claimed to implement has at least one test naming it. If an LLSR genuinely cannot be
unit-tested, the answer is an explicit, reviewed exception with a recorded justification — not a lower
threshold that quietly permits any gap.
9 · Bootstrapping a new repository
The order matters: each step makes the next one enforceable. Do them in this sequence and the process holds itself up.
| # | Do this | Done when |
|---|---|---|
| 1 | Create the directory skeleton of §3 | Paths exist; the scripts know where to look |
| 2 | Write down the operational needs, unedited, in the stakeholders' words | Every need has a named owner |
| 3 | Allocate an id block (e.g. 4000–4999) and write the HLSR YAML | Every need maps to ≥1 HLSR; every HLSR has rationale and a verification method |
| 4 | Review the HLSRs before writing code | Systems and safety have signed; this is the cheapest review you will ever do |
| 5 | Write headers with @post LLSRs — signatures only, no bodies | Every HLSR has ≥1 LLSR; the shape of the module is now visible |
| 6 | Add the four scripts and the CI stage, and watch them fail | They fail for the right reason: no implementation, no tests |
| 7 | Implement, marking each branch with its LLSR | Format and trace checks pass |
| 8 | Write tests with VERIFIES | Coverage check passes at 100% |
| 9 | Baseline the spec; changes now go through change control | A tagged, reviewable starting point exists |
10 · Where this goes wrong
| Anti-pattern | Why it happens | What it costs |
|---|---|---|
| LLSRs that restate the code "increments the counter by one" |
Written after the fact, by reading the implementation | Every refactor churns requirements; reviewers stop reading them |
| HLSRs that specify design | The author already knows how they will build it | Design is frozen by a document that needs change control to unfreeze |
Everything derived: true |
Finding the real parent is work | The upward trace becomes fiction; nobody can scope a change impact |
| One giant LLSR per function | Fewer annotations to write | Error paths are never separately tested, because nothing names them |
| Tests that claim coverage they do not provide | Coverage gate is red and the deadline is today | The worst outcome available: verified-looking, unverified software |
| Hand-edited trace CSV | The generator was inconvenient once | Trace and code diverge silently, and the CSV is what gets reviewed |
A pattern connects all six: each is a local shortcut whose cost lands on somebody else, later — usually during verification, when schedule pressure is highest and options are fewest. That is exactly why these checks belong in CI rather than in a style guide. A style guide asks; a pipeline declines.
11 · Further reading
- DO-178C, Software Considerations in Airborne Systems and Equipment Certification, RTCA. §5 defines the high-level/low-level split this page describes and §6 the verification objectives; the traceability tables of Annex A are what the CI scripts are ultimately producing evidence for.
- DO-331, model-based supplement — relevant if requirements or design are captured as models rather than text.
- NASA-STD-8739.8, Software Assurance and Software Safety Standard — a readable treatment of bidirectional traceability and why derived requirements get extra scrutiny.
- ISO 26262-6 (automotive) and IEC 61508-3 (industrial) — the same two-tier structure under different names, useful for seeing which parts are essential rather than domain convention.
- INCOSE Guide for Writing Requirements — the sentence patterns and quality rules of §4.2 and §4.4, with far more examples.
Related pages: Rocket navigation — the architecture a monitor like this sits inside · Ground alignment & the drift test — deriving the threshold the example requirements refer to · NASA nav-filter best practices — the engineering-standards view of estimator design.