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