#!/usr/bin/env nu # reflection/modules/backlog.nu — backlog management commands. use env.nu * use store.nu [daemon-export-safe] export def "backlog list" [ --status: string = "", --kind: string = "", --fmt: string = "", ] { let f = if ($fmt | is-not-empty) { $fmt } else { "table" } let items = (backlog-load) let rows = if ($status | is-not-empty) { $items | where status == $status } else if ($kind | is-not-empty) { $items | where kind == $kind } else { $items | where status != "Done" | where status != "Cancelled" } let display = $rows | select id title kind priority status | sort-by priority match $f { "json" => { $display | to json }, "md" => { $display | to md }, _ => { $display | table }, } } export def "backlog show" [id: string] { let item = (backlog-load) | where id == $id | first print "" print $"[($item.status)] ($item.id) — ($item.title)" print $"Kind: ($item.kind) Priority: ($item.priority)" if ($item.detail? | default "" | str length) > 0 { print ""; print $item.detail } let adrs = ($item.related_adrs? | default []) let modes = ($item.related_modes? | default []) if ($adrs | is-not-empty) { print $"ADRs: ($adrs | str join ', ')" } if ($modes | is-not-empty) { print $"Modes: ($modes | str join ', ')" } if ($item.related_dim? | is-not-empty) { print $"Dimension: ($item.related_dim)" } if ($item.graduates_to? | is-not-empty) { print $"Graduates to: ($item.graduates_to)" } print "" } export def "backlog add" [ title: string, --kind: string = "Todo", --priority: string = "Medium", --detail: string = "", --dim: string = "", --adr: string = "", --mode: string = "", ] { let root = (backlog-root) let file = $"($root)/reflection/backlog.ncl" if not ($file | path exists) { print $"No backlog.ncl at ($file)" return } let items = (backlog-load) let next_num = if ($items | is-empty) { 1 } else { ($items | each { |i| $i.id | str replace "bl-" "" | into int } | sort | last) + 1 } let new_id = $"bl-($next_num | fill --alignment right --width 3 --character '0')" let today = (date now | format date "%Y-%m-%d") let adrs_list = if ($adr | str length) > 0 { [$adr] } else { [] } let modes_list = if ($mode | str length) > 0 { [$mode] } else { [] } print "" print $"New backlog item: ($new_id)" print $" title = ($title)" print $" kind = ($kind)" print $" priority = ($priority)" print "" print $"Add to reflection/backlog.ncl:" print $" \{" print $" id = \"($new_id)\"," print $" title = \"($title)\"," print $" kind = '($kind)," print $" priority = '($priority)," print $" status = 'Open," print $" detail = \"($detail)\"," print $" created = \"($today)\"," print $" updated = \"($today)\"," print $" \}," } export def "backlog done" [id: string] { backlog-set-status $id "Done" } export def "backlog cancel" [id: string] { backlog-set-status $id "Cancelled" } export def "backlog promote" [id: string] { let item = (backlog-load) | where id == $id | first print "" print $"Promoting ($item.id): ($item.title)" print $"Graduates to: ($item.graduates_to? | default 'unset')" print "" let target = ($item.graduates_to? | default "") if $target == "Adr" { let flag = if ($item.priority == "Critical" or $item.priority == "High") { "-a " } else { "" } print "Run in a Claude session:" print $" /create-adr ($flag)\"($item.title)\"" } else if $target == "Mode" { let mode_id = ($item.title | str downcase | str replace --all " " "-" | str replace --all --regex '[^a-z0-9-]' "") print $" ontoref register → affects_capability=true, mode_id=($mode_id)" } else if $target == "StateTransition" { let dim = ($item.related_dim? | default "") print " ontoref register → changes_ontology_state=true" if ($dim | str length) > 0 { print $" dimension_id: ($dim)" } } else if $target == "PrItem" { print " ontoref mode create-pr" } else { print " No graduation target set. Edit backlog.ncl to add graduates_to." } print "" } export def "backlog roadmap" [] { let root = (backlog-root) let bl = (backlog-load) | where status != "Done" | where status != "Cancelled" let state_file = $"($root)/.ontology/state.ncl" let dims = if ($state_file | path exists) { let data = (daemon-export-safe $state_file) if $data != null { $data | get dimensions } else { [] } } else { [] } print "" print "═══ ROADMAP ═══════════════════════════════════════" if ($dims | is-not-empty) { print "" print "STATE DIMENSIONS" for d in $dims { if $d.current_state != $d.desired_state { print $" ($d.id) ($d.current_state) → ($d.desired_state) [($d.horizon)]" } } } for p in ["Critical", "High", "Medium", "Low"] { let p_items = $bl | where priority == $p if ($p_items | is-not-empty) { print "" print ($p | str upcase) for i in $p_items { let tag = if $i.status == "InProgress" { "[~]" } else { "[ ]" } print $" ($tag) ($i.id) ($i.title) [($i.kind)]" } } } print "" print "═══════════════════════════════════════════════════" print "" } # ── Internal ──────────────────────────────────────────────────────────────────── def backlog-root []: nothing -> string { $env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT } def backlog-load [] { let file = $"($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)/reflection/backlog.ncl" if not ($file | path exists) { return [] } let data = (daemon-export-safe $file) if $data == null { return [] } $data | get items } def backlog-set-status [id: string, new_status: string] { let file = $"($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)/reflection/backlog.ncl" let today = (date now | format date "%Y-%m-%d") let lines = (open --raw $file | lines) # Find the line index of the id field for this item let id_pattern = $"\"($id)\"" let id_line = ($lines | enumerate | where { |r| $r.item | str contains $id_pattern } | first) if ($id_line | is-empty) { print $" error: id '($id)' not found in ($file)" return } let start = $id_line.index # Scan forward from the id line to find the closing `},` of this item record. # Track brace depth: the id line is already inside the outer record (depth=1 going in). mut depth = 0 mut end_line = $start for i in ($start..($lines | length | $in - 1)) { let l = ($lines | get $i) let opens = ($l | split chars | where $it == "{" | length) let closes = ($l | split chars | where $it == "}" | length) $depth = $depth + $opens - $closes if $depth < 0 { $end_line = $i break } } # Bind end_line to immutable before closure (Nu forbids capturing mut vars in closures) let end_idx = $end_line # Patch status and updated within the located range [start, end_idx] let patched = ($lines | enumerate | each { |r| if $r.index >= $start and $r.index <= $end_idx { $r.item | str replace --regex "status[[:space:]]*=[[:space:]]*'[A-Za-z]+" $"status = '($new_status)" | str replace --regex "updated[[:space:]]*=[[:space:]]*\"[^\"]*\"" $"updated = \"($today)\"" } else { $r.item } } | str join "\n") $patched | save --force $file let tc = do { ^nickel typecheck $file } | complete if $tc.exit_code != 0 { print " error: typecheck failed — reverting" do { ^git checkout -- $file } | complete | ignore return } print $" ($id) → ($new_status)" }