Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions cert/floors.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ schema_version = 1

# evidence_core::RULES length — every diagnostic code the tool can emit,
# hand-curated. Lives in a single crate, so workspace-wide.
diagnostic_codes = 150
diagnostic_codes = 151

# evidence_core::TERMINAL_CODES length — hand-emitted terminals
# (VERIFY_OK / VERIFY_FAIL / VERIFY_ERROR / CLI_SUBCOMMAND_ERROR /
Expand All @@ -52,11 +52,11 @@ terminal_codes = 13
# tool/trace/sys.toml — System Requirements.
trace_sys = 28
# tool/trace/hlr.toml — High-Level Requirements.
trace_hlr = 65
trace_hlr = 66
# tool/trace/llr.toml — Low-Level Requirements.
trace_llr = 72
trace_llr = 73
# tool/trace/tests.toml — Test Cases.
trace_test = 77
trace_test = 78

# evidence_core::trace::surfaces::KNOWN_SURFACES length — hand-curated
# catalog of CLI verbs + named observable contracts. Matching HLR
Expand All @@ -78,10 +78,10 @@ known_surfaces = 21

[per_crate.evidence-core]
# `#[test]` attribute count inside crates/evidence-core/**/*.rs.
test_count = 351
test_count = 355

[per_crate.cargo-evidence]
test_count = 133
test_count = 134

[per_crate.evidence-mcp]
test_count = 37
Expand Down
3 changes: 3 additions & 0 deletions crates/cargo-evidence/src/cli/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ pub fn cmd_generate(args: GenerateArgs) -> Result<i32> {
if let Some(code) = policy::enforce_boundary_policy(&derived, profile, json_output)? {
return Ok(code);
}
if let Some(code) = policy::enforce_dal_qualification(&derived, profile, json_output)? {
return Ok(code);
}
let strict = matches!(profile, Profile::Cert | Profile::Record);
let mut builder = match phases::init_builder(config, profile, quiet, json_output)? {
Ok(b) => b,
Expand Down
7 changes: 7 additions & 0 deletions crates/cargo-evidence/src/cli/generate/phases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ pub(super) struct BoundaryDerived {
/// Raw policy flags, carried so the policy-implementability
/// check can fire before the builder is constructed.
pub(super) policy: BoundaryPolicy,
/// Auxiliary MC/DC tool reference, propagated from
/// `boundary.toml`'s `[dal]` section so the DAL-A
/// qualification gate can read it without re-loading the
/// config. `None` ⇒ no external MC/DC evidence claimed.
pub(super) auxiliary_mcdc_tool: Option<evidence_core::AuxiliaryMcdcTool>,
}

// Phase 1 — preflight checks (shallow-clone, cert-dirty)
Expand Down Expand Up @@ -77,6 +82,7 @@ pub(super) fn build_config(
let dal_map = boundary_config.dal_map();
let max_dal = dal_map.values().copied().max().unwrap_or_default();
let policy = boundary_config.policy.clone();
let auxiliary_mcdc_tool = boundary_config.dal.auxiliary_mcdc_tool.clone();
let strict = matches!(profile, Profile::Cert | Profile::Record);
let config = EvidenceBuildConfig {
output_root,
Expand All @@ -96,6 +102,7 @@ pub(super) fn build_config(
dal_map,
max_dal,
policy,
auxiliary_mcdc_tool,
},
)
}
Expand Down
55 changes: 55 additions & 0 deletions crates/cargo-evidence/src/cli/generate/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,58 @@ pub(super) fn enforce_boundary_policy(

Ok(None)
}

/// DAL-A qualification gate: refuse to assemble a cert/record bundle
/// when any in-scope crate is at DAL-A and the policy does not record
/// an [`evidence_core::AuxiliaryMcdcTool`] reference. DO-178C Annex A
/// Table A-7 Obj-7 (MC/DC) is required at DAL-A; stable Rust cannot
/// emit MC/DC instrumentation today (rust-lang/rust#144999 removed
/// the unstable flag), so the only viable path is to record an
/// external qualified tool's evidence by reference.
///
/// On `Profile::Dev`, fires a Warning-severity diagnostic and
/// returns `Ok(None)` — dev iteration is unblocked. On
/// `Profile::Cert` / `Profile::Record`, fails the run with a
/// JSON/text envelope so the caller can short-circuit.
pub(super) fn enforce_dal_qualification(
derived: &BoundaryDerived,
profile: Profile,
json_output: bool,
) -> Result<Option<i32>> {
match evidence_core::check_dal_a_mcdc_evidence(
&derived.dal_map,
derived.auxiliary_mcdc_tool.as_ref(),
) {
Ok(()) => Ok(None),
Err(evidence_core::BoundaryCheckError::DalAMissingAuxiliaryMcdc {
dal_a_crates, ..
}) => {
let lines: Vec<String> = dal_a_crates.iter().map(|c| format!(" - {}", c)).collect();
let msg = format!(
"DAL-A qualification gap: stable Rust cannot emit MC/DC \
instrumentation (rust-lang/rust#144999, merged 2025-08-08). \
{} in-scope crate(s) at DAL-A but no `[dal.auxiliary_mcdc_tool]` \
entry in cert/boundary.toml records an external qualified MC/DC \
tool's evidence:\n{}\n\
Either: (a) record the auxiliary tool reference in \
boundary.toml's `[dal]` section (name, qualification_id, \
report) so the bundle binds external MC/DC evidence by \
reference, OR (b) lower the affected crate(s) to DAL-B \
(which does not require MC/DC), OR (c) wait for upstream \
rustc to reintroduce MC/DC instrumentation \
(tracking: rust-lang/rust#124144).",
dal_a_crates.len(),
lines.join("\n")
);
// dev profile downgrades to a stderr warning + continue;
// cert/record fails the run.
if matches!(profile, Profile::Dev) {
eprintln!("warning: {}", msg);
Ok(None)
} else {
fail(json_output, profile, msg).map(Some)
}
}
Err(e) => Err(anyhow::Error::new(e).context("running DAL-A MC/DC qualification gate")),
}
}
5 changes: 5 additions & 0 deletions crates/cargo-evidence/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,8 @@ fn test_init_template_does_not_trip_policy_gate() {
stderr
);
}

// TEST-080's integration arm lives in
// `crates/cargo-evidence/tests/dal_qualification_gate.rs` (its own
// integration-test file) so this orchestrator stays under the
// workspace 500-line limit.
94 changes: 94 additions & 0 deletions crates/cargo-evidence/tests/dal_qualification_gate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Integration tests for the DAL-A MC/DC qualification gate
//! (HLR-066 / LLR-073).
//!
//! Lives as its own integration-test file (rather than a sibling
//! function in `cli.rs`) so the orchestrator stays under the
//! workspace 500-line limit.

#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test setup failures should panic immediately"
)]

