Browse Source

commandstack 0.7.1: reload contract state guards (G-160 follow-on landed) (0.50.1)

- known_renamers and debug are now info-exists guarded like the rest of the
  module state (all_stacks, renamer_command_tokens, token_implementations) -
  a module re-source refreshes proc definitions only, no longer resetting
  them while stacks/tokens survive
- previously the reset stranded live stacks: the removal forms gate on
  known_renamers membership, so surviving records' renamers became unknown
  and removal errored
- contract documented in commandstack::help; pinned by
  commandstack_reload_preserves_state
- suite 41/41 on tclsh90 (9.0.3) + punk86 (8.6); packagepreference consumer
  suites 6/6 on both; minted modules/commandstack-0.7.1.tm
  (bootsupport/vfscommon promotion left to the next cycle, as with 0.6.0/0.7.0)

Assisted-by: harness=opencode; primary-model=openrouter/moonshotai/kimi-k3; api-location=openrouter.ai
master
Julian Noble 6 days ago
parent
commit
8ed9cc33fe
  1. 15
      CHANGELOG.md
  2. 2
      goals/archive/G-160-commandstack-hygiene.md
  3. 2
      punkproject.toml
  4. 41
      src/modules/commandstack-999999.0a1.0.tm
  5. 9
      src/modules/commandstack-buildversion.txt
  6. 2
      src/tests/modules/AGENTS.md
  7. 52
      src/tests/modules/commandstack/testsuites/commandstack/commandstack.test

15
CHANGELOG.md

@ -5,6 +5,21 @@ 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.50.1] - 2026-08-04
- commandstack 0.7.1 (G-160 follow-on, landed at user direction): the
reload contract - known_renamers and debug are now info-exists guarded
like the rest of the module state (all_stacks, renamer_command_tokens,
token_implementations), so a module re-source refreshes proc definitions
only and no longer resets them while stacks/tokens survive. Previously
the reset stranded live stacks: the removal forms gate on known_renamers
membership, so the surviving records' renamers became unknown and
removal errored. Contract documented in commandstack::help; pinned by
commandstack_reload_preserves_state (suite 41/41 on tclsh90 9.0.3 and
punk86 8.6; packagepreference consumer suites 6/6 on both). Minted as
modules/commandstack-0.7.1.tm; bootsupport/vfscommon promotion left to
the next promotion cycle (as with 0.6.0/0.7.0).
## [0.50.0] - 2026-08-04
- punk executable -e one-liner support (G-077, achieved): `<punkexe> -e

2
goals/archive/G-160-commandstack-hygiene.md

@ -181,4 +181,4 @@ 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) => 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
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 => landed 2026-08-04 (commandstack 0.7.1 - both guarded; reload contract documented in commandstack::help, pinned by commandstack_reload_preserves_state)

2
punkproject.toml

@ -1,6 +1,6 @@
[project]
name = "punkshell"
version = "0.50.0"
version = "0.50.1"
license = "BSD-2-Clause"
url = "https://www.gitea1.intx.com.au/jn/punkshell"
#packager: declared identity for published artifacts (declarative, not proof -

41
src/modules/commandstack-999999.0a1.0.tm

@ -11,6 +11,12 @@
# - 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 <cmd> <renamer>' for delegating to command as it was prior to rename
#changes:
#2026-08-04 (G-160 follow-on 2)
# - reload contract: known_renamers and debug are now info-exists guarded
# like the rest of the module state (all_stacks, renamer_command_tokens,
# token_implementations) - previously a module re-source reset both while
# the stacks/tokens survived, stranding live stacks whose renamers were
# no longer known (the removal forms gate on known_renamers membership)
#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
@ -63,12 +69,23 @@
namespace eval commandstack {
#Reload contract: ALL module state survives a re-source (only proc
#definitions refresh) - every state variable here is info-exists guarded.
#known_renamers in particular must survive with all_stacks: stack records
#reference renamer strings and the removal forms gate on known_renamers
#membership, so a reset would strand live stacks. ('variable name <val>'
#is no guard - with a value it re-assigns on every re-source.)
variable all_stacks
variable debug
set debug 0
#the strings the vendored cooperating packages actually pass as -renamer
#(reconciled 2026-08-03 - the historical defaults ::packagetrace ::packageSuppress matched no actual registration)
variable known_renamers [list packagetrace packagesuppress]
if {![info exists debug]} {
set debug 0
}
variable known_renamers
if {![info exists known_renamers]} {
#the strings the vendored cooperating packages actually pass as -renamer
#(reconciled 2026-08-03 - the historical defaults ::packagetrace ::packageSuppress matched no actual registration)
set known_renamers [list packagetrace packagesuppress]
}
if {![info exists all_stacks]} {
#don't wipe it
set all_stacks [dict create]
@ -239,6 +256,11 @@ namespace eval commandstack {
when enabled; errors always raise)
Notes
- Reload contract: a module re-source (re-source / package forget+require
during development) refreshes proc definitions only - ALL module state
survives: all_stacks, renamer_command_tokens, token_implementations,
known_renamers and debug are each info-exists guarded at load, so live
stacks never lose the state that references them.
- The renamer string defaults to the calling namespace.
- Cooperating packages are identified by a magic comment in installed proc
bodies: IMPLEMENTOR_<renamer>! (added automatically by rename_command).
@ -264,7 +286,8 @@ namespace eval commandstack {
argument. When enabled, rename_command and remove_rename
report progress and informational warnings on stderr - with
debug off (the default) they emit nothing (errors are still
raised as errors)."
raised as errors). The setting survives a module re-source
(the reload contract - see commandstack::help)."
@values -min 0 -max 1
on_off -type boolean -optional 1 -help -&
"New debug state. Omit to query the current state."
@ -936,8 +959,8 @@ namespace eval commandstack {
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.
restore still works after state loss such as known_renamers
being cleared 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).}
@ -959,8 +982,8 @@ namespace eval commandstack {
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
#a repair operation must not be gated by known_renamers (which state
#surgery may have lost while stacks survived) - register the renamers
#the stack evidences, mirroring rename_command
foreach record $stack {
set record_renamer [dict get $record renamer]

9
src/modules/commandstack-buildversion.txt

@ -1,6 +1,13 @@
0.7.0
0.7.1
#First line must be a tm version number
#all other lines are ignored.
#0.7.1 - G-160 follow-on: reload contract - known_renamers and debug are now
# info-exists guarded like the rest of the module state (all_stacks,
# renamer_command_tokens, token_implementations): a module re-source no
# longer resets them while stacks/tokens survive (previously the reset
# stranded live stacks - the removal forms gate on known_renamers
# membership). Contract documented in commandstack::help; pinned by
# commandstack_reload_preserves_state
#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

2
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 <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 + 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)
- `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 + the 0.7.1 reload-contract state guards (G-160 follow-ons, 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 reload contract (a module re-source preserves ALL state - stacks/token counters/token map/known_renamers/debug each info-exists guarded; delegation and exact-token removal keep working after reload), 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)
- `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack)
- `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)

