feat(engine, cli): op-script doc slot -- the vocabulary grows the C29 meaning line
Iteration 3 completion (tasks 5-6): the construction vocabulary gains the slot its own #125 scope-cut comment anticipated. Op::Doc { text } is the op-script twin of the builder's .doc(...) -- a closed, typed metadata construct (C25, no logic content); at most one per script, a second refuses via OpError::DuplicateDoc (the existing duplicate-fault family), rendered "a doc op may appear at most once". GraphSession threads the text through Composite::with_doc at finish, so op-built composites pass the C29 store gate like builder-built ones. The op-script register test runs on the described variant (SIGNAL_DOC_DESCRIBED) for both shapes; the doc-less script now pins the C29 refusal, and the E2E phase added the RestatesName op-script case. Full gates: cargo test --workspace green (98 suites, 0 failed); clippy -D warnings clean. refs #316
This commit is contained in:
@@ -54,6 +54,40 @@ const SIGNAL_DOC: &str = r#"[
|
||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||
]"#;
|
||||
|
||||
/// SIGNAL_DOC with the C29 doc op — the register-reaching variant (a store
|
||||
/// write requires a described composite; build-only tests keep SIGNAL_DOC).
|
||||
const SIGNAL_DOC_DESCRIBED: &str = r#"[
|
||||
{"op":"doc","text":"fast/slow SMA difference clamped into a directional bias"},
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
|
||||
{"op":"add","type":"Sub"},
|
||||
{"op":"add","type":"Bias"},
|
||||
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||
]"#;
|
||||
|
||||
/// SIGNAL_DOC with a `doc` op whose text merely restates the op-script
|
||||
/// composite's name ("graph", the literal `replay("graph", ..)` gives every
|
||||
/// CLI-built op-script) -- the RestatesName arm of the C29 shape gate,
|
||||
/// reached via the op-script route rather than a builder-authored blueprint.
|
||||
const SIGNAL_DOC_NAME_RESTATED: &str = r#"[
|
||||
{"op":"doc","text":"Graph"},
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
|
||||
{"op":"add","type":"Sub"},
|
||||
{"op":"add","type":"Bias"},
|
||||
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||
]"#;
|
||||
|
||||
/// Every op below applies cleanly (all eager checks pass), but `sub.rhs` is
|
||||
/// never wired — a 0-cover input slot only the holistic finalize gate can
|
||||
/// reject. The smallest document that is op-valid yet whole-document-invalid.
|
||||
@@ -74,6 +108,21 @@ fn graph_build_emits_blueprint_for_a_valid_document() {
|
||||
assert!(stdout.contains("\"SMA\""), "carries the nodes: {stdout}");
|
||||
}
|
||||
|
||||
/// C29 (#316): the op-script `doc` op's text lands verbatim in the built
|
||||
/// blueprint's `doc` field -- `graph build` is a pure translation from ops to
|
||||
/// the canonical envelope, so a doc op is not consumed or reworded on the way
|
||||
/// through, only threaded (the shape gate is a separate, register-time check;
|
||||
/// see `graph_register_refuses_an_op_script_doc_that_restates_the_composite_name`).
|
||||
#[test]
|
||||
fn graph_build_emits_the_op_script_doc_verbatim_into_the_blueprint() {
|
||||
let (stdout, _stderr, ok) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
|
||||
assert!(ok, "exit success");
|
||||
assert!(
|
||||
stdout.contains(r#""doc":"fast/slow SMA difference clamped into a directional bias""#),
|
||||
"the doc op's exact text appears in the blueprint's doc field: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The canonical blueprint artifact carries no trailing newline: `aura graph build`
|
||||
/// emits exactly the library `blueprint_to_json` bytes (the form #158 content-addresses),
|
||||
/// not a display-framed line. Byte-canonical across the CLI and library surfaces (#164).
|
||||
@@ -688,11 +737,12 @@ fn graph_register_rejects_an_unknown_node_type_with_prose() {
|
||||
#[test]
|
||||
fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() {
|
||||
let dir = temp_cwd("register-op-script");
|
||||
// The op-script form (a JSON array of ops), written to a file.
|
||||
// The op-script form (a JSON array of ops), written to a file. Register
|
||||
// requires a described composite (C29), so the described twin is used here.
|
||||
let op_script = dir.join("op-script.json");
|
||||
std::fs::write(&op_script, SIGNAL_DOC).expect("write op-script fixture");
|
||||
std::fs::write(&op_script, SIGNAL_DOC_DESCRIBED).expect("write op-script fixture");
|
||||
// Its `graph build` envelope (a JSON object), the shape `register` already accepts.
|
||||
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC);
|
||||
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
|
||||
assert!(built, "graph build produces the envelope");
|
||||
let envelope = dir.join("envelope.json");
|
||||
std::fs::write(&envelope, &envelope_bytes).expect("write envelope fixture");
|
||||
@@ -710,6 +760,33 @@ fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() {
|
||||
registered_id(&env_out),
|
||||
"op-script and its built envelope register to the same content id"
|
||||
);
|
||||
|
||||
// C29: the doc-less op-script form refuses at register like any other
|
||||
// doc-less composite entering the store.
|
||||
let docless_script = dir.join("docless-script.json");
|
||||
std::fs::write(&docless_script, SIGNAL_DOC).expect("write op-script fixture");
|
||||
let (dl_out, dl_err, dl_code) =
|
||||
run_in(&dir, &["graph", "register", docless_script.to_str().unwrap()]);
|
||||
assert_eq!(dl_code, Some(1), "doc-less op-script refuses: {dl_out} {dl_err}");
|
||||
assert!(dl_err.contains("carries no doc"), "stderr names the rule: {dl_err}");
|
||||
assert!(dl_err.contains("C29"), "stderr cites the contract: {dl_err}");
|
||||
}
|
||||
|
||||
/// C29: an op-script's `doc` op is not a bare presence checkbox -- its text
|
||||
/// goes through the same shape gate a builder-authored doc does. A doc that
|
||||
/// merely restates the composite's name (here "graph", every CLI op-script's
|
||||
/// literal name) refuses at register exactly like the builder-authored
|
||||
/// RestatesName case already pinned at the registry-unit layer, but reached
|
||||
/// end-to-end through the op-script CLI seam this time.
|
||||
#[test]
|
||||
fn graph_register_refuses_an_op_script_doc_that_restates_the_composite_name() {
|
||||
let dir = temp_cwd("register-op-script-restated-doc");
|
||||
let restated_script = dir.join("restated-doc-script.json");
|
||||
std::fs::write(&restated_script, SIGNAL_DOC_NAME_RESTATED).expect("write op-script fixture");
|
||||
let (out, err, code) = run_in(&dir, &["graph", "register", restated_script.to_str().unwrap()]);
|
||||
assert_eq!(code, Some(1), "name-restating doc refuses: {out} {err}");
|
||||
assert!(err.contains("merely restates"), "stderr names the rule: {err}");
|
||||
assert!(err.contains("C29"), "stderr cites the contract: {err}");
|
||||
}
|
||||
|
||||
/// Property (#202, the same on-ramp seam at the sibling verb): `aura graph
|
||||
@@ -1198,6 +1275,26 @@ fn graph_build_reports_gang_kind_mismatch_as_prose() {
|
||||
assert!(!stderr.contains("GangKindMismatch"), "does not leak the Debug variant name: {stderr}");
|
||||
}
|
||||
|
||||
/// The `doc` op's duplicate refusal (a second meaning line in one op-script)
|
||||
/// reads as prose at the binary seam, attributed to the second `doc` op's
|
||||
/// index -- never the raw `DuplicateDoc` Debug variant name.
|
||||
#[test]
|
||||
fn graph_build_reports_duplicate_doc_fault_as_prose() {
|
||||
let doc = r#"[
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"doc","text":"first"},
|
||||
{"op":"doc","text":"second"}
|
||||
]"#;
|
||||
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
|
||||
assert!(!ok, "non-zero exit on a duplicate doc fault");
|
||||
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
|
||||
assert!(
|
||||
stderr.contains("op 2 (doc): a doc op may appear at most once"),
|
||||
"names the op + cause as prose: {stderr}"
|
||||
);
|
||||
assert!(!stderr.contains("DuplicateDoc"), "does not leak the Debug variant name: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (corrupt-gang-blueprint, the `gang_fault_prose` seam): `aura graph
|
||||
/// register` on a hand-corrupted blueprint whose gangs section fails structural
|
||||
/// validation (a single-member gang) refuses with the `blueprint_load_prose` ->
|
||||
|
||||
Reference in New Issue
Block a user