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).
248 lines
8.4 KiB
Text
248 lines
8.4 KiB
Text
#!/usr/bin/env nu
|
|
# reflection/modules/migrate.nu — ontoref protocol migration runner.
|
|
#
|
|
# Migrations live in $ONTOREF_ROOT/reflection/migrations/NNN-slug.ncl
|
|
# Each has: id, slug, description, check, instructions
|
|
# "Applied" = check passes. No state file — checks are the source of truth.
|
|
#
|
|
# Commands:
|
|
# migrate list — all migrations with applied/pending status
|
|
# migrate pending — only pending migrations
|
|
# migrate show <id> — instructions for a specific migration
|
|
|
|
use env.nu *
|
|
use store.nu [daemon-export-safe]
|
|
use describe.nu [nickel-import-path]
|
|
|
|
def project-root []: nothing -> string {
|
|
$env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT
|
|
}
|
|
|
|
def migrations-dir []: nothing -> string {
|
|
[$env.ONTOREF_ROOT, "reflection", "migrations"] | path join
|
|
}
|
|
|
|
# Run a check record against the given project root. Returns { passed, detail }.
|
|
def run-migration-check [check: record, root: string]: nothing -> record {
|
|
match $check.tag {
|
|
"FileExists" => {
|
|
let p = ([$root, $check.path] | path join)
|
|
let exists = ($p | path exists)
|
|
let want = ($check.present? | default true)
|
|
{
|
|
passed: ($exists == $want),
|
|
detail: (if ($exists == $want) { "ok" } else if $want { $"missing: ($p)" } else { $"unexpected: ($p)" })
|
|
}
|
|
}
|
|
"Grep" => {
|
|
let paths = ($check.paths | each { |p| [$root, $p] | path join } | where { |p| $p | path exists })
|
|
if ($paths | is-empty) {
|
|
return { passed: false, detail: "no target paths exist" }
|
|
}
|
|
let r = do { ^rg --no-heading -l $check.pattern ...$paths } | complete
|
|
let has_matches = ($r.exit_code == 0)
|
|
let must_empty = ($check.must_be_empty? | default false)
|
|
if $must_empty {
|
|
let files = ($r.stdout | str trim)
|
|
{ passed: (not $has_matches), detail: (if $has_matches { $"pattern found in: ($files)" } else { "ok" }) }
|
|
} else {
|
|
{ passed: $has_matches, detail: (if $has_matches { "ok" } else { "required pattern absent" }) }
|
|
}
|
|
}
|
|
"NuCmd" => {
|
|
# KNOWN BUG: Including $ONTOREF_ROOT/ontology (the parent ontology dir) in
|
|
# NICKEL_IMPORT_PATH causes the installed ontoref binary to fail with
|
|
# "Not a directory" inside its nu subprocess. Subdirs (ontology/schemas,
|
|
# ontology/defaults) are safe. Root cause: uninvestigated interaction between
|
|
# nickel's directory traversal and nushell's path handling in the subprocess chain.
|
|
# Fix: build the import path explicitly, omitting $ONTOREF_ROOT/ontology.
|
|
# When root is a consumer project (not the ontoref root), include root/ontology
|
|
# so consumer-local ontology defaults (e.g. defaults/manifest.ncl) are resolvable.
|
|
# Excluded when root == ONTOREF_ROOT: that specific path triggers a "Not a directory"
|
|
# bug in the nu subprocess chain (see comment above).
|
|
let project_ontology = if $root != $env.ONTOREF_ROOT { [$"($root)/ontology"] } else { [] }
|
|
let safe_ip = (
|
|
[
|
|
$"($root)/.ontology"
|
|
$"($root)/adrs"
|
|
$"($root)/.ontoref/ontology/schemas"
|
|
$"($root)/.ontoref/adrs"
|
|
$root
|
|
$"($env.ONTOREF_ROOT)/ontology/schemas"
|
|
$"($env.ONTOREF_ROOT)/adrs"
|
|
$env.ONTOREF_ROOT
|
|
]
|
|
| append $project_ontology
|
|
| where { |p| $p | path exists }
|
|
| uniq
|
|
| str join ":"
|
|
)
|
|
let r = (do {
|
|
with-env {
|
|
NICKEL_IMPORT_PATH: $safe_ip
|
|
ONTOREF_PROJECT_ROOT: $root
|
|
} { nu --no-config-file -c $check.cmd }
|
|
} | complete)
|
|
let expected = ($check.expect_exit? | default 0)
|
|
{
|
|
passed: ($r.exit_code == $expected),
|
|
detail: (if ($r.exit_code == $expected) { "ok" } else { $"exit ($r.exit_code) ≠ ($expected): ($r.stderr | str trim | str substring 0..120)" })
|
|
}
|
|
}
|
|
_ => { passed: false, detail: $"unknown check tag: ($check.tag)" }
|
|
}
|
|
}
|
|
|
|
def load-all-migrations []: nothing -> list<record> {
|
|
let dir = (migrations-dir)
|
|
if not ($dir | path exists) { return [] }
|
|
# Avoid $ONTOREF_ROOT/ontology in NICKEL_IMPORT_PATH — it triggers "Not a directory"
|
|
# in nickel's directory traversal inside the nu subprocess chain. Subdirs are safe.
|
|
# See NuCmd branch comment for root cause details.
|
|
let ip = (
|
|
[
|
|
$"($env.ONTOREF_ROOT)/.ontology"
|
|
$"($env.ONTOREF_ROOT)/adrs"
|
|
$"($env.ONTOREF_ROOT)/ontology/schemas"
|
|
$"($env.ONTOREF_ROOT)/ontology/defaults"
|
|
$env.ONTOREF_ROOT
|
|
]
|
|
| where { |p| $p | path exists }
|
|
| uniq
|
|
| str join ":"
|
|
)
|
|
glob $"($dir)/*.ncl"
|
|
| sort
|
|
| each { |f|
|
|
try {
|
|
let m = (daemon-export-safe $f --import-path $ip)
|
|
if $m != null { [$m] } else { [] }
|
|
} catch { [] }
|
|
}
|
|
| flatten
|
|
}
|
|
|
|
def migration-status [migrations: list<record>, root: string]: nothing -> list<record> {
|
|
$migrations | each { |m|
|
|
let r = (run-migration-check $m.check $root)
|
|
{
|
|
id: $m.id,
|
|
slug: $m.slug,
|
|
description: $m.description,
|
|
applied: $r.passed,
|
|
detail: $r.detail,
|
|
}
|
|
}
|
|
}
|
|
|
|
# List all protocol migrations with applied/pending status.
|
|
export def "migrate list" [
|
|
--fmt (-f): string = "",
|
|
--actor (-a): string = "",
|
|
]: nothing -> nothing {
|
|
let root = (project-root)
|
|
let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
|
|
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
|
|
|
|
let statuses = (migration-status (load-all-migrations) $root)
|
|
|
|
if $fmt == "json" {
|
|
print ($statuses | to json)
|
|
return
|
|
}
|
|
|
|
let n_applied = ($statuses | where applied == true | length)
|
|
let n_total = ($statuses | length)
|
|
print $" migrations: ($n_applied)/($n_total) applied"
|
|
print ""
|
|
for s in $statuses {
|
|
let mark = if $s.applied { "✓" } else { "○" }
|
|
print $" ($mark) ($s.id) ($s.slug)"
|
|
print $" ($s.description)"
|
|
if not $s.applied and $s.detail != "ok" {
|
|
print $" ($s.detail)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# List only pending (not yet applied) migrations.
|
|
export def "migrate pending" [
|
|
--fmt (-f): string = "",
|
|
--actor (-a): string = "",
|
|
]: nothing -> nothing {
|
|
let root = (project-root)
|
|
let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
|
|
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
|
|
|
|
let pending = (
|
|
migration-status (load-all-migrations) $root
|
|
| where applied == false
|
|
)
|
|
|
|
if $fmt == "json" {
|
|
print ($pending | to json)
|
|
return
|
|
}
|
|
|
|
if ($pending | is-empty) {
|
|
print " all migrations applied"
|
|
return
|
|
}
|
|
|
|
print $" ($pending | length) pending:"
|
|
for p in $pending {
|
|
print $" ○ ($p.id) ($p.slug) — ($p.description)"
|
|
}
|
|
}
|
|
|
|
# Show the instructions for a specific migration. Accepts id ("0001") or slug.
|
|
export def "migrate show" [
|
|
id: string,
|
|
--fmt (-f): string = "",
|
|
--actor (-a): string = "",
|
|
]: nothing -> nothing {
|
|
let root = (project-root)
|
|
let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
|
|
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
|
|
|
|
let migrations = (load-all-migrations)
|
|
let id_norm = ($id | str replace --regex '^0+' '')
|
|
let id_norm = if $id_norm == "" { "0" } else { $id_norm }
|
|
let matching = ($migrations | where { |m| $m.id == $id or $m.slug == $id or (($m.id | str replace --regex '^0+' '') == $id_norm) })
|
|
if ($matching | is-empty) {
|
|
error make { msg: $"migration '($id)' not found" }
|
|
}
|
|
let m = ($matching | first)
|
|
let r = (run-migration-check $m.check $root)
|
|
|
|
# Interpolate runtime values into instruction text
|
|
let project_name = ($root | path basename)
|
|
let instructions = ($m.instructions
|
|
| str replace --all "{project_root}" $root
|
|
| str replace --all "{project_name}" $project_name
|
|
| str replace --all "PROJECT_NAME" $project_name
|
|
| str replace --all "$ONTOREF_PROJECT_ROOT" $root
|
|
)
|
|
|
|
if $fmt == "json" {
|
|
print ({
|
|
id: $m.id,
|
|
slug: $m.slug,
|
|
description: $m.description,
|
|
applied: $r.passed,
|
|
project_root: $root,
|
|
project_name: $project_name,
|
|
instructions: $instructions,
|
|
} | to json)
|
|
return
|
|
}
|
|
|
|
let status = if $r.passed { "✓ applied" } else { "○ pending" }
|
|
print $" ($m.id) ($m.slug) — ($status)"
|
|
print $" ($m.description)"
|
|
if not $r.passed and $r.detail != "ok" {
|
|
print $" check: ($r.detail)"
|
|
}
|
|
print ""
|
|
print $instructions
|
|
}
|