use crate::error::Result;
use crate::manifest::{AnalyzerManifest, Params};
use crate::model::GrainRecord;
use crate::recommendation::RecDraft;
use crate::substrate::{HeadGroup, ReadOpts, SubstrateRead, TelemetryView};
#[derive(Debug, Clone, PartialEq)]
pub struct OutcomeInput {
pub rec_hash: String,
pub target_ref: String,
pub metric: String,
pub baseline: f64,
pub current: f64,
pub unit: String,
}
pub struct AnalyzeCtx<'a> {
reader: &'a dyn SubstrateRead,
params: &'a Params,
namespaces: &'a [String],
watermark_ms: Option<i64>,
now_ms: i64,
outcome_inputs: &'a [OutcomeInput],
}
impl<'a> AnalyzeCtx<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
reader: &'a dyn SubstrateRead,
params: &'a Params,
namespaces: &'a [String],
watermark_ms: Option<i64>,
now_ms: i64,
outcome_inputs: &'a [OutcomeInput],
) -> Self {
AnalyzeCtx {
reader,
params,
namespaces,
watermark_ms,
now_ms,
outcome_inputs,
}
}
pub fn params(&self) -> &Params {
self.params
}
pub fn now_ms(&self) -> i64 {
self.now_ms
}
pub fn watermark_ms(&self) -> Option<i64> {
self.watermark_ms
}
pub fn capabilities(&self) -> crate::substrate::Capabilities {
self.reader.capabilities()
}
pub fn outcome_inputs(&self) -> &[OutcomeInput] {
self.outcome_inputs
}
pub fn grains_of_type(&self, grain_type: &str, opts: ReadOpts) -> Result<Vec<GrainRecord>> {
if self.namespaces.is_empty() {
self.reader.grains_of_type(grain_type, None, opts)
} else {
let mut out = Vec::new();
for ns in self.namespaces {
out.extend(self.reader.grains_of_type(grain_type, Some(ns), opts)?);
}
Ok(out)
}
}
pub fn facts(&self) -> Result<Vec<GrainRecord>> {
self.grains_of_type(crate::model::grain_type::FACT, ReadOpts::default())
}
pub fn observations(&self) -> Result<Vec<GrainRecord>> {
self.grains_of_type(crate::model::grain_type::OBSERVATION, ReadOpts::default())
}
pub fn grains_in(&self, grain_type: &str, namespace: &str) -> Result<Vec<GrainRecord>> {
self.reader
.grains_of_type(grain_type, Some(namespace), ReadOpts::default())
}
pub fn skills(&self) -> Result<Vec<GrainRecord>> {
self.grains_of_type(crate::model::grain_type::SKILL, ReadOpts::default())
}
pub fn goals(&self) -> Result<Vec<GrainRecord>> {
self.grains_of_type(crate::model::grain_type::GOAL, ReadOpts::default())
}
pub fn tools_since(&self, since_ms: Option<i64>) -> Result<Vec<GrainRecord>> {
self.grains_of_type(
crate::model::grain_type::TOOL,
ReadOpts {
live_only: true,
since_ms,
},
)
}
pub fn heads(&self) -> Result<Vec<HeadGroup>> {
let ns = if self.namespaces.len() == 1 {
Some(self.namespaces[0].as_str())
} else {
None
};
self.reader.heads(ns)
}
pub fn telemetry(&self) -> Result<Option<TelemetryView>> {
let ns = if self.namespaces.len() == 1 {
Some(self.namespaces[0].as_str())
} else {
None
};
self.reader.telemetry(ns)
}
}
pub trait Analyzer: Send + Sync {
fn manifest(&self) -> &AnalyzerManifest;
fn analyze(&self, ctx: &AnalyzeCtx) -> Result<Vec<RecDraft>>;
}
pub fn builtin_analyzers() -> Vec<Box<dyn Analyzer>> {
vec![
Box::new(crate::analyzers::tool_failure::ToolFailureClustering::new()),
Box::new(crate::analyzers::duplicate_sweep::DuplicateSweep::new()),
Box::new(crate::analyzers::contradiction_sweep::ContradictionSweep::new()),
Box::new(crate::analyzers::fork_surfacing::ForkSurfacing::new()),
Box::new(crate::analyzers::staleness::Staleness::new()),
Box::new(crate::analyzers::skill_stall::SkillStall::new()),
Box::new(crate::analyzers::goal_stagnation::GoalStagnation::new()),
Box::new(crate::analyzers::cold_grains::ColdGrains::new()),
Box::new(crate::analyzers::coverage_gap::CoverageGap::new()),
Box::new(crate::analyzers::budget_pressure::BudgetPressure::new()),
Box::new(crate::analyzers::outcome_review::OutcomeReview::new()),
Box::new(crate::analyzers::retention_sweep::RetentionSweep::new()),
Box::new(crate::analyzers::run_outcome::RunOutcome::new()),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtins_have_unique_ids() {
let a = builtin_analyzers();
assert_eq!(
a.len(),
13,
"6 hygiene + skill/goal trajectory + 3 telemetry-fed (cold/coverage/budget) \
+ retention (default-off) + run_outcome"
);
let mut ids: Vec<&str> = a.iter().map(|x| x.manifest().id.as_str()).collect();
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), 13, "analyzer ids must be unique");
}
#[test]
fn all_builtins_are_trust_class_builtin() {
for a in builtin_analyzers() {
assert_eq!(
a.manifest().trust_class,
crate::manifest::TrustClass::Builtin,
"{} must be builtin trust class",
a.manifest().id
);
}
}
}