Tutorial#

This tutorial covers requirements traceability workflows for regulated software projects. We use a Patient Monitoring System (Class B medical device software per IEC 62304) as our example.

Prerequisites#

You’ll need Python 3.10+, Git (for version-controlled requirements), and a basic understanding of requirements traceability.

Install jamb:

pip install jamb

Tip

A complete example project is available in the tutorial/ directory of the GitHub repository. Clone it to follow along:

git clone https://github.com/vanandrew/jamb.git
cd jamb/tutorial

Getting Started#

Initialize a New Project#

jamb uses git as its storage backend, so every project starts as a git repository. The jamb init command scaffolds the standard IEC 62304 document hierarchy — a set of directories and config files that define how requirements trace from high-level user needs down to testable software specifications.

mkdir my-project && cd my-project
git init
jamb init

This creates a reqs/ folder with PRJ, UN, SYS, SRS, HAZ, and RC documents, along with a [tool.jamb] configuration section in pyproject.toml (if the file exists).

Basic Commands#

The core workflow is: create items (individual requirements) within documents, then link child items to parent items to build the traceability chain. Links always point upward — an SRS item links to the SYS item it implements, and a SYS item links to the UN item it satisfies. This upward linking is how jamb records that every low-level requirement traces back to a user need.

# Add items
jamb item add UN
jamb item add SYS
jamb item add SRS

# Create links
jamb link add SYS001 UN001
jamb link add SRS001 SYS001

# Edit content
jamb item edit SRS001

# View structure
jamb doc list
jamb item list

Check Coverage#

There are two ways to verify coverage. jamb check is static — it scans test files for requirement markers without running tests. pytest --jamb is runtime — it executes tests and records outcomes. In CI, you typically use both: jamb check for fast feedback and pytest --jamb for the full traceability matrix.

jamb check
pytest --jamb --jamb-trace-matrix matrix.html

Configuration#

The test_documents setting tells jamb which documents require test coverage (usually the leaf documents like SRS). Setting fail_uncovered to true makes CI fail if any requirement lacks a test. The matrix options control where the matrices are written.

Add to pyproject.toml:

[tool.jamb]
test_documents = ["SRS", "SYS"]
fail_uncovered = false
trace_matrix_output = "traceability.html"
test_matrix_output = "test-records.html"

Tip: When trace_matrix_output or test_matrix_output is set in [tool.jamb], the matrices are automatically generated at the specified paths on each pytest --jamb run, without needing to pass --jamb-trace-matrix or --jamb-test-matrix on the command line. This is useful for ensuring the matrices are always up to date.

What You’ll Learn#

The rest of this tutorial walks through the full lifecycle of a regulated project: handling review cycles when requirements change, publishing documentation for regulatory submissions, batch-importing requirements from stakeholders, and enforcing traceability in CI/CD pipelines.

Key Concept: Git-Linked Documents#

jamb documents are stored as plain text files in your git repository. Every change to a requirement is a git commit with full history, so teams can collaborate on requirements using standard git workflows — branches, pull requests, and code review. Git history provides the audit trail that regulators need to see how requirements evolved, and the “suspect link” system uses content hashing to detect when upstream requirements have changed since downstream items were last reviewed.

When you run jamb item add or jamb item edit, you’re creating or modifying files that should be committed to git like any other source file. This design makes requirements a first-class part of your codebase rather than documents stored in external tools.

Part 1: Project Overview#

The Patient Monitoring System#

This example project implements a patient vital signs monitoring system with two document hierarchies. The following breakdown shows each document type and its item count:

Project Root:

  • 1 Project Requirement (PRJ001): Top-level system definition

User Needs Hierarchy:

  • 5 User Needs (UN001-UN005): High-level user requirements → link to PRJ

  • 8 System Requirements (SYS001-SYS008): System behavior specifications → link to UN

  • 15 Software Requirements (SRS001-SRS015): Detailed software specifications → link to SYS

Risk Management Hierarchy:

  • 2 Hazards (HAZ001-HAZ002): Identified hazardous situations → link to PRJ

  • 2 Risk Controls (RC001-RC002): Mitigations for identified hazards → link to HAZ

