#!/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 find" [
  term: string,            # Search term (case-insensitive substring match)
  --level: string = "",    # Filter by level: Axiom | Tension | Practice | Project
  --fmt: string = "",
]: 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" {
    let results = ($matches | each { |n| build-howto $n $nodes $edges $root })
    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 {
    render-howto ($matches | first) $nodes $edges $root
    return
  }

  find-interactive-loop $matches $nodes $edges $root $term
}

# ── 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<record> {
  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<record> {
  # 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
}

# Build full HOWTO record for a node.
def build-howto [
  n: record,
  all_nodes: list<record>,
  edges: list<record>,
  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)/*.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,
  }
}

# ── Render HOWTO (human) ──────────────────────────────────────────────────────

def render-howto [
  n: record,
  all_nodes: list<record>,
  edges: list<record>,
  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)"
    }
  }

  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)" }
  }
  print ""
}

# ── Interactive loop ──────────────────────────────────────────────────────────

def find-interactive-loop [
  matches: list<record>,
  all_nodes: list<record>,
  edges: list<record>,
  root: string,
  term: string,
] {
  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 }

    render-howto ($node_matches | first) $all_nodes $edges $root

    # Offer to jump to a related node, back to results, or quit.
    let h = (build-howto ($node_matches | first) $all_nodes $edges $root)
    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) {
        render-howto ($jumped | first) $all_nodes $edges $root
      }
    }
  }
}

# ── 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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<string> {
  [
    "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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  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<record> {
  let files = (glob $"($root)/adrs/adr-*.ncl")
  let ip = (nickel-import-path $root)
  $files | each { |f|
    daemon-export-safe $f --import-path $ip
  } | compact
}

# ── Impact tracer ───────────────────────────────────────────────────────────────

def trace-impacts [
  node_id: string,
  edges: list<record>,
  nodes: list<record>,
  max_depth: int,
]: nothing -> list<record> {
  # 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<record>,
  nodes: list<record>,
  visited: list<string>,
  depth: int,
]: nothing -> list<record> {
  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 <id>' 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 ""
  }
}
