Skip to main content

lean_ctx/core/
conformance.rs

1//! Conformance & reproducibility scorecard (`conformance-v1`, EPIC 12.17).
2//!
3//! A self-check any user or CI can run to prove this instance honors its own
4//! contracts and that its extension surface behaves. It exercises three areas:
5//!
6//! * **contracts** — every machine-verified contract version is present.
7//! * **reproducibility** — the public discovery documents (`/v1/capabilities`,
8//!   `/v1/openapi.json`) are deterministic (same bytes across two builds).
9//! * **extensions** — every registered compressor / chunker / read-mode in the
10//!   [`extension_registry`](super::extension_registry) satisfies the stable
11//!   invariants the engine relies on (determinism, budget honoring, coverage).
12//!
13//! The output is a [`Scorecard`]: a flat list of [`Check`]s plus a pass count.
14//! It is data, not prose, so it can be rendered (CLI), shared (JSON), or gated
15//! on (`all_passed()` in `tests/conformance_suite.rs`).
16
17use serde::Serialize;
18use serde_json::{Value, json};
19
20/// A representative corpus the extension invariants run against. Mixes blank
21/// lines, multibyte UTF-8, and paragraph boundaries to stress edge cases.
22const CORPUS: &[&str] = &[
23    "",
24    "single line",
25    "a\n\n\n\nb  \n",
26    "para one\n\npara two\n\n\npara three",
27    "mültibyte ä ö ü 漢字 \n\n end",
28];
29
30/// One conformance check result.
31#[derive(Debug, Clone, Serialize)]
32pub struct Check {
33    pub name: String,
34    pub category: String,
35    pub passed: bool,
36    pub detail: String,
37}
38
39impl Check {
40    fn pass(category: &str, name: impl Into<String>) -> Self {
41        Self {
42            name: name.into(),
43            category: category.to_string(),
44            passed: true,
45            detail: String::new(),
46        }
47    }
48
49    fn fail(category: &str, name: impl Into<String>, detail: impl Into<String>) -> Self {
50        Self {
51            name: name.into(),
52            category: category.to_string(),
53            passed: false,
54            detail: detail.into(),
55        }
56    }
57
58    fn from_bool(category: &str, name: impl Into<String>, ok: bool, fail_detail: &str) -> Self {
59        if ok {
60            Self::pass(category, name)
61        } else {
62            Self::fail(category, name, fail_detail)
63        }
64    }
65}
66
67/// The full result of a conformance run.
68#[derive(Debug, Clone, Serialize)]
69pub struct Scorecard {
70    pub version: u32,
71    pub checks: Vec<Check>,
72}
73
74impl Scorecard {
75    #[must_use]
76    pub fn passed(&self) -> usize {
77        self.checks.iter().filter(|c| c.passed).count()
78    }
79
80    #[must_use]
81    pub fn total(&self) -> usize {
82        self.checks.len()
83    }
84
85    #[must_use]
86    pub fn all_passed(&self) -> bool {
87        self.checks.iter().all(|c| c.passed)
88    }
89
90    #[must_use]
91    pub fn failures(&self) -> Vec<&Check> {
92        self.checks.iter().filter(|c| !c.passed).collect()
93    }
94
95    #[must_use]
96    pub fn to_json(&self) -> Value {
97        json!({
98            "version": self.version,
99            "passed": self.passed(),
100            "total": self.total(),
101            "all_passed": self.all_passed(),
102            "checks": self.checks,
103        })
104    }
105}
106
107/// Run the full conformance suite against this instance.
108#[must_use]
109pub fn run() -> Scorecard {
110    let mut checks = Vec::new();
111    checks.extend(contract_checks());
112    checks.extend(reproducibility_checks());
113    checks.extend(extension_checks());
114    checks.extend(accuracy_checks());
115    checks.extend(a2a_checks());
116    Scorecard { version: 1, checks }
117}
118
119// ---------------------------------------------------------------------------
120// A2A: agent card and JSON-RPC contract conformance (GL#449).
121// ---------------------------------------------------------------------------
122
123/// Fields the A2A spec requires on a published agent card.
124const A2A_CARD_REQUIRED: &[&str] = &[
125    "name",
126    "description",
127    "version",
128    "protocolVersion",
129    "capabilities",
130    "skills",
131    "defaultInputModes",
132    "defaultOutputModes",
133    "authentication",
134];
135
136fn a2a_checks() -> Vec<Check> {
137    let mut checks = Vec::new();
138
139    let card = crate::core::a2a::agent_card::build_agent_card("conformance");
140    let missing: Vec<&&str> = A2A_CARD_REQUIRED
141        .iter()
142        .filter(|f| card.get(**f).is_none())
143        .collect();
144    checks.push(Check::from_bool(
145        "a2a",
146        "agent_card_required_fields",
147        missing.is_empty(),
148        &format!("agent card missing fields: {missing:?}"),
149    ));
150
151    checks.push(Check::from_bool(
152        "a2a",
153        "agent_card_deterministic",
154        card == crate::core::a2a::agent_card::build_agent_card("conformance"),
155        "two agent card builds differ",
156    ));
157
158    let skills_ok = card
159        .get("skills")
160        .and_then(serde_json::Value::as_array)
161        .is_some_and(|skills| {
162            !skills.is_empty()
163                && skills.iter().all(|s| {
164                    s.get("id").is_some()
165                        && s.get("name").is_some()
166                        && s.get("description").is_some()
167                })
168        });
169    checks.push(Check::from_bool(
170        "a2a",
171        "agent_card_skills_complete",
172        skills_ok,
173        "skills missing id/name/description",
174    ));
175
176    // JSON-RPC error contract: wrong version → -32600, unknown method → -32601.
177    let bad_version = crate::core::a2a::a2a_compat::handle_a2a_jsonrpc(
178        &crate::core::a2a::a2a_compat::JsonRpcRequest {
179            jsonrpc: "1.0".to_string(),
180            id: serde_json::Value::Number(1.into()),
181            method: "tasks/get".to_string(),
182            params: serde_json::Value::Null,
183        },
184    );
185    checks.push(Check::from_bool(
186        "a2a",
187        "jsonrpc_rejects_bad_version",
188        bad_version.error.as_ref().is_some_and(|e| e.code == -32600),
189        "jsonrpc 1.0 not rejected with -32600",
190    ));
191
192    let unknown_method = crate::core::a2a::a2a_compat::handle_a2a_jsonrpc(
193        &crate::core::a2a::a2a_compat::JsonRpcRequest {
194            jsonrpc: "2.0".to_string(),
195            id: serde_json::Value::Number(2.into()),
196            method: "tasks/nonexistent".to_string(),
197            params: serde_json::Value::Null,
198        },
199    );
200    checks.push(Check::from_bool(
201        "a2a",
202        "jsonrpc_unknown_method_code",
203        unknown_method
204            .error
205            .as_ref()
206            .is_some_and(|e| e.code == -32601),
207        "unknown method not rejected with -32601",
208    ));
209
210    checks
211}
212
213// ---------------------------------------------------------------------------
214// Accuracy: structural invariants of the lossy read modes (GL#441).
215//
216// Byte-golden snapshots would break on every intentional format improvement;
217// these checks instead pin down what each mode must *preserve* (symbols,
218// deps) and must *drop* (bodies), plus determinism and size bounds — the
219// properties an agent's correctness actually depends on.
220// ---------------------------------------------------------------------------
221
222/// A stable Rust fixture exercising pub fns, a struct, imports, and a body
223/// secret that lossy modes must strip.
224const ACCURACY_FIXTURE: &str = r"use std::collections::HashMap;
225use std::path::PathBuf;
226
227pub struct Inventory {
228    items: HashMap<String, u32>,
229}
230
231pub fn add_item(inv: &mut Inventory, name: &str, qty: u32) {
232    let body_secret_alpha = qty + 1;
233    inv.items.insert(name.to_string(), body_secret_alpha);
234}
235
236pub fn total_count(inv: &Inventory) -> u32 {
237    let body_secret_beta: u32 = inv.items.values().sum();
238    body_secret_beta
239}
240
241fn internal_rebalance(inv: &mut Inventory) {
242    inv.items.retain(|_, qty| *qty > 0);
243}
244";
245
246/// Symbols every lossy structural mode must keep visible.
247const MUST_KEEP_SYMBOLS: &[&str] = &["add_item", "total_count", "Inventory"];
248
249/// Body-local identifiers `signatures`/`map` must strip.
250const MUST_DROP_BODIES: &[&str] = &["body_secret_alpha", "body_secret_beta"];
251
252fn render_mode(mode: &str) -> String {
253    render_mode_full(mode).0
254}
255
256/// Rendered view plus its pre-footer body token count. The compression-size
257/// invariant measures `.1` (the body), since the reactive recovery footer and the
258/// savings line are orthogonal affordances appended after compression, not part of
259/// the compressed content.
260fn render_mode_full(mode: &str) -> (String, usize) {
261    crate::tools::ctx_read::render::process_mode(
262        ACCURACY_FIXTURE,
263        mode,
264        "",
265        "fixture.rs",
266        "rs",
267        crate::core::tokens::count_tokens(ACCURACY_FIXTURE),
268        crate::tools::CrpMode::Off,
269        "conformance/fixture.rs",
270        None,
271    )
272}
273
274fn accuracy_checks() -> Vec<Check> {
275    let mut checks = Vec::new();
276
277    for mode in ["map", "signatures", "aggressive", "entropy"] {
278        checks.push(Check::from_bool(
279            "accuracy",
280            format!("read_mode_deterministic:{mode}"),
281            render_mode(mode) == render_mode(mode),
282            "two renders of the same fixture differ",
283        ));
284    }
285
286    for mode in ["map", "signatures"] {
287        let out = render_mode(mode);
288        let missing: Vec<&&str> = MUST_KEEP_SYMBOLS
289            .iter()
290            .filter(|s| !out.contains(**s))
291            .collect();
292        checks.push(Check::from_bool(
293            "accuracy",
294            format!("read_mode_keeps_symbols:{mode}"),
295            missing.is_empty(),
296            &format!("symbols lost: {missing:?}"),
297        ));
298        let leaked: Vec<&&str> = MUST_DROP_BODIES
299            .iter()
300            .filter(|s| out.contains(**s))
301            .collect();
302        checks.push(Check::from_bool(
303            "accuracy",
304            format!("read_mode_strips_bodies:{mode}"),
305            leaked.is_empty(),
306            &format!("body content leaked: {leaked:?}"),
307        ));
308    }
309
310    let fixture_tokens = crate::core::tokens::count_tokens(ACCURACY_FIXTURE);
311    for mode in ["map", "signatures", "aggressive"] {
312        let sent = render_mode_full(mode).1;
313        checks.push(Check::from_bool(
314            "accuracy",
315            format!("read_mode_compresses:{mode}"),
316            sent < fixture_tokens,
317            &format!("no compression: {sent} >= {fixture_tokens} tokens"),
318        ));
319    }
320
321    // Target-density mode (GL#444): the body (excluding header/savings lines)
322    // must stay within the token budget, and the render must be deterministic.
323    {
324        let target = 0.4_f64;
325        let result = crate::core::entropy::entropy_compress_to_density(ACCURACY_FIXTURE, target);
326        let actual = result.compressed_tokens as f64 / fixture_tokens.max(1) as f64;
327        checks.push(Check::from_bool(
328            "accuracy",
329            "density_respects_budget:0.4",
330            actual <= target + 0.10,
331            &format!("density {actual:.2} exceeds target {target:.2} (+0.10 tolerance)"),
332        ));
333        checks.push(Check::from_bool(
334            "accuracy",
335            "density_deterministic:0.4",
336            render_mode("density:0.4") == render_mode("density:0.4"),
337            "two density renders of the same fixture differ",
338        ));
339    }
340
341    checks
342}
343
344fn contract_checks() -> Vec<Check> {
345    let present = !crate::core::contracts::versions_kv().is_empty();
346    vec![Check::from_bool(
347        "contracts",
348        "contract_versions_present",
349        present,
350        "versions_kv() is empty",
351    )]
352}
353
354fn reproducibility_checks() -> Vec<Check> {
355    let caps_stable = crate::core::server_capabilities::capabilities_value()
356        == crate::core::server_capabilities::capabilities_value();
357    let openapi_stable =
358        crate::core::openapi::openapi_value() == crate::core::openapi::openapi_value();
359    vec![
360        Check::from_bool(
361            "reproducibility",
362            "capabilities_deterministic",
363            caps_stable,
364            "capabilities document differs across builds",
365        ),
366        Check::from_bool(
367            "reproducibility",
368            "openapi_deterministic",
369            openapi_stable,
370            "openapi document differs across builds",
371        ),
372    ]
373}
374
375fn extension_checks() -> Vec<Check> {
376    let mut checks = Vec::new();
377    let Ok(reg) = crate::core::extension_registry::global().read() else {
378        checks.push(Check::fail(
379            "extensions",
380            "registry_readable",
381            "extension registry lock poisoned",
382        ));
383        return checks;
384    };
385
386    for name in reg.compressor_names() {
387        if let Some(c) = reg.compressor(&name) {
388            checks.push(compressor_invariants(&name, c.as_ref()));
389        }
390    }
391    for name in reg.chunker_names() {
392        if let Some(c) = reg.chunker(&name) {
393            checks.push(chunker_invariants(&name, c.as_ref()));
394        }
395    }
396    for name in reg.read_mode_names() {
397        if let Some(m) = reg.read_mode(&name) {
398            checks.push(read_mode_invariants(&name, m.as_ref()));
399        }
400    }
401    checks
402}
403
404fn compressor_invariants(name: &str, c: &dyn crate::core::extension_registry::Compressor) -> Check {
405    for input in CORPUS {
406        // Determinism.
407        if c.compress(input, None) != c.compress(input, None) {
408            return Check::fail(
409                "extensions",
410                format!("compressor:{name}"),
411                "non-deterministic",
412            );
413        }
414        // Budget is a hard byte ceiling, never split mid-char (valid UTF-8).
415        let budget = 4;
416        let out = c.compress(input, Some(budget));
417        if out.len() > budget {
418            return Check::fail(
419                "extensions",
420                format!("compressor:{name}"),
421                format!("exceeded byte budget: {} > {budget}", out.len()),
422            );
423        }
424    }
425    Check::pass("extensions", format!("compressor:{name}"))
426}
427
428fn chunker_invariants(name: &str, c: &dyn crate::core::extension_registry::Chunker) -> Check {
429    // Empty input ⇒ no chunks.
430    if !c.chunk("").is_empty() {
431        return Check::fail(
432            "extensions",
433            format!("chunker:{name}"),
434            "empty input produced chunks",
435        );
436    }
437    for input in CORPUS.iter().filter(|s| !s.trim().is_empty()) {
438        // Determinism.
439        if c.chunk(input) != c.chunk(input) {
440            return Check::fail("extensions", format!("chunker:{name}"), "non-deterministic");
441        }
442        let chunks = c.chunk(input);
443        // Non-empty input ⇒ at least one chunk, none empty after trim.
444        if chunks.is_empty() {
445            return Check::fail(
446                "extensions",
447                format!("chunker:{name}"),
448                "non-empty input produced no chunks",
449            );
450        }
451        if chunks.iter().any(|c| c.trim().is_empty()) {
452            return Check::fail(
453                "extensions",
454                format!("chunker:{name}"),
455                "produced an empty chunk",
456            );
457        }
458    }
459    Check::pass("extensions", format!("chunker:{name}"))
460}
461
462fn read_mode_invariants(name: &str, m: &dyn crate::core::extension_registry::ReadMode) -> Check {
463    for input in CORPUS {
464        if m.render(input, "x.txt") != m.render(input, "x.txt") {
465            return Check::fail(
466                "extensions",
467                format!("read_mode:{name}"),
468                "non-deterministic",
469            );
470        }
471    }
472    // The byte-faithful `full` mode must round-trip source verbatim.
473    if name == "full" {
474        let sample = "verbatim\nsource\n漢字";
475        if m.render(sample, "x.txt") != sample {
476            return Check::fail("extensions", "read_mode:full", "full mode altered source");
477        }
478    }
479    Check::pass("extensions", format!("read_mode:{name}"))
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn builtin_suite_passes() {
488        let card = run();
489        assert!(
490            card.all_passed(),
491            "conformance failures: {:?}",
492            card.failures()
493        );
494        assert!(card.total() >= 6, "expected a meaningful number of checks");
495    }
496
497    #[test]
498    fn scorecard_json_shape() {
499        let v = run().to_json();
500        assert_eq!(v["version"], 1);
501        assert!(v["checks"].is_array());
502        // Shape only: `passed == total` is covered by builtin_suite_passes.
503        // Asserting it here races with tests that register an intentionally
504        // broken compressor in the global extension registry.
505        let passed = v["passed"].as_u64().expect("passed is a number");
506        let total = v["total"].as_u64().expect("total is a number");
507        assert!(passed <= total);
508        assert_eq!(v["checks"].as_array().map(|c| c.len() as u64), Some(total));
509    }
510
511    #[test]
512    fn detects_a_nondeterministic_compressor() {
513        use std::sync::atomic::{AtomicU64, Ordering};
514        struct Flaky(AtomicU64);
515        impl crate::core::extension_registry::Compressor for Flaky {
516            #[allow(clippy::unnecessary_literal_bound)]
517            fn name(&self) -> &str {
518                "flaky"
519            }
520            fn compress(&self, _input: &str, _budget: Option<usize>) -> String {
521                self.0.fetch_add(1, Ordering::SeqCst).to_string()
522            }
523        }
524        let check = compressor_invariants("flaky", &Flaky(AtomicU64::new(0)));
525        assert!(!check.passed);
526        assert!(check.detail.contains("non-deterministic"));
527    }
528}