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