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.

The one-sentence version. A requirement that no code claims is unimplemented; code that claims no requirement is unauthorised; a requirement that no test names is unverified. The whole apparatus below exists to make those three statements checkable by a script instead of by a person reading documents, because people miss things and scripts do not.

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-levelLLSR — low-level
AnswersWhat must the software do?How does this function do it?
AudienceSystems, safety, certification, customerDevelopers, reviewers, test authors
Lives inA controlled specification documentThe code itself, as annotations
GranularityOne observable behaviourOne branch of one function
Changes whenThe mission or interface changesThe implementation changes
Verified byTest, analysis, inspection, demonstrationAlmost always a unit test
Owned bySystems engineering; change is controlledThe 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.

┌──────────────────────────────────────────────────────────────────────────┐ │ 1. OPERATIONAL / BUSINESS NEED (prose, humans) │ │ "We must not fly on a degraded IMU." │ └───────────────────────────────┬──────────────────────────────────────────┘ │ systems engineering decomposition v ┌──────────────────────────────────────────────────────────────────────────┐ │ 2. HLSR docs/req/specs/<SPEC-ID>/<spec>.yaml (id: 4001) │ │ "WHEN in PREFLIGHT, the software shall declare the IMU unhealthy │ │ within 2 s of the drift metric exceeding its limit." │ │ + rationale, verificationMethod, tags, satisfies:[parent] │ └───────────────────────────────┬──────────────────────────────────────────┘ │ developer decomposition v ┌──────────────────────────────────────────────────────────────────────────┐ │ 3. LLSR an @post line in the HEADER, next to the declaration │ │ [drift_monitor-DriftMonitor-evaluate-MAIN-4001] │ └───────────────────────────────┬──────────────────────────────────────────┘ │ ┌──────────────┴───────────────┐ v v ┌────────────────────────────────┐ ┌────────────────────────────────────┐ │ 4. TRACE docs/trace_data/… │ │ 5. CODE the .cpp that implements │ │ <module>.csv │ │ the behaviour the LLSR names │ │ LLR, HLR, Derived, Rationale│ └────────────────────────────────────┘ └────────────────┬───────────────┘ │ │ └──────────────┬───────────────┘ v ┌──────────────────────────────────────────────────────────────────────────┐ │ 6. TEST VERIFIES("drift_monitor-DriftMonitor-evaluate-MAIN-4001"); │ │ the test that fails if the behaviour regresses │ └───────────────────────────────┬──────────────────────────────────────────┘ │ v CI: four scripts, every merge request ─ no orphans, no dangling ids, no untested requirements
Note what is not in the chain: a separate LLSR document. The LLSRs live in the header files beside the functions they describe. This is deliberate — a requirement kept in a separate document drifts from the code within weeks, while one sitting three lines above the declaration is seen by every reviewer of every change to that function. The trace CSV is generated from those annotations, not hand-maintained.

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.

ElementValue in this tutorial
Repositorydrift_monitor
Specification idSPEC-4000 (stands in for a document number)
Module / prefixdrift_monitor
ClassDriftMonitor
BehaviourAccumulate 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:

drift_monitor/ ├── docs/ │ ├── req/ │ │ └── specs/ │ │ └── SPEC-4000-drift-monitor-hlsr/ │ │ └── SPEC-4000-drift-monitor-hlsr.yaml ← the HLSRs │ └── trace_data/ │ └── drift_monitor/ │ └── drift_monitor.csv ← GENERATED trace ├── include/ │ └── drift_monitor/ │ └── DriftMonitor.hpp ← LLSRs live here ├── src/ │ └── DriftMonitor.cpp ← implementation ├── test/ │ └── unit/ │ └── testDriftMonitor.cpp ← VERIFIES(...) calls ├── scripts/ │ ├── gen_trace.py generate the CSV from @post annotations │ ├── check_llsr_format.py ids well-formed, unique, match their location │ ├── check_trace.py every LLSR ↔ HLSR link resolves both ways │ └── check_verification.py every LLSR is named by at least one test └── .gitlab-ci.yml

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 needWhere it comes from
N1The vehicle must not begin flight relying on a degraded inertial sensor. Flight safety
N2Operators need the health verdict early enough to recycle the count. Launch operations
N3A 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:

