KeiSeiKit-1.0/_primitives/_rust/kei-pipe/src/report.rs
Parfii-bot 4a9dd98fde feat(p-pipe-cache): wire kei-cache into kei-pipe DAG executor
Optional per-step and DAG-level cache config in dag.toml:
  [[steps]]
  cache = { enabled = true, ttl_sec = 3600 }
OR
  [pipe]
  cache = { enabled = true, ttl_sec = 3600 }

Cache gated by AtomKind — only query/transform cacheable; command/stream
always re-invoke even with cache.enabled=true.

StepReport.source: Some('cache'|'fresh') | None shows cache outcome.

Constructor Pattern: extracted src/config.rs (CacheConfig + StepKind
+ TOML raw types + split_pipe_cache parser) + src/topo.rs (topo-sort)
to keep dag.rs under 200 LOC.

Tests: 8/8 (was 5, +3: cache-hit reuse, cache-disabled always invokes,
command-kind not cached even if enabled).

kei-cache 22/22 preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:26:11 +08:00

103 lines
3.2 KiB
Rust

//! Per-step and DAG-level run reports.
//!
//! A [`StepReport`] is emitted for every step actually attempted, in
//! execution order. A [`DagReport`] aggregates them and exposes the
//! resolver lookup map so later steps can reference earlier outputs.
//!
//! When a step fails, execution halts (sequential runtime) and the
//! failing step is the last entry in `steps`. Callers can check
//! `final_ok()` and inspect `steps.last()` for the error.
use serde::Serialize;
use serde_json::{json, Value};
use std::collections::HashMap;
/// One step's outcome.
///
/// `source` is set only when caching was active for the step:
/// `Some("cache")` on a cache hit, `Some("fresh")` on a cache miss (atom
/// was invoked and its result stored), `None` when caching was disabled
/// or the atom kind gated it out.
#[derive(Debug, Clone, Serialize)]
pub struct StepReport {
pub id: String,
pub atom: String,
pub ok: bool,
pub result: Option<Value>,
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
impl StepReport {
pub fn ok(id: &str, atom: &str, result: Value) -> Self {
Self {
id: id.into(),
atom: atom.into(),
ok: true,
result: Some(result),
error: None,
source: None,
}
}
pub fn fail(id: &str, atom: &str, error: String) -> Self {
Self {
id: id.into(),
atom: atom.into(),
ok: false,
result: None,
error: Some(error),
source: None,
}
}
/// Builder-style: attach a cache source label (`"cache"` or `"fresh"`).
pub fn with_source(mut self, source: &str) -> Self {
self.source = Some(source.into());
self
}
}
/// Full-DAG outcome. `final_result` is the `result` of the last
/// successful step, or `null` when nothing ran successfully.
#[derive(Debug, Clone, Default, Serialize)]
pub struct DagReport {
pub steps: Vec<StepReport>,
pub final_result: Value,
/// Resolver lookup — envelope shape `{"atom":..., "result":...}`.
#[serde(skip)]
resolver: HashMap<String, Value>,
}
impl DagReport {
pub fn new() -> Self {
Self {
steps: Vec::new(),
final_result: Value::Null,
resolver: HashMap::new(),
}
}
/// Append one step's report. On success, also updates the resolver
/// map so downstream `$step.result.foo` references work.
pub fn push(&mut self, step: StepReport) {
if step.ok {
let envelope = json!({ "atom": step.atom, "result": step.result });
self.resolver.insert(step.id.clone(), envelope);
if let Some(ref r) = step.result {
self.final_result = r.clone();
}
}
self.steps.push(step);
}
/// Borrow the resolver map for downstream `$step.path` lookups.
pub fn results(&self) -> &HashMap<String, Value> {
&self.resolver
}
/// True when every step completed with `ok = true` AND at least one
/// step ran (an empty DAG counts as ok-but-empty).
pub fn final_ok(&self) -> bool {
self.steps.iter().all(|s| s.ok)
}
}