feat(engine): composite doc field — authoring, serde, identity, model emit (refs #125)
Composite gains doc: Option<String> (the prose twin of name, a C23 debug symbol): with_doc/doc() on Composite, a doc knob on GraphBuilder, a Tier-1 additive-optional CompositeData field (no format-version bump; absent-field documents keep their exact bytes), identity-stripped like the name. The graph model emits an optional trailing doc fragment in both scopes; json_str is hardened for free text (\n/\t/\r named, control chars as \u00XX — the doc is the first multi-line value through it). e2e: a hand-authored doc'd blueprint renders with the doc embedded; the doc moves the content id but never the identity id. refs #125
This commit is contained in:
@@ -86,6 +86,7 @@ pub enum BuildError {
|
||||
/// and port/field names. See the module docs.
|
||||
pub struct GraphBuilder {
|
||||
name: String,
|
||||
doc: Option<String>,
|
||||
nodes: Vec<BlueprintNode>,
|
||||
schemas: Vec<NodeSchema>,
|
||||
edges: Vec<(OutPort, InPort)>,
|
||||
@@ -99,6 +100,7 @@ impl GraphBuilder {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
doc: None,
|
||||
nodes: Vec::new(),
|
||||
schemas: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
@@ -152,6 +154,12 @@ impl GraphBuilder {
|
||||
self.gangs.push((name.to_string(), members.into_iter().collect()));
|
||||
}
|
||||
|
||||
/// Attach the authored rationale for the composite under construction
|
||||
/// (the prose twin of the builder's name; a C23 debug symbol, #125).
|
||||
pub fn doc(&mut self, doc: impl Into<String>) {
|
||||
self.doc = Some(doc.into());
|
||||
}
|
||||
|
||||
/// Resolve every accumulated name to an index and assemble the [`Composite`].
|
||||
pub fn build(self) -> Result<Composite, BuildError> {
|
||||
let mut edges = Vec::with_capacity(self.edges.len());
|
||||
@@ -222,7 +230,10 @@ impl GraphBuilder {
|
||||
let kind = kind.unwrap_or(ScalarKind::F64);
|
||||
gangs.push(Gang { name: name.clone(), kind, members });
|
||||
}
|
||||
let composite = Composite::new(self.name, self.nodes, edges, roles, output);
|
||||
let mut composite = Composite::new(self.name, self.nodes, edges, roles, output);
|
||||
if let Some(doc) = self.doc {
|
||||
composite = composite.with_doc(doc);
|
||||
}
|
||||
match composite.with_gangs(gangs) {
|
||||
Ok(c) => Ok(c),
|
||||
Err(CompileError::BadGang(f)) => Err(BuildError::BadGang(f)),
|
||||
@@ -340,6 +351,25 @@ mod tests {
|
||||
assert_eq!(built.output(), hand.output());
|
||||
}
|
||||
|
||||
/// #125: the builder's doc knob threads into the built composite; absent
|
||||
/// knob leaves the field empty.
|
||||
#[test]
|
||||
fn builder_doc_threads_into_the_composite() {
|
||||
let build = |with_doc: bool| {
|
||||
let mut g = GraphBuilder::new("sma_cross");
|
||||
if with_doc {
|
||||
g.doc("fast/slow spread rationale");
|
||||
}
|
||||
let fast = g.add(Sma::builder().named("fast"));
|
||||
let price = g.input_role("price");
|
||||
g.feed(price, [fast.input("series")]);
|
||||
g.expose(fast.output("value"), "out");
|
||||
g.build().expect("resolves")
|
||||
};
|
||||
assert_eq!(build(true).doc(), Some("fast/slow spread rationale"));
|
||||
assert_eq!(build(false).doc(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_harness_compiles_identically_to_hand_wired() {
|
||||
use crate::test_fixtures::composite_sma_cross_harness;
|
||||
|
||||
Reference in New Issue
Block a user