WHEN <condition>, the <subject> shall <observable behaviour> within / to within <quantified bound>.
WordMeaning — this is a convention, enforce it
shallA requirement. Binding, verifiable, traceable. The only word that creates an obligation.
shouldA goal. Never put this in a requirement — it cannot be failed, so it cannot be verified.
willA statement of fact about something else (the environment, another subsystem). Context, not obligation.
mayPermission. Implementation latitude, deliberately granted.

Applying the pattern to N1–N3 gives three HLSRs:

# docs/req/specs/SPEC-4000-drift-monitor-hlsr/SPEC-4000-drift-monitor-hlsr.yaml - id: 4000 type: Hdg name: Drift Monitor Requirements headingId: drift-monitor-requirements headingLevel: 1 - id: 4001 type: Req name: Drift Monitor Fault Declaration headingId: drift-monitor-fault-declaration headingLevel: 2 contents: 'WHEN in the PREFLIGHT mode, the Drift Monitor shall declare the inertial sensor UNHEALTHY within 2.0 seconds of the radial error rate exceeding the configured drift limit.' rationale: Operators need the verdict before the commit point; 2 s is the shortest interval that survives one lost message at the monitor rate. verificationMethod: - Test verificationExpectations: Inject a truth offset that ramps past the limit; confirm the verdict changes within 2.0 s of the crossing. derived: false satisfies: - 3001 # vehicle-level: do not fly a degraded sensor tags: - SAFETY pullUpdatable: false - id: 4002 type: Req name: Drift Monitor Verdict Latching headingId: drift-monitor-verdict-latching headingLevel: 2 contents: 'WHEN the Drift Monitor has declared the inertial sensor UNHEALTHY, the Drift Monitor shall retain that verdict until it is explicitly reset.' rationale: A verdict that clears itself lets a transient fault disappear before an operator sees it. verificationMethod: - Test derived: false satisfies: - 3001 tags: - SAFETY pullUpdatable: false - id: 4003 type: Req name: Drift Monitor Unevaluated State headingId: drift-monitor-unevaluated-state headingLevel: 2 contents: 'WHEN the Drift Monitor has not yet completed an evaluation, the Drift Monitor shall report the sensor health as UNKNOWN.' rationale: UNKNOWN and HEALTHY must be distinguishable, so that "no data" can never be mistaken for "passed". verificationMethod: - Test derived: true satisfies: - 3001 tags: - SAFETY pullUpdatable: false

4.3 What each field is for

FieldPurposeFailure mode if you skip it
idImmutable numeric handle. Everything else points at this. Renaming a requirement silently breaks every trace to it
contentsThe requirement text, in the mandated pattern. Untestable requirements discovered at verification time, when they are expensive
rationaleWhy this requirement exists, and why the number is that number. Nobody dares change it later; dead requirements accumulate forever
verificationMethodTest · Analysis · Inspection · Demonstration. Verification planning cannot be scoped or scheduled
verificationExpectationsWhat evidence would convince a reviewer. A test exists, passes, and proves something adjacent to the requirement
satisfiesUpward link to the parent (vehicle or system) requirement. Orphan requirement — no one can say why it is in the build
derivedTrue when no parent asked for this; it follows from the design. Derived requirements escape the extra safety review they are owed
tagsApplicability 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

SmellBadBetter
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:

drift_monitor - DriftMonitor - evaluate - MAIN - 4001 └─────┬──────┘ └─────┬────┘ └───┬───┘ └─┬─┘ └─┬─┘ module class function path HLSR id (or a local counter: 1,2,3) MAIN the nominal path — what the function does when all is well ALT an alternative path — an error case, a guard, an early return
PartRuleWhy a script can check it
moduleMatches the directory and the trace filenameLocates the CSV that must contain this id
classMatches the enclosing class or fileCatches copy-paste between modules — the commonest error
functionMatches the declaration the annotation sits aboveCatches annotations left behind after a rename
MAIN / ALTNominal versus off-nominal pathMakes "did we test the error paths?" answerable by counting
suffixThe HLSR id when it maps to exactly one; else a counterA human reading code sees the requirement without opening the spec
Why the HLSR id is embedded when it can be. It costs nothing and it means a developer reading 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.

