use aristo_core::config::Aggressiveness;
use aristo_core::metrics::Metrics;
pub mod intents;
pub mod state;
pub mod throttle;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Audience {
Agent,
Human,
}
#[derive(Debug, Clone)]
pub struct EngineInputs {
pub metrics: Metrics,
pub edits_since_annotation: usize,
pub unreviewed_intents: usize,
pub proofs_awaiting_review: usize,
pub canon_pending: usize,
pub prior_score: Option<f64>,
pub tier_increased: bool,
pub signed_in: bool,
}
pub struct Signal {
pub id: &'static str,
pub audience: Audience,
pub base: f64,
pub metric: fn(&EngineInputs) -> f64,
}
impl Signal {
pub fn pressure(&self, inputs: &EngineInputs) -> f64 {
(self.metric)(inputs) / self.base
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fired {
pub id: &'static str,
pub audience: Audience,
pub pressure: f64,
pub metric: f64,
pub base: f64,
}
pub static SIGNALS: &[Signal] = &[
Signal {
id: "congrats",
audience: Audience::Human,
base: 0.05,
metric: metric_congrats,
},
Signal {
id: "review_backlog",
audience: Audience::Human,
base: 3.0,
metric: metric_review_backlog,
},
Signal {
id: "canon_pending",
audience: Audience::Human,
base: 3.0,
metric: metric_canon_pending,
},
Signal {
id: "verify_backlog",
audience: Audience::Human,
base: 0.25,
metric: metric_verify_backlog,
},
Signal {
id: "proof_review_backlog",
audience: Audience::Human,
base: 3.0,
metric: metric_proof_review_backlog,
},
Signal {
id: "score_slump",
audience: Audience::Human,
base: 0.05,
metric: metric_score_slump,
},
Signal {
id: "authoring_debt",
audience: Audience::Agent,
base: 3.0,
metric: metric_authoring_debt,
},
];
fn metric_authoring_debt(i: &EngineInputs) -> f64 {
i.edits_since_annotation as f64
}
fn metric_review_backlog(i: &EngineInputs) -> f64 {
i.unreviewed_intents as f64
}
fn metric_proof_review_backlog(i: &EngineInputs) -> f64 {
i.proofs_awaiting_review as f64
}
fn metric_canon_pending(i: &EngineInputs) -> f64 {
if i.signed_in {
i.canon_pending as f64
} else {
0.0
}
}
fn metric_verify_backlog(i: &EngineInputs) -> f64 {
if i.metrics.verifiable == 0 {
0.0
} else {
i.metrics.unverified as f64 / i.metrics.verifiable as f64
}
}
fn metric_score_slump(i: &EngineInputs) -> f64 {
match i.prior_score {
Some(prior) if prior > 0.0 => {
let drop = prior - i.metrics.visible_score;
if drop > 0.0 {
drop / prior
} else {
0.0
}
}
_ => 0.0,
}
}
fn metric_congrats(i: &EngineInputs) -> f64 {
if i.tier_increased {
return f64::INFINITY;
}
match i.prior_score {
Some(prior) => (i.metrics.visible_score - prior).max(0.0),
None => 0.0,
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Decision {
pub human: Vec<Fired>,
pub agent: Vec<Fired>,
}
impl Decision {
pub fn recommended(&self) -> Option<&'static str> {
self.human.first().map(|f| f.id)
}
pub fn is_silent(&self) -> bool {
self.human.is_empty() && self.agent.is_empty()
}
}
#[aristo::intent(
"Two invariants the scorer must preserve. First, `aggressiveness = off` \
is an absolute silence: its factor is 0.0 and the fire test is \
`pressure * factor >= 1`, so no signal fires at any pressure — even an \
infinite one. Second, the surfaced order is the static \
SIGNALS priority order, NOT the pressures: a count-pressure and a \
fraction-pressure are incommensurable, so sorting by pressure would let \
a noisy low-priority signal jump the queue ahead of a review the user \
actually needs to see first.",
verify = "test",
id = "nudge_scorer_off_silences_and_order_is_static_priority"
)]
pub fn score(inputs: &EngineInputs, aggressiveness: Aggressiveness) -> Decision {
let factor = aggressiveness.factor();
let mut decision = Decision::default();
for signal in SIGNALS {
let metric = (signal.metric)(inputs);
let pressure = metric / signal.base;
if pressure * factor >= 1.0 {
let fired = Fired {
id: signal.id,
audience: signal.audience,
pressure,
metric,
base: signal.base,
};
match signal.audience {
Audience::Human => decision.human.push(fired),
Audience::Agent => decision.agent.push(fired),
}
}
}
decision
}
#[aristo::intent(
"Scoring the authoring-debt (agent) signal needs ONLY the edit counter — \
never the index-derived Metrics. The PostToolUse hook that drives it \
fires on every edit, so it must not walk the source tree per edit; this \
scores the one signal straight from the counter, reusing the registry's \
base and the identical `pressure * factor >= 1` fire rule so it can't \
drift from `score`.",
verify = "test",
id = "score_authoring_debt_needs_no_index_walk"
)]
pub fn score_authoring_debt(
edits_since_annotation: usize,
aggressiveness: Aggressiveness,
) -> Option<Fired> {
let signal = SIGNALS.iter().find(|s| s.id == "authoring_debt")?;
let metric = edits_since_annotation as f64;
let pressure = metric / signal.base;
if pressure * aggressiveness.factor() >= 1.0 {
Some(Fired {
id: signal.id,
audience: signal.audience,
pressure,
metric,
base: signal.base,
})
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use aristo_core::badge::Tier;
use aristo_core::metrics::{Metrics, METRICS_SCHEMA_VERSION};
fn metrics(verifiable: usize, unverified: usize, score: f64, tier: Tier) -> Metrics {
Metrics {
schema_version: METRICS_SCHEMA_VERSION,
intents: verifiable,
assumes: 0,
verifiable,
verified_clean: verifiable - unverified,
unverified,
verification_rate: if verifiable == 0 {
0.0
} else {
(verifiable - unverified) as f64 / verifiable as f64
},
tier,
visible_score: score,
}
}
fn inputs() -> EngineInputs {
EngineInputs {
metrics: metrics(0, 0, 0.0, Tier::Aspirant),
edits_since_annotation: 0,
unreviewed_intents: 0,
proofs_awaiting_review: 0,
canon_pending: 0,
prior_score: None,
tier_increased: false,
signed_in: false,
}
}
#[test]
fn off_is_a_hard_silence_even_under_extreme_pressure() {
let mut i = inputs();
i.unreviewed_intents = 1000;
i.edits_since_annotation = 1000;
i.tier_increased = true;
let d = score(&i, Aggressiveness::Off);
assert!(d.is_silent(), "off must silence everything: {d:?}");
}
#[test]
fn nothing_fires_at_zero_pressure() {
let d = score(&inputs(), Aggressiveness::High);
assert!(d.is_silent());
}
#[test]
fn review_backlog_fires_at_base_under_medium() {
let mut i = inputs();
i.unreviewed_intents = 3; let d = score(&i, Aggressiveness::Medium);
assert!(d.human.iter().any(|f| f.id == "review_backlog"));
}
#[test]
fn low_aggressiveness_needs_more_pressure_than_medium() {
let mut i = inputs();
i.unreviewed_intents = 3; assert!(score(&i, Aggressiveness::Low)
.human
.iter()
.all(|f| f.id != "review_backlog"));
assert!(score(&i, Aggressiveness::Medium)
.human
.iter()
.any(|f| f.id == "review_backlog"));
}
#[test]
fn canon_pending_is_silent_until_signed_in() {
let mut i = inputs();
i.canon_pending = 10;
assert!(score(&i, Aggressiveness::High)
.human
.iter()
.all(|f| f.id != "canon_pending"));
i.signed_in = true;
assert!(score(&i, Aggressiveness::High)
.human
.iter()
.any(|f| f.id == "canon_pending"));
}
#[test]
fn tier_up_always_congratulates_when_on() {
let mut i = inputs();
i.tier_increased = true;
let d = score(&i, Aggressiveness::Low);
assert_eq!(
d.recommended(),
Some("congrats"),
"tier-up leads as the banner"
);
}
#[test]
fn verify_backlog_uses_unverified_fraction() {
let mut i = inputs();
i.metrics = metrics(1, 1, 0.0, Tier::Aspirant);
assert!(score(&i, Aggressiveness::Low)
.human
.iter()
.any(|f| f.id == "verify_backlog"));
i.metrics = metrics(4, 0, 0.5, Tier::Adept);
assert!(score(&i, Aggressiveness::High)
.human
.iter()
.all(|f| f.id != "verify_backlog"));
}
#[test]
fn score_slump_fires_on_a_relative_drop_from_baseline() {
let mut i = inputs();
i.prior_score = Some(0.50);
i.metrics = metrics(0, 0, 0.40, Tier::Adept); assert!(score(&i, Aggressiveness::Low)
.human
.iter()
.any(|f| f.id == "score_slump"));
i.metrics = metrics(0, 0, 0.60, Tier::Adept);
assert!(score(&i, Aggressiveness::High)
.human
.iter()
.all(|f| f.id != "score_slump"));
}
#[test]
fn authoring_debt_is_agent_audience() {
let mut i = inputs();
i.edits_since_annotation = 3;
let d = score(&i, Aggressiveness::Medium);
assert!(d.agent.iter().any(|f| f.id == "authoring_debt"));
assert!(d.human.iter().all(|f| f.id != "authoring_debt"));
}
#[test]
fn score_authoring_debt_agrees_with_full_score() {
for edits in [0usize, 2, 3, 10] {
for agg in [
Aggressiveness::Off,
Aggressiveness::Low,
Aggressiveness::Medium,
Aggressiveness::High,
] {
let mut i = inputs();
i.edits_since_annotation = edits;
let full = score(&i, agg)
.agent
.iter()
.any(|f| f.id == "authoring_debt");
let fast = score_authoring_debt(edits, agg).is_some();
assert_eq!(full, fast, "edits={edits} agg={agg:?}");
}
}
assert!(score_authoring_debt(1000, Aggressiveness::Off).is_none());
}
#[test]
fn human_signals_keep_static_priority_order() {
let mut i = inputs();
i.signed_in = true;
i.unreviewed_intents = 100; i.canon_pending = 3; i.metrics = metrics(1, 1, 0.0, Tier::Aspirant); let d = score(&i, Aggressiveness::Medium);
let order: Vec<&str> = d.human.iter().map(|f| f.id).collect();
let review = order.iter().position(|x| *x == "review_backlog").unwrap();
let canon = order.iter().position(|x| *x == "canon_pending").unwrap();
let verify = order.iter().position(|x| *x == "verify_backlog").unwrap();
assert!(
review < canon && canon < verify,
"priority order: {order:?}"
);
assert_eq!(d.recommended(), Some("review_backlog"));
}
}