ontoref/reflection/modules/register.nu
Jesús Pérez 0396e4037b
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
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
chore: add ontology and reflection
2026-03-13 00:21:04 +00:00

249 lines
9.2 KiB
Plaintext

#!/usr/bin/env nu
# reflection/modules/register.nu — ontoref register implementation.
#
# Reads a structured change record (from register_change.ncl form output or
# piped JSON) and writes the appropriate artifacts:
# - CHANGELOG entry (always)
# - ADR hint (if needs_adr = true — tells user the /create-adr command)
# - .ontology/state.ncl patch (if changes_ontology_state = true)
# - reflection/modes/<id>.ncl stub (if affects_capability + add)
#
# Ontology patch uses sed range pattern — no mutable closures.
# All modified files are nickel-typechecked before write.
use env.nu *
use store.nu [daemon-export-safe]
export def "register run" [
--backend: string = "cli",
] {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let form_file = $"($env.ONTOREF_ROOT)/reflection/forms/register_change.ncl"
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
if $actor == "agent" {
print "Agent: pipe a JSON record matching register_change.ncl fields to 'register apply'"
print $" nickel export ($form_file) | get elements"
print " | where type != \"section_header\" | select name prompt required nickel_path"
return
}
if (which typedialog | is-empty) {
print "typedialog required for interactive register. Run: cargo install typedialog"
exit 1
}
let tmp_out = (mktemp --suffix ".json")
match $backend {
"tui" => { ^typedialog tui nickel-roundtrip $form_file $form_file --output $tmp_out }
"web" => { ^typedialog web nickel-roundtrip $form_file $form_file --output $tmp_out --open }
_ => { ^typedialog nickel-roundtrip $form_file $form_file --output $tmp_out }
}
if not ($tmp_out | path exists) {
print "register cancelled — no output written"
return
}
let data = open $tmp_out | from json
rm $tmp_out
$data | register apply
}
# Apply a structured change record. Pipe JSON or call directly.
export def "register apply" [] {
let data = $in
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
if ($data.ready? | default true) == false {
print "register: ready = false — no artifacts written"
return
}
print ""
print $"ontoref register — ($data.summary)"
print "────────────────────────────────────────────────"
# ── CHANGELOG ───────────────────────────────────────────────────────────────
let changelog = $"($root)/CHANGELOG.md"
if ($changelog | path exists) {
register-changelog $data $changelog
} else {
print $" skip : CHANGELOG.md not found at ($changelog)"
}
# ── ADR hint ─────────────────────────────────────────────────────────────────
if ($data.needs_adr? | default false) {
let title = ($data.adr_title? | default "")
if ($title | str length) > 0 {
let flag = if ($data.adr_accept_immediately? | default false) { "-a " } else { "" }
print $" adr : run in a Claude session:"
print $" /create-adr ($flag)\"($title)\""
}
}
# ── ONTOLOGY STATE ──────────────────────────────────────────────────────────
if ($data.changes_ontology_state? | default false) {
let dim_id = ($data.ontology_dimension_id? | default "")
let new_state = ($data.ontology_new_state? | default "")
if ($dim_id | str length) > 0 and ($new_state | str length) > 0 {
register-ontology-state $root $dim_id $new_state
} else {
print " skip : ontology — dimension_id or new_state missing"
}
}
# ── MODE STUB ───────────────────────────────────────────────────────────────
if ($data.affects_capability? | default false) {
let action = ($data.capability_action? | default "add")
let mode_id = ($data.capability_mode_id? | default "")
if $action == "add" and ($mode_id | str length) > 0 {
register-mode-stub $root $mode_id $data.summary
} else if $action != "add" {
print $" mode : ($action) '($mode_id)' — edit reflection/modes/($mode_id).ncl manually"
} else {
print " skip : mode — capability_mode_id missing"
}
}
# ── CONFIG SEAL ─────────────────────────────────────────────────────────────
if ($data.seals_config_profile? | default false) {
let profile = ($data.config_profile? | default "")
if ($profile | str length) > 0 {
let adr_ref = ($data.adr_title? | default "")
let note_ref = ($data.summary? | default "")
print $" config : sealing profile '($profile)' via ontoref config apply"
let entry_bin = $"($root)/onref"
do { ^$entry_bin config apply $profile --note $note_ref } | complete | ignore
} else {
print " skip : config seal — config_profile missing"
}
}
print ""
print "Done. Review written artifacts before committing."
print ""
}
# ── Internal ────────────────────────────────────────────────────────────────────
def changelog-section [change_type: string]: nothing -> string {
match $change_type {
"feature" => "Added",
"fix" => "Fixed",
"architectural" => "Changed",
"refactor" => "Changed",
"tooling" => "Changed",
"docs" => "Changed",
"config" => "Changed",
_ => "Changed",
}
}
def register-changelog [data: record, changelog: string] {
let section = (changelog-section ($data.change_type? | default "feature"))
let detail = ($data.detail? | default "")
let detail_block = if ($detail | str length) > 0 {
let indented = ($detail | str trim | lines | each { |l| $" ($l)" } | str join "\n")
$"\n($indented)"
} else {
""
}
let entry = $"- **($data.summary)**($detail_block)"
let content = open $changelog
let updated = if ($content | str contains $"### ($section)") {
$content | str replace $"### ($section)" $"### ($section)\n\n($entry)"
} else {
$content | str replace "## [Unreleased]" $"## [Unreleased]\n\n### ($section)\n\n($entry)"
}
$updated | save --force $changelog
print $" changelog: ($section) — ($data.summary)"
}
def register-ontology-state [root: string, dim_id: string, new_state: string] {
let state_file = $"($root)/.ontology/state.ncl"
if not ($state_file | path exists) {
print $" skip : ($state_file) not found"
return
}
let state_data = (daemon-export-safe $state_file)
if $state_data == null {
print $" error : cannot export ($state_file)"
return
}
let dims = ($state_data | get dimensions)
if not ($dims | any { |d| $d.id == $dim_id }) {
print $" skip : dimension '($dim_id)' not found in state.ncl"
return
}
# sed range: from line matching id = "dim_id" to the closing brace of that block,
# replace the first occurrence of current_state = "...".
# macOS sed uses '' for -i; GNU sed uses -i without arg — try macOS first.
let sed_script = $"/id.*=.*\"($dim_id)\"/,/},/ s/current_state[[:space:]]*=[[:space:]]*\"[^\"]*\"/current_state = \"($new_state)\"/"
let sed_result = do { ^sed -i '' $sed_script $state_file } | complete
let sed_result = if $sed_result.exit_code != 0 {
do { ^sed -i $sed_script $state_file } | complete
} else {
$sed_result
}
if $sed_result.exit_code != 0 {
print $" error : sed patch failed — edit ($state_file) manually"
return
}
let typecheck = do { ^nickel typecheck $state_file } | complete
if $typecheck.exit_code != 0 {
print $" error : typecheck failed after patch — reverting"
print $typecheck.stderr
do { ^git checkout -- $state_file } | complete | ignore
return
}
print $" ontology: ($dim_id) → ($new_state)"
}
def register-mode-stub [root: string, mode_id: string, summary: string] {
let modes_dir = $"($root)/reflection/modes"
let mode_file = $"($modes_dir)/($mode_id).ncl"
if ($mode_file | path exists) {
print $" skip : ($mode_id).ncl already exists — edit manually"
return
}
if not ($modes_dir | path exists) {
^mkdir -p $modes_dir
}
let header = "let s = import \"../schema.ncl\" in\n\n"
let body = $"{
id = \"($mode_id)\",
trigger = \"($summary)\",
preconditions = [],
steps = [
{
id = \"step-1\",
action = \"describe the action\",
actor = 'Both,
cmd = \"\",
verify = \"\",
on_error = { strategy = 'Stop },
},
],
postconditions = [],
} | s.Mode std.string.NonEmpty\n"
let stub = $header + $body
$stub | save --force $mode_file
print $" mode : stub written at reflection/modes/($mode_id).ncl — fill steps"
}