Skip to main content

candle_graph/
query.rs

1//! Bounded queries over [`crate::model_ir::ModelIr`].
2//!
3//! Agents should not have to ingest an entire model graph to answer a local question. Queries
4//! therefore return compact, deterministically ordered records and make truncation explicit.
5//! Listing kinds omit tensor contracts and evidence payloads; narrow singular queries (or a
6//! selector on evidence-bearing lists) unlock detail, with `drill_down` hints pointing the way.
7
8use 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    /// Compact agent-oriented rollup for model repair and improvement workflows.
47    ModelImprovement,
48    /// Phase-filtered graph: tensors + operations for training autograd paths.
49    GraphTrain,
50    /// Phase-filtered graph: tensors + operations for inference no-grad paths.
51    GraphInfer,
52    /// Runtime profile rollup: operation and edge timings merged from a v3 trace.
53    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    /// Deterministic start index into the sorted result set; exposed as CLI `--offset`.
104    #[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                        &parameter.id.0,
375                        &parameter.key,
376                        &parameter.builder_root,
377                        &parameter.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                            &parameter.id.0,
412                            &parameter.key,
413                            &parameter.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 &parameter.uses {
1236            connect(&mut adjacency, &parameter.id, use_id);
1237        }
1238        for optimizer in &parameter.optimizer_memberships {
1239            connect(&mut adjacency, optimizer, &parameter.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(&current).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}