// include/drift_monitor/DriftMonitor.hpp #pragma once #include <cstdint> namespace drift_monitor { enum class Health : uint8_t { UNKNOWN = 0, HEALTHY = 1, UNHEALTHY = 2 }; class DriftMonitor { public: /// @brief Evaluate one measurement of accumulated radial error. /// /// @pre initialise() has been called /// @pre dt_s > 0 /// /// @post Declare Fault On Limit Exceedance /// [drift_monitor-DriftMonitor-evaluate-MAIN-4001] /// WHEN the radial error rate exceeds the configured limit, the /// health is set to UNHEALTHY. /// /// @post Latch Unhealthy Verdict /// [drift_monitor-DriftMonitor-evaluate-ALT-4002] /// WHEN the health is already UNHEALTHY, it is left unchanged. /// /// @post Reject Non-Finite Input /// [drift_monitor-DriftMonitor-evaluate-ALT-1] /// WHEN the input is not finite, the sample is discarded and the /// health is left unchanged. /// /// @param radial_error_m accumulated radial error, metres /// @param dt_s interval since the previous evaluation, s void evaluate(double radial_error_m, double dt_s); /// @brief Current health verdict. /// /// @post Report Unknown Before Evaluation /// [drift_monitor-DriftMonitor-health-MAIN-4003] /// WHEN no evaluation has completed, UNKNOWN is returned. Health health() const; private: Health _health { Health::UNKNOWN }; double _limit_m_per_s { 0.0 }; double _prev_error_m { 0.0 }; bool _has_prev { false }; }; }
One LLSR per behaviour, not per line. 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.

// src/DriftMonitor.cpp #include "drift_monitor/DriftMonitor.hpp" #include <cmath> namespace drift_monitor { void DriftMonitor::evaluate(double radial_error_m, double dt_s) { // [drift_monitor-DriftMonitor-evaluate-ALT-1] reject non-finite input. // Checked FIRST: a NaN that reaches the comparison below would make it // false and silently pass a broken sensor. if (!std::isfinite(radial_error_m) || !std::isfinite(dt_s) || dt_s <= 0.0) { return; } // [drift_monitor-DriftMonitor-evaluate-ALT-4002] the verdict latches. if (_health == Health::UNHEALTHY) { return; } if (!_has_prev) { _prev_error_m = radial_error_m; _has_prev = true; return; // need two samples to form a rate } const double rate = (radial_error_m - _prev_error_m) / dt_s; _prev_error_m = radial_error_m; // [drift_monitor-DriftMonitor-evaluate-MAIN-4001] declare on exceedance. _health = (rate > _limit_m_per_s) ? Health::UNHEALTHY : Health::HEALTHY; } // [drift_monitor-DriftMonitor-health-MAIN-4003] UNKNOWN until evaluated; // the member initialiser, not this function, is what guarantees it. Health DriftMonitor::health() const { return _health; } }

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.

# docs/trace_data/drift_monitor/drift_monitor.csv LLR, HLR, Derived, Rationale drift_monitor-DriftMonitor-evaluate-MAIN-4001,4001,false,Declare fault on limit exceedance drift_monitor-DriftMonitor-evaluate-ALT-4002,4002,false,Latch the unhealthy verdict drift_monitor-DriftMonitor-evaluate-ALT-1,4001,true,Input guard protects the comparison in 4001 drift_monitor-DriftMonitor-health-MAIN-4003,4003,false,Report UNKNOWN before first evaluation
ColumnMeaning
LLRThe LLSR id, exactly as written in the @post
HLRThe numeric HLSR id it contributes to
Derivedtrue when no HLSR directly asked for this LLSR
RationaleOne 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.

# scripts/gen_trace.py — sketch; ~100 lines in practice # # 1. walk include/ for *.hpp # 2. regex out every "@post <name> [<llsr-id>]" # 3. parse the id into module/class/function/path/suffix # 4. HLR column: numeric suffix if present, else look up an override map # 5. write docs/trace_data/<module>/<module>.csv, sorted # # Run with --check in CI: regenerate into a temp file and diff. A difference # means someone edited the CSV by hand or forgot to regenerate it, and the # pipeline fails with that diff as the message.

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.

