#!/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/.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" }