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    crate::tools::ctx_read::render::process_mode(
254        ACCURACY_FIXTURE,
255        mode,
256        "",
257        "fixture.rs",
258        "rs",
259        crate::core::tokens::count_tokens(ACCURACY_FIXTURE),
260        crate::tools::CrpMode::Off,
261        "conformance/fixture.rs",
262        None,
263    )
264    .0
265}
266
267fn accuracy_checks() -> Vec<Check> {
268    let mut checks = Vec::new();
269
270    for mode in ["map", "signatures", "aggressive", "entropy"] {
271        checks.push(Check::from_bool(
272            "accuracy",
273            format!("read_mode_deterministic:{mode}"),
274            render_mode(mode) == render_mode(mode),
275            "two renders of the same fixture differ",
276        ));
277    }
278
279    for mode in ["map", "signatures"] {
280        let out = render_mode(mode);
281        let missing: Vec<&&str> = MUST_KEEP_SYMBOLS
282            .iter()
283            .filter(|s| !out.contains(**s))
284            .collect();
285        checks.push(Check::from_bool(
286            "accuracy",
287            format!("read_mode_keeps_symbols:{mode}"),
288            missing.is_empty(),
289            &format!("symbols lost: {missing:?}"),
290        ));
291        let leaked: Vec<&&str> = MUST_DROP_BODIES
292            .iter()
293            .filter(|s| out.contains(**s))
294            .collect();
295        checks.push(Check::from_bool(
296            "accuracy",
297            format!("read_mode_strips_bodies:{mode}"),
298            leaked.is_empty(),
299            &format!("body content leaked: {leaked:?}"),
300        ));
301    }
302
303    let fixture_tokens = crate::core::tokens::count_tokens(ACCURACY_FIXTURE);
304    for mode in ["map", "signatures", "aggressive"] {
305        let sent = crate::core::tokens::count_tokens(&render_mode(mode));
306        checks.push(Check::from_bool(
307            "accuracy",
308            format!("read_mode_compresses:{mode}"),
309            sent < fixture_tokens,
310            &format!("no compression: {sent} >= {fixture_tokens} tokens"),
311        ));
312    }
313
314    // Target-density mode (GL#444): the body (excluding header/savings lines)
315    // must stay within the token budget, and the render must be deterministic.
316    {
317        let target = 0.4_f64;
318        let result = crate::core::entropy::entropy_compress_to_density(ACCURACY_FIXTURE, target);
319        let actual = result.compressed_tokens as f64 / fixture_tokens.max(1) as f64;
320        checks.push(Check::from_bool(
321            "accuracy",
322            "density_respects_budget:0.4",
323            actual <= target + 0.10,
324            &format!("density {actual:.2} exceeds target {target:.2} (+0.10 tolerance)"),
325        ));
326        checks.push(Check::from_bool(
327            "accuracy",
328            "density_deterministic:0.4",
329            render_mode("density:0.4") == render_mode("density:0.4"),
330            "two density renders of the same fixture differ",
331        ));
332    }
333
334    checks
335}
336
337fn contract_checks() -> Vec<Check> {
338    let present = !crate::core::contracts::versions_kv().is_empty();
339    vec![Check::from_bool(
340        "contracts",
341        "contract_versions_present",
342        present,
343        "versions_kv() is empty",
344    )]
345}
346
347fn reproducibility_checks() -> Vec<Check> {
348    let caps_stable = crate::core::server_capabilities::capabilities_value()
349        == crate::core::server_capabilities::capabilities_value();
350    let openapi_stable =
351        crate::core::openapi::openapi_value() == crate::core::openapi::openapi_value();
352    vec![
353        Check::from_bool(
354            "reproducibility",
355            "capabilities_deterministic",
356            caps_stable,
357            "capabilities document differs across builds",
358        ),
359        Check::from_bool(
360            "reproducibility",
361            "openapi_deterministic",
362            openapi_stable,
363            "openapi document differs across builds",
364        ),
365    ]
366}
367
368fn extension_checks() -> Vec<Check> {
369    let mut checks = Vec::new();
370    let Ok(reg) = crate::core::extension_registry::global().read() else {
371        checks.push(Check::fail(
372            "extensions",
373            "registry_readable",
374            "extension registry lock poisoned",
375        ));
376        return checks;
377    };
378
379    for name in reg.compressor_names() {
380        if let Some(c) = reg.compressor(&name) {
381            checks.push(compressor_invariants(&name, c.as_ref()));
382        }
383    }
384    for name in reg.chunker_names() {
385        if let Some(c) = reg.chunker(&name) {
386            checks.push(chunker_invariants(&name, c.as_ref()));
387        }
388    }
389    for name in reg.read_mode_names() {
390        if let Some(m) = reg.read_mode(&name) {
391            checks.push(read_mode_invariants(&name, m.as_ref()));
392        }
393    }
394    checks
395}
396
397fn compressor_invariants(name: &str, c: &dyn crate::core::extension_registry::Compressor) -> Check {
398    for input in CORPUS {
399        // Determinism.
400        if c.compress(input, None) != c.compress(input, None) {
401            return Check::fail(
402                "extensions",
403                format!("compressor:{name}"),
404                "non-deterministic",
405            );
406        }
407        // Budget is a hard byte ceiling, never split mid-char (valid UTF-8).
408        let budget = 4;
409        let out = c.compress(input, Some(budget));
410        if out.len() > budget {
411            return Check::fail(
412                "extensions",
413                format!("compressor:{name}"),
414                format!("exceeded byte budget: {} > {budget}", out.len()),
415            );
416        }
417    }
418    Check::pass("extensions", format!("compressor:{name}"))
419}
420
421fn chunker_invariants(name: &str, c: &dyn crate::core::extension_registry::Chunker) -> Check {
422    // Empty input ⇒ no chunks.
423    if !c.chunk("").is_empty() {
424        return Check::fail(
425            "extensions",
426            format!("chunker:{name}"),
427            "empty input produced chunks",
428        );
429    }
430    for input in CORPUS.iter().filter(|s| !s.trim().is_empty()) {
431        // Determinism.
432        if c.chunk(input) != c.chunk(input) {
433            return Check::fail("extensions", format!("chunker:{name}"), "non-deterministic");
434        }
435        let chunks = c.chunk(input);
436        // Non-empty input ⇒ at least one chunk, none empty after trim.
437        if chunks.is_empty() {
438            return Check::fail(
439                "extensions",
440                format!("chunker:{name}"),
441                "non-empty input produced no chunks",
442            );
443        }
444        if chunks.iter().any(|c| c.trim().is_empty()) {
445            return Check::fail(
446                "extensions",
447                format!("chunker:{name}"),
448                "produced an empty chunk",
449            );
450        }
451    }
452    Check::pass("extensions", format!("chunker:{name}"))
453}
454
455fn read_mode_invariants(name: &str, m: &dyn crate::core::extension_registry::ReadMode) -> Check {
456    for input in CORPUS {
457        if m.render(input, "x.txt") != m.render(input, "x.txt") {
458            return Check::fail(
459                "extensions",
460                format!("read_mode:{name}"),
461                "non-deterministic",
462            );
463        }
464    }
465    // The byte-faithful `full` mode must round-trip source verbatim.
466    if name == "full" {
467        let sample = "verbatim\nsource\n漢字";
468        if m.render(sample, "x.txt") != sample {
469            return Check::fail("extensions", "read_mode:full", "full mode altered source");
470        }
471    }
472    Check::pass("extensions", format!("read_mode:{name}"))
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn builtin_suite_passes() {
481        let card = run();
482        assert!(
483            card.all_passed(),
484            "conformance failures: {:?}",
485            card.failures()
486        );
487        assert!(card.total() >= 6, "expected a meaningful number of checks");
488    }
489
490    #[test]
491    fn scorecard_json_shape() {
492        let v = run().to_json();
493        assert_eq!(v["version"], 1);
494        assert!(v["checks"].is_array());
495        // Shape only: `passed == total` is covered by builtin_suite_passes.
496        // Asserting it here races with tests that register an intentionally
497        // broken compressor in the global extension registry.
498        let passed = v["passed"].as_u64().expect("passed is a number");
499        let total = v["total"].as_u64().expect("total is a number");
500        assert!(passed <= total);
501        assert_eq!(v["checks"].as_array().map(|c| c.len() as u64), Some(total));
502    }
503
504    #[test]
505    fn detects_a_nondeterministic_compressor() {
506        use std::sync::atomic::{AtomicU64, Ordering};
507        struct Flaky(AtomicU64);
508        impl crate::core::extension_registry::Compressor for Flaky {
509            #[allow(clippy::unnecessary_literal_bound)]
510            fn name(&self) -> &str {
511                "flaky"
512            }
513            fn compress(&self, _input: &str, _budget: Option<usize>) -> String {
514                self.0.fetch_add(1, Ordering::SeqCst).to_string()
515            }
516        }
517        let check = compressor_invariants("flaky", &Flaky(AtomicU64::new(0)));
518        assert!(!check.passed);
519        assert!(check.detail.contains("non-deterministic"));
520    }
521}