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