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::*;
10
11#[cfg(feature = "rpc")]
12pub mod rpc {
13	use jsonrpsee::core::SubscriptionResult;
14	use jsonrpsee::proc_macros::rpc;
15	use jsonrpsee::types::ErrorObjectOwned;
16
17	use crate::{
18		CommandRequest, CommandResponse, HandshakeResponse, QueryRequest, QueryResponse,
19		WorkspaceEventDto,
20	};
21
22	pub const RPC_NAMESPACE: &str = "moniker";
23
24	#[rpc(server, client, namespace = "moniker")]
25	pub trait DaemonRpc {
26		#[method(name = "handshake")]
27		async fn handshake(&self, client: String) -> Result<HandshakeResponse, ErrorObjectOwned>;
28
29		#[method(name = "query")]
30		async fn query(&self, request: QueryRequest) -> Result<QueryResponse, ErrorObjectOwned>;
31
32		#[method(name = "command")]
33		async fn command(
34			&self,
35			request: CommandRequest,
36		) -> Result<CommandResponse, ErrorObjectOwned>;
37
38		#[method(name = "shutdown")]
39		async fn shutdown(&self) -> Result<(), ErrorObjectOwned>;
40
41		#[subscription(name = "subscribeEvents" => "events", unsubscribe = "unsubscribeEvents", item = WorkspaceEventDto)]
42		async fn subscribe_events(&self) -> SubscriptionResult;
43	}
44}
45
46#[cfg(feature = "rpc")]
47pub use rpc::*;
48
49pub const PROTOCOL_VERSION: u32 = 3;
50
51#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[serde(tag = "type", rename_all = "snake_case")]
54pub enum ProtocolRequest {
55	Query(Box<QueryRequest>),
56	Command(CommandRequest),
57}
58
59#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
60#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
61#[serde(tag = "type", rename_all = "snake_case")]
62pub enum ProtocolResponse {
63	Query(Box<QueryResponse>),
64	Command(CommandResponse),
65	Error(QueryError),
66}
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
70pub struct HandshakeResponse {
71	pub protocol_version: u32,
72	pub daemon_version: String,
73	pub workspace_root: String,
74	pub workspace_roots: Vec<String>,
75	pub capabilities: CapabilitySet,
76}
77
78#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
79#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
80pub struct DaemonWorkspaceConfig {
81	pub roots: Vec<String>,
82	pub project: Option<String>,
83	pub cache_dir: Option<String>,
84	pub live_refresh: Option<String>,
85}
86
87#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89pub struct CapabilitySet {
90	pub queries: Vec<String>,
91	#[serde(default)]
92	pub query_mcp_tools: BTreeMap<String, String>,
93	pub commands: Vec<String>,
94	pub events: Vec<String>,
95}
96
97impl Default for CapabilitySet {
98	fn default() -> Self {
99		Self {
100			queries: query_capability_specs()
101				.iter()
102				.map(|spec| spec.name.to_string())
103				.collect(),
104			query_mcp_tools: query_capability_specs()
105				.iter()
106				.map(|spec| (spec.name.to_string(), spec.mcp_tool.to_string()))
107				.collect(),
108			commands: vec!["workspace.refresh".to_string()],
109			events: Vec::new(),
110		}
111	}
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub struct QueryCapabilitySpec {
116	pub name: &'static str,
117	pub category: &'static str,
118	pub read_only: bool,
119	pub mcp_tool: &'static str,
120	pub fields: &'static [&'static str],
121	pub required_fields: &'static [&'static str],
122	pub positionals: usize,
123	pub projection: bool,
124	pub paginated: bool,
125	pub example: &'static str,
126}
127
128const COMMON_FIELDS: &[&str] = &["limit", "cursor", "consistency"];
129const BRACKET_LIST_FIELDS: &[&str] = &["lang", "kind", "shape", "severity", "relation"];
130const MULTI_VALUE_FIELDS: &[&str] = &[
131	"path", "lang", "kind", "shape", "severity", "file", "relation",
132];
133
134const QUERY_CAPABILITY_SPECS: &[QueryCapabilitySpec] = &[
135	QueryCapabilitySpec {
136		name: "query.describe",
137		category: "discovery",
138		read_only: true,
139		mcp_tool: "code_moniker_query",
140		fields: &["verb"],
141		required_fields: &[],
142		positionals: 1,
143		projection: false,
144		paginated: false,
145		example: "query.describe verb:\"symbol.usages\"",
146	},
147	QueryCapabilitySpec {
148		name: "workspace.status",
149		category: "workspace",
150		read_only: true,
151		mcp_tool: "code_moniker_read",
152		fields: &[],
153		required_fields: &[],
154		positionals: 0,
155		projection: false,
156		paginated: false,
157		example: "workspace.status",
158	},
159	QueryCapabilitySpec {
160		name: "tree.children",
161		category: "navigation",
162		read_only: true,
163		mcp_tool: "code_moniker_read",
164		fields: &["workspace", "path", "depth", "lang"],
165		required_fields: &[],
166		positionals: 0,
167		projection: true,
168		paginated: true,
169		example: "tree.children path:\"src/**\" depth:2 limit:20",
170	},
171	QueryCapabilitySpec {
172		name: "symbol.search",
173		category: "symbol",
174		read_only: true,
175		mcp_tool: "code_moniker_symbols",
176		fields: &[
177			"workspace",
178			"path",
179			"lang",
180			"kind",
181			"shape",
182			"name",
183			"include_non_navigable",
184			"include_code",
185			"context_lines",
186		],
187		required_fields: &[],
188		positionals: 1,
189		projection: true,
190		paginated: true,
191		example: "symbol.search name:\"PaymentService\" shape:type limit:10",
192	},
193	QueryCapabilitySpec {
194		name: "symbol.insights",
195		category: "symbol",
196		read_only: true,
197		mcp_tool: "code_moniker_symbols",
198		fields: &[
199			"workspace",
200			"path",
201			"lang",
202			"kind",
203			"shape",
204			"name",
205			"include_non_navigable",
206		],
207		required_fields: &[],
208		positionals: 0,
209		projection: true,
210		paginated: false,
211		example: "symbol.insights path:\"src/**\"",
212	},
213	QueryCapabilitySpec {
214		name: "symbol.detail",
215		category: "symbol",
216		read_only: true,
217		mcp_tool: "code_moniker_read",
218		fields: &["workspace", "uri", "context_lines"],
219		required_fields: &["uri"],
220		positionals: 1,
221		projection: false,
222		paginated: false,
223		example: "symbol.detail uri:\"code+moniker://...\" context_lines:2",
224	},
225	QueryCapabilitySpec {
226		name: "symbol.usages",
227		category: "symbol",
228		read_only: true,
229		mcp_tool: "code_moniker_usages",
230		fields: &["workspace", "uri", "direction", "path", "lang"],
231		required_fields: &["uri"],
232		positionals: 1,
233		projection: true,
234		paginated: true,
235		example: "symbol.usages uri:\"code+moniker://...\" direction:incoming limit:20",
236	},
237	QueryCapabilitySpec {
238		name: "view.read",
239		category: "context",
240		read_only: true,
241		mcp_tool: "code_moniker_read",
242		fields: &["uri", "scheme", "context_lines", "include_code"],
243		required_fields: &["uri"],
244		positionals: 1,
245		projection: false,
246		paginated: false,
247		example: "view.read uri:\"workspace/views\"",
248	},
249	QueryCapabilitySpec {
250		name: "rules.list",
251		category: "rules",
252		read_only: true,
253		mcp_tool: "code_moniker_rules",
254		fields: &["workspace", "profile", "rules", "lang", "severity"],
255		required_fields: &[],
256		positionals: 0,
257		projection: false,
258		paginated: true,
259		example: "rules.list profile:agent limit:20",
260	},
261	QueryCapabilitySpec {
262		name: "rules.check",
263		category: "rules",
264		read_only: true,
265		mcp_tool: "code_moniker_rules",
266		fields: &["workspace", "profile", "rules", "file", "report"],
267		required_fields: &[],
268		positionals: 0,
269		projection: false,
270		paginated: true,
271		example: "rules.check profile:agent file:\"src/**\" limit:20",
272	},
273	QueryCapabilitySpec {
274		name: "rules.applicable",
275		category: "rules",
276		read_only: true,
277		mcp_tool: "code_moniker_query",
278		fields: &["workspace", "focus", "profile", "rules"],
279		required_fields: &["focus"],
280		positionals: 1,
281		projection: false,
282		paginated: true,
283		example: "rules.applicable focus:\"code+moniker://...\" profile:agent limit:20",
284	},
285	QueryCapabilitySpec {
286		name: "change.review",
287		category: "change",
288		read_only: true,
289		mcp_tool: "code_moniker_diff",
290		fields: &["workspace"],
291		required_fields: &[],
292		positionals: 0,
293		projection: false,
294		paginated: false,
295		example: "change.review",
296	},
297	QueryCapabilitySpec {
298		name: "change.context",
299		category: "change",
300		read_only: true,
301		mcp_tool: "code_moniker_context",
302		fields: &["workspace", "focus", "profile", "max_items"],
303		required_fields: &["focus"],
304		positionals: 1,
305		projection: false,
306		paginated: false,
307		example: "change.context focus:\"code+moniker://...\" profile:agent max_items:20",
308	},
309	QueryCapabilitySpec {
310		name: "symbol.graph",
311		category: "graph",
312		read_only: true,
313		mcp_tool: "code_moniker_graph",
314		fields: &[
315			"workspace",
316			"focus",
317			"direction",
318			"relation",
319			"min_count",
320			"include_internal",
321		],
322		required_fields: &["focus"],
323		positionals: 1,
324		projection: false,
325		paginated: false,
326		example: "symbol.graph focus:\"src/service.ts\"",
327	},
328	QueryCapabilitySpec {
329		name: "identity.children",
330		category: "graph",
331		read_only: true,
332		mcp_tool: "code_moniker_query",
333		fields: &["workspace", "prefix"],
334		required_fields: &[],
335		positionals: 1,
336		projection: false,
337		paginated: false,
338		example: "identity.children prefix:\"lang:rs/dir:crates\"",
339	},
340	QueryCapabilitySpec {
341		name: "identity.graph",
342		category: "graph",
343		read_only: true,
344		mcp_tool: "code_moniker_query",
345		fields: &["workspace", "prefix"],
346		required_fields: &[],
347		positionals: 1,
348		projection: false,
349		paginated: false,
350		example: "identity.graph prefix:\"lang:rs/dir:crates\"",
351	},
352	QueryCapabilitySpec {
353		name: "resolution.audit",
354		category: "diagnostic",
355		read_only: true,
356		mcp_tool: "code_moniker_query",
357		fields: &["workspace", "prefix", "cluster"],
358		required_fields: &[],
359		positionals: 1,
360		projection: false,
361		paginated: true,
362		example: "resolution.audit prefix:\"lang:java\" limit:20",
363	},
364	QueryCapabilitySpec {
365		name: "notes",
366		category: "notes",
367		read_only: false,
368		mcp_tool: "code_moniker_notes",
369		fields: &[
370			"action",
371			"id",
372			"moniker",
373			"kind",
374			"status",
375			"title",
376			"body",
377			"created_by",
378			"orphan",
379			"include_done",
380		],
381		required_fields: &[],
382		positionals: 0,
383		projection: false,
384		paginated: true,
385		example: "notes action:list limit:20",
386	},
387];
388
389pub fn query_capability_specs() -> &'static [QueryCapabilitySpec] {
390	QUERY_CAPABILITY_SPECS
391}
392
393pub fn query_capability_spec(name: &str) -> Option<&'static QueryCapabilitySpec> {
394	QUERY_CAPABILITY_SPECS.iter().find(|spec| spec.name == name)
395}
396
397pub fn query_projection_fields(name: &str) -> &'static [&'static str] {
398	match name {
399		"tree.children" => &[
400			"root",
401			"path",
402			"kind",
403			"language",
404			"defs",
405			"refs",
406			"change_count",
407		],
408		"symbol.search" => &[
409			"root",
410			"uri",
411			"id",
412			"name",
413			"kind",
414			"visibility",
415			"signature",
416			"file",
417			"language",
418			"line_range",
419			"navigable",
420			"score",
421			"match_reason",
422			"source",
423		],
424		"symbol.insights" => &[
425			"files",
426			"symbols",
427			"references",
428			"navigable_symbols",
429			"non_navigable_symbols",
430			"languages",
431			"kinds",
432			"shapes",
433			"top_files_by_symbols",
434			"top_files_by_refs",
435		],
436		"symbol.usages" => &[
437			"root",
438			"direction",
439			"reference",
440			"kind",
441			"actor",
442			"context",
443			"endpoint",
444			"file",
445			"prefix",
446			"location",
447			"line_range",
448			"via",
449		],
450		_ => &[],
451	}
452}
453
454pub fn describe_query_capabilities(verb: Option<&str>) -> Option<QueryDescribeResult> {
455	let specs: Vec<&QueryCapabilitySpec> = match verb {
456		Some(name) => vec![query_capability_spec(name)?],
457		None => QUERY_CAPABILITY_SPECS.iter().collect(),
458	};
459	Some(QueryDescribeResult {
460		capabilities: specs.into_iter().map(query_capability_dto).collect(),
461	})
462}
463
464fn query_capability_dto(spec: &QueryCapabilitySpec) -> QueryCapabilityDto {
465	let fields = spec
466		.fields
467		.iter()
468		.chain(COMMON_FIELDS)
469		.map(|name| QueryFieldDto {
470			name: (*name).to_string(),
471			value_type: query_field_type(name).to_string(),
472			multiple: MULTI_VALUE_FIELDS.contains(name),
473			required: spec.required_fields.contains(name),
474			default: query_field_default(spec.name, name).map(ToOwned::to_owned),
475		})
476		.collect();
477	QueryCapabilityDto {
478		name: spec.name.to_string(),
479		category: spec.category.to_string(),
480		read_only: spec.read_only,
481		mcp_tool: spec.mcp_tool.to_string(),
482		projection: spec.projection,
483		projection_fields: query_projection_fields(spec.name)
484			.iter()
485			.map(|field| (*field).to_string())
486			.collect(),
487		paginated: spec.paginated,
488		positionals: spec.positionals,
489		fields,
490		example: spec.example.to_string(),
491	}
492}
493
494fn query_field_type(name: &str) -> &'static str {
495	match name {
496		"limit" | "depth" | "context_lines" | "max_items" | "min_count" => "unsigned_integer",
497		"include_non_navigable"
498		| "include_code"
499		| "include_internal"
500		| "report"
501		| "orphan"
502		| "include_done" => "boolean",
503		"direction" => "enum:incoming|outgoing|both",
504		"consistency" => "enum:current|refresh-if-stale|stale-ok",
505		"action" => "enum:list|get|create|update|transition|delete",
506		"cursor" => "cursor",
507		name if MULTI_VALUE_FIELDS.contains(&name) => "string_list",
508		_ => "string",
509	}
510}
511
512fn query_field_default(verb: &str, name: &str) -> Option<&'static str> {
513	match (verb, name) {
514		("resolution.audit", "limit") => Some("20"),
515		(_, "limit") => Some("80"),
516		(_, "consistency") => Some("current"),
517		("tree.children", "depth") => Some("1"),
518		("symbol.detail" | "view.read", "context_lines") => Some("2"),
519		("symbol.search", "context_lines") => Some("0"),
520		("symbol.usages", "direction") => Some("incoming"),
521		("symbol.graph", "direction") => Some("both"),
522		("symbol.graph", "min_count") => Some("1"),
523		("symbol.graph", "include_internal") => Some("true"),
524		("rules.check", "report") => Some("true"),
525		("change.context", "max_items") => Some("20"),
526		("notes", "action") => Some("list"),
527		(_, "include_non_navigable" | "include_code" | "include_done") => Some("false"),
528		_ => None,
529	}
530}
531
532#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
533#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
534pub struct QueryRequest {
535	pub query: Query,
536	pub consistency: Consistency,
537	pub page: Page,
538}
539
540impl QueryRequest {
541	pub fn new(query: Query) -> Self {
542		Self {
543			query,
544			consistency: Consistency::Current,
545			page: Page::default(),
546		}
547	}
548}
549
550#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
551#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
552#[serde(tag = "op", rename_all = "snake_case")]
553pub enum Query {
554	QueryDescribe(QueryDescribeQuery),
555	WorkspaceStatus,
556	TreeChildren(TreeChildrenQuery),
557	SymbolSearch(SymbolSearchQuery),
558	SymbolInsights(SymbolSearchQuery),
559	SymbolDetail(SymbolDetailQuery),
560	SymbolUsages(SymbolUsagesQuery),
561	ViewRead(ViewReadQuery),
562	RulesList(RulesListQuery),
563	RulesCheck(RulesCheckQuery),
564	RulesApplicable(RulesApplicableQuery),
565	ChangeReview(ChangeReviewQuery),
566	ChangeContext(ChangeContextQuery),
567	SymbolGraph(SymbolGraphQuery),
568	IdentityChildren(IdentityChildrenQuery),
569	IdentityGraph(IdentityChildrenQuery),
570	ResolutionAudit(ResolutionAuditQuery),
571	Notes(NotesQuery),
572}
573
574impl Query {
575	pub fn capability(&self) -> &'static str {
576		match self {
577			Self::QueryDescribe(_) => "query.describe",
578			Self::WorkspaceStatus => "workspace.status",
579			Self::TreeChildren(_) => "tree.children",
580			Self::SymbolSearch(_) => "symbol.search",
581			Self::SymbolInsights(_) => "symbol.insights",
582			Self::SymbolDetail(_) => "symbol.detail",
583			Self::SymbolUsages(_) => "symbol.usages",
584			Self::ViewRead(_) => "view.read",
585			Self::RulesList(_) => "rules.list",
586			Self::RulesCheck(_) => "rules.check",
587			Self::RulesApplicable(_) => "rules.applicable",
588			Self::ChangeReview(_) => "change.review",
589			Self::ChangeContext(_) => "change.context",
590			Self::SymbolGraph(_) => "symbol.graph",
591			Self::IdentityChildren(_) => "identity.children",
592			Self::IdentityGraph(_) => "identity.graph",
593			Self::ResolutionAudit(_) => "resolution.audit",
594			Self::Notes(_) => "notes",
595		}
596	}
597}
598
599pub fn query_projection(query: &Query) -> &[String] {
600	match query {
601		Query::TreeChildren(query) => &query.projection,
602		Query::SymbolSearch(query) | Query::SymbolInsights(query) => &query.projection,
603		Query::SymbolUsages(query) => &query.projection,
604		_ => &[],
605	}
606}
607
608#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
609#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
610pub struct QueryDescribeQuery {
611	pub verb: Option<String>,
612}
613
614#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
615#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
616pub struct QueryCapabilityDto {
617	pub name: String,
618	pub category: String,
619	pub read_only: bool,
620	pub mcp_tool: String,
621	pub projection: bool,
622	pub projection_fields: Vec<String>,
623	pub paginated: bool,
624	pub positionals: usize,
625	pub fields: Vec<QueryFieldDto>,
626	pub example: String,
627}
628
629#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
631pub struct QueryFieldDto {
632	pub name: String,
633	pub value_type: String,
634	pub multiple: bool,
635	pub required: bool,
636	pub default: Option<String>,
637}
638
639#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
640#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
641pub struct QueryDescribeResult {
642	pub capabilities: Vec<QueryCapabilityDto>,
643}
644
645#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
647pub struct TreeChildrenQuery {
648	pub workspace: Option<String>,
649	pub path: Vec<String>,
650	pub depth: usize,
651	pub lang: Vec<String>,
652	pub projection: Vec<String>,
653}
654
655#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
656#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
657pub struct SymbolSearchQuery {
658	pub workspace: Option<String>,
659	pub text: Option<String>,
660	pub path: Vec<String>,
661	pub lang: Vec<String>,
662	pub kind: Vec<String>,
663	pub shape: Vec<String>,
664	pub name: Option<String>,
665	pub include_non_navigable: bool,
666	pub include_code: bool,
667	pub context_lines: usize,
668	pub projection: Vec<String>,
669}
670
671#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
672#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
673pub struct SymbolDetailQuery {
674	pub workspace: Option<String>,
675	pub uri: String,
676	pub context_lines: usize,
677}
678
679#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
680#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
681pub struct SymbolUsagesQuery {
682	pub workspace: Option<String>,
683	pub uri: String,
684	pub direction: UsageDirection,
685	pub path: Vec<String>,
686	pub lang: Vec<String>,
687	pub projection: Vec<String>,
688}
689
690#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
691#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
692pub struct ViewReadQuery {
693	pub uri: String,
694	pub scheme: Option<String>,
695	pub context_lines: usize,
696	pub include_code: bool,
697}
698
699#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
700#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
701pub struct ResolutionAuditQuery {
702	pub workspace: Option<String>,
703	pub prefix: String,
704	pub limit: usize,
705	pub cluster: Option<String>,
706}
707
708#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
709#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
710#[serde(rename_all = "snake_case")]
711pub enum UsageDirection {
712	#[default]
713	Incoming,
714	Outgoing,
715	Both,
716}
717
718impl UsageDirection {
719	pub fn as_str(self) -> &'static str {
720		match self {
721			Self::Incoming => "incoming",
722			Self::Outgoing => "outgoing",
723			Self::Both => "both",
724		}
725	}
726}
727
728impl FromStr for UsageDirection {
729	type Err = QueryParseError;
730
731	fn from_str(value: &str) -> Result<Self, Self::Err> {
732		match value {
733			"incoming" => Ok(Self::Incoming),
734			"outgoing" => Ok(Self::Outgoing),
735			"both" => Ok(Self::Both),
736			_ => Err(QueryParseError::InvalidValue {
737				key: "direction".to_string(),
738				value: value.to_string(),
739			}),
740		}
741	}
742}
743
744#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
745#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
746pub struct RulesListQuery {
747	pub workspace: Option<String>,
748	pub profile: Option<String>,
749	pub rules: Option<String>,
750	pub lang: Vec<String>,
751	pub severity: Vec<String>,
752}
753
754#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
755#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
756pub struct RulesCheckQuery {
757	pub workspace: Option<String>,
758	pub profile: Option<String>,
759	pub rules: Option<String>,
760	pub file: Vec<String>,
761	pub report: bool,
762}
763
764#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
765#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
766pub struct RulesApplicableQuery {
767	pub workspace: Option<String>,
768	pub focus: String,
769	pub profile: Option<String>,
770	pub rules: Option<String>,
771}
772
773#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
774#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
775pub struct ChangeReviewQuery {
776	pub workspace: Option<String>,
777}
778
779#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
780#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
781pub struct ChangeContextQuery {
782	pub workspace: Option<String>,
783	pub focus: String,
784	pub profile: Option<String>,
785	pub max_items: usize,
786}
787
788#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
789#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
790pub struct SymbolGraphQuery {
791	pub workspace: Option<String>,
792	pub focus: String,
793	pub direction: UsageDirection,
794	pub relation: Vec<String>,
795	pub min_count: usize,
796	pub include_internal: bool,
797}
798
799impl Default for SymbolGraphQuery {
800	fn default() -> Self {
801		Self {
802			workspace: None,
803			focus: String::new(),
804			direction: UsageDirection::Both,
805			relation: Vec::new(),
806			min_count: 1,
807			include_internal: true,
808		}
809	}
810}
811
812// One level of the identity tree: children of a moniker identity prefix
813// (`""` = the workspace root). The symbolic navigation surface - no
814// filesystem involved.
815#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
816#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
817pub struct IdentityChildrenQuery {
818	pub workspace: Option<String>,
819	pub prefix: String,
820}
821
822#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
823#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
824pub struct NotesQuery {
825	pub action: NotesAction,
826	pub id: Option<String>,
827	pub moniker: Option<String>,
828	pub kind: Option<String>,
829	pub status: Option<String>,
830	pub title: Option<String>,
831	pub body: Option<String>,
832	pub created_by: Option<String>,
833	pub orphan: Option<bool>,
834	pub include_done: bool,
835}
836
837#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
838#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
839#[serde(rename_all = "snake_case")]
840pub enum NotesAction {
841	#[default]
842	List,
843	Get,
844	Create,
845	Update,
846	Transition,
847	Delete,
848}
849
850#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
851#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
852pub struct CommandRequest {
853	pub command: Command,
854}
855
856#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
857#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
858#[serde(tag = "op", rename_all = "snake_case")]
859pub enum Command {
860	WorkspaceRefresh,
861}
862
863#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
864#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
865#[serde(rename_all = "snake_case")]
866pub enum Consistency {
867	#[default]
868	Current,
869	RefreshIfStale,
870	StaleOk,
871}
872
873impl FromStr for Consistency {
874	type Err = QueryParseError;
875
876	fn from_str(value: &str) -> Result<Self, Self::Err> {
877		match value {
878			"current" => Ok(Self::Current),
879			"refresh-if-stale" => Ok(Self::RefreshIfStale),
880			"stale-ok" => Ok(Self::StaleOk),
881			_ => Err(QueryParseError::InvalidValue {
882				key: "consistency".to_string(),
883				value: value.to_string(),
884			}),
885		}
886	}
887}
888
889#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
890#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
891pub struct Page {
892	pub cursor: Option<QueryCursor>,
893	pub limit: usize,
894}
895
896impl Default for Page {
897	fn default() -> Self {
898		Self {
899			cursor: None,
900			limit: 80,
901		}
902	}
903}
904
905#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
906#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
907pub struct QueryCursor {
908	pub offset: usize,
909	pub generation: Option<WorkspaceGeneration>,
910}
911
912impl QueryCursor {
913	pub fn new(offset: usize, generation: Option<WorkspaceGeneration>) -> Self {
914		Self { offset, generation }
915	}
916}
917
918#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
919#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
920pub struct WorkspaceGeneration(pub u64);
921
922/// A workspace change pushed to attached clients over a daemon subscription.
923#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
924#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
925pub struct WorkspaceEventDto {
926	pub kind: WorkspaceEventKind,
927	pub generation: Option<WorkspaceGeneration>,
928	pub stale_summary: Option<String>,
929}
930
931#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
932#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
933#[serde(rename_all = "snake_case")]
934pub enum WorkspaceEventKind {
935	Stale,
936	Refreshed,
937	Notes,
938	GitBase,
939}
940
941#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
942#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
943pub struct QueryResponse {
944	pub generation: Option<WorkspaceGeneration>,
945	pub result: QueryResult,
946	pub next_cursor: Option<QueryCursor>,
947}
948
949#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
950#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
951#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
952pub enum QueryResult {
953	QueryDescribe(QueryDescribeResult),
954	WorkspaceStatus(WorkspaceStatus),
955	TreeChildren(TreeChildrenResult),
956	SymbolList(SymbolListResult),
957	SymbolInsights(SymbolInsightsResult),
958	SymbolDetail(SymbolDetailResult),
959	SymbolUsages(Box<SymbolUsagesResult>),
960	ViewRead(ViewReadResult),
961	RulesList(RulesListResult),
962	RulesCheck(RulesCheckResult),
963	RulesApplicable(Box<RulesApplicableResult>),
964	ChangeReview(Box<ChangeReviewResult>),
965	ChangeContext(Box<ChangeContextResult>),
966	SymbolGraph(Box<SymbolGraphResult>),
967	IdentityChildren(IdentityChildrenResult),
968	IdentityGraph(Box<IdentityGraphResult>),
969	ResolutionAudit(Box<ResolutionAuditResult>),
970	Notes(NotesResult),
971}
972
973// Refs without a unique in-workspace target, decomposed so explained decisions
974// never masquerade as resolution gaps. Candidate and dynamic references remain
975// outside the graph while preserving their honest classification.
976#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
977#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
978pub struct UnlinkedRefsDto {
979	pub external: usize,
980	pub sdk: usize,
981	pub dependency: usize,
982	pub injected_external: usize,
983	pub unknown_external: usize,
984	pub candidate: usize,
985	pub dynamic: usize,
986	pub manifest_blocked: usize,
987	pub unresolved: usize,
988	pub unresolved_reasons: BTreeMap<String, usize>,
989}
990
991#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
992#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
993pub struct SymbolGraphResult {
994	pub focus: SymbolGraphFocus,
995	pub members: Vec<SymbolDto>,
996	pub internal_edges: Vec<SymbolGraphEdge>,
997	pub callers: Vec<SymbolGraphNeighbor>,
998	pub callees: Vec<SymbolGraphNeighbor>,
999	pub unlinked: UnlinkedRefsDto,
1000}
1001
1002#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1003#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1004#[serde(tag = "kind", rename_all = "snake_case")]
1005pub enum SymbolGraphFocus {
1006	Symbol { symbol: Box<SymbolDto> },
1007	File { path: String },
1008}
1009
1010#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1011#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1012pub struct SymbolGraphNeighbor {
1013	pub symbol: SymbolDto,
1014	pub kinds: Vec<String>,
1015	pub count: usize,
1016}
1017
1018#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1019#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1020pub struct SymbolGraphEdge {
1021	pub source: String,
1022	pub target: String,
1023	pub kinds: Vec<String>,
1024	pub count: usize,
1025}
1026
1027#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1028#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1029pub struct IdentityChildrenResult {
1030	pub prefix: String,
1031	pub children: Vec<IdentitySegmentDto>,
1032}
1033
1034// One child segment under the requested prefix. `symbol` is attached when the
1035// segment itself is a navigable definition; organizational segments (package,
1036// dir, srcset, lang, module wrappers) only aggregate what lives below.
1037#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1038#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1039pub struct IdentitySegmentDto {
1040	pub segment: String,
1041	pub kind: String,
1042	pub name: String,
1043	pub identity: String,
1044	pub defs: usize,
1045	pub has_children: bool,
1046	pub symbol: Option<Box<SymbolDto>>,
1047}
1048
1049// The embedded resolution audit partitions every reference by decision class
1050// and clusters candidates, dynamic references, and unresolved references under
1051// mechanical pattern keys. Stable cluster ids support paginated drill-downs.
1052#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1053#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1054pub struct ResolutionAuditResult {
1055	pub prefix: String,
1056	pub totals: AuditTotalsDto,
1057	pub clusters: Vec<AuditClusterDto>,
1058	pub zones: Vec<AuditZoneDto>,
1059}
1060
1061#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1062#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1063pub struct AuditTotalsDto {
1064	pub references: usize,
1065	pub resolved: usize,
1066	pub unique: usize,
1067	pub candidate: usize,
1068	pub external: usize,
1069	pub sdk: usize,
1070	pub dependency: usize,
1071	pub injected_external: usize,
1072	pub unknown_external: usize,
1073	pub dynamic: usize,
1074	pub blocked: usize,
1075	pub unresolved: usize,
1076	pub explained: usize,
1077	pub weak_or_unexplained: usize,
1078	pub name_match_resolved: usize,
1079	pub name_match_candidate: usize,
1080}
1081
1082#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1083#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1084pub struct AuditClusterDto {
1085	pub id: String,
1086	pub pattern: String,
1087	pub count: usize,
1088	pub samples: Vec<AuditSampleDto>,
1089}
1090
1091#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1092#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1093pub struct AuditSampleDto {
1094	pub file: String,
1095	pub line_range: Option<(u32, u32)>,
1096	pub snippet: String,
1097	pub source: String,
1098	pub call_name: String,
1099	pub receiver: String,
1100	pub target: String,
1101	pub evidence: String,
1102	pub constraints: Vec<String>,
1103	pub candidates: Vec<String>,
1104}
1105
1106#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1107#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1108pub struct AuditZoneDto {
1109	pub zone: String,
1110	pub unresolved: usize,
1111	pub dominant_pattern: String,
1112}
1113
1114// The scoped exploration graph: one level of the identity tree projected as
1115// a graph. Nodes are the prefix's children; edges are resolved references
1116// rolled up to the pair of child segments they connect; ports aggregate what
1117// crosses the scope boundary.
1118#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1120pub struct IdentityGraphResult {
1121	pub prefix: String,
1122	pub nodes: Vec<IdentitySegmentDto>,
1123	pub edges: Vec<IdentityGraphEdge>,
1124	pub ports_in: Vec<IdentityGraphPort>,
1125	pub ports_out: Vec<IdentityGraphPort>,
1126	pub unlinked: UnlinkedRefsDto,
1127}
1128
1129// source/target are child segment identities of the requested prefix.
1130#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1132pub struct IdentityGraphEdge {
1133	pub source: String,
1134	pub target: String,
1135	pub kinds: Vec<String>,
1136	pub count: usize,
1137}
1138
1139// Aggregated boundary crossing: `identity` is the nearest out-of-scope
1140// segment (rolled up to the scope's own depth in the identity tree).
1141#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1142#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1143pub struct IdentityGraphPort {
1144	pub identity: String,
1145	pub kinds: Vec<String>,
1146	pub count: usize,
1147}
1148
1149#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1150#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1151pub struct ChangeReviewResult {
1152	pub scope: String,
1153	pub summary: ChangeReviewSummary,
1154	pub files: Vec<ChangeReviewFile>,
1155	pub symbol_changes: Vec<ChangeReviewSymbol>,
1156	pub ref_changes: Vec<ChangeReviewRef>,
1157	pub diagnostics: Vec<String>,
1158}
1159
1160#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1162pub struct ChangeReviewSummary {
1163	pub files: usize,
1164	pub analyzable_files: usize,
1165	pub symbol_changes: usize,
1166	pub ref_changes: usize,
1167	pub retargeted_refs: usize,
1168	pub residual_files: usize,
1169}
1170
1171#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1172#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1173pub struct ChangeReviewFile {
1174	pub old_path: Option<String>,
1175	pub new_path: Option<String>,
1176	pub disposition: String,
1177	pub analyzable: bool,
1178	pub symbol_changes: usize,
1179	pub moved_symbols: usize,
1180	pub coverage_explained: bool,
1181	pub old_residual: Vec<(u32, u32)>,
1182	pub new_residual: Vec<(u32, u32)>,
1183}
1184
1185#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1187pub struct ChangeReviewSymbol {
1188	pub kind: String,
1189	pub confidence: String,
1190	pub body_changed: bool,
1191	pub signature_changed: bool,
1192	pub visibility_changed: bool,
1193	pub header_changed: bool,
1194	pub file_moved: bool,
1195	pub old: Option<ChangeReviewSide>,
1196	pub new: Option<ChangeReviewSide>,
1197}
1198
1199#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1201pub struct ChangeReviewSide {
1202	pub identity: String,
1203	pub file: String,
1204	pub kind: String,
1205	pub name: String,
1206	pub visibility: String,
1207	pub lines: Option<(u32, u32)>,
1208}
1209
1210#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1211#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1212pub struct ChangeReviewRef {
1213	pub kind: String,
1214	pub file: String,
1215	pub ref_kind: String,
1216	pub old_target: Option<String>,
1217	pub new_target: Option<String>,
1218	pub old_lines: Option<(u32, u32)>,
1219	pub new_lines: Option<(u32, u32)>,
1220}
1221
1222#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1224pub struct CommandResponse {
1225	pub generation: Option<WorkspaceGeneration>,
1226	pub message: String,
1227	pub status: Option<WorkspaceStatus>,
1228}
1229
1230#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1232#[serde(tag = "kind", rename_all = "snake_case")]
1233pub enum ViewReadResult {
1234	List(ViewListResult),
1235	Detail(Box<ViewDetailResult>),
1236}
1237
1238#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1240pub struct ViewListResult {
1241	pub views: Vec<ViewSummaryDto>,
1242}
1243
1244#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1246pub struct ViewSummaryDto {
1247	pub id: String,
1248	pub title: Option<String>,
1249	pub fragment: String,
1250	pub anchor: String,
1251	pub scope: String,
1252}
1253
1254#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1255#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1256pub struct ViewDetailResult {
1257	pub id: String,
1258	pub title: Option<String>,
1259	pub fragment: String,
1260	pub anchor: String,
1261	pub scope: String,
1262	pub intent: Option<String>,
1263	pub summary: Option<String>,
1264	pub rules: Vec<ViewRuleDto>,
1265	pub boundaries: Vec<ViewBoundaryDto>,
1266	pub gotchas: Vec<ViewGotchaDto>,
1267}
1268
1269#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1270#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1271pub struct ViewRuleDto {
1272	pub id: String,
1273	pub severity: String,
1274	pub domain: String,
1275	pub rationale: Option<String>,
1276}
1277
1278#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1279#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1280pub struct ViewRuleRefDto {
1281	pub id: String,
1282	pub present: bool,
1283}
1284
1285#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1287pub struct ViewBoundaryDto {
1288	pub id: String,
1289	pub owns: Vec<String>,
1290	pub forbids: Vec<String>,
1291	pub forbid_rules: Vec<String>,
1292	pub rationale: Option<String>,
1293	pub rule_refs: Vec<ViewRuleRefDto>,
1294	pub evidence: Vec<ViewEvidenceDto>,
1295	pub missing: Vec<String>,
1296}
1297
1298#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1299#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1300pub struct ViewGotchaDto {
1301	pub id: String,
1302	pub rationale: String,
1303	pub check: Option<String>,
1304	pub rule_refs: Vec<ViewRuleRefDto>,
1305	pub evidence: Vec<ViewEvidenceDto>,
1306	pub missing: Vec<String>,
1307}
1308
1309#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1310#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1311pub struct ViewEvidenceDto {
1312	pub selector: String,
1313	pub label: String,
1314	pub moniker: String,
1315	pub file: String,
1316	pub slice: Option<(u32, u32)>,
1317	pub active_slice: Option<(u32, u32)>,
1318	pub code: Vec<SourceLine>,
1319}
1320
1321#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1322#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1323pub struct WorkspaceStatus {
1324	pub root: String,
1325	pub phase: String,
1326	pub roots: Vec<WorkspaceRootStatus>,
1327	pub generation: Option<WorkspaceGeneration>,
1328	pub files: usize,
1329	pub symbols: usize,
1330	pub references: usize,
1331	pub stale: bool,
1332	pub stale_summary: String,
1333}
1334
1335#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1336#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1337pub struct WorkspaceRootStatus {
1338	pub root: String,
1339	pub generation: Option<WorkspaceGeneration>,
1340	pub files: usize,
1341	pub symbols: usize,
1342	pub references: usize,
1343	pub stale: bool,
1344	pub stale_summary: String,
1345}
1346
1347#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1348#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1349pub struct TreeChildrenResult {
1350	pub root: String,
1351	pub roots: Vec<String>,
1352	pub rows: Vec<TreeNode>,
1353	pub total: usize,
1354	pub total_files: usize,
1355	pub scoped_files: usize,
1356	pub languages: Vec<CountDto>,
1357	pub prefixes: Vec<CountDto>,
1358}
1359
1360#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1361#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1362pub struct TreeNode {
1363	pub root: String,
1364	pub path: String,
1365	pub kind: TreeNodeKind,
1366	pub language: Option<String>,
1367	pub defs: usize,
1368	pub refs: usize,
1369	pub change_count: usize,
1370}
1371
1372#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1373#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1374#[serde(rename_all = "snake_case")]
1375pub enum TreeNodeKind {
1376	File,
1377	Directory,
1378}
1379
1380#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1381#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1382pub struct SymbolListResult {
1383	pub rows: Vec<SymbolDto>,
1384	pub total: usize,
1385}
1386
1387#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1388#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1389pub struct SymbolDto {
1390	pub root: String,
1391	pub uri: String,
1392	pub id: String,
1393	pub name: String,
1394	pub kind: String,
1395	pub visibility: String,
1396	pub signature: String,
1397	pub file: String,
1398	pub language: String,
1399	pub line_range: Option<(u32, u32)>,
1400	pub navigable: bool,
1401	pub score: Option<u32>,
1402	pub match_reason: Option<String>,
1403	pub source: Option<SourceSnippet>,
1404}
1405
1406#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1407#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1408pub struct SymbolInsightsResult {
1409	pub files: usize,
1410	pub symbols: usize,
1411	pub references: usize,
1412	pub navigable_symbols: usize,
1413	pub non_navigable_symbols: usize,
1414	pub languages: Vec<CountDto>,
1415	pub kinds: Vec<CountDto>,
1416	pub shapes: Vec<CountDto>,
1417	pub top_files_by_symbols: Vec<CountDto>,
1418	pub top_files_by_refs: Vec<CountDto>,
1419}
1420
1421#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1422#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1423pub struct SymbolDetailResult {
1424	pub symbol: SymbolDto,
1425	pub source: Option<SourceSnippet>,
1426}
1427
1428#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1429#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1430pub struct SourceSnippet {
1431	pub file: String,
1432	pub first_line: u32,
1433	pub last_line: u32,
1434	pub lines: Vec<SourceLine>,
1435}
1436
1437#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1438#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1439pub struct SourceLine {
1440	pub number: u32,
1441	pub text: String,
1442}
1443
1444#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1445#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1446pub struct SymbolUsagesResult {
1447	pub target: SymbolDto,
1448	pub direction: UsageDirection,
1449	pub rows: Vec<UsageDto>,
1450	pub total: usize,
1451	pub incoming_summary: Option<UsageSummaryDto>,
1452	pub outgoing_summary: Option<UsageSummaryDto>,
1453}
1454
1455#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1457pub struct UsageSummaryDto {
1458	pub refs: usize,
1459	pub files: usize,
1460	pub contexts: usize,
1461	pub prefixes: usize,
1462	pub dominant_prefix: String,
1463	pub kinds: Vec<CountDto>,
1464	pub top_actors: Vec<CountDto>,
1465	pub top_prefixes: Vec<CountDto>,
1466	pub shared_helper_signal: String,
1467}
1468
1469#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1470#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1471pub struct UsageDto {
1472	pub root: String,
1473	pub direction: UsageDirection,
1474	pub reference: String,
1475	pub kind: String,
1476	pub actor: String,
1477	pub context: String,
1478	pub endpoint: String,
1479	pub file: String,
1480	pub prefix: String,
1481	pub location: String,
1482	pub line_range: Option<(u32, u32)>,
1483	pub via: Option<String>,
1484}
1485
1486#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1487#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1488pub struct RulesListResult {
1489	pub roots: Vec<String>,
1490	pub rows: Vec<RuleDto>,
1491	pub total: usize,
1492}
1493
1494#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1495#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1496pub struct RuleDto {
1497	pub root: String,
1498	pub id: String,
1499	pub severity: String,
1500	pub lang: String,
1501	pub domain: String,
1502	pub kind: Option<String>,
1503	pub expr: String,
1504	pub expanded_expr: String,
1505	pub message: Option<String>,
1506	pub rationale: Option<String>,
1507	pub require_doc_comment: Option<String>,
1508}
1509
1510#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1511#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1512pub struct RulesApplicableResult {
1513	pub focus: SymbolGraphFocus,
1514	pub file: String,
1515	pub language: String,
1516	pub symbol_kind: Option<String>,
1517	pub total: usize,
1518	pub rows: Vec<RuleApplicabilityDto>,
1519}
1520
1521#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1522#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1523pub struct RuleApplicabilityDto {
1524	pub rule: RuleDto,
1525	pub status: String,
1526	pub reason: String,
1527}
1528
1529#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1530#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1531pub struct ChangeContextResult {
1532	pub focus: SymbolGraphFocus,
1533	pub source: Option<SourceSnippet>,
1534	pub graph: Box<SymbolGraphResult>,
1535	pub notes: Vec<NoteDto>,
1536	pub rules: Vec<RuleApplicabilityDto>,
1537	pub changed_files: Vec<ChangeReviewFile>,
1538	pub changed_symbols: Vec<ChangeReviewSymbol>,
1539	pub suggested_checks: Vec<String>,
1540	pub coverage: ChangeContextCoverageDto,
1541}
1542
1543#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1544#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1545pub struct ChangeContextCoverageDto {
1546	pub members_total: usize,
1547	pub members_emitted: usize,
1548	pub internal_edges_total: usize,
1549	pub internal_edges_emitted: usize,
1550	pub callers_total: usize,
1551	pub callers_emitted: usize,
1552	pub callees_total: usize,
1553	pub callees_emitted: usize,
1554	pub notes_total: usize,
1555	pub notes_emitted: usize,
1556	pub rules_total: usize,
1557	pub rules_emitted: usize,
1558	pub changes_total: usize,
1559	pub changes_emitted: usize,
1560}
1561
1562#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1563#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1564pub struct RulesCheckResult {
1565	pub exit: String,
1566	pub summary: CheckSummaryDto,
1567	pub roots: Vec<RulesCheckRootResult>,
1568	pub violations: Vec<ViolationDto>,
1569	pub errors: Vec<FileErrorDto>,
1570	pub rule_reports: Vec<RuleReportDto>,
1571	pub skip_reasons: Vec<CheckSkipReasonDto>,
1572}
1573
1574#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1576pub struct RulesCheckRootResult {
1577	pub root: String,
1578	pub exit: String,
1579	pub summary: CheckSummaryDto,
1580	pub violations: Vec<ViolationDto>,
1581	pub errors: Vec<FileErrorDto>,
1582	pub rule_reports: Vec<RuleReportDto>,
1583	pub skip_reason: Option<CheckSkipReasonDto>,
1584}
1585
1586#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1587#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1588pub struct CheckSummaryDto {
1589	pub files_scanned: usize,
1590	pub files_with_violations: usize,
1591	pub total_violations: usize,
1592	pub total_rule_errors: usize,
1593	pub total_warnings: usize,
1594	pub files_with_errors: usize,
1595	pub total_errors: usize,
1596	pub elapsed_ms: u64,
1597	pub failed_rules: Vec<FailedRuleDto>,
1598}
1599
1600#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1601#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1602pub struct FailedRuleDto {
1603	pub rule_id: String,
1604	pub severity: String,
1605	pub violations: usize,
1606}
1607
1608#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1609#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1610pub struct ViolationDto {
1611	pub root: String,
1612	pub path: String,
1613	pub rule_id: String,
1614	pub severity: String,
1615	pub moniker: String,
1616	pub kind: String,
1617	pub lines: (u32, u32),
1618	pub message: String,
1619}
1620
1621#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1622#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1623pub struct FileErrorDto {
1624	pub root: String,
1625	pub path: String,
1626	pub error: String,
1627}
1628
1629#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1631pub struct RuleReportDto {
1632	pub root: String,
1633	pub path: Option<String>,
1634	pub rule_id: String,
1635	pub severity: String,
1636	pub domain: String,
1637	pub evaluated: usize,
1638	pub matches: usize,
1639	pub violations: usize,
1640	pub antecedent_matches: Option<usize>,
1641	pub warning: Option<String>,
1642}
1643
1644#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1645#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1646pub struct CheckSkipReasonDto {
1647	pub root: String,
1648	pub reason: String,
1649}
1650
1651#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1652#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1653pub struct NotesResult {
1654	pub action: String,
1655	pub total: usize,
1656	pub rows: Vec<NoteDto>,
1657	pub deleted: Option<NoteDto>,
1658}
1659
1660#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1661#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1662pub struct NoteDto {
1663	pub id: String,
1664	pub moniker: String,
1665	pub kind: String,
1666	pub status: String,
1667	pub title: String,
1668	pub body: String,
1669	pub created_by: String,
1670	pub updated_at: String,
1671	pub resolution: NoteResolutionDto,
1672}
1673
1674#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1676#[serde(tag = "status", rename_all = "snake_case")]
1677pub enum NoteResolutionDto {
1678	Resolved {
1679		target: String,
1680		file: String,
1681		slice: Option<(u32, u32)>,
1682	},
1683	Orphan,
1684}
1685
1686#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1687#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1688pub struct CountDto {
1689	pub name: String,
1690	pub count: usize,
1691}
1692
1693#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1694#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1695#[serde(tag = "event", rename_all = "snake_case")]
1696pub enum DaemonEvent {
1697	WorkspaceStale {
1698		generation: Option<WorkspaceGeneration>,
1699		summary: String,
1700	},
1701	WorkspaceRefreshed {
1702		generation: WorkspaceGeneration,
1703	},
1704}
1705
1706#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1707#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1708pub struct QueryError {
1709	pub code: String,
1710	pub message: String,
1711}
1712
1713impl QueryError {
1714	pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
1715		Self {
1716			code: code.into(),
1717			message: message.into(),
1718		}
1719	}
1720}
1721
1722impl fmt::Display for QueryError {
1723	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1724		write!(f, "{}: {}", self.code, self.message)
1725	}
1726}
1727
1728impl std::error::Error for QueryError {}
1729
1730#[derive(Debug, thiserror::Error)]
1731pub enum QueryParseError {
1732	#[error("empty query")]
1733	Empty,
1734	#[error("unknown query operation `{0}`")]
1735	UnknownOperation(String),
1736	#[error("invalid token `{0}`")]
1737	InvalidToken(String),
1738	#[error("invalid value for `{key}`: `{value}`")]
1739	InvalidValue { key: String, value: String },
1740	#[error("missing required `{0}`")]
1741	MissingRequired(&'static str),
1742	#[error("unknown field `{key}` for `{op}`{hint}")]
1743	UnknownField {
1744		op: String,
1745		key: String,
1746		hint: String,
1747	},
1748	#[error("unexpected argument `{value}` for `{op}`")]
1749	UnexpectedArgument { op: String, value: String },
1750	#[error("`project` is not supported by `{op}`")]
1751	UnsupportedProjection { op: String },
1752	#[error("unknown projection field `{field}` for `{op}`{hint}")]
1753	UnknownProjectionField {
1754		op: String,
1755		field: String,
1756		hint: String,
1757	},
1758}
1759
1760pub fn parse_query(input: &str) -> Result<QueryRequest, QueryParseError> {
1761	let mut lines = input.lines().map(str::trim).filter(|line| !line.is_empty());
1762	let first = lines.next().ok_or(QueryParseError::Empty)?;
1763	let mut tokens = tokenize(first)?;
1764	let op = tokens
1765		.first()
1766		.map(|token| token.text.clone())
1767		.ok_or(QueryParseError::Empty)?;
1768	tokens.remove(0);
1769	let mut fields = FieldBag::default();
1770	let mut positional = Vec::new();
1771	collect_tokens(&tokens, &mut fields, &mut positional)?;
1772	for line in lines {
1773		collect_line(line, &mut fields, &mut positional)?;
1774	}
1775	fields.positional = positional;
1776	let spec = verb_spec(&op).ok_or_else(|| QueryParseError::UnknownOperation(op.clone()))?;
1777	validate_fields(&op, spec, &fields)?;
1778	if let Some(value) = fields.one("consistency") {
1779		fields.consistency = value.parse()?;
1780	}
1781	let page = fields.page()?;
1782	let consistency = fields.consistency;
1783	let query = build_query(&op, fields)?;
1784	Ok(QueryRequest {
1785		query,
1786		consistency,
1787		page,
1788	})
1789}
1790
1791fn collect_line(
1792	line: &str,
1793	fields: &mut FieldBag,
1794	positional: &mut Vec<String>,
1795) -> Result<(), QueryParseError> {
1796	let mut tokens = tokenize(line)?;
1797	if tokens.is_empty() {
1798		return Ok(());
1799	}
1800	let section = tokens.remove(0);
1801	if section.text.contains(':') {
1802		let mut all = vec![section];
1803		all.extend(tokens);
1804		return collect_tokens(&all, fields, positional);
1805	}
1806	match section.text.as_str() {
1807		"filter" | "page" => collect_tokens(&tokens, fields, positional)?,
1808		"project" => {
1809			fields.projection.extend(
1810				tokens
1811					.into_iter()
1812					.map(|token| token.text.trim_end_matches(',').to_string())
1813					.filter(|token| !token.is_empty()),
1814			);
1815		}
1816		"consistency" => {
1817			let value = tokens
1818				.first()
1819				.ok_or(QueryParseError::MissingRequired("consistency value"))?;
1820			fields.consistency = value.text.parse()?;
1821		}
1822		"direction" => {
1823			let value = tokens
1824				.first()
1825				.ok_or(QueryParseError::MissingRequired("direction value"))?;
1826			fields.values.push((
1827				"direction".to_string(),
1828				FieldValue {
1829					text: value.text.clone(),
1830					quoted: value.quoted,
1831				},
1832			));
1833		}
1834		_ => return Err(QueryParseError::InvalidToken(section.text)),
1835	}
1836	Ok(())
1837}
1838
1839fn build_query(op: &str, fields: FieldBag) -> Result<Query, QueryParseError> {
1840	let query = match op {
1841		"query.describe" => Query::QueryDescribe(QueryDescribeQuery {
1842			verb: fields
1843				.one("verb")
1844				.or_else(|| fields.positional.first().cloned()),
1845		}),
1846		"workspace.status" => Query::WorkspaceStatus,
1847		"tree.children" => Query::TreeChildren(TreeChildrenQuery {
1848			workspace: fields.one("workspace"),
1849			path: fields.many("path"),
1850			depth: fields.usize("depth")?.unwrap_or(1),
1851			lang: fields.many("lang"),
1852			projection: fields.projection,
1853		}),
1854		"symbol.search" => Query::SymbolSearch(symbol_search_query(&fields)?),
1855		"symbol.insights" => Query::SymbolInsights(symbol_insights_query(&fields)?),
1856		"symbol.detail" => Query::SymbolDetail(SymbolDetailQuery {
1857			workspace: fields.one("workspace"),
1858			uri: fields
1859				.one("uri")
1860				.or_else(|| fields.positional.first().cloned())
1861				.ok_or(QueryParseError::MissingRequired("uri"))?,
1862			context_lines: fields.usize("context_lines")?.unwrap_or(2),
1863		}),
1864		"symbol.usages" => Query::SymbolUsages(SymbolUsagesQuery {
1865			workspace: fields.one("workspace"),
1866			uri: fields
1867				.one("uri")
1868				.or_else(|| fields.positional.first().cloned())
1869				.ok_or(QueryParseError::MissingRequired("uri"))?,
1870			direction: fields
1871				.one("direction")
1872				.unwrap_or_else(|| "incoming".to_string())
1873				.parse()?,
1874			path: fields.many("path"),
1875			lang: fields.many("lang"),
1876			projection: fields.projection,
1877		}),
1878		"view.read" => Query::ViewRead(ViewReadQuery {
1879			uri: fields
1880				.one("uri")
1881				.or_else(|| fields.positional.first().cloned())
1882				.ok_or(QueryParseError::MissingRequired("uri"))?,
1883			scheme: fields.one("scheme"),
1884			context_lines: fields.usize("context_lines")?.unwrap_or(2),
1885			include_code: fields.bool("include_code")?.unwrap_or(false),
1886		}),
1887		"rules.list" => Query::RulesList(RulesListQuery {
1888			workspace: fields.one("workspace"),
1889			profile: fields.one("profile"),
1890			rules: fields.one("rules"),
1891			lang: fields.many("lang"),
1892			severity: fields.many("severity"),
1893		}),
1894		"rules.check" => Query::RulesCheck(RulesCheckQuery {
1895			workspace: fields.one("workspace"),
1896			profile: fields.one("profile"),
1897			rules: fields.one("rules"),
1898			file: fields.many("file"),
1899			report: fields.bool("report")?.unwrap_or(true),
1900		}),
1901		"rules.applicable" => Query::RulesApplicable(RulesApplicableQuery {
1902			workspace: fields.one("workspace"),
1903			focus: fields
1904				.one("focus")
1905				.or_else(|| fields.positional.first().cloned())
1906				.ok_or(QueryParseError::MissingRequired("focus"))?,
1907			profile: fields.one("profile"),
1908			rules: fields.one("rules"),
1909		}),
1910		"change.review" => Query::ChangeReview(ChangeReviewQuery {
1911			workspace: fields.one("workspace"),
1912		}),
1913		"change.context" => Query::ChangeContext(ChangeContextQuery {
1914			workspace: fields.one("workspace"),
1915			focus: fields
1916				.one("focus")
1917				.or_else(|| fields.positional.first().cloned())
1918				.ok_or(QueryParseError::MissingRequired("focus"))?,
1919			profile: fields.one("profile"),
1920			max_items: fields.usize("max_items")?.unwrap_or(20),
1921		}),
1922		"symbol.graph" => Query::SymbolGraph(symbol_graph_query(&fields)?),
1923		"identity.children" => Query::IdentityChildren(identity_children_query(&fields)),
1924		"identity.graph" => Query::IdentityGraph(identity_children_query(&fields)),
1925		"resolution.audit" => Query::ResolutionAudit(ResolutionAuditQuery {
1926			workspace: fields.one("workspace"),
1927			prefix: fields
1928				.one("prefix")
1929				.or_else(|| fields.positional.first().cloned())
1930				.unwrap_or_default(),
1931			limit: fields.usize("limit")?.unwrap_or(20),
1932			cluster: fields.one("cluster"),
1933		}),
1934		"notes" => Query::Notes(notes_query(&fields)?),
1935		_ => return Err(QueryParseError::UnknownOperation(op.to_string())),
1936	};
1937	Ok(query)
1938}
1939
1940fn verb_spec(op: &str) -> Option<&'static QueryCapabilitySpec> {
1941	query_capability_spec(op)
1942}
1943
1944fn validate_fields(
1945	op: &str,
1946	spec: &QueryCapabilitySpec,
1947	fields: &FieldBag,
1948) -> Result<(), QueryParseError> {
1949	for (key, value) in &fields.values {
1950		let key = key.as_str();
1951		if !COMMON_FIELDS.contains(&key) && !spec.fields.contains(&key) {
1952			return Err(QueryParseError::UnknownField {
1953				op: op.to_string(),
1954				key: key.to_string(),
1955				hint: field_hint(key, spec.fields),
1956			});
1957		}
1958		if !value.quoted
1959			&& BRACKET_LIST_FIELDS.contains(&key)
1960			&& value.text.starts_with('[') != value.text.ends_with(']')
1961		{
1962			return Err(QueryParseError::InvalidValue {
1963				key: key.to_string(),
1964				value: value.text.clone(),
1965			});
1966		}
1967	}
1968	if let Some(extra) = fields.positional.get(spec.positionals) {
1969		return Err(QueryParseError::UnexpectedArgument {
1970			op: op.to_string(),
1971			value: extra.clone(),
1972		});
1973	}
1974	if !spec.projection && !fields.projection.is_empty() {
1975		return Err(QueryParseError::UnsupportedProjection { op: op.to_string() });
1976	}
1977	let projection_fields = query_projection_fields(op);
1978	for field in &fields.projection {
1979		if !projection_fields.contains(&field.as_str()) {
1980			return Err(QueryParseError::UnknownProjectionField {
1981				op: op.to_string(),
1982				field: field.clone(),
1983				hint: projection_field_hint(field, projection_fields),
1984			});
1985		}
1986	}
1987	Ok(())
1988}
1989
1990fn projection_field_hint(field: &str, allowed: &'static [&'static str]) -> String {
1991	if let Some(suggestion) = allowed
1992		.iter()
1993		.copied()
1994		.map(|candidate| (candidate, levenshtein(field, candidate)))
1995		.filter(|(_, distance)| *distance <= 2)
1996		.min_by_key(|(_, distance)| *distance)
1997		.map(|(candidate, _)| candidate)
1998	{
1999		return format!(", did you mean `{suggestion}`?");
2000	}
2001	format!(" (valid projection fields: {})", allowed.join(", "))
2002}
2003
2004fn field_hint(key: &str, allowed: &'static [&'static str]) -> String {
2005	if let Some(suggestion) = suggest_field(key, allowed) {
2006		return format!(", did you mean `{suggestion}`?");
2007	}
2008	let mut valid: Vec<&str> = allowed.iter().chain(COMMON_FIELDS).copied().collect();
2009	valid.sort_unstable();
2010	format!(" (valid fields: {})", valid.join(", "))
2011}
2012
2013fn suggest_field(key: &str, allowed: &'static [&'static str]) -> Option<&'static str> {
2014	const ALIASES: &[(&str, &str)] = &[("text", "name"), ("query", "name"), ("filename", "path")];
2015	for (alias, target) in ALIASES {
2016		if *alias == key && allowed.contains(target) {
2017			return Some(target);
2018		}
2019	}
2020	allowed
2021		.iter()
2022		.chain(COMMON_FIELDS)
2023		.copied()
2024		.map(|candidate| (candidate, levenshtein(key, candidate)))
2025		.filter(|(_, distance)| *distance <= 2)
2026		.min_by_key(|(_, distance)| *distance)
2027		.map(|(candidate, _)| candidate)
2028}
2029
2030fn levenshtein(a: &str, b: &str) -> usize {
2031	let b: Vec<char> = b.chars().collect();
2032	let mut row: Vec<usize> = (0..=b.len()).collect();
2033	for (i, ca) in a.chars().enumerate() {
2034		let mut previous = row[0];
2035		row[0] = i + 1;
2036		for (j, cb) in b.iter().enumerate() {
2037			let substitution = previous + usize::from(ca != *cb);
2038			previous = row[j + 1];
2039			row[j + 1] = substitution.min(previous + 1).min(row[j] + 1);
2040		}
2041	}
2042	row[b.len()]
2043}
2044
2045fn symbol_search_query(fields: &FieldBag) -> Result<SymbolSearchQuery, QueryParseError> {
2046	Ok(SymbolSearchQuery {
2047		workspace: fields.one("workspace"),
2048		text: fields.positional.first().cloned(),
2049		path: fields.many("path"),
2050		lang: fields.many("lang"),
2051		kind: fields.many("kind"),
2052		shape: fields.many("shape"),
2053		name: fields.one("name"),
2054		include_non_navigable: fields.bool("include_non_navigable")?.unwrap_or(false),
2055		include_code: fields.bool("include_code")?.unwrap_or(false),
2056		context_lines: fields.usize("context_lines")?.unwrap_or(0),
2057		projection: fields.projection.clone(),
2058	})
2059}
2060
2061fn symbol_insights_query(fields: &FieldBag) -> Result<SymbolSearchQuery, QueryParseError> {
2062	let mut query = symbol_search_query(fields)?;
2063	query.text = None;
2064	query.include_code = false;
2065	query.context_lines = 0;
2066	Ok(query)
2067}
2068
2069fn notes_query(fields: &FieldBag) -> Result<NotesQuery, QueryParseError> {
2070	Ok(NotesQuery {
2071		action: parse_notes_action(fields.one("action").as_deref().unwrap_or("list"))?,
2072		id: fields.one("id"),
2073		moniker: fields.one("moniker"),
2074		kind: fields.one("kind"),
2075		status: fields.one("status"),
2076		title: fields.one("title"),
2077		body: fields.one("body"),
2078		created_by: fields.one("created_by"),
2079		orphan: fields.bool("orphan")?,
2080		include_done: fields.bool("include_done")?.unwrap_or(false),
2081	})
2082}
2083
2084fn identity_children_query(fields: &FieldBag) -> IdentityChildrenQuery {
2085	IdentityChildrenQuery {
2086		workspace: fields.one("workspace"),
2087		prefix: fields
2088			.one("prefix")
2089			.or_else(|| fields.positional.first().cloned())
2090			.unwrap_or_default(),
2091	}
2092}
2093
2094fn symbol_graph_query(fields: &FieldBag) -> Result<SymbolGraphQuery, QueryParseError> {
2095	Ok(SymbolGraphQuery {
2096		workspace: fields.one("workspace"),
2097		focus: fields
2098			.one("focus")
2099			.or_else(|| fields.positional.first().cloned())
2100			.ok_or(QueryParseError::MissingRequired("focus"))?,
2101		direction: fields
2102			.one("direction")
2103			.unwrap_or_else(|| "both".to_string())
2104			.parse()?,
2105		relation: fields.many("relation"),
2106		min_count: fields.usize("min_count")?.unwrap_or(1).max(1),
2107		include_internal: fields.bool("include_internal")?.unwrap_or(true),
2108	})
2109}
2110
2111fn parse_notes_action(value: &str) -> Result<NotesAction, QueryParseError> {
2112	match value {
2113		"list" => Ok(NotesAction::List),
2114		"get" => Ok(NotesAction::Get),
2115		"create" => Ok(NotesAction::Create),
2116		"update" => Ok(NotesAction::Update),
2117		"transition" => Ok(NotesAction::Transition),
2118		"delete" => Ok(NotesAction::Delete),
2119		_ => Err(QueryParseError::InvalidValue {
2120			key: "action".to_string(),
2121			value: value.to_string(),
2122		}),
2123	}
2124}
2125
2126pub fn format_query_response(response: &QueryResponse) -> String {
2127	format_query_response_projected(response, &[])
2128}
2129
2130pub fn format_query_response_projected(response: &QueryResponse, projection: &[String]) -> String {
2131	let mut out = String::new();
2132	if let Some(generation) = response.generation {
2133		let _ = writeln!(out, "generation: {}", generation.0);
2134	}
2135	if let Some(cursor) = &response.next_cursor {
2136		if let Some(generation) = cursor.generation {
2137			let _ = writeln!(out, "next_cursor: {}:{}", generation.0, cursor.offset);
2138		} else {
2139			let _ = writeln!(out, "next_cursor: {}", cursor.offset);
2140		}
2141	}
2142	match &response.result {
2143		QueryResult::QueryDescribe(result) => format_query_describe(&mut out, result),
2144		QueryResult::WorkspaceStatus(status) => format_workspace_status(&mut out, status),
2145		QueryResult::TreeChildren(result) => format_tree_children(&mut out, result, projection),
2146		QueryResult::SymbolList(result) => format_symbol_list(&mut out, result, projection),
2147		QueryResult::SymbolInsights(result) if !projection.is_empty() => {
2148			format_projected_value(&mut out, result, projection);
2149		}
2150		QueryResult::SymbolInsights(result) => format_symbol_insights(&mut out, result),
2151		QueryResult::SymbolDetail(result) => format_symbol_detail(&mut out, result),
2152		QueryResult::SymbolUsages(result) => format_symbol_usages(&mut out, result, projection),
2153		QueryResult::ViewRead(result) => format_view_read(&mut out, result),
2154		QueryResult::RulesList(result) => {
2155			let _ = writeln!(out, "rules: {}", result.total);
2156			format_rules_list_rows(&mut out, result);
2157		}
2158		QueryResult::RulesCheck(result) => format_rules_check(&mut out, result),
2159		QueryResult::RulesApplicable(result) => format_rules_applicable(&mut out, result),
2160		QueryResult::ChangeReview(result) => format_change_review(&mut out, result),
2161		QueryResult::ChangeContext(result) => format_change_context(&mut out, result),
2162		QueryResult::SymbolGraph(result) => format_symbol_graph(&mut out, result),
2163		QueryResult::IdentityChildren(result) => format_identity_children(&mut out, result),
2164		QueryResult::IdentityGraph(result) => format_identity_graph(&mut out, result),
2165		QueryResult::ResolutionAudit(result) => format_resolution_audit(&mut out, result),
2166		QueryResult::Notes(result) => format_notes(&mut out, result),
2167	}
2168	out
2169}
2170
2171fn format_workspace_status(out: &mut String, status: &WorkspaceStatus) {
2172	let _ = writeln!(out, "workspace: {}", status.root);
2173	let _ = writeln!(out, "phase: {}", status.phase);
2174	let _ = writeln!(
2175		out,
2176		"files: {} symbols: {} references: {}",
2177		status.files, status.symbols, status.references
2178	);
2179	let _ = writeln!(out, "stale: {} ({})", status.stale, status.stale_summary);
2180	if status.roots.len() > 1 {
2181		let _ = writeln!(out, "roots:");
2182		for root in &status.roots {
2183			let _ = writeln!(
2184				out,
2185				"- {} files:{} symbols:{} references:{} stale:{}",
2186				root.root, root.files, root.symbols, root.references, root.stale
2187			);
2188		}
2189	}
2190}
2191
2192fn format_tree_children(out: &mut String, result: &TreeChildrenResult, projection: &[String]) {
2193	let _ = writeln!(out, "tree: {}", result.root);
2194	for row in &result.rows {
2195		if !projection.is_empty() {
2196			format_projected_value(out, row, projection);
2197			continue;
2198		}
2199		let kind = match row.kind {
2200			TreeNodeKind::File => "file",
2201			TreeNodeKind::Directory => "dir",
2202		};
2203		let _ = writeln!(
2204			out,
2205			"- {kind} {} defs:{} refs:{}",
2206			row.path, row.defs, row.refs
2207		);
2208	}
2209}
2210
2211fn format_symbol_list(out: &mut String, result: &SymbolListResult, projection: &[String]) {
2212	let _ = writeln!(out, "symbols: {}", result.total);
2213	for row in &result.rows {
2214		if projection.is_empty() {
2215			let _ = writeln!(out, "- {} {} {} {}", row.kind, row.name, row.file, row.uri);
2216		} else {
2217			format_projected_value(out, row, projection);
2218		}
2219	}
2220}
2221
2222fn format_symbol_detail(out: &mut String, result: &SymbolDetailResult) {
2223	let symbol = &result.symbol;
2224	let _ = writeln!(out, "symbol: {} {}", symbol.kind, symbol.name);
2225	let _ = writeln!(out, "uri: {}", symbol.uri);
2226	let _ = writeln!(out, "file: {}", symbol.file);
2227	if let Some(source) = &result.source {
2228		for line in &source.lines {
2229			let _ = writeln!(out, "{:>6} | {}", line.number, line.text);
2230		}
2231	}
2232}
2233
2234fn format_symbol_usages(out: &mut String, result: &SymbolUsagesResult, projection: &[String]) {
2235	let _ = writeln!(out, "uri: {}", result.target.uri);
2236	let _ = writeln!(out, "direction: {}", result.direction.as_str());
2237	let _ = writeln!(out, "usages: {}", result.total);
2238	for row in &result.rows {
2239		if projection.is_empty() {
2240			let _ = writeln!(
2241				out,
2242				"- {} {} {} {}",
2243				row.direction.as_str(),
2244				row.kind,
2245				row.actor,
2246				row.file
2247			);
2248		} else {
2249			format_projected_value(out, row, projection);
2250		}
2251	}
2252}
2253
2254fn format_view_read(out: &mut String, result: &ViewReadResult) {
2255	match result {
2256		ViewReadResult::List(list) => {
2257			let _ = writeln!(out, "views: {}", list.views.len());
2258			for view in &list.views {
2259				let _ = writeln!(out, "- {} ({})", view.id, view.scope);
2260			}
2261		}
2262		ViewReadResult::Detail(detail) => {
2263			let _ = writeln!(out, "view: {}", detail.id);
2264			let _ = writeln!(out, "fragment: {}", detail.fragment);
2265			let _ = writeln!(out, "scope: {}", detail.scope);
2266			let _ = writeln!(
2267				out,
2268				"rules: {} boundaries: {} gotchas: {}",
2269				detail.rules.len(),
2270				detail.boundaries.len(),
2271				detail.gotchas.len()
2272			);
2273		}
2274	}
2275}
2276
2277fn format_projected_value(out: &mut String, value: &impl Serialize, projection: &[String]) {
2278	let Ok(Value::Object(fields)) = serde_json::to_value(value) else {
2279		return;
2280	};
2281	let rendered = projection
2282		.iter()
2283		.filter_map(|name| fields.get(name).map(|value| (name, value)))
2284		.map(|(name, value)| format!("{name}={}", compact_json_value(value)))
2285		.collect::<Vec<_>>()
2286		.join(" ");
2287	let _ = writeln!(out, "- {rendered}");
2288}
2289
2290fn compact_json_value(value: &Value) -> String {
2291	match value {
2292		Value::String(value) => value.clone(),
2293		Value::Null => "-".to_string(),
2294		_ => serde_json::to_string(value).unwrap_or_else(|_| "?".to_string()),
2295	}
2296}
2297
2298fn format_query_describe(out: &mut String, result: &QueryDescribeResult) {
2299	let _ = writeln!(out, "queries: {}", result.capabilities.len());
2300	for capability in &result.capabilities {
2301		let _ = writeln!(
2302			out,
2303			"- {} [{}] read_only={} mcp={} projection={} paginated={}",
2304			capability.name,
2305			capability.category,
2306			capability.read_only,
2307			capability.mcp_tool,
2308			capability.projection,
2309			capability.paginated
2310		);
2311		let fields = capability
2312			.fields
2313			.iter()
2314			.map(|field| {
2315				let required = if field.required { "!" } else { "" };
2316				let multiple = if field.multiple { "[]" } else { "" };
2317				let default = field
2318					.default
2319					.as_deref()
2320					.map_or(String::new(), |value| format!("={value}"));
2321				format!(
2322					"{}{}:{}{}{}",
2323					field.name, required, field.value_type, multiple, default
2324				)
2325			})
2326			.collect::<Vec<_>>()
2327			.join(", ");
2328		let _ = writeln!(out, "  fields: {fields}");
2329		if !capability.projection_fields.is_empty() {
2330			let _ = writeln!(
2331				out,
2332				"  project: {}",
2333				capability.projection_fields.join(", ")
2334			);
2335		}
2336		let _ = writeln!(out, "  example: {}", capability.example);
2337	}
2338}
2339
2340fn format_rules_applicable(out: &mut String, result: &RulesApplicableResult) {
2341	let _ = writeln!(out, "focus: {}", result.file);
2342	let _ = writeln!(out, "language: {}", result.language);
2343	if let Some(kind) = &result.symbol_kind {
2344		let _ = writeln!(out, "symbol_kind: {kind}");
2345	}
2346	let applicable = result
2347		.rows
2348		.iter()
2349		.filter(|row| row.status == "applicable")
2350		.count();
2351	let _ = writeln!(out, "rules: {} applicable: {applicable}", result.total);
2352	for row in &result.rows {
2353		let _ = writeln!(
2354			out,
2355			"- {} [{}] {} — {}",
2356			row.rule.id, row.rule.severity, row.status, row.reason
2357		);
2358	}
2359}
2360
2361fn format_change_context(out: &mut String, result: &ChangeContextResult) {
2362	let _ = writeln!(out, "facts:");
2363	match &result.focus {
2364		SymbolGraphFocus::Symbol { symbol } => {
2365			let _ = writeln!(out, "focus: {} {}", symbol.kind, symbol.name);
2366			let _ = writeln!(out, "uri: {}", symbol.uri);
2367			let _ = writeln!(out, "file: {}", symbol.file);
2368		}
2369		SymbolGraphFocus::File { path } => {
2370			let _ = writeln!(out, "focus: file {path}");
2371		}
2372	}
2373	if let Some(source) = &result.source {
2374		let _ = writeln!(out, "source:");
2375		for line in &source.lines {
2376			let _ = writeln!(out, "{:>6} | {}", line.number, line.text);
2377		}
2378	}
2379	format_context_graph(out, &result.graph);
2380	let coverage = result.coverage;
2381	let _ = writeln!(out, "coverage:");
2382	let _ = writeln!(
2383		out,
2384		"- members {}/{} · internal_edges {}/{} · callers {}/{} · callees {}/{}",
2385		coverage.members_emitted,
2386		coverage.members_total,
2387		coverage.internal_edges_emitted,
2388		coverage.internal_edges_total,
2389		coverage.callers_emitted,
2390		coverage.callers_total,
2391		coverage.callees_emitted,
2392		coverage.callees_total
2393	);
2394	let _ = writeln!(
2395		out,
2396		"- notes {}/{} · rules {}/{} · changes {}/{}",
2397		coverage.notes_emitted,
2398		coverage.notes_total,
2399		coverage.rules_emitted,
2400		coverage.rules_total,
2401		coverage.changes_emitted,
2402		coverage.changes_total
2403	);
2404	if !result.notes.is_empty() {
2405		let _ = writeln!(out, "notes:");
2406		for note in &result.notes {
2407			let _ = writeln!(out, "- {} [{}] {}", note.id, note.status, note.title);
2408		}
2409	}
2410	if !result.rules.is_empty() {
2411		let _ = writeln!(out, "applicable_rules:");
2412		for row in &result.rules {
2413			let _ = writeln!(out, "- {} [{}]", row.rule.id, row.rule.severity);
2414		}
2415	}
2416	format_context_changes(out, &result.changed_files, &result.changed_symbols);
2417	if !result.suggested_checks.is_empty() {
2418		let _ = writeln!(out, "suggested_checks:");
2419		for check in &result.suggested_checks {
2420			let _ = writeln!(out, "- {check}");
2421		}
2422	}
2423}
2424
2425fn format_context_graph(out: &mut String, graph: &SymbolGraphResult) {
2426	if !graph.members.is_empty() {
2427		let _ = writeln!(out, "members:");
2428		for member in &graph.members {
2429			let _ = writeln!(
2430				out,
2431				"- {} {} ({}) {}",
2432				member.kind, member.name, member.file, member.uri
2433			);
2434		}
2435	}
2436	if !graph.internal_edges.is_empty() {
2437		let _ = writeln!(out, "internal_edges:");
2438		for edge in &graph.internal_edges {
2439			let _ = writeln!(
2440				out,
2441				"- {} -> {} x{} [{}]",
2442				edge.source,
2443				edge.target,
2444				edge.count,
2445				edge.kinds.join(",")
2446			);
2447		}
2448	}
2449	format_unlinked(out, &graph.unlinked);
2450	for (marker, neighbors) in [("<", &graph.callers), (">", &graph.callees)] {
2451		for neighbor in neighbors {
2452			let _ = writeln!(
2453				out,
2454				"{marker} {} {} x{} [{}] {}",
2455				neighbor.symbol.kind,
2456				neighbor.symbol.name,
2457				neighbor.count,
2458				neighbor.kinds.join(","),
2459				neighbor.symbol.uri
2460			);
2461		}
2462	}
2463}
2464
2465fn format_context_changes(
2466	out: &mut String,
2467	files: &[ChangeReviewFile],
2468	symbols: &[ChangeReviewSymbol],
2469) {
2470	if !files.is_empty() {
2471		let _ = writeln!(out, "changed_files:");
2472		for file in files {
2473			let old = file.old_path.as_deref().unwrap_or("-");
2474			let new = file.new_path.as_deref().unwrap_or("-");
2475			let _ = writeln!(out, "- {old} -> {new} [{}]", file.disposition);
2476		}
2477	}
2478	if !symbols.is_empty() {
2479		let _ = writeln!(out, "changed_symbols:");
2480		for symbol in symbols {
2481			let old = symbol
2482				.old
2483				.as_ref()
2484				.map(|side| side.identity.as_str())
2485				.unwrap_or("-");
2486			let new = symbol
2487				.new
2488				.as_ref()
2489				.map(|side| side.identity.as_str())
2490				.unwrap_or("-");
2491			let _ = writeln!(
2492				out,
2493				"- {} [{}] {old} -> {new}",
2494				symbol.kind, symbol.confidence
2495			);
2496		}
2497	}
2498}
2499
2500fn format_resolution_audit(out: &mut String, result: &ResolutionAuditResult) {
2501	let t = &result.totals;
2502	if !result.prefix.is_empty() {
2503		let _ = writeln!(out, "prefix: {}", result.prefix);
2504	}
2505	let _ = writeln!(
2506		out,
2507		"refs: {} unique: {} candidate: {} external: {} sdk: {} dependency: {} injected_external: {} unknown_external: {} dynamic: {} blocked: {} unresolved: {} explained: {} weak_or_unexplained: {} name_match_candidate: {}",
2508		t.references,
2509		t.unique,
2510		t.candidate,
2511		t.external,
2512		t.sdk,
2513		t.dependency,
2514		t.injected_external,
2515		t.unknown_external,
2516		t.dynamic,
2517		t.blocked,
2518		t.unresolved,
2519		t.explained,
2520		t.weak_or_unexplained,
2521		t.name_match_candidate
2522	);
2523	let _ = writeln!(out, "clusters:");
2524	for cluster in &result.clusters {
2525		let _ = writeln!(
2526			out,
2527			"- [{:>6}] {} {}",
2528			cluster.count, cluster.id, cluster.pattern
2529		);
2530		let sample_limit = if result.clusters.len() == 1 {
2531			cluster.samples.len()
2532		} else {
2533			1
2534		};
2535		for sample in cluster.samples.iter().take(sample_limit) {
2536			let location = match sample.line_range {
2537				Some((start, end)) if start == end => format!("{}:{start}", sample.file),
2538				Some((start, end)) => format!("{}:{start}-{end}", sample.file),
2539				None => sample.file.clone(),
2540			};
2541			let _ = writeln!(
2542				out,
2543				"           ex: {} {} {} -> {} evidence:{}",
2544				location, sample.call_name, sample.receiver, sample.target, sample.evidence
2545			);
2546			if !sample.candidates.is_empty() {
2547				let _ = writeln!(
2548					out,
2549					"           candidates: {}",
2550					sample.candidates.join(", ")
2551				);
2552			}
2553			if !sample.constraints.is_empty() {
2554				let _ = writeln!(
2555					out,
2556					"           constraints: {}",
2557					sample.constraints.join(", ")
2558				);
2559			}
2560			if !sample.snippet.is_empty() {
2561				let _ = writeln!(out, "           code: {}", sample.snippet);
2562			}
2563		}
2564	}
2565	let _ = writeln!(out, "zones:");
2566	for zone in &result.zones {
2567		let _ = writeln!(
2568			out,
2569			"- [{:>5}] {} — {}",
2570			zone.unresolved, zone.zone, zone.dominant_pattern
2571		);
2572	}
2573}
2574
2575fn format_symbol_insights(out: &mut String, result: &SymbolInsightsResult) {
2576	let _ = writeln!(out, "files: {}", result.files);
2577	let _ = writeln!(out, "symbols: {}", result.symbols);
2578	let _ = writeln!(out, "refs: {}", result.references);
2579	let _ = writeln!(out, "languages:");
2580	for row in &result.languages {
2581		let _ = writeln!(out, "- {}: {}", row.name, row.count);
2582	}
2583}
2584
2585fn format_notes(out: &mut String, result: &NotesResult) {
2586	let _ = writeln!(out, "action: {}", result.action);
2587	let _ = writeln!(out, "notes: {}", result.total);
2588	for row in &result.rows {
2589		let _ = writeln!(out, "- {} [{}] {}", row.id, row.status, row.title);
2590	}
2591}
2592
2593fn format_rules_list_rows(out: &mut String, result: &RulesListResult) {
2594	for row in &result.rows {
2595		let _ = writeln!(
2596			out,
2597			"- {} [{}] root={} lang={} domain={}",
2598			row.id, row.severity, row.root, row.lang, row.domain
2599		);
2600		if let Some(message) = &row.message {
2601			let _ = writeln!(out, "  message: {message}");
2602		}
2603	}
2604}
2605
2606fn format_identity_children(out: &mut String, result: &IdentityChildrenResult) {
2607	let prefix = if result.prefix.is_empty() {
2608		"<root>"
2609	} else {
2610		&result.prefix
2611	};
2612	let _ = writeln!(out, "prefix: {prefix}");
2613	let _ = writeln!(out, "children: {}", result.children.len());
2614	for child in &result.children {
2615		let marker = if child.symbol.is_some() { "def" } else { "…" };
2616		let _ = writeln!(
2617			out,
2618			"- {} [{}] defs={} {}",
2619			child.segment, marker, child.defs, child.identity
2620		);
2621	}
2622}
2623
2624fn format_unlinked(out: &mut String, unlinked: &UnlinkedRefsDto) {
2625	let _ = writeln!(
2626		out,
2627		"unlinked refs: external {} (sdk {} · dependency {} · injected {} · unknown {}) · candidate {} · dynamic {} · manifest-blocked {} · unresolved {}",
2628		unlinked.external,
2629		unlinked.sdk,
2630		unlinked.dependency,
2631		unlinked.injected_external,
2632		unlinked.unknown_external,
2633		unlinked.candidate,
2634		unlinked.dynamic,
2635		unlinked.manifest_blocked,
2636		unlinked.unresolved
2637	);
2638	if !unlinked.unresolved_reasons.is_empty() {
2639		let reasons = unlinked
2640			.unresolved_reasons
2641			.iter()
2642			.map(|(reason, count)| format!("{reason} {count}"))
2643			.collect::<Vec<_>>()
2644			.join(" · ");
2645		let _ = writeln!(out, "unresolved by reason: {reasons}");
2646	}
2647}
2648
2649fn format_identity_graph(out: &mut String, result: &IdentityGraphResult) {
2650	let prefix = if result.prefix.is_empty() {
2651		"<root>"
2652	} else {
2653		&result.prefix
2654	};
2655	let _ = writeln!(out, "scope: {prefix}");
2656	let _ = writeln!(
2657		out,
2658		"nodes: {} edges: {}",
2659		result.nodes.len(),
2660		result.edges.len()
2661	);
2662	format_unlinked(out, &result.unlinked);
2663	for edge in &result.edges {
2664		let _ = writeln!(
2665			out,
2666			"- {} -> {} x{} [{}]",
2667			edge.source,
2668			edge.target,
2669			edge.count,
2670			edge.kinds.join(",")
2671		);
2672	}
2673	for port in &result.ports_in {
2674		let _ = writeln!(
2675			out,
2676			"< {} x{} [{}]",
2677			port.identity,
2678			port.count,
2679			port.kinds.join(",")
2680		);
2681	}
2682	for port in &result.ports_out {
2683		let _ = writeln!(
2684			out,
2685			"> {} x{} [{}]",
2686			port.identity,
2687			port.count,
2688			port.kinds.join(",")
2689		);
2690	}
2691}
2692
2693fn format_symbol_graph(out: &mut String, result: &SymbolGraphResult) {
2694	match &result.focus {
2695		SymbolGraphFocus::Symbol { symbol } => {
2696			let _ = writeln!(
2697				out,
2698				"focus: {} {} ({})",
2699				symbol.kind, symbol.name, symbol.file
2700			);
2701		}
2702		SymbolGraphFocus::File { path } => {
2703			let _ = writeln!(out, "focus: file {path}");
2704		}
2705	}
2706	let _ = writeln!(
2707		out,
2708		"members: {} internal edges: {}",
2709		result.members.len(),
2710		result.internal_edges.len()
2711	);
2712	format_unlinked(out, &result.unlinked);
2713	for caller in &result.callers {
2714		let _ = writeln!(
2715			out,
2716			"< {} {} ({}) x{} [{}]",
2717			caller.symbol.kind,
2718			caller.symbol.name,
2719			caller.symbol.file,
2720			caller.count,
2721			caller.kinds.join(",")
2722		);
2723	}
2724	for callee in &result.callees {
2725		let _ = writeln!(
2726			out,
2727			"> {} {} ({}) x{} [{}]",
2728			callee.symbol.kind,
2729			callee.symbol.name,
2730			callee.symbol.file,
2731			callee.count,
2732			callee.kinds.join(",")
2733		);
2734	}
2735}
2736
2737fn format_change_review(out: &mut String, result: &ChangeReviewResult) {
2738	let _ = writeln!(out, "scope: {}", result.scope);
2739	let _ = writeln!(
2740		out,
2741		"files: {} ({} analyzable) symbols: {} refs: {} ({} retargeted) residual: {}",
2742		result.summary.files,
2743		result.summary.analyzable_files,
2744		result.summary.symbol_changes,
2745		result.summary.ref_changes,
2746		result.summary.retargeted_refs,
2747		result.summary.residual_files
2748	);
2749	for file in &result.files {
2750		let path = match (&file.old_path, &file.new_path) {
2751			(Some(old), Some(new)) if old != new => format!("{old} -> {new}"),
2752			(_, Some(new)) => new.clone(),
2753			(Some(old), None) => old.clone(),
2754			(None, None) => "<unknown>".to_string(),
2755		};
2756		let _ = writeln!(
2757			out,
2758			"- {path} {}{}{}",
2759			file.disposition,
2760			if file.analyzable {
2761				""
2762			} else {
2763				" (not analyzable)"
2764			},
2765			if file.coverage_explained {
2766				""
2767			} else {
2768				" [residual]"
2769			}
2770		);
2771	}
2772	for change in &result.symbol_changes {
2773		let side = change.new.as_ref().or(change.old.as_ref());
2774		let Some(side) = side else { continue };
2775		let _ = writeln!(
2776			out,
2777			"  {} {} {} [{}]",
2778			change.kind, side.kind, side.name, change.confidence
2779		);
2780	}
2781	for diagnostic in &result.diagnostics {
2782		let _ = writeln!(out, "diagnostic: {diagnostic}");
2783	}
2784}
2785
2786fn format_rules_check(out: &mut String, result: &RulesCheckResult) {
2787	let _ = writeln!(out, "exit: {}", result.exit);
2788	let _ = writeln!(
2789		out,
2790		"violations: {} errors: {} elapsed_ms: {}",
2791		result.summary.total_violations, result.summary.total_errors, result.summary.elapsed_ms
2792	);
2793	for violation in &result.violations {
2794		let _ = writeln!(
2795			out,
2796			"- {} {}:{}-{} [{}] {}",
2797			violation.root,
2798			violation.path,
2799			violation.lines.0,
2800			violation.lines.1,
2801			violation.rule_id,
2802			violation.message
2803		);
2804	}
2805	if !result.rule_reports.is_empty() {
2806		let _ = writeln!(out, "rule_reports: {}", result.rule_reports.len());
2807	}
2808}
2809
2810#[derive(Default)]
2811struct FieldBag {
2812	values: Vec<(String, FieldValue)>,
2813	positional: Vec<String>,
2814	projection: Vec<String>,
2815	consistency: Consistency,
2816}
2817
2818#[derive(Clone, Debug, Eq, PartialEq)]
2819struct FieldValue {
2820	text: String,
2821	quoted: bool,
2822}
2823
2824#[derive(Clone, Debug, Eq, PartialEq)]
2825struct QueryToken {
2826	text: String,
2827	quoted: bool,
2828}
2829
2830impl FieldBag {
2831	fn bool(&self, key: &str) -> Result<Option<bool>, QueryParseError> {
2832		self.one(key)
2833			.map(|value| match value.as_str() {
2834				"true" => Ok(true),
2835				"false" => Ok(false),
2836				_ => Err(QueryParseError::InvalidValue {
2837					key: key.to_string(),
2838					value,
2839				}),
2840			})
2841			.transpose()
2842	}
2843
2844	fn page(&self) -> Result<Page, QueryParseError> {
2845		let limit = self.usize("limit")?.unwrap_or(80);
2846		let cursor = self
2847			.one("cursor")
2848			.map(|value| parse_cursor(&value))
2849			.transpose()?;
2850		Ok(Page { cursor, limit })
2851	}
2852
2853	fn usize(&self, key: &str) -> Result<Option<usize>, QueryParseError> {
2854		self.one(key)
2855			.map(|value| {
2856				value
2857					.parse::<usize>()
2858					.map_err(|_| QueryParseError::InvalidValue {
2859						key: key.to_string(),
2860						value,
2861					})
2862			})
2863			.transpose()
2864	}
2865
2866	fn one(&self, key: &str) -> Option<String> {
2867		self.values
2868			.iter()
2869			.find(|(candidate, _)| candidate == key)
2870			.map(|(_, value)| value.text.clone())
2871	}
2872
2873	fn many(&self, key: &str) -> Vec<String> {
2874		self.values
2875			.iter()
2876			.filter(|(candidate, _)| candidate == key)
2877			.flat_map(|(_, value)| {
2878				if value.quoted || !MULTI_VALUE_FIELDS.contains(&key) {
2879					vec![value.text.clone()]
2880				} else {
2881					split_csv(strip_bracket_list(key, &value.text))
2882				}
2883			})
2884			.collect()
2885	}
2886}
2887
2888fn collect_tokens(
2889	tokens: &[QueryToken],
2890	fields: &mut FieldBag,
2891	positional: &mut Vec<String>,
2892) -> Result<(), QueryParseError> {
2893	for token in tokens {
2894		if let Some((key, value)) = token.text.split_once(':') {
2895			fields.values.push((
2896				key.to_string(),
2897				FieldValue {
2898					text: value.to_string(),
2899					quoted: token.quoted,
2900				},
2901			));
2902		} else {
2903			positional.push(token.text.trim_end_matches(',').to_string());
2904		}
2905	}
2906	Ok(())
2907}
2908
2909fn parse_cursor(value: &str) -> Result<QueryCursor, QueryParseError> {
2910	let Some((generation, offset)) = value.split_once(':') else {
2911		return Err(QueryParseError::InvalidValue {
2912			key: "cursor".to_string(),
2913			value: value.to_string(),
2914		});
2915	};
2916	let generation = generation
2917		.parse::<u64>()
2918		.map_err(|_| QueryParseError::InvalidValue {
2919			key: "cursor".to_string(),
2920			value: value.to_string(),
2921		})?;
2922	let offset = offset
2923		.parse::<usize>()
2924		.map_err(|_| QueryParseError::InvalidValue {
2925			key: "cursor".to_string(),
2926			value: value.to_string(),
2927		})?;
2928	Ok(QueryCursor::new(
2929		offset,
2930		Some(WorkspaceGeneration(generation)),
2931	))
2932}
2933
2934fn tokenize(input: &str) -> Result<Vec<QueryToken>, QueryParseError> {
2935	let mut tokens = Vec::new();
2936	let mut current = String::new();
2937	let mut chars = input.chars().peekable();
2938	let mut quoted = false;
2939	let mut token_quoted = false;
2940	while let Some(ch) = chars.next() {
2941		match ch {
2942			'"' => {
2943				quoted = !quoted;
2944				token_quoted = true;
2945			}
2946			'\\' if quoted => {
2947				if let Some(next) = chars.next() {
2948					current.push(next);
2949				}
2950			}
2951			ch if ch.is_whitespace() && !quoted => {
2952				if !current.is_empty() {
2953					tokens.push(QueryToken {
2954						text: std::mem::take(&mut current),
2955						quoted: std::mem::take(&mut token_quoted),
2956					});
2957				}
2958			}
2959			ch => current.push(ch),
2960		}
2961	}
2962	if quoted {
2963		return Err(QueryParseError::InvalidToken(input.to_string()));
2964	}
2965	if !current.is_empty() {
2966		tokens.push(QueryToken {
2967			text: current,
2968			quoted: token_quoted,
2969		});
2970	}
2971	Ok(tokens)
2972}
2973
2974// `shape:[callable,type]` list sugar, restricted to enum-like fields so glob
2975// character classes in `path:`/`file:` values stay untouched.
2976fn strip_bracket_list<'a>(key: &str, value: &'a str) -> &'a str {
2977	if !BRACKET_LIST_FIELDS.contains(&key) {
2978		return value;
2979	}
2980	value
2981		.strip_prefix('[')
2982		.and_then(|inner| inner.strip_suffix(']'))
2983		.unwrap_or(value)
2984}
2985
2986pub fn split_csv(value: &str) -> Vec<String> {
2987	value
2988		.split(',')
2989		.map(str::trim)
2990		.filter(|entry| !entry.is_empty())
2991		.map(ToOwned::to_owned)
2992		.collect()
2993}
2994
2995#[cfg(test)]
2996mod tests {
2997	use super::*;
2998	use serde::Serialize;
2999
3000	fn serialized_fields(value: impl Serialize) -> Vec<String> {
3001		let mut fields = serde_json::to_value(value)
3002			.expect("serialize query DTO")
3003			.as_object()
3004			.expect("query DTO object")
3005			.keys()
3006			.cloned()
3007			.collect::<Vec<_>>();
3008		fields.sort();
3009		fields
3010	}
3011
3012	fn dto_fields(verb: &str) -> Vec<String> {
3013		match verb {
3014			"query.describe" => serialized_fields(QueryDescribeQuery::default()),
3015			"workspace.status" => Vec::new(),
3016			"tree.children" => serialized_fields(TreeChildrenQuery::default()),
3017			"symbol.search" | "symbol.insights" => serialized_fields(SymbolSearchQuery::default()),
3018			"symbol.detail" => serialized_fields(SymbolDetailQuery {
3019				workspace: None,
3020				uri: String::new(),
3021				context_lines: 0,
3022			}),
3023			"symbol.usages" => serialized_fields(SymbolUsagesQuery {
3024				workspace: None,
3025				uri: String::new(),
3026				direction: UsageDirection::Incoming,
3027				path: Vec::new(),
3028				lang: Vec::new(),
3029				projection: Vec::new(),
3030			}),
3031			"view.read" => serialized_fields(ViewReadQuery {
3032				uri: String::new(),
3033				scheme: None,
3034				context_lines: 0,
3035				include_code: false,
3036			}),
3037			"rules.list" => serialized_fields(RulesListQuery::default()),
3038			"rules.check" => serialized_fields(RulesCheckQuery::default()),
3039			"rules.applicable" => serialized_fields(RulesApplicableQuery::default()),
3040			"change.review" => serialized_fields(ChangeReviewQuery::default()),
3041			"change.context" => serialized_fields(ChangeContextQuery::default()),
3042			"symbol.graph" => serialized_fields(SymbolGraphQuery::default()),
3043			"identity.children" | "identity.graph" => {
3044				serialized_fields(IdentityChildrenQuery::default())
3045			}
3046			"resolution.audit" => serialized_fields(ResolutionAuditQuery::default()),
3047			"notes" => serialized_fields(NotesQuery {
3048				action: NotesAction::List,
3049				id: None,
3050				moniker: None,
3051				kind: None,
3052				status: None,
3053				title: None,
3054				body: None,
3055				created_by: None,
3056				orphan: None,
3057				include_done: false,
3058			}),
3059			other => panic!("missing DTO field fixture for {other}"),
3060		}
3061	}
3062
3063	#[test]
3064	fn capability_registry_fields_exist_on_query_dtos() {
3065		for spec in query_capability_specs() {
3066			let dto = dto_fields(spec.name);
3067			for field in spec.fields {
3068				assert!(
3069					dto.iter().any(|candidate| candidate == field),
3070					"{} field `{field}` missing from DTO fields {dto:?}",
3071					spec.name
3072				);
3073			}
3074			assert!(
3075				spec.fields
3076					.iter()
3077					.all(|field| !COMMON_FIELDS.contains(field)),
3078				"{} repeats a common request field",
3079				spec.name
3080			);
3081		}
3082	}
3083
3084	#[test]
3085	fn describes_live_query_contract() {
3086		let request = parse_query("query.describe symbol.usages").expect("query describe");
3087		assert!(matches!(
3088			request.query,
3089			Query::QueryDescribe(QueryDescribeQuery { verb: Some(ref verb) })
3090				if verb == "symbol.usages"
3091		));
3092		let result = describe_query_capabilities(Some("symbol.usages")).expect("capability");
3093		let capability = result.capabilities.first().expect("described query");
3094		assert!(capability.read_only);
3095		assert_eq!(capability.mcp_tool, "code_moniker_usages");
3096		assert!(capability.projection);
3097		assert!(capability.paginated);
3098		assert!(
3099			capability
3100				.fields
3101				.iter()
3102				.any(|field| field.name == "uri" && field.required)
3103		);
3104		assert!(capability.projection_fields.contains(&"actor".to_string()));
3105		assert_eq!(
3106			CapabilitySet::default().query_mcp_tools["symbol.usages"],
3107			"code_moniker_usages"
3108		);
3109	}
3110
3111	#[test]
3112	fn parses_symbol_graph_relational_filters() {
3113		let request = parse_query(
3114			"symbol.graph focus:\"src/lib.rs\" direction:incoming relation:[calls,uses_type] min_count:2 include_internal:false",
3115		)
3116		.expect("symbol graph filters");
3117		let Query::SymbolGraph(query) = request.query else {
3118			panic!("expected symbol graph query");
3119		};
3120		assert_eq!(query.direction, UsageDirection::Incoming);
3121		assert_eq!(query.relation, vec!["calls", "uses_type"]);
3122		assert_eq!(query.min_count, 2);
3123		assert!(!query.include_internal);
3124	}
3125
3126	#[test]
3127	fn preserves_commas_in_quoted_symbol_uri() {
3128		let uri = "code+moniker://./lang:ts/dir:src/module:Session/class:Session/method:launchRequest(response:DebugProtocol.LaunchResponse,args:LaunchRequestArguments)";
3129		let request = parse_query(&format!(
3130			"symbol.usages uri:\"{uri}\" direction:outgoing limit:5"
3131		))
3132		.expect("quoted symbol URI");
3133		let Query::SymbolUsages(query) = request.query else {
3134			panic!("expected symbol usages query");
3135		};
3136		assert_eq!(query.uri, uri);
3137	}
3138
3139	#[test]
3140	fn quoted_multi_value_field_does_not_split_commas() {
3141		let request =
3142			parse_query("symbol.search path:\"src/generated,a.ts\" shape:\"callable,type\"")
3143				.expect("quoted multi-value fields");
3144		let Query::SymbolSearch(query) = request.query else {
3145			panic!("expected symbol search query");
3146		};
3147		assert_eq!(query.path, vec!["src/generated,a.ts"]);
3148		assert_eq!(query.shape, vec!["callable,type"]);
3149	}
3150
3151	#[test]
3152	fn unquoted_multi_value_fields_still_split_commas() {
3153		let request = parse_query("symbol.search path:src,tests shape:callable,type")
3154			.expect("unquoted multi-value fields");
3155		let Query::SymbolSearch(query) = request.query else {
3156			panic!("expected symbol search query");
3157		};
3158		assert_eq!(query.path, vec!["src", "tests"]);
3159		assert_eq!(query.shape, vec!["callable", "type"]);
3160	}
3161
3162	#[test]
3163	fn scalar_field_preserves_unquoted_regex_commas() {
3164		let request = parse_query("symbol.search name:^launch(Request){1,3}$").expect("name regex");
3165		let Query::SymbolSearch(query) = request.query else {
3166			panic!("expected symbol search query");
3167		};
3168		assert_eq!(query.name.as_deref(), Some("^launch(Request){1,3}$"));
3169	}
3170
3171	#[test]
3172	fn parses_resolution_audit_positional_prefix() {
3173		let request = parse_query("resolution.audit java limit:7 cluster:resolution-abc")
3174			.expect("audit query");
3175		let Query::ResolutionAudit(query) = request.query else {
3176			panic!("expected resolution audit query");
3177		};
3178		assert_eq!(query.prefix, "java");
3179		assert_eq!(query.limit, 7);
3180		assert_eq!(query.cluster.as_deref(), Some("resolution-abc"));
3181	}
3182
3183	#[test]
3184	fn resolution_audit_default_limit_matches_its_documented_contract() {
3185		let request = parse_query("resolution.audit python").expect("audit query");
3186		let Query::ResolutionAudit(query) = request.query else {
3187			panic!("expected resolution audit query");
3188		};
3189		assert_eq!(query.limit, 20);
3190	}
3191
3192	#[test]
3193	fn formats_resolution_audit_explanation_metrics() {
3194		let response = QueryResponse {
3195			generation: None,
3196			result: QueryResult::ResolutionAudit(Box::new(ResolutionAuditResult {
3197				prefix: "lang:python".to_string(),
3198				totals: AuditTotalsDto {
3199					references: 10,
3200					resolved: 4,
3201					unique: 4,
3202					candidate: 2,
3203					external: 1,
3204					sdk: 1,
3205					dependency: 0,
3206					injected_external: 0,
3207					unknown_external: 0,
3208					dynamic: 1,
3209					blocked: 1,
3210					unresolved: 1,
3211					explained: 9,
3212					weak_or_unexplained: 3,
3213					name_match_resolved: 0,
3214					name_match_candidate: 2,
3215				},
3216				clusters: vec![AuditClusterDto {
3217					id: "resolution-abc".to_string(),
3218					pattern: "candidate name_match/method_call".to_string(),
3219					count: 2,
3220					samples: vec![
3221						AuditSampleDto {
3222							file: "src/a.py".to_string(),
3223							line_range: Some((10, 10)),
3224							snippet: "value.first()".to_string(),
3225							source: "module:a".to_string(),
3226							call_name: "first".to_string(),
3227							receiver: "value".to_string(),
3228							target: "method:first".to_string(),
3229							evidence: "name_match".to_string(),
3230							constraints: vec!["scope:global".to_string()],
3231							candidates: vec!["class:A/method:first".to_string()],
3232						},
3233						AuditSampleDto {
3234							file: "src/b.py".to_string(),
3235							line_range: Some((20, 21)),
3236							snippet: "other.second()".to_string(),
3237							source: "module:b".to_string(),
3238							call_name: "second".to_string(),
3239							receiver: "other".to_string(),
3240							target: "method:second".to_string(),
3241							evidence: "name_match".to_string(),
3242							constraints: Vec::new(),
3243							candidates: Vec::new(),
3244						},
3245					],
3246				}],
3247				zones: Vec::new(),
3248			})),
3249			next_cursor: None,
3250		};
3251
3252		let formatted = format_query_response(&response);
3253
3254		assert!(formatted.contains("unique: 4 candidate: 2"));
3255		assert!(formatted.contains("explained: 9 weak_or_unexplained: 3"));
3256		assert!(formatted.contains("name_match_candidate: 2"));
3257		assert!(formatted.contains("resolution-abc"));
3258		assert!(formatted.contains("first value"));
3259		assert!(formatted.contains("second other"));
3260		assert!(formatted.contains("src/a.py:10"));
3261		assert!(formatted.contains("src/b.py:20-21"));
3262		assert!(formatted.contains("constraints: scope:global"));
3263		assert!(formatted.contains("code: value.first()"));
3264	}
3265
3266	#[test]
3267	fn rejects_unknown_projection_field_with_suggestion() {
3268		let error =
3269			parse_query("symbol.search name:App\nproject nme uri").expect_err("unknown projection");
3270		let message = error.to_string();
3271		assert!(
3272			message.contains("unknown projection field `nme`"),
3273			"{message}"
3274		);
3275		assert!(message.contains("did you mean `name`?"), "{message}");
3276	}
3277
3278	#[test]
3279	fn projected_formatter_emits_only_requested_symbol_fields() {
3280		let response = QueryResponse {
3281			generation: Some(WorkspaceGeneration(3)),
3282			result: QueryResult::SymbolList(SymbolListResult {
3283				rows: vec![SymbolDto {
3284					root: ".".to_string(),
3285					uri: "code+moniker://./lang:rs/fn:run()".to_string(),
3286					id: "id".to_string(),
3287					name: "run".to_string(),
3288					kind: "fn".to_string(),
3289					visibility: "public".to_string(),
3290					signature: "run()".to_string(),
3291					file: "src/lib.rs".to_string(),
3292					language: "rs".to_string(),
3293					line_range: Some((4, 8)),
3294					navigable: true,
3295					score: None,
3296					match_reason: None,
3297					source: None,
3298				}],
3299				total: 1,
3300			}),
3301			next_cursor: None,
3302		};
3303		let formatted =
3304			format_query_response_projected(&response, &["name".to_string(), "uri".to_string()]);
3305		assert!(
3306			formatted.contains("name=run uri=code+moniker://"),
3307			"{formatted}"
3308		);
3309		assert!(!formatted.contains("src/lib.rs"), "{formatted}");
3310		assert!(!formatted.contains("signature="), "{formatted}");
3311	}
3312
3313	#[test]
3314	fn parses_human_symbol_search() {
3315		let query = parse_query(
3316			r#"symbol.search "SharedWorkspaceIndex"
3317  filter path:"crates/**" shape:type
3318  project name, kind, uri
3319  page limit:20 cursor:7:40"#,
3320		)
3321		.expect("query");
3322		assert_eq!(query.page.limit, 20);
3323		assert_eq!(
3324			query.page.cursor,
3325			Some(QueryCursor::new(40, Some(WorkspaceGeneration(7))))
3326		);
3327		match query.query {
3328			Query::SymbolSearch(search) => {
3329				assert_eq!(search.text.as_deref(), Some("SharedWorkspaceIndex"));
3330				assert_eq!(search.path, vec!["crates/**"]);
3331				assert_eq!(search.shape, vec!["type"]);
3332				assert_eq!(search.projection, vec!["name", "kind", "uri"]);
3333			}
3334			other => panic!("unexpected query {other:?}"),
3335		}
3336	}
3337
3338	#[test]
3339	fn parses_rules_check_consistency() {
3340		let query = parse_query(
3341			r#"rules.check profile:"agent"
3342  consistency refresh-if-stale
3343  page limit:50"#,
3344		)
3345		.expect("query");
3346		assert_eq!(query.consistency, Consistency::RefreshIfStale);
3347		assert_eq!(query.page.limit, 50);
3348	}
3349
3350	#[test]
3351	fn rejects_offset_only_human_cursor() {
3352		let error =
3353			parse_query("symbol.search Customer\npage cursor:40").expect_err("offset-only cursor");
3354		assert!(matches!(
3355			error,
3356			QueryParseError::InvalidValue { ref key, .. } if key == "cursor"
3357		));
3358	}
3359
3360	#[test]
3361	fn parses_bracket_list_shape() {
3362		let query = parse_query("symbol.search shape:[callable,type] limit:5").expect("query");
3363		match query.query {
3364			Query::SymbolSearch(search) => assert_eq!(search.shape, vec!["callable", "type"]),
3365			other => panic!("unexpected query {other:?}"),
3366		}
3367	}
3368
3369	#[test]
3370	fn rejects_unterminated_bracket_list() {
3371		let error = parse_query("symbol.search shape:[callable").expect_err("unterminated list");
3372		assert!(matches!(
3373			error,
3374			QueryParseError::InvalidValue { ref key, .. } if key == "shape"
3375		));
3376	}
3377
3378	#[test]
3379	fn rejects_unknown_field_with_alias_suggestion() {
3380		let error = parse_query(r#"symbol.search text:"foo""#).expect_err("unknown field");
3381		let message = error.to_string();
3382		assert!(
3383			message.contains("unknown field `text` for `symbol.search`"),
3384			"{message}"
3385		);
3386		assert!(message.contains("did you mean `name`?"), "{message}");
3387	}
3388
3389	#[test]
3390	fn rejects_typo_field_with_suggestion() {
3391		let error = parse_query("rules.check profil:agent").expect_err("typo field");
3392		let message = error.to_string();
3393		assert!(message.contains("did you mean `profile`?"), "{message}");
3394	}
3395
3396	#[test]
3397	fn lists_valid_fields_without_close_match() {
3398		let error = parse_query("change.review foobarbaz:1").expect_err("unknown field");
3399		let message = error.to_string();
3400		assert!(
3401			message.contains("valid fields: consistency, cursor, limit, workspace"),
3402			"{message}"
3403		);
3404	}
3405
3406	#[test]
3407	fn rejects_unexpected_positional() {
3408		let error = parse_query("workspace.status extra").expect_err("positional");
3409		assert!(matches!(
3410			error,
3411			QueryParseError::UnexpectedArgument { ref value, .. } if value == "extra"
3412		));
3413	}
3414
3415	#[test]
3416	fn rejects_projection_on_unsupported_verb() {
3417		let error = parse_query("rules.list\nproject name").expect_err("projection");
3418		assert!(matches!(
3419			error,
3420			QueryParseError::UnsupportedProjection { .. }
3421		));
3422	}
3423
3424	#[test]
3425	fn parses_inline_consistency() {
3426		let query =
3427			parse_query("rules.check profile:agent consistency:refresh-if-stale").expect("query");
3428		assert_eq!(query.consistency, Consistency::RefreshIfStale);
3429	}
3430
3431	#[test]
3432	fn formats_generation_aware_cursor() {
3433		let response = QueryResponse {
3434			generation: Some(WorkspaceGeneration(7)),
3435			result: QueryResult::SymbolList(SymbolListResult {
3436				rows: Vec::new(),
3437				total: 0,
3438			}),
3439			next_cursor: Some(QueryCursor::new(40, Some(WorkspaceGeneration(7)))),
3440		};
3441		let formatted = format_query_response(&response);
3442		assert!(formatted.contains("next_cursor: 7:40"));
3443	}
3444}
3445
3446/// Umbrella over every root RPC type, used only to emit a single JSON Schema
3447/// document (`export-schema`) whose definitions cover the whole wire contract.
3448#[cfg(feature = "schema")]
3449#[derive(schemars::JsonSchema)]
3450#[allow(dead_code)]
3451pub struct DaemonProtocol {
3452	pub handshake: HandshakeResponse,
3453	pub registry_entry: DaemonRegistryEntry,
3454	pub workspace_config: DaemonWorkspaceConfig,
3455	pub query_request: QueryRequest,
3456	pub query: Query,
3457	pub query_response: QueryResponse,
3458	pub query_result: QueryResult,
3459	pub command_request: CommandRequest,
3460	pub command_response: CommandResponse,
3461	pub event: WorkspaceEventDto,
3462	pub error: QueryError,
3463}
3464
3465#[cfg(test)]
3466mod contract_tests {
3467	//! Lock the serde wire shapes the JSON Schema (and every generated client)
3468	//! depends on. These guard the contract, not the Rust layout.
3469	use super::*;
3470	use serde_json::json;
3471
3472	#[test]
3473	fn query_is_op_tagged() {
3474		let query = Query::SymbolSearch(SymbolSearchQuery {
3475			text: Some("widget".to_string()),
3476			..Default::default()
3477		});
3478		let value = serde_json::to_value(&query).unwrap();
3479		assert_eq!(value["op"], "symbol_search");
3480		assert_eq!(value["text"], "widget");
3481	}
3482
3483	#[test]
3484	fn query_result_is_kind_and_data_tagged() {
3485		let result = QueryResult::SymbolList(SymbolListResult {
3486			rows: Vec::new(),
3487			total: 0,
3488		});
3489		assert_eq!(
3490			serde_json::to_value(&result).unwrap(),
3491			json!({ "kind": "symbol_list", "data": { "rows": [], "total": 0 } }),
3492		);
3493	}
3494
3495	#[test]
3496	fn generation_serializes_as_scalar() {
3497		assert_eq!(
3498			serde_json::to_value(WorkspaceGeneration(7)).unwrap(),
3499			json!(7)
3500		);
3501	}
3502
3503	#[test]
3504	fn line_range_is_a_two_element_array() {
3505		let range: Option<(u32, u32)> = Some((3, 9));
3506		assert_eq!(serde_json::to_value(range).unwrap(), json!([3, 9]));
3507		assert_eq!(
3508			serde_json::to_value(Option::<(u32, u32)>::None).unwrap(),
3509			json!(null)
3510		);
3511	}
3512
3513	#[test]
3514	fn consistency_is_snake_case() {
3515		assert_eq!(
3516			serde_json::to_value(Consistency::RefreshIfStale).unwrap(),
3517			json!("refresh_if_stale"),
3518		);
3519	}
3520
3521	#[test]
3522	fn event_kind_is_snake_case() {
3523		let event = WorkspaceEventDto {
3524			kind: WorkspaceEventKind::GitBase,
3525			generation: None,
3526			stale_summary: None,
3527		};
3528		assert_eq!(serde_json::to_value(&event).unwrap()["kind"], "git_base");
3529	}
3530}