Skip to main content
The Evidence Center (page id traceability, issue #282) answers one question per requirement: is this actually proven, or just claimed? It renders a matrix of every requirement in scope against five evidence kinds — implementation, test, security, compliance, approval — sourced entirely from a run’s own on-disk artifacts (evidence.jsonl, validation.json, approvals.json). Nothing here is inferred from the browser; the page’s own header comment states the rule directly: it “never derives a pass from missing browser data and never recalculates readiness.” Below the matrix sits the Traceability Drift panel (issue #74), an on-demand scan of the same run for requirements nobody picked up, completed work missing its contracts, and stale file references.

How it works

The evidence projection (server-owned)

buildEvidenceProjection in src/observability/dashboard/state/evidence.js builds the whole model server-side on every poll; the client only renders it. For each run in scope, every requirement (run.requirements) becomes one row, and each of the five EVIDENCE_KINDS (implementation, test, security, compliance, approval) becomes one cell in that row:
  • implementation / security / compliance cells read evidence.jsonl entries whose kind (via a keyword match on kind/task_id/stage_id) matches the column and whose content references the requirement (referencesRequirement — an explicit requirement_id field, or a text-includes fallback).
  • test cells add validator check results (task.validation.checks) on top of the ledger signals.
  • approval cells read run.approvals records, cross-checked with approvalHistoryIssues/validateApprovalRecord from the harness’s own approval-audit module — a record that fails that audit renders as INVALID, not a false pass.
Each cell resolves to one of three states — verified (a real PASS/APPROVED signal, not integrity-damaged, no invalid records), failed (any FAIL/BLOCKED/REJECTED signal), or unknown (nothing observed, or the backing file is flagged in the run’s integrity collector). unknown is a first-class state, never silently coerced into a pass. The top-level model also carries: This same evidenceCenter object also feeds the Release Readiness verdict (state/readiness.js) as one check among several — so a requirement with failed or unknown evidence shows up as a release blocker, not just a quiet cell in this page.
A separate, older per-run summary (buildTraceMap in state/traceability.js, sent to the client as state.traceMap) is still computed on every poll, but no UI page currently renders it — the Evidence Center you see today is built entirely on evidenceCenter. Treat traceMap as legacy plumbing, not the live model.

Three views over one model

The page (src/observability/dashboard/ui/pages/traceability.js) never re-fetches — EVIDENCE_VIEW just re-renders the same model three ways:
  • Summary — the KPI strip (coverage / verified / failed / unknown) plus a release evidence verdict, followed by the same matrix as below.
  • Matrix — one row per requirement, one column per evidence kind, filterable by cell status (EVIDENCE_STATUS: all / verified / failed / unknown) via setEvidenceStatus.
  • Readiness rationale — the flat rationale list, one card per requirement×kind pair, each showing its status pill and up to two clickable source references.
A cell’s source reference renders as a clickable button (evidenceSourceButton) only when the source is linkable — i.e. its path is scoped to .rstack/ with no traversal. Clicking it calls viewArtifact() (ui/drawer.js), which fetches /api/artifact?run=...&path=... and opens the file in the drawer (rendered by artifact-render.js, or a raw <pre> fallback). This is the same mechanism the Run Report and other pages use to open a real deliverable in place — Evidence Center never links out to a filesystem path directly, only through that sandboxed endpoint. A toolbar button, “Export same projection,” downloads the exact in-memory model object as rstack-evidence-projection.json — what you see is what you get, byte for byte.

Traceability Drift panel

Below the matrix, fillDriftCard fetches /api/drift?run=<runId> for the first run in scope ((s.runs || [])[0]) and renders its findings. The server route (handleDrift in server.js) calls the shared harness scanner, scanRunDrift (src/core/harness/drift.js) — the identical function the rstack-agents drift CLI verb runs. The scanner reads the run’s real artifacts and reports:
  1. Requirements without a task — a stage-02 requirement no task’s id/title/description/ acceptance criteria mentions.
  2. Task completeness — a task missing status, owner/agent, or a traceable stage/artifact.
  3. Missing builder/validator contracts on a task marked PASS/DONE_WITH_CONCERNS (a waived task, per an on-record approval, gets a warning instead of an error).
  4. Validator PASS with zero passing checks recorded — a verdict with no evidence behind it.
  5. Stale file references — files a builder claims to have modified, or evidence points at, that no longer exist on disk.
  6. Evidence pointing at an unknown task, or an approved artifact that no longer exists (virtual gate names like guardrail-override:004-impl are excluded — they name a gate, not a file).
  7. Readiness contradictionreadiness.json reports READY while tasks are still FAIL/BLOCKED.
Each finding carries a severity (error = a broken evidence chain, warning = intact but degrading), a type, the artifact path it’s about, a human message, and a remediation. The run’s overall drift status is FAIL on any error, WARN on any warning, else PASS. The card shows the top 12 findings plus four summary chips (requirements / tasks / missing evidence / stale references); an empty findings list renders as an honest “No drift detected.”

Try it

Open the Evidence Center from the Business Hub nav, or drive the same scanner from the CLI:
--json/--all are real flags on the drift command (bin/rstack-agents.js); default exit is warning-tolerant (non-zero only on error-severity findings) so a CI pipeline can adopt the gate without breaking on cosmetic drift.