Both hierarchies share the same project root (PRJ), allowing a unified hierarchy.

Cross-linking: Some SRS items also link to RC items, demonstrating that a software requirement can trace to both its functional parent (SYS) and a risk control (RC). This shows jamb’s support for multi-parent traceability.

Exploring the Hierarchy#

View the document structure:

cd tutorial
jamb doc list

Output:

Found 6 documents:
  PRJ: 1 active items (parents: (root))
  UN: 5 active items (parents: PRJ)
  SYS: 8 active items (parents: UN)
  SRS: 15 active items (parents: SYS, RC)
  HAZ: 2 active items (parents: PRJ)
  RC: 2 active items (parents: HAZ)

Hierarchy:
`-- PRJ
    |-- UN (parents: PRJ)
    |   `-- SYS (parents: UN)
    |       `-- SRS (parents: SYS, RC)
    `-- HAZ (parents: PRJ)
        `-- RC (parents: HAZ)
            `-- SRS (parents: SYS, RC)

List all requirements:

jamb item list

Inspect a single requirement:

jamb item show SRS001

Verify all links are valid:

jamb info

Part 2: Batch Import Requirements#

In regulated workflows, requirements often originate in Word documents, spreadsheets, or design review meetings. Rather than manually creating each item via jamb item add, you can author them in a single YAML file and import the batch. This is especially useful at project kickoff or when incorporating feedback from clinical experts who don’t use the CLI.

Examining the Import File#

The import file has a documents section (for creating new document types, left empty here since we’re using existing ones) and an items section where each entry specifies a UID, text, header, and links. The links field establishes traceability — each item points to its parent, just like items created via the CLI.

Look at sample-import.yml:

# Sample requirements for import demonstration
documents: []  # Use existing documents

items:
  # Additional user need (links to project root)
  - uid: UN006
    text: |
      User shall be able to configure personal alert preferences
      for different vital sign types.
    header: Alert Preferences
    links: [PRJ001]

  # Additional system requirement
  - uid: SYS009
    text: |
      System shall allow users to customize alert thresholds
      per vital sign type within clinically safe ranges.
    header: Customizable Thresholds
    links: [UN006]

  # Additional software requirements
  - uid: SRS016
    text: |
      Software shall store user alert preferences in the database
      with per-vital-sign threshold configurations.
    header: Preference Storage
    links: [SYS009]

  - uid: SRS017
    text: |
      Software shall validate custom thresholds against
      clinically safe minimum and maximum bounds.
    header: Threshold Validation
    links: [SYS009]

  # Additional hazard (links to project root)
  - uid: HAZ003
    text: |
      Custom alert thresholds outside safe ranges could mask
      critical patient conditions.
    header: Unsafe Custom Thresholds
    links: [PRJ001]

  # Additional risk control
  - uid: RC003
    text: |
      System shall enforce clinically validated minimum and maximum
      bounds on all user-configurable thresholds.
    header: Threshold Bounds Enforcement
    links: [HAZ003]

Preview the Import#

In a regulated project, unintended changes to the requirements baseline can trigger review cycles and audit findings. The --dry-run flag shows exactly what will be created without modifying any files, so you can verify the import before committing to it.

Always preview before importing:

jamb import sample-import.yml --dry-run

Output:

Dry run - no changes will be made:
  Would create item: UN006 (links: PRJ001)
  Would create item: SYS009 (links: UN006)
  Would create item: SRS016 (links: SYS009)
  Would create item: SRS017 (links: SYS009)
  Would create item: HAZ003 (links: PRJ001)
  Would create item: RC003 (links: HAZ003)

Would create 6 items

Execute the Import#

jamb import sample-import.yml -v

Updating Existing Items#

By default, import skips items that already exist. To update existing items, use the --update flag:

jamb import sample-import.yml --update

When updating, text, header, and links are replaced with values from the import file, while fields not in the import file (like active) are preserved. The reviewed status is cleared, marking the item as needing re-review.

Preview updates before applying:

jamb import sample-import.yml --update --dry-run

Verify the Import#

jamb item list
jamb info

You should now have 39 items (1 PRJ + 6 UN + 9 SYS + 17 SRS + 3 HAZ + 3 RC).

