Skip to main content

projectatlas/runtime/graph_projection/binding_resolution/
rust.rs

1//! Cargo-root and module ownership for Rust static bindings.
2use super::super::cargo_roots::{CargoRootCohort, repository_join};
3use super::{
4    BindingResolver, BindingSource, CallBindingResolution, CallBindings, CanonicalResolutionKey,
5    CauseWorkspace, CliError, EntitySelector, GraphEntity, IndexWorkStage, MAX_FORWARDING_DEPTH,
6    MAX_INCREMENTAL_RESOLUTION_ITEMS, ResolverScratch, STAGED_GRAPH_ROW_BYTES, SourceCallBinding,
7    SourceGraph, SourceSemanticFacts, document_file_resolution_key, invalid_graph_contract,
8    resolution_key_limit_failure,
9};
10use projectatlas_core::graph::{ExternalSelector, RepositoryFilePath};
11use projectatlas_symbols::{
12    SourceCargoTargetKind as TargetKind, SourceRustAnchor as Anchor, SourceRustBinding,
13    SourceRustPath, SourceRustUseRole as UseRole, SourceRustVisibility as Visibility,
14    cargo_target_inventory_key,
15};
16use std::collections::{BTreeMap, BTreeSet};
17use std::sync::Arc;
18
19/// Filesystem placement is independent from a module's logical crate address.
20#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
21struct Module {
22    /// Actual target source, selected by Cargo evidence.
23    root: String,
24    /// Admitted file containing the module.
25    file: String,
26    /// Inline source module owner.
27    inline: Arc<[String]>,
28    /// Crate-relative module path.
29    logical: Vec<String>,
30    /// Default directory for external children.
31    directory: String,
32    /// Visibility of this module from its declaring parent.
33    visibility: Visibility,
34}
35
36/// Conservative owned module payload, including shared inline-path capacity.
37fn module_bytes(module: &Module) -> u64 {
38    [
39        module.root.capacity(),
40        module.file.capacity(),
41        module.directory.capacity(),
42    ]
43    .into_iter()
44    .fold(STAGED_GRAPH_ROW_BYTES, |bytes, capacity| {
45        bytes.saturating_add(capacity as u64)
46    })
47    .saturating_add((module.logical.capacity() * std::mem::size_of::<String>()) as u64)
48    .saturating_add(
49        module
50            .logical
51            .iter()
52            .map(|part| part.capacity() as u64)
53            .sum::<u64>(),
54    )
55    .saturating_add(std::mem::size_of_val(module.inline.as_ref()) as u64)
56    .saturating_add(
57        module
58            .inline
59            .iter()
60            .map(|part| part.capacity() as u64)
61            .sum::<u64>(),
62    )
63}
64
65/// Cached membership retains causes even when ownership is unproved.
66#[derive(Default)]
67struct Membership {
68    /// Unique actual Cargo root and module address.
69    owner: Option<Module>,
70    /// Manifest, root, module and missing-path causes.
71    causes: BTreeSet<CanonicalResolutionKey>,
72}
73
74/// Preindexed source slots avoid a whole-file scan for every call.
75#[derive(Default)]
76struct Slots {
77    /// Value declaration ordinals by actual inline owner and name.
78    values: BTreeMap<Arc<[String]>, BTreeMap<String, Vec<usize>>>,
79    /// Module ordinals by actual inline owner and name.
80    modules: BTreeMap<Arc<[String]>, BTreeMap<String, Vec<usize>>>,
81    /// Shadow-checked toolchain identity indexed by the shared use occurrence.
82    toolchain: BTreeMap<*const projectatlas_symbols::SourceRustUse, String>,
83    /// Inherent method ordinals grouped by actual module, type and method name.
84    associated: BTreeMap<(Arc<[String]>, String, String), Vec<usize>>,
85}
86
87/// Rust value and module namespaces can share a spelling.
88#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
89enum Namespace {
90    /// Callable values.
91    Value,
92    /// Declared or explicitly imported module namespaces.
93    Module,
94}
95
96/// Actual selected declaration or source module, never a callable file marker.
97enum Selection {
98    /// Source proves the requested namespace is absent, independently from unknown lookup.
99    Absent,
100    /// Actual admitted callable declaration.
101    Value(GraphEntity, Visibility, Vec<String>),
102    /// Source-proven namespace address.
103    Module(Module),
104    /// Type namespace owner proved by a native declaration, never a direct callee.
105    Type {
106        /// Actual source module declaring this type.
107        owner: Module,
108        /// Source-proven type identifier.
109        name: String,
110        /// Type namespace visibility, independent from constructor visibility.
111        visibility: Visibility,
112    },
113}
114
115/// Cache key retains source syntax, inline owner and call/import meaning.
116#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
117struct Request {
118    /// Source that owns the syntax.
119    source: String,
120    /// Inline source module of the reference.
121    module: Arc<[String]>,
122    /// Complete AST path including any static call member suffix.
123    path: SourceRustPath,
124    /// A grouped self or wildcard import selects only a namespace.
125    namespace: bool,
126    /// Imports target files; calls target declarations.
127    import: bool,
128    /// The exact source import proves an unshadowed static toolchain root.
129    toolchain: bool,
130}
131
132/// One admitted Cargo root and its optional shared sibling invalidation cause.
133struct RootCandidate {
134    /// Exact source path from the manifest/discovery inventory.
135    path: String,
136    /// Whether Cargo ownership is proven for this target.
137    certain: bool,
138    /// Shared only when multiple actual candidates have this physical parent.
139    cohort: Option<Arc<CargoRootCohort>>,
140}
141
142/// Physical directories narrow ownership before following actual module declarations.
143#[derive(Default)]
144struct RootDirectoryCandidates {
145    /// Negative observations share a cohort, or retain an exact ungrouped root.
146    causes: BTreeSet<CanonicalResolutionKey>,
147    /// Source-free ordinals allow unrelated directories to remain unopened.
148    roots: Vec<usize>,
149}
150
151/// Top-level module spelling selects only roots that could own that module.
152type RootModuleCandidates = BTreeMap<String, Vec<usize>>;
153
154/// One manifest-local index shares the already admitted, sorted root inventory.
155struct RootMembershipIndex {
156    /// Ordinals below address this exact immutable inventory.
157    roots: Arc<[RootCandidate]>,
158    /// A source visits only its physical ancestors, rather than every Cargo root.
159    directories: BTreeMap<String, RootDirectoryCandidates>,
160}
161
162/// Bounded query caches reuse the existing source capture and entity hydration.
163struct RustResolver<'r, 'a, 'g, 'n> {
164    /// Shared operation authority and budgets.
165    shared: &'r mut BindingResolver<'a, 'g, 'n>,
166    /// Each source membership is checked once.
167    memberships: BTreeMap<String, Arc<Membership>>,
168    /// Actual manifest target candidates are collected once.
169    roots: BTreeMap<String, Arc<[RootCandidate]>>,
170    /// Source-free root path/cause buckets are built once per owning manifest.
171    root_memberships: BTreeMap<String, Arc<RootMembershipIndex>>,
172    /// Source slots are inspected lazily, only in queried ancestor directories.
173    root_modules: BTreeMap<String, BTreeMap<String, Arc<RootModuleCandidates>>>,
174    /// Module/value slots indexed once per required source.
175    slots: BTreeMap<String, Slots>,
176    /// Identical calls share one complete result and cause set.
177    queries: BTreeMap<Request, Arc<CallBindingResolution>>,
178    /// Competing slots observed in the current complete root query.
179    ambiguous: u32,
180    /// Repeated local import groups share merged namespace outcomes.
181    candidate_queries: BTreeMap<
182        (
183            String,
184            *const [(usize, usize)],
185            *const [usize],
186            Option<String>,
187        ),
188        Arc<CallBindingResolution>,
189    >,
190}
191
192/// Resolve native Rust calls and dependencies with actual Cargo ownership evidence.
193pub(super) fn resolve_rust_bindings<'g>(
194    shared: &mut BindingResolver<'_, 'g, '_>,
195    graphs: &[&SourceGraph<'g>],
196    bindings: &mut CallBindings,
197) -> Result<(), CliError> {
198    let mut resolver = RustResolver {
199        shared,
200        memberships: BTreeMap::new(),
201        roots: BTreeMap::new(),
202        root_memberships: BTreeMap::new(),
203        root_modules: BTreeMap::new(),
204        slots: BTreeMap::new(),
205        queries: BTreeMap::new(),
206        ambiguous: 0,
207        candidate_queries: BTreeMap::new(),
208    };
209    for graph in graphs {
210        resolver.shared.check_budget()?;
211        let raw = graph.graph();
212        let Some(facts) = graph
213            .semantics()
214            .filter(|facts| facts.rust().is_some_and(|rust| rust.complete))
215        else {
216            continue;
217        };
218        if resolver.shared.packages.package_owner(&raw.path).is_none() {
219            continue;
220        }
221        resolver.with_scratch(|resolver, scratch| {
222            let candidate_index_bytes = (facts.rust().map_or(0, |rust| rust.candidate_calls.len())
223                as u64)
224                .saturating_mul(STAGED_GRAPH_ROW_BYTES);
225            scratch.reserve(resolver.shared, candidate_index_bytes)?;
226            let candidate_calls = facts
227                .rust()
228                .into_iter()
229                .flat_map(|rust| &rust.candidate_calls)
230                .map(|call| (call.relation, call))
231                .collect::<BTreeMap<_, _>>();
232            for call in facts.calls() {
233                resolver.shared.check_budget()?;
234                if let Some(candidates) = candidate_calls.get(&call.relation) {
235                    let outcome = resolver.resolve_candidates(&raw.path, facts, candidates)?;
236                    resolver
237                        .charge(STAGED_GRAPH_ROW_BYTES.saturating_add(raw.path.len() as u64))?;
238                    bindings
239                        .entry(raw.path.clone())
240                        .or_default()
241                        .insert(call.relation, outcome);
242                    continue;
243                }
244                let (module, path, namespace) = match &call.binding {
245                    SourceCallBinding::Imported {
246                        import,
247                        member,
248                        rust_toolchain: None,
249                        ..
250                    } => {
251                        let Some(binding) = facts
252                            .imports()
253                            .get(*import)
254                            .and_then(|import| import.rust.as_deref())
255                        else {
256                            continue;
257                        };
258                        if !binding.supported || binding.role == UseRole::Wildcard {
259                            continue;
260                        }
261                        let mut path = binding.path.clone();
262                        if let Some(member) = member {
263                            path.segments
264                                .extend(member.split("::").map(ToString::to_string));
265                        }
266                        (
267                            Arc::clone(&binding.module),
268                            path,
269                            binding.role == UseRole::SelfNamespace && member.is_none(),
270                        )
271                    }
272                    SourceCallBinding::RustPath(call) => {
273                        (Arc::clone(&call.module), call.path.clone(), false)
274                    }
275                    _ => continue,
276                };
277                // Preserve complete paths; no packed key or display-name reinterpretation.
278                let outcome = resolver.resolve(Request {
279                    source: raw.path.clone(),
280                    module,
281                    path,
282                    namespace,
283                    import: false,
284                    toolchain: false,
285                })?;
286                resolver.charge(STAGED_GRAPH_ROW_BYTES.saturating_add(raw.path.len() as u64))?;
287                bindings
288                    .entry(raw.path.clone())
289                    .or_default()
290                    .insert(call.relation, outcome);
291            }
292            for import in facts.imports() {
293                resolver.shared.check_budget()?;
294                let Some(binding) = import.rust.as_deref() else {
295                    continue;
296                };
297                if !binding.supported || import.rust_toolchain.is_some() {
298                    continue;
299                }
300                let outcome = resolver.resolve(Request {
301                    source: raw.path.clone(),
302                    module: Arc::clone(&binding.module),
303                    path: binding.path.clone(),
304                    namespace: binding.role != UseRole::Item,
305                    import: true,
306                    toolchain: false,
307                })?;
308                resolver.charge(STAGED_GRAPH_ROW_BYTES.saturating_add(raw.path.len() as u64))?;
309                bindings
310                    .entry(raw.path.clone())
311                    .or_default()
312                    .insert(import.relation, outcome);
313            }
314            Ok(((), 0))
315        })?;
316    }
317    Ok(())
318}
319
320impl<'g> RustResolver<'_, '_, 'g, '_> {
321    /// Account all additional caches under the operation's existing memory boundary.
322    fn charge(&mut self, bytes: u64) -> Result<(), CliError> {
323        self.shared.reserve_scratch(bytes)
324    }
325
326    /// Release temporary construction on every exit and charge the completed owner once.
327    fn with_scratch<T>(
328        &mut self,
329        operation: impl FnOnce(&mut Self, &mut ResolverScratch) -> Result<(T, u64), CliError>,
330    ) -> Result<T, CliError> {
331        let mut scratch = ResolverScratch::default();
332        let result = operation(self, &mut scratch);
333        self.shared.release_scratch(scratch.retained_bytes)?;
334        let (value, retained_bytes) = result?;
335        self.shared.reserve_scratch(retained_bytes)?;
336        Ok(value)
337    }
338
339    /// Use the shared bounded cause owner for Rust-specific source traversal.
340    fn insert_cause(
341        &mut self,
342        causes: &mut CauseWorkspace<'_>,
343        key: &CanonicalResolutionKey,
344    ) -> Result<(), CliError> {
345        self.shared.insert_cause(causes, key)
346    }
347
348    /// Missing paths remain invalidation causes, without attempting to read them.
349    fn cause(&mut self, path: &str, causes: &mut CauseWorkspace<'_>) -> Result<bool, CliError> {
350        let key = document_file_resolution_key(self.shared.project, path)?;
351        self.insert_cause(causes, &key)?;
352        Ok(self.shared.sources.nodes.contains_key(path))
353    }
354
355    /// Preserve root certainty while reserving each new owned path before insertion.
356    fn insert_root(
357        &mut self,
358        scratch: &mut ResolverScratch,
359        roots: &mut BTreeMap<String, bool>,
360        path: String,
361        certain: bool,
362    ) -> Result<(), CliError> {
363        self.shared.check_budget()?;
364        if let Some(existing) = roots.get_mut(&path) {
365            *existing &= certain;
366            return Ok(());
367        }
368        if roots.len() >= MAX_INCREMENTAL_RESOLUTION_ITEMS as usize {
369            return Err(resolution_key_limit_failure(roots.len().saturating_add(1)));
370        }
371        scratch.reserve(
372            self.shared,
373            STAGED_GRAPH_ROW_BYTES.saturating_add(path.capacity() as u64),
374        )?;
375        roots.insert(path, certain);
376        Ok(())
377    }
378
379    /// Index complete native ownership facts once, sharing source/module payloads.
380    fn source(&mut self, path: &str) -> Result<Option<Arc<BindingSource<'g>>>, CliError> {
381        let source = self.shared.source(path)?;
382        let Some(facts) = source
383            .source
384            .semantics()
385            .and_then(SourceSemanticFacts::rust)
386            .filter(|facts| facts.complete)
387        else {
388            return Ok(None);
389        };
390        if !self.slots.contains_key(path) {
391            let slots = self.with_scratch(|this, scratch| {
392                let mut slots = Slots::default();
393                let mut bytes = STAGED_GRAPH_ROW_BYTES.saturating_add(path.len() as u64);
394                scratch.reserve(this.shared, bytes)?;
395                for (ordinal, declaration) in facts.declarations.iter().enumerate() {
396                    this.shared.control.check(IndexWorkStage::SymbolParsing)?;
397                    scratch.reserve(
398                        this.shared,
399                        STAGED_GRAPH_ROW_BYTES
400                            .saturating_mul(4)
401                            .saturating_add(declaration.name.len() as u64),
402                    )?;
403                    slots
404                        .values
405                        .entry(Arc::clone(&declaration.module))
406                        .or_default()
407                        .entry(declaration.name.clone())
408                        .or_default()
409                        .push(ordinal);
410                    bytes = bytes
411                        .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
412                        .saturating_add(declaration.name.len() as u64);
413                }
414                for (ordinal, module) in facts.modules.iter().enumerate() {
415                    this.shared.control.check(IndexWorkStage::SymbolParsing)?;
416                    scratch.reserve(
417                        this.shared,
418                        STAGED_GRAPH_ROW_BYTES
419                            .saturating_mul(4)
420                            .saturating_add(module.name.len() as u64),
421                    )?;
422                    slots
423                        .modules
424                        .entry(Arc::clone(&module.parent))
425                        .or_default()
426                        .entry(module.name.clone())
427                        .or_default()
428                        .push(ordinal);
429                    bytes = bytes
430                        .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
431                        .saturating_add(module.name.len() as u64);
432                }
433                for (ordinal, method) in facts.associated.iter().enumerate() {
434                    this.shared.control.check(IndexWorkStage::SymbolParsing)?;
435                    scratch.reserve(
436                        this.shared,
437                        STAGED_GRAPH_ROW_BYTES
438                            .saturating_mul(4)
439                            .saturating_add(method.owner.len() as u64)
440                            .saturating_add(method.name.len() as u64),
441                    )?;
442                    slots
443                        .associated
444                        .entry((
445                            Arc::clone(&method.module),
446                            method.owner.clone(),
447                            method.name.clone(),
448                        ))
449                        .or_default()
450                        .push(ordinal);
451                    bytes = bytes
452                        .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
453                        .saturating_add(method.owner.len() as u64)
454                        .saturating_add(method.name.len() as u64);
455                }
456                if let Some(facts) = source.source.semantics() {
457                    for import in facts.imports() {
458                        this.shared.control.check(IndexWorkStage::SymbolParsing)?;
459                        if let (Some(binding), Some(reference)) =
460                            (&import.rust, &import.rust_toolchain)
461                            && let Some(identity) = super::super::rust_import_identity(reference)
462                        {
463                            scratch.reserve(
464                                this.shared,
465                                STAGED_GRAPH_ROW_BYTES
466                                    .saturating_mul(4)
467                                    .saturating_add(identity.capacity() as u64),
468                            )?;
469                            bytes = bytes
470                                .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
471                                .saturating_add(identity.capacity() as u64);
472                            slots.toolchain.insert(Arc::as_ptr(binding), identity);
473                        }
474                    }
475                }
476                Ok((slots, bytes))
477            })?;
478            self.slots.insert(path.to_string(), slots);
479        }
480        Ok(Some(source))
481    }
482
483    /// Gather roots from the owning manifest and admitted default target inventory.
484    fn roots(
485        &mut self,
486        path: &str,
487        causes: &mut CauseWorkspace<'_>,
488    ) -> Result<Arc<[RootCandidate]>, CliError> {
489        let Some(package) = self.shared.packages.package_owner(path) else {
490            return Ok(Arc::from([]));
491        };
492        let manifest = package.manifest.clone();
493        let directory = package.root.clone();
494        let package_name = package.name.clone();
495        let cohort_index = package.cohorts.as_ref().map(Arc::clone);
496        if !self.cause(&manifest, causes)? {
497            return Ok(Arc::from([]));
498        }
499        let source = self.shared.source(&manifest)?;
500        let Some(targets) = source
501            .source
502            .semantics()
503            .and_then(SourceSemanticFacts::cargo)
504            .filter(|targets| targets.complete)
505        else {
506            return Ok(Arc::from([]));
507        };
508        for auto in &targets.automatic {
509            if let Some(relative) = auto.kind.conventional_root() {
510                self.cause(&join(&directory, relative), causes)?;
511            }
512            if let Some(relative) = auto.kind.automatic_directory() {
513                let target_directory = join(&directory, relative);
514                let prefix = format!("{target_directory}/");
515                if let Some(relative) = path.strip_prefix(&prefix) {
516                    let key = cargo_target_inventory_key(
517                        self.shared.project,
518                        &package_name,
519                        &manifest,
520                        &target_directory,
521                    )
522                    .map_err(invalid_graph_contract)?;
523                    self.insert_cause(causes, &key)?;
524                    if let Some(component) = relative
525                        .split('/')
526                        .next()
527                        .filter(|part| part.strip_suffix(".rs").is_none())
528                    {
529                        self.cause(&format!("{target_directory}/{component}/main.rs"), causes)?;
530                    }
531                }
532            }
533        }
534        if let Some(roots) = self.roots.get(&manifest) {
535            return Ok(Arc::clone(roots));
536        }
537        let roots = self.with_scratch(|this, scratch| {
538            scratch.reserve(
539                this.shared,
540                (targets.targets.len() as u64).saturating_mul(STAGED_GRAPH_ROW_BYTES),
541            )?;
542            let overrides = targets
543                .targets
544                .iter()
545                .filter_map(|target| {
546                    target
547                        .name
548                        .as_ref()
549                        .map(|name| (target.kind, name.as_str()))
550                })
551                .collect::<BTreeSet<_>>();
552            let mut roots = BTreeMap::<String, bool>::new();
553            for target in &targets.targets {
554                this.shared.control.check(IndexWorkStage::SymbolParsing)?;
555                if let Some(path) = repository_join(&directory, &target.path) {
556                    this.insert_root(scratch, &mut roots, path, true)?;
557                }
558            }
559            for auto in &targets.automatic {
560                this.shared.control.check(IndexWorkStage::SymbolParsing)?;
561                if let Some(path) = auto.kind.conventional_root() {
562                    let overridden = auto.kind == TargetKind::Binary
563                        && overrides.contains(&(auto.kind, package_name.as_str()));
564                    if !overridden {
565                        this.insert_root(
566                            scratch,
567                            &mut roots,
568                            join(&directory, path),
569                            auto.certain,
570                        )?;
571                    }
572                }
573                let Some(relative) = auto.kind.automatic_directory() else {
574                    continue;
575                };
576                let prefix = format!("{}/", join(&directory, relative));
577                let nodes = Arc::clone(&this.shared.sources.nodes);
578                for (path, _) in nodes.range(prefix.as_str()..) {
579                    this.shared.control.check(IndexWorkStage::SymbolParsing)?;
580                    let Some(relative) = path.strip_prefix(&prefix) else {
581                        break;
582                    };
583                    let name = relative
584                        .strip_suffix(".rs")
585                        .filter(|name| !name.contains('/'))
586                        .or_else(|| {
587                            relative
588                                .strip_suffix("/main.rs")
589                                .filter(|name| !name.contains('/'))
590                        });
591                    let Some(name) = name.filter(|name| !name.is_empty()) else {
592                        continue;
593                    };
594                    if overrides.contains(&(auto.kind, name)) {
595                        continue;
596                    }
597                    this.insert_root(scratch, &mut roots, (*path).to_string(), auto.certain)?;
598                }
599            }
600            let mut parent_counts = BTreeMap::<String, usize>::new();
601            for path in roots.keys() {
602                this.shared.control.check(IndexWorkStage::SymbolParsing)?;
603                let directory = parent(path);
604                if let Some(count) = parent_counts.get_mut(directory) {
605                    *count += 1;
606                } else {
607                    scratch.reserve(
608                        this.shared,
609                        STAGED_GRAPH_ROW_BYTES.saturating_add(directory.len() as u64),
610                    )?;
611                    parent_counts.insert(directory.to_string(), 1);
612                }
613            }
614            let roots: Arc<[RootCandidate]> = roots
615                .into_iter()
616                .map(|(path, certain)| {
617                    let cohort = if parent_counts
618                        .get(parent(&path))
619                        .is_some_and(|count| *count > 1)
620                    {
621                        cohort_index
622                            .as_ref()
623                            .and_then(|index| index.candidate(&path, &directory))
624                    } else {
625                        None
626                    };
627                    RootCandidate {
628                        path,
629                        certain,
630                        cohort,
631                    }
632                })
633                .collect::<Vec<_>>()
634                .into();
635            let retained_bytes = roots.iter().fold(
636                STAGED_GRAPH_ROW_BYTES.saturating_add(manifest.capacity() as u64),
637                |bytes, root| {
638                    bytes
639                        .saturating_add(STAGED_GRAPH_ROW_BYTES)
640                        .saturating_add(root.path.capacity() as u64)
641                },
642            );
643            Ok((roots, retained_bytes))
644        })?;
645        self.roots.insert(manifest, Arc::clone(&roots));
646        Ok(roots)
647    }
648
649    /// Bucket paths and negative causes without opening any candidate root source.
650    fn root_membership_index(
651        &mut self,
652        path: &str,
653        roots: Arc<[RootCandidate]>,
654    ) -> Result<Arc<RootMembershipIndex>, CliError> {
655        let manifest = self
656            .shared
657            .packages
658            .package_owner(path)
659            .map(|package| package.manifest.as_str())
660            .unwrap_or_default();
661        if let Some(index) = self.root_memberships.get(manifest) {
662            return Ok(Arc::clone(index));
663        }
664        let manifest = manifest.to_string();
665        let index = self.with_scratch(|this, scratch| {
666            let mut bytes = STAGED_GRAPH_ROW_BYTES.saturating_add(manifest.len() as u64);
667            scratch.reserve(this.shared, bytes)?;
668            let mut directories = BTreeMap::<String, RootDirectoryCandidates>::new();
669            for (ordinal, candidate) in roots.iter().enumerate() {
670                this.shared.check_budget()?;
671                let directory = parent(&candidate.path);
672                if !directories.contains_key(directory) {
673                    let extra = STAGED_GRAPH_ROW_BYTES.saturating_add(directory.len() as u64);
674                    scratch.reserve(this.shared, extra)?;
675                    bytes = bytes.saturating_add(extra);
676                    directories.insert(directory.to_string(), RootDirectoryCandidates::default());
677                }
678                let Some(entry) = directories.get_mut(directory) else {
679                    return Err(CliError::InvalidInput(
680                        "root directory index disappeared".to_string(),
681                    ));
682                };
683                let key = if let Some(cohort) = &candidate.cohort {
684                    &cohort.key
685                } else {
686                    &document_file_resolution_key(this.shared.project, &candidate.path)?
687                };
688                if !entry.causes.contains(key) {
689                    let extra = STAGED_GRAPH_ROW_BYTES
690                        .saturating_add(key.canonical_identity().len() as u64);
691                    scratch.reserve(this.shared, extra)?;
692                    bytes = bytes.saturating_add(extra);
693                    entry.causes.insert(key.clone());
694                }
695                // A row envelope covers amortized ordinal-vector spare capacity.
696                scratch.reserve(this.shared, STAGED_GRAPH_ROW_BYTES)?;
697                bytes = bytes.saturating_add(STAGED_GRAPH_ROW_BYTES);
698                entry.roots.push(ordinal);
699            }
700            Ok((RootMembershipIndex { roots, directories }, bytes))
701        })?;
702        let index = Arc::new(index);
703        self.root_memberships.insert(manifest, Arc::clone(&index));
704        Ok(index)
705    }
706
707    /// Recover module slots once per relevant directory, leaving other roots unopened.
708    fn root_directory_modules(
709        &mut self,
710        path: &str,
711        index: &RootMembershipIndex,
712        directory: &str,
713        candidates: &RootDirectoryCandidates,
714    ) -> Result<Arc<RootModuleCandidates>, CliError> {
715        let manifest = self
716            .shared
717            .packages
718            .package_owner(path)
719            .map(|package| package.manifest.as_str())
720            .unwrap_or_default();
721        if let Some(modules) = self
722            .root_modules
723            .get(manifest)
724            .and_then(|rows| rows.get(directory))
725        {
726            return Ok(Arc::clone(modules));
727        }
728        let manifest = manifest.to_string();
729        let modules = self.with_scratch(|this, scratch| {
730            let mut bytes = STAGED_GRAPH_ROW_BYTES.saturating_add(directory.len() as u64);
731            if !this.root_modules.contains_key(&manifest) {
732                bytes = bytes
733                    .saturating_add(STAGED_GRAPH_ROW_BYTES)
734                    .saturating_add(manifest.len() as u64);
735            }
736            scratch.reserve(this.shared, bytes)?;
737            let mut selected = RootModuleCandidates::new();
738            for ordinal in &candidates.roots {
739                this.shared.check_budget()?;
740                let root = &index.roots[*ordinal].path;
741                if !this.shared.sources.nodes.contains_key(root.as_str())
742                    || this.source(root)?.is_none()
743                {
744                    continue;
745                }
746                let Some(modules) = this
747                    .slots
748                    .get(root)
749                    .and_then(|slots| slots.modules.get::<[String]>(&[]))
750                else {
751                    continue;
752                };
753                for name in modules.keys() {
754                    this.shared.check_budget()?;
755                    // The row envelope covers vector growth, including spare capacity.
756                    let extra =
757                        STAGED_GRAPH_ROW_BYTES.saturating_add(if selected.contains_key(name) {
758                            0
759                        } else {
760                            STAGED_GRAPH_ROW_BYTES.saturating_add(name.len() as u64)
761                        });
762                    scratch.reserve(this.shared, extra)?;
763                    bytes = bytes.saturating_add(extra);
764                    selected.entry(name.clone()).or_default().push(*ordinal);
765                }
766            }
767            Ok((selected, bytes))
768        })?;
769        let modules = Arc::new(modules);
770        self.root_modules
771            .entry(manifest)
772            .or_default()
773            .insert(directory.to_string(), Arc::clone(&modules));
774        Ok(modules)
775    }
776
777    /// Prove membership through actual module declarations along a narrowed path.
778    fn membership(&mut self, path: &str) -> Result<Arc<Membership>, CliError> {
779        if let Some(result) = self.memberships.get(path) {
780            return Ok(Arc::clone(result));
781        }
782        let result = self.with_scratch(|this, scratch| {
783            let mut causes = CauseWorkspace::new(scratch);
784            let mut selected_owner = None;
785            let roots = this.roots(path, &mut causes)?;
786            let index = this.root_membership_index(path, roots)?;
787            let mut owner = None;
788            let mut count = 0;
789            let direct = index
790                .roots
791                .binary_search_by(|candidate| candidate.path.as_str().cmp(path))
792                .ok();
793            let mut directory = parent(path);
794            loop {
795                this.shared.check_budget()?;
796                if let Some(entry) = index.directories.get(directory)
797                    && let Some(parts) = candidate_path(directory, path)
798                {
799                    for key in &entry.causes {
800                        this.insert_cause(&mut causes, key)?;
801                    }
802                    let direct =
803                        direct.filter(|ordinal| parent(&index.roots[*ordinal].path) == directory);
804                    let modules = this.root_directory_modules(path, &index, directory, entry)?;
805                    let declared = parts.first().and_then(|name| modules.get(name));
806                    for ordinal in direct.into_iter().chain(
807                        declared
808                            .into_iter()
809                            .flatten()
810                            .copied()
811                            .filter(|ordinal| Some(*ordinal) != direct),
812                    ) {
813                        this.shared.check_budget()?;
814                        let candidate = &index.roots[ordinal];
815                        let root = &candidate.path;
816                        if !this.cause(root, &mut causes)? {
817                            continue;
818                        }
819                        let mut module = Module {
820                            root: root.clone(),
821                            file: root.clone(),
822                            inline: Arc::from([]),
823                            logical: Vec::new(),
824                            directory: directory.to_string(),
825                            visibility: Visibility::Public,
826                        };
827                        if module.file != path {
828                            for part in &parts {
829                                let Some(child) =
830                                    this.declared_module(&module, part, &mut causes)?
831                                else {
832                                    break;
833                                };
834                                module = child;
835                                if module.file == path {
836                                    break;
837                                }
838                            }
839                        }
840                        if module.file == path && module.inline.is_empty() {
841                            count += 1;
842                            if candidate.certain {
843                                owner = Some(module);
844                            }
845                        }
846                    }
847                }
848                if directory.is_empty() {
849                    break;
850                }
851                directory = parent(directory);
852            }
853            if count == 1 {
854                selected_owner = owner;
855            }
856            let result = Membership {
857                owner: selected_owner,
858                causes: causes.keys,
859            };
860            let cause_bytes = result.causes.iter().fold(0_u64, |bytes, key| {
861                bytes
862                    .saturating_add(STAGED_GRAPH_ROW_BYTES)
863                    .saturating_add(key.canonical_identity().len() as u64)
864            });
865            let retained_bytes = STAGED_GRAPH_ROW_BYTES
866                .saturating_add(path.len() as u64)
867                .saturating_add(cause_bytes)
868                .saturating_add(result.owner.as_ref().map_or(0, module_bytes));
869            Ok((result, retained_bytes))
870        })?;
871        let result = Arc::new(result);
872        self.memberships
873            .insert(path.to_string(), Arc::clone(&result));
874        Ok(result)
875    }
876
877    /// An external module requires one declaration and exactly one admitted layout.
878    fn declared_module(
879        &mut self,
880        owner: &Module,
881        name: &str,
882        causes: &mut CauseWorkspace<'_>,
883    ) -> Result<Option<Module>, CliError> {
884        if !self.cause(&owner.file, causes)? {
885            return Ok(None);
886        }
887        let Some(source) = self.source(&owner.file)? else {
888            return Ok(None);
889        };
890        let Some(indices) = self
891            .slots
892            .get(&owner.file)
893            .and_then(|slots| slots.modules.get(&owner.inline))
894            .and_then(|slots| slots.get(name))
895        else {
896            return Ok(None);
897        };
898        if indices.len() != 1 {
899            return Ok(None);
900        }
901        let Some(module) = source
902            .source
903            .semantics()
904            .and_then(SourceSemanticFacts::rust)
905            .and_then(|facts| facts.modules.get(indices[0]))
906        else {
907            return Ok(None);
908        };
909        if !module.supported {
910            return Ok(None);
911        }
912        let mut child = owner.clone();
913        child.visibility = module.visibility;
914        child.logical.push(name.to_string());
915        child.directory = join(&owner.directory, name);
916        if module.body.is_some() {
917            let mut inline = owner.inline.to_vec();
918            inline.push(name.to_string());
919            child.inline = inline.into();
920        } else {
921            let direct = join(&owner.directory, &format!("{name}.rs"));
922            let nested = join(&owner.directory, &format!("{name}/mod.rs"));
923            child.file = match (self.cause(&direct, causes)?, self.cause(&nested, causes)?) {
924                (true, false) => direct,
925                (false, true) => nested,
926                _ => return Ok(None),
927            };
928            child.inline = Arc::from([]);
929        }
930        Ok(Some(child))
931    }
932
933    /// Recover an actual containing module from its target root.
934    fn address(
935        &mut self,
936        owner: &Module,
937        logical: &[String],
938        causes: &mut CauseWorkspace<'_>,
939    ) -> Result<Option<Module>, CliError> {
940        let mut module = Module {
941            root: owner.root.clone(),
942            file: owner.root.clone(),
943            inline: Arc::from([]),
944            logical: Vec::new(),
945            directory: parent(&owner.root).to_string(),
946            visibility: Visibility::Public,
947        };
948        for name in logical {
949            let Some(child) = self.declared_module(&module, name, causes)? else {
950                return Ok(None);
951            };
952            module = child;
953        }
954        Ok(Some(module))
955    }
956
957    /// Merge only candidates from the actual winning import-only lexical group.
958    fn resolve_candidates(
959        &mut self,
960        source: &str,
961        facts: &SourceSemanticFacts,
962        call: &projectatlas_symbols::SourceRustCallImports,
963    ) -> Result<Arc<CallBindingResolution>, CliError> {
964        let key = (
965            source.to_string(),
966            Arc::as_ptr(&call.bindings),
967            Arc::as_ptr(&call.declarations),
968            call.member.clone(),
969        );
970        if let Some(result) = self.candidate_queries.get(&key) {
971            return Ok(Arc::clone(result));
972        }
973        let result = self.with_scratch(|this, scratch| {
974            let mut causes = CauseWorkspace::new(scratch);
975            let mut result = CallBindingResolution {
976                followed_export: true,
977                ..CallBindingResolution::default()
978            };
979            let mut count = 0_u32;
980            if call.member.is_none() && !call.declarations.is_empty() {
981                let owner = this.shared.source(source)?;
982                for ordinal in call.declarations.iter() {
983                    this.shared.check_budget()?;
984                    count += 1;
985                    result.target = this.shared.target(&owner, *ordinal)?;
986                }
987            }
988            for (import, reference) in call.bindings.iter() {
989                this.shared.check_budget()?;
990                let Some(import) = facts
991                    .imports()
992                    .get(*import)
993                    .filter(|import| import.references.get(*reference).is_some())
994                else {
995                    count += 1;
996                    continue;
997                };
998                let Some(binding) = import
999                    .rust
1000                    .as_deref()
1001                    .filter(|binding| binding.supported && binding.role != UseRole::Wildcard)
1002                else {
1003                    count += 1;
1004                    continue;
1005                };
1006                let mut path = binding.path.clone();
1007                if let Some(member) = &call.member {
1008                    path.segments
1009                        .extend(member.split("::").map(ToString::to_string));
1010                }
1011                let outcome = this.resolve(Request {
1012                    source: source.to_string(),
1013                    module: Arc::clone(&binding.module),
1014                    path,
1015                    namespace: binding.role == UseRole::SelfNamespace && call.member.is_none(),
1016                    import: false,
1017                    toolchain: import.rust_toolchain.is_some(),
1018                })?;
1019                for key in &outcome.dependencies {
1020                    this.insert_cause(&mut causes, key)?;
1021                }
1022                if outcome.absent {
1023                    continue;
1024                }
1025                count += 1;
1026                result.target.clone_from(&outcome.target);
1027                result.ambiguous = result.ambiguous.max(outcome.ambiguous);
1028            }
1029            let source_key = document_file_resolution_key(this.shared.project, source)?;
1030            this.insert_cause(&mut causes, &source_key)?;
1031            if count > 1 {
1032                result.ambiguous = result.ambiguous.max(std::num::NonZeroU32::new(count));
1033                result.target = None;
1034            }
1035            result.absent = count == 0;
1036            result.dependencies = causes.keys;
1037            let retained_bytes = result
1038                .retained_bytes()
1039                .saturating_add(STAGED_GRAPH_ROW_BYTES * 2)
1040                .saturating_add(key.0.capacity() as u64)
1041                .saturating_add(key.3.as_ref().map_or(0, String::capacity) as u64);
1042            Ok((result, retained_bytes))
1043        })?;
1044        let result = Arc::new(result);
1045        self.candidate_queries.insert(key, Arc::clone(&result));
1046        Ok(result)
1047    }
1048
1049    /// Resolve one complete call/import request and retain all failed-branch causes.
1050    fn resolve(&mut self, request: Request) -> Result<Arc<CallBindingResolution>, CliError> {
1051        if let Some(result) = self.queries.get(&request) {
1052            return Ok(Arc::clone(result));
1053        }
1054        self.ambiguous = 0;
1055        let membership = if request.toolchain {
1056            None
1057        } else {
1058            Some(self.membership(&request.source)?)
1059        };
1060        let result = self.with_scratch(|this, scratch| {
1061            let mut causes = CauseWorkspace::new(scratch);
1062            let mut result = CallBindingResolution {
1063                followed_export: true,
1064                absent: !request.toolchain && request.namespace && !request.import,
1065                ..CallBindingResolution::default()
1066            };
1067            if request.toolchain {
1068                result.target = Some(this.external(&request.path.segments.join("::"))?);
1069                let key = document_file_resolution_key(this.shared.project, &request.source)?;
1070                this.insert_cause(&mut causes, &key)?;
1071            }
1072            if let Some(membership) = &membership {
1073                for key in &membership.causes {
1074                    this.insert_cause(&mut causes, key)?;
1075                }
1076            }
1077            if let Some(owner) = membership
1078                .as_ref()
1079                .and_then(|membership| membership.owner.as_ref())
1080            {
1081                let mut logical = owner.logical.clone();
1082                logical.extend(request.module.iter().cloned());
1083                if let Some(module) = this.address(owner, &logical, &mut causes)? {
1084                    let mut visited = BTreeSet::new();
1085                    if request.import {
1086                        let namespace = this.path(
1087                            &module,
1088                            &request.path,
1089                            Namespace::Module,
1090                            &logical,
1091                            &mut causes,
1092                            &mut visited,
1093                            0,
1094                        )?;
1095                        let value = if request.namespace {
1096                            None
1097                        } else {
1098                            this.path(
1099                                &module,
1100                                &request.path,
1101                                Namespace::Value,
1102                                &logical,
1103                                &mut causes,
1104                                &mut visited,
1105                                0,
1106                            )?
1107                        };
1108                        result.target = this.import_target(namespace, value)?;
1109                    } else if !request.namespace {
1110                        match this.path(
1111                            &module,
1112                            &request.path,
1113                            Namespace::Value,
1114                            &logical,
1115                            &mut causes,
1116                            &mut visited,
1117                            0,
1118                        )? {
1119                            Some(Selection::Value(entity, _, _)) => result.target = Some(entity),
1120                            Some(Selection::Absent) => result.absent = true,
1121                            _ => {}
1122                        }
1123                    }
1124                }
1125            }
1126            if result.target.is_none() && this.ambiguous > 1 {
1127                result.ambiguous = std::num::NonZeroU32::new(this.ambiguous);
1128            }
1129            result.dependencies = causes.keys;
1130            let retained_bytes = result
1131                .retained_bytes()
1132                .saturating_add(STAGED_GRAPH_ROW_BYTES)
1133                .saturating_add(request.source.capacity() as u64)
1134                .saturating_add(
1135                    (request.path.segments.capacity() * std::mem::size_of::<String>()) as u64,
1136                )
1137                .saturating_add(request.path.segments.iter().fold(0_u64, |bytes, part| {
1138                    bytes.saturating_add(part.capacity() as u64)
1139                }));
1140            Ok((result, retained_bytes))
1141        })?;
1142        let result = Arc::new(result);
1143        self.queries.insert(request, Arc::clone(&result));
1144        Ok(result)
1145    }
1146
1147    /// Preserve external identities and exact dependency file owners independently.
1148    fn import_target(
1149        &mut self,
1150        namespace: Option<Selection>,
1151        value: Option<Selection>,
1152    ) -> Result<Option<GraphEntity>, CliError> {
1153        let target = |selection: Selection| -> Result<Option<GraphEntity>, CliError> {
1154            let file = match selection {
1155                Selection::Absent => return Ok(None),
1156                Selection::Module(module) | Selection::Type { owner: module, .. } => module.file,
1157                Selection::Value(entity, _, _) => match entity.selector() {
1158                    EntitySelector::External { .. } => return Ok(Some(entity)),
1159                    EntitySelector::Symbol { symbol } => symbol.file.as_str().to_string(),
1160                    _ => return Ok(None),
1161                },
1162            };
1163            Ok(Some(
1164                GraphEntity::new(
1165                    self.shared.project,
1166                    EntitySelector::File {
1167                        path: RepositoryFilePath::new(std::path::Path::new(&file))
1168                            .map_err(invalid_graph_contract)?,
1169                    },
1170                    self.shared.generation,
1171                )
1172                .map_err(invalid_graph_contract)?,
1173            ))
1174        };
1175        let namespace = namespace.map(target).transpose()?.flatten();
1176        let value = value.map(target).transpose()?.flatten();
1177        match (namespace, value) {
1178            (Some(left), Some(right)) if left.key() != right.key() => {
1179                self.ambiguous = self.ambiguous.max(2);
1180                Ok(None)
1181            }
1182            (Some(target), _) | (_, Some(target)) => Ok(Some(target)),
1183            (None, None) => Ok(None),
1184        }
1185    }
1186
1187    /// Create a content-free toolchain identity only from source or target-edition proof.
1188    fn external(&self, identity: &str) -> Result<GraphEntity, CliError> {
1189        GraphEntity::new(
1190            self.shared.project,
1191            EntitySelector::External {
1192                external: ExternalSelector {
1193                    system: super::GraphIdentityText::new(super::super::RUST_TOOLCHAIN_SYSTEM)
1194                        .map_err(invalid_graph_contract)?,
1195                    identity: super::GraphIdentityText::new(identity)
1196                        .map_err(invalid_graph_contract)?,
1197                },
1198            },
1199            self.shared.generation,
1200        )
1201        .map_err(invalid_graph_contract)
1202    }
1203
1204    /// Resolve an explicit anchor before walking namespace members.
1205    #[allow(clippy::too_many_arguments)]
1206    fn path(
1207        &mut self,
1208        owner: &Module,
1209        path: &SourceRustPath,
1210        namespace: Namespace,
1211        requester: &[String],
1212        causes: &mut CauseWorkspace<'_>,
1213        visited: &mut BTreeSet<(Module, Vec<String>, Namespace)>,
1214        depth: usize,
1215    ) -> Result<Option<Selection>, CliError> {
1216        self.shared.check_budget()?;
1217        if depth >= MAX_FORWARDING_DEPTH {
1218            return Ok(None);
1219        }
1220        let absolute_edition = if matches!(path.anchor, Anchor::Absolute | Anchor::Unqualified) {
1221            let manifest = self
1222                .shared
1223                .packages
1224                .package_owner(&owner.file)
1225                .map(|package| package.manifest.clone());
1226            if let Some(manifest) = manifest {
1227                self.cause(&manifest, causes)?;
1228                self.shared
1229                    .source(&manifest)?
1230                    .source
1231                    .semantics()
1232                    .and_then(SourceSemanticFacts::cargo)
1233                    .and_then(|targets| targets.edition)
1234            } else {
1235                None
1236            }
1237        } else {
1238            None
1239        };
1240        if path.anchor == Anchor::Absolute
1241            && absolute_edition.is_some_and(|edition| edition >= 2018)
1242        {
1243            if path
1244                .segments
1245                .first()
1246                .is_some_and(|root| matches!(root.as_str(), "std" | "core" | "alloc"))
1247            {
1248                return Ok(Some(Selection::Value(
1249                    self.external(&path.segments.join("::"))?,
1250                    Visibility::Public,
1251                    Vec::new(),
1252                )));
1253            }
1254            return Ok(None);
1255        }
1256        let logical = match path.anchor {
1257            Anchor::Crate => &[][..],
1258            Anchor::SelfModule => owner.logical.as_slice(),
1259            Anchor::Unqualified | Anchor::Absolute if absolute_edition == Some(2015) => &[][..],
1260            Anchor::Unqualified
1261                if absolute_edition.is_some_and(|edition| edition >= 2018)
1262                    || owner.logical.is_empty() =>
1263            {
1264                owner.logical.as_slice()
1265            }
1266            Anchor::Super(parents) if parents <= owner.logical.len() => {
1267                &owner.logical[..owner.logical.len() - parents]
1268            }
1269            Anchor::Unqualified | Anchor::Super(_) | Anchor::Absolute => return Ok(None),
1270        };
1271        let Some(module) = self.address(owner, logical, causes)? else {
1272            return Ok(None);
1273        };
1274        self.select(
1275            &module,
1276            &path.segments,
1277            namespace,
1278            requester,
1279            causes,
1280            visited,
1281            depth,
1282        )
1283    }
1284
1285    /// Preserve duplicate slots and terminate cycles without narrowing partial proof.
1286    #[allow(clippy::too_many_arguments)]
1287    fn select(
1288        &mut self,
1289        owner: &Module,
1290        parts: &[String],
1291        namespace: Namespace,
1292        requester: &[String],
1293        causes: &mut CauseWorkspace<'_>,
1294        visited: &mut BTreeSet<(Module, Vec<String>, Namespace)>,
1295        depth: usize,
1296    ) -> Result<Option<Selection>, CliError> {
1297        self.shared.check_budget()?;
1298        if depth >= MAX_FORWARDING_DEPTH {
1299            return Ok(None);
1300        }
1301        // Reserve both owned query copies and their set node before cloning.
1302        let frame_bytes = STAGED_GRAPH_ROW_BYTES.saturating_mul(2).saturating_add(
1303            module_bytes(owner)
1304                .saturating_mul(2)
1305                .saturating_add(std::mem::size_of_val(parts) as u64 * 2)
1306                .saturating_add(parts.iter().fold(0_u64, |bytes, part| {
1307                    bytes.saturating_add(part.len() as u64 * 2)
1308                })),
1309        );
1310        self.shared.reserve_scratch(frame_bytes)?;
1311        let query = (owner.clone(), parts.to_vec(), namespace);
1312        if !visited.insert(query.clone()) {
1313            self.shared.release_scratch(frame_bytes)?;
1314            return Ok(None);
1315        }
1316        let result = self.select_inner(owner, parts, namespace, requester, causes, visited, depth);
1317        visited.remove(&query);
1318        self.shared.release_scratch(frame_bytes)?;
1319        result
1320    }
1321
1322    /// One namespace step checks visibility and source-owned competing bindings.
1323    #[allow(clippy::too_many_arguments)]
1324    fn select_inner(
1325        &mut self,
1326        owner: &Module,
1327        parts: &[String],
1328        namespace: Namespace,
1329        requester: &[String],
1330        causes: &mut CauseWorkspace<'_>,
1331        visited: &mut BTreeSet<(Module, Vec<String>, Namespace)>,
1332        depth: usize,
1333    ) -> Result<Option<Selection>, CliError> {
1334        if !self.cause(&owner.file, causes)? {
1335            return Ok(None);
1336        }
1337        let Some(source) = self.source(&owner.file)? else {
1338            return Ok(None);
1339        };
1340        let Some((name, rest)) = parts.split_first() else {
1341            return Ok((namespace == Namespace::Module).then(|| Selection::Module(owner.clone())));
1342        };
1343        let wanted = if rest.is_empty() {
1344            namespace
1345        } else {
1346            Namespace::Module
1347        };
1348        let Some(facts) = source
1349            .source
1350            .semantics()
1351            .and_then(SourceSemanticFacts::rust)
1352        else {
1353            return Ok(None);
1354        };
1355        let slots = &self.slots[&owner.file];
1356        let values = slots
1357            .values
1358            .get(&owner.inline)
1359            .and_then(|slots| slots.get(name))
1360            .cloned()
1361            .unwrap_or_default();
1362        let modules = slots
1363            .modules
1364            .get(&owner.inline)
1365            .and_then(|slots| slots.get(name))
1366            .cloned()
1367            .unwrap_or_default();
1368        let mut count = 0_usize;
1369        let mut selected = None;
1370        let mut opposite = false;
1371        if wanted == Namespace::Module {
1372            for index in &modules {
1373                self.shared.control.check(IndexWorkStage::SymbolParsing)?;
1374                let module = &facts.modules[*index];
1375                count += 1;
1376                if modules.len() == 1 && visible(module.visibility, &owner.logical, requester) {
1377                    selected = self
1378                        .declared_module(owner, name, causes)?
1379                        .map(Selection::Module);
1380                }
1381            }
1382        } else {
1383            opposite = !modules.is_empty();
1384        }
1385        for index in values {
1386            self.shared.control.check(IndexWorkStage::SymbolParsing)?;
1387            let declaration = &facts.declarations[index];
1388            let candidate = match (&declaration.binding, wanted) {
1389                (
1390                    SourceRustBinding::TypeDeclaration(_) | SourceRustBinding::UnresolvedType,
1391                    Namespace::Value,
1392                )
1393                | (
1394                    SourceRustBinding::Declaration(_) | SourceRustBinding::Unresolved,
1395                    Namespace::Module,
1396                ) => {
1397                    opposite = true;
1398                    continue;
1399                }
1400                (SourceRustBinding::Use(binding), Namespace::Value)
1401                    if binding.role == UseRole::SelfNamespace =>
1402                {
1403                    opposite = true;
1404                    continue;
1405                }
1406                _ if !visible(declaration.visibility, &owner.logical, requester) => None,
1407                (SourceRustBinding::TypeDeclaration(_), Namespace::Module) => {
1408                    Some(Selection::Type {
1409                        owner: owner.clone(),
1410                        name: name.clone(),
1411                        visibility: declaration.visibility,
1412                    })
1413                }
1414                (SourceRustBinding::Declaration(index), Namespace::Value) => {
1415                    self.shared.target(&source, *index)?.map(|entity| {
1416                        Selection::Value(entity, declaration.visibility, owner.logical.clone())
1417                    })
1418                }
1419                (SourceRustBinding::Use(binding), _)
1420                    if binding.supported && binding.role != UseRole::Wildcard =>
1421                {
1422                    if let Some(identity) =
1423                        self.slots[&owner.file].toolchain.get(&Arc::as_ptr(binding))
1424                    {
1425                        Some(Selection::Value(
1426                            self.external(identity)?,
1427                            Visibility::Public,
1428                            Vec::new(),
1429                        ))
1430                    } else {
1431                        self.path(
1432                            owner,
1433                            &binding.path,
1434                            wanted,
1435                            &owner.logical,
1436                            causes,
1437                            visited,
1438                            depth + 1,
1439                        )?
1440                        .filter(|selection| selection_visible(selection, requester))
1441                    }
1442                }
1443                _ => None,
1444            };
1445            if matches!(candidate, Some(Selection::Absent)) {
1446                opposite = true;
1447                continue;
1448            }
1449            count += 1;
1450            selected = candidate;
1451        }
1452        if count != 1 {
1453            self.ambiguous = self.ambiguous.max(u32::try_from(count).map_err(|error| {
1454                CliError::InvalidInput(format!("Rust slot count exceeds graph range: {error}"))
1455            })?);
1456            return Ok((count == 0 && opposite).then_some(Selection::Absent));
1457        }
1458        if rest.is_empty() {
1459            return Ok(selected);
1460        }
1461        if matches!(selected, Some(Selection::Absent)) {
1462            return Ok(selected);
1463        }
1464        if let Some(Selection::Value(entity, _, _)) = &selected
1465            && let EntitySelector::External { external } = entity.selector()
1466        {
1467            let identity = format!("{}::{}", external.identity.as_str(), rest.join("::"));
1468            return Ok(Some(Selection::Value(
1469                self.external(&identity)?,
1470                Visibility::Public,
1471                Vec::new(),
1472            )));
1473        }
1474        if let Some(Selection::Type { owner, name, .. }) = &selected {
1475            if rest.len() != 1 || namespace != Namespace::Value {
1476                return Ok(None);
1477            }
1478            let Some(indices) = self.slots.get(&owner.file).and_then(|slots| {
1479                slots
1480                    .associated
1481                    .get(&(Arc::clone(&owner.inline), name.clone(), rest[0].clone()))
1482            }) else {
1483                return Ok(None);
1484            };
1485            if indices.len() != 1 {
1486                self.ambiguous =
1487                    self.ambiguous
1488                        .max(u32::try_from(indices.len()).map_err(|error| {
1489                            CliError::InvalidInput(format!(
1490                                "Rust method count exceeds graph range: {error}"
1491                            ))
1492                        })?);
1493                return Ok(None);
1494            }
1495            let ordinal = indices[0];
1496            let Some(source) = self.source(&owner.file)? else {
1497                return Ok(None);
1498            };
1499            let Some(method) = source
1500                .source
1501                .semantics()
1502                .and_then(SourceSemanticFacts::rust)
1503                .and_then(|facts| facts.associated.get(ordinal))
1504            else {
1505                return Ok(None);
1506            };
1507            if !method.supported || !visible(method.visibility, &owner.logical, requester) {
1508                return Ok(None);
1509            }
1510            return method.declaration.map_or_else(
1511                || Ok(None),
1512                |ordinal| {
1513                    self.shared.target(&source, ordinal).map(|entity| {
1514                        entity.map(|entity| {
1515                            Selection::Value(entity, method.visibility, owner.logical.clone())
1516                        })
1517                    })
1518                },
1519            );
1520        }
1521        let Some(Selection::Module(module)) = selected else {
1522            return Ok(None);
1523        };
1524        self.select(
1525            &module,
1526            rest,
1527            namespace,
1528            requester,
1529            causes,
1530            visited,
1531            depth + 1,
1532        )
1533    }
1534}
1535
1536/// A public alias cannot expose an item whose own visibility excludes the caller.
1537fn selection_visible(selection: &Selection, requester: &[String]) -> bool {
1538    match selection {
1539        Selection::Absent => true,
1540        Selection::Value(_, visibility, owner) => visible(*visibility, owner, requester),
1541        Selection::Type {
1542            owner, visibility, ..
1543        } => visible(*visibility, &owner.logical, requester),
1544        Selection::Module(owner) => visible(
1545            owner.visibility,
1546            &owner.logical[..owner.logical.len().saturating_sub(1)],
1547            requester,
1548        ),
1549    }
1550}
1551
1552/// Private values remain visible to descendants, including child root-function imports.
1553fn visible(visibility: Visibility, owner: &[String], requester: &[String]) -> bool {
1554    match visibility {
1555        Visibility::Public | Visibility::Crate => true,
1556        Visibility::Private => requester.starts_with(owner),
1557        Visibility::Unsupported => false,
1558    }
1559}
1560
1561/// Borrow the directory of a canonical repository path.
1562fn parent(path: &str) -> &str {
1563    path.rsplit_once('/').map_or("", |(parent, _)| parent)
1564}
1565
1566/// Join canonical relative components without inventing a leading separator.
1567fn join(directory: &str, path: &str) -> String {
1568    if directory.is_empty() {
1569        path.to_string()
1570    } else {
1571        format!("{directory}/{path}")
1572    }
1573}
1574
1575/// Layout only narrows candidates; actual module declarations establish membership.
1576fn candidate_path(directory: &str, path: &str) -> Option<Vec<String>> {
1577    let relative = if directory.is_empty() {
1578        path
1579    } else {
1580        path.strip_prefix(directory)?.strip_prefix('/')?
1581    };
1582    let relative = relative.strip_suffix(".rs")?;
1583    let mut parts = relative
1584        .split('/')
1585        .map(ToString::to_string)
1586        .collect::<Vec<_>>();
1587    if parts.last().is_some_and(|part| part == "mod") {
1588        parts.pop();
1589    }
1590    (parts.len() <= MAX_FORWARDING_DEPTH).then_some(parts)
1591}
1592
1593#[cfg(test)]
1594mod tests {
1595    use super::super::super::{
1596        PackageIndex, ProjectResolutionRegistry, SourceBindingCache, build_entity_projection,
1597    };
1598    use super::super::BindingResolver;
1599    use super::{Module, Namespace, RustResolver, Visibility};
1600    use projectatlas_core::IndexGeneration;
1601    use projectatlas_core::{IndexCancellation, IndexWorkControl, IndexWorkFailure};
1602    use projectatlas_db::AtlasStore;
1603    use projectatlas_symbols::ConfiguredModuleResolution;
1604    use std::collections::{BTreeMap, BTreeSet};
1605    use std::error::Error;
1606    use std::io;
1607    use std::sync::Arc;
1608
1609    /// Report a failed boundary proof through the test's fallible result.
1610    fn require(condition: bool, message: &str) -> Result<(), super::CliError> {
1611        if !condition {
1612            return Err(super::CliError::InvalidInput(message.to_string()));
1613        }
1614        Ok(())
1615    }
1616
1617    /// Preserve actual and expected values without panicking in a fallible test.
1618    fn require_eq<T: std::fmt::Debug + PartialEq>(
1619        actual: &T,
1620        expected: &T,
1621    ) -> Result<(), super::CliError> {
1622        if actual != expected {
1623            return Err(super::CliError::InvalidInput(format!(
1624                "actual {actual:?}, expected {expected:?}"
1625            )));
1626        }
1627        Ok(())
1628    }
1629
1630    /// Use the real resolver and empty captured inventory with an isolated store.
1631    fn with_empty_resolver(
1632        check: impl FnOnce(&mut RustResolver<'_, '_, '_, '_>, &Module) -> Result<(), Box<dyn Error>>,
1633    ) -> Result<(), Box<dyn Error>> {
1634        with_source_resolver(&[], check)
1635    }
1636
1637    /// Capture real source identities while allowing recovery-work assertions.
1638    fn with_source_resolver(
1639        files: &[(&str, &str)],
1640        check: impl FnOnce(&mut RustResolver<'_, '_, '_, '_>, &Module) -> Result<(), Box<dyn Error>>,
1641    ) -> Result<(), Box<dyn Error>> {
1642        let temp = tempfile::tempdir()?;
1643        let root = temp.path();
1644        let mut nodes = Vec::new();
1645        for (path, source) in files {
1646            let native = root.join(path);
1647            if let Some(parent) = native.parent() {
1648                std::fs::create_dir_all(parent)?;
1649            }
1650            std::fs::write(native, source)?;
1651            nodes.push(projectatlas_core::Node {
1652                path: (*path).to_string(),
1653                kind: projectatlas_core::NodeKind::File,
1654                parent_path: path
1655                    .rsplit_once('/')
1656                    .map(|(directory, _)| directory.to_string()),
1657                extension: Some(".rs".to_string()),
1658                language: Some("rust".to_string()),
1659                size_bytes: Some(source.len() as u64),
1660                mtime_ns: None,
1661                content_hash: Some(blake3::hash(source.as_bytes()).to_hex().to_string()),
1662            });
1663        }
1664        let store = AtlasStore::open_for_project(&root.join("atlas.db"), root)?;
1665        let project = store
1666            .project_instance_id()?
1667            .ok_or_else(|| io::Error::other("test project identity missing"))?;
1668        let control = IndexWorkControl::new(IndexCancellation::new(), None);
1669        let generation = IndexGeneration::new(1);
1670        let graphs: &[&projectatlas_core::symbols::SymbolGraph] = &[];
1671        let packages = PackageIndex::from_graphs(graphs)?;
1672        let configured = ConfiguredModuleResolution::default();
1673        let entities =
1674            build_entity_projection(project, generation, &[], graphs, &packages, true, &control)?;
1675        let direct = BTreeSet::from(["src/bin/a.rs".to_string()]);
1676        let mut sources = SourceBindingCache::new(root, &nodes, Some(&direct));
1677        let mut candidates = ProjectResolutionRegistry::default();
1678        let replaced = BTreeSet::new();
1679        let mut shared = BindingResolver {
1680            store: &store,
1681            project,
1682            generation,
1683            base_generation: None,
1684            graphs: BTreeMap::new(),
1685            entities: &entities,
1686            packages: &packages,
1687            configured: &configured,
1688            sources: &mut sources,
1689            candidates: &mut candidates,
1690            replaced: &replaced,
1691            loaded: None,
1692            context: BTreeMap::new(),
1693            hydrated: BTreeMap::new(),
1694            modules: super::super::ModuleInterner::default(),
1695            queries: BTreeMap::new(),
1696            retained_bytes: 128 + std::mem::size_of::<super::super::ModuleInterner>() as u64,
1697            retained_limit: 64 * 1_024,
1698            control: &control,
1699        };
1700        let mut resolver = RustResolver {
1701            shared: &mut shared,
1702            memberships: BTreeMap::new(),
1703            roots: BTreeMap::new(),
1704            root_memberships: BTreeMap::new(),
1705            root_modules: BTreeMap::new(),
1706            slots: BTreeMap::new(),
1707            queries: BTreeMap::new(),
1708            ambiguous: 0,
1709            candidate_queries: BTreeMap::new(),
1710        };
1711        let module = Module {
1712            root: "src/lib.rs".to_string(),
1713            file: "missing.rs".to_string(),
1714            inline: Arc::from([]),
1715            logical: Vec::new(),
1716            directory: "src".to_string(),
1717            visibility: Visibility::Public,
1718        };
1719        check(&mut resolver, &module)
1720    }
1721
1722    #[test]
1723    fn root_directory_recovery_keeps_unrelated_target_sources_unopened()
1724    -> Result<(), Box<dyn Error>> {
1725        with_source_resolver(
1726            &[
1727                ("src/bin/a.rs", "mod needed; fn main(){}"),
1728                ("src/bin/b.rs", "fn main(){}"),
1729                ("examples/unrelated.rs", "mod unrelated; fn main(){}"),
1730            ],
1731            |resolver, _| {
1732                let roots: Arc<[super::RootCandidate]> =
1733                    ["examples/unrelated.rs", "src/bin/a.rs", "src/bin/b.rs"]
1734                        .into_iter()
1735                        .map(|path| super::RootCandidate {
1736                            path: path.to_string(),
1737                            certain: true,
1738                            cohort: None,
1739                        })
1740                        .collect::<Vec<_>>()
1741                        .into();
1742                let index = resolver.root_membership_index("src/bin/a.rs", roots)?;
1743                require(
1744                    resolver.shared.sources.captured.is_empty(),
1745                    "path buckets opened source files",
1746                )?;
1747                let candidates = index
1748                    .directories
1749                    .get("src/bin")
1750                    .ok_or_else(|| io::Error::other("root directory missing"))?;
1751                let modules = resolver.root_directory_modules(
1752                    "src/bin/a.rs",
1753                    &index,
1754                    "src/bin",
1755                    candidates,
1756                )?;
1757                require_eq(&modules.get("needed"), &Some(&vec![1]))?;
1758                require_eq(
1759                    &resolver
1760                        .shared
1761                        .sources
1762                        .captured
1763                        .keys()
1764                        .map(String::as_str)
1765                        .collect::<Vec<_>>(),
1766                    &vec!["src/bin/a.rs", "src/bin/b.rs"],
1767                )?;
1768                require_eq(&resolver.shared.sources.work.additional_candidates, &1)?;
1769                require(
1770                    !resolver.slots.contains_key("examples/unrelated.rs"),
1771                    "unrelated slots were retained",
1772                )?;
1773                let bytes = resolver.shared.retained_staging_bytes();
1774                let repeated = resolver.root_directory_modules(
1775                    "src/bin/a.rs",
1776                    &index,
1777                    "src/bin",
1778                    candidates,
1779                )?;
1780                require(
1781                    Arc::ptr_eq(&modules, &repeated),
1782                    "directory modules were rebuilt",
1783                )?;
1784                require_eq(&resolver.shared.retained_staging_bytes(), &bytes)?;
1785                Ok(())
1786            },
1787        )
1788    }
1789
1790    #[test]
1791    fn root_candidate_index_refuses_without_partial_publication_and_reuses_complete_state()
1792    -> Result<(), Box<dyn Error>> {
1793        with_empty_resolver(|resolver, _| {
1794            let cohort = Arc::new(super::CargoRootCohort {
1795                directory: Arc::from("src/bin"),
1796                key: super::document_file_resolution_key(resolver.shared.project, "Cargo.toml")?,
1797            });
1798            let roots: Arc<[super::RootCandidate]> = (0..256)
1799                .map(|ordinal| super::RootCandidate {
1800                    path: format!("src/bin/t{ordinal:03}.rs"),
1801                    certain: true,
1802                    cohort: Some(Arc::clone(&cohort)),
1803                })
1804                .collect::<Vec<_>>()
1805                .into();
1806            let before = resolver.shared.retained_bytes;
1807            let limit = resolver.shared.retained_limit;
1808            resolver.shared.retained_limit = resolver.shared.retained_staging_bytes() + 1;
1809            let refused = resolver.root_membership_index("src/bin/t000.rs", Arc::clone(&roots));
1810            require(
1811                matches!(
1812                    refused,
1813                    Err(super::CliError::IndexWork(
1814                        IndexWorkFailure::ResourceLimitExceeded { .. }
1815                    ))
1816                ),
1817                "root index must refuse before publication",
1818            )?;
1819            require(
1820                resolver.root_memberships.is_empty(),
1821                "refused root index was published",
1822            )?;
1823            require_eq(&resolver.shared.retained_bytes, &before)?;
1824            resolver.shared.retained_limit = limit;
1825            let first = resolver.root_membership_index("src/bin/t000.rs", Arc::clone(&roots))?;
1826            let charged = resolver.shared.retained_bytes;
1827            let second = resolver.root_membership_index("src/bin/t255.rs", roots)?;
1828            require(
1829                Arc::ptr_eq(&first, &second),
1830                "completed root index was rebuilt",
1831            )?;
1832            require_eq(&resolver.shared.retained_bytes, &charged)?;
1833            // Missing sibling observations must remain one shared invalidation cause.
1834            let causes = first.directories.get("src/bin").map(|entry| &entry.causes);
1835            require_eq(&causes, &Some(&BTreeSet::from([cohort.key.clone()])))?;
1836            Ok(())
1837        })
1838    }
1839
1840    #[test]
1841    fn resolver_frames_refuse_before_erasing_retained_state() -> Result<(), Box<dyn Error>> {
1842        with_empty_resolver(|resolver, module| {
1843            let before = resolver.shared.retained_bytes;
1844            resolver.with_scratch(|this, scratch| {
1845                let mut causes = super::CauseWorkspace::new(scratch);
1846                // The output cause is already owned: the refusal must be at frame admission.
1847                let key = super::document_file_resolution_key(this.shared.project, &module.file)?;
1848                this.insert_cause(&mut causes, &key)?;
1849                this.shared.retained_limit = this.shared.retained_staging_bytes() + 1;
1850                this.shared.check_budget()?;
1851                let before_frame = this.shared.retained_bytes;
1852                let mut visited = BTreeSet::new();
1853                let result = this.select(
1854                    module,
1855                    &["absent".to_string()],
1856                    Namespace::Value,
1857                    &[],
1858                    &mut causes,
1859                    &mut visited,
1860                    0,
1861                );
1862                require(
1863                    matches!(
1864                        result,
1865                        Err(super::CliError::IndexWork(
1866                            IndexWorkFailure::ResourceLimitExceeded { .. }
1867                        ))
1868                    ),
1869                    "resolver boundary proof failed",
1870                )?;
1871                require_eq(&(this.shared.retained_bytes), &(before_frame))?;
1872                require(visited.is_empty(), "resolver boundary proof failed")?;
1873                require_eq(&(causes.keys), &(BTreeSet::from([key])))?;
1874                Ok(((), 0))
1875            })?;
1876            require_eq(&(resolver.shared.retained_bytes), &(before))?;
1877            Ok(())
1878        })
1879    }
1880
1881    #[test]
1882    fn resolver_frames_release_cycle_and_inner_error_ownership() -> Result<(), Box<dyn Error>> {
1883        with_empty_resolver(|resolver, module| {
1884            let before = resolver.shared.retained_bytes;
1885            resolver.with_scratch(|this, scratch| {
1886                let parts = vec!["absent".to_string()];
1887                let ancestor = (module.clone(), parts.clone(), Namespace::Value);
1888                let mut visited = BTreeSet::from([ancestor.clone()]);
1889                let mut causes = super::CauseWorkspace::new(scratch);
1890                let result = this.select(
1891                    module,
1892                    &parts,
1893                    Namespace::Value,
1894                    &[],
1895                    &mut causes,
1896                    &mut visited,
1897                    0,
1898                )?;
1899                require(result.is_none(), "resolver boundary proof failed")?;
1900                require_eq(&(visited), &(BTreeSet::from([ancestor.clone()])))?;
1901                require_eq(&(this.shared.retained_bytes), &(before))?;
1902                let mut invalid = module.clone();
1903                invalid.file = "x".repeat(projectatlas_core::graph::MAX_GRAPH_IDENTITY_BYTES + 1);
1904                let result = this.select(
1905                    &invalid,
1906                    &parts,
1907                    Namespace::Value,
1908                    &[],
1909                    &mut causes,
1910                    &mut visited,
1911                    0,
1912                );
1913                require(
1914                    matches!(result, Err(super::CliError::InvalidInput(_))),
1915                    "resolver boundary proof failed",
1916                )?;
1917                require_eq(&(visited), &(BTreeSet::from([ancestor])))?;
1918                require_eq(&(this.shared.retained_bytes), &(before))?;
1919                Ok(((), 0))
1920            })?;
1921            require_eq(&(resolver.shared.retained_bytes), &(before))?;
1922            Ok(())
1923        })
1924    }
1925
1926    #[test]
1927    fn cause_workspace_deduplicates_and_refuses_before_copying() -> Result<(), Box<dyn Error>> {
1928        with_empty_resolver(|resolver, _| {
1929            let before = resolver.shared.retained_bytes;
1930            resolver.with_scratch(|this, scratch| {
1931                let mut causes = super::CauseWorkspace::new(scratch);
1932                let first = super::document_file_resolution_key(this.shared.project, "first.rs")?;
1933                let next = super::document_file_resolution_key(this.shared.project, "next.rs")?;
1934                this.insert_cause(&mut causes, &first)?;
1935                let retained = this.shared.retained_bytes;
1936                this.insert_cause(&mut causes, &first)?;
1937                require_eq(&(this.shared.retained_bytes), &(retained))?;
1938                require_eq(&(causes.keys.len()), &(1))?;
1939                this.shared.retained_limit = this.shared.retained_staging_bytes();
1940                this.shared.check_budget()?;
1941                let result = this.insert_cause(&mut causes, &next);
1942                require(
1943                    matches!(
1944                        result,
1945                        Err(super::CliError::IndexWork(
1946                            IndexWorkFailure::ResourceLimitExceeded { .. }
1947                        ))
1948                    ),
1949                    "resolver boundary proof failed",
1950                )?;
1951                require_eq(&(this.shared.retained_bytes), &(retained))?;
1952                require_eq(&(causes.keys), &(BTreeSet::from([first])))?;
1953                Ok(((), 0))
1954            })?;
1955            require_eq(&(resolver.shared.retained_bytes), &(before))?;
1956            Ok(())
1957        })
1958    }
1959
1960    #[test]
1961    fn cause_workspace_bounds_rows_and_releases_on_cancellation() -> Result<(), Box<dyn Error>> {
1962        with_empty_resolver(|resolver, _| {
1963            resolver.shared.retained_limit =
1964                super::super::super::super::MAX_PUBLICATION_STAGING_BYTES;
1965            let before = resolver.shared.retained_bytes;
1966            resolver.with_scratch(|this, scratch| {
1967                let mut causes = super::CauseWorkspace::new(scratch);
1968                for index in 0..super::super::GraphLimits::MAX_ROWS {
1969                    let key = super::document_file_resolution_key(
1970                        this.shared.project,
1971                        &format!("file_{index}.rs"),
1972                    )?;
1973                    this.insert_cause(&mut causes, &key)?;
1974                }
1975                let retained = this.shared.retained_bytes;
1976                let excess = super::document_file_resolution_key(this.shared.project, "excess.rs")?;
1977                require(
1978                    this.insert_cause(&mut causes, &excess).is_err(),
1979                    "resolver boundary proof failed",
1980                )?;
1981                require_eq(
1982                    &(causes.keys.len()),
1983                    &(super::super::GraphLimits::MAX_ROWS as usize),
1984                )?;
1985                require_eq(&(this.shared.retained_bytes), &(retained))?;
1986                let duplicate =
1987                    super::document_file_resolution_key(this.shared.project, "file_0.rs")?;
1988                this.insert_cause(&mut causes, &duplicate)?;
1989                require_eq(&(this.shared.retained_bytes), &(retained))?;
1990                Ok(((), 0))
1991            })?;
1992            require_eq(&(resolver.shared.retained_bytes), &(before))?;
1993            let cancelled = resolver.with_scratch(|this, scratch| {
1994                let mut causes = super::CauseWorkspace::new(scratch);
1995                let key = super::document_file_resolution_key(this.shared.project, "cancel.rs")?;
1996                this.insert_cause(&mut causes, &key)?;
1997                this.shared.control.cancel();
1998                this.insert_cause(&mut causes, &key)?;
1999                Ok(((), 0))
2000            });
2001            require(
2002                matches!(
2003                    cancelled,
2004                    Err(super::CliError::IndexWork(
2005                        IndexWorkFailure::Cancelled { .. }
2006                    ))
2007                ),
2008                "resolver boundary proof failed",
2009            )?;
2010            require_eq(&(resolver.shared.retained_bytes), &(before))?;
2011            Ok(())
2012        })
2013    }
2014
2015    #[test]
2016    fn root_workspace_refuses_growth_and_preserves_duplicate_certainty()
2017    -> Result<(), Box<dyn Error>> {
2018        with_empty_resolver(|resolver, _| {
2019            let before = resolver.shared.retained_bytes;
2020            resolver.with_scratch(|this, scratch| {
2021                let mut roots = BTreeMap::new();
2022                this.insert_root(scratch, &mut roots, "src/lib.rs".to_string(), true)?;
2023                let retained = this.shared.retained_bytes;
2024                this.shared.retained_limit = this.shared.retained_staging_bytes();
2025                this.shared.check_budget()?;
2026                this.insert_root(scratch, &mut roots, "src/lib.rs".to_string(), false)?;
2027                require_eq(&this.shared.retained_bytes, &retained)?;
2028                require_eq(&roots, &BTreeMap::from([("src/lib.rs".to_string(), false)]))?;
2029                let refusal =
2030                    this.insert_root(scratch, &mut roots, "src/main.rs".to_string(), true);
2031                require(
2032                    matches!(
2033                        refusal,
2034                        Err(super::CliError::IndexWork(
2035                            IndexWorkFailure::ResourceLimitExceeded { .. }
2036                        ))
2037                    ),
2038                    "root growth was not refused",
2039                )?;
2040                require_eq(&this.shared.retained_bytes, &retained)?;
2041                require_eq(&roots.len(), &1)?;
2042                Ok(((), 0))
2043            })?;
2044            require_eq(&resolver.shared.retained_bytes, &before)?;
2045            Ok(())
2046        })
2047    }
2048
2049    #[test]
2050    fn alias_workspace_refusal_cleans_frames_and_preserves_source_cache()
2051    -> Result<(), Box<dyn Error>> {
2052        with_empty_resolver(|resolver, _| {
2053            let shared = &mut *resolver.shared;
2054            let references =
2055                projectatlas_symbols::parse_import_references("import {run} from './worker.js';");
2056            let reference = references
2057                .first()
2058                .ok_or_else(|| io::Error::other("native import reference missing"))?;
2059            let query = shared.query("main.js", reference, None)?;
2060            let graph = projectatlas_symbols::extract_symbol_graph(
2061                "main.js",
2062                Some("javascript"),
2063                "import {run} from './worker.js'; run();",
2064            );
2065            let keys = ["first.js", "second.js"]
2066                .into_iter()
2067                .map(|path| super::document_file_resolution_key(shared.project, path))
2068                .collect::<Result<Vec<_>, _>>()?;
2069            let source = Arc::new(super::BindingSource {
2070                source: super::SourceGraph::Persisted(graph),
2071                slots: BTreeMap::new(),
2072                modules: BTreeMap::from([(query.module.to_string(), keys.clone())]),
2073                parents: Vec::new(),
2074            });
2075            shared.reserve_scratch(super::STAGED_GRAPH_ROW_BYTES * 8)?;
2076            shared
2077                .context
2078                .insert(query.source.clone(), Arc::clone(&source));
2079            let before = shared.retained_bytes;
2080            shared.retained_limit = shared.retained_staging_bytes()
2081                + query.retained_bytes().saturating_mul(2)
2082                + super::STAGED_GRAPH_ROW_BYTES
2083                + keys[0].canonical_identity().len() as u64;
2084            shared.check_budget()?;
2085            let mut visited = BTreeMap::new();
2086            let result = shared.resolve(query.clone(), &mut visited, 0);
2087            require(
2088                matches!(
2089                    result,
2090                    Err(super::CliError::IndexWork(
2091                        IndexWorkFailure::ResourceLimitExceeded { .. }
2092                    ))
2093                ),
2094                "alias workspace growth was not refused",
2095            )?;
2096            require_eq(&shared.retained_bytes, &before)?;
2097            require(
2098                visited.is_empty() && shared.queries.is_empty(),
2099                "alias refusal retained a frame or failed cache entry",
2100            )?;
2101            require(
2102                Arc::ptr_eq(
2103                    shared
2104                        .context
2105                        .get(&query.source)
2106                        .ok_or_else(|| io::Error::other("source cache missing"))?,
2107                    &source,
2108                ),
2109                "alias refusal replaced captured context",
2110            )?;
2111            shared.retained_limit = 64 * 1_024;
2112            let outcome = shared.resolve(query.clone(), &mut visited, 0)?;
2113            let mut expected = keys.into_iter().collect::<BTreeSet<_>>();
2114            expected.insert(super::document_file_resolution_key(
2115                shared.project,
2116                &query.source,
2117            )?);
2118            require_eq(&outcome.dependencies, &expected)?;
2119            require(visited.is_empty(), "alias success retained a frame")?;
2120            let retained = shared.retained_bytes;
2121            let cached = shared.resolve(query, &mut visited, 0)?;
2122            require(
2123                Arc::ptr_eq(&outcome, &cached),
2124                "alias query did not reuse its completed outcome",
2125            )?;
2126            require_eq(&shared.retained_bytes, &retained)?;
2127            Ok(())
2128        })
2129    }
2130
2131    #[test]
2132    fn alias_workspace_cycles_retain_only_completed_owners_and_errors_release_frames()
2133    -> Result<(), Box<dyn Error>> {
2134        with_empty_resolver(|resolver, _| {
2135            let shared = &mut *resolver.shared;
2136            let (_, graph, facts) =
2137                projectatlas_symbols::extract_symbol_graph_with_semantics_controlled(
2138                    "self.js",
2139                    Some("javascript"),
2140                    "export {run} from './self.js';",
2141                    shared.control,
2142                )?
2143                .into_parts();
2144            let slot = facts
2145                .exports()
2146                .iter()
2147                .position(|export| export.name == "run")
2148                .ok_or_else(|| io::Error::other("native reexport slot missing"))?;
2149            let projectatlas_symbols::SourceExportBinding::ReExport(reference) =
2150                &facts.exports()[slot].binding
2151            else {
2152                return Err(io::Error::other("native reexport reference missing").into());
2153            };
2154            let query = shared.query("self.js", reference, None)?;
2155            let key = super::document_file_resolution_key(shared.project, "self.js")?;
2156            let entity = super::GraphEntity::new(
2157                shared.project,
2158                super::EntitySelector::File {
2159                    path: projectatlas_core::graph::RepositoryFilePath::new(std::path::Path::new(
2160                        "self.js",
2161                    ))?,
2162                },
2163                shared.generation,
2164            )?;
2165            shared.candidates.insert_candidate(&key, &entity)?;
2166            let source = Arc::new(super::BindingSource {
2167                source: super::SourceGraph::Captured(Arc::new(
2168                    super::super::super::CapturedSourceGraph {
2169                        graph,
2170                        semantics: facts,
2171                        admission: super::super::super::GraphIdentityAdmission::default(),
2172                    },
2173                )),
2174                slots: BTreeMap::from([("run".to_string(), vec![slot])]),
2175                modules: BTreeMap::from([(query.module.to_string(), vec![key.clone()])]),
2176                parents: Vec::new(),
2177            });
2178            shared.reserve_scratch(super::STAGED_GRAPH_ROW_BYTES * 8)?;
2179            shared.context.insert(query.source.clone(), source);
2180            let before = shared.retained_bytes;
2181            let frame_query = super::super::BindingQuery {
2182                source: "unindexed.js".to_string(),
2183                ..query.clone()
2184            };
2185            shared.retained_limit = shared.retained_staging_bytes() + frame_query.retained_bytes();
2186            shared.check_budget()?;
2187            let mut visited = BTreeMap::new();
2188            let refusal = shared.resolve(frame_query, &mut visited, 0);
2189            require(
2190                matches!(
2191                    refusal,
2192                    Err(super::CliError::IndexWork(
2193                        IndexWorkFailure::ResourceLimitExceeded { .. }
2194                    ))
2195                ),
2196                "second query copy was not admitted before cloning",
2197            )?;
2198            require_eq(&shared.retained_bytes, &before)?;
2199            require(
2200                visited.is_empty() && shared.queries.is_empty(),
2201                "frame refusal retained owned state",
2202            )?;
2203            shared.retained_limit = 64 * 1_024;
2204            let outcome = shared.resolve(query.clone(), &mut visited, 0)?;
2205            require(
2206                outcome.followed_export && outcome.target.is_none(),
2207                "self reexport did not abstain",
2208            )?;
2209            require_eq(&outcome.dependencies, &BTreeSet::from([key]))?;
2210            require_eq(
2211                &shared.retained_bytes,
2212                &(before + outcome.retained_bytes() + query.retained_bytes()),
2213            )?;
2214            require(visited.is_empty(), "cycle retained an ancestor frame")?;
2215            let retained = shared.retained_bytes;
2216            let cached = shared.resolve(query, &mut visited, 0)?;
2217            require(
2218                Arc::ptr_eq(&outcome, &cached),
2219                "cycle did not reuse completed root outcome",
2220            )?;
2221            require_eq(&shared.retained_bytes, &retained)?;
2222            let references =
2223                projectatlas_symbols::parse_import_references("import {run} from './absent.js';");
2224            let reference = references
2225                .first()
2226                .ok_or_else(|| io::Error::other("native missing-source import absent"))?;
2227            let missing = shared.query("missing.js", reference, None)?;
2228            // Module identity metadata is a completed resolver owner before frame admission.
2229            let retained = shared.retained_bytes;
2230            require(
2231                shared.resolve(missing, &mut visited, 0).is_err(),
2232                "missing source unexpectedly resolved",
2233            )?;
2234            require(visited.is_empty(), "alias error retained a frame")?;
2235            require_eq(&shared.retained_bytes, &retained)?;
2236            Ok(())
2237        })
2238    }
2239
2240    #[test]
2241    fn loaded_lookup_keys_reserve_before_copy_and_reuse_completed_admission()
2242    -> Result<(), Box<dyn Error>> {
2243        with_empty_resolver(|resolver, _| {
2244            let shared = &mut *resolver.shared;
2245            let key = super::document_file_resolution_key(shared.project, "worker.js")?;
2246            shared.loaded = Some(BTreeSet::new());
2247            let before = shared.retained_bytes;
2248            shared.retained_limit = shared.retained_staging_bytes() + 1;
2249            shared.check_budget()?;
2250            let refusal = shared.module_file(std::slice::from_ref(&key));
2251            require(
2252                matches!(
2253                    refusal,
2254                    Err(super::CliError::IndexWork(
2255                        IndexWorkFailure::ResourceLimitExceeded { .. }
2256                    ))
2257                ),
2258                "lookup copy was not refused before read",
2259            )?;
2260            require_eq(&shared.retained_bytes, &before)?;
2261            require(
2262                shared.loaded.as_ref().is_some_and(BTreeSet::is_empty),
2263                "refused lookup was marked loaded",
2264            )?;
2265            shared.retained_limit = 64 * 1_024;
2266            require_eq(&shared.module_file(std::slice::from_ref(&key))?, &None)?;
2267            require(
2268                shared
2269                    .loaded
2270                    .as_ref()
2271                    .is_some_and(|loaded| loaded.contains(&key)),
2272                "completed lookup was not retained",
2273            )?;
2274            let retained = shared.retained_bytes;
2275            shared.module_file(std::slice::from_ref(&key))?;
2276            require_eq(&shared.retained_bytes, &retained)?;
2277            Ok(())
2278        })
2279    }
2280
2281    /// Seed real parsed import leaves and completed child outcomes for aggregate-only proof.
2282    fn cached_candidates(
2283        resolver: &mut RustResolver<'_, '_, '_, '_>,
2284    ) -> Result<
2285        (
2286            super::SourceSemanticFacts,
2287            projectatlas_symbols::SourceRustCallImports,
2288        ),
2289        Box<dyn Error>,
2290    > {
2291        const SOURCE: &str = "src/main.rs";
2292        let extracted = projectatlas_symbols::extract_symbol_graph_with_semantics_controlled(
2293            SOURCE,
2294            Some("rust"),
2295            "use crate::one::run; use crate::two::run; fn caller(){run();}",
2296            resolver.shared.control,
2297        )?;
2298        let (_, _, facts) = extracted.into_parts();
2299        require_eq(&(facts.imports().len()), &(2))?;
2300        for (index, import) in facts.imports().iter().enumerate() {
2301            let binding = import
2302                .rust
2303                .as_ref()
2304                .ok_or_else(|| io::Error::other("native import binding missing"))?;
2305            let request = super::Request {
2306                source: SOURCE.to_string(),
2307                module: Arc::clone(&binding.module),
2308                path: binding.path.clone(),
2309                namespace: false,
2310                import: false,
2311                toolchain: false,
2312            };
2313            let key = super::document_file_resolution_key(
2314                resolver.shared.project,
2315                &format!("dependency_{index}.rs"),
2316            )?;
2317            let outcome = Arc::new(super::CallBindingResolution {
2318                absent: true,
2319                dependencies: BTreeSet::from([key]),
2320                ..super::CallBindingResolution::default()
2321            });
2322            resolver.charge(
2323                outcome
2324                    .retained_bytes()
2325                    .saturating_add(super::STAGED_GRAPH_ROW_BYTES * 4),
2326            )?;
2327            resolver.queries.insert(request, outcome);
2328        }
2329        let call = projectatlas_symbols::SourceRustCallImports {
2330            relation: 0,
2331            bindings: Arc::from([(0, 0), (1, 0)]),
2332            declarations: Arc::from([]),
2333            member: None,
2334        };
2335        Ok((facts, call))
2336    }
2337
2338    #[test]
2339    fn candidate_union_refusal_preserves_cached_children() -> Result<(), Box<dyn Error>> {
2340        with_empty_resolver(|resolver, _| {
2341            let (facts, call) = cached_candidates(resolver)?;
2342            let before = resolver.shared.retained_bytes;
2343            resolver.shared.retained_limit =
2344                resolver.shared.retained_staging_bytes() + super::STAGED_GRAPH_ROW_BYTES;
2345            resolver.shared.check_budget()?;
2346            let result = resolver.resolve_candidates("src/main.rs", &facts, &call);
2347            require(
2348                matches!(
2349                    result,
2350                    Err(super::CliError::IndexWork(
2351                        IndexWorkFailure::ResourceLimitExceeded { .. }
2352                    ))
2353                ),
2354                "resolver boundary proof failed",
2355            )?;
2356            require_eq(&(resolver.shared.retained_bytes), &(before))?;
2357            require_eq(&(resolver.queries.len()), &(call.bindings.len()))?;
2358            require(
2359                resolver.candidate_queries.is_empty(),
2360                "resolver boundary proof failed",
2361            )?;
2362            Ok(())
2363        })
2364    }
2365
2366    #[test]
2367    fn candidate_union_transfers_unique_causes_once_and_reuses_cache() -> Result<(), Box<dyn Error>>
2368    {
2369        with_empty_resolver(|resolver, _| {
2370            let (facts, call) = cached_candidates(resolver)?;
2371            let result = resolver.resolve_candidates("src/main.rs", &facts, &call)?;
2372            require(result.absent, "resolver boundary proof failed")?;
2373            require(result.target.is_none(), "resolver boundary proof failed")?;
2374            let expected = ["src/main.rs", "dependency_0.rs", "dependency_1.rs"]
2375                .into_iter()
2376                .map(|path| super::document_file_resolution_key(resolver.shared.project, path))
2377                .collect::<Result<BTreeSet<_>, _>>()?;
2378            require_eq(&(result.dependencies), &(expected))?;
2379            let retained = resolver.shared.retained_bytes;
2380            let repeated = resolver.resolve_candidates("src/main.rs", &facts, &call)?;
2381            require(
2382                Arc::ptr_eq(&result, &repeated),
2383                "resolver boundary proof failed",
2384            )?;
2385            require_eq(&(resolver.shared.retained_bytes), &(retained))?;
2386            Ok(())
2387        })
2388    }
2389}