#!/usr/bin/env nu # reflection/modules/opmode.nu — operational mode detection, transition, and preflight. # # Mode is auto-detected on every invocation by comparing: # desired = config.ncl mode field (what the project is configured for) # actual = runtime connectivity probe (what is reachable right now) # # .ontoref/mode.lock stores { mode, since } across invocations. # state.ncl operational-mode dimension tracks the architectural intent. # # Command levels: # local → no service checks. Never blocks on connectivity. # service → daemon optional. Degrades gracefully if unreachable. # committed → daemon + DB required. Fail-fast if unavailable. use env.nu * use store.nu [daemon-available] use ../nulib/shared.nu [project-root] # ── Internal helpers ────────────────────────────────────────────────────────── def mode-lock-path []: nothing -> string { let root = (project-root) $"($root)/.ontoref/mode.lock" } def read-lock []: nothing -> record { let p = (mode-lock-path) if not ($p | path exists) { return { mode: "unknown", since: "" } } try { open $p | from json } catch { { mode: "unknown", since: "" } } } def write-lock [mode: string]: nothing -> nothing { let p = (mode-lock-path) { mode: $mode, since: (date now | format date "%Y-%m-%dT%H:%M:%SZ") } | to json | save -f $p } def desired-mode []: nothing -> string { let root = (project-root) let cfg = $"($root)/.ontoref/config.ncl" if not ($cfg | path exists) { return "local" } let r = (do { ^nickel export --format json $cfg } | complete) if $r.exit_code != 0 { return "local" } try { $r.stdout | from json | get -o mode | default "local" | into string } catch { "local" } } def probe-actual [desired: string]: nothing -> string { if $desired == "local" { return "local" } if (daemon-available) { return "daemon" } else { return "local" } } # ── On-enter / on-exit actions ──────────────────────────────────────────────── def on-enter-daemon []: nothing -> nothing { # Push ontology projection to daemon DB let r = (do { ^nu -c $"use ($env.ONTOREF_ROOT)/reflection/modules/store.nu *; store sync-push" } | complete) if $r.exit_code == 0 { print $" (ansi green)sync(ansi reset) ontology pushed to daemon" } else { print $" (ansi yellow)sync(ansi reset) push failed — daemon up but export error" } # Update hooks to active mode (install-hooks "daemon") } def on-exit-daemon []: nothing -> nothing { (install-hooks "local") } def on-enter-local []: nothing -> nothing { (install-hooks "local") } def install-hooks [mode: string]: nothing -> nothing { let git_dir = ( do { ^git rev-parse --git-dir } | complete | if $in.exit_code == 0 { $in.stdout | str trim } else { "" } ) if ($git_dir | is-empty) { return } let hook_body = if $mode == "daemon" { $"#!/usr/bin/env bash\nnu \"($env.ONTOREF_ROOT)/reflection/hooks/git-event.nu\" \"$1\" 2>/dev/null || true\n" } else { "#!/usr/bin/env bash\n# ontoref local mode — no-op\nexit 0\n" } for hook in ["post-merge" "post-checkout"] { let path = $"($git_dir)/hooks/($hook)" $hook_body | save -f $path do { ^chmod +x $path } | complete | null } } # ── Notification helpers ────────────────────────────────────────────────────── def notify-mode-change [from: string, to: string]: nothing -> nothing { let implications = match $to { "daemon" => "Push-based sync active. Hooks will push ontology on git merge/checkout.", "local" => "File-only mode. Hooks are no-ops. DB projection may be stale.", _ => "", } print "" print $" (ansi cyan_bold)Mode changed(ansi reset): (ansi yellow)($from)(ansi reset) → (ansi green)($to)(ansi reset)" if ($implications | is-not-empty) { print $" (ansi dark_gray)($implications)(ansi reset)" } print "" # Emit NATS event if available (best-effort) let nats_script = $"($env.ONTOREF_ROOT)/reflection/modules/nats.nu" if ($nats_script | path exists) { do { ^nu -c $"use ($nats_script) *; nats-emit 'mode.changed' { from: '($from)', to: '($to)' }" } | complete | null } } # ── Public API ──────────────────────────────────────────────────────────────── # Detect actual operational mode and trigger transition if changed. # Returns { desired, actual, previous, changed }. export def "opmode detect" []: nothing -> record { let desired = (desired-mode) let actual = (probe-actual $desired) let previous = (read-lock).mode if $actual != $previous { match [$previous $actual] { ["daemon" "local"] => { on-exit-daemon }, [_ "daemon"] => { on-enter-daemon }, [_ "local"] => { on-enter-local }, } write-lock $actual if $previous != "unknown" { notify-mode-change $previous $actual } } { desired: $desired, actual: $actual, previous: $previous, changed: ($actual != $previous) } } # Show current mode status without triggering transitions. export def "opmode status" []: nothing -> nothing { let desired = (desired-mode) let lock = (read-lock) let actual = (probe-actual $desired) let drift_tag = if ($desired != $actual) { $" (ansi yellow)⚠ drift(ansi reset)" } else { "" } let since_tag = if ($lock.since | is-not-empty) { $" since ($lock.since)" } else { "" } let actual_line = $"(ansi cyan)($actual)(ansi reset)($drift_tag)" let locked_line = $"($lock.mode)($since_tag)" print "" print $" (ansi white_bold)Operational Mode(ansi reset)" print $" desired: (ansi cyan)($desired)(ansi reset)" print $" actual: ($actual_line)" print $" locked: ($locked_line)" print "" } # Pre-flight check for dispatcher. Levels: local | service | committed. # local → no-op (always passes) # service → warn if daemon unreachable, continues # committed → fail-fast if daemon or required services unreachable export def "opmode preflight" [ level: string, # local | service | committed --require-db = false, # also check DB reachability --require-nats = false, # also check NATS reachability ]: nothing -> nothing { match $level { "local" => { return }, "service" => { if not (daemon-available) { print $" (ansi yellow)WARN(ansi reset) daemon unreachable — running in degraded mode" } }, "committed" => { if not (daemon-available) { error make { msg: "daemon unreachable — this command requires an active daemon. Check ONTOREF_DAEMON_URL." } } if $require_db { let root = (project-root) let cfg_path = $"($root)/.ontoref/config.ncl" if ($cfg_path | path exists) { let r = (do { ^nickel export --format json $cfg_path } | complete) let db_enabled = if $r.exit_code == 0 { try { $r.stdout | from json | get -o db.enabled | default false } catch { false } } else { false } if not $db_enabled { error make { msg: "DB not enabled in .ontoref/config.ncl — this command requires db. Set db.enabled = true." } } } } }, _ => { error make { msg: $"unknown preflight level: ($level). Use: local | service | committed" } }, } }