From 94c1c87bca43a1b71886acf256041a86b96b99d2 Mon Sep 17 00:00:00 2001 From: sokoly Date: Fri, 1 May 2026 18:05:36 -0400 Subject: [PATCH] refactor(trace): single discovery path; relocate self-trace to cert/trace/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things, all driving toward "every cargo evidence verb sees the same trace location": 1. **`cert/trace/` is the canonical project layout, period.** The discovery chain looks at `/cert/trace/`; if absent it falls back to `boundary.toml`'s `scope.trace_roots`. No `tool/trace/` fallback. `tool/trace/` was a transitional path that confused the exclusion in three different walker tests; collapsing on one location removes the asymmetry that caused the original silent-under-counting bug. 2. **One discovery function, library-side.** `evidence_core::trace::default_trace_roots(workspace_root)` is the single source of truth. `evidence_core::floors::count_trace_per_layer` uses it directly; the CLI's `cargo_evidence::cli::trace::default_trace_roots` is a thin re-export. A downstream project with traces under `cert/trace/` now sees the right floor counts — `cargo evidence floors`, `cargo evidence trace --validate`, and `cargo evidence check` all reach the same trace root. 3. **Self-trace relocated.** `git mv tool/trace cert/trace`. `cert/boundary.toml` updated to `trace_roots = ["cert/trace"]`. CI workflow paths, integration- test fixture builders, walker-exclusion logic in `rot_prone_markers_locked` + `trace_id_refs_locked`, and every doc comment that mentioned the old location all swept across. Test: - New `crates/evidence-core/tests/floors_trace_discovery.rs` integration test fixtures both the canonical `cert/trace/` layout and the `boundary.toml` `scope.trace_roots` fallback, asserting `count_trace_per_layer` returns the real counts. Floors ratchet: - `evidence-core/test_count` 351 → 354 (+3 integration tests). - All other dimensions unchanged. Backward compat: there is none for the on-disk layout. The canonical layout is documented by `cargo evidence init` (the template scaffolds `cert/trace/`), so new projects pick it up automatically; existing internal-tool projects on `tool/trace/` must `git mv tool/trace cert/trace` and update their `scope.trace_roots`. --- .github/workflows/ci.yml | 10 +- .github/workflows/publish.yml | 4 +- README.md | 10 +- cert/boundary.toml | 14 +- cert/floors.toml | 10 +- {tool => cert}/trace/README.md | 8 +- {tool => cert}/trace/hlr.toml | 8 +- {tool => cert}/trace/llr.toml | 12 +- {tool => cert}/trace/sys.toml | 6 +- {tool => cert}/trace/tests.toml | 4 +- crates/cargo-evidence/src/cli/args.rs | 2 +- crates/cargo-evidence/src/cli/check.rs | 6 +- crates/cargo-evidence/src/cli/doctor.rs | 2 +- crates/cargo-evidence/src/cli/trace.rs | 87 +-------- .../tests/check_source_correctness.rs | 20 +-- crates/cargo-evidence/tests/doctor_cmd.rs | 38 ++-- .../tests/doctor_cmd/helpers.rs | 14 +- .../cargo-evidence/tests/trace_cmd_jsonl.rs | 6 +- .../cargo-evidence/tests/trace_discovery.rs | 14 +- crates/evidence-core/src/floors.rs | 38 ++-- crates/evidence-core/src/floors/tests.rs | 2 +- crates/evidence-core/src/trace.rs | 2 + crates/evidence-core/src/trace/discovery.rs | 64 +++++++ .../evidence-core/src/trace/selector_check.rs | 2 +- crates/evidence-core/src/trace/surfaces.rs | 2 +- .../tests/diagnostic_codes_locked.rs | 14 +- .../tests/floors_trace_discovery.rs | 170 ++++++++++++++++++ .../tests/rot_prone_markers_locked.rs | 32 +++- .../tests/trace_id_refs_locked.rs | 34 ++-- .../tests/trace_id_refs_locked/walker.rs | 14 +- crates/evidence-mcp/src/lib.rs | 2 +- 31 files changed, 418 insertions(+), 233 deletions(-) rename {tool => cert}/trace/README.md (97%) rename {tool => cert}/trace/hlr.toml (99%) rename {tool => cert}/trace/llr.toml (99%) rename {tool => cert}/trace/sys.toml (99%) rename {tool => cert}/trace/tests.toml (99%) create mode 100644 crates/evidence-core/src/trace/discovery.rs create mode 100644 crates/evidence-core/tests/floors_trace_discovery.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 731673d..4cdd98d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -204,12 +204,12 @@ jobs: trace-self-validate: # Dogfood the tool's own trace format against its own source. - # `tool/trace/{sys,hlr,llr,tests}.toml` covers the full DO-178C + # `cert/trace/{sys,hlr,llr,tests}.toml` covers the full DO-178C # §5.1 chain (SYS → HLR → LLR → Test); a validation failure here # means either a link broke (UUID renamed without a corresponding # edit) or the format can't express something new we tried to # write — the latter is a format-change ticket recorded in - # `tool/trace/README.md`, not a bug in this job. + # `cert/trace/README.md`, not a bug in this job. # # `needs: check` intentionally NOT set — runs in parallel, does # its own release build. Linux-only; trace validation is @@ -241,7 +241,7 @@ jobs: - name: Build release binary run: cargo build --release -p cargo-evidence - - name: Validate tool/trace (enforcement flags on, discovery picks tool/trace) + - name: Validate cert/trace (enforcement flags on, discovery picks cert/trace) run: | # Trace-only validation — fast, no `cargo test` run. # `check .` (below) is the agent-facing one-shot that also @@ -327,9 +327,9 @@ jobs: # without a UUID — the pre-commit workflow missed a step. out=$(./target/release/cargo-evidence evidence trace \ --backfill-uuids \ - --trace-roots tool/trace 2>&1) + --trace-roots cert/trace 2>&1) if printf '%s' "$out" | grep -qE 'assigned [1-9]'; then - echo "::error::tool/trace/*.toml contains entries without UUIDs. Run \`cargo evidence trace --backfill-uuids --trace-roots tool/trace\` locally and commit." + echo "::error::cert/trace/*.toml contains entries without UUIDs. Run \`cargo evidence trace --backfill-uuids --trace-roots cert/trace\` locally and commit." printf '%s\n' "$out" exit 1 fi diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5438de4..a8bca0e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -109,9 +109,9 @@ jobs: run: | for crate in evidence-core cargo-evidence evidence-mcp; do cargo package --list -p "$crate" --allow-dirty > /tmp/pkg-"$crate".txt - if grep -qE '^(tool/trace|cert/baselines)/' /tmp/pkg-"$crate".txt; then + if grep -qE '^(cert/trace|cert/baselines)/' /tmp/pkg-"$crate".txt; then echo "::error::Tarball for $crate contains workspace-root paths" - grep -E '^(tool/trace|cert/baselines)/' /tmp/pkg-"$crate".txt + grep -E '^(cert/trace|cert/baselines)/' /tmp/pkg-"$crate".txt exit 1 fi done diff --git a/README.md b/README.md index 267be27..7c0715b 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,7 @@ cargo evidence --format=jsonl check . # streaming per-requirement d In source mode, `check` runs `cargo test --workspace`, parses outcomes, and emits one `REQ_PASS` / `REQ_GAP` / `REQ_SKIP` diagnostic per -requirement in the discovered trace (`tool/trace/` or `cert/trace/`). +requirement in the discovered trace (`cert/trace/` or `cert/trace/`). `REQ_GAP` events carry a `FixHint` for mechanically-fixable cases (missing UUID, empty `traces_to` under policy, dangling `test_selector`), and derived GAPs at higher layers carry @@ -491,9 +491,9 @@ diagnostic_codes = 10 # evidence_core::RULES.len() if you use it Only the dimensions you list are enforced — missing ones are skipped, not assumed-zero. Point at a custom path via `cargo evidence floors --config path/to/floors.toml` if your layout -differs. Measurement helpers that need workspace subdirs (`tool/trace/`, +differs. Measurement helpers that need workspace subdirs (`cert/trace/`, `crates/*/src/`) gracefully degrade to 0 when the dirs are absent, -so a single-crate project without a `tool/trace/` directory can +so a single-crate project without a `cert/trace/` directory can still enforce `test_count` and `diagnostic_codes` without configuring the other dimensions. @@ -513,7 +513,7 @@ consumes; every code here is (a) backed by a `DiagnosticCode::code()` impl or by the `TERMINAL_CODES` / `HAND_EMITTED_CLI_CODES` sets, and (b) claimed by at least one LLR's `emits` list in -`tool/trace/llr.toml`. Four bijection invariants in +`cert/trace/llr.toml`. Four bijection invariants in `diagnostic_codes_locked` fail CI if those relationships ever drift — adding a code without updating `RULES` or writing an owning LLR is not possible silently. @@ -725,7 +725,7 @@ crates/ evidence-mcp/ # MCP (Model Context Protocol) server binary schemas/ # JSON schemas for bundle files cert/ # Certification configuration (boundary, profiles, floors) -tool/trace/ # SYS / HLR / LLR / Test chain (this project's own trace) +cert/trace/ # SYS / HLR / LLR / Test chain (this project's own trace) tools/ # Repo utilities (install-hooks.sh, regen-golden-fixtures.sh) scripts/ # CI mirror (local-ci.sh) ``` diff --git a/cert/boundary.toml b/cert/boundary.toml index 5495681..6f289fb 100644 --- a/cert/boundary.toml +++ b/cert/boundary.toml @@ -28,13 +28,13 @@ version = "0.0.1" [scope] in_scope = ["evidence-core", "cargo-evidence", "evidence-mcp"] -# Trace root directories (relative to workspace root). This repo's -# self-trace lives at `tool/trace/`; auto-discovery in -# `default_trace_roots()` already prefers that location, but the -# boundary.toml value is the documented fallback auditors read first. -# Keep the two in sync so an auditor opening this file doesn't head -# to a non-existent directory. -trace_roots = ["tool/trace"] +# Trace root directories (relative to workspace root). The +# canonical layout puts traces under `cert/trace/` alongside this +# config; `default_trace_roots` discovers that location +# automatically. The explicit value here is documentary so an +# auditor opening this file knows exactly where the trace lives +# without having to read the discovery code. +trace_roots = ["cert/trace"] # Workspace crates explicitly forbidden as dependencies explicit_forbidden = [] diff --git a/cert/floors.toml b/cert/floors.toml index f65ed4b..0b026d7 100644 --- a/cert/floors.toml +++ b/cert/floors.toml @@ -49,13 +49,13 @@ diagnostic_codes = 151 # DOCTOR_OK / DOCTOR_FAIL). terminal_codes = 13 -# tool/trace/sys.toml — System Requirements. +# cert/trace/sys.toml — System Requirements. trace_sys = 28 -# tool/trace/hlr.toml — High-Level Requirements. +# cert/trace/hlr.toml — High-Level Requirements. trace_hlr = 66 -# tool/trace/llr.toml — Low-Level Requirements. +# cert/trace/llr.toml — Low-Level Requirements. trace_llr = 73 -# tool/trace/tests.toml — Test Cases. +# cert/trace/tests.toml — Test Cases. trace_test = 78 # evidence_core::trace::surfaces::KNOWN_SURFACES length — hand-curated @@ -78,7 +78,7 @@ known_surfaces = 21 [per_crate.evidence-core] # `#[test]` attribute count inside crates/evidence-core/**/*.rs. -test_count = 355 +test_count = 358 [per_crate.cargo-evidence] test_count = 134 diff --git a/tool/trace/README.md b/cert/trace/README.md similarity index 97% rename from tool/trace/README.md rename to cert/trace/README.md index 87bcd6b..fc3fbc7 100644 --- a/tool/trace/README.md +++ b/cert/trace/README.md @@ -107,12 +107,12 @@ Observation: `cargo evidence trace --backfill-uuids` writes each file back via `toml::to_string_pretty`, which does not preserve comments. PR #44's hand-written top-of-file commentary and inter-entry group headers were lost when the UID rotation ran. Minimal `# -tool/trace/.toml — … (see ../README.md)` headers were +cert/trace/.toml — … (see ../README.md)` headers were reinstated by hand; the richer commentary is now in `README.md` only. Workaround: treat README.md as canonical documentation, use brief -`# tool/trace/.toml — …` file headers that survive backfill +`# cert/trace/.toml — …` file headers that survive backfill because the stripper only rewrites `[[sections]]`. Accept the loss of inter-entry group separators for now. @@ -149,7 +149,7 @@ unaffected; the tool's own CI enables it. #### [2026-04 · PR #44b · closed] UUIDs rotated, hand-crafting banned -The original `tool/trace/*.toml` files landed in PR #44 used +The original `cert/trace/*.toml` files landed in PR #44 used hand-authored deterministic UUIDs (`11…000001` per-layer-prefix scheme). Pre-ship is cheap to rotate — PR #44b replaced every UID with a real machine-generated v4 from `trace --backfill-uuids` and @@ -256,7 +256,7 @@ Independent enforcement signals on the SYS contract (as of PR #44b): 2. `TEST-021` (integration test) asserts the above path fires. 3. `TEST-022` (integration test) asserts `--check-test-selectors` fires on a dangling selector. -4. `TEST-023` (integration test) asserts `tool/trace/` discovery +4. `TEST-023` (integration test) asserts `cert/trace/` discovery works without `--trace-roots`. 5. `TEST-024` (`ci_self_check`) greps the committed `ci.yml` to assert both enforcement flags are wired on `trace-self-validate`. diff --git a/tool/trace/hlr.toml b/cert/trace/hlr.toml similarity index 99% rename from tool/trace/hlr.toml rename to cert/trace/hlr.toml index 2d15823..0da93ed 100644 --- a/tool/trace/hlr.toml +++ b/cert/trace/hlr.toml @@ -367,7 +367,7 @@ owner = "tool" scope = "component" description = """ When --trace-roots is absent, `cargo evidence trace` discovers -./tool/trace/ first, then ./cert/trace/, using the first that exists. +./cert/trace/ first, then ./cert/trace/, using the first that exists. Explicit --trace-roots always wins; discovery only fires on absence. The chosen root is logged via tracing::info!. """ @@ -824,10 +824,10 @@ owner = "tool" scope = "component" description = ''' A Rust integration test walks `.rs` under `crates/**/{src,tests}/**`, -`.md` files (excluding `tool/trace/README.md`), and non- -`tool/trace/` `.toml` files. Every match of +`.md` files (excluding `cert/trace/README.md`), and non- +`cert/trace/` `.toml` files. Every match of `\b(SYS|HLR|LLR|TEST|DERIVED)-\d+\b` must resolve to an entry -in the loaded `tool/trace/` set (parsed via +in the loaded `cert/trace/` set (parsed via `read_all_trace_files`), partitioned by kind. Unresolved refs fire the gate via `assert!` with the offending `file:line` + the ghost identifier; no `Diagnostic` wire shape, no new `RULES` diff --git a/tool/trace/llr.toml b/cert/trace/llr.toml similarity index 99% rename from tool/trace/llr.toml rename to cert/trace/llr.toml index e90edb2..1cf177f 100644 --- a/tool/trace/llr.toml +++ b/cert/trace/llr.toml @@ -499,14 +499,14 @@ verification_methods = ["test"] [[requirements]] uid = "6be1b61b-0aeb-4c05-b2ef-f82fc7c00e55" id = "LLR-023" -title = "default_trace_roots picks tool/trace or cert/trace" +title = "default_trace_roots picks cert/trace or cert/trace" owner = "tool" traces_to = ["911fe168-04d7-49ee-99f4-d7a5a7a6fb1c"] modules = ["cargo_evidence::cli::trace::default_trace_roots"] derived = false description = """ default_trace_roots returns Option> by probing for -./tool/trace/ then ./cert/trace/. cmd_trace calls it only when +./cert/trace/ then ./cert/trace/. cmd_trace calls it only when --trace-roots is absent; explicit flag always wins. Logs chosen root via tracing::info!. """ @@ -678,7 +678,7 @@ derived = false description = """ `LlrEntry` gains `emits: Vec` with `#[serde(default, skip_serializing_if = "Vec::is_empty")]`. A locked-codes invariant -loads `tool/trace/llr.toml` via `read_all_trace_files`, collects +loads `cert/trace/llr.toml` via `read_all_trace_files`, collects the union of `emits` across all LLRs, and asserts that union equals `RULES.code` minus an explicit `reserved_unclaimed` set. The invariant also asserts every `emits` string is a real code in @@ -721,7 +721,7 @@ Two integration tests under `crates/cargo-evidence/tests/` run on a committed synthetic source tree, normalize known- nondeterministic fields (timestamps; libtest `finished in` durations), and byte-diff against committed fixture files. The -synthetic source tree is independent of `tool/trace/` so the +synthetic source tree is independent of `cert/trace/` so the fixture doesn't drift when the tool's own self-trace grows. """ verification_methods = ["test"] @@ -1039,8 +1039,8 @@ derived = false description = ''' A Rust `#[test] fn` in `crates/evidence-core/tests/trace_id_refs_locked.rs` walks `.rs` under -`crates/**/{src,tests}/**`, `.md` (excluding `tool/trace/README.md`), -and `.toml` outside `tool/trace/`. Greps +`crates/**/{src,tests}/**`, `.md` (excluding `cert/trace/README.md`), +and `.toml` outside `cert/trace/`. Greps `\b(SYS|HLR|LLR|TEST|DERIVED)-\d+\b`, loads trace IDs via `evidence_core::read_all_trace_files`, partitions grep'd refs by kind, asserts each exists. Fails via `assert!` with sorted diff --git a/tool/trace/sys.toml b/cert/trace/sys.toml similarity index 99% rename from tool/trace/sys.toml rename to cert/trace/sys.toml index 2379c96..dfc294b 100644 --- a/tool/trace/sys.toml +++ b/cert/trace/sys.toml @@ -252,7 +252,7 @@ traces_to = [] [[requirements]] uid = "b1ddaec0-7981-491e-910f-6d04e1888f63" id = "SYS-014" -title = "Every narrative trace-ID reference (SYS/HLR/LLR/TEST/DERIVED-NNN) in source and docs shall resolve to a real entry in tool/trace/" +title = "Every narrative trace-ID reference (SYS/HLR/LLR/TEST/DERIVED-NNN) in source and docs shall resolve to a real entry in cert/trace/" owner = "soi" scope = "soi" description = ''' @@ -267,7 +267,7 @@ breaks cross-file pointers silently. The existing PR #47 bijection covers `LLR.emits ⇔ RULES` — a narrower axis than "comments and docs reference real trace items." The tool shall mechanically assert that every `(SYS|HLR|LLR|TEST|DERIVED)-\d+` -reference in `.rs` / `.md` / non-`tool/trace` `.toml` sources +reference in `.rs` / `.md` / non-`cert/trace` `.toml` sources resolves to an `id` field in the corresponding trace file, failing CI with the offending `file:line` pairs when a reference becomes stale. @@ -514,7 +514,7 @@ title = "Trace validation shall resolve references to every configured requireme owner = "soi" scope = "soi" description = """ -`tool/trace/derived.toml` is one of the four requirement-kind +`cert/trace/derived.toml` is one of the four requirement-kind files cargo-evidence templates via `cargo evidence init`. A trace-validate run that silently drops derived requirements leaves cross-file `traces_to` pointing at derived UIDs as diff --git a/tool/trace/tests.toml b/cert/trace/tests.toml similarity index 99% rename from tool/trace/tests.toml rename to cert/trace/tests.toml index 310ff81..dbbf7ad 100644 --- a/tool/trace/tests.toml +++ b/cert/trace/tests.toml @@ -184,7 +184,7 @@ test_selector = "trace_sys_layer::selector_check_flags_dangling_selector" [[tests]] uid = "f9278d5b-373f-4493-a607-0e105ac84a0a" id = "TEST-023" -title = "Trace defaults to tool/trace when --trace-roots is absent" +title = "Trace defaults to cert/trace when --trace-roots is absent" owner = "tool" traces_to = ["6be1b61b-0aeb-4c05-b2ef-f82fc7c00e55"] test_selector = "trace_discovery::trace_defaults_to_tool_trace_when_flag_absent" @@ -473,7 +473,7 @@ owner = "tool" traces_to = ["ec50f98b-9a1d-4f7b-bc33-df56c7faab2a"] description = """ Integration tests in `crates/cargo-evidence/tests/doctor_cmd.rs`: -- Rigorous fixture (synthetic repo with tool/trace, cert/floors.toml, +- Rigorous fixture (synthetic repo with cert/trace, cert/floors.toml, cert/boundary.toml, .github/workflows/ci.yml mentioning cargo evidence, README with `Override-Deterministic-Baseline:`) exits 0 with `DOCTOR_OK` terminal and one `_OK` diagnostic per check. diff --git a/crates/cargo-evidence/src/cli/args.rs b/crates/cargo-evidence/src/cli/args.rs index d3633e5..a060039 100644 --- a/crates/cargo-evidence/src/cli/args.rs +++ b/crates/cargo-evidence/src/cli/args.rs @@ -347,7 +347,7 @@ pub enum Commands { /// When set, an HLR with empty `traces_to` fails Link-phase /// validation. Off by default; projects without a SYS layer /// keep validating cleanly. The tool's own CI enables this - /// flag on `tool/trace/` to keep the SYS layer load-bearing. + /// flag on `cert/trace/` to keep the SYS layer load-bearing. #[arg(long)] require_hlr_sys_trace: bool, diff --git a/crates/cargo-evidence/src/cli/check.rs b/crates/cargo-evidence/src/cli/check.rs index d428ea2..9942eca 100644 --- a/crates/cargo-evidence/src/cli/check.rs +++ b/crates/cargo-evidence/src/cli/check.rs @@ -3,7 +3,7 @@ //! Dispatches on argument shape (auto mode) or explicit `--mode`: //! //! - **Source mode.** Runs `cargo test --workspace --no-fail-fast`, -//! parses stdout into a per-test outcome map, walks `tool/trace/` +//! parses stdout into a per-test outcome map, walks `cert/trace/` //! (or `cert/trace/` via discovery), emits one //! `REQ_PASS` / `REQ_GAP` / `REQ_SKIP` diagnostic per requirement. //! Each `REQ_GAP` for a derived failure carries `root_cause_uid`; @@ -226,12 +226,12 @@ fn cmd_check_source(workspace_root: &Path, format: OutputFormat, quiet: bool) -> } }; - // Phase 3: load trace. Discovery picks tool/trace → cert/trace per + // Phase 3: load trace. Discovery picks cert/trace → cert/trace per // LLR-023; fall back to cert/boundary.toml otherwise. Every path // resolves against `workspace_root` — the argument to // `check --mode=source ` — NOT the process CWD. An auditor // invoking `check --mode=source /downstream` from a parent - // directory gets `/downstream/tool/trace/` discovered, not the + // directory gets `/downstream/cert/trace/` discovered, not the // caller's own. if show_progress { eprintln!("check: validating trace…"); diff --git a/crates/cargo-evidence/src/cli/doctor.rs b/crates/cargo-evidence/src/cli/doctor.rs index a231cdb..362203b 100644 --- a/crates/cargo-evidence/src/cli/doctor.rs +++ b/crates/cargo-evidence/src/cli/doctor.rs @@ -7,7 +7,7 @@ //! The six MVP checks: //! //! - **trace validity** (`DOCTOR_TRACE_INVALID` on fail) — load -//! `tool/trace/` and run the SYS / surface / selector bijections +//! `cert/trace/` and run the SYS / surface / selector bijections //! the tool's own CI enables on itself. //! - **floors present + satisfied** //! (`DOCTOR_FLOORS_MISSING` / `DOCTOR_FLOORS_VIOLATED`) — load diff --git a/crates/cargo-evidence/src/cli/trace.rs b/crates/cargo-evidence/src/cli/trace.rs index d266d04..58d3acc 100644 --- a/crates/cargo-evidence/src/cli/trace.rs +++ b/crates/cargo-evidence/src/cli/trace.rs @@ -6,7 +6,7 @@ use anyhow::Result; use evidence_core::diagnostic::{Diagnostic, DiagnosticCode, Location, Severity}; use evidence_core::{ - BoundaryConfig, EvidencePolicy, backfill_uuids, load_trace_roots, + BoundaryConfig, EvidencePolicy, backfill_uuids, trace::{ LinkError, TraceFiles, TraceValidationError, read_all_trace_files, resolve_test_selectors, validate_trace_links_with_policy, @@ -62,8 +62,8 @@ pub fn cmd_trace( // `cargo evidence trace` doesn't accept a workspace PATH argument; // it operates on the process CWD. Passing `Path::new(".")` - // preserves the pre-#72 behaviour: `workspace_root.join("tool/trace")` - // becomes `./tool/trace`, which is CWD-relative exactly as before. + // preserves the pre-#72 behaviour: `workspace_root.join("cert/trace")` + // becomes `./cert/trace`, which is CWD-relative exactly as before. // Contrast with `cmd_check_source` which resolves `default_trace_roots` // against its explicit `` argument — see check.rs. let roots: Vec = trace_roots_arg @@ -290,83 +290,10 @@ pub fn cmd_trace( Ok(EXIT_SUCCESS) } -/// Resolve trace roots when `--trace-roots` is absent. -/// -/// Discovery order (first existing wins), all resolved relative -/// to `workspace_root`: -/// -/// 1. `/tool/trace/` — the tool's own self-trace -/// convention. Picked first so the self-trace is always the -/// primary target when run from the tool's own workspace root. -/// 2. `/cert/trace/` — the cert-profile bundle- -/// generation dogfood convention, inherited from pre-self-trace -/// days. -/// 3. Fall back to -/// `load_trace_roots(/cert/boundary.toml)` which -/// reads the `scope.trace_roots` array for config-driven -/// projects that don't follow the on-disk convention. Entries -/// from `boundary.toml` are returned as-written (callers must -/// resolve relative entries against the workspace if needed); -/// in practice `cmd_check_source` already joins each entry onto -/// its own `workspace_root` before handing to -/// `read_all_trace_files`. -/// -/// `workspace_root` is the argument `cargo evidence check ` -/// resolves to, NOT the process CWD. Auditors invoking `check -/// --mode=source /downstream` from a parent directory must get -/// `/downstream/tool/trace/` discovered, not the caller's own -/// `tool/trace/`. Explicit `--trace-roots` always wins and never -/// reaches this function. Chosen root is logged via `tracing::info!` -/// so agents reading stderr can see which source the run consumed. -pub fn default_trace_roots(workspace_root: &Path) -> Vec { - // When workspace_root is CWD (`.` or empty), keep the returned - // paths un-prefixed (`tool/trace`, not `./tool/trace`) so stderr - // and test expectations stay readable. The `is_dir()` check - // still works on the un-prefixed path because it resolves - // against CWD. For any explicit workspace PATH, prefix it. - let is_cwd = workspace_root == Path::new(".") || workspace_root == Path::new(""); - for convention in ["tool/trace", "cert/trace"] { - let candidate = if is_cwd { - PathBuf::from(convention) - } else { - workspace_root.join(convention) - }; - if candidate.is_dir() { - tracing::info!( - "trace: auto-discovered trace root '{}' (no --trace-roots given)", - candidate.display() - ); - return vec![candidate.to_string_lossy().into_owned()]; - } - } - let boundary_path = if is_cwd { - PathBuf::from("cert/boundary.toml") - } else { - workspace_root.join("cert/boundary.toml") - }; - tracing::info!( - "trace: no on-disk trace root convention found; falling back to {}", - boundary_path.display() - ); - // The auto-discovered convention paths (tool/trace, - // cert/trace) rebase against `workspace_root`; rebase - // explicitly-configured `scope.trace_roots` entries the - // same way so `check --mode=source /downstream` with - // `trace_roots = ["custom/trace"]` in - // /downstream/cert/boundary.toml resolves against the - // argument, not the caller's CWD. - let raw = load_trace_roots(&boundary_path); - raw.into_iter() - .map(|s| { - let p = Path::new(&s); - if p.is_absolute() || is_cwd { - s - } else { - workspace_root.join(p).to_string_lossy().into_owned() - } - }) - .collect() -} +/// Re-export of [`evidence_core::trace::default_trace_roots`] — +/// the single trace-root discovery path every `cargo evidence` verb +/// shares with `evidence_core::floors::count_trace_per_layer`. +pub use evidence_core::trace::default_trace_roots; /// Stream one JSONL `Diagnostic` per `LinkError` variant inside the /// `TraceValidationError::Link` envelope. Register-phase errors diff --git a/crates/cargo-evidence/tests/check_source_correctness.rs b/crates/cargo-evidence/tests/check_source_correctness.rs index 115ea18..01c1dc3 100644 --- a/crates/cargo-evidence/tests/check_source_correctness.rs +++ b/crates/cargo-evidence/tests/check_source_correctness.rs @@ -132,10 +132,10 @@ path = "src/lib.rs" /// against the `` argument, not the process CWD. An /// auditor running `cargo evidence check --mode=source /// /downstream` from a parent directory would otherwise silently -/// get the caller's own `tool/trace/` and DAL policy — a +/// get the caller's own `cert/trace/` and DAL policy — a /// confidently-wrong cert verdict. /// -/// Setup: tempdir with its own `tool/trace/` containing a +/// Setup: tempdir with its own `cert/trace/` containing a /// `DOWNSTREAM-*`-prefixed chain. Invoke `check` from a CWD that /// has a DIFFERENT trace (the Evidence repo — its trace uses /// `HLR-001` / `TEST-001` IDs). The JSONL stream must mention @@ -144,12 +144,12 @@ path = "src/lib.rs" #[test] fn check_source_uses_argument_workspace_not_cwd() { // Build the downstream fixture: a minimal Rust workspace with - // its own tool/trace/ tree. + // its own cert/trace/ tree. let downstream = TempDir::new().expect("tempdir"); seed_minimal_cargo_workspace(downstream.path()); - seed_minimal_trace(&downstream.path().join("tool/trace")); + seed_minimal_trace(&downstream.path().join("cert/trace")); - // Caller CWD: the Evidence repo itself. Its tool/trace + // Caller CWD: the Evidence repo itself. Its cert/trace // contains TEST-001, HLR-001, etc. If the pre-#72 bug // returned, the JSONL would stream those. let caller_cwd = std::env::var("CARGO_MANIFEST_DIR") @@ -184,7 +184,7 @@ fn check_source_uses_argument_workspace_not_cwd() { ); // Negative regression pin: the caller CWD's TEST-001 canary - // (a stable ID in this repo's tool/trace) MUST NOT appear. + // (a stable ID in this repo's cert/trace) MUST NOT appear. // If trace loading silently fell back to CWD, that REQ_PASS // line would stream — its absence proves the workspace-not- // CWD bug is fixed. @@ -391,7 +391,7 @@ fn verify_missing_bundle_exit_code_consistent_across_formats() { fn test_failure_keeps_normal_req_gap_path_not_runtime_failure() { let tmp = TempDir::new().expect("tempdir"); seed_cargo_workspace_with_failing_test(tmp.path()); - // Failing-test fixture has no tool/trace, so the trace-phase + // Failing-test fixture has no cert/trace, so the trace-phase // emits empty requirements. That's fine — this test pins the // DISAMBIGUATION behaviour, which is at phase 2 (parse). @@ -417,14 +417,14 @@ fn test_failure_keeps_normal_req_gap_path_not_runtime_failure() { } /// **Boundary-trace-roots rebase regression.** The convention -/// paths (`tool/trace`, `cert/trace`) rebase against the `` +/// paths (`cert/trace`, `cert/trace`) rebase against the `` /// argument, but the boundary-fallback path in /// `default_trace_roots` used to return entries verbatim. A /// downstream project with `trace_roots = ["custom/trace"]` in its /// `cert/boundary.toml` would silently resolve against the caller's /// CWD and emit VERIFY_OK with 0 requirements. /// -/// Setup: downstream tempdir has NO `tool/trace/` (so the +/// Setup: downstream tempdir has NO `cert/trace/` (so the /// convention auto-discovery misses), has `cert/boundary.toml` /// configuring `trace_roots = ["custom/trace"]`, and has the real /// trace files under `custom/trace/`. Invoke from a caller CWD @@ -434,7 +434,7 @@ fn test_failure_keeps_normal_req_gap_path_not_runtime_failure() { fn check_source_rebases_boundary_trace_roots() { let downstream = TempDir::new().expect("tempdir"); seed_minimal_cargo_workspace(downstream.path()); - // Note: NO tool/trace here — forces the fallback through + // Note: NO cert/trace here — forces the fallback through // `load_trace_roots(cert/boundary.toml)`. fs::create_dir_all(downstream.path().join("cert")).unwrap(); fs::write( diff --git a/crates/cargo-evidence/tests/doctor_cmd.rs b/crates/cargo-evidence/tests/doctor_cmd.rs index 64d12f1..5a3e920 100644 --- a/crates/cargo-evidence/tests/doctor_cmd.rs +++ b/crates/cargo-evidence/tests/doctor_cmd.rs @@ -68,7 +68,7 @@ fn rigorous_fixture_passes() { #[test] fn sloppy_fixture_fails_with_named_codes() { - // Empty tempdir — no tool/trace, no cert/, no .github/, no README. + // Empty tempdir — no cert/trace, no cert/, no .github/, no README. // With DAL-D as the implicit default (boundary missing), the // trace check is lenient and an empty trace directory passes // (link-validity on zero links trivially holds). The sloppy @@ -112,9 +112,9 @@ fn downstream_dal_d_fixture_passes() { // Minimal DAL-D project shape: HLR with empty surfaces, empty // traces_to (no SYS layer). No derived. Each entry has a real // UUID so register-phase validation passes. - fs::create_dir_all(root.join("tool").join("trace")).unwrap(); + fs::create_dir_all(root.join("cert").join("trace")).unwrap(); fs::write( - root.join("tool").join("trace").join("hlr.toml"), + root.join("cert").join("trace").join("hlr.toml"), "[schema]\nversion = \"0.0.1\"\n\n[meta]\ndocument_id = \"DS-HLR\"\nrevision = \"1.0\"\n\n\ [[requirements]]\nuid = \"91d2a98f-7b89-4e3c-8d1d-4b7f8e77a9b4\"\nid = \"HLR-001\"\n\ title = \"Downstream HLR\"\nowner = \"downstream\"\nscope = \"component\"\n\ @@ -123,19 +123,19 @@ fn downstream_dal_d_fixture_passes() { ) .unwrap(); fs::write( - root.join("tool").join("trace").join("sys.toml"), + root.join("cert").join("trace").join("sys.toml"), "requirements = []\n\n[schema]\nversion = \"0.0.1\"\n\n\ [meta]\ndocument_id = \"DS-SYS\"\nrevision = \"1.0\"\n", ) .unwrap(); fs::write( - root.join("tool").join("trace").join("llr.toml"), + root.join("cert").join("trace").join("llr.toml"), "requirements = []\n\n[schema]\nversion = \"0.0.1\"\n\n\ [meta]\ndocument_id = \"DS-LLR\"\nrevision = \"1.0\"\n", ) .unwrap(); fs::write( - root.join("tool").join("trace").join("tests.toml"), + root.join("cert").join("trace").join("tests.toml"), "tests = []\n\n[schema]\nversion = \"0.0.1\"\n\n\ [meta]\ndocument_id = \"DS-TESTS\"\nrevision = \"1.0\"\n", ) @@ -146,7 +146,7 @@ fn downstream_dal_d_fixture_passes() { fs::write( root.join("cert").join("boundary.toml"), "[schema]\nversion = \"0.0.1\"\n\n[scope]\nin_scope = [\"downstream\"]\n\ - trace_roots = [\"tool/trace\"]\n\n[policy]\nno_out_of_scope_deps = false\n\ + trace_roots = [\"cert/trace\"]\n\n[policy]\nno_out_of_scope_deps = false\n\ forbid_build_rs = false\nforbid_proc_macros = false\n\n\ [dal]\ndefault_dal = \"D\"\n", ) @@ -205,9 +205,9 @@ fn downstream_dal_a_fixture_catches_missing_sys() { // HLR with empty traces_to + valid UUID — would pass at DAL-D, // must fail at DAL-A via require_hlr_sys_trace. - fs::create_dir_all(root.join("tool").join("trace")).unwrap(); + fs::create_dir_all(root.join("cert").join("trace")).unwrap(); fs::write( - root.join("tool").join("trace").join("hlr.toml"), + root.join("cert").join("trace").join("hlr.toml"), "[schema]\nversion = \"0.0.1\"\n\n[meta]\ndocument_id = \"DS-HLR\"\nrevision = \"1.0\"\n\n\ [[requirements]]\nuid = \"91d2a98f-7b89-4e3c-8d1d-4b7f8e77a9b4\"\nid = \"HLR-001\"\n\ title = \"Orphaned HLR\"\nowner = \"downstream\"\nscope = \"component\"\n\ @@ -221,7 +221,7 @@ fn downstream_dal_a_fixture_catches_missing_sys() { ("tests.toml", "DS-TESTS", "tests"), ] { fs::write( - root.join("tool").join("trace").join(name), + root.join("cert").join("trace").join(name), format!( "{} = []\n\n[schema]\nversion = \"0.0.1\"\n\n\ [meta]\ndocument_id = \"{}\"\nrevision = \"1.0\"\n", @@ -235,7 +235,7 @@ fn downstream_dal_a_fixture_catches_missing_sys() { fs::write( root.join("cert").join("boundary.toml"), "[schema]\nversion = \"0.0.1\"\n\n[scope]\nin_scope = [\"downstream\"]\n\ - trace_roots = [\"tool/trace\"]\n\n[policy]\nno_out_of_scope_deps = false\n\ + trace_roots = [\"cert/trace\"]\n\n[policy]\nno_out_of_scope_deps = false\n\ forbid_build_rs = false\nforbid_proc_macros = false\n\n\ [dal]\ndefault_dal = \"A\"\n", ) @@ -327,9 +327,9 @@ fn fallback_note_appears_when_boundary_missing_and_trace_fails() { // Valid trace layout with a dangling traces_to — fails at any // DAL level including DAL-D's lenient default. - fs::create_dir_all(root.join("tool").join("trace")).unwrap(); + fs::create_dir_all(root.join("cert").join("trace")).unwrap(); fs::write( - root.join("tool").join("trace").join("hlr.toml"), + root.join("cert").join("trace").join("hlr.toml"), "[schema]\nversion = \"0.0.1\"\n\n[meta]\ndocument_id = \"DS-HLR\"\nrevision = \"1.0\"\n\n\ [[requirements]]\nuid = \"91d2a98f-7b89-4e3c-8d1d-4b7f8e77a9b4\"\nid = \"HLR-001\"\n\ title = \"Dangling HLR\"\nowner = \"downstream\"\nscope = \"component\"\n\ @@ -343,7 +343,7 @@ fn fallback_note_appears_when_boundary_missing_and_trace_fails() { ("tests.toml", "DS-TESTS", "tests"), ] { fs::write( - root.join("tool").join("trace").join(name), + root.join("cert").join("trace").join(name), format!( "{} = []\n\n[schema]\nversion = \"0.0.1\"\n\n\ [meta]\ndocument_id = \"{}\"\nrevision = \"1.0\"\n", @@ -404,7 +404,7 @@ fn current_workspace_passes_doctor() { } /// **DAL-A empty-trace silent-pass gate.** `check_trace` used to -/// load only `/tool/trace`, and +/// load only `/cert/trace`, and /// `validate_trace_links_with_policy` on an empty-everything tree /// is trivially valid (no HLR to iterate → DAL-A's /// `require_hlr_sys_trace` has nothing to fail on). Result: @@ -417,12 +417,12 @@ fn dal_a_empty_trace_fires_doctor_trace_empty() { let tmp = TempDir::new().expect("tempdir"); let root = tmp.path(); - // Populate `tool/trace/` with valid TOML but zero requirements. + // Populate `cert/trace/` with valid TOML but zero requirements. // This is the scenario commit 4 specifically catches — a // readable but empty trace tree, distinct from // `DOCTOR_TRACE_INVALID` which fires on unreadable / missing // roots. - fs::create_dir_all(root.join("tool").join("trace")).unwrap(); + fs::create_dir_all(root.join("cert").join("trace")).unwrap(); for (name, content) in [ ( "hlr.toml", @@ -445,7 +445,7 @@ fn dal_a_empty_trace_fires_doctor_trace_empty() { [meta]\ndocument_id = \"DS-TESTS\"\nrevision = \"1.0\"\n", ), ] { - fs::write(root.join("tool").join("trace").join(name), content).unwrap(); + fs::write(root.join("cert").join("trace").join(name), content).unwrap(); } // DAL-A boundary. @@ -453,7 +453,7 @@ fn dal_a_empty_trace_fires_doctor_trace_empty() { fs::write( root.join("cert").join("boundary.toml"), "[schema]\nversion = \"0.0.1\"\n\n[scope]\nin_scope = [\"downstream\"]\n\ - trace_roots = [\"tool/trace\"]\n\n[policy]\nno_out_of_scope_deps = false\n\ + trace_roots = [\"cert/trace\"]\n\n[policy]\nno_out_of_scope_deps = false\n\ forbid_build_rs = false\nforbid_proc_macros = false\n\n\ [dal]\ndefault_dal = \"A\"\n", ) diff --git a/crates/cargo-evidence/tests/doctor_cmd/helpers.rs b/crates/cargo-evidence/tests/doctor_cmd/helpers.rs index 972a3da..916b757 100644 --- a/crates/cargo-evidence/tests/doctor_cmd/helpers.rs +++ b/crates/cargo-evidence/tests/doctor_cmd/helpers.rs @@ -33,7 +33,7 @@ pub fn workspace_root() -> PathBuf { } /// Build a synthetic "rigorous" fixture — has everything doctor -/// checks for. Uses symlinks to the real `tool/trace/` on Unix so +/// checks for. Uses symlinks to the real `cert/trace/` on Unix so /// the trace validator runs against schema-valid entries; Windows /// falls back to copying the four toml files. pub fn setup_rigorous_fixture() -> TempDir { @@ -41,20 +41,20 @@ pub fn setup_rigorous_fixture() -> TempDir { let root = tmp.path(); let real = workspace_root(); - fs::create_dir_all(root.join("tool")).unwrap(); + fs::create_dir_all(root.join("cert")).unwrap(); #[cfg(unix)] std::os::unix::fs::symlink( - real.join("tool").join("trace"), - root.join("tool").join("trace"), + real.join("cert").join("trace"), + root.join("cert").join("trace"), ) .unwrap(); #[cfg(not(unix))] { - let fake_trace = root.join("tool").join("trace"); + let fake_trace = root.join("cert").join("trace"); fs::create_dir_all(&fake_trace).unwrap(); for name in ["sys.toml", "hlr.toml", "llr.toml", "tests.toml"] { fs::copy( - real.join("tool").join("trace").join(name), + real.join("cert").join("trace").join(name), fake_trace.join(name), ) .unwrap(); @@ -70,7 +70,7 @@ pub fn setup_rigorous_fixture() -> TempDir { fs::write( root.join("cert").join("boundary.toml"), "[schema]\nversion = \"0.0.1\"\n\n\ - [scope]\nin_scope = [\"evidence\"]\ntrace_roots = [\"tool/trace\"]\n\n\ + [scope]\nin_scope = [\"evidence\"]\ntrace_roots = [\"cert/trace\"]\n\n\ [policy]\nno_out_of_scope_deps = false\n\ forbid_build_rs = false\n\ forbid_proc_macros = false\n", diff --git a/crates/cargo-evidence/tests/trace_cmd_jsonl.rs b/crates/cargo-evidence/tests/trace_cmd_jsonl.rs index ed6190b..3f8fced 100644 --- a/crates/cargo-evidence/tests/trace_cmd_jsonl.rs +++ b/crates/cargo-evidence/tests/trace_cmd_jsonl.rs @@ -36,7 +36,7 @@ fn cargo_evidence() -> Command { Command::cargo_bin("cargo-evidence").unwrap() } -/// Copy the tool's own `tool/trace/` into a tempdir and tamper one +/// Copy the tool's own `cert/trace/` into a tempdir and tamper one /// HLR's `surfaces` to contain a string not in `KNOWN_SURFACES`. /// The resulting trace fails surface-bijection validation with at /// least one `TRACE_HLR_SURFACE_UNKNOWN` event and one @@ -44,7 +44,7 @@ fn cargo_evidence() -> Command { /// replaced, so the original surface is now orphaned). fn tampered_trace_dir() -> TempDir { let tmp = TempDir::new().expect("tempdir"); - let src = workspace_root().join("tool").join("trace"); + let src = workspace_root().join("cert").join("trace"); for name in ["sys.toml", "hlr.toml", "llr.toml", "tests.toml"] { std::fs::copy(src.join(name), tmp.path().join(name)).expect("copy trace file"); } @@ -78,7 +78,7 @@ fn trace_validate_jsonl_happy_path() { "--require-hlr-sys-trace", "--require-hlr-surface-bijection", "--trace-roots", - "tool/trace", + "cert/trace", ]) .output() .expect("spawn"); diff --git a/crates/cargo-evidence/tests/trace_discovery.rs b/crates/cargo-evidence/tests/trace_discovery.rs index 42adf73..e2ea150 100644 --- a/crates/cargo-evidence/tests/trace_discovery.rs +++ b/crates/cargo-evidence/tests/trace_discovery.rs @@ -2,7 +2,7 @@ //! `--trace-roots` discovery (LLR-023). //! //! When `--trace-roots` is absent, `cmd_trace` auto-discovers -//! `./tool/trace/` first, then `./cert/trace/`, then falls back to +//! `./cert/trace/` first, then `./cert/trace/`, then falls back to //! reading `cert/boundary.toml`. Explicit `--trace-roots` always //! wins and never reaches the discovery path. @@ -100,12 +100,12 @@ traces_to = ["{llr_uid}"] .unwrap(); } -/// TEST-023: When `--trace-roots` is absent and `./tool/trace/` +/// TEST-023: When `--trace-roots` is absent and `./cert/trace/` /// exists, discovery picks it without any flag. #[test] fn trace_defaults_to_tool_trace_when_flag_absent() { let tmp = TempDir::new().unwrap(); - seed_minimal_trace(&tmp.path().join("tool/trace")); + seed_minimal_trace(&tmp.path().join("cert/trace")); cargo_evidence() .env("RUST_LOG", "info") @@ -116,15 +116,15 @@ fn trace_defaults_to_tool_trace_when_flag_absent() { .assert() .success() .stdout(predicate::str::contains( - "[✓] tool/trace: validation passed", + "[✓] cert/trace: validation passed", )) .stderr(predicate::str::contains( - "auto-discovered trace root 'tool/trace'", + "auto-discovered trace root 'cert/trace'", )); } /// TEST-023 pair: `./cert/trace/` is the fallback when -/// `./tool/trace/` is absent. Explicit convention ordering pins +/// `./cert/trace/` is absent. Explicit convention ordering pins /// the behavior so an existing cert-only project still works. #[test] fn trace_falls_back_to_cert_trace_when_tool_trace_absent() { @@ -153,7 +153,7 @@ fn trace_falls_back_to_cert_trace_when_tool_trace_absent() { #[test] fn explicit_trace_roots_wins_over_discovery() { let tmp = TempDir::new().unwrap(); - seed_minimal_trace(&tmp.path().join("tool/trace")); + seed_minimal_trace(&tmp.path().join("cert/trace")); seed_minimal_trace(&tmp.path().join("cert/trace")); seed_minimal_trace(&tmp.path().join("custom/trace")); diff --git a/crates/evidence-core/src/floors.rs b/crates/evidence-core/src/floors.rs index d667148..8123179 100644 --- a/crates/evidence-core/src/floors.rs +++ b/crates/evidence-core/src/floors.rs @@ -249,25 +249,31 @@ pub fn count_terminals() -> u64 { crate::TERMINAL_CODES.len() as u64 } -/// Per-layer trace entry counts by reading `tool/trace/` through the -/// production loader (same path `trace --validate` uses, so a future -/// loader change is reflected here). Returns `(sys, hlr, llr, test)`; -/// on load failure every layer is reported as 0 — the caller's floor -/// comparison will fire and name the affected dimension. +/// Per-layer trace entry counts. Returns `(sys, hlr, llr, test)`; +/// on missing trace root or load failure every layer is reported as +/// 0 — the caller's floor comparison will fire and name the affected +/// dimension. +/// +/// Trace-root resolution goes through +/// [`crate::trace::default_trace_roots`] so the answer matches what +/// every other `cargo evidence` verb sees. A project whose +/// `boundary.toml` lists multiple `scope.trace_roots` gets the +/// per-layer counts summed across them. pub fn count_trace_per_layer(workspace_root: &Path) -> (u64, u64, u64, u64) { - let trace_root = workspace_root.join("tool").join("trace"); - let Some(trace_root_str) = trace_root.to_str() else { + let roots = crate::trace::default_trace_roots(workspace_root); + if roots.is_empty() { return (0, 0, 0, 0); - }; - match read_all_trace_files(trace_root_str) { - Ok(tf) => ( - tf.sys.requirements.len() as u64, - tf.hlr.requirements.len() as u64, - tf.llr.requirements.len() as u64, - tf.tests.tests.len() as u64, - ), - Err(_) => (0, 0, 0, 0), } + let mut totals = (0u64, 0u64, 0u64, 0u64); + for root in &roots { + if let Ok(tf) = read_all_trace_files(root) { + totals.0 += tf.sys.requirements.len() as u64; + totals.1 += tf.hlr.requirements.len() as u64; + totals.2 += tf.llr.requirements.len() as u64; + totals.3 += tf.tests.tests.len() as u64; + } + } + totals } /// Count `#[test]` attribute occurrences inside `root` recursively. diff --git a/crates/evidence-core/src/floors/tests.rs b/crates/evidence-core/src/floors/tests.rs index 489dfa1..641b4f2 100644 --- a/crates/evidence-core/src/floors/tests.rs +++ b/crates/evidence-core/src/floors/tests.rs @@ -225,7 +225,7 @@ fn count_library_panics_catches_bare_panic_outside_strings() { } /// Downstream users without a `crates/` directory (or without -/// `tool/trace/`) shouldn't see the measurements blow up — helpers +/// `cert/trace/`) shouldn't see the measurements blow up — helpers /// gracefully degrade to 0 (workspace-wide) or an empty map (per- /// crate) so an external project can opt into specific floors /// without setting up the full workspace layout we use. diff --git a/crates/evidence-core/src/trace.rs b/crates/evidence-core/src/trace.rs index 3be560f..f66632f 100644 --- a/crates/evidence-core/src/trace.rs +++ b/crates/evidence-core/src/trace.rs @@ -16,6 +16,7 @@ //! consumer can continue to `use evidence_core::trace::HlrEntry` without //! caring about the split. +mod discovery; mod entries; mod matrix; mod read; @@ -26,6 +27,7 @@ mod test_backlinks; mod uuid; mod validation; +pub use discovery::default_trace_roots; pub use entries::{ DerivedEntry, DerivedFile, HlrEntry, HlrFile, LlrEntry, LlrFile, Schema, TestEntry, TestsFile, TraceMeta, diff --git a/crates/evidence-core/src/trace/discovery.rs b/crates/evidence-core/src/trace/discovery.rs new file mode 100644 index 0000000..065297e --- /dev/null +++ b/crates/evidence-core/src/trace/discovery.rs @@ -0,0 +1,64 @@ +//! Trace-root discovery: pick the project's trace location when the +//! caller hasn't passed `--trace-roots` explicitly. +//! +//! Single source of truth for every consumer — `cargo evidence trace +//! --validate`, `cargo evidence check`, `cargo evidence floors`, and +//! the corresponding `evidence_core::floors::count_trace_per_layer`. +//! All routes through one function so a downstream project's +//! observable trace count is identical across verbs. +//! +//! Discovery order (first existing wins), all resolved relative to +//! `workspace_root`: +//! +//! 1. `/cert/trace/` — the canonical project layout +//! (lives alongside `cert/boundary.toml`, `cert/floors.toml`). +//! 2. Fall back to +//! `load_trace_roots(/cert/boundary.toml)` which +//! reads the `scope.trace_roots` array for projects whose layout +//! cannot follow the convention. +//! +//! Explicit `--trace-roots` always wins and never reaches this +//! function — the CLI's `cmd_*` paths short-circuit on the flag +//! before consulting discovery. + +use std::path::{Path, PathBuf}; + +use crate::policy::load_trace_roots; + +/// Resolve trace roots when the caller has no explicit override. +/// See module docs for discovery semantics. +pub fn default_trace_roots(workspace_root: &Path) -> Vec { + let is_cwd = workspace_root == Path::new(".") || workspace_root == Path::new(""); + let candidate = if is_cwd { + PathBuf::from("cert/trace") + } else { + workspace_root.join("cert/trace") + }; + if candidate.is_dir() { + tracing::info!( + "trace: auto-discovered trace root '{}' (no --trace-roots given)", + candidate.display() + ); + return vec![candidate.to_string_lossy().into_owned()]; + } + let boundary_path = if is_cwd { + PathBuf::from("cert/boundary.toml") + } else { + workspace_root.join("cert/boundary.toml") + }; + tracing::info!( + "trace: cert/trace/ not found; falling back to {}", + boundary_path.display() + ); + let raw = load_trace_roots(&boundary_path); + raw.into_iter() + .map(|s| { + let p = Path::new(&s); + if p.is_absolute() || is_cwd { + s + } else { + workspace_root.join(p).to_string_lossy().into_owned() + } + }) + .collect() +} diff --git a/crates/evidence-core/src/trace/selector_check.rs b/crates/evidence-core/src/trace/selector_check.rs index bc99766..103b587 100644 --- a/crates/evidence-core/src/trace/selector_check.rs +++ b/crates/evidence-core/src/trace/selector_check.rs @@ -31,7 +31,7 @@ //! `#[test]` throughout. If a downstream project hits a macro //! collision, a future PR can swap the resolver for a `syn`-based //! one behind a `--strict` flag; the journal entry in -//! `tool/trace/README.md` documents that escape hatch. +//! `cert/trace/README.md` documents that escape hatch. use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/evidence-core/src/trace/surfaces.rs b/crates/evidence-core/src/trace/surfaces.rs index 68f3a4f..6b6ebb3 100644 --- a/crates/evidence-core/src/trace/surfaces.rs +++ b/crates/evidence-core/src/trace/surfaces.rs @@ -31,7 +31,7 @@ /// to KNOWN_SURFACES would fire the unclaimed-surface rule /// immediately. The gap is itself the point of this bijection — /// a follow-up PR adds HLRs for these subcommands (tracked in -/// tool/trace/README journal). +/// cert/trace/README journal). pub const KNOWN_SURFACES: &[&str] = &[ // Group 1 — CLI verb names (lowercase; match the `Commands::*` // variants exactly). diff --git a/crates/evidence-core/tests/diagnostic_codes_locked.rs b/crates/evidence-core/tests/diagnostic_codes_locked.rs index 779491c..d564743 100644 --- a/crates/evidence-core/tests/diagnostic_codes_locked.rs +++ b/crates/evidence-core/tests/diagnostic_codes_locked.rs @@ -252,12 +252,12 @@ fn rules_terminal_set_matches_terminal_codes() { fn every_code_is_claimed_by_an_llr() { let trace = evidence_core::read_all_trace_files( workspace_root() - .join("tool") + .join("cert") .join("trace") .to_str() .expect("workspace path is UTF-8"), ) - .expect("tool/trace must load"); + .expect("cert/trace must load"); let rules: std::collections::BTreeSet<&str> = evidence_core::RULES.iter().map(|r| r.code).collect(); @@ -290,7 +290,7 @@ fn every_code_is_claimed_by_an_llr() { .collect(); assert!( unclaimed.is_empty(), - "RULES code(s) not claimed by any LLR.emits in tool/trace/llr.toml \ + "RULES code(s) not claimed by any LLR.emits in cert/trace/llr.toml \ (add the code to an LLR that owns its emit path, or add a \ RESERVED_UNCLAIMED_CODES entry with written justification): {:?}", unclaimed @@ -318,7 +318,7 @@ fn code_regex_validator_catches_known_shapes() { /// TEST-043 bijection regression. Every `LinkError::code()` /// return must (a) appear in `RULES` and (b) be claimed by at least -/// one `LLR.emits` list in `tool/trace/llr.toml`. Catches a new +/// one `LLR.emits` list in `cert/trace/llr.toml`. Catches a new /// variant landing in `LinkError` without the matching `RULES` + /// `LLR.emits` edits — the kind of silent-drift hit with the /// three pseudo-codes. The `every_code_is_claimed_by_an_llr` @@ -400,12 +400,12 @@ fn link_error_codes_in_rules_and_claimed() { let trace = evidence_core::read_all_trace_files( workspace_root() - .join("tool") + .join("cert") .join("trace") .to_str() .expect("workspace path is UTF-8"), ) - .expect("tool/trace must load"); + .expect("cert/trace must load"); let claimed: std::collections::BTreeSet = trace .llr .requirements @@ -433,7 +433,7 @@ fn link_error_codes_in_rules_and_claimed() { assert!( unclaimed.is_empty(), "LinkError variants whose code() is not claimed by any LLR.emits \ - in tool/trace/llr.toml: {:?} — add the code to LLR-041's emits (or \ + in cert/trace/llr.toml: {:?} — add the code to LLR-041's emits (or \ another LLR that describes the new emit path)", unclaimed ); diff --git a/crates/evidence-core/tests/floors_trace_discovery.rs b/crates/evidence-core/tests/floors_trace_discovery.rs new file mode 100644 index 0000000..1ba0350 --- /dev/null +++ b/crates/evidence-core/tests/floors_trace_discovery.rs @@ -0,0 +1,170 @@ +//! `floors::count_trace_per_layer` reads the same trace location as +//! every other `cargo evidence` verb (via +//! `evidence_core::trace::default_trace_roots`). A project that places +//! traces under the canonical `cert/trace/` must see floor counts that +//! match what the loader actually parsed. + +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + reason = "test setup failures should panic immediately" +)] + +use std::fs; +use std::path::Path; + +use evidence_core::floors::count_trace_per_layer; +use evidence_core::schema_versions::TRACE; +use tempfile::TempDir; + +fn write_minimal_trace(trace_dir: &Path) { + fs::create_dir_all(trace_dir).unwrap(); + + fs::write( + trace_dir.join("sys.toml"), + format!( + r#" +[meta] +document_id = "SYS-001" +revision = "1.0" + +[schema] +version = "{TRACE}" + +[[requirements]] +id = "SYS-1" +title = "System requirement under test" +owner = "soi" +uid = "00000000-0000-0000-0000-000000000001" +verification_methods = ["test"] +"# + ), + ) + .unwrap(); + + fs::write( + trace_dir.join("hlr.toml"), + format!( + r#" +[meta] +document_id = "HLR-001" +revision = "1.0" + +[schema] +version = "{TRACE}" + +[[requirements]] +id = "HLR-1" +title = "Test requirement" +owner = "soi" +uid = "00000000-0000-0000-0000-000000000010" +traces_to = ["00000000-0000-0000-0000-000000000001"] +verification_methods = ["test"] +"# + ), + ) + .unwrap(); + + fs::write( + trace_dir.join("llr.toml"), + format!( + r#" +[meta] +document_id = "LLR-001" +revision = "1.0" + +[schema] +version = "{TRACE}" + +[[requirements]] +id = "LLR-1" +title = "LLR test" +owner = "soi" +uid = "00000000-0000-0000-0000-000000000020" +derived = false +traces_to = ["00000000-0000-0000-0000-000000000010"] +verification_methods = ["test"] +"# + ), + ) + .unwrap(); + + fs::write( + trace_dir.join("tests.toml"), + format!( + r#" +[meta] +document_id = "TESTS-001" +revision = "1.0" + +[schema] +version = "{TRACE}" + +[[tests]] +id = "TEST-1" +title = "Verify LLR-1" +owner = "soi" +uid = "00000000-0000-0000-0000-000000000030" +traces_to = ["00000000-0000-0000-0000-000000000020"] +test_selector = "fixture::test_one" +"# + ), + ) + .unwrap(); +} + +#[test] +fn count_trace_per_layer_finds_cert_trace_layout() { + let tmp = TempDir::new().unwrap(); + let trace_dir = tmp.path().join("cert").join("trace"); + write_minimal_trace(&trace_dir); + + let (sys, hlr, llr, tests) = count_trace_per_layer(tmp.path()); + assert_eq!((sys, hlr, llr, tests), (1, 1, 1, 1)); +} + +#[test] +fn count_trace_per_layer_returns_zero_on_missing_workspace() { + let tmp = TempDir::new().unwrap(); + let (sys, hlr, llr, tests) = count_trace_per_layer(tmp.path()); + assert_eq!((sys, hlr, llr, tests), (0, 0, 0, 0)); +} + +/// `boundary.toml` `scope.trace_roots` fallback: when the canonical +/// `cert/trace/` is absent, the discovery chain reads +/// `cert/boundary.toml` and resolves its declared roots against +/// `workspace_root`. A project with split traces (e.g. +/// `requirements/system/`, `requirements/software/`) gets per-layer +/// counts summed across them. +#[test] +fn count_trace_per_layer_reads_boundary_scope_trace_roots() { + let tmp = TempDir::new().unwrap(); + let custom_dir = tmp.path().join("requirements"); + write_minimal_trace(&custom_dir); + + fs::create_dir_all(tmp.path().join("cert")).unwrap(); + fs::write( + tmp.path().join("cert/boundary.toml"), + format!( + r#" +[schema] +version = "{ver}" + +[scope] +in_scope = [] +trace_roots = ["requirements"] + +[policy] +no_out_of_scope_deps = false +forbid_build_rs = false +forbid_proc_macros = false +"#, + ver = evidence_core::schema_versions::BOUNDARY + ), + ) + .unwrap(); + + let (sys, hlr, llr, tests) = count_trace_per_layer(tmp.path()); + assert_eq!((sys, hlr, llr, tests), (1, 1, 1, 1)); +} diff --git a/crates/evidence-core/tests/rot_prone_markers_locked.rs b/crates/evidence-core/tests/rot_prone_markers_locked.rs index d2b4484..4e0178e 100644 --- a/crates/evidence-core/tests/rot_prone_markers_locked.rs +++ b/crates/evidence-core/tests/rot_prone_markers_locked.rs @@ -4,10 +4,10 @@ //! Walks: //! //! - `crates/**/{src,tests}/**/*.rs` — production + test source. -//! - `**/*.md` except `tool/trace/README.md` — top-level docs (README, +//! - `**/*.md` except `cert/trace/README.md` — top-level docs (README, //! per-crate docs). The trace journal is audit provenance and stays //! excluded. -//! - `cert/**/*.toml` — our own cert state. `tool/trace/**` stays +//! - `cert/**/*.toml` — our own cert state. `cert/trace/**` stays //! excluded (legitimate audit trail). //! //! Applies a pinned regex pattern set and fails via `assert!` with @@ -28,7 +28,7 @@ //! //! ## Out of scope //! -//! - `tool/trace/**` — PR refs in trace TOML are audit provenance, +//! - `cert/trace/**` — PR refs in trace TOML are audit provenance, //! legitimate. //! - Commit messages — immutable history. //! - Stable identifiers (`LLR-NNN`, `TEST-NNN`, function names). @@ -123,8 +123,8 @@ fn banned_patterns() -> Vec<(&'static str, Regex)> { /// Scope: /// - `crates/**/*.rs` (excluding `target/`, `fixtures/`). /// - `**/*.md` at the workspace root and under `crates/`, but NOT -/// `tool/trace/README.md` (audit journal; legitimate PR refs). -/// - `cert/**/*.toml` (our own cert state); `tool/trace/**/*.toml` +/// `cert/trace/README.md` (audit journal; legitimate PR refs). +/// - `cert/**/*.toml` (our own cert state); `cert/trace/**/*.toml` /// stays excluded — entries legitimately cite the implementing PR. fn collect_scan_targets(workspace_root: &Path) -> Vec { let mut out = Vec::new(); @@ -146,7 +146,7 @@ fn collect_rs(root: &Path, out: &mut Vec) { } /// Walk `.md` files. Skips `target/`, `.git/`, `node_modules/`, -/// `tool/trace/` (journal = audit provenance). When invoked from +/// `cert/trace/` (journal = audit provenance). When invoked from /// the workspace root, also skips `cert/` (the toml walker handles /// it). fn collect_md(root: &Path, out: &mut Vec, is_workspace_root: bool) { @@ -166,7 +166,7 @@ fn collect_md(root: &Path, out: &mut Vec, is_workspace_root: bool) { ) { return false; } - // Skip tool/trace at any depth: the journal there is + // Skip cert/trace at any depth: the journal there is // audit trail, not rot. if e.file_type().is_dir() && e.file_name().to_str() == Some("trace") @@ -174,7 +174,7 @@ fn collect_md(root: &Path, out: &mut Vec, is_workspace_root: bool) { .parent() .and_then(|p| p.file_name()) .and_then(|n| n.to_str()) - == Some("tool") + == Some("cert") { return false; } @@ -191,6 +191,22 @@ fn collect_md(root: &Path, out: &mut Vec, is_workspace_root: bool) { fn collect_toml_under(root: &Path, out: &mut Vec) { let files = traversal::walk(root) + .filter_entry(|e| { + // Skip cert/trace/ — the trace TOMLs are an audit + // journal whose PR refs are legitimate provenance, not + // rot. + if e.file_type().is_dir() + && e.file_name().to_str() == Some("trace") + && e.path() + .parent() + .and_then(|p| p.file_name()) + .and_then(|n| n.to_str()) + == Some("cert") + { + return false; + } + true + }) .filter_map(Result::ok) .filter(|e| e.file_type().is_file() && traversal::has_ext(e.path(), "toml")) .map(|e| e.into_path()); diff --git a/crates/evidence-core/tests/trace_id_refs_locked.rs b/crates/evidence-core/tests/trace_id_refs_locked.rs index 9398775..15ceb73 100644 --- a/crates/evidence-core/tests/trace_id_refs_locked.rs +++ b/crates/evidence-core/tests/trace_id_refs_locked.rs @@ -1,13 +1,13 @@ //! Gate against narrative trace-ID references (SYS/HLR/LLR/TEST/ -//! DERIVED-NNN) that don't resolve to a real entry in `tool/trace/` +//! DERIVED-NNN) that don't resolve to a real entry in `cert/trace/` //! (LLR-046). //! //! Walks: //! //! - `crates/**/{src,tests}/**/*.rs` — production + test source. -//! - `**/*.md` except `tool/trace/README.md` — top-level docs. -//! - `**/*.toml` outside `tool/trace/` — our own cert state, Cargo -//! manifests, etc. `tool/trace/**/*.toml` is the ground-truth +//! - `**/*.md` except `cert/trace/README.md` — top-level docs. +//! - `**/*.toml` outside `cert/trace/` — our own cert state, Cargo +//! manifests, etc. `cert/trace/**/*.toml` is the ground-truth //! registry and is excluded from the walk (its own `id` fields //! are what we're validating against). //! @@ -64,7 +64,7 @@ fn workspace_root() -> PathBuf { /// "LLR-NNN")`). /// /// Current exemptions — all illustrative / local-fixture refs in -/// test-file prose, not cross-references to `tool/trace/`: +/// test-file prose, not cross-references to `cert/trace/`: const RESERVED_TEXT_REFS: &[(&str, &str)] = &[ // Illustrative example of the single-digit fixture-ID shape in // `comment_window`'s docstring. Not a trace ref. @@ -74,16 +74,16 @@ const RESERVED_TEXT_REFS: &[(&str, &str)] = &[ ("trace_id_refs_locked.rs", "LLR-999"), // Narrative reference to a locally-constructed synthetic // fixture in `check_source_tree.rs`. Not a cross-reference to - // `tool/trace/`. + // `cert/trace/`. ("check_source_tree.rs", "TEST-1"), // `check_source_correctness.rs` seeds a downstream tempdir // fixture with a synthetic `DOWNSTREAM-`-prefixed trace ID; // the scanner strips the prefix and sees a bare single-digit - // test ref. Not a cross-reference to `tool/trace/`. + // test ref. Not a cross-reference to `cert/trace/`. ("check_source_correctness.rs", "TEST-1"), ]; -/// Per-kind valid-ID set loaded from `tool/trace/`. +/// Per-kind valid-ID set loaded from `cert/trace/`. struct TraceIdSets { sys: BTreeSet, hlr: BTreeSet, @@ -94,13 +94,13 @@ struct TraceIdSets { fn load_trace_id_sets(workspace: &Path) -> TraceIdSets { let trace_root = workspace - .join("tool") + .join("cert") .join("trace") .to_str() .expect("path is UTF-8") .to_string(); let trace = - evidence_core::read_all_trace_files(&trace_root).expect("tool/trace must load cleanly"); + evidence_core::read_all_trace_files(&trace_root).expect("cert/trace must load cleanly"); let sys: BTreeSet = trace .sys .requirements @@ -281,24 +281,24 @@ fn is_reserved(rel: &str, full_ref: &str) -> bool { .any(|(path_suffix, r)| normalized.ends_with(path_suffix) && *r == full_ref) } -/// Populate `/tool/trace/` from the real workspace so +/// Populate `/cert/trace/` from the real workspace so /// `scan_tree(workspace)` resolves ghost refs against the real /// `read_all_trace_files` valid-ID set. Unix uses a symlink; /// Windows falls back to copying the four toml files (cheap; they /// are small). fn setup_fake_workspace_with_real_trace(workspace: &Path) { let real = workspace_root(); - let fake_trace = workspace.join("tool").join("trace"); - fs::create_dir_all(workspace.join("tool")).expect("mkdir tool"); + let fake_trace = workspace.join("cert").join("trace"); + fs::create_dir_all(workspace.join("cert")).expect("mkdir tool"); #[cfg(unix)] - std::os::unix::fs::symlink(real.join("tool").join("trace"), &fake_trace) + std::os::unix::fs::symlink(real.join("cert").join("trace"), &fake_trace) .expect("symlink trace"); #[cfg(not(unix))] { fs::create_dir_all(&fake_trace).expect("mkdir trace"); for name in ["sys.toml", "hlr.toml", "llr.toml", "tests.toml"] { fs::copy( - real.join("tool").join("trace").join(name), + real.join("cert").join("trace").join(name), fake_trace.join(name), ) .expect("copy trace file"); @@ -313,7 +313,7 @@ fn current_tree_is_clean() { assert!( hits.is_empty(), "found {} trace-ID reference(s) in source/docs that don't resolve to any \ - entry in tool/trace/. A deleted or renumbered trace entry has left stale \ + entry in cert/trace/. A deleted or renumbered trace entry has left stale \ narrative pointers behind. Either restore the referenced entry, update \ the reference to a still-valid identifier, or add the (file, ref) pair \ to RESERVED_TEXT_REFS with written justification.\n\n{}", @@ -336,7 +336,7 @@ fn id_with_kind(kind: &str, id: &str) -> String { /// gate, naming the offending file:line. #[test] fn fires_on_ghost_reference() { - // The gate always loads the real `tool/trace/` (via + // The gate always loads the real `cert/trace/` (via // `read_all_trace_files`), so the fixture only needs to // contain a grep'd ref that isn't in the real set. LLR-999 // is guaranteed not to exist. diff --git a/crates/evidence-core/tests/trace_id_refs_locked/walker.rs b/crates/evidence-core/tests/trace_id_refs_locked/walker.rs index 8bc1fa0..047f256 100644 --- a/crates/evidence-core/tests/trace_id_refs_locked/walker.rs +++ b/crates/evidence-core/tests/trace_id_refs_locked/walker.rs @@ -22,10 +22,10 @@ use super::traversal; /// Scope: /// - `crates/**/*.rs` (excluding `target/`, `fixtures/`). /// - `**/*.md` at workspace root and under `crates/`, but NOT -/// `tool/trace/README.md` (journal = audit provenance; stale +/// `cert/trace/README.md` (journal = audit provenance; stale /// refs there would be historical artifacts, not drift). -/// - `**/*.toml` outside `tool/trace/` — Cargo manifests, cert -/// baselines, floors.toml. `tool/trace/**/*.toml` is the +/// - `**/*.toml` outside `cert/trace/` — Cargo manifests, cert +/// baselines, floors.toml. `cert/trace/**/*.toml` is the /// ground truth we validate against and is explicitly excluded. pub fn collect_scan_targets(workspace: &Path) -> Vec { let mut out = Vec::new(); @@ -56,7 +56,7 @@ fn collect_by_ext(root: &Path, ext: &str, out: &mut Vec) { out.extend(files); } -/// Walk `.md` files, skipping `tool/trace/` (journal = audit +/// Walk `.md` files, skipping `cert/trace/` (journal = audit /// provenance) at any depth, and — when invoked from the workspace /// root — top-level `cert/` so the toml walker owns that subtree. fn collect_md_non_trace(root: &Path, out: &mut Vec, is_workspace_root: bool) { @@ -82,7 +82,7 @@ fn collect_md_non_trace(root: &Path, out: &mut Vec, is_workspace_root: .parent() .and_then(|p| p.file_name()) .and_then(|n| n.to_str()) - == Some("tool") + == Some("cert") { return false; } @@ -97,7 +97,7 @@ fn collect_md_non_trace(root: &Path, out: &mut Vec, is_workspace_root: out.extend(files); } -/// Walk `.toml` files, skipping `tool/trace/` (the source of +/// Walk `.toml` files, skipping `cert/trace/` (the source of /// truth) and standard noise dirs. fn collect_toml_non_trace(root: &Path, out: &mut Vec) { let files = traversal::walk(root) @@ -121,7 +121,7 @@ fn collect_toml_non_trace(root: &Path, out: &mut Vec) { .parent() .and_then(|p| p.file_name()) .and_then(|n| n.to_str()) - == Some("tool") + == Some("cert") { return false; } diff --git a/crates/evidence-mcp/src/lib.rs b/crates/evidence-mcp/src/lib.rs index 7915d9e..1b99588 100644 --- a/crates/evidence-mcp/src/lib.rs +++ b/crates/evidence-mcp/src/lib.rs @@ -23,7 +23,7 @@ //! value for `mode`). //! //! See [`Server`] for the handler + tool methods. See SYS-018 / -//! HLR-050 / LLR-050 / TEST-050 in `tool/trace/` for the requirements +//! HLR-050 / LLR-050 / TEST-050 in `cert/trace/` for the requirements //! chain behind this crate; LLR-062 covers the `serverInfo` identity //! override and the `lib.rs`-as-facade split; LLR-063 covers the //! version-skew probe; LLR-064 pins the structured tool-layer