1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4
5use serde::Deserialize;
6use serde_json::Value;
7
8use crate::error::{CssMatrixReadSource, QecError};
9use crate::family_contract::{
10 CssCodeStats, CssConstructionResult, RequestedFamilyId, construct_css,
11 parse_css_construction_json, verify_css_orthogonality,
12};
13
14const MANIFEST_REL_PATH: &str = "tests/fixtures/family_manifest/manifest.v1.json";
15const EXPECTED_FAMILY_IDS: [&str; 14] = [
16 "directional",
17 "quantum_tanner",
18 "generalized_bicycle",
19 "la_cross",
20 "random_hgp",
21 "lifted_product",
22 "hyperbolic_5_5",
23 "coprime_bb",
24 "toric_3d",
25 "color_666",
26 "surface",
27 "shor_like",
28 "random_two_block",
29 "perturbed_hgp",
30];
31const EXPECTED_SUPPORTED_FAMILIES: usize = 12;
32const EXPECTED_DEFERRED_FAMILIES: usize = 2;
33
34#[derive(Debug, Clone, Copy)]
35struct DeferredFamilyBoundary {
36 tracking_issue: usize,
37 contract_path: &'static str,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct FamilyVerificationReport {
42 pub output: String,
43 pub failed: usize,
44}
45
46pub fn verify_checked_in_family_manifest() -> Result<FamilyVerificationReport, QecError> {
47 let path = checked_in_manifest_path();
48 let text = fs::read_to_string(&path).map_err(|error| QecError::CssMatrixReadFailed {
49 path: path.display().to_string(),
50 source: CssMatrixReadSource(error.to_string()),
51 })?;
52 Ok(verify_family_manifest_text(&text))
53}
54
55pub fn verify_family_manifest_text(text: &str) -> FamilyVerificationReport {
56 match serde_json::from_str::<FamilyCatalog>(text) {
57 Ok(catalog) => verify_catalog(&catalog),
58 Err(error) => failure_report(format!("FAIL manifest invalid_json={error}"), 0, 0),
59 }
60}
61
62fn checked_in_manifest_path() -> PathBuf {
63 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(MANIFEST_REL_PATH)
64}
65
66#[derive(Debug, Deserialize)]
67struct FamilyCatalog {
68 families: Vec<FamilyCatalogEntry>,
69}
70
71#[derive(Debug, Deserialize)]
72struct FamilyCatalogEntry {
73 family_id: String,
74 disposition: FamilyDisposition,
75 availability: RuntimeAvailability,
76 #[serde(default)]
77 research_contracts: Vec<String>,
78 callable_constructor: Option<CallableConstructorRef>,
79 expected: Option<ExpectedStats>,
80 row_weight_summary: Option<RowWeightSummary>,
81 #[serde(default)]
82 executable_cases: Vec<ExecutableCase>,
83}
84
85#[derive(Debug, Deserialize)]
86struct CallableConstructorRef {
87 rust_path: String,
88}
89
90#[derive(Debug, PartialEq, Eq, Deserialize)]
91struct ExpectedStats {
92 n: usize,
93 m_x: usize,
94 m_z: usize,
95 rank_x: usize,
96 rank_z: usize,
97 k: usize,
98 d_x: Option<usize>,
99 d_z: Option<usize>,
100}
101
102#[derive(Debug, PartialEq, Eq, Deserialize)]
103struct RowWeightSummary {
104 h_x: Vec<RowWeightBucket>,
105 h_z: Vec<RowWeightBucket>,
106}
107
108#[derive(Debug, PartialEq, Eq, Deserialize)]
109struct RowWeightBucket {
110 weight: usize,
111 count: usize,
112}
113
114#[derive(Debug, Deserialize)]
115struct ExecutableCase {
116 case_kind: ExecutableCaseKind,
117 expected_outcome: ExpectedOutcome,
118 request: Option<Value>,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
122#[serde(rename_all = "snake_case")]
123enum FamilyDisposition {
124 Supported,
125 Deferred,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
129#[serde(rename_all = "snake_case")]
130enum RuntimeAvailability {
131 Planned,
132 Available,
133 NotApplicable,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
137#[serde(rename_all = "snake_case")]
138enum ExecutableCaseKind {
139 Positive,
140 Negative,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
144#[serde(rename_all = "snake_case")]
145enum ExpectedOutcome {
146 Success,
147 Rejection,
148}
149
150enum EntryVerification {
151 Line(String),
152 Failure(String),
153}
154
155fn verify_catalog(catalog: &FamilyCatalog) -> FamilyVerificationReport {
156 let supported = catalog
157 .families
158 .iter()
159 .filter(|entry| entry.disposition == FamilyDisposition::Supported)
160 .count();
161 let deferred = catalog
162 .families
163 .iter()
164 .filter(|entry| entry.disposition == FamilyDisposition::Deferred)
165 .count();
166 let mut lines = manifest_contract_failures(catalog, supported, deferred);
167 let mut failed = lines.len();
168
169 for entry in &catalog.families {
170 match verify_entry(entry) {
171 EntryVerification::Line(line) => lines.push(line),
172 EntryVerification::Failure(line) => {
173 failed += 1;
174 lines.push(line);
175 }
176 }
177 }
178
179 let status = if failed == 0 { "PASS" } else { "FAIL" };
180 lines.push(format!(
181 "SUMMARY {status} supported={supported} deferred={deferred} failed={failed}"
182 ));
183 FamilyVerificationReport {
184 output: lines.join("\n"),
185 failed,
186 }
187}
188
189fn manifest_contract_failures(
190 catalog: &FamilyCatalog,
191 supported: usize,
192 deferred: usize,
193) -> Vec<String> {
194 let mut failures = Vec::new();
195
196 for family_id in EXPECTED_FAMILY_IDS {
197 let occurrences = catalog
198 .families
199 .iter()
200 .filter(|entry| entry.family_id == family_id)
201 .count();
202 match occurrences {
203 0 => failures.push(format!("FAIL manifest missing family_id={family_id}")),
204 1 => {}
205 _ => failures.push(format!("FAIL manifest duplicate family_id={family_id}")),
206 }
207 }
208
209 for entry in &catalog.families {
210 if !EXPECTED_FAMILY_IDS.contains(&entry.family_id.as_str()) {
211 failures.push(format!(
212 "FAIL manifest unexpected family_id={}",
213 entry.family_id
214 ));
215 }
216 }
217
218 for (index, entry) in catalog.families.iter().enumerate() {
219 let expected = EXPECTED_FAMILY_IDS.get(index).copied().unwrap_or("none");
220 if entry.family_id != expected {
221 failures.push(format!(
222 "FAIL manifest family_id_order index={index} expected={expected} actual={}",
223 entry.family_id
224 ));
225 }
226 }
227
228 if supported != EXPECTED_SUPPORTED_FAMILIES {
229 failures.push(format!(
230 "FAIL manifest expected supported={EXPECTED_SUPPORTED_FAMILIES} actual supported={supported}"
231 ));
232 }
233 if deferred != EXPECTED_DEFERRED_FAMILIES {
234 failures.push(format!(
235 "FAIL manifest expected deferred={EXPECTED_DEFERRED_FAMILIES} actual deferred={deferred}"
236 ));
237 }
238
239 failures
240}
241
242fn verify_entry(entry: &FamilyCatalogEntry) -> EntryVerification {
243 match entry.disposition {
244 FamilyDisposition::Supported => verify_supported_entry(entry),
245 FamilyDisposition::Deferred => verify_deferred_entry(entry),
246 }
247}
248
249fn verify_supported_entry(entry: &FamilyCatalogEntry) -> EntryVerification {
250 if entry.availability != RuntimeAvailability::Available {
251 return failure(format!(
252 "FAIL {} disposition=supported availability={} expected=available",
253 entry.family_id,
254 availability_name(entry.availability)
255 ));
256 }
257
258 let Some(case) = entry.executable_cases.iter().find(|case| {
259 case.case_kind == ExecutableCaseKind::Positive
260 && case.expected_outcome == ExpectedOutcome::Success
261 }) else {
262 return failure(format!(
263 "FAIL {} missing positive success case",
264 entry.family_id
265 ));
266 };
267 let Some(request) = case.request.as_ref() else {
268 return failure(format!(
269 "FAIL {} positive success case missing request",
270 entry.family_id
271 ));
272 };
273 let request_text = match serde_json::to_string(request) {
274 Ok(text) => text,
275 Err(error) => return failure(format!("FAIL {} request_json={error}", entry.family_id)),
276 };
277 let result = match parse_css_construction_json(&request_text).and_then(construct_css) {
278 Ok(result) => result,
279 Err(error) => return failure(format!("FAIL {} construction={error}", entry.family_id)),
280 };
281
282 if let Err(error) =
283 verify_css_orthogonality(result.stats.n, &result.checks.h_x, &result.checks.h_z)
284 {
285 return failure(format!("FAIL {} orthogonality={error}", entry.family_id));
286 }
287
288 if let Some(line) = verify_result_metadata(entry, &result) {
289 return failure(line);
290 }
291
292 EntryVerification::Line(format_pass_line(&entry.family_id, &result))
293}
294
295fn verify_result_metadata(
296 entry: &FamilyCatalogEntry,
297 result: &CssConstructionResult,
298) -> Option<String> {
299 if result.requested_family_id.map(RequestedFamilyId::as_str) != Some(entry.family_id.as_str()) {
300 return Some(format!(
301 "FAIL {} expected requested_family_id={} actual requested_family_id={}",
302 entry.family_id,
303 entry.family_id,
304 result
305 .requested_family_id
306 .map(RequestedFamilyId::as_str)
307 .unwrap_or("none")
308 ));
309 }
310
311 let Some(callable) = entry.callable_constructor.as_ref() else {
312 return Some(format!(
313 "FAIL {} missing callable_constructor",
314 entry.family_id
315 ));
316 };
317 if result.provenance.source != callable.rust_path {
318 return Some(format!(
319 "FAIL {} expected provenance={} actual provenance={}",
320 entry.family_id, callable.rust_path, result.provenance.source
321 ));
322 }
323
324 let Some(expected) = entry.expected.as_ref() else {
325 return Some(format!("FAIL {} missing expected stats", entry.family_id));
326 };
327 if let Some(line) = stats_mismatch(&entry.family_id, expected, &result.stats) {
328 return Some(line);
329 }
330
331 let Some(expected_weights) = entry.row_weight_summary.as_ref() else {
332 return Some(format!(
333 "FAIL {} missing row_weight_summary",
334 entry.family_id
335 ));
336 };
337 let actual_h_x = row_weight_summary(&result.checks.h_x);
338 if actual_h_x != expected_weights.h_x {
339 return Some(format!(
340 "FAIL {} expected row_weights_h_x={} actual row_weights_h_x={}",
341 entry.family_id,
342 format_row_weights(&expected_weights.h_x),
343 format_row_weights(&actual_h_x)
344 ));
345 }
346 let actual_h_z = row_weight_summary(&result.checks.h_z);
347 if actual_h_z != expected_weights.h_z {
348 return Some(format!(
349 "FAIL {} expected row_weights_h_z={} actual row_weights_h_z={}",
350 entry.family_id,
351 format_row_weights(&expected_weights.h_z),
352 format_row_weights(&actual_h_z)
353 ));
354 }
355
356 None
357}
358
359fn stats_mismatch(
360 family_id: &str,
361 expected: &ExpectedStats,
362 actual: &CssCodeStats,
363) -> Option<String> {
364 macro_rules! compare_stat {
365 ($field:ident) => {
366 if expected.$field != actual.$field {
367 return Some(format!(
368 "FAIL {family_id} expected {}={} actual {}={}",
369 stringify!($field),
370 expected.$field,
371 stringify!($field),
372 actual.$field
373 ));
374 }
375 };
376 }
377
378 compare_stat!(n);
379 compare_stat!(m_x);
380 compare_stat!(m_z);
381 compare_stat!(rank_x);
382 compare_stat!(rank_z);
383 compare_stat!(k);
384 if expected.d_x != actual.d_x {
385 return Some(format!(
386 "FAIL {family_id} expected d_x={:?} actual d_x={:?}",
387 expected.d_x, actual.d_x
388 ));
389 }
390 if expected.d_z != actual.d_z {
391 return Some(format!(
392 "FAIL {family_id} expected d_z={:?} actual d_z={:?}",
393 expected.d_z, actual.d_z
394 ));
395 }
396 None
397}
398
399fn verify_deferred_entry(entry: &FamilyCatalogEntry) -> EntryVerification {
400 if entry.availability != RuntimeAvailability::NotApplicable {
401 return failure(format!(
402 "FAIL {} disposition=deferred availability={} expected=not_applicable",
403 entry.family_id,
404 availability_name(entry.availability)
405 ));
406 }
407 if entry.research_contracts.len() != 1 {
408 return failure(format!(
409 "FAIL {} expected exactly one research_contract",
410 entry.family_id
411 ));
412 }
413 let Some(boundary) = deferred_family_boundary(&entry.family_id) else {
414 return failure(format!("FAIL {} unknown deferred family", entry.family_id));
415 };
416 if entry.research_contracts[0] != boundary.contract_path {
417 return failure(format!(
418 "FAIL {} expected contract={} actual contract={}",
419 entry.family_id, boundary.contract_path, entry.research_contracts[0]
420 ));
421 }
422 EntryVerification::Line(format!(
423 "DEFERRED {} tracking_issue=#{} contract={}",
424 entry.family_id, boundary.tracking_issue, boundary.contract_path
425 ))
426}
427
428fn deferred_family_boundary(family_id: &str) -> Option<DeferredFamilyBoundary> {
429 match family_id {
430 "hyperbolic_5_5" => Some(DeferredFamilyBoundary {
431 tracking_issue: 571,
432 contract_path: "qec-code/doc/hyperbolic_5_5_contract.md",
433 }),
434 "perturbed_hgp" => Some(DeferredFamilyBoundary {
435 tracking_issue: 572,
436 contract_path: "qec-code/doc/perturbed_hgp_contract.md",
437 }),
438 _ => None,
439 }
440}
441
442fn format_pass_line(family_id: &str, result: &CssConstructionResult) -> String {
443 let params = serde_json::to_string(&result.normalized_parameters)
444 .expect("normalized CSS construction parameters should serialize");
445 format!(
446 "PASS {family_id} params={params} n={} checks=h_x:{},h_z:{} ranks=rank_x:{},rank_z:{} k={} row_weights=h_x:{},h_z:{} orthogonal=true provenance={}@{}",
447 result.stats.n,
448 result.stats.m_x,
449 result.stats.m_z,
450 result.stats.rank_x,
451 result.stats.rank_z,
452 result.stats.k,
453 format_row_weights(&row_weight_summary(&result.checks.h_x)),
454 format_row_weights(&row_weight_summary(&result.checks.h_z)),
455 result.provenance.source,
456 result.provenance.normalized_input_digest,
457 )
458}
459
460fn row_weight_summary(rows: &[Vec<usize>]) -> Vec<RowWeightBucket> {
461 let mut counts = BTreeMap::new();
462 for row in rows {
463 *counts.entry(row.len()).or_insert(0usize) += 1;
464 }
465 counts
466 .into_iter()
467 .map(|(weight, count)| RowWeightBucket { weight, count })
468 .collect()
469}
470
471fn format_row_weights(buckets: &[RowWeightBucket]) -> String {
472 let values = buckets
473 .iter()
474 .map(|bucket| format!("w{}={}", bucket.weight, bucket.count))
475 .collect::<Vec<_>>();
476 format!("[{}]", values.join(","))
477}
478
479fn availability_name(availability: RuntimeAvailability) -> &'static str {
480 match availability {
481 RuntimeAvailability::Planned => "planned",
482 RuntimeAvailability::Available => "available",
483 RuntimeAvailability::NotApplicable => "not_applicable",
484 }
485}
486
487fn failure(line: String) -> EntryVerification {
488 EntryVerification::Failure(line)
489}
490
491fn failure_report(line: String, supported: usize, deferred: usize) -> FamilyVerificationReport {
492 FamilyVerificationReport {
493 output: format!("{line}\nSUMMARY FAIL supported={supported} deferred={deferred} failed=1"),
494 failed: 1,
495 }
496}