ontoref-derive: #[onto_mcp_tool] attribute macro registers MCP tool unit-structs in
the catalog at link time via inventory::submit!; annotated item is emitted unchanged,
ToolBase/AsyncTool impls stay on the struct. All 34 tools migrated from manual wiring
(net +5: ontoref_list_projects, ontoref_search, ontoref_describe,
ontoref_list_ontology_extensions, ontoref_get_ontology_extension).
validate modes (ADR-018): reads level_hierarchy from workflow.ncl and checks every
.ncl mode for level declared, strategy declared, delegate chain coherent, compose
extends valid. mode resolve <id> shows which hierarchy level handles a mode and why.
--self-test generates synthetic fixtures in a temp dir for CI smoke-testing.
validate run-cargo: two-step Cargo.toml resolution — workspace layout first
(crates/<check.crate>/Cargo.toml), single-crate fallback by package name or repo
basename. Lets the same ADR constraint shape apply to workspace and single-crate repos.
ontology/schemas/manifest.ncl: registry_topology_type contract — multi-registry
coordination, push targets, participant scopes, per-namespace capability.
reflection/requirements/base.ncl: oras ≥1.2.0, cosign ≥2.0.0, sops ≥3.9.0, age
≥1.1.0, restic declared as Hard/Soft requirements with version_min, check_cmd, and
install_hint (ADR-017 toolchain surface).
ADR-019: per-file recipient routing for tenant isolation without multi-vault. Schema
additions: sops.recipient_groups + sops.recipient_rules in ontoref-project.ncl.
secrets-bootstrap generates .sops.yaml from project.ncl in declarative mode. Three
new secrets-audit checks: recipient-routing-coherent, recipient-routing-coverage,
no-multi-vault. Adoption templates: single-team/, multi-tenant/, agent-first/.
Integration templates: domain-producer/, mode-producer/, mode-consumer/.
UI: project_picker surfaces registry badge (⟳ participant) and vault badge
(⛁ vault_id · N, green=declarative / amber=legacy) per project card. Expanded panel
adds collapsible Registry section with namespace, endpoint, and push/pull capability.
manage.html gains Runtime Services card — MCP and GraphQL toggleable without restart
via HTMX POST /ui/manage/services/{service}/toggle.
describe.nu: capabilities JSON includes registry_topology and vault_state per project.
sync.nu: drift check extended to detect //! absence on newly registered crates.
qa.ncl: six entries — credential-vault-best-practice (layered data-flow diagram),
credential-vault-templates (paths A/B/C), credential-vault-troubleshooting (15 named
errors), integration-what-and-why (ADR-042 OCI federation), integration-how-to-implement,
integration-troubleshooting.
on+re: core.ncl + manifest.ncl updated to reflect OCI, MCP, and mode-hierarchy nodes.
Deleted stale presentation assets (2026-02 slides + voice notes).
494 lines
18 KiB
Text
494 lines
18 KiB
Text
# reflection/nulib/modes.nu — Mode listing, detail, rendering, and execution.
|
|
|
|
use ./shared.nu [all-mode-files, project-root]
|
|
use ./help.nu [help-group]
|
|
use ../modules/store.nu [daemon-export, daemon-export-safe]
|
|
|
|
# ── List / Show ──────────────────────────────────────────────────────────────
|
|
|
|
export def list-modes []: nothing -> list<record> {
|
|
let mode_files = (all-mode-files)
|
|
$mode_files | each { |mf|
|
|
let m = (daemon-export-safe $mf)
|
|
if $m != null {
|
|
{
|
|
id: ($m.id? | default ""),
|
|
trigger: ($m.trigger? | default ""),
|
|
steps: ($m.steps? | default [] | length),
|
|
preconditions: ($m.preconditions? | default [] | length),
|
|
}
|
|
} else { null }
|
|
} | compact
|
|
}
|
|
|
|
export def show-mode [id: string, fmt_resolved: string] {
|
|
let mode_files = (all-mode-files)
|
|
let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id })
|
|
if ($candidates | is-empty) {
|
|
let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" })
|
|
error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" }
|
|
}
|
|
let m = (daemon-export ($candidates | first))
|
|
match $fmt_resolved {
|
|
"json" => { $m | to json },
|
|
"yaml" => { $m | to yaml },
|
|
"toml" => { $m | to toml },
|
|
"table" => { $m | table --expand },
|
|
_ => { mode-to-md $m },
|
|
}
|
|
}
|
|
|
|
def mode-to-md [m: record]: nothing -> string {
|
|
mut lines = []
|
|
$lines = ($lines | append $"# ($m.id)")
|
|
$lines = ($lines | append "")
|
|
$lines = ($lines | append $"**Trigger**: ($m.trigger)")
|
|
$lines = ($lines | append "")
|
|
if ($m.preconditions? | is-not-empty) and (($m.preconditions | length) > 0) {
|
|
$lines = ($lines | append "## Preconditions")
|
|
for p in $m.preconditions { $lines = ($lines | append $"- ($p)") }
|
|
$lines = ($lines | append "")
|
|
}
|
|
$lines = ($lines | append "## Steps")
|
|
for s in $m.steps {
|
|
$lines = ($lines | append $"### ($s.id) [($s.actor)]")
|
|
$lines = ($lines | append $s.action)
|
|
if ($s.cmd? | is-not-empty) { $lines = ($lines | append $"```\n($s.cmd)\n```") }
|
|
if ($s.depends_on? | is-not-empty) and (($s.depends_on | length) > 0) {
|
|
$lines = ($lines | append $"Depends on: ($s.depends_on | each { |d| $d.step } | str join ', ')")
|
|
}
|
|
$lines = ($lines | append $"On error: ($s.on_error.strategy)")
|
|
$lines = ($lines | append "")
|
|
}
|
|
if ($m.postconditions? | is-not-empty) and (($m.postconditions | length) > 0) {
|
|
$lines = ($lines | append "## Postconditions")
|
|
for p in $m.postconditions { $lines = ($lines | append $"- ($p)") }
|
|
}
|
|
$lines | str join "\n"
|
|
}
|
|
|
|
export def run-modes-interactive [modes: list<record>] {
|
|
let ids = ($modes | get id)
|
|
let picked = ($ids | input list $"(ansi cyan_bold)Mode:(ansi reset) ")
|
|
if ($picked | is-not-empty) {
|
|
let actor = ($env.ONTOREF_ACTOR? | default "developer")
|
|
let f = if $actor == "agent" { "json" } else { "md" }
|
|
show-mode $picked $f
|
|
}
|
|
}
|
|
|
|
# ── Authorization ────────────────────────────────────────────────────────────
|
|
|
|
# Load mode_run config from .ontoref/config.ncl.
|
|
def load-mode-run-config [root: string]: nothing -> record {
|
|
let defaults = {
|
|
rules: [],
|
|
default_allow: false,
|
|
confirm_each_step: true,
|
|
}
|
|
|
|
let config_file = $"($root)/.ontoref/config.ncl"
|
|
if not ($config_file | path exists) { return $defaults }
|
|
|
|
let cfg = (daemon-export-safe $config_file)
|
|
if $cfg == null { return $defaults }
|
|
|
|
let mr = ($cfg.mode_run? | default {})
|
|
{
|
|
rules: ($mr.rules? | default $defaults.rules),
|
|
default_allow: ($mr.default_allow? | default $defaults.default_allow),
|
|
confirm_each_step: ($mr.confirm_each_step? | default $defaults.confirm_each_step),
|
|
}
|
|
}
|
|
|
|
# Load profile name from .ontoref/config.ncl.
|
|
def load-profile [root: string]: nothing -> string {
|
|
let config_file = $"($root)/.ontoref/config.ncl"
|
|
if not ($config_file | path exists) { return "unknown" }
|
|
|
|
let cfg = (daemon-export-safe $config_file)
|
|
if $cfg == null { return "unknown" }
|
|
|
|
$cfg.profile? | default "unknown"
|
|
}
|
|
|
|
# Check if a single rule's `when` clause matches the context.
|
|
# All present fields must match (AND). Missing fields = don't care.
|
|
def rule-matches [when_clause: record, context: record]: nothing -> bool {
|
|
let profile_ok = if ($when_clause.profile? | is-not-empty) {
|
|
($when_clause.profile == $context.profile)
|
|
} else { true }
|
|
|
|
let actor_ok = if ($when_clause.actor? | is-not-empty) {
|
|
($when_clause.actor == $context.actor)
|
|
} else { true }
|
|
|
|
let mode_ok = if ($when_clause.mode_id? | is-not-empty) {
|
|
($when_clause.mode_id == $context.mode_id)
|
|
} else { true }
|
|
|
|
$profile_ok and $actor_ok and $mode_ok
|
|
}
|
|
|
|
# Evaluate authorization rules. Returns { allowed: bool, reason: string }.
|
|
def authorize-mode [mode_id: string, root: string]: nothing -> record {
|
|
let mr_cfg = (load-mode-run-config $root)
|
|
let profile = (load-profile $root)
|
|
let actor = ($env.ONTOREF_ACTOR? | default "developer")
|
|
|
|
let context = { profile: $profile, actor: $actor, mode_id: $mode_id }
|
|
|
|
# First matching rule wins.
|
|
mut result = { allowed: $mr_cfg.default_allow, reason: "no matching rule — default policy" }
|
|
for rule in $mr_cfg.rules {
|
|
if (rule-matches $rule.when $context) {
|
|
let reason = if ($rule.reason? | is-not-empty) { $rule.reason } else { "matched rule" }
|
|
$result = { allowed: $rule.allow, reason: $reason }
|
|
break
|
|
}
|
|
}
|
|
$result
|
|
}
|
|
|
|
# ── Runner ───────────────────────────────────────────────────────────────────
|
|
|
|
# Load a mode by id, returning the full NCL-exported record.
|
|
def load-mode [id: string]: nothing -> record {
|
|
let mode_files = (all-mode-files)
|
|
let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id })
|
|
if ($candidates | is-empty) {
|
|
let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" })
|
|
error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" }
|
|
}
|
|
daemon-export ($candidates | first)
|
|
}
|
|
|
|
# Check if the current actor can execute a step based on the step's actor field.
|
|
def actor-can-run-step [step_actor: string]: nothing -> bool {
|
|
let current = ($env.ONTOREF_ACTOR? | default "developer")
|
|
match $step_actor {
|
|
"Both" => true,
|
|
"Human" => ($current == "developer" or $current == "admin"),
|
|
"Agent" => ($current == "agent" or $current == "ci"),
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
# Build the substitution context for a step command. Reads project slug from
|
|
# .ontoref/project.ncl when present; falls back to project_dir basename.
|
|
# Used for {project_dir} / {ontoref_dir} / {project_name} placeholder substitution
|
|
# in step cmd strings before they reach bash or nu.
|
|
def build-cmd-context []: nothing -> record {
|
|
let project_dir = ($env.ONTOREF_PROJECT_ROOT? | default ($env.ONTOREF_ROOT? | default $env.PWD))
|
|
let ontoref_dir = ($env.ONTOREF_ROOT? | default "")
|
|
let basename_fallback = ($project_dir | path basename)
|
|
let project_ncl = $"($project_dir)/.ontoref/project.ncl"
|
|
let project_name = if ($project_ncl | path exists) {
|
|
let exported = (do { ^nickel export $project_ncl } | complete)
|
|
if $exported.exit_code == 0 {
|
|
let parsed = (try { $exported.stdout | from json } catch { {} })
|
|
($parsed.slug? | default $basename_fallback)
|
|
} else { $basename_fallback }
|
|
} else { $basename_fallback }
|
|
{ project_dir: $project_dir, ontoref_dir: $ontoref_dir, project_name: $project_name }
|
|
}
|
|
|
|
# Execute a single step's command. Returns { success: bool, output: string }.
|
|
# Substitutes {project_dir} / {ontoref_dir} / {project_name} placeholders before
|
|
# dispatching to nu (when the cmd contains nu-only pipeline ops) or bash.
|
|
def exec-step-cmd [cmd: string]: nothing -> record {
|
|
let ctx = (build-cmd-context)
|
|
let resolved = (
|
|
$cmd
|
|
| str replace --all "{project_dir}" $ctx.project_dir
|
|
| str replace --all "{ontoref_dir}" $ctx.ontoref_dir
|
|
| str replace --all "{project_name}" $ctx.project_name
|
|
)
|
|
let nu_patterns = ["| from json", "| get ", "| where ", "| each ", "| select ", "| sort-by"]
|
|
let is_nu = ($nu_patterns | any { |p| $resolved | str contains $p })
|
|
let result = if $is_nu {
|
|
do { ^nu -c $resolved } | complete
|
|
} else {
|
|
do { ^bash -c $resolved } | complete
|
|
}
|
|
{
|
|
success: ($result.exit_code == 0),
|
|
output: (if $result.exit_code == 0 { $result.stdout } else { $result.stderr }),
|
|
}
|
|
}
|
|
|
|
# Print step summary before execution.
|
|
def print-step-header [step: record, index: int, total: int] {
|
|
let actor_color = match ($step.actor? | default "Both") {
|
|
"Human" => (ansi yellow),
|
|
"Agent" => (ansi magenta),
|
|
_ => (ansi cyan),
|
|
}
|
|
print $" (ansi white_bold)\(($index + 1)/($total)\)(ansi reset) (ansi cyan_bold)($step.id)(ansi reset) (ansi dark_gray)[($actor_color)($step.actor? | default 'Both')(ansi reset)(ansi dark_gray)](ansi reset)"
|
|
print $" ($step.action? | default '')"
|
|
if ($step.cmd? | is-not-empty) {
|
|
print $" (ansi dark_gray)$ ($step.cmd)(ansi reset)"
|
|
}
|
|
}
|
|
|
|
# Run a mode: authorize → preconditions → steps → postconditions.
|
|
export def run-mode [id: string, --dry-run, --yes] {
|
|
let root = (project-root)
|
|
|
|
# ── Load mode ──
|
|
let m = (load-mode $id)
|
|
|
|
# ── Authorization ──
|
|
let auth = (authorize-mode $id $root)
|
|
if not $auth.allowed {
|
|
let profile = (load-profile $root)
|
|
let actor = ($env.ONTOREF_ACTOR? | default "developer")
|
|
let steps = ($m.steps? | default [])
|
|
|
|
print ""
|
|
print $" (ansi white_bold)($m.id)(ansi reset) (ansi dark_gray)($steps | length) steps(ansi reset)"
|
|
print $" ($m.trigger? | default '')"
|
|
print ""
|
|
|
|
# Show steps with their commands so the user knows what to run manually.
|
|
if ($steps | is-not-empty) {
|
|
print $" (ansi white_bold)Steps \(manual execution\):(ansi reset)"
|
|
for step in ($steps | enumerate) {
|
|
let s = $step.item
|
|
let cmd_display = if ($s.cmd? | is-not-empty) { $"(ansi dark_gray)$ ($s.cmd)(ansi reset)" } else { $"(ansi dark_gray)— informational(ansi reset)" }
|
|
print $" (ansi dark_gray)($step.index + 1).(ansi reset) (ansi cyan)($s.id)(ansi reset) [($s.actor? | default 'Both')] ($cmd_display)"
|
|
print $" ($s.action? | default '')"
|
|
}
|
|
print ""
|
|
}
|
|
|
|
print $" (ansi red_bold)DENIED(ansi reset) Automated execution blocked."
|
|
print $" (ansi dark_gray)Reason: ($auth.reason)(ansi reset)"
|
|
print $" (ansi dark_gray)Context: profile=($profile), actor=($actor)(ansi reset)"
|
|
print $" (ansi dark_gray)Configure in .ontoref/config.ncl → mode_run.rules(ansi reset)"
|
|
print ""
|
|
return
|
|
}
|
|
let steps = ($m.steps? | default [])
|
|
let preconditions = ($m.preconditions? | default [])
|
|
let postconditions = ($m.postconditions? | default [])
|
|
let mr_cfg = (load-mode-run-config $root)
|
|
|
|
print ""
|
|
print $" (ansi green_bold)AUTHORIZED(ansi reset) ($auth.reason)"
|
|
print $" (ansi white_bold)Mode:(ansi reset) ($m.id) (ansi dark_gray)($steps | length) steps(ansi reset)"
|
|
print $" ($m.trigger? | default '')"
|
|
print ""
|
|
|
|
# ── Preconditions ──
|
|
if ($preconditions | is-not-empty) {
|
|
print $" (ansi white_bold)Preconditions(ansi reset)"
|
|
for p in $preconditions {
|
|
print $" (ansi dark_gray)●(ansi reset) ($p)"
|
|
}
|
|
print ""
|
|
}
|
|
|
|
# ── Guards (Active Partner) ──
|
|
let guards = ($m.guards? | default [])
|
|
if ($guards | is-not-empty) {
|
|
print $" (ansi white_bold)Guards(ansi reset)"
|
|
mut blocked = false
|
|
for g in $guards {
|
|
let result = (do { bash -c $g.cmd } | complete)
|
|
let severity = ($g.severity? | default "Block")
|
|
if $result.exit_code != 0 {
|
|
if $severity == "Block" {
|
|
print $" (ansi red_bold)✗ BLOCKED(ansi reset) ($g.id): ($g.reason)"
|
|
$blocked = true
|
|
} else {
|
|
print $" (ansi yellow)⚠ WARN(ansi reset) ($g.id): ($g.reason)"
|
|
}
|
|
} else {
|
|
print $" (ansi green)✓ PASS(ansi reset) ($g.id)"
|
|
}
|
|
}
|
|
print ""
|
|
if $blocked {
|
|
print $" (ansi red)Execution blocked by guard failure. Fix the issue and retry.(ansi reset)"
|
|
return
|
|
}
|
|
}
|
|
|
|
if $dry_run {
|
|
print $" (ansi yellow_bold)DRY RUN(ansi reset) — showing steps without executing"
|
|
print ""
|
|
for step in ($steps | enumerate) {
|
|
print-step-header $step.item $step.index ($steps | length)
|
|
print ""
|
|
}
|
|
if ($postconditions | is-not-empty) {
|
|
print $" (ansi white_bold)Postconditions(ansi reset)"
|
|
for p in $postconditions { print $" (ansi dark_gray)●(ansi reset) ($p)" }
|
|
}
|
|
return
|
|
}
|
|
|
|
# ── Execute steps ──
|
|
mut failed_steps = []
|
|
for step in ($steps | enumerate) {
|
|
let s = $step.item
|
|
let idx = $step.index
|
|
|
|
# Skip steps whose actor doesn't match current actor.
|
|
if not (actor-can-run-step ($s.actor? | default "Both")) {
|
|
print $" (ansi dark_gray)SKIP(ansi reset) ($s.id) (ansi dark_gray)[requires ($s.actor)](ansi reset)"
|
|
continue
|
|
}
|
|
|
|
# Check depends_on: if a dependency failed and its kind is OnSuccess, skip.
|
|
let deps = ($s.depends_on? | default [])
|
|
mut dep_blocked = false
|
|
for dep in $deps {
|
|
let dep_kind = ($dep.kind? | default "OnSuccess")
|
|
if $dep_kind == "OnSuccess" and ($failed_steps | any { |f| $f == $dep.step }) {
|
|
$dep_blocked = true
|
|
break
|
|
}
|
|
}
|
|
if $dep_blocked {
|
|
print $" (ansi yellow)BLOCKED(ansi reset) ($s.id) (ansi dark_gray)— dependency failed(ansi reset)"
|
|
continue
|
|
}
|
|
|
|
print-step-header $s $idx ($steps | length)
|
|
|
|
# No command = informational step, just display.
|
|
if ($s.cmd? | is-empty) or ($s.cmd == "") {
|
|
print $" (ansi dark_gray)no command — informational step(ansi reset)"
|
|
print ""
|
|
continue
|
|
}
|
|
|
|
# Confirm if required and not --yes.
|
|
if $mr_cfg.confirm_each_step and (not $yes) {
|
|
let answer = (input $" (ansi cyan)Execute? [y/N/q](ansi reset) " | str trim | str downcase)
|
|
if $answer == "q" {
|
|
print $" (ansi yellow)Aborted by user.(ansi reset)"
|
|
return
|
|
}
|
|
if $answer != "y" {
|
|
print $" (ansi dark_gray)skipped(ansi reset)"
|
|
print ""
|
|
continue
|
|
}
|
|
}
|
|
|
|
# Execute.
|
|
let result = (exec-step-cmd $s.cmd)
|
|
if $result.success {
|
|
print $" (ansi green)OK(ansi reset)"
|
|
if ($result.output | is-not-empty) {
|
|
let trimmed = ($result.output | str trim)
|
|
if ($trimmed | is-not-empty) {
|
|
$trimmed | lines | each { |l| print $" (ansi dark_gray)│(ansi reset) ($l)" } | ignore
|
|
}
|
|
}
|
|
} else {
|
|
let strategy = ($s.on_error? | default {} | get -o strategy | default "Stop")
|
|
$failed_steps = ($failed_steps | append $s.id)
|
|
if $strategy == "Stop" {
|
|
print $" (ansi red_bold)FAILED(ansi reset) (ansi dark_gray)— strategy: Stop(ansi reset)"
|
|
if ($result.output | is-not-empty) {
|
|
$result.output | str trim | lines | each { |l| print $" (ansi red)│(ansi reset) ($l)" } | ignore
|
|
}
|
|
print ""
|
|
print $" (ansi red)Execution halted at step ($s.id).(ansi reset)"
|
|
return
|
|
} else {
|
|
print $" (ansi yellow)FAILED(ansi reset) (ansi dark_gray)— strategy: Continue(ansi reset)"
|
|
if ($result.output | is-not-empty) {
|
|
$result.output | str trim | lines | each { |l| print $" (ansi yellow)│(ansi reset) ($l)" } | ignore
|
|
}
|
|
}
|
|
}
|
|
print ""
|
|
}
|
|
|
|
# ── Convergence check (Refinement Loop) ──
|
|
let converge = ($m.converge? | default null)
|
|
if $converge != null {
|
|
let max_iter = ($converge.max_iterations? | default 3)
|
|
let conv_strategy = ($converge.strategy? | default "RetryFailed")
|
|
let conv_cmd = ($converge.condition? | default "")
|
|
|
|
if ($conv_cmd | is-not-empty) {
|
|
mut iteration = 1
|
|
mut converged = false
|
|
|
|
# Check initial convergence after first execution
|
|
let check_result = (do { bash -c $conv_cmd } | complete)
|
|
if $check_result.exit_code == 0 {
|
|
$converged = true
|
|
}
|
|
|
|
while (not $converged) and ($iteration <= $max_iter) {
|
|
print $" (ansi cyan_bold)CONVERGE(ansi reset) iteration ($iteration)/($max_iter) — condition not met, re-executing ($conv_strategy)"
|
|
print ""
|
|
|
|
# Re-execute steps based on strategy
|
|
let retry_steps = if $conv_strategy == "RetryFailed" {
|
|
$steps | where { |s| $failed_steps | any { |f| $f == $s.id } }
|
|
} else {
|
|
$steps
|
|
}
|
|
|
|
$failed_steps = []
|
|
for step in ($retry_steps | enumerate) {
|
|
let s = $step.item
|
|
if not (actor-can-run-step ($s.actor? | default "Both")) { continue }
|
|
if ($s.cmd? | is-empty) or ($s.cmd == "") { continue }
|
|
|
|
print-step-header $s $step.index ($retry_steps | length)
|
|
let result = (exec-step-cmd $s.cmd)
|
|
if $result.success {
|
|
print $" (ansi green)OK(ansi reset)"
|
|
} else {
|
|
let strategy = ($s.on_error? | default {} | get -o strategy | default "Stop")
|
|
$failed_steps = ($failed_steps | append $s.id)
|
|
if $strategy == "Stop" {
|
|
print $" (ansi red_bold)FAILED(ansi reset) — aborting convergence"
|
|
break
|
|
} else {
|
|
print $" (ansi yellow)FAILED(ansi reset) — continuing"
|
|
}
|
|
}
|
|
print ""
|
|
}
|
|
|
|
let check_result = (do { bash -c $conv_cmd } | complete)
|
|
if $check_result.exit_code == 0 {
|
|
$converged = true
|
|
print $" (ansi green_bold)CONVERGED(ansi reset) condition met after ($iteration) iteration(s)"
|
|
}
|
|
$iteration = ($iteration + 1)
|
|
}
|
|
|
|
if not $converged {
|
|
print $" (ansi yellow_bold)NOT CONVERGED(ansi reset) condition not met after ($max_iter) iterations"
|
|
}
|
|
print ""
|
|
}
|
|
}
|
|
|
|
# ── Postconditions ──
|
|
if ($postconditions | is-not-empty) {
|
|
print $" (ansi white_bold)Postconditions(ansi reset)"
|
|
for p in $postconditions { print $" (ansi dark_gray)●(ansi reset) ($p)" }
|
|
print ""
|
|
}
|
|
|
|
let fail_count = ($failed_steps | length)
|
|
if $fail_count == 0 {
|
|
print $" (ansi green_bold)COMPLETE(ansi reset) All steps executed successfully."
|
|
} else {
|
|
let step_word = if $fail_count == 1 { "step" } else { "steps" }
|
|
print $" (ansi yellow_bold)PARTIAL(ansi reset) ($fail_count) ($step_word) failed: ($failed_steps | str join ', ')"
|
|
}
|
|
print ""
|
|
}
|