#!/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. 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 just_modules = (scan-just-modules $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 data = { just_modules: $just_modules, ontoref_commands: $ontoref_commands, reflection_modes: $modes, claude_capabilities: $claude, ci_tools: $ci_tools, manifest_modes: $manifest_modes, } emit-output $data $f { || render-capabilities-text $data $a $root } } # ── 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 --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 impacts = (trace-impacts $node_id $edges $nodes $depth) let data = { source: ($target | first), impacts: $impacts, } 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 } } } # ── 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.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 } # ── 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 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, 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", ] } 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 } # ── 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 "══════════════════════════════════════════════════════════════════" 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]" } } } 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 "" } }