agent_profile/adapter/gate.rs
1// Copyright (c) 2026 Costas Kirgoussios
2//
3// This software is licensed under the PolyForm Noncommercial License 1.0.0.
4// You may obtain a copy of the License at:
5// https://polyformproject.org/licenses/noncommercial/1.0.0/
6
7//! The evidence gates (SP4 design §5), as predicates over `AdapterMetadata`.
8//!
9//! These live in the library rather than in `tests/adapter_contract.rs` for one reason: a gate whose only
10//! expression is an assertion over `static METADATA` items cannot be proven non-vacuous. `cargo mutants`
11//! mutates functions, not statics, so a weaker-than-intended gate — Gate B's biconditional written as a
12//! single implication, say — would pass purely because no shipped adapter exhibits the excluded
13//! combination. As functions they take negative fixtures, and a mutant reaches them.
14
15use super::metadata::{AdapterMetadata, Capability, CapabilityState, SupportLevel};
16
17/// The provenance prefixes a `basis` may start with (SP4 design D5).
18pub const MEASURED: &str = "measured: ";
19pub const CITED: &str = "cited: ";
20pub const UNMEASURED: &str = "unmeasured: ";
21
22/// The encoding of a failed probe in `evidence.upstream_version` (SP4 design §9 outcome 2).
23pub const UNKNOWN_VERSION: &str = "unknown";
24
25/// Why a gate refused. One variant per rule, so a test names the rule it is pinning.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum GateFailure {
28 /// Gate A: `source_url` is neither a URL nor the literal `measured`.
29 SourceUrlShape { id: &'static str, source_url: &'static str },
30 /// Gate A: `source_url` is `measured` but `notes` is empty.
31 MeasuredWithoutNotes { id: &'static str },
32 /// Gate A: `upstream_version` is outside `[A-Za-z0-9._-]+`, so it cannot name one evidence file.
33 VersionCharset { id: &'static str, version: &'static str },
34 /// Gate B: a `basis` carries no provenance prefix.
35 BasisPrefix { id: &'static str, capability: Capability, basis: &'static str },
36 /// Gate B: a `basis` is only its provenance prefix, so it cites a provenance for nothing.
37 BasisWithoutContent { id: &'static str, capability: Capability, basis: &'static str },
38 /// Gate B: a `basis` contains a newline, which would render as two report lines.
39 BasisNewline { id: &'static str, capability: Capability },
40 /// Gate B: `unmeasured:` without `Unknown`, or `Unknown` without `unmeasured:`.
41 UnmeasuredMismatch { id: &'static str, capability: Capability, state: CapabilityState },
42 /// Gate C: `Proven` with empty `notes`.
43 ProvenWithoutNotes { id: &'static str },
44 /// Gate C: `Proven` with more than one `Unknown` claim.
45 ProvenWithTooManyUnknowns { id: &'static str, unknowns: usize },
46 /// Gate C: a failed probe that did not degrade to `Experimental` with an unknown config claim.
47 UnknownVersionNotExperimental { id: &'static str },
48}
49
50/// Gate A's shape half: what can be checked before any transcript exists (SP4 design §5.2).
51///
52/// The transcript clauses are deliberately absent. They assert files the probe run produces, and the probe
53/// run happens after the phase that adds this function, so asserting them here would leave the branch red
54/// for three adapters with no artefact able to make it green.
55pub fn gate_a_shape(metadata: &AdapterMetadata) -> Result<(), GateFailure> {
56 let evidence = metadata.evidence;
57 let url =
58 evidence.source_url.starts_with("https://") || evidence.source_url.starts_with("http://");
59 if !url && evidence.source_url != "measured" {
60 return Err(GateFailure::SourceUrlShape {
61 id: metadata.id,
62 source_url: evidence.source_url,
63 });
64 }
65 if evidence.source_url == "measured" && evidence.notes.is_empty() {
66 return Err(GateFailure::MeasuredWithoutNotes { id: metadata.id });
67 }
68 if evidence.upstream_version.is_empty()
69 || !evidence
70 .upstream_version
71 .bytes()
72 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
73 {
74 return Err(GateFailure::VersionCharset {
75 id: metadata.id,
76 version: evidence.upstream_version,
77 });
78 }
79 Ok(())
80}
81
82/// Gate B: every `basis` carries a provenance prefix, no newline, and `unmeasured:` exactly when `Unknown`.
83///
84/// The biconditional is what makes D7 mechanical. Without it an implementer reaches `Proven` by writing
85/// `NotGuaranteed` with an `unmeasured:` basis, and every other gate still passes.
86pub fn gate_b(metadata: &AdapterMetadata) -> Result<(), GateFailure> {
87 for claim in metadata.capabilities {
88 let unmeasured = claim.basis.starts_with(UNMEASURED);
89 let prefix = if unmeasured {
90 Some(UNMEASURED)
91 } else if claim.basis.starts_with(MEASURED) {
92 Some(MEASURED)
93 } else if claim.basis.starts_with(CITED) {
94 Some(CITED)
95 } else {
96 None
97 };
98 let Some(prefix) = prefix else {
99 return Err(GateFailure::BasisPrefix {
100 id: metadata.id,
101 capability: claim.capability,
102 basis: claim.basis,
103 });
104 };
105 if claim.basis[prefix.len()..].trim().is_empty() {
106 return Err(GateFailure::BasisWithoutContent {
107 id: metadata.id,
108 capability: claim.capability,
109 basis: claim.basis,
110 });
111 }
112 if claim.basis.contains('\n') {
113 return Err(GateFailure::BasisNewline {
114 id: metadata.id,
115 capability: claim.capability,
116 });
117 }
118 if unmeasured != (claim.state == CapabilityState::Unknown) {
119 return Err(GateFailure::UnmeasuredMismatch {
120 id: metadata.id,
121 capability: claim.capability,
122 state: claim.state,
123 });
124 }
125 }
126 Ok(())
127}
128
129/// Gate C: what `Proven` costs, and what a failed probe forces (SP4 design §5, D6).
130pub fn gate_c(metadata: &AdapterMetadata) -> Result<(), GateFailure> {
131 let unknowns = metadata
132 .capabilities
133 .iter()
134 .filter(|claim| claim.state == CapabilityState::Unknown)
135 .count();
136 if metadata.support == SupportLevel::Proven {
137 if metadata.evidence.notes.is_empty() {
138 return Err(GateFailure::ProvenWithoutNotes { id: metadata.id });
139 }
140 // V3:132-133 permits `Proven` while ONE capability remains unresolved; a second means the
141 // mechanism itself is not understood, which is what `Experimental` is for.
142 if unknowns > 1 {
143 return Err(GateFailure::ProvenWithTooManyUnknowns { id: metadata.id, unknowns });
144 }
145 }
146 // A failed probe observed nothing, so it cannot support a claim about the mechanism. Without this the
147 // token exemption in Gate A becomes a hole: `upstream_version: "unknown"` beside a `Supported` config
148 // claim would reach `Proven` with no mechanism token observed anywhere.
149 // Case-insensitive: Gate A's charset ([A-Za-z0-9._-]) accepts "UNKNOWN"/"Unknown" as well as the
150 // lowercase sentinel, and a cased spelling must degrade exactly like the lowercase one. This is
151 // deliberately NOT mirrored in any shell script: the harness only ever *writes* the sentinel
152 // (sandbox/probes/common.sh via `${probe_extracted:-unknown}`, sandbox/transcript.sh's
153 // `version=unknown` default), never compares it — sandbox/verify-transcripts.sh's conclusion rule
154 // reads probe-exit, not the version — so the harness cannot produce a cased variant. The only
155 // reachable path to a cased sentinel is a maintainer hand-typing it into Rust metadata, which this
156 // comparison alone must catch.
157 if metadata.evidence.upstream_version.eq_ignore_ascii_case(UNKNOWN_VERSION) {
158 let config_unknown = metadata.capabilities.iter().any(|claim| {
159 claim.capability == Capability::ConfigIsolation
160 && claim.state == CapabilityState::Unknown
161 });
162 if metadata.support != SupportLevel::Experimental || !config_unknown {
163 return Err(GateFailure::UnknownVersionNotExperimental { id: metadata.id });
164 }
165 }
166 Ok(())
167}
168
169/// Every gate that applies before the probe run, for one adapter.
170pub fn gates_before_transcripts(metadata: &AdapterMetadata) -> Result<(), GateFailure> {
171 gate_a_shape(metadata)?;
172 gate_b(metadata)?;
173 gate_c(metadata)
174}