xtask/codex_schema/
bisect.rs1use std::collections::BTreeSet;
10use std::path::{Path, PathBuf};
11
12use anyhow::Result;
13use serde_json::{Map, Value};
14
15use super::typify_probe::{self, ProbeOutcome};
16use super::{load_combined_defs, merge, parse_gen_dir, PROTOCOL_SCHEMA_PATH};
17
18pub fn run(args: &[String]) -> Result<()> {
19 let gen_dir = parse_args(args)?;
20 bisect(&gen_dir)
21}
22
23fn parse_args(args: &[String]) -> Result<PathBuf> {
24 parse_gen_dir(
25 args,
26 "Usage: cargo xtask codex-schema bisect <path-to-codex-generate-json-schema-output-dir>",
27 )
28}
29
30pub fn bisect(gen_dir: &Path) -> Result<()> {
31 let (combined, defs) = load_combined_defs(gen_dir)?;
32
33 println!(
34 "==> probing the full merged schema ({} definitions)...",
35 defs.len()
36 );
37 let full_outcome = typify_probe::probe(&combined);
38 println!(" {}", full_outcome.summary());
39
40 if !full_outcome.reproduces_target() {
41 println!(
42 "\n==> the full merged schema does not panic with the target typify merge.rs shape."
43 );
44 match full_outcome {
45 ProbeOutcome::Success => {
46 println!(" typify converted it successfully - nothing to bisect.")
47 }
48 other => println!(
49 " outcome was instead: {}\n this tool only bisects the specific merge.rs \
50 \"not yet implemented\" panic - investigate this outcome by hand.",
51 other.summary()
52 ),
53 }
54 return Ok(());
55 }
56
57 let universe = suspect_universe(&defs);
58 let scope_note = if universe.len() == defs.len() {
59 " (no usable baseline diff - searching every definition)".to_string()
60 } else {
61 format!(" (definitions new or changed vs. the committed {PROTOCOL_SCHEMA_PATH})")
62 };
63 println!(
64 "\n==> search universe: {} suspect definition(s){scope_note}",
65 universe.len()
66 );
67
68 let culprits = minimize(&defs, universe);
69
70 println!("\n==> bisection complete. culprit definition(s):");
71 for name in &culprits {
72 println!("\n--- {name} ---");
73 match defs.get(name) {
74 Some(schema) => println!(
75 "{}",
76 serde_json::to_string_pretty(schema).unwrap_or_else(|_| "<unserializable>".into())
77 ),
78 None => println!("<definition not found - internal bisection bug>"),
79 }
80 }
81 println!(
82 "\nNext step: either flatten the offending shape the way \
83 McpServerElicitationRequestParams was handled (see \
84 `xtask/src/codex_schema/merge.rs::flatten_base_plus_oneof` and the crate README's \"How \
85 the typed protocol layer is built\" section), or, as a last resort, opaque this \
86 definition to `true` (serde_json::Value) in `build_combined` and document the loss of \
87 type fidelity."
88 );
89 Ok(())
90}
91
92fn suspect_universe(defs: &Map<String, Value>) -> BTreeSet<String> {
97 let baseline_defs = std::fs::read_to_string(PROTOCOL_SCHEMA_PATH)
98 .ok()
99 .and_then(|text| serde_json::from_str::<Value>(&text).ok())
100 .and_then(|v| v.get("definitions").and_then(Value::as_object).cloned());
101
102 let Some(baseline_defs) = baseline_defs else {
103 return defs.keys().cloned().collect();
104 };
105
106 let changed: BTreeSet<String> = defs
107 .iter()
108 .filter(|(name, schema)| baseline_defs.get(*name) != Some(schema))
109 .map(|(name, _)| name.clone())
110 .collect();
111
112 if changed.is_empty() {
113 defs.keys().cloned().collect()
114 } else {
115 changed
116 }
117}
118
119fn minimize(all_defs: &Map<String, Value>, mut universe: BTreeSet<String>) -> Vec<String> {
123 loop {
124 if universe.len() <= 1 {
125 return universe.into_iter().collect();
126 }
127
128 let mid = universe.len() / 2;
129 let (first, second) = split_at(&universe, mid);
130
131 println!(
132 "==> {} candidate(s) remaining - trying first half alone ({} defs)...",
133 universe.len(),
134 first.len()
135 );
136 if probe_with_kept(all_defs, &universe, &first).reproduces_target() {
137 universe = first;
138 continue;
139 }
140
141 println!(
142 " not reproduced; trying second half alone ({} defs)...",
143 second.len()
144 );
145 if probe_with_kept(all_defs, &universe, &second).reproduces_target() {
146 universe = second;
147 continue;
148 }
149
150 println!(
151 " neither half alone reproduces the panic - these {} definition(s) appear to \
152 jointly trigger it. Reporting the full remaining set.",
153 universe.len()
154 );
155 return universe.into_iter().collect();
156 }
157}
158
159fn split_at(set: &BTreeSet<String>, mid: usize) -> (BTreeSet<String>, BTreeSet<String>) {
160 let mut first = BTreeSet::new();
161 let mut second = BTreeSet::new();
162 for (i, item) in set.iter().enumerate() {
163 if i < mid {
164 first.insert(item.clone());
165 } else {
166 second.insert(item.clone());
167 }
168 }
169 (first, second)
170}
171
172fn probe_with_kept(
176 all_defs: &Map<String, Value>,
177 universe: &BTreeSet<String>,
178 keep_real: &BTreeSet<String>,
179) -> ProbeOutcome {
180 let mut candidate_defs = all_defs.clone();
181 for name in universe {
182 if !keep_real.contains(name) {
183 candidate_defs.insert(name.clone(), Value::Bool(true));
184 }
185 }
186 typify_probe::probe(&merge::wrap_definitions(candidate_defs))
187}
188
189#[cfg(test)]
190#[path = "bisect_tests.rs"]
191mod tests;