52
src/tests/modules/commandstack/testsuites/commandstack/commandstack.test

@ -42,6 +42,11 @@
# header/code round-trip, lib::splitx, debug accessor, show_stack
# fallback rendering, Rename_stack parking, help overview text,
# and lazy punk::args registration of the PUNKARGS documentation.
# 7. The reload contract (0.7.1, the second G-160 follow-on): a module
# re-source preserves ALL state - all_stacks, renamer_command_tokens,
# token_implementations, known_renamers and debug are each info-exists
# guarded, so a reload can never strand live stacks by resetting the
# state that references them.
#
# Tests run against the SOURCE-TREE module. Behavioural tests use a fresh
# child interp per test (sourcing the module by path) so that: module state
@ -127,6 +132,8 @@ namespace eval ::testspace {
}
}
interp eval $i [list source $commandstack_src]
#the sourced module's path, for tests that re-source it (reload contract)
interp eval $i [list set ::COMMANDSTACK_SRC $commandstack_src]
interp eval $i $script
} finally {
interp delete $i
@ -950,10 +957,11 @@ namespace eval ::testspace {
} -result {{A2 {B {A {base x}}}} 3 {base x} 0 0 0 {} 0}
#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).
#loss (known_renamers lost while stacks survive - simulated here by manual
#reset; a module re-source no longer resets it, per the 0.7.1 reload
#contract pinned below) 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}\
-constraints commandstacksrc -body {
cs_probe {
@ -961,7 +969,7 @@ namespace eval ::testspace {
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
#simulate a module re-source: known_renamers resets, stacks survive
#simulate state loss: known_renamers reset while stacks survive
set ::commandstack::known_renamers {packagetrace packagesuppress}
set c1 [catch {commandstack::remove_renamer ::csA} msg1]
set c2 [catch {commandstack::pop_rename ::csA ::tgt} msg2]
@ -972,6 +980,40 @@ namespace eval ::testspace {
}
} -result {1 1 1 1 1 {base x} 1}
#added 2026-08-04 (agent, G-160) - reload contract pins (the last G-160 follow-on)
#The reload contract (0.7.1): a module re-source refreshes proc definitions
#only - ALL state survives (every state variable is info-exists guarded).
#Pre-0.7.1, known_renamers and debug reset while stacks/tokens survived,
#stranding live stacks: the removal forms gate on known_renamers, so the
#surviving records' renamers became unknown and removal errored. Pinned:
#re-source with a live stack, a non-default renamer and debug on, then a
#second rename continues the token sequence, delegation still dispatches
#through the re-sourced get_next_command, and removal by exact token works.
test commandstack_reload_preserves_state {re-sourcing the module preserves known_renamers, debug, stacks, token counters and dispatch}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::debug 1
#the module re-source itself
source $::COMMANDSTACK_SRC
set debug_after_reload [commandstack::debug]
#a second rename continues the token sequence (the counter survived)
set r2 [commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A2 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}]
list [expr {"::csA" in $::commandstack::known_renamers}] \
$debug_after_reload \
[lindex [dict get $r2 token] 2] \
[::tgt x] \
[catch {commandstack::remove_rename [dict get $r2 token]}] \
[::tgt x] \
[llength [commandstack::get_stack ::tgt]]
}
} -result {1 1 2 {A2 {A {base x}}} 0 {A {base x}} 1}
#channel discipline holds for the convenience forms: with debug off a full
#pop_rename / remove_renamer / restore_original workout emits nothing on
#stdout or stderr (the ::puts shim would have captured any emission).

Loading…
Cancel
Save