# Task 8 — `explain_result` response (AST-based gate breakdown) > **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). ## Goal Build a structured pedagogical breakdown of an executed circuit: gate-by-gate description, key concept (entanglement / superposition / interference / rotation / measurement), dominant and missing outcomes, optional statevector summary. Spec §2.3 mandates AST-based analysis — we use `CircuitAnalyzer::list_gates`, no string-matching. ## Prerequisites - Task 4 merged (`CircuitAnalyzer`). - Task 7 merged (`tutor_tools.rs` populated). ## Files - Modify: `src/tools/tutor_tools.rs` ## Steps - [ ] **Step 1: Append failing tests** ```rust #[test] fn explain_result_bell_circuit_lists_h_then_cx_with_descriptions() { let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; let counts = json!({"00": 512, "11": 512}); let resp = explain_result_response(&loader(), bell, &counts, None); let breakdown = resp["gate_breakdown"].as_array().unwrap(); assert_eq!(breakdown.len(), 2); assert_eq!(breakdown[0]["name"].as_str().unwrap(), "h"); assert_eq!(breakdown[1]["name"].as_str().unwrap(), "cx"); assert!(breakdown[0]["description"].as_str().unwrap().contains("Hadamard")); assert_eq!(resp["key_concept"].as_str().unwrap(), "entanglement"); assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); } #[test] fn explain_result_dominant_outcomes_match_counts() { let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; let counts = json!({"00": 480, "11": 544}); let resp = explain_result_response(&loader(), bell, &counts, None); let dominant: Vec<&str> = resp["dominant_outcomes"] .as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect(); assert!(dominant.contains(&"00")); assert!(dominant.contains(&"11")); } #[test] fn explain_result_for_invalid_circuit_returns_error_payload() { let resp = explain_result_response(&loader(), "not valid qasm", &json!({}), None); assert!(resp.get("error").is_some()); } ``` - [ ] **Step 2: Implementation (top of `tutor_tools.rs`)** ```rust use crate::circuit_analyzer::{CircuitAnalyzer, GateCallInfo}; use crate::types::CircuitSource; pub fn explain_result_response( loader: &CurriculumLoader, circuit: &str, counts: &Value, statevector: Option<&Value>, ) -> Value { let curriculum = match loader.curriculum() { Ok(c) => c, Err(e) => return json!({ "error": e.to_string() }), }; let gates = match CircuitAnalyzer::list_gates(&CircuitSource(circuit.to_string())) { Ok(g) => g, Err(e) => return json!({ "error": e.to_string() }), }; let gate_breakdown: Vec = gates .iter() .map(|g| gate_entry(g, &curriculum.gate_descriptions)) .collect(); let key_concept = classify_key_concept(&gates); let num_qubits = max_qubit_index(&gates).map(|m| m + 1).unwrap_or(0); let (dominant, missing) = outcomes_summary(counts, num_qubits); let statevector_summary = statevector_summary(statevector); json!({ "gate_breakdown": gate_breakdown, "num_qubits": num_qubits, "key_concept": key_concept, "dominant_outcomes": dominant, "missing_outcomes": missing, "statevector_summary": statevector_summary, }) } fn gate_entry( g: &GateCallInfo, descs: &std::collections::HashMap, ) -> Value { let mut entry = json!({ "name": g.name, "qubits": g.qubits, "params": g.params, }); if let Some(d) = descs.get(&g.name) { entry["description"] = json!(d.short); if let Some(eff) = &d.effect_on_zero { entry["effect_on_zero"] = json!(eff); } } entry } fn classify_key_concept(gates: &[GateCallInfo]) -> &'static str { let names: std::collections::HashSet<&str> = gates.iter().map(|g| g.name.as_str()).collect(); if names.contains("cx") || names.contains("cz") || names.contains("ccx") || names.contains("swap") { "entanglement" } else if names.contains("h") && (names.contains("rz") || names.contains("z") || names.contains("s") || names.contains("t")) { "interference" } else if names.contains("h") { "superposition" } else if names.contains("rx") || names.contains("ry") || names.contains("rz") { "rotation" } else { "measurement" } } fn max_qubit_index(gates: &[GateCallInfo]) -> Option { gates.iter().flat_map(|g| g.qubits.iter().copied()).max() } fn outcomes_summary(counts: &Value, num_qubits: usize) -> (Vec, Vec) { let map = match counts.as_object() { Some(m) => m, None => return (vec![], vec![]), }; let total: u64 = map.values().filter_map(|v| v.as_u64()).sum(); if total == 0 { return (vec![], vec![]); } let threshold = (total as f64 * 0.05) as u64; let mut dominant: Vec = map .iter() .filter(|(_, v)| v.as_u64().unwrap_or(0) > threshold) .map(|(k, _)| k.clone()) .collect(); dominant.sort(); let missing: Vec = if num_qubits == 0 || num_qubits > 4 { vec![] } else { let n = num_qubits; let mut out: Vec = (0u64..(1u64 << n)) .map(|i| format!("{i:0>n$b}")) .filter(|bs| map.get(bs).and_then(|v| v.as_u64()).unwrap_or(0) == 0) .collect(); out.sort(); out }; (dominant, missing) } fn statevector_summary(sv: Option<&Value>) -> Value { let sv = match sv.and_then(|v| v.as_array()) { Some(a) => a, None => return Value::Null, }; let mut non_zero = Vec::new(); let mut zero = Vec::new(); for (i, amp) in sv.iter().enumerate() { let r = amp.as_array().and_then(|a| a.first()).and_then(|v| v.as_f64()).unwrap_or(0.0); let im = amp.as_array().and_then(|a| a.get(1)).and_then(|v| v.as_f64()).unwrap_or(0.0); if r * r + im * im > 1e-10 { non_zero.push(i); } else { zero.push(i); } } json!({"non_zero_amplitudes": non_zero, "zero_amplitudes": zero}) } ``` - [ ] **Step 3: Run tests** ```bash cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED" ``` Expected: `test result: ok. 11 passed`. - [ ] **Step 4: Commit** ```bash git add src/tools/tutor_tools.rs git commit -m "feat: explain_result with AST-based gate breakdown (no string matching)" ```