use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::serde_path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticCapability {
SymbolUse,
SymbolTrace,
ApiSurface,
SymbolImpact,
TypeCoupling,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticCompletenessRequirement {
#[default]
BestEffort,
Complete,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticCompleteness {
Complete,
Partial,
#[default]
Unavailable,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticAnalysisMode {
#[default]
Syntactic,
TypeAware,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticAnalysisIdentity {
pub mode: SemanticAnalysisMode,
pub semantic_schema_version: u32,
pub capabilities: Vec<SemanticCapability>,
pub project_config_hash: String,
pub backend_family: String,
pub completeness: SemanticCompleteness,
}
pub const DEFERRED_PROJECT_CONFIG_HASH: &str = "deferred:no-semantic-queries";
impl Default for SemanticAnalysisIdentity {
fn default() -> Self {
Self {
mode: SemanticAnalysisMode::Syntactic,
semantic_schema_version: 1,
capabilities: Vec::new(),
project_config_hash: String::new(),
backend_family: String::new(),
completeness: SemanticCompleteness::Complete,
}
}
}
impl SemanticAnalysisIdentity {
#[must_use]
pub fn syntactic() -> Self {
Self::default()
}
#[must_use]
pub fn incompatible_fields(&self, other: &Self) -> Vec<&'static str> {
let mut fields = Vec::new();
if self.mode != other.mode {
fields.push("mode");
}
if self.semantic_schema_version != other.semantic_schema_version {
fields.push("semantic_schema_version");
}
if self.capabilities != other.capabilities {
fields.push("capabilities");
}
let project_hash_deferred = self.project_config_hash == DEFERRED_PROJECT_CONFIG_HASH
|| other.project_config_hash == DEFERRED_PROJECT_CONFIG_HASH;
if !project_hash_deferred && self.project_config_hash != other.project_config_hash {
fields.push("project_config_hash");
}
if self.backend_family != other.backend_family {
fields.push("backend_family");
}
if self.completeness != other.completeness {
fields.push("completeness");
}
fields
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticNamespace {
#[default]
Value,
Type,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticSymbol {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub namespace: SemanticNamespace,
pub declaration_kind: String,
pub exported_name: String,
pub local_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticSourceLocation {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticGapReason {
NoProject,
AmbiguousProject,
BlockingDiagnostics,
SvelteVirtualModuleExports,
UnknownSymbol,
UnknownEntryPoint,
EvidenceLimit,
DynamicBehavior,
VirtualDispatch,
DynamicMemberAccess,
DecoratedDeclaration,
OptionalContract,
AccessorPair,
OverloadSet,
AttachedComment,
AbstractDeclaration,
IncompleteProjectCoverage,
FrameworkContractProvenance,
Capacity,
UnsupportedSyntax,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticOmission {
pub reason_code: SemanticGapReason,
pub count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticCandidateDecisionKind {
ConfirmedUsed,
ContractPreserved,
ConfirmedNoStaticReferences,
RetainedAbstained,
RetainedUnresolved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticContractRelation {
InterfaceImplementation,
AbstractImplementation,
Override,
OptionalContract,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticContractEvidence {
pub relation: SemanticContractRelation,
pub declaration: SemanticSymbol,
pub optional: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticFrameworkRelation {
Extends,
Implements,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticFrameworkContract {
pub framework: String,
pub package: String,
pub heritage_symbol: String,
pub heritage_names: Vec<String>,
pub relation: SemanticFrameworkRelation,
pub members: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticFrameworkContractEvidence {
pub framework: String,
pub package: String,
pub relation: SemanticFrameworkRelation,
pub declaration: SemanticSymbol,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticEditGuard {
pub start: usize,
pub end: usize,
pub declaration_sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticCandidateDecision {
pub query_id: usize,
pub subject: SemanticSymbol,
pub decision: SemanticCandidateDecisionKind,
pub status: SemanticCompleteness,
pub owning_projects: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<SemanticReference>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contract: Option<SemanticContractEvidence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub framework_contract: Option<SemanticFrameworkContractEvidence>,
pub closed_world_eligible: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edit_guard: Option<SemanticEditGuard>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason_code: Option<SemanticGapReason>,
pub explanation: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
pub total_evidence_count: usize,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omissions: Vec<SemanticOmission>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticQuerySummary {
pub query_id: usize,
pub capability: SemanticCapability,
pub assertion: String,
pub status: SemanticCompleteness,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason_code: Option<SemanticGapReason>,
pub total_evidence_count: usize,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omissions: Vec<SemanticOmission>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticReference {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
pub col: u32,
pub role: String,
pub namespace: SemanticNamespace,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub via: Vec<SemanticAliasHop>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticAliasHop {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub from_name: String,
pub to_name: String,
pub relation: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticSymbolTrace {
pub target: SemanticSymbol,
pub identity: SemanticAnalysisIdentity,
pub selected_project: String,
pub assertion: String,
pub status: SemanticCompleteness,
pub references: Vec<SemanticReference>,
pub total_reference_count: usize,
pub checker_evidence_count: usize,
pub graph_evidence_count: usize,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omissions: Vec<SemanticOmission>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PublicTypeReference {
pub declaration: SemanticSymbol,
pub relation: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApiSurfaceEntry {
pub exposed: SemanticSymbol,
pub origin: SemanticSymbol,
pub signature_fingerprint: String,
pub referenced_types: Vec<PublicTypeReference>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticPrivateTypeLeak {
pub exposed: SemanticSymbol,
pub private_declaration: SemanticSymbol,
pub relation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diagnostic_code: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApiSurfaceResult {
pub assertion: String,
pub status: SemanticCompleteness,
pub entries: Vec<ApiSurfaceEntry>,
pub private_type_leaks: Vec<SemanticPrivateTypeLeak>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omissions: Vec<SemanticOmission>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticImpactPath {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub relation: String,
pub distance: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(serialize_with = "serde_path::serialize_vec")]
pub via: Vec<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SemanticImpactConfidence {
High,
Bounded,
Unavailable,
}
impl std::fmt::Display for SemanticImpactConfidence {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::High => "high",
Self::Bounded => "bounded",
Self::Unavailable => "unavailable",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SemanticSymbolImpact {
pub target: SemanticSymbol,
pub identity: SemanticAnalysisIdentity,
pub selected_project: String,
pub assertion: String,
pub status: SemanticCompleteness,
pub direct_consumers: Vec<SemanticImpactPath>,
pub total_direct_consumer_count: usize,
pub affected_files: Vec<SemanticImpactPath>,
pub total_affected_file_count: usize,
pub targeted_tests: Vec<SemanticImpactPath>,
pub total_targeted_test_count: usize,
pub confidence: SemanticImpactConfidence,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omissions: Vec<SemanticOmission>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TypeCouplingEdge {
pub source: SemanticSymbol,
pub target: SemanticSymbol,
pub relation: String,
pub evidence: SemanticSourceLocation,
pub scope: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TypeCouplingFile {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub public_api_depends_on: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(serialize_with = "serde_path::serialize_vec")]
pub public_api_depends_on_files: Vec<PathBuf>,
pub public_types_used_by: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(serialize_with = "serde_path::serialize_vec")]
pub public_types_used_by_files: Vec<PathBuf>,
pub edges: Vec<TypeCouplingEdge>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TypeCouplingCycle {
#[serde(serialize_with = "serde_path::serialize_vec")]
pub files: Vec<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TypeCouplingSummary {
pub scope: String,
pub direction: String,
pub project_size: usize,
pub files_analyzed: usize,
pub distinct_coupled_files: usize,
pub edge_count: usize,
pub coupled_file_pct: f64,
pub p50_distinct_connections: f64,
pub p90_distinct_connections: f64,
pub p95_public_types_used_by: f64,
pub p95_public_api_depends_on: f64,
pub high_coupling_pct: f64,
pub concentration: f64,
pub cycle_count: usize,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TypeCouplingReport {
pub identity: SemanticAnalysisIdentity,
pub assertion: String,
pub status: SemanticCompleteness,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<TypeCouplingSummary>,
pub files: Vec<TypeCouplingFile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub top_contributors: Vec<TypeCouplingFile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cycles: Vec<TypeCouplingCycle>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omissions: Vec<SemanticOmission>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantic_identity_reports_each_compatibility_dimension() {
let syntactic = SemanticAnalysisIdentity::syntactic();
assert!(syntactic.incompatible_fields(&syntactic).is_empty());
let type_aware = SemanticAnalysisIdentity {
mode: SemanticAnalysisMode::TypeAware,
semantic_schema_version: 2,
capabilities: vec![SemanticCapability::SymbolUse],
project_config_hash: "sha256:project".to_string(),
backend_family: "typescript-go".to_string(),
completeness: SemanticCompleteness::Partial,
};
assert_eq!(
syntactic.incompatible_fields(&type_aware),
vec![
"mode",
"semantic_schema_version",
"capabilities",
"project_config_hash",
"backend_family",
"completeness",
]
);
let mut deferred = type_aware.clone();
deferred.project_config_hash = DEFERRED_PROJECT_CONFIG_HASH.to_string();
assert!(
deferred
.incompatible_fields(&type_aware)
.iter()
.all(|field| *field != "project_config_hash")
);
}
}