feat(cli): surface the composite doc in the graph viewer (#125)
The viewer threads the model's doc through normalizeModel into both composite view states — appended to INFO.B (collapsed box) and INFO.C (expanded cluster frame) — and shows the root composite's doc as a muted header line (#rootdoc span in GRAPH_HEAD, empty when absent; the DOM population sits in the browser-only block, the same boundary as the breadcrumb). viewer_tooltip.mjs pins the doc'd cases beside the byte-exact un-doc'd pins; sample-model.json gains the additive doc key; e2e drives aura graph over a nested-doc blueprint into the emitted page. closes #125
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"root","nodes":[{"composite":{"name":"sma_cross","doc":"momentum leg: change over a lookback, gated by the trend sign","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}]}],"output":[{"node":3,"field":0,"name":"bias"}]}}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"bias"}]}}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"root":{"nodes":{"0":{"comp":"signals"},"1":{"prim":{"type":"Bias","role":"node","params":[["scale","f64"]],"ins":[["f64","any","signal"]],"outs":[["bias","f64"]]}},"2":{"prim":{"type":"SimBroker","role":"node","params":[],"ins":[["f64","any","exposure"],["f64","any","price"]],"outs":[["equity","f64"]]}},"3":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"4":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"src_price":{"prim":{"type":"price","role":"source","params":[],"ins":[],"outs":[["price","f64"]]}}},"edges":[["0.o0","1.i0"],["1.o0","2.i0"],["2.o0","3.i0"],["1.o0","4.i0"],["src_price.o0","0.i0"],["src_price.o0","2.i1"]]},"composites":{"signals":{"inputs":[["f64","any","price"]],"outputs":[["signal","f64"]],"nodes":{"0":{"comp":"trend"},"1":{"comp":"momentum"},"2":{"prim":{"name":"blend","type":"LinComb","role":"node","params":[["weights[0]","f64"],["weights[1]","f64"]],"bound":[[2,"weights[2]","f64","0.5"]],"ins":[["f64","any","term[0]"],["f64","any","term[1]"],["f64","any","term[2]"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o2","2.i1"],["1.o1","2.i2"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]},"trend":{"inputs":[["f64","any","price"]],"outputs":[["cross","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]},"momentum":{"inputs":[["f64","any","price"]],"outputs":[["macd","f64"],["signal","f64"],["histogram","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}},"3":{"prim":{"name":"signal","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"4":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["2.o0","3.i0"],["2.o0","4.i0"],["3.o0","4.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"],["3.o0","#1"],["4.o0","#2"]]}}}
|
||||
{"root":{"nodes":{"0":{"comp":"signals"},"1":{"prim":{"type":"Bias","role":"node","params":[["scale","f64"]],"ins":[["f64","any","signal"]],"outs":[["bias","f64"]]}},"2":{"prim":{"type":"SimBroker","role":"node","params":[],"ins":[["f64","any","exposure"],["f64","any","price"]],"outs":[["equity","f64"]]}},"3":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"4":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"src_price":{"prim":{"type":"price","role":"source","params":[],"ins":[],"outs":[["price","f64"]]}}},"edges":[["0.o0","1.i0"],["1.o0","2.i0"],["2.o0","3.i0"],["1.o0","4.i0"],["src_price.o0","0.i0"],["src_price.o0","2.i1"]]},"composites":{"signals":{"inputs":[["f64","any","price"]],"outputs":[["signal","f64"]],"nodes":{"0":{"comp":"trend"},"1":{"comp":"momentum"},"2":{"prim":{"name":"blend","type":"LinComb","role":"node","params":[["weights[0]","f64"],["weights[1]","f64"]],"bound":[[2,"weights[2]","f64","0.5"]],"ins":[["f64","any","term[0]"],["f64","any","term[1]"],["f64","any","term[2]"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o2","2.i1"],["1.o1","2.i2"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]},"trend":{"inputs":[["f64","any","price"]],"outputs":[["cross","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]},"momentum":{"inputs":[["f64","any","price"]],"outputs":[["macd","f64"],["signal","f64"],["histogram","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}},"3":{"prim":{"name":"signal","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"4":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["2.o0","3.i0"],["2.o0","4.i0"],["3.o0","4.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"],["3.o0","#1"],["4.o0","#2"]],"doc":"momentum leg: change over a lookback, gated by the trend sign"}}}
|
||||
|
||||
@@ -1040,6 +1040,37 @@ fn graph_doc_field_moves_content_id_but_not_identity_id() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#125 viewer/#3, nested-composite doc through the real render
|
||||
/// pipeline): the sibling test above only proves a ROOT-level `doc` reaches
|
||||
/// the rendered page — the viewer threads a composite's doc onto its own
|
||||
/// tooltip (INFO.B/INFO.C in graph-viewer.js), which needs the doc present on
|
||||
/// a NESTED composite def, not just the root. `aura graph <file>` over a
|
||||
/// blueprint whose one node is a nested composite carrying `doc` must embed
|
||||
/// that exact string in the emitted page's `composites` section of
|
||||
/// `window.AURA_MODEL` — the composite-def half of `graph_model.rs`'s
|
||||
/// `doc_fragment`, exercised through the actual file-load -> render CLI
|
||||
/// binary, not just the library function called directly.
|
||||
#[test]
|
||||
fn graph_render_embeds_a_nested_composites_doc() {
|
||||
let out = std::process::Command::new(BIN)
|
||||
.arg("graph")
|
||||
.arg(fixture("nested_doc_blueprint.json"))
|
||||
.output()
|
||||
.expect("spawn aura graph <blueprint>");
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(0),
|
||||
"render exit: {:?}\nstderr: {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let html = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
assert!(
|
||||
html.contains(r#","doc":"momentum leg: change over a lookback, gated by the trend sign""#),
|
||||
"rendered page does not embed the nested composite's doc: {html}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The `gang` op's arity refusal (fewer than two members) reads as prose at the
|
||||
/// binary seam, attributed to its op index, never the raw `GangArity` Debug name.
|
||||
#[test]
|
||||
|
||||
@@ -117,5 +117,59 @@ const expect = (actual, expected, msg) => {
|
||||
);
|
||||
}
|
||||
|
||||
// --- #125: doc'd composite + root -------------------------------------------
|
||||
// A minimal model whose composite def and root both carry a "doc"; pins that
|
||||
// the doc reaches INFO.B (collapsed), INFO.C (expanded), and root.doc.
|
||||
//
|
||||
// genDot resolves a nested composite reference via the module-scope COMP
|
||||
// captured once at require time from window.AURA_MODEL (see the file-top
|
||||
// comment above the `model`/`ROOT` constants and the sibling
|
||||
// viewer_nested_depth.mjs guard's identical note) — reusing the
|
||||
// already-required module's genDot would resolve "sub" back to THIS file's
|
||||
// original doc-less "sub" composite of the same name, not docModel's. Fresh-
|
||||
// require the module against docModel so its own COMP reflects the doc'd
|
||||
// composite.
|
||||
const docModel = {
|
||||
root: {
|
||||
nodes: { "0": { comp: "sub" } },
|
||||
edges: [],
|
||||
doc: "root rationale line",
|
||||
},
|
||||
composites: {
|
||||
sub: {
|
||||
inputs: [["f64", "any", "a"]],
|
||||
outputs: [["y", "f64"]],
|
||||
nodes: {},
|
||||
edges: [],
|
||||
doc: "why this sub-graph is wired this way",
|
||||
},
|
||||
},
|
||||
};
|
||||
const viewerPath = join(here, "..", "assets", "graph-viewer.js");
|
||||
delete require.cache[require.resolve(viewerPath)];
|
||||
global.window = { AURA_MODEL: docModel };
|
||||
const { normalizeModel: normalizeModelDoc, genDot: genDotDoc } = require(viewerPath);
|
||||
const docNorm = normalizeModelDoc(docModel);
|
||||
expect(docNorm.root.doc, "root rationale line", "root doc carried");
|
||||
expect(docNorm.composites.sub.doc, "why this sub-graph is wired this way", "composite doc carried");
|
||||
|
||||
{
|
||||
const { info } = genDotDoc(docNorm.root, "root", new Set(), false);
|
||||
expect(
|
||||
info.B["n0"],
|
||||
"**sub** <span class='dim'>· composite</span>\nparams: <span class='dim'>none</span>\nwhy this sub-graph is wired this way",
|
||||
"collapsed tooltip carries the doc paragraph"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const { info } = genDotDoc(docNorm.root, "root", new Set(["n0"]), false);
|
||||
expect(
|
||||
info.C["clust_n0"],
|
||||
"**sub** <span class='dim'>· composite · click frame to collapse</span>\nwhy this sub-graph is wired this way",
|
||||
"expanded cluster tooltip carries the doc paragraph"
|
||||
);
|
||||
}
|
||||
|
||||
console.log("OK — INFO.B/INFO.C/INFO.P tooltip shapes and md() conversion pinned.");
|
||||
process.exit(0);
|
||||
|
||||
Reference in New Issue
Block a user