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, ExecutionPhase};
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 "avg_duration_ns": operation.avg_duration_ns,
795 "timing_samples": operation.timing_samples,
796 })
797 })
798 .collect();
799 json!({
800 "id": format!("graph-{}", phase.as_str()),
801 "phase": phase,
802 "tensor_count": tensors.len(),
803 "operation_count": operations.len(),
804 "tensors": tensors,
805 "operations": operations,
806 "drill_down": [
807 {"kind": "tensors"},
808 {"kind": "operations"},
809 {"kind": "profile"},
810 ],
811 })
812}
813
814fn profile_query(model: &ModelIr) -> Value {
815 let mut slowest: Vec<Value> = model
816 .operations
817 .iter()
818 .filter_map(|operation| {
819 operation.avg_duration_ns.map(|duration| {
820 json!({
821 "id": operation.id,
822 "name": operation.name,
823 "phase": operation.execution_phase,
824 "avg_duration_ns": duration,
825 "samples": operation.timing_samples,
826 })
827 })
828 })
829 .collect();
830 slowest.sort_by(|left, right| {
831 right["avg_duration_ns"]
832 .as_u64()
833 .unwrap_or(0)
834 .cmp(&left["avg_duration_ns"].as_u64().unwrap_or(0))
835 });
836 slowest.truncate(50);
837 json!({
838 "id": "profile",
839 "runtime": model.runtime,
840 "slowest_operations": slowest,
841 "drill_down": [
842 {"kind": "graph-train"},
843 {"kind": "graph-infer"},
844 {"kind": "runtime"},
845 ],
846 })
847}
848
849fn model_improvement(model: &ModelIr) -> Value {
850 use crate::model_ir::{Confidence, FindingSeverity};
851
852 let proven_errors: Vec<Value> = model
853 .findings
854 .iter()
855 .filter(|f| {
856 matches!(f.severity, FindingSeverity::Error)
857 && matches!(f.confidence, Confidence::Proven)
858 })
859 .map(finding_listing)
860 .collect();
861
862 let numeric_hazards: Vec<Value> = model
863 .findings
864 .iter()
865 .filter(|f| {
866 matches!(
867 f.rule.as_str(),
868 "numeric-domain-violation"
869 | "zero-times-infinity"
870 | "unstable-library-loss"
871 ) && matches!(f.confidence, Confidence::Proven)
872 })
873 .map(finding_listing)
874 .collect();
875
876 let coverage_gaps: Vec<String> = model
877 .findings
878 .iter()
879 .filter(|f| {
880 matches!(f.confidence, Confidence::Unknown | Confidence::Heuristic)
881 && !matches!(f.severity, FindingSeverity::Information)
882 })
883 .map(|f| format!("{}: {}", f.rule, f.message))
884 .take(20)
885 .collect();
886
887 let gradient_gaps = model.runtime.as_ref().map(|rt| {
888 json!({
889 "missing": rt.missing_gradients,
890 "zero": rt.zero_gradients,
891 "non_finite": rt.non_finite_gradients,
892 "first_non_finite_step": rt.first_non_finite_step,
893 "saturating_activations": rt.saturating_activations,
894 "value_observations": rt.value_observations,
895 })
896 });
897
898 let mut suggested = vec![
899 json!({"kind": "doctor"}),
900 json!({"kind": "findings"}),
901 ];
902 if model.components.is_empty() {
903 suggested.push(json!({"kind": "components"}));
904 } else {
905 for component in model.components.iter().take(3) {
906 suggested.push(json!({
907 "kind": "component",
908 "select": component.qualified_name,
909 }));
910 }
911 }
912 if model.runtime.is_some() {
913 suggested.push(json!({"kind": "runtime"}));
914 }
915
916 json!({
917 "id": "model-improvement",
918 "analysis_id": model.analysis_id,
919 "trust": doctor(model).get("trust").cloned().unwrap_or(json!({})),
920 "proven_errors": proven_errors,
921 "proven_error_count": proven_errors.len(),
922 "numeric_hazards": numeric_hazards,
923 "gradient_gaps": gradient_gaps,
924 "coverage_gaps": coverage_gaps,
925 "components": model.components.iter().map(|c| &c.name).collect::<Vec<_>>(),
926 "parameter_count": model.parameters.len(),
927 "suggested_next_queries": suggested,
928 "drill_down": [
929 {"kind": "doctor"},
930 {"kind": "findings"},
931 {"kind": "model-improvement"},
932 ],
933 })
934}
935
936fn architecture(model: &ModelIr) -> Value {
937 json!({
938 "id": "architecture",
939 "components": model.components.iter().map(|component| json!({
940 "id": component.id,
941 "name": component.name,
942 "qualified_name": component.qualified_name,
943 "source": component.source,
944 "modules": component.modules.len(),
945 "parameters": component.parameters.len(),
946 "entrypoints": component.entrypoints.len(),
947 "drill_down": [
948 {"kind": "component", "select": component.qualified_name},
949 ],
950 })).collect::<Vec<_>>(),
951 "edges": model.architecture_edges.iter().map(|edge| json!({
952 "id": edge.id,
953 "from": edge.from,
954 "to": edge.to,
955 "via_function": edge.via_function,
956 "kind": if edge.id.0.starts_with("composition-edge:") {
957 "composition"
958 } else {
959 "call_flow"
960 },
961 })).collect::<Vec<_>>(),
962 "composition_edges": model.coverage.composition_edges,
963 "stages": model.stages.iter().map(|stage| json!({
964 "id": stage.id,
965 "name": stage.name,
966 "kind": stage.kind,
967 "order": stage.order,
968 })).collect::<Vec<_>>(),
969 "artifacts": model.artifacts.iter().map(|artifact| json!({
970 "id": artifact.id,
971 "name": artifact.name,
972 })).collect::<Vec<_>>(),
973 "entrypoints": model.functions.iter()
974 .filter(|function| function.is_entrypoint)
975 .count(),
976 "drill_down": [
977 {"kind": "components"},
978 {"kind": "composition"},
979 {"kind": "modules"},
980 {"kind": "functions"},
981 {"kind": "tensors"},
982 {"kind": "findings"},
983 ],
984 })
985}
986
987fn function_listing(function: &Function, is_component_entrypoint: bool) -> Value {
988 json!({
989 "id": function.id,
990 "name": function.name,
991 "qualified_name": function.qualified_name,
992 "owner_type": function.owner_type,
993 "visibility": function.visibility,
994 "source": function.source,
995 "is_entrypoint": function.is_entrypoint,
996 "is_component_entrypoint": is_component_entrypoint,
997 "is_loss": function.is_loss,
998 "cfg_active": function.cfg_active,
999 "calls": function.calls.len(),
1000 "tensor_inputs": function.tensor_inputs.len(),
1001 "tensor_outputs": function.tensor_outputs.len(),
1002 "drill_down": [
1003 {"kind": "function", "select": function.qualified_name},
1004 {"kind": "tensors", "select": function.qualified_name},
1005 {"kind": "operations", "select": function.id.0},
1006 ],
1007 })
1008}
1009
1010fn function_detail(function: &Function) -> Value {
1011 let mut drill_down = vec![
1012 json!({"kind": "tensors", "select": function.qualified_name}),
1013 json!({"kind": "operations", "select": function.id.0}),
1014 ];
1015 if let Some(id) = function.tensor_inputs.first() {
1016 drill_down.push(json!({"kind": "tensor", "select": id.0}));
1017 } else if let Some(id) = function.tensor_outputs.first() {
1018 drill_down.push(json!({"kind": "tensor", "select": id.0}));
1019 }
1020 json!({
1021 "id": function.id,
1022 "name": function.name,
1023 "qualified_name": function.qualified_name,
1024 "owner_type": function.owner_type,
1025 "visibility": function.visibility,
1026 "parameters": function.parameters,
1027 "return_type": function.return_type,
1028 "cfg_predicates": function.cfg_predicates,
1029 "cfg_active": function.cfg_active,
1030 "source": function.source,
1031 "calls": function.calls,
1032 "tensor_inputs": function.tensor_inputs,
1033 "tensor_outputs": function.tensor_outputs,
1034 "is_entrypoint": function.is_entrypoint,
1035 "is_loss": function.is_loss,
1036 "drill_down": drill_down,
1037 })
1038}
1039
1040fn tensor_listing(model: &ModelIr, tensor: &TensorContract) -> Value {
1041 let owner = owner_name(model, tensor);
1042 json!({
1043 "id": tensor.id,
1044 "name": tensor.name,
1045 "role": tensor.role,
1046 "owner_function": tensor.owner_function,
1047 "owner": owner,
1048 "dtype": tensor.dtype,
1049 "shape_rank": tensor.shape.rank,
1050 "requires_grad": tensor.requires_grad,
1051 "drill_down": [
1052 {"kind": "tensor", "select": tensor.id.0},
1053 {"kind": "function", "select": owner},
1054 ],
1055 })
1056}
1057
1058fn finding_listing(finding: &Finding) -> Value {
1059 json!({
1060 "id": finding.id,
1061 "rule": finding.rule,
1062 "severity": finding.severity,
1063 "confidence": finding.confidence,
1064 "message": finding.message,
1065 "source": finding.source,
1066 "related": finding.related.len(),
1067 "drill_down": [
1068 {"kind": "findings", "select": finding.id.0},
1069 ],
1070 })
1071}
1072
1073fn owner_name(model: &ModelIr, tensor: &TensorContract) -> String {
1074 model
1075 .functions
1076 .iter()
1077 .find(|function| function.id == tensor.owner_function)
1078 .map(|function| function.qualified_name.clone())
1079 .unwrap_or_default()
1080}
1081
1082fn tensor_matches(model: &ModelIr, tensor: &TensorContract, selector: Option<&str>) -> bool {
1083 let owner = owner_name(model, tensor);
1084 optional_matches(
1085 selector,
1086 [
1087 tensor.id.0.as_str(),
1088 tensor.name.as_str(),
1089 tensor.owner_function.0.as_str(),
1090 owner.as_str(),
1091 ],
1092 )
1093}
1094
1095fn finding_counts(model: &ModelIr) -> BTreeMap<String, usize> {
1096 let mut counts = BTreeMap::new();
1097 for finding in &model.findings {
1098 *counts
1099 .entry(format!("{:?}", finding.severity).to_lowercase())
1100 .or_insert(0) += 1;
1101 }
1102 counts
1103}
1104
1105fn required_selector(request: &QueryRequest) -> Result<&str> {
1106 request
1107 .selector
1108 .as_deref()
1109 .ok_or_else(|| anyhow::anyhow!("{:?} query requires a selector", request.kind))
1110}
1111
1112fn optional_matches<const N: usize>(selector: Option<&str>, values: [&str; N]) -> bool {
1113 selector.is_none_or(|selector| matches_text(selector, values))
1114}
1115
1116fn matches_text<const N: usize>(selector: &str, values: [&str; N]) -> bool {
1117 let selector = selector.to_ascii_lowercase();
1118 values
1119 .iter()
1120 .any(|value| value.to_ascii_lowercase().contains(&selector))
1121}
1122
1123fn stable_value_key(value: &Value) -> String {
1124 let key = value
1125 .get("id")
1126 .and_then(Value::as_str)
1127 .or_else(|| value.get("name").and_then(Value::as_str))
1128 .or_else(|| value.get("key").and_then(Value::as_str))
1129 .unwrap_or_default()
1130 .to_string();
1131 if let Some((prefix, suffix)) = key.rsplit_once(':') {
1132 if let Ok(sequence) = suffix.parse::<u64>() {
1133 return format!("{prefix}:{sequence:020}");
1134 }
1135 }
1136 key
1137}
1138
1139fn resolve_id(model: &ModelIr, selector: &str) -> Result<StableId> {
1140 let mut hits = all_ids(model)
1141 .into_iter()
1142 .filter(|(id, labels)| {
1143 matches_text(
1144 selector,
1145 [
1146 id.0.as_str(),
1147 labels.first().map(String::as_str).unwrap_or_default(),
1148 ],
1149 )
1150 })
1151 .map(|(id, _)| id)
1152 .collect::<Vec<_>>();
1153 hits.sort();
1154 hits.dedup();
1155 match hits.as_slice() {
1156 [id] => Ok(id.clone()),
1157 [] => bail!("selector `{selector}` did not match any model object"),
1158 _ => bail!(
1159 "selector `{selector}` is ambiguous; matched {} objects",
1160 hits.len()
1161 ),
1162 }
1163}
1164
1165fn all_ids(model: &ModelIr) -> Vec<(StableId, Vec<String>)> {
1166 let mut values = Vec::new();
1167 values.extend(
1168 model
1169 .components
1170 .iter()
1171 .map(|v| (v.id.clone(), vec![v.name.clone(), v.qualified_name.clone()])),
1172 );
1173 values.extend(
1174 model
1175 .functions
1176 .iter()
1177 .map(|v| (v.id.clone(), vec![v.name.clone(), v.qualified_name.clone()])),
1178 );
1179 values.extend(
1180 model
1181 .parameters
1182 .iter()
1183 .map(|v| (v.id.clone(), vec![v.key.clone()])),
1184 );
1185 values.extend(
1186 model
1187 .tensors
1188 .iter()
1189 .map(|v| (v.id.clone(), vec![v.name.clone()])),
1190 );
1191 values.extend(
1192 model
1193 .operations
1194 .iter()
1195 .map(|v| (v.id.clone(), vec![v.name.clone()])),
1196 );
1197 values.extend(
1198 model
1199 .stages
1200 .iter()
1201 .map(|v| (v.id.clone(), vec![v.name.clone()])),
1202 );
1203 values.extend(
1204 model
1205 .artifacts
1206 .iter()
1207 .map(|v| (v.id.clone(), vec![v.name.clone(), v.path_expr.clone()])),
1208 );
1209 values
1210}
1211
1212fn shortest_path(model: &ModelIr, from: &StableId, to: &StableId) -> Result<Vec<StableId>> {
1213 let mut adjacency: BTreeMap<StableId, BTreeSet<StableId>> = BTreeMap::new();
1214 for function in &model.functions {
1215 for callee in &function.calls {
1216 connect(&mut adjacency, &function.id, callee);
1217 }
1218 for input in &function.tensor_inputs {
1219 connect(&mut adjacency, input, &function.id);
1220 }
1221 for output in &function.tensor_outputs {
1222 connect(&mut adjacency, &function.id, output);
1223 }
1224 }
1225 for edge in &model.architecture_edges {
1226 connect(&mut adjacency, &edge.from, &edge.to);
1227 }
1228 for operation in &model.operations {
1229 for input in &operation.inputs {
1230 connect(&mut adjacency, input, &operation.id);
1231 }
1232 connect(&mut adjacency, &operation.id, &operation.output);
1233 }
1234 for parameter in &model.parameters {
1235 for use_id in ¶meter.uses {
1236 connect(&mut adjacency, ¶meter.id, use_id);
1237 }
1238 for optimizer in ¶meter.optimizer_memberships {
1239 connect(&mut adjacency, optimizer, ¶meter.id);
1240 }
1241 }
1242 for stage in &model.stages {
1243 for dependency in &stage.depends_on {
1244 connect(&mut adjacency, dependency, &stage.id);
1245 }
1246 connect(&mut adjacency, &stage.id, &stage.function);
1247 for artifact in &stage.consumes {
1248 connect(&mut adjacency, artifact, &stage.id);
1249 }
1250 for artifact in &stage.produces {
1251 connect(&mut adjacency, &stage.id, artifact);
1252 }
1253 }
1254
1255 let mut queue = VecDeque::from([from.clone()]);
1256 let mut previous: BTreeMap<StableId, Option<StableId>> = BTreeMap::from([(from.clone(), None)]);
1257 while let Some(current) = queue.pop_front() {
1258 if current == *to {
1259 let mut path = Vec::new();
1260 let mut cursor = Some(current);
1261 while let Some(id) = cursor {
1262 cursor = previous.get(&id).cloned().flatten();
1263 path.push(id);
1264 }
1265 path.reverse();
1266 return Ok(path);
1267 }
1268 for next in adjacency.get(¤t).into_iter().flatten() {
1269 if previous.contains_key(next) {
1270 continue;
1271 }
1272 previous.insert(next.clone(), Some(current.clone()));
1273 queue.push_back(next.clone());
1274 }
1275 }
1276 bail!("no path found from `{from}` to `{to}`")
1277}
1278
1279fn connect(adjacency: &mut BTreeMap<StableId, BTreeSet<StableId>>, from: &StableId, to: &StableId) {
1280 adjacency
1281 .entry(from.clone())
1282 .or_default()
1283 .insert(to.clone());
1284}
1285
1286fn describe_id(model: &ModelIr, id: &StableId) -> Value {
1287 if let Some(value) = model.components.iter().find(|v| v.id == *id) {
1288 return json!({"id": id, "kind": "component", "name": value.name, "source": value.source});
1289 }
1290 if let Some(value) = model.functions.iter().find(|v| v.id == *id) {
1291 return json!({"id": id, "kind": "function", "name": value.qualified_name, "source": value.source});
1292 }
1293 if let Some(value) = model.parameters.iter().find(|v| v.id == *id) {
1294 return json!({"id": id, "kind": "parameter", "name": value.key, "source": value.source});
1295 }
1296 if let Some(value) = model.tensors.iter().find(|v| v.id == *id) {
1297 return json!({"id": id, "kind": "tensor", "name": value.name});
1298 }
1299 if let Some(value) = model.operations.iter().find(|v| v.id == *id) {
1300 return json!({"id": id, "kind": "operation", "name": value.name, "source": value.source});
1301 }
1302 if let Some(value) = model.stages.iter().find(|v| v.id == *id) {
1303 return json!({"id": id, "kind": "stage", "name": value.name, "source": value.source});
1304 }
1305 if let Some(value) = model.artifacts.iter().find(|v| v.id == *id) {
1306 return json!({"id": id, "kind": "artifact", "name": value.name, "source": value.source});
1307 }
1308 json!({"id": id, "kind": "unknown"})
1309}
1310
1311fn pct(part: usize, total: usize) -> f64 {
1312 if total == 0 {
1313 0.0
1314 } else {
1315 ((part as f64 / total as f64) * 1000.0).round() / 10.0
1316 }
1317}
1318
1319fn component_name_lookup(model: &ModelIr) -> BTreeMap<StableId, String> {
1320 model
1321 .components
1322 .iter()
1323 .map(|component| (component.id.clone(), component.name.clone()))
1324 .collect()
1325}
1326
1327fn is_component_entrypoint(function: &Function, model: &ModelIr) -> bool {
1328 function.owner_type.as_ref().is_some_and(|owner| {
1329 model
1330 .components
1331 .iter()
1332 .any(|component| component.qualified_name == *owner || component.name == *owner)
1333 })
1334}
1335
1336fn module_listing(
1337 module: &crate::model_ir::Module,
1338 component_names: &BTreeMap<StableId, String>,
1339) -> Value {
1340 let component_name = component_names
1341 .get(&module.component)
1342 .cloned()
1343 .unwrap_or_default();
1344 let mut drill_down = vec![json!({"kind": "parameters", "select": module.prefix})];
1345 if module.qualified_type.as_deref().is_some_and(|type_name| {
1346 component_names.values().any(|name| name == type_name)
1347 || type_name.contains("::")
1348 && component_names
1349 .values()
1350 .any(|name| type_name.ends_with(name))
1351 }) {
1352 drill_down.push(json!({
1353 "kind": "composition",
1354 "select": module.qualified_type.clone().unwrap_or(module.type_name.clone()),
1355 }));
1356 }
1357 json!({
1358 "id": module.id,
1359 "component": module.component,
1360 "component_name": component_name,
1361 "parent": module.parent,
1362 "type_name": module.type_name,
1363 "qualified_type": module.qualified_type,
1364 "field": module.field,
1365 "builder_root": module.builder_root,
1366 "prefix": module.prefix,
1367 "repeat": module.repeat,
1368 "source": module.source,
1369 "confidence": module.confidence,
1370 "drill_down": drill_down,
1371 })
1372}
1373
1374fn assembly_listing(site: &crate::model_ir::AssemblySite) -> Value {
1375 json!({
1376 "id": site.id,
1377 "function_name": site.function_name,
1378 "component_name": site.component_name,
1379 "component": site.component,
1380 "builder_root": site.builder_root,
1381 "prefix_chain": site.prefix_chain,
1382 "varmap": site.varmap,
1383 "source_kind": site.source_kind,
1384 "role": site.role,
1385 "checkpoint_load": site.checkpoint_load,
1386 "source": site.source,
1387 "drill_down": [
1388 {"kind": "component", "select": site.component_name},
1389 {"kind": "parameters", "select": site.component_name},
1390 {"kind": "function", "select": site.function_name},
1391 ],
1392 })
1393}
1394
1395fn composition_listing(model: &ModelIr, edge: &crate::model_ir::ArchitectureEdge) -> Value {
1396 let from = model
1397 .components
1398 .iter()
1399 .find(|component| component.id == edge.from);
1400 let to = model
1401 .components
1402 .iter()
1403 .find(|component| component.id == edge.to);
1404 let mut drill_down = Vec::new();
1405 if let Some(component) = from {
1406 drill_down.push(json!({"kind": "component", "select": component.qualified_name}));
1407 drill_down.push(json!({"kind": "modules", "select": component.name}));
1408 }
1409 if let Some(component) = to {
1410 drill_down.push(json!({"kind": "component", "select": component.qualified_name}));
1411 }
1412 json!({
1413 "id": edge.id,
1414 "from": from.map(|component| json!({
1415 "id": component.id,
1416 "name": component.name,
1417 "qualified_name": component.qualified_name,
1418 })).unwrap_or_else(|| json!({"id": edge.from})),
1419 "to": to.map(|component| json!({
1420 "id": component.id,
1421 "name": component.name,
1422 "qualified_name": component.qualified_name,
1423 })).unwrap_or_else(|| json!({"id": edge.to})),
1424 "via_function": edge.via_function,
1425 "source": edge.source,
1426 "confidence": edge.evidence.first().map(|evidence| &evidence.confidence),
1427 "detail": edge.evidence.first().map(|evidence| &evidence.detail),
1428 "drill_down": drill_down,
1429 })
1430}
1431
1432fn composition_matches(
1433 model: &ModelIr,
1434 selector: Option<&str>,
1435 from: StableId,
1436 to: StableId,
1437) -> bool {
1438 let Some(selector) = selector else {
1439 return true;
1440 };
1441 let from_labels = component_labels(model, &from);
1442 let to_labels = component_labels(model, &to);
1443 matches_text(selector, from_labels) || matches_text(selector, to_labels)
1444}
1445
1446fn component_labels<'a>(model: &'a ModelIr, id: &StableId) -> [&'a str; 3] {
1447 if let Some(component) = model
1448 .components
1449 .iter()
1450 .find(|component| component.id == *id)
1451 {
1452 [
1453 component.id.0.as_str(),
1454 component.name.as_str(),
1455 component.qualified_name.as_str(),
1456 ]
1457 } else {
1458 ["", "", ""]
1459 }
1460}