1use crate::morphism::{Morphism, ProvenMorphism};
44use crate::sexpr::{
45 ContentHash, FromSExpr, SExpr, SExprError, ToSExpr, parse_struct, struct_expr, take_field,
46};
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
63pub struct Quality(pub String);
64
65impl Quality {
66 pub fn new(name: impl Into<String>) -> Self {
67 Self(name.into())
68 }
69
70 pub fn as_str(&self) -> &str {
71 &self.0
72 }
73}
74
75impl std::fmt::Display for Quality {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.write_str(&self.0)
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum StageKind {
91 Promotion,
93 Mutation,
95}
96
97impl std::fmt::Display for StageKind {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Self::Promotion => f.write_str("promotion"),
101 Self::Mutation => f.write_str("mutation"),
102 }
103 }
104}
105
106pub struct Stage<Src, Dst> {
113 name: String,
114 kind: StageKind,
115 requires: Vec<Quality>,
116 establishes: Vec<Quality>,
117 morphism: Box<dyn ProvenMorphism<Src, Dst>>,
118}
119
120impl<Src, Dst> Stage<Src, Dst> {
121 pub fn new<M: ProvenMorphism<Src, Dst> + 'static>(
123 name: impl Into<String>,
124 kind: StageKind,
125 morphism: M,
126 ) -> Self {
127 Self {
128 name: name.into(),
129 kind,
130 requires: Vec::new(),
131 establishes: Vec::new(),
132 morphism: Box::new(morphism),
133 }
134 }
135
136 #[must_use]
138 pub fn requires(mut self, q: Quality) -> Self {
139 self.requires.push(q);
140 self
141 }
142
143 #[must_use]
145 pub fn establishes(mut self, q: Quality) -> Self {
146 self.establishes.push(q);
147 self
148 }
149
150 pub fn name(&self) -> &str {
152 &self.name
153 }
154 pub fn kind(&self) -> StageKind {
155 self.kind
156 }
157 pub fn requires_qualities(&self) -> &[Quality] {
158 &self.requires
159 }
160 pub fn establishes_qualities(&self) -> &[Quality] {
161 &self.establishes
162 }
163}
164
165impl<Src, Dst> Stage<Src, Dst>
166where
167 Src: ToSExpr,
168 Dst: ToSExpr,
169{
170 pub fn run(
174 &self,
175 input: &Src,
176 held_qualities: &[Quality],
177 ) -> Result<(Dst, TraceStep), PipelineError> {
178 for req in &self.requires {
180 if !held_qualities.contains(req) {
181 return Err(PipelineError::MissingQuality {
182 stage: self.name.clone(),
183 quality: req.clone(),
184 });
185 }
186 }
187
188 let output = self.morphism.apply(input);
189 let input_hash = input.content_hash();
190 let output_hash = output.content_hash();
191 let violations = self.morphism.check_invariants(input, &output);
192
193 let step = TraceStep {
194 stage_name: self.name.clone(),
195 morphism_name: self.morphism.name().to_string(),
196 kind: self.kind,
197 input_hash,
198 output_hash,
199 established: self.establishes.clone(),
200 violations: violations.clone(),
201 };
202
203 if !violations.is_empty() {
204 return Err(PipelineError::InvariantViolations {
205 stage: self.name.clone(),
206 violations,
207 });
208 }
209
210 Ok((output, step))
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct TraceStep {
219 pub stage_name: String,
220 pub morphism_name: String,
221 pub kind: StageKind,
222 pub input_hash: ContentHash,
223 pub output_hash: ContentHash,
224 pub established: Vec<Quality>,
225 pub violations: Vec<String>,
226}
227
228impl TraceStep {
229 pub fn is_clean(&self) -> bool {
230 self.violations.is_empty()
231 }
232}
233
234impl ToSExpr for TraceStep {
235 fn to_sexpr(&self) -> SExpr {
236 struct_expr(
237 "trace-step",
238 vec![
239 ("stage", self.stage_name.to_sexpr()),
240 ("morphism", self.morphism_name.to_sexpr()),
241 ("kind", SExpr::Symbol(format!("{}", self.kind))),
242 ("input-hash", SExpr::String(self.input_hash.to_hex())),
243 ("output-hash", SExpr::String(self.output_hash.to_hex())),
244 (
245 "established",
246 SExpr::List({
247 let mut v = vec![SExpr::Symbol("list".into())];
248 v.extend(self.established.iter().map(|q| SExpr::String(q.0.clone())));
249 v
250 }),
251 ),
252 (
253 "violations",
254 SExpr::List({
255 let mut v = vec![SExpr::Symbol("list".into())];
256 v.extend(self.violations.iter().map(|s| SExpr::String(s.clone())));
257 v
258 }),
259 ),
260 ],
261 )
262 }
263}
264
265impl FromSExpr for TraceStep {
266 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
267 let f = parse_struct(s, "trace-step")?;
268 let kind_sym = take_field(&f, "kind")?.as_symbol()?;
269 let kind = match kind_sym {
270 "promotion" => StageKind::Promotion,
271 "mutation" => StageKind::Mutation,
272 other => return Err(SExprError::UnknownVariant(format!("StageKind::{other}"))),
273 };
274 let input_hex = String::from_sexpr(take_field(&f, "input-hash")?)?;
275 let output_hex = String::from_sexpr(take_field(&f, "output-hash")?)?;
276 Ok(Self {
277 stage_name: String::from_sexpr(take_field(&f, "stage")?)?,
278 morphism_name: String::from_sexpr(take_field(&f, "morphism")?)?,
279 kind,
280 input_hash: hex_to_hash(&input_hex)?,
281 output_hash: hex_to_hash(&output_hex)?,
282 established: Vec::<String>::from_sexpr(take_field(&f, "established")?)?
283 .into_iter()
284 .map(Quality::new)
285 .collect(),
286 violations: Vec::<String>::from_sexpr(take_field(&f, "violations")?)?,
287 })
288 }
289}
290
291fn hex_to_hash(hex: &str) -> Result<ContentHash, SExprError> {
292 if hex.len() != 64 {
293 return Err(SExprError::Shape(format!(
294 "content hash must be 64 hex chars, got {}",
295 hex.len()
296 )));
297 }
298 let mut bytes = [0u8; 32];
299 for (i, pair) in hex.as_bytes().chunks(2).enumerate() {
300 let s = std::str::from_utf8(pair)
301 .map_err(|_| SExprError::Parse("non-ascii in hash hex".into()))?;
302 bytes[i] = u8::from_str_radix(s, 16)
303 .map_err(|e| SExprError::Parse(format!("bad hex byte: {e}")))?;
304 }
305 Ok(ContentHash(bytes))
306}
307
308#[derive(Debug, Clone, Default, PartialEq, Eq)]
310pub struct Trace {
311 pub steps: Vec<TraceStep>,
312}
313
314impl Trace {
315 pub fn new() -> Self {
316 Self::default()
317 }
318
319 pub fn push(&mut self, step: TraceStep) {
320 self.steps.push(step);
321 }
322
323 pub fn is_empty(&self) -> bool {
324 self.steps.is_empty()
325 }
326
327 pub fn len(&self) -> usize {
328 self.steps.len()
329 }
330
331 pub fn is_clean(&self) -> bool {
333 self.steps.iter().all(TraceStep::is_clean)
334 }
335
336 pub fn established(&self) -> Vec<Quality> {
338 let mut out: Vec<Quality> = Vec::new();
339 for step in &self.steps {
340 for q in &step.established {
341 if !out.contains(q) {
342 out.push(q.clone());
343 }
344 }
345 }
346 out.sort();
347 out
348 }
349}
350
351impl ToSExpr for Trace {
352 fn to_sexpr(&self) -> SExpr {
353 struct_expr("trace", vec![("steps", self.steps.to_sexpr())])
354 }
355}
356
357impl FromSExpr for Trace {
358 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
359 let f = parse_struct(s, "trace")?;
360 Ok(Self {
361 steps: Vec::<TraceStep>::from_sexpr(take_field(&f, "steps")?)?,
362 })
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
369pub enum PipelineError {
370 MissingQuality {
371 stage: String,
372 quality: Quality,
373 },
374 InvariantViolations {
375 stage: String,
376 violations: Vec<String>,
377 },
378}
379
380impl std::fmt::Display for PipelineError {
381 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382 match self {
383 Self::MissingQuality { stage, quality } => write!(
384 f,
385 "stage '{stage}' requires quality '{quality}' which is not established"
386 ),
387 Self::InvariantViolations { stage, violations } => write!(
388 f,
389 "stage '{stage}' produced invariant violations: {violations:?}"
390 ),
391 }
392 }
393}
394
395impl std::error::Error for PipelineError {}
396
397pub fn run_mutation_chain<T>(
406 start: T,
407 stages: Vec<Stage<T, T>>,
408 initial_qualities: &[Quality],
409) -> Result<(T, Trace), (PipelineError, Trace)>
410where
411 T: ToSExpr + Clone,
412{
413 let mut current = start;
414 let mut trace = Trace::new();
415 let mut held: Vec<Quality> = initial_qualities.to_vec();
416
417 for stage in stages {
418 match stage.run(¤t, &held) {
419 Ok((output, step)) => {
420 for q in &step.established {
421 if !held.contains(q) {
422 held.push(q.clone());
423 }
424 }
425 trace.push(step);
426 current = output;
427 }
428 Err(e) => return Err((e, trace)),
429 }
430 }
431
432 Ok((current, trace))
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::morphism::{Morphism, ProvenMorphism};
439
440 struct AddOne;
443 impl Morphism<i64, i64> for AddOne {
444 fn name(&self) -> &'static str {
445 "AddOne"
446 }
447 fn apply(&self, src: &i64) -> i64 {
448 src + 1
449 }
450 }
451 impl ProvenMorphism<i64, i64> for AddOne {
452 fn check_invariants(&self, src: &i64, dst: &i64) -> Vec<String> {
453 if *dst == src + 1 {
454 Vec::new()
455 } else {
456 vec!["AddOne: dst != src + 1".into()]
457 }
458 }
459 }
460
461 struct Double;
462 impl Morphism<i64, i64> for Double {
463 fn name(&self) -> &'static str {
464 "Double"
465 }
466 fn apply(&self, src: &i64) -> i64 {
467 src * 2
468 }
469 }
470 impl ProvenMorphism<i64, i64> for Double {
471 fn check_invariants(&self, src: &i64, dst: &i64) -> Vec<String> {
472 if *dst == src * 2 {
473 Vec::new()
474 } else {
475 vec!["Double: dst != 2 * src".into()]
476 }
477 }
478 }
479
480 #[test]
483 fn stage_records_hashes_on_clean_run() {
484 let stage = Stage::new("add-one", StageKind::Mutation, AddOne);
485 let (out, step) = stage.run(&5, &[]).unwrap();
486 assert_eq!(out, 6);
487 assert_eq!(step.input_hash, 5i64.content_hash());
488 assert_eq!(step.output_hash, 6i64.content_hash());
489 assert!(step.is_clean());
490 assert_eq!(step.kind, StageKind::Mutation);
491 }
492
493 #[test]
494 fn stage_requires_quality_present_in_held() {
495 let stage =
496 Stage::new("x", StageKind::Mutation, AddOne).requires(Quality::new("validated"));
497 let err = stage.run(&5, &[]).unwrap_err();
498 assert!(matches!(err, PipelineError::MissingQuality { .. }));
499
500 let ok = stage.run(&5, &[Quality::new("validated")]);
501 assert!(ok.is_ok());
502 }
503
504 #[test]
505 fn stage_establishes_quality_recorded_in_step() {
506 let stage =
507 Stage::new("x", StageKind::Mutation, AddOne).establishes(Quality::new("normalized"));
508 let (_, step) = stage.run(&5, &[]).unwrap();
509 assert_eq!(step.established, vec![Quality::new("normalized")]);
510 }
511
512 #[test]
515 fn mutation_chain_runs_in_order() {
516 let stages = vec![
517 Stage::new("s1", StageKind::Mutation, AddOne),
518 Stage::new("s2", StageKind::Mutation, Double),
519 ];
520 let (out, trace) = run_mutation_chain(5_i64, stages, &[]).unwrap();
521 assert_eq!(out, 12); assert_eq!(trace.len(), 2);
523 assert!(trace.is_clean());
524 }
525
526 #[test]
527 fn mutation_chain_propagates_established_qualities() {
528 let stages = vec![
529 Stage::new("promote", StageKind::Mutation, AddOne)
530 .establishes(Quality::new("normalized")),
531 Stage::new("then-requires", StageKind::Mutation, Double)
532 .requires(Quality::new("normalized")),
533 ];
534 let result = run_mutation_chain(1_i64, stages, &[]);
535 assert!(result.is_ok(), "qualities should propagate: {result:?}");
536 }
537
538 #[test]
539 fn mutation_chain_halts_on_missing_quality() {
540 let stages = vec![
541 Stage::new("needs-validated", StageKind::Mutation, AddOne)
542 .requires(Quality::new("validated")),
543 ];
544 let (err, partial) = run_mutation_chain(1_i64, stages, &[]).unwrap_err();
545 assert!(matches!(err, PipelineError::MissingQuality { .. }));
546 assert!(partial.is_empty(), "no steps should have run");
547 }
548
549 struct BadDouble;
552 impl Morphism<i64, i64> for BadDouble {
553 fn name(&self) -> &'static str {
554 "BadDouble"
555 }
556 fn apply(&self, src: &i64) -> i64 {
557 src * 3 }
559 }
560 impl ProvenMorphism<i64, i64> for BadDouble {
561 fn check_invariants(&self, src: &i64, dst: &i64) -> Vec<String> {
562 if *dst == src * 2 {
563 Vec::new()
564 } else {
565 vec!["Double: dst != 2 * src".into()]
566 }
567 }
568 }
569
570 #[test]
571 fn mutation_chain_halts_on_invariant_violation() {
572 let stages = vec![
573 Stage::new("s1", StageKind::Mutation, AddOne),
574 Stage::new("bad", StageKind::Mutation, BadDouble),
575 Stage::new("s3", StageKind::Mutation, AddOne),
576 ];
577 let (err, partial) = run_mutation_chain(5_i64, stages, &[]).unwrap_err();
578 assert!(matches!(err, PipelineError::InvariantViolations { .. }));
579 assert_eq!(partial.len(), 1);
581 assert_eq!(partial.steps[0].stage_name, "s1");
582 }
583
584 #[test]
587 fn trace_step_round_trips_via_sexpr() {
588 let stage = Stage::new("x", StageKind::Mutation, AddOne).establishes(Quality::new("q1"));
589 let (_, step) = stage.run(&5, &[]).unwrap();
590 let round = TraceStep::from_sexpr(&step.to_sexpr()).expect("parse");
591 assert_eq!(round, step);
592 }
593
594 #[test]
595 fn trace_round_trips_via_sexpr() {
596 let stages = vec![
597 Stage::new("a", StageKind::Mutation, AddOne),
598 Stage::new("b", StageKind::Mutation, Double),
599 ];
600 let (_, trace) = run_mutation_chain(1_i64, stages, &[]).unwrap();
601 let round = Trace::from_sexpr(&trace.to_sexpr()).expect("parse");
602 assert_eq!(round, trace);
603 }
604
605 #[test]
608 fn trace_content_hash_is_deterministic() {
609 let make = || {
610 let stages = vec![
611 Stage::new("a", StageKind::Mutation, AddOne),
612 Stage::new("b", StageKind::Mutation, Double),
613 ];
614 run_mutation_chain(1_i64, stages, &[]).unwrap().1
615 };
616 assert_eq!(make().content_hash(), make().content_hash());
617 }
618
619 #[test]
620 fn trace_hash_differs_for_different_inputs() {
621 let stages1 = vec![Stage::new("a", StageKind::Mutation, AddOne)];
622 let stages2 = vec![Stage::new("a", StageKind::Mutation, AddOne)];
623 let t1 = run_mutation_chain(1_i64, stages1, &[]).unwrap().1;
624 let t2 = run_mutation_chain(99_i64, stages2, &[]).unwrap().1;
625 assert_ne!(t1.content_hash(), t2.content_hash());
626 }
627
628 #[test]
631 fn trace_aggregates_all_established_qualities() {
632 let stages = vec![
633 Stage::new("a", StageKind::Mutation, AddOne).establishes(Quality::new("q1")),
634 Stage::new("b", StageKind::Mutation, Double)
635 .establishes(Quality::new("q2"))
636 .establishes(Quality::new("q1")), ];
638 let (_, trace) = run_mutation_chain(1_i64, stages, &[]).unwrap();
639 let qs = trace.established();
640 assert_eq!(qs.len(), 2);
641 assert!(qs.contains(&Quality::new("q1")));
642 assert!(qs.contains(&Quality::new("q2")));
643 }
644
645 #[test]
648 fn trace_step_preserves_stage_kind() {
649 let m = Stage::new("m", StageKind::Mutation, AddOne);
650 let p = Stage::new("p", StageKind::Promotion, AddOne);
651 let (_, step_m) = m.run(&1, &[]).unwrap();
652 let (_, step_p) = p.run(&1, &[]).unwrap();
653 assert_eq!(step_m.kind, StageKind::Mutation);
654 assert_eq!(step_p.kind, StageKind::Promotion);
655 }
656}