1use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::serde_path;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[serde(rename_all = "kebab-case")]
17pub enum SemanticCapability {
18 SymbolUse,
20 SymbolTrace,
22 ApiSurface,
24 SymbolImpact,
26 TypeCoupling,
28}
29
30#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33#[serde(rename_all = "kebab-case")]
34pub enum SemanticCompletenessRequirement {
35 #[default]
37 BestEffort,
38 Complete,
40}
41
42#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(rename_all = "kebab-case")]
46pub enum SemanticCompleteness {
47 Complete,
49 Partial,
51 #[default]
53 Unavailable,
54}
55
56#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
59#[serde(rename_all = "kebab-case")]
60pub enum SemanticAnalysisMode {
61 #[default]
63 Syntactic,
64 TypeAware,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
70#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
71pub struct SemanticAnalysisIdentity {
72 pub mode: SemanticAnalysisMode,
74 pub semantic_schema_version: u32,
76 pub capabilities: Vec<SemanticCapability>,
78 pub project_config_hash: String,
80 pub backend_family: String,
82 pub completeness: SemanticCompleteness,
84}
85
86pub const DEFERRED_PROJECT_CONFIG_HASH: &str = "deferred:no-semantic-queries";
92
93impl Default for SemanticAnalysisIdentity {
94 fn default() -> Self {
95 Self {
96 mode: SemanticAnalysisMode::Syntactic,
97 semantic_schema_version: 1,
98 capabilities: Vec::new(),
99 project_config_hash: String::new(),
100 backend_family: String::new(),
101 completeness: SemanticCompleteness::Complete,
102 }
103 }
104}
105
106impl SemanticAnalysisIdentity {
107 #[must_use]
110 pub fn syntactic() -> Self {
111 Self::default()
112 }
113
114 #[must_use]
118 pub fn incompatible_fields(&self, other: &Self) -> Vec<&'static str> {
119 let mut fields = Vec::new();
120 if self.mode != other.mode {
121 fields.push("mode");
122 }
123 if self.semantic_schema_version != other.semantic_schema_version {
124 fields.push("semantic_schema_version");
125 }
126 if self.capabilities != other.capabilities {
127 fields.push("capabilities");
128 }
129 let project_hash_deferred = self.project_config_hash == DEFERRED_PROJECT_CONFIG_HASH
130 || other.project_config_hash == DEFERRED_PROJECT_CONFIG_HASH;
131 if !project_hash_deferred && self.project_config_hash != other.project_config_hash {
132 fields.push("project_config_hash");
133 }
134 if self.backend_family != other.backend_family {
135 fields.push("backend_family");
136 }
137 if self.completeness != other.completeness {
138 fields.push("completeness");
139 }
140 fields
141 }
142}
143
144#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
147#[serde(rename_all = "kebab-case")]
148pub enum SemanticNamespace {
149 #[default]
151 Value,
152 Type,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159pub struct SemanticSymbol {
160 #[serde(serialize_with = "serde_path::serialize")]
162 pub path: PathBuf,
163 pub namespace: SemanticNamespace,
165 pub declaration_kind: String,
167 pub exported_name: String,
169 pub local_name: String,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub owner: Option<String>,
174 pub line: u32,
176 pub col: u32,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
182#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
183pub struct SemanticSourceLocation {
184 #[serde(serialize_with = "serde_path::serialize")]
186 pub path: PathBuf,
187 pub line: u32,
189 pub col: u32,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
195#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
196#[serde(rename_all = "kebab-case")]
197pub enum SemanticGapReason {
198 NoProject,
200 AmbiguousProject,
202 BlockingDiagnostics,
204 UnknownSymbol,
206 UnknownEntryPoint,
208 EvidenceLimit,
210 DynamicBehavior,
212 VirtualDispatch,
215 DynamicMemberAccess,
217 DecoratedDeclaration,
219 OptionalContract,
221 AccessorPair,
223 OverloadSet,
225 AttachedComment,
227 AbstractDeclaration,
229 IncompleteProjectCoverage,
231 FrameworkContractProvenance,
234 Capacity,
236 UnsupportedSyntax,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243pub struct SemanticOmission {
244 pub reason_code: SemanticGapReason,
246 pub count: usize,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253#[serde(rename_all = "kebab-case")]
254pub enum SemanticCandidateDecisionKind {
255 ConfirmedUsed,
257 ContractPreserved,
259 ConfirmedNoStaticReferences,
261 RetainedAbstained,
263 RetainedUnresolved,
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
269#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
270#[serde(rename_all = "kebab-case")]
271pub enum SemanticContractRelation {
272 InterfaceImplementation,
274 AbstractImplementation,
276 Override,
278 OptionalContract,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285pub struct SemanticContractEvidence {
286 pub relation: SemanticContractRelation,
288 pub declaration: SemanticSymbol,
290 pub optional: bool,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
297#[serde(rename_all = "kebab-case")]
298pub enum SemanticFrameworkRelation {
299 Extends,
301 Implements,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
307#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
308pub struct SemanticFrameworkContract {
309 pub framework: String,
311 pub package: String,
313 pub heritage_symbol: String,
315 pub heritage_names: Vec<String>,
317 pub relation: SemanticFrameworkRelation,
319 pub members: Vec<String>,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
325#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
326pub struct SemanticFrameworkContractEvidence {
327 pub framework: String,
329 pub package: String,
331 pub relation: SemanticFrameworkRelation,
333 pub declaration: SemanticSymbol,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
339#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
340pub struct SemanticEditGuard {
341 pub start: usize,
343 pub end: usize,
345 pub declaration_sha256: String,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
352pub struct SemanticCandidateDecision {
353 pub query_id: usize,
355 pub subject: SemanticSymbol,
357 pub decision: SemanticCandidateDecisionKind,
359 pub status: SemanticCompleteness,
361 pub owning_projects: Vec<String>,
363 #[serde(default, skip_serializing_if = "Vec::is_empty")]
365 pub evidence: Vec<SemanticReference>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub contract: Option<SemanticContractEvidence>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub framework_contract: Option<SemanticFrameworkContractEvidence>,
372 pub closed_world_eligible: bool,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub edit_guard: Option<SemanticEditGuard>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub reason_code: Option<SemanticGapReason>,
380 pub explanation: String,
382 #[serde(default, skip_serializing_if = "Vec::is_empty")]
384 pub actions: Vec<String>,
385 pub total_evidence_count: usize,
387 pub truncated: bool,
389 #[serde(default, skip_serializing_if = "Vec::is_empty")]
391 pub omissions: Vec<SemanticOmission>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
396#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
397pub struct SemanticQuerySummary {
398 pub query_id: usize,
400 pub capability: SemanticCapability,
402 pub assertion: String,
404 pub status: SemanticCompleteness,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub reason_code: Option<SemanticGapReason>,
409 pub total_evidence_count: usize,
411 pub truncated: bool,
413 #[serde(default, skip_serializing_if = "Vec::is_empty")]
415 pub omissions: Vec<SemanticOmission>,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub actions: Vec<String>,
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
423#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
424pub struct SemanticReference {
425 #[serde(serialize_with = "serde_path::serialize")]
427 pub path: PathBuf,
428 pub line: u32,
430 pub col: u32,
432 pub role: String,
434 pub namespace: SemanticNamespace,
436 #[serde(default, skip_serializing_if = "Vec::is_empty")]
438 pub via: Vec<SemanticAliasHop>,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
443#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
444pub struct SemanticAliasHop {
445 #[serde(serialize_with = "serde_path::serialize")]
447 pub path: PathBuf,
448 pub from_name: String,
450 pub to_name: String,
452 pub relation: String,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
458#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
459pub struct SemanticSymbolTrace {
460 pub target: SemanticSymbol,
462 pub identity: SemanticAnalysisIdentity,
464 pub selected_project: String,
466 pub assertion: String,
468 pub status: SemanticCompleteness,
470 pub references: Vec<SemanticReference>,
472 pub total_reference_count: usize,
474 pub checker_evidence_count: usize,
476 pub graph_evidence_count: usize,
478 pub truncated: bool,
480 #[serde(default, skip_serializing_if = "Vec::is_empty")]
482 pub omissions: Vec<SemanticOmission>,
483 #[serde(default, skip_serializing_if = "Vec::is_empty")]
485 pub actions: Vec<String>,
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
490#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
491pub struct PublicTypeReference {
492 pub declaration: SemanticSymbol,
494 pub relation: String,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
501pub struct ApiSurfaceEntry {
502 pub exposed: SemanticSymbol,
504 pub origin: SemanticSymbol,
506 pub signature_fingerprint: String,
508 pub referenced_types: Vec<PublicTypeReference>,
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
514#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
515pub struct SemanticPrivateTypeLeak {
516 pub exposed: SemanticSymbol,
518 pub private_declaration: SemanticSymbol,
520 pub relation: String,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub diagnostic_code: Option<u32>,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
529#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
530pub struct ApiSurfaceResult {
531 pub assertion: String,
533 pub status: SemanticCompleteness,
535 pub entries: Vec<ApiSurfaceEntry>,
537 pub private_type_leaks: Vec<SemanticPrivateTypeLeak>,
539 #[serde(default, skip_serializing_if = "Vec::is_empty")]
541 pub omissions: Vec<SemanticOmission>,
542 #[serde(default, skip_serializing_if = "Vec::is_empty")]
544 pub actions: Vec<String>,
545}
546
547#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
549#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
550pub struct SemanticImpactPath {
551 #[serde(serialize_with = "serde_path::serialize")]
553 pub path: PathBuf,
554 pub relation: String,
556 pub distance: usize,
558 #[serde(default, skip_serializing_if = "Vec::is_empty")]
560 #[serde(serialize_with = "serde_path::serialize_vec")]
561 pub via: Vec<PathBuf>,
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
566#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
567#[serde(rename_all = "kebab-case")]
568pub enum SemanticImpactConfidence {
569 High,
572 Bounded,
575 Unavailable,
577}
578
579impl std::fmt::Display for SemanticImpactConfidence {
580 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 formatter.write_str(match self {
582 Self::High => "high",
583 Self::Bounded => "bounded",
584 Self::Unavailable => "unavailable",
585 })
586 }
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
592pub struct SemanticSymbolImpact {
593 pub target: SemanticSymbol,
595 pub identity: SemanticAnalysisIdentity,
597 pub selected_project: String,
599 pub assertion: String,
601 pub status: SemanticCompleteness,
603 pub direct_consumers: Vec<SemanticImpactPath>,
605 pub total_direct_consumer_count: usize,
607 pub affected_files: Vec<SemanticImpactPath>,
609 pub total_affected_file_count: usize,
611 pub targeted_tests: Vec<SemanticImpactPath>,
613 pub total_targeted_test_count: usize,
615 pub confidence: SemanticImpactConfidence,
617 #[serde(default, skip_serializing_if = "Vec::is_empty")]
619 pub omissions: Vec<SemanticOmission>,
620 #[serde(default, skip_serializing_if = "Vec::is_empty")]
622 pub actions: Vec<String>,
623}
624
625#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
627#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
628pub struct TypeCouplingEdge {
629 pub source: SemanticSymbol,
631 pub target: SemanticSymbol,
633 pub relation: String,
635 pub evidence: SemanticSourceLocation,
637 pub scope: String,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
643#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
644pub struct TypeCouplingFile {
645 #[serde(serialize_with = "serde_path::serialize")]
647 pub path: PathBuf,
648 pub public_api_depends_on: usize,
650 #[serde(default, skip_serializing_if = "Vec::is_empty")]
652 #[serde(serialize_with = "serde_path::serialize_vec")]
653 pub public_api_depends_on_files: Vec<PathBuf>,
654 pub public_types_used_by: usize,
656 #[serde(default, skip_serializing_if = "Vec::is_empty")]
658 #[serde(serialize_with = "serde_path::serialize_vec")]
659 pub public_types_used_by_files: Vec<PathBuf>,
660 pub edges: Vec<TypeCouplingEdge>,
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
666#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
667pub struct TypeCouplingCycle {
668 #[serde(serialize_with = "serde_path::serialize_vec")]
670 pub files: Vec<PathBuf>,
671}
672
673#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
676pub struct TypeCouplingSummary {
677 pub scope: String,
679 pub direction: String,
681 pub project_size: usize,
683 pub files_analyzed: usize,
685 pub distinct_coupled_files: usize,
687 pub edge_count: usize,
689 pub coupled_file_pct: f64,
691 pub p50_distinct_connections: f64,
693 pub p90_distinct_connections: f64,
695 pub p95_public_types_used_by: f64,
697 pub p95_public_api_depends_on: f64,
699 pub high_coupling_pct: f64,
701 pub concentration: f64,
703 pub cycle_count: usize,
705}
706
707#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
709#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
710pub struct TypeCouplingReport {
711 pub identity: SemanticAnalysisIdentity,
713 pub assertion: String,
715 pub status: SemanticCompleteness,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub summary: Option<TypeCouplingSummary>,
720 pub files: Vec<TypeCouplingFile>,
722 #[serde(default, skip_serializing_if = "Vec::is_empty")]
724 pub top_contributors: Vec<TypeCouplingFile>,
725 #[serde(default, skip_serializing_if = "Vec::is_empty")]
727 pub cycles: Vec<TypeCouplingCycle>,
728 #[serde(default, skip_serializing_if = "Vec::is_empty")]
730 pub omissions: Vec<SemanticOmission>,
731 #[serde(default, skip_serializing_if = "Vec::is_empty")]
733 pub actions: Vec<String>,
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739
740 #[test]
741 fn semantic_identity_reports_each_compatibility_dimension() {
742 let syntactic = SemanticAnalysisIdentity::syntactic();
743 assert!(syntactic.incompatible_fields(&syntactic).is_empty());
744
745 let type_aware = SemanticAnalysisIdentity {
746 mode: SemanticAnalysisMode::TypeAware,
747 semantic_schema_version: 2,
748 capabilities: vec![SemanticCapability::SymbolUse],
749 project_config_hash: "sha256:project".to_string(),
750 backend_family: "typescript-go".to_string(),
751 completeness: SemanticCompleteness::Partial,
752 };
753 assert_eq!(
754 syntactic.incompatible_fields(&type_aware),
755 vec![
756 "mode",
757 "semantic_schema_version",
758 "capabilities",
759 "project_config_hash",
760 "backend_family",
761 "completeness",
762 ]
763 );
764
765 let mut deferred = type_aware.clone();
766 deferred.project_config_hash = DEFERRED_PROJECT_CONFIG_HASH.to_string();
767 assert!(
768 deferred
769 .incompatible_fields(&type_aware)
770 .iter()
771 .all(|field| *field != "project_config_hash")
772 );
773 }
774}