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	#[serde(default)]
1978	pub memory_source_refresh: Option<MemorySourceRefreshDto>,
1979}
1980
1981#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1982#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1983pub struct MemorySourceRefreshDto {
1984	pub mode: MemorySourceRefreshModeDto,
1985	pub documents_total: usize,
1986	pub added: usize,
1987	pub modified: usize,
1988	pub removed: usize,
1989	pub unchanged: usize,
1990	pub extraction_jobs: usize,
1991	pub extraction_workers: usize,
1992	pub linkage_invocations: usize,
1993}
1994
1995#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1996#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1997#[serde(rename_all = "snake_case")]
1998pub enum MemorySourceRefreshModeDto {
1999	Bulk,
2000	Incremental,
2001}
2002
2003impl fmt::Display for MemorySourceRefreshModeDto {
2004	fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2005		formatter.write_str(match self {
2006			Self::Bulk => "bulk",
2007			Self::Incremental => "incremental",
2008		})
2009	}
2010}
2011
2012#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2013#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2014pub struct WorkspaceRootStatus {
2015	pub root: String,
2016	pub generation: Option<WorkspaceGeneration>,
2017	pub files: usize,
2018	pub symbols: usize,
2019	pub references: usize,
2020	pub stale: bool,
2021	pub stale_summary: String,
2022}
2023
2024#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2025#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2026pub struct TreeChildrenResult {
2027	pub root: String,
2028	pub roots: Vec<String>,
2029	pub rows: Vec<TreeNode>,
2030	pub total: usize,
2031	pub total_files: usize,
2032	pub scoped_files: usize,
2033	pub languages: Vec<CountDto>,
2034	pub prefixes: Vec<CountDto>,
2035}
2036
2037#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2038#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2039pub struct TreeNode {
2040	pub root: String,
2041	pub path: String,
2042	pub kind: TreeNodeKind,
2043	pub language: Option<String>,
2044	pub defs: usize,
2045	pub refs: usize,
2046	pub change_count: usize,
2047}
2048
2049#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2050#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2051#[serde(rename_all = "snake_case")]
2052pub enum TreeNodeKind {
2053	File,
2054	Directory,
2055}
2056
2057#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2058#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2059pub struct SymbolListResult {
2060	pub rows: Vec<SymbolDto>,
2061	pub total: usize,
2062}
2063
2064#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2065#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2066pub struct SymbolDto {
2067	pub root: String,
2068	pub uri: String,
2069	pub id: String,
2070	pub name: String,
2071	pub kind: String,
2072	pub visibility: String,
2073	pub signature: String,
2074	pub file: String,
2075	pub language: String,
2076	pub line_range: Option<(u32, u32)>,
2077	pub navigable: bool,
2078	pub score: Option<u32>,
2079	pub match_reason: Option<String>,
2080	pub source: Option<SourceSnippet>,
2081}
2082
2083pub fn symbol_is_test_artifact(kind: &str, file: &str, uri: &str) -> bool {
2084	kind == "test"
2085		|| uri
2086			.split('/')
2087			.any(|segment| matches!(segment, "module:test" | "module:tests"))
2088		|| file.split(['/', '\\']).any(|component| {
2089			matches!(
2090				component,
2091				"test"
2092					| "tests" | "bench"
2093					| "benches" | "fixture"
2094					| "fixtures" | "testdata"
2095					| "__tests__"
2096			)
2097		})
2098}
2099
2100#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2102pub struct SymbolInsightsResult {
2103	pub files: usize,
2104	pub symbols: usize,
2105	pub references: usize,
2106	pub navigable_symbols: usize,
2107	pub non_navigable_symbols: usize,
2108	pub languages: Vec<CountDto>,
2109	pub kinds: Vec<CountDto>,
2110	pub shapes: Vec<CountDto>,
2111	pub top_files_by_symbols: Vec<CountDto>,
2112	pub top_files_by_refs: Vec<CountDto>,
2113}
2114
2115#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2117pub struct SymbolDetailResult {
2118	pub symbol: SymbolDto,
2119	pub source: Option<SourceSnippet>,
2120}
2121
2122#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2123#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2124pub struct SyntaxTreeResult {
2125	pub file: String,
2126	pub language: String,
2127	pub focus: String,
2128	pub focus_line_range: Option<(u32, u32)>,
2129	pub root: SyntaxNodeDto,
2130	pub emitted_nodes: usize,
2131	pub total_nodes: usize,
2132	pub max_depth: usize,
2133	pub truncated: bool,
2134	pub has_error: bool,
2135}
2136
2137#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2138#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2139pub struct SyntaxNodeDto {
2140	pub kind: String,
2141	#[serde(default, skip_serializing_if = "Option::is_none")]
2142	pub language: Option<String>,
2143	pub named: bool,
2144	pub error: bool,
2145	pub missing: bool,
2146	pub byte_range: (usize, usize),
2147	pub start: SyntaxPointDto,
2148	pub end: SyntaxPointDto,
2149	pub text: Option<String>,
2150	pub children: Vec<SyntaxNodeDto>,
2151}
2152
2153#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2155pub struct SyntaxPointDto {
2156	/// One-based line number.
2157	pub line: u32,
2158	/// Zero-based UTF-8 byte column, matching Tree-sitter.
2159	pub column: u32,
2160}
2161
2162#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2164pub struct SourceSnippet {
2165	pub file: String,
2166	pub first_line: u32,
2167	pub last_line: u32,
2168	pub lines: Vec<SourceLine>,
2169}
2170
2171#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2172#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2173pub struct SourceLine {
2174	pub number: u32,
2175	pub text: String,
2176}
2177
2178#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2179#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2180pub struct SymbolUsagesResult {
2181	pub target: SymbolDto,
2182	pub direction: UsageDirection,
2183	pub include_descendants: bool,
2184	pub targets: usize,
2185	pub rows: Vec<UsageDto>,
2186	pub total: usize,
2187	pub incoming_summary: Option<UsageSummaryDto>,
2188	pub outgoing_summary: Option<UsageSummaryDto>,
2189}
2190
2191#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2192#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2193pub struct UsageSummaryDto {
2194	pub refs: usize,
2195	pub files: usize,
2196	pub contexts: usize,
2197	pub prefixes: usize,
2198	pub dominant_prefix: String,
2199	pub kinds: Vec<CountDto>,
2200	pub top_actors: Vec<CountDto>,
2201	pub top_prefixes: Vec<CountDto>,
2202	pub shared_helper_signal: String,
2203}
2204
2205#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2206#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2207pub struct UsageDto {
2208	pub root: String,
2209	pub direction: UsageDirection,
2210	pub reference: String,
2211	pub kind: String,
2212	pub actor: String,
2213	pub context: String,
2214	pub endpoint: String,
2215	pub file: String,
2216	pub prefix: String,
2217	pub location: String,
2218	pub line_range: Option<(u32, u32)>,
2219	pub via: Option<String>,
2220}
2221
2222#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2224pub struct RulesListResult {
2225	pub roots: Vec<String>,
2226	pub rows: Vec<RuleDto>,
2227	pub total: usize,
2228}
2229
2230#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2232pub struct RuleDto {
2233	pub root: String,
2234	pub id: String,
2235	pub severity: String,
2236	pub lang: String,
2237	pub rule_root: String,
2238	pub subject: String,
2239	pub plan: String,
2240	pub capabilities: Vec<String>,
2241	#[serde(default)]
2242	pub group_by: Vec<String>,
2243	pub domain: String,
2244	pub kind: Option<String>,
2245	pub expr: String,
2246	pub expanded_expr: String,
2247	pub message: Option<String>,
2248	pub rationale: Option<String>,
2249	pub require_doc_comment: Option<String>,
2250}
2251
2252#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2254pub struct RulesApplicableResult {
2255	pub focus: SymbolGraphFocus,
2256	pub file: String,
2257	pub language: String,
2258	pub symbol_kind: Option<String>,
2259	pub total: usize,
2260	pub rows: Vec<RuleApplicabilityDto>,
2261}
2262
2263#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2264#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2265pub struct RuleApplicabilityDto {
2266	pub rule: RuleDto,
2267	pub status: String,
2268	pub reason: String,
2269}
2270
2271#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2273pub struct ChangeContextResult {
2274	pub focus: SymbolGraphFocus,
2275	pub source: Option<SourceSnippet>,
2276	pub graph: Box<SymbolGraphResult>,
2277	pub notes: Vec<NoteDto>,
2278	pub rules: Vec<RuleApplicabilityDto>,
2279	pub changed_files: Vec<ChangeReviewFile>,
2280	pub changed_symbols: Vec<ChangeReviewSymbol>,
2281	pub suggested_checks: Vec<String>,
2282	pub coverage: ChangeContextCoverageDto,
2283}
2284
2285#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2287pub struct ChangeContextCoverageDto {
2288	pub members_total: usize,
2289	pub members_emitted: usize,
2290	pub internal_edges_total: usize,
2291	pub internal_edges_emitted: usize,
2292	pub callers_total: usize,
2293	pub callers_emitted: usize,
2294	pub callees_total: usize,
2295	pub callees_emitted: usize,
2296	pub notes_total: usize,
2297	pub notes_emitted: usize,
2298	pub rules_total: usize,
2299	pub rules_emitted: usize,
2300	pub changes_total: usize,
2301	pub changes_emitted: usize,
2302}
2303
2304#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2306pub struct RulesCheckResult {
2307	pub verdict: RulesCheckVerdict,
2308	pub exit: String,
2309	pub summary: CheckSummaryDto,
2310	pub roots: Vec<RulesCheckRootResult>,
2311	pub violations: Vec<ViolationDto>,
2312	pub errors: Vec<FileErrorDto>,
2313	pub rule_reports: Vec<RuleReportDto>,
2314	pub skip_reasons: Vec<CheckSkipReasonDto>,
2315}
2316
2317#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2318#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2319pub struct RulesCheckRootResult {
2320	pub root: String,
2321	pub verdict: RulesCheckVerdict,
2322	pub exit: String,
2323	pub summary: CheckSummaryDto,
2324	pub violations: Vec<ViolationDto>,
2325	pub errors: Vec<FileErrorDto>,
2326	pub rule_reports: Vec<RuleReportDto>,
2327	pub skip_reason: Option<CheckSkipReasonDto>,
2328}
2329
2330#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2331#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2332#[serde(rename_all = "snake_case")]
2333pub enum RulesCheckVerdict {
2334	Pass,
2335	Fail,
2336	Error,
2337}
2338
2339impl RulesCheckVerdict {
2340	pub fn from_exit(exit: &str) -> Self {
2341		match exit {
2342			"match" => Self::Pass,
2343			"no_match" => Self::Fail,
2344			_ => Self::Error,
2345		}
2346	}
2347
2348	pub fn as_str(self) -> &'static str {
2349		match self {
2350			Self::Pass => "pass",
2351			Self::Fail => "fail",
2352			Self::Error => "error",
2353		}
2354	}
2355}
2356
2357#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2358#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2359pub struct CheckSummaryDto {
2360	pub files_scanned: usize,
2361	pub files_with_violations: usize,
2362	pub total_violations: usize,
2363	pub total_rule_errors: usize,
2364	pub total_warnings: usize,
2365	pub files_with_errors: usize,
2366	pub total_errors: usize,
2367	pub elapsed_ms: u64,
2368	pub failed_rules: Vec<FailedRuleDto>,
2369	#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2370	pub violations_by_srcset: BTreeMap<String, usize>,
2371}
2372
2373#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2374#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2375pub struct FailedRuleDto {
2376	pub rule_id: String,
2377	pub severity: String,
2378	pub violations: usize,
2379}
2380
2381#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2383pub struct ViolationDto {
2384	pub root: String,
2385	pub path: String,
2386	pub rule_id: String,
2387	pub severity: String,
2388	pub moniker: String,
2389	#[serde(default, skip_serializing_if = "Option::is_none")]
2390	pub srcset: Option<String>,
2391	pub kind: String,
2392	pub lines: (u32, u32),
2393	pub message: String,
2394}
2395
2396#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2398pub struct FileErrorDto {
2399	pub root: String,
2400	pub path: String,
2401	pub error: String,
2402}
2403
2404#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2405#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2406pub struct RuleReportDto {
2407	pub root: String,
2408	pub path: Option<String>,
2409	pub rule_id: String,
2410	pub severity: String,
2411	pub domain: String,
2412	pub evaluated: usize,
2413	pub matches: usize,
2414	pub violations: usize,
2415	pub antecedent_matches: Option<usize>,
2416	pub warning: Option<String>,
2417	#[serde(default, skip_serializing_if = "Option::is_none")]
2418	pub inconclusive: Option<usize>,
2419	#[serde(default, skip_serializing_if = "Option::is_none")]
2420	pub verdict: Option<String>,
2421	#[serde(default, skip_serializing_if = "Option::is_none")]
2422	pub coverage: Option<RuleCoverageDto>,
2423	#[serde(default, skip_serializing_if = "Option::is_none")]
2424	pub path_analysis: Option<RulePathReportDto>,
2425}
2426
2427#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2429pub struct RuleCoverageDto {
2430	pub total: usize,
2431	pub decided: usize,
2432	pub resolved: usize,
2433	pub external: usize,
2434	pub candidate: usize,
2435	pub dynamic: usize,
2436	pub blocked: usize,
2437	pub unresolved: usize,
2438	pub percent: usize,
2439	pub min_percent: usize,
2440}
2441
2442#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2443#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2444pub struct RulePathStepDto {
2445	pub source: String,
2446	pub target: String,
2447	pub relation: String,
2448	pub reference: String,
2449	pub file: String,
2450	pub line_range: Option<(u32, u32)>,
2451}
2452
2453#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2454#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2455pub struct RulePathReportDto {
2456	pub expectation: String,
2457	pub relation: Vec<String>,
2458	pub max_depth: usize,
2459	pub max_symbols: usize,
2460	pub max_edges: usize,
2461	pub max_pairs: usize,
2462	pub min_coverage: usize,
2463	pub source_symbols: usize,
2464	pub target_symbols: usize,
2465	pub via_symbols: usize,
2466	pub evaluated_pairs: usize,
2467	pub explored_symbols: usize,
2468	pub explored_edges: usize,
2469	pub depth_limit_reached: bool,
2470	pub symbol_limit_reached: bool,
2471	pub edge_limit_reached: bool,
2472	pub pair_limit_reached: bool,
2473	pub reasons: Vec<String>,
2474	pub witness: Vec<RulePathStepDto>,
2475}
2476
2477#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2478#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2479pub struct CheckSkipReasonDto {
2480	pub root: String,
2481	pub reason: String,
2482}
2483
2484#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2485#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2486pub struct NotesResult {
2487	pub action: String,
2488	pub total: usize,
2489	pub rows: Vec<NoteDto>,
2490	pub deleted: Option<NoteDto>,
2491}
2492
2493#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2494#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2495pub struct NoteDto {
2496	pub id: String,
2497	pub moniker: String,
2498	pub kind: String,
2499	pub status: String,
2500	pub title: String,
2501	pub body: String,
2502	pub created_by: String,
2503	pub updated_at: String,
2504	pub resolution: NoteResolutionDto,
2505}
2506
2507#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2508#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2509#[serde(tag = "status", rename_all = "snake_case")]
2510pub enum NoteResolutionDto {
2511	Resolved {
2512		target: String,
2513		file: String,
2514		slice: Option<(u32, u32)>,
2515	},
2516	Orphan,
2517}
2518
2519#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2520#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2521pub struct CountDto {
2522	pub name: String,
2523	pub count: usize,
2524}
2525
2526#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2527#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2528#[serde(tag = "event", rename_all = "snake_case")]
2529pub enum DaemonEvent {
2530	WorkspaceStale {
2531		generation: Option<WorkspaceGeneration>,
2532		summary: String,
2533	},
2534	WorkspaceRefreshed {
2535		generation: WorkspaceGeneration,
2536	},
2537}
2538
2539#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2540#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2541pub struct QueryError {
2542	pub code: String,
2543	pub message: String,
2544}
2545
2546impl QueryError {
2547	pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
2548		Self {
2549			code: code.into(),
2550			message: message.into(),
2551		}
2552	}
2553}
2554
2555impl fmt::Display for QueryError {
2556	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2557		write!(f, "{}: {}", self.code, self.message)
2558	}
2559}
2560
2561impl std::error::Error for QueryError {}
2562
2563#[derive(Debug, thiserror::Error)]
2564pub enum QueryParseError {
2565	#[error("empty query")]
2566	Empty,
2567	#[error("unknown query operation `{0}`")]
2568	UnknownOperation(String),
2569	#[error("invalid token `{0}`")]
2570	InvalidToken(String),
2571	#[error("invalid value for `{key}`: `{value}`")]
2572	InvalidValue { key: String, value: String },
2573	#[error("missing required `{0}`")]
2574	MissingRequired(&'static str),
2575	#[error("unknown field `{key}` for `{op}`{hint}")]
2576	UnknownField {
2577		op: String,
2578		key: String,
2579		hint: String,
2580	},
2581	#[error("unexpected argument `{value}` for `{op}`")]
2582	UnexpectedArgument { op: String, value: String },
2583	#[error("`project` is not supported by `{op}`")]
2584	UnsupportedProjection { op: String },
2585	#[error("unknown projection field `{field}` for `{op}`{hint}")]
2586	UnknownProjectionField {
2587		op: String,
2588		field: String,
2589		hint: String,
2590	},
2591}
2592
2593pub fn parse_query(input: &str) -> Result<QueryRequest, QueryParseError> {
2594	let mut lines = input.lines().map(str::trim).filter(|line| !line.is_empty());
2595	let first = lines.next().ok_or(QueryParseError::Empty)?;
2596	let mut tokens = tokenize(first)?;
2597	let op = tokens
2598		.first()
2599		.map(|token| token.text.clone())
2600		.ok_or(QueryParseError::Empty)?;
2601	tokens.remove(0);
2602	let mut fields = FieldBag::default();
2603	let mut positional = Vec::new();
2604	collect_tokens(&tokens, &mut fields, &mut positional)?;
2605	for line in lines {
2606		collect_line(line, &mut fields, &mut positional)?;
2607	}
2608	fields.positional = positional;
2609	let spec = verb_spec(&op).ok_or_else(|| QueryParseError::UnknownOperation(op.clone()))?;
2610	validate_fields(&op, spec, &fields)?;
2611	if let Some(value) = fields.one("consistency") {
2612		fields.consistency = value.parse()?;
2613	}
2614	let page = fields.page()?;
2615	let consistency = fields.consistency;
2616	let query = build_query(&op, fields)?;
2617	Ok(QueryRequest {
2618		query,
2619		consistency,
2620		page,
2621	})
2622}
2623
2624fn collect_line(
2625	line: &str,
2626	fields: &mut FieldBag,
2627	positional: &mut Vec<String>,
2628) -> Result<(), QueryParseError> {
2629	let mut tokens = tokenize(line)?;
2630	if tokens.is_empty() {
2631		return Ok(());
2632	}
2633	let section = tokens.remove(0);
2634	if section.text.contains(':') {
2635		let mut all = vec![section];
2636		all.extend(tokens);
2637		return collect_tokens(&all, fields, positional);
2638	}
2639	match section.text.as_str() {
2640		"filter" | "page" => collect_tokens(&tokens, fields, positional)?,
2641		"project" => {
2642			fields.projection.extend(
2643				tokens
2644					.into_iter()
2645					.map(|token| token.text.trim_end_matches(',').to_string())
2646					.filter(|token| !token.is_empty()),
2647			);
2648		}
2649		"consistency" => {
2650			let value = tokens
2651				.first()
2652				.ok_or(QueryParseError::MissingRequired("consistency value"))?;
2653			fields.consistency = value.text.parse()?;
2654		}
2655		"direction" => {
2656			let value = tokens
2657				.first()
2658				.ok_or(QueryParseError::MissingRequired("direction value"))?;
2659			fields.values.push((
2660				"direction".to_string(),
2661				FieldValue {
2662					text: value.text.clone(),
2663					quoted: value.quoted,
2664				},
2665			));
2666		}
2667		_ => return Err(QueryParseError::InvalidToken(section.text)),
2668	}
2669	Ok(())
2670}
2671
2672fn build_query(op: &str, fields: FieldBag) -> Result<Query, QueryParseError> {
2673	let query = match op {
2674		"query.describe" => Query::QueryDescribe(QueryDescribeQuery {
2675			verb: fields
2676				.one("verb")
2677				.or_else(|| fields.positional.first().cloned()),
2678		}),
2679		"workspace.status" => Query::WorkspaceStatus,
2680		"tree.children" => Query::TreeChildren(TreeChildrenQuery {
2681			workspace: fields.one("workspace"),
2682			path: fields.many("path"),
2683			depth: fields.usize("depth")?.unwrap_or(1),
2684			lang: fields.many("lang"),
2685			projection: fields.projection,
2686		}),
2687		"symbol.search" => Query::SymbolSearch(symbol_search_query(&fields)?),
2688		"symbol.insights" => Query::SymbolInsights(symbol_insights_query(&fields)?),
2689		"symbol.detail" => Query::SymbolDetail(SymbolDetailQuery {
2690			workspace: fields.one("workspace"),
2691			uri: fields
2692				.one("uri")
2693				.or_else(|| fields.positional.first().cloned())
2694				.ok_or(QueryParseError::MissingRequired("uri"))?,
2695			context_lines: fields.usize("context_lines")?.unwrap_or(2),
2696		}),
2697		"syntax.tree" => Query::SyntaxTree(SyntaxTreeQuery {
2698			workspace: fields.one("workspace"),
2699			focus: fields
2700				.one("focus")
2701				.or_else(|| fields.positional.first().cloned())
2702				.ok_or(QueryParseError::MissingRequired("focus"))?,
2703			max_depth: fields
2704				.usize("max_depth")?
2705				.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_DEPTH),
2706			max_nodes: fields
2707				.usize("max_nodes")?
2708				.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_NODES),
2709			named_only: fields.bool("named_only")?.unwrap_or(true),
2710			include_text: fields.bool("include_text")?.unwrap_or(false),
2711			max_text_chars: fields
2712				.usize("max_text_chars")?
2713				.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_TEXT_CHARS),
2714		}),
2715		"syntax.parse" => Query::SyntaxParse(syntax_parse_query(&fields)?),
2716		"symbol.usages" => Query::SymbolUsages(symbol_usages_query(&fields)?),
2717		"view.read" => Query::ViewRead(ViewReadQuery {
2718			uri: fields
2719				.one("uri")
2720				.or_else(|| fields.positional.first().cloned())
2721				.ok_or(QueryParseError::MissingRequired("uri"))?,
2722			scheme: fields.one("scheme"),
2723			context_lines: fields.usize("context_lines")?.unwrap_or(2),
2724			include_code: fields.bool("include_code")?.unwrap_or(false),
2725		}),
2726		"rules.list" => Query::RulesList(RulesListQuery {
2727			workspace: fields.one("workspace"),
2728			profile: fields.one("profile"),
2729			rules: fields.one("rules"),
2730			lang: fields.many("lang"),
2731			severity: fields.many("severity"),
2732		}),
2733		"rules.check" => Query::RulesCheck(RulesCheckQuery {
2734			workspace: fields.one("workspace"),
2735			profile: fields.one("profile"),
2736			rules: fields.one("rules"),
2737			file: fields.many("file"),
2738			report: fields.bool("report")?.unwrap_or(true),
2739		}),
2740		"rules.applicable" => Query::RulesApplicable(RulesApplicableQuery {
2741			workspace: fields.one("workspace"),
2742			focus: fields
2743				.one("focus")
2744				.or_else(|| fields.positional.first().cloned())
2745				.ok_or(QueryParseError::MissingRequired("focus"))?,
2746			profile: fields.one("profile"),
2747			rules: fields.one("rules"),
2748		}),
2749		"change.review" => Query::ChangeReview(ChangeReviewQuery {
2750			workspace: fields.one("workspace"),
2751		}),
2752		"change.context" => Query::ChangeContext(ChangeContextQuery {
2753			workspace: fields.one("workspace"),
2754			focus: fields
2755				.one("focus")
2756				.or_else(|| fields.positional.first().cloned())
2757				.ok_or(QueryParseError::MissingRequired("focus"))?,
2758			profile: fields.one("profile"),
2759			max_items: fields.usize("max_items")?.unwrap_or(20),
2760		}),
2761		"symbol.graph" => Query::SymbolGraph(symbol_graph_query(&fields)?),
2762		"graph.path" => Query::GraphPath(graph_path_query(&fields)?),
2763		"identity.children" => Query::IdentityChildren(identity_children_query(&fields)),
2764		"identity.graph" => Query::IdentityGraph(identity_graph_query(&fields)?),
2765		"metrics.coupling" => Query::MetricsCoupling(MetricsCouplingQuery {
2766			workspace: fields.one("workspace"),
2767			from: fields
2768				.one("from")
2769				.ok_or(QueryParseError::MissingRequired("from"))?,
2770			to: fields
2771				.one("to")
2772				.ok_or(QueryParseError::MissingRequired("to"))?,
2773			relation: fields.many("relation"),
2774			snapshot: fields.one("snapshot"),
2775			export: fields.bool("export")?.unwrap_or(false),
2776		}),
2777		"resolution.audit" => Query::ResolutionAudit(ResolutionAuditQuery {
2778			workspace: fields.one("workspace"),
2779			prefix: fields
2780				.one("prefix")
2781				.or_else(|| fields.positional.first().cloned())
2782				.unwrap_or_default(),
2783			limit: fields.usize("limit")?.unwrap_or(20),
2784			cluster: fields.one("cluster"),
2785		}),
2786		"notes" => Query::Notes(notes_query(&fields)?),
2787		_ => return Err(QueryParseError::UnknownOperation(op.to_string())),
2788	};
2789	Ok(query)
2790}
2791
2792fn verb_spec(op: &str) -> Option<&'static QueryCapabilitySpec> {
2793	query_capability_spec(op)
2794}
2795
2796fn validate_fields(
2797	op: &str,
2798	spec: &QueryCapabilitySpec,
2799	fields: &FieldBag,
2800) -> Result<(), QueryParseError> {
2801	for (key, value) in &fields.values {
2802		let key = key.as_str();
2803		if !COMMON_FIELDS.contains(&key) && !spec.fields.contains(&key) {
2804			return Err(QueryParseError::UnknownField {
2805				op: op.to_string(),
2806				key: key.to_string(),
2807				hint: field_hint(key, spec.fields),
2808			});
2809		}
2810		if !value.quoted
2811			&& BRACKET_LIST_FIELDS.contains(&key)
2812			&& value.text.starts_with('[') != value.text.ends_with(']')
2813		{
2814			return Err(QueryParseError::InvalidValue {
2815				key: key.to_string(),
2816				value: value.text.clone(),
2817			});
2818		}
2819	}
2820	if let Some(extra) = fields.positional.get(spec.positionals) {
2821		return Err(QueryParseError::UnexpectedArgument {
2822			op: op.to_string(),
2823			value: extra.clone(),
2824		});
2825	}
2826	if !spec.projection && !fields.projection.is_empty() {
2827		return Err(QueryParseError::UnsupportedProjection { op: op.to_string() });
2828	}
2829	let projection_fields = query_projection_fields(op);
2830	for field in &fields.projection {
2831		if !projection_fields.contains(&field.as_str()) {
2832			return Err(QueryParseError::UnknownProjectionField {
2833				op: op.to_string(),
2834				field: field.clone(),
2835				hint: projection_field_hint(field, projection_fields),
2836			});
2837		}
2838	}
2839	Ok(())
2840}
2841
2842fn projection_field_hint(field: &str, allowed: &'static [&'static str]) -> String {
2843	if let Some(suggestion) = allowed
2844		.iter()
2845		.copied()
2846		.map(|candidate| (candidate, levenshtein(field, candidate)))
2847		.filter(|(_, distance)| *distance <= 2)
2848		.min_by_key(|(_, distance)| *distance)
2849		.map(|(candidate, _)| candidate)
2850	{
2851		return format!(", did you mean `{suggestion}`?");
2852	}
2853	format!(" (valid projection fields: {})", allowed.join(", "))
2854}
2855
2856fn field_hint(key: &str, allowed: &'static [&'static str]) -> String {
2857	if let Some(suggestion) = suggest_field(key, allowed) {
2858		return format!(", did you mean `{suggestion}`?");
2859	}
2860	let mut valid: Vec<&str> = allowed.iter().chain(COMMON_FIELDS).copied().collect();
2861	valid.sort_unstable();
2862	format!(" (valid fields: {})", valid.join(", "))
2863}
2864
2865fn suggest_field(key: &str, allowed: &'static [&'static str]) -> Option<&'static str> {
2866	const ALIASES: &[(&str, &str)] = &[("text", "name"), ("query", "name"), ("filename", "path")];
2867	for (alias, target) in ALIASES {
2868		if *alias == key && allowed.contains(target) {
2869			return Some(target);
2870		}
2871	}
2872	allowed
2873		.iter()
2874		.chain(COMMON_FIELDS)
2875		.copied()
2876		.map(|candidate| (candidate, levenshtein(key, candidate)))
2877		.filter(|(_, distance)| *distance <= 2)
2878		.min_by_key(|(_, distance)| *distance)
2879		.map(|(candidate, _)| candidate)
2880}
2881
2882fn levenshtein(a: &str, b: &str) -> usize {
2883	let b: Vec<char> = b.chars().collect();
2884	let mut row: Vec<usize> = (0..=b.len()).collect();
2885	for (i, ca) in a.chars().enumerate() {
2886		let mut previous = row[0];
2887		row[0] = i + 1;
2888		for (j, cb) in b.iter().enumerate() {
2889			let substitution = previous + usize::from(ca != *cb);
2890			previous = row[j + 1];
2891			row[j + 1] = substitution.min(previous + 1).min(row[j] + 1);
2892		}
2893	}
2894	row[b.len()]
2895}
2896
2897fn symbol_search_query(fields: &FieldBag) -> Result<SymbolSearchQuery, QueryParseError> {
2898	Ok(SymbolSearchQuery {
2899		workspace: fields.one("workspace"),
2900		text: fields.positional.first().cloned(),
2901		path: fields.many("path"),
2902		lang: fields.many("lang"),
2903		kind: fields.many("kind"),
2904		shape: fields.many("shape"),
2905		name: fields.one("name"),
2906		include_non_navigable: fields.bool("include_non_navigable")?.unwrap_or(false),
2907		include_code: fields.bool("include_code")?.unwrap_or(false),
2908		context_lines: fields.usize("context_lines")?.unwrap_or(0),
2909		projection: fields.projection.clone(),
2910	})
2911}
2912
2913fn symbol_usages_query(fields: &FieldBag) -> Result<SymbolUsagesQuery, QueryParseError> {
2914	Ok(SymbolUsagesQuery {
2915		workspace: fields.one("workspace"),
2916		uri: fields
2917			.one("uri")
2918			.or_else(|| fields.positional.first().cloned())
2919			.ok_or(QueryParseError::MissingRequired("uri"))?,
2920		direction: fields
2921			.one("direction")
2922			.unwrap_or_else(|| "incoming".to_string())
2923			.parse()?,
2924		path: fields.many("path"),
2925		lang: fields.many("lang"),
2926		include_descendants: fields.bool("include_descendants")?.unwrap_or(false),
2927		projection: fields.projection.clone(),
2928	})
2929}
2930
2931fn syntax_parse_query(fields: &FieldBag) -> Result<SyntaxParseQuery, QueryParseError> {
2932	Ok(SyntaxParseQuery {
2933		language: fields
2934			.one("language")
2935			.ok_or(QueryParseError::MissingRequired("language"))?,
2936		source: fields
2937			.one("source")
2938			.ok_or(QueryParseError::MissingRequired("source"))?,
2939		uri: fields.one("uri"),
2940		max_depth: fields
2941			.usize("max_depth")?
2942			.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_DEPTH),
2943		max_nodes: fields
2944			.usize("max_nodes")?
2945			.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_NODES),
2946		named_only: fields.bool("named_only")?.unwrap_or(true),
2947		include_text: fields.bool("include_text")?.unwrap_or(false),
2948		max_text_chars: fields
2949			.usize("max_text_chars")?
2950			.unwrap_or(SYNTAX_TREE_DEFAULT_MAX_TEXT_CHARS),
2951	})
2952}
2953
2954fn symbol_insights_query(fields: &FieldBag) -> Result<SymbolSearchQuery, QueryParseError> {
2955	let mut query = symbol_search_query(fields)?;
2956	query.text = None;
2957	query.include_code = false;
2958	query.context_lines = 0;
2959	Ok(query)
2960}
2961
2962fn notes_query(fields: &FieldBag) -> Result<NotesQuery, QueryParseError> {
2963	Ok(NotesQuery {
2964		action: parse_notes_action(fields.one("action").as_deref().unwrap_or("list"))?,
2965		id: fields.one("id"),
2966		moniker: fields.one("moniker"),
2967		kind: fields.one("kind"),
2968		status: fields.one("status"),
2969		title: fields.one("title"),
2970		body: fields.one("body"),
2971		created_by: fields.one("created_by"),
2972		orphan: fields.bool("orphan")?,
2973		include_done: fields.bool("include_done")?.unwrap_or(false),
2974	})
2975}
2976
2977fn identity_children_query(fields: &FieldBag) -> IdentityChildrenQuery {
2978	IdentityChildrenQuery {
2979		workspace: fields.one("workspace"),
2980		prefix: fields
2981			.one("prefix")
2982			.or_else(|| fields.positional.first().cloned())
2983			.unwrap_or_default(),
2984	}
2985}
2986
2987fn identity_graph_query(fields: &FieldBag) -> Result<IdentityGraphQuery, QueryParseError> {
2988	Ok(IdentityGraphQuery {
2989		workspace: fields.one("workspace"),
2990		prefix: fields
2991			.one("prefix")
2992			.or_else(|| fields.positional.first().cloned())
2993			.unwrap_or_default(),
2994		path: fields.many("path"),
2995		min_count: fields.usize("min_count")?.unwrap_or(1).max(1),
2996	})
2997}
2998
2999fn symbol_graph_query(fields: &FieldBag) -> Result<SymbolGraphQuery, QueryParseError> {
3000	Ok(SymbolGraphQuery {
3001		workspace: fields.one("workspace"),
3002		focus: fields
3003			.one("focus")
3004			.or_else(|| fields.positional.first().cloned())
3005			.ok_or(QueryParseError::MissingRequired("focus"))?,
3006		direction: fields
3007			.one("direction")
3008			.unwrap_or_else(|| "both".to_string())
3009			.parse()?,
3010		relation: fields.many("relation"),
3011		min_count: fields.usize("min_count")?.unwrap_or(1).max(1),
3012		include_internal: fields.bool("include_internal")?.unwrap_or(true),
3013	})
3014}
3015
3016fn graph_path_query(fields: &FieldBag) -> Result<GraphPathQuery, QueryParseError> {
3017	let max_depth = fields.usize("max_depth")?.unwrap_or(12);
3018	if max_depth > 64 {
3019		return Err(QueryParseError::InvalidValue {
3020			key: "max_depth".to_string(),
3021			value: max_depth.to_string(),
3022		});
3023	}
3024	let min_coverage = fields.usize("min_coverage")?.unwrap_or(100);
3025	if min_coverage > 100 {
3026		return Err(QueryParseError::InvalidValue {
3027			key: "min_coverage".to_string(),
3028			value: min_coverage.to_string(),
3029		});
3030	}
3031	let max_symbols = bounded_non_zero_field(fields, "max_symbols", 10_000, 100_000)?;
3032	let max_edges = bounded_non_zero_field(fields, "max_edges", 50_000, 500_000)?;
3033	let relation = fields.many("relation");
3034	Ok(GraphPathQuery {
3035		workspace: fields.one("workspace"),
3036		from: fields
3037			.one("from")
3038			.ok_or(QueryParseError::MissingRequired("from"))?,
3039		to: fields
3040			.one("to")
3041			.ok_or(QueryParseError::MissingRequired("to"))?,
3042		expect: fields
3043			.one("expect")
3044			.unwrap_or_else(|| "reachable".to_string())
3045			.parse()?,
3046		relation: if relation.is_empty() {
3047			vec!["calls".to_string(), "method_call".to_string()]
3048		} else {
3049			relation
3050		},
3051		max_depth,
3052		max_symbols,
3053		max_edges,
3054		min_coverage,
3055	})
3056}
3057
3058fn bounded_non_zero_field(
3059	fields: &FieldBag,
3060	key: &str,
3061	default: usize,
3062	maximum: usize,
3063) -> Result<usize, QueryParseError> {
3064	let value = fields.usize(key)?.unwrap_or(default);
3065	if value == 0 || value > maximum {
3066		return Err(QueryParseError::InvalidValue {
3067			key: key.to_string(),
3068			value: value.to_string(),
3069		});
3070	}
3071	Ok(value)
3072}
3073
3074fn parse_notes_action(value: &str) -> Result<NotesAction, QueryParseError> {
3075	match value {
3076		"list" => Ok(NotesAction::List),
3077		"get" => Ok(NotesAction::Get),
3078		"create" => Ok(NotesAction::Create),
3079		"update" => Ok(NotesAction::Update),
3080		"transition" => Ok(NotesAction::Transition),
3081		"delete" => Ok(NotesAction::Delete),
3082		_ => Err(QueryParseError::InvalidValue {
3083			key: "action".to_string(),
3084			value: value.to_string(),
3085		}),
3086	}
3087}
3088
3089pub fn format_query_response(response: &QueryResponse) -> String {
3090	format_query_response_projected(response, &[])
3091}
3092
3093pub fn format_query_response_projected(response: &QueryResponse, projection: &[String]) -> String {
3094	let mut out = String::new();
3095	if let Some(generation) = response.generation {
3096		let _ = writeln!(out, "generation: {}", generation.0);
3097	}
3098	if let Some(cursor) = &response.next_cursor {
3099		if let Some(generation) = cursor.generation {
3100			let _ = writeln!(out, "next_cursor: {}:{}", generation.0, cursor.offset);
3101		} else {
3102			let _ = writeln!(out, "next_cursor: {}", cursor.offset);
3103		}
3104	}
3105	match &response.result {
3106		QueryResult::QueryDescribe(result) => format_query_describe(&mut out, result),
3107		QueryResult::WorkspaceStatus(status) => format_workspace_status(&mut out, status),
3108		QueryResult::TreeChildren(result) => format_tree_children(&mut out, result, projection),
3109		QueryResult::SymbolList(result) => format_symbol_list(&mut out, result, projection),
3110		QueryResult::SymbolInsights(result) if !projection.is_empty() => {
3111			format_projected_value(&mut out, result, projection);
3112		}
3113		QueryResult::SymbolInsights(result) => format_symbol_insights(&mut out, result),
3114		QueryResult::SymbolDetail(result) => format_symbol_detail(&mut out, result),
3115		QueryResult::SyntaxTree(result) => format_syntax_tree(&mut out, result),
3116		QueryResult::SymbolUsages(result) => format_symbol_usages(&mut out, result, projection),
3117		QueryResult::ViewRead(result) => format_view_read(&mut out, result),
3118		QueryResult::RulesList(result) => {
3119			let _ = writeln!(out, "rules: {}", result.total);
3120			format_rules_list_rows(&mut out, result);
3121		}
3122		QueryResult::RulesCheck(result) => format_rules_check(&mut out, result),
3123		QueryResult::RulesApplicable(result) => format_rules_applicable(&mut out, result),
3124		QueryResult::ChangeReview(result) => format_change_review(&mut out, result),
3125		QueryResult::DiffImpact(result) => format_diff_impact(&mut out, result),
3126		QueryResult::ChangeContext(result) => format_change_context(&mut out, result),
3127		QueryResult::SymbolGraph(result) => format_symbol_graph(&mut out, result),
3128		QueryResult::GraphPath(result) => format_graph_path(&mut out, result),
3129		QueryResult::IdentityChildren(result) => format_identity_children(&mut out, result),
3130		QueryResult::IdentityGraph(result) => format_identity_graph(&mut out, result),
3131		QueryResult::MetricsCoupling(result) => format_metrics_coupling(&mut out, result),
3132		QueryResult::ResolutionAudit(result) => format_resolution_audit(&mut out, result),
3133		QueryResult::Notes(result) => format_notes(&mut out, result),
3134	}
3135	out
3136}
3137
3138fn format_workspace_status(out: &mut String, status: &WorkspaceStatus) {
3139	let _ = writeln!(out, "workspace: {}", status.root);
3140	let _ = writeln!(
3141		out,
3142		"producer: {} {}",
3143		status.producer.version, status.producer.fingerprint
3144	);
3145	let _ = writeln!(out, "phase: {}", status.phase);
3146	let _ = writeln!(
3147		out,
3148		"files: {} symbols: {} references: {}",
3149		status.files, status.symbols, status.references
3150	);
3151	let _ = writeln!(out, "stale: {} ({})", status.stale, status.stale_summary);
3152	let timings = &status.timings;
3153	let _ = writeln!(
3154		out,
3155		"timings_ms: total={} catalog={} extract={} semantic={} linkage={} changes={}",
3156		timings.total_ms,
3157		timings.source_catalog_ms,
3158		timings.extract_sources_ms,
3159		timings.semantic_index_ms,
3160		timings.linkage_ms,
3161		timings.change_overlay_ms,
3162	);
3163	if let Some(refresh) = &timings.memory_source_refresh {
3164		let _ = writeln!(
3165			out,
3166			"memory_refresh: mode={} documents={} added={} modified={} removed={} unchanged={} extraction_jobs={} workers={} linkage_invocations={}",
3167			refresh.mode,
3168			refresh.documents_total,
3169			refresh.added,
3170			refresh.modified,
3171			refresh.removed,
3172			refresh.unchanged,
3173			refresh.extraction_jobs,
3174			refresh.extraction_workers,
3175			refresh.linkage_invocations,
3176		);
3177	}
3178	if status.roots.len() > 1 {
3179		let _ = writeln!(out, "roots:");
3180		for root in &status.roots {
3181			let _ = writeln!(
3182				out,
3183				"- {} files:{} symbols:{} references:{} stale:{}",
3184				root.root, root.files, root.symbols, root.references, root.stale
3185			);
3186		}
3187	}
3188}
3189
3190fn format_tree_children(out: &mut String, result: &TreeChildrenResult, projection: &[String]) {
3191	let _ = writeln!(out, "tree: {}", result.root);
3192	for row in &result.rows {
3193		if !projection.is_empty() {
3194			format_projected_value(out, row, projection);
3195			continue;
3196		}
3197		let kind = match row.kind {
3198			TreeNodeKind::File => "file",
3199			TreeNodeKind::Directory => "dir",
3200		};
3201		let _ = writeln!(
3202			out,
3203			"- {kind} {} defs:{} refs:{}",
3204			row.path, row.defs, row.refs
3205		);
3206	}
3207}
3208
3209fn format_symbol_list(out: &mut String, result: &SymbolListResult, projection: &[String]) {
3210	let _ = writeln!(out, "symbols: {}", result.total);
3211	for row in &result.rows {
3212		if projection.is_empty() {
3213			let _ = writeln!(out, "- {} {} {} {}", row.kind, row.name, row.file, row.uri);
3214		} else {
3215			format_projected_value(out, row, projection);
3216		}
3217	}
3218}
3219
3220fn format_symbol_detail(out: &mut String, result: &SymbolDetailResult) {
3221	let symbol = &result.symbol;
3222	let _ = writeln!(out, "symbol: {} {}", symbol.kind, symbol.name);
3223	let _ = writeln!(out, "uri: {}", symbol.uri);
3224	let _ = writeln!(out, "file: {}", symbol.file);
3225	if let Some(source) = &result.source {
3226		for line in &source.lines {
3227			let _ = writeln!(out, "{:>6} | {}", line.number, line.text);
3228		}
3229	}
3230}
3231
3232fn format_syntax_tree(out: &mut String, result: &SyntaxTreeResult) {
3233	let _ = writeln!(out, "file: {}", result.file);
3234	let _ = writeln!(out, "language: {}", result.language);
3235	let _ = writeln!(out, "focus: {}", result.focus);
3236	let _ = writeln!(
3237		out,
3238		"nodes: {}/{} max_depth:{} truncated:{} parse_error:{}",
3239		result.emitted_nodes,
3240		result.total_nodes,
3241		result.max_depth,
3242		result.truncated,
3243		result.has_error
3244	);
3245	let _ = writeln!(out, "tree:");
3246	format_syntax_node(out, &result.root, 0);
3247}
3248
3249fn format_syntax_node(out: &mut String, node: &SyntaxNodeDto, depth: usize) {
3250	let language = node
3251		.language
3252		.as_deref()
3253		.map(|language| format!(" lang:{language}"))
3254		.unwrap_or_default();
3255	let marker = if node.error {
3256		" error"
3257	} else if node.missing {
3258		" missing"
3259	} else {
3260		""
3261	};
3262	let text = node
3263		.text
3264		.as_deref()
3265		.map(|text| format!(" text={text:?}"))
3266		.unwrap_or_default();
3267	let _ = writeln!(
3268		out,
3269		"{}- {}{} {}:{}-{}:{}{}{}",
3270		"  ".repeat(depth),
3271		node.kind,
3272		language,
3273		node.start.line,
3274		node.start.column,
3275		node.end.line,
3276		node.end.column,
3277		marker,
3278		text
3279	);
3280	for child in &node.children {
3281		format_syntax_node(out, child, depth + 1);
3282	}
3283}
3284
3285fn format_symbol_usages(out: &mut String, result: &SymbolUsagesResult, projection: &[String]) {
3286	let _ = writeln!(out, "uri: {}", result.target.uri);
3287	let _ = writeln!(out, "direction: {}", result.direction.as_str());
3288	let scope = if result.include_descendants {
3289		"descendants"
3290	} else {
3291		"exact"
3292	};
3293	let _ = writeln!(out, "target_scope: {scope} ({} symbols)", result.targets);
3294	let _ = writeln!(out, "usages: {}", result.total);
3295	for row in &result.rows {
3296		if projection.is_empty() {
3297			let _ = writeln!(
3298				out,
3299				"- {} {} {} {}",
3300				row.direction.as_str(),
3301				row.kind,
3302				row.actor,
3303				row.file
3304			);
3305		} else {
3306			format_projected_value(out, row, projection);
3307		}
3308	}
3309}
3310
3311fn format_view_read(out: &mut String, result: &ViewReadResult) {
3312	match result {
3313		ViewReadResult::List(list) => {
3314			let _ = writeln!(out, "views: {}", list.views.len());
3315			for view in &list.views {
3316				let _ = writeln!(out, "- {} ({})", view.id, view.scope);
3317			}
3318		}
3319		ViewReadResult::Detail(detail) => {
3320			let _ = writeln!(out, "view: {}", detail.id);
3321			let _ = writeln!(out, "fragment: {}", detail.fragment);
3322			let _ = writeln!(out, "scope: {}", detail.scope);
3323			let _ = writeln!(
3324				out,
3325				"rules: {} boundaries: {} gotchas: {}",
3326				detail.rules.len(),
3327				detail.boundaries.len(),
3328				detail.gotchas.len()
3329			);
3330		}
3331	}
3332}
3333
3334fn format_projected_value(out: &mut String, value: &impl Serialize, projection: &[String]) {
3335	let Ok(Value::Object(fields)) = serde_json::to_value(value) else {
3336		return;
3337	};
3338	let rendered = projection
3339		.iter()
3340		.filter_map(|name| fields.get(name).map(|value| (name, value)))
3341		.map(|(name, value)| format!("{name}={}", compact_json_value(value)))
3342		.collect::<Vec<_>>()
3343		.join(" ");
3344	let _ = writeln!(out, "- {rendered}");
3345}
3346
3347fn compact_json_value(value: &Value) -> String {
3348	match value {
3349		Value::String(value) => value.clone(),
3350		Value::Null => "-".to_string(),
3351		_ => serde_json::to_string(value).unwrap_or_else(|_| "?".to_string()),
3352	}
3353}
3354
3355fn format_query_describe(out: &mut String, result: &QueryDescribeResult) {
3356	let _ = writeln!(out, "queries: {}", result.capabilities.len());
3357	for capability in &result.capabilities {
3358		let _ = writeln!(
3359			out,
3360			"- {} [{}] read_only={} mcp={} projection={} paginated={}",
3361			capability.name,
3362			capability.category,
3363			capability.read_only,
3364			capability.mcp_tool,
3365			capability.projection,
3366			capability.paginated
3367		);
3368		let fields = capability
3369			.fields
3370			.iter()
3371			.map(|field| {
3372				let required = if field.required { "!" } else { "" };
3373				let multiple = if field.multiple { "[]" } else { "" };
3374				let default = field
3375					.default
3376					.as_deref()
3377					.map_or(String::new(), |value| format!("={value}"));
3378				format!(
3379					"{}{}:{}{}{}",
3380					field.name, required, field.value_type, multiple, default
3381				)
3382			})
3383			.collect::<Vec<_>>()
3384			.join(", ");
3385		let _ = writeln!(out, "  fields: {fields}");
3386		if !capability.projection_fields.is_empty() {
3387			let _ = writeln!(
3388				out,
3389				"  project: {}",
3390				capability.projection_fields.join(", ")
3391			);
3392		}
3393		let _ = writeln!(out, "  example: {}", capability.example);
3394	}
3395}
3396
3397fn format_rules_applicable(out: &mut String, result: &RulesApplicableResult) {
3398	let _ = writeln!(out, "focus: {}", result.file);
3399	let _ = writeln!(out, "language: {}", result.language);
3400	if let Some(kind) = &result.symbol_kind {
3401		let _ = writeln!(out, "symbol_kind: {kind}");
3402	}
3403	let applicable = result
3404		.rows
3405		.iter()
3406		.filter(|row| row.status == "applicable")
3407		.count();
3408	let _ = writeln!(out, "rules: {} applicable: {applicable}", result.total);
3409	for row in &result.rows {
3410		let _ = writeln!(
3411			out,
3412			"- {} [{}] {} — {}",
3413			row.rule.id, row.rule.severity, row.status, row.reason
3414		);
3415	}
3416}
3417
3418fn format_change_context(out: &mut String, result: &ChangeContextResult) {
3419	let _ = writeln!(out, "facts:");
3420	match &result.focus {
3421		SymbolGraphFocus::Symbol { symbol } => {
3422			let _ = writeln!(out, "focus: {} {}", symbol.kind, symbol.name);
3423			let _ = writeln!(out, "uri: {}", symbol.uri);
3424			let _ = writeln!(out, "file: {}", symbol.file);
3425		}
3426		SymbolGraphFocus::File { path } => {
3427			let _ = writeln!(out, "focus: file {path}");
3428		}
3429	}
3430	if let Some(source) = &result.source {
3431		let _ = writeln!(out, "source:");
3432		for line in &source.lines {
3433			let _ = writeln!(out, "{:>6} | {}", line.number, line.text);
3434		}
3435	}
3436	format_context_graph(out, &result.graph);
3437	let coverage = result.coverage;
3438	let _ = writeln!(out, "coverage:");
3439	let _ = writeln!(
3440		out,
3441		"- members {}/{} · internal_edges {}/{} · callers {}/{} · callees {}/{}",
3442		coverage.members_emitted,
3443		coverage.members_total,
3444		coverage.internal_edges_emitted,
3445		coverage.internal_edges_total,
3446		coverage.callers_emitted,
3447		coverage.callers_total,
3448		coverage.callees_emitted,
3449		coverage.callees_total
3450	);
3451	let _ = writeln!(
3452		out,
3453		"- notes {}/{} · rules {}/{} · changes {}/{}",
3454		coverage.notes_emitted,
3455		coverage.notes_total,
3456		coverage.rules_emitted,
3457		coverage.rules_total,
3458		coverage.changes_emitted,
3459		coverage.changes_total
3460	);
3461	if !result.notes.is_empty() {
3462		let _ = writeln!(out, "notes:");
3463		for note in &result.notes {
3464			let _ = writeln!(out, "- {} [{}] {}", note.id, note.status, note.title);
3465		}
3466	}
3467	if !result.rules.is_empty() {
3468		let _ = writeln!(out, "applicable_rules:");
3469		for row in &result.rules {
3470			let _ = writeln!(out, "- {} [{}]", row.rule.id, row.rule.severity);
3471		}
3472	}
3473	format_context_changes(out, &result.changed_files, &result.changed_symbols);
3474	if !result.suggested_checks.is_empty() {
3475		let _ = writeln!(out, "suggested_checks:");
3476		for check in &result.suggested_checks {
3477			let _ = writeln!(out, "- {check}");
3478		}
3479	}
3480}
3481
3482fn format_context_graph(out: &mut String, graph: &SymbolGraphResult) {
3483	if !graph.members.is_empty() {
3484		let _ = writeln!(out, "members:");
3485		for member in &graph.members {
3486			let _ = writeln!(
3487				out,
3488				"- {} {} ({}) {}",
3489				member.kind, member.name, member.file, member.uri
3490			);
3491		}
3492	}
3493	if !graph.internal_edges.is_empty() {
3494		let member_labels: BTreeMap<&str, String> = graph
3495			.members
3496			.iter()
3497			.map(|member| {
3498				(
3499					member.id.as_str(),
3500					format!("{} {}", member.kind, member.name),
3501				)
3502			})
3503			.collect();
3504		let unlisted_labels: BTreeMap<&str, String> = graph
3505			.internal_edges
3506			.iter()
3507			.flat_map(|edge| [edge.source.as_str(), edge.target.as_str()])
3508			.filter(|endpoint| {
3509				endpoint.starts_with("symbol:") && !member_labels.contains_key(endpoint)
3510			})
3511			.collect::<std::collections::BTreeSet<_>>()
3512			.into_iter()
3513			.enumerate()
3514			.map(|(index, endpoint)| {
3515				(
3516					endpoint,
3517					format!("unlisted internal member {}", index.saturating_add(1)),
3518				)
3519			})
3520			.collect();
3521		let endpoint_label = |endpoint: &str| {
3522			member_labels
3523				.get(endpoint)
3524				.or_else(|| unlisted_labels.get(endpoint))
3525				.cloned()
3526				.unwrap_or_else(|| endpoint.to_string())
3527		};
3528		let _ = writeln!(out, "internal_edges:");
3529		for edge in &graph.internal_edges {
3530			let _ = writeln!(
3531				out,
3532				"- {} -> {} x{} [{}]",
3533				endpoint_label(&edge.source),
3534				endpoint_label(&edge.target),
3535				edge.count,
3536				edge.kinds.join(",")
3537			);
3538		}
3539	}
3540	format_unlinked(out, &graph.unlinked);
3541	for (marker, neighbors) in [("<", &graph.callers), (">", &graph.callees)] {
3542		for neighbor in neighbors {
3543			let _ = writeln!(
3544				out,
3545				"{marker} {} {} x{} [{}] {}",
3546				neighbor.symbol.kind,
3547				neighbor.symbol.name,
3548				neighbor.count,
3549				neighbor.kinds.join(","),
3550				neighbor.symbol.uri
3551			);
3552		}
3553	}
3554}
3555
3556fn format_context_changes(
3557	out: &mut String,
3558	files: &[ChangeReviewFile],
3559	symbols: &[ChangeReviewSymbol],
3560) {
3561	if !files.is_empty() {
3562		let _ = writeln!(out, "changed_files:");
3563		for file in files {
3564			let old = file.old_path.as_deref().unwrap_or("-");
3565			let new = file.new_path.as_deref().unwrap_or("-");
3566			let _ = writeln!(out, "- {old} -> {new} [{}]", file.disposition);
3567		}
3568	}
3569	if !symbols.is_empty() {
3570		let _ = writeln!(out, "changed_symbols:");
3571		for symbol in symbols {
3572			let old = symbol
3573				.old
3574				.as_ref()
3575				.map(|side| side.identity.as_str())
3576				.unwrap_or("-");
3577			let new = symbol
3578				.new
3579				.as_ref()
3580				.map(|side| side.identity.as_str())
3581				.unwrap_or("-");
3582			let _ = writeln!(
3583				out,
3584				"- {} [{}] {old} -> {new}",
3585				symbol.kind, symbol.confidence
3586			);
3587		}
3588	}
3589}
3590
3591fn format_resolution_audit(out: &mut String, result: &ResolutionAuditResult) {
3592	let t = &result.totals;
3593	if !result.prefix.is_empty() {
3594		let _ = writeln!(out, "prefix: {}", result.prefix);
3595	}
3596	let _ = writeln!(
3597		out,
3598		"refs: {} unique: {} candidate: {} external: {} sdk: {} dependency: {} injected_external: {} unknown_external: {} dynamic: {} blocked: {} unresolved: {} explained: {} weak_or_unexplained: {} name_match_candidate: {}",
3599		t.references,
3600		t.unique,
3601		t.candidate,
3602		t.external,
3603		t.sdk,
3604		t.dependency,
3605		t.injected_external,
3606		t.unknown_external,
3607		t.dynamic,
3608		t.blocked,
3609		t.unresolved,
3610		t.explained,
3611		t.weak_or_unexplained,
3612		t.name_match_candidate
3613	);
3614	let _ = writeln!(out, "clusters:");
3615	for cluster in &result.clusters {
3616		let _ = writeln!(
3617			out,
3618			"- [{:>6}] {} {}",
3619			cluster.count, cluster.id, cluster.pattern
3620		);
3621		let sample_limit = if result.clusters.len() == 1 {
3622			cluster.samples.len()
3623		} else {
3624			1
3625		};
3626		for sample in cluster.samples.iter().take(sample_limit) {
3627			let location = match sample.line_range {
3628				Some((start, end)) if start == end => format!("{}:{start}", sample.file),
3629				Some((start, end)) => format!("{}:{start}-{end}", sample.file),
3630				None => sample.file.clone(),
3631			};
3632			let _ = writeln!(
3633				out,
3634				"           ex: {} {} {} -> {} evidence:{}",
3635				location, sample.call_name, sample.receiver, sample.target, sample.evidence
3636			);
3637			if !sample.candidates.is_empty() {
3638				let _ = writeln!(
3639					out,
3640					"           candidates: {}",
3641					sample.candidates.join(", ")
3642				);
3643			}
3644			if !sample.constraints.is_empty() {
3645				let _ = writeln!(
3646					out,
3647					"           constraints: {}",
3648					sample.constraints.join(", ")
3649				);
3650			}
3651			if !sample.snippet.is_empty() {
3652				let _ = writeln!(out, "           code: {}", sample.snippet);
3653			}
3654		}
3655	}
3656	let _ = writeln!(out, "zones:");
3657	for zone in &result.zones {
3658		let _ = writeln!(
3659			out,
3660			"- [{:>5}] {} — {}",
3661			zone.unresolved, zone.zone, zone.dominant_pattern
3662		);
3663	}
3664}
3665
3666fn format_symbol_insights(out: &mut String, result: &SymbolInsightsResult) {
3667	let _ = writeln!(out, "files: {}", result.files);
3668	let _ = writeln!(out, "symbols: {}", result.symbols);
3669	let _ = writeln!(out, "refs: {}", result.references);
3670	let _ = writeln!(out, "languages:");
3671	for row in &result.languages {
3672		let _ = writeln!(out, "- {}: {}", row.name, row.count);
3673	}
3674}
3675
3676fn format_notes(out: &mut String, result: &NotesResult) {
3677	let _ = writeln!(out, "action: {}", result.action);
3678	let _ = writeln!(out, "notes: {}", result.total);
3679	for row in &result.rows {
3680		let _ = writeln!(out, "- {} [{}] {}", row.id, row.status, row.title);
3681	}
3682}
3683
3684fn format_rules_list_rows(out: &mut String, result: &RulesListResult) {
3685	for row in &result.rows {
3686		let _ = writeln!(
3687			out,
3688			"- {} [{}] root={} lang={} domain={}",
3689			row.id, row.severity, row.root, row.lang, row.domain
3690		);
3691		if let Some(message) = &row.message {
3692			let _ = writeln!(out, "  message: {message}");
3693		}
3694	}
3695}
3696
3697fn format_identity_children(out: &mut String, result: &IdentityChildrenResult) {
3698	let prefix = if result.prefix.is_empty() {
3699		"<root>"
3700	} else {
3701		&result.prefix
3702	};
3703	let _ = writeln!(out, "prefix: {prefix}");
3704	let _ = writeln!(out, "children: {}", result.children.len());
3705	for child in &result.children {
3706		let marker = if child.symbol.is_some() { "def" } else { "…" };
3707		let _ = writeln!(
3708			out,
3709			"- {} [{}] defs={} {}",
3710			child.segment, marker, child.defs, child.identity
3711		);
3712	}
3713}
3714
3715fn format_unlinked(out: &mut String, unlinked: &UnlinkedRefsDto) {
3716	let _ = writeln!(
3717		out,
3718		"unlinked refs: external {} (sdk {} · dependency {} · injected {} · unknown {}) · candidate {} · dynamic {} · manifest-blocked {} · unresolved {}",
3719		unlinked.external,
3720		unlinked.sdk,
3721		unlinked.dependency,
3722		unlinked.injected_external,
3723		unlinked.unknown_external,
3724		unlinked.candidate,
3725		unlinked.dynamic,
3726		unlinked.manifest_blocked,
3727		unlinked.unresolved
3728	);
3729	if !unlinked.unresolved_reasons.is_empty() {
3730		let reasons = unlinked
3731			.unresolved_reasons
3732			.iter()
3733			.map(|(reason, count)| format!("{reason} {count}"))
3734			.collect::<Vec<_>>()
3735			.join(" · ");
3736		let _ = writeln!(out, "unresolved by reason: {reasons}");
3737	}
3738}
3739
3740fn format_identity_graph(out: &mut String, result: &IdentityGraphResult) {
3741	let prefix = if result.prefix.is_empty() {
3742		"<root>"
3743	} else {
3744		&result.prefix
3745	};
3746	let _ = writeln!(out, "scope: {prefix}");
3747	if !result.path.is_empty() {
3748		let _ = writeln!(out, "path: {}", result.path.join(", "));
3749	}
3750	let _ = writeln!(out, "min_count: {}", result.min_count);
3751	let _ = writeln!(
3752		out,
3753		"coverage: rows {}/{} matching ({} total)",
3754		result.coverage.rows_emitted, result.coverage.rows_matching, result.coverage.rows_total
3755	);
3756	let _ = writeln!(
3757		out,
3758		"nodes: {}/{} edges: {}/{} matching ({} total) ports_in: {}/{} matching ({} total) ports_out: {}/{} matching ({} total)",
3759		result.coverage.nodes_emitted,
3760		result.coverage.nodes_total,
3761		result.coverage.edges_emitted,
3762		result.coverage.edges_matching,
3763		result.coverage.edges_total,
3764		result.coverage.ports_in_emitted,
3765		result.coverage.ports_in_matching,
3766		result.coverage.ports_in_total,
3767		result.coverage.ports_out_emitted,
3768		result.coverage.ports_out_matching,
3769		result.coverage.ports_out_total
3770	);
3771	format_unlinked(out, &result.unlinked);
3772	for node in &result.nodes {
3773		let _ = writeln!(
3774			out,
3775			"- node {} defs:{} children:{}",
3776			node.identity, node.defs, node.has_children
3777		);
3778	}
3779	for edge in &result.edges {
3780		let _ = writeln!(
3781			out,
3782			"- {} -> {} x{} [{}]",
3783			edge.source,
3784			edge.target,
3785			edge.count,
3786			edge.kinds.join(",")
3787		);
3788	}
3789	for port in &result.ports_in {
3790		let _ = writeln!(
3791			out,
3792			"< {} x{} [{}]",
3793			port.identity,
3794			port.count,
3795			port.kinds.join(",")
3796		);
3797	}
3798	for port in &result.ports_out {
3799		let _ = writeln!(
3800			out,
3801			"> {} x{} [{}]",
3802			port.identity,
3803			port.count,
3804			port.kinds.join(",")
3805		);
3806	}
3807}
3808
3809fn format_metrics_coupling(out: &mut String, result: &MetricsCouplingResult) {
3810	let _ = writeln!(out, "from: {}", result.from);
3811	let _ = writeln!(out, "to: {}", result.to);
3812	let _ = writeln!(out, "snapshot: {}", result.snapshot);
3813	if let Some(git) = &result.git {
3814		let _ = writeln!(
3815			out,
3816			"git: branch {} commit {} dirty {}",
3817			git.branch, git.commit, git.dirty
3818		);
3819	}
3820	if !result.relation.is_empty() {
3821		let _ = writeln!(out, "relation: {}", result.relation.join(", "));
3822	}
3823	let export = match (result.export_requested, result.export_recorded) {
3824		(false, _) => "not requested",
3825		(true, true) => "recorded",
3826		(true, false) => "telemetry disabled",
3827	};
3828	let _ = writeln!(out, "otel export: {export}");
3829	let _ = writeln!(
3830		out,
3831		"coupling: references {} connections {} source_symbols {} target_symbols {}",
3832		result.references, result.connections, result.source_symbols, result.target_symbols
3833	);
3834	let _ = writeln!(
3835		out,
3836		"coverage: resolved source references {}/{}",
3837		result.coverage.resolved_source_references, result.coverage.source_references
3838	);
3839	if result.same_symbol_references > 0 {
3840		let _ = writeln!(
3841			out,
3842			"same-symbol references excluded: {}",
3843			result.same_symbol_references
3844		);
3845	}
3846	format_unlinked(out, &result.unlinked);
3847	if !result.by_target.is_empty() {
3848		let _ = writeln!(out, "public boundary usage by target:");
3849		for target in &result.by_target {
3850			let _ = writeln!(out, "- {} x{}", target.moniker, target.references);
3851		}
3852	}
3853	if !result.by_kind.is_empty() {
3854		let _ = writeln!(out, "usage by relation kind:");
3855	}
3856	for kind in &result.by_kind {
3857		let _ = writeln!(out, "- {} x{}", kind.name, kind.count);
3858	}
3859}
3860
3861fn format_symbol_graph(out: &mut String, result: &SymbolGraphResult) {
3862	match &result.focus {
3863		SymbolGraphFocus::Symbol { symbol } => {
3864			let _ = writeln!(
3865				out,
3866				"focus: {} {} ({})",
3867				symbol.kind, symbol.name, symbol.file
3868			);
3869		}
3870		SymbolGraphFocus::File { path } => {
3871			let _ = writeln!(out, "focus: file {path}");
3872		}
3873	}
3874	let _ = writeln!(
3875		out,
3876		"members: {}/{} internal edges: {}/{} matching ({} total)",
3877		result.coverage.members.returned,
3878		result.coverage.members.total,
3879		result.coverage.internal_edges.returned,
3880		result.coverage.internal_edges.matching,
3881		result.coverage.internal_edges.total
3882	);
3883	format_unlinked(out, &result.unlinked);
3884	let _ = writeln!(
3885		out,
3886		"callers: {}/{} matching ({} total)",
3887		result.coverage.callers.returned,
3888		result.coverage.callers.matching,
3889		result.coverage.callers.total
3890	);
3891	for caller in &result.callers {
3892		let _ = writeln!(
3893			out,
3894			"< {} {} ({}) x{} [{}]",
3895			caller.symbol.kind,
3896			caller.symbol.name,
3897			caller.symbol.file,
3898			caller.count,
3899			caller.kinds.join(",")
3900		);
3901	}
3902	let _ = writeln!(
3903		out,
3904		"callees: {}/{} matching ({} total)",
3905		result.coverage.callees.returned,
3906		result.coverage.callees.matching,
3907		result.coverage.callees.total
3908	);
3909	for callee in &result.callees {
3910		let _ = writeln!(
3911			out,
3912			"> {} {} ({}) x{} [{}]",
3913			callee.symbol.kind,
3914			callee.symbol.name,
3915			callee.symbol.file,
3916			callee.count,
3917			callee.kinds.join(",")
3918		);
3919	}
3920}
3921
3922fn format_graph_path(out: &mut String, result: &GraphPathResult) {
3923	let _ = writeln!(
3924		out,
3925		"expectation: {} verdict: {}",
3926		result.expectation.as_str(),
3927		result.verdict.as_str()
3928	);
3929	let _ = writeln!(
3930		out,
3931		"from: {} {} ({})",
3932		result.from.kind, result.from.name, result.from.file
3933	);
3934	let _ = writeln!(
3935		out,
3936		"to: {} {} ({})",
3937		result.to.kind, result.to.name, result.to.file
3938	);
3939	let truth = |value: Option<bool>| match value {
3940		Some(true) => "true",
3941		Some(false) => "false",
3942		None => "unknown",
3943	};
3944	let _ = writeln!(
3945		out,
3946		"reachable: {} no_path: {} path_length: {}",
3947		truth(result.reachable),
3948		truth(result.no_path),
3949		result.path.len()
3950	);
3951	let _ = writeln!(
3952		out,
3953		"coverage: {}% ({}/{}) resolved={} external={} candidate={} dynamic={} manifest_blocked={} unresolved={}",
3954		result.coverage.percent,
3955		result.coverage.decided,
3956		result.coverage.total,
3957		result.coverage.resolved,
3958		result.coverage.external,
3959		result.coverage.candidate,
3960		result.coverage.dynamic,
3961		result.coverage.manifest_blocked,
3962		result.coverage.unresolved
3963	);
3964	let _ = writeln!(
3965		out,
3966		"search: depth={}/{} symbols={} edges={} depth_limit_reached={} symbol_limit_reached={} edge_limit_reached={}",
3967		result.search.depth_reached,
3968		result.search.max_depth,
3969		result.search.explored_symbols,
3970		result.search.explored_edges,
3971		result.search.depth_limit_reached,
3972		result.search.symbol_limit_reached,
3973		result.search.edge_limit_reached
3974	);
3975	if !result.reasons.is_empty() {
3976		let _ = writeln!(out, "reasons: {}", result.reasons.join(", "));
3977	}
3978	for (index, step) in result.path.iter().enumerate() {
3979		let location = step.line_range.map_or_else(
3980			|| step.file.clone(),
3981			|(start, end)| {
3982				if start == end {
3983					format!("{}:L{start}", step.file)
3984				} else {
3985					format!("{}:L{start}-L{end}", step.file)
3986				}
3987			},
3988		);
3989		let _ = writeln!(
3990			out,
3991			"{}. {} -> {} [{}] {}",
3992			index + 1,
3993			step.source.uri,
3994			step.target.uri,
3995			step.relation,
3996			location
3997		);
3998	}
3999}
4000
4001fn format_change_review(out: &mut String, result: &ChangeReviewResult) {
4002	let _ = writeln!(out, "scope: {}", result.scope);
4003	let _ = writeln!(
4004		out,
4005		"files: {} ({} analyzable) symbols: {} refs: {} ({} retargeted) residual: {}",
4006		result.summary.files,
4007		result.summary.analyzable_files,
4008		result.summary.symbol_changes,
4009		result.summary.ref_changes,
4010		result.summary.retargeted_refs,
4011		result.summary.residual_files
4012	);
4013	for file in &result.files {
4014		let path = match (&file.old_path, &file.new_path) {
4015			(Some(old), Some(new)) if old != new => format!("{old} -> {new}"),
4016			(_, Some(new)) => new.clone(),
4017			(Some(old), None) => old.clone(),
4018			(None, None) => "<unknown>".to_string(),
4019		};
4020		let _ = writeln!(
4021			out,
4022			"- {path} {}{}{}",
4023			file.disposition,
4024			if file.analyzable {
4025				""
4026			} else {
4027				" (not analyzable)"
4028			},
4029			if file.coverage_explained {
4030				""
4031			} else {
4032				" [residual]"
4033			}
4034		);
4035	}
4036	for change in &result.symbol_changes {
4037		let side = change.new.as_ref().or(change.old.as_ref());
4038		let Some(side) = side else { continue };
4039		let _ = writeln!(
4040			out,
4041			"  {} {} {} [{}]",
4042			change.kind, side.kind, side.name, change.confidence
4043		);
4044	}
4045	for diagnostic in &result.diagnostics {
4046		let _ = writeln!(out, "diagnostic: {diagnostic}");
4047	}
4048}
4049
4050fn format_diff_impact(out: &mut String, result: &DiffImpactResult) {
4051	let _ = writeln!(out, "scope: {}", result.scope);
4052	let _ = writeln!(
4053		out,
4054		"files: {} ({} analyzable) symbols: {} refs: {} ({} retargeted) residual: {}",
4055		result.summary.files,
4056		result.summary.analyzable_files,
4057		result.summary.symbol_changes,
4058		result.summary.ref_changes,
4059		result.summary.retargeted_refs,
4060		result.summary.residual_files
4061	);
4062	for file in &result.files {
4063		let path = match (&file.old_path, &file.new_path) {
4064			(Some(old), Some(new)) if old != new => format!("{old} -> {new}"),
4065			(_, Some(new)) => new.clone(),
4066			(Some(old), None) => old.clone(),
4067			(None, None) => "<unknown>".to_string(),
4068		};
4069		let _ = writeln!(
4070			out,
4071			"- {path} {}{}{}",
4072			file.disposition,
4073			if file.analyzable {
4074				""
4075			} else {
4076				" (not analyzable)"
4077			},
4078			if file.coverage_explained {
4079				""
4080			} else {
4081				" [residual]"
4082			}
4083		);
4084	}
4085	for change in &result.symbol_changes {
4086		let side = change.new.as_ref().or(change.old.as_ref());
4087		let Some(side) = side else { continue };
4088		let _ = writeln!(
4089			out,
4090			"  {} {} {} [{}]",
4091			change.kind, side.kind, side.compact_identity, change.confidence
4092		);
4093	}
4094	for diagnostic in &result.diagnostics {
4095		let _ = writeln!(out, "diagnostic: {diagnostic}");
4096	}
4097}
4098
4099fn format_rules_check(out: &mut String, result: &RulesCheckResult) {
4100	let _ = writeln!(out, "verdict: {}", result.verdict.as_str());
4101	let _ = writeln!(out, "exit: {}", result.exit);
4102	let _ = writeln!(
4103		out,
4104		"violations: {} errors: {} elapsed_ms: {}",
4105		result.summary.total_violations, result.summary.total_errors, result.summary.elapsed_ms
4106	);
4107	for violation in &result.violations {
4108		let _ = writeln!(
4109			out,
4110			"- {} {}:{}-{} [{}] {}",
4111			violation.root,
4112			violation.path,
4113			violation.lines.0,
4114			violation.lines.1,
4115			violation.rule_id,
4116			violation.message
4117		);
4118	}
4119	if !result.rule_reports.is_empty() {
4120		let _ = writeln!(out, "rule_reports: {}", result.rule_reports.len());
4121		for report in result.rule_reports.iter().filter(|report| {
4122			report.verdict.is_some() || report.coverage.is_some() || report.path_analysis.is_some()
4123		}) {
4124			let _ = writeln!(
4125				out,
4126				"  - {} verdict={} evaluated={} violations={}",
4127				report.rule_id,
4128				report.verdict.as_deref().unwrap_or("not_applicable"),
4129				report.evaluated,
4130				report.violations
4131			);
4132			if let Some(coverage) = &report.coverage {
4133				let _ = writeln!(
4134					out,
4135					"    coverage={}%, minimum={}%",
4136					coverage.percent, coverage.min_percent
4137				);
4138			}
4139			if let Some(path) = &report.path_analysis {
4140				let _ = writeln!(
4141					out,
4142					"    path expect={} pairs={} explored_symbols={} explored_edges={} witness_steps={}",
4143					path.expectation,
4144					path.evaluated_pairs,
4145					path.explored_symbols,
4146					path.explored_edges,
4147					path.witness.len()
4148				);
4149			}
4150		}
4151	}
4152}
4153
4154#[derive(Default)]
4155struct FieldBag {
4156	values: Vec<(String, FieldValue)>,
4157	positional: Vec<String>,
4158	projection: Vec<String>,
4159	consistency: Consistency,
4160}
4161
4162#[derive(Clone, Debug, Eq, PartialEq)]
4163struct FieldValue {
4164	text: String,
4165	quoted: bool,
4166}
4167
4168#[derive(Clone, Debug, Eq, PartialEq)]
4169struct QueryToken {
4170	text: String,
4171	quoted: bool,
4172}
4173
4174impl FieldBag {
4175	fn bool(&self, key: &str) -> Result<Option<bool>, QueryParseError> {
4176		self.one(key)
4177			.map(|value| match value.as_str() {
4178				"true" => Ok(true),
4179				"false" => Ok(false),
4180				_ => Err(QueryParseError::InvalidValue {
4181					key: key.to_string(),
4182					value,
4183				}),
4184			})
4185			.transpose()
4186	}
4187
4188	fn page(&self) -> Result<Page, QueryParseError> {
4189		let limit = self.usize("limit")?.unwrap_or(80);
4190		let cursor = self
4191			.one("cursor")
4192			.map(|value| parse_cursor(&value))
4193			.transpose()?;
4194		Ok(Page { cursor, limit })
4195	}
4196
4197	fn usize(&self, key: &str) -> Result<Option<usize>, QueryParseError> {
4198		self.one(key)
4199			.map(|value| {
4200				value
4201					.parse::<usize>()
4202					.map_err(|_| QueryParseError::InvalidValue {
4203						key: key.to_string(),
4204						value,
4205					})
4206			})
4207			.transpose()
4208	}
4209
4210	fn one(&self, key: &str) -> Option<String> {
4211		self.values
4212			.iter()
4213			.find(|(candidate, _)| candidate == key)
4214			.map(|(_, value)| value.text.clone())
4215	}
4216
4217	fn many(&self, key: &str) -> Vec<String> {
4218		self.values
4219			.iter()
4220			.filter(|(candidate, _)| candidate == key)
4221			.flat_map(|(_, value)| {
4222				if value.quoted || !MULTI_VALUE_FIELDS.contains(&key) {
4223					vec![value.text.clone()]
4224				} else {
4225					split_csv(strip_bracket_list(key, &value.text))
4226				}
4227			})
4228			.collect()
4229	}
4230}
4231
4232fn collect_tokens(
4233	tokens: &[QueryToken],
4234	fields: &mut FieldBag,
4235	positional: &mut Vec<String>,
4236) -> Result<(), QueryParseError> {
4237	for token in tokens {
4238		if let Some((key, value)) = token.text.split_once(':') {
4239			fields.values.push((
4240				key.to_string(),
4241				FieldValue {
4242					text: value.to_string(),
4243					quoted: token.quoted,
4244				},
4245			));
4246		} else {
4247			positional.push(token.text.trim_end_matches(',').to_string());
4248		}
4249	}
4250	Ok(())
4251}
4252
4253fn parse_cursor(value: &str) -> Result<QueryCursor, QueryParseError> {
4254	let Some((generation, offset)) = value.split_once(':') else {
4255		return Err(QueryParseError::InvalidValue {
4256			key: "cursor".to_string(),
4257			value: value.to_string(),
4258		});
4259	};
4260	let generation = generation
4261		.parse::<u64>()
4262		.map_err(|_| QueryParseError::InvalidValue {
4263			key: "cursor".to_string(),
4264			value: value.to_string(),
4265		})?;
4266	let offset = offset
4267		.parse::<usize>()
4268		.map_err(|_| QueryParseError::InvalidValue {
4269			key: "cursor".to_string(),
4270			value: value.to_string(),
4271		})?;
4272	Ok(QueryCursor::new(
4273		offset,
4274		Some(WorkspaceGeneration(generation)),
4275	))
4276}
4277
4278fn tokenize(input: &str) -> Result<Vec<QueryToken>, QueryParseError> {
4279	let mut tokens = Vec::new();
4280	let mut current = String::new();
4281	let mut chars = input.chars().peekable();
4282	let mut quoted = false;
4283	let mut token_quoted = false;
4284	while let Some(ch) = chars.next() {
4285		match ch {
4286			'"' => {
4287				quoted = !quoted;
4288				token_quoted = true;
4289			}
4290			'\\' if quoted => {
4291				if let Some(next) = chars.next() {
4292					current.push(next);
4293				}
4294			}
4295			ch if ch.is_whitespace() && !quoted => {
4296				if !current.is_empty() {
4297					tokens.push(QueryToken {
4298						text: std::mem::take(&mut current),
4299						quoted: std::mem::take(&mut token_quoted),
4300					});
4301				}
4302			}
4303			ch => current.push(ch),
4304		}
4305	}
4306	if quoted {
4307		return Err(QueryParseError::InvalidToken(input.to_string()));
4308	}
4309	if !current.is_empty() {
4310		tokens.push(QueryToken {
4311			text: current,
4312			quoted: token_quoted,
4313		});
4314	}
4315	Ok(tokens)
4316}
4317
4318// `shape:[callable,type]` list sugar, restricted to enum-like fields so glob
4319// character classes in `path:`/`file:` values stay untouched.
4320fn strip_bracket_list<'a>(key: &str, value: &'a str) -> &'a str {
4321	if !BRACKET_LIST_FIELDS.contains(&key) {
4322		return value;
4323	}
4324	value
4325		.strip_prefix('[')
4326		.and_then(|inner| inner.strip_suffix(']'))
4327		.unwrap_or(value)
4328}
4329
4330pub fn split_csv(value: &str) -> Vec<String> {
4331	value
4332		.split(',')
4333		.map(str::trim)
4334		.filter(|entry| !entry.is_empty())
4335		.map(ToOwned::to_owned)
4336		.collect()
4337}
4338
4339#[cfg(test)]
4340mod tests {
4341	use super::*;
4342	use serde::Serialize;
4343
4344	fn serialized_fields(value: impl Serialize) -> Vec<String> {
4345		let mut fields = serde_json::to_value(value)
4346			.expect("serialize query DTO")
4347			.as_object()
4348			.expect("query DTO object")
4349			.keys()
4350			.cloned()
4351			.collect::<Vec<_>>();
4352		fields.sort();
4353		fields
4354	}
4355
4356	fn dto_fields(verb: &str) -> Vec<String> {
4357		match verb {
4358			"query.describe" => serialized_fields(QueryDescribeQuery::default()),
4359			"workspace.status" => Vec::new(),
4360			"tree.children" => serialized_fields(TreeChildrenQuery::default()),
4361			"symbol.search" | "symbol.insights" => serialized_fields(SymbolSearchQuery::default()),
4362			"symbol.detail" => serialized_fields(SymbolDetailQuery {
4363				workspace: None,
4364				uri: String::new(),
4365				context_lines: 0,
4366			}),
4367			"syntax.tree" => serialized_fields(SyntaxTreeQuery {
4368				workspace: None,
4369				focus: String::new(),
4370				max_depth: 0,
4371				max_nodes: 0,
4372				named_only: true,
4373				include_text: false,
4374				max_text_chars: 0,
4375			}),
4376			"syntax.parse" => serialized_fields(SyntaxParseQuery {
4377				language: String::new(),
4378				source: String::new(),
4379				uri: None,
4380				max_depth: 0,
4381				max_nodes: 0,
4382				named_only: true,
4383				include_text: false,
4384				max_text_chars: 0,
4385			}),
4386			"symbol.usages" => serialized_fields(SymbolUsagesQuery {
4387				workspace: None,
4388				uri: String::new(),
4389				direction: UsageDirection::Incoming,
4390				path: Vec::new(),
4391				lang: Vec::new(),
4392				include_descendants: false,
4393				projection: Vec::new(),
4394			}),
4395			"view.read" => serialized_fields(ViewReadQuery {
4396				uri: String::new(),
4397				scheme: None,
4398				context_lines: 0,
4399				include_code: false,
4400			}),
4401			"rules.list" => serialized_fields(RulesListQuery::default()),
4402			"rules.check" => serialized_fields(RulesCheckQuery::default()),
4403			"rules.applicable" => serialized_fields(RulesApplicableQuery::default()),
4404			"change.review" => serialized_fields(ChangeReviewQuery::default()),
4405			"change.context" => serialized_fields(ChangeContextQuery::default()),
4406			"symbol.graph" => serialized_fields(SymbolGraphQuery::default()),
4407			"graph.path" => serialized_fields(GraphPathQuery::default()),
4408			"identity.children" => serialized_fields(IdentityChildrenQuery::default()),
4409			"identity.graph" => serialized_fields(IdentityGraphQuery::default()),
4410			"metrics.coupling" => serialized_fields(MetricsCouplingQuery::default()),
4411			"resolution.audit" => serialized_fields(ResolutionAuditQuery::default()),
4412			"notes" => serialized_fields(NotesQuery {
4413				action: NotesAction::List,
4414				id: None,
4415				moniker: None,
4416				kind: None,
4417				status: None,
4418				title: None,
4419				body: None,
4420				created_by: None,
4421				orphan: None,
4422				include_done: false,
4423			}),
4424			other => panic!("missing DTO field fixture for {other}"),
4425		}
4426	}
4427
4428	#[test]
4429	fn capability_registry_fields_exist_on_query_dtos() {
4430		for spec in query_capability_specs() {
4431			let dto = dto_fields(spec.name);
4432			for field in spec.fields {
4433				assert!(
4434					dto.iter().any(|candidate| candidate == field),
4435					"{} field `{field}` missing from DTO fields {dto:?}",
4436					spec.name
4437				);
4438			}
4439			assert!(
4440				spec.fields
4441					.iter()
4442					.all(|field| !COMMON_FIELDS.contains(field)),
4443				"{} repeats a common request field",
4444				spec.name
4445			);
4446		}
4447	}
4448
4449	#[test]
4450	fn describes_live_query_contract() {
4451		let request = parse_query("query.describe symbol.usages").expect("query describe");
4452		assert!(matches!(
4453			request.query,
4454			Query::QueryDescribe(QueryDescribeQuery { verb: Some(ref verb) })
4455				if verb == "symbol.usages"
4456		));
4457		let result = describe_query_capabilities(Some("symbol.usages")).expect("capability");
4458		let capability = result.capabilities.first().expect("described query");
4459		assert!(capability.read_only);
4460		assert_eq!(capability.mcp_tool, "code_moniker_usages");
4461		assert!(capability.projection);
4462		assert!(capability.paginated);
4463		assert!(
4464			capability
4465				.fields
4466				.iter()
4467				.any(|field| field.name == "uri" && field.required)
4468		);
4469		assert!(capability.projection_fields.contains(&"actor".to_string()));
4470		assert_eq!(
4471			CapabilitySet::default().query_mcp_tools["symbol.usages"],
4472			"code_moniker_usages"
4473		);
4474	}
4475
4476	#[test]
4477	fn parses_bounded_syntax_tree_contract() {
4478		let request = parse_query(
4479			"syntax.tree focus:\"src/service.ts\" max_depth:4 max_nodes:120 named_only:false include_text:true max_text_chars:40",
4480		)
4481		.expect("syntax tree query");
4482		assert_eq!(
4483			request.query,
4484			Query::SyntaxTree(SyntaxTreeQuery {
4485				workspace: None,
4486				focus: "src/service.ts".to_string(),
4487				max_depth: 4,
4488				max_nodes: 120,
4489				named_only: false,
4490				include_text: true,
4491				max_text_chars: 40,
4492			})
4493		);
4494	}
4495
4496	#[test]
4497	fn parses_stateless_syntax_contract() {
4498		let request = parse_query(
4499			"syntax.parse language:\"rs\" source:\"fn main() {}\" uri:\"snippet.rs\" max_depth:4 max_nodes:120",
4500		)
4501		.expect("stateless syntax query");
4502		assert_eq!(
4503			request.query,
4504			Query::SyntaxParse(SyntaxParseQuery {
4505				language: "rs".to_string(),
4506				source: "fn main() {}".to_string(),
4507				uri: Some("snippet.rs".to_string()),
4508				max_depth: 4,
4509				max_nodes: 120,
4510				named_only: true,
4511				include_text: false,
4512				max_text_chars: SYNTAX_TREE_DEFAULT_MAX_TEXT_CHARS,
4513			})
4514		);
4515		assert!(!request.query.requires_workspace_snapshot());
4516
4517		let request = parse_query(
4518			"syntax.parse language:\"sql\" source:\"SELECT 1\" max_depth:1000 max_nodes:20000",
4519		)
4520		.expect("client-selected syntax budgets");
4521		let Query::SyntaxParse(query) = request.query else {
4522			panic!("expected syntax.parse query");
4523		};
4524		assert_eq!(query.max_depth, 1_000);
4525		assert_eq!(query.max_nodes, 20_000);
4526	}
4527
4528	#[test]
4529	fn parses_symbol_graph_relational_filters() {
4530		let request = parse_query(
4531			"symbol.graph focus:\"src/lib.rs\" direction:incoming relation:[calls,uses_type] min_count:2 include_internal:false",
4532		)
4533		.expect("symbol graph filters");
4534		let Query::SymbolGraph(query) = request.query else {
4535			panic!("expected symbol graph query");
4536		};
4537		assert_eq!(query.direction, UsageDirection::Incoming);
4538		assert_eq!(query.relation, vec!["calls", "uses_type"]);
4539		assert_eq!(query.min_count, 2);
4540		assert!(!query.include_internal);
4541	}
4542
4543	#[test]
4544	fn parses_bounded_graph_path_contract() {
4545		let request = parse_query(
4546			"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",
4547		)
4548		.expect("graph path contract");
4549		let Query::GraphPath(query) = request.query else {
4550			panic!("expected graph path query");
4551		};
4552		assert_eq!(query.from, "code+moniker://./fn:callback()");
4553		assert_eq!(query.to, "code+moniker://./fn:repository()");
4554		assert_eq!(query.expect, GraphPathExpectation::NoPath);
4555		assert_eq!(query.relation, vec!["calls", "method_call"]);
4556		assert_eq!(query.max_depth, 6);
4557		assert_eq!(query.max_symbols, 200);
4558		assert_eq!(query.max_edges, 500);
4559		assert_eq!(query.min_coverage, 95);
4560	}
4561
4562	#[test]
4563	fn rejects_unbounded_graph_path_limits() {
4564		assert!(matches!(
4565			parse_query(
4566				"graph.path from:\"symbol:0:0\" to:\"symbol:0:1\" max_depth:65"
4567			),
4568			Err(QueryParseError::InvalidValue { ref key, .. }) if key == "max_depth"
4569		));
4570		assert!(matches!(
4571			parse_query(
4572				"graph.path from:\"symbol:0:0\" to:\"symbol:0:1\" min_coverage:101"
4573			),
4574			Err(QueryParseError::InvalidValue { ref key, .. }) if key == "min_coverage"
4575		));
4576		assert!(matches!(
4577			parse_query(
4578				"graph.path from:\"symbol:0:0\" to:\"symbol:0:1\" max_symbols:0"
4579			),
4580			Err(QueryParseError::InvalidValue { ref key, .. }) if key == "max_symbols"
4581		));
4582	}
4583
4584	#[test]
4585	fn preserves_commas_in_quoted_symbol_uri() {
4586		let uri = "code+moniker://./lang:ts/dir:src/module:Session/class:Session/method:launchRequest(response:DebugProtocol.LaunchResponse,args:LaunchRequestArguments)";
4587		let request = parse_query(&format!(
4588			"symbol.usages uri:\"{uri}\" direction:outgoing limit:5"
4589		))
4590		.expect("quoted symbol URI");
4591		let Query::SymbolUsages(query) = request.query else {
4592			panic!("expected symbol usages query");
4593		};
4594		assert_eq!(query.uri, uri);
4595	}
4596
4597	#[test]
4598	fn quoted_multi_value_field_does_not_split_commas() {
4599		let request =
4600			parse_query("symbol.search path:\"src/generated,a.ts\" shape:\"callable,type\"")
4601				.expect("quoted multi-value fields");
4602		let Query::SymbolSearch(query) = request.query else {
4603			panic!("expected symbol search query");
4604		};
4605		assert_eq!(query.path, vec!["src/generated,a.ts"]);
4606		assert_eq!(query.shape, vec!["callable,type"]);
4607	}
4608
4609	#[test]
4610	fn symbol_usages_can_roll_up_descendant_member_activity_explicitly() {
4611		let request = parse_query(
4612			"symbol.usages uri:\"symbol:1:2\" direction:incoming include_descendants:true",
4613		)
4614		.expect("owner rollup usage query");
4615		let Query::SymbolUsages(query) = request.query else {
4616			panic!("expected symbol usages query");
4617		};
4618		assert!(query.include_descendants);
4619		let capability = describe_query_capabilities(Some("symbol.usages"))
4620			.expect("symbol usages capability")
4621			.capabilities
4622			.pop()
4623			.expect("symbol usages descriptor");
4624		let field = capability
4625			.fields
4626			.iter()
4627			.find(|field| field.name == "include_descendants")
4628			.expect("descendant scope field");
4629		assert_eq!(field.value_type, "boolean");
4630		assert_eq!(field.default.as_deref(), Some("false"));
4631	}
4632
4633	#[test]
4634	fn unquoted_multi_value_fields_still_split_commas() {
4635		let request = parse_query("symbol.search path:src,tests shape:callable,type")
4636			.expect("unquoted multi-value fields");
4637		let Query::SymbolSearch(query) = request.query else {
4638			panic!("expected symbol search query");
4639		};
4640		assert_eq!(query.path, vec!["src", "tests"]);
4641		assert_eq!(query.shape, vec!["callable", "type"]);
4642	}
4643
4644	#[test]
4645	fn identity_graph_accepts_path_scope_without_changing_identity_children() {
4646		let request = parse_query(
4647			"identity.graph prefix:\"lang:java/package:com/package:acme\" path:src/main/**,src/test/**",
4648		)
4649		.expect("path-scoped identity graph");
4650		let Query::IdentityGraph(query) = request.query else {
4651			panic!("expected identity graph query");
4652		};
4653		assert_eq!(query.path, vec!["src/main/**", "src/test/**"]);
4654
4655		assert!(matches!(
4656			parse_query("identity.children path:src/main/**"),
4657			Err(QueryParseError::UnknownField { .. })
4658		));
4659	}
4660
4661	#[test]
4662	fn identity_graph_declares_weight_filter_and_real_pagination() {
4663		let request = parse_query("identity.graph prefix:\"lang:rs/dir:src\" min_count:3 limit:2")
4664			.expect("filtered identity graph");
4665		let Query::IdentityGraph(query) = request.query else {
4666			panic!("expected identity graph query");
4667		};
4668		assert_eq!(query.min_count, 3);
4669		assert_eq!(request.page.limit, 2);
4670		assert!(
4671			query_capability_spec("identity.graph")
4672				.expect("identity graph capability")
4673				.paginated
4674		);
4675	}
4676
4677	#[test]
4678	fn coupling_metrics_requires_explicit_scopes_and_accepts_relations() {
4679		let request = parse_query(
4680			"metrics.coupling from:\"lang:java/package:com/package:acme\" to:\"lang:java/package:com/package:storage\" relation:calls,imports_symbol snapshot:current export:true",
4681		)
4682		.expect("coupling metrics query");
4683		let Query::MetricsCoupling(query) = request.query else {
4684			panic!("expected coupling metrics query");
4685		};
4686		assert_eq!(query.from, "lang:java/package:com/package:acme");
4687		assert_eq!(query.to, "lang:java/package:com/package:storage");
4688		assert_eq!(query.relation, vec!["calls", "imports_symbol"]);
4689		assert_eq!(query.snapshot.as_deref(), Some("current"));
4690		assert!(query.export);
4691		let Query::MetricsCoupling(default_export) =
4692			parse_query("metrics.coupling from:\"lang:java\" to:\"lang:java/package:com\"")
4693				.expect("coupling metrics without export")
4694				.query
4695		else {
4696			panic!("expected coupling metrics query");
4697		};
4698		assert!(!default_export.export);
4699		assert!(matches!(
4700			parse_query("metrics.coupling from:\"lang:java\""),
4701			Err(QueryParseError::MissingRequired("to"))
4702		));
4703		assert!(
4704			!query_capability_spec("metrics.coupling")
4705				.expect("metrics capability")
4706				.paginated
4707		);
4708	}
4709
4710	#[test]
4711	fn scalar_field_preserves_unquoted_regex_commas() {
4712		let request = parse_query("symbol.search name:^launch(Request){1,3}$").expect("name regex");
4713		let Query::SymbolSearch(query) = request.query else {
4714			panic!("expected symbol search query");
4715		};
4716		assert_eq!(query.name.as_deref(), Some("^launch(Request){1,3}$"));
4717	}
4718
4719	#[test]
4720	fn parses_resolution_audit_positional_prefix() {
4721		let request = parse_query("resolution.audit java limit:7 cluster:resolution-abc")
4722			.expect("audit query");
4723		let Query::ResolutionAudit(query) = request.query else {
4724			panic!("expected resolution audit query");
4725		};
4726		assert_eq!(query.prefix, "java");
4727		assert_eq!(query.limit, 7);
4728		assert_eq!(query.cluster.as_deref(), Some("resolution-abc"));
4729	}
4730
4731	#[test]
4732	fn resolution_audit_default_limit_matches_its_documented_contract() {
4733		let request = parse_query("resolution.audit python").expect("audit query");
4734		let Query::ResolutionAudit(query) = request.query else {
4735			panic!("expected resolution audit query");
4736		};
4737		assert_eq!(query.limit, 20);
4738	}
4739
4740	#[test]
4741	fn formats_resolution_audit_explanation_metrics() {
4742		let response = QueryResponse {
4743			generation: None,
4744			result: QueryResult::ResolutionAudit(Box::new(ResolutionAuditResult {
4745				prefix: "lang:python".to_string(),
4746				totals: AuditTotalsDto {
4747					references: 10,
4748					resolved: 4,
4749					unique: 4,
4750					candidate: 2,
4751					external: 1,
4752					sdk: 1,
4753					dependency: 0,
4754					injected_external: 0,
4755					unknown_external: 0,
4756					dynamic: 1,
4757					blocked: 1,
4758					unresolved: 1,
4759					explained: 9,
4760					weak_or_unexplained: 3,
4761					name_match_resolved: 0,
4762					name_match_candidate: 2,
4763				},
4764				clusters: vec![AuditClusterDto {
4765					id: "resolution-abc".to_string(),
4766					pattern: "candidate name_match/method_call".to_string(),
4767					count: 2,
4768					samples: vec![
4769						AuditSampleDto {
4770							file: "src/a.py".to_string(),
4771							line_range: Some((10, 10)),
4772							snippet: "value.first()".to_string(),
4773							source: "module:a".to_string(),
4774							call_name: "first".to_string(),
4775							receiver: "value".to_string(),
4776							target: "method:first".to_string(),
4777							evidence: "name_match".to_string(),
4778							constraints: vec!["scope:global".to_string()],
4779							candidates: vec!["class:A/method:first".to_string()],
4780						},
4781						AuditSampleDto {
4782							file: "src/b.py".to_string(),
4783							line_range: Some((20, 21)),
4784							snippet: "other.second()".to_string(),
4785							source: "module:b".to_string(),
4786							call_name: "second".to_string(),
4787							receiver: "other".to_string(),
4788							target: "method:second".to_string(),
4789							evidence: "name_match".to_string(),
4790							constraints: Vec::new(),
4791							candidates: Vec::new(),
4792						},
4793					],
4794				}],
4795				zones: Vec::new(),
4796			})),
4797			next_cursor: None,
4798		};
4799
4800		let formatted = format_query_response(&response);
4801
4802		assert!(formatted.contains("unique: 4 candidate: 2"));
4803		assert!(formatted.contains("explained: 9 weak_or_unexplained: 3"));
4804		assert!(formatted.contains("name_match_candidate: 2"));
4805		assert!(formatted.contains("resolution-abc"));
4806		assert!(formatted.contains("first value"));
4807		assert!(formatted.contains("second other"));
4808		assert!(formatted.contains("src/a.py:10"));
4809		assert!(formatted.contains("src/b.py:20-21"));
4810		assert!(formatted.contains("constraints: scope:global"));
4811		assert!(formatted.contains("code: value.first()"));
4812	}
4813
4814	#[test]
4815	fn rejects_unknown_projection_field_with_suggestion() {
4816		let error =
4817			parse_query("symbol.search name:App\nproject nme uri").expect_err("unknown projection");
4818		let message = error.to_string();
4819		assert!(
4820			message.contains("unknown projection field `nme`"),
4821			"{message}"
4822		);
4823		assert!(message.contains("did you mean `name`?"), "{message}");
4824	}
4825
4826	#[test]
4827	fn projected_formatter_emits_only_requested_symbol_fields() {
4828		let response = QueryResponse {
4829			generation: Some(WorkspaceGeneration(3)),
4830			result: QueryResult::SymbolList(SymbolListResult {
4831				rows: vec![SymbolDto {
4832					root: ".".to_string(),
4833					uri: "code+moniker://./lang:rs/fn:run()".to_string(),
4834					id: "id".to_string(),
4835					name: "run".to_string(),
4836					kind: "fn".to_string(),
4837					visibility: "public".to_string(),
4838					signature: "run()".to_string(),
4839					file: "src/lib.rs".to_string(),
4840					language: "rs".to_string(),
4841					line_range: Some((4, 8)),
4842					navigable: true,
4843					score: None,
4844					match_reason: None,
4845					source: None,
4846				}],
4847				total: 1,
4848			}),
4849			next_cursor: None,
4850		};
4851		let formatted =
4852			format_query_response_projected(&response, &["name".to_string(), "uri".to_string()]);
4853		assert!(
4854			formatted.contains("name=run uri=code+moniker://"),
4855			"{formatted}"
4856		);
4857		assert!(!formatted.contains("src/lib.rs"), "{formatted}");
4858		assert!(!formatted.contains("signature="), "{formatted}");
4859	}
4860
4861	#[test]
4862	fn parses_human_symbol_search() {
4863		let query = parse_query(
4864			r#"symbol.search "SharedWorkspaceIndex"
4865  filter path:"crates/**" shape:type
4866  project name, kind, uri
4867  page limit:20 cursor:7:40"#,
4868		)
4869		.expect("query");
4870		assert_eq!(query.page.limit, 20);
4871		assert_eq!(
4872			query.page.cursor,
4873			Some(QueryCursor::new(40, Some(WorkspaceGeneration(7))))
4874		);
4875		match query.query {
4876			Query::SymbolSearch(search) => {
4877				assert_eq!(search.text.as_deref(), Some("SharedWorkspaceIndex"));
4878				assert_eq!(search.path, vec!["crates/**"]);
4879				assert_eq!(search.shape, vec!["type"]);
4880				assert_eq!(search.projection, vec!["name", "kind", "uri"]);
4881			}
4882			other => panic!("unexpected query {other:?}"),
4883		}
4884	}
4885
4886	#[test]
4887	fn parses_rules_check_consistency() {
4888		let query = parse_query(
4889			r#"rules.check profile:"agent"
4890  consistency refresh-if-stale
4891  page limit:50"#,
4892		)
4893		.expect("query");
4894		assert_eq!(query.consistency, Consistency::RefreshIfStale);
4895		assert_eq!(query.page.limit, 50);
4896	}
4897
4898	#[test]
4899	fn rejects_offset_only_human_cursor() {
4900		let error =
4901			parse_query("symbol.search Customer\npage cursor:40").expect_err("offset-only cursor");
4902		assert!(matches!(
4903			error,
4904			QueryParseError::InvalidValue { ref key, .. } if key == "cursor"
4905		));
4906	}
4907
4908	#[test]
4909	fn parses_bracket_list_shape() {
4910		let query = parse_query("symbol.search shape:[callable,type] limit:5").expect("query");
4911		match query.query {
4912			Query::SymbolSearch(search) => assert_eq!(search.shape, vec!["callable", "type"]),
4913			other => panic!("unexpected query {other:?}"),
4914		}
4915	}
4916
4917	#[test]
4918	fn rejects_unterminated_bracket_list() {
4919		let error = parse_query("symbol.search shape:[callable").expect_err("unterminated list");
4920		assert!(matches!(
4921			error,
4922			QueryParseError::InvalidValue { ref key, .. } if key == "shape"
4923		));
4924	}
4925
4926	#[test]
4927	fn rejects_unknown_field_with_alias_suggestion() {
4928		let error = parse_query(r#"symbol.search text:"foo""#).expect_err("unknown field");
4929		let message = error.to_string();
4930		assert!(
4931			message.contains("unknown field `text` for `symbol.search`"),
4932			"{message}"
4933		);
4934		assert!(message.contains("did you mean `name`?"), "{message}");
4935	}
4936
4937	#[test]
4938	fn rejects_typo_field_with_suggestion() {
4939		let error = parse_query("rules.check profil:agent").expect_err("typo field");
4940		let message = error.to_string();
4941		assert!(message.contains("did you mean `profile`?"), "{message}");
4942	}
4943
4944	#[test]
4945	fn lists_valid_fields_without_close_match() {
4946		let error = parse_query("change.review foobarbaz:1").expect_err("unknown field");
4947		let message = error.to_string();
4948		assert!(
4949			message.contains("valid fields: consistency, cursor, limit, workspace"),
4950			"{message}"
4951		);
4952	}
4953
4954	#[test]
4955	fn rejects_unexpected_positional() {
4956		let error = parse_query("workspace.status extra").expect_err("positional");
4957		assert!(matches!(
4958			error,
4959			QueryParseError::UnexpectedArgument { ref value, .. } if value == "extra"
4960		));
4961	}
4962
4963	#[test]
4964	fn rejects_projection_on_unsupported_verb() {
4965		let error = parse_query("rules.list\nproject name").expect_err("projection");
4966		assert!(matches!(
4967			error,
4968			QueryParseError::UnsupportedProjection { .. }
4969		));
4970	}
4971
4972	#[test]
4973	fn parses_inline_consistency() {
4974		let query =
4975			parse_query("rules.check profile:agent consistency:refresh-if-stale").expect("query");
4976		assert_eq!(query.consistency, Consistency::RefreshIfStale);
4977	}
4978
4979	#[test]
4980	fn formats_generation_aware_cursor() {
4981		let response = QueryResponse {
4982			generation: Some(WorkspaceGeneration(7)),
4983			result: QueryResult::SymbolList(SymbolListResult {
4984				rows: Vec::new(),
4985				total: 0,
4986			}),
4987			next_cursor: Some(QueryCursor::new(40, Some(WorkspaceGeneration(7)))),
4988		};
4989		let formatted = format_query_response(&response);
4990		assert!(formatted.contains("next_cursor: 7:40"));
4991	}
4992
4993	#[test]
4994	fn context_graph_internal_edges_join_member_names() {
4995		let member = |id: &str, name: &str| SymbolDto {
4996			root: String::new(),
4997			uri: format!("code+moniker://./module:m/fn:{name}"),
4998			id: id.to_string(),
4999			name: name.to_string(),
5000			kind: "fn".to_string(),
5001			visibility: "pub".to_string(),
5002			signature: String::new(),
5003			file: "src/lib.rs".to_string(),
5004			language: "rs".to_string(),
5005			line_range: None,
5006			navigable: true,
5007			score: None,
5008			match_reason: None,
5009			source: None,
5010		};
5011		let graph = SymbolGraphResult {
5012			focus: SymbolGraphFocus::File {
5013				path: "src/lib.rs".to_string(),
5014			},
5015			coverage: SymbolGraphCoverage::default(),
5016			members: vec![
5017				member("symbol:40:2", "alpha()"),
5018				member("symbol:40:7", "beta()"),
5019			],
5020			internal_edges: vec![
5021				SymbolGraphEdge {
5022					source: "symbol:40:2".to_string(),
5023					target: "symbol:40:7".to_string(),
5024					kinds: vec!["calls".to_string()],
5025					count: 2,
5026				},
5027				SymbolGraphEdge {
5028					source: "symbol:40:2".to_string(),
5029					target: "symbol:99:9".to_string(),
5030					kinds: vec!["reads".to_string()],
5031					count: 1,
5032				},
5033				SymbolGraphEdge {
5034					source: "symbol:98:8".to_string(),
5035					target: "symbol:99:9".to_string(),
5036					kinds: vec!["calls".to_string()],
5037					count: 1,
5038				},
5039			],
5040			callers: Vec::new(),
5041			callees: Vec::new(),
5042			unlinked: UnlinkedRefsDto::default(),
5043		};
5044		let mut out = String::new();
5045		format_context_graph(&mut out, &graph);
5046		assert!(
5047			out.contains("- fn alpha() -> fn beta() x2 [calls]"),
5048			"internal edges must join member names, got:\n{out}"
5049		);
5050		assert!(
5051			out.contains("- fn alpha() -> unlisted internal member 2 x1 [reads]"),
5052			"bounded contexts must explain omitted endpoints, got:\n{out}"
5053		);
5054		assert!(
5055			out.contains("- unlisted internal member 1 -> unlisted internal member 2 x1 [calls]"),
5056			"distinct omitted members must preserve the graph topology, got:\n{out}"
5057		);
5058		assert!(
5059			!out.contains("symbol:98:8") && !out.contains("symbol:99:9"),
5060			"internal storage ordinals must not leak into agent output:\n{out}"
5061		);
5062	}
5063}
5064
5065/// Umbrella over every root RPC type, used only to emit a single JSON Schema
5066/// document (`export-schema`) whose definitions cover the whole wire contract.
5067#[cfg(feature = "schema")]
5068#[derive(schemars::JsonSchema)]
5069#[allow(dead_code)]
5070pub struct DaemonProtocol {
5071	pub handshake: HandshakeResponse,
5072	pub registry_entry: DaemonRegistryEntry,
5073	pub workspace_config: DaemonWorkspaceConfig,
5074	pub query_request: QueryRequest,
5075	pub query: Query,
5076	pub query_response: QueryResponse,
5077	pub query_result: QueryResult,
5078	pub command_request: CommandRequest,
5079	pub command_response: CommandResponse,
5080	pub event: WorkspaceEventDto,
5081	pub error: QueryError,
5082}
5083
5084#[cfg(test)]
5085mod contract_tests {
5086	//! Lock the serde wire shapes the JSON Schema (and every generated client)
5087	//! depends on. These guard the contract, not the Rust layout.
5088	use super::*;
5089	use serde_json::json;
5090
5091	#[test]
5092	fn query_is_op_tagged() {
5093		let query = Query::SymbolSearch(SymbolSearchQuery {
5094			text: Some("widget".to_string()),
5095			..Default::default()
5096		});
5097		let value = serde_json::to_value(&query).unwrap();
5098		assert_eq!(value["op"], "symbol_search");
5099		assert_eq!(value["text"], "widget");
5100	}
5101
5102	#[test]
5103	fn query_result_is_kind_and_data_tagged() {
5104		let result = QueryResult::SymbolList(SymbolListResult {
5105			rows: Vec::new(),
5106			total: 0,
5107		});
5108		assert_eq!(
5109			serde_json::to_value(&result).unwrap(),
5110			json!({ "kind": "symbol_list", "data": { "rows": [], "total": 0 } }),
5111		);
5112	}
5113
5114	#[test]
5115	fn generation_serializes_as_scalar() {
5116		assert_eq!(
5117			serde_json::to_value(WorkspaceGeneration(7)).unwrap(),
5118			json!(7)
5119		);
5120	}
5121
5122	#[test]
5123	fn line_range_is_a_two_element_array() {
5124		let range: Option<(u32, u32)> = Some((3, 9));
5125		assert_eq!(serde_json::to_value(range).unwrap(), json!([3, 9]));
5126		assert_eq!(
5127			serde_json::to_value(Option::<(u32, u32)>::None).unwrap(),
5128			json!(null)
5129		);
5130	}
5131
5132	#[test]
5133	fn consistency_is_snake_case() {
5134		assert_eq!(
5135			serde_json::to_value(Consistency::RefreshIfStale).unwrap(),
5136			json!("refresh_if_stale"),
5137		);
5138	}
5139
5140	#[test]
5141	fn event_kind_is_snake_case() {
5142		let event = WorkspaceEventDto {
5143			kind: WorkspaceEventKind::GitBase,
5144			generation: None,
5145			stale_summary: None,
5146		};
5147		assert_eq!(serde_json::to_value(&event).unwrap()["kind"], "git_base");
5148	}
5149
5150	#[test]
5151	fn workspace_source_set_commands_are_op_tagged() {
5152		let replace = Command::WorkspaceSourceSetReplace {
5153			source_set: WorkspaceSourceSetDto {
5154				srcset: "database".to_string(),
5155				revision: Some("42".to_string()),
5156				documents: vec![WorkspaceSourceDocumentDto {
5157					uri: "schema/accounts.sql".to_string(),
5158					language: "sql".to_string(),
5159					content: "CREATE TABLE accounts (id bigint);".to_string(),
5160				}],
5161			},
5162		};
5163		assert_eq!(
5164			serde_json::to_value(replace).unwrap(),
5165			json!({
5166				"op": "workspace_source_set_replace",
5167				"source_set": {
5168					"srcset": "database",
5169					"revision": "42",
5170					"documents": [{
5171						"uri": "schema/accounts.sql",
5172						"language": "sql",
5173						"content": "CREATE TABLE accounts (id bigint);"
5174					}]
5175				}
5176			}),
5177		);
5178
5179		assert_eq!(
5180			serde_json::to_value(Command::WorkspaceSourceSetRemove {
5181				srcset: "database".to_string(),
5182			})
5183			.unwrap(),
5184			json!({
5185				"op": "workspace_source_set_remove",
5186				"srcset": "database"
5187			}),
5188		);
5189		assert!(
5190			CapabilitySet::default()
5191				.commands
5192				.iter()
5193				.any(|command| command == "workspace.source_set.replace")
5194		);
5195		assert!(
5196			CapabilitySet::default()
5197				.queries
5198				.iter()
5199				.any(|query| query == "diff-impact.compare")
5200		);
5201		assert_eq!(PROTOCOL_VERSION, 17);
5202	}
5203
5204	#[test]
5205	fn test_artifact_classification_covers_canonical_paths_kinds_and_modules() {
5206		for path in [
5207			"tests/service.rs",
5208			"benches/speed.rs",
5209			"fixtures/input.py",
5210			"testdata/schema.sql",
5211			"src/__tests__/service.ts",
5212		] {
5213			assert!(symbol_is_test_artifact("function", path, ""), "{path}");
5214		}
5215		assert!(symbol_is_test_artifact("test", "src/lib.rs", ""));
5216		assert!(symbol_is_test_artifact(
5217			"function",
5218			"src/lib.rs",
5219			"code+moniker://module:tests/function:works()"
5220		));
5221		assert!(!symbol_is_test_artifact("function", "src/lib.rs", ""));
5222	}
5223}