areev_loop/analyzers/
fork_surfacing.rs1use crate::analyzer::{AnalyzeCtx, Analyzer};
8use crate::analyzers::bound_evidence;
9use crate::cal;
10use crate::error::Result;
11use crate::manifest::*;
12use crate::model::{ActionKind, Severity};
13use crate::recommendation::{Proposal, RecDraft, Summary};
14use serde_json::{json, Map};
15
16pub struct ForkSurfacing {
17 manifest: AnalyzerManifest,
18}
19
20impl ForkSurfacing {
21 pub fn new() -> Self {
22 ForkSurfacing {
23 manifest: AnalyzerManifest {
24 id: "loop.fork_surfacing/1".into(),
25 title: "Fork surfacing".into(),
26 description: "Surfaces entities with multiple live heads and proposes a merge."
27 .into(),
28 tier: Tier::T0,
29 cadence: CadenceClass::Fast,
30 requires: vec![Capability::Forks],
31 target_classes: vec![TargetClass::Memory],
32 auto_apply: AutoApplyClass::Never,
36 trust_class: TrustClass::Builtin,
37 params: vec![],
38 default_on: true,
39 },
40 }
41 }
42}
43
44impl Default for ForkSurfacing {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl Analyzer for ForkSurfacing {
51 fn manifest(&self) -> &AnalyzerManifest {
52 &self.manifest
53 }
54
55 fn analyze(&self, ctx: &AnalyzeCtx) -> Result<Vec<RecDraft>> {
56 if !ctx.capabilities().forks {
58 return Ok(vec![]);
59 }
60 let mut groups = ctx.heads()?;
61 groups.sort_by(|a, b| a.entity.cmp(&b.entity));
62
63 let mut drafts = Vec::new();
64 for group in groups {
65 if group.heads.len() < 2 {
66 continue;
67 }
68 let mut heads = group.heads.clone();
69 heads.sort(); let primary = heads[0].clone();
71
72 let mut statements = Vec::new();
74 for secondary in &heads[1..] {
75 let mut fields = Map::new();
76 fields.insert("merge_into".into(), json!(primary));
77 statements.push(cal::supersede(secondary, "state", &fields));
78 }
79 let evidence = bound_evidence(heads.clone());
80
81 let mut args = Map::new();
82 args.insert("entity".into(), json!(group.entity));
83 args.insert("count".into(), json!(heads.len()));
84
85 drafts.push(
86 RecDraft::new(
87 format!("entity:{}", group.entity),
88 ActionKind::MergeHeads,
89 Summary::new("fork.multi_head", args),
90 Proposal::Cal {
91 cal: cal::batch(&statements),
92 },
93 )
94 .severity(Severity::Medium)
95 .evidence(evidence),
96 );
97 }
98 Ok(drafts)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::testkit::TestSubstrate;
106
107 #[test]
108 fn surfaces_multi_head_entity() {
109 let mut sub = TestSubstrate::new();
110 sub.add_fork("caller/john", &["ref-a", "ref-b"]);
111 let drafts = sub.analyze(&ForkSurfacing::new(), 10_000);
112 assert_eq!(drafts.len(), 1);
113 assert_eq!(drafts[0].action_kind, ActionKind::MergeHeads);
114 assert_eq!(drafts[0].evidence.len(), 2);
115 }
116
117 #[test]
118 fn degrades_when_forks_unavailable() {
119 let sub = TestSubstrate::new(); assert!(sub.analyze(&ForkSurfacing::new(), 10_000).is_empty());
121 }
122}