name: Traceability Validation # Mirrors JellyTau's .gitea/workflows/traceability-check.yml, adapted for a # C++/Python repo: the extractor is Python and needs nothing but python3, so # there is no toolchain install step and no jq. # # NOTE: the runner here is an Intel N100 with no discrete GPU. This job is only # ever static analysis of source comments plus markdown parsing, so it is cheap; # the requirements it reports as "tagged but unexecuted" are the ones that need # a GPU host, and they are deliberately never counted as covered. on: push: branches: - main - master - develop pull_request: branches: - main - master - develop jobs: validate-traces: runs-on: linux/amd64 name: Check requirement traces steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check Python is available run: | set -e command -v python3 >/dev/null 2>&1 || { echo "python3 is missing from the runner image." echo "The traceability tooling is stdlib-only Python 3.9+;" echo "no other dependency is needed." exit 1 } python3 --version # The gate's own arithmetic is the thing being trusted, so its tests run # before it does. JellyTau's gate was believed for months while it was # dividing by frozen literals; untested gate logic is how that happens. - name: Test the extractor run: python3 scripts/traceability/test_extract_traces.py # Threshold policy lives in traceability-gate.sh, not here, so local runs # and CI runs cannot disagree about what "passing" means. Denominators # come from docs/requirements.md at run time and are never hardcoded -- # in this file or anywhere else. - name: Traceability gate run: sh scripts/traceability/traceability-gate.sh - name: Check modified files for traces if: github.event_name == 'pull_request' run: | set -e echo "Checking modified sources for TRACES tags..." CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" \ | grep -E '\.(cpp|cc|cxx|hpp|hxx|h|cu|cuh|py)$' || true) if [ -z "$CHANGED" ]; then echo "No C++/Python files changed." exit 0 fi echo "Changed files:" echo "$CHANGED" | sed 's/^/ /' echo "" # Advisory by design: not every file implements a requirement, and a # tag on every function is noise that rots faster than it helps # (CLAUDE.md: tag the unit that decides). This step exists to prompt, # not to block. The blocking checks are in the gate step above. # # Piped into the loop rather than a here-string, and `case` rather # than `[[ == ]]`, so this works under dash as well as bash. The loop # body runs in a subshell, so misses are recorded in a file. MISSING=$(mktemp) echo "$CHANGED" | while IFS= read -r file; do case "$file" in */test_*.py|*_test.py|tests/*|*/tests/*) continue ;; esac [ -f "$file" ] || continue if ! grep -q 'TRACES:' "$file"; then echo " no TRACES tag: $file" echo "$file" >> "$MISSING" fi done COUNT=$(wc -l < "$MISSING" | tr -d ' ') rm -f "$MISSING" if [ "$COUNT" -gt 0 ]; then echo "" echo "$COUNT changed file(s) carry no requirement tag." echo "Format: // TRACES: AR-012, AR-013 | SR-002" echo " (pipe separates requirement types, comma separates IDs)" echo "A deliberate invariant exception is tagged separately:" echo " // EXCEPTION: AR-024 " echo "See CLAUDE.md and SPEC.md section 6." fi - name: Report summary if: always() run: | echo "Traceability matrix: docs/traceability.md" echo "" head -40 docs/traceability.md || true - name: Save reports if: always() uses: actions/upload-artifact@v3 with: name: traceability-reports path: | traces-report.json docs/traceability.md retention-days: 30