1use std::collections::BTreeMap;
8
9use crate::log::LogRecord;
10use crate::manifest::AttributionManifest;
11use crate::slug::Stage;
12use crate::token::Token;
13
14pub trait Fold {
18 fn apply(&mut self, record: &LogRecord);
20}
21
22impl<A: Fold, B: Fold> Fold for (A, B) {
24 fn apply(&mut self, record: &LogRecord) {
25 self.0.apply(record);
26 self.1.apply(record);
27 }
28}
29
30pub fn replay<F: Fold>(records: impl IntoIterator<Item = LogRecord>, mut fold: F) -> F {
32 for rec in &records.into_iter().collect::<Vec<_>>() {
33 fold.apply(rec);
34 }
35 fold
36}
37
38#[derive(Debug, Default)]
43pub struct ManifestFold {
44 pub manifests: BTreeMap<Token, AttributionManifest>,
46}
47
48impl Fold for ManifestFold {
49 fn apply(&mut self, record: &LogRecord) {
50 match record {
51 LogRecord::Minted { manifest } => {
52 self.manifests
55 .entry(manifest.token)
56 .or_insert_with(|| (**manifest).clone());
57 }
58 LogRecord::Mutation {
59 token, at, change, ..
60 } => {
61 let Some(m) = self.manifests.get_mut(token) else {
62 return;
63 };
64 crate::manifest::apply_change(m, change, *at);
65 }
66 LogRecord::Event(_) => {}
67 }
68 }
69}
70
71#[derive(Debug, Default)]
74pub struct FunnelFold {
75 pub per_token: BTreeMap<Token, BTreeMap<Stage, u64>>,
77}
78
79impl Fold for FunnelFold {
80 fn apply(&mut self, record: &LogRecord) {
81 if let LogRecord::Event(e) = record {
82 *self
83 .per_token
84 .entry(e.token)
85 .or_default()
86 .entry(e.stage.clone())
87 .or_insert(0) += 1;
88 }
89 }
90}
91
92#[derive(Debug, Default)]
95pub struct LineageFold {
96 pub children: BTreeMap<Token, Vec<Token>>,
98}
99
100impl Fold for LineageFold {
101 fn apply(&mut self, record: &LogRecord) {
102 if let LogRecord::Minted { manifest } = record {
103 if let Some(parent) = manifest.parent {
104 let siblings = self.children.entry(parent).or_default();
105 if !siblings.contains(&manifest.token) {
106 siblings.push(manifest.token);
107 }
108 }
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::event::{ActorClass, Event, Seq};
117 use crate::log::Change;
118 use crate::{CanonicalUrl, Channel, MintOptions, MintSpec, ResolverContext, Sharer, Timestamp};
119
120 fn minted(tag: u8, parent: Option<Token>) -> AttributionManifest {
121 let mut entropy = move |buf: &mut [u8]| {
122 buf.fill(tag);
123 Ok(())
124 };
125 let mut spec = MintSpec::new(
126 CanonicalUrl::new("ws://a/b").unwrap(),
127 Sharer::new("lead").unwrap(),
128 Channel::subagent_general(),
129 );
130 if let Some(p) = parent {
131 spec = spec.child_of(p);
132 }
133 crate::mint(
134 spec,
135 &MintOptions::default(),
136 &mut entropy,
137 Timestamp::from_unix_ms(1),
138 )
139 .unwrap()
140 }
141
142 fn event(token: Token, stage: Stage, seq: u32) -> LogRecord {
143 LogRecord::Event(Event {
144 token,
145 stage,
146 actor: ActorClass::from_context(&ResolverContext::anonymous_agent()),
147 at: Timestamp::from_unix_ms(2),
148 seq: Seq(seq),
149 variant: None,
150 })
151 }
152
153 #[test]
154 fn one_pass_multi_fold_agrees_with_separate_passes() {
155 let parent = minted(1, None);
157 let child = minted(2, Some(parent.token));
158 let records = vec![
159 LogRecord::Minted {
160 manifest: Box::new(parent.clone()),
161 },
162 LogRecord::Minted {
163 manifest: Box::new(child.clone()),
164 },
165 event(parent.token, Stage::resolve(), 1),
166 event(parent.token, Stage::run(), 2),
167 LogRecord::Mutation {
168 token: parent.token,
169 at: Timestamp::from_unix_ms(3),
170 seq: Seq(3),
171 change: Change::Revoked,
172 },
173 ];
174
175 let (manifests, (funnel, lineage)) = replay(
176 records.clone(),
177 (
178 ManifestFold::default(),
179 (FunnelFold::default(), LineageFold::default()),
180 ),
181 );
182 let solo_m = replay(records.clone(), ManifestFold::default());
183 let solo_f = replay(records.clone(), FunnelFold::default());
184 let solo_l = replay(records, LineageFold::default());
185
186 assert_eq!(manifests.manifests, solo_m.manifests);
187 assert_eq!(funnel.per_token, solo_f.per_token);
188 assert_eq!(lineage.children, solo_l.children);
189
190 assert!(manifests.manifests[&parent.token].revoked_at.is_some());
192 assert_eq!(
193 manifests.manifests[&parent.token].version, 2,
194 "lifecycle bumps version"
195 );
196 assert_eq!(funnel.per_token[&parent.token][&Stage::run()], 1);
197 assert_eq!(lineage.children[&parent.token], vec![child.token]);
198 }
199
200 #[test]
201 fn cosmetic_changes_do_not_bump_version() {
202 let m = minted(5, None);
203 let records = vec![
204 LogRecord::Minted {
205 manifest: Box::new(m.clone()),
206 },
207 LogRecord::Mutation {
208 token: m.token,
209 at: Timestamp::from_unix_ms(2),
210 seq: Seq(1),
211 change: Change::LabelSet {
212 key: "campaign".into(),
213 value: "q3".into(),
214 },
215 },
216 ];
217 let folded = replay(records, ManifestFold::default());
218 let out = &folded.manifests[&m.token];
219 assert_eq!(out.version, 1);
220 assert_eq!(out.labels["campaign"], "q3");
221 }
222
223 #[test]
224 fn mutation_before_mint_is_ignored_not_a_panic() {
225 let m = minted(6, None);
228 let records = vec![LogRecord::Mutation {
229 token: m.token,
230 at: Timestamp::from_unix_ms(1),
231 seq: Seq(1),
232 change: Change::Revoked,
233 }];
234 let folded = replay(records, ManifestFold::default());
235 assert!(folded.manifests.is_empty());
236 }
237}