Skip to main content

cairn_core/
edit.rs

1//! The transactional authoring API — the substance behind the MCP tool
2//! surface (`docs/design.md` Sections 6 and 9).
3//!
4//! These operations live in Core, not in the MCP-server crate, because the
5//! CLI wraps the same API: anything an agent can do structurally, the CLI can
6//! do for automation, and there is exactly one implementation (Section 7
7//! symmetry). The MCP server (slice 5b) and the CLI (step 6) are thin
8//! transports over this.
9//!
10//! Editing is transactional and non-nesting, all-or-nothing for v1 (the
11//! decided model, Section 9): `begin_edit` opens a working draft, fine-grained
12//! ops mutate it in memory, and `commit_edit` materializes it into the
13//! content-addressed store, runs the single Core checker once, and returns the
14//! [`Report`]. `abort_edit` discards the draft. Nothing is hashed or stored
15//! until commit.
16//!
17//! Surface: the Section 6 worked-session loop for authoring one function
18//! (`create_function`, `add_param`, `set_produces`, `set_effects`,
19//! `add_step`, `set_yield`, `describe_hole`) plus `run`, **and** the
20//! stored-tree tools (`fill_hole`, `replace_node`, `query_type`,
21//! `find_references`, `put_expr`) and the MCP stdio transport — all
22//! **shipped** (D13 / the `cairn-mcp-server` binary), all real, none
23//! stubbed. (Historically these were later step-5 slices; that is done.)
24
25use crate::check::{Checker, Report};
26use crate::node::{BinOp, MatchArm, Node, NodeHash, Param, Produces};
27use crate::store::Store;
28use crate::ty::{Confidence, Effect, Type};
29use serde::{Deserialize, Serialize};
30use std::collections::BTreeSet;
31
32/// A structural expression an agent supplies to `add_step` / `set_yield`. It
33/// mirrors the seed expression model; the editor materializes it into
34/// content-addressed nodes at commit.
35#[derive(Clone, Debug, Serialize, Deserialize)]
36pub enum ExprSpec {
37    Lit(i64),
38    /// A `Float` literal (stored in the node as its bit pattern).
39    Float(f64),
40    FloatOp {
41        op: BinOp,
42        lhs: Box<ExprSpec>,
43        rhs: Box<ExprSpec>,
44    },
45    IntToFloat(Box<ExprSpec>),
46    FloatToInt(Box<ExprSpec>),
47    /// A `Decimal` literal, given as its real value; stored pre-scaled by
48    /// 10_000 and rounded to 4 fractional digits.
49    Decimal(f64),
50    DecimalOp {
51        op: BinOp,
52        lhs: Box<ExprSpec>,
53        rhs: Box<ExprSpec>,
54    },
55    IntToDecimal(Box<ExprSpec>),
56    DecimalToInt(Box<ExprSpec>),
57    DecimalRaw(Box<ExprSpec>),
58    Bool(bool),
59    Not(Box<ExprSpec>),
60    Str(String),
61    StrLen(Box<ExprSpec>),
62    StrLower(Box<ExprSpec>),
63    StrFromCode(Box<ExprSpec>),
64    NumberToStr(Box<ExprSpec>),
65    StrToNumber(Box<ExprSpec>),
66    StrToNumberOpt(Box<ExprSpec>),
67    StrConcat(Box<ExprSpec>, Box<ExprSpec>),
68    StrSlice {
69        s: Box<ExprSpec>,
70        start: Box<ExprSpec>,
71        len: Box<ExprSpec>,
72    },
73    StrEq(Box<ExprSpec>, Box<ExprSpec>),
74    StrContains {
75        haystack: Box<ExprSpec>,
76        needle: Box<ExprSpec>,
77    },
78    StrStartsWith {
79        s: Box<ExprSpec>,
80        prefix: Box<ExprSpec>,
81    },
82    StrIndexOf {
83        haystack: Box<ExprSpec>,
84        needle: Box<ExprSpec>,
85    },
86    Now,
87    List(Vec<ExprSpec>),
88    ListEmpty {
89        elem: Type,
90    },
91    ListCons {
92        head: Box<ExprSpec>,
93        tail: Box<ExprSpec>,
94    },
95    OptionSome(Box<ExprSpec>),
96    OptionNone {
97        elem: Type,
98    },
99    OptionElse {
100        opt: Box<ExprSpec>,
101        default: Box<ExprSpec>,
102    },
103    OptionMatch {
104        opt: Box<ExprSpec>,
105        some_bind: String,
106        some_body: Box<ExprSpec>,
107        none_body: Box<ExprSpec>,
108    },
109    ListTryGet {
110        list: Box<ExprSpec>,
111        index: Box<ExprSpec>,
112    },
113    ListLen(Box<ExprSpec>),
114    ListGet {
115        list: Box<ExprSpec>,
116        index: Box<ExprSpec>,
117    },
118    Map(Vec<(ExprSpec, ExprSpec)>),
119    MapGet {
120        map: Box<ExprSpec>,
121        key: Box<ExprSpec>,
122    },
123    MapTryGet {
124        map: Box<ExprSpec>,
125        key: Box<ExprSpec>,
126    },
127    MapLen(Box<ExprSpec>),
128    Log(Box<ExprSpec>),
129    Publish(Box<ExprSpec>),
130    SetHeader {
131        name: Box<ExprSpec>,
132        value: Box<ExprSpec>,
133    },
134    Rand,
135    MutNew(Box<ExprSpec>),
136    MutGet(Box<ExprSpec>),
137    MutSet {
138        cell: Box<ExprSpec>,
139        value: Box<ExprSpec>,
140    },
141    DiskWrite {
142        path: Box<ExprSpec>,
143        content: Box<ExprSpec>,
144    },
145    DiskRead(Box<ExprSpec>),
146    NetGet(Box<ExprSpec>),
147    DbQuery {
148        sql: Box<ExprSpec>,
149        params: Box<ExprSpec>,
150    },
151    Ref(String),
152    Call {
153        func: String,
154        args: Vec<ExprSpec>,
155    },
156    FuncRef(String),
157    CallValue {
158        callee: Box<ExprSpec>,
159        args: Vec<ExprSpec>,
160    },
161    /// An anonymous closure. Param min-confidence is `External` (weakest):
162    /// a closure does not gate its caller's confidence.
163    Lambda {
164        params: Vec<(String, Type)>,
165        body: Box<ExprSpec>,
166    },
167    BinOp {
168        op: BinOp,
169        lhs: Box<ExprSpec>,
170        rhs: Box<ExprSpec>,
171    },
172    If {
173        cond: Box<ExprSpec>,
174        then_branch: Box<ExprSpec>,
175        else_branch: Box<ExprSpec>,
176    },
177    Fail(String),
178    Handle {
179        body: Box<ExprSpec>,
180        handlers: Vec<(String, ExprSpec)>,
181    },
182    Record {
183        type_name: String,
184        fields: Vec<(String, ExprSpec)>,
185    },
186    Field {
187        base: Box<ExprSpec>,
188        type_name: String,
189        field: String,
190    },
191    Variant {
192        type_name: String,
193        case: String,
194        fields: Vec<(String, ExprSpec)>,
195    },
196    Match {
197        scrutinee: Box<ExprSpec>,
198        type_name: String,
199        /// (case, payload bindings, body)
200        arms: Vec<(String, Vec<String>, ExprSpec)>,
201    },
202    Hole {
203        expects: String,
204    },
205}
206
207/// What `describe_hole` reports: what the position expects and what is
208/// available there.
209#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
210pub struct HoleInfo {
211    pub expects: String,
212    pub in_scope: Vec<String>,
213    pub effects_allowed: Vec<Effect>,
214}
215
216/// A function's full signature, for `query_type`: what an agent needs to
217/// call it correctly (and a human-reviewable rendered form). The
218/// structural answer to "what does `fold`/`render_html` expect?".
219#[derive(Clone, Debug, Serialize)]
220pub struct SignatureInfo {
221    pub name: String,
222    pub type_params: Vec<String>,
223    pub params: Vec<Param>,
224    pub produces: Produces,
225    pub requires: Vec<Effect>,
226    pub on_failure: Vec<String>,
227    /// The Section 5 projection of the whole function (review surface).
228    pub rendered: String,
229}
230
231/// One chain step in a declarative function spec.
232#[derive(Clone, Debug, Serialize, Deserialize)]
233pub struct StepSpec {
234    pub binding: String,
235    pub value: ExprSpec,
236}
237
238/// A type definition an agent supplies to `define_type` — a record
239/// (product) or variant (sum). The typed substrate a framework and app
240/// share (`Request`/`Response`, `Post`, `Element`, …).
241#[derive(Clone, Debug, Serialize, Deserialize)]
242pub enum TypeDefSpec {
243    Record {
244        name: String,
245        fields: Vec<(String, Type)>,
246    },
247    Variant {
248        name: String,
249        /// (case, payload field name/type pairs)
250        cases: Vec<(String, Vec<(String, Type)>)>,
251    },
252}
253
254/// A whole module described in one value: its type definitions plus its
255/// functions. The unit the checker resolves calls within — so a framework
256/// function and an app function that calls it live in one `ModuleSpec`,
257/// and reuse is just listing a function's hash. The Tier-2/Tier-3
258/// authoring primitive.
259#[derive(Clone, Debug, Serialize, Deserialize)]
260pub struct ModuleSpec {
261    pub name: String,
262    #[serde(default)]
263    pub types: Vec<TypeDefSpec>,
264    #[serde(default)]
265    pub functions: Vec<FunctionSpec>,
266}
267
268/// A whole function described in one value — the declarative form both the
269/// CLI and automation drive, equivalent to the fine-grained tool sequence.
270#[derive(Clone, Debug, Serialize, Deserialize)]
271pub struct FunctionSpec {
272    pub name: String,
273    #[serde(default)]
274    pub type_params: Vec<String>,
275    #[serde(default)]
276    pub params: Vec<Param>,
277    pub produces: Produces,
278    #[serde(default)]
279    pub requires: BTreeSet<Effect>,
280    #[serde(default)]
281    pub on_failure: Vec<String>,
282    #[serde(default)]
283    pub steps: Vec<StepSpec>,
284    pub result: ExprSpec,
285}
286
287#[derive(Debug)]
288pub enum EditError {
289    Store(crate::store::Error),
290    /// An op was called with no open transaction.
291    NoTransaction,
292    /// `begin_edit` while a transaction is already open (non-nesting).
293    TransactionOpen,
294    /// A draft op before `create_function`.
295    NoFunction,
296    /// `create_function` when the draft already has one.
297    FunctionExists,
298    /// `commit_edit` before the result expression was set.
299    ResultNotSet,
300    Lower(String),
301    Run(String),
302    /// `fill_hole` was pointed at a node that is not a `Node::Hole`
303    /// (use `replace_node` for a general structural replace).
304    NotAHole(NodeHash),
305}
306
307impl std::fmt::Display for EditError {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        match self {
310            EditError::Store(e) => write!(f, "store error: {e}"),
311            EditError::NoTransaction => write!(f, "no edit transaction is open"),
312            EditError::TransactionOpen => write!(f, "an edit transaction is already open"),
313            EditError::NoFunction => write!(f, "no function in the draft; call create_function"),
314            EditError::FunctionExists => write!(f, "the draft already has a function"),
315            EditError::ResultNotSet => write!(f, "result not set; call set_yield before commit"),
316            EditError::Lower(e) => write!(f, "lower error: {e}"),
317            EditError::Run(e) => write!(f, "run error: {e}"),
318            EditError::NotAHole(h) => {
319                write!(f, "node {h} is not a hole; fill_hole only fills Node::Hole")
320            }
321        }
322    }
323}
324
325impl std::error::Error for EditError {}
326
327impl From<crate::store::Error> for EditError {
328    fn from(e: crate::store::Error) -> Self {
329        EditError::Store(e)
330    }
331}
332
333type Result<T> = std::result::Result<T, EditError>;
334
335#[derive(Clone)]
336struct FunctionDraft {
337    name: String,
338    type_params: Vec<String>,
339    params: Vec<Param>,
340    produces: Produces,
341    requires: BTreeSet<Effect>,
342    on_failure: Vec<String>,
343    steps: Vec<(String, ExprSpec)>,
344    result: Option<ExprSpec>,
345}
346
347/// Authoring session over a [`Store`]. Holds the in-memory draft of an open
348/// transaction; nothing reaches the store until `commit_edit`.
349pub struct Editor {
350    store: Store,
351    /// `Some(None)` = transaction open, no function yet;
352    /// `Some(Some(draft))` = function being authored; `None` = no transaction.
353    txn: Option<Option<FunctionDraft>>,
354}
355
356impl Editor {
357    pub fn new(store: Store) -> Self {
358        Self { store, txn: None }
359    }
360
361    /// The underlying store, for read-only inspection (render, etc.).
362    pub fn store(&self) -> &Store {
363        &self.store
364    }
365
366    pub fn begin_edit(&mut self) -> Result<()> {
367        if self.txn.is_some() {
368            return Err(EditError::TransactionOpen);
369        }
370        self.txn = Some(None);
371        Ok(())
372    }
373
374    pub fn abort_edit(&mut self) {
375        self.txn = None;
376    }
377
378    pub fn create_function(&mut self, name: &str) -> Result<()> {
379        match self.txn.as_mut() {
380            None => Err(EditError::NoTransaction),
381            Some(Some(_)) => Err(EditError::FunctionExists),
382            Some(slot @ None) => {
383                *slot = Some(FunctionDraft {
384                    name: name.to_string(),
385                    type_params: Vec::new(),
386                    params: Vec::new(),
387                    produces: Produces {
388                        ty: Type::Number,
389                        confidence: Confidence::Structural,
390                    },
391                    requires: BTreeSet::new(),
392                    on_failure: Vec::new(),
393                    steps: Vec::new(),
394                    result: None,
395                });
396                Ok(())
397            }
398        }
399    }
400
401    fn draft(&mut self) -> Result<&mut FunctionDraft> {
402        match self.txn.as_mut() {
403            None => Err(EditError::NoTransaction),
404            Some(None) => Err(EditError::NoFunction),
405            Some(Some(d)) => Ok(d),
406        }
407    }
408
409    pub fn add_param(&mut self, name: &str, ty: Type, min_confidence: Confidence) -> Result<()> {
410        self.draft()?.params.push(Param {
411            name: name.to_string(),
412            ty,
413            min_confidence,
414        });
415        Ok(())
416    }
417
418    pub fn set_type_params(&mut self, type_params: Vec<String>) -> Result<()> {
419        self.draft()?.type_params = type_params;
420        Ok(())
421    }
422
423    pub fn set_produces(&mut self, ty: Type, confidence: Confidence) -> Result<()> {
424        self.draft()?.produces = Produces { ty, confidence };
425        Ok(())
426    }
427
428    pub fn set_effects(&mut self, effects: BTreeSet<Effect>) -> Result<()> {
429        self.draft()?.requires = effects;
430        Ok(())
431    }
432
433    pub fn set_on_failure(&mut self, failures: Vec<String>) -> Result<()> {
434        self.draft()?.on_failure = failures;
435        Ok(())
436    }
437
438    pub fn add_step(&mut self, binding: &str, value: ExprSpec) -> Result<()> {
439        self.draft()?.steps.push((binding.to_string(), value));
440        Ok(())
441    }
442
443    pub fn set_yield(&mut self, value: ExprSpec) -> Result<()> {
444        self.draft()?.result = Some(value);
445        Ok(())
446    }
447
448    /// What the body position expects and what is in scope there: the declared
449    /// produced type, the parameter and prior-step bindings, and the effects
450    /// the function is allowed to perform.
451    pub fn describe_hole(&mut self) -> Result<HoleInfo> {
452        let d = self.draft()?;
453        let mut in_scope: Vec<String> = d.params.iter().map(|p| p.name.clone()).collect();
454        in_scope.extend(d.steps.iter().map(|(b, _)| b.clone()));
455        Ok(HoleInfo {
456            expects: format!("{:?}", d.produces.ty),
457            in_scope,
458            effects_allowed: d.requires.iter().copied().collect(),
459        })
460    }
461
462    /// Materialize the draft into the store, run the single checker once, and
463    /// return the new function's hash with its [`Report`]. All-or-nothing: on
464    /// any store error nothing is left dangling because content-addressed
465    /// writes are idempotent and unreferenced. Closes the transaction.
466    pub fn commit_edit(&mut self) -> Result<(NodeHash, Report)> {
467        let draft = match self.txn.take() {
468            None => return Err(EditError::NoTransaction),
469            Some(None) => return Err(EditError::NoFunction),
470            Some(Some(d)) => d,
471        };
472        let result_spec = draft.result.as_ref().ok_or(EditError::ResultNotSet)?;
473
474        let mut body = Vec::with_capacity(draft.steps.len());
475        for (binding, spec) in &draft.steps {
476            let value = self.put_expr(spec)?;
477            body.push(self.store.put(&Node::Step {
478                binding: binding.clone(),
479                value,
480            })?);
481        }
482        let result = self.put_expr(result_spec)?;
483
484        let fn_hash = self.store.put(&Node::Function {
485            name: draft.name.clone(),
486            type_params: draft.type_params.clone(),
487            params: draft.params.clone(),
488            produces: draft.produces.clone(),
489            requires: draft.requires.clone(),
490            on_failure: draft.on_failure.clone(),
491            body,
492            result,
493        })?;
494
495        let report = Checker::new(&self.store).check(&fn_hash)?;
496        Ok((fn_hash, report))
497    }
498
499    /// Apply a whole [`FunctionSpec`] as one transaction and commit it. This
500    /// is the declarative equivalent of the fine-grained tool sequence; both
501    /// go through the same ops, so they produce the same content hash. Expects
502    /// no transaction already open.
503    pub fn apply_function(&mut self, spec: &FunctionSpec) -> Result<(NodeHash, Report)> {
504        self.begin_edit()?;
505        self.create_function(&spec.name)?;
506        self.set_type_params(spec.type_params.clone())?;
507        for p in &spec.params {
508            self.add_param(&p.name, p.ty.clone(), p.min_confidence)?;
509        }
510        self.set_produces(spec.produces.ty.clone(), spec.produces.confidence)?;
511        self.set_effects(spec.requires.clone())?;
512        self.set_on_failure(spec.on_failure.clone())?;
513        for s in &spec.steps {
514            self.add_step(&s.binding, s.value.clone())?;
515        }
516        self.set_yield(spec.result.clone())?;
517        self.commit_edit()
518    }
519
520    /// Materialize one type definition into the store, returning its hash.
521    /// Type defs are leaf nodes — no draft/transaction is involved.
522    pub fn define_type(&self, spec: &TypeDefSpec) -> Result<NodeHash> {
523        let node = match spec {
524            TypeDefSpec::Record { name, fields } => Node::RecordDef {
525                name: name.clone(),
526                fields: fields.clone(),
527            },
528            TypeDefSpec::Variant { name, cases } => Node::VariantDef {
529                name: name.clone(),
530                cases: cases.clone(),
531            },
532        };
533        Ok(self.store.put(&node)?)
534    }
535
536    /// Materialize a [`FunctionSpec`] into a stored `Function` node
537    /// **without** checking it. Module authoring checks the assembled
538    /// module once (calls resolve against the module's signature table),
539    /// so a function that calls another must not be checked in isolation.
540    fn materialize_function(&self, spec: &FunctionSpec) -> Result<NodeHash> {
541        let mut body = Vec::with_capacity(spec.steps.len());
542        for s in &spec.steps {
543            let value = self.put_expr(&s.value)?;
544            body.push(self.store.put(&Node::Step {
545                binding: s.binding.clone(),
546                value,
547            })?);
548        }
549        let result = self.put_expr(&spec.result)?;
550        Ok(self.store.put(&Node::Function {
551            name: spec.name.clone(),
552            type_params: spec.type_params.clone(),
553            params: spec.params.clone(),
554            produces: spec.produces.clone(),
555            requires: spec.requires.clone(),
556            on_failure: spec.on_failure.clone(),
557            body,
558            result,
559        })?)
560    }
561
562    /// Author a whole module — type definitions plus functions — and run
563    /// the single Core checker on it **once**, as a unit. Calls resolve
564    /// across every function in the module (forward, mutual, recursive),
565    /// so a framework function and an app function that calls it compose
566    /// in one assembled module with zero language change: the
567    /// content-addressed store is the linker, and reusing a function is
568    /// referencing its hash.
569    pub fn apply_module(&self, spec: &ModuleSpec) -> Result<(NodeHash, Report)> {
570        let mut types = Vec::with_capacity(spec.types.len());
571        for t in &spec.types {
572            types.push(self.define_type(t)?);
573        }
574        let mut functions = Vec::with_capacity(spec.functions.len());
575        for f in &spec.functions {
576            functions.push(self.materialize_function(f)?);
577        }
578        let module = self.store.put(&Node::Module {
579            name: spec.name.clone(),
580            types,
581            functions,
582        })?;
583        let report = Checker::new(&self.store).check(&module)?;
584        Ok((module, report))
585    }
586
587    /// Lower an authored module and run one of its functions under
588    /// `wasmtime`, returning its `i64` result. The module-level
589    /// counterpart of [`run`](Self::run).
590    pub fn run_module(
591        &self,
592        module: &NodeHash,
593        name: &str,
594        args: &[i64],
595    ) -> Result<i64> {
596        let wasm = crate::wasm::lower(&self.store, module)
597            .map_err(|e| EditError::Lower(e.to_string()))?;
598        crate::wasm::run_i64(&wasm, name, args)
599            .map_err(|e| EditError::Run(e.to_string()))
600    }
601
602    /// Drive a web handler `handler(req: Request) -> Response` once,
603    /// against a real SQLite file at `db_path` (state persists across
604    /// calls — the load/persist round-trip an `i64` `run_module`
605    /// cannot exercise). The module-level counterpart of
606    /// [`run_module`](Self::run_module) for the framework's primary
607    /// artifact: same lowering, but the canonical `serve_request`
608    /// path the CLI/HTTP servers use instead of an `i64` entry — so
609    /// an MCP-only agent can *execute and verify* the Tier-3 app it
610    /// authored, not only type-check it.
611    #[allow(clippy::too_many_arguments)]
612    pub fn run_handler(
613        &self,
614        module: &NodeHash,
615        handler: &str,
616        db_path: &str,
617        method: &str,
618        path: &str,
619        body: &str,
620        headers: &str,
621    ) -> Result<crate::wasm::HttpResponse> {
622        let wasm = crate::wasm::lower(&self.store, module)
623            .map_err(|e| EditError::Lower(e.to_string()))?;
624        crate::wasm::serve_request_db_h(
625            &wasm, handler, db_path, method, path, body, headers,
626        )
627        .map_err(|e| EditError::Run(e.to_string()))
628    }
629
630    /// The signature of `name` within an applied module — the structural
631    /// answer to "what does this function expect?", which an agent needs
632    /// to call the stdlib/framework correctly. `None` if absent.
633    pub fn query_type(
634        &self,
635        module: &NodeHash,
636        name: &str,
637    ) -> Result<Option<SignatureInfo>> {
638        let Some(Node::Module { functions, .. }) = self.store.get(module)?
639        else {
640            return Ok(None);
641        };
642        for fh in &functions {
643            if let Some(Node::Function {
644                name: n,
645                type_params,
646                params,
647                produces,
648                requires,
649                on_failure,
650                ..
651            }) = self.store.get(fh)?
652            {
653                if n == name {
654                    let rendered = crate::render::render(&self.store, fh)
655                        .unwrap_or_else(|_| String::new());
656                    return Ok(Some(SignatureInfo {
657                        name: n,
658                        type_params,
659                        params,
660                        produces,
661                        requires: requires.into_iter().collect(),
662                        on_failure,
663                        rendered,
664                    }));
665                }
666            }
667        }
668        Ok(None)
669    }
670
671    /// Every node within `root`'s subtree that has `target` as a direct
672    /// child — the structural reference set (design.md §6). Because the
673    /// store is content-addressed and immutable, a "reference" is a
674    /// parent whose child-hash list contains `target`; this is the basis
675    /// for a rename being a label change, not a structural edit. The
676    /// returned hashes are unique and sorted (the DAG is walked once per
677    /// distinct node — shared subtrees are not double-counted). `root`
678    /// itself is never reported (it is not a reference to a child).
679    pub fn find_references(
680        &self,
681        root: &NodeHash,
682        target: &NodeHash,
683    ) -> Result<Vec<String>> {
684        let mut refs: BTreeSet<String> = BTreeSet::new();
685        let mut seen: std::collections::HashSet<NodeHash> =
686            std::collections::HashSet::new();
687        let mut stack = vec![root.clone()];
688        while let Some(h) = stack.pop() {
689            if !seen.insert(h.clone()) {
690                continue;
691            }
692            let Some(node) = self.store.get(&h)? else {
693                continue;
694            };
695            for child in crate::check::child_hashes(&node) {
696                if child == target {
697                    refs.insert(h.to_string());
698                }
699                stack.push(child.clone());
700            }
701        }
702        Ok(refs.into_iter().collect())
703    }
704
705    /// Structurally replace every occurrence of `target` within `root`
706    /// with `replacement`, returning the new root hash and its
707    /// verification [`Report`] (design.md §6: every mutating call
708    /// returns a report). The store is content-addressed and immutable,
709    /// so this rebuilds each ancestor on the path with the new child
710    /// hash and rehashes up the spine; subtrees with no `target` keep
711    /// their hash and are shared (structural sharing — a no-op replace
712    /// returns `root` unchanged). The DAG is rewritten once per distinct
713    /// node (memoized), so shared subtrees do not blow up.
714    pub fn replace_node(
715        &self,
716        root: &NodeHash,
717        target: &NodeHash,
718        replacement: &NodeHash,
719    ) -> Result<(NodeHash, Report)> {
720        let mut cache: std::collections::HashMap<NodeHash, NodeHash> =
721            std::collections::HashMap::new();
722        let new_root = self.rewrite(root, target, replacement, &mut cache)?;
723        let report = Checker::new(&self.store).check(&new_root)?;
724        Ok((new_root, report))
725    }
726
727    fn rewrite(
728        &self,
729        h: &NodeHash,
730        target: &NodeHash,
731        replacement: &NodeHash,
732        cache: &mut std::collections::HashMap<NodeHash, NodeHash>,
733    ) -> Result<NodeHash> {
734        if h == target {
735            return Ok(replacement.clone());
736        }
737        if let Some(c) = cache.get(h) {
738            return Ok(c.clone());
739        }
740        let Some(node) = self.store.get(h)? else {
741            return Ok(h.clone());
742        };
743        let kids = crate::check::child_hashes(&node);
744        let mut new_kids = Vec::with_capacity(kids.len());
745        let mut changed = false;
746        for c in kids {
747            let nc = self.rewrite(c, target, replacement, cache)?;
748            if &nc != c {
749                changed = true;
750            }
751            new_kids.push(nc);
752        }
753        let out = if changed {
754            let rebuilt = crate::check::with_child_hashes(&node, &new_kids);
755            self.store.put(&rebuilt)?
756        } else {
757            h.clone()
758        };
759        cache.insert(h.clone(), out.clone());
760        Ok(out)
761    }
762
763    /// Fill a typed hole with a node (design.md §6). `hole` must resolve
764    /// to a `Node::Hole`; otherwise this is a `NotAHole` error (use
765    /// [`replace_node`](Self::replace_node) for a general replace). The
766    /// candidate tree (hole → `replacement`) is checked: if it satisfies
767    /// the principles (`report.ok()`) the fill is **accepted** —
768    /// `(new_root, report)`. If not, the fill is **rejected**: the hole
769    /// remains, so the returned root is the *original* `root`, and the
770    /// returned report is the candidate's — it names the violating
771    /// principle, the §6 "rejected with a structural reason" contract.
772    /// `incomplete` (other holes elsewhere) is not a violation and does
773    /// not reject the fill.
774    pub fn fill_hole(
775        &self,
776        root: &NodeHash,
777        hole: &NodeHash,
778        replacement: &NodeHash,
779    ) -> Result<(NodeHash, Report)> {
780        match self.store.get(hole)? {
781            Some(Node::Hole { .. }) => {}
782            _ => return Err(EditError::NotAHole(hole.clone())),
783        }
784        let (candidate, report) = self.replace_node(root, hole, replacement)?;
785        if report.ok() {
786            Ok((candidate, report))
787        } else {
788            Ok((root.clone(), report))
789        }
790    }
791
792    /// Materialize an [`ExprSpec`] into the content-addressed store and
793    /// return its node hash — the editor's *construction* primitive,
794    /// the missing half of structural editing (`replace_node`/
795    /// `fill_hole` consume a replacement hash; this mints one). Pure
796    /// surface over the same materialization `add_step`/`set_yield`
797    /// already use, exposed as a one-shot for a projectional editor.
798    pub fn put_expr(&self, spec: &ExprSpec) -> std::result::Result<NodeHash, crate::store::Error> {
799        let node = match spec {
800            ExprSpec::Lit(v) => Node::Lit(*v),
801            ExprSpec::Float(f) => Node::FloatLit(f.to_bits()),
802            ExprSpec::FloatOp { op, lhs, rhs } => Node::FloatOp {
803                op: *op,
804                lhs: self.put_expr(lhs)?,
805                rhs: self.put_expr(rhs)?,
806            },
807            ExprSpec::IntToFloat(a) => Node::IntToFloat(self.put_expr(a)?),
808            ExprSpec::FloatToInt(a) => Node::FloatToInt(self.put_expr(a)?),
809            ExprSpec::Decimal(v) => {
810                Node::DecimalLit((v * 10_000.0).round() as i64)
811            }
812            ExprSpec::DecimalOp { op, lhs, rhs } => Node::DecimalOp {
813                op: *op,
814                lhs: self.put_expr(lhs)?,
815                rhs: self.put_expr(rhs)?,
816            },
817            ExprSpec::IntToDecimal(a) => Node::IntToDecimal(self.put_expr(a)?),
818            ExprSpec::DecimalToInt(a) => Node::DecimalToInt(self.put_expr(a)?),
819            ExprSpec::DecimalRaw(a) => Node::DecimalRaw(self.put_expr(a)?),
820            ExprSpec::Bool(b) => Node::Bool(*b),
821            ExprSpec::Not(a) => Node::Not(self.put_expr(a)?),
822            ExprSpec::Str(s) => Node::Str(s.clone()),
823            ExprSpec::StrLen(a) => Node::StrLen(self.put_expr(a)?),
824            ExprSpec::StrLower(a) => Node::StrLower(self.put_expr(a)?),
825            ExprSpec::StrFromCode(a) => {
826                Node::StrFromCode(self.put_expr(a)?)
827            }
828            ExprSpec::NumberToStr(a) => Node::NumberToStr(self.put_expr(a)?),
829            ExprSpec::StrToNumber(a) => Node::StrToNumber(self.put_expr(a)?),
830            ExprSpec::StrToNumberOpt(a) => {
831                Node::StrToNumberOpt(self.put_expr(a)?)
832            }
833            ExprSpec::StrConcat(a, b) => {
834                Node::StrConcat(self.put_expr(a)?, self.put_expr(b)?)
835            }
836            ExprSpec::StrSlice { s, start, len } => Node::StrSlice {
837                s: self.put_expr(s)?,
838                start: self.put_expr(start)?,
839                len: self.put_expr(len)?,
840            },
841            ExprSpec::StrEq(a, b) => {
842                Node::StrEq(self.put_expr(a)?, self.put_expr(b)?)
843            }
844            ExprSpec::StrContains { haystack, needle } => Node::StrContains {
845                haystack: self.put_expr(haystack)?,
846                needle: self.put_expr(needle)?,
847            },
848            ExprSpec::StrStartsWith { s, prefix } => Node::StrStartsWith {
849                s: self.put_expr(s)?,
850                prefix: self.put_expr(prefix)?,
851            },
852            ExprSpec::StrIndexOf { haystack, needle } => Node::StrIndexOf {
853                haystack: self.put_expr(haystack)?,
854                needle: self.put_expr(needle)?,
855            },
856            ExprSpec::Now => Node::Now,
857            ExprSpec::List(es) => {
858                let mut hs = Vec::with_capacity(es.len());
859                for e in es {
860                    hs.push(self.put_expr(e)?);
861                }
862                Node::List(hs)
863            }
864            ExprSpec::ListEmpty { elem } => Node::ListEmpty { elem: elem.clone() },
865            ExprSpec::ListCons { head, tail } => Node::ListCons {
866                head: self.put_expr(head)?,
867                tail: self.put_expr(tail)?,
868            },
869            ExprSpec::OptionSome(v) => Node::OptionSome(self.put_expr(v)?),
870            ExprSpec::OptionNone { elem } => Node::OptionNone {
871                elem: elem.clone(),
872            },
873            ExprSpec::OptionElse { opt, default } => Node::OptionElse {
874                opt: self.put_expr(opt)?,
875                default: self.put_expr(default)?,
876            },
877            ExprSpec::OptionMatch {
878                opt,
879                some_bind,
880                some_body,
881                none_body,
882            } => Node::OptionMatch {
883                opt: self.put_expr(opt)?,
884                some_bind: some_bind.clone(),
885                some_body: self.put_expr(some_body)?,
886                none_body: self.put_expr(none_body)?,
887            },
888            ExprSpec::ListTryGet { list, index } => Node::ListTryGet {
889                list: self.put_expr(list)?,
890                index: self.put_expr(index)?,
891            },
892            ExprSpec::ListLen(a) => Node::ListLen(self.put_expr(a)?),
893            ExprSpec::ListGet { list, index } => Node::ListGet {
894                list: self.put_expr(list)?,
895                index: self.put_expr(index)?,
896            },
897            ExprSpec::Map(pairs) => {
898                let mut hs = Vec::with_capacity(pairs.len());
899                for (k, v) in pairs {
900                    hs.push((self.put_expr(k)?, self.put_expr(v)?));
901                }
902                Node::Map(hs)
903            }
904            ExprSpec::MapGet { map, key } => Node::MapGet {
905                map: self.put_expr(map)?,
906                key: self.put_expr(key)?,
907            },
908            ExprSpec::MapTryGet { map, key } => Node::MapTryGet {
909                map: self.put_expr(map)?,
910                key: self.put_expr(key)?,
911            },
912            ExprSpec::MapLen(a) => Node::MapLen(self.put_expr(a)?),
913            ExprSpec::Log(a) => Node::Log(self.put_expr(a)?),
914            ExprSpec::Publish(a) => Node::Publish(self.put_expr(a)?),
915            ExprSpec::SetHeader { name, value } => Node::SetHeader {
916                name: self.put_expr(name)?,
917                value: self.put_expr(value)?,
918            },
919            ExprSpec::Rand => Node::Rand,
920            ExprSpec::MutNew(v) => Node::MutNew(self.put_expr(v)?),
921            ExprSpec::MutGet(cl) => Node::MutGet(self.put_expr(cl)?),
922            ExprSpec::MutSet { cell, value } => Node::MutSet {
923                cell: self.put_expr(cell)?,
924                value: self.put_expr(value)?,
925            },
926            ExprSpec::DiskWrite { path, content } => Node::DiskWrite {
927                path: self.put_expr(path)?,
928                content: self.put_expr(content)?,
929            },
930            ExprSpec::DiskRead(p) => Node::DiskRead(self.put_expr(p)?),
931            ExprSpec::NetGet(u) => Node::NetGet(self.put_expr(u)?),
932            ExprSpec::DbQuery { sql, params } => Node::DbQuery {
933                sql: self.put_expr(sql)?,
934                params: self.put_expr(params)?,
935            },
936            ExprSpec::Ref(n) => Node::Ref(n.clone()),
937            ExprSpec::Hole { expects } => Node::Hole {
938                expects: expects.clone(),
939            },
940            ExprSpec::Call { func, args } => {
941                let mut hs = Vec::with_capacity(args.len());
942                for a in args {
943                    hs.push(self.put_expr(a)?);
944                }
945                Node::Call {
946                    func: func.clone(),
947                    args: hs,
948                }
949            }
950            ExprSpec::Lambda { params, body } => {
951                let b = self.put_expr(body)?;
952                Node::Lambda {
953                    params: params
954                        .iter()
955                        .map(|(n, t)| Param {
956                            name: n.clone(),
957                            ty: t.clone(),
958                            min_confidence: Confidence::External,
959                        })
960                        .collect(),
961                    body: b,
962                }
963            }
964            ExprSpec::FuncRef(name) => Node::FuncRef(name.clone()),
965            ExprSpec::CallValue { callee, args } => {
966                let cl = self.put_expr(callee)?;
967                let mut hs = Vec::with_capacity(args.len());
968                for a in args {
969                    hs.push(self.put_expr(a)?);
970                }
971                Node::CallValue {
972                    callee: cl,
973                    args: hs,
974                }
975            }
976            ExprSpec::BinOp { op, lhs, rhs } => {
977                let l = self.put_expr(lhs)?;
978                let r = self.put_expr(rhs)?;
979                Node::BinOp {
980                    op: *op,
981                    lhs: l,
982                    rhs: r,
983                }
984            }
985            ExprSpec::If {
986                cond,
987                then_branch,
988                else_branch,
989            } => {
990                let c = self.put_expr(cond)?;
991                let t = self.put_expr(then_branch)?;
992                let e = self.put_expr(else_branch)?;
993                Node::If {
994                    cond: c,
995                    then_branch: t,
996                    else_branch: e,
997                }
998            }
999            ExprSpec::Fail(v) => Node::Fail(v.clone()),
1000            ExprSpec::Handle { body, handlers } => {
1001                let b = self.put_expr(body)?;
1002                let mut hs = Vec::with_capacity(handlers.len());
1003                for (variant, recover) in handlers {
1004                    hs.push((variant.clone(), self.put_expr(recover)?));
1005                }
1006                Node::Handle {
1007                    body: b,
1008                    handlers: hs,
1009                }
1010            }
1011            ExprSpec::Record { type_name, fields } => {
1012                let mut fs = Vec::with_capacity(fields.len());
1013                for (n, v) in fields {
1014                    fs.push((n.clone(), self.put_expr(v)?));
1015                }
1016                Node::Record {
1017                    type_name: type_name.clone(),
1018                    fields: fs,
1019                }
1020            }
1021            ExprSpec::Field {
1022                base,
1023                type_name,
1024                field,
1025            } => {
1026                let b = self.put_expr(base)?;
1027                Node::Field {
1028                    base: b,
1029                    type_name: type_name.clone(),
1030                    field: field.clone(),
1031                }
1032            }
1033            ExprSpec::Variant {
1034                type_name,
1035                case,
1036                fields,
1037            } => {
1038                let mut fs = Vec::with_capacity(fields.len());
1039                for (n, v) in fields {
1040                    fs.push((n.clone(), self.put_expr(v)?));
1041                }
1042                Node::Variant {
1043                    type_name: type_name.clone(),
1044                    case: case.clone(),
1045                    fields: fs,
1046                }
1047            }
1048            ExprSpec::Match {
1049                scrutinee,
1050                type_name,
1051                arms,
1052            } => {
1053                let sc = self.put_expr(scrutinee)?;
1054                let mut ms = Vec::with_capacity(arms.len());
1055                for (case, bindings, body) in arms {
1056                    ms.push(MatchArm {
1057                        case: case.clone(),
1058                        bindings: bindings.clone(),
1059                        body: self.put_expr(body)?,
1060                    });
1061                }
1062                Node::Match {
1063                    scrutinee: sc,
1064                    type_name: type_name.clone(),
1065                    arms: ms,
1066                }
1067            }
1068        };
1069        self.store.put(&node)
1070    }
1071
1072    /// Build a one-function module around `func`, lower it, and run it under
1073    /// `wasmtime`, returning its `i64` result. The lifecycle `build`+`run`
1074    /// tools, composed.
1075    pub fn run(&self, func: &NodeHash, name: &str, args: &[i64]) -> Result<i64> {
1076        let module = self.store.put(&Node::Module {
1077            name: "main".into(),
1078            types: vec![],
1079            functions: vec![func.clone()],
1080        })?;
1081        let wasm = crate::wasm::lower(&self.store, &module)
1082            .map_err(|e| EditError::Lower(e.to_string()))?;
1083        crate::wasm::run_i64(&wasm, name, args).map_err(|e| EditError::Run(e.to_string()))
1084    }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090    use crate::check::Status;
1091
1092    /// The Section 6 worked session, in API form: author `id(n)` and run it.
1093    #[test]
1094    fn worked_session_author_check_and_run() {
1095        let s = Store::open_in_memory().unwrap();
1096        let mut e = Editor::new(s);
1097
1098        e.begin_edit().unwrap();
1099        e.create_function("id").unwrap();
1100        e.add_param("n", Type::Number, Confidence::External).unwrap();
1101        // returns the External param, so it produces External to stay clean
1102        e.set_produces(Type::Number, Confidence::External).unwrap();
1103        e.set_effects(BTreeSet::new()).unwrap(); // pure
1104        e.set_yield(ExprSpec::Ref("n".into())).unwrap();
1105
1106        let (h, report) = e.commit_edit().unwrap();
1107        assert!(report.ok(), "violations: {:?}", report.violations);
1108        assert_eq!(report.status, Status::Complete);
1109        assert_eq!(e.run(&h, "id", &[7]).unwrap(), 7);
1110    }
1111
1112    #[test]
1113    fn transactions_are_non_nesting() {
1114        let s = Store::open_in_memory().unwrap();
1115        let mut e = Editor::new(s);
1116        e.begin_edit().unwrap();
1117        assert!(matches!(e.begin_edit(), Err(EditError::TransactionOpen)));
1118    }
1119
1120    #[test]
1121    fn abort_discards_the_draft() {
1122        let s = Store::open_in_memory().unwrap();
1123        let mut e = Editor::new(s);
1124        e.begin_edit().unwrap();
1125        e.create_function("f").unwrap();
1126        e.abort_edit();
1127        assert!(matches!(e.commit_edit(), Err(EditError::NoTransaction)));
1128    }
1129
1130    #[test]
1131    fn ops_require_an_open_transaction() {
1132        let s = Store::open_in_memory().unwrap();
1133        let mut e = Editor::new(s);
1134        assert!(matches!(
1135            e.create_function("f"),
1136            Err(EditError::NoTransaction)
1137        ));
1138    }
1139
1140    #[test]
1141    fn describe_hole_reports_scope_and_effects() {
1142        let s = Store::open_in_memory().unwrap();
1143        let mut e = Editor::new(s);
1144        e.begin_edit().unwrap();
1145        e.create_function("f").unwrap();
1146        e.add_param("a", Type::Number, Confidence::Structural)
1147            .unwrap();
1148        e.add_step("b", ExprSpec::Lit(1)).unwrap();
1149        let mut db = BTreeSet::new();
1150        db.insert(Effect::Db);
1151        e.set_effects(db).unwrap();
1152        let info = e.describe_hole().unwrap();
1153        assert_eq!(info.in_scope, vec!["a".to_string(), "b".to_string()]);
1154        assert_eq!(info.effects_allowed, vec![Effect::Db]);
1155        assert_eq!(info.expects, "Number");
1156    }
1157
1158    #[test]
1159    fn committing_with_a_hole_is_incomplete_but_not_invalid_and_will_not_run() {
1160        let s = Store::open_in_memory().unwrap();
1161        let mut e = Editor::new(s);
1162        e.begin_edit().unwrap();
1163        e.create_function("f").unwrap();
1164        e.set_produces(Type::Number, Confidence::Structural).unwrap();
1165        e.set_yield(ExprSpec::Hole {
1166            expects: "Number".into(),
1167        })
1168        .unwrap();
1169        let (h, report) = e.commit_edit().unwrap();
1170        assert_eq!(report.status, Status::Incomplete);
1171        assert!(report.ok()); // a hole is not a violation
1172        assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
1173    }
1174
1175    #[test]
1176    fn apply_function_matches_fine_grained() {
1177        // Fine-grained authoring of id(n) -> n.
1178        let mut e1 = Editor::new(Store::open_in_memory().unwrap());
1179        e1.begin_edit().unwrap();
1180        e1.create_function("id").unwrap();
1181        e1.add_param("n", Type::Number, Confidence::External).unwrap();
1182        e1.set_produces(Type::Number, Confidence::External).unwrap();
1183        e1.set_effects(BTreeSet::new()).unwrap();
1184        e1.set_yield(ExprSpec::Ref("n".into())).unwrap();
1185        let (h1, r1) = e1.commit_edit().unwrap();
1186
1187        // The same function as one declarative spec.
1188        let spec = FunctionSpec {
1189            name: "id".into(),
1190            type_params: vec![],
1191            params: vec![Param {
1192                name: "n".into(),
1193                ty: Type::Number,
1194                min_confidence: Confidence::External,
1195            }],
1196            produces: Produces {
1197                ty: Type::Number,
1198                confidence: Confidence::External,
1199            },
1200            requires: BTreeSet::new(),
1201            on_failure: vec![],
1202            steps: vec![],
1203            result: ExprSpec::Ref("n".into()),
1204        };
1205        let mut e2 = Editor::new(Store::open_in_memory().unwrap());
1206        let (h2, r2) = e2.apply_function(&spec).unwrap();
1207
1208        // Same authoring, regardless of path → same content hash.
1209        assert_eq!(h1, h2);
1210        assert_eq!(r1.ok(), r2.ok());
1211        assert_eq!(r1.status, r2.status);
1212    }
1213
1214    #[test]
1215    fn commit_without_a_result_errors() {
1216        let s = Store::open_in_memory().unwrap();
1217        let mut e = Editor::new(s);
1218        e.begin_edit().unwrap();
1219        e.create_function("f").unwrap();
1220        assert!(matches!(e.commit_edit(), Err(EditError::ResultNotSet)));
1221    }
1222
1223    /// The Tier-2/Tier-3 substrate: a record, a variant, and four
1224    /// functions — one calling the others (listed *before* them) and
1225    /// using both types — authored entirely through the API, assembled
1226    /// into one module, checked once, and run. This is the composition
1227    /// mechanism a Cairn framework + app rely on: no language change, the
1228    /// store is the linker.
1229    #[test]
1230    fn module_authoring_composes_types_and_functions() {
1231        let ext_num = || Produces {
1232            ty: Type::Number,
1233            confidence: Confidence::External,
1234        };
1235        let p = |name: &str, ty: Type| Param {
1236            name: name.into(),
1237            ty,
1238            min_confidence: Confidence::External,
1239        };
1240        let spec = ModuleSpec {
1241            name: "m".into(),
1242            types: vec![
1243                TypeDefSpec::Record {
1244                    name: "Box".into(),
1245                    fields: vec![("v".into(), Type::Number)],
1246                },
1247                TypeDefSpec::Variant {
1248                    name: "Opt".into(),
1249                    cases: vec![
1250                        ("Some".into(), vec![("v".into(), Type::Number)]),
1251                        ("None".into(), vec![]),
1252                    ],
1253                },
1254            ],
1255            functions: vec![
1256                // `demo` is listed first — it forward-calls mk/unwrap/
1257                // get_or, proving module-level resolution + two-pass
1258                // lowering through the authoring API.
1259                FunctionSpec {
1260                    name: "demo".into(),
1261                    type_params: vec![],
1262                    params: vec![p("n", Type::Number)],
1263                    produces: ext_num(),
1264                    requires: BTreeSet::new(),
1265                    on_failure: vec![],
1266                    steps: vec![
1267                        StepSpec {
1268                            binding: "b".into(),
1269                            value: ExprSpec::Call {
1270                                func: "mk".into(),
1271                                args: vec![ExprSpec::Ref("n".into())],
1272                            },
1273                        },
1274                        StepSpec {
1275                            binding: "x".into(),
1276                            value: ExprSpec::Call {
1277                                func: "unwrap".into(),
1278                                args: vec![ExprSpec::Ref("b".into())],
1279                            },
1280                        },
1281                    ],
1282                    result: ExprSpec::Call {
1283                        func: "get_or".into(),
1284                        args: vec![
1285                            ExprSpec::Variant {
1286                                type_name: "Opt".into(),
1287                                case: "Some".into(),
1288                                fields: vec![(
1289                                    "v".into(),
1290                                    ExprSpec::Ref("x".into()),
1291                                )],
1292                            },
1293                            ExprSpec::Lit(0),
1294                        ],
1295                    },
1296                },
1297                FunctionSpec {
1298                    name: "mk".into(),
1299                    type_params: vec![],
1300                    params: vec![p("n", Type::Number)],
1301                    produces: Produces {
1302                        ty: Type::Named("Box".into()),
1303                        confidence: Confidence::External,
1304                    },
1305                    requires: BTreeSet::new(),
1306                    on_failure: vec![],
1307                    steps: vec![],
1308                    result: ExprSpec::Record {
1309                        type_name: "Box".into(),
1310                        fields: vec![("v".into(), ExprSpec::Ref("n".into()))],
1311                    },
1312                },
1313                FunctionSpec {
1314                    name: "unwrap".into(),
1315                    type_params: vec![],
1316                    params: vec![p("b", Type::Named("Box".into()))],
1317                    produces: ext_num(),
1318                    requires: BTreeSet::new(),
1319                    on_failure: vec![],
1320                    steps: vec![],
1321                    result: ExprSpec::Field {
1322                        base: Box::new(ExprSpec::Ref("b".into())),
1323                        type_name: "Box".into(),
1324                        field: "v".into(),
1325                    },
1326                },
1327                FunctionSpec {
1328                    name: "get_or".into(),
1329                    type_params: vec![],
1330                    params: vec![
1331                        p("o", Type::Named("Opt".into())),
1332                        p("d", Type::Number),
1333                    ],
1334                    produces: ext_num(),
1335                    requires: BTreeSet::new(),
1336                    on_failure: vec![],
1337                    steps: vec![],
1338                    result: ExprSpec::Match {
1339                        scrutinee: Box::new(ExprSpec::Ref("o".into())),
1340                        type_name: "Opt".into(),
1341                        arms: vec![
1342                            (
1343                                "Some".into(),
1344                                vec!["v".into()],
1345                                ExprSpec::Ref("v".into()),
1346                            ),
1347                            ("None".into(), vec![], ExprSpec::Ref("d".into())),
1348                        ],
1349                    },
1350                },
1351            ],
1352        };
1353
1354        let e = Editor::new(Store::open_in_memory().unwrap());
1355        let (module, report) = e.apply_module(&spec).unwrap();
1356        assert!(report.ok(), "violations: {:?}", report.violations);
1357        assert_eq!(report.status, Status::Complete);
1358        // demo(7) = get_or(Some(unwrap(mk(7))), 0) = 7
1359        assert_eq!(e.run_module(&module, "demo", &[7]).unwrap(), 7);
1360    }
1361
1362    /// find_references is the structural reference set: every parent
1363    /// that holds a node by hash, deduped (a shared subtree is walked
1364    /// once; a parent that holds the target twice is reported once),
1365    /// and the root is never invented as a reference to a child it does
1366    /// not hold.
1367    #[test]
1368    fn find_references_is_the_structural_reference_set() {
1369        let s = Store::open_in_memory().unwrap();
1370        let two = s.put(&Node::Lit(2)).unwrap();
1371        let a = s
1372            .put(&Node::BinOp {
1373                op: BinOp::Add,
1374                lhs: two.clone(),
1375                rhs: two.clone(),
1376            })
1377            .unwrap();
1378        let b = s
1379            .put(&Node::BinOp {
1380                op: BinOp::Mul,
1381                lhs: a.clone(),
1382                rhs: two.clone(),
1383            })
1384            .unwrap();
1385        let root = s
1386            .put(&Node::BinOp {
1387                op: BinOp::Sub,
1388                lhs: b.clone(),
1389                rhs: a.clone(),
1390            })
1391            .unwrap();
1392        let e = Editor::new(s);
1393
1394        // Parents that hold `a`: b (Mul a,two) and root (Sub b,a).
1395        let mut want_a = vec![b.to_string(), root.to_string()];
1396        want_a.sort();
1397        assert_eq!(e.find_references(&root, &a).unwrap(), want_a);
1398
1399        // Parents that hold `two`: a (Add two,two — once, not twice)
1400        // and b (Mul a,two). The shared `a` subtree is walked once.
1401        let mut want_two = vec![a.to_string(), b.to_string()];
1402        want_two.sort();
1403        assert_eq!(e.find_references(&root, &two).unwrap(), want_two);
1404
1405        // Nothing points at the root → empty (no self-reference).
1406        assert!(e.find_references(&root, &root).unwrap().is_empty());
1407    }
1408
1409    /// `with_child_hashes` is the exact structural inverse of
1410    /// `child_hashes`: rebuilding a node with its own children (in
1411    /// `child_hashes` order) reproduces the node identically — across
1412    /// every shape, including the compound ones (Map, Record, Variant,
1413    /// Match, Handle, Call, CallValue, Function, Module). This is the
1414    /// invariant `replace_node` depends on; if the big match drifts,
1415    /// this fails.
1416    #[test]
1417    fn with_child_hashes_round_trips() {
1418        use crate::check::{child_hashes, with_child_hashes};
1419        let s = Store::open_in_memory().unwrap();
1420        let h = s.put(&Node::Lit(1)).unwrap();
1421        let h2 = s.put(&Node::Lit(2)).unwrap();
1422        let h3 = s.put(&Node::Lit(3)).unwrap();
1423        let prod = Produces {
1424            ty: Type::Number,
1425            confidence: Confidence::External,
1426        };
1427        let nodes = vec![
1428            Node::Lit(7),
1429            Node::Now,
1430            Node::Ref("x".into()),
1431            Node::Hole { expects: "Number".into() },
1432            Node::Not(h.clone()),
1433            Node::StrConcat(h.clone(), h2.clone()),
1434            Node::StrSlice { s: h.clone(), start: h2.clone(), len: h3.clone() },
1435            Node::If {
1436                cond: h.clone(),
1437                then_branch: h2.clone(),
1438                else_branch: h3.clone(),
1439            },
1440            Node::BinOp { op: BinOp::Add, lhs: h.clone(), rhs: h2.clone() },
1441            Node::List(vec![h.clone(), h2.clone(), h3.clone()]),
1442            Node::Map(vec![(h.clone(), h2.clone()), (h3.clone(), h.clone())]),
1443            Node::Record {
1444                type_name: "R".into(),
1445                fields: vec![("x".into(), h.clone()), ("y".into(), h2.clone())],
1446            },
1447            Node::Variant {
1448                type_name: "V".into(),
1449                case: "C".into(),
1450                fields: vec![("a".into(), h.clone())],
1451            },
1452            Node::Field {
1453                base: h.clone(),
1454                type_name: "R".into(),
1455                field: "x".into(),
1456            },
1457            Node::OptionMatch {
1458                opt: h.clone(),
1459                some_bind: "v".into(),
1460                some_body: h2.clone(),
1461                none_body: h3.clone(),
1462            },
1463            Node::Match {
1464                scrutinee: h.clone(),
1465                type_name: "V".into(),
1466                arms: vec![
1467                    MatchArm { case: "C".into(), bindings: vec!["a".into()], body: h2.clone() },
1468                    MatchArm { case: "D".into(), bindings: vec![], body: h3.clone() },
1469                ],
1470            },
1471            Node::Handle {
1472                body: h.clone(),
1473                handlers: vec![("E".into(), h2.clone()), ("F".into(), h3.clone())],
1474            },
1475            Node::Call { func: "g".into(), args: vec![h.clone(), h2.clone()] },
1476            Node::CallValue { callee: h.clone(), args: vec![h2.clone(), h3.clone()] },
1477            Node::Lambda { params: vec![], body: h.clone() },
1478            Node::Step { binding: "b".into(), value: h.clone() },
1479            Node::Function {
1480                name: "f".into(),
1481                type_params: vec![],
1482                params: vec![],
1483                produces: prod.clone(),
1484                requires: BTreeSet::new(),
1485                on_failure: vec![],
1486                body: vec![h.clone(), h2.clone()],
1487                result: h3.clone(),
1488            },
1489            Node::Module {
1490                name: "m".into(),
1491                types: vec![h.clone()],
1492                functions: vec![h2.clone(), h3.clone()],
1493            },
1494        ];
1495        for n in nodes {
1496            let kids: Vec<NodeHash> =
1497                child_hashes(&n).into_iter().cloned().collect();
1498            assert_eq!(
1499                with_child_hashes(&n, &kids),
1500                n,
1501                "round-trip failed for {n:?}"
1502            );
1503        }
1504    }
1505
1506    /// replace_node rehashes the spine, leaves the original tree intact
1507    /// (content-addressed immutability), returns a re-checked report,
1508    /// and is a structural-sharing no-op when the target is absent.
1509    #[test]
1510    fn replace_node_rehashes_and_rechecks() {
1511        // f() -> Number { yield 2 + 3 }
1512        let spec = ModuleSpec {
1513            name: "m".into(),
1514            types: vec![],
1515            functions: vec![FunctionSpec {
1516                name: "f".into(),
1517                type_params: vec![],
1518                params: vec![],
1519                produces: Produces {
1520                    ty: Type::Number,
1521                    confidence: Confidence::External,
1522                },
1523                requires: BTreeSet::new(),
1524                on_failure: vec![],
1525                steps: vec![],
1526                result: ExprSpec::BinOp {
1527                    op: BinOp::Add,
1528                    lhs: Box::new(ExprSpec::Lit(2)),
1529                    rhs: Box::new(ExprSpec::Lit(3)),
1530                },
1531            }],
1532        };
1533        let e = Editor::new(Store::open_in_memory().unwrap());
1534        let (m, report) = e.apply_module(&spec).unwrap();
1535        assert!(report.ok());
1536        assert_eq!(e.run_module(&m, "f", &[]).unwrap(), 5);
1537
1538        // Content-addressing: the literal `3` already in the tree has a
1539        // deterministic hash; mint `4` and swap.
1540        let three = e.store().put(&Node::Lit(3)).unwrap();
1541        let four = e.store().put(&Node::Lit(4)).unwrap();
1542        let (m2, r2) = e.replace_node(&m, &three, &four).unwrap();
1543        assert!(r2.ok() && r2.status == Status::Complete);
1544        assert_ne!(m2, m, "the spine must rehash");
1545        assert_eq!(e.run_module(&m2, "f", &[]).unwrap(), 6, "2 + 4");
1546        // The original is untouched — immutable, still 5.
1547        assert_eq!(e.run_module(&m, "f", &[]).unwrap(), 5);
1548
1549        // A target absent from the tree → the same root, shared.
1550        let absent = e.store().put(&Node::Lit(999)).unwrap();
1551        let (m3, _) = e.replace_node(&m, &absent, &four).unwrap();
1552        assert_eq!(m3, m, "no-op replace returns the shared root");
1553    }
1554
1555    /// fill_hole accepts a node only if the result checks; otherwise the
1556    /// hole remains and the report names the violated principle (§6).
1557    /// Pointing it at a non-hole is a NotAHole error.
1558    #[test]
1559    fn fill_hole_accepts_only_a_valid_fill_else_the_hole_remains() {
1560        // f() -> Number @ structural { yield <hole "Number"> }
1561        let mut e = Editor::new(Store::open_in_memory().unwrap());
1562        e.begin_edit().unwrap();
1563        e.create_function("f").unwrap();
1564        e.set_produces(Type::Number, Confidence::Structural).unwrap();
1565        e.set_effects(BTreeSet::new()).unwrap();
1566        e.set_yield(ExprSpec::Hole {
1567            expects: "Number".into(),
1568        })
1569        .unwrap();
1570        let (h, report) = e.commit_edit().unwrap();
1571        assert_eq!(report.status, Status::Incomplete);
1572        // The hole node's hash is deterministic (content-addressed).
1573        let hole = e
1574            .store()
1575            .put(&Node::Hole {
1576                expects: "Number".into(),
1577            })
1578            .unwrap();
1579        // With a live hole the function cannot run.
1580        assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
1581
1582        // Reject: a String where Number @ structural is produced → P2.
1583        // The hole remains (original root), the report carries the
1584        // violating principle.
1585        let bad = e.store().put(&Node::Str("x".into())).unwrap();
1586        let (hr, rr) = e.fill_hole(&h, &hole, &bad).unwrap();
1587        assert!(!rr.ok(), "a contract-violating fill must be rejected");
1588        assert!(!rr.violations.is_empty(), "the principle is reported");
1589        assert_eq!(hr, h, "rejected → the hole remains, root unchanged");
1590        assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
1591
1592        // Accept: a Number literal satisfies the hole; the tree
1593        // completes, the spine rehashes, and it runs. The original is
1594        // immutable (still incomplete).
1595        let good = e.store().put(&Node::Lit(42)).unwrap();
1596        let (h2, r2) = e.fill_hole(&h, &hole, &good).unwrap();
1597        assert!(r2.ok() && r2.status == Status::Complete);
1598        assert_ne!(h2, h);
1599        assert_eq!(e.run(&h2, "f", &[]).unwrap(), 42);
1600        assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
1601
1602        // Pointing fill_hole at a non-hole is a typed error.
1603        assert!(matches!(
1604            e.fill_hole(&h, &good, &good),
1605            Err(EditError::NotAHole(_))
1606        ));
1607    }
1608}