Skip to main content

code_moniker_query/
lib.rs

1use std::collections::BTreeMap;
2use std::fmt::{self, Write};
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8mod discovery;
9pub use discovery::*;
10mod bounded;
11pub use bounded::*;
12
13#[cfg(feature = "rpc")]
14pub mod rpc {
15	use jsonrpsee::core::SubscriptionResult;
16	use jsonrpsee::proc_macros::rpc;
17	use jsonrpsee::types::ErrorObjectOwned;
18
19	use crate::{
20		CommandRequest, CommandResponse, HandshakeResponse, QueryRequest, QueryResponse,
21		WorkspaceEventDto,
22	};
23
24	pub const RPC_NAMESPACE: &str = "moniker";
25
26	#[rpc(server, client, namespace = "moniker")]
27	pub trait DaemonRpc {
28		#[method(name = "handshake")]
29		async fn handshake(&self, client: String) -> Result<HandshakeResponse, ErrorObjectOwned>;
30
31		#[method(name = "query")]
32		async fn query(&self, request: QueryRequest) -> Result<QueryResponse, ErrorObjectOwned>;
33
34		#[method(name = "command")]
35		async fn command(
36			&self,
37			request: CommandRequest,
38		) -> Result<CommandResponse, ErrorObjectOwned>;
39
40		#[method(name = "shutdown")]
41		async fn shutdown(&self) -> Result<(), ErrorObjectOwned>;
42
43		#[subscription(name = "subscribeEvents" => "events", unsubscribe = "unsubscribeEvents", item = WorkspaceEventDto)]
44		async fn subscribe_events(&self) -> SubscriptionResult;
45	}
46}
47
48#[cfg(feature = "rpc")]
49pub use rpc::*;
50
51pub const PROTOCOL_VERSION: u32 = 17;
52pub const SYNTAX_TREE_DEFAULT_MAX_DEPTH: usize = 6;
53pub const SYNTAX_TREE_DEFAULT_MAX_NODES: usize = 100;
54pub const SYNTAX_TREE_DEFAULT_MAX_TEXT_CHARS: usize = 80;
55pub const SYNTAX_TREE_MAX_TEXT_CHARS: usize = 1_000;
56pub const SYNTAX_PARSE_MAX_SOURCE_BYTES: usize = 1024 * 1024;
57
58#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[serde(tag = "type", rename_all = "snake_case")]
61pub enum ProtocolRequest {
62	Query(Box<QueryRequest>),
63	Command(CommandRequest),
64}
65
66#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68#[serde(tag = "type", rename_all = "snake_case")]
69pub enum ProtocolResponse {
70	Query(Box<QueryResponse>),
71	Command(CommandResponse),
72	Error(QueryError),
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77pub struct HandshakeResponse {
78	pub protocol_version: u32,
79	pub daemon_version: String,
80	#[serde(default)]
81	pub build: BuildIdentity,
82	pub workspace_root: String,
83	pub workspace_roots: Vec<String>,
84	pub capabilities: CapabilitySet,
85}
86
87#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89pub struct DaemonWorkspaceConfig {
90	pub roots: Vec<String>,
91	pub project: Option<String>,
92	pub cache_dir: Option<String>,
93	pub live_refresh: Option<String>,
94}
95
96#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98pub struct CapabilitySet {
99	pub queries: Vec<String>,
100	#[serde(default)]
101	pub query_mcp_tools: BTreeMap<String, String>,
102	pub commands: Vec<String>,
103	pub events: Vec<String>,
104}
105
106impl Default for CapabilitySet {
107	fn default() -> Self {
108		let mut queries: Vec<String> = query_capability_specs()
109			.iter()
110			.map(|spec| spec.name.to_string())
111			.collect();
112		queries.push("diff-impact.compare".to_string());
113		Self {
114			queries,
115			query_mcp_tools: query_capability_specs()
116				.iter()
117				.map(|spec| (spec.name.to_string(), spec.mcp_tool.to_string()))
118				.collect(),
119			commands: [
120				"workspace.refresh",
121				"workspace.source_set.replace",
122				"workspace.source_set.remove",
123			]
124			.into_iter()
125			.map(str::to_string)
126			.collect(),
127			events: Vec::new(),
128		}
129	}
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub struct QueryCapabilitySpec {
134	pub name: &'static str,
135	pub category: &'static str,
136	pub read_only: bool,
137	pub mcp_tool: &'static str,
138	pub fields: &'static [&'static str],
139	pub required_fields: &'static [&'static str],
140	pub positionals: usize,
141	pub projection: bool,
142	pub paginated: bool,
143	pub example: &'static str,
144}
145
146const COMMON_FIELDS: &[&str] = &["limit", "cursor", "consistency"];
147const BRACKET_LIST_FIELDS: &[&str] = &["lang", "kind", "shape", "severity", "relation"];
148const MULTI_VALUE_FIELDS: &[&str] = &[
149	"path", "lang", "kind", "shape", "severity", "file", "relation",
150];
151
152const QUERY_CAPABILITY_SPECS: &[QueryCapabilitySpec] = &[
153	QueryCapabilitySpec {
154		name: "query.describe",
155		category: "discovery",
156		read_only: true,
157		mcp_tool: "code_moniker_query",
158		fields: &["verb"],
159		required_fields: &[],
160		positionals: 1,
161		projection: false,
162		paginated: false,
163		example: "query.describe verb:\"symbol.usages\"",
164	},
165	QueryCapabilitySpec {
166		name: "workspace.status",
167		category: "workspace",
168		read_only: true,
169		mcp_tool: "code_moniker_read",
170		fields: &[],
171		required_fields: &[],
172		positionals: 0,
173		projection: false,
174		paginated: false,
175		example: "workspace.status",
176	},
177	QueryCapabilitySpec {
178		name: "tree.children",
179		category: "navigation",
180		read_only: true,
181		mcp_tool: "code_moniker_read",
182		fields: &["workspace", "path", "depth", "lang"],
183		required_fields: &[],
184		positionals: 0,
185		projection: true,
186		paginated: true,
187		example: "tree.children path:\"src/**\" depth:2 limit:20",
188	},
189	QueryCapabilitySpec {
190		name: "symbol.search",
191		category: "symbol",
192		read_only: true,
193		mcp_tool: "code_moniker_symbols",
194		fields: &[
195			"workspace",
196			"path",
197			"lang",
198			"kind",
199			"shape",
200			"name",
201			"include_non_navigable",
202			"include_code",
203			"context_lines",
204		],
205		required_fields: &[],
206		positionals: 1,
207		projection: true,
208		paginated: true,
209		example: "symbol.search name:\"PaymentService\" shape:type limit:10",
210	},
211	QueryCapabilitySpec {
212		name: "symbol.insights",
213		category: "symbol",
214		read_only: true,
215		mcp_tool: "code_moniker_symbols",
216		fields: &[
217			"workspace",
218			"path",
219			"lang",
220			"kind",
221			"shape",
222			"name",
223			"include_non_navigable",
224		],
225		required_fields: &[],
226		positionals: 0,
227		projection: true,
228		paginated: false,
229		example: "symbol.insights path:\"src/**\"",
230	},
231	QueryCapabilitySpec {
232		name: "symbol.detail",
233		category: "symbol",
234		read_only: true,
235		mcp_tool: "code_moniker_read",
236		fields: &["workspace", "uri", "context_lines"],
237		required_fields: &["uri"],
238		positionals: 1,
239		projection: false,
240		paginated: false,
241		example: "symbol.detail uri:\"code+moniker://...\" context_lines:2",
242	},
243	QueryCapabilitySpec {
244		name: "syntax.tree",
245		category: "syntax",
246		read_only: true,
247		mcp_tool: "code_moniker_read",
248		fields: &[
249			"workspace",
250			"focus",
251			"max_depth",
252			"max_nodes",
253			"named_only",
254			"include_text",
255			"max_text_chars",
256		],
257		required_fields: &["focus"],
258		positionals: 1,
259		projection: false,
260		paginated: false,
261		example: "syntax.tree focus:\"src/service.ts\" max_depth:6 max_nodes:100",
262	},
263	QueryCapabilitySpec {
264		name: "syntax.parse",
265		category: "syntax",
266		read_only: true,
267		mcp_tool: "code_moniker_read",
268		fields: &[
269			"language",
270			"source",
271			"uri",
272			"max_depth",
273			"max_nodes",
274			"named_only",
275			"include_text",
276			"max_text_chars",
277		],
278		required_fields: &["language", "source"],
279		positionals: 0,
280		projection: false,
281		paginated: false,
282		example: "syntax.parse language:\"rs\" source:\"fn main() {}\"",
283	},
284	QueryCapabilitySpec {
285		name: "symbol.usages",
286		category: "symbol",
287		read_only: true,
288		mcp_tool: "code_moniker_usages",
289		fields: &[
290			"workspace",
291			"uri",
292			"direction",
293			"path",
294			"lang",
295			"include_descendants",
296		],
297		required_fields: &["uri"],
298		positionals: 1,
299		projection: true,
300		paginated: true,
301		example: "symbol.usages uri:\"code+moniker://...\" direction:incoming limit:20",
302	},
303	QueryCapabilitySpec {
304		name: "view.read",
305		category: "context",
306		read_only: true,
307		mcp_tool: "code_moniker_read",
308		fields: &["uri", "scheme", "context_lines", "include_code"],
309		required_fields: &["uri"],
310		positionals: 1,
311		projection: false,
312		paginated: false,
313		example: "view.read uri:\"workspace/views\"",
314	},
315	QueryCapabilitySpec {
316		name: "rules.list",
317		category: "rules",
318		read_only: true,
319		mcp_tool: "code_moniker_rules",
320		fields: &["workspace", "profile", "rules", "lang", "severity"],
321		required_fields: &[],
322		positionals: 0,
323		projection: false,
324		paginated: true,
325		example: "rules.list profile:agent limit:20",
326	},
327	QueryCapabilitySpec {
328		name: "rules.check",
329		category: "rules",
330		read_only: true,
331		mcp_tool: "code_moniker_rules",
332		fields: &["workspace", "profile", "rules", "file", "report"],
333		required_fields: &[],
334		positionals: 0,
335		projection: false,
336		paginated: true,
337		example: "rules.check profile:agent file:\"src/**\" limit:20",
338	},
339	QueryCapabilitySpec {
340		name: "rules.applicable",
341		category: "rules",
342		read_only: true,
343		mcp_tool: "code_moniker_query",
344		fields: &["workspace", "focus", "profile", "rules"],
345		required_fields: &["focus"],
346		positionals: 1,
347		projection: false,
348		paginated: true,
349		example: "rules.applicable focus:\"code+moniker://...\" profile:agent limit:20",
350	},
351	QueryCapabilitySpec {
352		name: "change.review",
353		category: "change",
354		read_only: true,
355		mcp_tool: "code_moniker_diff",
356		fields: &["workspace"],
357		required_fields: &[],
358		positionals: 0,
359		projection: false,
360		paginated: false,
361		example: "change.review",
362	},
363	QueryCapabilitySpec {
364		name: "change.context",
365		category: "change",
366		read_only: true,
367		mcp_tool: "code_moniker_context",
368		fields: &["workspace", "focus", "profile", "max_items"],
369		required_fields: &["focus"],
370		positionals: 1,
371		projection: false,
372		paginated: false,
373		example: "change.context focus:\"code+moniker://...\" profile:agent max_items:20",
374	},
375	QueryCapabilitySpec {
376		name: "symbol.graph",
377		category: "graph",
378		read_only: true,
379		mcp_tool: "code_moniker_graph",
380		fields: &[
381			"workspace",
382			"focus",
383			"direction",
384			"relation",
385			"min_count",
386			"include_internal",
387		],
388		required_fields: &["focus"],
389		positionals: 1,
390		projection: false,
391		paginated: false,
392		example: "symbol.graph focus:\"src/service.ts\"",
393	},
394	QueryCapabilitySpec {
395		name: "graph.path",
396		category: "graph",
397		read_only: true,
398		mcp_tool: "code_moniker_query",
399		fields: &[
400			"workspace",
401			"from",
402			"to",
403			"expect",
404			"relation",
405			"max_depth",
406			"max_symbols",
407			"max_edges",
408			"min_coverage",
409		],
410		required_fields: &["from", "to"],
411		positionals: 0,
412		projection: false,
413		paginated: false,
414		example: "graph.path from:\"code+moniker://...\" to:\"code+moniker://...\" expect:no_path",
415	},
416	QueryCapabilitySpec {
417		name: "identity.children",
418		category: "graph",
419		read_only: true,
420		mcp_tool: "code_moniker_query",
421		fields: &["workspace", "prefix"],
422		required_fields: &[],
423		positionals: 1,
424		projection: false,
425		paginated: false,
426		example: "identity.children prefix:\"lang:rs/dir:crates\"",
427	},
428	QueryCapabilitySpec {
429		name: "identity.graph",
430		category: "graph",
431		read_only: true,
432		mcp_tool: "code_moniker_query",
433		fields: &["workspace", "prefix", "path", "min_count"],
434		required_fields: &[],
435		positionals: 1,
436		projection: false,
437		paginated: true,
438		example: "identity.graph prefix:\"lang:rs/dir:crates\"",
439	},
440	QueryCapabilitySpec {
441		name: "metrics.coupling",
442		category: "metrics",
443		read_only: true,
444		mcp_tool: "code_moniker_query",
445		fields: &["workspace", "from", "to", "relation", "snapshot", "export"],
446		required_fields: &["from", "to"],
447		positionals: 0,
448		projection: false,
449		paginated: false,
450		example: "metrics.coupling from:\"lang:rs/dir:crates/dir:check\" to:\"lang:rs/dir:crates/dir:workspace\"",
451	},
452	QueryCapabilitySpec {
453		name: "resolution.audit",
454		category: "diagnostic",
455		read_only: true,
456		mcp_tool: "code_moniker_query",
457		fields: &["workspace", "prefix", "cluster"],
458		required_fields: &[],
459		positionals: 1,
460		projection: false,
461		paginated: true,
462		example: "resolution.audit prefix:\"lang:java\" limit:20",
463	},
464	QueryCapabilitySpec {
465		name: "notes",
466		category: "notes",
467		read_only: false,
468		mcp_tool: "code_moniker_notes",
469		fields: &[
470			"action",
471			"id",
472			"moniker",
473			"kind",
474			"status",
475			"title",
476			"body",
477			"created_by",
478			"orphan",
479			"include_done",
480		],
481		required_fields: &[],
482		positionals: 0,
483		projection: false,
484		paginated: true,
485		example: "notes action:list limit:20",
486	},
487];
488
489pub fn query_capability_specs() -> &'static [QueryCapabilitySpec] {
490	QUERY_CAPABILITY_SPECS
491}
492
493pub fn query_capability_spec(name: &str) -> Option<&'static QueryCapabilitySpec> {
494	QUERY_CAPABILITY_SPECS.iter().find(|spec| spec.name == name)
495}
496
497pub fn query_projection_fields(name: &str) -> &'static [&'static str] {
498	match name {
499		"tree.children" => &[
500			"root",
501			"path",
502			"kind",
503			"language",
504			"defs",
505			"refs",
506			"change_count",
507		],
508		"symbol.search" => &[
509			"root",
510			"uri",
511			"id",
512			"name",
513			"kind",
514			"visibility",
515			"signature",
516			"file",
517			"language",
518			"line_range",
519			"navigable",
520			"score",
521			"match_reason",
522			"source",
523		],
524		"symbol.insights" => &[
525			"files",
526			"symbols",
527			"references",
528			"navigable_symbols",
529			"non_navigable_symbols",
530			"languages",
531			"kinds",
532			"shapes",
533			"top_files_by_symbols",
534			"top_files_by_refs",
535		],
536		"symbol.usages" => &[
537			"root",
538			"direction",
539			"reference",
540			"kind",
541			"actor",
542			"context",
543			"endpoint",
544			"file",
545			"prefix",
546			"location",
547			"line_range",
548			"via",
549		],
550		_ => &[],
551	}
552}
553
554pub fn describe_query_capabilities(verb: Option<&str>) -> Option<QueryDescribeResult> {
555	let specs: Vec<&QueryCapabilitySpec> = match verb {
556		Some(name) => vec![query_capability_spec(name)?],
557		None => QUERY_CAPABILITY_SPECS.iter().collect(),
558	};
559	Some(QueryDescribeResult {
560		capabilities: specs.into_iter().map(query_capability_dto).collect(),
561	})
562}
563
564fn query_capability_dto(spec: &QueryCapabilitySpec) -> QueryCapabilityDto {
565	let fields = spec
566		.fields
567		.iter()
568		.chain(COMMON_FIELDS)
569		.map(|name| QueryFieldDto {
570			name: (*name).to_string(),
571			value_type: query_field_type(name).to_string(),
572			multiple: MULTI_VALUE_FIELDS.contains(name),
573			required: spec.required_fields.contains(name),
574			default: query_field_default(spec.name, name).map(ToOwned::to_owned),
575		})
576		.collect();
577	QueryCapabilityDto {
578		name: spec.name.to_string(),
579		category: spec.category.to_string(),
580		read_only: spec.read_only,
581		mcp_tool: spec.mcp_tool.to_string(),
582		projection: spec.projection,
583		projection_fields: query_projection_fields(spec.name)
584			.iter()
585			.map(|field| (*field).to_string())
586			.collect(),
587		paginated: spec.paginated,
588		positionals: spec.positionals,
589		fields,
590		example: spec.example.to_string(),
591	}
592}
593
594fn query_field_type(name: &str) -> &'static str {
595	match name {
596		"limit" | "depth" | "context_lines" | "max_items" | "min_count" | "max_depth"
597		| "max_nodes" | "max_text_chars" | "max_symbols" | "max_edges" | "min_coverage" => {
598			"unsigned_integer"
599		}
600		"include_non_navigable"
601		| "include_code"
602		| "include_descendants"
603		| "include_internal"
604		| "include_text"
605		| "named_only"
606		| "report"
607		| "orphan"
608		| "include_done" => "boolean",
609		"direction" => "enum:incoming|outgoing|both",
610		"expect" => "enum:reachable|no_path",
611		"consistency" => "enum:current|refresh-if-stale|stale-ok",
612		"action" => "enum:list|get|create|update|transition|delete",
613		"cursor" => "cursor",
614		name if MULTI_VALUE_FIELDS.contains(&name) => "string_list",
615		_ => "string",
616	}
617}
618
619fn query_field_default(verb: &str, name: &str) -> Option<&'static str> {
620	match (verb, name) {
621		("resolution.audit", "limit") => Some("20"),
622		(_, "limit") => Some("80"),
623		(_, "consistency") => Some("current"),
624		("tree.children", "depth") => Some("1"),
625		("symbol.detail" | "view.read", "context_lines") => Some("2"),
626		("syntax.tree" | "syntax.parse", "max_depth") => Some("6"),
627		("syntax.tree" | "syntax.parse", "max_nodes") => Some("100"),
628		("syntax.tree" | "syntax.parse", "named_only") => Some("true"),
629		("syntax.tree" | "syntax.parse", "include_text") => Some("false"),
630		("syntax.tree" | "syntax.parse", "max_text_chars") => Some("80"),
631		("symbol.search", "context_lines") => Some("0"),
632		("symbol.usages", "direction") => Some("incoming"),
633		("symbol.graph", "direction") => Some("both"),
634		("symbol.graph", "min_count") => Some("1"),
635		("symbol.graph", "include_internal") => Some("true"),
636		("graph.path", "expect") => Some("reachable"),
637		("graph.path", "relation") => Some("calls,method_call"),
638		("graph.path", "max_depth") => Some("12"),
639		("graph.path", "max_symbols") => Some("10000"),
640		("graph.path", "max_edges") => Some("50000"),
641		("graph.path", "min_coverage") => Some("100"),
642		("rules.check", "report") => Some("true"),
643		("change.context", "max_items") => Some("20"),
644		("notes", "action") => Some("list"),
645		(_, "include_non_navigable" | "include_code" | "include_descendants" | "include_done") => {
646			Some("false")
647		}
648		_ => None,
649	}
650}
651
652#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
653#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
654pub struct QueryRequest {
655	pub query: Query,
656	pub consistency: Consistency,
657	pub page: Page,
658}
659
660impl QueryRequest {
661	pub fn new(query: Query) -> Self {
662		Self {
663			query,
664			consistency: Consistency::Current,
665			page: Page::default(),
666		}
667	}
668}
669
670#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
672#[serde(tag = "op", rename_all = "snake_case")]
673pub enum Query {
674	QueryDescribe(QueryDescribeQuery),
675	WorkspaceStatus,
676	TreeChildren(TreeChildrenQuery),
677	SymbolSearch(SymbolSearchQuery),
678	SymbolInsights(SymbolSearchQuery),
679	SymbolDetail(SymbolDetailQuery),
680	SyntaxTree(SyntaxTreeQuery),
681	SyntaxParse(SyntaxParseQuery),
682	SymbolUsages(SymbolUsagesQuery),
683	ViewRead(ViewReadQuery),
684	RulesList(RulesListQuery),
685	RulesCheck(RulesCheckQuery),
686	RulesApplicable(RulesApplicableQuery),
687	ChangeReview(ChangeReviewQuery),
688	DiffImpactCompare(DiffImpactCompareQuery),
689	ChangeContext(ChangeContextQuery),
690	SymbolGraph(SymbolGraphQuery),
691	GraphPath(GraphPathQuery),
692	IdentityChildren(IdentityChildrenQuery),
693	IdentityGraph(IdentityGraphQuery),
694	MetricsCoupling(MetricsCouplingQuery),
695	ResolutionAudit(ResolutionAuditQuery),
696	Notes(NotesQuery),
697}
698
699impl Query {
700	pub fn capability(&self) -> &'static str {
701		match self {
702			Self::QueryDescribe(_) => "query.describe",
703			Self::WorkspaceStatus => "workspace.status",
704			Self::TreeChildren(_) => "tree.children",
705			Self::SymbolSearch(_) => "symbol.search",
706			Self::SymbolInsights(_) => "symbol.insights",
707			Self::SymbolDetail(_) => "symbol.detail",
708			Self::SyntaxTree(_) => "syntax.tree",
709			Self::SyntaxParse(_) => "syntax.parse",
710			Self::SymbolUsages(_) => "symbol.usages",
711			Self::ViewRead(_) => "view.read",
712			Self::RulesList(_) => "rules.list",
713			Self::RulesCheck(_) => "rules.check",
714			Self::RulesApplicable(_) => "rules.applicable",
715			Self::ChangeReview(_) => "change.review",
716			Self::DiffImpactCompare(_) => "diff-impact.compare",
717			Self::ChangeContext(_) => "change.context",
718			Self::SymbolGraph(_) => "symbol.graph",
719			Self::GraphPath(_) => "graph.path",
720			Self::IdentityChildren(_) => "identity.children",
721			Self::IdentityGraph(_) => "identity.graph",
722			Self::MetricsCoupling(_) => "metrics.coupling",
723			Self::ResolutionAudit(_) => "resolution.audit",
724			Self::Notes(_) => "notes",
725		}
726	}
727
728	pub fn requires_workspace_snapshot(&self) -> bool {
729		!matches!(
730			self,
731			Self::QueryDescribe(_)
732				| Self::WorkspaceStatus
733				| Self::SyntaxParse(_)
734				| Self::DiffImpactCompare(_)
735		)
736	}
737}
738
739pub fn query_projection(query: &Query) -> &[String] {
740	match query {
741		Query::TreeChildren(query) => &query.projection,
742		Query::SymbolSearch(query) | Query::SymbolInsights(query) => &query.projection,
743		Query::SymbolUsages(query) => &query.projection,
744		_ => &[],
745	}
746}
747
748#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
749#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
750pub struct QueryDescribeQuery {
751	pub verb: Option<String>,
752}
753
754#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
755#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
756pub struct QueryCapabilityDto {
757	pub name: String,
758	pub category: String,
759	pub read_only: bool,
760	pub mcp_tool: String,
761	pub projection: bool,
762	pub projection_fields: Vec<String>,
763	pub paginated: bool,
764	pub positionals: usize,
765	pub fields: Vec<QueryFieldDto>,
766	pub example: String,
767}
768
769#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
770#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
771pub struct QueryFieldDto {
772	pub name: String,
773	pub value_type: String,
774	pub multiple: bool,
775	pub required: bool,
776	pub default: Option<String>,
777}
778
779#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
780#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
781pub struct QueryDescribeResult {
782	pub capabilities: Vec<QueryCapabilityDto>,
783}
784
785#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
786#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
787pub struct TreeChildrenQuery {
788	pub workspace: Option<String>,
789	pub path: Vec<String>,
790	pub depth: usize,
791	pub lang: Vec<String>,
792	pub projection: Vec<String>,
793}
794
795#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
796#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
797pub struct SymbolSearchQuery {
798	pub workspace: Option<String>,
799	pub text: Option<String>,
800	pub path: Vec<String>,
801	pub lang: Vec<String>,
802	pub kind: Vec<String>,
803	pub shape: Vec<String>,
804	pub name: Option<String>,
805	pub include_non_navigable: bool,
806	pub include_code: bool,
807	pub context_lines: usize,
808	pub projection: Vec<String>,
809}
810
811#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
812#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
813pub struct SymbolDetailQuery {
814	pub workspace: Option<String>,
815	pub uri: String,
816	pub context_lines: usize,
817}
818
819#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
820#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
821pub struct SyntaxTreeQuery {
822	pub workspace: Option<String>,
823	pub focus: String,
824	#[cfg_attr(feature = "schema", schemars(range(min = 0)))]
825	pub max_depth: usize,
826	#[cfg_attr(feature = "schema", schemars(range(min = 1)))]
827	pub max_nodes: usize,
828	pub named_only: bool,
829	pub include_text: bool,
830	#[cfg_attr(feature = "schema", schemars(range(min = 0, max = 1000)))]
831	pub max_text_chars: usize,
832}
833
834#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
835#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
836pub struct SyntaxParseQuery {
837	pub language: String,
838	pub source: String,
839	pub uri: Option<String>,
840	#[cfg_attr(feature = "schema", schemars(range(min = 0)))]
841	pub max_depth: usize,
842	#[cfg_attr(feature = "schema", schemars(range(min = 1)))]
843	pub max_nodes: usize,
844	pub named_only: bool,
845	pub include_text: bool,
846	#[cfg_attr(feature = "schema", schemars(range(min = 0, max = 1000)))]
847	pub max_text_chars: usize,
848}
849
850#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
851#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
852pub struct SymbolUsagesQuery {
853	pub workspace: Option<String>,
854	pub uri: String,
855	pub direction: UsageDirection,
856	pub path: Vec<String>,
857	pub lang: Vec<String>,
858	pub include_descendants: bool,
859	pub projection: Vec<String>,
860}
861
862#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
863#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
864pub struct ViewReadQuery {
865	pub uri: String,
866	pub scheme: Option<String>,
867	pub context_lines: usize,
868	pub include_code: bool,
869}
870
871#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
872#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
873pub struct ResolutionAuditQuery {
874	pub workspace: Option<String>,
875	pub prefix: String,
876	pub limit: usize,
877	pub cluster: Option<String>,
878}
879
880#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
881#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
882#[serde(rename_all = "snake_case")]
883pub enum UsageDirection {
884	#[default]
885	Incoming,
886	Outgoing,
887	Both,
888}
889
890impl UsageDirection {
891	pub fn as_str(self) -> &'static str {
892		match self {
893			Self::Incoming => "incoming",
894			Self::Outgoing => "outgoing",
895			Self::Both => "both",
896		}
897	}
898}
899
900impl FromStr for UsageDirection {
901	type Err = QueryParseError;
902
903	fn from_str(value: &str) -> Result<Self, Self::Err> {
904		match value {
905			"incoming" => Ok(Self::Incoming),
906			"outgoing" => Ok(Self::Outgoing),
907			"both" => Ok(Self::Both),
908			_ => Err(QueryParseError::InvalidValue {
909				key: "direction".to_string(),
910				value: value.to_string(),
911			}),
912		}
913	}
914}
915
916#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
917#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
918pub struct RulesListQuery {
919	pub workspace: Option<String>,
920	pub profile: Option<String>,
921	pub rules: Option<String>,
922	pub lang: Vec<String>,
923	pub severity: Vec<String>,
924}
925
926#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
927#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
928pub struct RulesCheckQuery {
929	pub workspace: Option<String>,
930	pub profile: Option<String>,
931	pub rules: Option<String>,
932	pub file: Vec<String>,
933	pub report: bool,
934}
935
936#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
937#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
938pub struct RulesApplicableQuery {
939	pub workspace: Option<String>,
940	pub focus: String,
941	pub profile: Option<String>,
942	pub rules: Option<String>,
943}
944
945#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
946#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
947pub struct ChangeReviewQuery {
948	pub workspace: Option<String>,
949}
950
951#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
952#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
953pub struct DiffImpactCompareQuery {
954	pub scope: String,
955	pub project: Option<String>,
956	pub base: WorkspaceSourceSetDto,
957	pub head: WorkspaceSourceSetDto,
958	pub files: Vec<DiffImpactCompareFile>,
959}
960
961#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
962#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
963pub struct DiffImpactCompareFile {
964	pub status: DiffImpactFileStatus,
965	pub old_uri: Option<String>,
966	pub new_uri: Option<String>,
967	#[serde(default)]
968	pub old_hunks: Vec<DiffImpactLineSpan>,
969	#[serde(default)]
970	pub new_hunks: Vec<DiffImpactLineSpan>,
971	pub rename_score: Option<u8>,
972}
973
974#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
975#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
976#[serde(rename_all = "snake_case")]
977pub enum DiffImpactFileStatus {
978	Added,
979	Modified,
980	Deleted,
981	Renamed,
982}
983
984#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
985#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
986pub struct DiffImpactLineSpan {
987	pub start: u32,
988	pub end: u32,
989}
990
991#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
992#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
993pub struct ChangeContextQuery {
994	pub workspace: Option<String>,
995	pub focus: String,
996	pub profile: Option<String>,
997	pub max_items: usize,
998}
999
1000#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1001#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1002pub struct SymbolGraphQuery {
1003	pub workspace: Option<String>,
1004	pub focus: String,
1005	pub direction: UsageDirection,
1006	pub relation: Vec<String>,
1007	pub min_count: usize,
1008	pub include_internal: bool,
1009}
1010
1011impl Default for SymbolGraphQuery {
1012	fn default() -> Self {
1013		Self {
1014			workspace: None,
1015			focus: String::new(),
1016			direction: UsageDirection::Both,
1017			relation: Vec::new(),
1018			min_count: 1,
1019			include_internal: true,
1020		}
1021	}
1022}
1023
1024#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1025#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1026pub struct GraphPathQuery {
1027	pub workspace: Option<String>,
1028	pub from: String,
1029	pub to: String,
1030	pub expect: GraphPathExpectation,
1031	pub relation: Vec<String>,
1032	pub max_depth: usize,
1033	pub max_symbols: usize,
1034	pub max_edges: usize,
1035	pub min_coverage: usize,
1036}
1037
1038impl Default for GraphPathQuery {
1039	fn default() -> Self {
1040		Self {
1041			workspace: None,
1042			from: String::new(),
1043			to: String::new(),
1044			expect: GraphPathExpectation::Reachable,
1045			relation: vec!["calls".to_string(), "method_call".to_string()],
1046			max_depth: 12,
1047			max_symbols: 10_000,
1048			max_edges: 50_000,
1049			min_coverage: 100,
1050		}
1051	}
1052}
1053
1054#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1055#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1056#[serde(rename_all = "snake_case")]
1057pub enum GraphPathExpectation {
1058	#[default]
1059	Reachable,
1060	NoPath,
1061}
1062
1063impl GraphPathExpectation {
1064	pub fn as_str(self) -> &'static str {
1065		match self {
1066			Self::Reachable => "reachable",
1067			Self::NoPath => "no_path",
1068		}
1069	}
1070}
1071
1072impl FromStr for GraphPathExpectation {
1073	type Err = QueryParseError;
1074
1075	fn from_str(value: &str) -> Result<Self, Self::Err> {
1076		match value {
1077			"reachable" => Ok(Self::Reachable),
1078			"no_path" => Ok(Self::NoPath),
1079			_ => Err(QueryParseError::InvalidValue {
1080				key: "expect".to_string(),
1081				value: value.to_string(),
1082			}),
1083		}
1084	}
1085}
1086
1087// One level of the identity tree: children of a moniker identity prefix
1088// (`""` = the workspace root). The symbolic navigation surface - no
1089// filesystem involved.
1090#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1091#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1092pub struct IdentityChildrenQuery {
1093	pub workspace: Option<String>,
1094	pub prefix: String,
1095}
1096
1097#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1098#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1099pub struct IdentityGraphQuery {
1100	pub workspace: Option<String>,
1101	pub prefix: String,
1102	pub path: Vec<String>,
1103	pub min_count: usize,
1104}
1105
1106#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1107#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1108pub struct MetricsCouplingQuery {
1109	pub workspace: Option<String>,
1110	pub from: String,
1111	pub to: String,
1112	pub relation: Vec<String>,
1113	pub snapshot: Option<String>,
1114	pub export: bool,
1115}
1116
1117impl Default for IdentityGraphQuery {
1118	fn default() -> Self {
1119		Self {
1120			workspace: None,
1121			prefix: String::new(),
1122			path: Vec::new(),
1123			min_count: 1,
1124		}
1125	}
1126}
1127
1128#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1130pub struct NotesQuery {
1131	pub action: NotesAction,
1132	pub id: Option<String>,
1133	pub moniker: Option<String>,
1134	pub kind: Option<String>,
1135	pub status: Option<String>,
1136	pub title: Option<String>,
1137	pub body: Option<String>,
1138	pub created_by: Option<String>,
1139	pub orphan: Option<bool>,
1140	pub include_done: bool,
1141}
1142
1143#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1145#[serde(rename_all = "snake_case")]
1146pub enum NotesAction {
1147	#[default]
1148	List,
1149	Get,
1150	Create,
1151	Update,
1152	Transition,
1153	Delete,
1154}
1155
1156#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1158pub struct CommandRequest {
1159	pub command: Command,
1160}
1161
1162#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1164pub struct WorkspaceSourceSetDto {
1165	pub srcset: String,
1166	pub revision: Option<String>,
1167	pub documents: Vec<WorkspaceSourceDocumentDto>,
1168}
1169
1170#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1172pub struct WorkspaceSourceDocumentDto {
1173	pub uri: String,
1174	pub language: String,
1175	pub content: String,
1176}
1177
1178#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1179#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1180#[serde(tag = "op", rename_all = "snake_case")]
1181pub enum Command {
1182	WorkspaceRefresh,
1183	WorkspaceSourceSetReplace { source_set: WorkspaceSourceSetDto },
1184	WorkspaceSourceSetRemove { srcset: String },
1185}
1186
1187#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1189#[serde(rename_all = "snake_case")]
1190pub enum Consistency {
1191	#[default]
1192	Current,
1193	RefreshIfStale,
1194	StaleOk,
1195}
1196
1197impl FromStr for Consistency {
1198	type Err = QueryParseError;
1199
1200	fn from_str(value: &str) -> Result<Self, Self::Err> {
1201		match value {
1202			"current" => Ok(Self::Current),
1203			"refresh-if-stale" => Ok(Self::RefreshIfStale),
1204			"stale-ok" => Ok(Self::StaleOk),
1205			_ => Err(QueryParseError::InvalidValue {
1206				key: "consistency".to_string(),
1207				value: value.to_string(),
1208			}),
1209		}
1210	}
1211}
1212
1213#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1214#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1215pub struct Page {
1216	pub cursor: Option<QueryCursor>,
1217	pub limit: usize,
1218}
1219
1220impl Default for Page {
1221	fn default() -> Self {
1222		Self {
1223			cursor: None,
1224			limit: 80,
1225		}
1226	}
1227}
1228
1229#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1231pub struct QueryCursor {
1232	pub offset: usize,
1233	pub generation: Option<WorkspaceGeneration>,
1234}
1235
1236impl QueryCursor {
1237	pub fn new(offset: usize, generation: Option<WorkspaceGeneration>) -> Self {
1238		Self { offset, generation }
1239	}
1240}
1241
1242#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1244pub struct WorkspaceGeneration(pub u64);
1245
1246/// A workspace change pushed to attached clients over a daemon subscription.
1247#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1249pub struct WorkspaceEventDto {
1250	pub kind: WorkspaceEventKind,
1251	pub generation: Option<WorkspaceGeneration>,
1252	pub stale_summary: Option<String>,
1253}
1254
1255#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1257#[serde(rename_all = "snake_case")]
1258pub enum WorkspaceEventKind {
1259	Stale,
1260	Refreshed,
1261	Failed,
1262	Notes,
1263	GitBase,
1264}
1265
1266#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1267#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1268pub struct QueryResponse {
1269	pub generation: Option<WorkspaceGeneration>,
1270	pub result: QueryResult,
1271	pub next_cursor: Option<QueryCursor>,
1272}
1273
1274#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1275#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1276#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
1277pub enum QueryResult {
1278	QueryDescribe(QueryDescribeResult),
1279	WorkspaceStatus(WorkspaceStatus),
1280	TreeChildren(TreeChildrenResult),
1281	SymbolList(SymbolListResult),
1282	SymbolInsights(SymbolInsightsResult),
1283	SymbolDetail(SymbolDetailResult),
1284	SyntaxTree(SyntaxTreeResult),
1285	SymbolUsages(Box<SymbolUsagesResult>),
1286	ViewRead(ViewReadResult),
1287	RulesList(RulesListResult),
1288	RulesCheck(RulesCheckResult),
1289	RulesApplicable(Box<RulesApplicableResult>),
1290	ChangeReview(Box<ChangeReviewResult>),
1291	DiffImpact(Box<DiffImpactResult>),
1292	ChangeContext(Box<ChangeContextResult>),
1293	SymbolGraph(Box<SymbolGraphResult>),
1294	GraphPath(Box<GraphPathResult>),
1295	IdentityChildren(IdentityChildrenResult),
1296	IdentityGraph(Box<IdentityGraphResult>),
1297	MetricsCoupling(Box<MetricsCouplingResult>),
1298	ResolutionAudit(Box<ResolutionAuditResult>),
1299	Notes(NotesResult),
1300}
1301
1302// Refs without a unique in-workspace target, decomposed so explained decisions
1303// never masquerade as resolution gaps. Candidate and dynamic references remain
1304// outside the graph while preserving their honest classification.
1305#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1306#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1307pub struct UnlinkedRefsDto {
1308	pub external: usize,
1309	pub sdk: usize,
1310	pub dependency: usize,
1311	pub injected_external: usize,
1312	pub unknown_external: usize,
1313	pub candidate: usize,
1314	pub dynamic: usize,
1315	pub manifest_blocked: usize,
1316	pub unresolved: usize,
1317	pub unresolved_reasons: BTreeMap<String, usize>,
1318}
1319
1320#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1321#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1322pub struct SymbolGraphResult {
1323	pub focus: SymbolGraphFocus,
1324	pub coverage: SymbolGraphCoverage,
1325	pub members: Vec<SymbolDto>,
1326	pub internal_edges: Vec<SymbolGraphEdge>,
1327	pub callers: Vec<SymbolGraphNeighbor>,
1328	pub callees: Vec<SymbolGraphNeighbor>,
1329	pub unlinked: UnlinkedRefsDto,
1330}
1331
1332#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1333#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1334pub struct SymbolGraphCoverage {
1335	pub members: GraphSectionCoverage,
1336	pub internal_edges: GraphSectionCoverage,
1337	pub callers: GraphSectionCoverage,
1338	pub callees: GraphSectionCoverage,
1339}
1340
1341#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1343pub struct GraphSectionCoverage {
1344	pub total: usize,
1345	pub matching: usize,
1346	pub returned: usize,
1347}
1348
1349#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1350#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1351#[serde(tag = "kind", rename_all = "snake_case")]
1352pub enum SymbolGraphFocus {
1353	Symbol { symbol: Box<SymbolDto> },
1354	File { path: String },
1355}
1356
1357#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1358#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1359pub struct SymbolGraphNeighbor {
1360	pub symbol: SymbolDto,
1361	pub kinds: Vec<String>,
1362	pub count: usize,
1363}
1364
1365#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1366#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1367pub struct SymbolGraphEdge {
1368	pub source: String,
1369	pub target: String,
1370	pub kinds: Vec<String>,
1371	pub count: usize,
1372}
1373
1374#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1375#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1376pub struct GraphPathResult {
1377	pub from: SymbolDto,
1378	pub to: SymbolDto,
1379	pub expectation: GraphPathExpectation,
1380	pub verdict: GraphPathVerdict,
1381	pub reachable: Option<bool>,
1382	pub no_path: Option<bool>,
1383	pub path: Vec<GraphPathStep>,
1384	pub coverage: GraphPathCoverage,
1385	pub search: GraphPathSearchStats,
1386	pub reasons: Vec<String>,
1387}
1388
1389#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1390#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1391#[serde(rename_all = "snake_case")]
1392pub enum GraphPathVerdict {
1393	Pass,
1394	Fail,
1395	Inconclusive,
1396}
1397
1398impl GraphPathVerdict {
1399	pub fn as_str(self) -> &'static str {
1400		match self {
1401			Self::Pass => "pass",
1402			Self::Fail => "fail",
1403			Self::Inconclusive => "inconclusive",
1404		}
1405	}
1406}
1407
1408#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1409#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1410pub struct GraphPathStep {
1411	pub source: SymbolDto,
1412	pub target: SymbolDto,
1413	pub relation: String,
1414	pub reference: String,
1415	pub file: String,
1416	pub line_range: Option<(u32, u32)>,
1417}
1418
1419#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1420#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1421pub struct GraphPathCoverage {
1422	pub total: usize,
1423	pub decided: usize,
1424	pub resolved: usize,
1425	pub external: usize,
1426	pub candidate: usize,
1427	pub dynamic: usize,
1428	pub manifest_blocked: usize,
1429	pub unresolved: usize,
1430	pub percent: usize,
1431	pub gap_reasons: BTreeMap<String, usize>,
1432}
1433
1434#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1435#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1436pub struct GraphPathSearchStats {
1437	pub max_depth: usize,
1438	pub depth_reached: usize,
1439	pub explored_symbols: usize,
1440	pub explored_edges: usize,
1441	pub depth_limit_reached: bool,
1442	pub symbol_limit_reached: bool,
1443	pub edge_limit_reached: bool,
1444}
1445
1446#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1447#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1448pub struct IdentityChildrenResult {
1449	pub prefix: String,
1450	pub children: Vec<IdentitySegmentDto>,
1451}
1452
1453// One child segment under the requested prefix. `symbol` is attached when the
1454// segment itself is a navigable definition; organizational segments (package,
1455// dir, srcset, lang, module wrappers) only aggregate what lives below.
1456#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1457#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1458pub struct IdentitySegmentDto {
1459	pub segment: String,
1460	pub kind: String,
1461	pub name: String,
1462	pub identity: String,
1463	pub defs: usize,
1464	pub has_children: bool,
1465	pub symbol: Option<Box<SymbolDto>>,
1466}
1467
1468// The embedded resolution audit partitions every reference by decision class
1469// and clusters candidates, dynamic references, and unresolved references under
1470// mechanical pattern keys. Stable cluster ids support paginated drill-downs.
1471#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1472#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1473pub struct ResolutionAuditResult {
1474	pub prefix: String,
1475	pub totals: AuditTotalsDto,
1476	pub clusters: Vec<AuditClusterDto>,
1477	pub zones: Vec<AuditZoneDto>,
1478}
1479
1480#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1481#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1482pub struct AuditTotalsDto {
1483	pub references: usize,
1484	pub resolved: usize,
1485	pub unique: usize,
1486	pub candidate: usize,
1487	pub external: usize,
1488	pub sdk: usize,
1489	pub dependency: usize,
1490	pub injected_external: usize,
1491	pub unknown_external: usize,
1492	pub dynamic: usize,
1493	pub blocked: usize,
1494	pub unresolved: usize,
1495	pub explained: usize,
1496	pub weak_or_unexplained: usize,
1497	pub name_match_resolved: usize,
1498	pub name_match_candidate: usize,
1499}
1500
1501#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1502#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1503pub struct AuditClusterDto {
1504	pub id: String,
1505	pub pattern: String,
1506	pub count: usize,
1507	pub samples: Vec<AuditSampleDto>,
1508}
1509
1510#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1511#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1512pub struct AuditSampleDto {
1513	pub file: String,
1514	pub line_range: Option<(u32, u32)>,
1515	pub snippet: String,
1516	pub source: String,
1517	pub call_name: String,
1518	pub receiver: String,
1519	pub target: String,
1520	pub evidence: String,
1521	pub constraints: Vec<String>,
1522	pub candidates: Vec<String>,
1523}
1524
1525#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1526#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1527pub struct AuditZoneDto {
1528	pub zone: String,
1529	pub unresolved: usize,
1530	pub dominant_pattern: String,
1531}
1532
1533// The scoped exploration graph: one level of the identity tree projected as
1534// a graph. Nodes are the prefix's children; edges are resolved references
1535// rolled up to the pair of child segments they connect; ports aggregate what
1536// crosses the scope boundary.
1537#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1538#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1539pub struct IdentityGraphResult {
1540	pub prefix: String,
1541	pub path: Vec<String>,
1542	pub min_count: usize,
1543	pub coverage: IdentityGraphCoverage,
1544	pub nodes: Vec<IdentitySegmentDto>,
1545	pub edges: Vec<IdentityGraphEdge>,
1546	pub ports_in: Vec<IdentityGraphPort>,
1547	pub ports_out: Vec<IdentityGraphPort>,
1548	pub unlinked: UnlinkedRefsDto,
1549}
1550
1551#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1553pub struct IdentityGraphCoverage {
1554	pub rows_total: usize,
1555	pub rows_matching: usize,
1556	pub rows_emitted: usize,
1557	pub nodes_total: usize,
1558	pub nodes_emitted: usize,
1559	pub edges_total: usize,
1560	pub edges_matching: usize,
1561	pub edges_emitted: usize,
1562	pub ports_in_total: usize,
1563	pub ports_in_matching: usize,
1564	pub ports_in_emitted: usize,
1565	pub ports_out_total: usize,
1566	pub ports_out_matching: usize,
1567	pub ports_out_emitted: usize,
1568}
1569
1570// source/target are child segment identities of the requested prefix.
1571#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1572#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1573pub struct IdentityGraphEdge {
1574	pub source: String,
1575	pub target: String,
1576	pub kinds: Vec<String>,
1577	pub count: usize,
1578}
1579
1580// Aggregated boundary crossing: `identity` is the nearest out-of-scope
1581// segment (rolled up to the scope's own depth in the identity tree).
1582#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1583#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1584pub struct IdentityGraphPort {
1585	pub identity: String,
1586	pub kinds: Vec<String>,
1587	pub count: usize,
1588}
1589
1590#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1592pub struct MetricsCouplingResult {
1593	pub from: String,
1594	pub to: String,
1595	pub relation: Vec<String>,
1596	pub snapshot: String,
1597	pub git: Option<GitRevisionDto>,
1598	pub export_requested: bool,
1599	pub export_recorded: bool,
1600	pub references: usize,
1601	pub connections: usize,
1602	pub source_symbols: usize,
1603	pub target_symbols: usize,
1604	pub same_symbol_references: usize,
1605	pub coverage: MetricsCouplingCoverage,
1606	pub by_kind: Vec<CountDto>,
1607	pub by_target: Vec<MetricsCouplingTargetUsage>,
1608	pub unlinked: UnlinkedRefsDto,
1609}
1610
1611#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1612#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1613pub struct MetricsCouplingTargetUsage {
1614	pub moniker: String,
1615	pub references: usize,
1616}
1617
1618#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1619#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1620pub struct GitRevisionDto {
1621	pub branch: String,
1622	pub commit: String,
1623	pub dirty: bool,
1624}
1625
1626#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1627#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1628pub struct MetricsCouplingCoverage {
1629	pub source_references: usize,
1630	pub resolved_source_references: usize,
1631}
1632
1633#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1634#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1635pub struct ChangeReviewResult {
1636	pub scope: String,
1637	pub summary: ChangeReviewSummary,
1638	pub files: Vec<ChangeReviewFile>,
1639	pub symbol_changes: Vec<ChangeReviewSymbol>,
1640	pub ref_changes: Vec<ChangeReviewRef>,
1641	pub diagnostics: Vec<String>,
1642}
1643
1644#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1645#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1646pub struct ChangeReviewSummary {
1647	pub files: usize,
1648	pub analyzable_files: usize,
1649	pub symbol_changes: usize,
1650	pub ref_changes: usize,
1651	pub retargeted_refs: usize,
1652	pub residual_files: usize,
1653}
1654
1655#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1656#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1657pub struct ChangeReviewFile {
1658	pub old_path: Option<String>,
1659	pub new_path: Option<String>,
1660	pub disposition: String,
1661	pub analyzable: bool,
1662	pub symbol_changes: usize,
1663	pub moved_symbols: usize,
1664	pub coverage_explained: bool,
1665	pub old_residual: Vec<(u32, u32)>,
1666	pub new_residual: Vec<(u32, u32)>,
1667	pub test_artifact: bool,
1668}
1669
1670#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1672pub struct ChangeReviewSymbol {
1673	pub kind: String,
1674	pub confidence: String,
1675	pub body_changed: bool,
1676	pub signature_changed: bool,
1677	pub visibility_changed: bool,
1678	pub header_changed: bool,
1679	pub file_moved: bool,
1680	pub old: Option<ChangeReviewSide>,
1681	pub new: Option<ChangeReviewSide>,
1682}
1683
1684#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1685#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1686pub struct ChangeReviewSide {
1687	pub identity: String,
1688	pub file: String,
1689	pub kind: String,
1690	pub name: String,
1691	pub visibility: String,
1692	pub lines: Option<(u32, u32)>,
1693	pub test_artifact: bool,
1694}
1695
1696#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1697#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1698pub struct ChangeReviewRef {
1699	pub kind: String,
1700	pub file: String,
1701	pub ref_kind: String,
1702	pub old_target: Option<String>,
1703	pub new_target: Option<String>,
1704	pub old_lines: Option<(u32, u32)>,
1705	pub new_lines: Option<(u32, u32)>,
1706}
1707
1708#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1709#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1710pub struct DiffImpactResult {
1711	pub scope: String,
1712	pub summary: DiffImpactSummary,
1713	pub files: Vec<DiffImpactFile>,
1714	pub symbol_changes: Vec<DiffImpactSymbol>,
1715	pub ref_changes: Vec<DiffImpactRef>,
1716	pub diagnostics: Vec<String>,
1717}
1718
1719#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1720#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1721pub struct DiffImpactSummary {
1722	pub files: usize,
1723	pub analyzable_files: usize,
1724	pub symbol_changes: usize,
1725	pub ref_changes: usize,
1726	pub retargeted_refs: usize,
1727	pub residual_files: usize,
1728}
1729
1730#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1731#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1732pub struct DiffImpactFile {
1733	pub old_path: Option<String>,
1734	pub new_path: Option<String>,
1735	pub disposition: String,
1736	pub analyzable: bool,
1737	pub symbol_changes: usize,
1738	pub moved_symbols: usize,
1739	pub coverage_explained: bool,
1740	pub old_residual: Vec<(u32, u32)>,
1741	pub new_residual: Vec<(u32, u32)>,
1742	pub test_artifact: bool,
1743}
1744
1745#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1746#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1747pub struct DiffImpactSymbol {
1748	pub kind: String,
1749	pub confidence: String,
1750	pub body_changed: bool,
1751	pub signature_changed: bool,
1752	pub visibility_changed: bool,
1753	pub header_changed: bool,
1754	pub file_moved: bool,
1755	pub old: Option<DiffImpactSide>,
1756	pub new: Option<DiffImpactSide>,
1757}
1758
1759#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1760#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1761pub struct DiffImpactSide {
1762	pub identity: String,
1763	pub compact_identity: String,
1764	pub file: String,
1765	pub kind: String,
1766	pub name: String,
1767	pub visibility: String,
1768	pub lines: Option<(u32, u32)>,
1769	pub test_artifact: bool,
1770}
1771
1772#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1773#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1774pub struct DiffImpactRef {
1775	pub kind: String,
1776	pub file: String,
1777	pub ref_kind: String,
1778	pub old_target: Option<String>,
1779	pub new_target: Option<String>,
1780	pub old_target_compact: Option<String>,
1781	pub new_target_compact: Option<String>,
1782	pub old_lines: Option<(u32, u32)>,
1783	pub new_lines: Option<(u32, u32)>,
1784}
1785
1786#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1787#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1788pub struct CommandResponse {
1789	pub generation: Option<WorkspaceGeneration>,
1790	pub message: String,
1791	pub status: Option<Box<WorkspaceStatus>>,
1792}
1793
1794#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1795#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1796#[serde(tag = "kind", rename_all = "snake_case")]
1797pub enum ViewReadResult {
1798	List(ViewListResult),
1799	Detail(Box<ViewDetailResult>),
1800}
1801
1802#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1803#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1804pub struct ViewListResult {
1805	pub views: Vec<ViewSummaryDto>,
1806}
1807
1808#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1809#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1810pub struct ViewSummaryDto {
1811	pub id: String,
1812	pub title: Option<String>,
1813	pub fragment: String,
1814	pub anchor: String,
1815	pub scope: String,
1816}
1817
1818#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1819#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1820pub struct ViewDetailResult {
1821	pub id: String,
1822	pub title: Option<String>,
1823	pub fragment: String,
1824	pub anchor: String,
1825	pub scope: String,
1826	pub intent: Option<String>,
1827	pub summary: Option<String>,
1828	pub rules: Vec<ViewRuleDto>,
1829	pub boundaries: Vec<ViewBoundaryDto>,
1830	pub gotchas: Vec<ViewGotchaDto>,
1831}
1832
1833#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1834#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1835pub struct ViewRuleDto {
1836	pub id: String,
1837	pub severity: String,
1838	pub domain: String,
1839	pub rationale: Option<String>,
1840}
1841
1842#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1843#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1844pub struct ViewRuleRefDto {
1845	pub id: String,
1846	pub present: bool,
1847}
1848
1849#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1850#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1851pub struct ViewBoundaryDto {
1852	pub id: String,
1853	pub owns: Vec<String>,
1854	pub forbids: Vec<String>,
1855	pub forbid_rules: Vec<String>,
1856	pub rationale: Option<String>,
1857	pub rule_refs: Vec<ViewRuleRefDto>,
1858	pub evidence: Vec<ViewEvidenceDto>,
1859	pub missing: Vec<String>,
1860}
1861
1862#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1863#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1864pub struct ViewGotchaDto {
1865	pub id: String,
1866	pub rationale: String,
1867	pub check: Option<String>,
1868	pub rule_refs: Vec<ViewRuleRefDto>,
1869	pub evidence: Vec<ViewEvidenceDto>,
1870	pub missing: Vec<String>,
1871}
1872
1873#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1874#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1875pub struct ViewEvidenceDto {
1876	pub selector: String,
1877	pub label: String,
1878	pub moniker: String,
1879	pub file: String,
1880	pub slice: Option<(u32, u32)>,
1881	pub active_slice: Option<(u32, u32)>,
1882	pub code: Vec<SourceLine>,
1883}
1884
1885#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1886#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1887pub struct WorkspaceStatus {
1888	#[serde(default)]
1889	pub producer: BuildIdentity,
1890	pub root: String,
1891	pub phase: WorkspacePhase,
1892	#[serde(default)]
1893	pub failure: Option<WorkspaceFailureDto>,
1894	pub roots: Vec<WorkspaceRootStatus>,
1895	pub generation: Option<WorkspaceGeneration>,
1896	pub files: usize,
1897	pub symbols: usize,
1898	pub references: usize,
1899	pub stale: bool,
1900	pub stale_summary: String,
1901	#[serde(default)]
1902	pub timings: WorkspaceTimingsDto,
1903}
1904
1905#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1906#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1907pub struct WorkspaceLifecycle {
1908	pub phase: WorkspacePhase,
1909	#[serde(default)]
1910	pub failure: Option<WorkspaceFailureDto>,
1911}
1912
1913impl WorkspaceLifecycle {
1914	pub fn loading() -> Self {
1915		Self {
1916			phase: WorkspacePhase::Loading,
1917			failure: None,
1918		}
1919	}
1920
1921	pub fn ready() -> Self {
1922		Self {
1923			phase: WorkspacePhase::Ready,
1924			failure: None,
1925		}
1926	}
1927
1928	pub fn failed(message: impl Into<String>) -> Self {
1929		Self {
1930			phase: WorkspacePhase::Failed,
1931			failure: Some(WorkspaceFailureDto {
1932				resource: None,
1933				message: message.into(),
1934			}),
1935		}
1936	}
1937}
1938
1939#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1940#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1941#[serde(rename_all = "snake_case")]
1942pub enum WorkspacePhase {
1943	Loading,
1944	Ready,
1945	Refreshing,
1946	Failed,
1947}
1948
1949impl std::fmt::Display for WorkspacePhase {
1950	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1951		formatter.write_str(match self {
1952			Self::Loading => "loading",
1953			Self::Ready => "ready",
1954			Self::Refreshing => "refreshing",
1955			Self::Failed => "failed",
1956		})
1957	}
1958}
1959
1960#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1961#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1962pub struct WorkspaceFailureDto {
1963	pub resource: Option<String>,
1964	pub message: String,
1965}
1966
1967#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1968#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1969pub struct WorkspaceTimingsDto {
1970	pub source_catalog_ms: u64,
1971	pub extract_sources_ms: u64,
1972	pub semantic_index_ms: u64,
1973	pub code_index_ms: u64,
1974	pub linkage_ms: u64,
1975	pub change_overlay_ms: u64,
1976	pub total_ms: u64,
1977}
1978
1979#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1980#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1981pub struct WorkspaceRootStatus {
1982	pub root: String,
1983	pub generation: Option<WorkspaceGeneration>,
1984	pub files: usize,
1985	pub symbols: usize,
1986	pub references: usize,
1987	pub stale: bool,
1988	pub stale_summary: String,
1989}
1990
1991#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1992#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1993pub struct TreeChildrenResult {
1994	pub root: String,
1995	pub roots: Vec<String>,
1996	pub rows: Vec<TreeNode>,
1997	pub total: usize,
1998	pub total_files: usize,
1999	pub scoped_files: usize,
2000	pub languages: Vec<CountDto>,
2001	pub prefixes: Vec<CountDto>,
2002}
2003
2004#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2005#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2006pub struct TreeNode {
2007	pub root: String,
2008	pub path: String,
2009	pub kind: TreeNodeKind,
2010	pub language: Option<String>,
2011	pub defs: usize,
2012	pub refs: usize,
2013	pub change_count: usize,
2014}
2015
2016#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2017#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2018#[serde(rename_all = "snake_case")]
2019pub enum TreeNodeKind {
2020	File,
2021	Directory,
2022}
2023
2024#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2025#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2026pub struct SymbolListResult {
2027	pub rows: Vec<SymbolDto>,
2028	pub total: usize,
2029}
2030
2031#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2032#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2033pub struct SymbolDto {
2034	pub root: String,
2035	pub uri: String,
2036	pub id: String,
2037	pub name: String,
2038	pub kind: String,
2039	pub visibility: String,
2040	pub signature: String,
2041	pub file: String,
2042	pub language: String,
2043	pub line_range: Option<(u32, u32)>,
2044	pub navigable: bool,
2045	pub score: Option<u32>,
2046	pub match_reason: Option<String>,
2047	pub source: Option<SourceSnippet>,
2048}
2049
2050pub fn symbol_is_test_artifact(kind: &str, file: &str, uri: &str) -> bool {
2051	kind == "test"
2052		|| uri
2053			.split('/')
2054			.any(|segment| matches!(segment, "module:test" | "module:tests"))
2055		|| file.split(['/', '\\']).any(|component| {
2056			matches!(
2057				component,
2058				"test"
2059					| "tests" | "bench"
2060					| "benches" | "fixture"
2061					| "fixtures" | "testdata"
2062					| "__tests__"
2063			)
2064		})
2065}
2066
2067#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2068#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2069pub struct SymbolInsightsResult {
2070	pub files: usize,
2071	pub symbols: usize,
2072	pub references: usize,
2073	pub navigable_symbols: usize,
2074	pub non_navigable_symbols: usize,
2075	pub languages: Vec<CountDto>,
2076	pub kinds: Vec<CountDto>,
2077	pub shapes: Vec<CountDto>,
2078	pub top_files_by_symbols: Vec<CountDto>,
2079	pub top_files_by_refs: Vec<CountDto>,
2080}
2081
2082#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2083#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2084pub struct SymbolDetailResult {
2085	pub symbol: SymbolDto,
2086	pub source: Option<SourceSnippet>,
2087}
2088
2089#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2090#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2091pub struct SyntaxTreeResult {
2092	pub file: String,
2093	pub language: String,
2094	pub focus: String,
2095	pub focus_line_range: Option<(u32, u32)>,
2096	pub root: SyntaxNodeDto,
2097	pub emitted_nodes: usize,
2098	pub total_nodes: usize,
2099	pub max_depth: usize,
2100	pub truncated: bool,
2101	pub has_error: bool,
2102}
2103
2104#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2106pub struct SyntaxNodeDto {
2107	pub kind: String,
2108	#[serde(default, skip_serializing_if = "Option::is_none")]
2109	pub language: Option<String>,
2110	pub named: bool,
2111	pub error: bool,
2112	pub missing: bool,
2113	pub byte_range: (usize, usize),
2114	pub start: SyntaxPointDto,
2115	pub end: SyntaxPointDto,
2116	pub text: Option<String>,
2117	pub children: Vec<SyntaxNodeDto>,
2118}
2119
2120#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2122pub struct SyntaxPointDto {
2123	/// One-based line number.
2124	pub line: u32,
2125	/// Zero-based UTF-8 byte column, matching Tree-sitter.
2126	pub column: u32,
2127}
2128
2129#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2130#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2131pub struct SourceSnippet {
2132	pub file: String,
2133	pub first_line: u32,
2134	pub last_line: u32,
2135	pub lines: Vec<SourceLine>,
2136}
2137
2138#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2140pub struct SourceLine {
2141	pub number: u32,
2142	pub text: String,
2143}
2144
2145#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2147pub struct SymbolUsagesResult {
2148	pub target: SymbolDto,
2149	pub direction: UsageDirection,
2150	pub include_descendants: bool,
2151	pub targets: usize,
2152	pub rows: Vec<UsageDto>,
2153	pub total: usize,
2154	pub incoming_summary: Option<UsageSummaryDto>,
2155	pub outgoing_summary: Option<UsageSummaryDto>,
2156}
2157
2158#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2160pub struct UsageSummaryDto {
2161	pub refs: usize,
2162	pub files: usize,
2163	pub contexts: usize,
2164	pub prefixes: usize,
2165	pub dominant_prefix: String,
2166	pub kinds: Vec<CountDto>,
2167	pub top_actors: Vec<CountDto>,
2168	pub top_prefixes: Vec<CountDto>,
2169	pub shared_helper_signal: String,
2170}
2171
2172#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2174pub struct UsageDto {
2175	pub root: String,
2176	pub direction: UsageDirection,
2177	pub reference: String,
2178	pub kind: String,
2179	pub actor: String,
2180	pub context: String,
2181	pub endpoint: String,
2182	pub file: String,
2183	pub prefix: String,
2184	pub location: String,
2185	pub line_range: Option<(u32, u32)>,
2186	pub via: Option<String>,
2187}
2188
2189#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2190#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2191pub struct RulesListResult {
2192	pub roots: Vec<String>,
2193	pub rows: Vec<RuleDto>,
2194	pub total: usize,
2195}
2196
2197#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2199pub struct RuleDto {
2200	pub root: String,
2201	pub id: String,
2202	pub severity: String,
2203	pub lang: String,
2204	pub rule_root: String,
2205	pub subject: String,
2206	pub plan: String,
2207	pub capabilities: Vec<String>,
2208	#[serde(default)]
2209	pub group_by: Vec<String>,
2210	pub domain: String,
2211	pub kind: Option<String>,
2212	pub expr: String,
2213	pub expanded_expr: String,
2214	pub message: Option<String>,
2215	pub rationale: Option<String>,
2216	pub require_doc_comment: Option<String>,
2217}
2218
2219#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2220#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2221pub struct RulesApplicableResult {
2222	pub focus: SymbolGraphFocus,
2223	pub file: String,
2224	pub language: String,
2225	pub symbol_kind: Option<String>,
2226	pub total: usize,
2227	pub rows: Vec<RuleApplicabilityDto>,
2228}
2229
2230#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2232pub struct RuleApplicabilityDto {
2233	pub rule: RuleDto,
2234	pub status: String,
2235	pub reason: String,
2236}
2237
2238#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2240pub struct ChangeContextResult {
2241	pub focus: SymbolGraphFocus,
2242	pub source: Option<SourceSnippet>,
2243	pub graph: Box<SymbolGraphResult>,
2244	pub notes: Vec<NoteDto>,
2245	pub rules: Vec<RuleApplicabilityDto>,
2246	pub changed_files: Vec<ChangeReviewFile>,
2247	pub changed_symbols: Vec<ChangeReviewSymbol>,
2248	pub suggested_checks: Vec<String>,
2249	pub coverage: ChangeContextCoverageDto,
2250}
2251
2252#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2254pub struct ChangeContextCoverageDto {
2255	pub members_total: usize,
2256	pub members_emitted: usize,
2257	pub internal_edges_total: usize,
2258	pub internal_edges_emitted: usize,
2259	pub callers_total: usize,
2260	pub callers_emitted: usize,
2261	pub callees_total: usize,
2262	pub callees_emitted: usize,
2263	pub notes_total: usize,
2264	pub notes_emitted: usize,
2265	pub rules_total: usize,
2266	pub rules_emitted: usize,
2267	pub changes_total: usize,
2268	pub changes_emitted: usize,
2269}
2270
2271#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2273pub struct RulesCheckResult {
2274	pub verdict: RulesCheckVerdict,
2275	pub exit: String,
2276	pub summary: CheckSummaryDto,
2277	pub roots: Vec<RulesCheckRootResult>,
2278	pub violations: Vec<ViolationDto>,
2279	pub errors: Vec<FileErrorDto>,
2280	pub rule_reports: Vec<RuleReportDto>,
2281	pub skip_reasons: Vec<CheckSkipReasonDto>,
2282}
2283
2284#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2286pub struct RulesCheckRootResult {
2287	pub root: String,
2288	pub verdict: RulesCheckVerdict,
2289	pub exit: String,
2290	pub summary: CheckSummaryDto,
2291	pub violations: Vec<ViolationDto>,
2292	pub errors: Vec<FileErrorDto>,
2293	pub rule_reports: Vec<RuleReportDto>,
2294	pub skip_reason: Option<CheckSkipReasonDto>,
2295}
2296
2297#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2298#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2299#[serde(rename_all = "snake_case")]
2300pub enum RulesCheckVerdict {
2301	Pass,
2302	Fail,
2303	Error,
2304}
2305
2306impl RulesCheckVerdict {
2307	pub fn from_exit(exit: &str) -> Self {
2308		match exit {
2309			"match" => Self::Pass,
2310			"no_match" => Self::Fail,
2311			_ => Self::Error,
2312		}
2313	}
2314
2315	pub fn as_str(self) -> &'static str {
2316		match self {
2317			Self::Pass => "pass",
2318			Self::Fail => "fail",
2319			Self::Error => "error",
2320		}
2321	}
2322}
2323
2324#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2325#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2326pub struct CheckSummaryDto {
2327	pub files_scanned: usize,
2328	pub files_with_violations: usize,
2329	pub total_violations: usize,
2330	pub total_rule_errors: usize,
2331	pub total_warnings: usize,
2332	pub files_with_errors: usize,
2333	pub total_errors: usize,
2334	pub elapsed_ms: u64,
2335	pub failed_rules: Vec<FailedRuleDto>,
2336	#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2337	pub violations_by_srcset: BTreeMap<String, usize>,
2338}
2339
2340#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2342pub struct FailedRuleDto {
2343	pub rule_id: String,
2344	pub severity: String,
2345	pub violations: usize,
2346}
2347
2348#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2349#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2350pub struct ViolationDto {
2351	pub root: String,
2352	pub path: String,
2353	pub rule_id: String,
2354	pub severity: String,
2355	pub moniker: String,
2356	#[serde(default, skip_serializing_if = "Option::is_none")]
2357	pub srcset: Option<String>,
2358	pub kind: String,
2359	pub lines: (u32, u32),
2360	pub message: String,
2361}
2362
2363#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2364#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2365pub struct FileErrorDto {
2366	pub root: String,
2367	pub path: String,
2368	pub error: String,
2369}
2370
2371#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2372#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2373pub struct RuleReportDto {
2374	pub root: String,
2375	pub path: Option<String>,
2376	pub rule_id: String,
2377	pub severity: String,
2378	pub domain: String,
2379	pub evaluated: usize,
2380	pub matches: usize,
2381	pub violations: usize,
2382	pub antecedent_matches: Option<usize>,
2383	pub warning: Option<String>,
2384	#[serde(default, skip_serializing_if = "Option::is_none")]
2385	pub inconclusive: Option<usize>,
2386	#[serde(default, skip_serializing_if = "Option::is_none")]
2387	pub verdict: Option<String>,
2388	#[serde(default, skip_serializing_if = "Option::is_none")]
2389	pub coverage: Option<RuleCoverageDto>,
2390	#[serde(default, skip_serializing_if = "Option::is_none")]
2391	pub path_analysis: Option<RulePathReportDto>,
2392}
2393
2394#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2395#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2396pub struct RuleCoverageDto {
2397	pub total: usize,
2398	pub decided: usize,
2399	pub resolved: usize,
2400	pub external: usize,
2401	pub candidate: usize,
2402	pub dynamic: usize,
2403	pub blocked: usize,
2404	pub unresolved: usize,
2405	pub percent: usize,
2406	pub min_percent: usize,
2407}
2408
2409#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2410#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2411pub struct RulePathStepDto {
2412	pub source: String,
2413	pub target: String,
2414	pub relation: String,
2415	pub reference: String,
2416	pub file: String,
2417	pub line_range: Option<(u32, u32)>,
2418}
2419
2420#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2421#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2422pub struct RulePathReportDto {
2423	pub expectation: String,
2424	pub relation: Vec<String>,
2425	pub max_depth: usize,
2426	pub max_symbols: usize,
2427	pub max_edges: usize,
2428	pub max_pairs: usize,
2429	pub min_coverage: usize,
2430	pub source_symbols: usize,
2431	pub target_symbols: usize,
2432	pub via_symbols: usize,
2433	pub evaluated_pairs: usize,
2434	pub explored_symbols: usize,
2435	pub explored_edges: usize,
2436	pub depth_limit_reached: bool,
2437	pub symbol_limit_reached: bool,
2438	pub edge_limit_reached: bool,
2439	pub pair_limit_reached: bool,
2440	pub reasons: Vec<String>,
2441	pub witness: Vec<RulePathStepDto>,
2442}
2443
2444#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2445#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2446pub struct CheckSkipReasonDto {
2447	pub root: String,
2448	pub reason: String,
2449}
2450
2451#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2452#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2453pub struct NotesResult {
2454	pub action: String,
2455	pub total: usize,
2456	pub rows: Vec<NoteDto>,
2457	pub deleted: Option<NoteDto>,
2458}
2459
2460#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2461#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2462pub struct NoteDto {
2463	pub id: String,
2464	pub moniker: String,
2465	pub kind: String,
2466	pub status: String,
2467	pub title: String,
2468	pub body: String,
2469	pub created_by: String,
2470	pub updated_at: String,
2471	pub resolution: NoteResolutionDto,
2472}
2473
2474#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2475#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2476#[serde(tag = "status", rename_all = "snake_case")]
2477pub enum NoteResolutionDto {
2478	Resolved {
2479		target: String,
2480		file: String,
2481		slice: Option<(u32, u32)>,
2482	},
2483	Orphan,
2484}
2485
2486#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2487#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2488pub struct CountDto {
2489	pub name: String,
2490	pub count: usize,
2491}
2492
2493#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2494#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2495#[serde(tag = "event", rename_all = "snake_case")]
2496pub enum DaemonEvent {
2497	WorkspaceStale {
2498		generation: Option<WorkspaceGeneration>,
2499		summary: String,
2500	},
2501	WorkspaceRefreshed {
2502		generation: WorkspaceGeneration,
2503	},
2504}
2505
2506#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2507#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2508pub struct QueryError {
2509	pub code: String,
2510	pub message: String,
2511}
2512
2513impl QueryError {
2514	pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
2515		Self {
2516			code: code.into(),
2517			message: message.into(),
2518		}
2519	}
2520}
2521
2522impl fmt::Display for QueryError {
2523	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2524		write!(f, "{}: {}", self.code, self.message)
2525	}
2526}
2527
2528impl std::error::Error for QueryError {}
2529
2530#[derive(Debug, thiserror::Error)]
2531pub enum QueryParseError {
2532	#[error("empty query")]
2533	Empty,
2534	#[error("unknown query operation `{0}`")]
2535	UnknownOperation(String),
2536	#[error("invalid token `{0}`")]
2537	InvalidToken(String),
2538	#[error("invalid value for `{key}`: `{value}`")]
2539	InvalidValue { key: String, value: String },
2540	#[error("missing required `{0}`")]
2541	MissingRequired(&'static str),
2542	#[error("unknown field `{key}` for `{op}`{hint}")]
2543	UnknownField {
2544		op: String,
2545		key: String,
2546		hint: String,
2547	},
2548	#[error("unexpected argument `{value}` for `{op}`")]
2549	UnexpectedArgument { op: String, value: String },
2550	#[error("`project` is not supported by `{op}`")]
2551	UnsupportedProjection { op: String },
2552	#[error("unknown projection field `{field}` for `{op}`{hint}")]
2553	UnknownProjectionField {
2554		op: String,
2555		field: String,
2556		hint: String,
2557	},
2558}
2559
2560pub fn parse_query(input: &str) -> Result<QueryRequest, QueryParseError> {
2561	let mut lines = input.lines().map(str::trim).filter(|line| !line.is_empty());
2562	let first = lines.next().ok_or(QueryParseError::Empty)?;
2563	let mut tokens = tokenize(first)?;
2564	let op = tokens
2565		.first()
2566		.map(|token| token.text.clone())
2567		.ok_or(QueryParseError::Empty)?;
2568	tokens.remove(0);
2569	let mut fields = FieldBag::default();
2570	let mut positional = Vec::new();
2571	collect_tokens(&tokens, &mut fields, &mut positional)?;
2572	for line in lines {
2573		collect_line(line, &mut fields, &mut positional)?;
2574	}
2575	fields.positional = positional;
2576	let spec = verb_spec(&op).ok_or_else(|| QueryParseError::UnknownOperation(op.clone()))?;
2577	validate_fields(&op, spec, &fields)?;
2578	if let Some(value) = fields.one("consistency") {
2579		fields.consistency = value.parse()?;
2580	}
2581	let page = fields.page()?;
2582	let consistency = fields.consistency;
2583	let query = build_query(&op, fields)?;
2584	Ok(QueryRequest {
2585		query,
2586		consistency,
2587		page,
2588	})
2589}
2590
2591fn collect_line(
2592	line: &str,
2593	fields: &mut FieldBag,
2594	positional: &mut Vec<String>,
2595) -> Result<(), QueryParseError> {
2596	let mut tokens = tokenize(line)?;
2597	if tokens.is_empty() {
2598		return Ok(());
2599	}
2600	let section = tokens.remove(0);
2601	if section.text.contains(':') {
2602		let mut all = vec![section];
2603		all.extend(tokens);
2604		return collect_tokens(&all, fields, positional);
2605	}
2606	match section.text.as_str() {
2607		"filter" | "page" => collect_tokens(&tokens, fields, positional)?,
2608		"project" => {
2609			fields.projection.extend(
2610				tokens
2611					.into_iter()
2612					.map(|token| token.text.trim_end_matches(',').to_string())
2613					.filter(|token| !token.is_empty()),
2614			);
2615		}
2616		"consistency" => {
2617			let value = tokens
2618				.first()
2619				.ok_or(QueryParseError::MissingRequired("consistency value"))?;
2620			fields.consistency = value.text.parse()?;
2621		}
2622		"direction" => {
2623			let value = tokens
2624				.first()
2625				.ok_or(QueryParseError::MissingRequired("direction value"))?;
2626			fields.values.push((
2627				"direction".to_string(),
2628				FieldValue {
2629					text: value.text.clone(),
2630					quoted: value.quoted,
2631				},
2632			));
2633		}
2634		_ => return Err(QueryParseError::InvalidToken(section.text)),
2635	}
2636	Ok(())
2637}
2638
2639fn build_query(op: &str, fields: FieldBag) -> Result<Query, QueryParseError> {
2640	let query = match op {
2641		"query.describe" => Query::QueryDescribe(QueryDescribeQuery {
2642			verb: fields
2643				.one("verb")
2644				.or_else(|| fields.positional.first().cloned()),
2645		}),
2646		"workspace.status" => Query::WorkspaceStatus,
2647		"tree.children" => Query::TreeChildren(TreeChildrenQuery {
2648			workspace: fields.one("workspace"),
2649			path: fields.many("path"),
2650			depth: fields.usize("depth")?.unwrap_or(1),
2651			lang: fields.many("lang"),
2652			projection: fields.projection,
2653		}),
2654		"symbol.search" => Query::SymbolSearch(symbol_search_query(&fields)?),
2655		"symbol.insights" => Query::SymbolInsights(symbol_insights_query(&fields)?),
2656		"symbol.detail" => Query::SymbolDetail(SymbolDetailQuery {
2657			workspace: fields.one("workspace"),
2658			uri: fields
2659				.one("uri")
2660				.or_else(|| fields.positional.first().cloned())
2661				.ok_or(QueryParseError::MissingRequired("uri"))?,
2662			context_lines: fields.usize("context_lines")?.unwrap_or(2),
2663		}),
2664		"syntax.tree" => Query::SyntaxTree(SyntaxTreeQuery {
2665			workspace: fields.one("workspace"),
2666			focus: fields
2667				.one("focus")
2668				.or_else(|| fields.positional.first().cloned())
2669				.ok_or(QueryParseError::MissingRequired("focus"))?,
2670			max_depth: fields
2671				.usize("max_depth")?
2672				.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_DEPTH),
2673			max_nodes: fields
2674				.usize("max_nodes")?
2675				.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_NODES),
2676			named_only: fields.bool("named_only")?.unwrap_or(true),
2677			include_text: fields.bool("include_text")?.unwrap_or(false),
2678			max_text_chars: fields
2679				.usize("max_text_chars")?
2680				.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_TEXT_CHARS),
2681		}),
2682		"syntax.parse" => Query::SyntaxParse(syntax_parse_query(&fields)?),
2683		"symbol.usages" => Query::SymbolUsages(symbol_usages_query(&fields)?),
2684		"view.read" => Query::ViewRead(ViewReadQuery {
2685			uri: fields
2686				.one("uri")
2687				.or_else(|| fields.positional.first().cloned())
2688				.ok_or(QueryParseError::MissingRequired("uri"))?,
2689			scheme: fields.one("scheme"),
2690			context_lines: fields.usize("context_lines")?.unwrap_or(2),
2691			include_code: fields.bool("include_code")?.unwrap_or(false),
2692		}),
2693		"rules.list" => Query::RulesList(RulesListQuery {
2694			workspace: fields.one("workspace"),
2695			profile: fields.one("profile"),
2696			rules: fields.one("rules"),
2697			lang: fields.many("lang"),
2698			severity: fields.many("severity"),
2699		}),
2700		"rules.check" => Query::RulesCheck(RulesCheckQuery {
2701			workspace: fields.one("workspace"),
2702			profile: fields.one("profile"),
2703			rules: fields.one("rules"),
2704			file: fields.many("file"),
2705			report: fields.bool("report")?.unwrap_or(true),
2706		}),
2707		"rules.applicable" => Query::RulesApplicable(RulesApplicableQuery {
2708			workspace: fields.one("workspace"),
2709			focus: fields
2710				.one("focus")
2711				.or_else(|| fields.positional.first().cloned())
2712				.ok_or(QueryParseError::MissingRequired("focus"))?,
2713			profile: fields.one("profile"),
2714			rules: fields.one("rules"),
2715		}),
2716		"change.review" => Query::ChangeReview(ChangeReviewQuery {
2717			workspace: fields.one("workspace"),
2718		}),
2719		"change.context" => Query::ChangeContext(ChangeContextQuery {
2720			workspace: fields.one("workspace"),
2721			focus: fields
2722				.one("focus")
2723				.or_else(|| fields.positional.first().cloned())
2724				.ok_or(QueryParseError::MissingRequired("focus"))?,
2725			profile: fields.one("profile"),
2726			max_items: fields.usize("max_items")?.unwrap_or(20),
2727		}),
2728		"symbol.graph" => Query::SymbolGraph(symbol_graph_query(&fields)?),
2729		"graph.path" => Query::GraphPath(graph_path_query(&fields)?),
2730		"identity.children" => Query::IdentityChildren(identity_children_query(&fields)),
2731		"identity.graph" => Query::IdentityGraph(identity_graph_query(&fields)?),
2732		"metrics.coupling" => Query::MetricsCoupling(MetricsCouplingQuery {
2733			workspace: fields.one("workspace"),
2734			from: fields
2735				.one("from")
2736				.ok_or(QueryParseError::MissingRequired("from"))?,
2737			to: fields
2738				.one("to")
2739				.ok_or(QueryParseError::MissingRequired("to"))?,
2740			relation: fields.many("relation"),
2741			snapshot: fields.one("snapshot"),
2742			export: fields.bool("export")?.unwrap_or(false),
2743		}),
2744		"resolution.audit" => Query::ResolutionAudit(ResolutionAuditQuery {
2745			workspace: fields.one("workspace"),
2746			prefix: fields
2747				.one("prefix")
2748				.or_else(|| fields.positional.first().cloned())
2749				.unwrap_or_default(),
2750			limit: fields.usize("limit")?.unwrap_or(20),
2751			cluster: fields.one("cluster"),
2752		}),
2753		"notes" => Query::Notes(notes_query(&fields)?),
2754		_ => return Err(QueryParseError::UnknownOperation(op.to_string())),
2755	};
2756	Ok(query)
2757}
2758
2759fn verb_spec(op: &str) -> Option<&'static QueryCapabilitySpec> {
2760	query_capability_spec(op)
2761}
2762
2763fn validate_fields(
2764	op: &str,
2765	spec: &QueryCapabilitySpec,
2766	fields: &FieldBag,
2767) -> Result<(), QueryParseError> {
2768	for (key, value) in &fields.values {
2769		let key = key.as_str();
2770		if !COMMON_FIELDS.contains(&key) && !spec.fields.contains(&key) {
2771			return Err(QueryParseError::UnknownField {
2772				op: op.to_string(),
2773				key: key.to_string(),
2774				hint: field_hint(key, spec.fields),
2775			});
2776		}
2777		if !value.quoted
2778			&& BRACKET_LIST_FIELDS.contains(&key)
2779			&& value.text.starts_with('[') != value.text.ends_with(']')
2780		{
2781			return Err(QueryParseError::InvalidValue {
2782				key: key.to_string(),
2783				value: value.text.clone(),
2784			});
2785		}
2786	}
2787	if let Some(extra) = fields.positional.get(spec.positionals) {
2788		return Err(QueryParseError::UnexpectedArgument {
2789			op: op.to_string(),
2790			value: extra.clone(),
2791		});
2792	}
2793	if !spec.projection && !fields.projection.is_empty() {
2794		return Err(QueryParseError::UnsupportedProjection { op: op.to_string() });
2795	}
2796	let projection_fields = query_projection_fields(op);
2797	for field in &fields.projection {
2798		if !projection_fields.contains(&field.as_str()) {
2799			return Err(QueryParseError::UnknownProjectionField {
2800				op: op.to_string(),
2801				field: field.clone(),
2802				hint: projection_field_hint(field, projection_fields),
2803			});
2804		}
2805	}
2806	Ok(())
2807}
2808
2809fn projection_field_hint(field: &str, allowed: &'static [&'static str]) -> String {
2810	if let Some(suggestion) = allowed
2811		.iter()
2812		.copied()
2813		.map(|candidate| (candidate, levenshtein(field, candidate)))
2814		.filter(|(_, distance)| *distance <= 2)
2815		.min_by_key(|(_, distance)| *distance)
2816		.map(|(candidate, _)| candidate)
2817	{
2818		return format!(", did you mean `{suggestion}`?");
2819	}
2820	format!(" (valid projection fields: {})", allowed.join(", "))
2821}
2822
2823fn field_hint(key: &str, allowed: &'static [&'static str]) -> String {
2824	if let Some(suggestion) = suggest_field(key, allowed) {
2825		return format!(", did you mean `{suggestion}`?");
2826	}
2827	let mut valid: Vec<&str> = allowed.iter().chain(COMMON_FIELDS).copied().collect();
2828	valid.sort_unstable();
2829	format!(" (valid fields: {})", valid.join(", "))
2830}
2831
2832fn suggest_field(key: &str, allowed: &'static [&'static str]) -> Option<&'static str> {
2833	const ALIASES: &[(&str, &str)] = &[("text", "name"), ("query", "name"), ("filename", "path")];
2834	for (alias, target) in ALIASES {
2835		if *alias == key && allowed.contains(target) {
2836			return Some(target);
2837		}
2838	}
2839	allowed
2840		.iter()
2841		.chain(COMMON_FIELDS)
2842		.copied()
2843		.map(|candidate| (candidate, levenshtein(key, candidate)))
2844		.filter(|(_, distance)| *distance <= 2)
2845		.min_by_key(|(_, distance)| *distance)
2846		.map(|(candidate, _)| candidate)
2847}
2848
2849fn levenshtein(a: &str, b: &str) -> usize {
2850	let b: Vec<char> = b.chars().collect();
2851	let mut row: Vec<usize> = (0..=b.len()).collect();
2852	for (i, ca) in a.chars().enumerate() {
2853		let mut previous = row[0];
2854		row[0] = i + 1;
2855		for (j, cb) in b.iter().enumerate() {
2856			let substitution = previous + usize::from(ca != *cb);
2857			previous = row[j + 1];
2858			row[j + 1] = substitution.min(previous + 1).min(row[j] + 1);
2859		}
2860	}
2861	row[b.len()]
2862}
2863
2864fn symbol_search_query(fields: &FieldBag) -> Result<SymbolSearchQuery, QueryParseError> {
2865	Ok(SymbolSearchQuery {
2866		workspace: fields.one("workspace"),
2867		text: fields.positional.first().cloned(),
2868		path: fields.many("path"),
2869		lang: fields.many("lang"),
2870		kind: fields.many("kind"),
2871		shape: fields.many("shape"),
2872		name: fields.one("name"),
2873		include_non_navigable: fields.bool("include_non_navigable")?.unwrap_or(false),
2874		include_code: fields.bool("include_code")?.unwrap_or(false),
2875		context_lines: fields.usize("context_lines")?.unwrap_or(0),
2876		projection: fields.projection.clone(),
2877	})
2878}
2879
2880fn symbol_usages_query(fields: &FieldBag) -> Result<SymbolUsagesQuery, QueryParseError> {
2881	Ok(SymbolUsagesQuery {
2882		workspace: fields.one("workspace"),
2883		uri: fields
2884			.one("uri")
2885			.or_else(|| fields.positional.first().cloned())
2886			.ok_or(QueryParseError::MissingRequired("uri"))?,
2887		direction: fields
2888			.one("direction")
2889			.unwrap_or_else(|| "incoming".to_string())
2890			.parse()?,
2891		path: fields.many("path"),
2892		lang: fields.many("lang"),
2893		include_descendants: fields.bool("include_descendants")?.unwrap_or(false),
2894		projection: fields.projection.clone(),
2895	})
2896}
2897
2898fn syntax_parse_query(fields: &FieldBag) -> Result<SyntaxParseQuery, QueryParseError> {
2899	Ok(SyntaxParseQuery {
2900		language: fields
2901			.one("language")
2902			.ok_or(QueryParseError::MissingRequired("language"))?,
2903		source: fields
2904			.one("source")
2905			.ok_or(QueryParseError::MissingRequired("source"))?,
2906		uri: fields.one("uri"),
2907		max_depth: fields
2908			.usize("max_depth")?
2909			.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_DEPTH),
2910		max_nodes: fields
2911			.usize("max_nodes")?
2912			.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_NODES),
2913		named_only: fields.bool("named_only")?.unwrap_or(true),
2914		include_text: fields.bool("include_text")?.unwrap_or(false),
2915		max_text_chars: fields
2916			.usize("max_text_chars")?
2917			.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_TEXT_CHARS),
2918	})
2919}
2920
2921fn symbol_insights_query(fields: &FieldBag) -> Result<SymbolSearchQuery, QueryParseError> {
2922	let mut query = symbol_search_query(fields)?;
2923	query.text = None;
2924	query.include_code = false;
2925	query.context_lines = 0;
2926	Ok(query)
2927}
2928
2929fn notes_query(fields: &FieldBag) -> Result<NotesQuery, QueryParseError> {
2930	Ok(NotesQuery {
2931		action: parse_notes_action(fields.one("action").as_deref().unwrap_or("list"))?,
2932		id: fields.one("id"),
2933		moniker: fields.one("moniker"),
2934		kind: fields.one("kind"),
2935		status: fields.one("status"),
2936		title: fields.one("title"),
2937		body: fields.one("body"),
2938		created_by: fields.one("created_by"),
2939		orphan: fields.bool("orphan")?,
2940		include_done: fields.bool("include_done")?.unwrap_or(false),
2941	})
2942}
2943
2944fn identity_children_query(fields: &FieldBag) -> IdentityChildrenQuery {
2945	IdentityChildrenQuery {
2946		workspace: fields.one("workspace"),
2947		prefix: fields
2948			.one("prefix")
2949			.or_else(|| fields.positional.first().cloned())
2950			.unwrap_or_default(),
2951	}
2952}
2953
2954fn identity_graph_query(fields: &FieldBag) -> Result<IdentityGraphQuery, QueryParseError> {
2955	Ok(IdentityGraphQuery {
2956		workspace: fields.one("workspace"),
2957		prefix: fields
2958			.one("prefix")
2959			.or_else(|| fields.positional.first().cloned())
2960			.unwrap_or_default(),
2961		path: fields.many("path"),
2962		min_count: fields.usize("min_count")?.unwrap_or(1).max(1),
2963	})
2964}
2965
2966fn symbol_graph_query(fields: &FieldBag) -> Result<SymbolGraphQuery, QueryParseError> {
2967	Ok(SymbolGraphQuery {
2968		workspace: fields.one("workspace"),
2969		focus: fields
2970			.one("focus")
2971			.or_else(|| fields.positional.first().cloned())
2972			.ok_or(QueryParseError::MissingRequired("focus"))?,
2973		direction: fields
2974			.one("direction")
2975			.unwrap_or_else(|| "both".to_string())
2976			.parse()?,
2977		relation: fields.many("relation"),
2978		min_count: fields.usize("min_count")?.unwrap_or(1).max(1),
2979		include_internal: fields.bool("include_internal")?.unwrap_or(true),
2980	})
2981}
2982
2983fn graph_path_query(fields: &FieldBag) -> Result<GraphPathQuery, QueryParseError> {
2984	let max_depth = fields.usize("max_depth")?.unwrap_or(12);
2985	if max_depth > 64 {
2986		return Err(QueryParseError::InvalidValue {
2987			key: "max_depth".to_string(),
2988			value: max_depth.to_string(),
2989		});
2990	}
2991	let min_coverage = fields.usize("min_coverage")?.unwrap_or(100);
2992	if min_coverage > 100 {
2993		return Err(QueryParseError::InvalidValue {
2994			key: "min_coverage".to_string(),
2995			value: min_coverage.to_string(),
2996		});
2997	}
2998	let max_symbols = bounded_non_zero_field(fields, "max_symbols", 10_000, 100_000)?;
2999	let max_edges = bounded_non_zero_field(fields, "max_edges", 50_000, 500_000)?;
3000	let relation = fields.many("relation");
3001	Ok(GraphPathQuery {
3002		workspace: fields.one("workspace"),
3003		from: fields
3004			.one("from")
3005			.ok_or(QueryParseError::MissingRequired("from"))?,
3006		to: fields
3007			.one("to")
3008			.ok_or(QueryParseError::MissingRequired("to"))?,
3009		expect: fields
3010			.one("expect")
3011			.unwrap_or_else(|| "reachable".to_string())
3012			.parse()?,
3013		relation: if relation.is_empty() {
3014			vec!["calls".to_string(), "method_call".to_string()]
3015		} else {
3016			relation
3017		},
3018		max_depth,
3019		max_symbols,
3020		max_edges,
3021		min_coverage,
3022	})
3023}
3024
3025fn bounded_non_zero_field(
3026	fields: &FieldBag,
3027	key: &str,
3028	default: usize,
3029	maximum: usize,
3030) -> Result<usize, QueryParseError> {
3031	let value = fields.usize(key)?.unwrap_or(default);
3032	if value == 0 || value > maximum {
3033		return Err(QueryParseError::InvalidValue {
3034			key: key.to_string(),
3035			value: value.to_string(),
3036		});
3037	}
3038	Ok(value)
3039}
3040
3041fn parse_notes_action(value: &str) -> Result<NotesAction, QueryParseError> {
3042	match value {
3043		"list" => Ok(NotesAction::List),
3044		"get" => Ok(NotesAction::Get),
3045		"create" => Ok(NotesAction::Create),
3046		"update" => Ok(NotesAction::Update),
3047		"transition" => Ok(NotesAction::Transition),
3048		"delete" => Ok(NotesAction::Delete),
3049		_ => Err(QueryParseError::InvalidValue {
3050			key: "action".to_string(),
3051			value: value.to_string(),
3052		}),
3053	}
3054}
3055
3056pub fn format_query_response(response: &QueryResponse) -> String {
3057	format_query_response_projected(response, &[])
3058}
3059
3060pub fn format_query_response_projected(response: &QueryResponse, projection: &[String]) -> String {
3061	let mut out = String::new();
3062	if let Some(generation) = response.generation {
3063		let _ = writeln!(out, "generation: {}", generation.0);
3064	}
3065	if let Some(cursor) = &response.next_cursor {
3066		if let Some(generation) = cursor.generation {
3067			let _ = writeln!(out, "next_cursor: {}:{}", generation.0, cursor.offset);
3068		} else {
3069			let _ = writeln!(out, "next_cursor: {}", cursor.offset);
3070		}
3071	}
3072	match &response.result {
3073		QueryResult::QueryDescribe(result) => format_query_describe(&mut out, result),
3074		QueryResult::WorkspaceStatus(status) => format_workspace_status(&mut out, status),
3075		QueryResult::TreeChildren(result) => format_tree_children(&mut out, result, projection),
3076		QueryResult::SymbolList(result) => format_symbol_list(&mut out, result, projection),
3077		QueryResult::SymbolInsights(result) if !projection.is_empty() => {
3078			format_projected_value(&mut out, result, projection);
3079		}
3080		QueryResult::SymbolInsights(result) => format_symbol_insights(&mut out, result),
3081		QueryResult::SymbolDetail(result) => format_symbol_detail(&mut out, result),
3082		QueryResult::SyntaxTree(result) => format_syntax_tree(&mut out, result),
3083		QueryResult::SymbolUsages(result) => format_symbol_usages(&mut out, result, projection),
3084		QueryResult::ViewRead(result) => format_view_read(&mut out, result),
3085		QueryResult::RulesList(result) => {
3086			let _ = writeln!(out, "rules: {}", result.total);
3087			format_rules_list_rows(&mut out, result);
3088		}
3089		QueryResult::RulesCheck(result) => format_rules_check(&mut out, result),
3090		QueryResult::RulesApplicable(result) => format_rules_applicable(&mut out, result),
3091		QueryResult::ChangeReview(result) => format_change_review(&mut out, result),
3092		QueryResult::DiffImpact(result) => format_diff_impact(&mut out, result),
3093		QueryResult::ChangeContext(result) => format_change_context(&mut out, result),
3094		QueryResult::SymbolGraph(result) => format_symbol_graph(&mut out, result),
3095		QueryResult::GraphPath(result) => format_graph_path(&mut out, result),
3096		QueryResult::IdentityChildren(result) => format_identity_children(&mut out, result),
3097		QueryResult::IdentityGraph(result) => format_identity_graph(&mut out, result),
3098		QueryResult::MetricsCoupling(result) => format_metrics_coupling(&mut out, result),
3099		QueryResult::ResolutionAudit(result) => format_resolution_audit(&mut out, result),
3100		QueryResult::Notes(result) => format_notes(&mut out, result),
3101	}
3102	out
3103}
3104
3105fn format_workspace_status(out: &mut String, status: &WorkspaceStatus) {
3106	let _ = writeln!(out, "workspace: {}", status.root);
3107	let _ = writeln!(
3108		out,
3109		"producer: {} {}",
3110		status.producer.version, status.producer.fingerprint
3111	);
3112	let _ = writeln!(out, "phase: {}", status.phase);
3113	let _ = writeln!(
3114		out,
3115		"files: {} symbols: {} references: {}",
3116		status.files, status.symbols, status.references
3117	);
3118	let _ = writeln!(out, "stale: {} ({})", status.stale, status.stale_summary);
3119	let timings = &status.timings;
3120	let _ = writeln!(
3121		out,
3122		"timings_ms: total={} catalog={} extract={} semantic={} linkage={} changes={}",
3123		timings.total_ms,
3124		timings.source_catalog_ms,
3125		timings.extract_sources_ms,
3126		timings.semantic_index_ms,
3127		timings.linkage_ms,
3128		timings.change_overlay_ms,
3129	);
3130	if status.roots.len() > 1 {
3131		let _ = writeln!(out, "roots:");
3132		for root in &status.roots {
3133			let _ = writeln!(
3134				out,
3135				"- {} files:{} symbols:{} references:{} stale:{}",
3136				root.root, root.files, root.symbols, root.references, root.stale
3137			);
3138		}
3139	}
3140}
3141
3142fn format_tree_children(out: &mut String, result: &TreeChildrenResult, projection: &[String]) {
3143	let _ = writeln!(out, "tree: {}", result.root);
3144	for row in &result.rows {
3145		if !projection.is_empty() {
3146			format_projected_value(out, row, projection);
3147			continue;
3148		}
3149		let kind = match row.kind {
3150			TreeNodeKind::File => "file",
3151			TreeNodeKind::Directory => "dir",
3152		};
3153		let _ = writeln!(
3154			out,
3155			"- {kind} {} defs:{} refs:{}",
3156			row.path, row.defs, row.refs
3157		);
3158	}
3159}
3160
3161fn format_symbol_list(out: &mut String, result: &SymbolListResult, projection: &[String]) {
3162	let _ = writeln!(out, "symbols: {}", result.total);
3163	for row in &result.rows {
3164		if projection.is_empty() {
3165			let _ = writeln!(out, "- {} {} {} {}", row.kind, row.name, row.file, row.uri);
3166		} else {
3167			format_projected_value(out, row, projection);
3168		}
3169	}
3170}
3171
3172fn format_symbol_detail(out: &mut String, result: &SymbolDetailResult) {
3173	let symbol = &result.symbol;
3174	let _ = writeln!(out, "symbol: {} {}", symbol.kind, symbol.name);
3175	let _ = writeln!(out, "uri: {}", symbol.uri);
3176	let _ = writeln!(out, "file: {}", symbol.file);
3177	if let Some(source) = &result.source {
3178		for line in &source.lines {
3179			let _ = writeln!(out, "{:>6} | {}", line.number, line.text);
3180		}
3181	}
3182}
3183
3184fn format_syntax_tree(out: &mut String, result: &SyntaxTreeResult) {
3185	let _ = writeln!(out, "file: {}", result.file);
3186	let _ = writeln!(out, "language: {}", result.language);
3187	let _ = writeln!(out, "focus: {}", result.focus);
3188	let _ = writeln!(
3189		out,
3190		"nodes: {}/{} max_depth:{} truncated:{} parse_error:{}",
3191		result.emitted_nodes,
3192		result.total_nodes,
3193		result.max_depth,
3194		result.truncated,
3195		result.has_error
3196	);
3197	let _ = writeln!(out, "tree:");
3198	format_syntax_node(out, &result.root, 0);
3199}
3200
3201fn format_syntax_node(out: &mut String, node: &SyntaxNodeDto, depth: usize) {
3202	let language = node
3203		.language
3204		.as_deref()
3205		.map(|language| format!(" lang:{language}"))
3206		.unwrap_or_default();
3207	let marker = if node.error {
3208		" error"
3209	} else if node.missing {
3210		" missing"
3211	} else {
3212		""
3213	};
3214	let text = node
3215		.text
3216		.as_deref()
3217		.map(|text| format!(" text={text:?}"))
3218		.unwrap_or_default();
3219	let _ = writeln!(
3220		out,
3221		"{}- {}{} {}:{}-{}:{}{}{}",
3222		"  ".repeat(depth),
3223		node.kind,
3224		language,
3225		node.start.line,
3226		node.start.column,
3227		node.end.line,
3228		node.end.column,
3229		marker,
3230		text
3231	);
3232	for child in &node.children {
3233		format_syntax_node(out, child, depth + 1);
3234	}
3235}
3236
3237fn format_symbol_usages(out: &mut String, result: &SymbolUsagesResult, projection: &[String]) {
3238	let _ = writeln!(out, "uri: {}", result.target.uri);
3239	let _ = writeln!(out, "direction: {}", result.direction.as_str());
3240	let scope = if result.include_descendants {
3241		"descendants"
3242	} else {
3243		"exact"
3244	};
3245	let _ = writeln!(out, "target_scope: {scope} ({} symbols)", result.targets);
3246	let _ = writeln!(out, "usages: {}", result.total);
3247	for row in &result.rows {
3248		if projection.is_empty() {
3249			let _ = writeln!(
3250				out,
3251				"- {} {} {} {}",
3252				row.direction.as_str(),
3253				row.kind,
3254				row.actor,
3255				row.file
3256			);
3257		} else {
3258			format_projected_value(out, row, projection);
3259		}
3260	}
3261}
3262
3263fn format_view_read(out: &mut String, result: &ViewReadResult) {
3264	match result {
3265		ViewReadResult::List(list) => {
3266			let _ = writeln!(out, "views: {}", list.views.len());
3267			for view in &list.views {
3268				let _ = writeln!(out, "- {} ({})", view.id, view.scope);
3269			}
3270		}
3271		ViewReadResult::Detail(detail) => {
3272			let _ = writeln!(out, "view: {}", detail.id);
3273			let _ = writeln!(out, "fragment: {}", detail.fragment);
3274			let _ = writeln!(out, "scope: {}", detail.scope);
3275			let _ = writeln!(
3276				out,
3277				"rules: {} boundaries: {} gotchas: {}",
3278				detail.rules.len(),
3279				detail.boundaries.len(),
3280				detail.gotchas.len()
3281			);
3282		}
3283	}
3284}
3285
3286fn format_projected_value(out: &mut String, value: &impl Serialize, projection: &[String]) {
3287	let Ok(Value::Object(fields)) = serde_json::to_value(value) else {
3288		return;
3289	};
3290	let rendered = projection
3291		.iter()
3292		.filter_map(|name| fields.get(name).map(|value| (name, value)))
3293		.map(|(name, value)| format!("{name}={}", compact_json_value(value)))
3294		.collect::<Vec<_>>()
3295		.join(" ");
3296	let _ = writeln!(out, "- {rendered}");
3297}
3298
3299fn compact_json_value(value: &Value) -> String {
3300	match value {
3301		Value::String(value) => value.clone(),
3302		Value::Null => "-".to_string(),
3303		_ => serde_json::to_string(value).unwrap_or_else(|_| "?".to_string()),
3304	}
3305}
3306
3307fn format_query_describe(out: &mut String, result: &QueryDescribeResult) {
3308	let _ = writeln!(out, "queries: {}", result.capabilities.len());
3309	for capability in &result.capabilities {
3310		let _ = writeln!(
3311			out,
3312			"- {} [{}] read_only={} mcp={} projection={} paginated={}",
3313			capability.name,
3314			capability.category,
3315			capability.read_only,
3316			capability.mcp_tool,
3317			capability.projection,
3318			capability.paginated
3319		);
3320		let fields = capability
3321			.fields
3322			.iter()
3323			.map(|field| {
3324				let required = if field.required { "!" } else { "" };
3325				let multiple = if field.multiple { "[]" } else { "" };
3326				let default = field
3327					.default
3328					.as_deref()
3329					.map_or(String::new(), |value| format!("={value}"));
3330				format!(
3331					"{}{}:{}{}{}",
3332					field.name, required, field.value_type, multiple, default
3333				)
3334			})
3335			.collect::<Vec<_>>()
3336			.join(", ");
3337		let _ = writeln!(out, "  fields: {fields}");
3338		if !capability.projection_fields.is_empty() {
3339			let _ = writeln!(
3340				out,
3341				"  project: {}",
3342				capability.projection_fields.join(", ")
3343			);
3344		}
3345		let _ = writeln!(out, "  example: {}", capability.example);
3346	}
3347}
3348
3349fn format_rules_applicable(out: &mut String, result: &RulesApplicableResult) {
3350	let _ = writeln!(out, "focus: {}", result.file);
3351	let _ = writeln!(out, "language: {}", result.language);
3352	if let Some(kind) = &result.symbol_kind {
3353		let _ = writeln!(out, "symbol_kind: {kind}");
3354	}
3355	let applicable = result
3356		.rows
3357		.iter()
3358		.filter(|row| row.status == "applicable")
3359		.count();
3360	let _ = writeln!(out, "rules: {} applicable: {applicable}", result.total);
3361	for row in &result.rows {
3362		let _ = writeln!(
3363			out,
3364			"- {} [{}] {} — {}",
3365			row.rule.id, row.rule.severity, row.status, row.reason
3366		);
3367	}
3368}
3369
3370fn format_change_context(out: &mut String, result: &ChangeContextResult) {
3371	let _ = writeln!(out, "facts:");
3372	match &result.focus {
3373		SymbolGraphFocus::Symbol { symbol } => {
3374			let _ = writeln!(out, "focus: {} {}", symbol.kind, symbol.name);
3375			let _ = writeln!(out, "uri: {}", symbol.uri);
3376			let _ = writeln!(out, "file: {}", symbol.file);
3377		}
3378		SymbolGraphFocus::File { path } => {
3379			let _ = writeln!(out, "focus: file {path}");
3380		}
3381	}
3382	if let Some(source) = &result.source {
3383		let _ = writeln!(out, "source:");
3384		for line in &source.lines {
3385			let _ = writeln!(out, "{:>6} | {}", line.number, line.text);
3386		}
3387	}
3388	format_context_graph(out, &result.graph);
3389	let coverage = result.coverage;
3390	let _ = writeln!(out, "coverage:");
3391	let _ = writeln!(
3392		out,
3393		"- members {}/{} · internal_edges {}/{} · callers {}/{} · callees {}/{}",
3394		coverage.members_emitted,
3395		coverage.members_total,
3396		coverage.internal_edges_emitted,
3397		coverage.internal_edges_total,
3398		coverage.callers_emitted,
3399		coverage.callers_total,
3400		coverage.callees_emitted,
3401		coverage.callees_total
3402	);
3403	let _ = writeln!(
3404		out,
3405		"- notes {}/{} · rules {}/{} · changes {}/{}",
3406		coverage.notes_emitted,
3407		coverage.notes_total,
3408		coverage.rules_emitted,
3409		coverage.rules_total,
3410		coverage.changes_emitted,
3411		coverage.changes_total
3412	);
3413	if !result.notes.is_empty() {
3414		let _ = writeln!(out, "notes:");
3415		for note in &result.notes {
3416			let _ = writeln!(out, "- {} [{}] {}", note.id, note.status, note.title);
3417		}
3418	}
3419	if !result.rules.is_empty() {
3420		let _ = writeln!(out, "applicable_rules:");
3421		for row in &result.rules {
3422			let _ = writeln!(out, "- {} [{}]", row.rule.id, row.rule.severity);
3423		}
3424	}
3425	format_context_changes(out, &result.changed_files, &result.changed_symbols);
3426	if !result.suggested_checks.is_empty() {
3427		let _ = writeln!(out, "suggested_checks:");
3428		for check in &result.suggested_checks {
3429			let _ = writeln!(out, "- {check}");
3430		}
3431	}
3432}
3433
3434fn format_context_graph(out: &mut String, graph: &SymbolGraphResult) {
3435	if !graph.members.is_empty() {
3436		let _ = writeln!(out, "members:");
3437		for member in &graph.members {
3438			let _ = writeln!(
3439				out,
3440				"- {} {} ({}) {}",
3441				member.kind, member.name, member.file, member.uri
3442			);
3443		}
3444	}
3445	if !graph.internal_edges.is_empty() {
3446		let member_labels: BTreeMap<&str, String> = graph
3447			.members
3448			.iter()
3449			.map(|member| {
3450				(
3451					member.id.as_str(),
3452					format!("{} {}", member.kind, member.name),
3453				)
3454			})
3455			.collect();
3456		let unlisted_labels: BTreeMap<&str, String> = graph
3457			.internal_edges
3458			.iter()
3459			.flat_map(|edge| [edge.source.as_str(), edge.target.as_str()])
3460			.filter(|endpoint| {
3461				endpoint.starts_with("symbol:") && !member_labels.contains_key(endpoint)
3462			})
3463			.collect::<std::collections::BTreeSet<_>>()
3464			.into_iter()
3465			.enumerate()
3466			.map(|(index, endpoint)| {
3467				(
3468					endpoint,
3469					format!("unlisted internal member {}", index.saturating_add(1)),
3470				)
3471			})
3472			.collect();
3473		let endpoint_label = |endpoint: &str| {
3474			member_labels
3475				.get(endpoint)
3476				.or_else(|| unlisted_labels.get(endpoint))
3477				.cloned()
3478				.unwrap_or_else(|| endpoint.to_string())
3479		};
3480		let _ = writeln!(out, "internal_edges:");
3481		for edge in &graph.internal_edges {
3482			let _ = writeln!(
3483				out,
3484				"- {} -> {} x{} [{}]",
3485				endpoint_label(&edge.source),
3486				endpoint_label(&edge.target),
3487				edge.count,
3488				edge.kinds.join(",")
3489			);
3490		}
3491	}
3492	format_unlinked(out, &graph.unlinked);
3493	for (marker, neighbors) in [("<", &graph.callers), (">", &graph.callees)] {
3494		for neighbor in neighbors {
3495			let _ = writeln!(
3496				out,
3497				"{marker} {} {} x{} [{}] {}",
3498				neighbor.symbol.kind,
3499				neighbor.symbol.name,
3500				neighbor.count,
3501				neighbor.kinds.join(","),
3502				neighbor.symbol.uri
3503			);
3504		}
3505	}
3506}
3507
3508fn format_context_changes(
3509	out: &mut String,
3510	files: &[ChangeReviewFile],
3511	symbols: &[ChangeReviewSymbol],
3512) {
3513	if !files.is_empty() {
3514		let _ = writeln!(out, "changed_files:");
3515		for file in files {
3516			let old = file.old_path.as_deref().unwrap_or("-");
3517			let new = file.new_path.as_deref().unwrap_or("-");
3518			let _ = writeln!(out, "- {old} -> {new} [{}]", file.disposition);
3519		}
3520	}
3521	if !symbols.is_empty() {
3522		let _ = writeln!(out, "changed_symbols:");
3523		for symbol in symbols {
3524			let old = symbol
3525				.old
3526				.as_ref()
3527				.map(|side| side.identity.as_str())
3528				.unwrap_or("-");
3529			let new = symbol
3530				.new
3531				.as_ref()
3532				.map(|side| side.identity.as_str())
3533				.unwrap_or("-");
3534			let _ = writeln!(
3535				out,
3536				"- {} [{}] {old} -> {new}",
3537				symbol.kind, symbol.confidence
3538			);
3539		}
3540	}
3541}
3542
3543fn format_resolution_audit(out: &mut String, result: &ResolutionAuditResult) {
3544	let t = &result.totals;
3545	if !result.prefix.is_empty() {
3546		let _ = writeln!(out, "prefix: {}", result.prefix);
3547	}
3548	let _ = writeln!(
3549		out,
3550		"refs: {} unique: {} candidate: {} external: {} sdk: {} dependency: {} injected_external: {} unknown_external: {} dynamic: {} blocked: {} unresolved: {} explained: {} weak_or_unexplained: {} name_match_candidate: {}",
3551		t.references,
3552		t.unique,
3553		t.candidate,
3554		t.external,
3555		t.sdk,
3556		t.dependency,
3557		t.injected_external,
3558		t.unknown_external,
3559		t.dynamic,
3560		t.blocked,
3561		t.unresolved,
3562		t.explained,
3563		t.weak_or_unexplained,
3564		t.name_match_candidate
3565	);
3566	let _ = writeln!(out, "clusters:");
3567	for cluster in &result.clusters {
3568		let _ = writeln!(
3569			out,
3570			"- [{:>6}] {} {}",
3571			cluster.count, cluster.id, cluster.pattern
3572		);
3573		let sample_limit = if result.clusters.len() == 1 {
3574			cluster.samples.len()
3575		} else {
3576			1
3577		};
3578		for sample in cluster.samples.iter().take(sample_limit) {
3579			let location = match sample.line_range {
3580				Some((start, end)) if start == end => format!("{}:{start}", sample.file),
3581				Some((start, end)) => format!("{}:{start}-{end}", sample.file),
3582				None => sample.file.clone(),
3583			};
3584			let _ = writeln!(
3585				out,
3586				"           ex: {} {} {} -> {} evidence:{}",
3587				location, sample.call_name, sample.receiver, sample.target, sample.evidence
3588			);
3589			if !sample.candidates.is_empty() {
3590				let _ = writeln!(
3591					out,
3592					"           candidates: {}",
3593					sample.candidates.join(", ")
3594				);
3595			}
3596			if !sample.constraints.is_empty() {
3597				let _ = writeln!(
3598					out,
3599					"           constraints: {}",
3600					sample.constraints.join(", ")
3601				);
3602			}
3603			if !sample.snippet.is_empty() {
3604				let _ = writeln!(out, "           code: {}", sample.snippet);
3605			}
3606		}
3607	}
3608	let _ = writeln!(out, "zones:");
3609	for zone in &result.zones {
3610		let _ = writeln!(
3611			out,
3612			"- [{:>5}] {} — {}",
3613			zone.unresolved, zone.zone, zone.dominant_pattern
3614		);
3615	}
3616}
3617
3618fn format_symbol_insights(out: &mut String, result: &SymbolInsightsResult) {
3619	let _ = writeln!(out, "files: {}", result.files);
3620	let _ = writeln!(out, "symbols: {}", result.symbols);
3621	let _ = writeln!(out, "refs: {}", result.references);
3622	let _ = writeln!(out, "languages:");
3623	for row in &result.languages {
3624		let _ = writeln!(out, "- {}: {}", row.name, row.count);
3625	}
3626}
3627
3628fn format_notes(out: &mut String, result: &NotesResult) {
3629	let _ = writeln!(out, "action: {}", result.action);
3630	let _ = writeln!(out, "notes: {}", result.total);
3631	for row in &result.rows {
3632		let _ = writeln!(out, "- {} [{}] {}", row.id, row.status, row.title);
3633	}
3634}
3635
3636fn format_rules_list_rows(out: &mut String, result: &RulesListResult) {
3637	for row in &result.rows {
3638		let _ = writeln!(
3639			out,
3640			"- {} [{}] root={} lang={} domain={}",
3641			row.id, row.severity, row.root, row.lang, row.domain
3642		);
3643		if let Some(message) = &row.message {
3644			let _ = writeln!(out, "  message: {message}");
3645		}
3646	}
3647}
3648
3649fn format_identity_children(out: &mut String, result: &IdentityChildrenResult) {
3650	let prefix = if result.prefix.is_empty() {
3651		"<root>"
3652	} else {
3653		&result.prefix
3654	};
3655	let _ = writeln!(out, "prefix: {prefix}");
3656	let _ = writeln!(out, "children: {}", result.children.len());
3657	for child in &result.children {
3658		let marker = if child.symbol.is_some() { "def" } else { "…" };
3659		let _ = writeln!(
3660			out,
3661			"- {} [{}] defs={} {}",
3662			child.segment, marker, child.defs, child.identity
3663		);
3664	}
3665}
3666
3667fn format_unlinked(out: &mut String, unlinked: &UnlinkedRefsDto) {
3668	let _ = writeln!(
3669		out,
3670		"unlinked refs: external {} (sdk {} · dependency {} · injected {} · unknown {}) · candidate {} · dynamic {} · manifest-blocked {} · unresolved {}",
3671		unlinked.external,
3672		unlinked.sdk,
3673		unlinked.dependency,
3674		unlinked.injected_external,
3675		unlinked.unknown_external,
3676		unlinked.candidate,
3677		unlinked.dynamic,
3678		unlinked.manifest_blocked,
3679		unlinked.unresolved
3680	);
3681	if !unlinked.unresolved_reasons.is_empty() {
3682		let reasons = unlinked
3683			.unresolved_reasons
3684			.iter()
3685			.map(|(reason, count)| format!("{reason} {count}"))
3686			.collect::<Vec<_>>()
3687			.join(" · ");
3688		let _ = writeln!(out, "unresolved by reason: {reasons}");
3689	}
3690}
3691
3692fn format_identity_graph(out: &mut String, result: &IdentityGraphResult) {
3693	let prefix = if result.prefix.is_empty() {
3694		"<root>"
3695	} else {
3696		&result.prefix
3697	};
3698	let _ = writeln!(out, "scope: {prefix}");
3699	if !result.path.is_empty() {
3700		let _ = writeln!(out, "path: {}", result.path.join(", "));
3701	}
3702	let _ = writeln!(out, "min_count: {}", result.min_count);
3703	let _ = writeln!(
3704		out,
3705		"coverage: rows {}/{} matching ({} total)",
3706		result.coverage.rows_emitted, result.coverage.rows_matching, result.coverage.rows_total
3707	);
3708	let _ = writeln!(
3709		out,
3710		"nodes: {}/{} edges: {}/{} matching ({} total) ports_in: {}/{} matching ({} total) ports_out: {}/{} matching ({} total)",
3711		result.coverage.nodes_emitted,
3712		result.coverage.nodes_total,
3713		result.coverage.edges_emitted,
3714		result.coverage.edges_matching,
3715		result.coverage.edges_total,
3716		result.coverage.ports_in_emitted,
3717		result.coverage.ports_in_matching,
3718		result.coverage.ports_in_total,
3719		result.coverage.ports_out_emitted,
3720		result.coverage.ports_out_matching,
3721		result.coverage.ports_out_total
3722	);
3723	format_unlinked(out, &result.unlinked);
3724	for node in &result.nodes {
3725		let _ = writeln!(
3726			out,
3727			"- node {} defs:{} children:{}",
3728			node.identity, node.defs, node.has_children
3729		);
3730	}
3731	for edge in &result.edges {
3732		let _ = writeln!(
3733			out,
3734			"- {} -> {} x{} [{}]",
3735			edge.source,
3736			edge.target,
3737			edge.count,
3738			edge.kinds.join(",")
3739		);
3740	}
3741	for port in &result.ports_in {
3742		let _ = writeln!(
3743			out,
3744			"< {} x{} [{}]",
3745			port.identity,
3746			port.count,
3747			port.kinds.join(",")
3748		);
3749	}
3750	for port in &result.ports_out {
3751		let _ = writeln!(
3752			out,
3753			"> {} x{} [{}]",
3754			port.identity,
3755			port.count,
3756			port.kinds.join(",")
3757		);
3758	}
3759}
3760
3761fn format_metrics_coupling(out: &mut String, result: &MetricsCouplingResult) {
3762	let _ = writeln!(out, "from: {}", result.from);
3763	let _ = writeln!(out, "to: {}", result.to);
3764	let _ = writeln!(out, "snapshot: {}", result.snapshot);
3765	if let Some(git) = &result.git {
3766		let _ = writeln!(
3767			out,
3768			"git: branch {} commit {} dirty {}",
3769			git.branch, git.commit, git.dirty
3770		);
3771	}
3772	if !result.relation.is_empty() {
3773		let _ = writeln!(out, "relation: {}", result.relation.join(", "));
3774	}
3775	let export = match (result.export_requested, result.export_recorded) {
3776		(false, _) => "not requested",
3777		(true, true) => "recorded",
3778		(true, false) => "telemetry disabled",
3779	};
3780	let _ = writeln!(out, "otel export: {export}");
3781	let _ = writeln!(
3782		out,
3783		"coupling: references {} connections {} source_symbols {} target_symbols {}",
3784		result.references, result.connections, result.source_symbols, result.target_symbols
3785	);
3786	let _ = writeln!(
3787		out,
3788		"coverage: resolved source references {}/{}",
3789		result.coverage.resolved_source_references, result.coverage.source_references
3790	);
3791	if result.same_symbol_references > 0 {
3792		let _ = writeln!(
3793			out,
3794			"same-symbol references excluded: {}",
3795			result.same_symbol_references
3796		);
3797	}
3798	format_unlinked(out, &result.unlinked);
3799	if !result.by_target.is_empty() {
3800		let _ = writeln!(out, "public boundary usage by target:");
3801		for target in &result.by_target {
3802			let _ = writeln!(out, "- {} x{}", target.moniker, target.references);
3803		}
3804	}
3805	if !result.by_kind.is_empty() {
3806		let _ = writeln!(out, "usage by relation kind:");
3807	}
3808	for kind in &result.by_kind {
3809		let _ = writeln!(out, "- {} x{}", kind.name, kind.count);
3810	}
3811}
3812
3813fn format_symbol_graph(out: &mut String, result: &SymbolGraphResult) {
3814	match &result.focus {
3815		SymbolGraphFocus::Symbol { symbol } => {
3816			let _ = writeln!(
3817				out,
3818				"focus: {} {} ({})",
3819				symbol.kind, symbol.name, symbol.file
3820			);
3821		}
3822		SymbolGraphFocus::File { path } => {
3823			let _ = writeln!(out, "focus: file {path}");
3824		}
3825	}
3826	let _ = writeln!(
3827		out,
3828		"members: {}/{} internal edges: {}/{} matching ({} total)",
3829		result.coverage.members.returned,
3830		result.coverage.members.total,
3831		result.coverage.internal_edges.returned,
3832		result.coverage.internal_edges.matching,
3833		result.coverage.internal_edges.total
3834	);
3835	format_unlinked(out, &result.unlinked);
3836	let _ = writeln!(
3837		out,
3838		"callers: {}/{} matching ({} total)",
3839		result.coverage.callers.returned,
3840		result.coverage.callers.matching,
3841		result.coverage.callers.total
3842	);
3843	for caller in &result.callers {
3844		let _ = writeln!(
3845			out,
3846			"< {} {} ({}) x{} [{}]",
3847			caller.symbol.kind,
3848			caller.symbol.name,
3849			caller.symbol.file,
3850			caller.count,
3851			caller.kinds.join(",")
3852		);
3853	}
3854	let _ = writeln!(
3855		out,
3856		"callees: {}/{} matching ({} total)",
3857		result.coverage.callees.returned,
3858		result.coverage.callees.matching,
3859		result.coverage.callees.total
3860	);
3861	for callee in &result.callees {
3862		let _ = writeln!(
3863			out,
3864			"> {} {} ({}) x{} [{}]",
3865			callee.symbol.kind,
3866			callee.symbol.name,
3867			callee.symbol.file,
3868			callee.count,
3869			callee.kinds.join(",")
3870		);
3871	}
3872}
3873
3874fn format_graph_path(out: &mut String, result: &GraphPathResult) {
3875	let _ = writeln!(
3876		out,
3877		"expectation: {} verdict: {}",
3878		result.expectation.as_str(),
3879		result.verdict.as_str()
3880	);
3881	let _ = writeln!(
3882		out,
3883		"from: {} {} ({})",
3884		result.from.kind, result.from.name, result.from.file
3885	);
3886	let _ = writeln!(
3887		out,
3888		"to: {} {} ({})",
3889		result.to.kind, result.to.name, result.to.file
3890	);
3891	let truth = |value: Option<bool>| match value {
3892		Some(true) => "true",
3893		Some(false) => "false",
3894		None => "unknown",
3895	};
3896	let _ = writeln!(
3897		out,
3898		"reachable: {} no_path: {} path_length: {}",
3899		truth(result.reachable),
3900		truth(result.no_path),
3901		result.path.len()
3902	);
3903	let _ = writeln!(
3904		out,
3905		"coverage: {}% ({}/{}) resolved={} external={} candidate={} dynamic={} manifest_blocked={} unresolved={}",
3906		result.coverage.percent,
3907		result.coverage.decided,
3908		result.coverage.total,
3909		result.coverage.resolved,
3910		result.coverage.external,
3911		result.coverage.candidate,
3912		result.coverage.dynamic,
3913		result.coverage.manifest_blocked,
3914		result.coverage.unresolved
3915	);
3916	let _ = writeln!(
3917		out,
3918		"search: depth={}/{} symbols={} edges={} depth_limit_reached={} symbol_limit_reached={} edge_limit_reached={}",
3919		result.search.depth_reached,
3920		result.search.max_depth,
3921		result.search.explored_symbols,
3922		result.search.explored_edges,
3923		result.search.depth_limit_reached,
3924		result.search.symbol_limit_reached,
3925		result.search.edge_limit_reached
3926	);
3927	if !result.reasons.is_empty() {
3928		let _ = writeln!(out, "reasons: {}", result.reasons.join(", "));
3929	}
3930	for (index, step) in result.path.iter().enumerate() {
3931		let location = step.line_range.map_or_else(
3932			|| step.file.clone(),
3933			|(start, end)| {
3934				if start == end {
3935					format!("{}:L{start}", step.file)
3936				} else {
3937					format!("{}:L{start}-L{end}", step.file)
3938				}
3939			},
3940		);
3941		let _ = writeln!(
3942			out,
3943			"{}. {} -> {} [{}] {}",
3944			index + 1,
3945			step.source.uri,
3946			step.target.uri,
3947			step.relation,
3948			location
3949		);
3950	}
3951}
3952
3953fn format_change_review(out: &mut String, result: &ChangeReviewResult) {
3954	let _ = writeln!(out, "scope: {}", result.scope);
3955	let _ = writeln!(
3956		out,
3957		"files: {} ({} analyzable) symbols: {} refs: {} ({} retargeted) residual: {}",
3958		result.summary.files,
3959		result.summary.analyzable_files,
3960		result.summary.symbol_changes,
3961		result.summary.ref_changes,
3962		result.summary.retargeted_refs,
3963		result.summary.residual_files
3964	);
3965	for file in &result.files {
3966		let path = match (&file.old_path, &file.new_path) {
3967			(Some(old), Some(new)) if old != new => format!("{old} -> {new}"),
3968			(_, Some(new)) => new.clone(),
3969			(Some(old), None) => old.clone(),
3970			(None, None) => "<unknown>".to_string(),
3971		};
3972		let _ = writeln!(
3973			out,
3974			"- {path} {}{}{}",
3975			file.disposition,
3976			if file.analyzable {
3977				""
3978			} else {
3979				" (not analyzable)"
3980			},
3981			if file.coverage_explained {
3982				""
3983			} else {
3984				" [residual]"
3985			}
3986		);
3987	}
3988	for change in &result.symbol_changes {
3989		let side = change.new.as_ref().or(change.old.as_ref());
3990		let Some(side) = side else { continue };
3991		let _ = writeln!(
3992			out,
3993			"  {} {} {} [{}]",
3994			change.kind, side.kind, side.name, change.confidence
3995		);
3996	}
3997	for diagnostic in &result.diagnostics {
3998		let _ = writeln!(out, "diagnostic: {diagnostic}");
3999	}
4000}
4001
4002fn format_diff_impact(out: &mut String, result: &DiffImpactResult) {
4003	let _ = writeln!(out, "scope: {}", result.scope);
4004	let _ = writeln!(
4005		out,
4006		"files: {} ({} analyzable) symbols: {} refs: {} ({} retargeted) residual: {}",
4007		result.summary.files,
4008		result.summary.analyzable_files,
4009		result.summary.symbol_changes,
4010		result.summary.ref_changes,
4011		result.summary.retargeted_refs,
4012		result.summary.residual_files
4013	);
4014	for file in &result.files {
4015		let path = match (&file.old_path, &file.new_path) {
4016			(Some(old), Some(new)) if old != new => format!("{old} -> {new}"),
4017			(_, Some(new)) => new.clone(),
4018			(Some(old), None) => old.clone(),
4019			(None, None) => "<unknown>".to_string(),
4020		};
4021		let _ = writeln!(
4022			out,
4023			"- {path} {}{}{}",
4024			file.disposition,
4025			if file.analyzable {
4026				""
4027			} else {
4028				" (not analyzable)"
4029			},
4030			if file.coverage_explained {
4031				""
4032			} else {
4033				" [residual]"
4034			}
4035		);
4036	}
4037	for change in &result.symbol_changes {
4038		let side = change.new.as_ref().or(change.old.as_ref());
4039		let Some(side) = side else { continue };
4040		let _ = writeln!(
4041			out,
4042			"  {} {} {} [{}]",
4043			change.kind, side.kind, side.compact_identity, change.confidence
4044		);
4045	}
4046	for diagnostic in &result.diagnostics {
4047		let _ = writeln!(out, "diagnostic: {diagnostic}");
4048	}
4049}
4050
4051fn format_rules_check(out: &mut String, result: &RulesCheckResult) {
4052	let _ = writeln!(out, "verdict: {}", result.verdict.as_str());
4053	let _ = writeln!(out, "exit: {}", result.exit);
4054	let _ = writeln!(
4055		out,
4056		"violations: {} errors: {} elapsed_ms: {}",
4057		result.summary.total_violations, result.summary.total_errors, result.summary.elapsed_ms
4058	);
4059	for violation in &result.violations {
4060		let _ = writeln!(
4061			out,
4062			"- {} {}:{}-{} [{}] {}",
4063			violation.root,
4064			violation.path,
4065			violation.lines.0,
4066			violation.lines.1,
4067			violation.rule_id,
4068			violation.message
4069		);
4070	}
4071	if !result.rule_reports.is_empty() {
4072		let _ = writeln!(out, "rule_reports: {}", result.rule_reports.len());
4073		for report in result.rule_reports.iter().filter(|report| {
4074			report.verdict.is_some() || report.coverage.is_some() || report.path_analysis.is_some()
4075		}) {
4076			let _ = writeln!(
4077				out,
4078				"  - {} verdict={} evaluated={} violations={}",
4079				report.rule_id,
4080				report.verdict.as_deref().unwrap_or("not_applicable"),
4081				report.evaluated,
4082				report.violations
4083			);
4084			if let Some(coverage) = &report.coverage {
4085				let _ = writeln!(
4086					out,
4087					"    coverage={}%, minimum={}%",
4088					coverage.percent, coverage.min_percent
4089				);
4090			}
4091			if let Some(path) = &report.path_analysis {
4092				let _ = writeln!(
4093					out,
4094					"    path expect={} pairs={} explored_symbols={} explored_edges={} witness_steps={}",
4095					path.expectation,
4096					path.evaluated_pairs,
4097					path.explored_symbols,
4098					path.explored_edges,
4099					path.witness.len()
4100				);
4101			}
4102		}
4103	}
4104}
4105
4106#[derive(Default)]
4107struct FieldBag {
4108	values: Vec<(String, FieldValue)>,
4109	positional: Vec<String>,
4110	projection: Vec<String>,
4111	consistency: Consistency,
4112}
4113
4114#[derive(Clone, Debug, Eq, PartialEq)]
4115struct FieldValue {
4116	text: String,
4117	quoted: bool,
4118}
4119
4120#[derive(Clone, Debug, Eq, PartialEq)]
4121struct QueryToken {
4122	text: String,
4123	quoted: bool,
4124}
4125
4126impl FieldBag {
4127	fn bool(&self, key: &str) -> Result<Option<bool>, QueryParseError> {
4128		self.one(key)
4129			.map(|value| match value.as_str() {
4130				"true" => Ok(true),
4131				"false" => Ok(false),
4132				_ => Err(QueryParseError::InvalidValue {
4133					key: key.to_string(),
4134					value,
4135				}),
4136			})
4137			.transpose()
4138	}
4139
4140	fn page(&self) -> Result<Page, QueryParseError> {
4141		let limit = self.usize("limit")?.unwrap_or(80);
4142		let cursor = self
4143			.one("cursor")
4144			.map(|value| parse_cursor(&value))
4145			.transpose()?;
4146		Ok(Page { cursor, limit })
4147	}
4148
4149	fn usize(&self, key: &str) -> Result<Option<usize>, QueryParseError> {
4150		self.one(key)
4151			.map(|value| {
4152				value
4153					.parse::<usize>()
4154					.map_err(|_| QueryParseError::InvalidValue {
4155						key: key.to_string(),
4156						value,
4157					})
4158			})
4159			.transpose()
4160	}
4161
4162	fn one(&self, key: &str) -> Option<String> {
4163		self.values
4164			.iter()
4165			.find(|(candidate, _)| candidate == key)
4166			.map(|(_, value)| value.text.clone())
4167	}
4168
4169	fn many(&self, key: &str) -> Vec<String> {
4170		self.values
4171			.iter()
4172			.filter(|(candidate, _)| candidate == key)
4173			.flat_map(|(_, value)| {
4174				if value.quoted || !MULTI_VALUE_FIELDS.contains(&key) {
4175					vec![value.text.clone()]
4176				} else {
4177					split_csv(strip_bracket_list(key, &value.text))
4178				}
4179			})
4180			.collect()
4181	}
4182}
4183
4184fn collect_tokens(
4185	tokens: &[QueryToken],
4186	fields: &mut FieldBag,
4187	positional: &mut Vec<String>,
4188) -> Result<(), QueryParseError> {
4189	for token in tokens {
4190		if let Some((key, value)) = token.text.split_once(':') {
4191			fields.values.push((
4192				key.to_string(),
4193				FieldValue {
4194					text: value.to_string(),
4195					quoted: token.quoted,
4196				},
4197			));
4198		} else {
4199			positional.push(token.text.trim_end_matches(',').to_string());
4200		}
4201	}
4202	Ok(())
4203}
4204
4205fn parse_cursor(value: &str) -> Result<QueryCursor, QueryParseError> {
4206	let Some((generation, offset)) = value.split_once(':') else {
4207		return Err(QueryParseError::InvalidValue {
4208			key: "cursor".to_string(),
4209			value: value.to_string(),
4210		});
4211	};
4212	let generation = generation
4213		.parse::<u64>()
4214		.map_err(|_| QueryParseError::InvalidValue {
4215			key: "cursor".to_string(),
4216			value: value.to_string(),
4217		})?;
4218	let offset = offset
4219		.parse::<usize>()
4220		.map_err(|_| QueryParseError::InvalidValue {
4221			key: "cursor".to_string(),
4222			value: value.to_string(),
4223		})?;
4224	Ok(QueryCursor::new(
4225		offset,
4226		Some(WorkspaceGeneration(generation)),
4227	))
4228}
4229
4230fn tokenize(input: &str) -> Result<Vec<QueryToken>, QueryParseError> {
4231	let mut tokens = Vec::new();
4232	let mut current = String::new();
4233	let mut chars = input.chars().peekable();
4234	let mut quoted = false;
4235	let mut token_quoted = false;
4236	while let Some(ch) = chars.next() {
4237		match ch {
4238			'"' => {
4239				quoted = !quoted;
4240				token_quoted = true;
4241			}
4242			'\\' if quoted => {
4243				if let Some(next) = chars.next() {
4244					current.push(next);
4245				}
4246			}
4247			ch if ch.is_whitespace() && !quoted => {
4248				if !current.is_empty() {
4249					tokens.push(QueryToken {
4250						text: std::mem::take(&mut current),
4251						quoted: std::mem::take(&mut token_quoted),
4252					});
4253				}
4254			}
4255			ch => current.push(ch),
4256		}
4257	}
4258	if quoted {
4259		return Err(QueryParseError::InvalidToken(input.to_string()));
4260	}
4261	if !current.is_empty() {
4262		tokens.push(QueryToken {
4263			text: current,
4264			quoted: token_quoted,
4265		});
4266	}
4267	Ok(tokens)
4268}
4269
4270// `shape:[callable,type]` list sugar, restricted to enum-like fields so glob
4271// character classes in `path:`/`file:` values stay untouched.
4272fn strip_bracket_list<'a>(key: &str, value: &'a str) -> &'a str {
4273	if !BRACKET_LIST_FIELDS.contains(&key) {
4274		return value;
4275	}
4276	value
4277		.strip_prefix('[')
4278		.and_then(|inner| inner.strip_suffix(']'))
4279		.unwrap_or(value)
4280}
4281
4282pub fn split_csv(value: &str) -> Vec<String> {
4283	value
4284		.split(',')
4285		.map(str::trim)
4286		.filter(|entry| !entry.is_empty())
4287		.map(ToOwned::to_owned)
4288		.collect()
4289}
4290
4291#[cfg(test)]
4292mod tests {
4293	use super::*;
4294	use serde::Serialize;
4295
4296	fn serialized_fields(value: impl Serialize) -> Vec<String> {
4297		let mut fields = serde_json::to_value(value)
4298			.expect("serialize query DTO")
4299			.as_object()
4300			.expect("query DTO object")
4301			.keys()
4302			.cloned()
4303			.collect::<Vec<_>>();
4304		fields.sort();
4305		fields
4306	}
4307
4308	fn dto_fields(verb: &str) -> Vec<String> {
4309		match verb {
4310			"query.describe" => serialized_fields(QueryDescribeQuery::default()),
4311			"workspace.status" => Vec::new(),
4312			"tree.children" => serialized_fields(TreeChildrenQuery::default()),
4313			"symbol.search" | "symbol.insights" => serialized_fields(SymbolSearchQuery::default()),
4314			"symbol.detail" => serialized_fields(SymbolDetailQuery {
4315				workspace: None,
4316				uri: String::new(),
4317				context_lines: 0,
4318			}),
4319			"syntax.tree" => serialized_fields(SyntaxTreeQuery {
4320				workspace: None,
4321				focus: String::new(),
4322				max_depth: 0,
4323				max_nodes: 0,
4324				named_only: true,
4325				include_text: false,
4326				max_text_chars: 0,
4327			}),
4328			"syntax.parse" => serialized_fields(SyntaxParseQuery {
4329				language: String::new(),
4330				source: String::new(),
4331				uri: None,
4332				max_depth: 0,
4333				max_nodes: 0,
4334				named_only: true,
4335				include_text: false,
4336				max_text_chars: 0,
4337			}),
4338			"symbol.usages" => serialized_fields(SymbolUsagesQuery {
4339				workspace: None,
4340				uri: String::new(),
4341				direction: UsageDirection::Incoming,
4342				path: Vec::new(),
4343				lang: Vec::new(),
4344				include_descendants: false,
4345				projection: Vec::new(),
4346			}),
4347			"view.read" => serialized_fields(ViewReadQuery {
4348				uri: String::new(),
4349				scheme: None,
4350				context_lines: 0,
4351				include_code: false,
4352			}),
4353			"rules.list" => serialized_fields(RulesListQuery::default()),
4354			"rules.check" => serialized_fields(RulesCheckQuery::default()),
4355			"rules.applicable" => serialized_fields(RulesApplicableQuery::default()),
4356			"change.review" => serialized_fields(ChangeReviewQuery::default()),
4357			"change.context" => serialized_fields(ChangeContextQuery::default()),
4358			"symbol.graph" => serialized_fields(SymbolGraphQuery::default()),
4359			"graph.path" => serialized_fields(GraphPathQuery::default()),
4360			"identity.children" => serialized_fields(IdentityChildrenQuery::default()),
4361			"identity.graph" => serialized_fields(IdentityGraphQuery::default()),
4362			"metrics.coupling" => serialized_fields(MetricsCouplingQuery::default()),
4363			"resolution.audit" => serialized_fields(ResolutionAuditQuery::default()),
4364			"notes" => serialized_fields(NotesQuery {
4365				action: NotesAction::List,
4366				id: None,
4367				moniker: None,
4368				kind: None,
4369				status: None,
4370				title: None,
4371				body: None,
4372				created_by: None,
4373				orphan: None,
4374				include_done: false,
4375			}),
4376			other => panic!("missing DTO field fixture for {other}"),
4377		}
4378	}
4379
4380	#[test]
4381	fn capability_registry_fields_exist_on_query_dtos() {
4382		for spec in query_capability_specs() {
4383			let dto = dto_fields(spec.name);
4384			for field in spec.fields {
4385				assert!(
4386					dto.iter().any(|candidate| candidate == field),
4387					"{} field `{field}` missing from DTO fields {dto:?}",
4388					spec.name
4389				);
4390			}
4391			assert!(
4392				spec.fields
4393					.iter()
4394					.all(|field| !COMMON_FIELDS.contains(field)),
4395				"{} repeats a common request field",
4396				spec.name
4397			);
4398		}
4399	}
4400
4401	#[test]
4402	fn describes_live_query_contract() {
4403		let request = parse_query("query.describe symbol.usages").expect("query describe");
4404		assert!(matches!(
4405			request.query,
4406			Query::QueryDescribe(QueryDescribeQuery { verb: Some(ref verb) })
4407				if verb == "symbol.usages"
4408		));
4409		let result = describe_query_capabilities(Some("symbol.usages")).expect("capability");
4410		let capability = result.capabilities.first().expect("described query");
4411		assert!(capability.read_only);
4412		assert_eq!(capability.mcp_tool, "code_moniker_usages");
4413		assert!(capability.projection);
4414		assert!(capability.paginated);
4415		assert!(
4416			capability
4417				.fields
4418				.iter()
4419				.any(|field| field.name == "uri" && field.required)
4420		);
4421		assert!(capability.projection_fields.contains(&"actor".to_string()));
4422		assert_eq!(
4423			CapabilitySet::default().query_mcp_tools["symbol.usages"],
4424			"code_moniker_usages"
4425		);
4426	}
4427
4428	#[test]
4429	fn parses_bounded_syntax_tree_contract() {
4430		let request = parse_query(
4431			"syntax.tree focus:\"src/service.ts\" max_depth:4 max_nodes:120 named_only:false include_text:true max_text_chars:40",
4432		)
4433		.expect("syntax tree query");
4434		assert_eq!(
4435			request.query,
4436			Query::SyntaxTree(SyntaxTreeQuery {
4437				workspace: None,
4438				focus: "src/service.ts".to_string(),
4439				max_depth: 4,
4440				max_nodes: 120,
4441				named_only: false,
4442				include_text: true,
4443				max_text_chars: 40,
4444			})
4445		);
4446	}
4447
4448	#[test]
4449	fn parses_stateless_syntax_contract() {
4450		let request = parse_query(
4451			"syntax.parse language:\"rs\" source:\"fn main() {}\" uri:\"snippet.rs\" max_depth:4 max_nodes:120",
4452		)
4453		.expect("stateless syntax query");
4454		assert_eq!(
4455			request.query,
4456			Query::SyntaxParse(SyntaxParseQuery {
4457				language: "rs".to_string(),
4458				source: "fn main() {}".to_string(),
4459				uri: Some("snippet.rs".to_string()),
4460				max_depth: 4,
4461				max_nodes: 120,
4462				named_only: true,
4463				include_text: false,
4464				max_text_chars: SYNTAX_TREE_DEFAULT_MAX_TEXT_CHARS,
4465			})
4466		);
4467		assert!(!request.query.requires_workspace_snapshot());
4468
4469		let request = parse_query(
4470			"syntax.parse language:\"sql\" source:\"SELECT 1\" max_depth:1000 max_nodes:20000",
4471		)
4472		.expect("client-selected syntax budgets");
4473		let Query::SyntaxParse(query) = request.query else {
4474			panic!("expected syntax.parse query");
4475		};
4476		assert_eq!(query.max_depth, 1_000);
4477		assert_eq!(query.max_nodes, 20_000);
4478	}
4479
4480	#[test]
4481	fn parses_symbol_graph_relational_filters() {
4482		let request = parse_query(
4483			"symbol.graph focus:\"src/lib.rs\" direction:incoming relation:[calls,uses_type] min_count:2 include_internal:false",
4484		)
4485		.expect("symbol graph filters");
4486		let Query::SymbolGraph(query) = request.query else {
4487			panic!("expected symbol graph query");
4488		};
4489		assert_eq!(query.direction, UsageDirection::Incoming);
4490		assert_eq!(query.relation, vec!["calls", "uses_type"]);
4491		assert_eq!(query.min_count, 2);
4492		assert!(!query.include_internal);
4493	}
4494
4495	#[test]
4496	fn parses_bounded_graph_path_contract() {
4497		let request = parse_query(
4498			"graph.path from:\"code+moniker://./fn:callback()\" to:\"code+moniker://./fn:repository()\" expect:no_path relation:[calls,method_call] max_depth:6 max_symbols:200 max_edges:500 min_coverage:95",
4499		)
4500		.expect("graph path contract");
4501		let Query::GraphPath(query) = request.query else {
4502			panic!("expected graph path query");
4503		};
4504		assert_eq!(query.from, "code+moniker://./fn:callback()");
4505		assert_eq!(query.to, "code+moniker://./fn:repository()");
4506		assert_eq!(query.expect, GraphPathExpectation::NoPath);
4507		assert_eq!(query.relation, vec!["calls", "method_call"]);
4508		assert_eq!(query.max_depth, 6);
4509		assert_eq!(query.max_symbols, 200);
4510		assert_eq!(query.max_edges, 500);
4511		assert_eq!(query.min_coverage, 95);
4512	}
4513
4514	#[test]
4515	fn rejects_unbounded_graph_path_limits() {
4516		assert!(matches!(
4517			parse_query(
4518				"graph.path from:\"symbol:0:0\" to:\"symbol:0:1\" max_depth:65"
4519			),
4520			Err(QueryParseError::InvalidValue { ref key, .. }) if key == "max_depth"
4521		));
4522		assert!(matches!(
4523			parse_query(
4524				"graph.path from:\"symbol:0:0\" to:\"symbol:0:1\" min_coverage:101"
4525			),
4526			Err(QueryParseError::InvalidValue { ref key, .. }) if key == "min_coverage"
4527		));
4528		assert!(matches!(
4529			parse_query(
4530				"graph.path from:\"symbol:0:0\" to:\"symbol:0:1\" max_symbols:0"
4531			),
4532			Err(QueryParseError::InvalidValue { ref key, .. }) if key == "max_symbols"
4533		));
4534	}
4535
4536	#[test]
4537	fn preserves_commas_in_quoted_symbol_uri() {
4538		let uri = "code+moniker://./lang:ts/dir:src/module:Session/class:Session/method:launchRequest(response:DebugProtocol.LaunchResponse,args:LaunchRequestArguments)";
4539		let request = parse_query(&format!(
4540			"symbol.usages uri:\"{uri}\" direction:outgoing limit:5"
4541		))
4542		.expect("quoted symbol URI");
4543		let Query::SymbolUsages(query) = request.query else {
4544			panic!("expected symbol usages query");
4545		};
4546		assert_eq!(query.uri, uri);
4547	}
4548
4549	#[test]
4550	fn quoted_multi_value_field_does_not_split_commas() {
4551		let request =
4552			parse_query("symbol.search path:\"src/generated,a.ts\" shape:\"callable,type\"")
4553				.expect("quoted multi-value fields");
4554		let Query::SymbolSearch(query) = request.query else {
4555			panic!("expected symbol search query");
4556		};
4557		assert_eq!(query.path, vec!["src/generated,a.ts"]);
4558		assert_eq!(query.shape, vec!["callable,type"]);
4559	}
4560
4561	#[test]
4562	fn symbol_usages_can_roll_up_descendant_member_activity_explicitly() {
4563		let request = parse_query(
4564			"symbol.usages uri:\"symbol:1:2\" direction:incoming include_descendants:true",
4565		)
4566		.expect("owner rollup usage query");
4567		let Query::SymbolUsages(query) = request.query else {
4568			panic!("expected symbol usages query");
4569		};
4570		assert!(query.include_descendants);
4571		let capability = describe_query_capabilities(Some("symbol.usages"))
4572			.expect("symbol usages capability")
4573			.capabilities
4574			.pop()
4575			.expect("symbol usages descriptor");
4576		let field = capability
4577			.fields
4578			.iter()
4579			.find(|field| field.name == "include_descendants")
4580			.expect("descendant scope field");
4581		assert_eq!(field.value_type, "boolean");
4582		assert_eq!(field.default.as_deref(), Some("false"));
4583	}
4584
4585	#[test]
4586	fn unquoted_multi_value_fields_still_split_commas() {
4587		let request = parse_query("symbol.search path:src,tests shape:callable,type")
4588			.expect("unquoted multi-value fields");
4589		let Query::SymbolSearch(query) = request.query else {
4590			panic!("expected symbol search query");
4591		};
4592		assert_eq!(query.path, vec!["src", "tests"]);
4593		assert_eq!(query.shape, vec!["callable", "type"]);
4594	}
4595
4596	#[test]
4597	fn identity_graph_accepts_path_scope_without_changing_identity_children() {
4598		let request = parse_query(
4599			"identity.graph prefix:\"lang:java/package:com/package:acme\" path:src/main/**,src/test/**",
4600		)
4601		.expect("path-scoped identity graph");
4602		let Query::IdentityGraph(query) = request.query else {
4603			panic!("expected identity graph query");
4604		};
4605		assert_eq!(query.path, vec!["src/main/**", "src/test/**"]);
4606
4607		assert!(matches!(
4608			parse_query("identity.children path:src/main/**"),
4609			Err(QueryParseError::UnknownField { .. })
4610		));
4611	}
4612
4613	#[test]
4614	fn identity_graph_declares_weight_filter_and_real_pagination() {
4615		let request = parse_query("identity.graph prefix:\"lang:rs/dir:src\" min_count:3 limit:2")
4616			.expect("filtered identity graph");
4617		let Query::IdentityGraph(query) = request.query else {
4618			panic!("expected identity graph query");
4619		};
4620		assert_eq!(query.min_count, 3);
4621		assert_eq!(request.page.limit, 2);
4622		assert!(
4623			query_capability_spec("identity.graph")
4624				.expect("identity graph capability")
4625				.paginated
4626		);
4627	}
4628
4629	#[test]
4630	fn coupling_metrics_requires_explicit_scopes_and_accepts_relations() {
4631		let request = parse_query(
4632			"metrics.coupling from:\"lang:java/package:com/package:acme\" to:\"lang:java/package:com/package:storage\" relation:calls,imports_symbol snapshot:current export:true",
4633		)
4634		.expect("coupling metrics query");
4635		let Query::MetricsCoupling(query) = request.query else {
4636			panic!("expected coupling metrics query");
4637		};
4638		assert_eq!(query.from, "lang:java/package:com/package:acme");
4639		assert_eq!(query.to, "lang:java/package:com/package:storage");
4640		assert_eq!(query.relation, vec!["calls", "imports_symbol"]);
4641		assert_eq!(query.snapshot.as_deref(), Some("current"));
4642		assert!(query.export);
4643		let Query::MetricsCoupling(default_export) =
4644			parse_query("metrics.coupling from:\"lang:java\" to:\"lang:java/package:com\"")
4645				.expect("coupling metrics without export")
4646				.query
4647		else {
4648			panic!("expected coupling metrics query");
4649		};
4650		assert!(!default_export.export);
4651		assert!(matches!(
4652			parse_query("metrics.coupling from:\"lang:java\""),
4653			Err(QueryParseError::MissingRequired("to"))
4654		));
4655		assert!(
4656			!query_capability_spec("metrics.coupling")
4657				.expect("metrics capability")
4658				.paginated
4659		);
4660	}
4661
4662	#[test]
4663	fn scalar_field_preserves_unquoted_regex_commas() {
4664		let request = parse_query("symbol.search name:^launch(Request){1,3}$").expect("name regex");
4665		let Query::SymbolSearch(query) = request.query else {
4666			panic!("expected symbol search query");
4667		};
4668		assert_eq!(query.name.as_deref(), Some("^launch(Request){1,3}$"));
4669	}
4670
4671	#[test]
4672	fn parses_resolution_audit_positional_prefix() {
4673		let request = parse_query("resolution.audit java limit:7 cluster:resolution-abc")
4674			.expect("audit query");
4675		let Query::ResolutionAudit(query) = request.query else {
4676			panic!("expected resolution audit query");
4677		};
4678		assert_eq!(query.prefix, "java");
4679		assert_eq!(query.limit, 7);
4680		assert_eq!(query.cluster.as_deref(), Some("resolution-abc"));
4681	}
4682
4683	#[test]
4684	fn resolution_audit_default_limit_matches_its_documented_contract() {
4685		let request = parse_query("resolution.audit python").expect("audit query");
4686		let Query::ResolutionAudit(query) = request.query else {
4687			panic!("expected resolution audit query");
4688		};
4689		assert_eq!(query.limit, 20);
4690	}
4691
4692	#[test]
4693	fn formats_resolution_audit_explanation_metrics() {
4694		let response = QueryResponse {
4695			generation: None,
4696			result: QueryResult::ResolutionAudit(Box::new(ResolutionAuditResult {
4697				prefix: "lang:python".to_string(),
4698				totals: AuditTotalsDto {
4699					references: 10,
4700					resolved: 4,
4701					unique: 4,
4702					candidate: 2,
4703					external: 1,
4704					sdk: 1,
4705					dependency: 0,
4706					injected_external: 0,
4707					unknown_external: 0,
4708					dynamic: 1,
4709					blocked: 1,
4710					unresolved: 1,
4711					explained: 9,
4712					weak_or_unexplained: 3,
4713					name_match_resolved: 0,
4714					name_match_candidate: 2,
4715				},
4716				clusters: vec![AuditClusterDto {
4717					id: "resolution-abc".to_string(),
4718					pattern: "candidate name_match/method_call".to_string(),
4719					count: 2,
4720					samples: vec![
4721						AuditSampleDto {
4722							file: "src/a.py".to_string(),
4723							line_range: Some((10, 10)),
4724							snippet: "value.first()".to_string(),
4725							source: "module:a".to_string(),
4726							call_name: "first".to_string(),
4727							receiver: "value".to_string(),
4728							target: "method:first".to_string(),
4729							evidence: "name_match".to_string(),
4730							constraints: vec!["scope:global".to_string()],
4731							candidates: vec!["class:A/method:first".to_string()],
4732						},
4733						AuditSampleDto {
4734							file: "src/b.py".to_string(),
4735							line_range: Some((20, 21)),
4736							snippet: "other.second()".to_string(),
4737							source: "module:b".to_string(),
4738							call_name: "second".to_string(),
4739							receiver: "other".to_string(),
4740							target: "method:second".to_string(),
4741							evidence: "name_match".to_string(),
4742							constraints: Vec::new(),
4743							candidates: Vec::new(),
4744						},
4745					],
4746				}],
4747				zones: Vec::new(),
4748			})),
4749			next_cursor: None,
4750		};
4751
4752		let formatted = format_query_response(&response);
4753
4754		assert!(formatted.contains("unique: 4 candidate: 2"));
4755		assert!(formatted.contains("explained: 9 weak_or_unexplained: 3"));
4756		assert!(formatted.contains("name_match_candidate: 2"));
4757		assert!(formatted.contains("resolution-abc"));
4758		assert!(formatted.contains("first value"));
4759		assert!(formatted.contains("second other"));
4760		assert!(formatted.contains("src/a.py:10"));
4761		assert!(formatted.contains("src/b.py:20-21"));
4762		assert!(formatted.contains("constraints: scope:global"));
4763		assert!(formatted.contains("code: value.first()"));
4764	}
4765
4766	#[test]
4767	fn rejects_unknown_projection_field_with_suggestion() {
4768		let error =
4769			parse_query("symbol.search name:App\nproject nme uri").expect_err("unknown projection");
4770		let message = error.to_string();
4771		assert!(
4772			message.contains("unknown projection field `nme`"),
4773			"{message}"
4774		);
4775		assert!(message.contains("did you mean `name`?"), "{message}");
4776	}
4777
4778	#[test]
4779	fn projected_formatter_emits_only_requested_symbol_fields() {
4780		let response = QueryResponse {
4781			generation: Some(WorkspaceGeneration(3)),
4782			result: QueryResult::SymbolList(SymbolListResult {
4783				rows: vec![SymbolDto {
4784					root: ".".to_string(),
4785					uri: "code+moniker://./lang:rs/fn:run()".to_string(),
4786					id: "id".to_string(),
4787					name: "run".to_string(),
4788					kind: "fn".to_string(),
4789					visibility: "public".to_string(),
4790					signature: "run()".to_string(),
4791					file: "src/lib.rs".to_string(),
4792					language: "rs".to_string(),
4793					line_range: Some((4, 8)),
4794					navigable: true,
4795					score: None,
4796					match_reason: None,
4797					source: None,
4798				}],
4799				total: 1,
4800			}),
4801			next_cursor: None,
4802		};
4803		let formatted =
4804			format_query_response_projected(&response, &["name".to_string(), "uri".to_string()]);
4805		assert!(
4806			formatted.contains("name=run uri=code+moniker://"),
4807			"{formatted}"
4808		);
4809		assert!(!formatted.contains("src/lib.rs"), "{formatted}");
4810		assert!(!formatted.contains("signature="), "{formatted}");
4811	}
4812
4813	#[test]
4814	fn parses_human_symbol_search() {
4815		let query = parse_query(
4816			r#"symbol.search "SharedWorkspaceIndex"
4817  filter path:"crates/**" shape:type
4818  project name, kind, uri
4819  page limit:20 cursor:7:40"#,
4820		)
4821		.expect("query");
4822		assert_eq!(query.page.limit, 20);
4823		assert_eq!(
4824			query.page.cursor,
4825			Some(QueryCursor::new(40, Some(WorkspaceGeneration(7))))
4826		);
4827		match query.query {
4828			Query::SymbolSearch(search) => {
4829				assert_eq!(search.text.as_deref(), Some("SharedWorkspaceIndex"));
4830				assert_eq!(search.path, vec!["crates/**"]);
4831				assert_eq!(search.shape, vec!["type"]);
4832				assert_eq!(search.projection, vec!["name", "kind", "uri"]);
4833			}
4834			other => panic!("unexpected query {other:?}"),
4835		}
4836	}
4837
4838	#[test]
4839	fn parses_rules_check_consistency() {
4840		let query = parse_query(
4841			r#"rules.check profile:"agent"
4842  consistency refresh-if-stale
4843  page limit:50"#,
4844		)
4845		.expect("query");
4846		assert_eq!(query.consistency, Consistency::RefreshIfStale);
4847		assert_eq!(query.page.limit, 50);
4848	}
4849
4850	#[test]
4851	fn rejects_offset_only_human_cursor() {
4852		let error =
4853			parse_query("symbol.search Customer\npage cursor:40").expect_err("offset-only cursor");
4854		assert!(matches!(
4855			error,
4856			QueryParseError::InvalidValue { ref key, .. } if key == "cursor"
4857		));
4858	}
4859
4860	#[test]
4861	fn parses_bracket_list_shape() {
4862		let query = parse_query("symbol.search shape:[callable,type] limit:5").expect("query");
4863		match query.query {
4864			Query::SymbolSearch(search) => assert_eq!(search.shape, vec!["callable", "type"]),
4865			other => panic!("unexpected query {other:?}"),
4866		}
4867	}
4868
4869	#[test]
4870	fn rejects_unterminated_bracket_list() {
4871		let error = parse_query("symbol.search shape:[callable").expect_err("unterminated list");
4872		assert!(matches!(
4873			error,
4874			QueryParseError::InvalidValue { ref key, .. } if key == "shape"
4875		));
4876	}
4877
4878	#[test]
4879	fn rejects_unknown_field_with_alias_suggestion() {
4880		let error = parse_query(r#"symbol.search text:"foo""#).expect_err("unknown field");
4881		let message = error.to_string();
4882		assert!(
4883			message.contains("unknown field `text` for `symbol.search`"),
4884			"{message}"
4885		);
4886		assert!(message.contains("did you mean `name`?"), "{message}");
4887	}
4888
4889	#[test]
4890	fn rejects_typo_field_with_suggestion() {
4891		let error = parse_query("rules.check profil:agent").expect_err("typo field");
4892		let message = error.to_string();
4893		assert!(message.contains("did you mean `profile`?"), "{message}");
4894	}
4895
4896	#[test]
4897	fn lists_valid_fields_without_close_match() {
4898		let error = parse_query("change.review foobarbaz:1").expect_err("unknown field");
4899		let message = error.to_string();
4900		assert!(
4901			message.contains("valid fields: consistency, cursor, limit, workspace"),
4902			"{message}"
4903		);
4904	}
4905
4906	#[test]
4907	fn rejects_unexpected_positional() {
4908		let error = parse_query("workspace.status extra").expect_err("positional");
4909		assert!(matches!(
4910			error,
4911			QueryParseError::UnexpectedArgument { ref value, .. } if value == "extra"
4912		));
4913	}
4914
4915	#[test]
4916	fn rejects_projection_on_unsupported_verb() {
4917		let error = parse_query("rules.list\nproject name").expect_err("projection");
4918		assert!(matches!(
4919			error,
4920			QueryParseError::UnsupportedProjection { .. }
4921		));
4922	}
4923
4924	#[test]
4925	fn parses_inline_consistency() {
4926		let query =
4927			parse_query("rules.check profile:agent consistency:refresh-if-stale").expect("query");
4928		assert_eq!(query.consistency, Consistency::RefreshIfStale);
4929	}
4930
4931	#[test]
4932	fn formats_generation_aware_cursor() {
4933		let response = QueryResponse {
4934			generation: Some(WorkspaceGeneration(7)),
4935			result: QueryResult::SymbolList(SymbolListResult {
4936				rows: Vec::new(),
4937				total: 0,
4938			}),
4939			next_cursor: Some(QueryCursor::new(40, Some(WorkspaceGeneration(7)))),
4940		};
4941		let formatted = format_query_response(&response);
4942		assert!(formatted.contains("next_cursor: 7:40"));
4943	}
4944
4945	#[test]
4946	fn context_graph_internal_edges_join_member_names() {
4947		let member = |id: &str, name: &str| SymbolDto {
4948			root: String::new(),
4949			uri: format!("code+moniker://./module:m/fn:{name}"),
4950			id: id.to_string(),
4951			name: name.to_string(),
4952			kind: "fn".to_string(),
4953			visibility: "pub".to_string(),
4954			signature: String::new(),
4955			file: "src/lib.rs".to_string(),
4956			language: "rs".to_string(),
4957			line_range: None,
4958			navigable: true,
4959			score: None,
4960			match_reason: None,
4961			source: None,
4962		};
4963		let graph = SymbolGraphResult {
4964			focus: SymbolGraphFocus::File {
4965				path: "src/lib.rs".to_string(),
4966			},
4967			coverage: SymbolGraphCoverage::default(),
4968			members: vec![
4969				member("symbol:40:2", "alpha()"),
4970				member("symbol:40:7", "beta()"),
4971			],
4972			internal_edges: vec![
4973				SymbolGraphEdge {
4974					source: "symbol:40:2".to_string(),
4975					target: "symbol:40:7".to_string(),
4976					kinds: vec!["calls".to_string()],
4977					count: 2,
4978				},
4979				SymbolGraphEdge {
4980					source: "symbol:40:2".to_string(),
4981					target: "symbol:99:9".to_string(),
4982					kinds: vec!["reads".to_string()],
4983					count: 1,
4984				},
4985				SymbolGraphEdge {
4986					source: "symbol:98:8".to_string(),
4987					target: "symbol:99:9".to_string(),
4988					kinds: vec!["calls".to_string()],
4989					count: 1,
4990				},
4991			],
4992			callers: Vec::new(),
4993			callees: Vec::new(),
4994			unlinked: UnlinkedRefsDto::default(),
4995		};
4996		let mut out = String::new();
4997		format_context_graph(&mut out, &graph);
4998		assert!(
4999			out.contains("- fn alpha() -> fn beta() x2 [calls]"),
5000			"internal edges must join member names, got:\n{out}"
5001		);
5002		assert!(
5003			out.contains("- fn alpha() -> unlisted internal member 2 x1 [reads]"),
5004			"bounded contexts must explain omitted endpoints, got:\n{out}"
5005		);
5006		assert!(
5007			out.contains("- unlisted internal member 1 -> unlisted internal member 2 x1 [calls]"),
5008			"distinct omitted members must preserve the graph topology, got:\n{out}"
5009		);
5010		assert!(
5011			!out.contains("symbol:98:8") && !out.contains("symbol:99:9"),
5012			"internal storage ordinals must not leak into agent output:\n{out}"
5013		);
5014	}
5015}
5016
5017/// Umbrella over every root RPC type, used only to emit a single JSON Schema
5018/// document (`export-schema`) whose definitions cover the whole wire contract.
5019#[cfg(feature = "schema")]
5020#[derive(schemars::JsonSchema)]
5021#[allow(dead_code)]
5022pub struct DaemonProtocol {
5023	pub handshake: HandshakeResponse,
5024	pub registry_entry: DaemonRegistryEntry,
5025	pub workspace_config: DaemonWorkspaceConfig,
5026	pub query_request: QueryRequest,
5027	pub query: Query,
5028	pub query_response: QueryResponse,
5029	pub query_result: QueryResult,
5030	pub command_request: CommandRequest,
5031	pub command_response: CommandResponse,
5032	pub event: WorkspaceEventDto,
5033	pub error: QueryError,
5034}
5035
5036#[cfg(test)]
5037mod contract_tests {
5038	//! Lock the serde wire shapes the JSON Schema (and every generated client)
5039	//! depends on. These guard the contract, not the Rust layout.
5040	use super::*;
5041	use serde_json::json;
5042
5043	#[test]
5044	fn query_is_op_tagged() {
5045		let query = Query::SymbolSearch(SymbolSearchQuery {
5046			text: Some("widget".to_string()),
5047			..Default::default()
5048		});
5049		let value = serde_json::to_value(&query).unwrap();
5050		assert_eq!(value["op"], "symbol_search");
5051		assert_eq!(value["text"], "widget");
5052	}
5053
5054	#[test]
5055	fn query_result_is_kind_and_data_tagged() {
5056		let result = QueryResult::SymbolList(SymbolListResult {
5057			rows: Vec::new(),
5058			total: 0,
5059		});
5060		assert_eq!(
5061			serde_json::to_value(&result).unwrap(),
5062			json!({ "kind": "symbol_list", "data": { "rows": [], "total": 0 } }),
5063		);
5064	}
5065
5066	#[test]
5067	fn generation_serializes_as_scalar() {
5068		assert_eq!(
5069			serde_json::to_value(WorkspaceGeneration(7)).unwrap(),
5070			json!(7)
5071		);
5072	}
5073
5074	#[test]
5075	fn line_range_is_a_two_element_array() {
5076		let range: Option<(u32, u32)> = Some((3, 9));
5077		assert_eq!(serde_json::to_value(range).unwrap(), json!([3, 9]));
5078		assert_eq!(
5079			serde_json::to_value(Option::<(u32, u32)>::None).unwrap(),
5080			json!(null)
5081		);
5082	}
5083
5084	#[test]
5085	fn consistency_is_snake_case() {
5086		assert_eq!(
5087			serde_json::to_value(Consistency::RefreshIfStale).unwrap(),
5088			json!("refresh_if_stale"),
5089		);
5090	}
5091
5092	#[test]
5093	fn event_kind_is_snake_case() {
5094		let event = WorkspaceEventDto {
5095			kind: WorkspaceEventKind::GitBase,
5096			generation: None,
5097			stale_summary: None,
5098		};
5099		assert_eq!(serde_json::to_value(&event).unwrap()["kind"], "git_base");
5100	}
5101
5102	#[test]
5103	fn workspace_source_set_commands_are_op_tagged() {
5104		let replace = Command::WorkspaceSourceSetReplace {
5105			source_set: WorkspaceSourceSetDto {
5106				srcset: "database".to_string(),
5107				revision: Some("42".to_string()),
5108				documents: vec![WorkspaceSourceDocumentDto {
5109					uri: "schema/accounts.sql".to_string(),
5110					language: "sql".to_string(),
5111					content: "CREATE TABLE accounts (id bigint);".to_string(),
5112				}],
5113			},
5114		};
5115		assert_eq!(
5116			serde_json::to_value(replace).unwrap(),
5117			json!({
5118				"op": "workspace_source_set_replace",
5119				"source_set": {
5120					"srcset": "database",
5121					"revision": "42",
5122					"documents": [{
5123						"uri": "schema/accounts.sql",
5124						"language": "sql",
5125						"content": "CREATE TABLE accounts (id bigint);"
5126					}]
5127				}
5128			}),
5129		);
5130
5131		assert_eq!(
5132			serde_json::to_value(Command::WorkspaceSourceSetRemove {
5133				srcset: "database".to_string(),
5134			})
5135			.unwrap(),
5136			json!({
5137				"op": "workspace_source_set_remove",
5138				"srcset": "database"
5139			}),
5140		);
5141		assert!(
5142			CapabilitySet::default()
5143				.commands
5144				.iter()
5145				.any(|command| command == "workspace.source_set.replace")
5146		);
5147		assert!(
5148			CapabilitySet::default()
5149				.queries
5150				.iter()
5151				.any(|query| query == "diff-impact.compare")
5152		);
5153		assert_eq!(PROTOCOL_VERSION, 17);
5154	}
5155
5156	#[test]
5157	fn test_artifact_classification_covers_canonical_paths_kinds_and_modules() {
5158		for path in [
5159			"tests/service.rs",
5160			"benches/speed.rs",
5161			"fixtures/input.py",
5162			"testdata/schema.sql",
5163			"src/__tests__/service.ts",
5164		] {
5165			assert!(symbol_is_test_artifact("function", path, ""), "{path}");
5166		}
5167		assert!(symbol_is_test_artifact("test", "src/lib.rs", ""));
5168		assert!(symbol_is_test_artifact(
5169			"function",
5170			"src/lib.rs",
5171			"code+moniker://module:tests/function:works()"
5172		));
5173		assert!(!symbol_is_test_artifact("function", "src/lib.rs", ""));
5174	}
5175}