use std::fs;

use assert_cmd::Command;
use tempfile::TempDir;

fn cargo_evidence() -> Command {
#[allow(deprecated)]
Command::cargo_bin("cargo-evidence").unwrap()
}

fn write_dal_a_boundary_toml(path: &std::path::Path) {
fs::create_dir_all(path.join("cert")).unwrap();
fs::write(
path.join("cert/boundary.toml"),
format!(
r#"
[schema]
version = "{ver}"

[scope]
in_scope = ["flight_core"]

[policy]
no_out_of_scope_deps = false
forbid_build_rs = false
forbid_proc_macros = false

[dal]
default_dal = "A"
"#,
ver = evidence_core::schema_versions::BOUNDARY
),
)
.unwrap();
}

/// TEST-080 integration arm. Set up a fixture workspace with an
/// in-scope crate at DAL-A and no `[dal.auxiliary_mcdc_tool]`, then
/// run `cargo evidence generate --profile dev`. The DAL-A MC/DC
/// gate must emit a `warning:` line on stderr (LLR-073's dev-side
/// soft path) and not abort on that gate alone — downstream phases
/// may still fail because the tempdir isn't a real workspace, but
/// the failure must NOT come from the DAL-A gate's `error:` envelope.
#[test]
fn test_generate_dev_profile_warns_on_dal_a_without_mcdc_tool_but_continues() {
let tmp = TempDir::new().unwrap();
write_dal_a_boundary_toml(tmp.path());

let out = TempDir::new().unwrap();
let result = cargo_evidence()
.arg("evidence")
.arg("generate")
.arg("--out-dir")
.arg(out.path())
.arg("--profile")
.arg("dev")
.current_dir(tmp.path())
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&result.stderr);
// Dev path must emit the gate's prose with a `warning:` prefix
// so an iterating user sees the issue but isn't blocked by the
// gate itself. (Later phases — cargo metadata against a tempdir
// that isn't a real workspace, missing trace roots — may still
// fail downstream; the warn-and-continue contract is purely
// about THIS gate's behavior.)
assert!(
stderr.contains("warning: DAL-A qualification gap"),
"expected dev-profile DAL-A warning prefixed with `warning:`, got stderr:\n{}",
stderr
);
// The fail-loud envelope at cert/record uses `error:` (via the
// shared `fail` helper). Pin that the dev path took the warn
// branch by absence of `error: DAL-A qualification gap`.
assert!(
!stderr.contains("error: DAL-A qualification gap"),
"dev profile must not emit the cert-style `error:` envelope; stderr:\n{}",
stderr
);
}
7 changes: 7 additions & 0 deletions crates/cargo-evidence/tests/fixtures/golden_rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
"has_fix_hint": false,
"terminal": false
},
{
"code": "BOUNDARY_DAL_A_MISSING_AUXILIARY_MCDC",
"severity": "error",
"domain": "boundary",
"has_fix_hint": false,
"terminal": false
},
{
"code": "BOUNDARY_FORBIDDEN_BUILD_RS",
"severity": "error",
Expand Down
Loading
Loading