Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
"crates/holon-viz-wasm",
"crates/beankeeper-bridge",
"crates/ledgerr-desktop-agent",
"crates/sysml-derive",
]
resolver = "2"

Expand All @@ -52,6 +53,9 @@ kasuari = "0.4"
statig = "0.4"
anyhow = "1"
ordered-float = { version = "4", features = ["serde"] }
syn = "2"
quote = "1"
proc-macro2 = "1"
b00t-reflect-types = { path = "crates/b00t-reflect-types" }
b00t-reflect = { path = "crates/b00t-reflect" }
ledger-core = { path = "crates/ledger-core" }
Expand Down
5 changes: 5 additions & 0 deletions crates/holon-viz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ path = "src/bin/demo.rs"
[dev-dependencies]
serde_json.workspace = true
tempfile.workspace = true
# Test-only: validates SysmlV2Emitter's own output against a real SysML v2
# grammar (see docs/sysml-v2-parser-spike.md). Dev-dependency only, since
# ufo-types' sysml-v2-parser dependency is not wasm32-compatible and
# holon-viz-wasm must never pull it in.
ufo-types = { path = "../ufo-types" }
# CDP integration test dependency — only required when TEST_WEBVIEW=1 is set
# cargo test --test holon_viz_visual_e2e # e2e with live Tauri WebView2
# cargo test # unit + integration (no webview)
15 changes: 12 additions & 3 deletions crates/holon-viz/src/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::cytoscape::CytoscapeGraph;

/// Emits SysML-v2 textual block definition fragments from a [`CytoscapeGraph`].
///
/// Output is a minimal SysML-v2 `package` block containing one `block def`
/// Output is a minimal SysML-v2 `package` block containing one `part def`
/// per node and `part def` references for containment edges.
pub struct SysmlV2Emitter;

