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