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