use scc_api::{RankFeatures, RankItem, RankRequest, RankResult};
pub struct Ranker<'a> {
engine: &'a crate::workspace::Engine<'a>,
}
impl<'a> Ranker<'a> {
pub fn new(engine: &'a crate::workspace::Engine<'a>) -> Self { Ranker { engine } }
fn ctx(&self) -> scc_context::ContextCompiler<'_> { self.engine.ctx() }
pub fn pagerank_global(&self) -> crate::Result<Vec<(String, f64)>> {
self.pagerank_global_with(&[])
}
pub fn pagerank_global_with(
&self,
contributors: &[EdgeWeightFn],
) -> crate::Result<Vec<(String, f64)>> {
self.pagerank_global_with_hooks(contributors, &[])
}
pub fn pagerank_global_with_hooks(
&self,
contributors: &[EdgeWeightFn],
extra: &[(String, String, String, f64)],
) -> crate::Result<Vec<(String, f64)>> {
let ctx = self.ctx();
let owned: Vec<EdgeWeightFn> = contributors.iter().cloned().collect();
let ranker = scc_context::pagerank::SystemRanker::with_edge_adjust_and_extra(
&ctx.view,
move |s, p, o, b| Self::fold_edge_contributors(&owned, s, p, o, b).0,
extra,
);
let v = ranker.global_vector();
Ok(ranker.nodes().iter().cloned().zip(v).collect())
}
pub fn reference_graph(&self) -> crate::Result<Vec<scc_core::ReferenceEdge>> {
let ctx = self.ctx();
Ok(scc_context::pagerank::build_reference_graph(&ctx.view))
}
pub fn universe(&self) -> crate::Result<Vec<(String, String)>> {
self.universe_with(&RankHooks::default(), "")
}
pub fn universe_with(&self, hooks: &RankHooks, goal: &str) -> crate::Result<Vec<(String, String)>> {
let ctx = self.ctx();
let mut extra_nodes: Vec<(String, String)> = Vec::new();
for prov in &hooks.rank_nodes {
extra_nodes.extend(prov(goal));
}
let ranker = scc_context::pagerank::SystemRanker::with_edge_adjust_extra_and_nodes(
&ctx.view,
|_s: &str, _p: &str, _o: &str, _b: f64| None,
&[],
&extra_nodes,
);
Ok(ranker.nodes().iter().cloned().zip(ranker.kinds().iter().cloned()).collect())
}
pub fn project_symbols(&self, vector: &[(String, f64)]) -> crate::Result<Vec<(String, f64)>> {
let ctx = self.ctx();
let ranker = scc_context::pagerank::SystemRanker::new(&ctx.view);
let by_id: std::collections::BTreeMap<&str, f64> =
vector.iter().map(|(id, s)| (id.as_str(), *s)).collect();
let full: Vec<f64> = ranker.nodes().iter().map(|id| by_id.get(id.as_str()).copied().unwrap_or(0.0)).collect();
Ok(ranker.project_to_symbols(&full))
}
pub fn rank_edges(&self) -> crate::Result<Vec<(String, String, String, f64)>> {
let ctx = self.ctx();
let ranker = scc_context::pagerank::SystemRanker::new(&ctx.view);
Ok(ranker.rank_edges())
}
pub fn pagerank_task(&self, goal: &str) -> crate::Result<Vec<(String, f64)>> {
self.pagerank_task_with(goal, &[])
}
pub fn pagerank_task_with(
&self,
goal: &str,
contributors: &[EdgeWeightFn],
) -> crate::Result<Vec<(String, f64)>> {
self.pagerank_task_with_hooks(goal, contributors, &[])
}
pub fn pagerank_task_with_hooks(
&self,
goal: &str,
contributors: &[EdgeWeightFn],
extra: &[(String, String, String, f64)],
) -> crate::Result<Vec<(String, f64)>> {
let ctx = self.ctx();
let owned: Vec<EdgeWeightFn> = contributors.iter().cloned().collect();
let ranker = scc_context::pagerank::SystemRanker::with_edge_adjust_and_extra(
&ctx.view,
move |s, p, o, b| Self::fold_edge_contributors(&owned, s, p, o, b).0,
extra,
);
let seeds = lexical_seeds(&ctx, goal);
let v = ranker.task_vector(&seeds);
Ok(ranker.nodes().iter().cloned().zip(v).collect())
}
fn fold_edge_contributors(
contributors: &[EdgeWeightFn],
subject: &str,
predicate: &str,
object: &str,
base: f64,
) -> (Option<(String, f64)>, Vec<String>) {
let mut acc: Option<(String, f64)> = None;
let mut applied = Vec::new();
for c in contributors {
let current = match &acc {
None => base,
Some((m, v)) => apply_edge_weight(base, m, *v),
};
if let Some((mode, value)) = c(subject, predicate, object, current) {
match mode.as_str() {
"add" | "multiply" | "replace" | "veto" => {
applied.push(mode.clone());
acc = Some((mode, value));
}
_ => {}
}
}
}
(acc, applied)
}
pub fn required_with(&self, goal: &str, hooks: &RankHooks) -> crate::Result<std::collections::BTreeSet<String>> {
let ctx = self.ctx();
let map = scc_context::surface::compile_surface_map(&ctx);
let mut required = scc_context::surface::required_ids(&map, &ctx);
for cov in &hooks.coverage {
for id in cov(goal) {
required.insert(id);
}
}
Ok(required)
}
pub fn seeds_with(&self, goal: &str, hooks: &RankHooks) -> crate::Result<Vec<scc_core::TaskSeed>> {
let ctx = self.ctx();
let mut seeds = lexical_seeds(&ctx, goal);
for seed_fn in &hooks.seed_providers {
for x in seed_fn(goal) {
if let Some(e) = seeds.iter_mut().find(|s| s.id == x.id) { e.weight += x.weight; }
else { seeds.push(x); }
}
}
seeds.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal).then_with(|| a.id.cmp(&b.id)));
Ok(seeds)
}
pub fn candidates(&self, goal: &str, limit: usize) -> crate::Result<Vec<scc_context::rank::ScoredEntity>> {
self.candidates_with(goal, limit, &RankHooks::default())
}
pub fn candidates_with(
&self,
goal: &str,
limit: usize,
hooks: &RankHooks,
) -> crate::Result<Vec<scc_context::rank::ScoredEntity>> {
let ctx = self.ctx();
let mut merged: std::collections::BTreeMap<String, scc_context::rank::ScoredEntity> =
scc_context::rank::collect_lexical_candidates(ctx.store, &ctx.view, goal, &[], limit.max(1))
.into_iter().map(|c| (c.id.clone(), c)).collect();
for prov in &hooks.candidates {
for mut c in prov(goal) {
if c.id.is_empty() {
continue;
}
c.reason = if c.reason.is_empty() { "plugin-candidate".into() } else { c.reason };
match merged.get(&c.id) {
Some(prev) if prev.score >= c.score => {}
_ => { merged.insert(c.id.clone(), c); }
}
}
}
let mut out: Vec<scc_context::rank::ScoredEntity> = merged.into_values().collect();
out.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal).then_with(|| a.id.cmp(&b.id)));
out.truncate(limit.max(1));
Ok(out)
}
pub fn symbols(&self, req: &RankRequest) -> crate::Result<RankResult> {
self.symbols_with_hooks(req, &RankHooks::default())
}
pub fn trace_with_hooks(&self, req: &RankRequest, hooks: &RankHooks) -> crate::Result<(RankResult, Vec<String>, Vec<String>)> {
let goal = req.goal.as_deref().unwrap_or("");
let out = self.symbols_with_hooks(req, hooks)?;
let mut seeds: Vec<String> = self.seeds_with(goal, hooks)?.into_iter().map(|x| x.id).collect();
seeds.sort();
let mut required: Vec<String> = self.required_with(goal, hooks)?.into_iter().collect();
required.sort();
Ok((out, seeds, required))
}
pub fn symbols_with_hooks(&self, req: &RankRequest, hooks: &RankHooks) -> crate::Result<RankResult> {
let profile_w: Option<BlendWeights> = match req.profile.as_deref() {
None | Some("default") => None,
Some(name) => Some(hooks.profiles.get(name).cloned().ok_or_else(|| {
let mut avail: Vec<&str> = hooks.profiles.keys().map(|s| s.as_str()).collect();
avail.insert(0, "default");
crate::EngineError::Other(format!(
"unknown ranking profile '{name}' (available: {})",
avail.join(", ")
))
})?),
};
let profile_name: Option<&str> = req.profile.as_deref().filter(|p| *p != "default");
let ctx = self.ctx();
let goal = req.goal.as_deref().unwrap_or("");
let goal_terms = scc_context::rank::terms(goal);
let mut seeds = lexical_seeds(&ctx, goal);
for seed_fn in &hooks.seed_providers {
for s in seed_fn(goal) {
if let Some(e) = seeds.iter_mut().find(|x| x.id == s.id) { e.weight += s.weight; }
else { seeds.push(s); }
}
}
let seed_ids: std::collections::BTreeSet<&str> =
seeds.iter().map(|s| s.id.as_str()).collect();
let owned: Vec<EdgeWeightFn> = hooks.edge_weights.iter().cloned().collect();
let mut extra: Vec<(String, String, String, f64)> = Vec::new();
for prov in &hooks.rank_edges {
extra.extend(prov(goal));
}
let extra_count = extra.len();
let mut extra_nodes: Vec<(String, String)> = Vec::new();
for prov in &hooks.rank_nodes {
extra_nodes.extend(prov(goal));
}
let node_count = extra_nodes.len();
let ranker = scc_context::pagerank::SystemRanker::with_edge_adjust_extra_and_nodes(
&ctx.view,
move |s, p, o, b| Self::fold_edge_contributors(&owned, s, p, o, b).0,
&extra,
&extra_nodes,
);
let global_of: std::collections::BTreeMap<String, f64> =
ranker.project_to_symbols(&ranker.global_vector()).into_iter().collect();
let task_of: std::collections::BTreeMap<String, f64> =
ranker.project_to_symbols(&ranker.task_vector(&seeds)).into_iter().collect();
let has_task = !goal.is_empty();
let map = scc_context::surface::compile_surface_map(&ctx);
let mut required = scc_context::surface::required_ids(&map, &ctx);
let mut required_by: usize = 0;
for cov in &hooks.coverage {
for id in cov(goal) {
if required.insert(id) {
required_by += 1;
}
}
}
let mut best: std::collections::BTreeMap<&str, RankItem> = std::collections::BTreeMap::new();
for e in &map.entries {
let task_ppr = task_of.get(&e.symbol_id).copied().unwrap_or(0.0);
let global_ppr = global_of.get(&e.symbol_id).copied().unwrap_or(0.0);
let lexical = scc_context::surface::entry_lexical(e, &goal_terms);
let confidence = e.confidence as f64;
let default_criticality = if seed_ids.contains(e.symbol_id.as_str()) || required.contains(&e.id) { 1.0 } else { importance_file_score(&e.path) };
let (criticality, criticality_src) = resolve_override(&hooks.criticality, &e.symbol_id, goal, default_criticality);
let (novelty, novelty_src) = resolve_override(&hooks.novelty, &e.symbol_id, goal, 1.0);
let default_risk = if !e.path.is_empty() && ctx.stale_paths.iter().any(|p| p == &e.path) { 1.0 } else { 0.0 };
let (change_risk, risk_src) = resolve_override(&hooks.risk, &e.symbol_id, goal, default_risk);
let (semantic, semantic_src) = resolve_override(&hooks.semantic, &e.symbol_id, goal, 0.0);
let blend = match profile_w.as_ref() {
None => scc_context::pagerank::final_importance(task_ppr, global_ppr, lexical, semantic, confidence, criticality, change_risk, 0.0, has_task),
Some(w) => {
use scc_context::pagerank as pr;
let (tw, gw) = if has_task { (w.task_ppr.unwrap_or(pr::TASK_PPR_WEIGHT), w.global_ppr.unwrap_or(pr::GLOBAL_PPR_WEIGHT)) }
else { (w.task_ppr.unwrap_or(0.0), w.global_ppr.unwrap_or(pr::NO_TASK_GLOBAL_WEIGHT)) };
tw * task_ppr
+ gw * global_ppr
+ w.lexical.unwrap_or(pr::LEXICAL_WEIGHT) * lexical
+ w.semantic.unwrap_or(pr::SEMANTIC_WEIGHT) * semantic
+ w.confidence.unwrap_or(pr::CONFIDENCE_WEIGHT) * confidence
+ w.criticality.unwrap_or(pr::CRITICALITY_WEIGHT) * criticality
+ w.change_risk.unwrap_or(pr::CHANGE_RISK_WEIGHT) * change_risk
+ w.novelty.unwrap_or(pr::NOVELTY_WEIGHT) * 0.0
}
};
let scale = 1.0 / (1.0 - scc_context::pagerank::SEMANTIC_WEIGHT);
let novelty_w = profile_w.as_ref().and_then(|w| w.novelty).unwrap_or(scc_context::pagerank::NOVELTY_WEIGHT);
let total = blend * scale + novelty_w * novelty;
let mut plugin_features = std::collections::BTreeMap::new();
let mut reasons: Vec<String> = Vec::new();
if seed_ids.contains(e.symbol_id.as_str()) { reasons.push("task-seed".into()); }
if let Some(src) = criticality_src { reasons.push(format!("criticality:{src}")); }
if let Some(src) = novelty_src { reasons.push(format!("novelty:{src}")); }
if let Some(src) = risk_src { reasons.push(format!("risk:{src}")); }
if let Some(src) = semantic_src { reasons.push(format!("semantic:{src}")); }
if required_by > 0 && required.contains(&e.id) && !seed_ids.contains(e.symbol_id.as_str()) { reasons.push(format!("required-by:plugin({required_by})")); }
let mut total = total;
for feat in &hooks.features {
let v = feat(&e.symbol_id, goal);
plugin_features.insert(v.name.clone(), v.score);
total += v.weight * v.score;
if !v.reason.is_empty() { reasons.push(v.reason.clone()); }
}
if let Some(pname) = profile_name {
reasons.push(format!("profile:{pname}"));
}
let specificity = if e.exported { 1.15 } else { 1.0 };
let item = RankItem { id: e.symbol_id.clone(), rank: total, position: 0,
features: RankFeatures { task_ppr, global_ppr, lexical, semantic,
confidence, criticality, change_risk, novelty },
specificity, reasons, plugin_features };
match best.get(&e.symbol_id.as_str()) {
Some(prev) if prev.rank >= total => {}
_ => { best.insert(&e.symbol_id, item); }
}
}
let mut items: Vec<RankItem> = best.into_values().collect();
if !hooks.edge_weights.is_empty() {
for it in items.iter_mut() {
it.reasons.push(format!("edge-weights({})", hooks.edge_weights.len()));
}
}
if extra_count > 0 {
for it in items.iter_mut() {
it.reasons.push(format!("rank-edges({extra_count})"));
}
}
if node_count > 0 {
for it in items.iter_mut() {
it.reasons.push(format!("rank-nodes({node_count})"));
}
}
for r in &hooks.rerankers { r(&mut items, goal); }
items.sort_by(|a, b| b.rank.partial_cmp(&a.rank).unwrap_or(std::cmp::Ordering::Equal).then_with(|| a.id.cmp(&b.id)));
items.truncate(req.limit.max(1));
for (i, it) in items.iter_mut().enumerate() { it.position = i + 1; }
let mut warnings = Vec::new();
if !hooks.edge_weights.is_empty() {
warnings.push(format!(
"{} edge-weight contributor(s) applied to the rank graph",
hooks.edge_weights.len()
));
}
if extra_count > 0 {
warnings.push(format!(
"{extra_count} extra rank-time edge(s) merged into diffusion (never canonical)"
));
}
if node_count > 0 {
warnings.push(format!(
"{node_count} extra rank-universe node(s) merged into diffusion (never canonical)"
));
}
Ok(RankResult { items, omitted_ids: Vec::new(), warnings })
}
}
fn lexical_seeds(ctx: &scc_context::ContextCompiler<'_>, goal: &str) -> Vec<scc_core::TaskSeed> {
if goal.is_empty() { return Vec::new(); }
scc_context::rank::collect_lexical_candidates(ctx.store, &ctx.view, goal, &[], 16)
.into_iter()
.map(|c| scc_core::TaskSeed { kind: c.kind, id: c.id, weight: c.score })
.collect()
}
fn importance_file_score(path: &str) -> f64 {
let base = path.rsplit('/').next().unwrap_or(path);
const IMPORTANT: &[&str] = &[
"package.json", "pnpm-workspace.yaml", "yarn.lock", "Cargo.toml",
"Cargo.lock", "go.mod", "pyproject.toml", "setup.py", "setup.cfg",
"requirements.txt", "pom.xml", "build.gradle", "build.gradle.kts",
"settings.gradle", "settings.gradle.kts", "gradlew", "Makefile",
"CMakeLists.txt", "mix.exs", "Gemfile", "composer.json",
"Dockerfile", "docker-compose.yml", "docker-compose.yaml",
"compose.yml", "compose.yaml", ".dockerignore",
".github/workflows/ci.yml", ".github/workflows/main.yml",
".gitlab-ci.yml", "Jenkinsfile", "azure-pipelines.yml",
".circleci/config.yml", "buildkite.yml",
"main.py", "main.go", "main.ts", "index.ts", "index.js",
"app.py", "server.py", "server.ts", "server.js", "cli.py",
"cli.ts", "cli.go", "src/main.rs", "bin/main.rs", "app.js",
"app.ts",
];
if IMPORTANT.contains(&base) || IMPORTANT.contains(&path) || path.starts_with(".github/workflows/") { 0.5 }
else { 0.0 }
}
pub struct RankFeatureValue {
pub name: String,
pub score: f64,
pub weight: f64,
pub reason: String,
}
pub type SeedProvider = Box<dyn Fn(&str) -> Vec<scc_core::TaskSeed> + Send + Sync>;
pub type CandidateProvider =
Box<dyn Fn(&str) -> Vec<scc_context::rank::ScoredEntity> + Send + Sync>;
pub type RankFeatureFn = Box<dyn Fn(&str, &str) -> RankFeatureValue + Send + Sync>;
pub type RerankerFn = Box<dyn Fn(&mut Vec<scc_api::RankItem>, &str) + Send + Sync>;
pub type EdgeWeightFn = std::sync::Arc<
dyn for<'a, 'b, 'c> Fn(&'a str, &'b str, &'c str, f64) -> Option<(String, f64)> + Send + Sync,
>;
#[derive(Clone, Debug, Default)]
pub struct BlendWeights {
pub task_ppr: Option<f64>,
pub global_ppr: Option<f64>,
pub lexical: Option<f64>,
pub semantic: Option<f64>,
pub confidence: Option<f64>,
pub criticality: Option<f64>,
pub change_risk: Option<f64>,
pub novelty: Option<f64>,
}
pub type CoverageProvider =
Box<dyn Fn(&str) -> Vec<String> + Send + Sync>;
pub type RankEdgeProvider =
Box<dyn Fn(&str) -> Vec<(String, String, String, f64)> + Send + Sync>;
pub type RankNodeProvider =
Box<dyn Fn(&str) -> Vec<(String, String)> + Send + Sync>;
pub type CriticalityProvider =
Box<dyn Fn(&str, &str) -> Option<f64> + Send + Sync>;
pub type NoveltyProvider =
Box<dyn Fn(&str, &str) -> Option<f64> + Send + Sync>;
pub type RiskProvider = ScalarOverrideProvider;
pub type SemanticProvider = ScalarOverrideProvider;
pub type ScalarOverrideProvider =
Box<dyn Fn(&str, &str) -> Option<f64> + Send + Sync>;
pub type SimilarityFn = std::sync::Arc<dyn Fn(&str, &str, Option<&str>, Option<&str>) -> f64 + Send + Sync>;
#[derive(Default)]
pub struct RankHooks {
pub seed_providers: Vec<SeedProvider>,
pub features: Vec<RankFeatureFn>,
pub rerankers: Vec<RerankerFn>,
pub edge_weights: Vec<EdgeWeightFn>,
pub similarities: Vec<SimilarityFn>,
pub coverage: Vec<CoverageProvider>,
pub candidates: Vec<CandidateProvider>,
pub rank_edges: Vec<RankEdgeProvider>,
pub rank_nodes: Vec<RankNodeProvider>,
pub criticality: Vec<CriticalityProvider>,
pub novelty: Vec<NoveltyProvider>,
pub risk: Vec<RiskProvider>,
pub semantic: Vec<SemanticProvider>,
pub profiles: std::collections::BTreeMap<String, BlendWeights>,
}
pub fn resolve_override(
providers: &[ScalarOverrideProvider],
symbol: &str,
goal: &str,
default: f64,
) -> (f64, Option<String>) {
for (i, prov) in providers.iter().enumerate() {
match prov(symbol, goal) {
Some(v) if v.is_finite() && (0.0..=1.0).contains(&v) => {
return (v, Some(format!("provider({i})")));
}
_ => {}
}
}
(default, None)
}
pub fn apply_edge_weight(base: f64, mode: &str, value: f64) -> f64 {
match mode {
"add" => base + value,
"multiply" => base * value,
"replace" => value,
"veto" => 0.0,
_ => base,
}
}
pub struct ScoreRow<'a> {
pub id: &'a str,
pub task_ppr: f64,
pub global_ppr: f64,
pub lexical: f64,
pub semantic: f64,
pub confidence: f64,
pub criticality: f64,
pub change_risk: f64,
pub novelty: f64,
pub has_task: bool,
}
pub fn score_entries(rows: &[ScoreRow<'_>]) -> Vec<(String, f64)> {
rows.iter().map(|r| (
r.id.to_string(),
scc_context::pagerank::final_importance(
r.task_ppr, r.global_ppr, r.lexical, r.semantic,
r.confidence, r.criticality, r.change_risk, r.novelty, r.has_task,
),
)).collect()
}
pub fn mmr_select(ranked: &[(String, f64)], similar: &dyn Fn(&str, &str) -> f64, lambda: f64, budget: usize) -> Vec<String> {
scc_context::selector::mmr_diversify(ranked, similar, lambda, budget.max(1))
}
pub fn default_similarity(a_group: Option<&str>, b_group: Option<&str>) -> f64 {
match (a_group, b_group) {
(Some(g1), Some(g2)) if !g1.is_empty() && g1 == g2 => 1.0,
_ => 0.0,
}
}
pub fn group_of2<'a>(ranked: &[(String, f64)], groups: &'a [Option<String>], id: &str) -> Option<&'a str> {
ranked.iter().position(|(rid, _)| rid == id).and_then(|i| groups.get(i).and_then(|g| g.as_deref()))
}
pub fn fold_similarity(
providers: &[SimilarityFn],
a: &str,
b: &str,
a_group: Option<&str>,
b_group: Option<&str>,
) -> f64 {
for p in providers {
let v = p(a, b, a_group, b_group);
if v != 0.0 {
return v.clamp(0.0, 1.0);
}
}
default_similarity(a_group, b_group)
}
pub fn apply_quotas(items: &[(String, String, f64, usize)], quotas: &[(String, f64)], available_tokens: usize) -> Vec<String> {
use std::collections::HashMap;
let mut caps: HashMap<&str, usize> = HashMap::new();
for (kind, frac) in quotas {
caps.insert(kind.as_str(), (frac.clamp(0.0, 1.0) * available_tokens as f64).round() as usize);
}
let mut spent: HashMap<&str, usize> = HashMap::new();
let mut kind_of: HashMap<&str, &str> = HashMap::new();
let mut cost_of: HashMap<&str, usize> = HashMap::new();
for (id, kind, _, cost) in items {
kind_of.insert(id.as_str(), kind.as_str());
cost_of.insert(id.as_str(), *cost);
}
let mut out = Vec::new();
let mut order: Vec<usize> = (0..items.len()).collect();
order.sort_by(|&a, &b| items[b].2.partial_cmp(&items[a].2).unwrap_or(std::cmp::Ordering::Equal).then_with(|| items[a].0.cmp(&items[b].0)));
for i in order {
let (id, _, _, cost) = &items[i];
let k = kind_of.get(id.as_str()).copied().unwrap_or("core");
let take = match caps.get(k) {
None => true,
Some(&cap) => {
let s = spent.entry(k).or_insert(0);
let next = s.saturating_add(*cost);
if next <= cap { *s = next; true } else { false }
}
};
if take { out.push(id.clone()); }
}
out
}
pub fn select_with_budget(items: &[scc_core::ContextItem], budget: usize, hard_max: usize) -> Vec<usize> {
scc_context::selector::select_with_budget(items, budget, hard_max)
}