Skip to content

Commit aa7f420

Browse files
Claude Sonnet (coordinator)claude
andcommitted
feat(viz): integrate Cytoscape.js into Tauri via VZ panel + CDP test
- ledgerr-host depends on holon-viz; get_holon_viz_graph Tauri command returns CytoscapeGraph JSON for the tax pipeline holarchy - ui/index.html: load cytoscape@3 CDN; ui/main.js: VZ sidebar panel with initVizPanel() calling the command and rendering on activation - scripts/test-holon-viz.ps1: CDP WebSocket test — navigates to VZ panel, asserts window._cy initialized with >= 5 nodes - Justfile: demo-viz (build+launch), test-holon-viz, test-holon-viz-fast - holon-viz demo binary cleaned up (browser-open logic removed — handled by PowerShell/Justfile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4802c6f commit aa7f420

9 files changed

Lines changed: 359 additions & 0 deletions

File tree

Justfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ test:
106106
wsl2-pwsh-tauri-build:
107107
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Projects\l3dg3rr\scripts\tauri-build.ps1"
108108

109+
# Build host-tauri (Windows toolchain via PowerShell) then launch with CDP for viz demo.
110+
# Opens the tray app — click VZ in the sidebar to see the Cytoscape pipeline graph.
111+
demo-viz:
112+
powershell.exe -NoProfile -Command '$env:PATH="C:\Users\wendy\.cargo\bin;C:\msys64\mingw64\bin;"+$env:PATH; Set-Location "D:\Projects\l3dg3rr"; cargo build -p ledgerr-host --bin host-tauri; if ($LASTEXITCODE -ne 0){throw "build failed"}; Write-Host "Build OK — launching with CDP on port 19222"; $env:WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS="--remote-debugging-port=19222"; Start-Process -FilePath "D:\Projects\l3dg3rr\target\debug\host-tauri.exe" -WorkingDirectory "D:\Projects\l3dg3rr\crates\ledgerr-host"; Write-Host "Launched — click VZ in the sidebar"'
113+
114+
# Run the CDP-based holon-viz acceptance test (builds, launches, asserts window._cy has nodes).
115+
test-holon-viz:
116+
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Projects\l3dg3rr\scripts\test-holon-viz.ps1"
117+
118+
# Same as test-holon-viz but skips the build step (uses existing binary).
119+
test-holon-viz-fast:
120+
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Projects\l3dg3rr\scripts\test-holon-viz.ps1" -SkipBuild
121+
109122
# ─── Local model assets ───────────────────────────────────────────────────────
110123

111124
# Install Microsoft Foundry Local on Windows, then print version and service status.

crates/holon-viz/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ chrono.workspace = true
1313
thiserror.workspace = true
1414
tracing.workspace = true
1515

16+
[[bin]]
17+
name = "holon-viz-demo"
18+
path = "src/bin/demo.rs"
19+
1620
[dev-dependencies]
1721
serde_json.workspace = true
1822
tempfile.workspace = true

crates/holon-viz/src/bin/demo.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! holon-viz demo — generates a sample tax-pipeline holarchy as a static HTML file.
2+
//!
3+
//! Usage (from project root):
4+
//! cargo run -p holon-viz --bin holon-viz-demo
5+
//!
6+
//! Writes: target/holon-viz-demo.html
7+
//! Open via Justfile: just demo-viz (builds + generates + opens via PowerShell)
8+
9+
use holon_viz::{CytoscapeGraph, Holon, HolonKind, HtmlRenderer};
10+
use std::collections::HashMap;
11+
use std::path::PathBuf;
12+
use serde_json::Value;
13+
14+
fn main() {
15+
let holons = sample_tax_pipeline();
16+
let graph = CytoscapeGraph::from_holons(&holons);
17+
18+
// Write alongside the cargo target dir so the Justfile PowerShell step
19+
// can find it at D:\Projects\l3dg3rr\target\holon-viz-demo.html
20+
let out_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
21+
.join("../../target/holon-viz-demo.html");
22+
23+
match HtmlRenderer::write_to_file(&graph, &out_path) {
24+
Ok(()) => {
25+
let canonical = out_path.canonicalize().unwrap_or(out_path);
26+
println!("{}", canonical.display());
27+
}
28+
Err(e) => {
29+
eprintln!("error: {e}");
30+
std::process::exit(1);
31+
}
32+
}
33+
}
34+
35+
/// Build a holarchy that represents the l3dg3rr tax-ledger pipeline.
36+
fn sample_tax_pipeline() -> Vec<Holon> {
37+
let mut meta: HashMap<String, Value> = HashMap::new();
38+
meta.insert("version".to_string(), Value::String("v1.9.0".to_string()));
39+
40+
vec![
41+
// ── Pipeline root ────────────────────────────────────────────────────
42+
Holon {
43+
id: "pipeline".to_string(),
44+
label: "Tax Ledger Pipeline".to_string(),
45+
kind: HolonKind::CapsuleGroup,
46+
parent_id: None,
47+
children: vec![
48+
"ingest".to_string(),
49+
"classify".to_string(),
50+
"reconcile".to_string(),
51+
"attest".to_string(),
52+
],
53+
metadata: meta.clone(),
54+
},
55+
// ── Stage nodes ──────────────────────────────────────────────────────
56+
Holon {
57+
id: "ingest".to_string(),
58+
label: "Ingest PDFs".to_string(),
59+
kind: HolonKind::SysmlBlock,
60+
parent_id: Some("pipeline".to_string()),
61+
children: vec!["docling".to_string(), "blake3-id".to_string()],
62+
metadata: HashMap::<String, Value>::new(),
63+
},
64+
Holon {
65+
id: "classify".to_string(),
66+
label: "Classify Transactions".to_string(),
67+
kind: HolonKind::SysmlBlock,
68+
parent_id: Some("pipeline".to_string()),
69+
children: vec!["rhai-rules".to_string(), "flag-queue".to_string()],
70+
metadata: HashMap::<String, Value>::new(),
71+
},
72+
Holon {
73+
id: "reconcile".to_string(),
74+
label: "Reconcile & Export".to_string(),
75+
kind: HolonKind::SysmlBlock,
76+
parent_id: Some("pipeline".to_string()),
77+
children: vec!["excel-workbook".to_string()],
78+
metadata: HashMap::<String, Value>::new(),
79+
},
80+
Holon {
81+
id: "attest".to_string(),
82+
label: "Attest (CPA Sign-off)".to_string(),
83+
kind: HolonKind::SysmlBlock,
84+
parent_id: Some("pipeline".to_string()),
85+
children: vec!["audit-log".to_string()],
86+
metadata: HashMap::<String, Value>::new(),
87+
},
88+
// ── Leaf nodes ───────────────────────────────────────────────────────
89+
Holon {
90+
id: "docling".to_string(),
91+
label: "Docling OCR".to_string(),
92+
kind: HolonKind::ProcessNode,
93+
parent_id: Some("ingest".to_string()),
94+
children: vec![],
95+
metadata: HashMap::<String, Value>::new(),
96+
},
97+
Holon {
98+
id: "blake3-id".to_string(),
99+
label: "Blake3 Content ID".to_string(),
100+
kind: HolonKind::ProcessNode,
101+
parent_id: Some("ingest".to_string()),
102+
children: vec![],
103+
metadata: HashMap::<String, Value>::new(),
104+
},
105+
Holon {
106+
id: "rhai-rules".to_string(),
107+
label: "Rhai Rule Engine".to_string(),
108+
kind: HolonKind::ProcessNode,
109+
parent_id: Some("classify".to_string()),
110+
children: vec![],
111+
metadata: HashMap::<String, Value>::new(),
112+
},
113+
Holon {
114+
id: "flag-queue".to_string(),
115+
label: "Flag Queue".to_string(),
116+
kind: HolonKind::ProcessNode,
117+
parent_id: Some("classify".to_string()),
118+
children: vec![],
119+
metadata: HashMap::<String, Value>::new(),
120+
},
121+
Holon {
122+
id: "excel-workbook".to_string(),
123+
label: "Excel Workbook".to_string(),
124+
kind: HolonKind::OwlClass,
125+
parent_id: Some("reconcile".to_string()),
126+
children: vec![],
127+
metadata: HashMap::<String, Value>::new(),
128+
},
129+
Holon {
130+
id: "audit-log".to_string(),
131+
label: "Immutable Audit Log".to_string(),
132+
kind: HolonKind::AuditEvent,
133+
parent_id: Some("attest".to_string()),
134+
children: vec![],
135+
metadata: HashMap::<String, Value>::new(),
136+
},
137+
]
138+
}

crates/ledgerr-host/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ reqwest = { workspace = true }
3434
rig-core = "0.35.0"
3535
ledger-core = { workspace = true }
3636
arc-kit-au = { path = "../arc-kit-au" }
37+
holon-viz = { path = "../holon-viz" }
3738
serde = { workspace = true }
3839
serde_json = { workspace = true }
3940
thiserror = { workspace = true }

crates/ledgerr-host/src/bin/tauri/commands.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,42 @@ pub fn get_tx_provenance(
523523
css_class: badge.css_class().to_string(),
524524
})
525525
}
526+
527+
/// Return the Cytoscape.js-compatible JSON for the holonic pipeline graph.
528+
///
529+
/// The frontend Viz panel calls this once on activation and passes the result
530+
/// directly to `cytoscape({ elements: ... })`.
531+
#[tauri::command]
532+
pub fn get_holon_viz_graph() -> Result<String, String> {
533+
use holon_viz::{CytoscapeGraph, Holon, HolonKind};
534+
use std::collections::HashMap;
535+
536+
let holons = vec![
537+
Holon { id: "pipeline".into(), label: "Tax Ledger Pipeline".into(), kind: HolonKind::CapsuleGroup,
538+
parent_id: None, children: vec!["ingest".into(),"classify".into(),"reconcile".into(),"attest".into()], metadata: HashMap::new() },
539+
Holon { id: "ingest".into(), label: "Ingest PDFs".into(), kind: HolonKind::SysmlBlock,
540+
parent_id: Some("pipeline".into()), children: vec!["docling".into(),"blake3-id".into()], metadata: HashMap::new() },
541+
Holon { id: "classify".into(), label: "Classify Transactions".into(), kind: HolonKind::SysmlBlock,
542+
parent_id: Some("pipeline".into()), children: vec!["rhai-rules".into(),"flag-queue".into()], metadata: HashMap::new() },
543+
Holon { id: "reconcile".into(), label: "Reconcile & Export".into(), kind: HolonKind::SysmlBlock,
544+
parent_id: Some("pipeline".into()), children: vec!["excel-workbook".into()], metadata: HashMap::new() },
545+
Holon { id: "attest".into(), label: "Attest (CPA)".into(), kind: HolonKind::SysmlBlock,
546+
parent_id: Some("pipeline".into()), children: vec!["audit-log".into()], metadata: HashMap::new() },
547+
Holon { id: "docling".into(), label: "Docling OCR".into(), kind: HolonKind::ProcessNode,
548+
parent_id: Some("ingest".into()), children: vec![], metadata: HashMap::new() },
549+
Holon { id: "blake3-id".into(), label: "Blake3 Content ID".into(), kind: HolonKind::ProcessNode,
550+
parent_id: Some("ingest".into()), children: vec![], metadata: HashMap::new() },
551+
Holon { id: "rhai-rules".into(), label: "Rhai Rule Engine".into(), kind: HolonKind::ProcessNode,
552+
parent_id: Some("classify".into()), children: vec![], metadata: HashMap::new() },
553+
Holon { id: "flag-queue".into(), label: "Flag Queue".into(), kind: HolonKind::ProcessNode,
554+
parent_id: Some("classify".into()), children: vec![], metadata: HashMap::new() },
555+
Holon { id: "excel-workbook".into(), label: "Excel Workbook".into(), kind: HolonKind::OwlClass,
556+
parent_id: Some("reconcile".into()), children: vec![], metadata: HashMap::new() },
557+
Holon { id: "audit-log".into(), label: "Immutable Audit Log".into(), kind: HolonKind::AuditEvent,
558+
parent_id: Some("attest".into()), children: vec![], metadata: HashMap::new() },
559+
];
560+
561+
CytoscapeGraph::from_holons(&holons)
562+
.to_json()
563+
.map_err(|e| e.to_string())
564+
}

