commandstack 0.7.0: remove_rename convenience forms (G-160 follow-on landed) (0.49.8)
- pop_rename renamer ?command? - pop the renamer's topmost stack entry and
return the removed record (searched across live stacks when no command
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;
Rename_stack-parked stacks are invisible to the renamer-wide forms
- suite 40/40 on tclsh90 (9.0.3) + punk86 (8.6); packagepreference
consumer suites 6/6 on both; minted modules/commandstack-0.7.0.tm
(bootsupport/vfscommon promotion left to the next cycle, as with 0.6.0)
Assisted-by: harness=opencode; primary-model=opencode/kimi-k3; api-location=unknown
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
{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)"
@ -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 <script>`)
- `punkboot/utils/` — punkboot::utils tests (`testsuites/utils/`): the make.tcl helper module. `utils.test` (punkproject.toml/CHANGELOG version parsing), `vcsdirty.test` (dirty fossil/git provenance warnings behind the producing-commands gate - git-fixture based), and `bootlibrary.test` (G-125 boot-precondition predicate `vfs_boot_library_report`: both tcl-library conventions - `tcl_library/` for zipfs-attached kits and `lib/tcl<major>.<minor>/` for starkit-style kits - the companion-file requirement that stops the `lib/BWidget1.10.1/init.tcl` every punkshell kit carries from answering for a tcl library, near-miss reporting, missing/empty trees, and a sweep asserting every assembled `src/_bake/*.vfs` tree still passes so the gate cannot fail kits that boot today). All three are pure fixture tests - no mint or bake is run; the make.tcl side of the gate is pinned separately in `shell/testsuites/punkexe/maketclbootgate.test`
- `modpod/` — modpod (vendored zip-based .tm wrapper) tests (`testsuites/modpod/roundtrip.test`, G-111 - the module's first suite): make_zip_modpod wrap emits stub+zip (\x1A separator, PK local header), is_valid_tm_version accept/reject, and child-process load round-trips of per-run generated fixture pods - require from a real-disk module path (stub self-mounts via zipfs, or the vfs::zip fallback on 8.6), the -offsettype file form, the unwrapped #modpod-<pkg>-<ver> redirect form (extracted folder beside the .tm is sourced directly - exact path asserted, no mount signature), a binary payload (dll discovered by a child probe among packages NOT already loaded in a bare child, repo lib_tcl<N> trees offered as auto_path - no committed binaries) loading from the mounted pod in a fresh child, and the tm-residing-on-a-zipfs-path zip-in-zip form (childzipfs-gated: skips on 8.6, the recorded G-034-class limitation). Child spawn probes the kit `script` subcommand form first, then plain script-file dispatch (native tclsh)
- `commandstack/` — commandstack (cooperative command renaming) tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - characterisation suite + the G-160 hygiene-pass contract at commandstack 0.6.0; usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (token first/renamer second dict key order for the lsearch -index 1/-index 3 convention, trailing `did_rename` 0|1, `{implementation {} did_rename 0}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation + the `commandstack::next` helper (caller-context parity with the manual uplevel convention pinned), unique+monotonic per-(renamer,command) tokenids (same-renamer re-renames chain and are removable by exact token, third rename succeeds), multi-renamer stacking with removal in any order (bottom-removal re-linking), builtin renames (next_implementor `original`), remove_rename's three argument forms + unknown-renamer errors, the token->implementation map get_next_command resolves through (map/stack consistency pinned across rename/remove; parked stacks keep dispatching), channel discipline (silent full cycle with debug off; warnings only under debug), debug argument validation, -renamer misplacement errors, get_stack raw-key-first retrieval of Rename_stack-parked records + Rename_stack 1/0 returns, Delete_stack live-record guard (errors; empty/missing return 1), get_IMPLEMENTOR classification incl builtin-where-cmdtype-exists (dynamic expectation - undetermined on 8.6), lib::split_body round-trip, lib::splitx, show_stack fallback render, the help overview, and lazy punk::args registration of the PUNKARGS docs. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module output for silence/warning assertions and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing style guidance in src/tests/AGENTS.md (a hard contract until G-161 made the parser multi-line tolerant). Green on tclsh90 (9.0.3) and punk86 (8.6)
- `commandstack/` — commandstack (cooperative command renaming) tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - characterisation suite + the G-160 hygiene-pass contract at commandstack 0.6.0 + the 0.7.0 convenience removal forms (G-160 follow-on, 2026-08-04); usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (token first/renamer second dict key order for the lsearch -index 1/-index 3 convention, trailing `did_rename` 0|1, `{implementation {} did_rename 0}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation + the `commandstack::next` helper (caller-context parity with the manual uplevel convention pinned), unique+monotonic per-(renamer,command) tokenids (same-renamer re-renames chain and are removable by exact token, third rename succeeds), multi-renamer stacking with removal in any order (bottom-removal re-linking), builtin renames (next_implementor `original`), remove_rename's three argument forms + unknown-renamer errors, the 0.7.0 convenience removal forms (pop_rename - command form pops topmost-for-renamer and returns the removed record, bare form searches live stacks with a multi-command ambiguity error; remove_renamer - all of a renamer's entries across live stacks with correct re-linking, Rename_stack-parked stacks skipped, removed records returned keyed by command; restore_original - whole-stack unwind to the original returning the record count, deliberately registering stack-evidenced renamers so it survives a known_renamers reset while the renamer-explicit forms keep the gate; all silent with debug off), the token->implementation map get_next_command resolves through (map/stack consistency pinned across rename/remove; parked stacks keep dispatching), channel discipline (silent full cycle with debug off; warnings only under debug), debug argument validation, -renamer misplacement errors, get_stack raw-key-first retrieval of Rename_stack-parked records + Rename_stack 1/0 returns, Delete_stack live-record guard (errors; empty/missing return 1), get_IMPLEMENTOR classification incl builtin-where-cmdtype-exists (dynamic expectation - undetermined on 8.6), lib::split_body round-trip, lib::splitx, show_stack fallback render, the help overview, and lazy punk::args registration of the PUNKARGS docs. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module output for silence/warning assertions and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing style guidance in src/tests/AGENTS.md (a hard contract until G-161 made the parser multi-line tolerant). Green on tclsh90 (9.0.3) and punk86 (8.6)
- `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify), grepstr characterization (`grepstr.test`: return modes incl summarydict (linemap pinned as always-present - the -help says -n-only, reconciliation deferred to the planned hygiene pass), exact highlight SGR wrapping, -n line numbering, invert + empty-highlight strip, -C context/breaks, capture groups, and the tab deficiency: warns once per call on stderr, single-pass tab line survives - the multi-pass mangling is pinned at consumer level in punk/ns corp.test), and untabify characterization (`untabify.test`: -stops int/list/terminal, -with spaces/unicode/custom-pair, multiline, errors, plus the EXPERIMENTAL -plastic elastic-tabstop mode deliberately pinned-as-interim and retained for possible repl editbuf use). Console queries (get_tabstops/get_size + punk::console::tabwidth) are mocked per the overtype renderline.test pattern - they emit live terminal queries that block/error headless. ANSI codes in these tests are literal escape strings so results are colour-state independent
- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`<type>` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus GAP pins for last-defined-member default precedence, cross-member -multiple value loss, parsekey/optname collision conflation, and values/leaders parsekey breakage - desired-behaviour pins disabled behind punkargsKnownBug in `testsuites/dev/parsekey-knownbugs.test`), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure)
#pop_rename without a command searches the live stacks: the pop lands on the
#single command holding the renamer's entries; a renamer with entries on
#MULTIPLE commands is an ambiguity error naming the commands; a known renamer
#with no entries returns the empty string; an unknown renamer errors.
test commandstack_pop_rename_search_and_ambiguity {pop_rename without command searches live stacks, errors on multi-command ambiguity, empty for no entries}\
#restore_original is deliberately NOT gated by known_renamers: after state
#loss (a module re-source resetting known_renamers while stacks survive -
#the open follow-on-2 scenario) it registers the stack-evidenced renamers
#and still restores. The renamer-explicit convenience forms keep the gate
#and error in that state (matching remove_rename).
test commandstack_restore_original_survives_known_renamers_reset {restore_original registers stack-evidenced renamers after a known_renamers reset; explicit forms keep the gate}\