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