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