Skip to main content

fallow_types/
semantic.rs

1//! Shared contracts for opt-in TypeScript semantic analysis.
2//!
3//! These types describe project-wide evidence that complements Fallow's
4//! syntactic graph. They do not model TypeScript compiler diagnostics or
5//! generic typed lint findings.
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::serde_path;
12
13/// Semantic capabilities that can share one TypeScript Program session.
14#[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    /// Confirm exact project-wide symbol use for an existing finding.
19    SymbolUse,
20    /// Explain exact declarations, references, aliases, and re-exports.
21    SymbolTrace,
22    /// Describe package-public signatures and private type leaks.
23    ApiSurface,
24    /// Find exact symbol consumers, affected files, and targeted tests.
25    SymbolImpact,
26    /// Measure project-local public-signature coupling.
27    TypeCoupling,
28}
29
30/// Effective policy applied when semantic evidence is incomplete.
31#[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    /// Keep incomplete semantic evidence advisory.
36    #[default]
37    BestEffort,
38    /// Make incomplete semantic evidence fail the command.
39    Complete,
40}
41
42/// Whether the semantic backend answered every requested query safely.
43#[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    /// Every requested query completed without omissions.
48    Complete,
49    /// Some evidence is valid, but bounded or unsupported relations remain.
50    Partial,
51    /// No safe semantic assertion could be made.
52    #[default]
53    Unavailable,
54}
55
56/// Analysis mode stored with baselines, snapshots, audit sides, and impact data.
57#[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    /// Normal Fallow analysis with no TypeScript semantic backend.
62    #[default]
63    Syntactic,
64    /// Opt-in analysis with one or more semantic capabilities.
65    TypeAware,
66}
67
68/// Compatibility identity for comparing two analysis results.
69#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
70#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
71pub struct SemanticAnalysisIdentity {
72    /// Syntactic or type-aware analysis mode.
73    pub mode: SemanticAnalysisMode,
74    /// Version of the semantic result schema, independent of tool versions.
75    pub semantic_schema_version: u32,
76    /// Sorted capability set requested for the analysis.
77    pub capabilities: Vec<SemanticCapability>,
78    /// Hash of normalized project ownership and compiler configuration.
79    pub project_config_hash: String,
80    /// Backend family, such as `typescript-go`.
81    pub backend_family: String,
82    /// Completeness of the resulting semantic analysis.
83    pub completeness: SemanticCompleteness,
84}
85
86/// Project-config identity used only when no semantic query was needed.
87///
88/// A clean run has no checker evidence whose compatibility depends on a
89/// concrete TypeScript project. Stored comparisons therefore treat this value
90/// as deferred until a later run has an actual semantic candidate.
91pub 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    /// Identity used by legacy and current analyses that did not request the
108    /// optional TypeScript semantic backend.
109    #[must_use]
110    pub fn syntactic() -> Self {
111        Self::default()
112    }
113
114    /// Name the compatibility fields that differ between two stored results.
115    /// Tool, package, protocol, and exact backend versions are provenance and
116    /// therefore intentionally excluded.
117    #[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/// Value or type namespace for one exact declaration or reference.
145#[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    /// Runtime value namespace.
150    #[default]
151    Value,
152    /// Type-only namespace.
153    Type,
154}
155
156/// Stable identity for a declaration sent to or returned by the backend.
157#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159pub struct SemanticSymbol {
160    /// Project-root-relative declaration path.
161    #[serde(serialize_with = "serde_path::serialize")]
162    pub path: PathBuf,
163    /// Value or type namespace.
164    pub namespace: SemanticNamespace,
165    /// Stable declaration kind, such as `function` or `class-method`.
166    pub declaration_kind: String,
167    /// Name exposed to consumers.
168    pub exported_name: String,
169    /// Local declaration name.
170    pub local_name: String,
171    /// Optional owning class or namespace.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub owner: Option<String>,
174    /// One-based declaration line.
175    pub line: u32,
176    /// Zero-based UTF-8 byte column.
177    pub col: u32,
178}
179
180/// One project-root-relative source location used as semantic evidence.
181#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
182#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
183pub struct SemanticSourceLocation {
184    /// Project-root-relative source path.
185    #[serde(serialize_with = "serde_path::serialize")]
186    pub path: PathBuf,
187    /// One-based source line.
188    pub line: u32,
189    /// Zero-based UTF-8 byte column.
190    pub col: u32,
191}
192
193/// Stable reason why semantic evidence is partial or unavailable.
194#[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    /// No selected TypeScript project owns the target.
199    NoProject,
200    /// More than one owning project produced incompatible declaration evidence.
201    AmbiguousProject,
202    /// Structural diagnostics make the selected project unsafe to query.
203    BlockingDiagnostics,
204    /// Named exports from a Svelte virtual module are unavailable without a
205    /// framework-aware TypeScript-Go host.
206    SvelteVirtualModuleExports,
207    /// The exact declaration could not be resolved.
208    UnknownSymbol,
209    /// A requested package-public entry point could not be resolved.
210    UnknownEntryPoint,
211    /// Evidence was truncated at the configured limit.
212    EvidenceLimit,
213    /// Dynamic runtime behavior is outside checker-visible semantics.
214    DynamicBehavior,
215    /// Interface, inherited, or virtual dispatch may reach an implementation
216    /// without referencing that concrete method symbol.
217    VirtualDispatch,
218    /// A computed or reflective member access can address the declaration.
219    DynamicMemberAccess,
220    /// A decorator can consume the declaration outside normal references.
221    DecoratedDeclaration,
222    /// An optional interface or inherited contract makes deletion unsafe.
223    OptionalContract,
224    /// A getter or setter has a paired accessor that must be changed atomically.
225    AccessorPair,
226    /// The declaration participates in a method overload set.
227    OverloadSet,
228    /// Source comments are attached to the declaration and must be reviewed.
229    AttachedComment,
230    /// The candidate itself declares an abstract contract.
231    AbstractDeclaration,
232    /// Not every project that owns the declaration completed the query.
233    IncompleteProjectCoverage,
234    /// An external framework declaration could not be attributed to an exact
235    /// package.
236    FrameworkContractProvenance,
237    /// A configured request or response capacity was reached.
238    Capacity,
239    /// The requested syntax or declaration kind is unsupported.
240    UnsupportedSyntax,
241}
242
243/// Counted omission attached to a partial or unavailable result.
244#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
246pub struct SemanticOmission {
247    /// Stable omission reason.
248    pub reason_code: SemanticGapReason,
249    /// Number of omitted items or relations.
250    pub count: usize,
251}
252
253/// Conservative outcome for one existing Fallow dead-code candidate.
254#[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    /// The checker resolved at least one exact static reference.
259    ConfirmedUsed,
260    /// Removing the declaration would change inherited behavior or an implemented contract.
261    ContractPreserved,
262    /// Complete closed-world analysis found no checker-resolved references.
263    ConfirmedNoStaticReferences,
264    /// Analysis deliberately declined a closed-world assertion.
265    RetainedAbstained,
266    /// The exact declaration or owning project could not be resolved.
267    RetainedUnresolved,
268}
269
270/// How a class member participates in an inherited or implemented relation.
271#[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    /// A required member declared by an implemented interface.
276    InterfaceImplementation,
277    /// An abstract base member implemented by the candidate.
278    AbstractImplementation,
279    /// A concrete inherited member overridden by the candidate.
280    Override,
281    /// An optional inherited or interface member that still blocks auto-fix.
282    OptionalContract,
283}
284
285/// Exact declaration evidence for an inherited or implemented class-member relation.
286#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
287#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
288pub struct SemanticContractEvidence {
289    /// Contract relation that makes deletion unsafe.
290    pub relation: SemanticContractRelation,
291    /// Exact interface or base-class declaration.
292    pub declaration: SemanticSymbol,
293    /// Whether the source contract marks this member optional.
294    pub optional: bool,
295}
296
297/// Heritage relation used by a framework-owned class-member contract.
298#[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    /// The class extends a framework base class.
303    Extends,
304    /// The class implements a framework interface.
305    Implements,
306}
307
308/// Exact framework contract supplied by a detected Fallow plugin.
309#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
310#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
311pub struct SemanticFrameworkContract {
312    /// Fallow plugin that supplied the contract.
313    pub framework: String,
314    /// Package that must own the resolved heritage declaration.
315    pub package: String,
316    /// Exported base class or interface name.
317    pub heritage_symbol: String,
318    /// Syntactic spellings accepted only for surfacing a latent candidate.
319    pub heritage_names: Vec<String>,
320    /// Extends or implements relation.
321    pub relation: SemanticFrameworkRelation,
322    /// Framework-dispatched members covered by this contract.
323    pub members: Vec<String>,
324}
325
326/// Checker-validated evidence that a framework contract preserves a member.
327#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
328#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
329pub struct SemanticFrameworkContractEvidence {
330    /// Fallow plugin that supplied the contract.
331    pub framework: String,
332    /// Exact package that owns the heritage declaration.
333    pub package: String,
334    /// Extends or implements relation.
335    pub relation: SemanticFrameworkRelation,
336    /// Exact framework base class or interface declaration.
337    pub declaration: SemanticSymbol,
338}
339
340/// Exact source span and content hash used to guard semantic source edits.
341#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
343pub struct SemanticEditGuard {
344    /// Zero-based UTF-8 byte offset where the declaration starts.
345    pub start: usize,
346    /// Exclusive zero-based UTF-8 byte offset where the declaration ends.
347    pub end: usize,
348    /// Lowercase SHA-256 digest of the exact declaration text.
349    pub declaration_sha256: String,
350}
351
352/// Bounded, inspectable decision record for one semantic dead-code candidate.
353#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
354#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
355pub struct SemanticCandidateDecision {
356    /// Query identifier used to correlate low-level query metadata.
357    pub query_id: usize,
358    /// Exact candidate declaration.
359    pub subject: SemanticSymbol,
360    /// Conservative Fallow-owned decision.
361    pub decision: SemanticCandidateDecisionKind,
362    /// Completeness of the supporting semantic evidence.
363    pub status: SemanticCompleteness,
364    /// Every selected TypeScript project that owns the declaration.
365    pub owning_projects: Vec<String>,
366    /// Bounded checker-resolved reference or uncertainty evidence.
367    #[serde(default, skip_serializing_if = "Vec::is_empty")]
368    pub evidence: Vec<SemanticReference>,
369    /// Inherited contract evidence, when present.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub contract: Option<SemanticContractEvidence>,
372    /// Framework-owned contract evidence, distinct from TypeScript contracts.
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub framework_contract: Option<SemanticFrameworkContractEvidence>,
375    /// Whether this exact decision may enable a guarded class-member fix.
376    pub closed_world_eligible: bool,
377    /// Exact declaration guard required before a source edit.
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub edit_guard: Option<SemanticEditGuard>,
380    /// Primary reason when the candidate remains unresolved or abstained.
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub reason_code: Option<SemanticGapReason>,
383    /// Concise explanation suitable for dry-run and agent output.
384    pub explanation: String,
385    /// Plain next actions.
386    #[serde(default, skip_serializing_if = "Vec::is_empty")]
387    pub actions: Vec<String>,
388    /// Evidence count before bounding.
389    pub total_evidence_count: usize,
390    /// Whether evidence was truncated.
391    pub truncated: bool,
392    /// Counted omissions.
393    #[serde(default, skip_serializing_if = "Vec::is_empty")]
394    pub omissions: Vec<SemanticOmission>,
395}
396
397/// Compact per-query status embedded in run metadata.
398#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
399#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
400pub struct SemanticQuerySummary {
401    /// Stable query identifier within this analysis run.
402    pub query_id: usize,
403    /// Capability that answered the query.
404    pub capability: SemanticCapability,
405    /// Operation-specific assertion, never a generic compiler verdict.
406    pub assertion: String,
407    /// Completeness of this query.
408    pub status: SemanticCompleteness,
409    /// Stable primary gap reason when partial or unavailable.
410    #[serde(default, skip_serializing_if = "Option::is_none")]
411    pub reason_code: Option<SemanticGapReason>,
412    /// Evidence count before bounding.
413    pub total_evidence_count: usize,
414    /// Whether evidence or payload arrays were truncated.
415    pub truncated: bool,
416    /// Counted omissions.
417    #[serde(default, skip_serializing_if = "Vec::is_empty")]
418    pub omissions: Vec<SemanticOmission>,
419    /// Plain next actions.
420    #[serde(default, skip_serializing_if = "Vec::is_empty")]
421    pub actions: Vec<String>,
422}
423
424/// Located semantic reference evidence.
425#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
426#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
427pub struct SemanticReference {
428    /// Project-root-relative reference path.
429    #[serde(serialize_with = "serde_path::serialize")]
430    pub path: PathBuf,
431    /// One-based source line.
432    pub line: u32,
433    /// Zero-based UTF-8 byte column.
434    pub col: u32,
435    /// Reference role, such as `read`, `type`, `alias`, or `re-export`.
436    pub role: String,
437    /// Value or type namespace used at this location.
438    pub namespace: SemanticNamespace,
439    /// Alias and re-export hops between the reference and declaration.
440    #[serde(default, skip_serializing_if = "Vec::is_empty")]
441    pub via: Vec<SemanticAliasHop>,
442}
443
444/// One alias or re-export hop in semantic provenance.
445#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
446#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
447pub struct SemanticAliasHop {
448    /// Project-root-relative hop path.
449    #[serde(serialize_with = "serde_path::serialize")]
450    pub path: PathBuf,
451    /// Name before this hop.
452    pub from_name: String,
453    /// Name exposed after this hop.
454    pub to_name: String,
455    /// Relation, such as `import-alias` or `re-export`.
456    pub relation: String,
457}
458
459/// Typed semantic trace attached to an existing syntactic trace.
460#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
461#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
462pub struct SemanticSymbolTrace {
463    /// Exact target declaration.
464    pub target: SemanticSymbol,
465    /// Semantic mode, capabilities, project selection, and completeness.
466    pub identity: SemanticAnalysisIdentity,
467    /// TypeScript project selected for this symbol.
468    pub selected_project: String,
469    /// Concrete assertion, such as `references-found`.
470    pub assertion: String,
471    /// Completeness of the trace.
472    pub status: SemanticCompleteness,
473    /// Bounded reference evidence.
474    pub references: Vec<SemanticReference>,
475    /// Count before evidence bounding.
476    pub total_reference_count: usize,
477    /// Exact reference locations found by the TypeScript checker.
478    pub checker_evidence_count: usize,
479    /// Alias and re-export hops derived from the semantic graph.
480    pub graph_evidence_count: usize,
481    /// Whether reference evidence was truncated.
482    pub truncated: bool,
483    /// Counted omissions.
484    #[serde(default, skip_serializing_if = "Vec::is_empty")]
485    pub omissions: Vec<SemanticOmission>,
486    /// Plain next actions for a user or automation consumer.
487    #[serde(default, skip_serializing_if = "Vec::is_empty")]
488    pub actions: Vec<String>,
489}
490
491/// One project-local type referenced by a public signature.
492#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
494pub struct PublicTypeReference {
495    /// Referenced declaration.
496    pub declaration: SemanticSymbol,
497    /// Signature relation, such as return type or generic constraint.
498    pub relation: String,
499}
500
501/// One package-public API entry described by the semantic backend.
502#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
503#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
504pub struct ApiSurfaceEntry {
505    /// Symbol exposed through a package entry point.
506    pub exposed: SemanticSymbol,
507    /// Canonical origin after aliases and re-exports.
508    pub origin: SemanticSymbol,
509    /// Stable normalized signature fingerprint.
510    pub signature_fingerprint: String,
511    /// Project-local types referenced by the signature.
512    pub referenced_types: Vec<PublicTypeReference>,
513}
514
515/// Exact semantic evidence for a private type leak.
516#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
518pub struct SemanticPrivateTypeLeak {
519    /// Public symbol whose signature exposes the type.
520    pub exposed: SemanticSymbol,
521    /// Project-local declaration that is not package-public.
522    pub private_declaration: SemanticSymbol,
523    /// Signature relation through which the type is exposed.
524    pub relation: String,
525    /// Stable TypeScript diagnostic code used as supporting evidence.
526    #[serde(default, skip_serializing_if = "Option::is_none")]
527    pub diagnostic_code: Option<u32>,
528}
529
530/// Package API surface result shared by inspect and private-leak analysis.
531#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
532#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
533pub struct ApiSurfaceResult {
534    /// Concrete assertion, such as `leak-confirmed`.
535    pub assertion: String,
536    /// Completeness of package-public traversal.
537    pub status: SemanticCompleteness,
538    /// Public API entries.
539    pub entries: Vec<ApiSurfaceEntry>,
540    /// Confirmed private type leaks.
541    pub private_type_leaks: Vec<SemanticPrivateTypeLeak>,
542    /// Counted omissions.
543    #[serde(default, skip_serializing_if = "Vec::is_empty")]
544    pub omissions: Vec<SemanticOmission>,
545    /// Plain next actions.
546    #[serde(default, skip_serializing_if = "Vec::is_empty")]
547    pub actions: Vec<String>,
548}
549
550/// One production file or test reached by exact-symbol impact analysis.
551#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
553pub struct SemanticImpactPath {
554    /// Project-root-relative affected path.
555    #[serde(serialize_with = "serde_path::serialize")]
556    pub path: PathBuf,
557    /// Relation to the target, such as `direct-value-consumer`.
558    pub relation: String,
559    /// Shortest graph distance from the target.
560    pub distance: usize,
561    /// Located provenance path.
562    #[serde(default, skip_serializing_if = "Vec::is_empty")]
563    #[serde(serialize_with = "serde_path::serialize_vec")]
564    pub via: Vec<PathBuf>,
565}
566
567/// Confidence of exact-symbol impact analysis after known dynamic gaps.
568#[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    /// All reported static paths are complete within the selected project
573    /// scope.
574    High,
575    /// Static paths are useful, but virtual dispatch or dynamic behavior
576    /// bounds completeness.
577    Bounded,
578    /// Impact analysis could not run.
579    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/// Exact-symbol impact and targeted-test recommendation.
593#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
595pub struct SemanticSymbolImpact {
596    /// Exact target declaration.
597    pub target: SemanticSymbol,
598    /// Semantic mode, capabilities, project selection, and completeness.
599    pub identity: SemanticAnalysisIdentity,
600    /// TypeScript project selected for this symbol.
601    pub selected_project: String,
602    /// Concrete assertion, such as `consumers-found`.
603    pub assertion: String,
604    /// Completeness of impact analysis.
605    pub status: SemanticCompleteness,
606    /// Files that reference the exact symbol directly.
607    pub direct_consumers: Vec<SemanticImpactPath>,
608    /// Direct consumer count before evidence bounding.
609    pub total_direct_consumer_count: usize,
610    /// Transitively affected production files.
611    pub affected_files: Vec<SemanticImpactPath>,
612    /// Transitive affected-file count before evidence bounding.
613    pub total_affected_file_count: usize,
614    /// Directly relevant test entry points.
615    pub targeted_tests: Vec<SemanticImpactPath>,
616    /// Targeted-test count before evidence bounding.
617    pub total_targeted_test_count: usize,
618    /// Confidence after accounting for dynamic behavior.
619    pub confidence: SemanticImpactConfidence,
620    /// Counted omissions, including dynamic behavior.
621    #[serde(default, skip_serializing_if = "Vec::is_empty")]
622    pub omissions: Vec<SemanticOmission>,
623    /// Plain next actions.
624    #[serde(default, skip_serializing_if = "Vec::is_empty")]
625    pub actions: Vec<String>,
626}
627
628/// One project-local public-signature type edge.
629#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
631pub struct TypeCouplingEdge {
632    /// Public API declaration that owns the signature.
633    pub source: SemanticSymbol,
634    /// Project-local type used by that signature.
635    pub target: SemanticSymbol,
636    /// Signature relation.
637    pub relation: String,
638    /// Source location where the public signature references the target type.
639    pub evidence: SemanticSourceLocation,
640    /// Scope, such as `module-export` or `package-public`.
641    pub scope: String,
642}
643
644/// Per-file project-local public-signature coupling.
645#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
647pub struct TypeCouplingFile {
648    /// Project-root-relative file path.
649    #[serde(serialize_with = "serde_path::serialize")]
650    pub path: PathBuf,
651    /// Distinct files this file's public API depends on.
652    pub public_api_depends_on: usize,
653    /// Located project files this file's public API depends on.
654    #[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    /// Distinct files whose public types use this file.
658    pub public_types_used_by: usize,
659    /// Located project files whose public types use this file.
660    #[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    /// Located public-signature edges.
664    pub edges: Vec<TypeCouplingEdge>,
665}
666
667/// One project-local cycle through public-signature type dependencies.
668#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
669#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
670pub struct TypeCouplingCycle {
671    /// Ordered project-root-relative files, ending at the start file.
672    #[serde(serialize_with = "serde_path::serialize_vec")]
673    pub files: Vec<PathBuf>,
674}
675
676/// Project summary for advisory type coupling.
677#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
678#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
679pub struct TypeCouplingSummary {
680    /// Measurement boundary, currently project-local public signatures.
681    pub scope: String,
682    /// Edge direction, currently directed.
683    pub direction: String,
684    /// Distinct project files in the selected TypeScript projects.
685    pub project_size: usize,
686    /// Distinct project files included in the denominator.
687    pub files_analyzed: usize,
688    /// Files participating in at least one project-local type edge.
689    pub distinct_coupled_files: usize,
690    /// Project-local public-signature edge count before evidence bounding.
691    pub edge_count: usize,
692    /// Percentage of analyzed files participating in a type edge.
693    pub coupled_file_pct: f64,
694    /// Median distinct-file type connections.
695    pub p50_distinct_connections: f64,
696    /// P90 distinct-file type connections.
697    pub p90_distinct_connections: f64,
698    /// P95 incoming distinct-file type coupling.
699    pub p95_public_types_used_by: f64,
700    /// P95 outgoing distinct-file type coupling.
701    pub p95_public_api_depends_on: f64,
702    /// Percentage of files above the adaptive high-coupling threshold.
703    pub high_coupling_pct: f64,
704    /// Share of edge endpoints represented by the top contributors.
705    pub concentration: f64,
706    /// Number of project-local public-signature cycles.
707    pub cycle_count: usize,
708}
709
710/// Advisory project-local public-signature coupling report.
711#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
712#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
713pub struct TypeCouplingReport {
714    /// Semantic mode, capability, project selection, and completeness.
715    pub identity: SemanticAnalysisIdentity,
716    /// Concrete assertion, such as `coupling-found`.
717    pub assertion: String,
718    /// Completeness of coupling traversal.
719    pub status: SemanticCompleteness,
720    /// Project summary. Absent when analysis is unavailable, never a fake zero.
721    #[serde(default, skip_serializing_if = "Option::is_none")]
722    pub summary: Option<TypeCouplingSummary>,
723    /// Per-file coupling details.
724    pub files: Vec<TypeCouplingFile>,
725    /// Highest-degree files contributing to project coupling.
726    #[serde(default, skip_serializing_if = "Vec::is_empty")]
727    pub top_contributors: Vec<TypeCouplingFile>,
728    /// Located project-local type cycles.
729    #[serde(default, skip_serializing_if = "Vec::is_empty")]
730    pub cycles: Vec<TypeCouplingCycle>,
731    /// Counted omissions.
732    #[serde(default, skip_serializing_if = "Vec::is_empty")]
733    pub omissions: Vec<SemanticOmission>,
734    /// Plain next actions.
735    #[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}