271 lines
10 KiB
Plaintext
271 lines
10 KiB
Plaintext
#!/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
|
|
}
|
|
}
|
|
}
|