name: Traceability # Mirrors JellyTau's traceability gate, including the reason it exists. # # That gate divided a traced count by frozen literal denominators while the # requirements file grew past them, reported 158% coverage, and so could never # fail its own threshold. Two rules follow, and the extractor's own tests # enforce both: # # 1. Denominators are parsed from docs/requirements.md at run time. # 2. Coverage is |traced ∩ defined| / |defined|, never a raw traced count. # # This job is static analysis of source comments plus markdown parsing, so it # needs no GPU and no Android SDK — only the Rust toolchain. on: push: branches: [main, master, develop] pull_request: branches: [main, master, develop] jobs: traceability: runs-on: linux/amd64 name: Requirement traces steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Cache cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: traces-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} # The gate's own arithmetic is the thing being trusted, so its tests run # before it does. Untested gate logic is exactly how JellyTau's 158% went # unnoticed for months. - name: Test the extractor run: cargo test -p traceability # Structural failures are unconditional and do not depend on the coverage # threshold: zero requirements parsed, zero files scanned, a ratio above # 100%, or any orphan tag all fail the build. A misconfigured run must not # report a plausible-looking 0%. - name: Traceability gate run: cargo run -q -p traceability -- check - name: Regenerate matrix and check it is committed run: | set -e cargo run -q -p traceability -- report if ! git diff --quiet docs/traceability.md; then echo "" echo "docs/traceability.md is out of date." echo "Run: cargo run -p traceability -- report" git diff --stat docs/traceability.md exit 1 fi # Advisory, not blocking: not every file implements a requirement, and a # tag on every function is noise that rots faster than it helps. Tag the # unit that decides. - name: Check changed files for tags if: github.event_name == 'pull_request' run: | set -e CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" \ | grep -E '\.(rs|slint|wgsl)$' || true) [ -z "$CHANGED" ] && { echo "No source files changed."; exit 0; } MISSING=0 for file in $CHANGED; do case "$file" in */tests/*|*/test_*|tools/*) continue ;; esac [ -f "$file" ] || continue if ! grep -q 'TRACES:' "$file"; then echo " no TRACES tag: $file" MISSING=$((MISSING + 1)) fi done if [ "$MISSING" -gt 0 ]; then echo "" echo "$MISSING changed file(s) carry no requirement tag." echo "Format: /// TRACES: FR-CAT-1, FR-CAT-2 | NFR-P1" echo " (comma separates IDs, pipe groups types)" fi - name: Summary if: always() run: head -30 docs/traceability.md || true