Skip to content

Commit 7ee66dd

Browse files
Haofeiclaude
andcommitted
Add valid_for_gating watermark + reir collect --strict (review #1)
Evidence built from invalid source (error diagnostics) could still be consumed as a passing gate input. Make that impossible to do silently: - CiGateOutput gains `valid_for_gating` (default true) + `gating_reason`. The gate sets it false and forces status=Fail when the bundle contains any error-severity diagnostic fact, regardless of capability reconciliation. - `reir collect --strict` refuses to emit a bundle that contains error diagnostics (fail closed at evidence-generation time). - tests: an error diagnostic makes the bundle invalid_for_gating + Fail. Together with the earlier `pkg metadata` fail-closed (3.1), review/REIR evidence from a half-broken AST no longer reads as authoritative. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a0f565a commit 7ee66dd

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

reir/src/format.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ pub fn format_pr_review_comment(
184184
pub struct CiGateOutput {
185185
pub schema: &'static str,
186186
pub status: CiGateStatus,
187+
/// False when the evidence was derived from invalid source (error
188+
/// diagnostics): the bundle must not be trusted as a gate input.
189+
#[serde(default = "default_true")]
190+
pub valid_for_gating: bool,
191+
#[serde(default, skip_serializing_if = "Option::is_none")]
192+
pub gating_reason: Option<String>,
187193
pub summary: CiGateSummary,
188194
pub required_capabilities: Vec<CiCapabilityFact>,
189195
pub granted_capabilities: Vec<CiCapabilityFact>,
@@ -295,6 +301,10 @@ impl Default for CiGatePolicy {
295301
}
296302
}
297303

304+
fn default_true() -> bool {
305+
true
306+
}
307+
298308
/// Whether a fact's acquisition mode is an author declaration (the package author
299309
/// asserted it) rather than independently established evidence.
300310
fn is_author_declared(fact: &Fact) -> bool {
@@ -379,7 +389,17 @@ pub fn format_ci_gate_output_with_policy(
379389
0.0
380390
};
381391

382-
let fail = (policy.fail_on_missing && !missing.is_empty())
392+
// Evidence derived from invalid source (error diagnostics) must not be
393+
// trusted as a gate input, regardless of capability reconciliation.
394+
let has_error_diagnostics = required_facts
395+
.iter()
396+
.chain(granted_facts.iter())
397+
.any(|fact| fact.kind == FactKind::Diagnostic && fact.unknown_reason.is_some());
398+
let valid_for_gating = !has_error_diagnostics;
399+
let gating_reason = (!valid_for_gating).then(|| "error_diagnostics".to_string());
400+
401+
let fail = !valid_for_gating
402+
|| (policy.fail_on_missing && !missing.is_empty())
383403
|| (policy.fail_on_unknown && !unknown_facts.is_empty())
384404
|| (policy.fail_on_excess && !excess.is_empty())
385405
|| (policy.require_verified_capabilities && !unverified_facts.is_empty());
@@ -450,6 +470,8 @@ pub fn format_ci_gate_output_with_policy(
450470
CiGateOutput {
451471
schema: "reir.ci.v0.2",
452472
status,
473+
valid_for_gating,
474+
gating_reason,
453475
summary: CiGateSummary {
454476
total_required,
455477
total_granted,
@@ -1517,6 +1539,24 @@ mod tests {
15171539
assert_eq!(strict.status, CiGateStatus::Fail);
15181540
}
15191541

1542+
#[test]
1543+
fn error_diagnostic_makes_bundle_invalid_for_gating() {
1544+
let required = capability_fact("fact.req", FactRole::Required, FactValue::True);
1545+
let granted = capability_fact("fact.grant", FactRole::Granted, FactValue::True);
1546+
let recon = crate::reconcile_capabilities(
1547+
std::slice::from_ref(&required),
1548+
std::slice::from_ref(&granted),
1549+
);
1550+
let out = format_ci_gate_output(
1551+
&[required, diagnostic_fact("fact.diag.error")],
1552+
std::slice::from_ref(&granted),
1553+
&recon,
1554+
);
1555+
assert!(!out.valid_for_gating);
1556+
assert_eq!(out.gating_reason.as_deref(), Some("error_diagnostics"));
1557+
assert_eq!(out.status, CiGateStatus::Fail);
1558+
}
1559+
15201560
#[test]
15211561
fn require_verified_capabilities_blocks_author_declared() {
15221562
let mut required = capability_fact("fact.req.s3", FactRole::Required, FactValue::True);

reir/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,12 @@ fn try_run_collect(args: &[String]) -> Result<ExitCode, CliError> {
113113
let mut from = None;
114114
let mut out = None;
115115
let mut json = false;
116+
let mut strict = false;
116117
let mut index = 0;
117118

118119
while index < args.len() {
119120
match args[index].as_str() {
121+
"--strict" => strict = true,
120122
"--producer" => producer = Some(take_value(args, &mut index, "--producer")?),
121123
"--review-map" => review_map = Some(take_value(args, &mut index, "--review-map")?),
122124
"--package-review" => {
@@ -263,6 +265,22 @@ fn try_run_collect(args: &[String]) -> Result<ExitCode, CliError> {
263265
package_name: package_name.as_deref(),
264266
})?;
265267

268+
if strict {
269+
let error_diagnostics = bundle
270+
.facts
271+
.iter()
272+
.filter(|fact| {
273+
fact.kind == reir::FactKind::Diagnostic && fact.unknown_reason.is_some()
274+
})
275+
.count();
276+
if error_diagnostics > 0 {
277+
return Err(CliError::usage(format!(
278+
"--strict: refusing to emit REIR evidence built from {error_diagnostics} error \
279+
diagnostic(s); fix the source and re-run"
280+
)));
281+
}
282+
}
283+
266284
if let Some(out_path) = &out {
267285
write_json_file(out_path, &bundle)?;
268286
if !json {

0 commit comments

Comments
 (0)