diff --git a/CHANGELOG.md b/CHANGELOG.md index 980231a7..5ea6a9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.49.8] - 2026-08-04 + +- commandstack 0.7.0 (G-160 follow-on, landed at user direction): the + remove_rename convenience forms parked as code todos since 2021 - + pop_rename (pop a renamer's topmost stack entry, per command or + searched across live stacks), remove_renamer (remove ALL of a + renamer's entries across every live stack - the unload-my-package + form), and restore_original (unwind a command's whole stack to the + original implementation regardless of renamer; deliberately not gated + by known_renamers so it survives state loss such as a module + re-source). All three route through remove_rename's re-linking/token + map machinery; Rename_stack-parked stacks are invisible to the + renamer-wide forms. commandstack suite 40/40 on tclsh90 (9.0.3) and + punk86 (8.6); packagepreference consumer suites 6/6 on both. Minted + as modules/commandstack-0.7.0.tm; bootsupport/vfscommon promotion + left to the next promotion cycle (as with 0.6.0). + ## [0.49.7] - 2026-08-04 - punk::tcltestrun 0.4.1 (G-161 follow-on, landed at user direction): diff --git a/goals/archive/G-160-commandstack-hygiene.md b/goals/archive/G-160-commandstack-hygiene.md index bed6dfad..bbc97ad0 100644 --- a/goals/archive/G-160-commandstack-hygiene.md +++ b/goals/archive/G-160-commandstack-hygiene.md @@ -180,5 +180,5 @@ Verification evidence: ## Follow-ons -Follow-on: remove_rename convenience forms parked as code todos (pop topmost-for-renamer, remove all entries for a renamer, restore-to-original regardless of stack) => open +Follow-on: remove_rename convenience forms parked as code todos (pop topmost-for-renamer, remove all entries for a renamer, restore-to-original regardless of stack) => landed 2026-08-04 (commandstack 0.7.0 - pop_rename/remove_renamer/restore_original) Follow-on: a module re-source resets known_renamers and debug while all_stacks/renamer_command_tokens/token_implementations survive their info-exists guards - guard known_renamers likewise or document the reload contract => open diff --git a/punkproject.toml b/punkproject.toml index 679818c3..7521c6c0 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,6 +1,6 @@ [project] name = "punkshell" -version = "0.49.7" +version = "0.49.8" license = "BSD-2-Clause" url = "https://www.gitea1.intx.com.au/jn/punkshell" #packager: declared identity for published artifacts (declarative, not proof - diff --git a/src/modules/commandstack-999999.0a1.0.tm b/src/modules/commandstack-999999.0a1.0.tm index decc833d..9bb93b14 100644 --- a/src/modules/commandstack-999999.0a1.0.tm +++ b/src/modules/commandstack-999999.0a1.0.tm @@ -11,6 +11,16 @@ # - oo dispatch features may be a better implementation - especially for allowing undoing command renames in the middle of a stack. # - document that replacement command should use 'commandstack::get_next_command ' for delegating to command as it was prior to rename #changes: +#2026-08-04 (G-160 follow-on) +# - remove_rename convenience forms (the long-parked code todos): +# pop_rename (pop a renamer's topmost entry - per command, or searched +# across live stacks when unambiguous), remove_renamer (remove ALL of a +# renamer's entries across every live stack - the unload-my-package form), +# and restore_original (unwind a command's whole stack to the original +# implementation regardless of renamer; not gated by known_renamers so it +# works after state loss such as a module re-source). All three route +# through remove_rename's re-linking/token-map machinery; stacks parked +# by Rename_stack are invisible to the renamer-wide forms. #2026-08-03 (hygiene pass - G-160) # - fix tokenid counter: was incremented on an apply-local variable so it was stuck at 1 - # tokenids are now unique and monotonic per (renamer,command), so same-renamer re-renames @@ -201,6 +211,21 @@ namespace eval commandstack { unique per (renamer, command), so any entry - not just the topmost - is removable by its exact token. + Convenience removal forms + commandstack::pop_rename ?command? + Pop the renamer's topmost entry and return the removed record - + on the given command, or searched across all live stacks when the + renamer's entries live on only one command (multi-command entries + are an ambiguity error). + commandstack::remove_renamer + Remove every entry the renamer recorded, across all live stacks - + the unload-my-package form. Returns the removed records keyed by + command. Stacks parked by Rename_stack are left untouched. + commandstack::restore_original + Unwind the command's whole stack regardless of renamer, restoring + the original implementation (a repair/reset operation - not gated + by known_renamers). Returns the number of records removed. + Inspection commandstack::get_stack ?command? - rename records (or all stacks; raw stacks-dict key tried first, @@ -653,11 +678,6 @@ namespace eval commandstack { uplevel 2 [list $COMMANDSTACKNEXT {*}$args] } - #todo - concept of 'pop' for renamer. Remove topmost entry specific to the renamer - #todo - removal by token to allow renamer to have multiple entries for one command but to remove one that is not the topmost - #todo - removal of all entries pertaining to a particular renamer - #todo - allow restore to bottom-most implementation (original) - regardless of what renamers have cooperated in the stack? - #remove by token, or by commandname if called from same context as original rename_command #If only a commandname is supplied, and there were multiple renames from the same context (same -renamer) only the topmost is removed. #A call to remove_rename with no token or renamer, and from a namespace context which didn't perform a rename will not remove anything. @@ -764,6 +784,202 @@ namespace eval commandstack { return [list] } + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::pop_rename + @cmd -name "commandstack::pop_rename" -& + -summary -& + "Pop a renamer's topmost rename-stack entry." -& + -help -& + {Removes the topmost (most recently stacked) rename entry + belonging to renamer and returns the removed record - the + LIFO undo for a package that renames as it loads and + unwinds as it unloads. With command given, the pop is + restricted to that command's stack (equivalent to + 'remove_rename [list $command $renamer]'). Without command, + every live stack is searched: when the renamer's entries + all live on one command the pop happens there; entries + spread across multiple commands are ambiguous and raise an + error naming those commands (supply command, or use + commandstack::remove_renamer to remove all of them). + Stacks parked by Rename_stack are maintenance state and are + not searched. The renamer must be known to commandstack or + an error is raised. Returns the empty string when the + renamer has no matching entry.} + @values -min 1 -max 2 + renamer -type string -help -& + "Renamer string recorded at rename time (must be known + to commandstack)." + command -type string -optional 1 -help -& + "Restrict the pop to this command's stack (resolved in + the caller's namespace context)." + }] + } + proc pop_rename {renamer {command ""}} { + variable all_stacks + variable known_renamers + variable debug + if {$renamer ni $known_renamers} { + error "(commandstack::pop_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time." + } + if {$command ne ""} { + set command [uplevel 1 [list namespace which $command]] + if {$command eq "" || ![dict exists $all_stacks $command]} { + return "" + } + set commands [list $command] + } else { + #find the live stacks holding entries for this renamer. Stacks parked + #by Rename_stack are skipped: their records keep the original token + #command, which never equals the parked key. + set commands [list] + dict for {key stack} $all_stacks { + if {![llength $stack]} { + continue + } + if {[lindex [dict get [lindex $stack 0] token] 0] ne $key} { + continue + } + if {[lsearch -index 3 $stack $renamer] > -1} { + lappend commands $key + } + } + if {[llength $commands] > 1} { + error "(commandstack::pop_rename) ERROR: renamer '$renamer' has entries on multiple commands ([join $commands {, }]) - supply the command argument, or use commandstack::remove_renamer to remove all of its entries" + } + if {![llength $commands]} { + return "" + } + } + set command [lindex $commands 0] + set stack [dict get $all_stacks $command] + set topmost [lindex [lsearch -all -index 3 $stack $renamer] end] + if {$topmost eq ""} { + return "" + } + set record [lindex $stack $topmost] + remove_rename [list $command $renamer] + if {$debug} { + puts stderr "(commandstack::pop_rename) popped [dict get $record token]" + } + return $record + } + + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::remove_renamer + @cmd -name "commandstack::remove_renamer" -& + -summary -& + "Remove every rename-stack entry belonging to a renamer." -& + -help -& + {The unload-my-package convenience: removes ALL entries + recorded for renamer across every live command stack. Each + command's entries are popped topmost-first through the same + re-linking machinery as remove_rename, so other renamers' + overrides keep delegating correctly. Stacks parked by + Rename_stack are maintenance state and are left untouched. + The renamer must be known to commandstack or an error is + raised; the renamer is NOT removed from known_renamers. + Returns a dict keyed by command name whose values are the + removed records (topmost-first); an empty dict when the + renamer had no entries.} + @values -min 1 -max 1 + renamer -type string -help -& + "Renamer string recorded at rename time (must be known + to commandstack)." + }] + } + proc remove_renamer {renamer} { + variable all_stacks + variable known_renamers + variable debug + if {$renamer ni $known_renamers} { + error "(commandstack::remove_renamer) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time." + } + set removed [dict create] + #remove_rename mutates all_stacks only under the command key it is + #given, so iterating over this snapshot of the stacks dict is safe. + #Stacks parked by Rename_stack are skipped: their records keep the + #original token command, which never equals the parked key. + dict for {command stack} $all_stacks { + if {![llength $stack]} { + continue + } + if {[lindex [dict get [lindex $stack 0] token] 0] ne $command} { + continue + } + while {[set topmost [lindex [lsearch -all -index 3 [dict get $all_stacks $command] $renamer] end]] ne ""} { + set record [lindex [dict get $all_stacks $command] $topmost] + remove_rename [list $command $renamer] + dict lappend removed $command $record + if {$debug} { + puts stderr "(commandstack::remove_renamer) removed [dict get $record token]" + } + } + } + return $removed + } + + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::restore_original + @cmd -name "commandstack::restore_original" -& + -summary -& + "Unwind a command's whole rename stack, restoring the original implementation." -& + -help -& + {Removes EVERY record on the command's live rename stack - + regardless of which renamers contributed them - and + restores the bottom-of-stack (original) implementation as + the live command. Records are removed topmost-first through + the same machinery as remove_rename. + This is the repair/reset operation: it is deliberately NOT + gated by known_renamers (unlike remove_rename) - renamers + evidenced by the stack records are registered into + known_renamers first, mirroring rename_command, so a + restore still works after state loss such as a module + re-source that reset known_renamers while stacks survived. + Returns the number of records removed: 0 when the command + has no live stack (never renamed, or only the empty residue + entry - prune that with Delete_stack).} + @values -min 1 -max 1 + command -type string -help -& + "Command name (resolved with 'namespace which' in the + caller's context)." + }] + } + proc restore_original {command} { + variable all_stacks + variable known_renamers + variable debug + set command [uplevel 1 [list namespace which $command]] + if {$command eq "" || ![dict exists $all_stacks $command]} { + return 0 + } + set stack [dict get $all_stacks $command] + if {![llength $stack]} { + return 0 + } + #a repair operation must not be gated by known_renamers (which a module + #re-source may have reset while stacks survived) - register the renamers + #the stack evidences, mirroring rename_command + foreach record $stack { + set record_renamer [dict get $record renamer] + if {$record_renamer ni $known_renamers} { + lappend known_renamers $record_renamer + } + } + set removed_count 0 + while {[llength $stack]} { + remove_rename [dict get [lindex $stack end] token] + set stack [dict get $all_stacks $command] + incr removed_count + } + if {$debug} { + puts stderr "(commandstack::restore_original) restored '$command' to its original implementation ($removed_count override(s) unwound)" + } + return $removed_count + } + namespace eval argdoc { lappend PUNKARGS [list { @id -id ::commandstack::show_stack diff --git a/src/modules/commandstack-buildversion.txt b/src/modules/commandstack-buildversion.txt index 3a035b10..6750059a 100644 --- a/src/modules/commandstack-buildversion.txt +++ b/src/modules/commandstack-buildversion.txt @@ -1,6 +1,18 @@ -0.6.0 +0.7.0 #First line must be a tm version number #all other lines are ignored. +#0.7.0 - G-160 follow-on: remove_rename convenience forms (the parked code todos): +# - pop_rename renamer ?command? - pop the renamer's topmost stack entry and +# return the removed record (searched across live stacks when no command is +# given; entries on multiple commands are an ambiguity error) +# - remove_renamer renamer - remove ALL of a renamer's entries across every +# live stack (the unload-my-package form); returns removed records keyed by +# command +# - restore_original command - unwind the whole stack to the original +# implementation regardless of renamer; registers stack-evidenced renamers +# into known_renamers so it is not gated by state loss (module re-source) +# - all three route through remove_rename's re-linking/token-map machinery; +# stacks parked by Rename_stack are invisible to the renamer-wide forms #0.6.0 - G-160 hygiene pass: # - tokenid unique+monotonic per (renamer,command) - the counter now increments the real # namespace variable (was an apply-local copy stuck at 1); same-renamer re-renames diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index 51833f1a..79c12195 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -40,7 +40,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src