Part 3: The Review Cycle#

IEC 62304 requires change impact analysis — when a requirement changes, you must assess whether downstream artifacts (more detailed requirements, tests) are still valid. jamb automates the detection side: it stores a content hash in each link, and when the parent item’s content changes, the hash no longer matches, flagging the link as “suspect.” This section walks through the full cycle: making a change, seeing the suspect flags, reviewing the impact, and clearing the flags.

Making a Change#

Edit a system requirement:

jamb item edit SYS001

Change the text (for example, add “multi-factor” to authentication):

text: |
  System shall authenticate users with username, password, and multi-factor authentication.

Understanding the Impact#

In practice, you’d read the changed requirement to understand what was modified, then check each suspect downstream item to decide whether it needs updating. In a real project, this review might involve the original author, a domain expert, or a formal review meeting — the tooling flags the items, but the engineering judgment is yours.

Trace which items are affected:

jamb item show SYS001

The links field shows upstream, and you can find downstream items by searching for items that link to SYS001.

Clearing Suspect Status#

Clearing suspect status is a two-step process. jamb review mark records that you’ve reviewed the item’s content (updating the reviewed hash), and jamb review clear records that you’ve reviewed the item’s links to its parents (updating the link hashes). Both steps are needed because a change might affect either the item itself or its relationship to its parent. Once both hashes are current, jamb validate will report a clean state.

# Mark items as reviewed and clear their suspect links
jamb review mark SRS001
jamb review clear SRS001

jamb review mark SRS002
jamb review clear SRS002

jamb review mark SRS003
jamb review clear SRS003

# Or do it for an entire document at once
jamb review mark SRS
jamb review clear SRS

# Or all documents
jamb review mark all
jamb review clear all

You can also clear suspect links to a specific parent only:

jamb review clear SRS001 SYS001

Verify clean state:

jamb validate

Part 4: Publishing for Regulatory Submission#

A regulatory submission package typically includes human-readable requirement documents for each specification level, a traceability matrix showing the chain from user needs through software requirements to test results, and evidence that all changes have been reviewed. jamb’s publish and matrix commands generate these artifacts directly from your requirements data, so the documentation stays in sync with the source of truth.

Generate HTML Documentation#

Export all documents as a single combined HTML file:

jamb publish all ./docs/requirements.html --html

This creates a single docs/requirements.html file containing all documents.

Generate Markdown for GitHub#

For GitHub-friendly documentation:

jamb publish SRS ./docs/srs.md --markdown

Generate Traceability Matrix#

The traceability matrix is the central audit artifact — it shows every requirement, its parent chain, the tests that verify it, and whether those tests passed. Auditors use this to confirm that every requirement is implemented and verified. The matrix is generated from a live test run, so it reflects the actual state of the codebase.

pytest tests/ --jamb --jamb-trace-matrix ./docs/matrix.html

For different formats (inferred from file extension):

# JSON for tooling integration
pytest tests/ --jamb --jamb-trace-matrix matrix.json

# Markdown for GitHub
pytest tests/ --jamb --jamb-trace-matrix matrix.md

For IEC 62304 compliant test records, include tester and version metadata:

pytest tests/ --jamb --jamb-trace-matrix matrix.html \
    --jamb-tester-id "QA Team" \
    --jamb-software-version "1.0.0"

For example, a markdown matrix renders as:

Metadata#

  • Software Version: 1.0.0

  • Tester: QA Team

  • Date: 2026-01-28T14:30:00Z

  • Environment: Darwin 25.2.0, Python 3.12.0, arm64, arm, dev-machine, 10 cores

  • Test Tools: jamb 1.2.0, pytest 8.0.0, pytest-cov 4.1.0

Coverage Details#

UID

Description

Traces To

Tests

Test Actions

Expected Results

Actual Results

Notes

Status

SRS001

Software shall authenticate users

SYS001, UN001

test_credential_validation [passed]

Submit valid credentials

Auth returns True

auth() returned True

Verified auth paths

Passed

SRS002

Software shall lock account after 3 failed attempts

SYS001, UN001

test_account_lockout [failed]

