sqry_cli/commands/graph/resolve.rs
1//! `sqry graph resolve <symbol> [--explain] [--json]`
2//!
3//! End-to-end proof point for the Phase 2 binding plane: loads a snapshot
4//! from disk, constructs a `BindingPlane` facade, runs a resolution query,
5//! and prints the outcome. With `--explain` the ordered step trace is rendered
6//! via [`WitnessRendering`].
7//!
8//! # Output contracts
9//!
10//! **Text mode** (default):
11//! ```text
12//! symbol: <name>
13//! outcome: <Debug repr of SymbolResolutionOutcome>
14//! - node_id=<NodeId> classification=<Classification> kind=<NodeKind>
15//! [blank line + "witness trace:" section when --explain is given]
16//! ```
17//!
18//! **JSON mode** (`--json`):
19//! ```json
20//! {
21//! "symbol": "<name>",
22//! "outcome": "<serde outcome>",
23//! "bindings": [
24//! { "node_id": "...", "classification": "...", "kind": "..." }
25//! ],
26//! "explain": null | { "steps": [...], "candidate_count": N, ... }
27//! }
28//! ```
29//! The JSON shape is the stable external contract for scripting consumers.
30//! Changes to key names or value types are a breaking public-API change.
31//!
32//! Spec: `01_SPEC.md#fr9-end-to-end-proof`
33//! Plan: `03_IMPLEMENTATION_PLAN.md` P2U10
34
35use anyhow::{Context, Result};
36use sqry_core::graph::unified::concurrent::GraphSnapshot;
37use sqry_core::graph::unified::resolution::{FileScope, ResolutionMode, SymbolQuery};
38
39/// Runs the `sqry graph resolve` subcommand.
40///
41/// # Arguments
42///
43/// - `snapshot` — immutable MVCC snapshot, already loaded by the caller.
44/// - `symbol` — raw symbol text from the CLI argument.
45/// - `explain` — when `true`, the ordered witness step trace is appended
46/// to the text output (or included as the `explain` field in JSON output).
47/// - `json` — when `true`, all output is emitted as a JSON document whose
48/// shape is the stable external contract documented in the module doc.
49///
50/// # Errors
51///
52/// Returns an error only when JSON serialization fails (an internal invariant
53/// violation — `serde_json` should never fail on our well-typed structs).
54pub fn run(snapshot: &GraphSnapshot, symbol: &str, explain: bool, json: bool) -> Result<()> {
55 let query = SymbolQuery {
56 symbol,
57 file_scope: FileScope::Any,
58 mode: ResolutionMode::AllowSuffixCandidates,
59 };
60
61 let plane = snapshot.binding_plane();
62 let resolution = plane.resolve(&query);
63
64 if json {
65 print_json(&plane, symbol, &resolution, explain)?;
66 } else {
67 print_text(&plane, symbol, &resolution, explain);
68 }
69
70 Ok(())
71}
72
73/// Renders the resolution result as human-readable text.
74fn print_text(
75 plane: &sqry_core::graph::unified::bind::BindingPlane<'_>,
76 symbol: &str,
77 resolution: &sqry_core::graph::unified::bind::BindingResolution,
78 explain: bool,
79) {
80 println!("symbol: {symbol}");
81 println!("outcome: {:?}", resolution.result.outcome);
82 for binding in &resolution.result.bindings {
83 println!(
84 " - node_id={:?} classification={:?} kind={:?}",
85 binding.node_id, binding.classification, binding.kind
86 );
87 }
88 if explain {
89 let rendering = plane.explain(resolution);
90 println!("\nwitness trace:");
91 print!("{}", rendering.text);
92 }
93}
94
95/// Renders the resolution result as a stable JSON document.
96///
97/// # Errors
98///
99/// Returns an error if `serde_json::to_string_pretty` fails (should not occur
100/// for this well-typed document).
101fn print_json(
102 plane: &sqry_core::graph::unified::bind::BindingPlane<'_>,
103 symbol: &str,
104 resolution: &sqry_core::graph::unified::bind::BindingResolution,
105 explain: bool,
106) -> Result<()> {
107 let bindings: Vec<serde_json::Value> = resolution
108 .result
109 .bindings
110 .iter()
111 .map(|b| {
112 serde_json::json!({
113 "node_id": format!("{:?}", b.node_id),
114 "classification": format!("{:?}", b.classification),
115 "kind": format!("{:?}", b.kind),
116 })
117 })
118 .collect();
119
120 let explain_value: serde_json::Value = if explain {
121 let rendering = plane.explain(resolution);
122 rendering.json.clone()
123 } else {
124 serde_json::Value::Null
125 };
126
127 let outcome_value = serde_json::to_value(&resolution.result.outcome)
128 .unwrap_or_else(|_| serde_json::Value::String(format!("{:?}", resolution.result.outcome)));
129
130 let doc = serde_json::json!({
131 "symbol": symbol,
132 "outcome": outcome_value,
133 "bindings": bindings,
134 "explain": explain_value,
135 });
136
137 println!(
138 "{}",
139 serde_json::to_string_pretty(&doc).context("JSON serialization failed")?
140 );
141 Ok(())
142}