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