1use serde::Serialize;
18use serde_json::{Value, json};
19
20const 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#[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#[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#[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
119const 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 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
213const 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
246const MUST_KEEP_SYMBOLS: &[&str] = &["add_item", "total_count", "Inventory"];
248
249const 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
256fn 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 {
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 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 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 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 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 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 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 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}