Skip to main content

ainl_runtime/adapters/
graph_patch.rs

1//! Reference [`GraphPatchAdapter`] — built-in fallback for procedural patches (`"graph_patch"`).
2//!
3//! Returns a small JSON summary for hosts; optional [`GraphPatchHostDispatch`] forwards that value.
4
5use std::sync::Arc;
6
7use serde_json::{json, Value};
8
9use super::PatchAdapter;
10use crate::engine::PatchDispatchContext;
11use ainl_memory::AinlNodeType;
12
13/// Optional host hook: receives the same JSON summary as [`GraphPatchAdapter::execute_patch`].
14pub trait GraphPatchHostDispatch: Send + Sync {
15    fn on_patch_dispatch(&self, envelope: Value) -> Result<Value, String>;
16}
17
18/// Reference adapter registered as [`Self::NAME`]. Used as a **fallback** when no label-specific
19/// [`PatchAdapter`] is registered for the procedural patch label.
20pub struct GraphPatchAdapter {
21    host: Option<Arc<dyn GraphPatchHostDispatch>>,
22}
23
24impl GraphPatchAdapter {
25    pub const NAME: &'static str = "graph_patch";
26
27    pub fn new() -> Self {
28        Self { host: None }
29    }
30
31    pub fn with_host(host: Arc<dyn GraphPatchHostDispatch>) -> Self {
32        Self { host: Some(host) }
33    }
34}
35
36impl Default for GraphPatchAdapter {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42fn build_summary(ctx: &PatchDispatchContext<'_>) -> Result<Value, String> {
43    let proc = match &ctx.node.node_type {
44        AinlNodeType::Procedural { procedural } => procedural,
45        _ => {
46            return Err("graph_patch: PatchDispatchContext.node is not procedural".to_string());
47        }
48    };
49    for key in &proc.declared_reads {
50        if !ctx.frame.contains_key(key) {
51            return Err(format!(
52                "graph_patch: declared read {key:?} missing from frame (adapter safety check)"
53            ));
54        }
55    }
56    let mut frame_keys: Vec<String> = ctx.frame.keys().cloned().collect();
57    frame_keys.sort_unstable();
58    // Optional `graph_writes` may be merged by the host (e.g. planner `DeterministicPlan`) before
59    // `on_patch_dispatch`; default empty keeps the envelope shape stable for forward compatibility.
60    Ok(json!({
61        "label": ctx.patch_label,
62        "patch_version": proc.patch_version,
63        "frame_keys": frame_keys,
64        "graph_writes": [],
65    }))
66}
67
68impl PatchAdapter for GraphPatchAdapter {
69    fn name(&self) -> &str {
70        Self::NAME
71    }
72
73    fn execute_patch(&self, ctx: &PatchDispatchContext<'_>) -> Result<Value, String> {
74        let summary = build_summary(ctx)?;
75        if let Some(h) = &self.host {
76            h.on_patch_dispatch(summary)
77        } else {
78            Ok(summary)
79        }
80    }
81}