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    /// The exact declaration could not be resolved.
205    UnknownSymbol,
206    /// A requested package-public entry point could not be resolved.
207    UnknownEntryPoint,
208    /// Evidence was truncated at the configured limit.
209    EvidenceLimit,
210    /// Dynamic runtime behavior is outside checker-visible semantics.
211    DynamicBehavior,
212    /// Interface, inherited, or virtual dispatch may reach an implementation
213    /// without referencing that concrete method symbol.
214    VirtualDispatch,
215    /// A computed or reflective member access can address the declaration.
216    DynamicMemberAccess,
217    /// A decorator can consume the declaration outside normal references.
218    DecoratedDeclaration,
219    /// An optional interface or inherited contract makes deletion unsafe.
220    OptionalContract,
221    /// A getter or setter has a paired accessor that must be changed atomically.
222    AccessorPair,
223    /// The declaration participates in a method overload set.
224    OverloadSet,
225    /// Source comments are attached to the declaration and must be reviewed.
226    AttachedComment,
227    /// The candidate itself declares an abstract contract.
228    AbstractDeclaration,
229    /// Not every project that owns the declaration completed the query.
230    IncompleteProjectCoverage,
231    /// An external framework declaration could not be attributed to an exact
232    /// package.
233    FrameworkContractProvenance,
234    /// A configured request or response capacity was reached.
235    Capacity,
236    /// The requested syntax or declaration kind is unsupported.
237    UnsupportedSyntax,
238}
239
240/// Counted omission attached to a partial or unavailable result.
241#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243pub struct SemanticOmission {
244    /// Stable omission reason.
245    pub reason_code: SemanticGapReason,
246    /// Number of omitted items or relations.
247    pub count: usize,
248}
249
250/// Conservative outcome for one existing Fallow dead-code candidate.
251#[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    /// The checker resolved at least one exact static reference.
256    ConfirmedUsed,
257    /// Removing the declaration would change inherited behavior or an implemented contract.
258    ContractPreserved,
259    /// Complete closed-world analysis found no checker-resolved references.
260    ConfirmedNoStaticReferences,
261    /// Analysis deliberately declined a closed-world assertion.
262    RetainedAbstained,
263    /// The exact declaration or owning project could not be resolved.
264    RetainedUnresolved,
265}
266
267/// How a class member participates in an inherited or implemented relation.
268#[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    /// A required member declared by an implemented interface.
273    InterfaceImplementation,
274    /// An abstract base member implemented by the candidate.
275    AbstractImplementation,
276    /// A concrete inherited member overridden by the candidate.
277    Override,
278    /// An optional inherited or interface member that still blocks auto-fix.
279    OptionalContract,
280}
281
282/// Exact declaration evidence for an inherited or implemented class-member relation.
283#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285pub struct SemanticContractEvidence {
286    /// Contract relation that makes deletion unsafe.
287    pub relation: SemanticContractRelation,
288    /// Exact interface or base-class declaration.
289    pub declaration: SemanticSymbol,
290    /// Whether the source contract marks this member optional.
291    pub optional: bool,
292}
293
294/// Heritage relation used by a framework-owned class-member contract.
295#[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    /// The class extends a framework base class.
300    Extends,
301    /// The class implements a framework interface.
302    Implements,
303}
304
305/// Exact framework contract supplied by a detected Fallow plugin.
306#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
307#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
308pub struct SemanticFrameworkContract {
309    /// Fallow plugin that supplied the contract.
310    pub framework: String,
311    /// Package that must own the resolved heritage declaration.
312    pub package: String,
313    /// Exported base class or interface name.
314    pub heritage_symbol: String,
315    /// Syntactic spellings accepted only for surfacing a latent candidate.
316    pub heritage_names: Vec<String>,
317    /// Extends or implements relation.
318    pub relation: SemanticFrameworkRelation,
319    /// Framework-dispatched members covered by this contract.
320    pub members: Vec<String>,
321}
322
323/// Checker-validated evidence that a framework contract preserves a member.
324#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
325#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
326pub struct SemanticFrameworkContractEvidence {
327    /// Fallow plugin that supplied the contract.
328    pub framework: String,
329    /// Exact package that owns the heritage declaration.
330    pub package: String,
331    /// Extends or implements relation.
332    pub relation: SemanticFrameworkRelation,
333    /// Exact framework base class or interface declaration.
334    pub declaration: SemanticSymbol,
335}
336
337/// Exact source span and content hash used to guard semantic source edits.
338#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
339#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
340pub struct SemanticEditGuard {
341    /// Zero-based UTF-8 byte offset where the declaration starts.
342    pub start: usize,
343    /// Exclusive zero-based UTF-8 byte offset where the declaration ends.
344    pub end: usize,
345    /// Lowercase SHA-256 digest of the exact declaration text.
346    pub declaration_sha256: String,
347}
348
349/// Bounded, inspectable decision record for one semantic dead-code candidate.
350#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
352pub struct SemanticCandidateDecision {
353    /// Query identifier used to correlate low-level query metadata.
354    pub query_id: usize,
355    /// Exact candidate declaration.
356    pub subject: SemanticSymbol,
357    /// Conservative Fallow-owned decision.
358    pub decision: SemanticCandidateDecisionKind,
359    /// Completeness of the supporting semantic evidence.
360    pub status: SemanticCompleteness,
361    /// Every selected TypeScript project that owns the declaration.
362    pub owning_projects: Vec<String>,
363    /// Bounded checker-resolved reference or uncertainty evidence.
364    #[serde(default, skip_serializing_if = "Vec::is_empty")]
365    pub evidence: Vec<SemanticReference>,
366    /// Inherited contract evidence, when present.
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub contract: Option<SemanticContractEvidence>,
369    /// Framework-owned contract evidence, distinct from TypeScript contracts.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub framework_contract: Option<SemanticFrameworkContractEvidence>,
372    /// Whether this exact decision may enable a guarded class-member fix.
373    pub closed_world_eligible: bool,
374    /// Exact declaration guard required before a source edit.
375    #[serde(default, skip_serializing_if = "Option::is_none")]
376    pub edit_guard: Option<SemanticEditGuard>,
377    /// Primary reason when the candidate remains unresolved or abstained.
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub reason_code: Option<SemanticGapReason>,
380    /// Concise explanation suitable for dry-run and agent output.
381    pub explanation: String,
382    /// Plain next actions.
383    #[serde(default, skip_serializing_if = "Vec::is_empty")]
384    pub actions: Vec<String>,
385    /// Evidence count before bounding.
386    pub total_evidence_count: usize,
387    /// Whether evidence was truncated.
388    pub truncated: bool,
389    /// Counted omissions.
390    #[serde(default, skip_serializing_if = "Vec::is_empty")]
391    pub omissions: Vec<SemanticOmission>,
392}
393
394/// Compact per-query status embedded in run metadata.
395#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
396#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
397pub struct SemanticQuerySummary {
398    /// Stable query identifier within this analysis run.
399    pub query_id: usize,
400    /// Capability that answered the query.
401    pub capability: SemanticCapability,
402    /// Operation-specific assertion, never a generic compiler verdict.
403    pub assertion: String,
404    /// Completeness of this query.
405    pub status: SemanticCompleteness,
406    /// Stable primary gap reason when partial or unavailable.
407    #[serde(default, skip_serializing_if = "Option::is_none")]
408    pub reason_code: Option<SemanticGapReason>,
409    /// Evidence count before bounding.
410    pub total_evidence_count: usize,
411    /// Whether evidence or payload arrays were truncated.
412    pub truncated: bool,
413    /// Counted omissions.
414    #[serde(default, skip_serializing_if = "Vec::is_empty")]
415    pub omissions: Vec<SemanticOmission>,
416    /// Plain next actions.
417    #[serde(default, skip_serializing_if = "Vec::is_empty")]
418    pub actions: Vec<String>,
419}
420
421/// Located semantic reference evidence.
422#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
423#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
424pub struct SemanticReference {
425    /// Project-root-relative reference path.
426    #[serde(serialize_with = "serde_path::serialize")]
427    pub path: PathBuf,
428    /// One-based source line.
429    pub line: u32,
430    /// Zero-based UTF-8 byte column.
431    pub col: u32,
432    /// Reference role, such as `read`, `type`, `alias`, or `re-export`.
433    pub role: String,
434    /// Value or type namespace used at this location.
435    pub namespace: SemanticNamespace,
436    /// Alias and re-export hops between the reference and declaration.
437    #[serde(default, skip_serializing_if = "Vec::is_empty")]
438    pub via: Vec<SemanticAliasHop>,
439}
440
441/// One alias or re-export hop in semantic provenance.
442#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
443#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
444pub struct SemanticAliasHop {
445    /// Project-root-relative hop path.
446    #[serde(serialize_with = "serde_path::serialize")]
447    pub path: PathBuf,
448    /// Name before this hop.
449    pub from_name: String,
450    /// Name exposed after this hop.
451    pub to_name: String,
452    /// Relation, such as `import-alias` or `re-export`.
453    pub relation: String,
454}
455
456/// Typed semantic trace attached to an existing syntactic trace.
457#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
458#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
459pub struct SemanticSymbolTrace {
460    /// Exact target declaration.
461    pub target: SemanticSymbol,
462    /// Semantic mode, capabilities, project selection, and completeness.
463    pub identity: SemanticAnalysisIdentity,
464    /// TypeScript project selected for this symbol.
465    pub selected_project: String,
466    /// Concrete assertion, such as `references-found`.
467    pub assertion: String,
468    /// Completeness of the trace.
469    pub status: SemanticCompleteness,
470    /// Bounded reference evidence.
471    pub references: Vec<SemanticReference>,
472    /// Count before evidence bounding.
473    pub total_reference_count: usize,
474    /// Exact reference locations found by the TypeScript checker.
475    pub checker_evidence_count: usize,
476    /// Alias and re-export hops derived from the semantic graph.
477    pub graph_evidence_count: usize,
478    /// Whether reference evidence was truncated.
479    pub truncated: bool,
480    /// Counted omissions.
481    #[serde(default, skip_serializing_if = "Vec::is_empty")]
482    pub omissions: Vec<SemanticOmission>,
483    /// Plain next actions for a user or automation consumer.
484    #[serde(default, skip_serializing_if = "Vec::is_empty")]
485    pub actions: Vec<String>,
486}
487
488/// One project-local type referenced by a public signature.
489#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
490#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
491pub struct PublicTypeReference {
492    /// Referenced declaration.
493    pub declaration: SemanticSymbol,
494    /// Signature relation, such as return type or generic constraint.
495    pub relation: String,
496}
497
498/// One package-public API entry described by the semantic backend.
499#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
501pub struct ApiSurfaceEntry {
502    /// Symbol exposed through a package entry point.
503    pub exposed: SemanticSymbol,
504    /// Canonical origin after aliases and re-exports.
505    pub origin: SemanticSymbol,
506    /// Stable normalized signature fingerprint.
507    pub signature_fingerprint: String,
508    /// Project-local types referenced by the signature.
509    pub referenced_types: Vec<PublicTypeReference>,
510}
511
512/// Exact semantic evidence for a private type leak.
513#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
514#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
515pub struct SemanticPrivateTypeLeak {
516    /// Public symbol whose signature exposes the type.
517    pub exposed: SemanticSymbol,
518    /// Project-local declaration that is not package-public.
519    pub private_declaration: SemanticSymbol,
520    /// Signature relation through which the type is exposed.
521    pub relation: String,
522    /// Stable TypeScript diagnostic code used as supporting evidence.
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub diagnostic_code: Option<u32>,
525}
526
527/// Package API surface result shared by inspect and private-leak analysis.
528#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
529#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
530pub struct ApiSurfaceResult {
531    /// Concrete assertion, such as `leak-confirmed`.
532    pub assertion: String,
533    /// Completeness of package-public traversal.
534    pub status: SemanticCompleteness,
535    /// Public API entries.
536    pub entries: Vec<ApiSurfaceEntry>,
537    /// Confirmed private type leaks.
538    pub private_type_leaks: Vec<SemanticPrivateTypeLeak>,
539    /// Counted omissions.
540    #[serde(default, skip_serializing_if = "Vec::is_empty")]
541    pub omissions: Vec<SemanticOmission>,
542    /// Plain next actions.
543    #[serde(default, skip_serializing_if = "Vec::is_empty")]
544    pub actions: Vec<String>,
545}
546
547/// One production file or test reached by exact-symbol impact analysis.
548#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
549#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
550pub struct SemanticImpactPath {
551    /// Project-root-relative affected path.
552    #[serde(serialize_with = "serde_path::serialize")]
553    pub path: PathBuf,
554    /// Relation to the target, such as `direct-value-consumer`.
555    pub relation: String,
556    /// Shortest graph distance from the target.
557    pub distance: usize,
558    /// Located provenance path.
559    #[serde(default, skip_serializing_if = "Vec::is_empty")]
560    #[serde(serialize_with = "serde_path::serialize_vec")]
561    pub via: Vec<PathBuf>,
562}
563
564/// Confidence of exact-symbol impact analysis after known dynamic gaps.
565#[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    /// All reported static paths are complete within the selected project
570    /// scope.
571    High,
572    /// Static paths are useful, but virtual dispatch or dynamic behavior
573    /// bounds completeness.
574    Bounded,
575    /// Impact analysis could not run.
576    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/// Exact-symbol impact and targeted-test recommendation.
590#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
592pub struct SemanticSymbolImpact {
593    /// Exact target declaration.
594    pub target: SemanticSymbol,
595    /// Semantic mode, capabilities, project selection, and completeness.
596    pub identity: SemanticAnalysisIdentity,
597    /// TypeScript project selected for this symbol.
598    pub selected_project: String,
599    /// Concrete assertion, such as `consumers-found`.
600    pub assertion: String,
601    /// Completeness of impact analysis.
602    pub status: SemanticCompleteness,
603    /// Files that reference the exact symbol directly.
604    pub direct_consumers: Vec<SemanticImpactPath>,
605    /// Direct consumer count before evidence bounding.
606    pub total_direct_consumer_count: usize,
607    /// Transitively affected production files.
608    pub affected_files: Vec<SemanticImpactPath>,
609    /// Transitive affected-file count before evidence bounding.
610    pub total_affected_file_count: usize,
611    /// Directly relevant test entry points.
612    pub targeted_tests: Vec<SemanticImpactPath>,
613    /// Targeted-test count before evidence bounding.
614    pub total_targeted_test_count: usize,
615    /// Confidence after accounting for dynamic behavior.
616    pub confidence: SemanticImpactConfidence,
617    /// Counted omissions, including dynamic behavior.
618    #[serde(default, skip_serializing_if = "Vec::is_empty")]
619    pub omissions: Vec<SemanticOmission>,
620    /// Plain next actions.
621    #[serde(default, skip_serializing_if = "Vec::is_empty")]
622    pub actions: Vec<String>,
623}
624
625/// One project-local public-signature type edge.
626#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
627#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
628pub struct TypeCouplingEdge {
629    /// Public API declaration that owns the signature.
630    pub source: SemanticSymbol,
631    /// Project-local type used by that signature.
632    pub target: SemanticSymbol,
633    /// Signature relation.
634    pub relation: String,
635    /// Source location where the public signature references the target type.
636    pub evidence: SemanticSourceLocation,
637    /// Scope, such as `module-export` or `package-public`.
638    pub scope: String,
639}
640
641/// Per-file project-local public-signature coupling.
642#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
643#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
644pub struct TypeCouplingFile {
645    /// Project-root-relative file path.
646    #[serde(serialize_with = "serde_path::serialize")]
647    pub path: PathBuf,
648    /// Distinct files this file's public API depends on.
649    pub public_api_depends_on: usize,
650    /// Located project files this file's public API depends on.
651    #[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    /// Distinct files whose public types use this file.
655    pub public_types_used_by: usize,
656    /// Located project files whose public types use this file.
657    #[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    /// Located public-signature edges.
661    pub edges: Vec<TypeCouplingEdge>,
662}
663
664/// One project-local cycle through public-signature type dependencies.
665#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
666#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
667pub struct TypeCouplingCycle {
668    /// Ordered project-root-relative files, ending at the start file.
669    #[serde(serialize_with = "serde_path::serialize_vec")]
670    pub files: Vec<PathBuf>,
671}
672
673/// Project summary for advisory type coupling.
674#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
676pub struct TypeCouplingSummary {
677    /// Measurement boundary, currently project-local public signatures.
678    pub scope: String,
679    /// Edge direction, currently directed.
680    pub direction: String,
681    /// Distinct project files in the selected TypeScript projects.
682    pub project_size: usize,
683    /// Distinct project files included in the denominator.
684    pub files_analyzed: usize,
685    /// Files participating in at least one project-local type edge.
686    pub distinct_coupled_files: usize,
687    /// Project-local public-signature edge count before evidence bounding.
688    pub edge_count: usize,
689    /// Percentage of analyzed files participating in a type edge.
690    pub coupled_file_pct: f64,
691    /// Median distinct-file type connections.
692    pub p50_distinct_connections: f64,
693    /// P90 distinct-file type connections.
694    pub p90_distinct_connections: f64,
695    /// P95 incoming distinct-file type coupling.
696    pub p95_public_types_used_by: f64,
697    /// P95 outgoing distinct-file type coupling.
698    pub p95_public_api_depends_on: f64,
699    /// Percentage of files above the adaptive high-coupling threshold.
700    pub high_coupling_pct: f64,
701    /// Share of edge endpoints represented by the top contributors.
702    pub concentration: f64,
703    /// Number of project-local public-signature cycles.
704    pub cycle_count: usize,
705}
706
707/// Advisory project-local public-signature coupling report.
708#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
709#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
710pub struct TypeCouplingReport {
711    /// Semantic mode, capability, project selection, and completeness.
712    pub identity: SemanticAnalysisIdentity,
713    /// Concrete assertion, such as `coupling-found`.
714    pub assertion: String,
715    /// Completeness of coupling traversal.
716    pub status: SemanticCompleteness,
717    /// Project summary. Absent when analysis is unavailable, never a fake zero.
718    #[serde(default, skip_serializing_if = "Option::is_none")]
719    pub summary: Option<TypeCouplingSummary>,
720    /// Per-file coupling details.
721    pub files: Vec<TypeCouplingFile>,
722    /// Highest-degree files contributing to project coupling.
723    #[serde(default, skip_serializing_if = "Vec::is_empty")]
724    pub top_contributors: Vec<TypeCouplingFile>,
725    /// Located project-local type cycles.
726    #[serde(default, skip_serializing_if = "Vec::is_empty")]
727    pub cycles: Vec<TypeCouplingCycle>,
728    /// Counted omissions.
729    #[serde(default, skip_serializing_if = "Vec::is_empty")]
730    pub omissions: Vec<SemanticOmission>,
731    /// Plain next actions.
732    #[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}