1use std::collections::{BTreeMap, BTreeSet, VecDeque};
9
10use anyhow::{bail, Result};
11use serde::{Deserialize, Serialize};
12use serde_json::{json, Value};
13
14use crate::model_ir::{Finding, Function, ModelIr, StableId, TensorContract};
15
16pub const QUERY_SCHEMA: &str = "candle-graph/query/1";
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum QueryKind {
21 Summary,
22 Doctor,
23 Architecture,
24 Cargo,
25 Components,
26 Component,
27 Modules,
28 Composition,
29 Assembly,
30 Pipeline,
31 Stages,
32 Artifacts,
33 Entrypoints,
34 Functions,
35 Function,
36 Parameters,
37 Parameter,
38 Tensors,
39 Tensor,
40 Operations,
41 Operation,
42 Optimizers,
43 Runtime,
44 Findings,
45 Path,
46 ModelImprovement,
48}
49
50impl std::str::FromStr for QueryKind {
51 type Err = anyhow::Error;
52
53 fn from_str(value: &str) -> Result<Self> {
54 match value.replace('-', "_").as_str() {
55 "summary" => Ok(Self::Summary),
56 "doctor" | "trust" | "coverage" => Ok(Self::Doctor),
57 "architecture" | "model" => Ok(Self::Architecture),
58 "cargo" | "cfg" | "features" => Ok(Self::Cargo),
59 "components" => Ok(Self::Components),
60 "component" => Ok(Self::Component),
61 "modules" | "module" => Ok(Self::Modules),
62 "composition" | "edges" | "contains" => Ok(Self::Composition),
63 "assembly" | "wiring" | "checkpoint_assembly" => Ok(Self::Assembly),
64 "pipeline" => Ok(Self::Pipeline),
65 "stages" => Ok(Self::Stages),
66 "artifacts" => Ok(Self::Artifacts),
67 "entrypoints" => Ok(Self::Entrypoints),
68 "functions" => Ok(Self::Functions),
69 "function" => Ok(Self::Function),
70 "parameters" => Ok(Self::Parameters),
71 "parameter" => Ok(Self::Parameter),
72 "tensors" => Ok(Self::Tensors),
73 "tensor" => Ok(Self::Tensor),
74 "operations" | "ops" => Ok(Self::Operations),
75 "operation" | "op" => Ok(Self::Operation),
76 "optimizers" | "optimizer" => Ok(Self::Optimizers),
77 "runtime" | "gradient_audit" | "gradients" => Ok(Self::Runtime),
78 "findings" | "diagnostics" => Ok(Self::Findings),
79 "path" | "trace" => Ok(Self::Path),
80 "model_improvement" | "model-improvement" | "improvement" | "agent" => {
81 Ok(Self::ModelImprovement)
82 }
83 other => bail!("unknown query kind `{other}`"),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct QueryRequest {
90 pub kind: QueryKind,
91 pub selector: Option<String>,
92 pub to: Option<String>,
93 pub limit: usize,
94 #[serde(default)]
96 pub offset: usize,
97}
98
99impl QueryRequest {
100 pub fn new(kind: QueryKind) -> Self {
101 Self {
102 kind,
103 selector: None,
104 to: None,
105 limit: 100,
106 offset: 0,
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct QueryResponse {
113 pub schema: String,
114 pub analysis_id: StableId,
115 pub kind: QueryKind,
116 pub selector: Option<String>,
117 pub total: usize,
118 pub returned: usize,
119 #[serde(default)]
120 pub offset: usize,
121 pub truncated: bool,
122 pub items: Vec<Value>,
123}
124
125pub fn execute(model: &ModelIr, request: &QueryRequest) -> Result<QueryResponse> {
126 if request.limit == 0 {
127 bail!("query limit must be greater than zero");
128 }
129 let mut items = match request.kind {
130 QueryKind::Summary => vec![summary(model)],
131 QueryKind::Doctor => vec![doctor(model)],
132 QueryKind::Architecture => vec![architecture(model)],
133 QueryKind::Cargo => vec![json!({
134 "id": "cargo",
135 "context": model.cargo,
136 "drill_down": [
137 {"kind": "summary"},
138 {"kind": "functions"},
139 ],
140 })],
141 QueryKind::Components => model
142 .components
143 .iter()
144 .map(|component| {
145 json!({
146 "id": component.id,
147 "name": component.name,
148 "qualified_name": component.qualified_name,
149 "source": component.source,
150 "builders": component.builders.iter().map(|builder| json!({
151 "name": builder.name,
152 "role": builder.role,
153 })).collect::<Vec<_>>(),
154 "modules": component.modules.len(),
155 "parameters": component.parameters.len(),
156 "entrypoints": component.entrypoints,
157 "drill_down": [
158 {"kind": "component", "select": component.qualified_name},
159 {"kind": "modules", "select": component.qualified_name},
160 {"kind": "entrypoints", "select": component.name},
161 {"kind": "parameters", "select": component.name},
162 ],
163 })
164 })
165 .collect(),
166 QueryKind::Component => {
167 let selector = required_selector(request)?;
168 model
169 .components
170 .iter()
171 .filter(|item| {
172 matches_text(selector, [&item.id.0, &item.name, &item.qualified_name])
173 })
174 .map(|component| {
175 let mut value = serde_json::to_value(component)?;
176 if let Some(object) = value.as_object_mut() {
177 object.insert(
178 "drill_down".into(),
179 json!([
180 {"kind": "modules", "select": component.qualified_name},
181 {"kind": "entrypoints", "select": component.name},
182 {"kind": "parameters", "select": component.name},
183 {"kind": "functions", "select": component.name},
184 {"kind": "tensors", "select": component.name},
185 {"kind": "composition", "select": component.qualified_name},
186 ]),
187 );
188 }
189 Ok(value)
190 })
191 .collect::<serde_json::Result<Vec<_>>>()?
192 }
193 QueryKind::Modules => {
194 let component_names = component_name_lookup(model);
195 model
196 .modules
197 .iter()
198 .filter(|module| {
199 let component_name = component_names
200 .get(&module.component)
201 .map(String::as_str)
202 .unwrap_or("");
203 optional_matches(
204 request.selector.as_deref(),
205 [
206 &module.id.0,
207 &module.type_name,
208 module.qualified_type.as_deref().unwrap_or(""),
209 module.field.as_deref().unwrap_or(""),
210 &module.prefix,
211 &module.builder_root,
212 component_name,
213 ],
214 )
215 })
216 .map(|module| module_listing(module, &component_names))
217 .collect()
218 }
219 QueryKind::Composition => model
220 .architecture_edges
221 .iter()
222 .filter(|edge| edge.id.0.starts_with("composition-edge:"))
223 .filter(|edge| {
224 composition_matches(
225 model,
226 request.selector.as_deref(),
227 edge.from.clone(),
228 edge.to.clone(),
229 )
230 })
231 .map(|edge| composition_listing(model, edge))
232 .collect(),
233 QueryKind::Assembly => model
234 .assembly_sites
235 .iter()
236 .filter(|site| {
237 optional_matches(
238 request.selector.as_deref(),
239 [
240 &site.id.0,
241 &site.component_name,
242 &site.function_name,
243 &site.builder_root,
244 site.varmap.as_deref().unwrap_or(""),
245 site.checkpoint_load.as_deref().unwrap_or(""),
246 ],
247 )
248 })
249 .map(assembly_listing)
250 .collect(),
251 QueryKind::Pipeline => vec![json!({
252 "id": "pipeline",
253 "stages": model.stages.iter().map(|stage| json!({
254 "id": stage.id,
255 "name": stage.name,
256 "kind": stage.kind,
257 "order": stage.order,
258 "dispatch": stage.dispatch,
259 "subprocess_key": stage.subprocess_key,
260 "cli_flags": stage.cli_flags,
261 })).collect::<Vec<_>>(),
262 "subprocess_stages": model.coverage.subprocess_stages,
263 "artifacts": model.artifacts.iter().map(|artifact| json!({
264 "id": artifact.id,
265 "name": artifact.name,
266 })).collect::<Vec<_>>(),
267 "optimizers": model.optimizers.len(),
268 "drill_down": [
269 {"kind": "stages"},
270 {"kind": "artifacts"},
271 {"kind": "optimizers"},
272 ],
273 })],
274 QueryKind::Stages => model
275 .stages
276 .iter()
277 .filter(|stage| {
278 optional_matches(request.selector.as_deref(), [&stage.id.0, &stage.name])
279 })
280 .map(|stage| {
281 json!({
282 "id": stage.id,
283 "name": stage.name,
284 "kind": stage.kind,
285 "order": stage.order,
286 "dispatch": stage.dispatch,
287 "subprocess_key": stage.subprocess_key,
288 "cli_flags": stage.cli_flags,
289 "launcher": stage.launcher,
290 "orchestrator": stage.orchestrator,
291 "source": stage.source,
292 "components": stage.components.len(),
293 "drill_down": [
294 {"kind": "function", "select": stage.source},
295 ],
296 })
297 })
298 .collect(),
299 QueryKind::Artifacts => model
300 .artifacts
301 .iter()
302 .filter(|artifact| {
303 optional_matches(
304 request.selector.as_deref(),
305 [&artifact.id.0, &artifact.name, &artifact.path_expr],
306 )
307 })
308 .map(serde_json::to_value)
309 .collect::<serde_json::Result<Vec<_>>>()?,
310 QueryKind::Entrypoints => {
311 let mut entrypoints: Vec<_> = model
312 .functions
313 .iter()
314 .filter(|function| function.is_entrypoint)
315 .filter(|function| {
316 optional_matches(
317 request.selector.as_deref(),
318 [&function.id.0, &function.name, &function.qualified_name],
319 )
320 })
321 .map(|function| (function, is_component_entrypoint(function, model)))
322 .collect();
323 entrypoints.sort_by(|(left, left_component), (right, right_component)| {
324 right_component
325 .cmp(left_component)
326 .then_with(|| left.qualified_name.cmp(&right.qualified_name))
327 });
328 entrypoints
329 .into_iter()
330 .map(|(function, is_component)| function_listing(function, is_component))
331 .collect()
332 }
333 QueryKind::Functions => model
334 .functions
335 .iter()
336 .filter(|function| {
337 optional_matches(
338 request.selector.as_deref(),
339 [&function.id.0, &function.name, &function.qualified_name],
340 )
341 })
342 .map(|function| function_listing(function, is_component_entrypoint(function, model)))
343 .collect(),
344 QueryKind::Function => {
345 let selector = required_selector(request)?;
346 model
347 .functions
348 .iter()
349 .filter(|function| {
350 matches_text(
351 selector,
352 [&function.id.0, &function.name, &function.qualified_name],
353 )
354 })
355 .map(function_detail)
356 .collect()
357 }
358 QueryKind::Parameters => model
359 .parameters
360 .iter()
361 .filter(|parameter| {
362 optional_matches(
363 request.selector.as_deref(),
364 [
365 ¶meter.id.0,
366 ¶meter.key,
367 ¶meter.builder_root,
368 ¶meter.kind,
369 ],
370 )
371 })
372 .map(|parameter| {
373 json!({
374 "id": parameter.id,
375 "component": parameter.component,
376 "module": parameter.module,
377 "key": parameter.key,
378 "builder_root": parameter.builder_root,
379 "role": parameter.role,
380 "kind": parameter.kind,
381 "symbolic_shape": parameter.symbolic_shape,
382 "checkpoint_shape": parameter.checkpoint_shape,
383 "checkpoint_dtype": parameter.checkpoint_dtype,
384 "source": parameter.source,
385 "uses": parameter.uses,
386 "optimizer_memberships": parameter.optimizer_memberships,
387 "drill_down": [
388 {"kind": "parameter", "select": parameter.id.0},
389 ],
390 })
391 })
392 .collect(),
393 QueryKind::Parameter => {
394 let selector = required_selector(request)?;
395 model
396 .parameters
397 .iter()
398 .filter(|parameter| {
399 matches_text(
400 selector,
401 [
402 ¶meter.id.0,
403 ¶meter.key,
404 ¶meter.builder_root,
405 &format!("{}:{}", parameter.builder_root, parameter.key),
406 ],
407 )
408 })
409 .map(serde_json::to_value)
410 .collect::<serde_json::Result<Vec<_>>>()?
411 }
412 QueryKind::Tensors => model
413 .tensors
414 .iter()
415 .filter(|tensor| tensor_matches(model, tensor, request.selector.as_deref()))
416 .map(|tensor| tensor_listing(model, tensor))
417 .collect(),
418 QueryKind::Tensor => {
419 let selector = required_selector(request)?;
420 model
421 .tensors
422 .iter()
423 .filter(|tensor| tensor_matches(model, tensor, Some(selector)))
424 .map(serde_json::to_value)
425 .collect::<serde_json::Result<Vec<_>>>()?
426 }
427 QueryKind::Operations => model
428 .operations
429 .iter()
430 .filter(|operation| {
431 let function_name = model
432 .functions
433 .iter()
434 .find(|function| function.id == operation.function)
435 .map(|function| function.qualified_name.as_str())
436 .unwrap_or_default();
437 optional_matches(
438 request.selector.as_deref(),
439 [
440 &operation.id.0,
441 &operation.name,
442 operation.qualified_name.as_deref().unwrap_or_default(),
443 &operation.function.0,
444 function_name,
445 ],
446 )
447 })
448 .map(|operation| {
449 json!({
450 "id": operation.id,
451 "name": operation.name,
452 "qualified_name": operation.qualified_name,
453 "function": operation.function,
454 "inputs": operation.inputs.len(),
455 "output": operation.output,
456 "source": operation.source,
457 "drill_down": [
458 {"kind": "operation", "select": operation.id.0},
459 {"kind": "function", "select": operation.function.0},
460 ],
461 })
462 })
463 .collect(),
464 QueryKind::Operation => {
465 let selector = required_selector(request)?;
466 model
467 .operations
468 .iter()
469 .filter(|operation| {
470 matches_text(
471 selector,
472 [
473 &operation.id.0,
474 &operation.name,
475 operation.qualified_name.as_deref().unwrap_or_default(),
476 ],
477 )
478 })
479 .map(serde_json::to_value)
480 .collect::<serde_json::Result<Vec<_>>>()?
481 }
482 QueryKind::Optimizers => model
483 .optimizers
484 .iter()
485 .filter(|optimizer| {
486 optional_matches(
487 request.selector.as_deref(),
488 [
489 &optimizer.id.0,
490 &optimizer.optimizer,
491 &optimizer.varmap,
492 &optimizer.stage.0,
493 ],
494 )
495 })
496 .map(serde_json::to_value)
497 .collect::<serde_json::Result<Vec<_>>>()?,
498 QueryKind::Runtime => vec![json!({
499 "id": "runtime",
500 "summary": model.runtime,
501 "gradient_finding_count": model.findings.iter()
502 .filter(|finding| finding.rule.starts_with("runtime-"))
503 .count(),
504 "drill_down": [
505 {"kind": "findings", "select": "runtime-"},
506 {"kind": "tensors"},
507 ],
508 })],
509 QueryKind::Findings => model
510 .findings
511 .iter()
512 .filter(|finding| {
513 optional_matches(
514 request.selector.as_deref(),
515 [&finding.id.0, &finding.rule, &finding.message],
516 )
517 })
518 .map(|finding| {
519 if request.selector.as_deref() == Some(finding.id.0.as_str()) {
520 serde_json::to_value(finding).unwrap_or_else(|_| json!({}))
521 } else {
522 finding_listing(finding)
523 }
524 })
525 .collect(),
526 QueryKind::ModelImprovement => vec![model_improvement(model)],
527 QueryKind::Path => {
528 let from = required_selector(request)?;
529 let to = request
530 .to
531 .as_deref()
532 .ok_or_else(|| anyhow::anyhow!("path query requires `to`"))?;
533 let from = resolve_id(model, from)?;
534 let to = resolve_id(model, to)?;
535 shortest_path(model, &from, &to)?
536 .into_iter()
537 .map(|id| describe_id(model, &id))
538 .collect()
539 }
540 };
541
542 if matches!(
543 request.kind,
544 QueryKind::Component
545 | QueryKind::Function
546 | QueryKind::Parameter
547 | QueryKind::Tensor
548 | QueryKind::Operation
549 ) && items.len() > 1
550 {
551 bail!(
552 "{:?} selector matched {} records; use an exact stable id from the compact listing",
553 request.kind,
554 items.len()
555 );
556 }
557
558 if !matches!(
559 request.kind,
560 QueryKind::Summary
561 | QueryKind::Doctor
562 | QueryKind::Architecture
563 | QueryKind::Cargo
564 | QueryKind::Pipeline
565 | QueryKind::Stages
566 | QueryKind::Runtime
567 | QueryKind::ModelImprovement
568 ) {
569 items.sort_by_key(stable_value_key);
570 }
571 let total = items.len();
572 let offset = request.offset.min(total);
573 items = items.into_iter().skip(offset).take(request.limit).collect();
574 Ok(QueryResponse {
575 schema: QUERY_SCHEMA.to_string(),
576 analysis_id: model.analysis_id.clone(),
577 kind: request.kind,
578 selector: request.selector.clone(),
579 total,
580 returned: items.len(),
581 offset,
582 truncated: offset + items.len() < total,
583 items,
584 })
585}
586
587pub fn render_text(response: &QueryResponse) -> String {
588 let mut out = format!(
589 "query {:?}: {} result{}",
590 response.kind,
591 response.total,
592 if response.total == 1 { "" } else { "s" }
593 );
594 if response.offset > 0 || response.truncated {
595 out.push_str(&format!(
596 " (offset {}, showing {})",
597 response.offset, response.returned
598 ));
599 }
600 out.push('\n');
601 for item in &response.items {
602 if let Some(object) = item.as_object() {
603 let id = object
604 .get("id")
605 .and_then(Value::as_str)
606 .or_else(|| object.get("name").and_then(Value::as_str))
607 .unwrap_or("-");
608 let name = object
609 .get("name")
610 .and_then(Value::as_str)
611 .or_else(|| object.get("key").and_then(Value::as_str))
612 .or_else(|| object.get("message").and_then(Value::as_str));
613 out.push_str(" ");
614 out.push_str(id);
615 if let Some(name) = name.filter(|name| *name != id) {
616 out.push_str(" ");
617 out.push_str(name);
618 }
619 if let Some(source) = object.get("source").and_then(Value::as_str) {
620 out.push_str(" @");
621 out.push_str(source);
622 }
623 if let Some(hints) = object.get("drill_down").and_then(Value::as_array) {
624 let kinds = hints
625 .iter()
626 .filter_map(|hint| hint.get("kind").and_then(Value::as_str))
627 .collect::<Vec<_>>()
628 .join(", ");
629 if !kinds.is_empty() {
630 out.push_str(" → ");
631 out.push_str(&kinds);
632 }
633 }
634 out.push('\n');
635 } else {
636 out.push_str(" ");
637 out.push_str(&item.to_string());
638 out.push('\n');
639 }
640 }
641 out
642}
643
644fn summary(model: &ModelIr) -> Value {
645 json!({
646 "id": "summary",
647 "schema": model.schema,
648 "analysis_id": model.analysis_id,
649 "cargo": model.cargo.as_ref().map(|cargo| json!({
650 "build_id": cargo.build_id,
651 "package": cargo.package_name,
652 "version": cargo.package_version,
653 "target": cargo.selected_target,
654 "active_features": cargo.active_features,
655 "candle_packages": cargo.candle_packages,
656 })),
657 "coverage": model.coverage,
658 "components": model.components.iter().map(|c| &c.name).collect::<Vec<_>>(),
659 "pipeline": model.stages.iter().map(|stage| json!({
660 "id": stage.id,
661 "name": stage.name,
662 "kind": stage.kind,
663 "order": stage.order,
664 "dispatch": stage.dispatch,
665 "subprocess_key": stage.subprocess_key,
666 "cli_flags": stage.cli_flags,
667 })).collect::<Vec<_>>(),
668 "finding_counts": finding_counts(model),
669 "runtime": model.runtime,
670 "drill_down": [
671 {"kind": "architecture"},
672 {"kind": "composition"},
673 {"kind": "components"},
674 {"kind": "modules"},
675 {"kind": "functions"},
676 {"kind": "entrypoints"},
677 {"kind": "tensors"},
678 {"kind": "findings"},
679 {"kind": "cargo"},
680 {"kind": "doctor"},
681 ],
682 })
683}
684
685fn doctor(model: &ModelIr) -> Value {
686 let mut by_rule = BTreeMap::new();
687 let mut unknown = 0usize;
688 for finding in &model.findings {
689 *by_rule.entry(finding.rule.clone()).or_insert(0usize) += 1;
690 if matches!(
691 finding.confidence,
692 crate::model_ir::Confidence::Unknown | crate::model_ir::Confidence::Heuristic
693 ) {
694 unknown += 1;
695 }
696 }
697 let source_incomplete = by_rule.get("source-load").copied().unwrap_or(0);
698 let semantic_version_gaps = by_rule
699 .get("candle-semantics-version")
700 .copied()
701 .unwrap_or(0);
702 let dtype_risks = by_rule.get("dtype-risk").copied().unwrap_or(0);
703 let dtype_conflicts = by_rule.get("dtype-conflict").copied().unwrap_or(0);
704 let tensor_dtype_pct = pct(model.coverage.tensors_with_dtype, model.coverage.tensors);
705 let tensor_shape_pct = pct(model.coverage.tensors_with_shape, model.coverage.tensors);
706 let tensor_device_pct = pct(model.coverage.tensors_with_device, model.coverage.tensors);
707 json!({
708 "id": "doctor",
709 "analysis_id": model.analysis_id,
710 "cargo_available": model.cargo.is_some(),
711 "coverage": model.coverage,
712 "coverage_quality": {
713 "tensor_dtype_pct": tensor_dtype_pct,
714 "tensor_shape_pct": tensor_shape_pct,
715 "tensor_device_pct": tensor_device_pct,
716 "dtype_risks": dtype_risks,
717 "dtype_conflicts": dtype_conflicts,
718 "component_entrypoints": model.coverage.component_entrypoints,
719 "total_entrypoints": model.coverage.entrypoints,
720 "composition_edges": model.coverage.composition_edges,
721 "assembly_sites": model.coverage.assembly_sites,
722 "subprocess_stages": model.coverage.subprocess_stages,
723 },
724 "trust": {
725 "source_complete": source_incomplete == 0,
726 "candle_catalog_matched": semantic_version_gaps == 0,
727 "compiler_resolved": false,
728 "runtime_evidence": model.runtime.is_some(),
729 "unknown_or_heuristic_findings": unknown,
730 "actionable_warnings": source_incomplete > 0
731 || semantic_version_gaps > 0
732 || dtype_risks > 0
733 || dtype_conflicts > 0,
734 },
735 "finding_counts_by_rule": by_rule,
736 "limitations": [
737 "Rust names and types are source-resolved, not rustc DefIds",
738 "macros and unresolved dynamic dispatch remain explicit Unknown evidence",
739 "call-order pipeline/optimizer relationships require compiler-resolved value flow",
740 "composition edges follow struct-field types with Heuristic confidence",
741 ],
742 "drill_down": [
743 {"kind": "cargo"},
744 {"kind": "findings"},
745 {"kind": "composition"},
746 {"kind": "assembly"},
747 {"kind": "entrypoints"},
748 {"kind": "modules"},
749 ],
750 })
751}
752
753fn model_improvement(model: &ModelIr) -> Value {
754 use crate::model_ir::{Confidence, FindingSeverity};
755
756 let proven_errors: Vec<Value> = model
757 .findings
758 .iter()
759 .filter(|f| {
760 matches!(f.severity, FindingSeverity::Error)
761 && matches!(f.confidence, Confidence::Proven)
762 })
763 .map(finding_listing)
764 .collect();
765
766 let numeric_hazards: Vec<Value> = model
767 .findings
768 .iter()
769 .filter(|f| {
770 matches!(
771 f.rule.as_str(),
772 "numeric-domain-violation"
773 | "zero-times-infinity"
774 | "unstable-library-loss"
775 ) && matches!(f.confidence, Confidence::Proven)
776 })
777 .map(finding_listing)
778 .collect();
779
780 let coverage_gaps: Vec<String> = model
781 .findings
782 .iter()
783 .filter(|f| {
784 matches!(f.confidence, Confidence::Unknown | Confidence::Heuristic)
785 && !matches!(f.severity, FindingSeverity::Information)
786 })
787 .map(|f| format!("{}: {}", f.rule, f.message))
788 .take(20)
789 .collect();
790
791 let gradient_gaps = model.runtime.as_ref().map(|rt| {
792 json!({
793 "missing": rt.missing_gradients,
794 "zero": rt.zero_gradients,
795 "non_finite": rt.non_finite_gradients,
796 "first_non_finite_step": rt.first_non_finite_step,
797 "saturating_activations": rt.saturating_activations,
798 "value_observations": rt.value_observations,
799 })
800 });
801
802 let mut suggested = vec![
803 json!({"kind": "doctor"}),
804 json!({"kind": "findings"}),
805 ];
806 if model.components.is_empty() {
807 suggested.push(json!({"kind": "components"}));
808 } else {
809 for component in model.components.iter().take(3) {
810 suggested.push(json!({
811 "kind": "component",
812 "select": component.qualified_name,
813 }));
814 }
815 }
816 if model.runtime.is_some() {
817 suggested.push(json!({"kind": "runtime"}));
818 }
819
820 json!({
821 "id": "model-improvement",
822 "analysis_id": model.analysis_id,
823 "trust": doctor(model).get("trust").cloned().unwrap_or(json!({})),
824 "proven_errors": proven_errors,
825 "proven_error_count": proven_errors.len(),
826 "numeric_hazards": numeric_hazards,
827 "gradient_gaps": gradient_gaps,
828 "coverage_gaps": coverage_gaps,
829 "components": model.components.iter().map(|c| &c.name).collect::<Vec<_>>(),
830 "parameter_count": model.parameters.len(),
831 "suggested_next_queries": suggested,
832 "drill_down": [
833 {"kind": "doctor"},
834 {"kind": "findings"},
835 {"kind": "model-improvement"},
836 ],
837 })
838}
839
840fn architecture(model: &ModelIr) -> Value {
841 json!({
842 "id": "architecture",
843 "components": model.components.iter().map(|component| json!({
844 "id": component.id,
845 "name": component.name,
846 "qualified_name": component.qualified_name,
847 "source": component.source,
848 "modules": component.modules.len(),
849 "parameters": component.parameters.len(),
850 "entrypoints": component.entrypoints.len(),
851 "drill_down": [
852 {"kind": "component", "select": component.qualified_name},
853 ],
854 })).collect::<Vec<_>>(),
855 "edges": model.architecture_edges.iter().map(|edge| json!({
856 "id": edge.id,
857 "from": edge.from,
858 "to": edge.to,
859 "via_function": edge.via_function,
860 "kind": if edge.id.0.starts_with("composition-edge:") {
861 "composition"
862 } else {
863 "call_flow"
864 },
865 })).collect::<Vec<_>>(),
866 "composition_edges": model.coverage.composition_edges,
867 "stages": model.stages.iter().map(|stage| json!({
868 "id": stage.id,
869 "name": stage.name,
870 "kind": stage.kind,
871 "order": stage.order,
872 })).collect::<Vec<_>>(),
873 "artifacts": model.artifacts.iter().map(|artifact| json!({
874 "id": artifact.id,
875 "name": artifact.name,
876 })).collect::<Vec<_>>(),
877 "entrypoints": model.functions.iter()
878 .filter(|function| function.is_entrypoint)
879 .count(),
880 "drill_down": [
881 {"kind": "components"},
882 {"kind": "composition"},
883 {"kind": "modules"},
884 {"kind": "functions"},
885 {"kind": "tensors"},
886 {"kind": "findings"},
887 ],
888 })
889}
890
891fn function_listing(function: &Function, is_component_entrypoint: bool) -> Value {
892 json!({
893 "id": function.id,
894 "name": function.name,
895 "qualified_name": function.qualified_name,
896 "owner_type": function.owner_type,
897 "visibility": function.visibility,
898 "source": function.source,
899 "is_entrypoint": function.is_entrypoint,
900 "is_component_entrypoint": is_component_entrypoint,
901 "is_loss": function.is_loss,
902 "cfg_active": function.cfg_active,
903 "calls": function.calls.len(),
904 "tensor_inputs": function.tensor_inputs.len(),
905 "tensor_outputs": function.tensor_outputs.len(),
906 "drill_down": [
907 {"kind": "function", "select": function.qualified_name},
908 {"kind": "tensors", "select": function.qualified_name},
909 {"kind": "operations", "select": function.id.0},
910 ],
911 })
912}
913
914fn function_detail(function: &Function) -> Value {
915 let mut drill_down = vec![
916 json!({"kind": "tensors", "select": function.qualified_name}),
917 json!({"kind": "operations", "select": function.id.0}),
918 ];
919 if let Some(id) = function.tensor_inputs.first() {
920 drill_down.push(json!({"kind": "tensor", "select": id.0}));
921 } else if let Some(id) = function.tensor_outputs.first() {
922 drill_down.push(json!({"kind": "tensor", "select": id.0}));
923 }
924 json!({
925 "id": function.id,
926 "name": function.name,
927 "qualified_name": function.qualified_name,
928 "owner_type": function.owner_type,
929 "visibility": function.visibility,
930 "parameters": function.parameters,
931 "return_type": function.return_type,
932 "cfg_predicates": function.cfg_predicates,
933 "cfg_active": function.cfg_active,
934 "source": function.source,
935 "calls": function.calls,
936 "tensor_inputs": function.tensor_inputs,
937 "tensor_outputs": function.tensor_outputs,
938 "is_entrypoint": function.is_entrypoint,
939 "is_loss": function.is_loss,
940 "drill_down": drill_down,
941 })
942}
943
944fn tensor_listing(model: &ModelIr, tensor: &TensorContract) -> Value {
945 let owner = owner_name(model, tensor);
946 json!({
947 "id": tensor.id,
948 "name": tensor.name,
949 "role": tensor.role,
950 "owner_function": tensor.owner_function,
951 "owner": owner,
952 "dtype": tensor.dtype,
953 "shape_rank": tensor.shape.rank,
954 "requires_grad": tensor.requires_grad,
955 "drill_down": [
956 {"kind": "tensor", "select": tensor.id.0},
957 {"kind": "function", "select": owner},
958 ],
959 })
960}
961
962fn finding_listing(finding: &Finding) -> Value {
963 json!({
964 "id": finding.id,
965 "rule": finding.rule,
966 "severity": finding.severity,
967 "confidence": finding.confidence,
968 "message": finding.message,
969 "source": finding.source,
970 "related": finding.related.len(),
971 "drill_down": [
972 {"kind": "findings", "select": finding.id.0},
973 ],
974 })
975}
976
977fn owner_name(model: &ModelIr, tensor: &TensorContract) -> String {
978 model
979 .functions
980 .iter()
981 .find(|function| function.id == tensor.owner_function)
982 .map(|function| function.qualified_name.clone())
983 .unwrap_or_default()
984}
985
986fn tensor_matches(model: &ModelIr, tensor: &TensorContract, selector: Option<&str>) -> bool {
987 let owner = owner_name(model, tensor);
988 optional_matches(
989 selector,
990 [
991 tensor.id.0.as_str(),
992 tensor.name.as_str(),
993 tensor.owner_function.0.as_str(),
994 owner.as_str(),
995 ],
996 )
997}
998
999fn finding_counts(model: &ModelIr) -> BTreeMap<String, usize> {
1000 let mut counts = BTreeMap::new();
1001 for finding in &model.findings {
1002 *counts
1003 .entry(format!("{:?}", finding.severity).to_lowercase())
1004 .or_insert(0) += 1;
1005 }
1006 counts
1007}
1008
1009fn required_selector(request: &QueryRequest) -> Result<&str> {
1010 request
1011 .selector
1012 .as_deref()
1013 .ok_or_else(|| anyhow::anyhow!("{:?} query requires a selector", request.kind))
1014}
1015
1016fn optional_matches<const N: usize>(selector: Option<&str>, values: [&str; N]) -> bool {
1017 selector.is_none_or(|selector| matches_text(selector, values))
1018}
1019
1020fn matches_text<const N: usize>(selector: &str, values: [&str; N]) -> bool {
1021 let selector = selector.to_ascii_lowercase();
1022 values
1023 .iter()
1024 .any(|value| value.to_ascii_lowercase().contains(&selector))
1025}
1026
1027fn stable_value_key(value: &Value) -> String {
1028 let key = value
1029 .get("id")
1030 .and_then(Value::as_str)
1031 .or_else(|| value.get("name").and_then(Value::as_str))
1032 .or_else(|| value.get("key").and_then(Value::as_str))
1033 .unwrap_or_default()
1034 .to_string();
1035 if let Some((prefix, suffix)) = key.rsplit_once(':') {
1036 if let Ok(sequence) = suffix.parse::<u64>() {
1037 return format!("{prefix}:{sequence:020}");
1038 }
1039 }
1040 key
1041}
1042
1043fn resolve_id(model: &ModelIr, selector: &str) -> Result<StableId> {
1044 let mut hits = all_ids(model)
1045 .into_iter()
1046 .filter(|(id, labels)| {
1047 matches_text(
1048 selector,
1049 [
1050 id.0.as_str(),
1051 labels.first().map(String::as_str).unwrap_or_default(),
1052 ],
1053 )
1054 })
1055 .map(|(id, _)| id)
1056 .collect::<Vec<_>>();
1057 hits.sort();
1058 hits.dedup();
1059 match hits.as_slice() {
1060 [id] => Ok(id.clone()),
1061 [] => bail!("selector `{selector}` did not match any model object"),
1062 _ => bail!(
1063 "selector `{selector}` is ambiguous; matched {} objects",
1064 hits.len()
1065 ),
1066 }
1067}
1068
1069fn all_ids(model: &ModelIr) -> Vec<(StableId, Vec<String>)> {
1070 let mut values = Vec::new();
1071 values.extend(
1072 model
1073 .components
1074 .iter()
1075 .map(|v| (v.id.clone(), vec![v.name.clone(), v.qualified_name.clone()])),
1076 );
1077 values.extend(
1078 model
1079 .functions
1080 .iter()
1081 .map(|v| (v.id.clone(), vec![v.name.clone(), v.qualified_name.clone()])),
1082 );
1083 values.extend(
1084 model
1085 .parameters
1086 .iter()
1087 .map(|v| (v.id.clone(), vec![v.key.clone()])),
1088 );
1089 values.extend(
1090 model
1091 .tensors
1092 .iter()
1093 .map(|v| (v.id.clone(), vec![v.name.clone()])),
1094 );
1095 values.extend(
1096 model
1097 .operations
1098 .iter()
1099 .map(|v| (v.id.clone(), vec![v.name.clone()])),
1100 );
1101 values.extend(
1102 model
1103 .stages
1104 .iter()
1105 .map(|v| (v.id.clone(), vec![v.name.clone()])),
1106 );
1107 values.extend(
1108 model
1109 .artifacts
1110 .iter()
1111 .map(|v| (v.id.clone(), vec![v.name.clone(), v.path_expr.clone()])),
1112 );
1113 values
1114}
1115
1116fn shortest_path(model: &ModelIr, from: &StableId, to: &StableId) -> Result<Vec<StableId>> {
1117 let mut adjacency: BTreeMap<StableId, BTreeSet<StableId>> = BTreeMap::new();
1118 for function in &model.functions {
1119 for callee in &function.calls {
1120 connect(&mut adjacency, &function.id, callee);
1121 }
1122 for input in &function.tensor_inputs {
1123 connect(&mut adjacency, input, &function.id);
1124 }
1125 for output in &function.tensor_outputs {
1126 connect(&mut adjacency, &function.id, output);
1127 }
1128 }
1129 for edge in &model.architecture_edges {
1130 connect(&mut adjacency, &edge.from, &edge.to);
1131 }
1132 for operation in &model.operations {
1133 for input in &operation.inputs {
1134 connect(&mut adjacency, input, &operation.id);
1135 }
1136 connect(&mut adjacency, &operation.id, &operation.output);
1137 }
1138 for parameter in &model.parameters {
1139 for use_id in ¶meter.uses {
1140 connect(&mut adjacency, ¶meter.id, use_id);
1141 }
1142 for optimizer in ¶meter.optimizer_memberships {
1143 connect(&mut adjacency, optimizer, ¶meter.id);
1144 }
1145 }
1146 for stage in &model.stages {
1147 for dependency in &stage.depends_on {
1148 connect(&mut adjacency, dependency, &stage.id);
1149 }
1150 connect(&mut adjacency, &stage.id, &stage.function);
1151 for artifact in &stage.consumes {
1152 connect(&mut adjacency, artifact, &stage.id);
1153 }
1154 for artifact in &stage.produces {
1155 connect(&mut adjacency, &stage.id, artifact);
1156 }
1157 }
1158
1159 let mut queue = VecDeque::from([from.clone()]);
1160 let mut previous: BTreeMap<StableId, Option<StableId>> = BTreeMap::from([(from.clone(), None)]);
1161 while let Some(current) = queue.pop_front() {
1162 if current == *to {
1163 let mut path = Vec::new();
1164 let mut cursor = Some(current);
1165 while let Some(id) = cursor {
1166 cursor = previous.get(&id).cloned().flatten();
1167 path.push(id);
1168 }
1169 path.reverse();
1170 return Ok(path);
1171 }
1172 for next in adjacency.get(¤t).into_iter().flatten() {
1173 if previous.contains_key(next) {
1174 continue;
1175 }
1176 previous.insert(next.clone(), Some(current.clone()));
1177 queue.push_back(next.clone());
1178 }
1179 }
1180 bail!("no path found from `{from}` to `{to}`")
1181}
1182
1183fn connect(adjacency: &mut BTreeMap<StableId, BTreeSet<StableId>>, from: &StableId, to: &StableId) {
1184 adjacency
1185 .entry(from.clone())
1186 .or_default()
1187 .insert(to.clone());
1188}
1189
1190fn describe_id(model: &ModelIr, id: &StableId) -> Value {
1191 if let Some(value) = model.components.iter().find(|v| v.id == *id) {
1192 return json!({"id": id, "kind": "component", "name": value.name, "source": value.source});
1193 }
1194 if let Some(value) = model.functions.iter().find(|v| v.id == *id) {
1195 return json!({"id": id, "kind": "function", "name": value.qualified_name, "source": value.source});
1196 }
1197 if let Some(value) = model.parameters.iter().find(|v| v.id == *id) {
1198 return json!({"id": id, "kind": "parameter", "name": value.key, "source": value.source});
1199 }
1200 if let Some(value) = model.tensors.iter().find(|v| v.id == *id) {
1201 return json!({"id": id, "kind": "tensor", "name": value.name});
1202 }
1203 if let Some(value) = model.operations.iter().find(|v| v.id == *id) {
1204 return json!({"id": id, "kind": "operation", "name": value.name, "source": value.source});
1205 }
1206 if let Some(value) = model.stages.iter().find(|v| v.id == *id) {
1207 return json!({"id": id, "kind": "stage", "name": value.name, "source": value.source});
1208 }
1209 if let Some(value) = model.artifacts.iter().find(|v| v.id == *id) {
1210 return json!({"id": id, "kind": "artifact", "name": value.name, "source": value.source});
1211 }
1212 json!({"id": id, "kind": "unknown"})
1213}
1214
1215fn pct(part: usize, total: usize) -> f64 {
1216 if total == 0 {
1217 0.0
1218 } else {
1219 ((part as f64 / total as f64) * 1000.0).round() / 10.0
1220 }
1221}
1222
1223fn component_name_lookup(model: &ModelIr) -> BTreeMap<StableId, String> {
1224 model
1225 .components
1226 .iter()
1227 .map(|component| (component.id.clone(), component.name.clone()))
1228 .collect()
1229}
1230
1231fn is_component_entrypoint(function: &Function, model: &ModelIr) -> bool {
1232 function.owner_type.as_ref().is_some_and(|owner| {
1233 model
1234 .components
1235 .iter()
1236 .any(|component| component.qualified_name == *owner || component.name == *owner)
1237 })
1238}
1239
1240fn module_listing(
1241 module: &crate::model_ir::Module,
1242 component_names: &BTreeMap<StableId, String>,
1243) -> Value {
1244 let component_name = component_names
1245 .get(&module.component)
1246 .cloned()
1247 .unwrap_or_default();
1248 let mut drill_down = vec![json!({"kind": "parameters", "select": module.prefix})];
1249 if module.qualified_type.as_deref().is_some_and(|type_name| {
1250 component_names.values().any(|name| name == type_name)
1251 || type_name.contains("::")
1252 && component_names
1253 .values()
1254 .any(|name| type_name.ends_with(name))
1255 }) {
1256 drill_down.push(json!({
1257 "kind": "composition",
1258 "select": module.qualified_type.clone().unwrap_or(module.type_name.clone()),
1259 }));
1260 }
1261 json!({
1262 "id": module.id,
1263 "component": module.component,
1264 "component_name": component_name,
1265 "parent": module.parent,
1266 "type_name": module.type_name,
1267 "qualified_type": module.qualified_type,
1268 "field": module.field,
1269 "builder_root": module.builder_root,
1270 "prefix": module.prefix,
1271 "repeat": module.repeat,
1272 "source": module.source,
1273 "confidence": module.confidence,
1274 "drill_down": drill_down,
1275 })
1276}
1277
1278fn assembly_listing(site: &crate::model_ir::AssemblySite) -> Value {
1279 json!({
1280 "id": site.id,
1281 "function_name": site.function_name,
1282 "component_name": site.component_name,
1283 "component": site.component,
1284 "builder_root": site.builder_root,
1285 "prefix_chain": site.prefix_chain,
1286 "varmap": site.varmap,
1287 "source_kind": site.source_kind,
1288 "role": site.role,
1289 "checkpoint_load": site.checkpoint_load,
1290 "source": site.source,
1291 "drill_down": [
1292 {"kind": "component", "select": site.component_name},
1293 {"kind": "parameters", "select": site.component_name},
1294 {"kind": "function", "select": site.function_name},
1295 ],
1296 })
1297}
1298
1299fn composition_listing(model: &ModelIr, edge: &crate::model_ir::ArchitectureEdge) -> Value {
1300 let from = model
1301 .components
1302 .iter()
1303 .find(|component| component.id == edge.from);
1304 let to = model
1305 .components
1306 .iter()
1307 .find(|component| component.id == edge.to);
1308 let mut drill_down = Vec::new();
1309 if let Some(component) = from {
1310 drill_down.push(json!({"kind": "component", "select": component.qualified_name}));
1311 drill_down.push(json!({"kind": "modules", "select": component.name}));
1312 }
1313 if let Some(component) = to {
1314 drill_down.push(json!({"kind": "component", "select": component.qualified_name}));
1315 }
1316 json!({
1317 "id": edge.id,
1318 "from": from.map(|component| json!({
1319 "id": component.id,
1320 "name": component.name,
1321 "qualified_name": component.qualified_name,
1322 })).unwrap_or_else(|| json!({"id": edge.from})),
1323 "to": to.map(|component| json!({
1324 "id": component.id,
1325 "name": component.name,
1326 "qualified_name": component.qualified_name,
1327 })).unwrap_or_else(|| json!({"id": edge.to})),
1328 "via_function": edge.via_function,
1329 "source": edge.source,
1330 "confidence": edge.evidence.first().map(|evidence| &evidence.confidence),
1331 "detail": edge.evidence.first().map(|evidence| &evidence.detail),
1332 "drill_down": drill_down,
1333 })
1334}
1335
1336fn composition_matches(
1337 model: &ModelIr,
1338 selector: Option<&str>,
1339 from: StableId,
1340 to: StableId,
1341) -> bool {
1342 let Some(selector) = selector else {
1343 return true;
1344 };
1345 let from_labels = component_labels(model, &from);
1346 let to_labels = component_labels(model, &to);
1347 matches_text(selector, from_labels) || matches_text(selector, to_labels)
1348}
1349
1350fn component_labels<'a>(model: &'a ModelIr, id: &StableId) -> [&'a str; 3] {
1351 if let Some(component) = model
1352 .components
1353 .iter()
1354 .find(|component| component.id == *id)
1355 {
1356 [
1357 component.id.0.as_str(),
1358 component.name.as_str(),
1359 component.qualified_name.as_str(),
1360 ]
1361 } else {
1362 ["", "", ""]
1363 }
1364}