Skip to main content

aft/callgraph_store/
join.rs

1//! Manifest-only callgraph assembly.
2//!
3//! This module deliberately accepts a manifest plus immutable blob payloads rather
4//! than a checkout root.  Extraction is content-addressed and path-free; binding a
5//! blob to a manifest path and resolving its cross-file references happens here.
6//! The existing resolver uses String file identities. Non-UTF-8 source members
7//! remain in byte-addressed facts but are reported as unbound rather than being
8//! converted lossily; supporting them requires a separate resolver identity change.
9
10use std::collections::{BTreeMap, BTreeSet};
11use std::fmt;
12
13use serde::{Deserialize, Serialize};
14use tree_sitter::Parser;
15
16use super::facts::{BlobKey, FactPaths, ManifestFacts, ProjectFacts};
17use crate::callgraph::{self, FileCallData, SymbolMeta};
18use crate::imports::{ImportBlock, ImportForm, ImportGroup, ImportKind, ImportStatement};
19use crate::parser::{grammar_for, LangId};
20use crate::symbols::SymbolKind;
21use crate::views::{Manifest, ManifestEntry, RelPath};
22use std::collections::HashMap;
23use std::path::Path;
24use std::rc::Rc;
25use std::sync::Arc;
26
27const TOP_LEVEL_SYMBOL: &str = "<top-level>";
28
29/// An error raised while decoding or assembling manifest-addressed callgraph data.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum ManifestJoinError {
32    UnsupportedLanguage(String),
33    Parse(String),
34    InvalidBlob(String),
35    MissingBlob(String),
36    InvalidConfig { path: Vec<u8>, reason: String },
37}
38
39impl fmt::Display for ManifestJoinError {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::UnsupportedLanguage(language) => {
43                write!(formatter, "unsupported callgraph blob language {language}")
44            }
45            Self::Parse(reason) => write!(formatter, "callgraph blob parse failed: {reason}"),
46            Self::InvalidBlob(reason) => write!(formatter, "invalid callgraph blob: {reason}"),
47            Self::MissingBlob(key) => write!(
48                formatter,
49                "manifest references missing callgraph blob {key}"
50            ),
51            Self::InvalidConfig { path, reason } => write!(
52                formatter,
53                "invalid manifest config {}: {reason}",
54                String::from_utf8_lossy(path)
55            ),
56        }
57    }
58}
59
60impl std::error::Error for ManifestJoinError {}
61
62/// Reads immutable payloads by the full key recorded in a manifest entry.
63///
64/// Implementations may be backed by the family blob store, but this interface
65/// intentionally exposes no checkout path or directory operation to the join.
66pub trait ManifestBlobReader {
67    fn read_callgraph_blob(&self, full_key: &str) -> Result<Option<Vec<u8>>, ManifestJoinError>;
68
69    fn read_callgraph_blob_decoded(
70        &self,
71        full_key: &str,
72    ) -> Result<Option<Arc<CallgraphBlob>>, ManifestJoinError> {
73        self.read_callgraph_blob(full_key)?
74            .map(|payload| CallgraphBlob::from_bytes(&payload).map(Arc::new))
75            .transpose()
76    }
77}
78
79/// Tree-sitter node position in canonical pre-order traversal order.
80#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
81pub struct AstPreorderNode {
82    pub ordinal: u32,
83    pub kind: String,
84    pub byte_start: usize,
85    pub byte_end: usize,
86}
87
88/// A symbol captured by extraction.  Its ordinal is the source AST node's
89/// pre-order position, not a per-path or database-generated identifier.
90#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
91pub struct BlobSymbol {
92    pub ordinal: u32,
93    pub name: String,
94    pub scoped_name: String,
95    pub kind: String,
96    pub exported: bool,
97    pub is_default_export: bool,
98    pub start_line: u32,
99    pub start_col: u32,
100    pub end_line: u32,
101    pub end_col: u32,
102    pub signature: Option<String>,
103}
104
105/// The parse-level class of a reference.  No target path is present in a blob.
106#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
107#[serde(rename_all = "snake_case")]
108pub enum BlobRefKind {
109    Call,
110    ValueRef,
111    Import,
112    Module,
113    Reexport,
114    ExportAlias,
115}
116
117/// An unresolved reference extracted from one source blob.
118#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub struct BlobRef {
120    /// Canonical tree-sitter AST pre-order position of this reference.
121    pub ordinal: u32,
122    pub kind: BlobRefKind,
123    pub caller_symbol: Option<String>,
124    pub short_name: Option<String>,
125    pub full_ref: Option<String>,
126    pub module_path: Option<String>,
127    pub line: u32,
128    pub byte_start: usize,
129    pub byte_end: usize,
130    pub path_override: Option<String>,
131    pub local_name: Option<String>,
132    pub requested_name: Option<String>,
133    pub namespace_alias: Option<String>,
134    pub wildcard: bool,
135    pub import_kind: Option<String>,
136}
137
138/// A parsed import retained in the blob so binding can resolve aliases without
139/// re-reading source text.
140#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
141pub struct BlobImport {
142    pub ordinal: u32,
143    pub module_path: String,
144    pub names: Vec<String>,
145    pub default_import: Option<String>,
146    pub namespace_import: Option<String>,
147    pub byte_start: usize,
148    pub byte_end: usize,
149    pub raw_text: String,
150    pub type_only: bool,
151    pub side_effect: bool,
152}
153
154/// Path-free parse output stored for a regular source file.
155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub struct ParseBlob {
157    pub extractor_version: String,
158    pub language: String,
159    pub ast_nodes: Vec<AstPreorderNode>,
160    pub symbols: Vec<BlobSymbol>,
161    pub default_export_symbol: Option<String>,
162    pub exported_symbols: Vec<String>,
163    pub callable_symbols: Vec<String>,
164    pub imports: Vec<BlobImport>,
165    pub refs: Vec<BlobRef>,
166}
167
168/// Raw source is retained only for configuration files and ignore-list members.
169/// These blobs are parsed during joining because their interpretation depends on
170/// the manifest view they configure.
171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172pub struct ConfigBlob {
173    pub extractor_version: String,
174    pub language: String,
175    pub source: Vec<u8>,
176}
177
178/// The immutable callgraph blob payload.  A regular source blob has parse output
179/// only; a configuration blob is intentionally raw so its manifest-scoped
180/// resolver settings can be interpreted during assembly.
181#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
182#[serde(tag = "kind", rename_all = "snake_case")]
183pub enum CallgraphBlob {
184    Parse(ParseBlob),
185    Config(ConfigBlob),
186}
187
188impl CallgraphBlob {
189    /// Extracts path-free callgraph parse output from source bytes and the
190    /// extractor version that names the corresponding content key.
191    pub fn extract(
192        source: &str,
193        language: &str,
194        extractor_version: impl Into<String>,
195    ) -> Result<Self, ManifestJoinError> {
196        let lang = language_id(language)
197            .ok_or_else(|| ManifestJoinError::UnsupportedLanguage(language.to_string()))?;
198        let extractor_version = extractor_version.into();
199        let ast_nodes = ast_preorder_nodes(source, lang)?;
200        let mut data = callgraph::build_file_data_from_source_with_lang(
201            std::path::Path::new("__callgraph_blob__"),
202            source,
203            lang,
204        )
205        .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
206        if lang == LangId::Rust {
207            super::extend_rust_imports_with_nested_uses(source, &mut data);
208        }
209        let symbols = blob_symbols(source, &data, &ast_nodes);
210        let imports = blob_imports(&data, &ast_nodes);
211        let mut refs = blob_refs(source, &data, &ast_nodes);
212        refs.extend(rust_module_refs(source, lang, &ast_nodes));
213        let empty =
214            Manifest::new([]).map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
215        let reader = |_: &BlobKey| None;
216        let empty_facts = ManifestFacts {
217            manifest: &empty,
218            blobs: &reader,
219        };
220        let paths = FactPaths {
221            root: Path::new("/"),
222            facts: &empty_facts,
223        };
224        let file = Path::new("/__callgraph_blob__");
225        let mut structural =
226            super::collect_reexport_refs(paths.root, file, "__callgraph_blob__", source, &paths)
227                .raw_refs;
228        structural.extend(
229            super::collect_source_less_export_alias_refs("__callgraph_blob__", source).raw_refs,
230        );
231        if lang == LangId::Rust {
232            structural.extend(
233                super::collect_rust_pub_use_reexport_refs(
234                    paths.root,
235                    file,
236                    "__callgraph_blob__",
237                    &data.import_block.imports,
238                    &super::LineIndex::new(source),
239                    &paths,
240                )
241                .raw_refs,
242            );
243        }
244        refs.extend(
245            structural
246                .into_iter()
247                .map(|raw| structural_ref(raw, &ast_nodes)),
248        );
249        let mut exported_symbols = data.exported_symbols.clone();
250        exported_symbols.sort();
251        let mut callable_symbols = data.calls_by_symbol.keys().cloned().collect::<Vec<_>>();
252        callable_symbols.sort();
253        refs.sort_by(|left, right| {
254            (
255                left.ordinal,
256                left.kind,
257                left.byte_start,
258                left.byte_end,
259                left.full_ref.as_deref(),
260            )
261                .cmp(&(
262                    right.ordinal,
263                    right.kind,
264                    right.byte_start,
265                    right.byte_end,
266                    right.full_ref.as_deref(),
267                ))
268        });
269        refs.dedup_by(|left, right| {
270            left.ordinal == right.ordinal
271                && left.kind == right.kind
272                && left.byte_start == right.byte_start
273                && left.byte_end == right.byte_end
274                && left.full_ref == right.full_ref
275        });
276
277        Ok(Self::Parse(ParseBlob {
278            extractor_version,
279            language: language.to_string(),
280            ast_nodes,
281            symbols,
282            default_export_symbol: data.default_export_symbol,
283            exported_symbols,
284            callable_symbols,
285            imports,
286            refs,
287        }))
288    }
289
290    /// Builds a manifest configuration input.  The caller must key it with
291    /// `language = "config"` and the same extractor version stored here.
292    pub fn config(source: impl Into<Vec<u8>>, extractor_version: impl Into<String>) -> Self {
293        Self::Config(ConfigBlob {
294            extractor_version: extractor_version.into(),
295            language: "config".to_string(),
296            source: source.into(),
297        })
298    }
299
300    /// Uses one canonical JSON encoding for the immutable payload bytes.
301    pub fn to_bytes(&self) -> Result<Vec<u8>, ManifestJoinError> {
302        serde_json::to_vec(self).map_err(|error| ManifestJoinError::InvalidBlob(error.to_string()))
303    }
304
305    /// Decodes a payload after the blob store has verified its digest and schema.
306    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestJoinError> {
307        serde_json::from_slice(bytes)
308            .map_err(|error| ManifestJoinError::InvalidBlob(error.to_string()))
309    }
310
311    pub fn parse(&self) -> Option<&ParseBlob> {
312        match self {
313            Self::Parse(blob) => Some(blob),
314            Self::Config(_) => None,
315        }
316    }
317
318    pub fn config_source(&self) -> Option<&ConfigBlob> {
319        match self {
320            Self::Parse(_) => None,
321            Self::Config(blob) => Some(blob),
322        }
323    }
324}
325
326/// The stable identity of one bound blob reference.  The path breaks ties when
327/// identical content is bound at more than one manifest path.
328#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
329pub struct CallerRefKey {
330    pub caller_blob_key: String,
331    pub ref_ordinal: u32,
332    pub caller_path: Vec<u8>,
333}
334
335/// The manifest-derived resolution state for one reference.
336#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
337pub enum ResolutionStatus {
338    Resolved,
339    Unresolved,
340}
341
342/// A logical derived row.  It is intentionally independent of SQLite rowids and
343/// other physical database details.
344#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
345pub struct DerivedRow {
346    pub caller_blob_key: String,
347    pub ref_ordinal: u32,
348    pub caller_path: Vec<u8>,
349    pub kind: BlobRefKind,
350    pub status: ResolutionStatus,
351    pub target_path: Option<Vec<u8>>,
352    pub target_symbol: Option<String>,
353}
354
355impl DerivedRow {
356    pub fn ref_key(&self) -> CallerRefKey {
357        CallerRefKey {
358            caller_blob_key: self.caller_blob_key.clone(),
359            ref_ordinal: self.ref_ordinal,
360            caller_path: self.caller_path.clone(),
361        }
362    }
363}
364
365/// The result of resolving one manifest.  `resolution_order` exposes the exact
366/// canonical order consumed by the resolver for deterministic test coverage.
367#[derive(Clone, Debug, Eq, PartialEq)]
368pub struct JoinResult {
369    pub rows: BTreeSet<DerivedRow>,
370    pub resolution_order: Vec<CallerRefKey>,
371    pub unbound_non_utf8_paths: Vec<Vec<u8>>,
372}
373
374impl JoinResult {
375    /// Serializes logical rows in canonical order.  This is the comparison form
376    /// for equal manifests; callers must not compare SQLite file bytes.
377    pub fn canonical_serialization(&self) -> Vec<u8> {
378        let mut output = Vec::new();
379        for row in &self.rows {
380            append_field(&mut output, row.caller_blob_key.as_bytes());
381            append_field(&mut output, &row.ref_ordinal.to_be_bytes());
382            append_field(&mut output, &row.caller_path);
383            append_field(&mut output, &[row.kind as u8]);
384            append_field(
385                &mut output,
386                &[match row.status {
387                    ResolutionStatus::Resolved => 1,
388                    ResolutionStatus::Unresolved => 0,
389                }],
390            );
391            append_optional_field(&mut output, row.target_path.as_deref());
392            append_optional_field(&mut output, row.target_symbol.as_deref().map(str::as_bytes));
393        }
394        output
395    }
396}
397
398/// Incremental assembly details used to verify precise invalidation behavior.
399#[derive(Clone, Debug, Eq, PartialEq)]
400pub struct IncrementalJoinResult {
401    pub result: JoinResult,
402    pub re_resolved: BTreeSet<CallerRefKey>,
403    pub full_re_resolve: bool,
404}
405
406fn changed_manifest_paths(previous: &Manifest, current: &Manifest) -> BTreeSet<Vec<u8>> {
407    let previous_entries = previous
408        .entries()
409        .map(|(path, entry)| (path.as_bytes().to_vec(), entry))
410        .collect::<BTreeMap<_, _>>();
411    let current_entries = current
412        .entries()
413        .map(|(path, entry)| (path.as_bytes().to_vec(), entry))
414        .collect::<BTreeMap<_, _>>();
415    previous_entries
416        .keys()
417        .chain(current_entries.keys())
418        .collect::<BTreeSet<_>>()
419        .into_iter()
420        .filter(|path| previous_entries.get(*path) != current_entries.get(*path))
421        .cloned()
422        .collect()
423}
424
425fn manifest_resolution_input(manifest: &Manifest, path: &[u8]) -> bool {
426    let lookup = if path.first() == Some(&0) {
427        return manifest
428            .entries()
429            .find(|(candidate, _)| candidate.as_bytes() == path)
430            .is_some_and(|(_, entry)| matches!(entry, ManifestEntry::Synthetic { .. }));
431    } else {
432        RelPath::new(path.to_vec()).ok()
433    };
434    lookup
435        .as_ref()
436        .and_then(|path| manifest.get(path))
437        .is_some_and(|entry| {
438            matches!(
439                entry,
440                ManifestEntry::Regular {
441                    resolution_input: true,
442                    ..
443                }
444            )
445        })
446}
447
448fn append_field(output: &mut Vec<u8>, value: &[u8]) {
449    output.extend_from_slice(&(value.len() as u64).to_be_bytes());
450    output.extend_from_slice(value);
451}
452
453fn append_optional_field(output: &mut Vec<u8>, value: Option<&[u8]>) {
454    match value {
455        Some(value) => {
456            output.push(1);
457            append_field(output, value);
458        }
459        None => output.push(0),
460    }
461}
462
463fn language_id(language: &str) -> Option<LangId> {
464    Some(match language {
465        "typescript" => LangId::TypeScript,
466        "tsx" => LangId::Tsx,
467        "javascript" => LangId::JavaScript,
468        "python" => LangId::Python,
469        "rust" => LangId::Rust,
470        "go" => LangId::Go,
471        "c" => LangId::C,
472        "cpp" => LangId::Cpp,
473        "cuda" => LangId::Cuda,
474        "metal" => LangId::Metal,
475        "zig" => LangId::Zig,
476        "csharp" => LangId::CSharp,
477        "bash" => LangId::Bash,
478        "html" => LangId::Html,
479        "markdown" => LangId::Markdown,
480        "solidity" => LangId::Solidity,
481        "scss" => LangId::Scss,
482        "vue" => LangId::Vue,
483        "json" => LangId::Json,
484        "scala" => LangId::Scala,
485        "java" => LangId::Java,
486        "ruby" => LangId::Ruby,
487        "kotlin" => LangId::Kotlin,
488        "swift" => LangId::Swift,
489        "php" => LangId::Php,
490        "lua" => LangId::Lua,
491        "perl" => LangId::Perl,
492        "yaml" => LangId::Yaml,
493        "pascal" => LangId::Pascal,
494        "r" => LangId::R,
495        "groovy" => LangId::Groovy,
496        "objc" => LangId::ObjC,
497        "toml" => LangId::Toml,
498        _ => return None,
499    })
500}
501
502fn ast_preorder_nodes(
503    source: &str,
504    lang: LangId,
505) -> Result<Vec<AstPreorderNode>, ManifestJoinError> {
506    let mut parser = Parser::new();
507    parser
508        .set_language(&grammar_for(lang))
509        .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
510    let tree = parser
511        .parse(source, None)
512        .ok_or_else(|| ManifestJoinError::Parse("tree-sitter returned no tree".to_string()))?;
513    let mut nodes = Vec::new();
514    let mut stack = vec![tree.root_node()];
515    while let Some(node) = stack.pop() {
516        nodes.push(AstPreorderNode {
517            ordinal: nodes.len() as u32,
518            kind: node.kind().to_string(),
519            byte_start: node.start_byte(),
520            byte_end: node.end_byte(),
521        });
522        let children = node.children(&mut node.walk()).collect::<Vec<_>>();
523        stack.extend(children.into_iter().rev());
524    }
525    Ok(nodes)
526}
527
528fn blob_symbols(
529    source: &str,
530    data: &FileCallData,
531    ast_nodes: &[AstPreorderNode],
532) -> Vec<BlobSymbol> {
533    let mut symbols = data
534        .symbol_metadata
535        .iter()
536        .map(|(scoped_name, meta)| {
537            blob_symbol(
538                source,
539                scoped_name,
540                meta,
541                &data.default_export_symbol,
542                ast_nodes,
543            )
544        })
545        .collect::<Vec<_>>();
546    symbols.sort_by(|left, right| {
547        (
548            left.ordinal,
549            left.start_line,
550            left.start_col,
551            left.scoped_name.as_str(),
552        )
553            .cmp(&(
554                right.ordinal,
555                right.start_line,
556                right.start_col,
557                right.scoped_name.as_str(),
558            ))
559    });
560    symbols
561}
562
563fn blob_symbol(
564    source: &str,
565    scoped_name: &str,
566    meta: &SymbolMeta,
567    default_export: &Option<String>,
568    ast_nodes: &[AstPreorderNode],
569) -> BlobSymbol {
570    let byte_start = byte_offset(source, meta.range.start_line, meta.range.start_col);
571    let byte_end = byte_offset(source, meta.range.end_line, meta.range.end_col).max(byte_start);
572    BlobSymbol {
573        ordinal: ordinal_for_range(ast_nodes, byte_start, byte_end),
574        name: unqualified_symbol_name(scoped_name).to_string(),
575        scoped_name: scoped_name.to_string(),
576        kind: symbol_kind_name(&meta.kind).to_string(),
577        exported: meta.exported,
578        is_default_export: default_export.as_deref() == Some(scoped_name),
579        start_line: meta.range.start_line,
580        start_col: meta.range.start_col,
581        end_line: meta.range.end_line,
582        end_col: meta.range.end_col,
583        signature: meta.signature.clone(),
584    }
585}
586
587fn blob_imports(data: &FileCallData, ast_nodes: &[AstPreorderNode]) -> Vec<BlobImport> {
588    let mut imports = data
589        .import_block
590        .imports
591        .iter()
592        .map(|import| BlobImport {
593            ordinal: ordinal_for_range(ast_nodes, import.byte_range.start, import.byte_range.end),
594            module_path: import.module_path.clone(),
595            names: import.names.clone(),
596            default_import: import.default_import.clone(),
597            namespace_import: import.namespace_import.clone(),
598            byte_start: import.byte_range.start,
599            byte_end: import.byte_range.end,
600            raw_text: import.raw_text.clone(),
601            type_only: import.kind == ImportKind::Type,
602            side_effect: import.kind == ImportKind::SideEffect,
603        })
604        .collect::<Vec<_>>();
605    imports.sort_by(|left, right| {
606        (left.ordinal, left.module_path.as_str()).cmp(&(right.ordinal, right.module_path.as_str()))
607    });
608    imports
609}
610
611fn blob_refs(source: &str, data: &FileCallData, ast_nodes: &[AstPreorderNode]) -> Vec<BlobRef> {
612    let mut refs = Vec::new();
613    for (caller_symbol, calls) in &data.calls_by_symbol {
614        for call in calls {
615            refs.push(call_ref(caller_symbol, call, BlobRefKind::Call, ast_nodes));
616        }
617    }
618    for (caller_symbol, calls) in &data.value_refs_by_symbol {
619        for call in calls {
620            refs.push(call_ref(
621                caller_symbol,
622                call,
623                BlobRefKind::ValueRef,
624                ast_nodes,
625            ));
626        }
627    }
628    for import in &data.import_block.imports {
629        refs.push(BlobRef {
630            ordinal: ordinal_for_range(ast_nodes, import.byte_range.start, import.byte_range.end),
631            kind: BlobRefKind::Import,
632            caller_symbol: None,
633            short_name: None,
634            full_ref: Some(import.module_path.clone()),
635            module_path: Some(import.module_path.clone()),
636            line: line_for_byte(source, import.byte_range.start),
637            byte_start: import.byte_range.start,
638            byte_end: import.byte_range.end,
639            path_override: None,
640            local_name: None,
641            requested_name: None,
642            namespace_alias: import.namespace_import.clone(),
643            wildcard: super::import_is_wildcard(import),
644            import_kind: None,
645        });
646    }
647    refs
648}
649
650fn call_ref(
651    caller_symbol: &str,
652    call: &callgraph::CallSite,
653    kind: BlobRefKind,
654    ast_nodes: &[AstPreorderNode],
655) -> BlobRef {
656    BlobRef {
657        ordinal: ordinal_for_range(ast_nodes, call.byte_start, call.byte_end),
658        kind,
659        caller_symbol: Some(caller_symbol.to_string()),
660        short_name: Some(call.callee_name.clone()),
661        full_ref: Some(call.full_callee.clone()),
662        module_path: None,
663        line: call.line,
664        byte_start: call.byte_start,
665        byte_end: call.byte_end,
666        path_override: None,
667        local_name: None,
668        requested_name: None,
669        namespace_alias: None,
670        wildcard: false,
671        import_kind: None,
672    }
673}
674
675fn rust_module_refs(source: &str, lang: LangId, ast_nodes: &[AstPreorderNode]) -> Vec<BlobRef> {
676    if lang != LangId::Rust {
677        return Vec::new();
678    }
679    let mut parser = Parser::new();
680    if parser.set_language(&grammar_for(lang)).is_err() {
681        return Vec::new();
682    }
683    let Some(tree) = parser.parse(source, None) else {
684        return Vec::new();
685    };
686    let mut refs = Vec::new();
687    let mut stack = vec![tree.root_node()];
688    while let Some(node) = stack.pop() {
689        if node.kind() == "mod_item"
690            && node
691                .named_children(&mut node.walk())
692                .all(|child| child.kind() != "declaration_list")
693        {
694            if let Some(name) = node.child_by_field_name("name") {
695                let module_name = source[name.byte_range()].to_string();
696                refs.push(BlobRef {
697                    ordinal: ordinal_for_range(ast_nodes, node.start_byte(), node.end_byte()),
698                    kind: BlobRefKind::Module,
699                    caller_symbol: None,
700                    short_name: Some(module_name.clone()),
701                    full_ref: Some(module_name.clone()),
702                    module_path: Some(module_name),
703                    line: node.start_position().row as u32 + 1,
704                    byte_start: node.start_byte(),
705                    byte_end: node.end_byte(),
706                    path_override: super::rust_module_path_override(source, node)
707                        .map(str::to_string),
708                    local_name: None,
709                    requested_name: None,
710                    namespace_alias: None,
711                    wildcard: false,
712                    import_kind: None,
713                });
714            }
715        }
716        let children = node.children(&mut node.walk()).collect::<Vec<_>>();
717        stack.extend(children.into_iter().rev());
718    }
719    refs
720}
721
722fn ordinal_for_range(ast_nodes: &[AstPreorderNode], byte_start: usize, byte_end: usize) -> u32 {
723    ast_nodes
724        .iter()
725        .filter(|node| node.byte_start <= byte_start && node.byte_end >= byte_end)
726        .min_by_key(|node| (node.byte_end.saturating_sub(node.byte_start), node.ordinal))
727        .map(|node| node.ordinal)
728        .unwrap_or(0)
729}
730
731fn byte_offset(source: &str, line: u32, column: u32) -> usize {
732    let mut offset = 0usize;
733    for (index, segment) in source.split_inclusive('\n').enumerate() {
734        if index as u32 == line {
735            return offset + (column as usize).min(segment.len());
736        }
737        offset += segment.len();
738    }
739    source.len()
740}
741
742fn line_for_byte(source: &str, byte_start: usize) -> u32 {
743    source[..byte_start.min(source.len())]
744        .bytes()
745        .filter(|byte| *byte == b'\n')
746        .count() as u32
747        + 1
748}
749
750fn symbol_kind_name(kind: &SymbolKind) -> &'static str {
751    match kind {
752        SymbolKind::Function => "function",
753        SymbolKind::Kernel => "kernel",
754        SymbolKind::Class => "class",
755        SymbolKind::Method => "method",
756        SymbolKind::Struct => "struct",
757        SymbolKind::Interface => "interface",
758        SymbolKind::Enum => "enum",
759        SymbolKind::TypeAlias => "type_alias",
760        SymbolKind::Variable => "variable",
761        SymbolKind::Heading => "heading",
762        SymbolKind::FileSummary => "file_summary",
763    }
764}
765
766fn unqualified_symbol_name(scoped_name: &str) -> &str {
767    if scoped_name == TOP_LEVEL_SYMBOL {
768        return scoped_name;
769    }
770    scoped_name.rsplit("::").next().unwrap_or(scoped_name)
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776    #[test]
777    fn blob_ordinals_are_tree_sitter_preorder_positions() {
778        let source = "export function run() { return helper(); }\nfunction helper() {}\n";
779        let blob = CallgraphBlob::extract(source, "typescript", "join-test-v1").unwrap();
780        let repeated = CallgraphBlob::extract(source, "typescript", "join-test-v1").unwrap();
781        let different_version =
782            CallgraphBlob::extract(source, "typescript", "join-test-v2").unwrap();
783        assert_eq!(blob.to_bytes().unwrap(), repeated.to_bytes().unwrap());
784        assert_ne!(
785            blob.to_bytes().unwrap(),
786            different_version.to_bytes().unwrap()
787        );
788        let parse = blob.parse().unwrap();
789        assert_eq!(parse.ast_nodes[0].ordinal, 0);
790        assert!(parse.refs.iter().all(|reference| parse
791            .ast_nodes
792            .iter()
793            .any(|node| node.ordinal == reference.ordinal)));
794        let helper = parse
795            .refs
796            .iter()
797            .find(|reference| reference.short_name.as_deref() == Some("helper"))
798            .unwrap();
799        let node = parse
800            .ast_nodes
801            .iter()
802            .find(|node| node.ordinal == helper.ordinal)
803            .unwrap();
804        assert!(node.byte_start <= helper.byte_start && node.byte_end >= helper.byte_end);
805        assert_eq!(node.kind, "call_expression");
806        let mut parser = Parser::new();
807        parser
808            .set_language(&grammar_for(LangId::TypeScript))
809            .unwrap();
810        let tree = parser.parse(source, None).unwrap();
811        let mut cursor = tree.walk();
812        let mut expected = Vec::new();
813        'preorder: loop {
814            let node = cursor.node();
815            expected.push(AstPreorderNode {
816                ordinal: expected.len() as u32,
817                kind: node.kind().to_string(),
818                byte_start: node.start_byte(),
819                byte_end: node.end_byte(),
820            });
821            if cursor.goto_first_child() {
822                continue;
823            }
824            while !cursor.goto_next_sibling() {
825                if !cursor.goto_parent() {
826                    break 'preorder;
827                }
828            }
829        }
830        assert_eq!(parse.ast_nodes, expected);
831    }
832}
833
834fn structural_ref(raw: super::RawRef, nodes: &[AstPreorderNode]) -> BlobRef {
835    BlobRef {
836        ordinal: ordinal_for_range(nodes, raw.byte_start, raw.byte_end),
837        kind: if raw.kind == "export_alias" {
838            BlobRefKind::ExportAlias
839        } else {
840            BlobRefKind::Reexport
841        },
842        caller_symbol: raw.caller_symbol,
843        short_name: raw.short_name,
844        full_ref: raw.full_ref,
845        module_path: raw.module_path,
846        line: raw.line,
847        byte_start: raw.byte_start,
848        byte_end: raw.byte_end,
849        path_override: None,
850        local_name: raw.local_name,
851        requested_name: raw.requested_name,
852        namespace_alias: raw.namespace_alias,
853        wildcard: raw.wildcard,
854        import_kind: raw.import_kind,
855    }
856}
857
858fn bound_name(name: &str, path: &str) -> String {
859    name.replace(
860        "<default:__callgraph_blob__>",
861        &format!(
862            "<default:{}>",
863            Path::new(path)
864                .file_name()
865                .unwrap_or_default()
866                .to_string_lossy()
867        ),
868    )
869}
870
871impl ParseBlob {
872    fn file_data(&self, path: &str) -> Result<FileCallData, ManifestJoinError> {
873        let lang = language_id(&self.language)
874            .ok_or_else(|| ManifestJoinError::UnsupportedLanguage(self.language.clone()))?;
875        let mut calls_by_symbol: HashMap<String, Vec<callgraph::CallSite>> = HashMap::new();
876        let mut value_refs_by_symbol: HashMap<String, Vec<callgraph::CallSite>> = HashMap::new();
877        for raw in &self.refs {
878            let map = match raw.kind {
879                BlobRefKind::Call => &mut calls_by_symbol,
880                BlobRefKind::ValueRef => &mut value_refs_by_symbol,
881                _ => continue,
882            };
883            let Some(caller) = &raw.caller_symbol else {
884                continue;
885            };
886            map.entry(bound_name(caller, path))
887                .or_default()
888                .push(callgraph::CallSite {
889                    callee_name: raw.short_name.clone().unwrap_or_default(),
890                    full_callee: raw.full_ref.clone().unwrap_or_default(),
891                    line: raw.line,
892                    byte_start: raw.byte_start,
893                    byte_end: raw.byte_end,
894                });
895        }
896        for symbol in &self.callable_symbols {
897            calls_by_symbol.entry(bound_name(symbol, path)).or_default();
898        }
899        let mut symbol_metadata = HashMap::new();
900        for symbol in &self.symbols {
901            let kind = match symbol.kind.as_str() {
902                "function" => SymbolKind::Function,
903                "method" => SymbolKind::Method,
904                "class" => SymbolKind::Class,
905                "struct" => SymbolKind::Struct,
906                "interface" => SymbolKind::Interface,
907                "enum" => SymbolKind::Enum,
908                "type_alias" => SymbolKind::TypeAlias,
909                "heading" => SymbolKind::Heading,
910                "file_summary" => SymbolKind::FileSummary,
911                _ => SymbolKind::Variable,
912            };
913            symbol_metadata.insert(
914                bound_name(&symbol.scoped_name, path),
915                SymbolMeta {
916                    kind,
917                    exported: symbol.exported,
918                    signature: symbol.signature.clone(),
919                    line: symbol.start_line + 1,
920                    range: crate::symbols::Range {
921                        start_line: symbol.start_line,
922                        start_col: symbol.start_col,
923                        end_line: symbol.end_line,
924                        end_col: symbol.end_col,
925                    },
926                    entry_point_attribute: None,
927                },
928            );
929        }
930        let imports = self
931            .imports
932            .iter()
933            .map(|import| ImportStatement {
934                module_path: import.module_path.clone(),
935                names: import.names.clone(),
936                default_import: import.default_import.clone(),
937                namespace_import: import.namespace_import.clone(),
938                kind: if import.type_only {
939                    ImportKind::Type
940                } else if import.side_effect {
941                    ImportKind::SideEffect
942                } else {
943                    ImportKind::Value
944                },
945                group: ImportGroup::Internal,
946                byte_range: import.byte_start..import.byte_end,
947                raw_text: import.raw_text.clone(),
948                form: if lang == LangId::Rust {
949                    ImportForm::RustUse {
950                        visibility: import.default_import.clone(),
951                        named: import.names.clone(),
952                    }
953                } else {
954                    ImportForm::Es {
955                        default_import: import.default_import.clone(),
956                        namespace_import: import.namespace_import.clone(),
957                        named: import.names.clone(),
958                        type_only: import.type_only,
959                        side_effect: import.side_effect,
960                        attribute_clause: None,
961                        attribute_type: None,
962                    }
963                },
964            })
965            .collect::<Vec<_>>();
966        Ok(FileCallData {
967            calls_by_symbol,
968            value_refs_by_symbol,
969            symbol_metadata,
970            exported_symbols: self
971                .exported_symbols
972                .iter()
973                .map(|s| bound_name(s, path))
974                .collect(),
975            default_export_symbol: self
976                .default_export_symbol
977                .as_ref()
978                .map(|s| bound_name(s, path)),
979            import_block: ImportBlock {
980                imports,
981                byte_range: None,
982            },
983            lang,
984        })
985    }
986
987    fn bind(
988        &self,
989        path: &str,
990        facts: &FactPaths<'_>,
991    ) -> Result<super::FileExtract, ManifestJoinError> {
992        self.bind_with_dependencies(path, facts, None)
993    }
994
995    fn bind_with_dependencies(
996        &self,
997        path: &str,
998        facts: &FactPaths<'_>,
999        cached: Option<&BTreeMap<u32, BTreeSet<String>>>,
1000    ) -> Result<super::FileExtract, ManifestJoinError> {
1001        let data = self.file_data(path)?;
1002        let nodes = self
1003            .symbols
1004            .iter()
1005            .map(|symbol| {
1006                let scoped_name = bound_name(&symbol.scoped_name, path);
1007                super::NodeRecord {
1008                    id: format!("{path}:{}:{scoped_name}", symbol.ordinal),
1009                    file_path: path.to_string(),
1010                    name: bound_name(&symbol.name, path),
1011                    scoped_name,
1012                    kind: symbol.kind.clone(),
1013                    range: crate::symbols::Range {
1014                        start_line: symbol.start_line,
1015                        start_col: symbol.start_col,
1016                        end_line: symbol.end_line,
1017                        end_col: symbol.end_col,
1018                    },
1019                    range_ordinal: symbol.ordinal,
1020                    signature: symbol.signature.clone(),
1021                    exported: symbol.exported,
1022                    is_default_export: symbol.is_default_export,
1023                    is_type_like: false,
1024                    is_callgraph_entry_point: false,
1025                }
1026            })
1027            .collect::<Vec<_>>();
1028        // Overloads can share a scoped name. Preserve the first node chosen by
1029        // the source-order scan without rescanning all symbols for every call.
1030        let mut caller_nodes = HashMap::with_capacity(nodes.len());
1031        for node in &nodes {
1032            #[cfg(test)]
1033            CALLER_NODE_LOOKUP_WORK.with(|work| work.set(work.get() + 1));
1034            caller_nodes.entry(&node.scoped_name).or_insert(&node.id);
1035        }
1036        let abs = facts.root.join(path);
1037        let mut raw_refs = Vec::new();
1038        for (position, raw) in self.refs.iter().enumerate() {
1039            let position = u32::try_from(position).expect("reference vector fits u32");
1040            let dependencies =
1041                if let Some(dependencies) = cached.and_then(|cache| cache.get(&position)) {
1042                    dependencies.clone()
1043                } else if raw.kind == BlobRefKind::Module {
1044                    super::rust_external_module_target(
1045                        &abs,
1046                        raw.path_override.as_deref(),
1047                        raw.module_path.as_deref().unwrap_or_default(),
1048                        facts,
1049                    )
1050                    .and_then(|p| facts.canonical(&p))
1051                    .map(|p| super::relative_path(facts.root, &p))
1052                    .into_iter()
1053                    .collect()
1054                } else if let Some(module) = &raw.module_path {
1055                    super::module_dependencies(facts.root, &abs, module, facts)
1056                } else {
1057                    BTreeSet::new()
1058                };
1059            let caller_symbol = raw
1060                .caller_symbol
1061                .as_ref()
1062                .map(|name| bound_name(name, path));
1063            let caller_node = caller_symbol.as_ref().and_then(|name| {
1064                #[cfg(test)]
1065                CALLER_NODE_LOOKUP_WORK.with(|work| work.set(work.get() + 1));
1066                #[cfg(test)]
1067                if SCAN_CALLER_NODES.with(std::cell::Cell::get) {
1068                    return nodes
1069                        .iter()
1070                        .find(|node| {
1071                            CALLER_NODE_LOOKUP_WORK.with(|work| work.set(work.get() + 1));
1072                            &node.scoped_name == name
1073                        })
1074                        .map(|node| node.id.clone());
1075                }
1076                caller_nodes.get(name).map(|id| (*id).clone())
1077            });
1078            raw_refs.push(super::RawRef {
1079                ref_id: format!("{path}:{}:{:?}", raw.ordinal, raw.kind),
1080                caller_node,
1081                caller_symbol,
1082                caller_file: path.to_string(),
1083                kind: match raw.kind {
1084                    BlobRefKind::Call => "call",
1085                    BlobRefKind::ValueRef => "value_ref",
1086                    BlobRefKind::Import => "import",
1087                    BlobRefKind::Module => "module",
1088                    BlobRefKind::Reexport => "reexport",
1089                    BlobRefKind::ExportAlias => "export_alias",
1090                }
1091                .to_string(),
1092                short_name: raw.short_name.clone(),
1093                full_ref: raw.full_ref.clone(),
1094                module_path: raw.module_path.clone(),
1095                import_kind: raw.import_kind.clone(),
1096                local_name: raw.local_name.clone(),
1097                requested_name: raw.requested_name.clone(),
1098                namespace_alias: raw.namespace_alias.clone(),
1099                wildcard: raw.wildcard,
1100                line: raw.line,
1101                byte_start: raw.byte_start,
1102                byte_end: raw.byte_end,
1103                dependencies,
1104            });
1105        }
1106        Ok(super::FileExtract {
1107            rel_path: path.to_string(),
1108            freshness: crate::cache_freshness::FileFreshness {
1109                mtime: std::time::UNIX_EPOCH,
1110                size: 0,
1111                content_hash: crate::cache_freshness::zero_hash(),
1112            },
1113            lang: data.lang,
1114            data,
1115            nodes,
1116            raw_refs,
1117            dispatch_hints: Vec::new(),
1118            surface_fingerprint: String::new(),
1119        })
1120    }
1121}
1122
1123/// A manifest-backed index is the ordinary resolver index with different facts.
1124type ManifestProjectIndex<'a> = super::ProjectIndex<'a>;
1125
1126impl JoinResult {
1127    pub fn from_manifest(
1128        manifest: &Manifest,
1129        blobs: &impl ManifestBlobReader,
1130    ) -> Result<Self, ManifestJoinError> {
1131        let loaded = manifest_payloads(manifest, blobs)?;
1132        let reader = |key: &BlobKey| loaded.get(key).cloned();
1133        let facts = Rc::new(ManifestFacts {
1134            manifest,
1135            blobs: &reader,
1136        });
1137        Self::from_facts(manifest, &loaded, Path::new("/"), facts, None)
1138    }
1139
1140    fn from_facts<'a>(
1141        manifest: &'a Manifest,
1142        loaded: &BTreeMap<String, Arc<[u8]>>,
1143        root: &Path,
1144        facts: Rc<dyn ProjectFacts + 'a>,
1145        selected: Option<&BTreeSet<CallerRefKey>>,
1146    ) -> Result<Self, ManifestJoinError> {
1147        let paths = FactPaths {
1148            root,
1149            facts: facts.as_ref(),
1150        };
1151        let mut extracts = HashMap::new();
1152        let mut work = Vec::new();
1153        let mut unbound_non_utf8_paths = Vec::new();
1154        for (path, entry) in manifest.entries() {
1155            let ManifestEntry::Regular { planes, .. } = entry else {
1156                continue;
1157            };
1158            let Some(key) = &planes.callgraph else {
1159                continue;
1160            };
1161            let bytes = loaded
1162                .get(key)
1163                .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
1164            let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(bytes)? else {
1165                continue;
1166            };
1167            let Ok(rel) = std::str::from_utf8(path.as_bytes()) else {
1168                unbound_non_utf8_paths.push(path.as_bytes().to_vec());
1169                continue;
1170            };
1171            let extract = blob.bind(rel, &paths)?;
1172            for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
1173                let ref_key = CallerRefKey {
1174                    caller_blob_key: key.clone(),
1175                    ref_ordinal: raw.ordinal,
1176                    caller_path: path.as_bytes().to_vec(),
1177                };
1178                if selected.is_none_or(|set| set.contains(&ref_key)) {
1179                    work.push((ref_key, (raw.kind, bound.clone())));
1180                }
1181            }
1182            extracts.insert(rel.to_string(), extract);
1183        }
1184        let files = extracts
1185            .iter()
1186            .map(|(path, extract)| {
1187                (
1188                    path.clone(),
1189                    super::DbFileIndex::from_extract(root, extract, &paths),
1190                )
1191            })
1192            .collect();
1193        let caller_data = extracts
1194            .iter()
1195            .map(|(path, extract)| (path.clone(), &extract.data))
1196            .collect();
1197        let mut index = ManifestProjectIndex::from_parts(
1198            root,
1199            files,
1200            caller_data,
1201            super::WorkspaceCratePrefixCache::default(),
1202            facts,
1203        );
1204        index.unbound_non_utf8_paths = unbound_non_utf8_paths;
1205        if !index.unbound_non_utf8_paths.is_empty() {
1206            log::warn!(
1207                "callgraph index left {} non-UTF-8 source paths unbound",
1208                index.unbound_non_utf8_paths.len()
1209            );
1210        }
1211        let mut result = Self {
1212            rows: BTreeSet::new(),
1213            resolution_order: Vec::new(),
1214            unbound_non_utf8_paths: index.unbound_non_utf8_paths.clone(),
1215        };
1216        work.sort_by(|a, b| (&a.0, a.1 .0).cmp(&(&b.0, b.1 .0)));
1217        for (key, (kind, raw)) in work {
1218            let resolved = super::resolve_ref(raw, &index)
1219                .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
1220            result.rows.insert(DerivedRow {
1221                caller_blob_key: key.caller_blob_key.clone(),
1222                ref_ordinal: key.ref_ordinal,
1223                caller_path: key.caller_path.clone(),
1224                kind,
1225                status: if resolved.target_file.is_some() {
1226                    ResolutionStatus::Resolved
1227                } else {
1228                    ResolutionStatus::Unresolved
1229                },
1230                target_path: resolved.target_file.map(String::into_bytes),
1231                target_symbol: resolved.target_symbol,
1232            });
1233            result.resolution_order.push(key);
1234        }
1235        Ok(result)
1236    }
1237
1238    pub fn update(
1239        &self,
1240        previous_manifest: &Manifest,
1241        manifest: &Manifest,
1242        blobs: &impl ManifestBlobReader,
1243    ) -> Result<IncrementalJoinResult, ManifestJoinError> {
1244        let changed = changed_manifest_paths(previous_manifest, manifest);
1245        let full_re_resolve = changed.iter().any(|path| {
1246            manifest_resolution_input(previous_manifest, path)
1247                || manifest_resolution_input(manifest, path)
1248        });
1249        let loaded = manifest_payloads(manifest, blobs)?;
1250        let reader = |key: &BlobKey| loaded.get(key).cloned();
1251        let mut current_keys = BTreeSet::new();
1252        for (path, entry) in manifest.entries() {
1253            if std::str::from_utf8(path.as_bytes()).is_err() {
1254                continue;
1255            }
1256            let ManifestEntry::Regular { planes, .. } = entry else {
1257                continue;
1258            };
1259            let Some(key) = &planes.callgraph else {
1260                continue;
1261            };
1262            if let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(&loaded[key])? {
1263                current_keys.extend(blob.refs.iter().map(|raw| CallerRefKey {
1264                    caller_blob_key: key.clone(),
1265                    ref_ordinal: raw.ordinal,
1266                    caller_path: path.as_bytes().to_vec(),
1267                }));
1268            }
1269        }
1270        let previous_rows = self
1271            .rows
1272            .iter()
1273            .map(|row| (row.ref_key(), row))
1274            .collect::<BTreeMap<_, _>>();
1275        let selected = current_keys
1276            .iter()
1277            .filter(|key| {
1278                full_re_resolve
1279                    || changed.contains(&key.caller_path)
1280                    || previous_rows.get(*key).is_none_or(|row| {
1281                        row.target_path
1282                            .as_ref()
1283                            .is_some_and(|path| changed.contains(path))
1284                    })
1285            })
1286            .cloned()
1287            .collect::<BTreeSet<_>>();
1288        let facts = Rc::new(ManifestFacts {
1289            manifest,
1290            blobs: &reader,
1291        });
1292        let mut result =
1293            Self::from_facts(manifest, &loaded, Path::new("/"), facts, Some(&selected))?;
1294        result.rows.extend(
1295            self.rows
1296                .iter()
1297                .filter(|row| {
1298                    current_keys.contains(&row.ref_key()) && !selected.contains(&row.ref_key())
1299                })
1300                .cloned(),
1301        );
1302        result.resolution_order = result.rows.iter().map(DerivedRow::ref_key).collect();
1303        result.resolution_order.sort();
1304        Ok(IncrementalJoinResult {
1305            result,
1306            re_resolved: selected,
1307            full_re_resolve,
1308        })
1309    }
1310}
1311
1312fn manifest_payloads(
1313    manifest: &Manifest,
1314    blobs: &impl ManifestBlobReader,
1315) -> Result<BTreeMap<String, Arc<[u8]>>, ManifestJoinError> {
1316    let mut loaded = BTreeMap::new();
1317    for (_, entry) in manifest.entries() {
1318        let ManifestEntry::Regular { planes, .. } = entry else {
1319            continue;
1320        };
1321        let Some(key) = &planes.callgraph else {
1322            continue;
1323        };
1324        if !loaded.contains_key(key) {
1325            let bytes = blobs
1326                .read_callgraph_blob(key)?
1327                .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
1328            loaded.insert(key.clone(), Arc::from(bytes));
1329        }
1330    }
1331    Ok(loaded)
1332}
1333
1334#[cfg(test)]
1335#[path = "../../tests/integration/join_manifest_test.rs"]
1336mod manifest_integration_tests;
1337
1338/// Bound reference dependencies and resolver probes are generation-specific, unlike
1339/// parse blobs. The owner must invalidate these whenever a probed path changes.
1340#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1341pub(crate) struct ViewBindingDependencies {
1342    // AST ordinals can collide for structural references. Vector positions are
1343    // unique and stable within the immutable caller blob used to validate reuse.
1344    pub references: BTreeMap<u32, BTreeSet<String>>,
1345    pub dependencies: BTreeSet<String>,
1346    #[serde(default)]
1347    pub consulted_facts: BTreeSet<(String, String)>,
1348    #[serde(default)]
1349    pub unattributed: bool,
1350    #[serde(default)]
1351    binding_facts: ConfigConsultations,
1352    #[serde(default)]
1353    resolution_facts: ConfigConsultations,
1354    binding_probes: BTreeSet<String>,
1355    resolved_dependencies: BTreeSet<String>,
1356    surface_queries: Vec<(ViewSurfaceQuery, String)>,
1357    #[serde(skip)]
1358    surface: Option<ViewFileSurface>,
1359}
1360
1361impl ViewBindingDependencies {
1362    pub(crate) fn from_surface_json(payload: &str) -> Result<Self, serde_json::Error> {
1363        Ok(Self {
1364            surface: Some(serde_json::from_str(payload)?),
1365            ..Self::default()
1366        })
1367    }
1368
1369    pub(crate) fn copy_surface_from(&mut self, source: &Self) {
1370        self.surface.clone_from(&source.surface);
1371    }
1372
1373    pub(crate) fn surface_json(&self) -> Result<Option<String>, serde_json::Error> {
1374        self.surface.as_ref().map(serde_json::to_string).transpose()
1375    }
1376}
1377
1378/// Inputs that determine a reference's target, excluding call-site identity.
1379/// Rust qualified imports are visible only after their declaration; the count
1380/// of visible imports identifies that monotone prefix even across nested uses.
1381#[derive(Hash, PartialEq, Eq)]
1382struct ViewResolutionBinding {
1383    kind: String,
1384    full_ref: Option<String>,
1385    short_name: Option<String>,
1386    visible_rust_imports: usize,
1387}
1388
1389impl ViewResolutionBinding {
1390    fn new(raw: &super::RawRef, caller: &FileCallData) -> Self {
1391        Self {
1392            kind: raw.kind.clone(),
1393            full_ref: raw.full_ref.clone(),
1394            short_name: raw.short_name.clone(),
1395            visible_rust_imports: if caller.lang == LangId::Rust {
1396                caller
1397                    .import_block
1398                    .imports
1399                    .partition_point(|import| import.byte_range.start <= raw.byte_start)
1400            } else {
1401                0
1402            },
1403        }
1404    }
1405}
1406
1407pub(crate) struct SelectedManifestJoin {
1408    pub result: JoinResult,
1409    pub bindings: BTreeMap<String, ViewBindingDependencies>,
1410    pub resolved_callers: BTreeSet<String>,
1411    pub rebuilt_surface_entries: usize,
1412    pub rebuilt_surface_paths: BTreeSet<String>,
1413    pub decoded_caller_blobs: usize,
1414    pub resolved_bindings: usize,
1415}
1416
1417/// Compact, deterministic snapshot of one file's resolver index. It deliberately
1418/// excludes source, AST nodes, and call sites: only callers actually resolved need
1419/// those payloads. Membership probes invalidate module/reexport targets along with
1420/// bindings; source changes invalidate the snapshot through the manifest diff.
1421#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1422struct ViewFileSurface {
1423    language: String,
1424    exports: BTreeSet<String>,
1425    default_export: Option<String>,
1426    export_aliases: BTreeMap<String, String>,
1427    node_by_scoped: BTreeMap<String, String>,
1428    node_by_bare: BTreeMap<String, String>,
1429    node_kind_by_id: BTreeMap<String, String>,
1430    module_targets: BTreeMap<String, Option<String>>,
1431    declared_module_targets: BTreeMap<String, Option<String>>,
1432    reexports: Vec<(Option<String>, BTreeMap<String, String>, bool)>,
1433}
1434
1435impl ViewFileSurface {
1436    fn capture(language: &str, index: &super::DbFileIndex) -> Self {
1437        Self {
1438            language: language.into(),
1439            exports: index.exports.iter().cloned().collect(),
1440            default_export: index.default_export.clone(),
1441            export_aliases: index.export_aliases.clone().into_iter().collect(),
1442            node_by_scoped: index.node_by_scoped.clone().into_iter().collect(),
1443            node_by_bare: index.node_by_bare.clone().into_iter().collect(),
1444            node_kind_by_id: index.node_kind_by_id.clone().into_iter().collect(),
1445            module_targets: index.module_targets.clone().into_iter().collect(),
1446            declared_module_targets: index.declared_module_targets.clone().into_iter().collect(),
1447            reexports: index
1448                .reexports
1449                .iter()
1450                .map(|r| {
1451                    (
1452                        r.target_file.clone(),
1453                        r.named.clone().into_iter().collect(),
1454                        r.wildcard,
1455                    )
1456                })
1457                .collect(),
1458        }
1459    }
1460
1461    fn restore(&self) -> super::DbFileIndex {
1462        super::DbFileIndex {
1463            lang: language_id(&self.language),
1464            exports: self.exports.iter().cloned().collect(),
1465            default_export: self.default_export.clone(),
1466            export_aliases: self.export_aliases.clone().into_iter().collect(),
1467            node_by_scoped: self.node_by_scoped.clone().into_iter().collect(),
1468            node_by_bare: self.node_by_bare.clone().into_iter().collect(),
1469            node_kind_by_id: self.node_kind_by_id.clone().into_iter().collect(),
1470            module_targets: self.module_targets.clone().into_iter().collect(),
1471            declared_module_targets: self.declared_module_targets.clone().into_iter().collect(),
1472            reexports: self
1473                .reexports
1474                .iter()
1475                .map(|(target_file, named, wildcard)| super::ReexportIndex {
1476                    target_file: target_file.clone(),
1477                    named: named.clone().into_iter().collect(),
1478                    wildcard: *wildcard,
1479                })
1480                .collect(),
1481        }
1482    }
1483}
1484
1485/// Configuration files read by the manifest resolver's workspace/package and
1486/// tsconfig lookup (callgraph.rs), and Rust crate lookup (callgraph_store).
1487/// Directory discovery uses a compact membership domain for these names. Their
1488/// content changes invalidate only callers that consulted changed fields.
1489pub(crate) fn view_resolution_config(path: &[u8]) -> bool {
1490    matches!(
1491        path.rsplit(|byte| *byte == b'/').next(),
1492        Some(b"package.json" | b"tsconfig.json" | b"pnpm-workspace.yaml" | b"Cargo.toml")
1493    )
1494}
1495
1496/// Field identities are generation-owned. Raw-read paths are transient validation
1497/// evidence, cleared after classifying a caller so memo-hit timing is not persisted.
1498#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1499struct ConfigConsultations {
1500    facts: BTreeSet<(String, String)>,
1501    reads: BTreeSet<String>,
1502    unattributed: bool,
1503}
1504impl ConfigConsultations {
1505    fn extend(&mut self, other: &Self) {
1506        self.facts.extend(other.facts.iter().cloned());
1507        self.reads.extend(other.reads.iter().cloned());
1508        self.unattributed |= other.unattributed;
1509    }
1510    fn unattributed(&self) -> bool {
1511        self.unattributed
1512            || self
1513                .reads
1514                .iter()
1515                .any(|path| !self.facts.iter().any(|(input, _)| input == path))
1516    }
1517}
1518
1519#[derive(Clone, Default)]
1520struct ConsultationTrace {
1521    config: ConfigConsultations,
1522    probes: BTreeSet<String>,
1523}
1524type ConsultationMemoKey = (std::path::PathBuf, String, String);
1525
1526/// Memo answers and their provenance belong to one immutable manifest join.
1527/// Replaying provenance on hits keeps missing candidates and facts attributable.
1528struct ViewBindingFacts<'a> {
1529    inner: Rc<dyn ProjectFacts + 'a>,
1530    probes: std::cell::RefCell<BTreeSet<String>>,
1531    consultations: std::cell::RefCell<ConfigConsultations>,
1532    memo_stack: std::cell::RefCell<Vec<(ConsultationMemoKey, ConsultationTrace)>>,
1533    memo_traces: std::cell::RefCell<BTreeMap<ConsultationMemoKey, ConsultationTrace>>,
1534    workspace_packages:
1535        std::cell::RefCell<BTreeMap<(std::path::PathBuf, String), Option<std::path::PathBuf>>>,
1536    workspace_members:
1537        std::cell::RefCell<BTreeMap<std::path::PathBuf, Arc<Vec<std::path::PathBuf>>>>,
1538    canonical_cache: std::cell::RefCell<HashMap<Vec<u8>, Option<Vec<u8>>>>,
1539    file_cache: std::cell::RefCell<HashMap<Vec<u8>, bool>>,
1540    config_cache: std::cell::RefCell<HashMap<Vec<u8>, Option<Arc<[u8]>>>>,
1541    directory_cache: std::cell::RefCell<HashMap<Vec<u8>, Vec<super::facts::DirEntry>>>,
1542}
1543
1544impl<'a> ViewBindingFacts<'a> {
1545    fn new(inner: Rc<dyn ProjectFacts + 'a>) -> Self {
1546        Self {
1547            inner,
1548            probes: Default::default(),
1549            consultations: Default::default(),
1550            memo_stack: Default::default(),
1551            memo_traces: Default::default(),
1552            workspace_packages: Default::default(),
1553            workspace_members: Default::default(),
1554            canonical_cache: Default::default(),
1555            file_cache: Default::default(),
1556            config_cache: Default::default(),
1557            directory_cache: Default::default(),
1558        }
1559    }
1560    fn file_fact(&self, rel: &[u8]) -> bool {
1561        if let Some(value) = self.file_cache.borrow().get(rel) {
1562            return *value;
1563        }
1564        let value = self.inner.is_file(rel);
1565        self.file_cache.borrow_mut().insert(rel.to_vec(), value);
1566        value
1567    }
1568
1569    fn record(&self, path: &[u8]) {
1570        // Directory discovery may probe thousands of missing package manifests.
1571        // One membership domain rechecks those bindings on config add/remove;
1572        // individual config content changes use the field consultations instead.
1573        if view_resolution_config(path) {
1574            self.record(VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes());
1575            return;
1576        }
1577        if let Ok(path) = std::str::from_utf8(path) {
1578            let mut parts = Vec::new();
1579            for part in path.split('/') {
1580                match part {
1581                    "" | "." => {}
1582                    ".." => {
1583                        parts.pop();
1584                    }
1585                    _ => parts.push(part),
1586                }
1587            }
1588            let path = parts.join("/");
1589            self.probes.borrow_mut().insert(path.clone());
1590            for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
1591                trace.probes.insert(path.clone());
1592            }
1593        }
1594    }
1595
1596    fn take_config(&self) -> ConfigConsultations {
1597        let mut result = std::mem::take(&mut *self.consultations.borrow_mut());
1598        result.unattributed = result.unattributed();
1599        result.reads.clear();
1600        result
1601    }
1602
1603    fn config_event(&self, path: &[u8], name: Option<&str>) {
1604        let Ok(path) = std::str::from_utf8(path) else {
1605            self.consultations.borrow_mut().unattributed = true;
1606            return;
1607        };
1608        let mut event = ConfigConsultations::default();
1609        if let Some(name) = name {
1610            event.facts.insert((path.into(), name.into()));
1611        } else {
1612            event.reads.insert(path.into());
1613        }
1614        self.consultations.borrow_mut().extend(&event);
1615        for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
1616            trace.config.extend(&event);
1617        }
1618    }
1619
1620    fn take(&self) -> BTreeSet<String> {
1621        std::mem::take(&mut *self.probes.borrow_mut())
1622    }
1623}
1624
1625impl ProjectFacts for ViewBindingFacts<'_> {
1626    fn records_config_facts(&self) -> bool {
1627        true
1628    }
1629    fn config_fact(&self, rel: &[u8], name: &str) {
1630        self.record(rel);
1631        if matches!(name, "workspaces" | "packages") {
1632            self.record(VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes());
1633        }
1634        self.config_event(rel, Some(name));
1635    }
1636    fn memo_start(&self, path: &Path, kind: &str, name: &str) {
1637        self.memo_stack.borrow_mut().push((
1638            (path.into(), kind.into(), name.into()),
1639            ConsultationTrace::default(),
1640        ));
1641    }
1642    fn memo_finish(&self, path: &Path, kind: &str, name: &str) {
1643        let (key, trace) = self
1644            .memo_stack
1645            .borrow_mut()
1646            .pop()
1647            .expect("balanced resolver memo recording");
1648        debug_assert_eq!(key, (path.into(), kind.into(), name.into()));
1649        self.memo_traces.borrow_mut().insert(key, trace);
1650    }
1651    fn memo_replay(&self, path: &Path, kind: &str, name: &str) {
1652        let traces = self.memo_traces.borrow();
1653        let Some(trace) = traces.get(&(path.into(), kind.into(), name.into())) else {
1654            self.consultations.borrow_mut().unattributed = true;
1655            return;
1656        };
1657        self.consultations.borrow_mut().extend(&trace.config);
1658        self.probes
1659            .borrow_mut()
1660            .extend(trace.probes.iter().cloned());
1661        for (_, parent) in self.memo_stack.borrow_mut().iter_mut() {
1662            parent.config.extend(&trace.config);
1663            parent.probes.extend(trace.probes.iter().cloned());
1664        }
1665    }
1666    fn workspace_package(&self, root: &Path, name: &str) -> Option<Option<std::path::PathBuf>> {
1667        self.workspace_packages
1668            .borrow()
1669            .get(&(root.into(), name.into()))
1670            .cloned()
1671    }
1672    fn remember_workspace_package(
1673        &self,
1674        root: &Path,
1675        name: &str,
1676        value: Option<std::path::PathBuf>,
1677    ) {
1678        self.workspace_packages
1679            .borrow_mut()
1680            .insert((root.into(), name.into()), value);
1681    }
1682    fn workspace_members(&self, root: &Path) -> Option<Arc<Vec<std::path::PathBuf>>> {
1683        self.workspace_members.borrow().get(root).cloned()
1684    }
1685    fn remember_workspace_members(&self, root: &Path, value: Arc<Vec<std::path::PathBuf>>) {
1686        self.workspace_members
1687            .borrow_mut()
1688            .insert(root.into(), value);
1689    }
1690    fn is_file(&self, rel: &[u8]) -> bool {
1691        let is_file = self.file_fact(rel);
1692        if is_file {
1693            self.record(rel);
1694        }
1695        is_file
1696    }
1697    fn is_dir(&self, rel: &[u8]) -> bool {
1698        self.inner.is_dir(rel)
1699    }
1700    fn config_bytes(&self, rel: &[u8]) -> Option<Arc<[u8]>> {
1701        self.consultations.borrow_mut().unattributed = true;
1702        for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
1703            trace.config.unattributed = true;
1704        }
1705        self.attributed_config_bytes(rel)
1706    }
1707    fn attributed_config_bytes(&self, rel: &[u8]) -> Option<Arc<[u8]>> {
1708        self.config_event(rel, None);
1709        self.record(rel);
1710        if let Some(value) = self.config_cache.borrow().get(rel) {
1711            return value.clone();
1712        }
1713        let value = self.inner.config_bytes(rel);
1714        self.config_cache
1715            .borrow_mut()
1716            .insert(rel.to_vec(), value.clone());
1717        value
1718    }
1719    fn symlink_target(&self, rel: &[u8]) -> Option<&[u8]> {
1720        self.inner.symlink_target(rel)
1721    }
1722    fn canonical(&self, rel: &[u8]) -> Option<Vec<u8>> {
1723        // FactPaths canonicalizes before testing existence, so misses must be
1724        // recorded here as well as in is_file (not only after canonicalization).
1725        let cached = self.canonical_cache.borrow().get(rel).cloned();
1726        let canonical = cached.unwrap_or_else(|| {
1727            let value = self.inner.canonical(rel);
1728            self.canonical_cache
1729                .borrow_mut()
1730                .insert(rel.to_vec(), value.clone());
1731            value
1732        });
1733        // Existing directory probes are workspace-discovery implementation detail.
1734        // Config add/remove rechecks that discovery through its membership domain;
1735        // source-file probes and misses remain caller-specific dependencies.
1736        if canonical.as_ref().is_none_or(|path| self.file_fact(path)) {
1737            self.record(rel);
1738        }
1739        canonical
1740    }
1741    fn list_dir(&self, rel: &[u8]) -> Vec<super::facts::DirEntry> {
1742        if let Some(value) = self.directory_cache.borrow().get(rel) {
1743            return value.clone();
1744        }
1745        let value = self.inner.list_dir(rel);
1746        self.directory_cache
1747            .borrow_mut()
1748            .insert(rel.to_vec(), value.clone());
1749        value
1750    }
1751}
1752
1753/// Resolve selected callers against the complete symbol index. Cached binding
1754/// dependencies avoid resolving unchanged imports merely to rebuild that index.
1755/// `selected=None` is the cold path; otherwise the owner supplies the transitive
1756/// reverse-dependency closure and caches from the same base generation.
1757#[cfg(test)]
1758pub(crate) fn join_selected_manifest(
1759    manifest: &Manifest,
1760    blobs: &impl ManifestBlobReader,
1761    selected: Option<&BTreeSet<String>>,
1762    cached: &BTreeMap<String, ViewBindingDependencies>,
1763) -> Result<SelectedManifestJoin, ManifestJoinError> {
1764    join_manifest_with_surfaces(manifest, blobs, selected, cached, None)
1765}
1766
1767/// Rebind only changed callers or callers that probed changed membership. Other
1768/// candidates replay their prior consumer-specific surface queries and retain
1769/// their reference rows when every answer is unchanged.
1770pub(crate) fn join_selected_manifest_reusing_surfaces(
1771    manifest: &Manifest,
1772    blobs: &impl ManifestBlobReader,
1773    selected: Option<&BTreeSet<String>>,
1774    cached: &BTreeMap<String, ViewBindingDependencies>,
1775    changed: &BTreeSet<String>,
1776    membership_changed: &BTreeSet<String>,
1777    fact_invalidated: &BTreeSet<String>,
1778) -> Result<SelectedManifestJoin, ManifestJoinError> {
1779    join_manifest_with_surfaces(
1780        manifest,
1781        blobs,
1782        selected,
1783        cached,
1784        Some((changed, membership_changed, fact_invalidated)),
1785    )
1786}
1787
1788fn join_manifest_with_surfaces(
1789    manifest: &Manifest,
1790    blobs: &impl ManifestBlobReader,
1791    selected: Option<&BTreeSet<String>>,
1792    cached: &BTreeMap<String, ViewBindingDependencies>,
1793    reuse: Option<(&BTreeSet<String>, &BTreeSet<String>, &BTreeSet<String>)>,
1794) -> Result<SelectedManifestJoin, ManifestJoinError> {
1795    let mut profile = crate::views::materialization::profile::PhaseTimer::new("join");
1796    let loaded = std::cell::RefCell::new(BTreeMap::<BlobKey, Arc<[u8]>>::new());
1797    let load = |key: &BlobKey| -> Result<Arc<[u8]>, ManifestJoinError> {
1798        if let Some(bytes) = loaded.borrow().get(key) {
1799            return Ok(bytes.clone());
1800        }
1801        let bytes: Arc<[u8]> = blobs
1802            .read_callgraph_blob(key)?
1803            .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?
1804            .into();
1805        loaded.borrow_mut().insert(key.clone(), bytes.clone());
1806        Ok(bytes)
1807    };
1808    let decoded = std::cell::RefCell::new(HashMap::<BlobKey, Arc<CallgraphBlob>>::new());
1809    let decode = |key: &BlobKey| -> Result<Arc<CallgraphBlob>, ManifestJoinError> {
1810        if let Some(blob) = decoded.borrow().get(key) {
1811            return Ok(blob.clone());
1812        }
1813        let blob = blobs
1814            .read_callgraph_blob_decoded(key)?
1815            .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
1816        decoded.borrow_mut().insert(key.clone(), blob.clone());
1817        Ok(blob)
1818    };
1819    let read_error = std::cell::RefCell::new(None);
1820    let reader = |key: &BlobKey| match load(key) {
1821        Ok(bytes) => Some(bytes),
1822        Err(error) => {
1823            *read_error.borrow_mut() = Some(error);
1824            None
1825        }
1826    };
1827    profile.finish("load_payloads");
1828    let facts = Rc::new(ViewBindingFacts::new(Rc::new(ManifestFacts {
1829        manifest,
1830        blobs: &reader,
1831    })));
1832    let root = Path::new("/");
1833    let paths = FactPaths {
1834        root,
1835        facts: facts.as_ref(),
1836    };
1837    let mut extracts = HashMap::new();
1838    let mut files = HashMap::new();
1839    let mut work: BTreeMap<String, Vec<_>> = BTreeMap::new();
1840    let mut bindings = BTreeMap::new();
1841    let mut unbound_non_utf8_paths = Vec::new();
1842    let mut rebuilt_surface_paths = BTreeSet::new();
1843    let mut decoded_caller_blobs = 0;
1844    let mut restored_surfaces = 0;
1845    let mut rebuilt_changed = 0;
1846    let mut rebuilt_facts = 0;
1847    let mut rebuilt_membership = 0;
1848    let mut rebuilt_missing = 0;
1849    for (path, entry) in manifest.entries() {
1850        let ManifestEntry::Regular { planes, .. } = entry else {
1851            continue;
1852        };
1853        let Some(key) = &planes.callgraph else {
1854            continue;
1855        };
1856        let Ok(rel) = std::str::from_utf8(path.as_bytes()) else {
1857            if matches!(decode(key)?.as_ref(), CallgraphBlob::Parse(_)) {
1858                unbound_non_utf8_paths.push(path.as_bytes().to_vec());
1859            }
1860            continue;
1861        };
1862        let resolve = selected.is_none_or(|set| set.contains(rel));
1863        let cache = cached.get(rel).filter(|cache| {
1864            if let Some((changed, membership, fact_invalidated)) = reuse {
1865                selected.is_some()
1866                    && !changed.contains(rel)
1867                    && !fact_invalidated.contains(rel)
1868                    && cache.dependencies.is_disjoint(membership)
1869            } else {
1870                !resolve
1871            }
1872        });
1873        if let Some((cache, surface)) =
1874            cache.and_then(|cache| cache.surface.as_ref().map(|surface| (cache, surface)))
1875        {
1876            restored_surfaces += 1;
1877            files.insert(rel.to_string(), surface.restore());
1878            if resolve {
1879                bindings.insert(rel.to_string(), cache.clone());
1880            }
1881            continue;
1882        }
1883        let decoded_blob = decode(key)?;
1884        let CallgraphBlob::Parse(blob) = decoded_blob.as_ref() else {
1885            continue;
1886        };
1887        decoded_caller_blobs += 1;
1888        facts.take();
1889        facts.take_config();
1890        let extract =
1891            blob.bind_with_dependencies(rel, &paths, cache.map(|cache| &cache.references))?;
1892        let file_index = super::DbFileIndex::from_extract(root, &extract, &paths);
1893        rebuilt_surface_paths.insert(rel.to_string());
1894        match reuse {
1895            Some((changed, _, _)) if changed.contains(rel) => rebuilt_changed += 1,
1896            Some((_, _, invalidated)) if invalidated.contains(rel) => rebuilt_facts += 1,
1897            Some((_, membership, _))
1898                if cached
1899                    .get(rel)
1900                    .is_some_and(|old| !old.dependencies.is_disjoint(membership)) =>
1901            {
1902                rebuilt_membership += 1;
1903            }
1904            _ => rebuilt_missing += 1,
1905        }
1906        let mut binding = cache.cloned().unwrap_or_default();
1907        binding.surface = Some(ViewFileSurface::capture(&blob.language, &file_index));
1908        files.insert(rel.to_string(), file_index);
1909        if cache.is_none() {
1910            binding.references = blob
1911                .refs
1912                .iter()
1913                .zip(&extract.raw_refs)
1914                .enumerate()
1915                .map(|(position, (_, bound))| {
1916                    (
1917                        u32::try_from(position).expect("reference vector fits u32"),
1918                        bound.dependencies.clone(),
1919                    )
1920                })
1921                .collect();
1922            binding.binding_probes = facts.take();
1923            binding.binding_facts = facts.take_config();
1924            binding.dependencies = binding.references.values().flatten().cloned().collect();
1925            binding
1926                .dependencies
1927                .extend(binding.binding_probes.iter().cloned());
1928        }
1929        bindings.insert(rel.to_string(), binding);
1930        if resolve {
1931            let caller_work = work.entry(rel.to_string()).or_default();
1932            for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
1933                caller_work.push((
1934                    CallerRefKey {
1935                        caller_blob_key: key.clone(),
1936                        ref_ordinal: raw.ordinal,
1937                        caller_path: path.as_bytes().to_vec(),
1938                    },
1939                    (raw.kind, bound.clone()),
1940                ));
1941            }
1942        }
1943        extracts.insert(rel.to_string(), extract);
1944    }
1945    profile.finish("decode_bind_index_entries");
1946    let index = ManifestProjectIndex::from_parts(
1947        root,
1948        files,
1949        HashMap::new(),
1950        super::WorkspaceCratePrefixCache::default(),
1951        facts.clone(),
1952    );
1953    let mut result = JoinResult {
1954        rows: BTreeSet::new(),
1955        resolution_order: Vec::new(),
1956        unbound_non_utf8_paths,
1957    };
1958    let resolved_callers = bindings
1959        .keys()
1960        .filter(|path| {
1961            if selected.is_some_and(|set| !set.contains(*path)) {
1962                return false;
1963            }
1964            let Some((changed, _, _)) = reuse else {
1965                return true;
1966            };
1967            if selected.is_none() || changed.contains(*path) {
1968                return true;
1969            }
1970            cached.get(*path).is_none_or(|old| {
1971                old.references != bindings[*path].references
1972                    || old
1973                        .surface_queries
1974                        .iter()
1975                        .any(|(query, expected)| query.answer(&index) != *expected)
1976            })
1977        })
1978        .cloned()
1979        .collect::<BTreeSet<_>>();
1980    for (path, binding) in &mut bindings {
1981        if resolved_callers.contains(path) {
1982            binding.resolved_dependencies.clear();
1983            binding.surface_queries.clear();
1984            binding.resolution_facts = ConfigConsultations::default();
1985        } else if let Some(old) = cached.get(path) {
1986            binding.resolved_dependencies = old.resolved_dependencies.clone();
1987            binding.surface_queries = old.surface_queries.clone();
1988        }
1989    }
1990    profile.finish("index_and_surface_replay");
1991    // Surface replay needs no call sites. Decode an unchanged caller only after
1992    // replay proves that its reference results may change.
1993    for caller in &resolved_callers {
1994        if extracts.contains_key(caller) {
1995            continue;
1996        }
1997        let path = RelPath::new(caller.as_bytes().to_vec()).expect("bound manifest path");
1998        let Some(ManifestEntry::Regular { planes, .. }) = manifest.get(&path) else {
1999            continue;
2000        };
2001        let key = planes.callgraph.as_ref().expect("bound caller key");
2002        let decoded_blob = decode(key)?;
2003        let CallgraphBlob::Parse(blob) = decoded_blob.as_ref() else {
2004            continue;
2005        };
2006        decoded_caller_blobs += 1;
2007        facts.take();
2008        facts.take_config();
2009        let extract =
2010            blob.bind_with_dependencies(caller, &paths, Some(&bindings[caller].references))?;
2011        let caller_work = work.entry(caller.clone()).or_default();
2012        for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
2013            caller_work.push((
2014                CallerRefKey {
2015                    caller_blob_key: key.clone(),
2016                    ref_ordinal: raw.ordinal,
2017                    caller_path: caller.as_bytes().to_vec(),
2018                },
2019                (raw.kind, bound.clone()),
2020            ));
2021        }
2022        extracts.insert(caller.clone(), extract);
2023    }
2024    let index = ManifestProjectIndex::from_parts(
2025        root,
2026        index.files,
2027        extracts
2028            .iter()
2029            .map(|(path, extract)| (path.clone(), &extract.data))
2030            .collect(),
2031        super::WorkspaceCratePrefixCache::default(),
2032        facts.clone(),
2033    );
2034    profile.finish("decode_resolved_callers");
2035    let surface_index = ViewSurfaceIndex {
2036        inner: &index,
2037        queries: Default::default(),
2038    };
2039    let bases: BTreeMap<_, BTreeSet<_>> = resolved_callers
2040        .iter()
2041        .map(|caller| {
2042            let binding = &bindings[caller];
2043            (
2044                caller.clone(),
2045                binding
2046                    .references
2047                    .values()
2048                    .flatten()
2049                    .chain(binding.binding_probes.iter())
2050                    .cloned()
2051                    .collect(),
2052            )
2053        })
2054        .collect();
2055    let mut resolved_bindings = 0;
2056    for (caller, mut caller_work) in work {
2057        if !resolved_callers.contains(&caller) {
2058            continue;
2059        }
2060        caller_work.sort_by(|a, b| (&a.0, a.1 .0).cmp(&(&b.0, b.1 .0)));
2061        let caller_data = &extracts[&caller].data;
2062        let basis = &bases[&caller];
2063        let binding = bindings
2064            .get_mut(&caller)
2065            .expect("bound caller dependencies");
2066        let mut resolutions =
2067            HashMap::<ViewResolutionBinding, (Option<String>, Option<String>)>::new();
2068        let mut caller_queries = BTreeMap::new();
2069        for (key, (kind, raw)) in caller_work {
2070            let memo_key = ViewResolutionBinding::new(&raw, caller_data);
2071            // Dependencies belonging to a call site are not part of the memoized
2072            // target. Preserve them even when another reference resolved its binding.
2073            binding.resolved_dependencies.extend(
2074                raw.dependencies
2075                    .iter()
2076                    .filter(|dependency| !basis.contains(*dependency))
2077                    .cloned(),
2078            );
2079            let (target_file, target_symbol) = match resolutions.entry(memo_key) {
2080                std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
2081                std::collections::hash_map::Entry::Vacant(entry) => {
2082                    facts.take();
2083                    facts.take_config();
2084                    let resolved = super::resolve_ref(raw, &surface_index)
2085                        .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
2086                    // The memo table is caller-local, so one consultation union per
2087                    // binding preserves caller ownership on cache hits.
2088                    binding.resolution_facts.extend(&facts.take_config());
2089                    caller_queries.extend(surface_index.take());
2090                    binding.resolved_dependencies.extend(
2091                        resolved
2092                            .dependencies
2093                            .into_iter()
2094                            .chain(facts.take())
2095                            .filter(|dependency| !basis.contains(dependency)),
2096                    );
2097                    entry
2098                        .insert((resolved.target_file, resolved.target_symbol))
2099                        .clone()
2100                }
2101            };
2102            result.rows.insert(DerivedRow {
2103                caller_blob_key: key.caller_blob_key.clone(),
2104                ref_ordinal: key.ref_ordinal,
2105                caller_path: key.caller_path.clone(),
2106                kind,
2107                status: if target_file.is_some() {
2108                    ResolutionStatus::Resolved
2109                } else {
2110                    ResolutionStatus::Unresolved
2111                },
2112                target_path: target_file.map(String::into_bytes),
2113                target_symbol,
2114            });
2115            result.resolution_order.push(key);
2116        }
2117        resolved_bindings += resolutions.len();
2118        binding.surface_queries = caller_queries.into_iter().collect();
2119    }
2120    result.resolution_order.sort();
2121    profile.finish("resolve_and_record");
2122    for binding in bindings.values_mut() {
2123        binding.consulted_facts = binding
2124            .binding_facts
2125            .facts
2126            .union(&binding.resolution_facts.facts)
2127            .cloned()
2128            .collect();
2129        binding.unattributed =
2130            binding.binding_facts.unattributed() || binding.resolution_facts.unattributed();
2131        binding.dependencies = binding
2132            .references
2133            .values()
2134            .flatten()
2135            .cloned()
2136            .chain(binding.binding_probes.iter().cloned())
2137            .chain(binding.resolved_dependencies.iter().cloned())
2138            .chain(
2139                binding
2140                    .surface_queries
2141                    .iter()
2142                    .flat_map(|(query, _)| query.dependencies()),
2143            )
2144            .collect();
2145    }
2146    profile.finish("dependency_union");
2147    if std::env::var_os("AFT_VIEW_PROFILE").is_some() {
2148        eprintln!("view_profile join.surfaces restored={restored_surfaces} rebuilt_changed={rebuilt_changed} rebuilt_facts={rebuilt_facts} rebuilt_membership={rebuilt_membership} rebuilt_missing={rebuilt_missing} selected={} resolved={} skipped={}", selected.map_or(bindings.len(), BTreeSet::len), resolved_callers.len(), bindings.keys().filter(|path| selected.is_none_or(|set| set.contains(*path)) && !resolved_callers.contains(*path)).count());
2149    }
2150    if let Some(error) = read_error.borrow_mut().take() {
2151        return Err(error);
2152    }
2153    Ok(SelectedManifestJoin {
2154        result,
2155        bindings,
2156        resolved_callers,
2157        rebuilt_surface_entries: rebuilt_surface_paths.len(),
2158        rebuilt_surface_paths,
2159        decoded_caller_blobs,
2160        resolved_bindings,
2161    })
2162}
2163
2164/// Consumer-specific export surface: record the answers the resolver actually
2165/// used, rather than invalidating every caller when an unrelated export changes.
2166/// Querying these answers against a new index is cheaper than walking references
2167/// and is sound only while the caller's immutable parse blob remains unchanged.
2168#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
2169enum ViewSurfaceQuery {
2170    Language(String),
2171    Module(String, String),
2172    Parent(String),
2173    Reexports(String),
2174    Node(String, String),
2175    Callable(String, String),
2176    Alias(String, String),
2177    Export(String, String),
2178    Default(String),
2179    Contains(String),
2180    Crate(String),
2181    CrateRoot(String),
2182    Inline(String, Vec<String>, String),
2183}
2184
2185fn surface_value(value: &impl Serialize) -> String {
2186    blake3::hash(&serde_json::to_vec(value).expect("resolver surface serializes"))
2187        .to_hex()
2188        .to_string()
2189}
2190
2191fn reexport_surface(value: &[super::ReexportIndex]) -> String {
2192    surface_value(
2193        &value
2194            .iter()
2195            .map(|entry| {
2196                (
2197                    &entry.target_file,
2198                    entry.named.iter().collect::<BTreeMap<_, _>>(),
2199                    entry.wildcard,
2200                )
2201            })
2202            .collect::<Vec<_>>(),
2203    )
2204}
2205
2206pub(crate) const VIEW_CONFIG_MEMBERSHIP_DOMAIN: &str = "\0view:config-membership";
2207pub(crate) const VIEW_RUST_MODULE_DOMAIN: &str = "\0view:rust-module-index";
2208
2209impl ViewSurfaceQuery {
2210    fn dependencies(&self) -> Vec<String> {
2211        match self {
2212            Self::Crate(_) => vec![VIEW_CONFIG_MEMBERSHIP_DOMAIN.into()],
2213            Self::CrateRoot(file) => {
2214                vec![file.clone(), VIEW_CONFIG_MEMBERSHIP_DOMAIN.into()]
2215            }
2216            Self::Parent(file) | Self::Inline(file, ..) => {
2217                vec![file.clone(), VIEW_RUST_MODULE_DOMAIN.into()]
2218            }
2219            Self::Language(file)
2220            | Self::Module(file, _)
2221            | Self::Reexports(file)
2222            | Self::Node(file, _)
2223            | Self::Callable(file, _)
2224            | Self::Alias(file, _)
2225            | Self::Export(file, _)
2226            | Self::Default(file)
2227            | Self::Contains(file) => vec![file.clone()],
2228        }
2229    }
2230
2231    fn answer(&self, index: &impl super::ResolverIndex) -> String {
2232        match self {
2233            Self::Language(file) => {
2234                surface_value(&index.lang_for(file).map(|lang| format!("{lang:?}")))
2235            }
2236            Self::Module(file, module) => surface_value(&index.module_target(file, module)),
2237            Self::Parent(file) => surface_value(&index.module_parent(file)),
2238            Self::Reexports(file) => reexport_surface(&index.reexports_for(file)),
2239            Self::Node(file, symbol) => surface_value(&index.node_for_symbol(file, symbol)),
2240            Self::Callable(file, node) => surface_value(&index.node_is_callable(file, node)),
2241            Self::Alias(file, symbol) => surface_value(&index.export_alias(file, symbol)),
2242            Self::Export(file, symbol) => surface_value(&index.has_export(file, symbol)),
2243            Self::Default(file) => surface_value(&index.default_export(file)),
2244            Self::Contains(file) => surface_value(&index.contains_file(file)),
2245            Self::Crate(name) => surface_value(&index.crate_src_prefix(name)),
2246            Self::CrateRoot(file) => surface_value(&index.rust_crate_root_file(file)),
2247            Self::Inline(file, segments, symbol) => {
2248                surface_value(&index.inline_scoped_target(file, segments, symbol))
2249            }
2250        }
2251    }
2252}
2253
2254struct ViewSurfaceIndex<'a, I> {
2255    inner: &'a I,
2256    queries: std::cell::RefCell<BTreeMap<ViewSurfaceQuery, String>>,
2257}
2258
2259impl<I> ViewSurfaceIndex<'_, I> {
2260    fn record(&self, query: ViewSurfaceQuery, value: &impl Serialize) {
2261        self.queries
2262            .borrow_mut()
2263            .insert(query, surface_value(value));
2264    }
2265    fn take(&self) -> BTreeMap<ViewSurfaceQuery, String> {
2266        std::mem::take(&mut *self.queries.borrow_mut())
2267    }
2268}
2269
2270impl<I: super::ResolverIndex> super::ResolverIndex for ViewSurfaceIndex<'_, I> {
2271    fn caller_data(&self, file: &str) -> Option<&FileCallData> {
2272        // resolve_ref reads only its own caller data; changed callers never reuse
2273        // surface answers, so the immutable blob itself guards this input.
2274        self.inner.caller_data(file)
2275    }
2276    fn lang_for(&self, file: &str) -> Option<LangId> {
2277        let value = self.inner.lang_for(file);
2278        self.record(
2279            ViewSurfaceQuery::Language(file.into()),
2280            &value.map(|lang| format!("{lang:?}")),
2281        );
2282        value
2283    }
2284    fn module_target(&self, file: &str, module: &str) -> Option<String> {
2285        let value = self.inner.module_target(file, module);
2286        self.record(ViewSurfaceQuery::Module(file.into(), module.into()), &value);
2287        value
2288    }
2289    fn module_parent(&self, file: &str) -> Option<(String, String)> {
2290        let value = self.inner.module_parent(file);
2291        self.record(ViewSurfaceQuery::Parent(file.into()), &value);
2292        value
2293    }
2294    fn reexports_for(&self, file: &str) -> Vec<super::ReexportIndex> {
2295        let value = self.inner.reexports_for(file);
2296        self.queries.borrow_mut().insert(
2297            ViewSurfaceQuery::Reexports(file.into()),
2298            reexport_surface(&value),
2299        );
2300        value
2301    }
2302    fn node_for_symbol(&self, file: &str, symbol: &str) -> Option<String> {
2303        let value = self.inner.node_for_symbol(file, symbol);
2304        self.record(ViewSurfaceQuery::Node(file.into(), symbol.into()), &value);
2305        value
2306    }
2307    fn node_is_callable(&self, file: &str, node: &str) -> bool {
2308        let value = self.inner.node_is_callable(file, node);
2309        self.record(ViewSurfaceQuery::Callable(file.into(), node.into()), &value);
2310        value
2311    }
2312    fn export_alias(&self, file: &str, symbol: &str) -> Option<String> {
2313        let value = self.inner.export_alias(file, symbol);
2314        self.record(ViewSurfaceQuery::Alias(file.into(), symbol.into()), &value);
2315        value
2316    }
2317    fn has_export(&self, file: &str, symbol: &str) -> bool {
2318        let value = self.inner.has_export(file, symbol);
2319        self.record(ViewSurfaceQuery::Export(file.into(), symbol.into()), &value);
2320        value
2321    }
2322    fn default_export(&self, file: &str) -> Option<String> {
2323        let value = self.inner.default_export(file);
2324        self.record(ViewSurfaceQuery::Default(file.into()), &value);
2325        value
2326    }
2327    fn contains_file(&self, file: &str) -> bool {
2328        let value = self.inner.contains_file(file);
2329        self.record(ViewSurfaceQuery::Contains(file.into()), &value);
2330        value
2331    }
2332    fn crate_src_prefix(&self, name: &str) -> Option<String> {
2333        let value = self.inner.crate_src_prefix(name);
2334        self.record(ViewSurfaceQuery::Crate(name.into()), &value);
2335        value
2336    }
2337    fn rust_crate_root_file(&self, file: &str) -> Option<String> {
2338        let value = self.inner.rust_crate_root_file(file);
2339        self.record(ViewSurfaceQuery::CrateRoot(file.into()), &value);
2340        value
2341    }
2342    fn inline_scoped_target(
2343        &self,
2344        file: &str,
2345        segments: &[String],
2346        symbol: &str,
2347    ) -> Option<(String, String)> {
2348        let value = self.inner.inline_scoped_target(file, segments, symbol);
2349        self.record(
2350            ViewSurfaceQuery::Inline(file.into(), segments.to_vec(), symbol.into()),
2351            &value,
2352        );
2353        value
2354    }
2355}
2356
2357#[cfg(test)]
2358mod consultation_tests {
2359    use super::*;
2360
2361    #[test]
2362    fn opaque_config_read_is_unattributed_even_after_a_known_field_read() {
2363        let manifest = Manifest::new([(
2364            RelPath::new(b"package.json".to_vec()).unwrap(),
2365            ManifestEntry::Regular {
2366                mode: 0o100644,
2367                planes: crate::views::RegularPlanes {
2368                    callgraph: Some("config".into()),
2369                    semantic: None,
2370                },
2371                resolution_input: true,
2372            },
2373        )])
2374        .unwrap();
2375        let bytes: Arc<[u8]> = CallgraphBlob::config(b"{}".to_vec(), "fixture")
2376            .to_bytes()
2377            .unwrap()
2378            .into();
2379        let reader = |_: &BlobKey| Some(bytes.clone());
2380        let facts = ViewBindingFacts::new(Rc::new(ManifestFacts {
2381            manifest: &manifest,
2382            blobs: &reader,
2383        }));
2384        facts.config_fact(b"package.json", "name");
2385        assert!(facts.attributed_config_bytes(b"package.json").is_some());
2386        assert!(!facts.consultations.borrow().unattributed());
2387        facts.memo_start(Path::new("/"), "test", "");
2388        assert!(facts.config_bytes(b"package.json").is_some());
2389        facts.memo_finish(Path::new("/"), "test", "");
2390        assert!(facts.take_config().unattributed());
2391        facts.memo_replay(Path::new("/"), "test", "");
2392        assert!(facts.take_config().unattributed());
2393    }
2394}
2395
2396#[cfg(test)]
2397thread_local! {
2398    static CALLER_NODE_LOOKUP_WORK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2399    // Select the original caller-node scan in tests so the benchmark can
2400    // compare scan and indexed lookup in the same process and host-load window.
2401    pub(crate) static SCAN_CALLER_NODES: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
2402}
2403
2404#[test]
2405fn binding_caller_node_lookup_is_linear_and_preserves_first_duplicate() {
2406    let source = (0..200)
2407        .map(|i| format!("export function f{i}() {{ f0(); }}\n"))
2408        .collect::<String>();
2409    let blob = CallgraphBlob::extract(&source, "typescript", "lookup-test").unwrap();
2410    let mut parse = blob.parse().unwrap().clone();
2411    let mut duplicate = parse.symbols[0].clone();
2412    duplicate.ordinal += 10000;
2413    parse.symbols.push(duplicate);
2414    let manifest = Manifest::new([]).unwrap();
2415    let reader = |_: &BlobKey| None;
2416    let facts = ManifestFacts {
2417        manifest: &manifest,
2418        blobs: &reader,
2419    };
2420    let paths = FactPaths {
2421        root: Path::new("/"),
2422        facts: &facts,
2423    };
2424    CALLER_NODE_LOOKUP_WORK.with(|work| work.set(0));
2425    let extract = parse
2426        .bind_with_dependencies("caller.ts", &paths, None)
2427        .unwrap();
2428    let work = CALLER_NODE_LOOKUP_WORK.with(std::cell::Cell::get);
2429    assert!(work > 0, "lookup work must actually be observed");
2430    assert!(
2431        work <= parse.symbols.len() + parse.refs.len(),
2432        "caller node lookup must be linear: {work} operations for {} symbols and {} refs",
2433        parse.symbols.len(),
2434        parse.refs.len()
2435    );
2436    for reference in &extract.raw_refs {
2437        let expected = reference.caller_symbol.as_ref().and_then(|caller| {
2438            extract
2439                .nodes
2440                .iter()
2441                .find(|node| &node.scoped_name == caller)
2442                .map(|node| node.id.clone())
2443        });
2444        assert_eq!(reference.caller_node, expected);
2445    }
2446}