ontoref/reflection/modules/migrate.nu
Jesús Pérez 472952e29b
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
feat: domain extension system, VCS abstraction, personal/provisioning domains, web subpages
Domain extension system (ADR-012): bash-layer dispatch activates repo_kind-conditional CLI
  domains. install.nu copies domains/ tree; short_alias wrappers generated (personal, prov).
  ore help and describe capabilities domain-aware.

  personal domain (PersonalOntology): career skills/talks/publications/positioning, CFP
  pipeline (Watching→Delivered), opportunities lifecycle, content pipeline, Sessionize
  integration. Daemon pages: /career, /personal.

  provisioning domain (DevWorkspace/Mixed): FSM state, next transitions, connections graph,
  gates, workspace card, capabilities, backlog. Daemon page: /provisioning.

  VCS abstraction layer (ADR-013): reflection/modules/vcs.nu — uniform jj/git API via
  filesystem detection (.jj/ vs .git/). opmode.nu and git-event.nu migrated off ^git.
  reflection/bin/jjw.nu — jj + ontoref + Radicle agent workspace lifecycle. jjw-ncl-merge.nu
  registered as jj merge tool for .ontology/ NCL conflicts. init-repo.nu for new_project mode.
  jj/rad not in ontoref requirements — belong in orchestration project manifests.

  'Framework RepoKind: ontology/schemas/manifest.ncl gains 'Framework variant; ontoref
  self-identifies as framework — no domain activates for the protocol itself.

  Web presence: personal.html and provisioning.html domain subpages. index.html gains
  "Project Types — Domain Extensions" section with type cards and subpage links. Nav
  compacted (Arch/Prov labels, solid backdrop-filter background).

  on+re: vcs-abstraction (adrs: adr-013) and agent-workspace-orchestration Practice nodes;
  21 manifest capabilities; state.ncl catalysts updated.
2026-04-07 23:08:29 +01:00

234 lines
8 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 [] }
let ip = (nickel-import-path $env.ONTOREF_ROOT)
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
}