crates/ledgerr-host/src/bin/tauri/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ fn main() {
113113
commands::get_test_harness_config,
114114
commands::write_dom_dump,
115115
commands::get_cargo_pkg_version,
116+
commands::get_holon_viz_graph,
116117
])
117118
.build(tauri::generate_context!())
118119
.unwrap_or_else(|e| {

crates/ledgerr-host/ui/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>l3dg3rr</title>
77
<link rel="stylesheet" href="style.css" />
8+
<script src="https://unpkg.com/cytoscape@3/dist/cytoscape.min.js"></script>
89
</head>
910
<body>
1011
<div id="app">

crates/ledgerr-host/ui/main.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ var PANELS=[
88
{id:'dash',icon:'DB',label:'Dashboard'},
99
{id:'settings',icon:'ST',label:'Settings'},
1010
{id:'docs',icon:'DK',label:'Docs Playbook'},
11+
{id:'viz',icon:'VZ',label:'Viz'},
1112
];
1213
var activePanel=0;
1314
var DASH_PANEL_INDEX=PANELS.findIndex(function(p){return p.id==='dash'});
15+
var VIZ_PANEL_INDEX=PANELS.findIndex(function(p){return p.id==='viz'});
16+
var _vizInitialized=false;
1417

1518
function showPanel(i){
1619
activePanel=i;
@@ -22,6 +25,7 @@ function showPanel(i){
2225
b.classList.toggle('active',j===i);
2326
});
2427
if(DASH_PANEL_INDEX!==-1&&i===DASH_PANEL_INDEX)refreshDashboard();
28+
if(VIZ_PANEL_INDEX!==-1&&i===VIZ_PANEL_INDEX)initVizPanel();
2529
}
2630

2731
function panelTemplate(id){
@@ -30,6 +34,7 @@ function panelTemplate(id){
3034
t.logs='<div class="panel-title-row"><span class="panel-title">Logs</span></div><div class="log-tabs"><button class="log-tab active" data-log="0">Transport</button><button class="log-tab" data-log="1">Review</button></div><div id="log-panel-0" class="log-subpanel transport-bg"><div class="log-label">Transport</div><div id="rig-log" class="log-content"></div></div><div id="log-panel-1" class="log-subpanel review-bg hidden"><div class="log-label review-label">Diffsets</div><div id="review-log" class="log-content"></div></div></div>';
3135
t.dash='<span class="panel-title">Dashboard</span><div id="evidence-summary" class="evidence-summary"><div class="ev-card ev-card-blocked"><div class="ev-card-value" id="blocked-value">-</div><div class="ev-card-label">Blocked</div></div><div class="ev-card ev-card-ready"><div class="ev-card-value" id="ready-value">-</div><div class="ev-card-label">Ready</div></div><div class="ev-card ev-card-exported"><div class="ev-card-value" id="exported-value">-</div><div class="ev-card-label">Exported</div></div><div class="ev-card ev-card-issues"><div class="ev-card-value" id="issues-value">-</div><div class="ev-card-label">Issues</div></div></div><div class="ev-section"><div class="ev-section-title">Last Action</div><div id="ev-last-action" class="ev-last-action">Loading...</div></div><div class="ev-section"><div class="ev-section-title">Next Actions</div><ul id="ev-next-actions" class="ev-next-actions"></ul></div><div class="ev-section"><div class="ev-section-title">Providers</div><div id="ev-provider-status" class="ev-provider-status">Loading...</div></div><div class="ev-refresh-row"><button id="btn-refresh-dashboard">Refresh</button></div>';
3236
t.settings='<span class="panel-title">Settings</span><label class="field-label" for="input-endpoint">Endpoint</label><input id="input-endpoint" type="text" class="field-input"/><label class="field-label" for="input-model">Model</label><input id="input-model" type="text" class="field-input"/><label class="field-label" for="input-api-key">Key</label><input id="input-api-key" type="text" class="field-input"/><label class="field-label" for="input-system-prompt">System Prompt</label><textarea id="input-system-prompt" class="field-input system-prompt-area" rows="6"></textarea><div class="settings-actions"><button id="btn-use-phi">Use Phi-4</button><button id="btn-use-foundry">Use Win AI</button><button id="btn-use-cloud">Use Cloud</button><button id="btn-save-settings">Save</button></div>';
37+
t.viz='<div class="panel-title-row"><span class="panel-title">Pipeline Viz</span><button id="btn-viz-refresh" style="margin-left:auto">Refresh</button></div><div id="cy" style="width:100%;height:calc(100vh - 80px);background:#111;"></div>';
3338
t.docs='<span class="panel-title">Docs Playbook</span><p id="docs-status-text" class="docs-status"></p><div class="docs-actions"><button id="btn-open-docs">Open Docs</button><button id="btn-load-rhai-mutation">Load Rhai</button></div><div class="docs-preview-wrap"><div id="docs-rig-log" class="log-content"></div></div>';
3439
return t[id]||'';
3540
}
@@ -301,3 +306,33 @@ document.addEventListener('DOMContentLoaded',function(){
301306
});
302307
});
303308
});
309+
310+
function initVizPanel(){
311+
if(_vizInitialized)return;
312+
var cy_div=document.getElementById('cy');
313+
if(!cy_div||typeof cytoscape==='undefined')return;
314+
invoke('get_holon_viz_graph').then(function(json){
315+
var data=JSON.parse(json);
316+
var elements=[];
317+
(data.nodes||[]).forEach(function(n){elements.push({data:n.data});});
318+
(data.edges||[]).forEach(function(e){elements.push({data:e.data});});
319+
window._cy=cytoscape({
320+
container:cy_div,
321+
elements:elements,
322+
layout:{name:'cose',animate:false},
323+
style:[
324+
{selector:'node',style:{'label':'data(label)','background-color':'#1a6fa8','color':'#fff',
325+
'text-valign':'center','text-halign':'center','font-size':'11px',
326+
'width':'label','height':'label','padding':'8px','shape':'roundrectangle'}},
327+
{selector:'edge',style:{'curve-style':'bezier','target-arrow-shape':'triangle',
328+
'line-color':'#555','target-arrow-color':'#555','width':1.5}},
329+
{selector:'node[kind="CapsuleGroup"]',style:{'background-color':'#5a3e8a'}},
330+
{selector:'node[kind="AuditEvent"]',style:{'background-color':'#7a3030'}},
331+
{selector:'node[kind="OwlClass"]',style:{'background-color':'#2e6e45'}},
332+
]
333+
});
334+
_vizInitialized=true;
335+
var btn=document.getElementById('btn-viz-refresh');
336+
if(btn)btn.addEventListener('click',function(){_vizInitialized=false;window._cy&&window._cy.destroy();initVizPanel();});
337+
}).catch(function(e){console.error('[viz] get_holon_viz_graph failed:',e);});
338+
}

0 commit comments

Comments
 (0)