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