Skip to main content

bynk_check/
index.rs

1//! v0.25: the project-wide binding index (ADR 0053).
2//!
3//! [`RefSink`] collects use→def edges at the resolution sites themselves —
4//! the resolver's reference walk, the checker's capability/service call
5//! dispatch, and the project driver's clause wiring — mirroring v0.24's
6//! `ErrorSink` collection-point pattern. The project pass then qualifies
7//! bare names per unit and assembles a [`ProjectIndex`]: every in-scope
8//! symbol's definition site plus all of its reference sites, binding-correct
9//! (never name-matched).
10//!
11//! In-scope symbol kinds this increment: top-level types, free `fn`s,
12//! capabilities, services, agents, and providers. Instance methods, record
13//! fields, capability op names, and local bindings are deferred (no edges
14//! are recorded for them).
15
16use std::collections::{HashMap, HashSet};
17use std::path::{Path, PathBuf};
18
19use bynk_syntax::ast::BaseType;
20use bynk_syntax::span::Span;
21
22/// The kind half of a symbol's structural key.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub enum SymbolKind {
25    Type,
26    Fn,
27    Capability,
28    Service,
29    Agent,
30    Provider,
31    /// v0.36 (ADR 0069): an instance method, keyed by the compound name
32    /// `"Type.method"` in the type's defining unit. The first parent-scoped
33    /// index kind (see the v0.36 members slice).
34    Method,
35    /// v0.36 (ADR 0069, slice 2): a record field, keyed by `"Type.field"`.
36    Field,
37    /// v0.36 (ADR 0069, slice 2): a capability operation, keyed by `"Cap.op"`.
38    CapabilityOp,
39    /// v0.45: an actor declaration — a boundary contract consumed by a
40    /// handler's `by` clause.
41    Actor,
42    /// (ADR 0069 follow-on, #304): an agent handler, keyed by the compound
43    /// name `"Agent.handler"`. Service handlers have no per-handler name
44    /// (`Handler.method_name` is `None`) so only agent dispatch is covered.
45    Handler,
46    /// A `messages <tag> { ... }` bundle, keyed by its `tag` (message-bundles
47    /// track, slice 1).
48    Messages,
49    /// #1883: an `asset name = "path"` declaration — a value of type `Asset`,
50    /// keyed by its name in the declaring unit.
51    Asset,
52    /// #2387: an `embed name = "path"` declaration — a `String` constant
53    /// holding a project file's text, keyed by its name in the declaring unit.
54    Embed,
55}
56
57impl SymbolKind {
58    pub fn display(self) -> &'static str {
59        match self {
60            SymbolKind::Type => "type",
61            SymbolKind::Fn => "fn",
62            SymbolKind::Capability => "capability",
63            SymbolKind::Service => "service",
64            SymbolKind::Agent => "agent",
65            SymbolKind::Provider => "provider",
66            SymbolKind::Method => "method",
67            SymbolKind::Field => "field",
68            SymbolKind::CapabilityOp => "operation",
69            SymbolKind::Actor => "actor",
70            SymbolKind::Handler => "handler",
71            SymbolKind::Messages => "messages",
72            SymbolKind::Asset => "asset",
73            SymbolKind::Embed => "embed",
74        }
75    }
76}
77
78/// Structural symbol identity (no `DefId` plumbing): the defining unit's
79/// qualified name, the declaration kind, and the declared name. Top-level
80/// names are unique within a unit, so the key is unambiguous.
81#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
82pub struct SymbolKey {
83    pub unit: String,
84    pub kind: SymbolKind,
85    pub name: String,
86}
87
88/// One recorded use→def edge, in collection-point context.
89///
90/// `unit: None` means the name resolved through the recording namespace's
91/// merged tables (local declarations + `uses` imports) and is qualified at
92/// assembly; `Some` means the resolution site already knew the defining
93/// unit (cross-context capability/service references, flattened caps).
94#[derive(Debug, Clone)]
95pub struct RefEdge {
96    /// The name-segment span (for dotted `B.Cap`, just `Cap`).
97    pub span: Span,
98    pub kind: SymbolKind,
99    pub name: String,
100    pub unit: Option<String>,
101    /// Project-relative file the span is an offset into (collection point).
102    pub file: PathBuf,
103    /// The unit whose merged namespace resolves a bare (`unit: None`) name.
104    /// For test/integration files this is the *target* unit.
105    pub namespace: Option<String>,
106    /// Display name of the enclosing top-level declaration, when known
107    /// (`"f"`, `"T.m"`, a service/provider name). Used at assembly to
108    /// re-attribute spans to the file that declares the owner — sibling-file
109    /// methods and unit-level handler tables are processed under a different
110    /// file than the one their spans index into.
111    pub owner: Option<String>,
112    /// v0.35 (ADR 0068): set only on the `Cap` of a `provides Cap = Provider`
113    /// clause (never on a `given Cap` dependency). With `owner` the provider,
114    /// this marks a capability→provider implementation edge — distinguishing
115    /// the provided capability from the provider's own `given` deps, which are
116    /// also capability refs owned by the same provider.
117    pub provides: bool,
118}
119
120/// Collection-point sink for use→def edges (the `ErrorSink` analogue).
121/// The pipeline sets the ambient file/namespace before each per-file phase;
122/// resolution sites only supply the span and target. A sink left in its
123/// default state (no file) discards edges — the single-file entry points
124/// resolve without recording.
125#[derive(Debug, Default)]
126pub struct RefSink {
127    pub edges: Vec<RefEdge>,
128    /// Synthetic namespaces (integration-test harness roots) → their `uses`
129    /// resolution order, merged with the project's `uses` table at assembly.
130    pub extra_uses: HashMap<String, Vec<String>>,
131    file: Option<PathBuf>,
132    namespace: Option<String>,
133    owner: Option<String>,
134    /// Set while processing synthetic (toolchain-injected) files: edges are
135    /// discarded — first-party units are not user-editable and out of index.
136    muted: bool,
137}
138
139impl RefSink {
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Declare a synthetic namespace's `uses` resolution order (integration
145    /// harness roots are not project units, so the project's `uses` table
146    /// has no entry for them).
147    pub fn declare_namespace(&mut self, namespace: &str, uses: Vec<String>) {
148        self.extra_uses.insert(namespace.to_string(), uses);
149    }
150
151    /// Enter a per-file recording context. `namespace` is the unit whose
152    /// merged tables resolve bare names in this file (the file's own unit,
153    /// or a test file's target unit).
154    pub fn enter_file(&mut self, file: &Path, namespace: &str, muted: bool) {
155        self.file = Some(file.to_path_buf());
156        self.namespace = Some(namespace.to_string());
157        self.owner = None;
158        self.muted = muted;
159    }
160
161    /// Set the enclosing top-level declaration for subsequent edges.
162    pub fn set_owner(&mut self, owner: impl Into<String>) {
163        self.owner = Some(owner.into());
164    }
165
166    pub fn clear_owner(&mut self) {
167        self.owner = None;
168    }
169
170    /// Record an edge whose defining unit is found at assembly.
171    pub fn record(&mut self, span: Span, kind: SymbolKind, name: &str) {
172        self.push(span, kind, name, None, false);
173    }
174
175    /// Record an edge whose defining unit the resolution site already knows.
176    pub fn record_in_unit(&mut self, span: Span, kind: SymbolKind, name: &str, unit: &str) {
177        self.push(span, kind, name, Some(unit.to_string()), false);
178    }
179
180    /// v0.35 (ADR 0068): record the `Cap` of a `provides Cap = Provider` clause
181    /// — a capability reference also flagged as an implementation edge (the
182    /// owner is the provider). `unit` is `Some` for a cross-context provided
183    /// capability, `None` when it resolves at assembly.
184    pub fn record_provides(&mut self, span: Span, name: &str, unit: Option<&str>) {
185        self.push(
186            span,
187            SymbolKind::Capability,
188            name,
189            unit.map(str::to_string),
190            true,
191        );
192    }
193
194    fn push(
195        &mut self,
196        span: Span,
197        kind: SymbolKind,
198        name: &str,
199        unit: Option<String>,
200        provides: bool,
201    ) {
202        if self.muted {
203            return;
204        }
205        let Some(file) = &self.file else {
206            return; // single-file mode: no recording context.
207        };
208        self.edges.push(RefEdge {
209            span,
210            kind,
211            name: name.to_string(),
212            unit,
213            file: file.clone(),
214            namespace: self.namespace.clone(),
215            owner: self.owner.clone(),
216            provides,
217        });
218    }
219}
220
221/// One occurrence of a symbol: the file (project-relative) and the
222/// name-segment span within that file's analysed snapshot.
223#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
224pub struct SiteRef {
225    pub path: PathBuf,
226    pub span: Span,
227}
228
229/// v0.28 (ADR 0057): the Bynk-specific semantic-token modifiers recorded on
230/// a symbol at assemble time. `refined` only when a refinement is present —
231/// `type Age = Int` parses as `Refined { refinement: None }` and is a plain
232/// alias, carrying neither; `opaque` is orthogonal, so `opaque B where …`
233/// carries both. `platform_native` when the declaring unit is a platform
234/// adapter (`firstparty::platform_of` is `Some`).
235#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
236pub struct SymbolModifiers {
237    pub refined: bool,
238    pub opaque: bool,
239    pub platform_native: bool,
240}
241
242/// A symbol's definition site plus every reference site.
243#[derive(Debug, Clone, Default)]
244pub struct SymbolEntry {
245    /// The declaration's name span. `None` only transiently during assembly;
246    /// symbols without a located definition are dropped from the index.
247    pub def: Option<SiteRef>,
248    /// Sorted, deduplicated. Does not include the definition site.
249    pub refs: Vec<SiteRef>,
250    /// v0.28 (ADR 0057): semantic-token modifiers, set from the declaration.
251    pub modifiers: SymbolModifiers,
252}
253
254/// v0.34 (ADR 0067): one resolved caller→callee call edge — a reference
255/// (`callee`) occurring inside a known top-level declaration (`caller`), at
256/// `site` (the callee-name span, in the caller's file). The backing data for
257/// call hierarchy: incoming calls group edges by `callee`, outgoing by
258/// `caller`. v0.36 (ADR 0069): `Fn` and `Method` callees/callers. #304:
259/// `CapabilityOp` and agent `Handler` callees/callers too — every index
260/// symbol capable of holding a call site is now a callee. Service
261/// cross-context dispatch remains the one uncovered relation (no per-handler
262/// index symbol to be its callee).
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct CallEdge {
265    pub caller: SymbolKey,
266    pub callee: SymbolKey,
267    pub site: SiteRef,
268}
269
270/// v0.35 (ADR 0068): one capability→provider implementation edge — a `provides
271/// Cap = P` clause records a `Capability` reference (`capability`) whose
272/// enclosing owner is the provider (`provider`), at `site` (the capability-name
273/// span in the `provides` clause). The backing data for implementation
274/// navigation: `implementation` on a capability returns its providers' defs.
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct ImplEdge {
277    pub capability: SymbolKey,
278    pub provider: SymbolKey,
279    pub site: SiteRef,
280}
281
282/// v0.129 (#259): one refined/opaque-type → builtin-base edge. A `type Email =
283/// String where …`, a plain alias `type UserId = String`, or an `opaque String`
284/// all record the builtin `base` they are declared over. The backing data for
285/// **refinement families** — every type over the same base. `ty` is the refined
286/// type's own key; its def site is `symbols[ty].def`. Unlike [`ImplEdge`] this
287/// needs no ref-resolution (the base is a builtin, the key is the def itself), so
288/// it is captured straight at the type-def walk.
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct RefineEdge {
291    pub base: BaseType,
292    pub ty: SymbolKey,
293}
294
295/// v0.28 (ADR 0057): one reference to a first-party (`bynk.*`) symbol.
296/// Tokens-only: first-party defs point at synthetic files not on disk, so
297/// these sites are **never** read by definition/rename/workspace-symbol —
298/// the v0.25 exclusion of synthetic units from `symbols` stands untouched.
299#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct ForeignRef {
301    pub site: SiteRef,
302    pub kind: SymbolKind,
303    pub modifiers: SymbolModifiers,
304}
305
306/// The project-wide binding index: every in-scope symbol's definition and
307/// references, keyed structurally. Built by the v0.24 project pass in
308/// analyse mode; empty in build mode.
309#[derive(Debug, Clone, Default)]
310pub struct ProjectIndex {
311    pub symbols: HashMap<SymbolKey, SymbolEntry>,
312    /// v0.28 (ADR 0057): references to first-party symbols, sorted by
313    /// (path, span), deduplicated — read only by the semantic-tokens
314    /// producer (see [`ForeignRef`]).
315    pub foreign_refs: Vec<ForeignRef>,
316    /// v0.34 (ADR 0067): caller→callee call edges (see [`CallEdge`] for which
317    /// kinds are covered), sorted by (caller, callee, site). The
318    /// call-hierarchy graph.
319    pub calls: Vec<CallEdge>,
320    /// v0.35 (ADR 0068): capability→provider implementation edges, sorted by
321    /// (capability, provider, site). The implementation-nav graph (see
322    /// [`ImplEdge`]).
323    pub impls: Vec<ImplEdge>,
324    /// v0.129 (#259): refined/opaque-type → builtin-base edges, sorted by
325    /// (base name, type key). The refinement-family graph (see [`RefineEdge`]).
326    pub refinements: Vec<RefineEdge>,
327}
328
329impl ProjectIndex {
330    /// The symbol whose definition or reference name-segment contains
331    /// `offset` within `path`. Spans are half-open; name segments never
332    /// overlap, so the first hit is the only hit.
333    pub fn symbol_at(&self, path: &Path, offset: usize) -> Option<(&SymbolKey, &SiteRef)> {
334        for (key, entry) in &self.symbols {
335            if let Some(def) = &entry.def
336                && def.path == path
337                && def.span.range().contains(&offset)
338            {
339                return Some((key, def));
340            }
341            for site in &entry.refs {
342                if site.path == path && site.span.range().contains(&offset) {
343                    return Some((key, site));
344                }
345            }
346        }
347        None
348    }
349
350    /// Definition + references for `key`, definition first.
351    pub fn sites(&self, key: &SymbolKey) -> Vec<&SiteRef> {
352        let Some(entry) = self.symbols.get(key) else {
353            return Vec::new();
354        };
355        entry.def.iter().chain(entry.refs.iter()).collect()
356    }
357
358    /// v0.34 (ADR 0067): call edges whose callee is `key` — its callers.
359    pub fn calls_into<'a>(&'a self, key: &SymbolKey) -> impl Iterator<Item = &'a CallEdge> {
360        let key = key.clone();
361        self.calls.iter().filter(move |e| e.callee == key)
362    }
363
364    /// v0.34 (ADR 0067): call edges whose caller is `key` — what it calls.
365    pub fn calls_from<'a>(&'a self, key: &SymbolKey) -> impl Iterator<Item = &'a CallEdge> {
366        let key = key.clone();
367        self.calls.iter().filter(move |e| e.caller == key)
368    }
369
370    /// v0.35 (ADR 0068): impl edges whose capability is `key` — its providers.
371    pub fn impls_of<'a>(&'a self, key: &SymbolKey) -> impl Iterator<Item = &'a ImplEdge> {
372        let key = key.clone();
373        self.impls.iter().filter(move |e| e.capability == key)
374    }
375
376    /// v0.129 (#259): the builtin base a `Type` `key` refines (a refined/opaque
377    /// type or plain alias), or `None` for a record/sum/unknown key.
378    pub fn refined_base(&self, key: &SymbolKey) -> Option<BaseType> {
379        self.refinements
380            .iter()
381            .find(|e| &e.ty == key)
382            .map(|e| e.base)
383    }
384
385    /// v0.129 (#259): the refine edges over builtin `base` — its refinement
386    /// family (every refined/opaque type, and plain alias, declared over it).
387    pub fn refinements_over(&self, base: BaseType) -> impl Iterator<Item = &RefineEdge> {
388        self.refinements.iter().filter(move |e| e.base == base)
389    }
390
391    /// Structural equality after mapping `self`'s sites through `remap`
392    /// and renaming `from` to `to_name` — the rename capture/escape
393    /// validator. `remap` converts a pre-edit site to its post-edit
394    /// position (rename edits shift spans within edited files).
395    pub fn equals_modulo_rename(
396        &self,
397        post: &ProjectIndex,
398        from: &SymbolKey,
399        to_name: &str,
400        mut remap: impl FnMut(&SiteRef) -> SiteRef,
401    ) -> bool {
402        if self.symbols.len() != post.symbols.len() {
403            return false;
404        }
405        for (key, entry) in &self.symbols {
406            let expect_key = if key == from {
407                SymbolKey {
408                    unit: key.unit.clone(),
409                    kind: key.kind,
410                    name: to_name.to_string(),
411                }
412            } else {
413                key.clone()
414            };
415            let Some(post_entry) = post.symbols.get(&expect_key) else {
416                return false;
417            };
418            let expect_def = entry.def.as_ref().map(&mut remap);
419            if expect_def != post_entry.def {
420                return false;
421            }
422            let mut expect_refs: Vec<SiteRef> = entry.refs.iter().map(&mut remap).collect();
423            expect_refs.sort();
424            let mut post_refs = post_entry.refs.clone();
425            post_refs.sort();
426            if expect_refs != post_refs {
427                return false;
428            }
429        }
430        true
431    }
432}
433
434/// Assembles the index from per-file declaration walks plus the sink's
435/// edges. Built by the project pass, which alone knows unit membership,
436/// `uses` targets, and which file declares each top-level item.
437#[derive(Debug, Default)]
438pub struct IndexBuilder {
439    /// (unit, kind, name) → definition site + modifiers.
440    defs: HashMap<SymbolKey, (SiteRef, SymbolModifiers)>,
441    /// v0.28 (ADR 0057): first-party (`bynk.*`) symbols — kind + modifiers
442    /// only, no usable def site (synthetic files are not on disk). Edges
443    /// qualifying here route into [`ProjectIndex::foreign_refs`].
444    first_party_defs: HashMap<SymbolKey, SymbolModifiers>,
445    /// (unit, owner display name) → declaring file, for span re-attribution.
446    /// Includes methods (`"T.m"`), which are not index symbols.
447    owner_files: HashMap<(String, String), PathBuf>,
448    /// v0.34 (ADR 0067): (unit, owner display name) → the owner's symbol key,
449    /// for resolving a call edge's caller. Populated by every `add_def`, so
450    /// any index symbol capable of enclosing a call site (`Fn`, `Method`,
451    /// `Service`, `Agent`, `Provider`, …) is a valid caller; an owner
452    /// registered only via `add_owner` (attribution-only, no symbol) is not.
453    owner_keys: HashMap<(String, String), SymbolKey>,
454    /// unit → `uses` targets, resolution order.
455    uses: HashMap<String, Vec<String>>,
456    /// unit → `consumes` targets — bare names can also resolve to a consumed
457    /// unit's exported types (the consumer's merged table layers them after
458    /// `uses` imports).
459    consumes: HashMap<String, Vec<String>>,
460    /// v0.129 (#259): refined/opaque-type → builtin-base edges, accumulated at
461    /// the type-def walk (no ref-resolution needed). Assembled in [`build`].
462    refinements: Vec<RefineEdge>,
463}
464
465impl IndexBuilder {
466    pub fn add_def(
467        &mut self,
468        unit: &str,
469        kind: SymbolKind,
470        name: &str,
471        site: SiteRef,
472        modifiers: SymbolModifiers,
473    ) {
474        self.owner_files
475            .insert((unit.to_string(), name.to_string()), site.path.clone());
476        let key = SymbolKey {
477            unit: unit.to_string(),
478            kind,
479            name: name.to_string(),
480        };
481        self.owner_keys
482            .insert((unit.to_string(), name.to_string()), key.clone());
483        self.defs.insert(key, (site, modifiers));
484    }
485
486    /// #1883: register a symbol that can never enclose a call site (an
487    /// `asset`) — its def only, with no owner entry. Owners are keyed by
488    /// (unit, name) without the kind, so registering one here would let an
489    /// asset take over a same-named service's or agent's owner entry and
490    /// re-attribute that owner's refs to the asset's file.
491    pub fn add_leaf_def(
492        &mut self,
493        unit: &str,
494        kind: SymbolKind,
495        name: &str,
496        site: SiteRef,
497        modifiers: SymbolModifiers,
498    ) {
499        let key = SymbolKey {
500            unit: unit.to_string(),
501            kind,
502            name: name.to_string(),
503        };
504        self.defs.insert(key, (site, modifiers));
505    }
506
507    /// v0.129 (#259): record that the `Type` `name` in `unit` is declared over
508    /// builtin `base` (a refined/opaque type or plain alias) — its refinement
509    /// family membership. The def site is looked up from `symbols` at query time,
510    /// so only the (key, base) pairing is stored here.
511    pub fn add_refinement(&mut self, unit: &str, name: &str, base: BaseType) {
512        self.refinements.push(RefineEdge {
513            base,
514            ty: SymbolKey {
515                unit: unit.to_string(),
516                kind: SymbolKind::Type,
517                name: name.to_string(),
518            },
519        });
520    }
521
522    /// v0.28 (ADR 0057): register a first-party symbol for the second
523    /// qualification pass — kind + modifiers only, no def site.
524    pub fn add_first_party_def(
525        &mut self,
526        unit: &str,
527        kind: SymbolKind,
528        name: &str,
529        modifiers: SymbolModifiers,
530    ) {
531        self.first_party_defs.insert(
532            SymbolKey {
533                unit: unit.to_string(),
534                kind,
535                name: name.to_string(),
536            },
537            modifiers,
538        );
539    }
540
541    /// Register a non-symbol owner (a method) for attribution only.
542    pub fn add_owner(&mut self, unit: &str, owner: &str, path: &Path) {
543        self.owner_files
544            .insert((unit.to_string(), owner.to_string()), path.to_path_buf());
545    }
546
547    pub fn set_uses(&mut self, uses: HashMap<String, Vec<String>>) {
548        self.uses = uses;
549    }
550
551    pub fn set_consumes(&mut self, consumes: HashMap<String, Vec<String>>) {
552        self.consumes = consumes;
553    }
554
555    /// Qualify, attribute, dedupe, and assemble.
556    pub fn build(self, edges: Vec<RefEdge>) -> ProjectIndex {
557        let mut index = ProjectIndex::default();
558        for (key, (def, modifiers)) in &self.defs {
559            index.symbols.insert(
560                key.clone(),
561                SymbolEntry {
562                    def: Some(def.clone()),
563                    refs: Vec::new(),
564                    modifiers: *modifiers,
565                },
566            );
567        }
568        let mut seen: HashSet<(PathBuf, Span, SymbolKey)> = HashSet::new();
569        let mut foreign_seen: HashSet<(PathBuf, Span, SymbolKind)> = HashSet::new();
570        let mut calls: Vec<CallEdge> = Vec::new();
571        let mut impls: Vec<ImplEdge> = Vec::new();
572        for edge in edges {
573            // Re-attribute to the owner's declaring file when the owner
574            // lives in a different file than the collection point: sibling-
575            // file methods and unit-level handler tables are processed under
576            // a file other than the one their spans index into. The owner is
577            // declared in the *namespace* unit (the unit being processed).
578            let path = edge
579                .owner
580                .as_ref()
581                .zip(edge.namespace.as_ref())
582                .and_then(|(o, ns)| self.owner_files.get(&(ns.clone(), o.clone())))
583                .cloned()
584                .unwrap_or_else(|| edge.file.clone());
585            let Some(key) = self.qualify(&edge) else {
586                // v0.28 (ADR 0057): second pass — a positive match against
587                // the first-party defs routes into the tokens-only side
588                // table; genuinely unresolved targets stay dropped.
589                if let Some(key) =
590                    self.qualify_with(&edge, |k| self.first_party_defs.contains_key(k))
591                    && foreign_seen.insert((path.clone(), edge.span, key.kind))
592                {
593                    index.foreign_refs.push(ForeignRef {
594                        site: SiteRef {
595                            path,
596                            span: edge.span,
597                        },
598                        kind: key.kind,
599                        modifiers: self.first_party_defs[&key],
600                    });
601                }
602                continue;
603            };
604            let entry = index.symbols.entry(key.clone()).or_default();
605            let Some(def) = &entry.def else {
606                continue;
607            };
608            let site = SiteRef {
609                path,
610                span: edge.span,
611            };
612            // The definition's own name span is not also a reference.
613            if site == *def {
614                continue;
615            }
616            if seen.insert((site.path.clone(), site.span, key.clone())) {
617                // v0.34 (ADR 0067): a `Fn` call inside a known top-level owner
618                // is a call edge. The caller resolves via `owner_keys` exactly
619                // as the file re-attribution above resolves `owner_files`.
620                // v0.36 (ADR 0069): methods are call targets too, now that they
621                // are `add_def`'d index symbols (and callers, since `add_def`
622                // populates `owner_keys` for `"T.m"` owners).
623                // #304: `CapabilityOp` (ADR 0069 scoped it out; reversed — the
624                // resulting reference-count/incoming-call mismatch was more
625                // confusing than the edge it withheld) and agent `Handler`
626                // (new this increment) join for the same reason methods did:
627                // both are `add_def`'d index symbols with a ref already
628                // recorded at the correct owner.
629                if matches!(
630                    key.kind,
631                    SymbolKind::Fn
632                        | SymbolKind::Method
633                        | SymbolKind::CapabilityOp
634                        | SymbolKind::Handler
635                ) && let Some(caller) = edge
636                    .owner
637                    .as_ref()
638                    .zip(edge.namespace.as_ref())
639                    .and_then(|(o, ns)| self.owner_keys.get(&(ns.clone(), o.clone())))
640                {
641                    calls.push(CallEdge {
642                        caller: caller.clone(),
643                        callee: key.clone(),
644                        site: site.clone(),
645                    });
646                }
647                // v0.35 (ADR 0068): a `provides Cap = Provider` clause — a
648                // provides-flagged `Capability` ref whose owner is the provider.
649                // The flag distinguishes it from the provider's `given` deps,
650                // which are also capability refs owned by the same provider.
651                if edge.provides
652                    && let Some(provider) = edge
653                        .owner
654                        .as_ref()
655                        .zip(edge.namespace.as_ref())
656                        .and_then(|(o, ns)| self.owner_keys.get(&(ns.clone(), o.clone())))
657                    && provider.kind == SymbolKind::Provider
658                {
659                    impls.push(ImplEdge {
660                        capability: key.clone(),
661                        provider: provider.clone(),
662                        site: site.clone(),
663                    });
664                }
665                entry.refs.push(site);
666            }
667        }
668        for entry in index.symbols.values_mut() {
669            entry.refs.sort();
670        }
671        index.symbols.retain(|_, e| e.def.is_some());
672        index.foreign_refs.sort_by(|a, b| a.site.cmp(&b.site));
673        calls.sort_by(|a, b| (&a.caller, &a.callee, &a.site).cmp(&(&b.caller, &b.callee, &b.site)));
674        index.calls = calls;
675        impls.sort_by(|a, b| {
676            (&a.capability, &a.provider, &a.site).cmp(&(&b.capability, &b.provider, &b.site))
677        });
678        index.impls = impls;
679        // v0.129 (#259): refinement families — keep edges whose type survived as
680        // an index symbol (a real def), sorted by (base name, type key) so the
681        // family listing is deterministic.
682        let mut refinements: Vec<RefineEdge> = self
683            .refinements
684            .into_iter()
685            .filter(|e| index.symbols.contains_key(&e.ty))
686            .collect();
687        refinements.sort_by(|a, b| (a.base.name(), &a.ty).cmp(&(b.base.name(), &b.ty)));
688        index.refinements = refinements;
689        index
690    }
691
692    fn qualify(&self, edge: &RefEdge) -> Option<SymbolKey> {
693        self.qualify_with(edge, |k| self.defs.contains_key(k))
694    }
695
696    /// The merged-table qualification against an arbitrary def set: a
697    /// site-known unit is looked up directly; a bare name layers local
698    /// first, then `uses` imports, then consumed units' exported types —
699    /// first hit wins, matching the pipeline's `or_insert` merge priority.
700    fn qualify_with(&self, edge: &RefEdge, has: impl Fn(&SymbolKey) -> bool) -> Option<SymbolKey> {
701        if let Some(unit) = &edge.unit {
702            let key = SymbolKey {
703                unit: unit.clone(),
704                kind: edge.kind,
705                name: edge.name.clone(),
706            };
707            return has(&key).then_some(key);
708        }
709        let ns = edge.namespace.as_ref()?;
710        let local = SymbolKey {
711            unit: ns.clone(),
712            kind: edge.kind,
713            name: edge.name.clone(),
714        };
715        if has(&local) {
716            return Some(local);
717        }
718        for target in self
719            .uses
720            .get(ns)
721            .into_iter()
722            .flatten()
723            .chain(self.consumes.get(ns).into_iter().flatten())
724        {
725            let imported = SymbolKey {
726                unit: target.clone(),
727                kind: edge.kind,
728                name: edge.name.clone(),
729            };
730            if has(&imported) {
731                return Some(imported);
732            }
733        }
734        None
735    }
736}