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, 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
152impl<'a> ResolutionProjectionContext<'a> {
153    /// Construct a projection context with one validated configured-module snapshot.
154    #[must_use]
155    pub const fn with_configured_modules(
156        configured_modules: &'a ConfiguredModuleResolution,
157    ) -> Self {
158        Self {
159            configured_modules: Some(configured_modules),
160        }
161    }
162}
163
164impl ResolutionKeyProjection {
165    /// Borrow module keys exported by the source file itself.
166    #[must_use]
167    pub fn source_keys(&self) -> &[CanonicalResolutionKey] {
168        &self.source
169    }
170
171    /// Borrow symbol-index-associated export keys.
172    #[must_use]
173    pub fn symbol_keys(&self) -> &[SymbolResolutionKeys] {
174        &self.symbols
175    }
176
177    /// Borrow relation-index-associated dependency keys.
178    #[must_use]
179    pub fn relation_keys(&self) -> &[RelationResolutionKeys] {
180        &self.relations
181    }
182}
183
184/// Failure while deriving bounded canonical keys from parser facts.
185#[derive(Debug)]
186pub enum ResolutionProjectionError {
187    /// An extracted or caller-supplied identity violated the graph contract.
188    Contract(GraphContractError),
189    /// One source fact would exceed the bounded key fan-out.
190    KeyLimit {
191        /// Kind of parser fact being projected.
192        fact: &'static str,
193        /// Index in the owning symbol or relation collection when applicable.
194        index: usize,
195        /// Number of distinct keys requested by the fact.
196        requested: usize,
197    },
198}
199
200impl fmt::Display for ResolutionProjectionError {
201    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match self {
203            Self::Contract(error) => write!(formatter, "invalid resolution identity: {error}"),
204            Self::KeyLimit {
205                fact,
206                index,
207                requested,
208            } => write!(
209                formatter,
210                "{fact} fact {index} requires {requested} canonical keys, exceeding the per-fact limit of {MAX_RESOLUTION_KEYS_PER_FACT}"
211            ),
212        }
213    }
214}
215
216impl Error for ResolutionProjectionError {
217    fn source(&self) -> Option<&(dyn Error + 'static)> {
218        match self {
219            Self::Contract(error) => Some(error),
220            Self::KeyLimit { .. } => None,
221        }
222    }
223}
224
225impl From<GraphContractError> for ResolutionProjectionError {
226    fn from(value: GraphContractError) -> Self {
227        Self::Contract(value)
228    }
229}
230
231/// Language syntax that produced one import reference.
232#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub enum ImportSyntax {
234    /// Rust `use` syntax.
235    Rust,
236    /// JavaScript or TypeScript `import` syntax.
237    EcmaScript,
238    /// Python `import` or `from ... import ...` syntax.
239    Python,
240}
241
242/// One normalized imported module or declaration and its caller-local name.
243#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct ImportReference {
245    /// Source-language syntax that produced the reference.
246    syntax: ImportSyntax,
247    /// Imported module specifier or path.
248    module: String,
249    /// Imported declaration name for named imports.
250    imported: Option<String>,
251    /// Name visible to callers in the importing source file.
252    local: String,
253}
254
255impl ImportReference {
256    /// Construct one normalized provider-owned import reference.
257    pub(crate) fn new(
258        syntax: ImportSyntax,
259        module: &str,
260        imported: Option<&str>,
261        local: &str,
262    ) -> Self {
263        Self {
264            syntax,
265            module: module.to_string(),
266            imported: imported.map(ToString::to_string),
267            local: local.to_string(),
268        }
269    }
270
271    /// Return the source-language import syntax.
272    #[must_use]
273    pub const fn syntax(&self) -> ImportSyntax {
274        self.syntax
275    }
276
277    /// Borrow the normalized module specifier or path.
278    #[must_use]
279    pub fn module(&self) -> &str {
280        &self.module
281    }
282
283    /// Borrow the imported declaration name, or `None` for a module/namespace import.
284    #[must_use]
285    pub fn imported(&self) -> Option<&str> {
286        self.imported.as_deref()
287    }
288
289    /// Borrow the name used by calls inside the importing source file.
290    #[must_use]
291    pub fn local(&self) -> &str {
292        &self.local
293    }
294}
295
296/// Parse one Rust, JavaScript/TypeScript, or Python import statement.
297///
298/// Malformed and unsupported forms return no references instead of inventing
299/// identities from the complete display statement.
300#[must_use]
301pub fn parse_import_references(import_text: &str) -> Vec<ImportReference> {
302    let mut references = semantic::parse_display_import(import_text);
303    references.sort_by(|left, right| {
304        left.module
305            .cmp(&right.module)
306            .then_with(|| left.imported.cmp(&right.imported))
307            .then_with(|| left.local.cmp(&right.local))
308    });
309    references.dedup();
310    references
311}
312
313/// Resolve one relative ECMAScript module specifier against a repository path.
314#[must_use]
315pub fn resolve_relative_import_path(caller_path: &str, module_spec: &str) -> Option<String> {
316    semantic::ecmascript::resolve_relative_import_path(caller_path, module_spec)
317}
318
319/// Return deterministic module aliases inferred from a repository source path.
320#[must_use]
321pub fn module_aliases_for_path(path: &str) -> Vec<String> {
322    let mut aliases = Vec::new();
323    for stem in source_stems_for_path(path) {
324        let mut components = stem
325            .split('/')
326            .filter(|component| !component.is_empty())
327            .collect::<Vec<_>>();
328        if components
329            .first()
330            .is_some_and(|component| *component == "src")
331        {
332            components.remove(0);
333        }
334        if components.last().is_some_and(|component| {
335            matches!(*component, "lib" | "main" | "mod" | "index" | "__init__")
336        }) {
337            components.pop();
338        }
339        if components.is_empty() {
340            continue;
341        }
342        aliases.push(components.join("::"));
343        aliases.push(components.join("."));
344        if let Some(last) = components.last() {
345            aliases.push((*last).to_string());
346        }
347    }
348    aliases.sort();
349    aliases.dedup();
350    aliases
351}
352
353/// Return source path stems, including package-entry aliases.
354#[must_use]
355pub fn source_stems_for_path(path: &str) -> Vec<String> {
356    let stem = strip_known_source_extension(path);
357    let mut stems = vec![stem.clone()];
358    if let Some((parent, entry_name)) = stem.rsplit_once('/')
359        && matches!(entry_name, "index" | "__init__" | "mod")
360    {
361        stems.push(parent.to_string());
362    }
363    stems.sort();
364    stems.dedup();
365    stems
366}
367
368/// Derive bounded canonical export and dependency keys from one extracted graph.
369///
370/// The returned symbol and relation indices associate parser facts with keys;
371/// runtime graph normalization binds those keys to final entity and logical-
372/// relation owners after resolution.
373///
374/// # Errors
375///
376/// Returns an error when a graph identity is invalid or one parser fact exceeds
377/// the bounded canonical-key fan-out.
378pub fn derive_resolution_keys(
379    project: ProjectInstanceId,
380    package: Option<&str>,
381    graph: &SymbolGraph,
382) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
383    derive_resolution_keys_with_context(
384        project,
385        package,
386        graph,
387        ResolutionProjectionContext::default(),
388    )
389}
390
391/// Derive bounded canonical keys with explicit repository module configuration.
392///
393/// # Errors
394///
395/// Returns an error when a graph identity is invalid or one parser fact exceeds
396/// the bounded canonical-key fan-out.
397pub fn derive_resolution_keys_with_context(
398    project: ProjectInstanceId,
399    package: Option<&str>,
400    graph: &SymbolGraph,
401    context: ResolutionProjectionContext<'_>,
402) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
403    let Some(provider_owner) = semantic::provider_for_graph(graph) else {
404        return Ok(ResolutionKeyProjection {
405            source: Vec::new(),
406            symbols: Vec::new(),
407            relations: Vec::new(),
408        });
409    };
410    let provider = GraphIdentityText::new(provider_owner.as_str())?;
411    let Some(resolution_family) = provider_owner.resolution_family() else {
412        return Ok(ResolutionKeyProjection {
413            source: Vec::new(),
414            symbols: Vec::new(),
415            relations: Vec::new(),
416        });
417    };
418    let language = GraphIdentityText::new(resolution_family)?;
419    let package = package.map(GraphIdentityText::new).transpose()?;
420
421    let source_scopes = canonical_source_scopes(&graph.path);
422    let configured_source_scopes =
423        if provider_owner == SemanticProviderOwner::EcmaScript && package.is_some() {
424            canonical_configured_source_scopes(&graph.path)
425        } else {
426            Vec::new()
427        };
428    let emits_source_module_keys = semantic::emits_source_module_keys(graph);
429    let mut source_keys = Vec::new();
430    if emits_source_module_keys {
431        source_keys.reserve(source_scopes.len() + configured_source_scopes.len());
432        for scope in &source_scopes {
433            source_keys.push(canonical_key(
434                project,
435                ResolutionKeyDomain::Module,
436                &provider,
437                &language,
438                package.as_ref(),
439                None,
440                RelationKind::Imports,
441                scope,
442            )?);
443        }
444        for scope in &configured_source_scopes {
445            source_keys.push(canonical_key(
446                project,
447                ResolutionKeyDomain::Module,
448                &provider,
449                &language,
450                None,
451                None,
452                RelationKind::Imports,
453                scope,
454            )?);
455        }
456    }
457    let source_keys = bounded_keys(source_keys, "source", 0)?;
458
459    let mut symbol_keys = Vec::new();
460    for (symbol_index, symbol) in graph.symbols.iter().enumerate() {
461        let mut keys = Vec::new();
462        match symbol.kind {
463            SymbolKind::Package if provider_owner == SemanticProviderOwner::Cargo => {
464                keys.push(canonical_key(
465                    project,
466                    ResolutionKeyDomain::Package,
467                    &provider,
468                    &language,
469                    None,
470                    None,
471                    RelationKind::DependsOn,
472                    &symbol.name,
473                )?);
474            }
475            SymbolKind::Dependency | SymbolKind::Import | SymbolKind::Workspace => {}
476            _ if emits_source_module_keys
477                && semantic::is_export_candidate(provider_owner, graph, symbol_index) =>
478            {
479                let identity = GraphIdentityText::new(symbol.name.clone())?;
480                let mut scopes = source_scopes.clone();
481                if let Some(parent) = symbol.parent.as_deref() {
482                    scopes.extend(
483                        source_scopes
484                            .iter()
485                            .map(|scope| format!("{scope}/{parent}")),
486                    );
487                    scopes.push(parent.to_string());
488                }
489                scopes.push(String::new());
490                scopes.sort();
491                scopes.dedup();
492                for scope in &scopes {
493                    let scope = (!scope.is_empty())
494                        .then(|| GraphIdentityText::new(scope.clone()))
495                        .transpose()?;
496                    keys.push(CanonicalResolutionKey::new(
497                        project,
498                        ResolutionKeyDomain::Declaration,
499                        &provider,
500                        &language,
501                        package.as_ref(),
502                        scope.as_ref(),
503                        Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
504                        &identity,
505                    ));
506                }
507                let mut configured_scopes = configured_source_scopes.clone();
508                if let Some(parent) = symbol.parent.as_deref() {
509                    configured_scopes.extend(
510                        configured_source_scopes
511                            .iter()
512                            .map(|scope| format!("{scope}/{parent}")),
513                    );
514                    configured_scopes.sort();
515                    configured_scopes.dedup();
516                }
517                for scope in configured_scopes {
518                    let scope = GraphIdentityText::new(scope)?;
519                    keys.push(CanonicalResolutionKey::new(
520                        project,
521                        ResolutionKeyDomain::Declaration,
522                        &provider,
523                        &language,
524                        None,
525                        Some(&scope),
526                        Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
527                        &identity,
528                    ));
529                }
530            }
531            _ => {}
532        }
533        let keys = bounded_keys(keys, "symbol", symbol_index)?;
534        if !keys.is_empty() {
535            symbol_keys.push(SymbolResolutionKeys { symbol_index, keys });
536        }
537    }
538
539    let parsed_imports = graph
540        .relations
541        .iter()
542        .map(|relation| {
543            if relation.kind == RelationKind::Imports {
544                semantic::parse_imports(provider_owner, &relation.target_name)
545            } else {
546                Vec::new()
547            }
548        })
549        .collect::<Vec<_>>();
550    let mut aliases: BTreeMap<&str, Vec<&ImportReference>> = BTreeMap::new();
551    for references in &parsed_imports {
552        for reference in references {
553            aliases
554                .entry(reference.local())
555                .or_default()
556                .push(reference);
557        }
558    }
559    let configured_source = context
560        .configured_modules
561        .map(|configured| configured.for_source(&graph.path));
562    let import_scopes = build_import_scope_cache(
563        provider_owner,
564        graph,
565        &parsed_imports,
566        configured_source.as_ref(),
567    );
568
569    let mut relation_keys = Vec::new();
570    for (relation_index, relation) in graph.relations.iter().enumerate() {
571        let mut keys = match relation.kind {
572            RelationKind::Imports if semantic::supports_source_dependencies(provider_owner) => {
573                import_dependency_keys(
574                    project,
575                    provider_owner,
576                    &provider,
577                    &language,
578                    package.as_ref(),
579                    &relation.path,
580                    &parsed_imports[relation_index],
581                    &import_scopes,
582                )?
583            }
584            RelationKind::Calls if semantic::supports_source_dependencies(provider_owner) => {
585                call_dependency_keys(
586                    project,
587                    provider_owner,
588                    &provider,
589                    &language,
590                    package.as_ref(),
591                    &relation.path,
592                    &relation.target_name,
593                    &aliases,
594                    &import_scopes,
595                )?
596            }
597            RelationKind::DependsOn if semantic::supports_package_dependencies(provider_owner) => {
598                vec![canonical_key(
599                    project,
600                    ResolutionKeyDomain::Package,
601                    &provider,
602                    &language,
603                    None,
604                    None,
605                    RelationKind::DependsOn,
606                    &relation.target_name,
607                )?]
608            }
609            RelationKind::Contains => continue,
610            RelationKind::Imports | RelationKind::Calls | RelationKind::DependsOn => Vec::new(),
611        };
612        keys = bounded_keys(keys, "relation", relation_index)?;
613        relation_keys.push(RelationResolutionKeys {
614            relation_index,
615            keys,
616        });
617    }
618
619    Ok(ResolutionKeyProjection {
620        source: source_keys,
621        symbols: symbol_keys,
622        relations: relation_keys,
623    })
624}
625
626/// Expand each unique import scope once for the complete source graph.
627fn build_import_scope_cache(
628    provider_owner: SemanticProviderOwner,
629    graph: &SymbolGraph,
630    parsed_imports: &[Vec<ImportReference>],
631    configured_source: Option<&ConfiguredModuleSource<'_>>,
632) -> ImportScopeCache {
633    let mut cache = ImportScopeCache::new();
634    for (relation, references) in graph.relations.iter().zip(parsed_imports) {
635        let caller_path = &relation.path;
636        let modules = cache.entry(caller_path.clone()).or_default();
637        for reference in references {
638            modules
639                .entry(reference.module().to_string())
640                .or_insert_with(|| {
641                    semantic::import_scopes(
642                        provider_owner,
643                        caller_path,
644                        reference,
645                        configured_source,
646                    )
647                });
648        }
649    }
650    cache
651}
652
653/// Borrow one pre-expanded import scope list without allocating lookup keys.
654fn cached_import_scopes<'a>(
655    cache: &'a ImportScopeCache,
656    caller_path: &str,
657    reference: &ImportReference,
658) -> &'a [String] {
659    cache
660        .get(caller_path)
661        .and_then(|modules| modules.get(reference.module()))
662        .map_or(&[], Vec::as_slice)
663}
664
665/// Derive canonical module-target keys for import references.
666fn import_dependency_keys(
667    project: ProjectInstanceId,
668    provider_owner: SemanticProviderOwner,
669    provider: &GraphIdentityText,
670    language: &GraphIdentityText,
671    package: Option<&GraphIdentityText>,
672    caller_path: &str,
673    references: &[ImportReference],
674    import_scopes: &ImportScopeCache,
675) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
676    let mut keys = Vec::new();
677    for reference in references {
678        let scopes = cached_import_scopes(import_scopes, caller_path, reference);
679        let package = dependency_package(provider_owner, reference, package);
680        for scope in scopes {
681            keys.push(canonical_key(
682                project,
683                ResolutionKeyDomain::Module,
684                provider,
685                language,
686                package,
687                None,
688                RelationKind::Imports,
689                scope,
690            )?);
691        }
692    }
693    Ok(keys)
694}
695
696/// Derive canonical declaration keys for one call, including local aliases.
697fn call_dependency_keys(
698    project: ProjectInstanceId,
699    provider_owner: SemanticProviderOwner,
700    provider: &GraphIdentityText,
701    language: &GraphIdentityText,
702    package: Option<&GraphIdentityText>,
703    caller_path: &str,
704    target: &str,
705    aliases: &BTreeMap<&str, Vec<&ImportReference>>,
706    import_scopes: &ImportScopeCache,
707) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
708    let (prefix, remainder) = split_qualified_target(target);
709    let alias = prefix.unwrap_or(target).trim();
710    if let Some(references) = aliases.get(alias) {
711        let mut keys = Vec::new();
712        for reference in references {
713            let identity = remainder.or_else(|| reference.imported());
714            let Some(identity) = identity else {
715                continue;
716            };
717            let scopes = cached_import_scopes(import_scopes, caller_path, reference);
718            let package = dependency_package(provider_owner, reference, package);
719            for scope in scopes {
720                let scoped_parent;
721                let scope = if remainder.is_some()
722                    && let Some(imported_parent) = reference.imported()
723                {
724                    scoped_parent = format!("{scope}/{imported_parent}");
725                    scoped_parent.as_str()
726                } else {
727                    scope.as_str()
728                };
729                keys.push(canonical_key(
730                    project,
731                    ResolutionKeyDomain::Declaration,
732                    provider,
733                    language,
734                    package,
735                    Some(scope),
736                    RelationKind::Calls,
737                    identity,
738                )?);
739            }
740        }
741        return Ok(keys);
742    }
743
744    let (scope, identity) = split_qualified_target(target);
745    if scope.is_some() {
746        return Ok(Vec::new());
747    }
748    let Some(identity) = identity.or_else(|| (!target.trim().is_empty()).then_some(target.trim()))
749    else {
750        return Ok(Vec::new());
751    };
752    Ok(vec![canonical_key(
753        project,
754        ResolutionKeyDomain::Declaration,
755        provider,
756        language,
757        package,
758        None,
759        RelationKind::Calls,
760        identity,
761    )?])
762}
763
764/// Configured ECMAScript targets are repository paths, so caller Cargo
765/// ownership must not prevent them from matching a sibling package export.
766fn dependency_package<'a>(
767    provider_owner: SemanticProviderOwner,
768    reference: &ImportReference,
769    package: Option<&'a GraphIdentityText>,
770) -> Option<&'a GraphIdentityText> {
771    if provider_owner == SemanticProviderOwner::EcmaScript
772        && !(reference.module().starts_with("./") || reference.module().starts_with("../"))
773    {
774        None
775    } else {
776        package
777    }
778}
779
780#[allow(clippy::too_many_arguments)]
781/// Construct one validated canonical key from parser-owned identity parts.
782fn canonical_key(
783    project: ProjectInstanceId,
784    domain: ResolutionKeyDomain,
785    provider: &GraphIdentityText,
786    language: &GraphIdentityText,
787    package: Option<&GraphIdentityText>,
788    scope: Option<&str>,
789    relation: RelationKind,
790    identity: &str,
791) -> Result<CanonicalResolutionKey, GraphContractError> {
792    let scope = scope.map(GraphIdentityText::new).transpose()?;
793    let identity = GraphIdentityText::new(identity.to_string())?;
794    Ok(CanonicalResolutionKey::new(
795        project,
796        domain,
797        provider,
798        language,
799        package,
800        scope.as_ref(),
801        Some(GraphRelationKind::from_legacy(relation)),
802        &identity,
803    ))
804}
805
806/// Sort, deduplicate, and enforce the per-fact canonical-key limit.
807fn bounded_keys(
808    mut keys: Vec<CanonicalResolutionKey>,
809    fact: &'static str,
810    index: usize,
811) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
812    keys.sort();
813    keys.dedup();
814    if keys.len() > MAX_RESOLUTION_KEYS_PER_FACT {
815        return Err(ResolutionProjectionError::KeyLimit {
816            fact,
817            index,
818            requested: keys.len(),
819        });
820    }
821    Ok(keys)
822}
823
824/// Return normalized canonical scopes exported by a repository source path.
825fn canonical_source_scopes(path: &str) -> Vec<String> {
826    let mut scopes = source_stems_for_path(path);
827    scopes.extend(module_aliases_for_path(path));
828    normalize_scopes(scopes, normalize_repository_scope)
829}
830
831/// Return only repository-exact scopes used by configured ECMAScript targets.
832fn canonical_configured_source_scopes(path: &str) -> Vec<String> {
833    let mut scopes = source_stems_for_path(path)
834        .into_iter()
835        .map(|scope| normalize_repository_scope(&scope))
836        .filter(|scope| !scope.is_empty())
837        .collect::<Vec<_>>();
838    scopes.sort();
839    scopes.dedup();
840    scopes
841}
842
843/// Normalize, expand, sort, and deduplicate candidate scopes.
844fn normalize_scopes(scopes: Vec<String>, normalize: fn(&str) -> String) -> Vec<String> {
845    let mut normalized = Vec::new();
846    for scope in scopes {
847        let scope = normalize(&scope);
848        if scope.is_empty() {
849            continue;
850        }
851        normalized.push(scope.clone());
852        if let Some(stripped) = scope.strip_prefix("src/") {
853            normalized.push(stripped.to_string());
854        }
855        if let Some(last) = scope.rsplit('/').next() {
856            normalized.push(last.to_string());
857        }
858    }
859    normalized.sort();
860    normalized.dedup();
861    normalized
862}
863
864/// Normalize repository path and Rust-style alias separators without changing dots.
865fn normalize_repository_scope(value: &str) -> String {
866    value
867        .trim()
868        .trim_start_matches("./")
869        .replace("::", "/")
870        .trim_matches('/')
871        .to_string()
872}
873
874/// Split a qualified call target into its scope and final identity.
875fn split_qualified_target(target: &str) -> (Option<&str>, Option<&str>) {
876    let target = target.trim();
877    let rust = target.rsplit_once("::");
878    let dotted = target.rsplit_once('.');
879    match (rust, dotted) {
880        (Some(rust), Some(dotted)) => {
881            if rust.0.len() >= dotted.0.len() {
882                (Some(rust.0), Some(rust.1))
883            } else {
884                (Some(dotted.0), Some(dotted.1))
885            }
886        }
887        (Some((scope, identity)), None) | (None, Some((scope, identity))) => {
888            (Some(scope), Some(identity))
889        }
890        (None, None) => (None, None),
891    }
892}
893
894/// Strip a terminal source extension while preserving dotted directory names.
895pub(super) fn strip_known_source_extension(path: &str) -> String {
896    for extension in [
897        ".d.ts", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs",
898    ] {
899        if let Some(stem) = path.strip_suffix(extension) {
900            return stem.to_string();
901        }
902    }
903    path.rsplit_once('.').map_or_else(
904        || path.to_string(),
905        |(stem, extension)| {
906            if extension.contains('/') {
907                path.to_string()
908            } else {
909                stem.to_string()
910            }
911        },
912    )
913}
914
915#[cfg(test)]
916mod tests {
917    use super::{
918        ImportReference, ImportSyntax, RelationKind, ResolutionProjectionContext,
919        ResolutionProjectionError, SEMANTIC_RESOLUTION_CONTRACT_VERSION, build_import_scope_cache,
920        derive_resolution_keys, derive_resolution_keys_with_context, parse_import_references,
921        resolve_relative_import_path, semantic_resolution_contract_digest,
922    };
923    use crate::{
924        ConfiguredModuleResolution, EcmaScriptConfigKind, EcmaScriptModuleConfig,
925        EcmaScriptPathMapping, extract_symbol_graph,
926    };
927    use projectatlas_core::graph::{CanonicalResolutionKey, ProjectInstanceId};
928    use projectatlas_core::language::SemanticProviderOwner;
929    use projectatlas_core::symbols::SymbolGraph;
930    use std::error::Error;
931    use std::io;
932
933    fn reference(
934        syntax: ImportSyntax,
935        module: &str,
936        imported: Option<&str>,
937        local: &str,
938    ) -> ImportReference {
939        ImportReference {
940            syntax,
941            module: module.to_string(),
942            imported: imported.map(ToString::to_string),
943            local: local.to_string(),
944        }
945    }
946
947    #[test]
948    fn semantic_resolution_contract_digest_is_bounded_and_deterministic() {
949        const PRE_MODULE_CALLBACK_DIGEST: &str =
950            "487625adf2f9ec76f98034d4ef5667e707960b6b8afd280b213021cb64a0f10f";
951        let first = semantic_resolution_contract_digest();
952        assert_eq!(SEMANTIC_RESOLUTION_CONTRACT_VERSION, 3);
953        assert_eq!(first.len(), 64);
954        assert!(first.bytes().all(|byte| byte.is_ascii_hexdigit()));
955        assert_eq!(first, semantic_resolution_contract_digest());
956        assert_ne!(first, PRE_MODULE_CALLBACK_DIGEST);
957    }
958
959    #[test]
960    fn parses_language_import_aliases_without_display_statement_identity() {
961        assert_eq!(
962            parse_import_references("use crate::worker::{run as execute, stop};"),
963            vec![
964                reference(ImportSyntax::Rust, "crate::worker", Some("run"), "execute"),
965                reference(ImportSyntax::Rust, "crate::worker", Some("stop"), "stop"),
966            ]
967        );
968        assert_eq!(
969            parse_import_references("use crate::worker::run_function_alias as run_rust_function;"),
970            vec![reference(
971                ImportSyntax::Rust,
972                "crate::worker",
973                Some("run_function_alias"),
974                "run_rust_function"
975            ),]
976        );
977        assert_eq!(
978            parse_import_references("import * as reader from './reader';"),
979            vec![reference(
980                ImportSyntax::EcmaScript,
981                "./reader",
982                None,
983                "reader",
984            )]
985        );
986        assert_eq!(
987            parse_import_references("import { runAlias as runLocal } from './worker';"),
988            vec![reference(
989                ImportSyntax::EcmaScript,
990                "./worker",
991                Some("runAlias"),
992                "runLocal",
993            )]
994        );
995        assert_eq!(
996            parse_import_references("from package.reader import read as load, close"),
997            vec![
998                reference(
999                    ImportSyntax::Python,
1000                    "package.reader",
1001                    Some("close"),
1002                    "close"
1003                ),
1004                reference(ImportSyntax::Python, "package.reader", Some("read"), "load"),
1005            ]
1006        );
1007        assert_eq!(
1008            parse_import_references("from package.worker import run_alias as execute"),
1009            vec![reference(
1010                ImportSyntax::Python,
1011                "package.worker",
1012                Some("run_alias"),
1013                "execute",
1014            )]
1015        );
1016        assert!(parse_import_references("import { broken from './reader'").is_empty());
1017        assert!(parse_import_references("use crate::worker::run as ;").is_empty());
1018        assert!(parse_import_references("import { read } from module './reader'").is_empty());
1019        for malformed in [
1020            "use crate::worker::{run, nested::{start, stop}};",
1021            "use crate::worker::{run, broken as };",
1022            "import defaultValue, { read } from './reader';",
1023            "import { read, nested { broken } } from './reader';",
1024            "import { read as load as fetch } from './reader';",
1025            "from package.reader import (read, close)",
1026            "from package.reader import read, broken as ",
1027            "import package.reader as reader as other",
1028        ] {
1029            assert!(
1030                parse_import_references(malformed).is_empty(),
1031                "unsupported syntax fabricated references: {malformed}"
1032            );
1033        }
1034    }
1035
1036    #[test]
1037    fn resolves_relative_imports_without_platform_path_rules() {
1038        assert_eq!(
1039            resolve_relative_import_path("src/features/main.ts", "../shared/reader.ts"),
1040            Some("src/shared/reader".to_string())
1041        );
1042        assert_eq!(
1043            resolve_relative_import_path("src/main.ts", "../../outside"),
1044            None
1045        );
1046        assert_eq!(resolve_relative_import_path("src/main.ts", "reader"), None);
1047        assert_eq!(
1048            resolve_relative_import_path("src/foo.bar/main.ts", "./reader"),
1049            Some("src/foo.bar/reader".to_string())
1050        );
1051    }
1052
1053    #[test]
1054    fn real_language_graphs_share_alias_qualified_keys() -> Result<(), Box<dyn Error>> {
1055        let project = project_id(1)?;
1056
1057        let rust_target = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
1058        let rust_caller = extract_symbol_graph(
1059            "src/main.rs",
1060            Some("rust"),
1061            "use crate::worker::run as execute;\nfn main() { execute(); }\n",
1062        );
1063        assert_alias_resolution(
1064            project,
1065            Some("atlas"),
1066            &rust_target,
1067            "run",
1068            &rust_caller,
1069            &["execute"],
1070        )?;
1071        let rust_module_target = extract_symbol_graph(
1072            "src/config.rs",
1073            Some("rust"),
1074            "pub fn load_timeout_millis() -> u64 { 250 }\n",
1075        );
1076        let rust_module_caller = extract_symbol_graph(
1077            "src/handler.rs",
1078            Some("rust"),
1079            "use crate::config;\npub fn health_response() { config::load_timeout_millis(); }\n",
1080        );
1081        assert_alias_resolution(
1082            project,
1083            Some("atlas"),
1084            &rust_module_target,
1085            "load_timeout_millis",
1086            &rust_module_caller,
1087            &["config::load_timeout_millis"],
1088        )?;
1089        let rust_callback_caller = extract_symbol_graph(
1090            "src/router.rs",
1091            Some("rust"),
1092            "use crate::handler;\nfn dispatch(path: &str) -> Option<()> { (path == \"/health\").then(handler::health_response) }\n",
1093        );
1094        assert_alias_resolution(
1095            project,
1096            Some("atlas"),
1097            &rust_module_caller,
1098            "health_response",
1099            &rust_callback_caller,
1100            &["handler::health_response"],
1101        )?;
1102        let user_defined_then = extract_symbol_graph(
1103            "src/scheduler.rs",
1104            Some("rust"),
1105            "use crate::handler;\nstruct Scheduler;\nimpl Scheduler { fn then(self, callback: fn()) { let _ = callback; } }\nfn schedule(scheduler: Scheduler) { scheduler.then(handler::health_response); }\n",
1106        );
1107        if user_defined_then.relations.iter().any(|relation| {
1108            relation.kind == RelationKind::Calls
1109                && relation.target_name == "handler::health_response"
1110        }) {
1111            return Err(io::Error::other(
1112                "a user-defined then method fabricated a callback call edge",
1113            )
1114            .into());
1115        }
1116
1117        let typescript_target = extract_symbol_graph(
1118            "src/shared/reader.ts",
1119            Some("typescript"),
1120            "export function read() {}\n",
1121        );
1122        let typescript_caller = extract_symbol_graph(
1123            "src/features/main.ts",
1124            Some("typescript"),
1125            "import { read as load } from '../shared/reader';\nimport * as reader from '../shared/reader';\nexport function main() { load(); reader.read(); }\n",
1126        );
1127        assert_alias_resolution(
1128            project,
1129            Some("web"),
1130            &typescript_target,
1131            "read",
1132            &typescript_caller,
1133            &["load", "reader.read"],
1134        )?;
1135
1136        let dotted_directory_target = extract_symbol_graph(
1137            "src/foo.bar/reader.ts",
1138            Some("typescript"),
1139            "export function read() {}\n",
1140        );
1141        let dotted_directory_caller = extract_symbol_graph(
1142            "src/foo.bar/main.ts",
1143            Some("typescript"),
1144            "import { read } from './reader';\nexport function main() { read(); }\n",
1145        );
1146        assert_alias_resolution(
1147            project,
1148            Some("web"),
1149            &dotted_directory_target,
1150            "read",
1151            &dotted_directory_caller,
1152            &["read"],
1153        )?;
1154
1155        let python_target = extract_symbol_graph(
1156            "src/package/reader.py",
1157            Some("python"),
1158            "def read():\n    pass\n",
1159        );
1160        let python_caller = extract_symbol_graph(
1161            "src/main.py",
1162            Some("python"),
1163            "import package.reader as reader\nreader.read()\n",
1164        );
1165        assert_alias_resolution(
1166            project,
1167            Some("python-app"),
1168            &python_target,
1169            "read",
1170            &python_caller,
1171            &["reader.read"],
1172        )?;
1173        Ok(())
1174    }
1175
1176    #[test]
1177    fn configured_aliases_share_file_and_declaration_keys_across_ecmascript_hosts()
1178    -> Result<(), Box<dyn Error>> {
1179        let project = project_id(21)?;
1180        let configured = ConfiguredModuleResolution::new(vec![
1181            EcmaScriptModuleConfig::new(
1182                "tsconfig.json",
1183                EcmaScriptConfigKind::TypeScript,
1184                Some("src".to_string()),
1185                vec![
1186                    EcmaScriptPathMapping::new("@/*", vec!["src/*".to_string()])?,
1187                    EcmaScriptPathMapping::new("@index/*", vec!["src/*/index.ts".to_string()])?,
1188                ],
1189            )?,
1190            EcmaScriptModuleConfig::new(
1191                "jsconfig.json",
1192                EcmaScriptConfigKind::JavaScript,
1193                Some("src".to_string()),
1194                vec![EcmaScriptPathMapping::new(
1195                    "@/*",
1196                    vec!["src/*".to_string()],
1197                )?],
1198            )?,
1199        ])?;
1200        let context = ResolutionProjectionContext::with_configured_modules(&configured);
1201
1202        for (target_path, language, target_source, caller_path, caller_source) in [
1203            (
1204                "src/controller.ts",
1205                "typescript",
1206                "export function useController(): string { return 'ok'; }\n",
1207                "src/page.ts",
1208                "import { useController } from '@/controller';\nexport const value = useController();\n",
1209            ),
1210            (
1211                "src/controller.ts",
1212                "typescript",
1213                "export function useController(): string { return 'ok'; }\n",
1214                "src/page.tsx",
1215                "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1216            ),
1217            (
1218                "src/controller.js",
1219                "javascript",
1220                "export function useController() { return 'ok'; }\n",
1221                "src/page.jsx",
1222                "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1223            ),
1224            (
1225                "src/controller.ts",
1226                "typescript",
1227                "export function useController(): string { return 'ok'; }\n",
1228                "src/Page.vue",
1229                "<script setup lang=\"ts\">\nimport { useController } from '@/controller';\nconst value = useController();\n</script>\n<template><div>{{ value }}</div></template>\n",
1230            ),
1231        ] {
1232            let target = extract_symbol_graph(target_path, Some(language), target_source);
1233            let caller_extension = std::path::Path::new(caller_path)
1234                .extension()
1235                .and_then(std::ffi::OsStr::to_str);
1236            let caller_language = match caller_extension {
1237                Some(extension) if extension.eq_ignore_ascii_case("vue") => "vue",
1238                Some(extension)
1239                    if extension.eq_ignore_ascii_case("js")
1240                        || extension.eq_ignore_ascii_case("jsx") =>
1241                {
1242                    "javascript"
1243                }
1244                Some(extension) if extension.eq_ignore_ascii_case("tsx") => "tsx",
1245                _ => "typescript",
1246            };
1247            let caller = extract_symbol_graph(caller_path, Some(caller_language), caller_source);
1248            let target_projection = derive_resolution_keys(project, Some("shared"), &target)?;
1249            let caller_projection =
1250                derive_resolution_keys_with_context(project, Some("app"), &caller, context)?;
1251            let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1252            require(
1253                imports
1254                    .iter()
1255                    .any(|keys| keys_intersect(keys, target_projection.source_keys())),
1256                "configured import did not share the target file key",
1257            )?;
1258            require(
1259                keys_intersect(
1260                    relation_keys(&caller, &caller_projection, "useController"),
1261                    symbol_keys(&target, &target_projection, "useController"),
1262                ),
1263                "configured call did not share the target declaration key",
1264            )?;
1265        }
1266
1267        let index_target = extract_symbol_graph(
1268            "src/tools/index.ts",
1269            Some("typescript"),
1270            "export function useTool() { return 'ok'; }\n",
1271        );
1272        let index_caller = extract_symbol_graph(
1273            "src/index-page.ts",
1274            Some("typescript"),
1275            "import { useTool } from '@index/tools';\nexport const value = useTool();\n",
1276        );
1277        let index_target_projection = derive_resolution_keys(project, Some("app"), &index_target)?;
1278        let index_caller_projection =
1279            derive_resolution_keys_with_context(project, Some("app"), &index_caller, context)?;
1280        require(
1281            relation_keys_of_kind(
1282                &index_caller,
1283                &index_caller_projection,
1284                RelationKind::Imports,
1285            )
1286            .iter()
1287            .any(|keys| keys_intersect(keys, index_target_projection.source_keys())),
1288            "configured index target did not reuse package-entry source keys",
1289        )?;
1290        require(
1291            keys_intersect(
1292                relation_keys(&index_caller, &index_caller_projection, "useTool"),
1293                symbol_keys(&index_target, &index_target_projection, "useTool"),
1294            ),
1295            "configured index target did not reuse declaration keys",
1296        )?;
1297
1298        let extension_caller = extract_symbol_graph(
1299            "src/extension-page.ts",
1300            Some("typescript"),
1301            "import { useController } from '@/controller.ts';\nexport const value = useController();\n",
1302        );
1303        let extension_target = extract_symbol_graph(
1304            "src/controller.ts",
1305            Some("typescript"),
1306            "export function useController(): string { return 'ok'; }\n",
1307        );
1308        let extension_caller_projection =
1309            derive_resolution_keys_with_context(project, Some("app"), &extension_caller, context)?;
1310        let extension_target_projection =
1311            derive_resolution_keys(project, Some("app"), &extension_target)?;
1312        require(
1313            relation_keys_of_kind(
1314                &extension_caller,
1315                &extension_caller_projection,
1316                RelationKind::Imports,
1317            )
1318            .iter()
1319            .any(|keys| keys_intersect(keys, extension_target_projection.source_keys())),
1320            "configured extension target did not reuse extensionless source keys",
1321        )?;
1322
1323        let root_configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1324            "tsconfig.json",
1325            EcmaScriptConfigKind::TypeScript,
1326            Some(String::new()),
1327            Vec::new(),
1328        )?])?;
1329        let root_context = ResolutionProjectionContext::with_configured_modules(&root_configured);
1330        let root_target = extract_symbol_graph(
1331            "controller.ts",
1332            Some("typescript"),
1333            "export function useController() { return 'root'; }\n",
1334        );
1335        let nested_same_name = extract_symbol_graph(
1336            "packages/app/src/controller.ts",
1337            Some("typescript"),
1338            "export function useController() { return 'nested'; }\n",
1339        );
1340        let root_caller = extract_symbol_graph(
1341            "page.ts",
1342            Some("typescript"),
1343            "import { useController } from 'controller';\nexport const value = useController();\n",
1344        );
1345        let root_target_projection = derive_resolution_keys(project, Some("shared"), &root_target)?;
1346        let nested_projection = derive_resolution_keys(project, Some("nested"), &nested_same_name)?;
1347        let root_caller_projection =
1348            derive_resolution_keys_with_context(project, Some("app"), &root_caller, root_context)?;
1349        let configured_imports =
1350            relation_keys_of_kind(&root_caller, &root_caller_projection, RelationKind::Imports);
1351        require(
1352            configured_imports
1353                .iter()
1354                .any(|keys| keys_intersect(keys, root_target_projection.source_keys())),
1355            "root baseUrl import did not resolve its repository-exact target",
1356        )?;
1357        require(
1358            configured_imports
1359                .iter()
1360                .all(|keys| !keys_intersect(keys, nested_projection.source_keys())),
1361            "root baseUrl import also matched a nested basename alias",
1362        )?;
1363        let configured_call = relation_keys(&root_caller, &root_caller_projection, "useController");
1364        require(
1365            keys_intersect(
1366                configured_call,
1367                symbol_keys(&root_target, &root_target_projection, "useController"),
1368            ),
1369            "root baseUrl call did not resolve its repository-exact declaration",
1370        )?;
1371        require(
1372            !keys_intersect(
1373                configured_call,
1374                symbol_keys(&nested_same_name, &nested_projection, "useController"),
1375            ),
1376            "root baseUrl call also matched a nested basename alias",
1377        )?;
1378        Ok(())
1379    }
1380
1381    #[test]
1382    fn configured_import_scopes_are_expanded_once_per_source_graph() -> Result<(), Box<dyn Error>> {
1383        let configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1384            "tsconfig.json",
1385            EcmaScriptConfigKind::TypeScript,
1386            Some("src".to_string()),
1387            vec![EcmaScriptPathMapping::new(
1388                "@/*",
1389                vec!["src/*".to_string()],
1390            )?],
1391        )?])?;
1392        let graph = extract_symbol_graph(
1393            "src/page.ts",
1394            Some("typescript"),
1395            "import { useController } from '@/controller';\n\
1396             export const first = useController();\n\
1397             export const second = useController();\n",
1398        );
1399        let parsed_imports = graph
1400            .relations
1401            .iter()
1402            .map(|relation| {
1403                if relation.kind == RelationKind::Imports {
1404                    crate::semantic::parse_imports(
1405                        SemanticProviderOwner::EcmaScript,
1406                        &relation.target_name,
1407                    )
1408                } else {
1409                    Vec::new()
1410                }
1411            })
1412            .collect::<Vec<_>>();
1413        let configured_source = configured.for_source(&graph.path);
1414        let cache = build_import_scope_cache(
1415            SemanticProviderOwner::EcmaScript,
1416            &graph,
1417            &parsed_imports,
1418            Some(&configured_source),
1419        );
1420        let modules = cache
1421            .get("src/page.ts")
1422            .ok_or_else(|| io::Error::other("source graph omitted its import-scope cache"))?;
1423        if modules.len() != 1 || modules.get("@/controller") != Some(&vec!["src/controller".into()])
1424        {
1425            return Err(io::Error::other(
1426                "repeated imported calls did not reuse one configured scope expansion",
1427            )
1428            .into());
1429        }
1430        Ok(())
1431    }
1432
1433    #[test]
1434    fn imports_target_modules_and_qualified_calls_target_members() -> Result<(), Box<dyn Error>> {
1435        let project = project_id(10)?;
1436        let target = extract_symbol_graph(
1437            "src/shared/reader.ts",
1438            Some("typescript"),
1439            "export function read() {}\nexport function close() {}\nexport function fetch() {}\nexport const client = { fetch() {} };\n",
1440        );
1441        let caller = extract_symbol_graph(
1442            "src/features/main.ts",
1443            Some("typescript"),
1444            "import { read, close, client } from '../shared/reader';\nexport function main() { read(); close(); client.fetch(); }\n",
1445        );
1446        let target_projection = derive_resolution_keys(project, Some("web"), &target)?;
1447        let caller_projection = derive_resolution_keys(project, Some("web"), &caller)?;
1448        let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1449        require(
1450            imports.len() == 1,
1451            "one import emitted more than one module target",
1452        )?;
1453        require(
1454            imports[0]
1455                .iter()
1456                .all(|key| key.domain() == projectatlas_core::graph::ResolutionKeyDomain::Module),
1457            "named imports emitted declaration resolution targets",
1458        )?;
1459        require(
1460            keys_intersect(imports[0], target_projection.source_keys()),
1461            "named import did not target its source module",
1462        )?;
1463        for symbol in ["read", "close"] {
1464            require(
1465                !keys_intersect(imports[0], symbol_keys(&target, &target_projection, symbol)),
1466                "module import also targeted a named declaration",
1467            )?;
1468        }
1469        assert_alias_resolution(project, Some("web"), &target, "read", &caller, &["read"])?;
1470        assert_alias_resolution(project, Some("web"), &target, "close", &caller, &["close"])?;
1471        let qualified_call = relation_keys(&caller, &caller_projection, "client.fetch");
1472        let fetch_candidates = target_projection
1473            .symbol_keys()
1474            .iter()
1475            .filter(|entry| target.symbols[entry.symbol_index()].name == "fetch")
1476            .map(|entry| {
1477                (
1478                    target.symbols[entry.symbol_index()].parent.as_deref(),
1479                    entry.keys(),
1480                )
1481            })
1482            .collect::<Vec<_>>();
1483        require(
1484            fetch_candidates.len() == 2,
1485            "qualified-call fixture lost one fetch declaration",
1486        )?;
1487        require(
1488            fetch_candidates.iter().any(|(parent, keys)| {
1489                *parent == Some("client") && keys_intersect(qualified_call, keys)
1490            }),
1491            "qualified call did not target the imported parent member",
1492        )?;
1493        require(
1494            fetch_candidates
1495                .iter()
1496                .all(|(parent, keys)| parent.is_some() || !keys_intersect(qualified_call, keys)),
1497            "qualified call also targeted a same-module top-level declaration",
1498        )
1499    }
1500
1501    #[test]
1502    fn actual_complex_import_syntax_abstains_instead_of_fabricating_keys()
1503    -> Result<(), Box<dyn Error>> {
1504        let project = project_id(15)?;
1505        for (path, language, source) in [
1506            (
1507                "src/main.rs",
1508                "rust",
1509                "use crate::worker::{run, nested::{start, stop}};\nfn main() { run(); }\n",
1510            ),
1511            (
1512                "src/main.ts",
1513                "typescript",
1514                "import defaultValue, { read } from './reader';\ndefaultValue();\n",
1515            ),
1516            (
1517                "src/main.py",
1518                "python",
1519                "from package.reader import (read, close)\nread()\n",
1520            ),
1521        ] {
1522            let graph = extract_symbol_graph(path, Some(language), source);
1523            let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1524            let imports = relation_keys_of_kind(&graph, &projection, RelationKind::Imports);
1525            require(
1526                !imports.is_empty(),
1527                "complex-import fixture did not reach provider normalization",
1528            )?;
1529            require(
1530                imports.iter().all(|keys| keys.is_empty()),
1531                "unsupported complex import syntax fabricated resolution keys",
1532            )?;
1533        }
1534        Ok(())
1535    }
1536
1537    #[test]
1538    fn provider_scopes_abstain_instead_of_matching_packages_or_wrong_siblings()
1539    -> Result<(), Box<dyn Error>> {
1540        let project = project_id(11)?;
1541        let correct_typescript = extract_symbol_graph(
1542            "src/a/reader.ts",
1543            Some("typescript"),
1544            "export function read() {}\n",
1545        );
1546        let wrong_typescript = extract_symbol_graph(
1547            "src/b/reader.ts",
1548            Some("typescript"),
1549            "export function read() {}\n",
1550        );
1551        let typescript_caller = extract_symbol_graph(
1552            "src/main.ts",
1553            Some("typescript"),
1554            "import { read } from './a/reader';\nread();\n",
1555        );
1556        assert_import_matches_only_source(
1557            project,
1558            Some("web"),
1559            &typescript_caller,
1560            &correct_typescript,
1561            &wrong_typescript,
1562        )?;
1563
1564        let package_caller = extract_symbol_graph(
1565            "src/package.ts",
1566            Some("typescript"),
1567            "import { read } from 'reader-package';\nread();\n",
1568        );
1569        let package_projection = derive_resolution_keys(project, Some("web"), &package_caller)?;
1570        require(
1571            relation_keys_of_kind(&package_caller, &package_projection, RelationKind::Imports)
1572                .iter()
1573                .all(|keys| keys.is_empty()),
1574            "bare ECMAScript package import entered a repository-local module scope",
1575        )?;
1576        require(
1577            relation_keys(&package_caller, &package_projection, "read").is_empty(),
1578            "call through an unresolved package import fell back to a global declaration",
1579        )?;
1580
1581        for (path, language, source, target) in [
1582            (
1583                "src/unimported.ts",
1584                "typescript",
1585                "export function run() { return client.fetch(); }\n",
1586                "client.fetch",
1587            ),
1588            (
1589                "src/unimported.rs",
1590                "rust",
1591                "pub fn run() { worker::execute(); }\n",
1592                "worker::execute",
1593            ),
1594            (
1595                "src/unimported.py",
1596                "python",
1597                "def run():\n    return reader.read()\n",
1598                "reader.read",
1599            ),
1600        ] {
1601            let graph = extract_symbol_graph(path, Some(language), source);
1602            let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1603            require(
1604                graph.relations.iter().any(|relation| {
1605                    relation.kind == RelationKind::Calls && relation.target_name == target
1606                }),
1607                "unimported qualified-call fixture did not reach semantic normalization",
1608            )?;
1609            require(
1610                relation_keys(&graph, &projection, target).is_empty(),
1611                "unimported qualified call fell back to a project basename scope",
1612            )?;
1613        }
1614
1615        let rust_target =
1616            extract_symbol_graph("src/feature/worker.rs", Some("rust"), "pub fn run() {}\n");
1617        let wrong_rust =
1618            extract_symbol_graph("src/other/worker.rs", Some("rust"), "pub fn run() {}\n");
1619        let rust_caller = extract_symbol_graph(
1620            "src/feature/main.rs",
1621            Some("rust"),
1622            "use super::worker::run;\nfn main() { run(); }\n",
1623        );
1624        let rust_crate_caller = extract_symbol_graph(
1625            "src/main.rs",
1626            Some("rust"),
1627            "use crate::feature::worker::run;\nfn main() { run(); }\n",
1628        );
1629        let rust_self_caller = extract_symbol_graph(
1630            "src/feature/mod.rs",
1631            Some("rust"),
1632            "use self::worker::run;\nfn main() { run(); }\n",
1633        );
1634        for caller in [&rust_caller, &rust_crate_caller, &rust_self_caller] {
1635            assert_import_matches_only_source(
1636                project,
1637                Some("atlas"),
1638                caller,
1639                &rust_target,
1640                &wrong_rust,
1641            )?;
1642        }
1643
1644        let python_target = extract_symbol_graph(
1645            "src/package/reader.py",
1646            Some("python"),
1647            "def read():\n    pass\n",
1648        );
1649        let wrong_python = extract_symbol_graph(
1650            "src/other/reader.py",
1651            Some("python"),
1652            "def read():\n    pass\n",
1653        );
1654        let python_caller = extract_symbol_graph(
1655            "src/package/main.py",
1656            Some("python"),
1657            "from .reader import read\nread()\n",
1658        );
1659        assert_import_matches_only_source(
1660            project,
1661            Some("python-app"),
1662            &python_caller,
1663            &python_target,
1664            &wrong_python,
1665        )?;
1666        let parent_python_target = extract_symbol_graph(
1667            "src/package/reader.py",
1668            Some("python"),
1669            "def read():\n    pass\n",
1670        );
1671        let parent_python_wrong = extract_symbol_graph(
1672            "src/package/sub/reader.py",
1673            Some("python"),
1674            "def read():\n    pass\n",
1675        );
1676        let parent_python_caller = extract_symbol_graph(
1677            "src/package/sub/main.py",
1678            Some("python"),
1679            "from ..reader import read\nread()\n",
1680        );
1681        assert_import_matches_only_source(
1682            project,
1683            Some("python-app"),
1684            &parent_python_caller,
1685            &parent_python_target,
1686            &parent_python_wrong,
1687        )
1688    }
1689
1690    #[test]
1691    fn direct_provider_families_retain_duplicate_ambiguity_keys() -> Result<(), Box<dyn Error>> {
1692        let project = project_id(12)?;
1693        for (path, language, target_source, caller_path, caller_source, target_name) in [
1694            (
1695                "src/shared/reader.ts",
1696                "typescript",
1697                "export function read() {}\nexport function read() {}\n",
1698                "src/main.ts",
1699                "import { read } from './shared/reader';\nread();\n",
1700                "read",
1701            ),
1702            (
1703                "src/package/reader.py",
1704                "python",
1705                "def read():\n    pass\ndef read():\n    pass\n",
1706                "src/main.py",
1707                "from package.reader import read\nread()\n",
1708                "read",
1709            ),
1710        ] {
1711            let target = extract_symbol_graph(path, Some(language), target_source);
1712            let caller = extract_symbol_graph(caller_path, Some(language), caller_source);
1713            let target_projection = derive_resolution_keys(project, Some("app"), &target)?;
1714            let caller_projection = derive_resolution_keys(project, Some("app"), &caller)?;
1715            let duplicate_keys = target_projection
1716                .symbol_keys()
1717                .iter()
1718                .filter(|entry| target.symbols[entry.symbol_index()].name == target_name)
1719                .map(super::SymbolResolutionKeys::keys)
1720                .collect::<Vec<_>>();
1721            require(
1722                duplicate_keys.len() == 2,
1723                "duplicate declarations were not both projected",
1724            )?;
1725            let dependency = relation_keys(&caller, &caller_projection, target_name);
1726            require(
1727                duplicate_keys
1728                    .iter()
1729                    .all(|keys| keys_intersect(dependency, keys)),
1730                "call dependency did not retain every duplicate declaration candidate",
1731            )?;
1732        }
1733        Ok(())
1734    }
1735
1736    #[test]
1737    fn resolution_identity_separates_provider_from_cross_dialect_family()
1738    -> Result<(), Box<dyn Error>> {
1739        let project = project_id(13)?;
1740        let typescript = extract_symbol_graph(
1741            "src/shared/reader.ts",
1742            Some("typescript"),
1743            "export function read() {}\n",
1744        );
1745        let javascript = extract_symbol_graph(
1746            "src/shared/reader.ts",
1747            Some("javascript"),
1748            "export function read() {}\n",
1749        );
1750        let typescript_projection = derive_resolution_keys(project, Some("web"), &typescript)?;
1751        let javascript_projection = derive_resolution_keys(project, Some("web"), &javascript)?;
1752        require(
1753            typescript_projection.source_keys() == javascript_projection.source_keys(),
1754            "ECMAScript-compatible dialects entered different resolution families",
1755        )?;
1756        let provider = projectatlas_core::graph::GraphIdentityText::new("ecma-script")?;
1757        let family = projectatlas_core::graph::GraphIdentityText::new("ecmascript")?;
1758        let package = projectatlas_core::graph::GraphIdentityText::new("web")?;
1759        let identity = projectatlas_core::graph::GraphIdentityText::new("src/shared/reader")?;
1760        let expected = CanonicalResolutionKey::new(
1761            project,
1762            projectatlas_core::graph::ResolutionKeyDomain::Module,
1763            &provider,
1764            &family,
1765            Some(&package),
1766            None,
1767            Some(projectatlas_core::graph::GraphRelationKind::from_legacy(
1768                RelationKind::Imports,
1769            )),
1770            &identity,
1771        );
1772        require(
1773            typescript_projection.source_keys().contains(&expected),
1774            "provider owner and cross-dialect family were not projected independently",
1775        )
1776    }
1777
1778    #[test]
1779    fn embedded_hosts_publish_module_keys_only_for_admitted_component_facts()
1780    -> Result<(), Box<dyn Error>> {
1781        let project = project_id(14)?;
1782        for (path, language, source) in [
1783            (
1784                "page.html",
1785                "html",
1786                "<script>export function run() {}</script>",
1787            ),
1788            ("empty.vue", "vue", "<template><p>empty</p></template>"),
1789            (
1790                "external.vue",
1791                "vue",
1792                "<script src='./external.js'></script>",
1793            ),
1794            ("empty.svelte", "svelte", "<p>empty</p>"),
1795            (
1796                "external.svelte",
1797                "svelte",
1798                "<script src='./external.js'></script>",
1799            ),
1800            (
1801                "external-inline.vue",
1802                "vue",
1803                "<script>import * as fs from 'node:fs';</script>",
1804            ),
1805            (
1806                "external-inline.svelte",
1807                "svelte",
1808                "<script>import * as fs from 'node:fs';</script>",
1809            ),
1810            ("malformed.vue", "vue", "<script"),
1811            ("malformed.svelte", "svelte", "<script"),
1812        ] {
1813            let graph = extract_symbol_graph(path, Some(language), source);
1814            let projection = derive_resolution_keys(project, Some("web"), &graph)?;
1815            require(
1816                projection.source_keys().is_empty(),
1817                "host without an admitted component module surface emitted source keys",
1818            )?;
1819            if path.starts_with("external-inline") {
1820                require(
1821                    relation_keys_of_kind(&graph, &projection, RelationKind::Imports).len() == 1,
1822                    "external-only component lost its outbound import relationship",
1823                )?;
1824            }
1825        }
1826        for (path, language) in [("component.vue", "vue"), ("component.svelte", "svelte")] {
1827            let graph = extract_symbol_graph(
1828                path,
1829                Some(language),
1830                "<script>export function run() {}</script>",
1831            );
1832            require(
1833                !derive_resolution_keys(project, Some("web"), &graph)?
1834                    .source_keys()
1835                    .is_empty(),
1836                "admitted component script facts did not expose their host module",
1837            )?;
1838        }
1839        Ok(())
1840    }
1841
1842    #[test]
1843    fn canonical_projection_is_stable_and_retains_ambiguous_unresolved_and_package_keys()
1844    -> Result<(), Box<dyn Error>> {
1845        let project = project_id(2)?;
1846        let first = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
1847        let moved = extract_symbol_graph(
1848            "src/worker.rs",
1849            Some("rust"),
1850            "\n\n// formatting moved the declaration\npub fn run() {}\n",
1851        );
1852        let first_projection = derive_resolution_keys(project, Some("atlas"), &first)?;
1853        let moved_projection = derive_resolution_keys(project, Some("atlas"), &moved)?;
1854        require(
1855            symbol_keys(&first, &first_projection, "run")
1856                == symbol_keys(&moved, &moved_projection, "run"),
1857            "formatting-only line movement changed canonical symbol identity",
1858        )?;
1859        require(
1860            first_projection.source_keys()
1861                != derive_resolution_keys(project_id(3)?, Some("atlas"), &first)?.source_keys(),
1862            "different projects produced the same canonical source identity",
1863        )?;
1864        require(
1865            symbol_keys(&first, &first_projection, "run")
1866                != symbol_keys(
1867                    &first,
1868                    &derive_resolution_keys(project, Some("other-package"), &first)?,
1869                    "run",
1870                ),
1871            "different packages produced the same canonical declaration identity",
1872        )?;
1873
1874        let duplicate = extract_symbol_graph("src/other.rs", Some("rust"), "pub fn run() {}\n");
1875        let caller = extract_symbol_graph(
1876            "src/main.rs",
1877            Some("rust"),
1878            "fn main() { run(); missing(); }\n",
1879        );
1880        let duplicate_projection = derive_resolution_keys(project, Some("atlas"), &duplicate)?;
1881        let caller_projection = derive_resolution_keys(project, Some("atlas"), &caller)?;
1882        let run_dependencies = relation_keys(&caller, &caller_projection, "run");
1883        require(
1884            keys_intersect(
1885                run_dependencies,
1886                symbol_keys(&first, &first_projection, "run"),
1887            ),
1888            "ambiguous call did not retain the first matching declaration key",
1889        )?;
1890        require(
1891            keys_intersect(
1892                run_dependencies,
1893                symbol_keys(&duplicate, &duplicate_projection, "run"),
1894            ),
1895            "ambiguous call did not retain the duplicate declaration key",
1896        )?;
1897        let missing_dependencies = relation_keys(&caller, &caller_projection, "missing");
1898        require(
1899            !missing_dependencies.is_empty(),
1900            "unresolved call lost its canonical dependency key",
1901        )?;
1902        require(
1903            !keys_intersect(
1904                missing_dependencies,
1905                symbol_keys(&first, &first_projection, "run"),
1906            ),
1907            "unresolved call matched an unrelated declaration key",
1908        )?;
1909
1910        let package = extract_symbol_graph(
1911            "vendor/library/Cargo.toml",
1912            Some("cargo-manifest"),
1913            "[package]\nname = \"library\"\n",
1914        );
1915        let manifest = extract_symbol_graph(
1916            "Cargo.toml",
1917            Some("cargo-manifest"),
1918            "[package]\nname = \"app\"\n[dependencies]\nlibrary = \"1\"\n",
1919        );
1920        let package_projection = derive_resolution_keys(project, None, &package)?;
1921        let manifest_projection = derive_resolution_keys(project, None, &manifest)?;
1922        require(
1923            package_projection.source_keys().is_empty()
1924                && manifest_projection.source_keys().is_empty(),
1925            "Cargo manifests advertised ordinary source-module identities",
1926        )?;
1927        require(
1928            keys_intersect(
1929                relation_keys(&manifest, &manifest_projection, "library"),
1930                symbol_keys(&package, &package_projection, "library"),
1931            ),
1932            "Cargo dependency did not match its package declaration key",
1933        )?;
1934        Ok(())
1935    }
1936
1937    #[test]
1938    fn cargo_provider_abstains_on_malformed_input_and_retains_duplicate_package_candidates()
1939    -> Result<(), Box<dyn Error>> {
1940        let project = project_id(16)?;
1941        let malformed = extract_symbol_graph(
1942            "Cargo.toml",
1943            Some("cargo-manifest"),
1944            "[package\nname = \"broken\"\n",
1945        );
1946        let malformed_projection = derive_resolution_keys(project, None, &malformed)?;
1947        require(
1948            malformed_projection.source_keys().is_empty()
1949                && malformed_projection.symbol_keys().is_empty()
1950                && malformed_projection.relation_keys().is_empty(),
1951            "malformed Cargo input fabricated semantic identities",
1952        )?;
1953
1954        let first = extract_symbol_graph(
1955            "vendor/first/Cargo.toml",
1956            Some("cargo-manifest"),
1957            "[package]\nname = \"shared-package\"\n",
1958        );
1959        let second = extract_symbol_graph(
1960            "vendor/second/Cargo.toml",
1961            Some("cargo-manifest"),
1962            "[package]\nname = \"shared-package\"\n",
1963        );
1964        let caller = extract_symbol_graph(
1965            "Cargo.toml",
1966            Some("cargo-manifest"),
1967            "[package]\nname = \"app\"\n[dependencies]\nshared-package = \"1\"\n",
1968        );
1969        let first_projection = derive_resolution_keys(project, None, &first)?;
1970        let second_projection = derive_resolution_keys(project, None, &second)?;
1971        let caller_projection = derive_resolution_keys(project, None, &caller)?;
1972        let dependency = relation_keys(&caller, &caller_projection, "shared-package");
1973        require(
1974            keys_intersect(
1975                dependency,
1976                symbol_keys(&first, &first_projection, "shared-package"),
1977            ) && keys_intersect(
1978                dependency,
1979                symbol_keys(&second, &second_projection, "shared-package"),
1980            ),
1981            "duplicate Cargo packages did not remain ambiguity candidates",
1982        )
1983    }
1984
1985    #[test]
1986    fn projection_rejects_invalid_identity_and_excessive_key_fanout() -> Result<(), Box<dyn Error>>
1987    {
1988        let graph = extract_symbol_graph("src/lib.rs", Some("rust"), "pub fn run() {}\n");
1989        require(
1990            matches!(
1991                derive_resolution_keys(project_id(4)?, Some("bad\0package"), &graph),
1992                Err(ResolutionProjectionError::Contract(_))
1993            ),
1994            "invalid package identity was accepted",
1995        )?;
1996
1997        let imports = (0..65)
1998            .map(|index| format!("import {{ run as execute }} from './module{index}';"))
1999            .collect::<Vec<_>>()
2000            .join("\n");
2001        let source = format!("{imports}\nexecute();\n");
2002        let graph = extract_symbol_graph("src/main.ts", Some("typescript"), &source);
2003        require(
2004            matches!(
2005                derive_resolution_keys(project_id(4)?, Some("web"), &graph),
2006                Err(ResolutionProjectionError::KeyLimit {
2007                    fact: "relation",
2008                    ..
2009                })
2010            ),
2011            "excessive relation-key fan-out was not rejected",
2012        )?;
2013        Ok(())
2014    }
2015
2016    #[test]
2017    fn unsupported_and_lockfile_languages_do_not_gain_generic_name_resolution()
2018    -> Result<(), Box<dyn Error>> {
2019        for graph in [
2020            extract_symbol_graph(
2021                "src/Service.java",
2022                Some("java"),
2023                "public class Service { void run() {} }\n",
2024            ),
2025            extract_symbol_graph(
2026                "Cargo.lock",
2027                Some("cargo-lock"),
2028                "[[package]]\nname = \"dependency\"\nversion = \"1.0.0\"\n",
2029            ),
2030        ] {
2031            let projection = derive_resolution_keys(project_id(9)?, Some("app"), &graph)?;
2032            require(
2033                projection.source_keys().is_empty()
2034                    && projection.symbol_keys().is_empty()
2035                    && projection.relation_keys().is_empty(),
2036                "unsupported language emitted generic resolution keys",
2037            )?;
2038        }
2039        Ok(())
2040    }
2041
2042    fn require(condition: bool, message: &'static str) -> Result<(), Box<dyn Error>> {
2043        if condition {
2044            Ok(())
2045        } else {
2046            Err(io::Error::other(message).into())
2047        }
2048    }
2049
2050    fn project_id(byte: u8) -> Result<ProjectInstanceId, Box<dyn Error>> {
2051        Ok(ProjectInstanceId::from_bytes([byte; 16])?)
2052    }
2053
2054    fn assert_alias_resolution(
2055        project: ProjectInstanceId,
2056        package: Option<&str>,
2057        target: &SymbolGraph,
2058        symbol_name: &str,
2059        caller: &SymbolGraph,
2060        call_targets: &[&str],
2061    ) -> Result<(), Box<dyn Error>> {
2062        let target_projection = derive_resolution_keys(project, package, target)?;
2063        let caller_projection = derive_resolution_keys(project, package, caller)?;
2064        let exports = symbol_keys(target, &target_projection, symbol_name);
2065        for call_target in call_targets {
2066            if !keys_intersect(
2067                relation_keys(caller, &caller_projection, call_target),
2068                exports,
2069            ) {
2070                return Err(io::Error::other(format!(
2071                    "{call_target} did not share a canonical key with {symbol_name}"
2072                ))
2073                .into());
2074            }
2075        }
2076        Ok(())
2077    }
2078
2079    fn assert_import_matches_only_source(
2080        project: ProjectInstanceId,
2081        package: Option<&str>,
2082        caller: &SymbolGraph,
2083        expected: &SymbolGraph,
2084        wrong: &SymbolGraph,
2085    ) -> Result<(), Box<dyn Error>> {
2086        let caller_projection = derive_resolution_keys(project, package, caller)?;
2087        let expected_projection = derive_resolution_keys(project, package, expected)?;
2088        let wrong_projection = derive_resolution_keys(project, package, wrong)?;
2089        let imports = relation_keys_of_kind(caller, &caller_projection, RelationKind::Imports);
2090        require(
2091            imports.len() == 1,
2092            "caller did not retain one import relationship",
2093        )?;
2094        require(
2095            keys_intersect(imports[0], expected_projection.source_keys()),
2096            "caller import missed its exact source",
2097        )?;
2098        require(
2099            !keys_intersect(imports[0], wrong_projection.source_keys()),
2100            "caller import matched a wrong-sibling basename",
2101        )
2102    }
2103
2104    fn symbol_keys<'a>(
2105        graph: &SymbolGraph,
2106        projection: &'a super::ResolutionKeyProjection,
2107        name: &str,
2108    ) -> &'a [CanonicalResolutionKey] {
2109        projection
2110            .symbol_keys()
2111            .iter()
2112            .find(|entry| graph.symbols[entry.symbol_index()].name == name)
2113            .map_or(&[], super::SymbolResolutionKeys::keys)
2114    }
2115
2116    fn relation_keys<'a>(
2117        graph: &SymbolGraph,
2118        projection: &'a super::ResolutionKeyProjection,
2119        target: &str,
2120    ) -> &'a [CanonicalResolutionKey] {
2121        projection
2122            .relation_keys()
2123            .iter()
2124            .find(|entry| {
2125                let relation = &graph.relations[entry.relation_index()];
2126                matches!(relation.kind, RelationKind::Calls | RelationKind::DependsOn)
2127                    && relation.target_name == target
2128            })
2129            .map_or(&[], super::RelationResolutionKeys::keys)
2130    }
2131
2132    fn relation_keys_of_kind<'a>(
2133        graph: &SymbolGraph,
2134        projection: &'a super::ResolutionKeyProjection,
2135        kind: RelationKind,
2136    ) -> Vec<&'a [CanonicalResolutionKey]> {
2137        projection
2138            .relation_keys()
2139            .iter()
2140            .filter(|entry| graph.relations[entry.relation_index()].kind == kind)
2141            .map(super::RelationResolutionKeys::keys)
2142            .collect()
2143    }
2144
2145    fn keys_intersect(left: &[CanonicalResolutionKey], right: &[CanonicalResolutionKey]) -> bool {
2146        left.iter().any(|key| right.binary_search(key).is_ok())
2147    }
2148}