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 SvelteVirtualModuleExports,
207 UnknownSymbol,
209 UnknownEntryPoint,
211 EvidenceLimit,
213 DynamicBehavior,
215 VirtualDispatch,
218 DynamicMemberAccess,
220 DecoratedDeclaration,
222 OptionalContract,
224 AccessorPair,
226 OverloadSet,
228 AttachedComment,
230 AbstractDeclaration,
232 IncompleteProjectCoverage,
234 FrameworkContractProvenance,
237 Capacity,
239 UnsupportedSyntax,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
246pub struct SemanticOmission {
247 pub reason_code: SemanticGapReason,
249 pub count: usize,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
255#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
256#[serde(rename_all = "kebab-case")]
257pub enum SemanticCandidateDecisionKind {
258 ConfirmedUsed,
260 ContractPreserved,
262 ConfirmedNoStaticReferences,
264 RetainedAbstained,
266 RetainedUnresolved,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
273#[serde(rename_all = "kebab-case")]
274pub enum SemanticContractRelation {
275 InterfaceImplementation,
277 AbstractImplementation,
279 Override,
281 OptionalContract,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
287#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
288pub struct SemanticContractEvidence {
289 pub relation: SemanticContractRelation,
291 pub declaration: SemanticSymbol,
293 pub optional: bool,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
299#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
300#[serde(rename_all = "kebab-case")]
301pub enum SemanticFrameworkRelation {
302 Extends,
304 Implements,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
310#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
311pub struct SemanticFrameworkContract {
312 pub framework: String,
314 pub package: String,
316 pub heritage_symbol: String,
318 pub heritage_names: Vec<String>,
320 pub relation: SemanticFrameworkRelation,
322 pub members: Vec<String>,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
328#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
329pub struct SemanticFrameworkContractEvidence {
330 pub framework: String,
332 pub package: String,
334 pub relation: SemanticFrameworkRelation,
336 pub declaration: SemanticSymbol,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
343pub struct SemanticEditGuard {
344 pub start: usize,
346 pub end: usize,
348 pub declaration_sha256: String,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
354#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
355pub struct SemanticCandidateDecision {
356 pub query_id: usize,
358 pub subject: SemanticSymbol,
360 pub decision: SemanticCandidateDecisionKind,
362 pub status: SemanticCompleteness,
364 pub owning_projects: Vec<String>,
366 #[serde(default, skip_serializing_if = "Vec::is_empty")]
368 pub evidence: Vec<SemanticReference>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub contract: Option<SemanticContractEvidence>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub framework_contract: Option<SemanticFrameworkContractEvidence>,
375 pub closed_world_eligible: bool,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub edit_guard: Option<SemanticEditGuard>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub reason_code: Option<SemanticGapReason>,
383 pub explanation: String,
385 #[serde(default, skip_serializing_if = "Vec::is_empty")]
387 pub actions: Vec<String>,
388 pub total_evidence_count: usize,
390 pub truncated: bool,
392 #[serde(default, skip_serializing_if = "Vec::is_empty")]
394 pub omissions: Vec<SemanticOmission>,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
399#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
400pub struct SemanticQuerySummary {
401 pub query_id: usize,
403 pub capability: SemanticCapability,
405 pub assertion: String,
407 pub status: SemanticCompleteness,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub reason_code: Option<SemanticGapReason>,
412 pub total_evidence_count: usize,
414 pub truncated: bool,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub omissions: Vec<SemanticOmission>,
419 #[serde(default, skip_serializing_if = "Vec::is_empty")]
421 pub actions: Vec<String>,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
426#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
427pub struct SemanticReference {
428 #[serde(serialize_with = "serde_path::serialize")]
430 pub path: PathBuf,
431 pub line: u32,
433 pub col: u32,
435 pub role: String,
437 pub namespace: SemanticNamespace,
439 #[serde(default, skip_serializing_if = "Vec::is_empty")]
441 pub via: Vec<SemanticAliasHop>,
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
446#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
447pub struct SemanticAliasHop {
448 #[serde(serialize_with = "serde_path::serialize")]
450 pub path: PathBuf,
451 pub from_name: String,
453 pub to_name: String,
455 pub relation: String,
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
461#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
462pub struct SemanticSymbolTrace {
463 pub target: SemanticSymbol,
465 pub identity: SemanticAnalysisIdentity,
467 pub selected_project: String,
469 pub assertion: String,
471 pub status: SemanticCompleteness,
473 pub references: Vec<SemanticReference>,
475 pub total_reference_count: usize,
477 pub checker_evidence_count: usize,
479 pub graph_evidence_count: usize,
481 pub truncated: bool,
483 #[serde(default, skip_serializing_if = "Vec::is_empty")]
485 pub omissions: Vec<SemanticOmission>,
486 #[serde(default, skip_serializing_if = "Vec::is_empty")]
488 pub actions: Vec<String>,
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
494pub struct PublicTypeReference {
495 pub declaration: SemanticSymbol,
497 pub relation: String,
499}
500
501#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
503#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
504pub struct ApiSurfaceEntry {
505 pub exposed: SemanticSymbol,
507 pub origin: SemanticSymbol,
509 pub signature_fingerprint: String,
511 pub referenced_types: Vec<PublicTypeReference>,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
518pub struct SemanticPrivateTypeLeak {
519 pub exposed: SemanticSymbol,
521 pub private_declaration: SemanticSymbol,
523 pub relation: String,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub diagnostic_code: Option<u32>,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
532#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
533pub struct ApiSurfaceResult {
534 pub assertion: String,
536 pub status: SemanticCompleteness,
538 pub entries: Vec<ApiSurfaceEntry>,
540 pub private_type_leaks: Vec<SemanticPrivateTypeLeak>,
542 #[serde(default, skip_serializing_if = "Vec::is_empty")]
544 pub omissions: Vec<SemanticOmission>,
545 #[serde(default, skip_serializing_if = "Vec::is_empty")]
547 pub actions: Vec<String>,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
553pub struct SemanticImpactPath {
554 #[serde(serialize_with = "serde_path::serialize")]
556 pub path: PathBuf,
557 pub relation: String,
559 pub distance: usize,
561 #[serde(default, skip_serializing_if = "Vec::is_empty")]
563 #[serde(serialize_with = "serde_path::serialize_vec")]
564 pub via: Vec<PathBuf>,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
570#[serde(rename_all = "kebab-case")]
571pub enum SemanticImpactConfidence {
572 High,
575 Bounded,
578 Unavailable,
580}
581
582impl std::fmt::Display for SemanticImpactConfidence {
583 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584 formatter.write_str(match self {
585 Self::High => "high",
586 Self::Bounded => "bounded",
587 Self::Unavailable => "unavailable",
588 })
589 }
590}
591
592#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
595pub struct SemanticSymbolImpact {
596 pub target: SemanticSymbol,
598 pub identity: SemanticAnalysisIdentity,
600 pub selected_project: String,
602 pub assertion: String,
604 pub status: SemanticCompleteness,
606 pub direct_consumers: Vec<SemanticImpactPath>,
608 pub total_direct_consumer_count: usize,
610 pub affected_files: Vec<SemanticImpactPath>,
612 pub total_affected_file_count: usize,
614 pub targeted_tests: Vec<SemanticImpactPath>,
616 pub total_targeted_test_count: usize,
618 pub confidence: SemanticImpactConfidence,
620 #[serde(default, skip_serializing_if = "Vec::is_empty")]
622 pub omissions: Vec<SemanticOmission>,
623 #[serde(default, skip_serializing_if = "Vec::is_empty")]
625 pub actions: Vec<String>,
626}
627
628#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
631pub struct TypeCouplingEdge {
632 pub source: SemanticSymbol,
634 pub target: SemanticSymbol,
636 pub relation: String,
638 pub evidence: SemanticSourceLocation,
640 pub scope: String,
642}
643
644#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
647pub struct TypeCouplingFile {
648 #[serde(serialize_with = "serde_path::serialize")]
650 pub path: PathBuf,
651 pub public_api_depends_on: usize,
653 #[serde(default, skip_serializing_if = "Vec::is_empty")]
655 #[serde(serialize_with = "serde_path::serialize_vec")]
656 pub public_api_depends_on_files: Vec<PathBuf>,
657 pub public_types_used_by: usize,
659 #[serde(default, skip_serializing_if = "Vec::is_empty")]
661 #[serde(serialize_with = "serde_path::serialize_vec")]
662 pub public_types_used_by_files: Vec<PathBuf>,
663 pub edges: Vec<TypeCouplingEdge>,
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
669#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
670pub struct TypeCouplingCycle {
671 #[serde(serialize_with = "serde_path::serialize_vec")]
673 pub files: Vec<PathBuf>,
674}
675
676#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
678#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
679pub struct TypeCouplingSummary {
680 pub scope: String,
682 pub direction: String,
684 pub project_size: usize,
686 pub files_analyzed: usize,
688 pub distinct_coupled_files: usize,
690 pub edge_count: usize,
692 pub coupled_file_pct: f64,
694 pub p50_distinct_connections: f64,
696 pub p90_distinct_connections: f64,
698 pub p95_public_types_used_by: f64,
700 pub p95_public_api_depends_on: f64,
702 pub high_coupling_pct: f64,
704 pub concentration: f64,
706 pub cycle_count: usize,
708}
709
710#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
712#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
713pub struct TypeCouplingReport {
714 pub identity: SemanticAnalysisIdentity,
716 pub assertion: String,
718 pub status: SemanticCompleteness,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub summary: Option<TypeCouplingSummary>,
723 pub files: Vec<TypeCouplingFile>,
725 #[serde(default, skip_serializing_if = "Vec::is_empty")]
727 pub top_contributors: Vec<TypeCouplingFile>,
728 #[serde(default, skip_serializing_if = "Vec::is_empty")]
730 pub cycles: Vec<TypeCouplingCycle>,
731 #[serde(default, skip_serializing_if = "Vec::is_empty")]
733 pub omissions: Vec<SemanticOmission>,
734 #[serde(default, skip_serializing_if = "Vec::is_empty")]
736 pub actions: Vec<String>,
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742
743 #[test]
744 fn semantic_identity_reports_each_compatibility_dimension() {
745 let syntactic = SemanticAnalysisIdentity::syntactic();
746 assert!(syntactic.incompatible_fields(&syntactic).is_empty());
747
748 let type_aware = SemanticAnalysisIdentity {
749 mode: SemanticAnalysisMode::TypeAware,
750 semantic_schema_version: 2,
751 capabilities: vec![SemanticCapability::SymbolUse],
752 project_config_hash: "sha256:project".to_string(),
753 backend_family: "typescript-go".to_string(),
754 completeness: SemanticCompleteness::Partial,
755 };
756 assert_eq!(
757 syntactic.incompatible_fields(&type_aware),
758 vec![
759 "mode",
760 "semantic_schema_version",
761 "capabilities",
762 "project_config_hash",
763 "backend_family",
764 "completeness",
765 ]
766 );
767
768 let mut deferred = type_aware.clone();
769 deferred.project_config_hash = DEFERRED_PROJECT_CONFIG_HASH.to_string();
770 assert!(
771 deferred
772 .incompatible_fields(&type_aware)
773 .iter()
774 .all(|field| *field != "project_config_hash")
775 );
776 }
777}