#!/usr/bin/env nu # reflection/modules/graph.nu — actor-aware graph output. # # Agent → Mermaid DSL (stdout, parseable, diffeable) # Human → URL to daemon UI (if running), else Mermaid # # Supported types: # ontology — core.ncl nodes + edges as flowchart # flow — last run steps as DAG # deps — Rust crate dependency graph (if has_rust) # mode — a reflection mode DAG use ../modules/store.nu [daemon-export-safe] use ../modules/describe.nu [nickel-import-path] def project-root []: nothing -> string { let pr = ($env.ONTOREF_PROJECT_ROOT? | default "") if ($pr | is-not-empty) and ($pr != $env.ONTOREF_ROOT) { $pr } else { $env.ONTOREF_ROOT } } def actor-default []: nothing -> string { $env.ONTOREF_ACTOR? | default "developer" } def daemon-url []: nothing -> string { $env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891" } def daemon-running []: nothing -> bool { let r = do { ^curl -sf $"(daemon-url)/health" } | complete $r.exit_code == 0 } # Sanitize a string for use as a Mermaid node ID (no hyphens, dots, spaces). def mermaid-id [s: string]: nothing -> string { $s | str replace --all "-" "_" | str replace --all "." "_" | str replace --all " " "_" } # Emit Mermaid or UI URL based on actor and daemon availability. def emit-graph [mermaid: string, ui_path: string, fmt: string, actor: string]: nothing -> nothing { if $fmt == "mermaid" or $actor == "agent" { print $mermaid } else if $fmt == "url" { print $"(daemon-url)($ui_path)" } else { # text/human: prefer URL if daemon running, else Mermaid if (daemon-running) { print $"Open in UI: (daemon-url)($ui_path)" print $"Or run: ONTOREF_ACTOR=agent ontoref graph (if ($ui_path | str contains "ontology") { "ontology" } else if ($ui_path | str contains "flow") { "flow" } else { "deps" }) --fmt mermaid" } else { print $mermaid } } } def ontology-to-mermaid [root: string]: nothing -> string { let ip = (nickel-import-path $root) let core_file = $"($root)/.ontology/core.ncl" if not ($core_file | path exists) { return "flowchart LR\n note[\"No .ontology/core.ncl found\"]" } let core = (daemon-export-safe $core_file --import-path $ip) if $core == null { return "flowchart LR\n note[\"Failed to export core.ncl\"]" } let nodes = ($core.nodes? | default []) let edges = ($core.edges? | default []) mut lines = ["flowchart LR"] $lines = ($lines | append " classDef axiom fill:#1a1a2e,stroke:#e94560,color:#fff,font-weight:bold") $lines = ($lines | append " classDef tension fill:#16213e,stroke:#f5a623,color:#fff") $lines = ($lines | append " classDef practice fill:#0f3460,stroke:#53c28b,color:#fff") $lines = ($lines | append " classDef project fill:#1b262c,stroke:#4fc3f7,color:#fff") $lines = ($lines | append " classDef moment fill:#2d2d2d,stroke:#aaa,color:#fff") $lines = ($lines | append "") for n in $nodes { let nid = (mermaid-id $n.id) let label = ($n.name? | default $n.id) let level = ($n.level? | default "Project" | str downcase) $lines = ($lines | append $" ($nid)[\"($label)\"]:::($level)") } if ($edges | is-not-empty) { $lines = ($lines | append "") for e in $edges { let fid = (mermaid-id $e.from) let tid = (mermaid-id $e.to) let kind = ($e.kind? | default "") let label = if ($kind | is-not-empty) { $" -->|\"($kind)\"| " } else { " --> " } $lines = ($lines | append $" ($fid)($label)($tid)") } } $lines | str join "\n" } def run-flow-to-mermaid [root: string, actor: string]: nothing -> string { let runs_dir = $"($root)/.coder/($actor)/runs" let current_file = $"($runs_dir)/current.json" if not ($current_file | path exists) { return "flowchart TD\n note[\"No active run found\"]" } let current = (open $current_file) let run_id = ($current.run_id? | default "") let mode_id = ($current.mode? | default "") if ($run_id | is-empty) { return "flowchart TD\n note[\"No active run\"]" } let steps_file = $"($runs_dir)/($run_id)/steps.jsonl" let reported = if ($steps_file | path exists) { open $steps_file | lines | where { |l| $l | str trim | is-not-empty } | each { |l| $l | from json } } else { [] } # Load mode DAG for structure let ontoref_file = $"($env.ONTOREF_ROOT)/reflection/modes/($mode_id).ncl" let project_file = $"($root)/reflection/modes/($mode_id).ncl" let mode_file = if ($project_file | path exists) { $project_file } else if ($ontoref_file | path exists) { $ontoref_file } else { "" } let steps = if ($mode_file | is-not-empty) { let ip = (nickel-import-path (if ($project_file | path exists) { $root } else { $env.ONTOREF_ROOT })) let m = (daemon-export-safe $mode_file --import-path $ip) if $m != null { $m.steps? | default [] } else { [] } } else { [] } mut lines = [$"flowchart TD", $" classDef pass fill:#1a472a,stroke:#53c28b,color:#fff", $" classDef fail fill:#4a0e0e,stroke:#e94560,color:#fff", $" classDef skip fill:#2d2d2d,stroke:#888,color:#aaa", $" classDef pending fill:#1a1a2e,stroke:#555,color:#666", ""] for s in $steps { let sid = (mermaid-id $s.id) let rep_list = ($reported | where { |r| $r.step == $s.id }) let rep = if ($rep_list | is-not-empty) { $rep_list | first } else { null } let status = if ($rep | is-not-empty) { $rep.status } else { "pending" } let warn = if ($rep | is-not-empty) and ($rep.warnings? | default 0) > 0 { $" ⚠($rep.warnings)" } else { "" } $lines = ($lines | append $" ($sid)[\"($s.id)\\n($status)($warn)\"]:::($status)") } $lines = ($lines | append "") for s in $steps { let sid = (mermaid-id $s.id) for dep in ($s.depends_on? | default []) { let did = (mermaid-id $dep.step) let kind = ($dep.kind? | default "Always") let arrow = if $kind == "OnFailure" { " -.->|\"OnFailure\"| " } else { " --> " } $lines = ($lines | append $" ($did)($arrow)($sid)") } } $lines | str join "\n" } def deps-to-mermaid [root: string]: nothing -> string { let cargo_toml = $"($root)/Cargo.toml" if not ($cargo_toml | path exists) { return "flowchart LR\n note[\"No Cargo.toml found\"]" } let cargo = (open $cargo_toml) let members = ($cargo | get -o workspace.members | default []) if ($members | is-empty) { return "flowchart LR\n note[\"Single-crate project — no inter-crate deps to show\"]" } # Collect crate names let crate_names = ($members | each { |m| glob $"($root)/($m)/Cargo.toml" } | flatten | each { |f| let c = (open $f) $c | get -o package.name | default ($f | path dirname | path basename) }) # Collect inter-workspace dependencies mut edges = [] for m in $members { let expanded = (glob $"($root)/($m)/Cargo.toml") for ct in $expanded { let c = (open $ct) let cname = ($c | get -o package.name | default ($ct | path dirname | path basename)) let all_deps = ( ($c | get -o dependencies | default {} | columns) ++ ($c | get -o "dev-dependencies" | default {} | columns) ++ ($c | get -o "build-dependencies" | default {} | columns) ) for dep in $all_deps { if $dep in $crate_names { $edges = ($edges | append { from: $cname, to: $dep }) } } } } mut lines = ["flowchart LR"] for n in $crate_names { let nid = (mermaid-id $n) $lines = ($lines | append $" ($nid)[\"($n)\"]") } $lines = ($lines | append "") for e in $edges { let fid = (mermaid-id $e.from) let tid = (mermaid-id $e.to) $lines = ($lines | append $" ($fid) --> ($tid)") } $lines | str join "\n" } def mode-to-mermaid [mode_id: string]: nothing -> string { let root = (project-root) let project_file = $"($root)/reflection/modes/($mode_id).ncl" let ontoref_file = $"($env.ONTOREF_ROOT)/reflection/modes/($mode_id).ncl" let mode_file = if ($project_file | path exists) { $project_file } else if ($ontoref_file | path exists) { $ontoref_file } else { "" } if ($mode_file | is-empty) { return $"flowchart TD\n note[\"Mode '($mode_id)' not found\"]" } let mode_root = if ($project_file | path exists) { $root } else { $env.ONTOREF_ROOT } let ip = (nickel-import-path $mode_root) let m = (daemon-export-safe $mode_file --import-path $ip) if $m == null { return $"flowchart TD\n note[\"Failed to export mode '($mode_id)'\"]" } let steps = ($m.steps? | default []) mut lines = [$"flowchart TD", $" classDef human fill:#16213e,stroke:#f5a623,color:#fff", $" classDef agent fill:#0f3460,stroke:#53c28b,color:#fff", $" classDef both fill:#1a1a2e,stroke:#4fc3f7,color:#fff", ""] for s in $steps { let sid = (mermaid-id $s.id) let actor_class = match ($s.actor? | default "Both") { "Human" => "human", "Agent" => "agent", _ => "both", } let cmd_hint = if ($s.cmd? | default "" | is-not-empty) { "\\n[cmd]" } else { "" } $lines = ($lines | append $" ($sid)[\"($s.id)($cmd_hint)\"]:::($actor_class)") } $lines = ($lines | append "") for s in $steps { let sid = (mermaid-id $s.id) for dep in ($s.depends_on? | default []) { let did = (mermaid-id $dep.step) $lines = ($lines | append $" ($did) --> ($sid)") } } $lines | str join "\n" } # Emit a graph. Type: ontology | flow | deps | mode: # Agent → Mermaid DSL. Human → daemon UI URL (if running) or Mermaid. export def "graph show" [ type: string = "ontology", # ontology | flow | deps | mode: --fmt (-f): string = "", # mermaid | url | text (default: actor-aware) --actor: string = "", ]: nothing -> nothing { let root = (project-root) let a = if ($actor | is-not-empty) { $actor } else { (actor-default) } let f = if ($fmt | is-not-empty) { $fmt } else if $a == "agent" { "mermaid" } else { "text" } match $type { "ontology" => { let mmd = (ontology-to-mermaid $root) emit-graph $mmd "/graph" $f $a } "flow" => { let mmd = (run-flow-to-mermaid $root $a) emit-graph $mmd "/graph" $f $a } "deps" => { let mmd = (deps-to-mermaid $root) emit-graph $mmd "/graph" $f $a } _ => { # mode: or just the mode id let mode_id = if ($type | str starts-with "mode:") { $type | str replace "mode:" "" } else { $type } let mmd = (mode-to-mermaid $mode_id) emit-graph $mmd "/graph" $f $a } } }