Expand All @@ -21,8 +21,17 @@ impl SysmlV2Emitter {

for node in &graph.nodes {
let safe_label = sanitize_sysml_id(&node.data.label);
// The trailing comment is on its own line, and the closing `}`
// on its own line after it -- putting them on the same line
// (as this used to) puts the brace inside the `//` comment,
// leaving the block syntactically unclosed. `part def`, not
// `block def`: SysML v1 called this construct `Block`; SysML
// v2 renamed the equivalent concept to `part def`, and `block`
// is not a SysML v2 keyword at all. See
// docs/sysml-v2-parser-spike.md for how this was found (fed
// through the real `sysml-v2-parser` crate, not eyeballed).
out.push_str(&format!(
" block def {} {{ // id: {}, kind: {} }}\n",
" part def {} {{\n // id: {}, kind: {}\n }}\n",
safe_label, node.data.id, node.data.kind
));
}
Expand All @@ -32,7 +41,7 @@ impl SysmlV2Emitter {
let src_label = find_label(graph, &edge.data.source);
let tgt_label = find_label(graph, &edge.data.target);
out.push_str(&format!(
" part def {} : {} {{ // edge: {} }}\n",
" part def {} : {} {{\n // edge: {}\n }}\n",
sanitize_sysml_id(&tgt_label),
sanitize_sysml_id(&src_label),
edge.data.id
Expand Down
19 changes: 13 additions & 6 deletions crates/holon-viz/tests/holon_viz_comprehensive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,8 @@ fn test_27_sysml_v2_empty() {
let out = SysmlV2Emitter::emit(&empty_graph());
assert!(out.contains("package HolonModel"));
assert!(out.contains("}"));
// No block def nodes
assert_eq!(out.matches("block def").count(), 0);
// No part def nodes
assert_eq!(out.matches("part def").count(), 0);
}

#[test]
Expand All @@ -511,7 +511,10 @@ fn test_28_sysml_v2_single_node() {
let g = CytoscapeGraph::from_holons(&[h]);
let out = SysmlV2Emitter::emit(&g);
assert!(out.contains("package HolonModel"));
assert!(out.contains("block def Alpha"));
// SysML v2 renamed SysML v1's `Block` to `part def`; `block def` is not
// valid SysML v2 syntax at all (confirmed against the real
// sysml-v2-parser crate — see docs/sysml-v2-parser-spike.md).
assert!(out.contains("part def Alpha"));
}

#[test]
Expand Down Expand Up @@ -774,10 +777,14 @@ fn test_40_cross_format_consistency() {

// SysML-v2
let sysml = SysmlV2Emitter::emit(&g);
let sysml_block_count = sysml.matches("block def").count();
// Both nodes and containment edges emit `part def` (SysML v2 has one
// keyword for this, unlike the old node="block def"/edge="part def"
// split): 2 node defs + 1 containment-edge def for this 2-node,
// 1-edge graph = 3.
let sysml_block_count = sysml.matches("part def").count();
assert_eq!(
sysml_block_count, 2,
"SysML-v2 should contain 2 block definitions, got {}",
sysml_block_count, 3,
"SysML-v2 should contain 3 part definitions (2 nodes + 1 containment edge), got {}",
sysml_block_count
);

Expand Down
57 changes: 57 additions & 0 deletions crates/holon-viz/tests/sysml_v2_roundtrip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Round-trips `SysmlV2Emitter::emit()`'s output through the real
//! `sysml-v2-parser` crate (wired in via `ufo_types::sysml`).
//!
//! `docs/sysml-v2-parser-spike.md` found `SysmlV2Emitter`'s output didn't
//! parse as SysML v2 at all: a `//` comment swallowed the closing `}`, and
//! it emitted SysML v1's `block def` instead of SysML v2's `part def`. Both
//! are fixed in `src/emitter.rs`. This test is the concrete "did we close
//! the round-trip" signal the spike asked for: it fails loudly if either
//! regresses, instead of only being checked by eyeballing the output text.

use holon_viz::{CytoscapeGraph, Holon, HolonKind, SysmlV2Emitter};
use ufo_types::sysml::validate_sysml_v2;

#[test]
fn emitted_output_is_valid_sysml_v2_for_a_single_node() {
let h = Holon::root("alpha-id", "Alpha", HolonKind::SysmlBlock);
let g = CytoscapeGraph::from_holons(&[h]);
let emitted = SysmlV2Emitter::emit(&g);

let result = validate_sysml_v2(&emitted);
assert!(
result.disposition.is_satisfied(),
"SysmlV2Emitter output failed to parse as SysML v2: {:?}\n---\n{emitted}",
result.disposition
);
}

#[test]
fn emitted_output_is_valid_sysml_v2_for_a_holarchy_with_containment_edges() {
let holons = vec![
Holon::root("pipeline", "Tax Ledger Pipeline", HolonKind::CapsuleGroup),
Holon::child("ingest", "Ingest PDFs", HolonKind::SysmlBlock, "pipeline"),
Holon::child(
"classify",
"Classify Transactions",
HolonKind::SysmlBlock,
"pipeline",
),
];
let g = CytoscapeGraph::from_holons(&holons);
let emitted = SysmlV2Emitter::emit(&g);

let result = validate_sysml_v2(&emitted);
assert!(
result.disposition.is_satisfied(),
"SysmlV2Emitter output failed to parse as SysML v2: {:?}\n---\n{emitted}",
result.disposition
);
}

#[test]
fn empty_graph_emits_valid_sysml_v2() {
let g = CytoscapeGraph::from_holons(&[]);
let emitted = SysmlV2Emitter::emit(&g);
let result = validate_sysml_v2(&emitted);
assert!(result.disposition.is_satisfied(), "{:?}", result.disposition);
}
21 changes: 21 additions & 0 deletions crates/sysml-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "sysml-derive"
version = "0.1.0"
edition.workspace = true
license.workspace = true
description = "Spike: #[derive(SysmlBlock)] walks a struct's fields via its AST (syn) and generates a SysML-v2 block definition at compile time. See docs/systems-modeling-registry-rescope.md (epic part 2, addendum)."

[lib]
proc-macro = true

[dependencies]
syn = { workspace = true, features = ["full"] }
quote = { workspace = true }
proc-macro2 = { workspace = true }

[dev-dependencies]
# Test-only: validates the actual generated sysml_block_def() text against
# a real SysML v2 grammar (ufo_types::sysml, wired to sysml-v2-parser) —
# see tests/real_grammar_validation.rs. Not a runtime dependency of the
# proc-macro itself.
ufo-types = { path = "../ufo-types" }
Loading
Loading