feat(core): doc_gate shape check + required NodeSchema.doc field
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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -345,6 +345,35 @@ pub struct NodeSchema {
|
||||
pub inputs: Vec<PortSpec>,
|
||||
pub output: Vec<FieldSpec>,
|
||||
pub params: Vec<ParamSpec>,
|
||||
/// 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::<ParamSpec>::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<Cell>);
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user