ontoref-derive: #[onto_mcp_tool] attribute macro registers MCP tool unit-structs in
the catalog at link time via inventory::submit!; annotated item is emitted unchanged,
ToolBase/AsyncTool impls stay on the struct. All 34 tools migrated from manual wiring
(net +5: ontoref_list_projects, ontoref_search, ontoref_describe,
ontoref_list_ontology_extensions, ontoref_get_ontology_extension).
validate modes (ADR-018): reads level_hierarchy from workflow.ncl and checks every
.ncl mode for level declared, strategy declared, delegate chain coherent, compose
extends valid. mode resolve <id> shows which hierarchy level handles a mode and why.
--self-test generates synthetic fixtures in a temp dir for CI smoke-testing.
validate run-cargo: two-step Cargo.toml resolution — workspace layout first
(crates/<check.crate>/Cargo.toml), single-crate fallback by package name or repo
basename. Lets the same ADR constraint shape apply to workspace and single-crate repos.
ontology/schemas/manifest.ncl: registry_topology_type contract — multi-registry
coordination, push targets, participant scopes, per-namespace capability.
reflection/requirements/base.ncl: oras ≥1.2.0, cosign ≥2.0.0, sops ≥3.9.0, age
≥1.1.0, restic declared as Hard/Soft requirements with version_min, check_cmd, and
install_hint (ADR-017 toolchain surface).
ADR-019: per-file recipient routing for tenant isolation without multi-vault. Schema
additions: sops.recipient_groups + sops.recipient_rules in ontoref-project.ncl.
secrets-bootstrap generates .sops.yaml from project.ncl in declarative mode. Three
new secrets-audit checks: recipient-routing-coherent, recipient-routing-coverage,
no-multi-vault. Adoption templates: single-team/, multi-tenant/, agent-first/.
Integration templates: domain-producer/, mode-producer/, mode-consumer/.
UI: project_picker surfaces registry badge (⟳ participant) and vault badge
(⛁ vault_id · N, green=declarative / amber=legacy) per project card. Expanded panel
adds collapsible Registry section with namespace, endpoint, and push/pull capability.
manage.html gains Runtime Services card — MCP and GraphQL toggleable without restart
via HTMX POST /ui/manage/services/{service}/toggle.
describe.nu: capabilities JSON includes registry_topology and vault_state per project.
sync.nu: drift check extended to detect //! absence on newly registered crates.
qa.ncl: six entries — credential-vault-best-practice (layered data-flow diagram),
credential-vault-templates (paths A/B/C), credential-vault-troubleshooting (15 named
errors), integration-what-and-why (ADR-042 OCI federation), integration-how-to-implement,
integration-troubleshooting.
on+re: core.ncl + manifest.ncl updated to reflect OCI, MCP, and mode-hierarchy nodes.
Deleted stale presentation assets (2026-02 slides + voice notes).
269 lines
10 KiB
Text
269 lines
10 KiB
Text
#!/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"
|
|
}
|
|
|
|
# daemon-url imported from store.nu — single source of truth (reads
|
|
# ~/.config/ontoref/config.ncl::daemon.port; respects ONTOREF_DAEMON_URL env override).
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|