Submit 3 invalid passwords

Account is locked

account.locked = False

[FAILURE] AssertionError

Failed

SRS003

Software shall display heart rate in real-time

SYS002, UN002

-

-

-

-

-

Not Covered

SRS099

User manual shall be provided

SYS003

-

-

-

-

Verified by inspection

N/A

What Auditors Want to See#

Auditors look for a complete traceability chain — every SRS traces to SYS, every SYS to UN, and every RC traces to HAZ — with software requirements that implement risk controls linking to both SYS and RC. They expect every testable requirement to have at least one linked test, no suspect links (demonstrating that all changes have been reviewed), and git commit history showing how requirements evolved over time.

The traceability matrix and test records are one component of the evidence package auditors review. You will also need design documentation, risk management files (ISO 14971), software development plans, and other lifecycle artifacts that jamb does not generate.

Part 5: Export for External Review#

jamb export serializes your requirements into a portable YAML file that stakeholders can review outside the repository. This supports a round-trip workflow: export your current requirements, send them to a clinical expert or product manager for review, receive the edited YAML back, and import the changes. Because the import command handles conflicts safely (skipping existing items by default), this workflow supports incremental collaboration.

Exporting Requirements#

Export all requirements for stakeholder review:

mkdir -p review
jamb export ./review/all-requirements.yml

Export specific documents:

jamb export ./review/srs-only.yml --documents SRS

Round-trip Workflow#

This workflow bridges the gap between engineering teams working in git and domain experts working in documents. The YAML format is simple enough for non-developers to read and edit, and the import/export cycle keeps the requirements repository as the single source of truth.

  1. Export: jamb export requirements.yml

  2. Send to stakeholders: Email the YAML file

  3. Receive feedback: Stakeholders edit the YAML

  4. Import changes: jamb import requirements.yml

Import skips existing items and adds new ones, making it safe for incremental updates.

Part 6: CI/CD Integration#

Automated pipelines should enforce traceability on every commit by validating the requirements tree, checking test coverage, and generating the traceability matrix as a build artifact. This catches broken links, missing coverage, and unreviewed changes before they reach the main branch — preventing audit findings from accumulating.

GitHub Actions Workflow#

Create .github/workflows/traceability.yml:

name: Requirements Traceability

on: [push, pull_request]

jobs:
  traceability:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.10'

      - name: Install dependencies
        run: pip install jamb pytest

      - name: Validate requirements
        run: jamb validate

      - name: Check requirement coverage
        run: jamb check

      - name: Run tests with traceability
        run: pytest --jamb --jamb-fail-uncovered --jamb-trace-matrix matrix.html

      - name: Upload traceability matrix
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: traceability-matrix
          path: matrix.html

In this workflow, jamb validate checks the structural integrity of the requirements tree (links, cycles, suspect items). jamb check verifies that every requirement in the configured test documents has at least one linked test. pytest --jamb --jamb-fail-uncovered runs the actual tests and fails if any requirement lacks coverage. Finally, the artifact upload preserves the matrix for review.

Key CI Commands#

Command

Purpose

jamb validate

Validate requirements tree (links, suspect items)

jamb info

Display document structure and hierarchy

jamb check

Check test coverage without running tests

pytest --jamb --jamb-fail-uncovered

Fail if requirements lack tests

Pre-commit Validation#

Pre-commit hooks catch validation errors before they even enter a commit, giving developers immediate feedback when they break a link or leave an item unreviewed.

Add to .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: jamb-validate
        name: Validate requirements
        entry: jamb validate
        language: system
        pass_filenames: false

Part 7: Advanced Validation#

jamb validate runs a suite of independent checks covering structural integrity, content correctness, and completeness. By default all checks run, but verbose mode (-v) includes info-level issues in addition to warnings and errors, giving you more detail about the validation results.

Detailed Tree Inspection#

jamb validate -v

Shows all validation issues including info-level details.

Specific Document Coverage#

Check coverage for specific documents:

jamb check --documents SRS,SYS

Understanding Coverage Reports#

The coverage report summarizes how well your test suite covers the requirements. In a regulatory context, uncovered requirements are gaps that auditors will flag, and failing requirements indicate verification issues that must be resolved before release.

