#!/usr/bin/env nu # reflection/modules/adr.nu — ADR management commands. # # Error handling policy: # adr list / constraints — graceful skip via do { ^nickel export } | complete # (draft ADRs may be incomplete; list must always work) # adr validate — ^nickel export | complete for fresh data (no plugin cache) # adr show — fail loud (caller passes a known valid id) use env.nu * use store.nu [daemon-export, daemon-export-safe] # Formats an ADR record as readable markdown text. def adr-to-md []: record -> string { let a = $in mut lines = [] $lines = ($lines | append $"# ($a.id): ($a.title)") $lines = ($lines | append $"") $lines = ($lines | append $"**Status**: ($a.status) **Date**: ($a.date)") $lines = ($lines | append $"") $lines = ($lines | append "## Context") $lines = ($lines | append $a.context) $lines = ($lines | append $"") $lines = ($lines | append "## Decision") $lines = ($lines | append $a.decision) $lines = ($lines | append $"") $lines = ($lines | append "## Rationale") for r in $a.rationale { $lines = ($lines | append $"- **($r.claim)**") $lines = ($lines | append $" ($r.detail)") } $lines = ($lines | append $"") $lines = ($lines | append "## Consequences") $lines = ($lines | append "**Positive**") for p in $a.consequences.positive { $lines = ($lines | append $"- ($p)") } $lines = ($lines | append "**Negative**") for n in $a.consequences.negative { $lines = ($lines | append $"- ($n)") } $lines = ($lines | append $"") $lines = ($lines | append "## Alternatives Considered") for alt in $a.alternatives_considered { $lines = ($lines | append $"- **($alt.option)**: ($alt.why_rejected)") } $lines = ($lines | append $"") $lines = ($lines | append "## Constraints") for c in $a.constraints { $lines = ($lines | append $"### ($c.id) [($c.severity)]") $lines = ($lines | append $c.claim) $lines = ($lines | append $"- Scope: ($c.scope)") $lines = ($lines | append $"- Check: `($c.check_hint)`") $lines = ($lines | append $"- Rationale: ($c.rationale)") $lines = ($lines | append $"") } let oc = $a.ontology_check $lines = ($lines | append "## Ontology Check") $lines = ($lines | append $"- Verdict: **($oc.verdict)**") if ($oc.invariants_at_risk | is-not-empty) { $lines = ($lines | append $"- Invariants at risk: ($oc.invariants_at_risk | str join ', ')") } if ($a.related_adrs | is-not-empty) { $lines = ($lines | append $"") $lines = ($lines | append $"## Related ADRs") $lines = ($lines | append ($a.related_adrs | str join ", ")) } $lines | str join "\n" } # Resolve effective format: explicit flag wins; otherwise md for humans, json for agents. def resolve-fmt [fmt: string, human_default: string]: nothing -> string { if ($fmt | is-not-empty) { return $fmt } let actor = ($env.ONTOREF_ACTOR? | default "developer") if $actor == "agent" { "json" } else { $human_default } } # Serialize or render a value in the requested format. # fmt: "md" (default readable markdown) | "table" (Nu expand) | "json" | "yaml" | "toml" def fmt-output [fmt: string]: any -> any { let data = $in match $fmt { "json" => { $data | to json }, "yaml" => { $data | to yaml }, "toml" => { # to toml requires a record at the top level — wrap lists if (($data | describe) =~ "^(list|table)") { { items: $data } | to toml } else { $data | to toml } }, "table" => { $data | table --expand }, _ => { if (($data | describe) =~ "^record") { $data | adr-to-md } else { # list/table — render as markdown table $data | to md } }, } } # Print a value — needed inside while loops where return values are discarded. def print-output [fmt: string]: any -> nothing { let data = $in match $fmt { "json" | "yaml" | "toml" => { print $data }, "table" => { print ($data | table --expand) }, _ => { if (($data | describe) =~ "^record") { print ($data | adr-to-md) } else { print ($data | to md) } }, } } export def "adr list" [ --fmt: string = "", # Output format: table (human) | json (agent) | yaml | toml ] { let f = (resolve-fmt $fmt "table") adr-files | each { |ncl| let data = (daemon-export-safe $ncl) if $data != null { $data | select id title status date } else { null } } | compact | sort-by date | fmt-output $f } export def "adr validate" [] { let hard = ( adr-files | each { |ncl| daemon-export-safe $ncl } | compact | where status == "Accepted" | get constraints | flatten | where severity == "Hard" ) let count = ($hard | length) print $"Running ($count) Hard constraints..." print "" let results = $hard | each { |c| let result = do { nu -c $c.check_hint } | complete { id: $c.id, claim: $c.claim, passed: ($result.exit_code == 0 and ($result.stdout | str trim | is-empty)), output: ($result.stdout | str trim), } } for r in $results { let icon = if $r.passed { "✓" } else { "✗" } print $"($icon) ($r.id): ($r.claim)" if not $r.passed and ($r.output | is-not-empty) { print $" Violation: ($r.output)" } } let failures = $results | where passed == false if ($failures | is-not-empty) { let n = ($failures | length) error make { msg: $"($n) Hard constraints violated" } } } export def "adr show" [ id?: string, # ADR id: "001", "adr-001", or "adr-001-slug" --interactive (-i), # Select from list interactively via typedialog --fmt: string = "", # Output format: md (human) | json (agent) | table | yaml | toml ] { let f = (resolve-fmt $fmt "md") if $interactive or ($id | is-empty) { adr-show-interactive $f } else { adr-show-by-id $id $f } } export def "constraints" [ --fmt: string = "", # Output format: table (human) | json (agent) | yaml | toml ] { let f = (resolve-fmt $fmt "table") adr-files | each { |ncl| daemon-export-safe $ncl } | compact | where status == "Accepted" | each { |a| $a.constraints | each { |c| $c | merge { adr: $a.id } } } | flatten | where severity == "Hard" | select adr id claim check_hint | fmt-output $f } export def "adr help" [] { let actor = ($env.ONTOREF_ACTOR? | default "developer") let cmd = ($env.ONTOREF_CALLER? | default "./onref") print "" print "ADR commands:" print $" ($cmd) adr list list all ADRs with status" print $" ($cmd) adr list --fmt json output as json/yaml/toml" print $" ($cmd) adr validate run Hard constraint checks" print $" ($cmd) adr show interactive ADR browser (no args)" print $" ($cmd) adr show show a specific ADR" print $" ($cmd) constraints show active Hard constraints" if $actor == "agent" { print "" print "Agent query pattern:" print " nickel-export adrs/adr-NNN-name.ncl" print " nickel-export adrs/adr-NNN-name.ncl | get constraints | where severity == 'Hard'" } print "" } # ── Internal ─────────────────────────────────────────────────────────────────── export def "adr accept" [ id: string, # adr-NNN or NNN ] { let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT) let canonical = if ($id | str starts-with "adr-") { $id } else { $"adr-($id)" } let files = glob $"($root)/adrs/($canonical)-*.ncl" if ($files | is-empty) { error make { msg: $"ADR '($id)' not found in adrs/" } } let path = ($files | first) let content = open $path if not ($content | str contains "'Proposed") { error make { msg: $"ADR '($id)' is not in Proposed status — current file does not contain \"'Proposed\"" } } $content | str replace --all "'Proposed" "'Accepted" | save --force $path print $" accepted: ($path | path basename)" do { ^nickel typecheck $path } | complete | if $in.exit_code != 0 { error make { msg: $"typecheck failed after accept:\n($in.stderr)" } } else { print " typecheck: ok" } } def adr-files [] { let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT) glob $"($root)/adrs/adr-*.ncl" } def adr-show-by-id [id: string, fmt: string] { let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT) let canonical = if ($id | str starts-with "adr-") { $id } else { $"adr-($id)" } let files = glob $"($root)/adrs/($canonical)-*.ncl" if ($files | is-empty) { error make { msg: $"ADR '($id)' not found in adrs/" } } let data = (daemon-export ($files | first)) $data | fmt-output $fmt } def adr-show-interactive [fmt: string] { let entries = ( adr-files | each { |ncl| let data = (daemon-export-safe $ncl) if $data != null { $data | select id title status } else { null } } | compact | sort-by id ) if ($entries | is-empty) { print "No ADRs found." return } let options = ($entries | each { |a| $"($a.id) ($a.title) [($a.status)]" }) let menu = ($options | append "— Exit —") mut browsing = true while $browsing { let choice = (typedialog select "Select ADR:" $menu) if $choice == "— Exit —" { $browsing = false } else { let idx = ($options | enumerate | where { |r| $r.item == $choice } | first).index let selected = ($entries | get $idx) print "" adr-show-by-id $selected.id $fmt | print-output $fmt print "" let next = (typedialog select "Continue?" ["Browse" "Exit"]) if $next == "Exit" { $browsing = false } } } }