Skip to main content

projectatlas_symbols/
resolution_keys.rs

1//! Canonical import facts and resolution-key projection for extracted symbol graphs.
2
3use crate::ConfiguredModuleResolution;
4use crate::configured_modules::ConfiguredModuleSource;
5use crate::semantic;
6use blake3::Hasher;
7use projectatlas_core::graph::{
8    CanonicalResolutionKey, GraphContractError, GraphIdentityText, GraphLimits, GraphRelationKind,
9    ProjectInstanceId, ResolutionKeyDomain,
10};
11use projectatlas_core::language::{LANGUAGE_CAPABILITIES, SemanticProviderOwner};
12use projectatlas_core::symbols::{RelationKind, SymbolGraph, SymbolKind};
13use std::collections::BTreeMap;
14use std::error::Error;
15use std::fmt;
16
17/// Maximum canonical keys emitted for one source, symbol, or relation fact.
18pub const MAX_RESOLUTION_KEYS_PER_FACT: usize = 64;
19/// Version of the currently implemented semantic relation-resolution contract.
20pub const SEMANTIC_RESOLUTION_CONTRACT_VERSION: u32 = 3;
21
22/// Return a deterministic digest of the live semantic resolution-key contract.
23///
24/// This deliberately describes only the provider-backed `imports`, `calls`, and
25/// Cargo `depends-on` behavior implemented here. The broader accepted relation-
26/// family inventory remains a later, independently versioned responsibility.
27#[must_use]
28pub fn semantic_resolution_contract_digest() -> String {
29    let mut hasher = Hasher::new();
30    hasher.update(&SEMANTIC_RESOLUTION_CONTRACT_VERSION.to_le_bytes());
31    hasher.update(&(MAX_RESOLUTION_KEYS_PER_FACT as u64).to_le_bytes());
32    let mut providers = LANGUAGE_CAPABILITIES
33        .iter()
34        .filter_map(|capability| capability.effective_semantic_provider())
35        .collect::<Vec<_>>();
36    providers.sort_by_key(|provider| provider.as_str());
37    providers.dedup();
38    for provider in providers {
39        hash_contract_value(&mut hasher, provider.as_str());
40        hash_contract_value(
41            &mut hasher,
42            provider.resolution_family().unwrap_or("unavailable"),
43        );
44        if semantic::supports_source_dependencies(provider) {
45            hash_relation_contract(
46                &mut hasher,
47                RelationKind::Imports,
48                ResolutionKeyDomain::Module,
49            );
50            hash_relation_contract(
51                &mut hasher,
52                RelationKind::Calls,
53                ResolutionKeyDomain::Declaration,
54            );
55        }
56        if semantic::supports_package_dependencies(provider) {
57            hash_relation_contract(
58                &mut hasher,
59                RelationKind::DependsOn,
60                ResolutionKeyDomain::Package,
61            );
62        }
63    }
64    for outcome in ["resolved", "ambiguous", "unresolved", "external"] {
65        hash_contract_value(&mut hasher, outcome);
66    }
67    hasher.finalize().to_hex().to_string()
68}
69
70/// Hash one supported relation and its target-identity domain.
71fn hash_relation_contract(
72    hasher: &mut Hasher,
73    relation: RelationKind,
74    domain: ResolutionKeyDomain,
75) {
76    hash_contract_value(hasher, &relation.to_string());
77    hash_contract_value(hasher, domain.as_str());
78}
79
80/// Hash one length-delimited contract label.
81fn hash_contract_value(hasher: &mut Hasher, value: &str) {
82    hasher.update(&(value.len() as u64).to_le_bytes());
83    hasher.update(value.as_bytes());
84}
85/// Canonical resolution keys associated with one extracted symbol index.
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct SymbolResolutionKeys {
88    /// Index of the source symbol fact owning these keys.
89    symbol_index: usize,
90    /// Sorted canonical export keys emitted for the symbol.
91    keys: Vec<CanonicalResolutionKey>,
92}
93
94impl SymbolResolutionKeys {
95    /// Return the corresponding index in [`SymbolGraph::symbols`].
96    #[must_use]
97    pub const fn symbol_index(&self) -> usize {
98        self.symbol_index
99    }
100
101    /// Borrow the sorted, deduplicated export keys for this symbol fact.
102    #[must_use]
103    pub fn keys(&self) -> &[CanonicalResolutionKey] {
104        &self.keys
105    }
106}
107
108/// Canonical dependency keys associated with one extracted relation index.
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct RelationResolutionKeys {
111    /// Index of the source relation fact owning these keys.
112    relation_index: usize,
113    /// Sorted canonical dependency keys emitted for the relation.
114    keys: Vec<CanonicalResolutionKey>,
115}
116
117impl RelationResolutionKeys {
118    /// Return the corresponding index in [`SymbolGraph::relations`].
119    #[must_use]
120    pub const fn relation_index(&self) -> usize {
121        self.relation_index
122    }
123
124    /// Borrow the sorted, deduplicated dependency keys for this relation fact.
125    #[must_use]
126    pub fn keys(&self) -> &[CanonicalResolutionKey] {
127        &self.keys
128    }
129}
130
131/// Parser-owned canonical keys ready for runtime entity/relation binding.
132#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct ResolutionKeyProjection {
134    /// Canonical module exports owned by the source file.
135    source: Vec<CanonicalResolutionKey>,
136    /// Canonical declaration exports associated with symbol facts.
137    symbols: Vec<SymbolResolutionKeys>,
138    /// Canonical dependencies associated with relation facts.
139    relations: Vec<RelationResolutionKeys>,
140}
141
142/// Repository configuration supplied to provider-owned semantic projection.
143#[derive(Clone, Copy, Debug, Default)]
144pub struct ResolutionProjectionContext<'a> {
145    /// Validated configured-module snapshot, when the runtime supplied one.
146    configured_modules: Option<&'a ConfiguredModuleResolution>,
147}
148
149/// Provider scopes expanded once per unique import in one source graph.
150type ImportScopeCache = BTreeMap<String, BTreeMap<String, Vec<String>>>;
151
152/// Bounded contract failures retained while a graph keeps valid siblings.
153struct ContractFailures {
154    /// Rejected derived-key facts retained for typed adapter detail.
155    failures: Vec<ResolutionProjectionFactFailure>,
156    /// Number of distinct rejected derived-key facts observed.
157    rejected_count: usize,
158    /// Whether the source fact has already been rejected.
159    source_seen: bool,
160    /// Rejected symbol ordinals, bit-packed by `Vec<bool>`.
161    symbol_seen: Vec<bool>,
162    /// Rejected relation ordinals, bit-packed by `Vec<bool>`.
163    relation_seen: Vec<bool>,
164}
165
166impl ContractFailures {
167    /// Allocate closed ordinal tracking for one parser graph.
168    fn new(symbol_count: usize, relation_count: usize) -> Self {
169        Self {
170            failures: Vec::new(),
171            rejected_count: 0,
172            source_seen: false,
173            symbol_seen: vec![false; symbol_count],
174            relation_seen: vec![false; relation_count],
175        }
176    }
177
178    /// Record the first failure for one dense parser fact.
179    fn remember(&mut self, fact: ResolutionProjectionFact, error: GraphContractError) {
180        let seen = match fact {
181            ResolutionProjectionFact::Source => {
182                let seen = self.source_seen;
183                self.source_seen = true;
184                !seen
185            }
186            ResolutionProjectionFact::Symbol(index) => {
187                self.symbol_seen.get_mut(index).is_some_and(|seen| {
188                    let was_seen = *seen;
189                    *seen = true;
190                    !was_seen
191                })
192            }
193            ResolutionProjectionFact::Relation(index) => {
194                self.relation_seen.get_mut(index).is_some_and(|seen| {
195                    let was_seen = *seen;
196                    *seen = true;
197                    !was_seen
198                })
199            }
200        };
201        if !seen {
202            return;
203        }
204        self.rejected_count = self.rejected_count.saturating_add(1);
205        if self.failures.len() < MAX_RESOLUTION_PROJECTION_FAILURES {
206            self.failures
207                .push(ResolutionProjectionFactFailure { error, fact });
208        }
209    }
210}
211
212impl<'a> ResolutionProjectionContext<'a> {
213    /// Construct a projection context with one validated configured-module snapshot.
214    #[must_use]
215    pub const fn with_configured_modules(
216        configured_modules: &'a ConfiguredModuleResolution,
217    ) -> Self {
218        Self {
219            configured_modules: Some(configured_modules),
220        }
221    }
222}
223
224impl ResolutionKeyProjection {
225    /// Construct an empty projection when one parser graph has no admissible
226    /// canonical identity keys. Entity and relation rows can still be staged;
227    /// the graph publication layer records the rejected key provenance.
228    #[must_use]
229    pub const fn empty() -> Self {
230        Self {
231            source: Vec::new(),
232            symbols: Vec::new(),
233            relations: Vec::new(),
234        }
235    }
236
237    /// Borrow module keys exported by the source file itself.
238    #[must_use]
239    pub fn source_keys(&self) -> &[CanonicalResolutionKey] {
240        &self.source
241    }
242
243    /// Borrow symbol-index-associated export keys.
244    #[must_use]
245    pub fn symbol_keys(&self) -> &[SymbolResolutionKeys] {
246        &self.symbols
247    }
248
249    /// Borrow relation-index-associated dependency keys.
250    #[must_use]
251    pub fn relation_keys(&self) -> &[RelationResolutionKeys] {
252        &self.relations
253    }
254}
255
256/// Parser fact whose derived canonical key was rejected by graph admission.
257#[derive(Clone, Copy, Debug, Eq, PartialEq)]
258pub enum ResolutionProjectionFact {
259    /// A source-file export scope.
260    Source,
261    /// A symbol-index-associated export.
262    Symbol(usize),
263    /// A relation-index-associated dependency.
264    Relation(usize),
265}
266
267/// Maximum rejected derived-key facts retained from one parser graph.
268pub const MAX_RESOLUTION_PROJECTION_FAILURES: usize = GraphLimits::MAX_ROWS as usize;
269
270/// One parser fact whose derived canonical key violated the graph contract.
271#[derive(Debug)]
272pub struct ResolutionProjectionFactFailure {
273    /// Stable contract error category for the rejected derived key.
274    error: GraphContractError,
275    /// Exact parser fact that produced the rejected derived key.
276    fact: ResolutionProjectionFact,
277}
278
279impl ResolutionProjectionFactFailure {
280    /// Return the graph-contract error without exposing rejected identity text.
281    #[must_use]
282    pub fn error(&self) -> &GraphContractError {
283        &self.error
284    }
285
286    /// Return the parser fact that produced the rejected derived key.
287    #[must_use]
288    pub const fn fact(&self) -> ResolutionProjectionFact {
289        self.fact
290    }
291
292    /// Consume one failure at an adapter boundary that needs its typed error.
293    #[must_use]
294    pub fn into_parts(self) -> (ResolutionProjectionFact, GraphContractError) {
295        (self.fact, self.error)
296    }
297}
298
299/// Bounded rejected derived-key facts and the partial valid projection.
300#[derive(Debug)]
301pub struct ResolutionProjectionFailure {
302    /// Every retained rejected derived-key fact in deterministic traversal order.
303    failures: Vec<ResolutionProjectionFactFailure>,
304    /// Number of rejected derived-key facts, including rows beyond the detail bound.
305    rejected_count: usize,
306    /// All valid canonical keys retained from the graph.
307    projection: ResolutionKeyProjection,
308}
309
310impl ResolutionProjectionFailure {
311    /// Borrow every retained rejected derived-key fact.
312    #[must_use]
313    pub fn failures(&self) -> &[ResolutionProjectionFactFailure] {
314        &self.failures
315    }
316
317    /// Return the total rejected derived-key count before bounded detail retention.
318    #[must_use]
319    pub const fn rejected_count(&self) -> usize {
320        self.rejected_count
321    }
322
323    /// Borrow the projection containing all valid keys derived before and after the failure.
324    #[must_use]
325    pub fn projection(&self) -> &ResolutionKeyProjection {
326        &self.projection
327    }
328
329    /// Consume the failure at an adapter boundary that needs all retained state.
330    #[must_use]
331    pub fn into_parts(
332        self,
333    ) -> (
334        Vec<ResolutionProjectionFactFailure>,
335        usize,
336        ResolutionKeyProjection,
337    ) {
338        (self.failures, self.rejected_count, self.projection)
339    }
340}
341
342/// Failure while deriving bounded canonical keys from parser facts.
343#[derive(Debug)]
344pub enum ResolutionProjectionError {
345    /// An extracted or caller-supplied identity violated the graph contract.
346    Contract(Box<ResolutionProjectionFailure>),
347    /// One source fact would exceed the bounded key fan-out.
348    KeyLimit {
349        /// Kind of parser fact being projected.
350        fact: &'static str,
351        /// Index in the owning symbol or relation collection when applicable.
352        index: usize,
353        /// Number of distinct keys requested by the fact.
354        requested: usize,
355    },
356}
357
358impl fmt::Display for ResolutionProjectionError {
359    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
360        match self {
361            Self::Contract(failure) => match failure.failures.first() {
362                Some(failure) => {
363                    write!(formatter, "invalid resolution identity: {}", failure.error)
364                }
365                None => formatter.write_str("invalid resolution identity"),
366            },
367            Self::KeyLimit {
368                fact,
369                index,
370                requested,
371            } => write!(
372                formatter,
373                "{fact} fact {index} requires {requested} canonical keys, exceeding the per-fact limit of {MAX_RESOLUTION_KEYS_PER_FACT}"
374            ),
375        }
376    }
377}
378
379impl Error for ResolutionProjectionError {
380    fn source(&self) -> Option<&(dyn Error + 'static)> {
381        match self {
382            Self::Contract(failure) => failure
383                .failures
384                .first()
385                .map(|failure| &failure.error as &(dyn Error + 'static)),
386            Self::KeyLimit { .. } => None,
387        }
388    }
389}
390
391impl From<GraphContractError> for ResolutionProjectionError {
392    fn from(value: GraphContractError) -> Self {
393        Self::Contract(Box::new(ResolutionProjectionFailure {
394            failures: vec![ResolutionProjectionFactFailure {
395                error: value,
396                fact: ResolutionProjectionFact::Source,
397            }],
398            rejected_count: 1,
399            projection: ResolutionKeyProjection::empty(),
400        }))
401    }
402}
403
404/// Language syntax that produced one import reference.
405#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub enum ImportSyntax {
407    /// Rust `use` syntax.
408    Rust,
409    /// JavaScript or TypeScript `import` syntax.
410    EcmaScript,
411    /// Python `import` or `from ... import ...` syntax.
412    Python,
413}
414
415/// One normalized imported module or declaration and its caller-local name.
416#[derive(Clone, Debug, Eq, PartialEq)]
417pub struct ImportReference {
418    /// Source-language syntax that produced the reference.
419    syntax: ImportSyntax,
420    /// Imported module specifier or path.
421    module: String,
422    /// Imported declaration name for named imports.
423    imported: Option<String>,
424    /// Name visible to callers in the importing source file.
425    local: String,
426}
427
428impl ImportReference {
429    /// Construct one normalized provider-owned import reference.
430    pub(crate) fn new(
431        syntax: ImportSyntax,
432        module: &str,
433        imported: Option<&str>,
434        local: &str,
435    ) -> Self {
436        Self {
437            syntax,
438            module: module.to_string(),
439            imported: imported.map(ToString::to_string),
440            local: local.to_string(),
441        }
442    }
443
444    /// Return the source-language import syntax.
445    #[must_use]
446    pub const fn syntax(&self) -> ImportSyntax {
447        self.syntax
448    }
449
450    /// Borrow the normalized module specifier or path.
451    #[must_use]
452    pub fn module(&self) -> &str {
453        &self.module
454    }
455
456    /// Borrow the imported declaration name, or `None` for a module/namespace import.
457    #[must_use]
458    pub fn imported(&self) -> Option<&str> {
459        self.imported.as_deref()
460    }
461
462    /// Borrow the name used by calls inside the importing source file.
463    #[must_use]
464    pub fn local(&self) -> &str {
465        &self.local
466    }
467}
468
469/// Parse one Rust, JavaScript/TypeScript, or Python import statement.
470///
471/// Malformed and unsupported forms return no references instead of inventing
472/// identities from the complete display statement.
473#[must_use]
474pub fn parse_import_references(import_text: &str) -> Vec<ImportReference> {
475    let mut references = semantic::parse_display_import(import_text);
476    references.sort_by(|left, right| {
477        left.module
478            .cmp(&right.module)
479            .then_with(|| left.imported.cmp(&right.imported))
480            .then_with(|| left.local.cmp(&right.local))
481    });
482    references.dedup();
483    references
484}
485
486/// Resolve one relative ECMAScript module specifier against a repository path.
487#[must_use]
488pub fn resolve_relative_import_path(caller_path: &str, module_spec: &str) -> Option<String> {
489    semantic::ecmascript::resolve_relative_import_path(caller_path, module_spec)
490}
491
492/// Return deterministic module aliases inferred from a repository source path.
493#[must_use]
494pub fn module_aliases_for_path(path: &str) -> Vec<String> {
495    let mut aliases = Vec::new();
496    for stem in source_stems_for_path(path) {
497        let mut components = stem
498            .split('/')
499            .filter(|component| !component.is_empty())
500            .collect::<Vec<_>>();
501        if components
502            .first()
503            .is_some_and(|component| *component == "src")
504        {
505            components.remove(0);
506        }
507        if components.last().is_some_and(|component| {
508            matches!(*component, "lib" | "main" | "mod" | "index" | "__init__")
509        }) {
510            components.pop();
511        }
512        if components.is_empty() {
513            continue;
514        }
515        aliases.push(components.join("::"));
516        aliases.push(components.join("."));
517        if let Some(last) = components.last() {
518            aliases.push((*last).to_string());
519        }
520    }
521    aliases.sort();
522    aliases.dedup();
523    aliases
524}
525
526/// Return source path stems, including package-entry aliases.
527#[must_use]
528pub fn source_stems_for_path(path: &str) -> Vec<String> {
529    let stem = strip_known_source_extension(path);
530    let mut stems = vec![stem.clone()];
531    if let Some((parent, entry_name)) = stem.rsplit_once('/')
532        && matches!(entry_name, "index" | "__init__" | "mod")
533    {
534        stems.push(parent.to_string());
535    }
536    stems.sort();
537    stems.dedup();
538    stems
539}
540
541/// Derive bounded canonical export and dependency keys from one extracted graph.
542///
543/// The returned symbol and relation indices associate parser facts with keys;
544/// runtime graph normalization binds those keys to final entity and logical-
545/// relation owners after resolution.
546///
547/// # Errors
548///
549/// Returns an error when a graph identity is invalid or one parser fact exceeds
550/// the bounded canonical-key fan-out.
551pub fn derive_resolution_keys(
552    project: ProjectInstanceId,
553    package: Option<&str>,
554    graph: &SymbolGraph,
555) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
556    derive_resolution_keys_with_context(
557        project,
558        package,
559        graph,
560        ResolutionProjectionContext::default(),
561    )
562}
563
564/// Derive bounded canonical keys with explicit repository module configuration.
565///
566/// # Errors
567///
568/// Returns an error when a graph identity is invalid or one parser fact exceeds
569/// the bounded canonical-key fan-out.
570pub fn derive_resolution_keys_with_context(
571    project: ProjectInstanceId,
572    package: Option<&str>,
573    graph: &SymbolGraph,
574    context: ResolutionProjectionContext<'_>,
575) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
576    let Some(provider_owner) = semantic::provider_for_graph(graph) else {
577        return Ok(ResolutionKeyProjection {
578            source: Vec::new(),
579            symbols: Vec::new(),
580            relations: Vec::new(),
581        });
582    };
583    let provider = GraphIdentityText::new(provider_owner.as_str())?;
584    let Some(resolution_family) = provider_owner.resolution_family() else {
585        return Ok(ResolutionKeyProjection {
586            source: Vec::new(),
587            symbols: Vec::new(),
588            relations: Vec::new(),
589        });
590    };
591    let mut contract_failures = ContractFailures::new(graph.symbols.len(), graph.relations.len());
592    let language = GraphIdentityText::new(resolution_family)?;
593    let package = package.map(GraphIdentityText::new).transpose()?;
594
595    let source_scopes = canonical_source_scopes(&graph.path);
596    let configured_source_scopes =
597        if provider_owner == SemanticProviderOwner::EcmaScript && package.is_some() {
598            canonical_configured_source_scopes(&graph.path)
599        } else {
600            Vec::new()
601        };
602    let emits_source_module_keys = semantic::emits_source_module_keys(graph);
603    let mut source_keys = Vec::new();
604    if emits_source_module_keys {
605        source_keys.reserve(source_scopes.len() + configured_source_scopes.len());
606        for scope in &source_scopes {
607            if let Some(key) = retain_derived_key(
608                canonical_key(
609                    project,
610                    ResolutionKeyDomain::Module,
611                    &provider,
612                    &language,
613                    package.as_ref(),
614                    None,
615                    RelationKind::Imports,
616                    scope,
617                ),
618                ResolutionProjectionFact::Source,
619                &mut contract_failures,
620            ) {
621                source_keys.push(key);
622            }
623        }
624        for scope in &configured_source_scopes {
625            if let Some(key) = retain_derived_key(
626                canonical_key(
627                    project,
628                    ResolutionKeyDomain::Module,
629                    &provider,
630                    &language,
631                    None,
632                    None,
633                    RelationKind::Imports,
634                    scope,
635                ),
636                ResolutionProjectionFact::Source,
637                &mut contract_failures,
638            ) {
639                source_keys.push(key);
640            }
641        }
642    }
643    let source_keys = bounded_keys(source_keys, "source", 0)?;
644
645    let mut symbol_keys = Vec::new();
646    for (symbol_index, symbol) in graph.symbols.iter().enumerate() {
647        let mut keys = Vec::new();
648        match symbol.kind {
649            SymbolKind::Package if provider_owner == SemanticProviderOwner::Cargo => {
650                if let Some(key) = retain_derived_key(
651                    canonical_key(
652                        project,
653                        ResolutionKeyDomain::Package,
654                        &provider,
655                        &language,
656                        None,
657                        None,
658                        RelationKind::DependsOn,
659                        &symbol.name,
660                    ),
661                    ResolutionProjectionFact::Symbol(symbol_index),
662                    &mut contract_failures,
663                ) {
664                    keys.push(key);
665                }
666            }
667            SymbolKind::Dependency | SymbolKind::Import | SymbolKind::Workspace => {}
668            _ if emits_source_module_keys
669                && semantic::is_export_candidate(provider_owner, graph, symbol_index) =>
670            {
671                let identity = match GraphIdentityText::new(symbol.name.clone()) {
672                    Ok(identity) => identity,
673                    Err(error) => {
674                        remember_contract_failure(
675                            &mut contract_failures,
676                            ResolutionProjectionFact::Symbol(symbol_index),
677                            error,
678                        );
679                        continue;
680                    }
681                };
682                let mut scopes = source_scopes.clone();
683                if let Some(parent) = symbol.parent.as_deref() {
684                    scopes.extend(
685                        source_scopes
686                            .iter()
687                            .map(|scope| format!("{scope}/{parent}")),
688                    );
689                    scopes.push(parent.to_string());
690                }
691                scopes.push(String::new());
692                scopes.sort();
693                scopes.dedup();
694                for scope in &scopes {
695                    let scope = if scope.is_empty() {
696                        None
697                    } else {
698                        match GraphIdentityText::new(scope.clone()) {
699                            Ok(scope) => Some(scope),
700                            Err(error) => {
701                                remember_contract_failure(
702                                    &mut contract_failures,
703                                    ResolutionProjectionFact::Symbol(symbol_index),
704                                    error,
705                                );
706                                continue;
707                            }
708                        }
709                    };
710                    keys.push(CanonicalResolutionKey::new(
711                        project,
712                        ResolutionKeyDomain::Declaration,
713                        &provider,
714                        &language,
715                        package.as_ref(),
716                        scope.as_ref(),
717                        Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
718                        &identity,
719                    ));
720                }
721                let mut configured_scopes = configured_source_scopes.clone();
722                if let Some(parent) = symbol.parent.as_deref() {
723                    configured_scopes.extend(
724                        configured_source_scopes
725                            .iter()
726                            .map(|scope| format!("{scope}/{parent}")),
727                    );
728                    configured_scopes.sort();
729                    configured_scopes.dedup();
730                }
731                for scope in configured_scopes {
732                    let scope = match GraphIdentityText::new(scope) {
733                        Ok(scope) => scope,
734                        Err(error) => {
735                            remember_contract_failure(
736                                &mut contract_failures,
737                                ResolutionProjectionFact::Symbol(symbol_index),
738                                error,
739                            );
740                            continue;
741                        }
742                    };
743                    keys.push(CanonicalResolutionKey::new(
744                        project,
745                        ResolutionKeyDomain::Declaration,
746                        &provider,
747                        &language,
748                        None,
749                        Some(&scope),
750                        Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
751                        &identity,
752                    ));
753                }
754            }
755            _ => {}
756        }
757        let keys = bounded_keys(keys, "symbol", symbol_index)?;
758        if !keys.is_empty() {
759            symbol_keys.push(SymbolResolutionKeys { symbol_index, keys });
760        }
761    }
762
763    let parsed_imports = graph
764        .relations
765        .iter()
766        .map(|relation| {
767            if relation.kind == RelationKind::Imports {
768                semantic::parse_imports(provider_owner, &relation.target_name)
769            } else {
770                Vec::new()
771            }
772        })
773        .collect::<Vec<_>>();
774    let mut aliases: BTreeMap<&str, Vec<&ImportReference>> = BTreeMap::new();
775    for references in &parsed_imports {
776        for reference in references {
777            aliases
778                .entry(reference.local())
779                .or_default()
780                .push(reference);
781        }
782    }
783    let configured_source = context
784        .configured_modules
785        .map(|configured| configured.for_source(&graph.path));
786    let import_scopes = build_import_scope_cache(
787        provider_owner,
788        graph,
789        &parsed_imports,
790        configured_source.as_ref(),
791    );
792
793    let mut relation_keys = Vec::new();
794    for (relation_index, relation) in graph.relations.iter().enumerate() {
795        let mut keys = match relation.kind {
796            RelationKind::Imports if semantic::supports_source_dependencies(provider_owner) => {
797                import_dependency_keys(
798                    project,
799                    provider_owner,
800                    &provider,
801                    &language,
802                    package.as_ref(),
803                    &relation.path,
804                    &parsed_imports[relation_index],
805                    &import_scopes,
806                    relation_index,
807                    &mut contract_failures,
808                )
809            }
810            RelationKind::Calls if semantic::supports_source_dependencies(provider_owner) => {
811                call_dependency_keys(
812                    project,
813                    provider_owner,
814                    &provider,
815                    &language,
816                    package.as_ref(),
817                    &relation.path,
818                    &relation.target_name,
819                    &aliases,
820                    &import_scopes,
821                    relation_index,
822                    &mut contract_failures,
823                )
824            }
825            RelationKind::DependsOn if semantic::supports_package_dependencies(provider_owner) => {
826                retain_derived_key(
827                    canonical_key(
828                        project,
829                        ResolutionKeyDomain::Package,
830                        &provider,
831                        &language,
832                        None,
833                        None,
834                        RelationKind::DependsOn,
835                        &relation.target_name,
836                    ),
837                    ResolutionProjectionFact::Relation(relation_index),
838                    &mut contract_failures,
839                )
840                .into_iter()
841                .collect()
842            }
843            RelationKind::Contains => continue,
844            RelationKind::Imports | RelationKind::Calls | RelationKind::DependsOn => Vec::new(),
845        };
846        keys = bounded_keys(keys, "relation", relation_index)?;
847        relation_keys.push(RelationResolutionKeys {
848            relation_index,
849            keys,
850        });
851    }
852
853    let projection = ResolutionKeyProjection {
854        source: source_keys,
855        symbols: symbol_keys,
856        relations: relation_keys,
857    };
858    if contract_failures.failures.is_empty() {
859        Ok(projection)
860    } else {
861        Err(ResolutionProjectionError::Contract(Box::new(
862            ResolutionProjectionFailure {
863                failures: contract_failures.failures,
864                rejected_count: contract_failures.rejected_count,
865                projection,
866            },
867        )))
868    }
869}
870
871/// Expand each unique import scope once for the complete source graph.
872fn build_import_scope_cache(
873    provider_owner: SemanticProviderOwner,
874    graph: &SymbolGraph,
875    parsed_imports: &[Vec<ImportReference>],
876    configured_source: Option<&ConfiguredModuleSource<'_>>,
877) -> ImportScopeCache {
878    let mut cache = ImportScopeCache::new();
879    for (relation, references) in graph.relations.iter().zip(parsed_imports) {
880        let caller_path = &relation.path;
881        let modules = cache.entry(caller_path.clone()).or_default();
882        for reference in references {
883            modules
884                .entry(reference.module().to_string())
885                .or_insert_with(|| {
886                    semantic::import_scopes(
887                        provider_owner,
888                        caller_path,
889                        reference,
890                        configured_source,
891                    )
892                });
893        }
894    }
895    cache
896}
897
898/// Borrow one pre-expanded import scope list without allocating lookup keys.
899fn cached_import_scopes<'a>(
900    cache: &'a ImportScopeCache,
901    caller_path: &str,
902    reference: &ImportReference,
903) -> &'a [String] {
904    cache
905        .get(caller_path)
906        .and_then(|modules| modules.get(reference.module()))
907        .map_or(&[], Vec::as_slice)
908}
909
910/// Derive canonical module-target keys for import references.
911fn import_dependency_keys(
912    project: ProjectInstanceId,
913    provider_owner: SemanticProviderOwner,
914    provider: &GraphIdentityText,
915    language: &GraphIdentityText,
916    package: Option<&GraphIdentityText>,
917    caller_path: &str,
918    references: &[ImportReference],
919    import_scopes: &ImportScopeCache,
920    relation_index: usize,
921    contract_failures: &mut ContractFailures,
922) -> Vec<CanonicalResolutionKey> {
923    let mut keys = Vec::new();
924    for reference in references {
925        let scopes = cached_import_scopes(import_scopes, caller_path, reference);
926        let package = dependency_package(provider_owner, reference, package);
927        for scope in scopes {
928            if let Some(key) = retain_derived_key(
929                canonical_key(
930                    project,
931                    ResolutionKeyDomain::Module,
932                    provider,
933                    language,
934                    package,
935                    None,
936                    RelationKind::Imports,
937                    scope,
938                ),
939                ResolutionProjectionFact::Relation(relation_index),
940                contract_failures,
941            ) {
942                keys.push(key);
943            }
944        }
945    }
946    keys
947}
948
949/// Derive canonical declaration keys for one call, including local aliases.
950fn call_dependency_keys(
951    project: ProjectInstanceId,
952    provider_owner: SemanticProviderOwner,
953    provider: &GraphIdentityText,
954    language: &GraphIdentityText,
955    package: Option<&GraphIdentityText>,
956    caller_path: &str,
957    target: &str,
958    aliases: &BTreeMap<&str, Vec<&ImportReference>>,
959    import_scopes: &ImportScopeCache,
960    relation_index: usize,
961    contract_failures: &mut ContractFailures,
962) -> Vec<CanonicalResolutionKey> {
963    let (prefix, remainder) = split_qualified_target(target);
964    let alias = prefix.unwrap_or(target).trim();
965    if let Some(references) = aliases.get(alias) {
966        let mut keys = Vec::new();
967        for reference in references {
968            let identity = remainder.or_else(|| reference.imported());
969            let Some(identity) = identity else {
970                continue;
971            };
972            let scopes = cached_import_scopes(import_scopes, caller_path, reference);
973            let package = dependency_package(provider_owner, reference, package);
974            for scope in scopes {
975                let scoped_parent;
976                let scope = if remainder.is_some()
977                    && let Some(imported_parent) = reference.imported()
978                {
979                    scoped_parent = format!("{scope}/{imported_parent}");
980                    scoped_parent.as_str()
981                } else {
982                    scope.as_str()
983                };
984                if let Some(key) = retain_derived_key(
985                    canonical_key(
986                        project,
987                        ResolutionKeyDomain::Declaration,
988                        provider,
989                        language,
990                        package,
991                        Some(scope),
992                        RelationKind::Calls,
993                        identity,
994                    ),
995                    ResolutionProjectionFact::Relation(relation_index),
996                    contract_failures,
997                ) {
998                    keys.push(key);
999                }
1000            }
1001        }
1002        return keys;
1003    }
1004
1005    let (scope, identity) = split_qualified_target(target);
1006    if scope.is_some() {
1007        return Vec::new();
1008    }
1009    let Some(identity) = identity.or_else(|| (!target.trim().is_empty()).then_some(target.trim()))
1010    else {
1011        return Vec::new();
1012    };
1013    retain_derived_key(
1014        canonical_key(
1015            project,
1016            ResolutionKeyDomain::Declaration,
1017            provider,
1018            language,
1019            package,
1020            None,
1021            RelationKind::Calls,
1022            identity,
1023        ),
1024        ResolutionProjectionFact::Relation(relation_index),
1025        contract_failures,
1026    )
1027    .into_iter()
1028    .collect()
1029}
1030
1031/// Configured ECMAScript targets are repository paths, so caller Cargo
1032/// ownership must not prevent them from matching a sibling package export.
1033fn dependency_package<'a>(
1034    provider_owner: SemanticProviderOwner,
1035    reference: &ImportReference,
1036    package: Option<&'a GraphIdentityText>,
1037) -> Option<&'a GraphIdentityText> {
1038    if provider_owner == SemanticProviderOwner::EcmaScript
1039        && !(reference.module().starts_with("./") || reference.module().starts_with("../"))
1040    {
1041        None
1042    } else {
1043        package
1044    }
1045}
1046
1047#[allow(clippy::too_many_arguments)]
1048/// Construct one validated canonical key from parser-owned identity parts.
1049fn canonical_key(
1050    project: ProjectInstanceId,
1051    domain: ResolutionKeyDomain,
1052    provider: &GraphIdentityText,
1053    language: &GraphIdentityText,
1054    package: Option<&GraphIdentityText>,
1055    scope: Option<&str>,
1056    relation: RelationKind,
1057    identity: &str,
1058) -> Result<CanonicalResolutionKey, GraphContractError> {
1059    let scope = scope.map(GraphIdentityText::new).transpose()?;
1060    let identity = GraphIdentityText::new(identity.to_string())?;
1061    Ok(CanonicalResolutionKey::new(
1062        project,
1063        domain,
1064        provider,
1065        language,
1066        package,
1067        scope.as_ref(),
1068        Some(GraphRelationKind::from_legacy(relation)),
1069        &identity,
1070    ))
1071}
1072
1073/// Retain one valid derived key while remembering the first rejected fact.
1074fn retain_derived_key(
1075    result: Result<CanonicalResolutionKey, GraphContractError>,
1076    fact: ResolutionProjectionFact,
1077    contract_failures: &mut ContractFailures,
1078) -> Option<CanonicalResolutionKey> {
1079    match result {
1080        Ok(key) => Some(key),
1081        Err(error) => {
1082            remember_contract_failure(contract_failures, fact, error);
1083            None
1084        }
1085    }
1086}
1087
1088/// Keep one deterministic contract failure without retaining rejected identity text.
1089fn remember_contract_failure(
1090    contract_failures: &mut ContractFailures,
1091    fact: ResolutionProjectionFact,
1092    error: GraphContractError,
1093) {
1094    contract_failures.remember(fact, error);
1095}
1096
1097/// Sort, deduplicate, and enforce the per-fact canonical-key limit.
1098fn bounded_keys(
1099    mut keys: Vec<CanonicalResolutionKey>,
1100    fact: &'static str,
1101    index: usize,
1102) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
1103    keys.sort();
1104    keys.dedup();
1105    if keys.len() > MAX_RESOLUTION_KEYS_PER_FACT {
1106        return Err(ResolutionProjectionError::KeyLimit {
1107            fact,
1108            index,
1109            requested: keys.len(),
1110        });
1111    }
1112    Ok(keys)
1113}
1114
1115/// Return normalized canonical scopes exported by a repository source path.
1116fn canonical_source_scopes(path: &str) -> Vec<String> {
1117    let mut scopes = source_stems_for_path(path);
1118    scopes.extend(module_aliases_for_path(path));
1119    normalize_scopes(scopes, normalize_repository_scope)
1120}
1121
1122/// Return only repository-exact scopes used by configured ECMAScript targets.
1123fn canonical_configured_source_scopes(path: &str) -> Vec<String> {
1124    let mut scopes = source_stems_for_path(path)
1125        .into_iter()
1126        .map(|scope| normalize_repository_scope(&scope))
1127        .filter(|scope| !scope.is_empty())
1128        .collect::<Vec<_>>();
1129    scopes.sort();
1130    scopes.dedup();
1131    scopes
1132}
1133
1134/// Normalize, expand, sort, and deduplicate candidate scopes.
1135fn normalize_scopes(scopes: Vec<String>, normalize: fn(&str) -> String) -> Vec<String> {
1136    let mut normalized = Vec::new();
1137    for scope in scopes {
1138        let scope = normalize(&scope);
1139        if scope.is_empty() {
1140            continue;
1141        }
1142        normalized.push(scope.clone());
1143        if let Some(stripped) = scope.strip_prefix("src/") {
1144            normalized.push(stripped.to_string());
1145        }
1146        if let Some(last) = scope.rsplit('/').next() {
1147            normalized.push(last.to_string());
1148        }
1149    }
1150    normalized.sort();
1151    normalized.dedup();
1152    normalized
1153}
1154
1155/// Normalize repository path and Rust-style alias separators without changing dots.
1156fn normalize_repository_scope(value: &str) -> String {
1157    value
1158        .trim()
1159        .trim_start_matches("./")
1160        .replace("::", "/")
1161        .trim_matches('/')
1162        .to_string()
1163}
1164
1165/// Split a qualified call target into its scope and final identity.
1166fn split_qualified_target(target: &str) -> (Option<&str>, Option<&str>) {
1167    let target = target.trim();
1168    let rust = target.rsplit_once("::");
1169    let dotted = target.rsplit_once('.');
1170    match (rust, dotted) {
1171        (Some(rust), Some(dotted)) => {
1172            if rust.0.len() >= dotted.0.len() {
1173                (Some(rust.0), Some(rust.1))
1174            } else {
1175                (Some(dotted.0), Some(dotted.1))
1176            }
1177        }
1178        (Some((scope, identity)), None) | (None, Some((scope, identity))) => {
1179            (Some(scope), Some(identity))
1180        }
1181        (None, None) => (None, None),
1182    }
1183}
1184
1185/// Strip a terminal source extension while preserving dotted directory names.
1186pub(super) fn strip_known_source_extension(path: &str) -> String {
1187    for extension in [
1188        ".d.ts", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs",
1189    ] {
1190        if let Some(stem) = path.strip_suffix(extension) {
1191            return stem.to_string();
1192        }
1193    }
1194    path.rsplit_once('.').map_or_else(
1195        || path.to_string(),
1196        |(stem, extension)| {
1197            if extension.contains('/') {
1198                path.to_string()
1199            } else {
1200                stem.to_string()
1201            }
1202        },
1203    )
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208    use super::{
1209        ImportReference, ImportSyntax, ResolutionProjectionContext, ResolutionProjectionError,
1210        SEMANTIC_RESOLUTION_CONTRACT_VERSION, build_import_scope_cache, derive_resolution_keys,
1211        derive_resolution_keys_with_context, parse_import_references, resolve_relative_import_path,
1212        semantic_resolution_contract_digest,
1213    };
1214    use crate::{
1215        ConfiguredModuleResolution, EcmaScriptConfigKind, EcmaScriptModuleConfig,
1216        EcmaScriptPathMapping, extract_symbol_graph,
1217    };
1218    use projectatlas_core::graph::{CanonicalResolutionKey, ProjectInstanceId};
1219    use projectatlas_core::language::SemanticProviderOwner;
1220    use projectatlas_core::symbols::{
1221        CodeSymbol, ParserKind, RelationKind, SymbolGraph, SymbolKind, SymbolRelation,
1222    };
1223    use std::error::Error;
1224    use std::io;
1225
1226    fn reference(
1227        syntax: ImportSyntax,
1228        module: &str,
1229        imported: Option<&str>,
1230        local: &str,
1231    ) -> ImportReference {
1232        ImportReference {
1233            syntax,
1234            module: module.to_string(),
1235            imported: imported.map(ToString::to_string),
1236            local: local.to_string(),
1237        }
1238    }
1239
1240    #[test]
1241    fn semantic_resolution_contract_digest_is_bounded_and_deterministic() {
1242        const PRE_MODULE_CALLBACK_DIGEST: &str =
1243            "487625adf2f9ec76f98034d4ef5667e707960b6b8afd280b213021cb64a0f10f";
1244        let first = semantic_resolution_contract_digest();
1245        assert_eq!(SEMANTIC_RESOLUTION_CONTRACT_VERSION, 3);
1246        assert_eq!(first.len(), 64);
1247        assert!(first.bytes().all(|byte| byte.is_ascii_hexdigit()));
1248        assert_eq!(first, semantic_resolution_contract_digest());
1249        assert_ne!(first, PRE_MODULE_CALLBACK_DIGEST);
1250    }
1251
1252    #[test]
1253    fn parses_language_import_aliases_without_display_statement_identity() {
1254        assert_eq!(
1255            parse_import_references("use crate::worker::{run as execute, stop};"),
1256            vec![
1257                reference(ImportSyntax::Rust, "crate::worker", Some("run"), "execute"),
1258                reference(ImportSyntax::Rust, "crate::worker", Some("stop"), "stop"),
1259            ]
1260        );
1261        assert_eq!(
1262            parse_import_references("use crate::worker::run_function_alias as run_rust_function;"),
1263            vec![reference(
1264                ImportSyntax::Rust,
1265                "crate::worker",
1266                Some("run_function_alias"),
1267                "run_rust_function"
1268            ),]
1269        );
1270        assert_eq!(
1271            parse_import_references("import * as reader from './reader';"),
1272            vec![reference(
1273                ImportSyntax::EcmaScript,
1274                "./reader",
1275                None,
1276                "reader",
1277            )]
1278        );
1279        assert_eq!(
1280            parse_import_references("import { runAlias as runLocal } from './worker';"),
1281            vec![reference(
1282                ImportSyntax::EcmaScript,
1283                "./worker",
1284                Some("runAlias"),
1285                "runLocal",
1286            )]
1287        );
1288        assert_eq!(
1289            parse_import_references("from package.reader import read as load, close"),
1290            vec![
1291                reference(
1292                    ImportSyntax::Python,
1293                    "package.reader",
1294                    Some("close"),
1295                    "close"
1296                ),
1297                reference(ImportSyntax::Python, "package.reader", Some("read"), "load"),
1298            ]
1299        );
1300        assert_eq!(
1301            parse_import_references("from package.worker import run_alias as execute"),
1302            vec![reference(
1303                ImportSyntax::Python,
1304                "package.worker",
1305                Some("run_alias"),
1306                "execute",
1307            )]
1308        );
1309        assert!(parse_import_references("import { broken from './reader'").is_empty());
1310        assert!(parse_import_references("use crate::worker::run as ;").is_empty());
1311        assert!(parse_import_references("import { read } from module './reader'").is_empty());
1312        for malformed in [
1313            "use crate::worker::{run, nested::{start, stop}};",
1314            "use crate::worker::{run, broken as };",
1315            "import defaultValue, { read } from './reader';",
1316            "import { read, nested { broken } } from './reader';",
1317            "import { read as load as fetch } from './reader';",
1318            "from package.reader import (read, close)",
1319            "from package.reader import read, broken as ",
1320            "import package.reader as reader as other",
1321        ] {
1322            assert!(
1323                parse_import_references(malformed).is_empty(),
1324                "unsupported syntax fabricated references: {malformed}"
1325            );
1326        }
1327    }
1328
1329    #[test]
1330    fn resolves_relative_imports_without_platform_path_rules() {
1331        assert_eq!(
1332            resolve_relative_import_path("src/features/main.ts", "../shared/reader.ts"),
1333            Some("src/shared/reader".to_string())
1334        );
1335        assert_eq!(
1336            resolve_relative_import_path("src/main.ts", "../../outside"),
1337            None
1338        );
1339        assert_eq!(resolve_relative_import_path("src/main.ts", "reader"), None);
1340        assert_eq!(
1341            resolve_relative_import_path("src/foo.bar/main.ts", "./reader"),
1342            Some("src/foo.bar/reader".to_string())
1343        );
1344    }
1345
1346    #[test]
1347    fn real_language_graphs_share_alias_qualified_keys() -> Result<(), Box<dyn Error>> {
1348        let project = project_id(1)?;
1349
1350        let rust_target = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
1351        let rust_caller = extract_symbol_graph(
1352            "src/main.rs",
1353            Some("rust"),
1354            "use crate::worker::run as execute;\nfn main() { execute(); }\n",
1355        );
1356        assert_alias_resolution(
1357            project,
1358            Some("atlas"),
1359            &rust_target,
1360            "run",
1361            &rust_caller,
1362            &["execute"],
1363        )?;
1364        let rust_module_target = extract_symbol_graph(
1365            "src/config.rs",
1366            Some("rust"),
1367            "pub fn load_timeout_millis() -> u64 { 250 }\n",
1368        );
1369        let rust_module_caller = extract_symbol_graph(
1370            "src/handler.rs",
1371            Some("rust"),
1372            "use crate::config;\npub fn health_response() { config::load_timeout_millis(); }\n",
1373        );
1374        assert_alias_resolution(
1375            project,
1376            Some("atlas"),
1377            &rust_module_target,
1378            "load_timeout_millis",
1379            &rust_module_caller,
1380            &["config::load_timeout_millis"],
1381        )?;
1382        let rust_callback_caller = extract_symbol_graph(
1383            "src/router.rs",
1384            Some("rust"),
1385            "use crate::handler;\nfn dispatch(path: &str) -> Option<()> { (path == \"/health\").then(handler::health_response) }\n",
1386        );
1387        assert_alias_resolution(
1388            project,
1389            Some("atlas"),
1390            &rust_module_caller,
1391            "health_response",
1392            &rust_callback_caller,
1393            &["handler::health_response"],
1394        )?;
1395        let user_defined_then = extract_symbol_graph(
1396            "src/scheduler.rs",
1397            Some("rust"),
1398            "use crate::handler;\nstruct Scheduler;\nimpl Scheduler { fn then(self, callback: fn()) { let _ = callback; } }\nfn schedule(scheduler: Scheduler) { scheduler.then(handler::health_response); }\n",
1399        );
1400        if user_defined_then.relations.iter().any(|relation| {
1401            relation.kind == RelationKind::Calls
1402                && relation.target_name == "handler::health_response"
1403        }) {
1404            return Err(io::Error::other(
1405                "a user-defined then method fabricated a callback call edge",
1406            )
1407            .into());
1408        }
1409
1410        let typescript_target = extract_symbol_graph(
1411            "src/shared/reader.ts",
1412            Some("typescript"),
1413            "export function read() {}\n",
1414        );
1415        let typescript_caller = extract_symbol_graph(
1416            "src/features/main.ts",
1417            Some("typescript"),
1418            "import { read as load } from '../shared/reader';\nimport * as reader from '../shared/reader';\nexport function main() { load(); reader.read(); }\n",
1419        );
1420        assert_alias_resolution(
1421            project,
1422            Some("web"),
1423            &typescript_target,
1424            "read",
1425            &typescript_caller,
1426            &["load", "reader.read"],
1427        )?;
1428
1429        let dotted_directory_target = extract_symbol_graph(
1430            "src/foo.bar/reader.ts",
1431            Some("typescript"),
1432            "export function read() {}\n",
1433        );
1434        let dotted_directory_caller = extract_symbol_graph(
1435            "src/foo.bar/main.ts",
1436            Some("typescript"),
1437            "import { read } from './reader';\nexport function main() { read(); }\n",
1438        );
1439        assert_alias_resolution(
1440            project,
1441            Some("web"),
1442            &dotted_directory_target,
1443            "read",
1444            &dotted_directory_caller,
1445            &["read"],
1446        )?;
1447
1448        let python_target = extract_symbol_graph(
1449            "src/package/reader.py",
1450            Some("python"),
1451            "def read():\n    pass\n",
1452        );
1453        let python_caller = extract_symbol_graph(
1454            "src/main.py",
1455            Some("python"),
1456            "import package.reader as reader\nreader.read()\n",
1457        );
1458        assert_alias_resolution(
1459            project,
1460            Some("python-app"),
1461            &python_target,
1462            "read",
1463            &python_caller,
1464            &["reader.read"],
1465        )?;
1466        Ok(())
1467    }
1468
1469    #[test]
1470    fn configured_aliases_share_file_and_declaration_keys_across_ecmascript_hosts()
1471    -> Result<(), Box<dyn Error>> {
1472        let project = project_id(21)?;
1473        let configured = ConfiguredModuleResolution::new(vec![
1474            EcmaScriptModuleConfig::new(
1475                "tsconfig.json",
1476                EcmaScriptConfigKind::TypeScript,
1477                Some("src".to_string()),
1478                vec![
1479                    EcmaScriptPathMapping::new("@/*", vec!["src/*".to_string()])?,
1480                    EcmaScriptPathMapping::new("@index/*", vec!["src/*/index.ts".to_string()])?,
1481                ],
1482            )?,
1483            EcmaScriptModuleConfig::new(
1484                "jsconfig.json",
1485                EcmaScriptConfigKind::JavaScript,
1486                Some("src".to_string()),
1487                vec![EcmaScriptPathMapping::new(
1488                    "@/*",
1489                    vec!["src/*".to_string()],
1490                )?],
1491            )?,
1492        ])?;
1493        let context = ResolutionProjectionContext::with_configured_modules(&configured);
1494
1495        for (target_path, language, target_source, caller_path, caller_source) in [
1496            (
1497                "src/controller.ts",
1498                "typescript",
1499                "export function useController(): string { return 'ok'; }\n",
1500                "src/page.ts",
1501                "import { useController } from '@/controller';\nexport const value = useController();\n",
1502            ),
1503            (
1504                "src/controller.ts",
1505                "typescript",
1506                "export function useController(): string { return 'ok'; }\n",
1507                "src/page.tsx",
1508                "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1509            ),
1510            (
1511                "src/controller.js",
1512                "javascript",
1513                "export function useController() { return 'ok'; }\n",
1514                "src/page.jsx",
1515                "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1516            ),
1517            (
1518                "src/controller.ts",
1519                "typescript",
1520                "export function useController(): string { return 'ok'; }\n",
1521                "src/Page.vue",
1522                "<script setup lang=\"ts\">\nimport { useController } from '@/controller';\nconst value = useController();\n</script>\n<template><div>{{ value }}</div></template>\n",
1523            ),
1524        ] {
1525            let target = extract_symbol_graph(target_path, Some(language), target_source);
1526            let caller_extension = std::path::Path::new(caller_path)
1527                .extension()
1528                .and_then(std::ffi::OsStr::to_str);
1529            let caller_language = match caller_extension {
1530                Some(extension) if extension.eq_ignore_ascii_case("vue") => "vue",
1531                Some(extension)
1532                    if extension.eq_ignore_ascii_case("js")
1533                        || extension.eq_ignore_ascii_case("jsx") =>
1534                {
1535                    "javascript"
1536                }
1537                Some(extension) if extension.eq_ignore_ascii_case("tsx") => "tsx",
1538                _ => "typescript",
1539            };
1540            let caller = extract_symbol_graph(caller_path, Some(caller_language), caller_source);
1541            let target_projection = derive_resolution_keys(project, Some("shared"), &target)?;
1542            let caller_projection =
1543                derive_resolution_keys_with_context(project, Some("app"), &caller, context)?;
1544            let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1545            require(
1546                imports
1547                    .iter()
1548                    .any(|keys| keys_intersect(keys, target_projection.source_keys())),
1549                "configured import did not share the target file key",
1550            )?;
1551            require(
1552                keys_intersect(
1553                    relation_keys(&caller, &caller_projection, "useController"),
1554                    symbol_keys(&target, &target_projection, "useController"),
1555                ),
1556                "configured call did not share the target declaration key",
1557            )?;
1558        }
1559
1560        let index_target = extract_symbol_graph(
1561            "src/tools/index.ts",
1562            Some("typescript"),
1563            "export function useTool() { return 'ok'; }\n",
1564        );
1565        let index_caller = extract_symbol_graph(
1566            "src/index-page.ts",
1567            Some("typescript"),
1568            "import { useTool } from '@index/tools';\nexport const value = useTool();\n",
1569        );
1570        let index_target_projection = derive_resolution_keys(project, Some("app"), &index_target)?;
1571        let index_caller_projection =
1572            derive_resolution_keys_with_context(project, Some("app"), &index_caller, context)?;
1573        require(
1574            relation_keys_of_kind(
1575                &index_caller,
1576                &index_caller_projection,
1577                RelationKind::Imports,
1578            )
1579            .iter()
1580            .any(|keys| keys_intersect(keys, index_target_projection.source_keys())),
1581            "configured index target did not reuse package-entry source keys",
1582        )?;
1583        require(
1584            keys_intersect(
1585                relation_keys(&index_caller, &index_caller_projection, "useTool"),
1586                symbol_keys(&index_target, &index_target_projection, "useTool"),
1587            ),
1588            "configured index target did not reuse declaration keys",
1589        )?;
1590
1591        let extension_caller = extract_symbol_graph(
1592            "src/extension-page.ts",
1593            Some("typescript"),
1594            "import { useController } from '@/controller.ts';\nexport const value = useController();\n",
1595        );
1596        let extension_target = extract_symbol_graph(
1597            "src/controller.ts",
1598            Some("typescript"),
1599            "export function useController(): string { return 'ok'; }\n",
1600        );
1601        let extension_caller_projection =
1602            derive_resolution_keys_with_context(project, Some("app"), &extension_caller, context)?;
1603        let extension_target_projection =
1604            derive_resolution_keys(project, Some("app"), &extension_target)?;
1605        require(
1606            relation_keys_of_kind(
1607                &extension_caller,
1608                &extension_caller_projection,
1609                RelationKind::Imports,
1610            )
1611            .iter()
1612            .any(|keys| keys_intersect(keys, extension_target_projection.source_keys())),
1613            "configured extension target did not reuse extensionless source keys",
1614        )?;
1615
1616        let root_configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1617            "tsconfig.json",
1618            EcmaScriptConfigKind::TypeScript,
1619            Some(String::new()),
1620            Vec::new(),
1621        )?])?;
1622        let root_context = ResolutionProjectionContext::with_configured_modules(&root_configured);
1623        let root_target = extract_symbol_graph(
1624            "controller.ts",
1625            Some("typescript"),
1626            "export function useController() { return 'root'; }\n",
1627        );
1628        let nested_same_name = extract_symbol_graph(
1629            "packages/app/src/controller.ts",
1630            Some("typescript"),
1631            "export function useController() { return 'nested'; }\n",
1632        );
1633        let root_caller = extract_symbol_graph(
1634            "page.ts",
1635            Some("typescript"),
1636            "import { useController } from 'controller';\nexport const value = useController();\n",
1637        );
1638        let root_target_projection = derive_resolution_keys(project, Some("shared"), &root_target)?;
1639        let nested_projection = derive_resolution_keys(project, Some("nested"), &nested_same_name)?;
1640        let root_caller_projection =
1641            derive_resolution_keys_with_context(project, Some("app"), &root_caller, root_context)?;
1642        let configured_imports =
1643            relation_keys_of_kind(&root_caller, &root_caller_projection, RelationKind::Imports);
1644        require(
1645            configured_imports
1646                .iter()
1647                .any(|keys| keys_intersect(keys, root_target_projection.source_keys())),
1648            "root baseUrl import did not resolve its repository-exact target",
1649        )?;
1650        require(
1651            configured_imports
1652                .iter()
1653                .all(|keys| !keys_intersect(keys, nested_projection.source_keys())),
1654            "root baseUrl import also matched a nested basename alias",
1655        )?;
1656        let configured_call = relation_keys(&root_caller, &root_caller_projection, "useController");
1657        require(
1658            keys_intersect(
1659                configured_call,
1660                symbol_keys(&root_target, &root_target_projection, "useController"),
1661            ),
1662            "root baseUrl call did not resolve its repository-exact declaration",
1663        )?;
1664        require(
1665            !keys_intersect(
1666                configured_call,
1667                symbol_keys(&nested_same_name, &nested_projection, "useController"),
1668            ),
1669            "root baseUrl call also matched a nested basename alias",
1670        )?;
1671        Ok(())
1672    }
1673
1674    #[test]
1675    fn configured_import_scopes_are_expanded_once_per_source_graph() -> Result<(), Box<dyn Error>> {
1676        let configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1677            "tsconfig.json",
1678            EcmaScriptConfigKind::TypeScript,
1679            Some("src".to_string()),
1680            vec![EcmaScriptPathMapping::new(
1681                "@/*",
1682                vec!["src/*".to_string()],
1683            )?],
1684        )?])?;
1685        let graph = extract_symbol_graph(
1686            "src/page.ts",
1687            Some("typescript"),
1688            "import { useController } from '@/controller';\n\
1689             export const first = useController();\n\
1690             export const second = useController();\n",
1691        );
1692        let parsed_imports = graph
1693            .relations
1694            .iter()
1695            .map(|relation| {
1696                if relation.kind == RelationKind::Imports {
1697                    crate::semantic::parse_imports(
1698                        SemanticProviderOwner::EcmaScript,
1699                        &relation.target_name,
1700                    )
1701                } else {
1702                    Vec::new()
1703                }
1704            })
1705            .collect::<Vec<_>>();
1706        let configured_source = configured.for_source(&graph.path);
1707        let cache = build_import_scope_cache(
1708            SemanticProviderOwner::EcmaScript,
1709            &graph,
1710            &parsed_imports,
1711            Some(&configured_source),
1712        );
1713        let modules = cache
1714            .get("src/page.ts")
1715            .ok_or_else(|| io::Error::other("source graph omitted its import-scope cache"))?;
1716        if modules.len() != 1 || modules.get("@/controller") != Some(&vec!["src/controller".into()])
1717        {
1718            return Err(io::Error::other(
1719                "repeated imported calls did not reuse one configured scope expansion",
1720            )
1721            .into());
1722        }
1723        Ok(())
1724    }
1725
1726    #[test]
1727    fn imports_target_modules_and_qualified_calls_target_members() -> Result<(), Box<dyn Error>> {
1728        let project = project_id(10)?;
1729        let target = extract_symbol_graph(
1730            "src/shared/reader.ts",
1731            Some("typescript"),
1732            "export function read() {}\nexport function close() {}\nexport function fetch() {}\nexport const client = { fetch() {} };\n",
1733        );
1734        let caller = extract_symbol_graph(
1735            "src/features/main.ts",
1736            Some("typescript"),
1737            "import { read, close, client } from '../shared/reader';\nexport function main() { read(); close(); client.fetch(); }\n",
1738        );
1739        let target_projection = derive_resolution_keys(project, Some("web"), &target)?;
1740        let caller_projection = derive_resolution_keys(project, Some("web"), &caller)?;
1741        let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1742        require(
1743            imports.len() == 1,
1744            "one import emitted more than one module target",
1745        )?;
1746        require(
1747            imports[0]
1748                .iter()
1749                .all(|key| key.domain() == projectatlas_core::graph::ResolutionKeyDomain::Module),
1750            "named imports emitted declaration resolution targets",
1751        )?;
1752        require(
1753            keys_intersect(imports[0], target_projection.source_keys()),
1754            "named import did not target its source module",
1755        )?;
1756        for symbol in ["read", "close"] {
1757            require(
1758                !keys_intersect(imports[0], symbol_keys(&target, &target_projection, symbol)),
1759                "module import also targeted a named declaration",
1760            )?;
1761        }
1762        assert_alias_resolution(project, Some("web"), &target, "read", &caller, &["read"])?;
1763        assert_alias_resolution(project, Some("web"), &target, "close", &caller, &["close"])?;
1764        let qualified_call = relation_keys(&caller, &caller_projection, "client.fetch");
1765        let fetch_candidates = target_projection
1766            .symbol_keys()
1767            .iter()
1768            .filter(|entry| target.symbols[entry.symbol_index()].name == "fetch")
1769            .map(|entry| {
1770                (
1771                    target.symbols[entry.symbol_index()].parent.as_deref(),
1772                    entry.keys(),
1773                )
1774            })
1775            .collect::<Vec<_>>();
1776        require(
1777            fetch_candidates.len() == 2,
1778            "qualified-call fixture lost one fetch declaration",
1779        )?;
1780        require(
1781            fetch_candidates.iter().any(|(parent, keys)| {
1782                *parent == Some("client") && keys_intersect(qualified_call, keys)
1783            }),
1784            "qualified call did not target the imported parent member",
1785        )?;
1786        require(
1787            fetch_candidates
1788                .iter()
1789                .all(|(parent, keys)| parent.is_some() || !keys_intersect(qualified_call, keys)),
1790            "qualified call also targeted a same-module top-level declaration",
1791        )
1792    }
1793
1794    #[test]
1795    fn actual_complex_import_syntax_abstains_instead_of_fabricating_keys()
1796    -> Result<(), Box<dyn Error>> {
1797        let project = project_id(15)?;
1798        for (path, language, source) in [
1799            (
1800                "src/main.rs",
1801                "rust",
1802                "use crate::worker::{run, nested::{start, stop}};\nfn main() { run(); }\n",
1803            ),
1804            (
1805                "src/main.ts",
1806                "typescript",
1807                "import defaultValue, { read } from './reader';\ndefaultValue();\n",
1808            ),
1809            (
1810                "src/main.py",
1811                "python",
1812                "from package.reader import (read, close)\nread()\n",
1813            ),
1814        ] {
1815            let graph = extract_symbol_graph(path, Some(language), source);
1816            let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1817            let imports = relation_keys_of_kind(&graph, &projection, RelationKind::Imports);
1818            require(
1819                !imports.is_empty(),
1820                "complex-import fixture did not reach provider normalization",
1821            )?;
1822            require(
1823                imports.iter().all(|keys| keys.is_empty()),
1824                "unsupported complex import syntax fabricated resolution keys",
1825            )?;
1826        }
1827        Ok(())
1828    }
1829
1830    #[test]
1831    fn provider_scopes_abstain_instead_of_matching_packages_or_wrong_siblings()
1832    -> Result<(), Box<dyn Error>> {
1833        let project = project_id(11)?;
1834        let correct_typescript = extract_symbol_graph(
1835            "src/a/reader.ts",
1836            Some("typescript"),
1837            "export function read() {}\n",
1838        );
1839        let wrong_typescript = extract_symbol_graph(
1840            "src/b/reader.ts",
1841            Some("typescript"),
1842            "export function read() {}\n",
1843        );
1844        let typescript_caller = extract_symbol_graph(
1845            "src/main.ts",
1846            Some("typescript"),
1847            "import { read } from './a/reader';\nread();\n",
1848        );
1849        assert_import_matches_only_source(
1850            project,
1851            Some("web"),
1852            &typescript_caller,
1853            &correct_typescript,
1854            &wrong_typescript,
1855        )?;
1856
1857        let package_caller = extract_symbol_graph(
1858            "src/package.ts",
1859            Some("typescript"),
1860            "import { read } from 'reader-package';\nread();\n",
1861        );
1862        let package_projection = derive_resolution_keys(project, Some("web"), &package_caller)?;
1863        require(
1864            relation_keys_of_kind(&package_caller, &package_projection, RelationKind::Imports)
1865                .iter()
1866                .all(|keys| keys.is_empty()),
1867            "bare ECMAScript package import entered a repository-local module scope",
1868        )?;
1869        require(
1870            relation_keys(&package_caller, &package_projection, "read").is_empty(),
1871            "call through an unresolved package import fell back to a global declaration",
1872        )?;
1873
1874        for (path, language, source, target) in [
1875            (
1876                "src/unimported.ts",
1877                "typescript",
1878                "export function run() { return client.fetch(); }\n",
1879                "client.fetch",
1880            ),
1881            (
1882                "src/unimported.rs",
1883                "rust",
1884                "pub fn run() { worker::execute(); }\n",
1885                "worker::execute",
1886            ),
1887            (
1888                "src/unimported.py",
1889                "python",
1890                "def run():\n    return reader.read()\n",
1891                "reader.read",
1892            ),
1893        ] {
1894            let graph = extract_symbol_graph(path, Some(language), source);
1895            let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1896            require(
1897                graph.relations.iter().any(|relation| {
1898                    relation.kind == RelationKind::Calls && relation.target_name == target
1899                }),
1900                "unimported qualified-call fixture did not reach semantic normalization",
1901            )?;
1902            require(
1903                relation_keys(&graph, &projection, target).is_empty(),
1904                "unimported qualified call fell back to a project basename scope",
1905            )?;
1906        }
1907
1908        let rust_target =
1909            extract_symbol_graph("src/feature/worker.rs", Some("rust"), "pub fn run() {}\n");
1910        let wrong_rust =
1911            extract_symbol_graph("src/other/worker.rs", Some("rust"), "pub fn run() {}\n");
1912        let rust_caller = extract_symbol_graph(
1913            "src/feature/main.rs",
1914            Some("rust"),
1915            "use super::worker::run;\nfn main() { run(); }\n",
1916        );
1917        let rust_crate_caller = extract_symbol_graph(
1918            "src/main.rs",
1919            Some("rust"),
1920            "use crate::feature::worker::run;\nfn main() { run(); }\n",
1921        );
1922        let rust_self_caller = extract_symbol_graph(
1923            "src/feature/mod.rs",
1924            Some("rust"),
1925            "use self::worker::run;\nfn main() { run(); }\n",
1926        );
1927        for caller in [&rust_caller, &rust_crate_caller, &rust_self_caller] {
1928            assert_import_matches_only_source(
1929                project,
1930                Some("atlas"),
1931                caller,
1932                &rust_target,
1933                &wrong_rust,
1934            )?;
1935        }
1936
1937        let python_target = extract_symbol_graph(
1938            "src/package/reader.py",
1939            Some("python"),
1940            "def read():\n    pass\n",
1941        );
1942        let wrong_python = extract_symbol_graph(
1943            "src/other/reader.py",
1944            Some("python"),
1945            "def read():\n    pass\n",
1946        );
1947        let python_caller = extract_symbol_graph(
1948            "src/package/main.py",
1949            Some("python"),
1950            "from .reader import read\nread()\n",
1951        );
1952        assert_import_matches_only_source(
1953            project,
1954            Some("python-app"),
1955            &python_caller,
1956            &python_target,
1957            &wrong_python,
1958        )?;
1959        let parent_python_target = extract_symbol_graph(
1960            "src/package/reader.py",
1961            Some("python"),
1962            "def read():\n    pass\n",
1963        );
1964        let parent_python_wrong = extract_symbol_graph(
1965            "src/package/sub/reader.py",
1966            Some("python"),
1967            "def read():\n    pass\n",
1968        );
1969        let parent_python_caller = extract_symbol_graph(
1970            "src/package/sub/main.py",
1971            Some("python"),
1972            "from ..reader import read\nread()\n",
1973        );
1974        assert_import_matches_only_source(
1975            project,
1976            Some("python-app"),
1977            &parent_python_caller,
1978            &parent_python_target,
1979            &parent_python_wrong,
1980        )
1981    }
1982
1983    #[test]
1984    fn direct_provider_families_retain_duplicate_ambiguity_keys() -> Result<(), Box<dyn Error>> {
1985        let project = project_id(12)?;
1986        for (path, language, target_source, caller_path, caller_source, target_name) in [
1987            (
1988                "src/shared/reader.ts",
1989                "typescript",
1990                "export function read() {}\nexport function read() {}\n",
1991                "src/main.ts",
1992                "import { read } from './shared/reader';\nread();\n",
1993                "read",
1994            ),
1995            (
1996                "src/package/reader.py",
1997                "python",
1998                "def read():\n    pass\ndef read():\n    pass\n",
1999                "src/main.py",
2000                "from package.reader import read\nread()\n",
2001                "read",
2002            ),
2003        ] {
2004            let target = extract_symbol_graph(path, Some(language), target_source);
2005            let caller = extract_symbol_graph(caller_path, Some(language), caller_source);
2006            let target_projection = derive_resolution_keys(project, Some("app"), &target)?;
2007            let caller_projection = derive_resolution_keys(project, Some("app"), &caller)?;
2008            let duplicate_keys = target_projection
2009                .symbol_keys()
2010                .iter()
2011                .filter(|entry| target.symbols[entry.symbol_index()].name == target_name)
2012                .map(super::SymbolResolutionKeys::keys)
2013                .collect::<Vec<_>>();
2014            require(
2015                duplicate_keys.len() == 2,
2016                "duplicate declarations were not both projected",
2017            )?;
2018            let dependency = relation_keys(&caller, &caller_projection, target_name);
2019            require(
2020                duplicate_keys
2021                    .iter()
2022                    .all(|keys| keys_intersect(dependency, keys)),
2023                "call dependency did not retain every duplicate declaration candidate",
2024            )?;
2025        }
2026        Ok(())
2027    }
2028
2029    #[test]
2030    fn resolution_identity_separates_provider_from_cross_dialect_family()
2031    -> Result<(), Box<dyn Error>> {
2032        let project = project_id(13)?;
2033        let typescript = extract_symbol_graph(
2034            "src/shared/reader.ts",
2035            Some("typescript"),
2036            "export function read() {}\n",
2037        );
2038        let javascript = extract_symbol_graph(
2039            "src/shared/reader.ts",
2040            Some("javascript"),
2041            "export function read() {}\n",
2042        );
2043        let typescript_projection = derive_resolution_keys(project, Some("web"), &typescript)?;
2044        let javascript_projection = derive_resolution_keys(project, Some("web"), &javascript)?;
2045        require(
2046            typescript_projection.source_keys() == javascript_projection.source_keys(),
2047            "ECMAScript-compatible dialects entered different resolution families",
2048        )?;
2049        let provider = projectatlas_core::graph::GraphIdentityText::new("ecma-script")?;
2050        let family = projectatlas_core::graph::GraphIdentityText::new("ecmascript")?;
2051        let package = projectatlas_core::graph::GraphIdentityText::new("web")?;
2052        let identity = projectatlas_core::graph::GraphIdentityText::new("src/shared/reader")?;
2053        let expected = CanonicalResolutionKey::new(
2054            project,
2055            projectatlas_core::graph::ResolutionKeyDomain::Module,
2056            &provider,
2057            &family,
2058            Some(&package),
2059            None,
2060            Some(projectatlas_core::graph::GraphRelationKind::from_legacy(
2061                RelationKind::Imports,
2062            )),
2063            &identity,
2064        );
2065        require(
2066            typescript_projection.source_keys().contains(&expected),
2067            "provider owner and cross-dialect family were not projected independently",
2068        )
2069    }
2070
2071    #[test]
2072    fn embedded_hosts_publish_module_keys_only_for_admitted_component_facts()
2073    -> Result<(), Box<dyn Error>> {
2074        let project = project_id(14)?;
2075        for (path, language, source) in [
2076            (
2077                "page.html",
2078                "html",
2079                "<script>export function run() {}</script>",
2080            ),
2081            ("empty.vue", "vue", "<template><p>empty</p></template>"),
2082            (
2083                "external.vue",
2084                "vue",
2085                "<script src='./external.js'></script>",
2086            ),
2087            ("empty.svelte", "svelte", "<p>empty</p>"),
2088            (
2089                "external.svelte",
2090                "svelte",
2091                "<script src='./external.js'></script>",
2092            ),
2093            (
2094                "external-inline.vue",
2095                "vue",
2096                "<script>import * as fs from 'node:fs';</script>",
2097            ),
2098            (
2099                "external-inline.svelte",
2100                "svelte",
2101                "<script>import * as fs from 'node:fs';</script>",
2102            ),
2103            ("malformed.vue", "vue", "<script"),
2104            ("malformed.svelte", "svelte", "<script"),
2105        ] {
2106            let graph = extract_symbol_graph(path, Some(language), source);
2107            let projection = derive_resolution_keys(project, Some("web"), &graph)?;
2108            require(
2109                projection.source_keys().is_empty(),
2110                "host without an admitted component module surface emitted source keys",
2111            )?;
2112            if path.starts_with("external-inline") {
2113                require(
2114                    relation_keys_of_kind(&graph, &projection, RelationKind::Imports).len() == 1,
2115                    "external-only component lost its outbound import relationship",
2116                )?;
2117            }
2118        }
2119        for (path, language) in [("component.vue", "vue"), ("component.svelte", "svelte")] {
2120            let graph = extract_symbol_graph(
2121                path,
2122                Some(language),
2123                "<script>export function run() {}</script>",
2124            );
2125            require(
2126                !derive_resolution_keys(project, Some("web"), &graph)?
2127                    .source_keys()
2128                    .is_empty(),
2129                "admitted component script facts did not expose their host module",
2130            )?;
2131        }
2132        Ok(())
2133    }
2134
2135    #[test]
2136    fn canonical_projection_is_stable_and_retains_ambiguous_unresolved_and_package_keys()
2137    -> Result<(), Box<dyn Error>> {
2138        let project = project_id(2)?;
2139        let first = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
2140        let moved = extract_symbol_graph(
2141            "src/worker.rs",
2142            Some("rust"),
2143            "\n\n// formatting moved the declaration\npub fn run() {}\n",
2144        );
2145        let first_projection = derive_resolution_keys(project, Some("atlas"), &first)?;
2146        let moved_projection = derive_resolution_keys(project, Some("atlas"), &moved)?;
2147        require(
2148            symbol_keys(&first, &first_projection, "run")
2149                == symbol_keys(&moved, &moved_projection, "run"),
2150            "formatting-only line movement changed canonical symbol identity",
2151        )?;
2152        require(
2153            first_projection.source_keys()
2154                != derive_resolution_keys(project_id(3)?, Some("atlas"), &first)?.source_keys(),
2155            "different projects produced the same canonical source identity",
2156        )?;
2157        require(
2158            symbol_keys(&first, &first_projection, "run")
2159                != symbol_keys(
2160                    &first,
2161                    &derive_resolution_keys(project, Some("other-package"), &first)?,
2162                    "run",
2163                ),
2164            "different packages produced the same canonical declaration identity",
2165        )?;
2166
2167        let duplicate = extract_symbol_graph("src/other.rs", Some("rust"), "pub fn run() {}\n");
2168        let caller = extract_symbol_graph(
2169            "src/main.rs",
2170            Some("rust"),
2171            "fn main() { run(); missing(); }\n",
2172        );
2173        let duplicate_projection = derive_resolution_keys(project, Some("atlas"), &duplicate)?;
2174        let caller_projection = derive_resolution_keys(project, Some("atlas"), &caller)?;
2175        let run_dependencies = relation_keys(&caller, &caller_projection, "run");
2176        require(
2177            keys_intersect(
2178                run_dependencies,
2179                symbol_keys(&first, &first_projection, "run"),
2180            ),
2181            "ambiguous call did not retain the first matching declaration key",
2182        )?;
2183        require(
2184            keys_intersect(
2185                run_dependencies,
2186                symbol_keys(&duplicate, &duplicate_projection, "run"),
2187            ),
2188            "ambiguous call did not retain the duplicate declaration key",
2189        )?;
2190        let missing_dependencies = relation_keys(&caller, &caller_projection, "missing");
2191        require(
2192            !missing_dependencies.is_empty(),
2193            "unresolved call lost its canonical dependency key",
2194        )?;
2195        require(
2196            !keys_intersect(
2197                missing_dependencies,
2198                symbol_keys(&first, &first_projection, "run"),
2199            ),
2200            "unresolved call matched an unrelated declaration key",
2201        )?;
2202
2203        let package = extract_symbol_graph(
2204            "vendor/library/Cargo.toml",
2205            Some("cargo-manifest"),
2206            "[package]\nname = \"library\"\n",
2207        );
2208        let manifest = extract_symbol_graph(
2209            "Cargo.toml",
2210            Some("cargo-manifest"),
2211            "[package]\nname = \"app\"\n[dependencies]\nlibrary = \"1\"\n",
2212        );
2213        let package_projection = derive_resolution_keys(project, None, &package)?;
2214        let manifest_projection = derive_resolution_keys(project, None, &manifest)?;
2215        require(
2216            package_projection.source_keys().is_empty()
2217                && manifest_projection.source_keys().is_empty(),
2218            "Cargo manifests advertised ordinary source-module identities",
2219        )?;
2220        require(
2221            keys_intersect(
2222                relation_keys(&manifest, &manifest_projection, "library"),
2223                symbol_keys(&package, &package_projection, "library"),
2224            ),
2225            "Cargo dependency did not match its package declaration key",
2226        )?;
2227        Ok(())
2228    }
2229
2230    #[test]
2231    fn cargo_provider_abstains_on_malformed_input_and_retains_duplicate_package_candidates()
2232    -> Result<(), Box<dyn Error>> {
2233        let project = project_id(16)?;
2234        let malformed = extract_symbol_graph(
2235            "Cargo.toml",
2236            Some("cargo-manifest"),
2237            "[package\nname = \"broken\"\n",
2238        );
2239        let malformed_projection = derive_resolution_keys(project, None, &malformed)?;
2240        require(
2241            malformed_projection.source_keys().is_empty()
2242                && malformed_projection.symbol_keys().is_empty()
2243                && malformed_projection.relation_keys().is_empty(),
2244            "malformed Cargo input fabricated semantic identities",
2245        )?;
2246
2247        let first = extract_symbol_graph(
2248            "vendor/first/Cargo.toml",
2249            Some("cargo-manifest"),
2250            "[package]\nname = \"shared-package\"\n",
2251        );
2252        let second = extract_symbol_graph(
2253            "vendor/second/Cargo.toml",
2254            Some("cargo-manifest"),
2255            "[package]\nname = \"shared-package\"\n",
2256        );
2257        let caller = extract_symbol_graph(
2258            "Cargo.toml",
2259            Some("cargo-manifest"),
2260            "[package]\nname = \"app\"\n[dependencies]\nshared-package = \"1\"\n",
2261        );
2262        let first_projection = derive_resolution_keys(project, None, &first)?;
2263        let second_projection = derive_resolution_keys(project, None, &second)?;
2264        let caller_projection = derive_resolution_keys(project, None, &caller)?;
2265        let dependency = relation_keys(&caller, &caller_projection, "shared-package");
2266        require(
2267            keys_intersect(
2268                dependency,
2269                symbol_keys(&first, &first_projection, "shared-package"),
2270            ) && keys_intersect(
2271                dependency,
2272                symbol_keys(&second, &second_projection, "shared-package"),
2273            ),
2274            "duplicate Cargo packages did not remain ambiguity candidates",
2275        )
2276    }
2277
2278    #[test]
2279    fn projection_rejects_invalid_identity_and_excessive_key_fanout() -> Result<(), Box<dyn Error>>
2280    {
2281        let graph = extract_symbol_graph("src/lib.rs", Some("rust"), "pub fn run() {}\n");
2282        require(
2283            matches!(
2284                derive_resolution_keys(project_id(4)?, Some("bad\0package"), &graph),
2285                Err(ResolutionProjectionError::Contract(_))
2286            ),
2287            "invalid package identity was accepted",
2288        )?;
2289
2290        let imports = (0..65)
2291            .map(|index| format!("import {{ run as execute }} from './module{index}';"))
2292            .collect::<Vec<_>>()
2293            .join("\n");
2294        let source = format!("{imports}\nexecute();\n");
2295        let graph = extract_symbol_graph("src/main.ts", Some("typescript"), &source);
2296        require(
2297            matches!(
2298                derive_resolution_keys(project_id(4)?, Some("web"), &graph),
2299                Err(ResolutionProjectionError::KeyLimit {
2300                    fact: "relation",
2301                    ..
2302                })
2303            ),
2304            "excessive relation-key fan-out was not rejected",
2305        )?;
2306        Ok(())
2307    }
2308
2309    #[test]
2310    fn contract_failure_retains_valid_resolution_projection_siblings() -> Result<(), Box<dyn Error>>
2311    {
2312        let path = format!("pkg/{0}/{0}/{0}/{0}/page.rs", "d".repeat(100));
2313        let imported = "i".repeat(3_800);
2314        let mut graph = extract_symbol_graph(
2315            &path,
2316            Some("rust"),
2317            "pub fn caller() { helper(); }\npub fn helper() {}\n",
2318        );
2319        graph
2320            .relations
2321            .retain(|relation| relation.kind == RelationKind::Calls);
2322        graph.relations.push(SymbolRelation {
2323            path,
2324            source_name: "<module>".to_string(),
2325            target_name: format!("use self::{{{imported}}};"),
2326            kind: RelationKind::Imports,
2327            line: 1,
2328            context: "partial projection fixture".to_string(),
2329            parser: ParserKind::TreeSitter,
2330        });
2331        graph.relations.push(SymbolRelation {
2332            path: graph.path.clone(),
2333            source_name: "caller".to_string(),
2334            target_name: format!("{imported}.member"),
2335            kind: RelationKind::Calls,
2336            line: 3,
2337            context: "partial projection fixture".to_string(),
2338            parser: ParserKind::TreeSitter,
2339        });
2340
2341        let result = derive_resolution_keys(project_id(23)?, None, &graph);
2342        let Err(ResolutionProjectionError::Contract(failure)) = result else {
2343            return Err(io::Error::other(
2344                "invalid derived resolution key did not retain a typed contract failure",
2345            )
2346            .into());
2347        };
2348        require(
2349            !failure.projection().source_keys().is_empty(),
2350            "contract failure discarded valid source resolution keys",
2351        )?;
2352        require(
2353            !failure.projection().symbol_keys().is_empty(),
2354            "contract failure discarded valid symbol resolution keys",
2355        )?;
2356        require(
2357            failure
2358                .projection()
2359                .relation_keys()
2360                .iter()
2361                .any(|relation| relation.relation_index() == 0 && !relation.keys().is_empty()),
2362            "contract failure discarded a valid sibling relation key",
2363        )?;
2364        require(
2365            failure
2366                .failures()
2367                .iter()
2368                .all(|failure| !format!("{:?}", failure.error()).contains(&imported)),
2369            "contract failure retained rejected raw identity text",
2370        )?;
2371        Ok(())
2372    }
2373
2374    #[test]
2375    fn contract_failure_deduplicates_repeated_facts_after_detail_ceiling()
2376    -> Result<(), Box<dyn Error>> {
2377        let mut graph = SymbolGraph {
2378            path: "src/main.rs".to_string(),
2379            language: Some("rust".to_string()),
2380            parser: ParserKind::TreeSitter,
2381            symbols: Vec::with_capacity(super::MAX_RESOLUTION_PROJECTION_FAILURES + 1),
2382            relations: Vec::new(),
2383        };
2384        for index in 0..super::MAX_RESOLUTION_PROJECTION_FAILURES {
2385            graph.symbols.push(CodeSymbol {
2386                path: graph.path.clone(),
2387                language: graph.language.clone(),
2388                name: format!("invalid\0{index}"),
2389                kind: SymbolKind::Function,
2390                signature: "fn invalid()".to_string(),
2391                exported: true,
2392                documentation: None,
2393                line_start: index + 1,
2394                line_end: index + 1,
2395                source_selector: None,
2396                parent: None,
2397                parser: ParserKind::TreeSitter,
2398                detail: None,
2399            });
2400        }
2401        graph.symbols.push(CodeSymbol {
2402            path: graph.path.clone(),
2403            language: graph.language.clone(),
2404            name: "valid_tail".to_string(),
2405            kind: SymbolKind::Function,
2406            signature: "fn valid_tail()".to_string(),
2407            exported: true,
2408            documentation: None,
2409            line_start: graph.symbols.len() + 1,
2410            line_end: graph.symbols.len() + 1,
2411            source_selector: None,
2412            parent: Some("\0invalid-parent".to_string()),
2413            parser: ParserKind::TreeSitter,
2414            detail: None,
2415        });
2416
2417        let Err(ResolutionProjectionError::Contract(failure)) =
2418            derive_resolution_keys(project_id(24)?, None, &graph)
2419        else {
2420            return Err(
2421                io::Error::other("repeated post-ceiling failures were not reported").into(),
2422            );
2423        };
2424        require(
2425            failure.failures().len() == super::MAX_RESOLUTION_PROJECTION_FAILURES,
2426            "detail ceiling retained repeated fact attempts",
2427        )?;
2428        require(
2429            failure.rejected_count() == super::MAX_RESOLUTION_PROJECTION_FAILURES + 1,
2430            "repeated post-ceiling fact inflated rejected count",
2431        )?;
2432        Ok(())
2433    }
2434
2435    #[test]
2436    fn supported_typescript_parser_retains_control_bearing_import_identities()
2437    -> Result<(), Box<dyn Error>> {
2438        let graph = extract_symbol_graph(
2439            "src/page.ts",
2440            Some("typescript"),
2441            "import * as first from './bad\0one';\nimport * as second from './bad\0two';\nexport function caller() { first(); second(); }\n",
2442        );
2443        require(
2444            graph.parser == ParserKind::TreeSitter,
2445            "supported TypeScript fixture did not use the semantic parser",
2446        )?;
2447        require(
2448            graph.relations.iter().any(|relation| {
2449                relation.kind == RelationKind::Imports && relation.target_name.contains('\0')
2450            }),
2451            "supported TypeScript parser did not retain the control-bearing import",
2452        )?;
2453        Ok(())
2454    }
2455
2456    #[test]
2457    fn unsupported_and_lockfile_languages_do_not_gain_generic_name_resolution()
2458    -> Result<(), Box<dyn Error>> {
2459        for graph in [
2460            extract_symbol_graph(
2461                "src/Service.java",
2462                Some("java"),
2463                "public class Service { void run() {} }\n",
2464            ),
2465            extract_symbol_graph(
2466                "Cargo.lock",
2467                Some("cargo-lock"),
2468                "[[package]]\nname = \"dependency\"\nversion = \"1.0.0\"\n",
2469            ),
2470        ] {
2471            let projection = derive_resolution_keys(project_id(9)?, Some("app"), &graph)?;
2472            require(
2473                projection.source_keys().is_empty()
2474                    && projection.symbol_keys().is_empty()
2475                    && projection.relation_keys().is_empty(),
2476                "unsupported language emitted generic resolution keys",
2477            )?;
2478        }
2479        Ok(())
2480    }
2481
2482    fn require(condition: bool, message: &'static str) -> Result<(), Box<dyn Error>> {
2483        if condition {
2484            Ok(())
2485        } else {
2486            Err(io::Error::other(message).into())
2487        }
2488    }
2489
2490    fn project_id(byte: u8) -> Result<ProjectInstanceId, Box<dyn Error>> {
2491        Ok(ProjectInstanceId::from_bytes([byte; 16])?)
2492    }
2493
2494    fn assert_alias_resolution(
2495        project: ProjectInstanceId,
2496        package: Option<&str>,
2497        target: &SymbolGraph,
2498        symbol_name: &str,
2499        caller: &SymbolGraph,
2500        call_targets: &[&str],
2501    ) -> Result<(), Box<dyn Error>> {
2502        let target_projection = derive_resolution_keys(project, package, target)?;
2503        let caller_projection = derive_resolution_keys(project, package, caller)?;
2504        let exports = symbol_keys(target, &target_projection, symbol_name);
2505        for call_target in call_targets {
2506            if !keys_intersect(
2507                relation_keys(caller, &caller_projection, call_target),
2508                exports,
2509            ) {
2510                return Err(io::Error::other(format!(
2511                    "{call_target} did not share a canonical key with {symbol_name}"
2512                ))
2513                .into());
2514            }
2515        }
2516        Ok(())
2517    }
2518
2519    fn assert_import_matches_only_source(
2520        project: ProjectInstanceId,
2521        package: Option<&str>,
2522        caller: &SymbolGraph,
2523        expected: &SymbolGraph,
2524        wrong: &SymbolGraph,
2525    ) -> Result<(), Box<dyn Error>> {
2526        let caller_projection = derive_resolution_keys(project, package, caller)?;
2527        let expected_projection = derive_resolution_keys(project, package, expected)?;
2528        let wrong_projection = derive_resolution_keys(project, package, wrong)?;
2529        let imports = relation_keys_of_kind(caller, &caller_projection, RelationKind::Imports);
2530        require(
2531            imports.len() == 1,
2532            "caller did not retain one import relationship",
2533        )?;
2534        require(
2535            keys_intersect(imports[0], expected_projection.source_keys()),
2536            "caller import missed its exact source",
2537        )?;
2538        require(
2539            !keys_intersect(imports[0], wrong_projection.source_keys()),
2540            "caller import matched a wrong-sibling basename",
2541        )
2542    }
2543
2544    fn symbol_keys<'a>(
2545        graph: &SymbolGraph,
2546        projection: &'a super::ResolutionKeyProjection,
2547        name: &str,
2548    ) -> &'a [CanonicalResolutionKey] {
2549        projection
2550            .symbol_keys()
2551            .iter()
2552            .find(|entry| graph.symbols[entry.symbol_index()].name == name)
2553            .map_or(&[], super::SymbolResolutionKeys::keys)
2554    }
2555
2556    fn relation_keys<'a>(
2557        graph: &SymbolGraph,
2558        projection: &'a super::ResolutionKeyProjection,
2559        target: &str,
2560    ) -> &'a [CanonicalResolutionKey] {
2561        projection
2562            .relation_keys()
2563            .iter()
2564            .find(|entry| {
2565                let relation = &graph.relations[entry.relation_index()];
2566                matches!(relation.kind, RelationKind::Calls | RelationKind::DependsOn)
2567                    && relation.target_name == target
2568            })
2569            .map_or(&[], super::RelationResolutionKeys::keys)
2570    }
2571
2572    fn relation_keys_of_kind<'a>(
2573        graph: &SymbolGraph,
2574        projection: &'a super::ResolutionKeyProjection,
2575        kind: RelationKind,
2576    ) -> Vec<&'a [CanonicalResolutionKey]> {
2577        projection
2578            .relation_keys()
2579            .iter()
2580            .filter(|entry| graph.relations[entry.relation_index()].kind == kind)
2581            .map(super::RelationResolutionKeys::keys)
2582            .collect()
2583    }
2584
2585    fn keys_intersect(left: &[CanonicalResolutionKey], right: &[CanonicalResolutionKey]) -> bool {
2586        left.iter().any(|key| right.binary_search(key).is_ok())
2587    }
2588}