Skip to main content

scc_context/
pagerank.rs

1//! Personalized PageRank over the System IR reference graph (Wave 14B).
2//!
3//! Layering invariants: ranking NEVER creates architectural truth — it only
4//! normalizes relationships that already exist as trusted facts in the
5//! `TrustedGraphView` and walks them. `FlowGraph` remains the causal
6//! authority; this module never orders flow steps.
7//!
8//! Structure:
9//! - [`build_reference_graph`] normalizes existing trusted relationships into
10//!   [`ReferenceEdge`]s (never invents new relationships).
11//! - [`RankEdgeKind`] is the rank-edge ontology: CONTAINS/PARTICIPATES_IN/
12//!   HANDLES/DEFINES enter the PageRank adjacency with deliberate direction
13//!   and weight (membership evidence, the member→container, flow→
14//!   participant, route→handler and schema→definer RANKING TRANSITIONS,
15//!   invocation/definition edges) — the mechanism that connects components,
16//!   flows, routes and schemas to the rank graph.
17//! - [`SystemRanker`] runs personalized PageRank (power iteration, damping
18//!   0.85, 50 iterations) over a HETEROGENEOUS node universe — every
19//!   rankable entity (symbol, component, subsystem, service, flow,
20//!   contract, state, reactive, route, topic, queue, store, schema, file),
21//!   not symbols alone. Edges survive when either endpoint is a rankable
22//!   entity, so OWNS→STATE, REGISTERS→CONTRACT, PUBLISHES/SUBSCRIBES→TOPIC
23//!   and READS/WRITES→DATA_STORE feed PageRank directly. The global vector
24//!   is architecturally seeded; task vectors personalize from `TaskSeed`s.
25//! - [`SystemRanker::project_to_symbols`] projects the heterogeneous node
26//!   scores back to surface-relevant symbol scores (a symbol's score = its
27//!   own + 0.4 × the scores of the entities it owns/registers/publishes/
28//!   reads/writes/participates-in/handles/defines, bonus capped at 0.5) —
29//!   the mechanism that carries component/flow/contract/state/route/schema
30//!   importance to the surface.
31//! - [`architectural_specificity`] boosts public facades/entrypoints/state
32//!   owners/contract endpoints/flow participants and penalizes generic
33//!   utilities, generated/vendored code, and ubiquitous symbols — keeping
34//!   central utilities from dominating the ranking.
35//! - [`final_importance`] is the spec's blend of task/global PPR with
36//!   lexical, semantic, confidence, criticality, change-risk and novelty
37//!   signals.
38//!
39//! Everything is deterministic (fixed iteration count, sorted indices,
40//! id-ordered traversals) and no-panic (clamped weights, guarded
41//! division).
42
43use scc_core::{
44    kinds, predicates, Provenance, ReferenceEdge, ReferenceKind, Relationship, SourceRange,
45    SurfaceEntry, TaskSeed,
46};
47use scc_graph::TrustedGraphView;
48use std::collections::{HashMap, HashSet};
49
50/// Per-edge weight adjust hook (DoD 23): `adjust(subject, predicate,
51/// object, base) -> Option<(mode, value)>`, modes add | multiply |
52/// replace | veto. `None` keeps the base weight.
53// trace:exempt reason=internal-detail
54pub type EdgeAdjust = dyn for<'a, 'b, 'c> Fn(&'a str, &'b str, &'c str, f64) -> Option<(String, f64)>;
55
56// trace:exempt reason=internal-detail
57fn no_adjust(_: &str, _: &str, _: &str, _: f64) -> Option<(String, f64)> {
58    None
59}
60
61// ---------------------------------------------------------------------------
62// Edge weight constants (spec §2)
63// ---------------------------------------------------------------------------
64
65/// Weight category for flow-participation edges (flows are trusted facts;
66/// the ranker never invents flow ordering).
67pub const FLOW_PARTICIPATION: f64 = 1.5;
68/// Weight category for invocation edges (`invokes`, callbacks).
69pub const INVOCATION: f64 = 1.5;
70/// Weight category for public-API edges (`exports`).
71pub const PUBLIC_API: f64 = 1.4;
72/// Weight category for state/reactive ownership edges (`owns`).
73pub const OWNS_STATE: f64 = 1.4;
74/// Weight category for data-flow edges (`reads`/`writes`/`produces`/`consumes`).
75pub const PRODUCES_CONSUMES: f64 = 1.3;
76/// Weight category for `implements` edges.
77pub const IMPLEMENTS: f64 = 1.25;
78/// Weight category for `inherits` edges.
79pub const EXTENDS: f64 = 1.2;
80/// Weight category for symbol-resolved `calls` edges.
81pub const CALLS_RESOLVED: f64 = 1.2;
82/// Weight category for contract-registration edges (`registers`).
83pub const CONTRACT_PARTICIPATION: f64 = 1.2;
84/// Weight category for `publishes`/`subscribes` edges.
85pub const SUBSCRIBES_PUBLISHES: f64 = 1.2;
86/// Neutral dependency weight (unmapped predicates, `depends_on`).
87pub const DEPENDS_ON: f64 = 1.0;
88/// Weight category for text-extracted (unresolved) `calls` edges.
89pub const CALLS_EXTRACTED: f64 = 0.9;
90/// Weight category for `imports` edges.
91pub const IMPORTS: f64 = 0.6;
92/// Rank-edge ontology weights — deliberate direction/weight pairs for the
93/// predicates that carry membership/participation/invocation/definition
94/// evidence into PageRank (see [`RankEdgeKind`]): `contains` container →
95/// member membership evidence (0.4); the reverse member → container
96/// RANKING TRANSITION (0.8) that derives container importance from its
97/// members; `participates_in` symbol → flow (0.6) plus the reverse flow →
98/// participant RANKING TRANSITION (1.2) that lets flow importance reach
99/// the participants (the mechanism that connects flow nodes into the rank
100/// graph); `handles` symbol → route (1.5, invocation-strong) plus the
101/// reverse route → handler RANKING TRANSITION (1.5); `defines` symbol →
102/// schema (1.2) plus the reverse schema → definer RANKING TRANSITION
103/// (1.2). The reverse directions are pure ranking edges — NEVER Reality
104/// Graph relationships.
105pub const RANK_MEMBERSHIP: f64 = 0.4;
106pub const RANK_MEMBER_OF: f64 = 0.8;
107pub const RANK_PARTICIPATES: f64 = 0.6;
108pub const RANK_FLOW_REACHES_PARTICIPANT: f64 = 1.2;
109pub const RANK_HANDLES: f64 = 1.5;
110pub const RANK_DEFINES: f64 = 1.2;
111pub const RANK_HANDLED_BY: f64 = 1.5;
112pub const RANK_DEFINED_BY: f64 = 1.2;
113
114/// Provenance weights (spec §2): STALE facts weigh zero (the trusted view
115/// already excludes them; the zero is a belt-and-suspenders floor).
116pub const PROV_OBSERVED: f64 = 1.0;
117pub const PROV_RESOLVED: f64 = 1.0;
118pub const PROV_EXTRACTED: f64 = 0.85;
119pub const PROV_DECLARED: f64 = 0.8;
120pub const PROV_INFERRED: f64 = 0.6;
121pub const PROV_STALE: f64 = 0.0;
122
123/// PageRank damping factor.
124pub const DAMPING_FACTOR: f64 = 0.85;
125/// Fixed power-iteration count (deterministic).
126pub const POWER_ITERATIONS: usize = 50;
127/// Warm-start blend of the global vector into the task vector's start state.
128pub const WARM_START_GLOBAL_BLEND: f64 = 0.3;
129
130/// Ubiquity threshold: a symbol referenced by more distinct sources than
131/// this is "ubiquitous" and gets penalized by [`architectural_specificity`].
132pub const UBIQUITY_THRESHOLD: usize = 20;
133
134/// Generic utility name tokens that make a symbol architecturally generic.
135const GENERIC_NAMES: [&str; 7] = [
136    "utils", "logger", "errors", "common", "types", "helpers", "config",
137];
138
139/// Entity kinds that count as invocation surfaces (route/endpoint/event/
140/// topic/queue handlers are reachable from outside the process).
141const INVOCATION_KINDS: [&str; 5] = [
142    kinds::ROUTE,
143    kinds::ENDPOINT,
144    kinds::EVENT,
145    kinds::TOPIC,
146    kinds::QUEUE,
147];
148
149/// Entity kinds that count as contract endpoints for registration edges.
150const CONTRACT_KINDS: [&str; 2] = [kinds::CONTRACT, kinds::REGISTRY];
151
152/// The HETEROGENEOUS PageRank universe: every entity kind that can carry
153/// surface-relevant importance. Symbol-only ranking let component/flow/
154/// contract/state importance die at the boundary; these kinds participate
155/// as first-class nodes (real strings from `scc_core::kinds`). SYSTEM is
156/// included so the top of the containment hierarchy (System →
157/// Subsystem/…) is a first-class rank node, not a dead endpoint.
158const RANKABLE_KINDS: [&str; 15] = [
159    kinds::SYMBOL,
160    kinds::COMPONENT,
161    kinds::SUBSYSTEM,
162    kinds::SERVICE,
163    kinds::SYSTEM,
164    kinds::FLOW,
165    kinds::CONTRACT,
166    kinds::STATE,
167    kinds::REACTIVE,
168    kinds::ROUTE,
169    kinds::TOPIC,
170    kinds::QUEUE,
171    kinds::DATA_STORE,
172    kinds::SCHEMA,
173    kinds::FILE,
174];
175
176/// Predicates whose non-symbol targets a symbol projects onto: the
177/// entities whose PageRank score lifts the owning symbol's surface score.
178/// HANDLES/DEFINES are included so a hot Route/Schema node (task seed or
179/// PPR) reaches its handler/definer symbol — the reviewer's
180/// route-hot → handler-reached scenario.
181const PROJECTION_PREDICATES: [&str; 8] = [
182    predicates::OWNS,
183    predicates::REGISTERS,
184    predicates::PUBLISHES,
185    predicates::READS,
186    predicates::WRITES,
187    predicates::PARTICIPATES_IN,
188    predicates::HANDLES,
189    predicates::DEFINES,
190];
191
192/// Projection factor: a symbol's score += 0.4 × the sum of the scores of
193/// the entities it OWNS/REGISTERS/PUBLISHES/READS/WRITES/PARTICIPATES_IN/
194/// HANDLES/DEFINES.
195pub const PROJECTION_BONUS_FACTOR: f64 = 0.4;
196/// The projection bonus is capped at 0.5 per symbol (a state-heavy symbol
197/// cannot accumulate an unbounded lift).
198pub const PROJECTION_BONUS_CAP: f64 = 0.5;
199
200// ---------------------------------------------------------------------------
201// Weight helpers
202// ---------------------------------------------------------------------------
203
204/// Provenance weight per spec §2.
205// trace:exempt reason=internal-detail
206pub fn provenance_weight(provenance: Provenance) -> f64 {
207    match provenance {
208        Provenance::Observed => PROV_OBSERVED,
209        Provenance::Resolved => PROV_RESOLVED,
210        Provenance::Extracted => PROV_EXTRACTED,
211        Provenance::Declared => PROV_DECLARED,
212        Provenance::Inferred => PROV_INFERRED,
213        Provenance::Stale => PROV_STALE,
214    }
215}
216
217/// Rarity of a target symbol: `log(total_symbols / (1 + in_degree))`
218/// clamped to [0.25, 1.5]. Occurrence = in-degree count of the target
219/// (distinct referencing sources). A ubiquitous `logger`-ish symbol is
220/// rare-except-common and gets downweighted; a distinctive symbol is rare
221/// and gets boosted.
222// trace:exempt reason=internal-detail
223pub fn rarity(total_symbols: usize, in_degree: usize) -> f64 {
224    if total_symbols == 0 {
225        return 1.0;
226    }
227    let r = (total_symbols as f64 / (1 + in_degree) as f64).ln();
228    r.clamp(0.25, 1.5)
229}
230
231/// Predicate weight category for an SCC predicate. `calls` is refined by
232/// provenance: symbol-resolved calls weigh more than text-extracted ones.
233// trace:exempt reason=internal-detail
234pub fn predicate_weight(predicate: &str, provenance: Provenance) -> f64 {
235    match predicate {
236        predicates::CALLS => match provenance {
237            Provenance::Resolved => CALLS_RESOLVED,
238            _ => CALLS_EXTRACTED,
239        },
240        predicates::INVOKES | predicates::HANDLES_CALLBACK => INVOCATION,
241        predicates::EXPORTS => PUBLIC_API,
242        predicates::OWNS => OWNS_STATE,
243        predicates::READS | predicates::WRITES => PRODUCES_CONSUMES,
244        predicates::IMPLEMENTS | predicates::IMPLEMENTED_BY => IMPLEMENTS,
245        predicates::INHERITS => EXTENDS,
246        predicates::REGISTERS => CONTRACT_PARTICIPATION,
247        predicates::PUBLISHES | predicates::SUBSCRIBES => SUBSCRIBES_PUBLISHES,
248        predicates::IMPORTS => IMPORTS,
249        predicates::CONTAINS => RANK_MEMBERSHIP,
250        _ => DEPENDS_ON,
251    }
252}
253
254/// Full edge weight (spec §2): `predicate_weight * provenance_weight *
255/// confidence * rarity`. `target_in_degree` is the in-degree count of the
256/// target symbol (distinct referencing sources).
257// trace:exempt reason=internal-detail
258pub fn edge_weight(
259    predicate: &str,
260    provenance: Provenance,
261    confidence: f64,
262    total_symbols: usize,
263    target_in_degree: usize,
264) -> f64 {
265    predicate_weight(predicate, provenance)
266        * provenance_weight(provenance)
267        * confidence.clamp(0.0, 1.0)
268        * rarity(total_symbols, target_in_degree)
269}
270
271// ---------------------------------------------------------------------------
272// Reference normalization
273// ---------------------------------------------------------------------------
274
275/// Map an SCC predicate to a [`ReferenceKind`], given the target entity
276/// kind (needed to scope `owns` to state/reactive per the spec). `None`
277/// means the predicate is not part of the normalized reference surface —
278/// it is left alone, never invented.
279// trace:exempt reason=internal-detail
280fn reference_kind(predicate: &str, target_kind: &str) -> Option<ReferenceKind> {
281    match predicate {
282        predicates::CALLS | predicates::INVOKES | predicates::HANDLES_CALLBACK => {
283            Some(ReferenceKind::Call)
284        }
285        predicates::READS => Some(ReferenceKind::Read),
286        predicates::WRITES => Some(ReferenceKind::Write),
287        predicates::OWNS if target_kind == kinds::STATE || target_kind == kinds::REACTIVE => {
288            Some(ReferenceKind::Write)
289        }
290        predicates::IMPLEMENTS | predicates::IMPLEMENTED_BY => Some(ReferenceKind::Implement),
291        predicates::INHERITS => Some(ReferenceKind::Extend),
292        predicates::REGISTERS | predicates::PUBLISHES | predicates::SUBSCRIBES => {
293            Some(ReferenceKind::Register)
294        }
295        predicates::IMPORTS => Some(ReferenceKind::Import),
296        predicates::EXPORTS => Some(ReferenceKind::Export),
297        predicates::DECORATES | predicates::ANNOTATES => Some(ReferenceKind::Decorate),
298        _ => None,
299    }
300}
301
302/// Evidence file/line for a relationship, when available. The trusted view
303/// does not expose store evidence rows, so this uses the symbol entities'
304/// recorded `file`/`start_line`/`end_line` attributes (the same source
305/// evidence the extractors recorded at index time). Subject first, then
306/// object; deduplicated; empty when neither endpoint is a symbol.
307// trace:exempt reason=internal-detail
308fn locations_for(view: &TrustedGraphView, rel: &Relationship) -> Vec<SourceRange> {
309    let mut locs: Vec<SourceRange> = Vec::new();
310    for id in [&rel.subject, &rel.object] {
311        let Some(e) = view.entity(id) else { continue };
312        if e.kind != kinds::SYMBOL {
313            continue;
314        }
315        let Some(path) = e.attributes.get("file").and_then(|v| v.as_str()) else {
316            continue;
317        };
318        let start = e
319            .attributes
320            .get("start_line")
321            .and_then(|v| v.as_u64())
322            .unwrap_or(0) as u32;
323        let end = e
324            .attributes
325            .get("end_line")
326            .and_then(|v| v.as_u64())
327            .unwrap_or(0) as u32;
328        let start = start.max(1);
329        let loc = SourceRange::new(path, start, end.max(start));
330        if !locs.contains(&loc) {
331            locs.push(loc);
332        }
333    }
334    locs
335}
336
337/// Normalize existing trusted relationships into reference edges. Never
338/// invents new relationships: every edge traces to one trusted fact.
339///
340/// Mapping (actual predicates from `scc_core::predicates`):
341/// - `calls`/`invokes`/`handles_callback` → Call
342/// - `reads` → Read; `writes` → Write
343/// - `owns` (state/reactive entity) → Write
344/// - `implements`/`implemented_by` → Implement
345/// - `inherits` → Extend
346/// - `registers`/`publishes`/`subscribes` → Register
347/// - `imports` → Import; `exports` → Export
348/// - `decorates`/`annotates` → Decorate
349///
350/// Confidence passes through from the relationship (default 1.0); locations
351/// are the evidence file/line when available. Deterministic: the trusted
352/// view returns relationships sorted by id.
353// trace:v1 id=impl.scc.pagerank work=WORK-SCC-014 satisfies=REQ-SCC-IR
354pub fn build_reference_graph(view: &TrustedGraphView) -> Vec<ReferenceEdge> {
355    let mut out: Vec<ReferenceEdge> = Vec::new();
356    for rel in view.all_rels() {
357        let target_kind = view
358            .entity(&rel.object)
359            .map(|e| e.kind.as_str())
360            .unwrap_or("");
361        let kind = match reference_kind(&rel.predicate, target_kind) {
362            Some(k) => k,
363            None => continue,
364        };
365        // `implemented_by` is stored interface→class; normalize to class→interface.
366        let (source, target) = if rel.predicate == predicates::IMPLEMENTED_BY {
367            (rel.object.clone(), rel.subject.clone())
368        } else {
369            (rel.subject.clone(), rel.object.clone())
370        };
371        out.push(ReferenceEdge {
372            source_symbol: source,
373            target_symbol: target,
374            kind,
375            locations: locations_for(view, rel),
376            provenance: rel.provenance,
377            confidence: rel.confidence as f32,
378        });
379    }
380    out
381}
382
383// ---------------------------------------------------------------------------
384// Rank-edge ontology
385// ---------------------------------------------------------------------------
386
387/// A directed rank edge in the heterogeneous PageRank adjacency: which
388/// trusted predicates enter the rank graph and in which direction/with
389/// which weight. Distinct from [`ReferenceKind`] (the normalized
390/// reference surface — these edges are the RANK-edge ontology, never
391/// `reference_kind` doubling). Every edge traces to one trusted
392/// relationship; the reverse directions of `contains`/`participates_in`/
393/// `handles`/`defines` are explicit RANKING TRANSITIONS — edges that
394/// exist only so container/flow/route/schema importance reaches its
395/// members/participants/handlers/definers — and are labeled as such,
396/// never as Reality Graph relationships.
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398// trace:v1 id=impl.crates-scc-context-src-pagerank.rank-edge-kind work=WORK-rank-edge-ontology-c-o-n-t-a-i-n-s-p-a-r-t-i-c-i-p-a-t-e-s-i-n-h-a-n-d-l-e-s-d-e-f-i-n-e-s-enter-the-p-p-r-adjacency satisfies=REQ-rank-edges-heterogeneous-predicates-enter-page-rank
399pub enum RankEdgeKind {
400    /// Container → member: a kind earlier in [`CONTAINMENT_HIERARCHY`]
401    /// (System/Subsystem/Service/Component/File) CONTAINS a kind later in
402    /// it (Subsystem/Service/Component/File/Symbol) — membership
403    /// evidence, weight 0.4.
404    Contains,
405    /// RANKING TRANSITION (never a Reality Graph relationship): member →
406    /// its container, so container importance is derived from the
407    /// members' aggregate (weight 0.8).
408    MemberOf,
409    /// `Symbol` PARTICIPATES_IN a `Flow` (weight 0.6).
410    ParticipatesIn,
411    /// RANKING TRANSITION: `Flow` → its participant symbol, so flow
412    /// importance reaches the participants — the mechanism that connects
413    /// flow nodes into the rank graph (weight 1.2).
414    FlowReachesParticipant,
415    /// `Symbol` HANDLES a `Route` (invocation-strong, weight 1.5).
416    Handles,
417    /// RANKING TRANSITION: `Route` → its handler symbol, so route
418    /// importance reaches the handler — the reverse of `Handles`
419    /// (weight 1.5).
420    HandledBy,
421    /// `Symbol` DEFINES a `Schema` (weight 1.2).
422    Defines,
423    /// RANKING TRANSITION: `Schema` → its defining symbol, so schema
424    /// importance reaches the definer — the reverse of `Defines`
425    /// (weight 1.2).
426    DefinedBy,
427}
428
429// trace:exempt reason=internal-detail
430impl RankEdgeKind {
431    /// The reverse ranking transition of this edge: `Contains` ↔
432    /// `MemberOf`, `ParticipatesIn` ↔ `FlowReachesParticipant`,
433    /// `Handles` ↔ `HandledBy`, `Defines` ↔ `DefinedBy`. Every evidence
434    /// edge has a ranking-only reverse — it propagates container/flow/
435    /// route/schema importance back to members/participants/handlers/
436    /// definers and NEVER claims a Reality Graph relationship.
437    // trace:exempt reason=internal-detail
438    pub fn reverse_ranking_transition(self) -> Option<RankEdgeKind> {
439        match self {
440            RankEdgeKind::Contains => Some(RankEdgeKind::MemberOf),
441            RankEdgeKind::MemberOf => Some(RankEdgeKind::Contains),
442            RankEdgeKind::ParticipatesIn => Some(RankEdgeKind::FlowReachesParticipant),
443            RankEdgeKind::FlowReachesParticipant => Some(RankEdgeKind::ParticipatesIn),
444            RankEdgeKind::Handles => Some(RankEdgeKind::HandledBy),
445            RankEdgeKind::HandledBy => Some(RankEdgeKind::Handles),
446            RankEdgeKind::Defines => Some(RankEdgeKind::DefinedBy),
447            RankEdgeKind::DefinedBy => Some(RankEdgeKind::Defines),
448        }
449    }
450
451    /// Base weight of the rank edge (before provenance × confidence).
452    // trace:exempt reason=internal-detail
453    pub fn weight(self) -> f64 {
454        match self {
455            RankEdgeKind::Contains => RANK_MEMBERSHIP,
456            RankEdgeKind::MemberOf => RANK_MEMBER_OF,
457            RankEdgeKind::ParticipatesIn => RANK_PARTICIPATES,
458            RankEdgeKind::FlowReachesParticipant => RANK_FLOW_REACHES_PARTICIPANT,
459            RankEdgeKind::Handles => RANK_HANDLES,
460            RankEdgeKind::HandledBy => RANK_HANDLED_BY,
461            RankEdgeKind::Defines => RANK_DEFINES,
462            RankEdgeKind::DefinedBy => RANK_DEFINED_BY,
463        }
464    }
465}
466
467/// The full containment hierarchy, top → bottom, real strings from
468/// `scc_core::kinds` (all six kinds exist there). A CONTAINS edge whose
469/// subject precedes its object in this list is container → member
470/// membership evidence; the stored member → container direction is the
471/// reverse RANKING TRANSITION (member importance flows up). Covers every
472/// hierarchy pair — (System, Subsystem), (Subsystem, Service),
473/// (Subsystem, Component), (Service, Component), (Component, File),
474/// (Component, Symbol), (File, Symbol) and every transitive container
475/// pair — not just the adjacent ones.
476const CONTAINMENT_HIERARCHY: [&str; 6] = [
477    kinds::SYSTEM,
478    kinds::SUBSYSTEM,
479    kinds::SERVICE,
480    kinds::COMPONENT,
481    kinds::FILE,
482    kinds::SYMBOL,
483];
484
485/// Position of a kind in [`CONTAINMENT_HIERARCHY`]; `None` for kinds
486/// outside the containment hierarchy (they never fire containment edges).
487// trace:exempt reason=internal-detail
488fn containment_position(kind: &str) -> Option<usize> {
489    CONTAINMENT_HIERARCHY.iter().position(|k| *k == kind)
490}
491
492/// Map a (predicate, subject kind, target kind) triple to its rank-edge
493/// kind, when the relationship is one of the explicit rank-edge
494/// predicates (real strings from `scc_core::predicates`). Direction is
495/// deliberate: only the documented endpoint kinds fire — `contains`
496/// only between kinds in [`CONTAINMENT_HIERARCHY`] with the container
497/// before the member (the stored member → container direction maps to
498/// the `MemberOf` RANKING TRANSITION), `participates_in` only between a
499/// symbol and a flow (stored flow → symbol maps to
500/// `FlowReachesParticipant`), `handles` only symbol ↔ route (stored
501/// route → symbol maps to the `HandledBy` RANKING TRANSITION), `defines`
502/// only symbol ↔ schema (stored schema → symbol maps to `DefinedBy`).
503// trace:exempt reason=internal-detail
504pub fn rank_edge_kind(
505    predicate: &str,
506    subject_kind: &str,
507    target_kind: &str,
508) -> Option<RankEdgeKind> {
509    match predicate {
510        predicates::CONTAINS => {
511            let (Some(sp), Some(tp)) = (
512                containment_position(subject_kind),
513                containment_position(target_kind),
514            ) else {
515                return None;
516            };
517            if sp < tp {
518                Some(RankEdgeKind::Contains)
519            } else if tp < sp {
520                Some(RankEdgeKind::MemberOf)
521            } else {
522                None
523            }
524        }
525        predicates::PARTICIPATES_IN
526            if subject_kind == kinds::SYMBOL && target_kind == kinds::FLOW =>
527        {
528            Some(RankEdgeKind::ParticipatesIn)
529        }
530        predicates::PARTICIPATES_IN
531            if subject_kind == kinds::FLOW && target_kind == kinds::SYMBOL =>
532        {
533            Some(RankEdgeKind::FlowReachesParticipant)
534        }
535        predicates::HANDLES if subject_kind == kinds::SYMBOL && target_kind == kinds::ROUTE => {
536            Some(RankEdgeKind::Handles)
537        }
538        // Reverse of `handles`: RANKING TRANSITION route → handler symbol.
539        predicates::HANDLES if subject_kind == kinds::ROUTE && target_kind == kinds::SYMBOL => {
540            Some(RankEdgeKind::HandledBy)
541        }
542        predicates::DEFINES if subject_kind == kinds::SYMBOL && target_kind == kinds::SCHEMA => {
543            Some(RankEdgeKind::Defines)
544        }
545        // Reverse of `defines`: RANKING TRANSITION schema → definer symbol.
546        predicates::DEFINES if subject_kind == kinds::SCHEMA && target_kind == kinds::SYMBOL => {
547            Some(RankEdgeKind::DefinedBy)
548        }
549        _ => None,
550    }
551}
552
553// ---------------------------------------------------------------------------
554// SystemRanker
555// ---------------------------------------------------------------------------
556
557/// Personalized PageRank over the HETEROGENEOUS entity reference graph.
558///
559/// Nodes are every rankable entity (id-sorted for determinism): symbols,
560/// components, subsystems, services, flows, contracts, state, reactive,
561/// routes, topics, queues, stores, schemas and files. Edges are trusted
562/// relationships whose both endpoints are rankable entities and whose
563/// predicate normalizes to a reference kind; weights follow the spec
564/// formula. The global vector is computed once per epoch (Main caches the
565/// `SystemRanker`); task vectors personalize from `TaskSeed`s with a
566/// warm start blended from the global vector.
567// trace:exempt reason=internal-detail
568pub struct SystemRanker<'a> {
569    view: &'a TrustedGraphView<'a>,
570    /// Sorted rankable entity ids; index i in every vector == `nodes[i]`.
571    nodes: Vec<String>,
572    /// Entity kinds, parallel to `nodes`.
573    kinds: Vec<String>,
574    index: HashMap<String, usize>,
575    /// Row-normalized out-edge adjacency: (target index, weight).
576    adjacency: Vec<Vec<(usize, f64)>>,
577    /// Surviving rank edges as (source idx, target idx, predicate,
578    /// weight): pre-aggregation triples `ranking.edges` serves.
579    edge_list: Vec<(usize, usize, String, f64)>,
580    /// Distinct referencing sources per node (rarity/ubiquity input).
581    in_degree: Vec<usize>,
582    /// For each node: indices of the non-symbol entities it projects onto
583    /// (OWNS/REGISTERS/PUBLISHES/READS/WRITES/PARTICIPATES_IN/HANDLES/
584    /// DEFINES targets — HANDLES/DEFINES so a hot Route/Schema node
585    /// reaches its handler/definer symbol).
586    projection: Vec<Vec<usize>>,
587    /// Precomputed global (architecturally seeded) PageRank vector.
588    global: Vec<f64>,
589}
590
591// trace:exempt reason=internal-detail
592impl<'a> SystemRanker<'a> {
593    /// Build the ranker from the trusted view. Deterministic; O(E) edges.
594// trace:exempt reason=internal-detail
595    pub fn new(view: &'a TrustedGraphView<'a>) -> SystemRanker<'a> {
596        Self::with_edge_adjust(view, no_adjust)
597    }
598
599    /// Build the ranker with a per-edge weight adjust hook (DoD 23):
600    /// `adjust(subject, predicate, object, base) -> Option<(mode, value)>`.
601    /// Modes: add | multiply | replace | veto. `None` keeps the base weight.
602    /// Veto (or non-positive/non-finite result) drops the edge. Deterministic
603    /// for a deterministic hook; every other construction step is unchanged.
604// trace:exempt reason=internal-detail
605    pub fn with_edge_adjust<F>(
606        view: &'a TrustedGraphView<'a>,
607        adjust: F,
608    ) -> SystemRanker<'a>
609    where
610        F: for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
611    {
612        Self::with_edge_adjust_and_extra(view, adjust, &[])
613    }
614
615    /// Build the ranker with an adjust hook plus extra rank-time edges
616    /// (§48): `(subject, predicate, object, weight)` triples that enter
617    /// diffusion without becoming canonical facts. Both endpoints must be
618    /// rankable universe nodes; unknown ids and non-positive/non-finite
619    /// weights are skipped (a bad contributor degrades to no extra edges,
620    /// never a failed build). Surviving edges take the same adjust +
621    /// rarity path as view edges.
622// trace:exempt reason=internal-detail
623    pub fn with_edge_adjust_and_extra<F>(
624        view: &'a TrustedGraphView<'a>,
625        adjust: F,
626        extra: &[(String, String, String, f64)],
627    ) -> SystemRanker<'a>
628    where
629        F: for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
630    {
631        Self::with_edge_adjust_extra_and_nodes(view, adjust, extra, &[])
632    }
633
634    /// Build the ranker with adjust + extra edges + extra rank nodes
635    /// (§124 item 17 RankNodeProvider): `(id, kind)` pairs merged into the
636    /// rank universe before edge indexing. Only ids with a rankable kind
637    /// enter (unknown kinds abstain — a contributor that cannot name a
638    /// real kind abstains); duplicates of view nodes are skipped; the
639    /// universe stays id-sorted and deterministic. Extra nodes are
640    /// rank-time only: no entity, relationship, or evidence is written.
641// trace:exempt reason=internal-detail
642    pub fn with_edge_adjust_extra_and_nodes<F>(
643        view: &'a TrustedGraphView<'a>,
644        adjust: F,
645        extra: &[(String, String, String, f64)],
646        extra_nodes: &[(String, String)],
647    ) -> SystemRanker<'a>
648    where
649        F: for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
650    {
651        let mut pairs: Vec<(String, String)> = view
652            .entities()
653            .filter(|e| RANKABLE_KINDS.contains(&e.kind.as_str()))
654            .map(|e| (e.id.clone(), e.kind.clone()))
655            .collect();
656        for (id, kind) in extra_nodes {
657            if id.is_empty() || !RANKABLE_KINDS.contains(&kind.as_str()) {
658                continue;
659            }
660            if pairs.iter().any(|(eid, _)| eid == id) {
661                continue;
662            }
663            pairs.push((id.clone(), kind.clone()));
664        }
665        pairs.sort_by(|a, b| a.0.cmp(&b.0));
666        let nodes: Vec<String> = pairs.iter().map(|(id, _)| id.clone()).collect();
667        let kinds: Vec<String> = pairs.iter().map(|(_, k)| k.clone()).collect();
668        let index: HashMap<String, usize> = nodes
669            .iter()
670            .enumerate()
671            .map(|(i, s)| (s.clone(), i))
672            .collect();
673        let n = nodes.len();
674
675        // Pass 1: collect edges + per-target distinct sources. An edge
676        // survives when BOTH endpoints are rankable entities (the universe
677        // is heterogeneous: non-symbol endpoints are first-class nodes) and
678        // the predicate normalizes to a reference kind OR carries a
679        // rank-edge transition (the deliberate direction/weight ontology —
680        // CONTAINS/PARTICIPATES_IN/HANDLES/DEFINES; see [`RankEdgeKind`]).
681        let mut edges: Vec<(usize, usize, String, f64)> = Vec::new();
682        let mut in_sources: Vec<HashSet<usize>> = vec![HashSet::new(); n];
683        for rel in view.all_rels() {
684            let (Some(&si), Some(&ti)) = (index.get(&rel.subject), index.get(&rel.object)) else {
685                continue;
686            };
687            let subject_kind = view
688                .entity(&rel.subject)
689                .map(|e| e.kind.as_str())
690                .unwrap_or("");
691            let target_kind = view
692                .entity(&rel.object)
693                .map(|e| e.kind.as_str())
694                .unwrap_or("");
695            // Normalized reference-surface edges (the existing surface:
696            // calls/reads/writes/owns/…).
697            let reference_weight = if reference_kind(&rel.predicate, target_kind).is_some() {
698                Some(
699                    predicate_weight(&rel.predicate, rel.provenance)
700                        * provenance_weight(rel.provenance)
701                        * rel.confidence.clamp(0.0, 1.0),
702                )
703            } else {
704                None
705            };
706            // Rank-edge transitions (the deliberate direction/weight
707            // ontology): the stored direction plus the reverse ranking
708            // transition when the ontology defines one.
709            if let Some(kind) = rank_edge_kind(&rel.predicate, subject_kind, target_kind) {
710                let w = kind.weight()
711                    * provenance_weight(rel.provenance)
712                    * rel.confidence.clamp(0.0, 1.0);
713                if let Some(w) = Self::adjust_edge(&adjust, &rel.subject, &rel.predicate, &rel.object, w) {
714                    in_sources[ti].insert(si);
715                    edges.push((si, ti, rel.predicate.clone(), w));
716                }
717                if let Some(rev) = kind.reverse_ranking_transition() {
718                    let rw = rev.weight()
719                        * provenance_weight(rel.provenance)
720                        * rel.confidence.clamp(0.0, 1.0);
721                    if let Some(rw) = Self::adjust_edge(&adjust, &rel.object, &rel.predicate, &rel.subject, rw) {
722                        in_sources[si].insert(ti);
723                        edges.push((ti, si, rel.predicate.clone(), rw));
724                    }
725                }
726            }
727            if let Some(w) = reference_weight {
728                if let Some(w) = Self::adjust_edge(&adjust, &rel.subject, &rel.predicate, &rel.object, w) {
729                    in_sources[ti].insert(si);
730                    edges.push((si, ti, rel.predicate.clone(), w));
731                }
732            }
733        }
734        for (subj, pred, obj, w) in extra {
735            if !w.is_finite() || *w <= 0.0 {
736                continue;
737            }
738            let (Some(&si), Some(&ti)) = (index.get(subj), index.get(obj)) else {
739                continue;
740            };
741            if let Some(w) = Self::adjust_edge(&adjust, subj, pred, obj, *w) {
742                in_sources[ti].insert(si);
743                edges.push((si, ti, pred.clone(), w));
744            }
745        }
746        let in_degree: Vec<usize> = in_sources.iter().map(|s| s.len()).collect();
747
748        // Rank-edge introspection (§123.12): snapshot the surviving
749        // triples before aggregation moves `edges`.
750        let edge_list: Vec<(usize, usize, String, f64)> = edges.to_vec();
751        // Aggregate parallel edges (sum weights), apply rarity, row-normalize.
752        let mut agg: Vec<HashMap<usize, f64>> = vec![HashMap::new(); n];
753        for (si, ti, _pred, w) in edges.iter() {
754            let (si, ti, w) = (*si, *ti, *w);
755            let r = rarity(n, in_degree[ti]);
756            *agg[si].entry(ti).or_insert(0.0) += w * r;
757        }
758        let mut adjacency: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
759        for (i, row) in agg.iter().enumerate() {
760            let sum: f64 = row.values().sum();
761            if sum > 0.0 {
762                let mut v: Vec<(usize, f64)> =
763                    row.iter().map(|(j, w)| (*j, w / sum)).collect();
764                v.sort_by_key(|(j, _)| *j);
765                adjacency[i] = v;
766            }
767        }
768
769        // Projection edges: non-symbol targets of the symbol's
770        // OWNS/REGISTERS/PUBLISHES/READS/WRITES/PARTICIPATES_IN/HANDLES/
771        // DEFINES rels (HANDLES/DEFINES so a hot Route/Schema node
772        // reaches its handler/definer symbol).
773        let mut projection: Vec<Vec<usize>> = vec![Vec::new(); n];
774        for rel in view.all_rels() {
775            if !PROJECTION_PREDICATES.contains(&rel.predicate.as_str()) {
776                continue;
777            }
778            let (Some(&si), Some(&ti)) = (index.get(&rel.subject), index.get(&rel.object)) else {
779                continue;
780            };
781            if kinds[ti] == kinds::SYMBOL {
782                continue; // the bonus carries entity importance, not symbol
783            }
784            projection[si].push(ti);
785        }
786        for p in projection.iter_mut() {
787            p.sort_unstable();
788            p.dedup();
789        }
790
791        let ranker = SystemRanker {
792            view,
793            nodes,
794            kinds,
795            index,
796            adjacency,
797            in_degree,
798            projection,
799            edge_list,
800            global: Vec::new(),
801        };
802        let global = Self::ppr(&ranker.adjacency, &ranker.global_personalization());
803        let mut ranker = ranker;
804        ranker.global = global;
805        ranker
806    }
807
808    /// Apply one adjust hook to a raw edge weight. `None` keeps base;
809    /// veto/non-positive/non-finite drops the edge (`None` here = drop).
810// trace:exempt reason=internal-detail
811    fn adjust_edge(
812        adjust: &impl for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
813        subject: &str,
814        predicate: &str,
815        object: &str,
816        base: f64,
817    ) -> Option<f64> {
818        match adjust(subject, predicate, object, base) {
819            None => Some(base),
820            Some((mode, value)) => match mode.as_str() {
821                "add" => Some(base + value),
822                "multiply" => Some(base * value),
823                "replace" => Some(value),
824                "veto" => None,
825                _ => Some(base),
826            }
827            .filter(|w| w.is_finite() && *w > 0.0),
828        }
829    }
830
831    /// Rankable entity ids in rank order (index i in every vector maps to
832    /// `nodes()[i]`; symbols are a subset).
833// trace:exempt reason=internal-detail
834    pub fn nodes(&self) -> &[String] {
835        &self.nodes
836    }
837
838    /// Entity kinds parallel to [`SystemRanker::nodes`] (index i in
839    /// every vector maps to `kinds()[i]`). Powers `ranking.universe`.
840    // trace:exempt reason=internal-detail
841    pub fn kinds(&self) -> &[String] {
842        &self.kinds
843    }
844
845    /// Surviving rank-universe edges as (subject, predicate, object,
846    /// base weight): pre-aggregation triples, id-resolved. Deterministic
847    /// (construction order). Powers `ranking.edges` (§123.12).
848    // trace:exempt reason=internal-detail
849    pub fn rank_edges(&self) -> Vec<(String, String, String, f64)> {
850        self.edge_list
851            .iter()
852            .map(|(si, ti, pred, w)| (self.nodes[*si].clone(), pred.clone(), self.nodes[*ti].clone(), *w))
853            .collect()
854    }
855
856    /// Ranked symbol entity ids (subset of [`SystemRanker::nodes`]),
857    /// id-sorted. Symbol-only callers use this to walk the symbol slice of
858    /// the heterogeneous vectors.
859// trace:exempt reason=internal-detail
860    pub fn symbols(&self) -> Vec<String> {
861        self.nodes
862            .iter()
863            .zip(self.kinds.iter())
864            .filter(|(_, k)| k.as_str() == kinds::SYMBOL)
865            .map(|(id, _)| id.clone())
866            .collect()
867    }
868
869    /// Node index for a rankable entity id, if it is in the universe.
870// trace:exempt reason=internal-detail
871    pub fn index_of(&self, id: &str) -> Option<usize> {
872        self.index.get(id).copied()
873    }
874
875    /// Distinct referencing sources of a node id (0 when absent).
876// trace:exempt reason=internal-detail
877    pub fn in_degree(&self, id: &str) -> usize {
878        self.index
879            .get(id)
880            .map(|i| self.in_degree[*i])
881            .unwrap_or(0)
882    }
883
884    /// Precomputed global PageRank vector (architecturally seeded; see
885    /// [`SystemRanker::global_personalization`]). Main caches the ranker per
886    /// epoch and reads this.
887// trace:exempt reason=internal-detail
888    pub fn global_vector(&self) -> Vec<f64> {
889        self.global.clone()
890    }
891
892    /// Project the heterogeneous node scores to surface-relevant symbol
893    /// scores: a symbol's score = its own score + 0.4 × the sum of the
894    /// scores of the entities it OWNS/REGISTERS/PUBLISHES/READS/WRITES/
895    /// PARTICIPATES_IN/HANDLES/DEFINES, with the total bonus capped at
896    /// 0.5. This is how component/flow/contract/state importance reaches
897    /// the surface — and, via the HANDLES/DEFINES projection, how a hot
898    /// Route/Schema node (task seed or PPR) reaches its handler/definer
899    /// symbol. Deterministic: nodes and projection edges are id-sorted.
900    ///
901    /// Returns `(symbol id, projected score)` pairs, id-sorted.
902// trace:v1 id=impl.scc.pagerank.project-to-symbols work=WORK-SCC-014 satisfies=REQ-SCC-IR
903    pub fn project_to_symbols(&self, vector: &[f64]) -> Vec<(String, f64)> {
904        let mut scores: Vec<f64> = Vec::with_capacity(self.nodes.len());
905        for (i, _id) in self.nodes.iter().enumerate() {
906            let own = vector.get(i).copied().unwrap_or(0.0);
907            let mut bonus = 0.0;
908            for &j in self.projection[i].iter() {
909                bonus += vector.get(j).copied().unwrap_or(0.0);
910            }
911            scores.push(own + (PROJECTION_BONUS_FACTOR * bonus).min(PROJECTION_BONUS_CAP));
912        }
913        let mut out: Vec<(String, f64)> = Vec::new();
914        for (i, id) in self.nodes.iter().enumerate() {
915            if self.kinds[i] == kinds::SYMBOL {
916                out.push((id.clone(), scores[i]));
917            }
918        }
919        out
920    }
921
922    /// Task-personalized PageRank vector. Seeds come from `TaskSeed`s whose
923    /// `id` is a rankable entity id; the seed vector carries `weight` at
924    /// each matched index. Warm start: 0.3 × global + 0.7 × normalized
925    /// seeds. Unresolvable seed sets fall back to the global vector (never
926    /// panic).
927// trace:exempt reason=internal-detail
928    pub fn task_vector(&self, seeds: &[TaskSeed]) -> Vec<f64> {
929        let n = self.nodes.len();
930        if n == 0 {
931            return Vec::new();
932        }
933        let mut s = vec![0.0; n];
934        let mut found = false;
935        for seed in seeds {
936            if let Some(&i) = self.index.get(&seed.id) {
937                s[i] += seed.weight.max(0.0);
938                found = true;
939            }
940        }
941        if !found {
942            return self.global.clone();
943        }
944        let sum: f64 = s.iter().sum();
945        if sum <= 0.0 {
946            return self.global.clone();
947        }
948        let s: Vec<f64> = s.iter().map(|v| v / sum).collect();
949        let mut start = vec![0.0; n];
950        for i in 0..n {
951            start[i] = WARM_START_GLOBAL_BLEND * self.global[i] + (1.0 - WARM_START_GLOBAL_BLEND) * s[i];
952        }
953        Self::ppr_with(&self.adjacency, &s, &start)
954    }
955
956    /// Architectural seed vector (weight 1.0 per seed): exported symbols,
957    /// symbols with entrypoint attributes, invocation-surface handlers,
958    /// primary entrypoints, state owners, contract producers, and flow
959    /// entrypoints. Applied to every node id (non-symbol nodes simply do
960    /// not carry symbol attributes).
961// trace:exempt reason=internal-detail
962    fn global_personalization(&self) -> Vec<f64> {
963        let n = self.nodes.len();
964        let mut seeds = vec![0.0; n];
965        // Flow entrypoints: flows record the entry symbol id in attributes.
966        let mut flow_eps: HashSet<String> = HashSet::new();
967        for f in self.view.flows() {
968            if let Some(ep) = f.attributes.get("entrypoint").and_then(|v| v.as_str()) {
969                flow_eps.insert(ep.to_string());
970            }
971        }
972        for (i, id) in self.nodes.iter().enumerate() {
973            let mut seed = false;
974            if let Some(e) = self.view.entity(id) {
975                let exported = e
976                    .attributes
977                    .get("exported")
978                    .and_then(|v| v.as_bool())
979                    .unwrap_or(false);
980                let has_entrypoints = e
981                    .attributes
982                    .get("entrypoints")
983                    .and_then(|v| v.as_array())
984                    .map(|a| !a.is_empty())
985                    .unwrap_or(false);
986                seed = seed || exported || has_entrypoints;
987            }
988            if flow_eps.contains(id.as_str()) {
989                seed = true;
990            }
991            for r in self.view.out_edges(id) {
992                let tk = self
993                    .view
994                    .entity(&r.object)
995                    .map(|e| e.kind.as_str())
996                    .unwrap_or("");
997                match r.predicate.as_str() {
998                    predicates::HANDLES if INVOCATION_KINDS.contains(&tk) => seed = true,
999                    predicates::OWNS if tk == kinds::STATE || tk == kinds::REACTIVE => seed = true,
1000                    predicates::REGISTERS | predicates::PUBLISHES
1001                        if CONTRACT_KINDS.contains(&tk) =>
1002                    {
1003                        seed = true;
1004                    }
1005                    _ => {}
1006                }
1007            }
1008            if seed {
1009                seeds[i] = 1.0;
1010            }
1011        }
1012        seeds
1013    }
1014
1015    /// Personalized power iteration: `r' = (1-d)·s + d·(M^T r +
1016    /// dangling/n)`, `POWER_ITERATIONS` times, starting from `start`.
1017// trace:exempt reason=internal-detail
1018    fn ppr_with(adjacency: &[Vec<(usize, f64)>], personalization: &[f64], start: &[f64]) -> Vec<f64> {
1019        let n = adjacency.len();
1020        if n == 0 {
1021            return Vec::new();
1022        }
1023        let mut r = start.to_vec();
1024        let d = DAMPING_FACTOR;
1025        for _ in 0..POWER_ITERATIONS {
1026            let mut nr = vec![0.0; n];
1027            let mut dangling = 0.0;
1028            for (i, row) in adjacency.iter().enumerate() {
1029                let ri = r[i];
1030                if row.is_empty() {
1031                    dangling += ri;
1032                } else {
1033                    for (j, w) in row {
1034                        nr[*j] += ri * w;
1035                    }
1036                }
1037            }
1038            let d_mass = d * dangling / n as f64;
1039            for k in 0..n {
1040                nr[k] = (1.0 - d) * personalization[k] + d * nr[k] + d_mass;
1041            }
1042            r = nr;
1043        }
1044        r
1045    }
1046
1047    /// Global run: personalization = normalized architectural seeds; start
1048    /// state = the same seed distribution (uniform when no seed fires).
1049// trace:exempt reason=internal-detail
1050    fn ppr(adjacency: &[Vec<(usize, f64)>], seeds: &[f64]) -> Vec<f64> {
1051        let n = adjacency.len();
1052        if n == 0 {
1053            return Vec::new();
1054        }
1055        let sum: f64 = seeds.iter().sum();
1056        let s: Vec<f64> = if sum > 0.0 {
1057            seeds.iter().map(|v| v / sum).collect()
1058        } else {
1059            vec![1.0 / n as f64; n]
1060        };
1061        Self::ppr_with(adjacency, &s, &s)
1062    }
1063}
1064
1065// ---------------------------------------------------------------------------
1066// Architectural specificity
1067// ---------------------------------------------------------------------------
1068
1069/// Is the qualified name or path segment a generic utility token?
1070// trace:exempt reason=internal-detail
1071fn is_generic_utility(qualified_name: &str, path: &str) -> bool {
1072    let mut segs: Vec<String> = Vec::new();
1073    for part in path.split('/') {
1074        for piece in part.split('.') {
1075            if !piece.is_empty() {
1076                segs.push(piece.to_lowercase());
1077            }
1078        }
1079    }
1080    for part in qualified_name.split(['.', ':']) {
1081        if !part.is_empty() {
1082            segs.push(part.to_lowercase());
1083        }
1084    }
1085    segs.iter().any(|s| GENERIC_NAMES.contains(&s.as_str()))
1086}
1087
1088/// Does the path point into test code (`test`/`tests`/`spec` segments,
1089/// `test_*`/`*_test` files)?
1090// trace:exempt reason=internal-detail
1091fn path_has_test(path: &str) -> bool {
1092    path.split('/').any(|seg| {
1093        let seg = seg.to_lowercase();
1094        seg == "test"
1095            || seg == "tests"
1096            || seg == "spec"
1097            || seg.starts_with("test_")
1098            || seg.ends_with("_test")
1099    })
1100}
1101
1102/// Does the path point at generated code (`generated`/`gen_*` segments,
1103/// protobuf markers)?
1104// trace:exempt reason=internal-detail
1105fn is_generated(path: &str) -> bool {
1106    path.split('/').any(|seg| {
1107        let seg = seg.to_lowercase();
1108        seg.contains("generated")
1109            || seg.starts_with("gen_")
1110            || seg.ends_with("_pb2.py")
1111            || seg.ends_with("_pb.go")
1112    })
1113}
1114
1115/// Does the path point into vendored code?
1116// trace:exempt reason=internal-detail
1117fn is_vendored(path: &str) -> bool {
1118    path.split('/').any(|seg| {
1119        matches!(
1120            seg,
1121            "vendor" | "third_party" | "node_modules" | ".venv" | "site-packages" | "bower_components"
1122        )
1123    })
1124}
1125
1126/// Ubiquity: distinct referencing sources of the symbol in the reference
1127/// graph (only predicates that normalize to a reference kind).
1128// trace:exempt reason=internal-detail
1129fn ubiquity(view: &TrustedGraphView, symbol_id: &str) -> usize {
1130    let mut sources: HashSet<&str> = HashSet::new();
1131    for r in view.in_edges(symbol_id) {
1132        let tk = view
1133            .entity(&r.object)
1134            .map(|e| e.kind.as_str())
1135            .unwrap_or("");
1136        if reference_kind(&r.predicate, tk).is_some() {
1137            sources.insert(&r.subject);
1138        }
1139    }
1140    sources.len()
1141}
1142
1143/// Architectural specificity of a surface entry: a multiplier in
1144/// [0.25, 1.25] that boosts public facades/entrypoints/state owners/
1145/// contract endpoints/flow participants and penalizes generic utilities,
1146/// test/generated/vendored code, and ubiquitous symbols (in-degree above
1147/// [`UBIQUITY_THRESHOLD`]). Apply it to PPR values before blending so
1148/// central utilities never dominate the ranking.
1149// trace:exempt reason=internal-detail
1150pub fn architectural_specificity(entry: &SurfaceEntry, view: &TrustedGraphView) -> f64 {
1151    let mut score: f64 = 1.0;
1152    if entry.exported {
1153        score += 0.15;
1154    }
1155    if !entry.invocation_surfaces.is_empty() {
1156        score += 0.10;
1157    }
1158    if !entry.state_authorities.is_empty() {
1159        score += 0.10;
1160    }
1161    if !entry.contracts.is_empty() {
1162        score += 0.10;
1163    }
1164    if !entry.flows.is_empty() {
1165        score += 0.05;
1166    }
1167    // Public facade: exported member of a component.
1168    if entry.exported && entry.component.is_some() {
1169        score += 0.05;
1170    }
1171
1172    let mut factor = 1.0;
1173    if is_generic_utility(&entry.qualified_name, &entry.path) {
1174        factor *= 0.5;
1175    }
1176    if path_has_test(&entry.path) {
1177        factor *= 0.5;
1178    }
1179    if is_generated(&entry.path) {
1180        factor *= 0.5;
1181    }
1182    if is_vendored(&entry.path) {
1183        factor *= 0.5;
1184    }
1185    if ubiquity(view, &entry.symbol_id) > UBIQUITY_THRESHOLD {
1186        factor *= 0.5;
1187    }
1188
1189    (score * factor).clamp(0.25, 1.25)
1190}
1191
1192// ---------------------------------------------------------------------------
1193// Final importance blend
1194// ---------------------------------------------------------------------------
1195
1196/// 30% task PPR.
1197pub const TASK_PPR_WEIGHT: f64 = 0.30;
1198/// 20% global PPR (50% when no task focus).
1199pub const GLOBAL_PPR_WEIGHT: f64 = 0.20;
1200/// 15% lexical overlap.
1201pub const LEXICAL_WEIGHT: f64 = 0.15;
1202/// 10% semantic relevance.
1203pub const SEMANTIC_WEIGHT: f64 = 0.10;
1204/// 10% evidence confidence.
1205pub const CONFIDENCE_WEIGHT: f64 = 0.10;
1206/// 10% criticality.
1207pub const CRITICALITY_WEIGHT: f64 = 0.10;
1208/// 5% change/risk.
1209pub const CHANGE_RISK_WEIGHT: f64 = 0.05;
1210/// Novelty bonus weight (additive term).
1211pub const NOVELTY_WEIGHT: f64 = 0.05;
1212/// Global PPR weight when there is no task focus (task share moves to
1213/// global: 20% + 30% = 50%).
1214pub const NO_TASK_GLOBAL_WEIGHT: f64 = 0.50;
1215
1216/// The spec's final importance blend. All inputs are expected in [0, 1];
1217/// the novelty term is additive on top. With no task focus, the task-PPR
1218/// share moves to the global vector (global gets 50%).
1219///
1220/// `has_task` — is this ranking task-focused (true) or the startup/global
1221/// surface (false)?
1222#[allow(clippy::too_many_arguments)]
1223// trace:exempt reason=internal-detail
1224pub fn final_importance(
1225    task_ppr: f64,
1226    global_ppr: f64,
1227    lexical: f64,
1228    semantic: f64,
1229    confidence: f64,
1230    criticality: f64,
1231    change_risk: f64,
1232    novelty: f64,
1233    has_task: bool,
1234) -> f64 {
1235    let (tw, gw) = if has_task {
1236        (TASK_PPR_WEIGHT, GLOBAL_PPR_WEIGHT)
1237    } else {
1238        (0.0, NO_TASK_GLOBAL_WEIGHT)
1239    };
1240    tw * task_ppr
1241        + gw * global_ppr
1242        + LEXICAL_WEIGHT * lexical
1243        + SEMANTIC_WEIGHT * semantic
1244        + CONFIDENCE_WEIGHT * confidence
1245        + CRITICALITY_WEIGHT * criticality
1246        + CHANGE_RISK_WEIGHT * change_risk
1247        + NOVELTY_WEIGHT * novelty
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252    use super::*;
1253    use scc_core::{entity_id, Entity, Relationship, Visibility};
1254    use scc_graph::{RealityGraph, TrustPolicy};
1255    use scc_store::Store;
1256    use std::collections::HashMap;
1257
1258// trace:exempt reason=internal-detail
1259    fn fixture(
1260        entities: Vec<Entity>,
1261        rels: Vec<Relationship>,
1262    ) -> (tempfile::TempDir, Store, RealityGraph) {
1263        let dir = tempfile::TempDir::new().unwrap();
1264        let root = dir.path().join("repo");
1265        std::fs::create_dir_all(&root).unwrap();
1266        let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
1267        for e in &entities {
1268            store.insert_entity(e, &["src/fixture.ts".to_string()]).unwrap();
1269        }
1270        for r in &rels {
1271            store.insert_relationship(r, "src/fixture.ts").unwrap();
1272        }
1273        let mut out: HashMap<String, Vec<Relationship>> = HashMap::new();
1274        let mut inn: HashMap<String, Vec<Relationship>> = HashMap::new();
1275        for r in &rels {
1276            out.entry(r.subject.clone()).or_default().push(r.clone());
1277            inn.entry(r.object.clone()).or_default().push(r.clone());
1278        }
1279        let graph = RealityGraph {
1280            repo_id: "r".into(),
1281            entities: entities.into_iter().map(|e| (e.id.clone(), e)).collect(),
1282            out,
1283            inn,
1284            components: vec![],
1285            flows: vec![],
1286            invariants: vec![],
1287        };
1288        (dir, store, graph)
1289    }
1290
1291// trace:exempt reason=internal-detail
1292    fn sym(repo: &str, path: &str, name: &str) -> Entity {
1293        let mut e = Entity::new(
1294            scc_core::symbol_id(repo, path, name),
1295            kinds::SYMBOL,
1296            name,
1297        );
1298        e.attr("file", serde_json::json!(path));
1299        e.attr("exported", serde_json::json!(false));
1300        e.attr("start_line", serde_json::json!(1));
1301        e.attr("end_line", serde_json::json!(10));
1302        e
1303    }
1304
1305// trace:exempt reason=internal-detail
1306    fn entity(id: &str, kind: &str, name: &str) -> Entity {
1307        Entity::new(id, kind, name)
1308    }
1309
1310// trace:exempt reason=internal-detail
1311    fn rel(n: u64, subject: &str, pred: &str, object: &str, prov: Provenance) -> Relationship {
1312        Relationship::new(format!("rel:{n}"), subject, pred, object, prov)
1313    }
1314
1315    // ---- (a) reference normalization maps the right kinds ----
1316
1317    #[test]
1318// trace:exempt reason=internal-detail
1319    fn reference_normalization_maps_kinds() {
1320        let (a, b, i, c, s, t, m, x, d) = (
1321            scc_core::symbol_id("r", "src/a.ts", "A"),
1322            scc_core::symbol_id("r", "src/b.ts", "B"),
1323            scc_core::symbol_id("r", "src/i.ts", "I"),
1324            "repo://r/contract/c",
1325            "repo://r/state/s",
1326            "repo://r/topic/t",
1327            "repo://r/module/m",
1328            "repo://r/export/x",
1329            "repo://r/annotation/d",
1330        );
1331        let mut a_ent = sym("r", "src/a.ts", "A");
1332        a_ent.attr("file", serde_json::json!("src/a.ts"));
1333        a_ent.attr("start_line", serde_json::json!(10));
1334        a_ent.attr("end_line", serde_json::json!(20));
1335        let b_ent = sym("r", "src/b.ts", "B");
1336        let i_ent = sym("r", "src/i.ts", "I");
1337        let c_ent = entity(c, kinds::CONTRACT, "c");
1338        let s_ent = entity(s, kinds::STATE, "s");
1339        let t_ent = entity(t, kinds::TOPIC, "t");
1340        let m_ent = entity(m, kinds::MODULE, "m");
1341        let x_ent = entity(x, kinds::EXPORT, "x");
1342        let d_ent = entity(d, kinds::ANNOTATION, "d");
1343
1344        let mut rels = vec![
1345            rel(1, &a, predicates::CALLS, &b, Provenance::Extracted),
1346            rel(2, &a, predicates::INVOKES, &b, Provenance::Extracted),
1347            rel(3, &a, predicates::HANDLES_CALLBACK, &b, Provenance::Extracted),
1348            rel(4, &a, predicates::IMPLEMENTS, &i, Provenance::Extracted),
1349            rel(5, &i, predicates::IMPLEMENTED_BY, &a, Provenance::Extracted),
1350            rel(6, &a, predicates::INHERITS, &b, Provenance::Extracted),
1351            rel(7, &a, predicates::REGISTERS, c, Provenance::Extracted),
1352            rel(8, &a, predicates::PUBLISHES, t, Provenance::Extracted),
1353            rel(9, &b, predicates::SUBSCRIBES, t, Provenance::Extracted),
1354            rel(10, &a, predicates::OWNS, s, Provenance::Extracted),
1355            rel(11, &a, predicates::READS, s, Provenance::Extracted),
1356            rel(12, &b, predicates::WRITES, s, Provenance::Extracted),
1357            rel(13, &a, predicates::IMPORTS, m, Provenance::Extracted),
1358            rel(14, &a, predicates::EXPORTS, x, Provenance::Extracted),
1359            rel(15, &a, predicates::DECORATES, d, Provenance::Extracted),
1360            // OWNS of a non-state entity must NOT normalize to Write.
1361            rel(16, &a, predicates::OWNS, c, Provenance::Extracted),
1362            // Confidence passthrough.
1363            rel(17, &a, predicates::CALLS, d, Provenance::Extracted),
1364        ];
1365        rels[16].confidence = 0.7;
1366        let (_dir, store, graph) = fixture(
1367            vec![a_ent.clone(), b_ent, i_ent, c_ent, s_ent, t_ent, m_ent, x_ent, d_ent],
1368            rels,
1369        );
1370        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1371        let edges = build_reference_graph(&view);
1372
1373        let kinds_between = |src: &str, tgt: &str| -> Vec<ReferenceKind> {
1374            let mut v: Vec<ReferenceKind> = edges
1375                .iter()
1376                .filter(|e| e.source_symbol == src && e.target_symbol == tgt)
1377                .map(|e| e.kind)
1378                .collect();
1379            v.sort();
1380            v
1381        };
1382
1383        // calls/invokes/handles_callback → Call (3 edges) and
1384        // inherits → Extend (all A -> B edges).
1385        assert_eq!(
1386            kinds_between(&a, &b),
1387            vec![
1388                ReferenceKind::Call,
1389                ReferenceKind::Call,
1390                ReferenceKind::Call,
1391                ReferenceKind::Extend
1392            ]
1393        );
1394        // implements + implemented_by → Implement (both directions normalize
1395        // to A -> I).
1396        assert_eq!(kinds_between(&a, &i), vec![ReferenceKind::Implement; 2]);
1397        // registers/publishes/subscribes → Register.
1398        assert_eq!(kinds_between(&a, c), vec![ReferenceKind::Register]);
1399        assert_eq!(kinds_between(&a, t), vec![ReferenceKind::Register]);
1400        assert_eq!(kinds_between(&b, t), vec![ReferenceKind::Register]);
1401        // owns(state) → Write; reads → Read; writes → Write.
1402        let mut as_edges = kinds_between(&a, s);
1403        as_edges.sort();
1404        assert_eq!(as_edges, vec![ReferenceKind::Read, ReferenceKind::Write]);
1405        assert_eq!(kinds_between(&b, s), vec![ReferenceKind::Write]);
1406        // imports → Import; exports → Export; decorates → Decorate.
1407        assert_eq!(kinds_between(&a, m), vec![ReferenceKind::Import]);
1408        assert_eq!(kinds_between(&a, x), vec![ReferenceKind::Export]);
1409        assert_eq!(kinds_between(&a, d), vec![ReferenceKind::Call, ReferenceKind::Decorate]);
1410        // OWNS of a contract is NOT normalized (no Write A->C).
1411        assert!(!kinds_between(&a, c).contains(&ReferenceKind::Write));
1412
1413        // Locations: evidence file/line from the subject symbol attrs.
1414        let call = edges
1415            .iter()
1416            .find(|e| e.source_symbol == a && e.target_symbol == b && e.kind == ReferenceKind::Call)
1417            .unwrap();
1418        assert!(call.locations.contains(&SourceRange::new("src/a.ts", 10, 20)));
1419        assert!(call.locations.contains(&SourceRange::new("src/b.ts", 1, 10)));
1420
1421        // Confidence passthrough: default 1.0; explicit 0.7 preserved.
1422        assert_eq!(call.confidence, 1.0);
1423        let low = edges
1424            .iter()
1425            .find(|e| e.source_symbol == a && e.target_symbol == d && e.kind == ReferenceKind::Call)
1426            .unwrap();
1427        assert!((low.confidence - 0.7).abs() < 1e-6);
1428    }
1429
1430    // ---- (b) rarity downweights ubiquitous vs distinctive ----
1431
1432    #[test]
1433// trace:exempt reason=internal-detail
1434    fn rarity_downweights_ubiquitous() {
1435        // Distinctive target (in-degree 1) is far rarer than a ubiquitous
1436        // logger-ish target (in-degree 50).
1437        let logger = rarity(100, 50);
1438        let distinctive = rarity(100, 1);
1439        assert!(logger < distinctive);
1440        assert!((logger - (100.0_f64 / 51.0).ln()).abs() < 1e-9);
1441
1442        // Clamp bounds.
1443        assert_eq!(rarity(100, 10_000), 0.25);
1444        assert_eq!(rarity(100, 0), 1.5);
1445        assert_eq!(rarity(0, 0), 1.0);
1446
1447        // Full edge weight: ubiquitous target downweights the edge.
1448        let w_common = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 50);
1449        let w_rare = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 1);
1450        assert!(w_common < w_rare);
1451
1452        // Provenance: STALE weighs zero; RESOLVED calls weigh more than
1453        // EXTRACTED calls.
1454        assert_eq!(edge_weight(predicates::CALLS, Provenance::Stale, 1.0, 100, 1), 0.0);
1455        let resolved = edge_weight(predicates::CALLS, Provenance::Resolved, 1.0, 100, 1);
1456        let extracted = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 1);
1457        assert!(resolved > extracted);
1458    }
1459
1460    // ---- (c) central utility pollution is corrected by specificity ----
1461
1462    #[test]
1463// trace:exempt reason=internal-detail
1464    fn utility_pollution_ranks_below_public_api() {
1465        let api_id = scc_core::symbol_id("r", "src/api/order.ts", "OrderApi");
1466        let util_id = scc_core::symbol_id("r", "src/utils/helpers.ts", "helpers");
1467        let mut api_ent = sym("r", "src/api/order.ts", "OrderApi");
1468        api_ent.attr("exported", serde_json::json!(true));
1469        api_ent.attr("entrypoints", serde_json::json!(["http"]));
1470        let util_ent = sym("r", "src/utils/helpers.ts", "helpers");
1471
1472        let mut entities = vec![api_ent.clone(), util_ent.clone()];
1473        let mut rels: Vec<Relationship> = Vec::new();
1474        let mut n = 1u64;
1475        // 40 callers all invoke the util symbol.
1476        for i in 0..40 {
1477            let caller = scc_core::symbol_id("r", "src/callers/c.rs", &format!("c{i}"));
1478            entities.push(sym("r", "src/callers/c.rs", &format!("c{i}")));
1479            rels.push(rel(
1480                n,
1481                &caller,
1482                predicates::CALLS,
1483                &util_id,
1484                Provenance::Extracted,
1485            ));
1486            n += 1;
1487        }
1488        // Two callers reach the public API.
1489        let c1 = scc_core::symbol_id("r", "src/callers/c.rs", "c0");
1490        let c2 = scc_core::symbol_id("r", "src/callers/c.rs", "c1");
1491        rels.push(rel(n, &c1, predicates::CALLS, &api_id, Provenance::Extracted));
1492        n += 1;
1493        rels.push(rel(n, &c2, predicates::CALLS, &api_id, Provenance::Extracted));
1494
1495        let (_dir, store, graph) = fixture(entities, rels);
1496        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1497        let ranker = SystemRanker::new(&view);
1498        let g = ranker.global_vector();
1499        let util_idx = ranker.index_of(&util_id).unwrap();
1500        let api_idx = ranker.index_of(&api_id).unwrap();
1501
1502        // Specificity: util is generic + ubiquitous (40 distinct callers >
1503        // threshold) → floor; api is an exported entrypoint facade → cap.
1504        let util_entry = surface_entry(&util_id, "helpers", "src/utils/helpers.ts", false);
1505        let api_entry = surface_entry(&api_id, "OrderApi", "src/api/order.ts", true);
1506        let spec_util = architectural_specificity(&util_entry, &view);
1507        let spec_api = architectural_specificity(&api_entry, &view);
1508        assert_eq!(spec_util, 0.25);
1509        assert_eq!(spec_api, 1.25);
1510        assert!(spec_api > spec_util);
1511
1512        // After specificity, the public API ranks above the central utility
1513        // even though the utility is referenced by far more callers.
1514        assert!(g[api_idx] * spec_api > g[util_idx] * spec_util);
1515    }
1516
1517    // ---- (d) task personalization lifts billing seeds above unrelated ----
1518
1519    #[test]
1520// trace:exempt reason=internal-detail
1521    fn task_personalization_ranks_billing_above_unrelated() {
1522        let billing_client = scc_core::symbol_id("r", "src/billing/client.ts", "BillingClient");
1523        let billing_retry = scc_core::symbol_id("r", "src/billing/client.ts", "BillingClient.retry");
1524        let billing_worker = scc_core::symbol_id("r", "src/billing/worker.ts", "BillingWorker");
1525        let billing_process = scc_core::symbol_id("r", "src/billing/worker.ts", "BillingWorker.process");
1526        let auth_service = scc_core::symbol_id("r", "src/auth/service.ts", "AuthService");
1527        let auth_login = scc_core::symbol_id("r", "src/auth/service.ts", "AuthService.login");
1528        let logger = scc_core::symbol_id("r", "src/logger.ts", "Logger");
1529        let logger_log = scc_core::symbol_id("r", "src/logger.ts", "Logger.log");
1530
1531        let entities = vec![
1532            sym("r", "src/billing/client.ts", "BillingClient"),
1533            sym("r", "src/billing/client.ts", "BillingClient.retry"),
1534            sym("r", "src/billing/worker.ts", "BillingWorker"),
1535            sym("r", "src/billing/worker.ts", "BillingWorker.process"),
1536            sym("r", "src/auth/service.ts", "AuthService"),
1537            sym("r", "src/auth/service.ts", "AuthService.login"),
1538            sym("r", "src/logger.ts", "Logger"),
1539            sym("r", "src/logger.ts", "Logger.log"),
1540        ];
1541        let rels = vec![
1542            rel(1, &billing_client, predicates::CALLS, &billing_retry, Provenance::Extracted),
1543            rel(2, &billing_worker, predicates::CALLS, &billing_process, Provenance::Extracted),
1544            rel(3, &billing_process, predicates::CALLS, &billing_retry, Provenance::Extracted),
1545            rel(4, &auth_service, predicates::CALLS, &auth_login, Provenance::Extracted),
1546            rel(5, &logger, predicates::CALLS, &logger_log, Provenance::Extracted),
1547        ];
1548
1549        let (_dir, store, graph) = fixture(entities, rels);
1550        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1551        let ranker = SystemRanker::new(&view);
1552
1553        let seeds = vec![
1554            TaskSeed {
1555                kind: "symbol".into(),
1556                id: billing_retry.clone(),
1557                weight: 1.0,
1558            },
1559            TaskSeed {
1560                kind: "symbol".into(),
1561                id: billing_process.clone(),
1562                weight: 1.0,
1563            },
1564        ];
1565        let tv = ranker.task_vector(&seeds);
1566        let i_retry = ranker.index_of(&billing_retry).unwrap();
1567        let i_process = ranker.index_of(&billing_process).unwrap();
1568        let i_login = ranker.index_of(&auth_login).unwrap();
1569        let i_log = ranker.index_of(&logger_log).unwrap();
1570
1571        // Billing seeds rank above unrelated symbols.
1572        assert!(tv[i_retry] > tv[i_login]);
1573        assert!(tv[i_retry] > tv[i_log]);
1574        assert!(tv[i_process] > tv[i_login]);
1575        assert!(tv[i_process] > tv[i_log]);
1576        // Both seeds beat a symbol that merely calls into the unrelated
1577        // cluster's leaf.
1578        assert!(tv[i_retry] > tv[ranker.index_of(&logger).unwrap()]);
1579
1580        // Unresolvable seeds degrade to the global vector (no panic).
1581        let junk = vec![TaskSeed { kind: "symbol".into(), id: "repo://r/symbol/nope/Nope".into(), weight: 1.0 }];
1582        let fallback = ranker.task_vector(&junk);
1583        let g = ranker.global_vector();
1584        assert_eq!(fallback, g);
1585        assert_eq!(fallback.len(), tv.len());
1586    }
1587
1588    // ---- helper for SurfaceEntry fixtures ----
1589
1590// trace:exempt reason=internal-detail
1591    fn surface_entry(symbol_id: &str, name: &str, path: &str, exported: bool) -> SurfaceEntry {
1592        let mut entry = SurfaceEntry {
1593            id: symbol_id.to_string(),
1594            symbol_id: symbol_id.to_string(),
1595            qualified_name: name.to_string(),
1596            kind: scc_core::SurfaceKind::Function,
1597            path: path.to_string(),
1598            range: SourceRange::new(path, 1, 1),
1599            source_signature: String::new(),
1600            canonical_signature: String::new(),
1601            semantic_signature: scc_core::SemanticSignature::default(),
1602            visibility: Visibility::Public,
1603            exported,
1604            modifiers: vec![],
1605            annotations: vec![],
1606            component: Some("Order".to_string()),
1607            subsystem: None,
1608            flows: vec![],
1609            contracts: vec![],
1610            state_authorities: vec![],
1611            invocation_surfaces: vec![],
1612            callers: vec![],
1613            callees: vec![],
1614            caller_count: 0,
1615            callee_count: 0,
1616            importance: None,
1617            provenance: Provenance::Extracted,
1618            confidence: 1.0,
1619            rank: scc_core::SurfaceRank::default(),
1620        };
1621        if exported {
1622            entry.invocation_surfaces.push("http".into());
1623            entry.flows.push("f1".into());
1624            entry.contracts.push("c1".into());
1625        }
1626        entry
1627    }
1628
1629    // ---- final_importance blend ----
1630
1631    #[test]
1632// trace:exempt reason=internal-detail
1633    fn final_importance_blend_constants() {
1634        // All signals at 1.0 with task focus: 0.30+0.20+0.15+0.10+0.10+
1635        // 0.10+0.05+0.05 = 1.05 (novelty is additive).
1636        let v = final_importance(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, true);
1637        assert!((v - 1.05).abs() < 1e-9);
1638
1639        // Task focus weights task PPR over global.
1640        let tasky = final_importance(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true);
1641        assert!((tasky - 0.30).abs() < 1e-9);
1642
1643        // No task: task share moves to global (50%).
1644        let globaly = final_importance(1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false);
1645        assert!((globaly - 0.25).abs() < 1e-9);
1646
1647        // No task with full global: 0.50 + 0.15 + 0.10 + 0.10 + 0.10 + 0.05
1648        // + 0.05 = 1.05.
1649        let full = final_importance(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, false);
1650        assert!((full - 1.05).abs() < 1e-9);
1651    }
1652
1653    // ---- architectural_specificity penalties ----
1654
1655    #[test]
1656// trace:exempt reason=internal-detail
1657    fn specificity_penalizes_generic_test_and_vendored() {
1658        let (_dir, store, graph) = fixture(vec![sym("r", "src/x.ts", "X")], vec![]);
1659        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1660
1661        let util = surface_entry("s1", "logger", "src/utils/logger.ts", false);
1662        // Generic utility name/path → 0.5 (symbol "s1" is not in the graph,
1663        // so no ubiquity penalty; 0.5 is the single-penalty floor here).
1664        assert_eq!(architectural_specificity(&util, &view), 0.5);
1665
1666        let testy = surface_entry("s2", "TestHelper", "tests/unit/helper_test.rs", false);
1667        assert_eq!(architectural_specificity(&testy, &view), 0.5);
1668
1669        let vendored = surface_entry("s3", "Dep", "vendor/dep/src/lib.rs", false);
1670        assert_eq!(architectural_specificity(&vendored, &view), 0.5);
1671
1672        let generated = surface_entry("s4", "Proto", "src/generated/models.rs", false);
1673        assert_eq!(architectural_specificity(&generated, &view), 0.5);
1674
1675        // Plain exported facade with no penalties hits the cap.
1676        let facade = surface_entry("s5", "OrderApi", "src/api/order.ts", true);
1677        assert_eq!(architectural_specificity(&facade, &view), 1.25);
1678    }
1679
1680    // ---- heterogeneous universe: non-symbol entities participate ----
1681
1682    #[test]
1683// trace:exempt reason=internal-detail
1684    fn ranker_universe_is_heterogeneous() {
1685        let owner = scc_core::symbol_id("r", "src/a.ts", "A");
1686        let state_id = entity_id("r", kinds::STATE, "sessions");
1687        let contract_id = entity_id("r", kinds::CONTRACT, "c1");
1688        let topic_id = entity_id("r", kinds::TOPIC, "orders");
1689        let store_id = entity_id("r", kinds::DATA_STORE, "pg");
1690        let flow_id = entity_id("r", kinds::FLOW, "signup");
1691        let file_id = entity_id("r", kinds::FILE, "src/a.ts");
1692
1693        let mut owner_ent = sym("r", "src/a.ts", "A");
1694        owner_ent.attr("exported", serde_json::json!(true));
1695        owner_ent.attr("entrypoints", serde_json::json!(["http"]));
1696        let entities = vec![
1697            owner_ent,
1698            entity(&state_id, kinds::STATE, "sessions"),
1699            entity(&contract_id, kinds::CONTRACT, "c1"),
1700            entity(&topic_id, kinds::TOPIC, "orders"),
1701            entity(&store_id, kinds::DATA_STORE, "pg"),
1702            entity(&flow_id, kinds::FLOW, "signup"),
1703            entity(&file_id, kinds::FILE, "src/a.ts"),
1704        ];
1705        let n = 1u64;
1706        let rels = vec![
1707            rel(n, &owner, predicates::OWNS, &state_id, Provenance::Extracted),
1708            rel(n + 1, &owner, predicates::REGISTERS, &contract_id, Provenance::Extracted),
1709            rel(n + 2, &owner, predicates::PUBLISHES, &topic_id, Provenance::Extracted),
1710            rel(n + 3, &owner, predicates::READS, &store_id, Provenance::Extracted),
1711            rel(n + 4, &owner, predicates::WRITES, &store_id, Provenance::Extracted),
1712            rel(n + 5, &owner, predicates::PARTICIPATES_IN, &flow_id, Provenance::Extracted),
1713        ];
1714
1715        let (_dir, store, graph) = fixture(entities, rels);
1716        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1717        let ranker = SystemRanker::new(&view);
1718
1719        // Every rankable entity kind is a node.
1720        for id in [
1721            owner.as_str(),
1722            state_id.as_str(),
1723            contract_id.as_str(),
1724            topic_id.as_str(),
1725            store_id.as_str(),
1726            flow_id.as_str(),
1727            file_id.as_str(),
1728        ] {
1729            assert!(ranker.index_of(id).is_some(), "{id} must be a node");
1730        }
1731        // Non-rankable kinds (module) are not nodes.
1732        assert!(ranker.index_of(&entity_id("r", kinds::MODULE, "m")).is_none());
1733
1734        // The heterogeneous vectors are indexed over all nodes.
1735        let g = ranker.global_vector();
1736        assert_eq!(g.len(), ranker.nodes().len());
1737
1738        // Non-symbol nodes carry real PPR mass (the edges feed them).
1739        let owner_i = ranker.index_of(&owner).unwrap();
1740        let state_i = ranker.index_of(&state_id).unwrap();
1741        assert!(g[state_i] > 0.0, "state node must receive PPR mass");
1742        assert!(g[owner_i] > 0.0);
1743
1744        // Symbols() returns exactly the symbol nodes.
1745        let syms = ranker.symbols();
1746        assert_eq!(syms, vec![owner.clone()]);
1747    }
1748
1749    #[test]
1750// trace:exempt reason=internal-detail
1751    fn project_to_symbols_lifts_owned_entity_scores() {
1752        let owner = scc_core::symbol_id("r", "src/a.ts", "A");
1753        let other = scc_core::symbol_id("r", "src/b.ts", "B");
1754        let state_id = entity_id("r", kinds::STATE, "sessions");
1755        let contract_id = entity_id("r", kinds::CONTRACT, "c1");
1756
1757        let mut owner_ent = sym("r", "src/a.ts", "A");
1758        owner_ent.attr("exported", serde_json::json!(true));
1759        owner_ent.attr("entrypoints", serde_json::json!(["http"]));
1760        let entities = vec![
1761            owner_ent,
1762            sym("r", "src/b.ts", "B"),
1763            entity(&state_id, kinds::STATE, "sessions"),
1764            entity(&contract_id, kinds::CONTRACT, "c1"),
1765        ];
1766        let rels = vec![
1767            rel(1, &owner, predicates::OWNS, &state_id, Provenance::Extracted),
1768            rel(2, &owner, predicates::REGISTERS, &contract_id, Provenance::Extracted),
1769            // B's OWNS of the same state also projects (both owners lift)
1770            rel(3, &other, predicates::OWNS, &state_id, Provenance::Extracted),
1771        ];
1772        let (_dir, store, graph) = fixture(entities, rels);
1773        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1774        let ranker = SystemRanker::new(&view);
1775
1776        let g = ranker.global_vector();
1777        let projected = ranker.project_to_symbols(&g);
1778        let score_of = |id: &str| -> f64 {
1779            projected
1780                .iter()
1781                .find(|(s, _)| s == id)
1782                .map(|(_, v)| *v)
1783                .unwrap_or(f64::NAN)
1784        };
1785
1786        let own_a = score_of(&owner);
1787        let own_b = score_of(&other);
1788        let state_score = g[ranker.index_of(&state_id).unwrap()];
1789        let contract_score = g[ranker.index_of(&contract_id).unwrap()];
1790
1791        // A owns state + registers contract → bonus = 0.4 × (state+contract),
1792        // capped at 0.5. B only owns the state → smaller bonus.
1793        let bonus_a = (0.4 * (state_score + contract_score)).min(0.5);
1794        let bonus_b = (0.4 * state_score).min(0.5);
1795        assert!((own_a - (g[ranker.index_of(&owner).unwrap()] + bonus_a)).abs() < 1e-9);
1796        assert!((own_b - (g[ranker.index_of(&other).unwrap()] + bonus_b)).abs() < 1e-9);
1797        // The owner with the richer entity set ranks above the plain owner.
1798        assert!(own_a > own_b);
1799
1800        // Cap: a symbol owning a huge-massed state cannot exceed own + 0.5.
1801        let mut big_v = vec![0.0; g.len()];
1802        big_v[ranker.index_of(&state_id).unwrap()] = 100.0;
1803        big_v[ranker.index_of(&contract_id).unwrap()] = 100.0;
1804        let capped = ranker.project_to_symbols(&big_v);
1805        let cap_a = capped.iter().find(|(s, _)| s == &owner).unwrap().1;
1806        let own_a0 = big_v[ranker.index_of(&owner).unwrap()];
1807        assert!((cap_a - (own_a0 + 0.5)).abs() < 1e-9);
1808
1809        // Deterministic: same input → identical output.
1810        let again = ranker.project_to_symbols(&g);
1811        assert_eq!(again, projected);
1812    }
1813
1814    // ---- (e) rank-edge ontology: CONTAINS/PARTICIPATES_IN/HANDLES/DEFINES ----
1815
1816    #[test]
1817// trace:exempt reason=internal-detail
1818    fn rank_edge_kind_direction_and_weights() {
1819        // Deliberate direction mapping (real predicate strings):
1820        // contains only between kinds in the containment hierarchy with
1821        // the container before the member (any ordered pair — adjacent or
1822        // transitive); participates_in only symbol ↔ flow; handles only
1823        // symbol ↔ route; defines only symbol ↔ schema.
1824        // Every reviewer-named hierarchy pair fires: System→Subsystem,
1825        // Subsystem→Service, Subsystem→Component, Service→Component,
1826        // Component→File, Component→Symbol, File→Symbol — plus every
1827        // transitive container pair — with the stored member → container
1828        // direction mapping to the MemberOf ranking transition.
1829        for (container, member) in [
1830            (kinds::SYSTEM, kinds::SUBSYSTEM),
1831            (kinds::SUBSYSTEM, kinds::SERVICE),
1832            (kinds::SUBSYSTEM, kinds::COMPONENT),
1833            (kinds::SERVICE, kinds::COMPONENT),
1834            (kinds::COMPONENT, kinds::FILE),
1835            (kinds::COMPONENT, kinds::SYMBOL),
1836            (kinds::FILE, kinds::SYMBOL),
1837            // Transitive container pairs (the generalization covers ANY
1838            // ordered pair, not a hardcoded adjacency set).
1839            (kinds::SYSTEM, kinds::SERVICE),
1840            (kinds::SYSTEM, kinds::FILE),
1841            (kinds::SUBSYSTEM, kinds::SYMBOL),
1842            (kinds::SERVICE, kinds::FILE),
1843        ] {
1844            assert_eq!(
1845                rank_edge_kind(predicates::CONTAINS, container, member),
1846                Some(RankEdgeKind::Contains)
1847            );
1848            assert_eq!(
1849                rank_edge_kind(predicates::CONTAINS, member, container),
1850                Some(RankEdgeKind::MemberOf)
1851            );
1852        }
1853        // Stored member → container direction maps to the ranking transition.
1854        assert_eq!(
1855            rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::COMPONENT),
1856            Some(RankEdgeKind::MemberOf)
1857        );
1858        assert_eq!(
1859            rank_edge_kind(predicates::PARTICIPATES_IN, kinds::SYMBOL, kinds::FLOW),
1860            Some(RankEdgeKind::ParticipatesIn)
1861        );
1862        assert_eq!(
1863            rank_edge_kind(predicates::PARTICIPATES_IN, kinds::FLOW, kinds::SYMBOL),
1864            Some(RankEdgeKind::FlowReachesParticipant)
1865        );
1866        assert_eq!(
1867            rank_edge_kind(predicates::HANDLES, kinds::SYMBOL, kinds::ROUTE),
1868            Some(RankEdgeKind::Handles)
1869        );
1870        assert_eq!(
1871            rank_edge_kind(predicates::DEFINES, kinds::SYMBOL, kinds::SCHEMA),
1872            Some(RankEdgeKind::Defines)
1873        );
1874        // Reverse transitions: stored route → symbol maps to HandledBy,
1875        // stored schema → symbol maps to DefinedBy (ranking-only).
1876        assert_eq!(
1877            rank_edge_kind(predicates::HANDLES, kinds::ROUTE, kinds::SYMBOL),
1878            Some(RankEdgeKind::HandledBy)
1879        );
1880        assert_eq!(
1881            rank_edge_kind(predicates::DEFINES, kinds::SCHEMA, kinds::SYMBOL),
1882            Some(RankEdgeKind::DefinedBy)
1883        );
1884
1885        // Non-target endpoint kinds never fire (no `reference_kind` drift).
1886        assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::COMPONENT, kinds::FLOW), None);
1887        assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::CONTRACT), None);
1888        assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::FLOW), None);
1889        // Same-kind containment is not a hierarchy edge.
1890        assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::COMPONENT, kinds::COMPONENT), None);
1891        assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::SYMBOL), None);
1892        assert_eq!(rank_edge_kind(predicates::PARTICIPATES_IN, kinds::SYMBOL, kinds::CONTRACT), None);
1893        assert_eq!(rank_edge_kind(predicates::HANDLES, kinds::SYMBOL, kinds::ENDPOINT), None);
1894        assert_eq!(rank_edge_kind(predicates::DEFINES, kinds::SYMBOL, kinds::CONTRACT), None);
1895        assert_eq!(rank_edge_kind(predicates::CALLS, kinds::SYMBOL, kinds::SYMBOL), None);
1896
1897        // Deliberate weights.
1898        assert_eq!(RankEdgeKind::Contains.weight(), RANK_MEMBERSHIP);
1899        assert_eq!(RankEdgeKind::MemberOf.weight(), RANK_MEMBER_OF);
1900        assert_eq!(RankEdgeKind::ParticipatesIn.weight(), RANK_PARTICIPATES);
1901        assert_eq!(RankEdgeKind::FlowReachesParticipant.weight(), RANK_FLOW_REACHES_PARTICIPANT);
1902        assert_eq!(RankEdgeKind::Handles.weight(), RANK_HANDLES);
1903        assert_eq!(RankEdgeKind::HandledBy.weight(), RANK_HANDLED_BY);
1904        assert_eq!(RankEdgeKind::Defines.weight(), RANK_DEFINES);
1905        assert_eq!(RankEdgeKind::DefinedBy.weight(), RANK_DEFINED_BY);
1906
1907        // The reverse edges are RANKING transitions (explicitly labeled,
1908        // never Reality Graph relationships): every evidence edge —
1909        // contains, participates_in, handles, defines — has a ranking-only
1910        // reverse that propagates container/flow/route/schema importance
1911        // back to members/participants/handlers/definers.
1912        assert_eq!(RankEdgeKind::Contains.reverse_ranking_transition(), Some(RankEdgeKind::MemberOf));
1913        assert_eq!(RankEdgeKind::MemberOf.reverse_ranking_transition(), Some(RankEdgeKind::Contains));
1914        assert_eq!(RankEdgeKind::ParticipatesIn.reverse_ranking_transition(), Some(RankEdgeKind::FlowReachesParticipant));
1915        assert_eq!(RankEdgeKind::FlowReachesParticipant.reverse_ranking_transition(), Some(RankEdgeKind::ParticipatesIn));
1916        assert_eq!(RankEdgeKind::Handles.reverse_ranking_transition(), Some(RankEdgeKind::HandledBy));
1917        assert_eq!(RankEdgeKind::HandledBy.reverse_ranking_transition(), Some(RankEdgeKind::Handles));
1918        assert_eq!(RankEdgeKind::Defines.reverse_ranking_transition(), Some(RankEdgeKind::DefinedBy));
1919        assert_eq!(RankEdgeKind::DefinedBy.reverse_ranking_transition(), Some(RankEdgeKind::Defines));
1920    }
1921
1922    #[test]
1923// trace:exempt reason=internal-detail
1924    fn contains_edges_spread_component_importance_both_ways() {
1925        // A component with two members: the membership edges (comp → m1,
1926        // comp → m2) spread component importance DOWN to the members, and
1927        // the reverse RANKING TRANSITIONS (m1 → comp, m2 → comp) derive
1928        // the component's importance UP from its members. An isolated
1929        // symbol (x) sits at the dangling floor for contrast.
1930        let comp_id = entity_id("r", kinds::COMPONENT, "orders");
1931        let m1 = scc_core::symbol_id("r", "src/orders/a.ts", "OrderService");
1932        let m2 = scc_core::symbol_id("r", "src/orders/b.ts", "OrderRepo");
1933        let x = scc_core::symbol_id("r", "src/other.ts", "Unrelated");
1934
1935        let entities = vec![
1936            entity(&comp_id, kinds::COMPONENT, "orders"),
1937            sym("r", "src/orders/a.ts", "OrderService"),
1938            sym("r", "src/orders/b.ts", "OrderRepo"),
1939            sym("r", "src/other.ts", "Unrelated"),
1940        ];
1941        let rels = vec![
1942            rel(1, &comp_id, predicates::CONTAINS, &m1, Provenance::Extracted),
1943            rel(2, &comp_id, predicates::CONTAINS, &m2, Provenance::Extracted),
1944        ];
1945        let (_dir, store, graph) = fixture(entities, rels);
1946        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1947        let ranker = SystemRanker::new(&view);
1948        let g = ranker.global_vector();
1949
1950        let c = g[ranker.index_of(&comp_id).unwrap()];
1951        let s1 = g[ranker.index_of(&m1).unwrap()];
1952        let s2 = g[ranker.index_of(&m2).unwrap()];
1953        let sx = g[ranker.index_of(&x).unwrap()];
1954
1955        // Both ways: the component ranks above its members (derived from
1956        // them via the ranking transitions — a dangling-only component
1957        // would sit at the uniform floor), and the members rank above the
1958        // isolated symbol (component importance reaches them via the
1959        // membership edges).
1960        assert!(c > 0.3, "component importance derived from members: {c}");
1961        assert!(s1 > sx && s2 > sx, "members lifted above the isolated symbol");
1962        assert!(c > s1 && c > s2, "component spreads importance down to members");
1963    }
1964
1965    #[test]
1966// trace:exempt reason=internal-detail
1967    fn participates_in_connects_flow_nodes() {
1968        // A flow with a participant symbol becomes CONNECTED: the
1969        // symbol → flow edge feeds the flow node, and the reverse
1970        // flow → participant RANKING TRANSITION carries flow importance
1971        // back to the participant. A bare flow (no participants) stays at
1972        // the dangling floor.
1973        let s = scc_core::symbol_id("r", "src/a.ts", "A");
1974        let f1 = entity_id("r", kinds::FLOW, "signup");
1975        let f2 = entity_id("r", kinds::FLOW, "checkout");
1976
1977        let entities = vec![
1978            sym("r", "src/a.ts", "A"),
1979            entity(&f1, kinds::FLOW, "signup"),
1980            entity(&f2, kinds::FLOW, "checkout"),
1981        ];
1982        let rels = vec![rel(1, &s, predicates::PARTICIPATES_IN, &f1, Provenance::Extracted)];
1983        let (_dir, store, graph) = fixture(entities, rels);
1984        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1985        let ranker = SystemRanker::new(&view);
1986        let g = ranker.global_vector();
1987
1988        let f1v = g[ranker.index_of(&f1).unwrap()];
1989        let f2v = g[ranker.index_of(&f2).unwrap()];
1990        let sv = g[ranker.index_of(&s).unwrap()];
1991
1992        // Previously the flow node was disconnected (no adjacency) and sat
1993        // at the dangling floor; with the rank edges it receives directed
1994        // mass from its participant.
1995        assert!(f1v > 0.3, "flow with a participant receives PPR mass: {f1v}");
1996        assert!(f1v > f2v, "connected flow ranks above the bare flow");
1997        // Flow importance reaches the participant (reverse transition).
1998        assert!(sv > f2v, "flow importance reaches the participant: {sv} vs {f2v}");
1999    }
2000
2001    #[test]
2002// trace:exempt reason=internal-detail
2003    fn handles_and_defines_edges_feed_ppr() {
2004        // HANDLES (symbol → route) and DEFINES (symbol → schema) edges
2005        // feed PageRank: the route and schema nodes receive directed mass
2006        // from their handling/defining symbols, above the isolated
2007        // baseline. The handler is an invocation-surface seed (HANDLES →
2008        // ROUTE) and the definer is an exported symbol — exactly the
2009        // production shape — so both cycles receive inflow; the reverse
2010        // RANKING TRANSITIONS (route → handler, schema → definer) make
2011        // both pairs connected nodes rather than dangling sinks.
2012        let h = scc_core::symbol_id("r", "src/api.ts", "OrderHandler");
2013        let route = entity_id("r", kinds::ROUTE, "/orders");
2014        let d = scc_core::symbol_id("r", "src/models.ts", "Order");
2015        let schema = entity_id("r", kinds::SCHEMA, "order");
2016        let x = scc_core::symbol_id("r", "src/util.ts", "Helper");
2017
2018        let mut d_ent = sym("r", "src/models.ts", "Order");
2019        d_ent.attr("exported", serde_json::json!(true));
2020        let entities = vec![
2021            sym("r", "src/api.ts", "OrderHandler"),
2022            entity(&route, kinds::ROUTE, "/orders"),
2023            d_ent,
2024            entity(&schema, kinds::SCHEMA, "order"),
2025            sym("r", "src/util.ts", "Helper"),
2026        ];
2027        let rels = vec![
2028            rel(1, &h, predicates::HANDLES, &route, Provenance::Extracted),
2029            rel(2, &d, predicates::DEFINES, &schema, Provenance::Extracted),
2030        ];
2031        let (_dir, store, graph) = fixture(entities, rels);
2032        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2033        let ranker = SystemRanker::new(&view);
2034        let g = ranker.global_vector();
2035
2036        let rv = g[ranker.index_of(&route).unwrap()];
2037        let sv = g[ranker.index_of(&schema).unwrap()];
2038        let xv = g[ranker.index_of(&x).unwrap()];
2039
2040        assert!(rv > xv, "route receives mass via the HANDLES edge: {rv} vs {xv}");
2041        assert!(sv > xv, "schema receives mass via the DEFINES edge: {sv} vs {xv}");
2042    }
2043
2044    // ---- (f) full containment hierarchy: File→Symbol, Subsystem→Component ----
2045
2046    #[test]
2047// trace:exempt reason=internal-detail
2048    fn file_contains_symbol_lifts_member() {
2049        // The reviewer's fracture: a FILE → SYMBOL containment pair was a
2050        // dead node — `contains` only fired between Component/Subsystem/
2051        // Service containers and their members, so a lone file (no
2052        // component) never connected its symbol. With the full hierarchy,
2053        // the file receives the symbol's importance (MemberOf) and the
2054        // symbol receives the file's (Contains): the member is lifted
2055        // above the isolated floor.
2056        let file_id = entity_id("r", kinds::FILE, "src/orders.ts");
2057        let sym_id = scc_core::symbol_id("r", "src/orders.ts", "OrderService");
2058        let x = scc_core::symbol_id("r", "src/other.ts", "Unrelated");
2059
2060        let entities = vec![
2061            entity(&file_id, kinds::FILE, "src/orders.ts"),
2062            sym("r", "src/orders.ts", "OrderService"),
2063            sym("r", "src/other.ts", "Unrelated"),
2064        ];
2065        let rels = vec![rel(1, &file_id, predicates::CONTAINS, &sym_id, Provenance::Extracted)];
2066        let (_dir, store, graph) = fixture(entities, rels);
2067        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2068        let ranker = SystemRanker::new(&view);
2069        let g = ranker.global_vector();
2070
2071        let fv = g[ranker.index_of(&file_id).unwrap()];
2072        let sv = g[ranker.index_of(&sym_id).unwrap()];
2073        let xv = g[ranker.index_of(&x).unwrap()];
2074
2075        // The file is no longer a dead node (it derives mass from its
2076        // symbol via the MemberOf ranking transition) and the contained
2077        // symbol ranks above the isolated symbol.
2078        assert!(fv > 0.3, "file node derives importance from its symbol: {fv}");
2079        assert!(sv > xv, "contained symbol lifted above the isolated one: {sv} vs {xv}");
2080        assert!(fv > xv, "file lifted above the isolated symbol: {fv} vs {xv}");
2081    }
2082
2083    #[test]
2084// trace:exempt reason=internal-detail
2085    fn subsystem_component_containment_spreads_both_ways() {
2086        // A Subsystem CONTAINS a Component: membership evidence flows
2087        // subsystem → component (down) and the reverse RANKING TRANSITION
2088        // component → subsystem derives the subsystem's importance from
2089        // its component (up). A bare component with no subsystem sits at
2090        // the dangling floor.
2091        let sub_id = entity_id("r", kinds::SUBSYSTEM, "billing");
2092        let comp_id = entity_id("r", kinds::COMPONENT, "orders");
2093        let bare = entity_id("r", kinds::COMPONENT, "auth");
2094
2095        let entities = vec![
2096            entity(&sub_id, kinds::SUBSYSTEM, "billing"),
2097            entity(&comp_id, kinds::COMPONENT, "orders"),
2098            entity(&bare, kinds::COMPONENT, "auth"),
2099        ];
2100        let rels = vec![rel(1, &sub_id, predicates::CONTAINS, &comp_id, Provenance::Extracted)];
2101        let (_dir, store, graph) = fixture(entities, rels);
2102        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2103        let ranker = SystemRanker::new(&view);
2104        let g = ranker.global_vector();
2105
2106        let subv = g[ranker.index_of(&sub_id).unwrap()];
2107        let compv = g[ranker.index_of(&comp_id).unwrap()];
2108        let barev = g[ranker.index_of(&bare).unwrap()];
2109
2110        // Both ways: the contained component receives the subsystem's
2111        // membership evidence (compv > barev) and the subsystem derives
2112        // importance from its component (subv > barev).
2113        assert!(compv > barev, "contained component ranks above the bare one: {compv} vs {barev}");
2114        assert!(subv > barev, "subsystem derives importance from its component: {subv} vs {barev}");
2115        assert!(subv > 0.3, "subsystem is a first-class rank node: {subv}");
2116    }
2117
2118    // ---- (g) projection through HandledBy/DefinedBy ----
2119
2120    #[test]
2121// trace:exempt reason=internal-detail
2122    fn hot_route_projects_to_handler_symbol() {
2123        // The reviewer's exact scenario: the task mentions /health → the
2124        // route node is hot (task seed) → its handler symbol is reached.
2125        // Two mechanisms fire: the HandledBy ranking transition carries
2126        // route mass to the handler in the PPR vector, and the HANDLES
2127        // projection adds 0.4 × the hot route's score (capped at 0.5) to
2128        // the handler's surface score.
2129        let handler = scc_core::symbol_id("r", "src/router.ts", "build_router");
2130        let route = entity_id("r", kinds::ROUTE, "/health");
2131        let other = scc_core::symbol_id("r", "src/util.ts", "Helper");
2132
2133        let entities = vec![
2134            sym("r", "src/router.ts", "build_router"),
2135            entity(&route, kinds::ROUTE, "/health"),
2136            sym("r", "src/util.ts", "Helper"),
2137        ];
2138        let rels = vec![rel(1, &handler, predicates::HANDLES, &route, Provenance::Extracted)];
2139        let (_dir, store, graph) = fixture(entities, rels);
2140        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2141        let ranker = SystemRanker::new(&view);
2142
2143        // Hot route: the task seed lands on the route node itself.
2144        let seeds = vec![TaskSeed {
2145            kind: "route".into(),
2146            id: route.clone(),
2147            weight: 1.0,
2148        }];
2149        let tv = ranker.task_vector(&seeds);
2150        let projected = ranker.project_to_symbols(&tv);
2151        let score_of = |id: &str| -> f64 {
2152            projected
2153                .iter()
2154                .find(|(s, _)| s == id)
2155                .map(|(_, v)| *v)
2156                .unwrap_or(f64::NAN)
2157        };
2158
2159        let route_mass = tv[ranker.index_of(&route).unwrap()];
2160        let handler_own = tv[ranker.index_of(&handler).unwrap()];
2161        let handler_score = score_of(&handler);
2162        let other_score = score_of(&other);
2163
2164        // The handler's surface score = its own PPR (which already grew
2165        // via the HandledBy transition) + 0.4 × the hot route's score,
2166        // capped at 0.5 — the exact projection pattern.
2167        let expected_bonus = (PROJECTION_BONUS_FACTOR * route_mass).min(PROJECTION_BONUS_CAP);
2168        assert!((handler_score - (handler_own + expected_bonus)).abs() < 1e-9);
2169        assert!(route_mass > 0.1, "seeded route is hot: {route_mass}");
2170        assert!(handler_score > other_score, "hot route lifts its handler above unrelated symbols");
2171    }
2172
2173    #[test]
2174// trace:exempt reason=internal-detail
2175    fn hot_schema_projects_to_definer_symbol() {
2176        // A hot Schema node (task seed) reaches its defining symbol: the
2177        // DefinedBy ranking transition feeds the definer in the PPR vector
2178        // and the DEFINES projection adds 0.4 × the schema's score (capped
2179        // at 0.5) to the definer's surface score.
2180        let definer = scc_core::symbol_id("r", "src/models.ts", "Order");
2181        let schema = entity_id("r", kinds::SCHEMA, "order");
2182        let other = scc_core::symbol_id("r", "src/util.ts", "Helper");
2183
2184        let entities = vec![
2185            sym("r", "src/models.ts", "Order"),
2186            entity(&schema, kinds::SCHEMA, "order"),
2187            sym("r", "src/util.ts", "Helper"),
2188        ];
2189        let rels = vec![rel(1, &definer, predicates::DEFINES, &schema, Provenance::Extracted)];
2190        let (_dir, store, graph) = fixture(entities, rels);
2191        let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2192        let ranker = SystemRanker::new(&view);
2193
2194        let seeds = vec![TaskSeed {
2195            kind: "schema".into(),
2196            id: schema.clone(),
2197            weight: 1.0,
2198        }];
2199        let tv = ranker.task_vector(&seeds);
2200        let projected = ranker.project_to_symbols(&tv);
2201        let score_of = |id: &str| -> f64 {
2202            projected
2203                .iter()
2204                .find(|(s, _)| s == id)
2205                .map(|(_, v)| *v)
2206                .unwrap_or(f64::NAN)
2207        };
2208
2209        let schema_mass = tv[ranker.index_of(&schema).unwrap()];
2210        let definer_own = tv[ranker.index_of(&definer).unwrap()];
2211        let definer_score = score_of(&definer);
2212        let other_score = score_of(&other);
2213
2214        let expected_bonus = (PROJECTION_BONUS_FACTOR * schema_mass).min(PROJECTION_BONUS_CAP);
2215        assert!((definer_score - (definer_own + expected_bonus)).abs() < 1e-9);
2216        assert!(schema_mass > 0.1, "seeded schema is hot: {schema_mass}");
2217        assert!(definer_score > other_score, "hot schema lifts its definer above unrelated symbols");
2218    }
2219}