# Design & Architecture This page describes jamb's internal architecture and the reasoning behind key design choices. ## Package Overview jamb is organized into focused packages, each with a single responsibility: | Package | Role | |---------|------| | `core` | Domain models (`Item`, `LinkedTest`, `ItemCoverage`, `TraceabilityGraph`) | | `storage` | Filesystem I/O: discovery, YAML reading/writing, graph building, validation | | `pytest_plugin` | pytest hooks, marker extraction, test-outcome recording | | `matrix` | Traceability-matrix generation in multiple formats | | `publish` | Human-readable document rendering (HTML, Markdown, DOCX) | | `cli` | Click-based command-line interface | Shared modules at the package root include `config` (configuration loading from `pyproject.toml`) and `yaml_io` (YAML utilities). ```{mermaid} graph TD CLI[cli] --> Storage[storage] CLI --> Matrix[matrix] CLI --> Publish[publish] Plugin[pytest_plugin] --> Storage Plugin --> Matrix Plugin --> Core[core] Matrix --> Core Publish --> Core Storage --> Core Storage --> Config[config] Storage --> YamlIO[yaml_io] ``` **Design rationale.** Domain models in `core/` have zero I/O dependencies, making them easy to test in isolation. All filesystem interaction is confined to `storage/`, so the rest of the codebase works with pure in-memory data structures. ## Domain Models The core domain lives in `core/models.py`. `Item` represents a single requirement, informational note, or heading. Its key fields are `uid`, `text`, `document_prefix`, `active`, `type`, `links`, `header`, `reviewed`, `derived`, and `testable`. `LinkedTest` models a test-to-requirement link. It records `test_nodeid`, `item_uid`, and `test_outcome`, plus optional `notes`, `test_actions`, `expected_results`, and `actual_results` captured via the `jamb_log` fixture. `ItemCoverage` pairs an `Item` with its `LinkedTest` list and exposes `is_covered` / `all_tests_passed` properties. `TraceabilityGraph` is the complete bidirectional graph. It stores items by UID and maintains two adjacency lists (`item_parents`, `item_children`) plus a document-level DAG (`document_parents`). It provides BFS-based `get_ancestors()` and `get_descendants()` traversals. `TraceabilityGraph` uses adjacency lists rather than a matrix because the graph is sparse -- most items link to only one or two parents. ## The Document DAG Documents form a **directed acyclic graph** (DAG), not a simple tree, because a child document can trace to multiple parents. For example, the Software Requirements Specification (SRS) traces to both System Requirements (SYS) and Risk Controls (RC). The `DocumentDAG` class (`storage/document_dag.py`) manages document relationships and provides two critical operations: 1. **Topological sort** (Kahn's algorithm) -- Orders documents so that parents are always loaded before children. This ensures that when an item's links are resolved, the target items already exist in memory. 2. **Cycle detection** -- After running Kahn's algorithm, any nodes not visited form cycles. The validator reports the specific prefixes involved. ```{mermaid} graph TD PRJ[PRJ
Project Plan] --> UN[UN
User Needs] PRJ --> HAZ[HAZ
Hazard Analysis] UN --> SYS[SYS
System Requirements] SYS --> SRS[SRS
Software Requirements] HAZ --> RC[RC
Risk Controls] RC --> SRS ``` ## Data Flow The discovery-to-graph pipeline runs in three phases: 1. **Discovery** (`storage/discovery.py`) -- Walks the filesystem from the project root, finding all `.jamb.yml` config files via `Path.rglob()`. Each config defines a document prefix and its parent documents. The result is a `DocumentDAG`. 2. **Graph building** (`storage/graph_builder.py`) -- Iterates documents in topological order, reading YAML item files via `storage/items.py`. Each file becomes an `Item` added to the `TraceabilityGraph`. 3. **Item reading** (`storage/items.py`) -- Parses individual YAML files, normalizing two link formats (plain `- UID` and hashed `- UID: hash_value`) into a consistent structure. ```{mermaid} flowchart LR FS["Filesystem
.jamb.yml files"] --> Disc["discovery.py
discover_documents()"] Disc --> DAG["DocumentDAG"] DAG --> GB["graph_builder.py
build_traceability_graph()"] GB --> Items["items.py
read_document_items()"] Items --> TG["TraceabilityGraph"] ``` ## pytest Integration Lifecycle The pytest plugin (`pytest_plugin/plugin.py` and `pytest_plugin/collector.py`) integrates with pytest through a chain of hooks: ```{mermaid} sequenceDiagram participant U as User participant P as pytest participant Pl as plugin.py participant C as RequirementCollector participant M as markers.py participant G as TraceabilityGraph participant Mx as matrix/generator.py U->>P: pytest --jamb P->>Pl: pytest_addoption() P->>Pl: pytest_configure() Pl->>C: create collector C->>G: discover & build graph P->>C: pytest_collection_modifyitems() C->>M: get_requirement_markers() M-->>C: list of UIDs C->>C: create LinkedTest entries loop each test P->>C: pytest_runtest_makereport() C->>C: record outcome, notes, actions end P->>Pl: pytest_sessionfinish() Pl->>Mx: generate_matrix() (if --jamb-trace-matrix or --jamb-test-matrix) P->>Pl: pytest_terminal_summary() Pl->>U: coverage report ``` Key hooks in order: 1. **`pytest_addoption`** -- Registers `--jamb`, `--jamb-test-matrix`, `--jamb-trace-matrix`, `--jamb-fail-uncovered`, `--jamb-documents`, `--jamb-tester-id`, `--jamb-software-version`, `--trace-from`, `--include-ancestors`. 2. **`pytest_configure`** -- Creates a `RequirementCollector` that discovers documents and builds the graph. 3. **`pytest_collection_modifyitems`** -- Scans collected tests for `@pytest.mark.requirement` markers and creates `LinkedTest` entries. 4. **`pytest_runtest_makereport`** -- After each test's call phase, records the outcome and any data from `jamb_log`. 5. **`pytest_sessionfinish`** -- Generates the traceability matrix if requested; sets exit code if uncovered items exist and `--jamb-fail-uncovered` is set. 6. **`pytest_terminal_summary`** -- Prints coverage statistics, uncovered items, and unknown UIDs. ## Suspect Link Detection When an item's content changes, any items that link *to* it may become outdated. jamb detects this through content hashing. **Hash computation** (`storage/items.py: compute_content_hash()`): the function concatenates `text`, `header`, sorted `links`, and `type` with `|` as delimiter, computes a SHA-256 digest, and encodes it as URL-safe base64 with padding stripped. **Why hashing over timestamps?** Hashes are deterministic across git clones -- every developer and CI runner computes the same hash for the same content. Timestamps would break on fresh clones or rebases. **Detection flow** (`storage/validation.py: _check_suspect_links()`): ```{mermaid} flowchart TD Start["validate()"] --> Load["Load all active items"] Load --> Loop{"For each item
with stored link hashes"} Loop --> Read["Read raw YAML to
get link_hashes dict"] Read --> Hash["Compute current hash
of linked item"] Hash --> Compare{"Stored hash ==
current hash?"} Compare -- Yes --> Next["Link is clean"] Compare -- No --> Flag["Flag as suspect link
(warning)"] Next --> Loop Flag --> Loop Loop -- "No stored hash" --> Warn["Warn: link not verified"] ``` ## Matrix & Publishing The matrix generator (`matrix/generator.py`) dispatches to format-specific renderers based on a simple if/elif chain. Five formats are supported: **HTML**, **Markdown**, **JSON**, **CSV**, and **XLSX**. Format modules are imported lazily inside the dispatch function. This avoids requiring optional dependencies (like `openpyxl` for XLSX) when they aren't used. The publish package renders human-readable requirement documents in **HTML**, **Markdown**, and **DOCX** formats, with internal hyperlinks between items and document-order grouping. HTML and DOCX renderers live in `publish/formats/`; Markdown rendering is handled in the CLI layer. ## Validation Architecture The `validate()` function (`storage/validation.py`) is the single entry point for all validation. It runs nine independent checks across three categories. Eight are controlled by keyword flags (all defaulting to `True`); DAG acyclicity always runs: **Structural checks:** - **DAG acyclicity** -- Detects cycles in the document hierarchy. - **Item link cycles** -- DFS with three-color marking (white/gray/black) to find cycles in item-to-item links. - **Link validity and conformance** -- A single `_check_links` function that catches self-links, links to non-existent items, links to inactive items, non-normative items with links, and verifies links point to items in valid parent documents per the DAG. **Content checks:** - **Suspect links** -- Content-hash comparison (see above). - **Review status** -- Checks that normative items have a `reviewed` hash matching current content. - **Empty text** -- Flags items with blank or whitespace-only text. **Completeness checks:** - **Child links** -- Non-leaf-document items should have children linking to them. - **Unlinked items** -- Child-document items (non-derived) should link to a parent document. - **Empty documents** -- Flags documents containing no items. Each check returns a list of `ValidationIssue` objects with a `level` (error, warning, or info), the relevant `uid` or `prefix`, and a descriptive message. ## Design Decisions Summary | Decision | Alternative | Rationale | |----------|------------|-----------| | DAG over tree | Tree hierarchy | Multi-parent tracing (e.g., software requirements linked to both system requirements and risk controls) | | Content hashing (SHA-256) | Timestamps | Deterministic across git clones; no breakage on fresh checkout or rebase | | YAML per item | Single file per document | Better git diffs, fewer merge conflicts when multiple authors edit the same document | | In-memory graph | Database | Data fits in memory; no need for a query language or external process | | pytest hooks | Custom test runner | Leverages the existing pytest ecosystem, markers, and reporting | | Lazy format imports | Plugin registry | Simple and sufficient for a small, stable set of output formats |