#!/usr/bin/env nu # reflection/modules/describe.nu — project self-knowledge query layer. # Aggregates from ontology, ADRs, modes, manifest, justfiles, .claude, CI config # and renders answers from the perspective of a specific actor. # # The entry point for anyone (human, agent, CI) arriving at a project cold. 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" } # Build NICKEL_IMPORT_PATH for a given project root. # Includes project-local ontology, onref symlinked schemas, ADR defaults, # and the existing NICKEL_IMPORT_PATH from the environment. export def nickel-import-path [root: string]: nothing -> string { let entries = [ $"($root)/.ontology" $"($root)/adrs" $"($root)/.ontoref/ontology/schemas" $"($root)/.ontoref/adrs" $"($root)/.onref" $root $"($env.ONTOREF_ROOT)/ontology" $"($env.ONTOREF_ROOT)/ontology/schemas" $"($env.ONTOREF_ROOT)/adrs" $env.ONTOREF_ROOT ] let valid = ($entries | where { |p| $p | path exists } | uniq) let existing = ($env.NICKEL_IMPORT_PATH? | default "") if ($existing | is-not-empty) { ($valid | append $existing) | str join ":" } else { $valid | str join ":" } } use ../modules/store.nu [daemon-export-safe] # Centralized output dispatcher for all describe commands. # Handles text (via render callback), json, yaml, toml, table. def emit-output [data: record, fmt: string, renderer: closure]: nothing -> nothing { match $fmt { "json" => { print ($data | to json) }, "yaml" => { print ($data | to yaml) }, "toml" => { print "# TOML cannot represent nested arrays of records. Falling back to JSON." print ($data | to json) }, "table" => { print ($data | table --expand) }, _ => { do $renderer }, } } # ── describe project ──────────────────────────────────────────────────────────── # "What IS this project? What does it believe? What does it protect?" export def "describe project" [ --fmt: string = "", # Output format: text* | json | yaml | toml | table --actor: string = "", # Perspective: developer | agent | ci | auditor ]: 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" { "json" } else { "text" } let identity = (collect-identity $root) let axioms = (collect-axioms $root) let tensions = (collect-tensions $root) let practices = (collect-practices $root) let gates = (collect-gates $root) let adrs = (collect-adr-summary $root) let dimensions = (collect-dimensions $root) let data = { identity: $identity, axioms: $axioms, tensions: $tensions, practices: $practices, gates: $gates, adrs: $adrs, dimensions: $dimensions, } emit-output $data $f { || render-project-text $data $a $root } } # ── describe capabilities ─────────────────────────────────────────────────────── # "What can I DO here? What commands, modes, recipes, tools exist?" export def "describe capabilities" [ --fmt: string = "", --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" { "json" } else { "text" } let project_flags = (scan-project-flags $root) let just_modules = (scan-just-modules $root) let just_recipes = (scan-just-recipes $root) let ontoref_commands = (scan-ontoref-commands) let modes = (scan-reflection-modes $root) let claude = (scan-claude-capabilities $root) let ci_tools = (scan-ci-tools $root) let manifest_modes = (scan-manifest-modes $root) let manifest = (load-manifest-safe $root) let manifest_capabilities = ($manifest.capabilities? | default []) let backlog_pending = (count-backlog-pending $root) let data = { project_flags: $project_flags, just_modules: $just_modules, just_recipes: $just_recipes, ontoref_commands: $ontoref_commands, reflection_modes: $modes, claude_capabilities: $claude, ci_tools: $ci_tools, manifest_modes: $manifest_modes, manifest_capabilities: $manifest_capabilities, backlog_pending: $backlog_pending, } emit-output $data $f { || render-capabilities-text $data $a $root } } # ── describe mode ──────────────────────────────────────────────────────────────── # "What steps does this mode define? In what order? What does each step do?" export def "describe mode" [ name?: string, # Mode ID (without .ncl extension). Omit to list all. --fmt: string = "", # Output format: text* | json | yaml | table --actor: string = "", # Perspective: developer | agent | ci --with-capabilities, # Annotate each step with applicable flag (requires capabilities scan) ]: 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" { "json" } else { "text" } # List mode — no name given if ($name | is-empty) { let modes = (scan-reflection-modes $root) let data = { modes: $modes } emit-output $data $f {|| print "" print "AVAILABLE MODES" print "══════════════════════════════════════════════════════════════════" for m in $modes { let src = if $m.source == "project" { " [project]" } else { "" } print $" ($m.id)($src) — ($m.steps) steps" if ($m.trigger | is-not-empty) { print $" ($m.trigger)" } } print "" print $"Run: ontoref describe mode " } return } # Locate mode file — project-local takes precedence over ontoref let project_file = $"($root)/reflection/modes/($name).ncl" let ontoref_file = $"($env.ONTOREF_ROOT)/reflection/modes/($name).ncl" let mode_root = if ($project_file | path exists) { $root } else { $env.ONTOREF_ROOT } let mode_file = if ($project_file | path exists) { $project_file } else { $ontoref_file } if not ($mode_file | path exists) { print $"(ansi red)Mode '($name)' not found.(ansi reset)" print $" Searched: ($project_file)" print $" ($ontoref_file)" return } let ip = (nickel-import-path $mode_root) let mode = (daemon-export-safe $mode_file --import-path $ip) if $mode == null { print $"(ansi red)Failed to export mode '($name)' — check NCL syntax.(ansi reset)" return } # Optionally annotate steps with capability flags let flags = if $with_capabilities { (scan-project-flags $root) } else { {} } let steps = ($mode.steps? | default [] | each { |s| if ($flags | is-not-empty) { $s | insert "_applicable" true # placeholder — extended in T3 schema with `needs` } else { $s } }) let data = { id: ($mode.id? | default $name), trigger: ($mode.trigger? | default ""), preconditions: ($mode.preconditions? | default []), steps: $steps, postconditions: ($mode.postconditions? | default []), source: (if ($project_file | path exists) { "project" } else { "ontoref" }), file: $mode_file, } emit-output $data $f {|| print "" print $"MODE: ($data.id) [($data.source)]" print "══════════════════════════════════════════════════════════════════" if ($data.trigger | is-not-empty) { print $" ($data.trigger)" } if ($data.preconditions | is-not-empty) { print "" print " PRECONDITIONS" for p in $data.preconditions { print $" · ($p)" } } print "" print " STEPS" print " ──────────────────────────────────────────────────────────────" for s in $data.steps { let deps = if ($s.depends_on? | default [] | is-not-empty) { let dep_ids = ($s.depends_on | each { |d| let kind = ($d.kind? | default "Always") if $kind != "Always" { $"($d.step)[($kind)]" } else { $d.step } }) $" after: ($dep_ids | str join ', ')" } else { "" } let actor_tag = match ($s.actor? | default "Both") { "Human" => " [human]", "Agent" => " [agent]", _ => "", } let err = ($s.on_error?.strategy? | default "Stop") print $" ($s.id)($actor_tag) on_error=($err)($deps)" print $" ($s.action? | default '')" if ($s.cmd? | default "" | is-not-empty) { print $" $ ($s.cmd)" } if ($s.verify? | default "" | is-not-empty) { print $" verify: ($s.verify)" } } if ($data.postconditions | is-not-empty) { print "" print " POSTCONDITIONS" for p in $data.postconditions { print $" · ($p)" } } print "" } } # ── describe requirements ──────────────────────────────────────────────────────── # "What does this project need to run? What are the prod/dev prerequisites?" export def "describe requirements" [ --fmt: string = "", --actor: string = "", --environment: string = "", # filter by environment: production | development | both ]: 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" { "json" } else { "text" } let manifest = (load-manifest-safe $root) let all_reqs = ($manifest.requirements? | default []) let critical = ($manifest.critical_deps? | default []) let requirements = if ($environment | is-not-empty) { $all_reqs | where { |r| ($r.env? | default "Both") == ($environment | str capitalize) } } else { $all_reqs } let data = { requirements: $requirements, critical_deps: $critical, } emit-output $data $f {|| print "" print "REQUIREMENTS" print "══════════════════════════════════════════════════════════════════" if ($requirements | is-not-empty) { let prod = ($requirements | where { |r| ($r.env? | default "Both") in ["Production", "Both"] }) let dev = ($requirements | where { |r| ($r.env? | default "Both") in ["Development", "Both"] }) if ($prod | is-not-empty) { print "" print "PRODUCTION" print "──────────────────────────────────────────────────────────────────" for r in $prod { let req_flag = if ($r.required? | default true) { "(required)" } else { "(optional)" } let ver = if ($r.version? | default "" | is-not-empty) { $" >= ($r.version)" } else { "" } print $" ($r.name)($ver) [($r.kind? | default '')] ($req_flag)" if ($r.impact? | default "" | is-not-empty) { print $" Impact: ($r.impact)" } if ($r.provision? | default "" | is-not-empty) { print $" Provision: ($r.provision)" } } } if ($dev | is-not-empty) { print "" print "DEVELOPMENT" print "──────────────────────────────────────────────────────────────────" for r in $dev { let req_flag = if ($r.required? | default true) { "(required)" } else { "(optional)" } let ver = if ($r.version? | default "" | is-not-empty) { $" >= ($r.version)" } else { "" } print $" ($r.name)($ver) [($r.kind? | default '')] ($req_flag)" if ($r.impact? | default "" | is-not-empty) { print $" Impact: ($r.impact)" } if ($r.provision? | default "" | is-not-empty) { print $" Provision: ($r.provision)" } } } } else { print " (no requirements declared in manifest)" } if ($critical | is-not-empty) { print "" print "CRITICAL DEPENDENCIES" print "──────────────────────────────────────────────────────────────────" for d in $critical { print $" ($d.name) [($d.ref? | default '')]" print $" Used for: ($d.used_for? | default '')" print $" Failure: ($d.failure_impact? | default '')" if ($d.mitigation? | default "" | is-not-empty) { print $" Mitigation: ($d.mitigation)" } } } print "" } } # ── describe constraints ──────────────────────────────────────────────────────── # "What can I NOT do? What are the Hard rules?" export def "describe constraints" [ --fmt: string = "", --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" { "json" } else { "text" } let axioms = (collect-axioms $root) let hard_constraints = (collect-hard-constraints $root) let gates = (collect-gates $root) let data = { invariants: $axioms, hard_constraints: $hard_constraints, active_gates: $gates, } emit-output $data $f { || render-constraints-text $data $a } } # ── describe tools ────────────────────────────────────────────────────────────── # "What dev/CI tools does this project use? How do I call them?" export def "describe tools" [ --fmt: string = "", --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" { "json" } else { "text" } let ci_tools = (scan-ci-tools $root) let just_recipes = (scan-just-recipes $root) let dev_tools = (scan-dev-tools $root) let data = { ci_tools: $ci_tools, just_recipes: $just_recipes, dev_tools: $dev_tools, } emit-output $data $f { || render-tools-text $data $a $root } } # ── describe impact ───────────────────────────────────────────────────────────── # "If I change X, what else is affected?" export def "describe impact" [ node_id: string, # Ontology node id to trace --depth: int = 2, # How many edge hops to follow --include-external, # Follow connections.ncl to external projects via daemon --fmt: string = "", ]: nothing -> nothing { let root = (project-root) let f = if ($fmt | is-not-empty) { $fmt } else { "text" } let ontology = (load-ontology-safe $root) if ($ontology | is-empty) { print " No .ontology/core.ncl found." return } let nodes = ($ontology.nodes? | default []) let edges = ($ontology.edges? | default []) let target = ($nodes | where id == $node_id) if ($target | is-empty) { let available = ($nodes | get id | str join ", ") print $" Node '($node_id)' not found. Available: ($available)" return } let local_impacts = (trace-impacts $node_id $edges $nodes $depth) # When --include-external, query the daemon for cross-project entries let external_impacts = if $include_external { let daemon_url = ($env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891") let result = do { http get $"($daemon_url)/graph/impact?node=($node_id)&depth=($depth)&include_external=true" } | complete if $result.exit_code == 0 { let resp = ($result.stdout | from json) $resp.impacts? | default [] | each { |e| { id: $e.node_id, name: ($e.node_name? | default $e.node_id), level: "external", description: $"[$($e.slug)] via ($e.via)", depth: $e.depth, direction: $e.direction, external: true, } } } else { [] } } else { [] } let all_impacts = ($local_impacts | append $external_impacts) let data = { source: ($target | first), impacts: $all_impacts, include_external: $include_external, } emit-output $data $f { || render-impact-text $data } } # ── describe why ──────────────────────────────────────────────────────────────── # "Why does this decision/constraint/practice exist?" export def "describe why" [ id: string, # Node id, ADR id, or constraint id --fmt: string = "", ]: nothing -> nothing { let root = (project-root) let f = if ($fmt | is-not-empty) { $fmt } else { "text" } let ontology = (load-ontology-safe $root) let adr_data = (load-all-adrs $root) # Search in ontology nodes let node_match = if ($ontology | is-not-empty) { $ontology.nodes? | default [] | where id == $id } else { [] } # Search in ADRs let adr_match = ($adr_data | where { |a| $a.id == $id or $a.id == $"adr-($id)" }) let data = { node: (if ($node_match | is-not-empty) { $node_match | first } else { null }), adr: (if ($adr_match | is-not-empty) { $adr_match | first } else { null }), edges_from: (if ($ontology | is-not-empty) { $ontology.edges? | default [] | where from == $id } else { [] }), edges_to: (if ($ontology | is-not-empty) { $ontology.edges? | default [] | where to == $id } else { [] }), } emit-output $data $f { || render-why-text $data $id } } # ── describe find ────────────────────────────────────────────────────────────── # HOWTO-oriented search: What is it, Why it exists, How to use it, Where to look. # Extracts doc comments from Rust source, finds examples/tests, shows related nodes. # Human: interactive selector loop. Agent: structured JSON. export def "describe search" [ term: string, # Search term (case-insensitive substring match) --level: string = "", # Filter by level: Axiom | Tension | Practice | Project --fmt: string = "", --clip, # Copy selected result to clipboard after rendering ]: nothing -> nothing { let root = (project-root) let actor = (actor-default) let raw_fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" } let f = match $raw_fmt { "j" => "json", "y" => "yaml", "t" => "toml", "m" => "md", _ => $raw_fmt, } let ontology = (load-ontology-safe $root) if ($ontology | is-empty) { print " No .ontology/core.ncl found." return } let nodes = ($ontology.nodes? | default []) let edges = ($ontology.edges? | default []) let term_lower = ($term | str downcase) mut matches = ($nodes | where { |n| let id_match = ($n.id | str downcase | str contains $term_lower) let label_match = ($n.name? | default "" | str downcase | str contains $term_lower) let desc_match = ($n.description? | default "" | str downcase | str contains $term_lower) $id_match or $label_match or $desc_match }) if ($level | is-not-empty) { $matches = ($matches | where { |n| ($n.level? | default "") == $level }) } if ($matches | is-empty) { print $" No nodes matching '($term)'." if ($level | is-not-empty) { print $" (ansi dark_gray)Level filter: ($level)(ansi reset)" } return } if $f == "json" or $f == "yaml" or $f == "toml" { # Use $matches directly — no daemon/build-howto needed for structured output. let results = ($matches | each { |n| { id: $n.id, name: ($n.name? | default ""), level: ($n.level? | default ""), description: ($n.description? | default ""), pole: ($n.pole? | default ""), invariant: ($n.invariant? | default false), edges_from: ($edges | where from == $n.id | select kind to), edges_to: ($edges | where to == $n.id | select kind from), }}) let payload = { term: $term, count: ($results | length), results: $results } match $f { "json" => { print ($payload | to json) }, "yaml" => { print ($payload | to yaml) }, "toml" => { print ({ find: $payload } | to toml) }, } return } if $f == "md" { let results = ($matches | each { |n| build-howto $n $nodes $edges $root }) for r in $results { render-howto-md $r } return } if ($matches | length) == 1 { let node = ($matches | first) render-howto $node $nodes $edges $root if $clip { let h = (build-howto $node $nodes $edges $root) clip-text (howto-to-md-string $h) } return } # No TTY (subprocess, pipe, CI): print summary list without interactive selector. let is_tty = (do { ^test -t 0 } | complete | get exit_code) == 0 if not $is_tty { print "" print $" (ansi white_bold)Search:(ansi reset) '($term)' ($matches | length) results" print "" for m in $matches { let level_str = ($m.level? | default "" | fill -w 9) let name_str = ($m.name? | default $m.id) let desc_str = ($m.description? | default "") print $" (ansi cyan)($level_str)(ansi reset) (ansi white_bold)($m.id)(ansi reset) ($name_str)" if ($desc_str | is-not-empty) { print $" (ansi dark_gray)($desc_str)(ansi reset)" } } print "" return } find-interactive-loop $matches $nodes $edges $root $term $clip } # Backward-compatible alias — delegates to describe search. export def "describe find" [ term: string, --level: string = "", --fmt: string = "", --clip, ]: nothing -> nothing { describe search $term --level $level --fmt $fmt --clip=$clip } # Load entries from a qa.ncl file path. Returns empty list on missing file or export failure. def qa-load-entries [qa_path: string]: nothing -> list { if not ($qa_path | path exists) { return [] } let r = (do { ^nickel export --format json $qa_path } | complete) if $r.exit_code != 0 { return [] } ($r.stdout | from json | get entries? | default []) } # Word-overlap score: count of query words present in the combined entry text. def qa-score-entry [words: list, entry: record]: nothing -> int { let text = ($"($entry.question? | default '') ($entry.answer? | default '') ($entry.tags? | default [] | str join ' ')" | str downcase) $words | each { |w| if ($text | str contains $w) { 1 } else { 0 } } | math sum } # Search Q&A entries in reflection/qa.ncl with word-overlap scoring. # Falls back to describe search when no QA hits are found. export def "qa search" [ term: string, # Natural-language query --global (-g), # Also search ONTOREF_ROOT qa.ncl --no-fallback, # Do not fall back to ontology search --fmt: string = "", --clip, # Copy output to clipboard after rendering ]: nothing -> nothing { let root = (project-root) let actor = (actor-default) let f = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" } let words = ($term | str downcase | split words | where { |w| ($w | str length) > 2 }) let project_entries = (qa-load-entries $"($root)/reflection/qa.ncl") | each { |e| $e | insert scope "project" } mut entries = $project_entries if $global { let global_root = $env.ONTOREF_ROOT if $global_root != $root { let global_entries = (qa-load-entries $"($global_root)/reflection/qa.ncl") | each { |e| $e | insert scope "global" } $entries = ($entries | append $global_entries) } } let scored = ($entries | each { |e| $e | insert _score (qa-score-entry $words $e) } | where { |e| $e._score > 0 } | sort-by _score --reverse ) if ($scored | is-empty) { if not $no_fallback { print $" (ansi dark_gray)No QA entries matching '($term)' — searching ontology…(ansi reset)" describe search $term --fmt $fmt --clip=$clip } else { print $" No QA entries matching '($term)'." } return } if $f == "json" { let out = ($scored | reject _score | to json) print $out if $clip { clip-text $out } return } mut clip_lines: list = [] for e in $scored { let scope_tag = $"(ansi dark_gray)[($e.scope)](ansi reset)" let id_tag = $"(ansi cyan)($e.id)(ansi reset)" print $"($scope_tag) ($id_tag) (ansi white_bold)($e.question)(ansi reset)" if ($e.answer? | default "" | is-not-empty) { print $" ($e.answer)" } print "" if $clip { $clip_lines = ($clip_lines | append $"[($e.scope)] ($e.id) ($e.question)") if ($e.answer? | default "" | is-not-empty) { $clip_lines = ($clip_lines | append $" ($e.answer)") } $clip_lines = ($clip_lines | append "") } } if $clip and ($clip_lines | is-not-empty) { clip-text ($clip_lines | str join "\n") } } # ── HOWTO builder ───────────────────────────────────────────────────────────── # Extract //! or /// doc comments from a Rust file (module-level docs). def extract-rust-docs [file_path: string]: nothing -> string { if not ($file_path | path exists) { return "" } let lines = (open $file_path --raw | lines) mut doc_lines = [] mut in_docs = true for line in $lines { if not $in_docs { break } let trimmed = ($line | str trim) if ($trimmed | str starts-with "//!") { let content = ($trimmed | str replace "//! " "" | str replace "//!" "") $doc_lines = ($doc_lines | append $content) } else if ($trimmed | str starts-with "///") { let content = ($trimmed | str replace "/// " "" | str replace "///" "") $doc_lines = ($doc_lines | append $content) } else if ($trimmed | is-empty) and ($doc_lines | is-not-empty) { $doc_lines = ($doc_lines | append "") } else if ($doc_lines | is-not-empty) { $in_docs = false } } $doc_lines | str join "\n" | str trim } # Find examples related to a crate. def find-examples [root: string, crate_name: string]: nothing -> list { let examples_dir = $"($root)/crates/($crate_name)/examples" if not ($examples_dir | path exists) { return [] } glob $"($examples_dir)/*.rs" | each { |e| let name = ($e | path basename | str replace ".rs" "") let docs = (extract-rust-docs $e) let short = if ($docs | is-not-empty) { $docs | lines | first } else { "" } { name: $name, cmd: $"cargo run -p ($crate_name) --example ($name)", description: $short } } } # Find test files that reference a module/artifact. def find-tests [root: string, artifact_path: string]: nothing -> list { # Derive crate name and module name from artifact path. let parts = ($artifact_path | split row "/") if ($parts | length) < 2 { return [] } if ($parts | first) != "crates" { return [] } let crate_name = ($parts | get 1) let tests_dir = $"($root)/crates/($crate_name)/tests" if not ($tests_dir | path exists) { return [] } # Search for test files whose name relates to the artifact. let module_name = ($artifact_path | path basename | str replace ".rs" "") let test_files = (glob $"($tests_dir)/*.rs") $test_files | each { |tf| let test_name = ($tf | path basename | str replace ".rs" "") let content = (open $tf --raw) let module_lower = ($module_name | str downcase) let test_lower = ($test_name | str downcase) if ($test_lower | str contains $module_lower) or ($content | str downcase | str contains $module_lower) { let docs = (extract-rust-docs $tf) let short = if ($docs | is-not-empty) { $docs | lines | first } else { "" } { name: $test_name, cmd: $"cargo test -p ($crate_name) --test ($test_name)", description: $short } } else { null } } | compact } # Copy text to system clipboard (pbcopy / xclip / wl-copy). def clip-text [text: string]: nothing -> nothing { if (which pbcopy | is-not-empty) { $text | ^pbcopy print --stderr " ✓ Copied to clipboard" } else if (which xclip | is-not-empty) { $text | ^xclip -selection clipboard print --stderr " ✓ Copied to clipboard" } else if (which "wl-copy" | is-not-empty) { $text | ^wl-copy print --stderr " ✓ Copied to clipboard" } else { print --stderr " No clipboard tool found (install pbcopy, xclip, or wl-copy)" } } # Build a plain markdown string from a howto record (mirrors render-howto-md). def howto-to-md-string [h: record]: nothing -> string { mut lines: list = [] let inv = if $h.invariant { " **invariant**" } else { "" } $lines = ($lines | append $"# ($h.id)($inv)") $lines = ($lines | append "") $lines = ($lines | append $"**Level**: ($h.level) **Name**: ($h.name)") $lines = ($lines | append "") $lines = ($lines | append "## What") $lines = ($lines | append "") $lines = ($lines | append $h.what) if ($h.what_docs | is-not-empty) { $lines = ($lines | append "") $lines = ($lines | append $h.what_docs) } if ($h.source | is-not-empty) { $lines = ($lines | append "") $lines = ($lines | append "## Source") $lines = ($lines | append "") for s in $h.source { if ($s.modules? | is-not-empty) { $lines = ($lines | append $"- `($s.path)/`") let mods = ($s.modules | each { |m| $m | str replace ".rs" "" } | where { |m| $m != "mod" }) if ($mods | is-not-empty) { let mod_str = ($mods | each { |m| $"`($m)`" } | str join ", ") $lines = ($lines | append $" Modules: ($mod_str)") } } else { $lines = ($lines | append $"- `($s.path)`") } } } if ($h.examples | is-not-empty) { $lines = ($lines | append "") $lines = ($lines | append "## Examples") $lines = ($lines | append "") for ex in $h.examples { $lines = ($lines | append "```sh") $lines = ($lines | append $ex.cmd) $lines = ($lines | append "```") if ($ex.description | is-not-empty) { $lines = ($lines | append $ex.description) } $lines = ($lines | append "") } } if ($h.tests | is-not-empty) { $lines = ($lines | append "") $lines = ($lines | append "## Tests") $lines = ($lines | append "") for t in $h.tests { $lines = ($lines | append "```sh") $lines = ($lines | append $t.cmd) $lines = ($lines | append "```") if ($t.description | is-not-empty) { $lines = ($lines | append $t.description) } $lines = ($lines | append "") } } if ($h.related_to | is-not-empty) { $lines = ($lines | append "") $lines = ($lines | append "## Related") $lines = ($lines | append "") for r in $h.related_to { $lines = ($lines | append $"- → `($r.id)` ($r.name)") } } if ($h.used_by | is-not-empty) { $lines = ($lines | append "") $lines = ($lines | append "## Used by") $lines = ($lines | append "") for u in $h.used_by { $lines = ($lines | append $"- ← `($u.id)` ($u.name)") } } if ($h.adrs | is-not-empty) { $lines = ($lines | append "") $lines = ($lines | append "## Validated by") $lines = ($lines | append "") for adr in $h.adrs { $lines = ($lines | append $"- `($adr)`") } } $lines = ($lines | append "") $lines | str join "\n" } # Build full HOWTO record for a node. def build-howto [ n: record, all_nodes: list, edges: list, root: string, ]: nothing -> record { let artifacts = ($n.artifact_paths? | default []) # WHAT: doc comments from the artifact entry point. mut what_docs = "" mut source_files = [] for a in $artifacts { let full = $"($root)/($a)" if ($full | path exists) { if ($full | path type) == "dir" { # Directory artifact: read mod.rs or lib.rs. let mod_rs = $"($full)/mod.rs" let lib_rs = $"($full)/lib.rs" let entry = if ($mod_rs | path exists) { $mod_rs } else if ($lib_rs | path exists) { $lib_rs } else { "" } if ($entry | is-not-empty) { let docs = (extract-rust-docs $entry) if ($docs | is-not-empty) { $what_docs = $docs } $source_files = ($source_files | append { path: $a, entry: ($entry | path basename) }) } # List public source files in the directory. let rs_files = (glob ($full | path join "*.rs") | each { |f| $f | path basename } | sort) $source_files = ($source_files | append { path: $a, modules: $rs_files }) } else if ($full | str ends-with ".rs") { let docs = (extract-rust-docs $full) if ($docs | is-not-empty) and ($what_docs | is-empty) { $what_docs = $docs } $source_files = ($source_files | append { path: $a }) } else { $source_files = ($source_files | append { path: $a }) } } } # HOW: examples and tests. mut examples = [] mut tests = [] for a in $artifacts { if ($a | str starts-with "crates/") { let crate_name = ($a | split row "/" | get 1) let found_examples = (find-examples $root $crate_name) $examples = ($examples | append $found_examples) let found_tests = (find-tests $root $a) $tests = ($tests | append $found_tests) } } $examples = ($examples | uniq-by name) $tests = ($tests | uniq-by name) # RELATED: connected nodes (compact — just id + name for context, not structural dump). let related = ($edges | where from == $n.id | each { |e| let t = ($all_nodes | where id == $e.to) { id: $e.to, name: (if ($t | is-not-empty) { ($t | first).name? | default $e.to } else { $e.to }), relation: $e.kind } }) let used_by = ($edges | where to == $n.id | each { |e| let s = ($all_nodes | where id == $e.from) { id: $e.from, name: (if ($s | is-not-empty) { ($s | first).name? | default $e.from } else { $e.from }), relation: $e.kind } }) { id: $n.id, name: ($n.name? | default ""), level: ($n.level? | default ""), invariant: ($n.invariant? | default false), what: ($n.description? | default ""), what_docs: $what_docs, source: $source_files, examples: $examples, tests: $tests, related_to: $related, used_by: $used_by, adrs: ($n.adrs? | default []), } } # ── Render HOWTO (human) ────────────────────────────────────────────────────── def render-howto [ n: record, all_nodes: list, edges: list, root: string, ] { let h = (build-howto $n $all_nodes $edges $root) let level_val_color = match $h.level { "Axiom" => (ansi red), "Tension" => (ansi yellow), "Practice" => (ansi green), _ => (ansi cyan), } let inv = if $h.invariant { $" (ansi red_bold)invariant(ansi reset)" } else { "" } print "" print $" (ansi white_bold)($h.id)(ansi reset)($inv)" print $" ($level_val_color)($h.level)(ansi reset) ($h.name)" # WHAT — ontology description + source doc comments. print "" print $" (ansi white_bold)What(ansi reset)" print $" ($h.what)" if ($h.what_docs | is-not-empty) { print "" let doc_lines = ($h.what_docs | lines) for line in $doc_lines { print $" (ansi dark_gray)($line)(ansi reset)" } } # SOURCE — where the code lives, public modules. if ($h.source | is-not-empty) { print "" print $" (ansi white_bold)Source(ansi reset)" for s in $h.source { if ($s.modules? | is-not-empty) { print $" (ansi cyan)($s.path)/(ansi reset)" let mods = ($s.modules | each { |m| $m | str replace ".rs" "" } | where { |m| $m != "mod" }) if ($mods | is-not-empty) { let mod_str = ($mods | str join " ") print $" (ansi dark_gray)($mod_str)(ansi reset)" } } else { print $" (ansi cyan)($s.path)(ansi reset)" } } } # HOW — examples to run. if ($h.examples | is-not-empty) { print "" print $" (ansi white_bold)Examples(ansi reset)" for ex in $h.examples { print $" (ansi green)(ansi reset) ($ex.cmd)" if ($ex.description | is-not-empty) { print $" (ansi dark_gray)($ex.description)(ansi reset)" } } } # HOW — tests to run. if ($h.tests | is-not-empty) { print "" print $" (ansi white_bold)Tests(ansi reset)" for t in $h.tests { print $" (ansi green)(ansi reset) ($t.cmd)" if ($t.description | is-not-empty) { print $" (ansi dark_gray)($t.description)(ansi reset)" } } } # RELATED — connected nodes (compact, for context). if ($h.related_to | is-not-empty) { print "" print $" (ansi white_bold)Related(ansi reset)" for r in $h.related_to { print $" (ansi green)→(ansi reset) (ansi cyan)($r.id)(ansi reset) ($r.name)" } } if ($h.used_by | is-not-empty) { print "" print $" (ansi white_bold)Used by(ansi reset)" for u in $h.used_by { print $" (ansi yellow)←(ansi reset) (ansi cyan)($u.id)(ansi reset) ($u.name)" } } if ($h.adrs | is-not-empty) { print "" print $" (ansi white_bold)Validated by(ansi reset)" for adr in $h.adrs { print $" (ansi magenta)◆(ansi reset) (ansi cyan)($adr)(ansi reset)" } } print "" } # ── Render HOWTO (markdown) ───────────────────────────────────────────────── def render-howto-md [h: record] { let inv = if $h.invariant { " **invariant**" } else { "" } print $"# ($h.id)($inv)" print "" print $"**Level**: ($h.level) **Name**: ($h.name)" print "" print "## What" print "" print $h.what if ($h.what_docs | is-not-empty) { print "" print $h.what_docs } if ($h.source | is-not-empty) { print "" print "## Source" print "" for s in $h.source { if ($s.modules? | is-not-empty) { print $"- `($s.path)/`" let mods = ($s.modules | each { |m| $m | str replace ".rs" "" } | where { |m| $m != "mod" }) if ($mods | is-not-empty) { let mod_str = ($mods | each { |m| $"`($m)`" } | str join ", ") print $" Modules: ($mod_str)" } } else { print $"- `($s.path)`" } } } if ($h.examples | is-not-empty) { print "" print "## Examples" print "" for ex in $h.examples { print "```sh" print $ex.cmd print "```" if ($ex.description | is-not-empty) { print $ex.description } print "" } } if ($h.tests | is-not-empty) { print "" print "## Tests" print "" for t in $h.tests { print "```sh" print $t.cmd print "```" if ($t.description | is-not-empty) { print $t.description } print "" } } if ($h.related_to | is-not-empty) { print "" print "## Related" print "" for r in $h.related_to { print $"- → `($r.id)` ($r.name)" } } if ($h.used_by | is-not-empty) { print "" print "## Used by" print "" for u in $h.used_by { print $"- ← `($u.id)` ($u.name)" } } if ($h.adrs | is-not-empty) { print "" print "## Validated by" print "" for adr in $h.adrs { print $"- `($adr)`" } } print "" } # ── Interactive loop ────────────────────────────────────────────────────────── def find-interactive-loop [ matches: list, all_nodes: list, edges: list, root: string, term: string, clip: bool, ] { let match_count = ($matches | length) print "" print $" (ansi white_bold)Search:(ansi reset) '($term)' ($match_count) results" print "" let labels = ($matches | each { |n| let level_val_tag = ($n.level? | default "?" | fill -w 9) $"($level_val_tag) ($n.id) — ($n.name? | default '')" }) let quit_label = "← quit" let selector_items = ($labels | append $quit_label) loop { let pick = ($selector_items | input list $" (ansi cyan_bold)Select:(ansi reset) ") if ($pick | is-empty) or ($pick == $quit_label) { break } let picked_parts = ($pick | split row " — ") let picked_prefix = ($picked_parts | first | str trim) let picked_id = ($picked_prefix | split row " " | last) let node_matches = ($matches | where id == $picked_id) if ($node_matches | is-empty) { continue } let selected_node = ($node_matches | first) render-howto $selected_node $all_nodes $edges $root # Offer to jump to a related node, back to results, or quit. let h = (build-howto $selected_node $all_nodes $edges $root) if $clip { clip-text (howto-to-md-string $h) } let conn_ids = ($h.related_to | get id) | append ($h.used_by | get id) | uniq if ($conn_ids | is-not-empty) { let jump_items = ($conn_ids | append "← back" | append "← quit") let jump = ($jump_items | input list $" (ansi cyan_bold)Jump to:(ansi reset) ") if ($jump | is-empty) or ($jump == "← quit") { break } if $jump == "← back" { continue } let jumped = ($all_nodes | where id == $jump) if ($jumped | is-not-empty) { let jumped_node = ($jumped | first) render-howto $jumped_node $all_nodes $edges $root if $clip { let jh = (build-howto $jumped_node $all_nodes $edges $root) clip-text (howto-to-md-string $jh) } } } } } # ── describe features ────────────────────────────────────────────────────────── # "What concrete capabilities does this project have?" # No args → list all features. With id → detailed view of one feature. export def "describe features" [ id?: string, # Feature id to detail (optional — omit for list) --fmt: string = "", --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" { "json" } else { "text" } let ontology = (load-ontology-safe $root) if ($ontology | is-empty) { print " No .ontology/core.ncl found." return } let nodes = ($ontology.nodes? | default []) let edges = ($ontology.edges? | default []) # Features = all non-Axiom, non-Tension nodes (Practice, etc.) let features = ($nodes | where { |n| let level_val = ($n.level? | default "") $level_val != "Axiom" and $level_val != "Tension" }) # Cargo features (compile-time toggles) let cargo_features = (collect-cargo-features $root) if ($id | is-empty) or ($id == "") { # List mode let data = { features: ($features | each { |n| { id: $n.id, name: ($n.name? | default $n.id), level: ($n.level? | default ""), description: ($n.description? | default ""), artifacts: ($n.artifact_paths? | default [] | length), }}), cargo_features: $cargo_features, } emit-output $data $f { || render-features-list-text $data $root } } else { # Detail mode let target = ($features | where { |n| $n.id == $id }) if ($target | is-empty) { let available = ($features | get id | str join ", ") print $" Feature '($id)' not found. Available: ($available)" return } let node = ($target | first) # Verify artifact paths let artifacts = ($node.artifact_paths? | default [] | each { |p| let full = $"($root)/($p)" { path: $p, exists: ($full | path exists) } }) # Edges: dependencies (outgoing) and dependents (incoming) let outgoing = ($edges | where from == $id | each { |e| let target_node = ($nodes | where id == $e.to) let label = if ($target_node | is-not-empty) { ($target_node | first).name? | default $e.to } else { $e.to } { id: $e.to, name: $label, relation: ($e.kind? | default ""), direction: "depends_on" } }) let incoming = ($edges | where to == $id | each { |e| let source_node = ($nodes | where id == $e.from) let label = if ($source_node | is-not-empty) { ($source_node | first).name? | default $e.from } else { $e.from } { id: $e.from, name: $label, relation: ($e.kind? | default ""), direction: "depended_by" } }) # Related state dimensions let dims = (collect-dimensions $root) let related_dims = ($dims | where { |d| ($d.id | str contains $id) or ($id | str contains $d.id) }) # Related ADR constraints let hard_constraints = (collect-hard-constraints $root) let related_constraints = ($hard_constraints | where { |c| ($c.check_hint? | default "" | str contains $id) or ($c.adr_id? | default "" | str contains $id) }) # Cargo deps for the crate if feature maps to one let crate_deps = (collect-crate-deps $root $id) let data = { id: $node.id, name: ($node.name? | default $node.id), level: ($node.level? | default ""), pole: ($node.pole? | default ""), description: ($node.description? | default ""), invariant: ($node.invariant? | default false), artifacts: $artifacts, depends_on: $outgoing, depended_by: $incoming, dimensions: $related_dims, constraints: $related_constraints, crate_deps: $crate_deps, } emit-output $data $f { || render-feature-detail-text $data $root } } } # ── describe guides ───────────────────────────────────────────────────────────── # "Give me everything an actor needs to operate correctly in this project." # Single deterministic JSON output: identity, axioms, practices, constraints, # gate_state, dimensions, available_modes, actor_policy, language_guides, # content_assets, templates, connections. export def "describe guides" [ --actor: string = "", # Actor context: developer | agent | ci | admin --fmt: string = "", # Output format: json | yaml | text (default json for agent, text otherwise) ]: 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" { "json" } else { "json" } let identity = (collect-identity $root) let axioms = (collect-axioms $root) let practices = (collect-practices $root) let gates = (collect-gates $root) let dimensions = (collect-dimensions $root) let adrs = (collect-adr-summary $root) let modes = (scan-reflection-modes $root) let claude = (scan-claude-capabilities $root) let manifest = (load-manifest-safe $root) let conns = (collect-connections $root) let constraints = (collect-constraint-summary $root) let actor_policy = (derive-actor-policy $gates $a) let content_assets = ($manifest.content_assets? | default []) let templates = ($manifest.templates? | default []) let manifest_capabilities = ($manifest.capabilities? | default []) let manifest_requirements = ($manifest.requirements? | default []) let manifest_critical_deps = ($manifest.critical_deps? | default []) # Fetch API surface from daemon; empty list if daemon is not reachable. let daemon_url = ($env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891") let api_surface = do { let r = (do { http get $"($daemon_url)/api/catalog" } | complete) if $r.exit_code == 0 { let resp = ($r.stdout | from json) let all = ($resp.routes? | default []) if ($a | is-not-empty) { $all | where { |route| $route.actors | any { |act| $act == $a } } } else { $all } } else { [] } } let data = { identity: $identity, axioms: $axioms, practices: $practices, constraints: $constraints, gate_state: $gates, dimensions: $dimensions, adrs: $adrs, available_modes: $modes, actor_policy: $actor_policy, language_guides: $claude, content_assets: $content_assets, templates: $templates, connections: $conns, api_surface: $api_surface, capabilities: $manifest_capabilities, requirements: $manifest_requirements, critical_deps: $manifest_critical_deps, } emit-output $data $f {|| print $"=== Project Guides: ($identity.name) [actor: ($a)] ===" print "" print $"Identity: ($identity.name) / ($identity.kind)" print $"Axioms: ($axioms | length)" print $"Practices: ($practices | length)" print $"Modes: ($modes | length)" print $"Gates: ($gates | length) active" print $"Connections: ($conns | length)" print $"API surface: ($api_surface | length) endpoints visible to actor" print "" print "Actor policy:" print ($actor_policy | table) print "" print "Constraint summary:" print ($constraints | table) } } # ── describe api ──────────────────────────────────────────────────────────────── # "What HTTP endpoints does the daemon expose? How do I call them?" # Queries GET /api/catalog from the daemon and renders the full surface. export def "describe api" [ --actor: string = "", # Filter to endpoints whose actors include this role --tag: string = "", # Filter by tag (e.g. "graph", "describe", "auth") --auth: string = "", # Filter by auth level: none | viewer | admin --fmt: string = "", # Output format: text* | json ]: nothing -> nothing { let a = if ($actor | is-not-empty) { $actor } else { (actor-default) } let f = if ($fmt | is-not-empty) { $fmt } else if $a == "agent" { "json" } else { "text" } let daemon_url = ($env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891") let result = (do { http get $"($daemon_url)/api/catalog" } | complete) if $result.exit_code != 0 { print $" (ansi red)Daemon unreachable at ($daemon_url) — is it running?(ansi reset)" return } let resp = ($result.stdout | from json) mut routes = ($resp.routes? | default []) if ($actor | is-not-empty) { $routes = ($routes | where { |r| $r.actors | any { |act| $act == $actor } }) } if ($tag | is-not-empty) { $routes = ($routes | where { |r| $r.tags | any { |t| $t == $tag } }) } if ($auth | is-not-empty) { $routes = ($routes | where auth == $auth) } let data = { count: ($routes | length), routes: $routes } emit-output $data $f { || render-api-text $data } } def render-api-text [data: record]: nothing -> nothing { print $"(ansi white_bold)Daemon API surface(ansi reset) ($data.count) endpoints" print "" # Group by first tag for readable sectioning let grouped = ($data.routes | group-by { |r| if ($r.tags | is-empty) { "other" } else { $r.tags | first } }) for section in ($grouped | transpose key value | sort-by key) { print $"(ansi cyan_bold)── ($section.key | str upcase) ──────────────────────────────────────(ansi reset)" for r in $section.value { let auth_badge = match $r.auth { "none" => $"(ansi dark_gray)[open](ansi reset)", "viewer" => $"(ansi yellow)[viewer](ansi reset)", "admin" => $"(ansi red)[admin](ansi reset)", _ => $"(ansi dark_gray)[?](ansi reset)" } let actors_str = ($r.actors | str join ", ") let feat = if ($r.feature | is-not-empty) { $" (ansi dark_gray)feature:($r.feature)(ansi reset)" } else { "" } print $" (ansi white_bold)($r.method)(ansi reset) (ansi green)($r.path)(ansi reset) ($auth_badge)($feat)" print $" ($r.description)" if ($r.actors | is-not-empty) { print $" (ansi dark_gray)actors: ($actors_str)(ansi reset)" } if ($r.params | is-not-empty) { for p in $r.params { let con = $"(ansi dark_gray)($p.constraint)(ansi reset)" print $" (ansi dark_gray)· ($p.name) [($p.kind)] ($con) — ($p.description)(ansi reset)" } } print "" } } } # ── describe state ────────────────────────────────────────────────────────────── # "What FSM dimensions exist and where are they currently?" # Reads .ontology/state.ncl and prints each dimension with current/desired state, # horizon, and whether the desired state has been reached. export def "describe state" [ id?: string, # Dimension id to detail with transitions (omit for list) --fmt: string = "", # Output format: text* | json --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" { "json" } else { "text" } let dims = (collect-dimensions $root) if ($dims | is-empty) { print " No .ontology/state.ncl found or no dimensions declared." return } if ($id | is-empty) or ($id == "") { let data = { dimensions: $dims } emit-output $data $f { || print $"(ansi white_bold)FSM Dimensions(ansi reset) ($dims | length) total" print "" for d in $dims { let reached = $d.reached? | default false let status = if $reached { $"(ansi green)✓ reached(ansi reset)" } else { $"(ansi yellow)→ in progress(ansi reset)" } print $" (ansi white_bold)($d.id)(ansi reset) ($status)" print $" (ansi cyan)($d.name)(ansi reset) horizon: ($d.horizon)" print $" current: (ansi white)($d.current_state)(ansi reset) desired: (ansi dark_gray)($d.desired_state)(ansi reset)" print "" } } } else { let ip = (nickel-import-path $root) let state = (daemon-export-safe $"($root)/.ontology/state.ncl" --import-path $ip) if $state == null { print " Failed to export .ontology/state.ncl" return } let target = ($state.dimensions? | default [] | where id == $id) if ($target | is-empty) { let avail = ($dims | get id | str join ", ") print $" Dimension '($id)' not found. Available: ($avail)" return } let dim = ($target | first) let transitions = ($dim.transitions? | default []) let data = { id: $dim.id, name: $dim.name, description: ($dim.description? | default ""), current_state: $dim.current_state, desired_state: $dim.desired_state, horizon: ($dim.horizon? | default ""), reached: ($dim.current_state == $dim.desired_state), coupled_with: ($dim.coupled_with? | default []), transitions: ($transitions | each { |t| { from: $t.from, to: $t.to, condition: ($t.condition? | default ""), catalyst: ($t.catalyst? | default ""), blocker: ($t.blocker? | default ""), }}), } emit-output $data $f { || let reached = $data.reached print $"(ansi white_bold)($data.id)(ansi reset)" print $" (ansi cyan)($data.name)(ansi reset) horizon: ($data.horizon)" print $" ($data.description)" print "" print $" current : (ansi white_bold)($data.current_state)(ansi reset)" let status_badge = if $reached { $"(ansi green)✓ reached(ansi reset)" } else { $"(ansi yellow)in progress(ansi reset)" } print $" desired : ($data.desired_state) ($status_badge)" if ($data.coupled_with | is-not-empty) { print $" coupled : ($data.coupled_with | str join ', ')" } if ($data.transitions | is-not-empty) { print "" print $" (ansi white_bold)Transitions(ansi reset)" for t in $data.transitions { let marker = if $t.from == $data.current_state { $"(ansi green)▶(ansi reset)" } else { " " } print $" ($marker) ($t.from) → ($t.to)" if ($t.condition | is-not-empty) { print $" condition : ($t.condition)" } if ($t.catalyst | is-not-empty) { print $" catalyst : ($t.catalyst)" } if ($t.blocker | is-not-empty) and ($t.blocker != "none") { print $" (ansi yellow)blocker : ($t.blocker)(ansi reset)" } } } } } } # ── describe workspace ─────────────────────────────────────────────────────────── # "What crates are in this workspace and how do they depend on each other?" # Reads workspace Cargo.toml + member manifests. Shows only intra-workspace deps — # external crate dependencies are omitted to keep the output focused. export def "describe workspace" [ --fmt: string = "", # Output format: text* | json --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" { "json" } else { "text" } let ws_toml = $"($root)/Cargo.toml" if not ($ws_toml | path exists) { print " No Cargo.toml found at project root — not a Rust workspace." return } let ws = (open $ws_toml) let member_globs = ($ws | get -o workspace.members | default []) if ($member_globs | is-empty) { print " No workspace.members declared in Cargo.toml." return } # Collect all member crate names + features. mut crates = [] for mg in $member_globs { let expanded = (glob $"($root)/($mg)/Cargo.toml") for ct in $expanded { let c = (open $ct) let name = ($c | get -o package.name | default ($ct | path dirname | path basename)) let features = ($c | get -o features | default {} | columns) let all_deps = ($c | get -o dependencies | default {}) $crates = ($crates | append [{ name: $name, features: $features, all_deps: $all_deps, path: ($ct | path dirname | path relative-to $root) }]) } } let crate_names = ($crates | get name) # Build intra-workspace dep edges. let crates_with_ws_deps = ($crates | each { |cr| let dep_names = (try { $cr.all_deps | columns } catch { [] }) let ws_deps = ($dep_names | where { |d| $d in $crate_names }) { name: $cr.name, features: $cr.features, path: $cr.path, depends_on: $ws_deps } }) let data = { crates: $crates_with_ws_deps } emit-output $data $f { || print $"(ansi white_bold)Workspace Crates(ansi reset) ($crates_with_ws_deps | length) members" print "" for cr in $crates_with_ws_deps { print $" (ansi white_bold)($cr.name)(ansi reset) (ansi dark_gray)($cr.path)(ansi reset)" if ($cr.features | is-not-empty) { print $" features : ($cr.features | str join ', ')" } if ($cr.depends_on | is-not-empty) { print $" deps : ($cr.depends_on | each { |d| $'(ansi cyan)($d)(ansi reset)' } | str join ', ')" } print "" } } } # ── describe diff ─────────────────────────────────────────────────────────────── # "What changed in the ontology since the last commit?" # Compares the current working-tree core.ncl against the HEAD-committed version. # Outputs structured added/removed/changed diffs for nodes and edges. export def "describe diff" [ --fmt: string = "", # Output format: text* | json --file: string = "", # Ontology file to diff (relative to project root, default .ontology/core.ncl) ]: nothing -> nothing { let root = (project-root) let f = if ($fmt | is-not-empty) { $fmt } else { "text" } let rel = if ($file | is-not-empty) { $file } else { ".ontology/core.ncl" } let current = (load-ontology-safe $root) let committed = (diff-export-committed $rel $root) let curr_nodes = ($current.nodes? | default [] | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), level: ($n.level? | default ""), pole: ($n.pole? | default ""), invariant: ($n.invariant? | default false) } }) let comm_nodes = ($committed.nodes? | default [] | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), level: ($n.level? | default ""), pole: ($n.pole? | default ""), invariant: ($n.invariant? | default false) } }) let curr_ids = ($curr_nodes | get id) let comm_ids = ($comm_nodes | get id) let nodes_added = ($curr_nodes | where { |n| not ($comm_ids | any { |id| $id == $n.id }) }) let nodes_removed = ($comm_nodes | where { |n| not ($curr_ids | any { |id| $id == $n.id }) }) # Nodes present in both — compare field by field. let both_ids = ($curr_ids | where { |id| $comm_ids | any { |cid| $cid == $id } }) let nodes_changed = ($both_ids | each { |id| let curr = ($curr_nodes | where id == $id | first) let prev = ($comm_nodes | where id == $id | first) if ($curr.name != $prev.name or $curr.description != $prev.description or $curr.level != $prev.level or $curr.pole != $prev.pole or $curr.invariant != $prev.invariant) { { id: $id, before: $prev, after: $curr } } else { null } } | compact) let curr_edges = ($current.edges? | default [] | each { |e| let ef = ($e.from? | default "") let et = ($e.to? | default "") let ek = ($e.kind? | default "") { key: $"($ef)->($et)[($ek)]", from: $ef, to: $et, kind: $ek } }) let comm_edges = ($committed.edges? | default [] | each { |e| let ef = ($e.from? | default "") let et = ($e.to? | default "") let ek = ($e.kind? | default "") { key: $"($ef)->($et)[($ek)]", from: $ef, to: $et, kind: $ek } }) let curr_ekeys = ($curr_edges | get key) let comm_ekeys = ($comm_edges | get key) let edges_added = ($curr_edges | where { |e| not ($comm_ekeys | any { |k| $k == $e.key }) }) let edges_removed = ($comm_edges | where { |e| not ($curr_ekeys | any { |k| $k == $e.key }) }) let data = { file: $rel, nodes_added: $nodes_added, nodes_removed: $nodes_removed, nodes_changed: $nodes_changed, edges_added: $edges_added, edges_removed: $edges_removed, summary: { nodes_added: ($nodes_added | length), nodes_removed: ($nodes_removed | length), nodes_changed: ($nodes_changed | length), edges_added: ($edges_added | length), edges_removed: ($edges_removed | length), }, } emit-output $data $f { || render-diff-text $data } } def diff-export-committed [rel_path: string, root: string]: nothing -> record { let ip = (nickel-import-path $root) let show = (do { ^git -C $root show $"HEAD:($rel_path)" } | complete) if $show.exit_code != 0 { return {} } let mk = (do { ^mktemp } | complete) if $mk.exit_code != 0 { return {} } let tmp = ($mk.stdout | str trim) $show.stdout | save --force $tmp let r = (do { ^nickel export --format json --import-path $ip $tmp } | complete) do { ^rm -f $tmp } | complete | ignore if $r.exit_code != 0 { return {} } $r.stdout | from json } def render-diff-text [data: record]: nothing -> nothing { let s = $data.summary let total = ($s.nodes_added + $s.nodes_removed + $s.nodes_changed + $s.edges_added + $s.edges_removed) print $"(ansi white_bold)Ontology diff vs HEAD:(ansi reset) ($data.file)" print "" if $total == 0 { print $" (ansi dark_gray)No changes — working tree matches HEAD.(ansi reset)" return } if $s.nodes_added > 0 { print $"(ansi green_bold)Nodes added ($s.nodes_added):(ansi reset)" for n in $data.nodes_added { print $" + (ansi green)($n.id)(ansi reset) [($n.level)] ($n.name)" } print "" } if $s.nodes_removed > 0 { print $"(ansi red_bold)Nodes removed ($s.nodes_removed):(ansi reset)" for n in $data.nodes_removed { print $" - (ansi red)($n.id)(ansi reset) [($n.level)] ($n.name)" } print "" } if $s.nodes_changed > 0 { print $"(ansi yellow_bold)Nodes changed ($s.nodes_changed):(ansi reset)" for c in $data.nodes_changed { print $" ~ (ansi yellow)($c.id)(ansi reset)" if $c.before.name != $c.after.name { print $" name: (ansi dark_gray)($c.before.name)(ansi reset) → ($c.after.name)" } if $c.before.description != $c.after.description { let prev = ($c.before.description | str substring 0..60) let curr = ($c.after.description | str substring 0..60) print $" description: (ansi dark_gray)($prev)…(ansi reset) → ($curr)…" } if $c.before.level != $c.after.level { print $" level: (ansi dark_gray)($c.before.level)(ansi reset) → ($c.after.level)" } if $c.before.pole != $c.after.pole { print $" pole: (ansi dark_gray)($c.before.pole)(ansi reset) → ($c.after.pole)" } if $c.before.invariant != $c.after.invariant { print $" invariant: (ansi dark_gray)($c.before.invariant)(ansi reset) → ($c.after.invariant)" } } print "" } if $s.edges_added > 0 { print $"(ansi cyan_bold)Edges added ($s.edges_added):(ansi reset)" for e in $data.edges_added { print $" + (ansi cyan)($e.from)(ansi reset) →[($e.kind)]→ (ansi cyan)($e.to)(ansi reset)" } print "" } if $s.edges_removed > 0 { print $"(ansi magenta_bold)Edges removed ($s.edges_removed):(ansi reset)" for e in $data.edges_removed { print $" - (ansi magenta)($e.from)(ansi reset) →[($e.kind)]→ (ansi magenta)($e.to)(ansi reset)" } print "" } } # ── Collectors ────────────────────────────────────────────────────────────────── def collect-identity [root: string]: nothing -> record { # From Cargo.toml or manifest let cargo = $"($root)/Cargo.toml" let name = if ($cargo | path exists) { let cargo_data = (open $cargo) if ($cargo_data | get -o package.name | is-not-empty) { $cargo_data | get package.name } else if ($cargo_data | get -o workspace | is-not-empty) { $root | path basename } else { $root | path basename } } else { $root | path basename } let manifest = (load-manifest-safe $root) let kind = if ($manifest | is-not-empty) { $manifest.repo_kind? | default "" } else { "" } let description = if ($manifest | is-not-empty) { $manifest.description? | default "" } else { "" } { name: $name, kind: $kind, description: $description, root: $root, has_ontology: ($"($root)/.ontology/core.ncl" | path exists), has_adrs: ((glob $"($root)/adrs/adr-*.ncl" | length) > 0), has_reflection: ($"($root)/reflection" | path exists), has_manifest: ($"($root)/.ontology/manifest.ncl" | path exists), has_coder: ($"($root)/.coder" | path exists), } } def collect-axioms [root: string]: nothing -> list { let ontology = (load-ontology-safe $root) if ($ontology | is-empty) { return [] } $ontology.nodes? | default [] | where { |n| ($n.invariant? | default false) == true } | each { |n| { id: $n.id, name: $n.name, description: $n.description } } } def collect-tensions [root: string]: nothing -> list { let ontology = (load-ontology-safe $root) if ($ontology | is-empty) { return [] } $ontology.nodes? | default [] | where { |n| ($n.level? | default "") == "Tension" } | each { |n| { id: $n.id, name: $n.name, description: $n.description } } } def collect-practices [root: string]: nothing -> list { let ontology = (load-ontology-safe $root) if ($ontology | is-empty) { return [] } $ontology.nodes? | default [] | where { |n| ($n.level? | default "") == "Practice" } | each { |n| { id: $n.id, name: $n.name, description: $n.description, artifact_paths: ($n.artifact_paths? | default []), }} } def collect-gates [root: string]: nothing -> list { let gate_file = $"($root)/.ontology/gate.ncl" if not ($gate_file | path exists) { return [] } let ip = (nickel-import-path $root) let gate = (daemon-export-safe $gate_file --import-path $ip) if $gate == null { return [] } $gate.membranes? | default [] | where { |m| ($m.active? | default false) == true } | each { |m| { id: $m.id, name: $m.name, permeability: ($m.permeability? | default ""), accepts: ($m.accepts? | default []), }} } def collect-dimensions [root: string]: nothing -> list { let state_file = $"($root)/.ontology/state.ncl" if not ($state_file | path exists) { return [] } let ip = (nickel-import-path $root) let state = (daemon-export-safe $state_file --import-path $ip) if $state == null { return [] } $state.dimensions? | default [] | each { |d| { id: $d.id, name: $d.name, current_state: $d.current_state, desired_state: $d.desired_state, horizon: ($d.horizon? | default ""), reached: ($d.current_state == $d.desired_state), }} } def collect-adr-summary [root: string]: nothing -> list { let files = (glob $"($root)/adrs/adr-*.ncl") let ip = (nickel-import-path $root) $files | each { |f| let adr = (daemon-export-safe $f --import-path $ip) if $adr != null { { id: ($adr.id? | default ""), title: ($adr.title? | default ""), status: ($adr.status? | default ""), constraint_count: ($adr.constraints? | default [] | length), } } else { null } } | compact } def collect-hard-constraints [root: string]: nothing -> list { let files = (glob $"($root)/adrs/adr-*.ncl") let ip = (nickel-import-path $root) $files | each { |f| let adr = (daemon-export-safe $f --import-path $ip) if $adr != null { if ($adr.status? | default "") == "Accepted" { let constraints = ($adr.constraints? | default []) $constraints | where { |c| ($c.severity? | default "") == "Hard" } | each { |c| { adr_id: ($adr.id? | default ""), check_hint: ($c.check_hint? | default ""), description: ($c.description? | default ""), }} } else { [] } } else { [] } } | flatten } def collect-constraint-summary [root: string]: nothing -> list { let files = (glob $"($root)/adrs/adr-*.ncl") let ip = (nickel-import-path $root) $files | each { |f| let adr = (daemon-export-safe $f --import-path $ip) if $adr != null { if ($adr.status? | default "") == "Accepted" { let constraints = ($adr.constraints? | default []) $constraints | each { |c| { adr_id: ($adr.id? | default ""), severity: ($c.severity? | default ""), description: ($c.description? | default ""), check_tag: ($c.check?.tag? | default ($c.check_hint? | default "")), }} } else { [] } } else { [] } } | flatten } def collect-connections [root: string]: nothing -> list { let conn_file = $"($root)/.ontology/connections.ncl" if not ($conn_file | path exists) { return [] } let ip = (nickel-import-path $root) let conn = (daemon-export-safe $conn_file --import-path $ip) if $conn == null { return [] } $conn.connections? | default [] } # Derive what an actor is allowed to do based on the active gate membranes. # Permeability: Open → full access; Controlled/Locked → restricted; Closed → read-only. def derive-actor-policy [gates: list, actor: string]: nothing -> record { let is_agent = ($actor == "agent") let is_ci = ($actor == "ci") # Find the most restrictive membrane that constrains the actor. let permeabilities = ($gates | get -o permeability | compact | uniq) let most_restrictive = if ($permeabilities | any { |p| $p == "Closed" }) { "Closed" } else if ($permeabilities | any { |p| $p == "Locked" }) { "Locked" } else if ($permeabilities | any { |p| $p == "Controlled" }) { "Controlled" } else { "Open" } let base_open = ($most_restrictive == "Open") let base_controlled = ($most_restrictive == "Controlled" or $most_restrictive == "Open") { actor: $actor, gate_permeability: $most_restrictive, can_read_ontology: true, can_read_adrs: true, can_read_manifest: true, can_run_modes: (if $is_agent { $base_controlled } else { true }), can_modify_adrs: (if ($is_agent or $is_ci) { $base_open } else { $base_controlled }), can_modify_ontology: (if ($is_agent or $is_ci) { $base_open } else { $base_controlled }), can_push_sync: (if $is_agent { false } else { $base_controlled }), } } # ── Scanners ──────────────────────────────────────────────────────────────────── def scan-just-modules [root: string]: nothing -> list { let justfile = $"($root)/justfile" if not ($justfile | path exists) { return [{ status: "no_justfile" }] } let content = (open $justfile --raw) let mod_lines = ($content | lines | where { |l| ($l | str starts-with "mod ") or ($l | str starts-with "mod? ") }) let import_lines = ($content | lines | where { |l| ($l | str starts-with "import ") or ($l | str starts-with "import? ") }) mut modules = [] for line in $mod_lines { let parts = ($line | split row " " | where { |p| ($p | is-not-empty) }) let name = if ($parts | length) >= 2 { $parts | get 1 | str replace "?" "" } else { "unknown" } $modules = ($modules | append { type: "mod", name: $name, line: $line }) } for line in $import_lines { let parts = ($line | split row "'" | where { |p| ($p | is-not-empty) }) let path = if ($parts | length) >= 2 { $parts | get 1 | str trim } else { "unknown" } let name = ($path | path basename | str replace ".just" "") $modules = ($modules | append { type: "import", name: $name, path: $path }) } # Also check justfiles/ directory let justfiles_dir = $"($root)/justfiles" if ($justfiles_dir | path exists) { let files = (glob $"($justfiles_dir)/*.just") for f in $files { let name = ($f | path basename | str replace ".just" "") let already = ($modules | where { |m| $m.name == $name }) if ($already | is-empty) { $modules = ($modules | append { type: "file_only", name: $name, path: ($f | str replace $root ".") }) } } } $modules } def categorize-recipe [name: string]: nothing -> string { if ($name | str starts-with "ci") { "ci" } else if ($name | str starts-with "build") { "build" } else if ($name | str starts-with "test") or ($name == "test") { "test" } else if ($name | str starts-with "doc") { "docs" } else if ($name | str starts-with "deploy") { "deploy" } else if ($name | str starts-with "nickel") { "nickel" } else if ($name | str starts-with "install") or ($name | str starts-with "release") or ($name | str starts-with "package") or ($name | str starts-with "dist") { "distro" } else if ($name in ["fmt", "format", "lint", "watch", "dev", "setup", "setup-hooks", "clean"]) or ($name | str starts-with "fmt") or ($name | str starts-with "lint") or ($name | str starts-with "watch") { "dev" } else { "other" } } def scan-just-recipes [root: string]: nothing -> list { let result = do { ^just --list --unsorted --justfile $"($root)/justfile" } | complete if $result.exit_code != 0 { return [] } $result.stdout | lines | where { |l| ($l | str trim | is-not-empty) and not ($l | str starts-with "Available") } | each { |l| let trimmed = ($l | str trim) let parts = ($trimmed | split row " # ") let name = ($parts | first | str trim) let desc = if ($parts | length) > 1 { $parts | skip 1 | str join " # " | str trim } else { "" } { name: $name, category: (categorize-recipe $name), description: $desc } } } def scan-ontoref-commands []: nothing -> list { [ "check", "form list", "form run", "mode list", "mode show", "mode run", "mode select", "adr list", "adr validate", "adr show", "adr accept", "constraint", "register", "backlog roadmap", "backlog list", "backlog add", "backlog done", "config show", "config verify", "config audit", "config apply", "sync scan", "sync diff", "sync propose", "sync apply", "sync audit", "coder init", "coder record", "coder log", "coder export", "coder triage", "manifest mode", "manifest publish", "manifest layers", "manifest consumers", "describe project", "describe capabilities", "describe constraints", "describe tools", "describe features", "describe impact", "describe why", "describe guides", "describe diff", "describe api", "describe state", "describe workspace", ] } def scan-reflection-modes [root: string]: nothing -> list { let ontoref_modes = (glob $"($env.ONTOREF_ROOT)/reflection/modes/*.ncl") let project_modes = if $root != $env.ONTOREF_ROOT { glob $"($root)/reflection/modes/*.ncl" } else { [] } let all_modes = ($ontoref_modes | append $project_modes | uniq) $all_modes | each { |f| let mode_root = if ($f | str starts-with $root) and ($root != $env.ONTOREF_ROOT) { $root } else { $env.ONTOREF_ROOT } let ip = (nickel-import-path $mode_root) let m = (daemon-export-safe $f --import-path $ip) if $m != null { { id: ($m.id? | default ""), trigger: ($m.trigger? | default ""), steps: ($m.steps? | default [] | length), source: (if ($f | str starts-with $root) and ($root != $env.ONTOREF_ROOT) { "project" } else { "ontoref" }), } } else { null } } | compact } def scan-claude-capabilities [root: string]: nothing -> record { let claude_dir = $"($root)/.claude" if not ($claude_dir | path exists) { return { present: false } } let has_claude_md = ($"($claude_dir)/CLAUDE.md" | path exists) let guidelines = (glob $"($claude_dir)/guidelines/*/" | each { |d| $d | path basename }) let commands = (glob $"($claude_dir)/commands/*.md" | length) let skills = (glob $"($claude_dir)/skills/*/" | each { |d| $d | path basename }) let hooks = (glob $"($claude_dir)/hooks/*" | length) let agents = (glob $"($claude_dir)/agents/*.md" | each { |f| $f | path basename | str replace ".md" "" }) let profiles = (glob $"($claude_dir)/profiles/*/" | each { |d| $d | path basename }) let stacks = (glob $"($claude_dir)/stacks/*/" | each { |d| $d | path basename }) { present: true, has_claude_md: $has_claude_md, guidelines: $guidelines, commands_count: $commands, skills: $skills, hooks_count: $hooks, agents: $agents, profiles: $profiles, stacks: $stacks, } } def scan-ci-tools [root: string]: nothing -> list { let config_path = $"($root)/.typedialog/ci/config.ncl" if not ($config_path | path exists) { return [] } let ip = (nickel-import-path $root) let config = (daemon-export-safe $config_path --import-path $ip) if $config == null { return [] } let tools = ($config.ci?.tools? | default {}) $tools | transpose name cfg | each { |t| { name: $t.name, enabled: ($t.cfg.enabled? | default false), install_method: ($t.cfg.install_method? | default "unknown"), }} } def scan-manifest-modes [root: string]: nothing -> list { let manifest = (load-manifest-safe $root) if ($manifest | is-empty) { return [] } $manifest.operational_modes? | default [] | each { |m| { id: ($m.id? | default ""), description: ($m.description? | default ""), audit_level: ($m.audit_level? | default ""), }} } def scan-dev-tools [root: string]: nothing -> list { mut tools = [] if ($"($root)/bacon.toml" | path exists) { $tools = ($tools | append { tool: "bacon", config: "bacon.toml", purpose: "File watcher with diagnostics export" }) } if ($"($root)/.cargo/config.toml" | path exists) { $tools = ($tools | append { tool: "cargo", config: ".cargo/config.toml", purpose: "Build system with custom aliases" }) } if ($"($root)/.pre-commit-config.yaml" | path exists) { $tools = ($tools | append { tool: "pre-commit", config: ".pre-commit-config.yaml", purpose: "Pre-commit hook runner" }) } if ($"($root)/deny.toml" | path exists) or ($"($root)/.cargo/deny.toml" | path exists) { $tools = ($tools | append { tool: "cargo-deny", config: "deny.toml", purpose: "License and advisory checker" }) } if ($"($root)/rustfmt.toml" | path exists) or ($"($root)/.rustfmt.toml" | path exists) { $tools = ($tools | append { tool: "rustfmt", config: "rustfmt.toml", purpose: "Code formatter" }) } if ($"($root)/clippy.toml" | path exists) or ($"($root)/.clippy.toml" | path exists) { $tools = ($tools | append { tool: "clippy", config: "clippy.toml", purpose: "Linter" }) } $tools } def git-remote-slug [root: string]: nothing -> string { let r = do { ^git -C $root remote get-url origin } | complete if $r.exit_code != 0 { return "" } let url = ($r.stdout | str trim) if ($url | str contains "@") { # git@host:owner/repo.git $url | split row ":" | last | str replace -r '\.git$' "" | str trim } else { # https://host/owner/repo.git let parts = ($url | split row "/" | last 2) ($parts | str join "/") | str replace -r '\.git$' "" } } def scan-project-flags [root: string]: nothing -> record { let cargo_toml = $"($root)/Cargo.toml" let has_rust = ($cargo_toml | path exists) let crates = if $has_rust { let cargo = (open $cargo_toml) let members = ($cargo | get -o workspace.members | default []) if ($members | is-not-empty) { $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) } } else { let name = ($cargo | get -o package.name | default ($root | path basename)) [$name] } } else { [] } let config_file = $"($root)/.ontoref/config.ncl" let has_nats = if ($config_file | path exists) { let ip = (nickel-import-path $root) let cfg = (daemon-export-safe $config_file --import-path $ip) if $cfg != null { $cfg.nats_events?.enabled? | default false } else { false } } else { false } let has_git = ($"($root)/.git" | path exists) let git_slug = if $has_git { git-remote-slug $root } else { "" } let has_git_remote = ($git_slug | is-not-empty) let open_prs = if $has_git_remote and (which gh | is-not-empty) { let r = do { ^gh pr list --repo $git_slug --state open --json number --jq length } | complete if $r.exit_code == 0 { $r.stdout | str trim | into int } else { 0 } } else { 0 } { has_rust: $has_rust, has_ui: (($"($root)/templates" | path exists) or ($"($root)/assets" | path exists)), has_mdbook: ($"($root)/docs/SUMMARY.md" | path exists), has_nats: $has_nats, has_precommit: ($"($root)/.pre-commit-config.yaml" | path exists), has_backlog: ($"($root)/reflection/backlog.ncl" | path exists), has_git_remote: $has_git_remote, git_slug: $git_slug, open_prs: $open_prs, crates: $crates, } } def count-backlog-pending [root: string]: nothing -> int { let file = $"($root)/reflection/backlog.ncl" if not ($file | path exists) { return 0 } let ip = (nickel-import-path $root) let backlog = (daemon-export-safe $file --import-path $ip) if $backlog == null { return 0 } ($backlog.items? | default []) | where { |i| ($i.status? | default "open") not-in ["done", "graduated"] } | length } # ── Feature collectors ──────────────────────────────────────────────────────── def collect-cargo-features [root: string]: nothing -> list { let cargo_toml = $"($root)/Cargo.toml" if not ($cargo_toml | path exists) { return [] } let cargo = (open $cargo_toml) let members = ($cargo | get -o workspace.members | default []) if ($members | is-empty) { let features = ($cargo | get -o features | default {}) if ($features | columns | is-empty) { return [] } $features | transpose name deps | each { |f| { crate: ($cargo | get -o package.name | default ($root | path basename)), feature: $f.name, enables: ($f.deps | str join ", "), }} } else { mut result = [] for member in $members { let expanded = (glob $"($root)/($member)/Cargo.toml") for ct in $expanded { let c = (open $ct) let cname = ($c | get -o package.name | default ($ct | path dirname | path basename)) let features = ($c | get -o features | default {}) if ($features | columns | is-not-empty) { let feats = ($features | transpose name deps | each { |f| { crate: $cname, feature: $f.name, enables: ($f.deps | str join ", "), }}) $result = ($result | append $feats) } } } $result } } def collect-crate-deps [root: string, feature_id: string]: nothing -> list { let cargo_toml = $"($root)/Cargo.toml" if not ($cargo_toml | path exists) { return [] } let cargo = (open $cargo_toml) let members = ($cargo | get -o workspace.members | default []) # Try to find a crate whose name matches or contains the feature_id let id_parts = ($feature_id | split row "-") mut found_deps = [] let tomls = if ($members | is-empty) { [$cargo_toml] } else { mut paths = [] for member in $members { let expanded = (glob $"($root)/($member)/Cargo.toml") $paths = ($paths | append $expanded) } $paths } for ct in $tomls { let c = (open $ct) let cname = ($c | get -o package.name | default ($ct | path dirname | path basename)) let cname_parts = ($cname | split row "-") # Match if feature_id contains crate name or vice versa let is_match = ($cname | str contains $feature_id) or ($feature_id | str contains $cname) or (($id_parts | where { |p| $p in $cname_parts }) | is-not-empty) if $is_match { let deps = ($c | get -o dependencies | default {}) $found_deps = ($found_deps | append ($deps | transpose name spec | each { |d| let version = if ($d.spec | describe | str starts-with "string") { $d.spec } else { $d.spec | get -o version | default "" } { crate: $cname, dep: $d.name, version: $version } })) } } $found_deps } # ── Loaders ───────────────────────────────────────────────────────────────────── def load-ontology-safe [root: string]: nothing -> record { let core = $"($root)/.ontology/core.ncl" if not ($core | path exists) { return {} } let ip = (nickel-import-path $root) daemon-export-safe $core --import-path $ip | default {} } def load-manifest-safe [root: string]: nothing -> record { let manifest = $"($root)/.ontology/manifest.ncl" if not ($manifest | path exists) { return {} } let ip = (nickel-import-path $root) daemon-export-safe $manifest --import-path $ip | default {} } def load-all-adrs [root: string]: nothing -> list { let files = (glob $"($root)/adrs/adr-*.ncl") let ip = (nickel-import-path $root) $files | each { |f| daemon-export-safe $f --import-path $ip } | compact } def list-ontology-extensions [root: string]: nothing -> list { let dir = $"($root)/.ontology" let core = ["core.ncl", "state.ncl", "gate.ncl"] glob ($dir | path join "*.ncl") | each { |f| $f | path basename } | where { |f| $f not-in $core } | each { |f| $f | str replace ".ncl" "" } | sort } def load-ontology-extension [root: string, stem: string]: nothing -> any { let file = $"($root)/.ontology/($stem).ncl" if not ($file | path exists) { return null } let ip = (nickel-import-path $root) daemon-export-safe $file --import-path $ip } # ── Impact tracer ─────────────────────────────────────────────────────────────── def trace-impacts [ node_id: string, edges: list, nodes: list, max_depth: int, ]: nothing -> list { # Depth-1: direct neighbors let depth1 = (find-neighbors $node_id $edges $nodes [$node_id] 1) if $max_depth < 2 or ($depth1 | is-empty) { return ($depth1 | sort-by depth id) } # Depth-2: neighbors of depth-1 nodes let visited_after_1 = ([$node_id] | append ($depth1 | get id)) let depth2 = ($depth1 | each { |d1| find-neighbors $d1.id $edges $nodes $visited_after_1 2 } | flatten) if $max_depth < 3 or ($depth2 | is-empty) { return ($depth1 | append $depth2 | sort-by depth id) } # Depth-3: neighbors of depth-2 nodes let visited_after_2 = ($visited_after_1 | append ($depth2 | get id)) let depth3 = ($depth2 | each { |d2| find-neighbors $d2.id $edges $nodes $visited_after_2 3 } | flatten) $depth1 | append $depth2 | append $depth3 | sort-by depth id } def find-neighbors [ current: string, edges: list, nodes: list, visited: list, depth: int, ]: nothing -> list { let outgoing = ($edges | where from == $current) let incoming = ($edges | where to == $current) let candidates = ($outgoing | each { |e| { target: $e.to, edge_type: $e.kind, direction: "outgoing" } }) | append ($incoming | each { |e| { target: $e.from, edge_type: $e.kind, direction: "incoming" } }) $candidates | where { |n| not ($n.target in $visited) } | each { |n| let target_node = ($nodes | where id == $n.target) let label = if ($target_node | is-not-empty) { ($target_node | first).name } else { $n.target } { id: $n.target, name: $label, edge_type: $n.edge_type, direction: $n.direction, depth: $depth, } } } # ── Renderers ─────────────────────────────────────────────────────────────────── def render-project-text [data: record, actor: string, root: string]: nothing -> nothing { let name = $data.identity.name print "" print $"($name)" print "══════════════════════════════════════════════════════════════════" if ($data.identity.description | is-not-empty) { print $data.identity.description print "" } if ($data.identity.kind | is-not-empty) { print $"Kind: ($data.identity.kind)" } # Systems present let systems = [ (if $data.identity.has_ontology { "ontology" } else { null }), (if $data.identity.has_adrs { "ADRs" } else { null }), (if $data.identity.has_reflection { "reflection" } else { null }), (if $data.identity.has_manifest { "manifest" } else { null }), (if $data.identity.has_coder { ".coder" } else { null }), ] | compact print $"Systems: ($systems | str join ', ')" print "" if ($data.axioms | is-not-empty) { print "INVARIANTS (non-negotiable)" print "──────────────────────────────────────────────────────────────────" for ax in $data.axioms { print $" ■ ($ax.name)" print $" ($ax.description)" } print "" } if $actor == "auditor" or $actor == "developer" { if ($data.tensions | is-not-empty) { print "TENSIONS (active trade-offs)" print "──────────────────────────────────────────────────────────────────" for t in $data.tensions { print $" ⇄ ($t.name)" print $" ($t.description)" } print "" } } if ($data.dimensions | is-not-empty) { print "STATE DIMENSIONS" print "──────────────────────────────────────────────────────────────────" for d in $data.dimensions { let marker = if $d.reached { "●" } else { "○" } print $" ($marker) ($d.name): ($d.current_state) → ($d.desired_state)" } print "" } if ($data.gates | is-not-empty) { print "ACTIVE GATES (protected boundaries)" print "──────────────────────────────────────────────────────────────────" for g in $data.gates { print $" ⊘ ($g.name) [permeability: ($g.permeability)]" if ($g.accepts | is-not-empty) { print $" Accepts: ($g.accepts | str join ', ')" } } print "" } if ($data.adrs | is-not-empty) { print "ARCHITECTURAL DECISIONS" print "──────────────────────────────────────────────────────────────────" for adr in $data.adrs { let status_marker = match $adr.status { "Accepted" => "✓", "Proposed" => "?", _ => "×" } print $" ($status_marker) ($adr.id): ($adr.title) [($adr.constraint_count) constraints]" } print "" } if ($data.practices | is-not-empty) and ($actor != "ci") { print "PRACTICES & SYSTEMS" print "──────────────────────────────────────────────────────────────────" for p in $data.practices { print $" ◆ ($p.name)" if ($p.artifact_paths | is-not-empty) { print $" Artifacts: ($p.artifact_paths | str join ', ')" } } print "" } } def render-capabilities-text [data: record, actor: string, root: string]: nothing -> nothing { print "" print "CAPABILITIES" print "══════════════════════════════════════════════════════════════════" let flags = ($data.project_flags? | default {}) if ($flags | is-not-empty) { print "" print "PROJECT FLAGS" print "──────────────────────────────────────────────────────────────────" let flag_pairs = [ ["has_rust", "Rust"], ["has_ui", "UI (templates/assets)"], ["has_mdbook", "mdBook (docs/SUMMARY.md)"], ["has_nats", "NATS events"], ["has_precommit", "pre-commit hooks"], ["has_backlog", "backlog (reflection/backlog.ncl)"], ["has_git_remote", "git remote"], ] for pair in $flag_pairs { let key = ($pair | first) let label = ($pair | last) let val = ($flags | get -o $key | default false) let mark = if $val { "✓" } else { "○" } print $" ($mark) ($label)" } let crates = ($flags.crates? | default []) if ($crates | is-not-empty) { print $" Crates: ($crates | str join ', ')" } if ($data.backlog_pending? | default 0) > 0 { print $" Backlog pending: ($data.backlog_pending)" } let open_prs = ($flags.open_prs? | default 0) if $open_prs > 0 { print $" Open PRs: ($open_prs) [($flags.git_slug? | default '')]" } } if ($data.just_recipes? | default [] | is-not-empty) { print "" print "JUST RECIPES (by category)" print "──────────────────────────────────────────────────────────────────" let by_cat = ($data.just_recipes | group-by category) for cat in ($by_cat | columns | sort) { let recipes = ($by_cat | get $cat) print $" [($cat)]" for r in $recipes { let desc = if ($r.description | is-not-empty) { $" — ($r.description)" } else { "" } print $" ($r.name)($desc)" } } } if ($data.just_modules | is-not-empty) { print "" print "JUST MODULES" print "──────────────────────────────────────────────────────────────────" for m in $data.just_modules { if ($m.status? | default "") == "no_justfile" { print " (no root justfile found)" } else { let prefix = if ($m.type? | default "") == "mod" { "mod" } else if ($m.type? | default "") == "import" { "import" } else { "file" } print $" [($prefix)] ($m.name)" } } } if ($data.manifest_modes | is-not-empty) { print "" print "PROJECT MODES (manifest)" print "──────────────────────────────────────────────────────────────────" for m in $data.manifest_modes { print $" ./onref manifest mode ($m.id)" if ($m.description | is-not-empty) { print $" ($m.description)" } } } if ($data.reflection_modes | is-not-empty) { print "" print "OPERATIONAL MODES (reflection)" print "──────────────────────────────────────────────────────────────────" for m in $data.reflection_modes { let src = if $m.source == "project" { " [local]" } else { "" } print $" ./onref mode ($m.id)($src)" if ($m.trigger | is-not-empty) { print $" ($m.trigger)" } } } if $actor == "agent" { print "" print "ONTOREF COMMANDS" print "──────────────────────────────────────────────────────────────────" for cmd in $data.ontoref_commands { print $" ./onref ($cmd)" } } if ($data.claude_capabilities.present? | default false) { let cc = $data.claude_capabilities print "" print ".CLAUDE CAPABILITIES" print "──────────────────────────────────────────────────────────────────" print $" CLAUDE.md: (if $cc.has_claude_md { 'yes' } else { 'no' })" if ($cc.guidelines | is-not-empty) { print $" Guidelines: ($cc.guidelines | str join ', ')" } print $" Commands: ($cc.commands_count)" if ($cc.skills | is-not-empty) { print $" Skills: ($cc.skills | str join ', ')" } if ($cc.agents | is-not-empty) { print $" Agents: ($cc.agents | str join ', ')" } print $" Hooks: ($cc.hooks_count)" if ($cc.profiles | is-not-empty) { print $" Profiles: ($cc.profiles | str join ', ')" } } if ($data.ci_tools | is-not-empty) and ($actor == "ci" or $actor == "agent") { print "" print "CI TOOLS" print "──────────────────────────────────────────────────────────────────" for t in ($data.ci_tools | where enabled == true) { print $" ✓ ($t.name) [($t.install_method)]" } let disabled = ($data.ci_tools | where enabled == false) if ($disabled | is-not-empty) { for t in $disabled { print $" ○ ($t.name) [disabled]" } } } if ($data.manifest_capabilities? | default [] | is-not-empty) { print "" print "PROJECT CAPABILITIES (manifest)" print "──────────────────────────────────────────────────────────────────" for c in ($data.manifest_capabilities? | default []) { print $" ($c.name): ($c.summary)" if ($c.artifacts? | default [] | is-not-empty) { print $" Artifacts: ($c.artifacts | str join ', ')" } } } print "" } def render-constraints-text [data: record, actor: string]: nothing -> nothing { print "" print "CONSTRAINTS" print "══════════════════════════════════════════════════════════════════" if ($data.invariants | is-not-empty) { print "" print "INVARIANTS (cannot be violated)" print "──────────────────────────────────────────────────────────────────" for ax in $data.invariants { print $" ■ ($ax.name)" print $" ($ax.description)" } } if ($data.hard_constraints | is-not-empty) { print "" print "HARD CONSTRAINTS (from Accepted ADRs)" print "──────────────────────────────────────────────────────────────────" for c in $data.hard_constraints { print $" [($c.adr_id)] ($c.description)" if ($c.check_hint | is-not-empty) { print $" Check: ($c.check_hint)" } } } if ($data.active_gates | is-not-empty) { print "" print "ACTIVE GATES" print "──────────────────────────────────────────────────────────────────" for g in $data.active_gates { print $" ⊘ ($g.name) [($g.permeability)]" if ($g.accepts | is-not-empty) { print $" Only accepts: ($g.accepts | str join ', ')" } } } print "" } def render-tools-text [data: record, actor: string, root: string]: nothing -> nothing { print "" print "TOOLS" print "══════════════════════════════════════════════════════════════════" if ($data.dev_tools | is-not-empty) { print "" print "DEV TOOLS (detected from config files)" print "──────────────────────────────────────────────────────────────────" for t in $data.dev_tools { print $" ($t.tool) — ($t.purpose)" print $" Config: ($t.config)" } } if ($data.ci_tools | is-not-empty) { print "" print "CI TOOLS (from .typedialog/ci/config.ncl)" print "──────────────────────────────────────────────────────────────────" for t in ($data.ci_tools | where enabled == true) { print $" ✓ ($t.name) via ($t.install_method)" } } if ($data.just_recipes | is-not-empty) { print "" let relevant = match $actor { "ci" => { $data.just_recipes | where { |r| ($r.name | str starts-with "ci") or ($r.name | str contains "::ci") } }, "developer" => { $data.just_recipes | where { |r| ($r.name | str starts-with "dev") or ($r.name | str starts-with "build") or ($r.name | str starts-with "test") or ($r.name | str contains "::dev") or ($r.name | str contains "::build") or ($r.name | str contains "::test") } }, _ => { $data.just_recipes }, } if ($relevant | is-not-empty) { let actor_label = $"JUST RECIPES [($actor)]" print $actor_label print "──────────────────────────────────────────────────────────────────" for r in $relevant { if ($r.description | is-not-empty) { print $" just ($r.name) # ($r.description)" } else { print $" just ($r.name)" } } } else { print "JUST RECIPES (all)" print "──────────────────────────────────────────────────────────────────" for r in $data.just_recipes { print $" just ($r.name)" } } } print "" } def render-impact-text [data: record]: nothing -> nothing { let src = $data.source print "" print $"IMPACT ANALYSIS: ($src.name? | default $src.id)" print "══════════════════════════════════════════════════════════════════" if ($src.description? | is-not-empty) { print $src.description } if ($src.artifact_paths? | default [] | is-not-empty) { print $"Artifacts: ($src.artifact_paths | str join ', ')" } print "" if ($data.impacts | is-not-empty) { print "AFFECTED NODES" print "──────────────────────────────────────────────────────────────────" for i in $data.impacts { let arrow = if $i.direction == "outgoing" { "→" } else { "←" } let indent = (0..<($i.depth) | each { " " } | str join "") print $"($indent)($arrow) ($i.name) [($i.edge_type)] depth=($i.depth)" } } else { print " No connected nodes found." } print "" } def render-why-text [data: record, id: string]: nothing -> nothing { print "" print $"WHY: ($id)" print "══════════════════════════════════════════════════════════════════" if ($data.node != null) { let n = $data.node print "" print $"Ontology node: ($n.name? | default $n.id)" print $"Level: ($n.level? | default 'unknown')" print $"Pole: ($n.pole? | default 'unknown')" print $"Invariant: ($n.invariant? | default false)" print "" print ($n.description? | default "") print "" if ($n.artifact_paths? | default [] | is-not-empty) { print $"Artifacts: ($n.artifact_paths | str join ', ')" print "" } } if ($data.adr != null) { let a = $data.adr print "DECISION (ADR)" print "──────────────────────────────────────────────────────────────────" print $" ($a.id? | default ''): ($a.titulo? | default ($a.title? | default ''))" print $" Status: ($a.status? | default '')" if ($a.contexto? | is-not-empty) { print $" Context: ($a.contexto)" } if ($a.decision? | is-not-empty) { print $" Decision: ($a.decision)" } if ($a.rationale? | is-not-empty) { print $" Rationale: ($a.rationale)" } print "" let constraints = ($a.constraints? | default []) if ($constraints | is-not-empty) { print " Constraints:" for c in $constraints { let sev = ($c.severity? | default "") let desc = ($c.description? | default "") print $" [($sev)] ($desc)" if ($c.check_hint? | is-not-empty) { print $" Check: ($c.check_hint)" } } } print "" } if ($data.edges_from | is-not-empty) { print "CONNECTIONS (outgoing)" print "──────────────────────────────────────────────────────────────────" for e in $data.edges_from { print $" → ($e.to) [($e.kind)]" } print "" } if ($data.edges_to | is-not-empty) { print "CONNECTIONS (incoming)" print "──────────────────────────────────────────────────────────────────" for e in $data.edges_to { print $" ← ($e.from) [($e.kind)]" } print "" } if ($data.node == null) and ($data.adr == null) { print $" No ontology node or ADR found with id '($id)'." print "" } } # ── Feature renderers ──────────────────────────────────────────────────────── def render-features-list-text [data: record, root: string]: nothing -> nothing { let name = ($root | path basename) print "" print $"FEATURES — ($name)" print "══════════════════════════════════════════════════════════════════" if ($data.features | is-not-empty) { print "" for f in $data.features { let art_count = $f.artifacts let art_label = if $art_count > 0 { $" ($art_count) artifacts" } else { "" } let level_val_label = $"[($f.level)]" print $" ◆ ($f.id) ($level_val_label)($art_label)" print $" ($f.name)" if ($f.description | is-not-empty) { # Truncate to first sentence for list view let first_sentence = ($f.description | split row "." | first) let desc = if ($first_sentence | str length) > 100 { $"($first_sentence | str substring 0..99)…" } else { $first_sentence } print $" ($desc)" } } } else { print " No ontology features found." } if ($data.cargo_features | is-not-empty) { print "" print "CARGO FEATURES (compile-time)" print "──────────────────────────────────────────────────────────────────" let grouped = ($data.cargo_features | group-by crate) for group in ($grouped | transpose crate_name feats) { print $" ($group.crate_name)" for cf in $group.feats { let enables = if ($cf.enables | is-not-empty) { $" → ($cf.enables)" } else { "" } print $" · ($cf.feature)($enables)" } } } print "" print $" Use 'describe features ' for details on a specific feature." print "" } def render-feature-detail-text [data: record, root: string]: nothing -> nothing { print "" print $"($data.name)" print "══════════════════════════════════════════════════════════════════" print $" id: ($data.id)" print $" level: ($data.level)" print $" pole: ($data.pole)" let inv_label = if $data.invariant { "yes" } else { "no" } print $" invariant: ($inv_label)" print "" if ($data.description | is-not-empty) { print $data.description print "" } if ($data.artifacts | is-not-empty) { print "ARTIFACTS" print "──────────────────────────────────────────────────────────────────" for a in $data.artifacts { let marker = if $a.exists { "✓" } else { "✗" } print $" ($marker) ($a.path)" } print "" } if ($data.depends_on | is-not-empty) { print "DEPENDS ON (outgoing edges)" print "──────────────────────────────────────────────────────────────────" for d in $data.depends_on { print $" → ($d.name) [($d.relation)]" } print "" } if ($data.depended_by | is-not-empty) { print "DEPENDED BY (incoming edges)" print "──────────────────────────────────────────────────────────────────" for d in $data.depended_by { print $" ← ($d.name) [($d.relation)]" } print "" } if ($data.dimensions | is-not-empty) { print "RELATED STATE DIMENSIONS" print "──────────────────────────────────────────────────────────────────" for d in $data.dimensions { let marker = if $d.reached { "●" } else { "○" } print $" ($marker) ($d.id): ($d.current_state) → ($d.desired_state)" } print "" } if ($data.constraints | is-not-empty) { print "RELATED CONSTRAINTS" print "──────────────────────────────────────────────────────────────────" for c in $data.constraints { print $" [($c.adr_id)] ($c.description)" if ($c.check_hint | is-not-empty) { print $" Check: ($c.check_hint)" } } print "" } if ($data.crate_deps | is-not-empty) { print "CRATE DEPENDENCIES" print "──────────────────────────────────────────────────────────────────" let grouped = ($data.crate_deps | group-by crate) for group in ($grouped | transpose crate_name deps) { print $" ($group.crate_name)" for d in $group.deps { let ver = if ($d.version | is-not-empty) { $" ($d.version)" } else { "" } print $" ($d.dep)($ver)" } } print "" } } # ── describe connections ────────────────────────────────────────────────────── # "How does this project connect to other projects?" export def "describe connections" [ --fmt: string = "", --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" { "json" } else { "text" } let conn_path = $"($root)/.ontology/connections.ncl" if not ($conn_path | path exists) { if $f == "json" { print ({"upstream": [], "downstream": [], "peers": []} | to json) } else { print "No connections.ncl found in .ontology/" } return } let data = (daemon-export-safe $conn_path) if $data == null { if $f == "json" { print ({"upstream": [], "downstream": [], "peers": [], "error": "export failed"} | to json) } else { print "Failed to export connections.ncl" } return } emit-output $data $f {|| let up = ($data | get upstream | default []) let down = ($data | get downstream | default []) let peer = ($data | get peers | default []) print "" print $"(ansi white_bold)PROJECT CONNECTIONS(ansi reset)" print $"(ansi dark_gray)─────────────────────────────────(ansi reset)" if ($up | is-not-empty) { print $"\n(ansi cyan_bold)Upstream(ansi reset) (consumes from)" for c in $up { print $" → (ansi white)($c.project)(ansi reset) (ansi dark_gray)[($c.kind)](ansi reset) ($c.note? | default '')" } } if ($down | is-not-empty) { print $"\n(ansi cyan_bold)Downstream(ansi reset) (consumed by)" for c in $down { print $" → (ansi white)($c.project)(ansi reset) (ansi dark_gray)[($c.kind)](ansi reset) ($c.note? | default '')" } } if ($peer | is-not-empty) { print $"\n(ansi cyan_bold)Peers(ansi reset) (co-developed or sibling)" for c in $peer { print $" → (ansi white)($c.project)(ansi reset) (ansi dark_gray)[($c.kind)](ansi reset) ($c.note? | default '')" } } if ($up | is-empty) and ($down | is-empty) and ($peer | is-empty) { print " (no connections declared)" } print "" } } # Coerce any NCL value to a plain string safe for a GFM table cell. # Uses `to json` throughout — accepts any input type including nothing. def md-cell []: any -> any { let value = $in let t = ($value | describe) if ($t | str starts-with "table") or ($t | str starts-with "list") { $value | to json | str replace -ar '^\[|\]$' '' | str replace -a '"' '' | str trim } else if ($t | str starts-with "record") { $value | to json } else if $t == "nothing" { "" } else { $value | to json | str replace -ar '^"|"$' '' } } # Render one value as a markdown section body (no heading). def render-val-md [val: any]: nothing -> any { if $val == null { return "" } let t = ($val | describe) if ($t | str starts-with "table") { # Render each record as vertical key: value block, separated by --- let cols = ($val | columns) $val | each { |row| $cols | each { |c| let v = ($row | get --optional $c) let cell = if $v == null { "" } else { $v | md-cell } $"**($c)**: ($cell) " } | str join "\n" } | str join "\n\n---\n\n" } else if ($t | str starts-with "list") { if ($val | is-empty) { "_empty_" } else { # split row returns list which each can accept; avoids each on any-typed val $val | to json | str replace -ar '^\[|\]$' '' | str replace -a '"' '' | str trim | split row ", " | each { |item| $"- ($item | str trim)" } | str join "\n" } } else if ($t | str starts-with "record") { $val | columns | each { |c| let raw = ($val | get $c) let v = if $raw == null { "" } else { $raw | md-cell } $"- **($c)**: ($v)" } | str join "\n" } else { $val | to json | str replace -ar '^"|"$' '' } } # Try to render a section via a Tera template at {root}/layouts/{stem}/{section}.tera. # Returns the rendered string if the template exists, null otherwise. def render-section-tera [root: string, stem: string, section: string, val: any]: nothing -> any { let tmpl = $"($root)/layouts/($stem)/($section).tera" if not ($tmpl | path exists) { return null } let t = ($val | describe) let ctx = if ($t | str starts-with "table") or ($t | str starts-with "list") { {items: $val} } else { $val } $ctx | tera-render $tmpl } # Render an arbitrary extension record as Markdown, using Tera templates when available. def render-extension-md [data: record, stem: string, root: string]: nothing -> any { let sections = ($data | columns | each { |key| let val = ($data | get $key) let body = ( render-section-tera $root $stem $key $val | default (render-val-md $val) ) $"\n## ($key)\n\n($body)\n" }) ([$"# ($stem)"] | append $sections | str join "\n") } # List and optionally dump ontology extension files (.ontology/*.ncl beyond core/state/gate) export def "describe extensions" [ --fmt: string = "", --actor: string = "", --dump: string = "", # stem to dump (e.g. career, personal); omit to list --clip, # copy output to clipboard (dump only) ]: 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" { "json" } else { "text" } let exts = (list-ontology-extensions $root) if ($dump | is-not-empty) { let data = (load-ontology-extension $root $dump) if $data == null { if $f == "json" { print ({"error": $"extension '($dump)' not found"} | to json) } else { print $"Extension '($dump).ncl' not found in .ontology/" } return } let is_rec = ($data | describe | str starts-with "record") let wrapped = if $is_rec { $data } else { {value: $data} } match $f { "md" => { let md = (render-extension-md $wrapped $dump $root) if $clip { $md | clip } else { print $md } }, "json" => { print ($wrapped | to json) }, "yaml" => { print ($wrapped | to yaml) }, _ => { emit-output $wrapped $f {|| print "" print $"(ansi white_bold)EXTENSION: ($dump)(ansi reset)" print $"(ansi dark_gray)─────────────────────────────────(ansi reset)" for key in ($wrapped | columns) { let val = ($wrapped | get $key) let t = ($val | describe) print $"\n(ansi cyan_bold)($key)(ansi reset)" if ($t | str starts-with "list") { if ($val | is-empty) { print " (empty)" } else if (($val | first | describe) | str starts-with "record") { print ($val | table) } else { for item in $val { print $" · ($item)" } } } else if ($t | str starts-with "record") { print ($val | table) } else { print $" ($val)" } } print "" } } } return } let payload = {extensions: $exts} emit-output $payload $f {|| print "" print $"(ansi white_bold)ONTOLOGY EXTENSIONS(ansi reset)" print $"(ansi dark_gray)─────────────────────────────────(ansi reset)" if ($exts | is-empty) { print " (no extensions — only core/state/gate declared)" } else { for stem in $exts { print $" (ansi cyan)◆(ansi reset) ($stem).ncl" } print "" print $"(ansi dark_gray)Use --dump to inspect a specific extension(ansi reset)" } print "" } }