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