The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup --gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
199 lines
7.5 KiB
Plaintext
199 lines
7.5 KiB
Plaintext
#!/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" }
|
|
},
|
|
}
|
|
}
|