The coverage report shows:

  • Covered: Requirements with at least one linked test

  • Uncovered: Requirements without tests (potential gaps)

  • Passing: Covered requirements where all tests pass

  • Failing: Covered requirements where at least one test fails

Troubleshooting#

Missing Test Coverage#

Problem: jamb check shows uncovered requirements.

Solution: Add tests with the @pytest.mark.requirement marker:

@pytest.mark.requirement("SRS001")
def test_something():
    ...

Part 8: Test Records in the Traceability Matrix#

The traceability matrix captures structured test record data aligned with IEC 62304 §5.7.5. This is valuable for documenting what actions a test performed and what results were expected, showing failure details when tests fail, and providing evidence for auditors about test execution.

The jamb_log fixture provides four methods for structured test records:

Method

Column

Purpose

jamb_log.test_action(...)

Test Actions

What the test does (steps performed)

jamb_log.expected_result(...)

Expected Results

What should happen (acceptance criteria)

jamb_log.actual_result(...)

Actual Results

What actually happened (observed behavior)

jamb_log.note(...)

Notes

Free-form observations, context, or commentary

Test Actions and Expected Results#

Use test_action() and expected_result() to document the steps and acceptance criteria of a test. These map directly to the “Test Actions” and “Expected Results” columns in the traceability matrix:

import pytest

@pytest.mark.requirement("SRS005")
def test_heart_rate_boundaries(jamb_log):
    """Test heart rate validation at boundary values."""
    jamb_log.test_action("Submit heart rate at lower boundary (30 BPM)")
    jamb_log.expected_result("Value is accepted as valid")
    assert validate_heart_rate(30) is True

    jamb_log.test_action("Submit heart rate at upper boundary (250 BPM)")
    jamb_log.expected_result("Value is accepted as valid")
    assert validate_heart_rate(250) is True

    jamb_log.test_action("Submit heart rate below minimum (29 BPM)")
    jamb_log.expected_result("Value is rejected as invalid")
    assert validate_heart_rate(29) is False

Notes#

Use note() for free-form observations that don’t fit the action/result structure:

@pytest.mark.requirement("SRS012")
def test_data_encryption(jamb_log):
    """Verify patient data encryption meets HIPAA requirements."""
    jamb_log.note("Using PHI sample data set #3")
    jamb_log.note("AES-256 encryption algorithm configured")

    jamb_log.test_action("Encrypt patient data")
    jamb_log.expected_result("Output is not plaintext readable")

    encrypted = encrypt_patient_data(sample_phi)
    assert is_encrypted(encrypted)
    assert sample_phi not in encrypted

Generating the Matrix#

When you generate the traceability matrix, all four fields appear in their own columns:

pytest tests/ --jamb --jamb-trace-matrix matrix.html

The HTML matrix will show each test’s name with its pass/fail status, a Test Actions column listing the steps performed, an Expected Results column with the acceptance criteria, an Actual Results column with observed behavior, a Notes column with free-form observations, and automatically captured failure messages if the test fails.

Automatic Failure Capture#

When a test fails, jamb automatically captures the failure message and includes it in the Notes column. This helps auditors understand why a test failed without needing to re-run it:

@pytest.mark.requirement("SRS007")
def test_alert_threshold():
    # If this fails, the assertion message is captured
    assert current_threshold == expected, f"Threshold {current_threshold} != {expected}"

The matrix will show [FAILURE] AssertionError: Threshold 95 != 100 in the Notes column.

Test Records in Different Output Formats#

Test actions, expected results, actual results, and notes appear in all matrix output formats:

Format

How They Appear

HTML

Separate columns with styled divs; notes are color-coded for failures

JSON

"test_actions", "expected_results", "actual_results", and "notes" arrays per linked test

CSV

Semicolon-separated in “Test Actions”, “Expected Results”, “Actual Results”, and “Notes” columns

XLSX

Newline-separated in dedicated columns with text wrap

Markdown

Semicolon-separated in dedicated columns

When to Use Each Method#

test_action() — what the test does:

  • “Enter username and password”

  • “Click submit button”

  • “Send POST request to /api/login”

