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