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