271 lines
10 KiB
Plaintext
Raw Permalink Normal View History

2026-03-29 00:19:56 +00:00
#!/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:<id>
# Agent → Mermaid DSL. Human → daemon UI URL (if running) or Mermaid.
export def "graph show" [
type: string = "ontology", # ontology | flow | deps | mode:<id>
--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:<id> 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
}
}
}