Skip to main content

projectatlas_symbols/
source_semantics.rs

1//! Parser-owned binding evidence kept separate from the compatible public graph.
2
3use crate::ImportReference;
4use projectatlas_core::graph::SourceSpan;
5use projectatlas_core::symbols::{ParserKind, RelationKind, SymbolGraph};
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9/// Invalid ordinal correspondence between captured evidence and an admitted graph.
10#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
11pub enum SourceSemanticRemapError {
12    /// Evidence or mapping dimensions belong to a different source graph.
13    #[error("source semantic evidence does not match the graph dimensions")]
14    GraphMismatch,
15    /// A mapping repeats an output ordinal or addresses a missing output fact.
16    #[error("source semantic ordinal mapping is not one-to-one and in bounds")]
17    InvalidOrdinalMap,
18    /// A surviving occurrence points to a different relation family.
19    #[error("source semantic occurrence does not match its relation family")]
20    RelationMismatch,
21}
22
23/// Exact UTF-8 byte extent in the captured source file.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub struct SourceByteSpan {
26    /// Inclusive source byte offset.
27    pub start: usize,
28    /// Exclusive source byte offset.
29    pub end: usize,
30}
31
32/// Static binding selected at one call site by its grammar owner.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub enum SourceCallBinding {
35    /// Exactly one declaration in this parser graph is lexically visible.
36    LocalDeclaration(usize),
37    /// Several visible declarations remain possible, shared without per-call copies.
38    LocalDeclarations(Arc<[usize]>),
39    /// An AST-qualified Rust anchor or unshadowed toolchain module owns this path.
40    Qualified(ImportReference),
41    /// An AST-proven Rust module or explicit crate-relative path owns this call.
42    RustPath(crate::SourceRustCall),
43    /// A particular visible import binding owns this call.
44    Imported {
45        /// Ordinal of the import evidence, not the raw graph relation.
46        import: usize,
47        /// Ordinal of the reference within that import evidence.
48        reference: usize,
49        /// Explicit member after an imported namespace or type, if any.
50        member: Option<String>,
51        /// Source-proven toolchain identity; never inferred from the alias spelling.
52        rust_toolchain: Option<Box<ImportReference>>,
53    },
54    /// A parameter, local value or not-yet-initialized binding hides a declaration.
55    Shadowed,
56    /// Source does not establish a static declaration or visible import target.
57    Unresolved,
58    /// The existing PHP namespace owner supplies its separately validated local rules.
59    PhpNamespace,
60}
61
62/// One call occurrence and its parser-owned lexical decision.
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct SourceCallFact {
65    /// Exact original source lines and Unicode-scalar columns, when captured.
66    pub source_span: Option<SourceSpan>,
67    /// Ordinal in the accompanying graph's relation vector.
68    pub relation: usize,
69    /// Exact original source occurrence, independent of display context.
70    pub span: SourceByteSpan,
71    /// Exact enclosing declaration ordinal, absent for module or unknown owners.
72    pub owner: Option<usize>,
73    /// Binding proof used before name-based candidate lookup.
74    pub binding: SourceCallBinding,
75}
76
77/// One import declaration's independently visible references.
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct SourceImportFact {
80    /// Exact original source lines and Unicode-scalar columns, when captured.
81    pub source_span: Option<SourceSpan>,
82    /// Ordinal in the accompanying graph's relation vector.
83    pub relation: usize,
84    /// Original import extent, before canonical display normalization.
85    pub span: SourceByteSpan,
86    /// Independently static module target, even when no local binding is valid.
87    pub module: Option<String>,
88    /// Provider-owned canonical bindings; no lexical scope is inferred from their names.
89    pub references: Vec<ImportReference>,
90    /// Rust's complete use-tree leaf before module versus value classification.
91    pub rust: Option<Arc<crate::SourceRustUse>>,
92    /// Source-proven toolchain import, absent for repository or unproved roots.
93    pub rust_toolchain: Option<Box<ImportReference>>,
94}
95
96/// Static declaration or forwarding source of one source-owned module export slot.
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub enum SourceExportBinding {
99    /// Export syntax exists but does not prove a declaration or forwarding target.
100    Unresolved,
101    /// A declaration in the exporting source owns this slot.
102    LocalDeclaration(usize),
103    /// Indistinguishable declaration metadata cannot establish one export target.
104    AmbiguousDeclarations(Arc<[usize]>),
105    /// Declared export owners exist, but this binding does not prove a callable target.
106    ///
107    /// Retain their ordinals to prevent raw exported metadata from re-admitting
108    /// an unproved variable or destructuring name through fallback.
109    UnresolvedDeclarations(Arc<[usize]>),
110    /// An explicit static module reference forwards this exported name.
111    ReExport(ImportReference),
112    /// An explicit Rust pub-use retains its complete path and module owner.
113    RustReExport(Arc<crate::SourceRustUse>),
114}
115
116/// Language namespace occupied by one explicitly exported name.
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum SourceExportNamespace {
119    /// A runtime value or callable binding.
120    Value,
121    /// Type-only syntax cannot conflict with or supply runtime callable evidence.
122    Type,
123}
124
125/// One parser-owned exported name without a synthesized declaration.
126///
127/// Includes Python's public module declarations, whose lexical writes can
128/// withdraw export authority without changing the declaration's display metadata.
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct SourceExportFact {
131    /// Namespace governing competing slots and callable-target eligibility.
132    pub namespace: SourceExportNamespace,
133    /// Cooked exported slot, including the explicit default slot.
134    pub name: String,
135    /// Exact original source extent of the export or its owning declaration.
136    pub span: SourceByteSpan,
137    /// One-based start line for identity-admission diagnostics.
138    pub line: usize,
139    /// Static declaration or module evidence owning the export.
140    pub binding: SourceExportBinding,
141}
142
143/// Distinguish safe declaration correspondence from a coalesced candidate row.
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub(crate) enum SourceSymbolRemap {
146    /// One original declaration uniquely owns the output ordinal.
147    Retained(usize),
148    /// An output row exists, but cannot select among original declarations.
149    Ambiguous(usize),
150    /// No surviving output declaration can retain this source role.
151    Omitted,
152}
153
154impl SourceSymbolRemap {
155    /// Return only a declaration whose exact correspondence is established.
156    pub(crate) const fn retained(self) -> Option<usize> {
157        match self {
158            Self::Retained(index) => Some(index),
159            Self::Ambiguous(_) | Self::Omitted => None,
160        }
161    }
162
163    /// Retain a candidate for ambiguity markers without authorizing a target.
164    const fn candidate(self) -> Option<usize> {
165        match self {
166            Self::Retained(index) | Self::Ambiguous(index) => Some(index),
167            Self::Omitted => None,
168        }
169    }
170}
171
172/// Transient binding proof paired with one exact captured source graph.
173///
174/// This is not a serialized public graph or a persisted second authority. A
175/// consumer must retain the source identity and remap ordinals when filtering or
176/// merging the accompanying graph. Missing facts never authorize an exact call.
177#[derive(Clone, Debug, Default, Eq, PartialEq)]
178pub struct SourceSemanticFacts {
179    /// Owning repository-relative path.
180    pub(crate) path: String,
181    /// Hash of the original captured bytes, including any masked purpose header.
182    pub(crate) content_hash: String,
183    /// Symbol count at the last ordinal-association boundary.
184    pub(crate) symbol_count: usize,
185    /// Relation count at the last ordinal-association boundary.
186    pub(crate) relation_count: usize,
187    /// Digest of the ordered fact identities to detect same-size ordinal drift.
188    pub(crate) graph_digest: [u8; 32],
189    /// Source-order import evidence.
190    pub(crate) imports: Vec<SourceImportFact>,
191    /// Omitted declaration/import evidence may compete with retained lexical bindings.
192    pub(crate) bindings_incomplete: bool,
193    /// Call occurrences in raw relation order.
194    pub(crate) calls: Vec<SourceCallFact>,
195    /// Explicit module export slots in captured source order.
196    pub(crate) exports: Vec<SourceExportFact>,
197    /// Export cardinality exceeded its bound; no export target may be narrowed.
198    pub(crate) exports_incomplete: bool,
199    /// Actual Rust module declarations and module-owned value slots.
200    pub(crate) rust: Option<crate::SourceRustFacts>,
201    /// Target settings retained from the existing Cargo TOML parse.
202    pub(crate) cargo: Option<crate::SourceCargoTargets>,
203}
204
205impl SourceSemanticFacts {
206    /// Borrow the exact source path that owns these ordinals.
207    #[must_use]
208    pub fn path(&self) -> &str {
209        &self.path
210    }
211
212    /// Borrow the hash that must match the captured inventory before reuse.
213    #[must_use]
214    pub fn content_hash(&self) -> &str {
215        &self.content_hash
216    }
217
218    /// Borrow source-order import bindings.
219    #[must_use]
220    pub fn imports(&self) -> &[SourceImportFact] {
221        &self.imports
222    }
223
224    /// Borrow call-site decisions and exact source extents.
225    #[must_use]
226    pub fn calls(&self) -> &[SourceCallFact] {
227        &self.calls
228    }
229
230    /// Borrow explicit module exports and their source-owned targets.
231    #[must_use]
232    pub fn exports(&self) -> &[SourceExportFact] {
233        &self.exports
234    }
235
236    /// Borrow Rust module ownership captured by the native source parser.
237    #[must_use]
238    pub const fn rust(&self) -> Option<&crate::SourceRustFacts> {
239        self.rust.as_ref()
240    }
241
242    /// Borrow Cargo target settings from this exact manifest's existing parse.
243    #[must_use]
244    pub const fn cargo(&self) -> Option<&crate::SourceCargoTargets> {
245        self.cargo.as_ref()
246    }
247
248    /// Return whether all competing source export slots fit within the evidence bound.
249    #[must_use]
250    pub const fn exports_complete(&self) -> bool {
251        !self.exports_incomplete
252    }
253
254    /// Estimate retained bytes from actual vector/string capacities and shared sets.
255    ///
256    /// Immutable overload arrays are charged once per shared allocation, including
257    /// their reference-count header. This is staging accounting, not measured RSS.
258    #[must_use]
259    pub fn retained_bytes(&self) -> u64 {
260        let mut rust_uses = HashSet::new();
261        let mut rust_modules = HashSet::new();
262        let mut import_modules = HashSet::new();
263        let mut bytes = (std::mem::size_of::<Self>() as u64)
264            .saturating_add(self.path.capacity() as u64)
265            .saturating_add(self.content_hash.capacity() as u64)
266            .saturating_add(
267                (self.imports.capacity() as u64)
268                    .saturating_mul(std::mem::size_of::<SourceImportFact>() as u64),
269            )
270            .saturating_add(
271                (self.calls.capacity() as u64)
272                    .saturating_mul(std::mem::size_of::<SourceCallFact>() as u64),
273            );
274        for import in &self.imports {
275            if let Some(reference) = &import.rust_toolchain {
276                bytes = bytes
277                    .saturating_add(std::mem::size_of::<ImportReference>() as u64)
278                    .saturating_add(reference.retained_heap_bytes(&mut import_modules));
279            }
280            if let Some(binding) = &import.rust {
281                bytes = bytes.saturating_add(crate::rust_binding_source::use_retained_bytes(
282                    binding,
283                    &mut rust_uses,
284                    &mut rust_modules,
285                ));
286            }
287            bytes = bytes
288                .saturating_add(import.module.as_ref().map_or(0, String::capacity) as u64)
289                .saturating_add(
290                    (import.references.capacity() as u64)
291                        .saturating_mul(std::mem::size_of::<ImportReference>() as u64),
292                );
293            for reference in &import.references {
294                bytes = bytes.saturating_add(reference.retained_heap_bytes(&mut import_modules));
295            }
296        }
297        let mut shared = HashSet::new();
298        bytes = bytes
299            .saturating_add(call_binding_heap_bytes_with_shared(
300                &self.calls,
301                &mut shared,
302                &mut rust_modules,
303                &mut import_modules,
304            ))
305            .saturating_add(
306                (self.exports.capacity() as u64)
307                    .saturating_mul(std::mem::size_of::<SourceExportFact>() as u64),
308            );
309        for export in &self.exports {
310            bytes = bytes
311                .saturating_add(export.name.capacity() as u64)
312                .saturating_add(match &export.binding {
313                    SourceExportBinding::ReExport(reference) => {
314                        reference.retained_heap_bytes(&mut import_modules)
315                    }
316                    SourceExportBinding::RustReExport(binding) => {
317                        crate::rust_binding_source::use_retained_bytes(
318                            binding,
319                            &mut rust_uses,
320                            &mut rust_modules,
321                        )
322                    }
323                    SourceExportBinding::AmbiguousDeclarations(symbols)
324                    | SourceExportBinding::UnresolvedDeclarations(symbols)
325                        if shared.insert(Arc::as_ptr(symbols)) =>
326                    {
327                        ((symbols.len() as u64).saturating_add(2))
328                            .saturating_mul(std::mem::size_of::<usize>() as u64)
329                    }
330                    SourceExportBinding::LocalDeclaration(_)
331                    | SourceExportBinding::AmbiguousDeclarations(_)
332                    | SourceExportBinding::UnresolvedDeclarations(_)
333                    | SourceExportBinding::Unresolved => 0,
334                });
335        }
336        if let Some(rust) = &self.rust {
337            bytes = bytes.saturating_add(rust.retained_bytes(&mut rust_uses, &mut rust_modules));
338        }
339        if let Some(cargo) = &self.cargo {
340            bytes = bytes.saturating_add(cargo.retained_bytes());
341        }
342        bytes
343    }
344
345    /// Check the owning path and vector shape before an ordinal-based operation.
346    #[must_use]
347    pub fn matches_graph(&self, graph: &SymbolGraph) -> bool {
348        let mut checked_declarations = HashSet::new();
349        let mut checked_rust_imports = HashSet::new();
350        self.path == graph.path
351            && self.symbol_count == graph.symbols.len()
352            && self.relation_count == graph.relations.len()
353            && self.graph_digest == graph_ordinal_digest(graph)
354            && self.imports.iter().all(|fact| {
355                graph
356                    .relations
357                    .get(fact.relation)
358                    .is_some_and(|relation| relation.kind == RelationKind::Imports)
359            })
360            && self.calls.iter().all(|fact| {
361                graph
362                    .relations
363                    .get(fact.relation)
364                    .is_some_and(|relation| relation.kind == RelationKind::Calls)
365                    && self.binding_is_in_bounds(&fact.binding, &mut checked_declarations)
366                    && fact.owner.is_none_or(|owner| owner < self.symbol_count)
367            })
368            && self.exports.iter().all(|fact| match &fact.binding {
369                SourceExportBinding::LocalDeclaration(symbol) => *symbol < self.symbol_count,
370                SourceExportBinding::AmbiguousDeclarations(symbols)
371                | SourceExportBinding::UnresolvedDeclarations(symbols) => {
372                    !symbols.is_empty()
373                        && (!checked_declarations.insert(Arc::as_ptr(symbols))
374                            || symbols.iter().all(|symbol| *symbol < self.symbol_count))
375                }
376                SourceExportBinding::ReExport(_)
377                | SourceExportBinding::RustReExport(_)
378                | SourceExportBinding::Unresolved => true,
379            })
380            && self.rust.as_ref().is_none_or(|rust| {
381                rust.declarations
382                    .iter()
383                    .all(|declaration| match declaration.binding {
384                        crate::SourceRustBinding::Declaration(symbol)
385                        | crate::SourceRustBinding::TypeDeclaration(symbol) => {
386                            symbol < self.symbol_count
387                        }
388                        crate::SourceRustBinding::Use(_)
389                        | crate::SourceRustBinding::Unresolved
390                        | crate::SourceRustBinding::UnresolvedType
391                        | crate::SourceRustBinding::UnresolvedImport => true,
392                    })
393                    && rust.candidate_calls.iter().all(|call| {
394                        graph
395                            .relations
396                            .get(call.relation)
397                            .is_some_and(|relation| relation.kind == RelationKind::Calls)
398                            && !call.bindings.is_empty()
399                            && (!checked_declarations.insert(Arc::as_ptr(&call.declarations))
400                                || call
401                                    .declarations
402                                    .iter()
403                                    .all(|symbol| *symbol < self.symbol_count))
404                            && (!checked_rust_imports.insert(Arc::as_ptr(&call.bindings))
405                                || call.bindings.iter().all(|(import, reference)| {
406                                    self.imports
407                                        .get(*import)
408                                        .is_some_and(|import| *reference < import.references.len())
409                                }))
410                    })
411                    && rust.associated.iter().all(|method| {
412                        method
413                            .declaration
414                            .is_none_or(|ordinal| ordinal < self.symbol_count)
415                    })
416            })
417    }
418
419    /// Move source evidence through explicit filtering or reordering maps.
420    ///
421    /// Each map addresses an original ordinal and optionally supplies its output
422    /// ordinal. Dropping any required binding proof makes a surviving call
423    /// unresolved; filtering cannot turn an ambiguous binding into an exact one.
424    /// New graph facts without captured evidence gain no binding proof.
425    ///
426    /// # Errors
427    ///
428    /// Returns an error for another source path, mismatched mapping dimensions,
429    /// duplicate/out-of-bounds outputs or a changed relation family. The original
430    /// evidence remains unchanged on failure.
431    pub fn remap(
432        &self,
433        original: &SymbolGraph,
434        graph: &SymbolGraph,
435        symbols: &[Option<usize>],
436        relations: &[Option<usize>],
437    ) -> Result<Self, SourceSemanticRemapError> {
438        if !self.matches_graph(original)
439            || self.path != graph.path
440            || self.symbol_count != symbols.len()
441            || self.relation_count != relations.len()
442        {
443            return Err(SourceSemanticRemapError::GraphMismatch);
444        }
445        validate_ordinal_map(symbols, graph.symbols.len())?;
446        validate_ordinal_map(relations, graph.relations.len())?;
447        let (imports, calls) = self.remap_associations(symbols, relations);
448        let exports = self.remap_exports(symbols, None);
449        let result = Self {
450            path: self.path.clone(),
451            content_hash: self.content_hash.clone(),
452            symbol_count: graph.symbols.len(),
453            relation_count: graph.relations.len(),
454            graph_digest: graph_ordinal_digest(graph),
455            imports,
456            bindings_incomplete: self.bindings_incomplete,
457            calls,
458            exports,
459            exports_incomplete: self.exports_incomplete,
460            rust: self.remap_rust(symbols, relations),
461            cargo: self.cargo.clone(),
462        };
463        if !result.matches_graph(graph) {
464            return Err(SourceSemanticRemapError::RelationMismatch);
465        }
466        Ok(result)
467    }
468
469    /// Retain graph facts and their source proof together without cloning the graph.
470    ///
471    /// True mask entries retain the corresponding original ordinal. The source
472    /// hash is preserved; dropped declaration/import proof cannot authorize a call.
473    ///
474    /// # Errors
475    ///
476    /// Returns a source-identity or mask-dimension mismatch before either graph or
477    /// evidence changes. Once validated, filtering and remapping are infallible.
478    pub fn retain_graph_facts(
479        &mut self,
480        graph: &mut SymbolGraph,
481        symbols: &[bool],
482        relations: &[bool],
483    ) -> Result<(), SourceSemanticRemapError> {
484        if !self.matches_graph(graph)
485            || symbols.len() != graph.symbols.len()
486            || relations.len() != graph.relations.len()
487        {
488            return Err(SourceSemanticRemapError::GraphMismatch);
489        }
490        let symbol_ordinals = retained_ordinals(symbols);
491        let relation_ordinals = retained_ordinals(relations);
492        let (imports, calls) = self.remap_associations(&symbol_ordinals, &relation_ordinals);
493        let exports = self.remap_exports(&symbol_ordinals, None);
494        let rust = self.remap_rust(&symbol_ordinals, &relation_ordinals);
495        let mut symbol = 0;
496        graph.symbols.retain(|_| {
497            let retained = symbols[symbol];
498            symbol += 1;
499            retained
500        });
501        let mut relation = 0;
502        graph.relations.retain(|_| {
503            let retained = relations[relation];
504            relation += 1;
505            retained
506        });
507        self.symbol_count = graph.symbols.len();
508        self.relation_count = graph.relations.len();
509        self.graph_digest = graph_ordinal_digest(graph);
510        self.imports = imports;
511        self.calls = calls;
512        self.exports = exports;
513        self.rust = rust;
514        Ok(())
515    }
516
517    /// Append parser-owned evidence through an append-only host graph merge.
518    ///
519    /// The parser owner seals `source` before moving its graph rows and supplies
520    /// one-to-one surviving ordinals. Existing host ordinals remain unchanged.
521    ///
522    /// # Errors
523    ///
524    /// Returns correspondence errors before adding any new source facts.
525    pub(crate) fn append_mapped(
526        &mut self,
527        source: Self,
528        graph: &SymbolGraph,
529        symbols: &[SourceSymbolRemap],
530        relations: &[Option<usize>],
531    ) -> Result<(), SourceSemanticRemapError> {
532        if source.rust.is_some()
533            || source.cargo.is_some()
534            || source.path != graph.path
535            || (!self.path.is_empty() && self.path != graph.path)
536            || source.symbol_count != symbols.len()
537            || source.relation_count != relations.len()
538            || self.symbol_count > graph.symbols.len()
539            || self.relation_count > graph.relations.len()
540        {
541            return Err(SourceSemanticRemapError::GraphMismatch);
542        }
543        if symbols.iter().any(|symbol| {
544            symbol
545                .candidate()
546                .is_some_and(|index| index >= graph.symbols.len())
547        }) {
548            return Err(SourceSemanticRemapError::InvalidOrdinalMap);
549        }
550        let retained_symbols = symbols
551            .iter()
552            .map(|symbol| symbol.retained())
553            .collect::<Vec<_>>();
554        validate_ordinal_map(&retained_symbols, graph.symbols.len())?;
555        validate_ordinal_map(relations, graph.relations.len())?;
556        let (imports, calls) = source.remap_associations(&retained_symbols, relations);
557        let exports = source.remap_exports(&retained_symbols, Some(symbols));
558        let mut mapped = Self {
559            path: source.path,
560            content_hash: String::new(),
561            symbol_count: graph.symbols.len(),
562            relation_count: graph.relations.len(),
563            graph_digest: graph_ordinal_digest(graph),
564            imports,
565            bindings_incomplete: source.bindings_incomplete,
566            calls,
567            exports,
568            exports_incomplete: source.exports_incomplete,
569            rust: None,
570            cargo: None,
571        };
572        if !mapped.matches_graph(graph) {
573            return Err(SourceSemanticRemapError::RelationMismatch);
574        }
575        for call in &mut mapped.calls {
576            if let SourceCallBinding::Imported { import, .. } = &mut call.binding {
577                *import = import
578                    .checked_add(self.imports.len())
579                    .ok_or(SourceSemanticRemapError::InvalidOrdinalMap)?;
580            }
581        }
582        self.bindings_incomplete |= mapped.bindings_incomplete;
583        self.imports.extend(mapped.imports);
584        self.calls.extend(mapped.calls);
585        self.exports.extend(mapped.exports);
586        self.exports_incomplete |= mapped.exports_incomplete;
587        self.path = mapped.path;
588        self.symbol_count = mapped.symbol_count;
589        self.relation_count = mapped.relation_count;
590        self.graph_digest = mapped.graph_digest;
591        Ok(())
592    }
593
594    /// Remap validated ordinals once, preserving shared overload-set allocation.
595    fn remap_associations(
596        &self,
597        symbols: &[Option<usize>],
598        relations: &[Option<usize>],
599    ) -> (Vec<SourceImportFact>, Vec<SourceCallFact>) {
600        let mut imports = Vec::with_capacity(self.imports.len());
601        let mut import_ordinals = vec![None; self.imports.len()];
602        for (ordinal, fact) in self.imports.iter().enumerate() {
603            let Some(relation) = relations.get(fact.relation).copied().flatten() else {
604                continue;
605            };
606            import_ordinals[ordinal] = Some(imports.len());
607            imports.push(SourceImportFact {
608                relation,
609                span: fact.span,
610                source_span: fact.source_span,
611                module: fact.module.clone(),
612                references: fact.references.clone(),
613                rust: fact.rust.clone(),
614                rust_toolchain: fact.rust_toolchain.clone(),
615            });
616        }
617        let mut calls = Vec::with_capacity(self.calls.len());
618        let mut declaration_remaps = HashMap::new();
619        for fact in &self.calls {
620            let Some(relation) = relations.get(fact.relation).copied().flatten() else {
621                continue;
622            };
623            calls.push(SourceCallFact {
624                relation,
625                span: fact.span,
626                source_span: fact.source_span,
627                owner: fact
628                    .owner
629                    .and_then(|owner| symbols.get(owner).copied().flatten()),
630                binding: remap_call_binding(
631                    &fact.binding,
632                    symbols,
633                    &import_ordinals,
634                    &mut declaration_remaps,
635                ),
636            });
637        }
638        (imports, calls)
639    }
640
641    /// Preserve discarded Rust declaration slots as unresolved competitors.
642    fn remap_rust(
643        &self,
644        symbols: &[Option<usize>],
645        relations: &[Option<usize>],
646    ) -> Option<crate::SourceRustFacts> {
647        self.rust.as_ref().map(|facts| {
648            let surviving_uses = self
649                .imports
650                .iter()
651                .filter(|import| relations.get(import.relation).copied().flatten().is_some())
652                .filter_map(|import| import.rust.as_ref().map(Arc::as_ptr))
653                .collect::<HashSet<_>>();
654            let mut mapped = facts.clone();
655            for declaration in &mut mapped.declarations {
656                declaration.binding = match &declaration.binding {
657                    crate::SourceRustBinding::Declaration(symbol) => {
658                        symbols.get(*symbol).copied().flatten().map_or(
659                            crate::SourceRustBinding::Unresolved,
660                            crate::SourceRustBinding::Declaration,
661                        )
662                    }
663                    crate::SourceRustBinding::TypeDeclaration(symbol) => {
664                        symbols.get(*symbol).copied().flatten().map_or(
665                            crate::SourceRustBinding::UnresolvedType,
666                            crate::SourceRustBinding::TypeDeclaration,
667                        )
668                    }
669                    crate::SourceRustBinding::Use(binding)
670                        if !surviving_uses.contains(&Arc::as_ptr(binding)) =>
671                    {
672                        crate::SourceRustBinding::UnresolvedImport
673                    }
674                    binding => binding.clone(),
675                };
676            }
677            for method in &mut mapped.associated {
678                method.declaration = method
679                    .declaration
680                    .and_then(|ordinal| symbols.get(ordinal).copied().flatten());
681            }
682            let mut next_import = 0;
683            let import_ordinals = self
684                .imports
685                .iter()
686                .map(|import| {
687                    relations.get(import.relation).copied().flatten().map(|_| {
688                        let ordinal = next_import;
689                        next_import += 1;
690                        ordinal
691                    })
692                })
693                .collect::<Vec<_>>();
694            let mut candidates =
695                HashMap::<*const [(usize, usize)], Option<Arc<[(usize, usize)]>>>::new();
696            let mut declaration_candidates = HashMap::<*const [usize], Option<Arc<[usize]>>>::new();
697            mapped.candidate_calls = facts
698                .candidate_calls
699                .iter()
700                .filter_map(|call| {
701                    let relation = relations.get(call.relation).copied().flatten()?;
702                    let bindings = candidates
703                        .entry(Arc::as_ptr(&call.bindings))
704                        .or_insert_with(|| {
705                            call.bindings
706                                .iter()
707                                .map(|(import, reference)| {
708                                    import_ordinals
709                                        .get(*import)
710                                        .copied()
711                                        .flatten()
712                                        .map(|import| (import, *reference))
713                                })
714                                .collect::<Option<Vec<_>>>()
715                                .map(Arc::from)
716                        })
717                        .as_ref()?;
718                    let declarations = declaration_candidates
719                        .entry(Arc::as_ptr(&call.declarations))
720                        .or_insert_with(|| {
721                            call.declarations
722                                .iter()
723                                .map(|symbol| symbols.get(*symbol).copied().flatten())
724                                .collect::<Option<Vec<_>>>()
725                                .map(Arc::from)
726                        })
727                        .as_ref()?;
728                    Some(crate::SourceRustCallImports {
729                        relation,
730                        declarations: Arc::clone(declarations),
731                        bindings: Arc::clone(bindings),
732                        member: call.member.clone(),
733                    })
734                })
735                .collect();
736            mapped
737        })
738    }
739
740    /// Preserve ambiguous export provenance even when only one candidate survives filtering.
741    fn remap_exports(
742        &self,
743        symbols: &[Option<usize>],
744        candidates: Option<&[SourceSymbolRemap]>,
745    ) -> Vec<SourceExportFact> {
746        let mut ambiguity = HashMap::new();
747        self.exports
748            .iter()
749            .map(|fact| {
750                let binding = match &fact.binding {
751                    SourceExportBinding::LocalDeclaration(symbol) => {
752                        if let Some(symbol) = symbols.get(*symbol).copied().flatten() {
753                            SourceExportBinding::LocalDeclaration(symbol)
754                        } else if let Some(candidate) = candidates
755                            .and_then(|candidates| candidates.get(*symbol))
756                            .and_then(|candidate| candidate.candidate())
757                        {
758                            SourceExportBinding::AmbiguousDeclarations(vec![candidate].into())
759                        } else {
760                            SourceExportBinding::Unresolved
761                        }
762                    }
763                    SourceExportBinding::AmbiguousDeclarations(original)
764                    | SourceExportBinding::UnresolvedDeclarations(original) => {
765                        let mapped: &Arc<[usize]> =
766                            ambiguity.entry(original.as_ptr()).or_insert_with(|| {
767                                let mut mapped = original
768                                    .iter()
769                                    .filter_map(|symbol| {
770                                        symbols
771                                            .get(*symbol)
772                                            .copied()
773                                            .flatten()
774                                            .or_else(|| candidates?.get(*symbol)?.candidate())
775                                    })
776                                    .collect::<Vec<_>>();
777                                mapped.sort_unstable();
778                                mapped.dedup();
779                                mapped.into()
780                            });
781                        if mapped.is_empty() {
782                            SourceExportBinding::Unresolved
783                        } else if matches!(
784                            fact.binding,
785                            SourceExportBinding::UnresolvedDeclarations(_)
786                        ) {
787                            SourceExportBinding::UnresolvedDeclarations(Arc::clone(mapped))
788                        } else {
789                            SourceExportBinding::AmbiguousDeclarations(Arc::clone(mapped))
790                        }
791                    }
792                    SourceExportBinding::ReExport(reference) => {
793                        SourceExportBinding::ReExport(reference.clone())
794                    }
795                    SourceExportBinding::RustReExport(binding) => {
796                        SourceExportBinding::RustReExport(Arc::clone(binding))
797                    }
798                    SourceExportBinding::Unresolved => SourceExportBinding::Unresolved,
799                };
800                SourceExportFact {
801                    namespace: fact.namespace,
802                    name: fact.name.clone(),
803                    span: fact.span,
804                    line: fact.line,
805                    binding,
806                }
807            })
808            .collect()
809    }
810
811    /// Require all binding ordinals to belong to their captured vectors.
812    fn binding_is_in_bounds(
813        &self,
814        binding: &SourceCallBinding,
815        checked_declarations: &mut HashSet<*const [usize]>,
816    ) -> bool {
817        match binding {
818            SourceCallBinding::LocalDeclaration(symbol) => *symbol < self.symbol_count,
819            SourceCallBinding::LocalDeclarations(symbols) => {
820                !symbols.is_empty()
821                    && (!checked_declarations.insert(Arc::as_ptr(symbols))
822                        || symbols.iter().all(|symbol| *symbol < self.symbol_count))
823            }
824            SourceCallBinding::Imported {
825                import, reference, ..
826            } => self
827                .imports
828                .get(*import)
829                .is_some_and(|fact| *reference < fact.references.len()),
830            SourceCallBinding::Qualified(_)
831            | SourceCallBinding::RustPath(_)
832            | SourceCallBinding::Shadowed
833            | SourceCallBinding::Unresolved
834            | SourceCallBinding::PhpNamespace => true,
835        }
836    }
837
838    /// Derive exact occurrence coordinates once, after final lexical and host mapping.
839    ///
840    /// # Errors
841    ///
842    /// Returns the caller's cancellation or deadline failure without exposing a partial extraction.
843    pub(crate) fn bind_source_spans<E>(
844        &mut self,
845        content: &str,
846        check: &mut impl FnMut() -> Result<(), E>,
847    ) -> Result<(), E> {
848        let mut offsets = Vec::with_capacity(
849            self.calls
850                .len()
851                .saturating_add(self.imports.len())
852                .saturating_mul(2),
853        );
854        for span in self
855            .calls
856            .iter()
857            .map(|fact| fact.span)
858            .chain(self.imports.iter().map(|fact| fact.span))
859        {
860            check()?;
861            offsets.push(span.start);
862            offsets.push(span.end);
863        }
864        let positions = crate::source_positions::source_positions(content, offsets, check)?;
865        for (span, target) in self
866            .calls
867            .iter_mut()
868            .map(|fact| (fact.span, &mut fact.source_span))
869            .chain(
870                self.imports
871                    .iter_mut()
872                    .map(|fact| (fact.span, &mut fact.source_span)),
873            )
874        {
875            check()?;
876            *target = occurrence_source_span(span, &positions);
877        }
878        Ok(())
879    }
880
881    /// Seal source identity after the final parser-side merge/remap.
882    pub(crate) fn bind_source(&mut self, graph: &SymbolGraph, content: &str) {
883        self.path.clone_from(&graph.path);
884        self.content_hash = blake3::hash(content.as_bytes()).to_hex().to_string();
885        self.symbol_count = graph.symbols.len();
886        self.relation_count = graph.relations.len();
887        self.graph_digest = graph_ordinal_digest(graph);
888    }
889}
890
891/// Preserve absent or invalid coordinate evidence rather than inventing a location.
892fn occurrence_source_span(
893    span: SourceByteSpan,
894    positions: &[(usize, usize, usize)],
895) -> Option<SourceSpan> {
896    let start = positions.get(
897        positions
898            .binary_search_by_key(&span.start, |point| point.0)
899            .ok()?,
900    )?;
901    let end = positions.get(
902        positions
903            .binary_search_by_key(&span.end, |point| point.0)
904            .ok()?,
905    )?;
906    SourceSpan::new(
907        u32::try_from(start.1).ok()?,
908        u32::try_from(start.2).ok()?,
909        u32::try_from(end.1).ok()?,
910        u32::try_from(end.2).ok()?,
911    )
912    .ok()
913}
914
915/// Bind ordered declaration/relation identities without serializing a second graph.
916fn graph_ordinal_digest(graph: &SymbolGraph) -> [u8; 32] {
917    let mut hash = blake3::Hasher::new();
918    hash.update(b"source-binding-ordinals");
919    hash_text(&mut hash, &graph.path);
920    hash_optional_text(&mut hash, graph.language.as_deref());
921    hash.update(&[graph.parser as u8]);
922    hash.update(&(graph.symbols.len() as u64).to_le_bytes());
923    for symbol in &graph.symbols {
924        hash_text(&mut hash, &symbol.path);
925        hash_text(&mut hash, &symbol.name);
926        hash_text(&mut hash, &symbol.signature);
927        hash_optional_text(&mut hash, symbol.parent.as_deref());
928        hash.update(&[
929            symbol.kind as u8,
930            symbol.parser as u8,
931            u8::from(symbol.exported),
932        ]);
933        hash.update(&(symbol.line_start as u64).to_le_bytes());
934        hash.update(&(symbol.line_end as u64).to_le_bytes());
935        if let Some(selector) = symbol.source_selector {
936            hash.update(&[1]);
937            for ordinal in [
938                selector.byte_start,
939                selector.byte_end,
940                selector.column_start,
941                selector.column_end,
942            ] {
943                hash.update(&(ordinal as u64).to_le_bytes());
944            }
945        } else {
946            hash.update(&[0]);
947        }
948    }
949    hash.update(&(graph.relations.len() as u64).to_le_bytes());
950    for relation in &graph.relations {
951        hash_text(&mut hash, &relation.path);
952        hash_text(&mut hash, &relation.source_name);
953        hash_text(&mut hash, &relation.target_name);
954        hash_text(&mut hash, &relation.context);
955        hash.update(&[relation.kind as u8, relation.parser as u8]);
956        hash.update(&(relation.line as u64).to_le_bytes());
957    }
958    *hash.finalize().as_bytes()
959}
960
961/// Keep adjacent UTF-8 identity components unambiguous in the digest.
962fn hash_text(hash: &mut blake3::Hasher, text: &str) {
963    hash.update(&(text.len() as u64).to_le_bytes());
964    hash.update(text.as_bytes());
965}
966
967/// Distinguish absent identity text from an explicitly empty component.
968fn hash_optional_text(hash: &mut blake3::Hasher, text: Option<&str>) {
969    if let Some(text) = text {
970        hash.update(&[1]);
971        hash_text(hash, text);
972    } else {
973        hash.update(&[0]);
974    }
975}
976
977/// Derive dense retained ordinals from one source-order mask.
978fn retained_ordinals(retained: &[bool]) -> Vec<Option<usize>> {
979    let mut next = 0;
980    retained
981        .iter()
982        .map(|retained| {
983            if *retained {
984                let ordinal = next;
985                next += 1;
986                Some(ordinal)
987            } else {
988                None
989            }
990        })
991        .collect()
992}
993
994/// Validate a bounded injective map without sorting or materializing graph facts.
995fn validate_ordinal_map(
996    mapping: &[Option<usize>],
997    output_count: usize,
998) -> Result<(), SourceSemanticRemapError> {
999    let mut seen = vec![false; output_count];
1000    for ordinal in mapping.iter().flatten() {
1001        let Some(slot) = seen.get_mut(*ordinal) else {
1002            return Err(SourceSemanticRemapError::InvalidOrdinalMap);
1003        };
1004        if *slot {
1005            return Err(SourceSemanticRemapError::InvalidOrdinalMap);
1006        }
1007        *slot = true;
1008    }
1009    Ok(())
1010}
1011
1012/// Count owned call payloads, charging each immutable overload allocation once.
1013pub(crate) fn call_binding_heap_bytes(calls: &[SourceCallFact]) -> u64 {
1014    let mut shared = HashSet::new();
1015    call_binding_heap_bytes_with_shared(
1016        calls,
1017        &mut shared,
1018        &mut HashSet::new(),
1019        &mut HashSet::new(),
1020    )
1021}
1022
1023/// Share allocation accounting across call policies and export ambiguity markers.
1024fn call_binding_heap_bytes_with_shared(
1025    calls: &[SourceCallFact],
1026    shared: &mut HashSet<*const [usize]>,
1027    rust_modules: &mut HashSet<*const [String]>,
1028    import_modules: &mut HashSet<*const str>,
1029) -> u64 {
1030    calls.iter().fold(0_u64, |bytes, call| {
1031        bytes.saturating_add(match &call.binding {
1032            SourceCallBinding::Qualified(reference) => {
1033                reference.retained_heap_bytes(import_modules)
1034            }
1035            SourceCallBinding::RustPath(call) => call.path.retained_bytes().saturating_add(
1036                crate::rust_binding_source::module_retained_bytes(&call.module, rust_modules),
1037            ),
1038            SourceCallBinding::Imported {
1039                member,
1040                rust_toolchain,
1041                ..
1042            } => (member.as_ref().map_or(0, String::capacity) as u64).saturating_add(
1043                rust_toolchain.as_ref().map_or(0, |reference| {
1044                    (std::mem::size_of::<ImportReference>() as u64)
1045                        .saturating_add(reference.retained_heap_bytes(import_modules))
1046                }),
1047            ),
1048            SourceCallBinding::LocalDeclarations(declarations)
1049                if shared.insert(Arc::as_ptr(declarations)) =>
1050            {
1051                ((declarations.len() as u64).saturating_add(2))
1052                    .saturating_mul(std::mem::size_of::<usize>() as u64)
1053            }
1054            SourceCallBinding::LocalDeclaration(_)
1055            | SourceCallBinding::LocalDeclarations(_)
1056            | SourceCallBinding::Shadowed
1057            | SourceCallBinding::Unresolved
1058            | SourceCallBinding::PhpNamespace => 0,
1059        })
1060    })
1061}
1062
1063/// Preserve a binding only when every required original owner survives.
1064fn remap_call_binding(
1065    binding: &SourceCallBinding,
1066    symbols: &[Option<usize>],
1067    imports: &[Option<usize>],
1068    declaration_remaps: &mut HashMap<*const usize, SourceCallBinding>,
1069) -> SourceCallBinding {
1070    match binding {
1071        SourceCallBinding::LocalDeclaration(symbol) => {
1072            symbols.get(*symbol).copied().flatten().map_or(
1073                SourceCallBinding::Unresolved,
1074                SourceCallBinding::LocalDeclaration,
1075            )
1076        }
1077        SourceCallBinding::LocalDeclarations(original) => declaration_remaps
1078            .entry(original.as_ptr())
1079            .or_insert_with(|| {
1080                original
1081                    .iter()
1082                    .map(|symbol| symbols.get(*symbol).copied().flatten())
1083                    .collect::<Option<Vec<_>>>()
1084                    .filter(|mapped| !mapped.is_empty())
1085                    .map_or(SourceCallBinding::Unresolved, |mapped| {
1086                        SourceCallBinding::LocalDeclarations(mapped.into())
1087                    })
1088            })
1089            .clone(),
1090        SourceCallBinding::Imported {
1091            import,
1092            reference,
1093            member,
1094            rust_toolchain,
1095        } => imports.get(*import).copied().flatten().map_or(
1096            SourceCallBinding::Unresolved,
1097            |import| SourceCallBinding::Imported {
1098                import,
1099                reference: *reference,
1100                member: member.clone(),
1101                rust_toolchain: rust_toolchain.clone(),
1102            },
1103        ),
1104        SourceCallBinding::Shadowed
1105        | SourceCallBinding::RustPath(_)
1106        | SourceCallBinding::Qualified(_)
1107        | SourceCallBinding::Unresolved
1108        | SourceCallBinding::PhpNamespace => binding.clone(),
1109    }
1110}
1111
1112/// One extraction with independent source provenance and transient binding proof.
1113#[derive(Debug)]
1114pub struct SourceExtraction {
1115    /// Parser that produced the captured source facts.
1116    pub(crate) source_parser: ParserKind,
1117    /// Compatible graph facts used by existing storage and public renderers.
1118    pub(crate) graph: SymbolGraph,
1119    /// Typed source evidence; never reconstructed from a cached display string.
1120    pub(crate) semantics: SourceSemanticFacts,
1121}
1122
1123impl SourceExtraction {
1124    /// Borrow the compatible graph without losing its associated evidence.
1125    #[must_use]
1126    pub const fn graph(&self) -> &SymbolGraph {
1127        &self.graph
1128    }
1129
1130    /// Borrow the binding decisions paired with this graph.
1131    #[must_use]
1132    pub const fn semantics(&self) -> &SourceSemanticFacts {
1133        &self.semantics
1134    }
1135
1136    /// Move the independently owned provenance, graph and semantic evidence together.
1137    #[must_use]
1138    pub fn into_parts(self) -> (ParserKind, SymbolGraph, SourceSemanticFacts) {
1139        (self.source_parser, self.graph, self.semantics)
1140    }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145    use super::*;
1146    use crate::{ImportSyntax, extract_symbol_graph};
1147    use projectatlas_core::symbols::SymbolKind;
1148    use std::fmt::Write as _;
1149
1150    /// Unpack positive fixtures while keeping assertions in unit-returning tests.
1151    macro_rules! fixture_ok {
1152        ($value:expr) => {{
1153            let outcome = $value;
1154            if let Err(error) = &outcome {
1155                assert!(outcome.is_ok(), "fixture operation failed: {error:?}");
1156            }
1157            let Ok(value) = outcome else {
1158                return;
1159            };
1160            value
1161        }};
1162    }
1163
1164    #[test]
1165    fn native_and_host_occurrences_keep_original_scalar_coordinates_through_filtering() {
1166        for (path, language, source) in [
1167            (
1168                "main.rs",
1169                "rust",
1170                "/* Purpose: 💡π */ fn target(){} fn caller(){let _=\"💡π\";target();target();}",
1171            ),
1172            (
1173                "build.rs",
1174                "rust-build-script",
1175                "fn target(){} fn caller(){let _=\"💡π\";target();target();}",
1176            ),
1177            (
1178                "main.py",
1179                "python",
1180                "def target(): pass\ndef caller(): \"💡π\";target();target()\n",
1181            ),
1182            (
1183                "main.js",
1184                "javascript",
1185                "function target(){} function caller(){const x=\"💡π\";target();target();}",
1186            ),
1187            (
1188                "main.ts",
1189                "typescript",
1190                "function target(){} function caller(){const x=\"💡π\";target();target();}",
1191            ),
1192            (
1193                "main.tsx",
1194                "tsx",
1195                "function target(){} function caller(){const x=\"💡π\";target();target();}",
1196            ),
1197            (
1198                "main.go",
1199                "go",
1200                "package p; func Target(){}; func Caller(){_ = \"💡π\";Target();Target()}",
1201            ),
1202            (
1203                "main.c",
1204                "c",
1205                "void target(){} void caller(){const char*x=\"💡π\";(void)x;target();target();}",
1206            ),
1207            (
1208                "main.cpp",
1209                "cpp",
1210                "void target(){} void caller(){const char*x=\"💡π\";(void)x;target();target();}",
1211            ),
1212            (
1213                "main.m",
1214                "objective-c",
1215                "void target(){} void caller(){const char*x=\"💡π\";(void)x;target();target();}",
1216            ),
1217            (
1218                "Main.java",
1219                "java",
1220                "class Main {void target(){} void caller(){String x=\"💡π\";target();target();}}",
1221            ),
1222            (
1223                "Main.cs",
1224                "csharp",
1225                "class Main {void target(){} void caller(){string x=\"💡π\";target();target();}}",
1226            ),
1227            (
1228                "main.kt",
1229                "kotlin",
1230                "fun target(){}\r\nfun caller(){val x=\"💡π\";target();target()}\r\n",
1231            ),
1232            (
1233                "main.zig",
1234                "zig",
1235                "fn target() void {} fn caller() void {const x=\"💡π\";_=x;target();target();}",
1236            ),
1237            (
1238                "main.php",
1239                "php",
1240                "<?php function target(){} function caller(){$x=\"💡π\";target();target();}",
1241            ),
1242            (
1243                "view.html",
1244                "html",
1245                "<div>💡π</div><script>function target(){} function caller(){target();target();}</script>",
1246            ),
1247            (
1248                "view.vue",
1249                "vue",
1250                "<template>💡π</template><script setup>function target(){} function caller(){target();target();}</script>",
1251            ),
1252            (
1253                "view.svelte",
1254                "svelte",
1255                "<div>💡π</div><script>function target(){} function caller(){target();target();}</script>",
1256            ),
1257        ] {
1258            let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1259                path,
1260                Some(language),
1261                source,
1262                &projectatlas_core::IndexWorkControl::new(
1263                    projectatlas_core::IndexCancellation::new(),
1264                    None
1265                )
1266            ));
1267            assert_eq!(
1268                result.semantics().calls().len(),
1269                2,
1270                "{language}: {:?}",
1271                result.graph()
1272            );
1273            let original = result.semantics().calls();
1274            assert_ne!(
1275                original[0].source_span, original[1].source_span,
1276                "{language}"
1277            );
1278            for call in original {
1279                let span = fixture_ok!(call.source_span.ok_or("missing captured coordinates"));
1280                let prefix = &source[..call.span.start];
1281                let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
1282                let column = prefix
1283                    .rsplit('\n')
1284                    .next()
1285                    .map_or(0, |line| line.chars().count());
1286                assert_eq!(
1287                    (span.start_line() as usize, span.start_column() as usize),
1288                    (line, column),
1289                    "{language}"
1290                );
1291                let end = &source[..call.span.end];
1292                let end_line = end.bytes().filter(|byte| *byte == b'\n').count() + 1;
1293                let end_column = end
1294                    .rsplit('\n')
1295                    .next()
1296                    .map_or(0, |line| line.chars().count());
1297                assert_eq!(
1298                    (span.end_line() as usize, span.end_column() as usize),
1299                    (end_line, end_column),
1300                    "{language}"
1301                );
1302            }
1303            let mut graph = result.graph().clone();
1304            let mut facts = result.semantics().clone();
1305            let expected = original[1].source_span;
1306            let mut relations = vec![true; graph.relations.len()];
1307            relations[original[0].relation] = false;
1308            fixture_ok!(facts.retain_graph_facts(
1309                &mut graph,
1310                &vec![true; result.graph().symbols.len()],
1311                &relations
1312            ));
1313            assert_eq!(facts.calls().len(), 1, "{language}");
1314            assert_eq!(facts.calls()[0].source_span, expected, "{language}");
1315            assert!(facts.matches_graph(&graph));
1316        }
1317    }
1318
1319    #[test]
1320    fn multiline_imports_and_refined_callbacks_keep_their_exact_coordinates() {
1321        for (path, language, source) in [
1322            (
1323                "main.rs",
1324                "rust",
1325                "/*Ï€*/ use std::{\r\n mem as aa,\r\n}; use std::mem as ab;",
1326            ),
1327            (
1328                "main.py",
1329                "python",
1330                "from worker import (\r\n target as aa,\r\n)\r\nfrom worker import target as ab\r\n",
1331            ),
1332            (
1333                "main.js",
1334                "javascript",
1335                "/*Ï€*/ import {\r\n target as aa\r\n} from './Ï€worker.js'; import {target as ab} from './Ï€worker.js';",
1336            ),
1337        ] {
1338            let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1339                path,
1340                Some(language),
1341                source,
1342                &projectatlas_core::IndexWorkControl::new(
1343                    projectatlas_core::IndexCancellation::new(),
1344                    None
1345                )
1346            ));
1347            let imports = result.semantics().imports();
1348            assert_eq!(imports.len(), 2, "{language}");
1349            let first = fixture_ok!(imports[0].source_span.ok_or("missing import coordinates"));
1350            let second = fixture_ok!(imports[1].source_span.ok_or("missing import coordinates"));
1351            assert_ne!(first, second);
1352            assert!(first.end_line() > first.start_line());
1353        }
1354        let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1355            "main.rs",
1356            Some("rust"),
1357            "mod worker {pub fn target(){}} fn caller(){let _=(1 == 1).then(worker::target);let _=(1 == 1).then(worker::target);}",
1358            &projectatlas_core::IndexWorkControl::new(
1359                projectatlas_core::IndexCancellation::new(),
1360                None
1361            )
1362        ));
1363        let callbacks = result
1364            .semantics()
1365            .calls()
1366            .iter()
1367            .filter(|call| result.graph().relations[call.relation].target_name == "worker::target")
1368            .collect::<Vec<_>>();
1369        assert_eq!(callbacks.len(), 2);
1370        assert_ne!(callbacks[0].source_span, callbacks[1].source_span);
1371        for callback in callbacks {
1372            let span = fixture_ok!(callback.source_span.ok_or("missing callback coordinates"));
1373            assert_eq!(
1374                span.end_column() - span.start_column(),
1375                "worker::target".len() as u32
1376            );
1377        }
1378    }
1379
1380    #[test]
1381    fn native_member_declarations_do_not_replace_ordinary_function_values() {
1382        let mut failures = Vec::new();
1383        for (path, language, source, name) in [
1384            (
1385                "source.rs",
1386                "rust",
1387                "fn run(){} struct A; impl A {fn run(){} fn caller(){run();}}",
1388                "run",
1389            ),
1390            (
1391                "trait.rs",
1392                "rust",
1393                "fn run(){} trait A {fn run(){} fn caller(){run();}}",
1394                "run",
1395            ),
1396            (
1397                "source.go",
1398                "go",
1399                "package p; func Run(){}; type A struct{}; func(A) Run(){}; func(A) Caller(){Run()}",
1400                "Run",
1401            ),
1402            (
1403                "source.js",
1404                "javascript",
1405                "function run(){} class A {run(){} caller(){run();}}",
1406                "run",
1407            ),
1408            (
1409                "object.js",
1410                "javascript",
1411                "function run(){} export const obj={run(){},caller(){run();}};",
1412                "run",
1413            ),
1414            (
1415                "source.ts",
1416                "typescript",
1417                "function run(){} class A {run(){} caller(){run();}}",
1418                "run",
1419            ),
1420            (
1421                "source.tsx",
1422                "tsx",
1423                "function run(){} class A {run(){} caller(){run();}}",
1424                "run",
1425            ),
1426            (
1427                "source.m",
1428                "objective-c",
1429                "void run(void){} @interface A -(void)run; -(void)caller; @end @implementation A -(void)run{} -(void)caller{run();} @end",
1430                "run",
1431            ),
1432            (
1433                "source.py",
1434                "python",
1435                "def run(): pass\nclass A:\n def run(self): pass\n def caller(self): run()\n",
1436                "run",
1437            ),
1438        ] {
1439            let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1440                path,
1441                Some(language),
1442                source,
1443                &projectatlas_core::IndexWorkControl::new(
1444                    projectatlas_core::IndexCancellation::new(),
1445                    None
1446                )
1447            ));
1448            let correct = !result.semantics().calls().is_empty()
1449                && result
1450                    .semantics()
1451                    .calls()
1452                    .iter()
1453                    .all(|call| match call.binding {
1454                        SourceCallBinding::LocalDeclaration(ordinal) => {
1455                            result.graph().symbols.get(ordinal).is_some_and(|symbol| {
1456                                symbol.name == name
1457                                    && symbol.kind == SymbolKind::Function
1458                                    && symbol.parent.is_none()
1459                            })
1460                        }
1461                        _ => false,
1462                    });
1463            if !correct {
1464                failures.push((path, result.semantics().calls().to_vec()));
1465            }
1466        }
1467        assert!(failures.is_empty(), "{failures:?}");
1468    }
1469
1470    #[test]
1471    fn distinct_declarations_keep_implicit_member_lookup_and_real_ambiguity() {
1472        for (path, language, source) in [
1473            (
1474                "source.java",
1475                "java",
1476                "class One {void run(){} void caller(){run();}} class Two {void run(){} void caller(){run();}}",
1477            ),
1478            (
1479                "source.cs",
1480                "csharp",
1481                "class One {void run(){} void caller(){run();}} class Two {void run(){} void caller(){run();}}",
1482            ),
1483            (
1484                "source.cpp",
1485                "cpp",
1486                "class One {public:void run(){} void caller(){run();}}; class Two {public:void run(){} void caller(){run();}};",
1487            ),
1488            (
1489                "source.kt",
1490                "kotlin",
1491                "class One {\n fun run() {}\n fun caller() { run() }\n}\nclass Two {\n fun run() {}\n fun caller() { run() }\n}\n",
1492            ),
1493        ] {
1494            let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1495                path,
1496                Some(language),
1497                source,
1498                &projectatlas_core::IndexWorkControl::new(
1499                    projectatlas_core::IndexCancellation::new(),
1500                    None
1501                )
1502            ));
1503            assert_eq!(
1504                result.semantics().calls().len(),
1505                2,
1506                "{language}: {:?}",
1507                result.graph()
1508            );
1509            for call in result.semantics().calls() {
1510                let owner = fixture_ok!(
1511                    call.owner
1512                        .and_then(|ordinal| result.graph().symbols.get(ordinal))
1513                        .ok_or("missing exact caller")
1514                );
1515                assert!(
1516                    matches!(call.binding, SourceCallBinding::LocalDeclaration(_)),
1517                    "{language}: {:?}",
1518                    call.binding
1519                );
1520                let SourceCallBinding::LocalDeclaration(ordinal) = call.binding else {
1521                    return;
1522                };
1523                let target =
1524                    fixture_ok!(result.graph().symbols.get(ordinal).ok_or("missing target"));
1525                assert_eq!(target.name, "run");
1526                assert_eq!(target.parent, owner.parent, "{language}");
1527            }
1528        }
1529        let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1530            "same.rs",
1531            Some("rust"),
1532            "fn run(){} fn run(){} fn caller(){run();}",
1533            &projectatlas_core::IndexWorkControl::new(
1534                projectatlas_core::IndexCancellation::new(),
1535                None
1536            )
1537        ));
1538        assert!(
1539            result
1540                .semantics()
1541                .calls()
1542                .iter()
1543                .all(|call| !matches!(call.binding, SourceCallBinding::LocalDeclaration(_)))
1544        );
1545    }
1546
1547    #[test]
1548    fn rust_scoped_import_candidates_preserve_sharing_and_cannot_narrow_after_filtering() {
1549        let source = "mod m {pub struct Foo{}} fn foo(){} fn caller(){use crate::m::Foo;use crate::foo as Foo; Foo();\nFoo();} fn local(){use crate::m::Foo;fn Foo(){} Foo();}";
1550        let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1551            "src/lib.rs",
1552            Some("rust"),
1553            source,
1554            &projectatlas_core::IndexWorkControl::new(
1555                projectatlas_core::IndexCancellation::new(),
1556                None
1557            )
1558        ));
1559        let graph = result.graph();
1560        let original = result.semantics();
1561        let rust = fixture_ok!(original.rust().ok_or("Rust facts missing"));
1562        assert_eq!(rust.candidate_calls.len(), 3);
1563        assert!(Arc::ptr_eq(
1564            &rust.candidate_calls[0].bindings,
1565            &rust.candidate_calls[1].bindings
1566        ));
1567        assert!(
1568            rust.candidate_calls
1569                .iter()
1570                .any(|call| !call.declarations.is_empty())
1571        );
1572        let mut filtered = graph.clone();
1573        let mut facts = original.clone();
1574        let symbols = graph
1575            .symbols
1576            .iter()
1577            .map(|symbol| symbol.name != "Foo" || symbol.kind != SymbolKind::Function)
1578            .collect::<Vec<_>>();
1579        let discarded = original.imports()[1].relation;
1580        let relations = graph
1581            .relations
1582            .iter()
1583            .enumerate()
1584            .map(|(ordinal, _)| ordinal != discarded)
1585            .collect::<Vec<_>>();
1586        fixture_ok!(facts.retain_graph_facts(&mut filtered, &symbols, &relations));
1587        assert!(facts.matches_graph(&filtered));
1588        assert!(
1589            facts
1590                .rust()
1591                .is_some_and(|rust| rust.candidate_calls.is_empty())
1592        );
1593        assert!(
1594            facts
1595                .calls()
1596                .iter()
1597                .all(|call| matches!(call.binding, SourceCallBinding::Unresolved))
1598        );
1599        assert_eq!(facts.imports().len(), original.imports().len() - 1);
1600    }
1601
1602    #[test]
1603    fn go_receiver_parameters_shadow_file_functions_without_hiding_siblings() {
1604        let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1605            "receiver.go",
1606            Some("go"),
1607            "package p; type T func(); func Run(){}; func(Run T) Caller(){Run()}; func(T) Clean(){Run()}",
1608            &projectatlas_core::IndexWorkControl::new(
1609                projectatlas_core::IndexCancellation::new(),
1610                None
1611            )
1612        ));
1613        assert_eq!(result.semantics().calls().len(), 2);
1614        for call in result.semantics().calls() {
1615            let name = result.graph().relations[call.relation].source_name.as_str();
1616            if name == "Caller" {
1617                assert_eq!(call.binding, SourceCallBinding::Shadowed);
1618            } else {
1619                assert_eq!(name, "Clean");
1620                assert!(matches!(
1621                    call.binding,
1622                    SourceCallBinding::LocalDeclaration(_)
1623                ));
1624            }
1625        }
1626    }
1627
1628    #[test]
1629    fn omitted_export_dependencies_cannot_retain_forwarding_authority() {
1630        let mut source = "function target(){} function caller(){".to_string();
1631        for _ in 0..crate::MAX_RELATIONS_PER_FILE {
1632            source.push_str("target();");
1633        }
1634        source.push_str("} export {run as launch} from './target.js';");
1635        let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1636            "forward.js",
1637            Some("javascript"),
1638            &source,
1639            &projectatlas_core::IndexWorkControl::new(
1640                projectatlas_core::IndexCancellation::new(),
1641                None
1642            )
1643        ));
1644        assert!(result.semantics().bindings_incomplete);
1645        assert!(result.semantics().exports_incomplete);
1646        assert!(result.semantics().exports().is_empty());
1647        assert!(!result.graph().relations.is_empty());
1648    }
1649
1650    #[test]
1651    fn omitted_native_imports_cannot_leave_exact_call_authority() {
1652        for (path, language, prefix, import) in [
1653            (
1654                "source.py",
1655                "python",
1656                "def target(): pass\ndef caller(): target()\n",
1657                "import module",
1658            ),
1659            (
1660                "source.js",
1661                "javascript",
1662                "function target(){} function caller(){target();}\n",
1663                "import * as module",
1664            ),
1665        ] {
1666            let mut source = prefix.to_string();
1667            for ordinal in 0..=crate::MAX_RELATIONS_PER_FILE {
1668                if language == "python" {
1669                    fixture_ok!(writeln!(source, "{import}{ordinal}"));
1670                } else {
1671                    fixture_ok!(writeln!(
1672                        source,
1673                        "{import}{ordinal} from './module{ordinal}';"
1674                    ));
1675                }
1676            }
1677            let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1678                path,
1679                Some(language),
1680                &source,
1681                &projectatlas_core::IndexWorkControl::new(
1682                    projectatlas_core::IndexCancellation::new(),
1683                    None
1684                )
1685            ));
1686            let facts = result.semantics();
1687            assert!(facts.bindings_incomplete, "{language}");
1688            assert!(
1689                !facts.imports().is_empty() && !facts.calls().is_empty(),
1690                "{language}"
1691            );
1692            assert!(
1693                facts
1694                    .calls()
1695                    .iter()
1696                    .all(|call| matches!(call.binding, SourceCallBinding::Unresolved)),
1697                "{language}"
1698            );
1699            let mut graph = result.graph().clone();
1700            let mut remapped = facts.clone();
1701            fixture_ok!(remapped.retain_graph_facts(
1702                &mut graph,
1703                &vec![true; result.graph().symbols.len()],
1704                &vec![true; result.graph().relations.len()]
1705            ));
1706            assert!(remapped.bindings_incomplete && remapped.matches_graph(&graph));
1707        }
1708    }
1709
1710    #[test]
1711    fn rust_remapping_retains_blockers_and_drops_lost_import_authority() {
1712        let source = "pub fn run(){} pub struct Service; impl Service {pub fn work(){}} pub use crate::run as launch; use std::mem::drop as dispose; fn caller(){launch();dispose(1);}";
1713        let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1714            "src/lib.rs",
1715            Some("rust"),
1716            source,
1717            &projectatlas_core::IndexWorkControl::new(
1718                projectatlas_core::IndexCancellation::new(),
1719                None
1720            )
1721        ));
1722        let (graph, original) = (result.graph(), result.semantics());
1723        let mut filtered = graph.clone();
1724        let mut facts = original.clone();
1725        let symbols = graph
1726            .symbols
1727            .iter()
1728            .map(|symbol| symbol.name != "run" && symbol.name != "Service" && symbol.name != "work")
1729            .collect::<Vec<_>>();
1730        let relations = graph
1731            .relations
1732            .iter()
1733            .map(|relation| relation.kind != RelationKind::Imports)
1734            .collect::<Vec<_>>();
1735        fixture_ok!(facts.retain_graph_facts(&mut filtered, &symbols, &relations));
1736        assert!(facts.matches_graph(&filtered));
1737        let rust = fixture_ok!(facts.rust().ok_or("Rust facts missing"));
1738        assert!(
1739            rust.declarations
1740                .iter()
1741                .any(|declaration| declaration.name == "run"
1742                    && declaration.binding == crate::SourceRustBinding::Unresolved)
1743        );
1744        assert!(
1745            rust.declarations
1746                .iter()
1747                .any(|declaration| declaration.name == "Service"
1748                    && declaration.binding == crate::SourceRustBinding::UnresolvedType)
1749        );
1750        assert!(
1751            rust.declarations
1752                .iter()
1753                .any(|declaration| declaration.name == "launch"
1754                    && declaration.binding == crate::SourceRustBinding::UnresolvedImport)
1755        );
1756        assert!(
1757            rust.associated
1758                .iter()
1759                .all(|method| method.declaration.is_none())
1760        );
1761        assert!(
1762            facts
1763                .calls()
1764                .iter()
1765                .all(|call| !matches!(call.binding, SourceCallBinding::Imported { .. }))
1766        );
1767        assert!(
1768            original
1769                .imports()
1770                .iter()
1771                .any(|import| import.rust_toolchain.is_some())
1772        );
1773        assert!(facts.imports().is_empty());
1774    }
1775
1776    #[test]
1777    fn embedded_hosts_keep_binding_ownership_and_original_byte_occurrences() {
1778        let content = "<template>🙂</template>\n<script lang=\"ts\">\nimport launch, { run as named } from './worker';\nfunction caller(launch: () => void) { launch(); }\nfunction clean() { launch(); named(); named(); }\n</script>\n<style>fake { content: 'launch()'; }</style>";
1779        for (path, language) in [
1780            ("view.vue", "vue"),
1781            ("view.svelte", "svelte"),
1782            ("view.html", "html"),
1783        ] {
1784            let content = if language == "html" {
1785                content.replace("<script lang", "<script type=module lang")
1786            } else {
1787                content.to_string()
1788            };
1789            let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1790                path,
1791                Some(language),
1792                &content,
1793                &projectatlas_core::IndexWorkControl::new(
1794                    projectatlas_core::IndexCancellation::new(),
1795                    None
1796                )
1797            ));
1798            let (_, graph, facts) = extraction.into_parts();
1799            assert!(facts.matches_graph(&graph), "{language}");
1800            assert_eq!(
1801                facts.content_hash(),
1802                blake3::hash(content.as_bytes()).to_hex().as_str()
1803            );
1804            assert_eq!(facts.imports().len(), 1, "{language}");
1805            assert_eq!(facts.imports()[0].references.len(), 2, "{language}");
1806            let calls = facts.calls();
1807            assert_eq!(calls.len(), 4, "{language}: {graph:?}");
1808            assert_eq!(calls[0].binding, SourceCallBinding::Shadowed, "{language}");
1809            for call in &calls[1..] {
1810                assert!(
1811                    matches!(call.binding, SourceCallBinding::Imported { .. }),
1812                    "{language}: {call:?}"
1813                );
1814                assert_eq!(
1815                    call.owner.map(|owner| graph.symbols[owner].name.as_str()),
1816                    Some("clean")
1817                );
1818                assert!(matches!(
1819                    &content[call.span.start..call.span.end],
1820                    "launch()" | "named()"
1821                ));
1822            }
1823            assert_ne!(
1824                calls[2].relation, calls[3].relation,
1825                "distinct same-line occurrences"
1826            );
1827            assert_eq!(
1828                graph.parser,
1829                if language == "svelte" {
1830                    ParserKind::Fallback
1831                } else {
1832                    ParserKind::Structural
1833                },
1834                "{language} host provenance"
1835            );
1836        }
1837    }
1838
1839    #[test]
1840    fn embedded_script_families_keep_independent_import_ordinals() {
1841        let content = "<script type=module>import { run as first } from './first'; function js_caller() { first(); }</script>\n<script type=module lang='ts'>import { run as second } from './second'; function ts_caller() { second(); }</script>";
1842        let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1843            "view.html",
1844            Some("html"),
1845            content,
1846            &projectatlas_core::IndexWorkControl::new(
1847                projectatlas_core::IndexCancellation::new(),
1848                None
1849            )
1850        ));
1851        let (_, graph, facts) = extraction.into_parts();
1852        assert!(facts.matches_graph(&graph));
1853        assert_eq!(facts.imports().len(), 2);
1854        assert_eq!(facts.calls().len(), 2);
1855        for fact in facts.calls() {
1856            assert!(
1857                matches!(fact.binding, SourceCallBinding::Imported { .. }),
1858                "missing independently mapped import: {fact:?}"
1859            );
1860            let SourceCallBinding::Imported {
1861                import, reference, ..
1862            } = fact.binding
1863            else {
1864                return;
1865            };
1866            let module = facts.imports()[import].references[reference].module();
1867            assert_eq!(
1868                module,
1869                if graph.relations[fact.relation].target_name == "first" {
1870                    "./first"
1871                } else {
1872                    "./second"
1873                }
1874            );
1875        }
1876    }
1877
1878    #[test]
1879    fn coalesced_embedded_declarations_cannot_narrow_ambiguous_binding_proof() {
1880        let script = "function run() {} function run() {} function clean() { run(); }";
1881        let control = projectatlas_core::IndexWorkControl::new(
1882            projectatlas_core::IndexCancellation::new(),
1883            None,
1884        );
1885        let native = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1886            "script.js",
1887            Some("javascript"),
1888            script,
1889            &control
1890        ));
1891        assert_eq!(
1892            native
1893                .graph()
1894                .symbols
1895                .iter()
1896                .filter(|symbol| symbol.name == "run")
1897                .count(),
1898            2
1899        );
1900        let mut merged = native.graph().clone();
1901        merged.symbols.clear();
1902        merged.relations.clear();
1903        let mut mapping = crate::GraphMergeOrdinals {
1904            symbols: vec![crate::SourceSymbolRemap::Omitted; native.graph().symbols.len()],
1905            relations: vec![None; native.graph().relations.len()],
1906        };
1907        fixture_ok!(crate::merge_missing_graph_entries_checked(
1908            &mut merged,
1909            native.graph().clone(),
1910            &std::collections::BTreeMap::new(),
1911            projectatlas_core::language::EmbeddedHostKind::HtmlLike,
1912            &mut || control.check(projectatlas_core::IndexWorkStage::SymbolParsing),
1913            Some(&mut mapping)
1914        ));
1915        for (ordinal, symbol) in native.graph().symbols.iter().enumerate() {
1916            assert_eq!(
1917                mapping.symbols[ordinal].retained().is_none(),
1918                symbol.name == "run",
1919                "all coalesced owners must lose correspondence"
1920            );
1921        }
1922        let content = format!("<script>{script}</script>");
1923        let host = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1924            "view.html",
1925            Some("html"),
1926            &content,
1927            &control
1928        ));
1929        assert_eq!(
1930            host.graph()
1931                .symbols
1932                .iter()
1933                .filter(|symbol| symbol.name == "run")
1934                .count(),
1935            1
1936        );
1937        assert_eq!(host.semantics().calls().len(), 1);
1938        assert_eq!(
1939            host.semantics().calls()[0].binding,
1940            SourceCallBinding::Unresolved
1941        );
1942        assert!(host.semantics().matches_graph(host.graph()));
1943    }
1944
1945    fn captured_graph() -> Result<(SymbolGraph, SourceSemanticFacts), Box<dyn std::error::Error>> {
1946        let content = "use dep::run as launch; fn run() {} fn caller() { launch(); }";
1947        let graph = extract_symbol_graph("src/main.rs", Some("rust"), content);
1948        let import = graph
1949            .relations
1950            .iter()
1951            .position(|fact| fact.kind == RelationKind::Imports);
1952        let call = graph
1953            .relations
1954            .iter()
1955            .position(|fact| fact.kind == RelationKind::Calls);
1956        let import = import.ok_or_else(|| std::io::Error::other("fixture import missing"))?;
1957        let call = call.ok_or_else(|| std::io::Error::other("fixture call missing"))?;
1958        let call_start = content
1959            .rfind("launch()")
1960            .ok_or_else(|| std::io::Error::other("fixture call span missing"))?;
1961        let mut facts = SourceSemanticFacts {
1962            imports: vec![SourceImportFact {
1963                source_span: None,
1964                relation: import,
1965                span: SourceByteSpan { start: 0, end: 23 },
1966                module: Some("dep".into()),
1967                references: vec![ImportReference::new(
1968                    ImportSyntax::Rust,
1969                    "dep",
1970                    Some("run"),
1971                    "launch",
1972                )],
1973                rust: None,
1974                rust_toolchain: None,
1975            }],
1976            calls: vec![SourceCallFact {
1977                source_span: None,
1978                relation: call,
1979                span: SourceByteSpan {
1980                    start: call_start,
1981                    end: call_start + "launch()".len(),
1982                },
1983                binding: SourceCallBinding::Imported {
1984                    import: 0,
1985                    reference: 0,
1986                    member: None,
1987                    rust_toolchain: None,
1988                },
1989                owner: None,
1990            }],
1991            ..SourceSemanticFacts::default()
1992        };
1993        facts.bind_source(&graph, content);
1994        Ok((graph, facts))
1995    }
1996
1997    #[test]
1998    fn same_size_graph_reordering_cannot_reuse_binding_ordinals() {
1999        let (mut graph, facts) = fixture_ok!(captured_graph());
2000        assert!(facts.matches_graph(&graph));
2001        graph.relations.swap(0, 1);
2002        assert!(!facts.matches_graph(&graph));
2003        let symbols = (0..graph.symbols.len()).map(Some).collect::<Vec<_>>();
2004        let relations = (0..graph.relations.len()).map(Some).collect::<Vec<_>>();
2005        assert_eq!(
2006            facts.remap(&graph, &graph, &symbols, &relations),
2007            Err(SourceSemanticRemapError::GraphMismatch)
2008        );
2009    }
2010
2011    #[test]
2012    fn retaining_graph_facts_rebases_targets_and_call_owners_without_source_drift() {
2013        let content = "fn padding() {} fn run() {} fn caller() { run(); }";
2014        let control = projectatlas_core::IndexWorkControl::new(
2015            projectatlas_core::IndexCancellation::new(),
2016            None,
2017        );
2018        let extracted = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2019            "main.rs",
2020            Some("rust"),
2021            content,
2022            &control
2023        ));
2024        let (_, mut graph, mut facts) = extracted.into_parts();
2025        let hash = facts.content_hash.clone();
2026        assert_eq!(graph.symbols.len(), 3);
2027        assert_eq!(facts.calls[0].owner, Some(2));
2028        let relations = vec![true; graph.relations.len()];
2029        fixture_ok!(facts.retain_graph_facts(&mut graph, &[false, true, true], &relations));
2030        assert!(facts.matches_graph(&graph));
2031        assert_eq!(facts.content_hash, hash);
2032        assert_eq!(facts.calls[0].owner, Some(1));
2033        assert_eq!(
2034            facts.calls[0].binding,
2035            SourceCallBinding::LocalDeclaration(0)
2036        );
2037        let original_graph = graph.clone();
2038        let original_facts = facts.clone();
2039        assert_eq!(
2040            facts.retain_graph_facts(&mut graph, &[true], &relations),
2041            Err(SourceSemanticRemapError::GraphMismatch)
2042        );
2043        assert_eq!(graph, original_graph);
2044        assert_eq!(facts, original_facts);
2045    }
2046
2047    #[test]
2048    fn shared_overload_sets_stay_shared_through_ordinal_remapping() {
2049        let content =
2050            "void run(int value) {}\nvoid run(double value) {}\nvoid caller() { run(0); run(1); }";
2051        let control = projectatlas_core::IndexWorkControl::new(
2052            projectatlas_core::IndexCancellation::new(),
2053            None,
2054        );
2055        let extracted = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2056            "main.cpp",
2057            Some("cpp"),
2058            content,
2059            &control
2060        ));
2061        let (_, mut graph, mut facts) = extracted.into_parts();
2062        let symbols = vec![true; graph.symbols.len()];
2063        let relations = vec![true; graph.relations.len()];
2064        fixture_ok!(facts.retain_graph_facts(&mut graph, &symbols, &relations));
2065        assert!(facts.matches_graph(&graph));
2066        let (
2067            SourceCallBinding::LocalDeclarations(first),
2068            SourceCallBinding::LocalDeclarations(second),
2069        ) = (&facts.calls[0].binding, &facts.calls[1].binding)
2070        else {
2071            assert!(
2072                facts
2073                    .calls
2074                    .iter()
2075                    .all(|fact| matches!(fact.binding, SourceCallBinding::LocalDeclarations(_))),
2076                "{:?}",
2077                facts.calls
2078            );
2079            return;
2080        };
2081        assert!(Arc::ptr_eq(first, second));
2082    }
2083
2084    #[test]
2085    fn filtering_an_import_does_not_rebind_its_surviving_call() {
2086        let (graph, facts) = fixture_ok!(captured_graph());
2087        let removed = facts.imports[0].relation;
2088        let mut admitted = graph.clone();
2089        admitted.relations.remove(removed);
2090        let symbols = (0..graph.symbols.len()).map(Some).collect::<Vec<_>>();
2091        let relations = (0..graph.relations.len())
2092            .map(|index| {
2093                if index == removed {
2094                    None
2095                } else {
2096                    Some(index - usize::from(index > removed))
2097                }
2098            })
2099            .collect::<Vec<_>>();
2100        let remapped = fixture_ok!(facts.remap(&graph, &admitted, &symbols, &relations));
2101        assert!(remapped.matches_graph(&admitted));
2102        assert!(remapped.imports.is_empty());
2103        assert_eq!(remapped.calls[0].binding, SourceCallBinding::Unresolved);
2104        assert_eq!(remapped.content_hash, facts.content_hash);
2105    }
2106
2107    #[test]
2108    fn filtering_ambiguous_declarations_does_not_create_an_exact_target() {
2109        let binding = SourceCallBinding::LocalDeclarations(vec![0, 1].into());
2110        assert_eq!(
2111            remap_call_binding(&binding, &[Some(0), None], &[], &mut HashMap::new()),
2112            SourceCallBinding::Unresolved
2113        );
2114        assert_eq!(
2115            remap_call_binding(&binding, &[Some(1), Some(0)], &[], &mut HashMap::new()),
2116            SourceCallBinding::LocalDeclarations(vec![1, 0].into())
2117        );
2118    }
2119
2120    #[test]
2121    fn duplicate_or_outside_output_ordinals_fail_without_changing_evidence() {
2122        let (graph, facts) = fixture_ok!(captured_graph());
2123        let symbols = (0..graph.symbols.len()).map(Some).collect::<Vec<_>>();
2124        let mut relations = (0..graph.relations.len()).map(Some).collect::<Vec<_>>();
2125        relations[1] = relations[0];
2126        assert_eq!(
2127            facts.remap(&graph, &graph, &symbols, &relations),
2128            Err(SourceSemanticRemapError::InvalidOrdinalMap)
2129        );
2130        relations[1] = Some(graph.relations.len());
2131        assert_eq!(
2132            facts.remap(&graph, &graph, &symbols, &relations),
2133            Err(SourceSemanticRemapError::InvalidOrdinalMap)
2134        );
2135        assert!(facts.matches_graph(&graph));
2136    }
2137
2138    #[test]
2139    fn malformed_ecmascript_imports_have_no_admitted_binding_evidence() {
2140        let control = projectatlas_core::IndexWorkControl::new(
2141            projectatlas_core::IndexCancellation::new(),
2142            None,
2143        );
2144        for language in ["javascript", "typescript", "tsx"] {
2145            for declaration in [
2146                "import foo.bar, { run } from './dep';",
2147                "import class, { run } from './dep';",
2148                "import { run as class } from './dep';",
2149                "import * as class from './dep';",
2150            ] {
2151                let extraction =
2152                    fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2153                        "src/main.tsx",
2154                        Some(language),
2155                        declaration,
2156                        &control,
2157                    ));
2158                assert!(extraction.semantics().matches_graph(extraction.graph()));
2159                assert!(
2160                    extraction
2161                        .semantics()
2162                        .imports()
2163                        .iter()
2164                        .flat_map(|fact| &fact.references)
2165                        .all(|reference| !matches!(reference.local(), "class" | "foo.bar" | "foo")),
2166                    "{language}: {declaration}"
2167                );
2168            }
2169        }
2170    }
2171
2172    #[test]
2173    fn valid_mixed_import_evidence_retains_independent_references_and_source() {
2174        let control = projectatlas_core::IndexWorkControl::new(
2175            projectatlas_core::IndexCancellation::new(),
2176            None,
2177        );
2178        let content =
2179            "import primary, { run as launch } from './dep'; function caller() { launch(); }";
2180        let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2181            "src/main.js",
2182            Some("javascript"),
2183            content,
2184            &control,
2185        ));
2186        let facts = extraction.semantics();
2187        assert!(facts.matches_graph(extraction.graph()));
2188        assert_eq!(facts.imports().len(), 1);
2189        assert_eq!(facts.imports()[0].references.len(), 2);
2190        assert_eq!(facts.imports()[0].references[0].local(), "primary");
2191        assert_eq!(facts.imports()[0].references[1].local(), "launch");
2192        assert_eq!(facts.calls().len(), 1);
2193        let span = facts.calls()[0].span;
2194        assert_eq!(&content[span.start..span.end], "launch()");
2195        assert_eq!(
2196            facts.content_hash(),
2197            blake3::hash(content.as_bytes()).to_hex().as_str()
2198        );
2199    }
2200
2201    #[test]
2202    fn invalid_binding_leaves_preserve_static_modules_and_valid_siblings() {
2203        let control = projectatlas_core::IndexWorkControl::new(
2204            projectatlas_core::IndexCancellation::new(),
2205            None,
2206        );
2207        for language in ["javascript", "typescript", "tsx"] {
2208            for statement in [
2209                "import class, { run as keep } from './dep';",
2210                "import foo.bar, { run as keep } from './dep';",
2211                "import primary, { run as class, stop as keep } from './dep';",
2212                r"import cl\u0061ss, { run as keep } from './dep';",
2213                r"import { run as ev\u0061l, stop as keep } from './dep';",
2214                "import { run as 🙂, stop as keep } from './dep';",
2215            ] {
2216                let extraction =
2217                    fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2218                        "src/main.tsx",
2219                        Some(language),
2220                        statement,
2221                        &control,
2222                    ));
2223                let imports = extraction.semantics().imports();
2224                assert!(
2225                    imports
2226                        .iter()
2227                        .any(|fact| fact.module.as_deref() == Some("./dep")),
2228                    "lost module: {language} {statement}"
2229                );
2230                let locals = imports
2231                    .iter()
2232                    .flat_map(|fact| &fact.references)
2233                    .map(ImportReference::local)
2234                    .collect::<Vec<_>>();
2235                let diagnostic = if locals.contains(&"keep") {
2236                    String::new()
2237                } else {
2238                    let language = fixture_ok!(
2239                        crate::tree_sitter_language(language)
2240                            .ok_or_else(|| std::io::Error::other("fixture language missing"))
2241                    );
2242                    let mut parser = tree_sitter::Parser::new();
2243                    fixture_ok!(parser.set_language(&language));
2244                    fixture_ok!(
2245                        parser
2246                            .parse(statement, None)
2247                            .ok_or_else(|| std::io::Error::other("fixture parse missing"))
2248                    )
2249                    .root_node()
2250                    .to_sexp()
2251                };
2252                assert!(
2253                    locals.contains(&"keep"),
2254                    "lost valid sibling: {language} {statement}: {locals:?} {diagnostic}"
2255                );
2256                assert!(
2257                    locals
2258                        .iter()
2259                        .all(|local| matches!(*local, "primary" | "keep")),
2260                    "invalid leaf admitted: {language} {statement}: {locals:?}"
2261                );
2262            }
2263            let statement = r"import { class as C, default as D, run as l\u0061unch, stop as undefined } from './dep';";
2264            let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2265                "src/main.tsx",
2266                Some(language),
2267                statement,
2268                &control,
2269            ));
2270            let locals = extraction
2271                .semantics()
2272                .imports()
2273                .iter()
2274                .flat_map(|fact| &fact.references)
2275                .map(ImportReference::local)
2276                .collect::<Vec<_>>();
2277            assert_eq!(locals, ["C", "D", "launch", "undefined"], "{language}");
2278        }
2279    }
2280
2281    #[test]
2282    fn recovery_uses_the_ast_separator_and_preserves_quoted_export_data() {
2283        let control = projectatlas_core::IndexWorkControl::new(
2284            projectatlas_core::IndexCancellation::new(),
2285            None,
2286        );
2287        for language in ["javascript", "typescript", "tsx"] {
2288            for (statement, module, imported) in [
2289                (
2290                    "import { from as keep, run as class } from './dep';",
2291                    "./dep",
2292                    "from",
2293                ),
2294                (
2295                    "import { run as class, stop as keep } from './dep from space.js';",
2296                    "./dep from space.js",
2297                    "stop",
2298                ),
2299                (
2300                    r#"import { "name from space" as keep, run as class } from './dep from space.js';"#,
2301                    "./dep from space.js",
2302                    "name from space",
2303                ),
2304                (
2305                    r#"import { "a, ghost as forged, b" as keep, run as class } from './dep';"#,
2306                    "./dep",
2307                    "a, ghost as forged, b",
2308                ),
2309                (
2310                    r#"import { "x as y" as keep, run as class } from './dep';"#,
2311                    "./dep",
2312                    "x as y",
2313                ),
2314                (
2315                    "import /*lead*/ primary, {run /*bad*/ as class, from /*good*/ as keep} /*tail*/ from './dep';",
2316                    "./dep",
2317                    "from",
2318                ),
2319            ] {
2320                let extraction =
2321                    fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2322                        "src/main.tsx",
2323                        Some(language),
2324                        statement,
2325                        &control,
2326                    ));
2327                let imports = extraction.semantics().imports();
2328                assert!(
2329                    imports
2330                        .iter()
2331                        .any(|fact| fact.module.as_deref() == Some(module))
2332                );
2333                let references = imports
2334                    .iter()
2335                    .flat_map(|fact| &fact.references)
2336                    .collect::<Vec<_>>();
2337                assert!(
2338                    references
2339                        .iter()
2340                        .any(|reference| reference.local() == "keep"
2341                            && reference.imported() == Some(imported)),
2342                    "{language}: {statement}: {references:?}"
2343                );
2344                assert!(
2345                    references
2346                        .iter()
2347                        .all(|reference| matches!(reference.local(), "keep" | "primary")),
2348                    "fabricated binding: {language}: {statement}: {references:?}"
2349                );
2350            }
2351            let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2352                "src/main.tsx",
2353                Some(language),
2354                "import { run as keep,, stop as other } from './dep';",
2355                &control,
2356            ));
2357            assert!(
2358                extraction
2359                    .semantics()
2360                    .imports()
2361                    .iter()
2362                    .all(|fact| fact.references.is_empty())
2363            );
2364        }
2365    }
2366}