expected_result() — what should happen:

  • “Login succeeds and session token is returned”

  • “Error message is displayed”

  • “Response status is 401 Unauthorized”

actual_result() — what actually happened:

  • “auth() returned True”

  • “Response status was 200 OK”

  • “Threshold value was 95”

note() — additional context:

  • “Using test data set #3”

  • “Server configured with TLS 1.3”

  • “Boundary value selected per risk analysis RA-005”

Example for regulatory evidence:

@pytest.mark.requirement("SRS001")
def test_credential_validation(jamb_log):
    """Verify credential validation per SRS001."""
    jamb_log.test_action("Submit valid credentials (nurse1 / secure123)")
    jamb_log.expected_result("Authentication returns True")
    assert validate_credentials("nurse1", "secure123") is True

    jamb_log.test_action("Submit invalid password (nurse1 / wrong)")
    jamb_log.expected_result("Authentication returns False")
    assert validate_credentials("nurse1", "wrong") is False

    jamb_log.note("Verified both positive and negative authentication paths")

This provides auditors with structured evidence of what was tested, what was expected, and any additional observations.

Import Conflicts#

Problem: Import skips existing items.

Solution: Use --update to modify existing items:

jamb import requirements.yml --update

This updates text, header, and links while preserving other fields. The reviewed status is cleared to trigger re-review.

Invalid Requirement References#

Problem: Tests reference requirements that don’t exist.

Solution: Run jamb info to check document structure, then verify the UID in your test matches an actual requirement.

Summary#

Complete Workflow#

  1. Develop: Add/edit requirements with jamb item add and jamb item edit

  2. Link: Connect requirements with jamb link add

  3. Validate: Check structure with jamb info and jamb validate

  4. Test: Run pytest --jamb to verify coverage

  5. Review: Mark items reviewed with jamb review mark and clear suspect links with jamb review clear

  6. Publish: Generate documentation with jamb publish

Key Commands Reference#

Task

Command

List documents

jamb doc list

List items

jamb item list

Show item

jamb item show UID

Add item

jamb item add PREFIX

Edit item

jamb item edit UID

Add link

jamb link add CHILD PARENT

Validate

jamb validate

Document info

jamb info

Check coverage

jamb check

Review item

jamb review mark UID

Clear suspect links

jamb review clear UID

Publish HTML

jamb publish all ./docs/requirements.html --html

Import batch

jamb import file.yml

Export

jamb export file.yml

CI/CD Checklist#

  • jamb validate passes (no suspect links)

  • jamb check shows no uncovered requirements

  • pytest --jamb --jamb-fail-uncovered passes

  • Traceability matrix uploaded as artifact

Derived Requirements#

Items that don’t link to any parent, should be marked as derived: true:

# SRS item that only implements a risk control
active: true
derived: true  # No SYS parent needed
header: Input Validation
links:
- RC001  # Links to risk control only
text: |
  Software shall validate all input against buffer overflow attacks.

This tells jamb the requirement is intentionally not linked to the parent document (SYS) because it emerges from outside considerations.

Heading Levels#

For longer documents, use type: heading with a level field to create nested section structure. The level controls the rendered heading depth — <h1><h6> in HTML, the equivalent in DOCX, and #-depth in markdown.

# SRS018.yml — top-level section heading
active: true
type: heading
level: 1
header: Alerting Requirements
text: |
  Requirements governing alert delivery, threshold configuration, and severity
  indicators for the patient monitoring system.

Nest subsections by incrementing level:

# SRS019.yml — subsection under SRS018
active: true
type: heading
level: 2
header: Alert Thresholds
text: |
  Configuration of thresholds that trigger alert conditions.
# SRS020.yml — second subsection under SRS018
active: true
type: heading
level: 2
header: Alert Delivery
text: |
  Mechanisms for delivering alerts to clinical staff.

The level maps directly to the rendered output:

level: 1  → <h1> / #   in markdown   (major chapter)
level: 2  → <h2> / ##  in markdown   (section, default)
level: 3  → <h3> / ### in markdown   (subsection)

When level is omitted, heading items default to depth 2. Jamb will warn if level is set on a requirement or info item.