// test/unit/testDriftMonitor.cpp #include "drift_monitor/DriftMonitor.hpp" #include "reqtrace/reqtrace.hpp" // provides VERIFIES #include <gtest/gtest.h> using namespace drift_monitor; TEST(DriftMonitorTest, ReportsUnknownBeforeEvaluation) { VERIFIES("drift_monitor-DriftMonitor-health-MAIN-4003"); DriftMonitor m; EXPECT_EQ(m.health(), Health::UNKNOWN); // never HEALTHY by default } TEST(DriftMonitorTest, DeclaresUnhealthyWhenRateExceedsLimit) { VERIFIES("drift_monitor-DriftMonitor-evaluate-MAIN-4001"); DriftMonitor m; m.initialise(/* limit_m_per_s = */ 1.0); m.evaluate(0.0, 1.0); // first sample: establishes the baseline m.evaluate(5.0, 1.0); // 5 m/s, five times the limit EXPECT_EQ(m.health(), Health::UNHEALTHY); } TEST(DriftMonitorTest, LatchesUnhealthyVerdict) { VERIFIES("drift_monitor-DriftMonitor-evaluate-ALT-4002"); DriftMonitor m; m.initialise(1.0); m.evaluate(0.0, 1.0); m.evaluate(5.0, 1.0); ASSERT_EQ(m.health(), Health::UNHEALTHY); m.evaluate(5.0, 1.0); // now perfectly well behaved EXPECT_EQ(m.health(), Health::UNHEALTHY); // must NOT clear itself } TEST(DriftMonitorTest, RejectsNonFiniteInput) { VERIFIES("drift_monitor-DriftMonitor-evaluate-ALT-1"); DriftMonitor m; m.initialise(1.0); m.evaluate(0.0, 1.0); m.evaluate(std::nan(""), 1.0); // The NaN must not have been compared against the limit: a NaN comparison // is false, which would have produced a spurious HEALTHY. EXPECT_NE(m.health(), Health::UNHEALTHY); EXPECT_EQ(m.health(), Health::HEALTHY); // unchanged from before }
The macro is bookkeeping, the assertions are the verification. 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 VERIFIES once 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.

ScriptQuestionTypical 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
# .gitlab-ci.yml stages: - static - requirements # ← cheap, runs early, blocks the expensive stages - build - test - evidence llsr-format: stage: requirements script: - python3 scripts/check_llsr_format.py trace-up-to-date: stage: requirements script: - python3 scripts/gen_trace.py --check # fails on any diff trace-resolves: stage: requirements script: - python3 scripts/check_trace.py --spec docs/req/specs/SPEC-4000-*/ # both directions: no dangling HLR ids, no HLSR without an LLSR unit-tests: stage: test script: - cmake --build build --target unit_tests - ./build/unit_tests --gtest_output=xml:reports/unit.xml artifacts: paths: [reports/, req_claims.json] # VERIFIES writes this verification-coverage: stage: evidence needs: [unit-tests] script: - python3 scripts/check_verification.py \ --claims req_claims.json \ --trace docs/trace_data/ \ --fail-under 100 # every LLSR, no exceptions artifacts: paths: [reports/traceability.html] # the review artefact
Put the requirements stage before the build. These checks take seconds and catch the errors a reviewer would otherwise have to find by eye. Running them after a twenty-minute build and test cycle means the feedback arrives long after the developer has moved on — and a check whose feedback is slow is a check people learn to route around.

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 thisDone when
1Create the directory skeleton of §3Paths exist; the scripts know where to look
2Write down the operational needs, unedited, in the stakeholders' wordsEvery need has a named owner
3Allocate an id block (e.g. 4000–4999) and write the HLSR YAMLEvery need maps to ≥1 HLSR; every HLSR has rationale and a verification method
4Review the HLSRs before writing codeSystems and safety have signed; this is the cheapest review you will ever do
5Write headers with @post LLSRs — signatures only, no bodiesEvery HLSR has ≥1 LLSR; the shape of the module is now visible
6Add the four scripts and the CI stage, and watch them failThey fail for the right reason: no implementation, no tests
7Implement, marking each branch with its LLSRFormat and trace checks pass
8Write tests with VERIFIESCoverage check passes at 100%
9Baseline the spec; changes now go through change controlA tagged, reviewable starting point exists
Step 6 before step 7 is the trick worth stealing. Turn the enforcement on while the repository is still empty, so you watch it fail and then make it pass. Retrofitting traceability onto a codebase that already works is an order of magnitude harder, because the scripts report hundreds of violations at once and the only rational response is to switch them off.

10 · Where this goes wrong

Anti-patternWhy it happensWhat 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.