199 lines
7.5 KiB
Plaintext
Raw Permalink Normal View History

#!/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<mode: string, since: string> {
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" }
},
}
}