From df3c2f06bdd4116c2a5bd3853d318ebd9c39d1ec Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 23 Jul 2026 15:10:42 +0200 Subject: [PATCH] feat(core): doc_gate shape check + required NodeSchema.doc field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C29 compile/unit seam, core carrier (iteration 1 of the self-description spec): DocGateFault { Empty, RestatesName } + doc_gate() — deterministic string-shape check, never content judgement — and NodeSchema gains the required pub doc: &'static str, so every construction site must declare a meaning line (compiler-enumerated). Core's own 10 test literals thread doc: "test-only schema"; doc_gate/DocGateFault re-exported at crate root. Downstream crates are E0063-broken by design until their sites thread the field (tasks 3-7 of the plan); gates here are crate-scoped: cargo test -p aura-core (59 green) + clippy -p aura-core (clean). Interim subset commit of the implement run after adjudicating a cross-task discard: task 2's file checkout had destroyed task 1's node.rs/lib.rs work; restored from the loop's recovery snapshot 53c55c3 and merged with task 2's field threading. refs #316 --- crates/aura-core/src/lib.rs | 4 +- crates/aura-core/src/node.rs | 75 ++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/crates/aura-core/src/lib.rs b/crates/aura-core/src/lib.rs index 8d6fdf1..2681be5 100644 --- a/crates/aura-core/src/lib.rs +++ b/crates/aura-core/src/lib.rs @@ -44,8 +44,8 @@ pub use column::{Column, Window}; pub use ctx::Ctx; pub use error::KindMismatch; pub use node::{ - zip_params, BindOpError, BoundParam, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, - PrimitiveBuilder, + doc_gate, zip_params, BindOpError, BoundParam, DocGateFault, FieldSpec, Firing, Node, + NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, }; pub use scalar::{Scalar, ScalarKind, Timestamp}; pub use series_fold::SeriesFold; diff --git a/crates/aura-core/src/node.rs b/crates/aura-core/src/node.rs index b04f0a8..dd5c1de 100644 --- a/crates/aura-core/src/node.rs +++ b/crates/aura-core/src/node.rs @@ -345,6 +345,35 @@ pub struct NodeSchema { pub inputs: Vec, pub output: Vec, pub params: Vec, + /// One-line meaning of the node — non-load-bearing metadata, shown by + /// introspection surfaces. + pub doc: &'static str, +} + +/// Shape fault of a vocabulary entry's meaning line (C29 gate). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DocGateFault { + Empty, + RestatesName, +} + +/// Deterministic shape check for a vocabulary entry's meaning line — +/// string shape only, never content judgement (the engine must not +/// evaluate prose). +pub fn doc_gate(name: &str, doc: &str) -> Result<(), DocGateFault> { + if doc.trim().is_empty() { + return Err(DocGateFault::Empty); + } + fn norm(s: &str) -> String { + s.chars() + .filter(|c| c.is_ascii_alphanumeric()) + .map(|c| c.to_ascii_lowercase()) + .collect() + } + if norm(doc) == norm(name) { + return Err(DocGateFault::RestatesName); + } + Ok(()) } /// The universal composable dataflow unit (C8): a **producer, consumer, or both**. @@ -446,13 +475,14 @@ mod tests { inputs: vec![], output: vec![], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], + doc: "test-only schema", }, |_| Box::new(Bare), ); assert_eq!(with.label(), "SMA"); let none = PrimitiveBuilder::new( "Sub", - NodeSchema { inputs: vec![], output: vec![], params: vec![] }, + NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" }, |_| Box::new(Bare), ); assert_eq!(none.label(), "Sub"); @@ -482,7 +512,7 @@ mod tests { fn node_name_strips_namespace_prefix() { let b = PrimitiveBuilder::new( "demo::Identity", - NodeSchema { inputs: vec![], output: vec![], params: vec![] }, + NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" }, |_| unreachable!("never built in this test"), ); assert_eq!(b.node_name(), "identity"); @@ -514,7 +544,7 @@ mod tests { fn primitive_builder_build_runs_the_closure() { let f = PrimitiveBuilder::new( "Bare", - NodeSchema { inputs: vec![], output: vec![], params: vec![] }, + NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" }, |_| Box::new(Bare), ); assert_eq!(f.params(), Vec::::new()); @@ -529,6 +559,7 @@ mod tests { inputs: vec![], output: vec![], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], + doc: "test-only schema", }, |_| Box::new(Bare), ); @@ -538,6 +569,21 @@ mod tests { ); } + #[test] + fn schema_carries_declared_doc() { + let b = PrimitiveBuilder::new( + "SMA", + NodeSchema { + inputs: vec![], + output: vec![], + params: vec![], + doc: "simple moving average", + }, + |_| Box::new(Bare), + ); + assert_eq!(b.schema().doc, "simple moving average"); + } + /// A node whose label echoes the param vector its constructor received — lets a /// test read back the positional vector `.build()` reconstructed after binds. struct Probe(Vec); @@ -565,6 +611,7 @@ mod tests { ParamSpec { name: "b".into(), kind: ScalarKind::I64 }, ParamSpec { name: "c".into(), kind: ScalarKind::F64 }, ], + doc: "test-only schema", }, |p| Box::new(Probe(p.to_vec())), ) @@ -641,6 +688,7 @@ mod tests { inputs: vec![], output: vec![], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], + doc: "test-only schema", }, |_| Box::new(Bare), ); @@ -659,6 +707,7 @@ mod tests { ParamSpec { name: "dup".into(), kind: ScalarKind::I64 }, ParamSpec { name: "dup".into(), kind: ScalarKind::I64 }, ], + doc: "test-only schema", }, |_| Box::new(Bare), ); @@ -674,6 +723,7 @@ mod tests { inputs: vec![], output: vec![], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], + doc: "test-only schema", }, |_| Box::new(Bare), ); @@ -725,6 +775,7 @@ mod tests { inputs: vec![], output: vec![], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], + doc: "test-only schema", }, |_| Box::new(Bare), ) @@ -746,6 +797,24 @@ mod tests { }) ); } + + #[test] + fn doc_gate_refuses_empty_and_whitespace() { + assert_eq!(doc_gate("EMA", ""), Err(DocGateFault::Empty)); + assert_eq!(doc_gate("EMA", " \t"), Err(DocGateFault::Empty)); + } + + #[test] + fn doc_gate_refuses_name_restatement() { + assert_eq!(doc_gate("EMA", "EMA"), Err(DocGateFault::RestatesName)); + assert_eq!(doc_gate("EMA", "e_m_a"), Err(DocGateFault::RestatesName)); + assert_eq!(doc_gate("rolling_max", "Rolling Max"), Err(DocGateFault::RestatesName)); + } + + #[test] + fn doc_gate_accepts_a_meaning_line() { + assert_eq!(doc_gate("EMA", "exponential moving average over the input series"), Ok(())); + } } #[cfg(test)]