1use std::sync::OnceLock;
18
19use serde::Serialize;
20use serde_json::{Value, json};
21
22const CORPUS: &[&str] = &[
25 "",
26 "single line",
27 "a\n\n\n\nb \n",
28 "para one\n\npara two\n\n\npara three",
29 "mültibyte ä ö ü 漢字 \n\n end",
30];
31
32#[derive(Debug, Clone, Serialize)]
34pub struct Check {
35 pub name: String,
36 pub category: String,
37 pub passed: bool,
38 pub detail: String,
39}
40
41impl Check {
42 fn pass(category: &str, name: impl Into<String>) -> Self {
43 Self {
44 name: name.into(),
45 category: category.to_string(),
46 passed: true,
47 detail: String::new(),
48 }
49 }
50
51 fn fail(category: &str, name: impl Into<String>, detail: impl Into<String>) -> Self {
52 Self {
53 name: name.into(),
54 category: category.to_string(),
55 passed: false,
56 detail: detail.into(),
57 }
58 }
59
60 fn from_bool(category: &str, name: impl Into<String>, ok: bool, fail_detail: &str) -> Self {
61 if ok {
62 Self::pass(category, name)
63 } else {
64 Self::fail(category, name, fail_detail)
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize)]
71pub struct Scorecard {
72 pub version: u32,
73 pub checks: Vec<Check>,
74}
75
76impl Scorecard {
77 #[must_use]
78 pub fn passed(&self) -> usize {
79 self.checks.iter().filter(|c| c.passed).count()
80 }
81
82 #[must_use]
83 pub fn total(&self) -> usize {
84 self.checks.len()
85 }
86
87 #[must_use]
88 pub fn all_passed(&self) -> bool {
89 self.checks.iter().all(|c| c.passed)
90 }
91
92 #[must_use]
93 pub fn failures(&self) -> Vec<&Check> {
94 self.checks.iter().filter(|c| !c.passed).collect()
95 }
96
97 #[must_use]
98 pub fn to_json(&self) -> Value {
99 json!({
100 "version": self.version,
101 "passed": self.passed(),
102 "total": self.total(),
103 "all_passed": self.all_passed(),
104 "checks": self.checks,
105 })
106 }
107}
108
109#[must_use]
111pub fn run() -> Scorecard {
112 let mut checks = Vec::new();
113 checks.extend(contract_checks());
114 checks.extend(reproducibility_checks());
115 checks.extend(extension_checks());
116 checks.extend(accuracy_checks());
117 checks.extend(a2a_checks());
118 Scorecard { version: 1, checks }
119}
120
121const A2A_CARD_REQUIRED: &[&str] = &[
127 "name",
128 "description",
129 "version",
130 "protocolVersion",
131 "capabilities",
132 "skills",
133 "defaultInputModes",
134 "defaultOutputModes",
135 "authentication",
136];
137
138fn a2a_checks() -> Vec<Check> {
139 let mut checks = Vec::new();
140
141 let card = crate::core::a2a::agent_card::build_agent_card("conformance", true);
142 let missing: Vec<&&str> = A2A_CARD_REQUIRED
143 .iter()
144 .filter(|f| card.get(**f).is_none())
145 .collect();
146 checks.push(Check::from_bool(
147 "a2a",
148 "agent_card_required_fields",
149 missing.is_empty(),
150 &format!("agent card missing fields: {missing:?}"),
151 ));
152
153 checks.push(Check::from_bool(
154 "a2a",
155 "agent_card_deterministic",
156 card == crate::core::a2a::agent_card::build_agent_card("conformance", true),
157 "two agent card builds differ",
158 ));
159
160 let skills_ok = card
161 .get("skills")
162 .and_then(serde_json::Value::as_array)
163 .is_some_and(|skills| {
164 !skills.is_empty()
165 && skills.iter().all(|s| {
166 s.get("id").is_some()
167 && s.get("name").is_some()
168 && s.get("description").is_some()
169 })
170 });
171 checks.push(Check::from_bool(
172 "a2a",
173 "agent_card_skills_complete",
174 skills_ok,
175 "skills missing id/name/description",
176 ));
177
178 let bad_version = crate::core::a2a::a2a_compat::handle_a2a_jsonrpc(
180 &crate::core::a2a::a2a_compat::JsonRpcRequest {
181 jsonrpc: "1.0".to_string(),
182 id: serde_json::Value::Number(1.into()),
183 method: "tasks/get".to_string(),
184 params: serde_json::Value::Null,
185 },
186 );
187 checks.push(Check::from_bool(
188 "a2a",
189 "jsonrpc_rejects_bad_version",
190 bad_version.error.as_ref().is_some_and(|e| e.code == -32600),
191 "jsonrpc 1.0 not rejected with -32600",
192 ));
193
194 let unknown_method = crate::core::a2a::a2a_compat::handle_a2a_jsonrpc(
195 &crate::core::a2a::a2a_compat::JsonRpcRequest {
196 jsonrpc: "2.0".to_string(),
197 id: serde_json::Value::Number(2.into()),
198 method: "tasks/nonexistent".to_string(),
199 params: serde_json::Value::Null,
200 },
201 );
202 checks.push(Check::from_bool(
203 "a2a",
204 "jsonrpc_unknown_method_code",
205 unknown_method
206 .error
207 .as_ref()
208 .is_some_and(|e| e.code == -32601),
209 "unknown method not rejected with -32601",
210 ));
211
212 checks
213}
214
215const ACCURACY_FIXTURE: &str = r"use std::collections::HashMap;
227use std::path::PathBuf;
228
229pub struct Inventory {
230 items: HashMap<String, u32>,
231}
232
233pub fn add_item(inv: &mut Inventory, name: &str, qty: u32) {
234 let body_secret_alpha = qty + 1;
235 inv.items.insert(name.to_string(), body_secret_alpha);
236}
237
238pub fn total_count(inv: &Inventory) -> u32 {
239 let body_secret_beta: u32 = inv.items.values().sum();
240 body_secret_beta
241}
242
243fn internal_rebalance(inv: &mut Inventory) {
244 inv.items.retain(|_, qty| *qty > 0);
245}
246";
247
248const MUST_KEEP_SYMBOLS: &[&str] = &["add_item", "total_count", "Inventory"];
250
251const MUST_DROP_BODIES: &[&str] = &["body_secret_alpha", "body_secret_beta"];
253
254pub type ReadModeRenderer = fn(
259 &str,
260 &str,
261 &str,
262 &str,
263 &str,
264 usize,
265 crate::core::protocol::CrpMode,
266 &str,
267 Option<&str>,
268) -> (String, usize);
269
270static READ_MODE_RENDERER: OnceLock<ReadModeRenderer> = OnceLock::new();
271
272pub fn register_read_mode_renderer(renderer: ReadModeRenderer) {
281 let _ = READ_MODE_RENDERER.set(renderer);
282}
283
284fn render_mode(render: ReadModeRenderer, mode: &str) -> String {
285 render_mode_full(render, mode).0
286}
287
288fn render_mode_full(render: ReadModeRenderer, mode: &str) -> (String, usize) {
293 render(
294 ACCURACY_FIXTURE,
295 mode,
296 "",
297 "fixture.rs",
298 "rs",
299 crate::core::tokens::count_tokens(ACCURACY_FIXTURE),
300 crate::core::protocol::CrpMode::Off,
301 "conformance/fixture.rs",
302 None,
303 )
304}
305
306fn accuracy_checks() -> Vec<Check> {
307 match READ_MODE_RENDERER.get() {
308 Some(render) => accuracy_checks_with(*render),
309 None => vec![unregistered_renderer_check()],
310 }
311}
312
313fn unregistered_renderer_check() -> Check {
314 Check::fail(
315 "accuracy",
316 "read_mode_renderer_registered",
317 "the ctx_read renderer is not registered; read-mode accuracy is unverified",
318 )
319}
320
321fn accuracy_checks_with(render: ReadModeRenderer) -> Vec<Check> {
322 let render_mode = |mode: &str| render_mode(render, mode);
323 let render_mode_full = |mode: &str| render_mode_full(render, mode);
324 let mut checks = Vec::new();
325
326 for mode in ["map", "signatures", "aggressive", "entropy"] {
327 checks.push(Check::from_bool(
328 "accuracy",
329 format!("read_mode_deterministic:{mode}"),
330 render_mode(mode) == render_mode(mode),
331 "two renders of the same fixture differ",
332 ));
333 }
334
335 for mode in ["map", "signatures"] {
336 let out = render_mode(mode);
337 let missing: Vec<&&str> = MUST_KEEP_SYMBOLS
338 .iter()
339 .filter(|s| !out.contains(**s))
340 .collect();
341 checks.push(Check::from_bool(
342 "accuracy",
343 format!("read_mode_keeps_symbols:{mode}"),
344 missing.is_empty(),
345 &format!("symbols lost: {missing:?}"),
346 ));
347 let leaked: Vec<&&str> = MUST_DROP_BODIES
348 .iter()
349 .filter(|s| out.contains(**s))
350 .collect();
351 checks.push(Check::from_bool(
352 "accuracy",
353 format!("read_mode_strips_bodies:{mode}"),
354 leaked.is_empty(),
355 &format!("body content leaked: {leaked:?}"),
356 ));
357 }
358
359 let fixture_tokens = crate::core::tokens::count_tokens(ACCURACY_FIXTURE);
360 for mode in ["map", "signatures", "aggressive"] {
361 let sent = render_mode_full(mode).1;
362 checks.push(Check::from_bool(
363 "accuracy",
364 format!("read_mode_compresses:{mode}"),
365 sent < fixture_tokens,
366 &format!("no compression: {sent} >= {fixture_tokens} tokens"),
367 ));
368 }
369
370 {
373 let target = 0.4_f64;
374 let result = crate::core::entropy::entropy_compress_to_density(ACCURACY_FIXTURE, target);
375 let actual = result.compressed_tokens as f64 / fixture_tokens.max(1) as f64;
376 checks.push(Check::from_bool(
377 "accuracy",
378 "density_respects_budget:0.4",
379 actual <= target + 0.10,
380 &format!("density {actual:.2} exceeds target {target:.2} (+0.10 tolerance)"),
381 ));
382 checks.push(Check::from_bool(
383 "accuracy",
384 "density_deterministic:0.4",
385 render_mode("density:0.4") == render_mode("density:0.4"),
386 "two density renders of the same fixture differ",
387 ));
388 }
389
390 checks
391}
392
393fn contract_checks() -> Vec<Check> {
394 let present = !crate::core::contracts::versions_kv().is_empty();
395 vec![Check::from_bool(
396 "contracts",
397 "contract_versions_present",
398 present,
399 "versions_kv() is empty",
400 )]
401}
402
403fn reproducibility_checks() -> Vec<Check> {
404 let caps_stable = crate::core::server_capabilities::capabilities_value()
405 == crate::core::server_capabilities::capabilities_value();
406 let openapi_stable =
407 crate::core::openapi::openapi_value() == crate::core::openapi::openapi_value();
408 vec![
409 Check::from_bool(
410 "reproducibility",
411 "capabilities_deterministic",
412 caps_stable,
413 "capabilities document differs across builds",
414 ),
415 Check::from_bool(
416 "reproducibility",
417 "openapi_deterministic",
418 openapi_stable,
419 "openapi document differs across builds",
420 ),
421 ]
422}
423
424fn extension_checks() -> Vec<Check> {
425 let mut checks = Vec::new();
426 let Ok(reg) = crate::core::extension_registry::global().read() else {
427 checks.push(Check::fail(
428 "extensions",
429 "registry_readable",
430 "extension registry lock poisoned",
431 ));
432 return checks;
433 };
434
435 for name in reg.compressor_names() {
436 if let Some(c) = reg.compressor(&name) {
437 checks.push(compressor_invariants(&name, c.as_ref()));
438 }
439 }
440 for name in reg.chunker_names() {
441 if let Some(c) = reg.chunker(&name) {
442 checks.push(chunker_invariants(&name, c.as_ref()));
443 }
444 }
445 for name in reg.read_mode_names() {
446 if let Some(m) = reg.read_mode(&name) {
447 checks.push(read_mode_invariants(&name, m.as_ref()));
448 }
449 }
450 checks
451}
452
453fn compressor_invariants(name: &str, c: &dyn crate::core::extension_registry::Compressor) -> Check {
454 for input in CORPUS {
455 if c.compress(input, None) != c.compress(input, None) {
457 return Check::fail(
458 "extensions",
459 format!("compressor:{name}"),
460 "non-deterministic",
461 );
462 }
463 let budget = 4;
465 let out = c.compress(input, Some(budget));
466 if out.len() > budget {
467 return Check::fail(
468 "extensions",
469 format!("compressor:{name}"),
470 format!("exceeded byte budget: {} > {budget}", out.len()),
471 );
472 }
473 }
474 Check::pass("extensions", format!("compressor:{name}"))
475}
476
477fn chunker_invariants(name: &str, c: &dyn crate::core::extension_registry::Chunker) -> Check {
478 if !c.chunk("").is_empty() {
480 return Check::fail(
481 "extensions",
482 format!("chunker:{name}"),
483 "empty input produced chunks",
484 );
485 }
486 for input in CORPUS.iter().filter(|s| !s.trim().is_empty()) {
487 if c.chunk(input) != c.chunk(input) {
489 return Check::fail("extensions", format!("chunker:{name}"), "non-deterministic");
490 }
491 let chunks = c.chunk(input);
492 if chunks.is_empty() {
494 return Check::fail(
495 "extensions",
496 format!("chunker:{name}"),
497 "non-empty input produced no chunks",
498 );
499 }
500 if chunks.iter().any(|c| c.trim().is_empty()) {
501 return Check::fail(
502 "extensions",
503 format!("chunker:{name}"),
504 "produced an empty chunk",
505 );
506 }
507 }
508 Check::pass("extensions", format!("chunker:{name}"))
509}
510
511fn read_mode_invariants(name: &str, m: &dyn crate::core::extension_registry::ReadMode) -> Check {
512 for input in CORPUS {
513 if m.render(input, "x.txt") != m.render(input, "x.txt") {
514 return Check::fail(
515 "extensions",
516 format!("read_mode:{name}"),
517 "non-deterministic",
518 );
519 }
520 }
521 if name == "full" {
523 let sample = "verbatim\nsource\n漢字";
524 if m.render(sample, "x.txt") != sample {
525 return Check::fail("extensions", "read_mode:full", "full mode altered source");
526 }
527 }
528 Check::pass("extensions", format!("read_mode:{name}"))
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
538 fn builtin_engine_checks_pass() {
539 let mut checks = Vec::new();
540 checks.extend(contract_checks());
541 checks.extend(reproducibility_checks());
542 checks.extend(extension_checks());
543 checks.extend(a2a_checks());
544 let card = Scorecard { version: 1, checks };
545 assert!(
546 card.all_passed(),
547 "conformance failures: {:?}",
548 card.failures()
549 );
550 assert!(card.total() >= 6, "expected a meaningful number of checks");
551 }
552
553 #[test]
556 fn an_unregistered_renderer_fails_the_scorecard() {
557 let check = unregistered_renderer_check();
558 assert!(!check.passed);
559 assert_eq!(check.category, "accuracy");
560 let card = Scorecard {
561 version: 1,
562 checks: vec![check],
563 };
564 assert!(!card.all_passed());
565 }
566
567 #[test]
568 fn scorecard_json_shape() {
569 let v = run().to_json();
570 assert_eq!(v["version"], 1);
571 assert!(v["checks"].is_array());
572 let passed = v["passed"].as_u64().expect("passed is a number");
576 let total = v["total"].as_u64().expect("total is a number");
577 assert!(passed <= total);
578 assert_eq!(v["checks"].as_array().map(|c| c.len() as u64), Some(total));
579 }
580
581 #[test]
582 fn detects_a_nondeterministic_compressor() {
583 use std::sync::atomic::{AtomicU64, Ordering};
584 struct Flaky(AtomicU64);
585 impl crate::core::extension_registry::Compressor for Flaky {
586 #[allow(clippy::unnecessary_literal_bound)]
587 fn name(&self) -> &str {
588 "flaky"
589 }
590 fn compress(&self, _input: &str, _budget: Option<usize>) -> String {
591 self.0.fetch_add(1, Ordering::SeqCst).to_string()
592 }
593 }
594 let check = compressor_invariants("flaky", &Flaky(AtomicU64::new(0)));
595 assert!(!check.passed);
596 assert!(check.detail.contains("non-deterministic"));
597 }
598}
599
600use crate::core::ocla::invocation::CapabilityResult;
603use lean_ctx_protocol::CapabilityManifestV1;
604
605#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
607pub struct ConformanceResult {
608 pub capability_id: String,
609 pub version: String,
610 pub checks_passed: u32,
611 pub checks_failed: u32,
612 pub failures: Vec<ConformanceFailure>,
613}
614
615#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
616pub struct ConformanceFailure {
617 pub check: String,
618 pub expected: String,
619 pub actual: String,
620}
621
622pub fn check_manifest_conformance(manifest: &CapabilityManifestV1) -> ConformanceResult {
624 let mut report = ConformanceResult {
625 capability_id: manifest.capability_id.as_str().to_owned(),
626 version: manifest.version.clone(),
627 checks_passed: 0,
628 checks_failed: 0,
629 failures: Vec::new(),
630 };
631 let validation = crate::core::ocla::capability_fabric::normalize_manifest(manifest.clone());
632 record_capability_check(
633 &mut report,
634 "manifest_contract",
635 validation.is_ok(),
636 "valid canonical capability contract",
637 validation
638 .err()
639 .map(|error| error.to_string())
640 .unwrap_or_default(),
641 );
642 report
643}
644
645fn record_capability_check(
646 report: &mut ConformanceResult,
647 check: &str,
648 passed: bool,
649 expected: &str,
650 actual: String,
651) {
652 if passed {
653 report.checks_passed += 1;
654 } else {
655 report.checks_failed += 1;
656 report.failures.push(ConformanceFailure {
657 check: check.to_owned(),
658 expected: expected.to_owned(),
659 actual,
660 });
661 }
662}
663
664pub fn check_invocation_conformance(
666 manifest: &CapabilityManifestV1,
667 result: &CapabilityResult,
668) -> ConformanceResult {
669 let mut report = check_manifest_conformance(manifest);
670 let observation = &result.observation;
671 record_capability_check(
672 &mut report,
673 "observation_identity",
674 observation.schema_version
675 == crate::core::ocla::invocation::CAPABILITY_OBSERVATION_SCHEMA_VERSION
676 && !observation.task_id.trim().is_empty()
677 && observation.capability_id == manifest.capability_id.as_str()
678 && observation.capability_version == manifest.version,
679 "versioned observation bound to task and capability",
680 "observation identity mismatch".into(),
681 );
682 record_capability_check(
683 &mut report,
684 "observation_consistency",
685 observation.success == result.success
686 && observation.output_tokens == result.output_tokens
687 && observation.latency_ms == result.latency_ms,
688 "result and observation agree",
689 "contradictory result measurements".into(),
690 );
691 record_capability_check(
692 &mut report,
693 "failure_classification",
694 matches!(
697 (result.success, observation.failure_mode),
698 (
699 true,
700 None | Some(crate::core::ocla::invocation::CapabilityFailureMode::FallbackToNative)
701 ) | (false, Some(_))
702 ),
703 "success and typed terminal state agree",
704 "contradictory success/failure classification".into(),
705 );
706
707 if let Some(bounds) = manifest.extra.get("latency_bounds_ms") {
709 let max_ms = bounds.get("max_ms").and_then(serde_json::Value::as_u64);
710 record_capability_check(
711 &mut report,
712 "latency_within_bounds",
713 max_ms.is_some_and(|max_ms| result.latency_ms <= max_ms),
714 "latency within declared unsigned max_ms",
715 format!("{} ms; bounds={bounds}", result.latency_ms),
716 );
717 }
718 report
719}
720
721#[cfg(test)]
722mod capability_conformance_tests {
723 use super::*;
724 use crate::core::ocla::adapters::PassthroughAdapter;
725 use crate::core::ocla::invocation::{
726 CapabilityAdapter, CapabilityInput, CapabilityInvocation, PolicyConstraints,
727 };
728
729 #[test]
730 fn terminal_failure_truth_table_is_enforced() {
731 use crate::core::ocla::invocation::CapabilityFailureMode::*;
732 let fixture: Value = serde_json::from_str(include_str!(concat!(
733 env!("CARGO_MANIFEST_DIR"),
734 "/../tests/ocla_contract_suite/v1/conformance/invocation_success.json"
735 )))
736 .unwrap();
737 let manifest: CapabilityManifestV1 =
738 serde_json::from_value(fixture["manifest"].clone()).unwrap();
739 let baseline: CapabilityResult = serde_json::from_value(fixture["result"].clone()).unwrap();
740 for (success, mode, valid) in [
741 (true, None, true),
742 (false, None, false),
743 (true, Some(Timeout), false),
744 (false, Some(Timeout), true),
745 (true, Some(Unavailable), false),
746 (false, Some(Unavailable), true),
747 (true, Some(RejectedByPolicy), false),
748 (false, Some(RejectedByPolicy), true),
749 (true, Some(InvalidOutput), false),
750 (false, Some(InvalidOutput), true),
751 (true, Some(Internal), false),
752 (false, Some(Internal), true),
753 (true, Some(Partial), false),
754 (false, Some(Partial), true),
755 (true, Some(FallbackToNative), true),
756 (false, Some(FallbackToNative), true),
757 ] {
758 let mut result = baseline.clone();
759 result.success = success;
760 result.observation.success = success;
761 result.observation.failure_mode = mode;
762 let report = check_invocation_conformance(&manifest, &result);
763 assert_eq!(
764 report.checks_failed == 0,
765 valid,
766 "success={success}, mode={mode:?}"
767 );
768 }
769 }
770
771 #[test]
772 fn invalid_contracts_never_receive_placeholder_passes() {
773 let mut manifest = PassthroughAdapter::new().manifest().clone();
774 manifest.schema_version = 0;
775 let report = check_manifest_conformance(&manifest);
776 assert_eq!(report.checks_passed, 0);
777 assert_eq!(report.checks_failed, 1);
778 assert_eq!(report.failures.len(), 1);
779 }
780
781 #[test]
782 fn real_adapter_result_passes_but_contradictory_measurements_fail() {
783 let adapter = PassthroughAdapter::new();
784 let invocation = CapabilityInvocation {
785 task_id: "conformance-task".into(),
786 capability_id: adapter.manifest().capability_id.as_str().into(),
787 capability_version: adapter.manifest().version.clone(),
788 input: CapabilityInput::ModelRequest {
789 prompt: "test input".into(),
790 model: None,
791 },
792 policy_constraints: PolicyConstraints::default(),
793 timeout_ms: 0,
794 };
795 let mut result = adapter.invoke(invocation).unwrap();
796 let report = check_invocation_conformance(adapter.manifest(), &result);
797 assert_eq!(report.checks_failed, 0);
798 assert!(report.checks_passed >= 4);
799 result.output_tokens += 1;
800 let report = check_invocation_conformance(adapter.manifest(), &result);
801 assert_eq!(report.checks_failed, 1);
802 assert_eq!(report.failures[0].check, "observation_consistency");
803 }
804}