Skip to main content

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}