Browse Source

punk::libunknown 0.2.1: zipfs tm skip-state keyed by scanned subset - fixes kit 'shell'

All tcl9 zip kits failed the 'shell' subcommand with "could not find
interpreter code": the repl codethread could not resolve root-level tm
packages (commandstack, via punk::packagepreference) after any subdir tm
(punk::args) had resolved on the same path. zipfs_tm_UnknownHandler's
static-path can_skip_update state was WRITTEN keyed by the tm-list root
(currentsearchpath) but READ keyed by the namespace-subdir searchpath
actually scanned (specificsearchpath) - so a punk/ scan poisoned the
ROOT's skip map and later root-level requires were skipped as
known-absent. Only zipfs tm paths take the skip branch, which is why
src/dev filesystem launches never showed it; kits have exactly one
(zipfs) tm path so every shell boot hit it. Independent of the epoch-copy
unsoundness fixed earlier today (reproduced with a fresh libunknown init).

Fix: both added-map writes now key by specificsearchpath, matching the
reader and libunknown::init's indexbase keying. Version bumped to 0.2.1
with history note (the file rename itself rode the previous commit via a
staged git mv; this commit carries the 0.2.1 content).

_vfscommon refreshed via make.tcl modules + vfscommonupdate + vfs (also
picks up today's punk::lib epoch-export, punk::repl collect-guard and
punk::config PUNK_FOSSIL_STORE changes); 5 kits rebuilt. Verified:
ordered probe (subdir-then-root require in a fresh thread) fails on
kit-bundled 0.2.0, passes with 0.2.1; 'shell' now reaches the repl and
evaluates on punk905_beta, punk902z and punk91; full native runtests
sweep twice at machine baseline (1037/1021/15/1 exec-14.3 only).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
98941f0c5b
  1. 25
      src/modules/punk/libunknown-0.2.1.tm
  2. 1
      src/vfs/_vfscommon.vfs/modules/punk/config-0.2.0.tm
  3. 64
      src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.0.tm
  4. 25
      src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm
  5. 63
      src/vfs/_vfscommon.vfs/modules/punk/repl-0.5.2.tm

25
src/modules/punk/libunknown-0.2.1.tm

@ -7,7 +7,7 @@
# (C) 2025
#
# @@ Meta Begin
# Application punk::libunknown 0.2.0
# Application punk::libunknown 0.2.1
# Meta platform tcl
# Meta license MIT
# @@ Meta End
@ -18,6 +18,14 @@
# from src/modules/AGENTS.md "Versioning And Releases" - bumping means
# renaming the file AND updating the Meta line above, the manpage_begin line
# below and the provide-block version, then appending a line here):
#0.2.1 - zipfs_tm_UnknownHandler: the zipfs static-path can_skip_update state is
# written keyed by the SPECIFIC searchpath actually scanned (tm path +
# package namespace subdir) to match its reader and libunknown::init's
# indexbase keying. It was written keyed by the tm-list root, so resolving
# any subdir tm (e.g punk::args) poisoned the ROOT's skip map and later
# root-level requires (e.g commandstack) were skipped as known-absent -
# breaking 'shell' on zipfs kits (single //zipfs:/app/modules tm path;
# filesystem tm paths never take the skip branch). Diagnosed 2026-07-20.
#0.2.0 - register_all_tm: deep discovery proc registering ifneeded scripts for
# .tm modules at every namespace depth (once per tm epoch, interp-local
# tm_fullscan guard); used by 'dev lib.search' by default
@ -34,7 +42,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::libunknown 0 0.2.0]
#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1]
#[copyright "2025"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
@ -355,8 +363,10 @@ tcl::namespace::eval ::punk::libunknown {
# the token-to-directory mapping may have changed.
#JMN - review.
#dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion]
dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch
#0.2.1: keyed by specificsearchpath (the subset actually scanned) - the
#can_skip_update reader and libunknown::init both key that way; keying by
#the tm-list root here let subdir scans poison root-level skip decisions.
dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch
if {$must_scan} {
#however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked
dict unset epoch tm untracked $pkgname
@ -389,8 +399,9 @@ tcl::namespace::eval ::punk::libunknown {
#JMN
#store only once for each name, although there may be multiple versions of same package within this searchpath
#dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion]
dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch
#0.2.1: keyed by specificsearchpath - see the matching note at the
#already-ifneeded branch above.
dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch
#pkgname here could be the 'name' passed at the beggning - or other .tms at the same location.
#we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations.
if {$must_scan} {
@ -2119,7 +2130,7 @@ namespace eval ::punk::args::register {
package provide punk::libunknown [tcl::namespace::eval ::punk::libunknown {
variable pkg punk::libunknown
variable version
set version 0.2.0
set version 0.2.1
}]
return

1
src/vfs/_vfscommon.vfs/modules/punk/config-0.2.0.tm

@ -275,6 +275,7 @@ tcl::namespace::eval punk::config {
PUNK_APPS {type pathlist}
PUNK_CONFIG {type string}
PUNK_CONFIGSET {type string}
PUNK_FOSSIL_STORE {type string help "location of the punkshell-owned fossil clone store\n(source mirrors for buildsuites - e.g src/buildsuites/suite_tcl90/suite.tcl).\nDefault: ~/.punkshell/fossils (machine-level cache, outside deletable _build areas).\nMissing clones are seeded read-only from ~/.fossils when present, else network-cloned."}
PUNK_SCRIPTLIB {type string}
PUNK_AUTO_EXEC_MECHANISM {type string}
PUNK_AUTO_NOEXEC {type string default 0 help "set 1 to set Tcl's ::auto_noexec true.\nStops 'unknown' from running external programs"}

64
src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.0.tm

@ -252,11 +252,14 @@ tcl::namespace::eval punk::lib::check {
#G-076: version gate for the tcl9 dead-console defect (upstream ticket f10d91c2d3, root-caused
#in G-039). Shared by the 'help tcl' warning and the repl dead-console watchdog arming (both
#consult has_tclbug_console_deadspin). Empty = no released Tcl is known to contain the upstream
#fix, so every Tcl 9 windows runtime classifies as affected. Set this only after the G-039 kill
#procedure, re-run on the fixed released runtime with the watchdog disabled, shows a clean
#script-visible eof exit (see goals/G-076-tcl9-deadconsole-fix-adoption.md).
variable tclbug_console_deadspin_fixed_in ""
#consult has_tclbug_console_deadspin). Empty would mean no Tcl is known to contain the upstream
#fix (every Tcl 9 windows runtime classifies as affected). Set to 9.0.5 2026-07-20: the upstream
#fix (ticket closed 2026-07-14; merged to core-9-0-branch, whose head reports 9.0.5) passed the
#G-039 kill procedure on a zig-built core-9-0-branch runtime with the watchdog disarmed - the
#dead console arrived as a readable fileevent and the repl exited via the normal eof path
#(requires the punk::repl 0.5.2 repl_handler guard; an official release is not required per the
#amended acceptance - see goals/archive/G-076-tcl9-deadconsole-fix-adoption.md 2026-07-20 entries).
variable tclbug_console_deadspin_fixed_in "9.0.5"
#pure classifier, separated for testability - facts in, verdict out
proc tclbug_console_deadspin_applies {platform tclversion fixed_in} {
@ -8412,7 +8415,9 @@ namespace eval punk::lib {
if {$do_libunknown} {
if {[info exists ::punk::libunknown::epoch]} {
append script "namespace eval ::punk::libunknown {}\n"
append script "set ::punk::libunknown::epoch [list $::punk::libunknown::epoch]\n"
#reduced copy - a verbatim copy carries 'added' short-circuit state that is
#only sound in the donor interp (see libunknown_epoch_export)
append script "set ::punk::libunknown::epoch [list [libunknown_epoch_export]]\n"
}
#find and source libunknown, then call init
append script {
@ -8538,7 +8543,9 @@ namespace eval punk::lib {
if {$do_libunknown} {
if {[info exists ::punk::libunknown::epoch]} {
interp eval $interp {namespace eval ::punk::libunknown {}}
interp eval $interp [list set ::punk::libunknown::epoch $::punk::libunknown::epoch]
#reduced copy - a verbatim copy carries 'added' short-circuit state that is
#only sound in the donor interp (see libunknown_epoch_export)
interp eval $interp [list set ::punk::libunknown::epoch [libunknown_epoch_export]]
}
#use snapshot's libunknown sourcing logic via interp eval
set libunknown_script [punk::lib::snapshot_package_paths -libunknown 1]
@ -8578,6 +8585,49 @@ namespace eval punk::lib {
}
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::lib::libunknown_epoch_export
@cmd -name punk::lib::libunknown_epoch_export\
-summary\
"Reduced copy of the punk::libunknown epoch record, safe to seed into a fresh interp or thread."\
-help\
"Returns ::punk::libunknown::epoch with every per-epoch 'added' map emptied.
The 'added' maps record which package names an interp's own scans registered from
each searchpath during each epoch - zipfs_tclPkgUnknown / zipfs_tm_UnknownHandler
consult the CURRENT epoch's map to skip re-sourcing a searchpath's indexes for a
name the path didn't provide before. That short-circuit is only sound in the interp
that performed the scans: packages the donor obtained at an EARLIER epoch, or with
no package unknown call at all (e.g Thread baked into a thread::create'd thread),
are absent from the donor's current-epoch map, so a fresh interp inheriting the
record verbatim skips their pkgIndex/tm sourcing and fails requires that stock
Tcl's tclPkgUnknown would satisfy (G-098 tkconsole diagnosis 2026-07-20).
Epoch counters, untracked markers, ifneeded-script records and index-glob caches
are preserved; the recipient performs its own first sourcing pass per searchpath
and accumulates its own 'added' state. Errors if ::punk::libunknown::epoch is not
set - callers that tolerate an uninitialised libunknown should guard with
info exists as they would for the raw variable."
@values -min 0 -max 0
}]
}
proc libunknown_epoch_export {} {
if {![info exists ::punk::libunknown::epoch]} {
error "punk::lib::libunknown_epoch_export - ::punk::libunknown::epoch not set (punk::libunknown not initialised in this interp?)"
}
set e $::punk::libunknown::epoch
foreach domain {tm pkg} {
if {![dict exists $e $domain epochs]} {continue}
dict for {ep state} [dict get $e $domain epochs] {
dict set state added [dict create]
dict set e $domain epochs $ep $state
}
}
return $e
}
proc valcopy {obj} {
append obj2 $obj {}
}

25
src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.0.tm → src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm

@ -7,7 +7,7 @@
# (C) 2025
#
# @@ Meta Begin
# Application punk::libunknown 0.2.0
# Application punk::libunknown 0.2.1
# Meta platform tcl
# Meta license MIT
# @@ Meta End
@ -18,6 +18,14 @@
# from src/modules/AGENTS.md "Versioning And Releases" - bumping means
# renaming the file AND updating the Meta line above, the manpage_begin line
# below and the provide-block version, then appending a line here):
#0.2.1 - zipfs_tm_UnknownHandler: the zipfs static-path can_skip_update state is
# written keyed by the SPECIFIC searchpath actually scanned (tm path +
# package namespace subdir) to match its reader and libunknown::init's
# indexbase keying. It was written keyed by the tm-list root, so resolving
# any subdir tm (e.g punk::args) poisoned the ROOT's skip map and later
# root-level requires (e.g commandstack) were skipped as known-absent -
# breaking 'shell' on zipfs kits (single //zipfs:/app/modules tm path;
# filesystem tm paths never take the skip branch). Diagnosed 2026-07-20.
#0.2.0 - register_all_tm: deep discovery proc registering ifneeded scripts for
# .tm modules at every namespace depth (once per tm epoch, interp-local
# tm_fullscan guard); used by 'dev lib.search' by default
@ -34,7 +42,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::libunknown 0 0.2.0]
#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1]
#[copyright "2025"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
@ -355,8 +363,10 @@ tcl::namespace::eval ::punk::libunknown {
# the token-to-directory mapping may have changed.
#JMN - review.
#dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion]
dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch
#0.2.1: keyed by specificsearchpath (the subset actually scanned) - the
#can_skip_update reader and libunknown::init both key that way; keying by
#the tm-list root here let subdir scans poison root-level skip decisions.
dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch
if {$must_scan} {
#however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked
dict unset epoch tm untracked $pkgname
@ -389,8 +399,9 @@ tcl::namespace::eval ::punk::libunknown {
#JMN
#store only once for each name, although there may be multiple versions of same package within this searchpath
#dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion]
dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch
#0.2.1: keyed by specificsearchpath - see the matching note at the
#already-ifneeded branch above.
dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch
#pkgname here could be the 'name' passed at the beggning - or other .tms at the same location.
#we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations.
if {$must_scan} {
@ -2119,7 +2130,7 @@ namespace eval ::punk::args::register {
package provide punk::libunknown [tcl::namespace::eval ::punk::libunknown {
variable pkg punk::libunknown
variable version
set version 0.2.0
set version 0.2.1
}]
return

63
src/vfs/_vfscommon.vfs/modules/punk/repl-0.5.2.tm

@ -1012,20 +1012,32 @@ proc repl::console_watchdog {inputchan} {
}
if {[catch {chan configure $inputchan -inputmode}]} {
#GetConsoleMode failed - the hosting console is gone.
#Close the channel (stops the tclWinConsole.c reader thread's error busy-loop)
#and finish the repl via the normal eof path. Do not attempt a console reopen:
#with the console dead, CONIN$ cannot be opened either (app-punkshell's eof
#handling makes the same discovery and exits cleanly).
array unset console_watchdog_afterids $inputchan
catch {chan event $inputchan readable {}}
catch {chan close $inputchan}
catch {puts stderr "|repl> console_watchdog: console unavailable for '$inputchan' - closing input and finishing repl (eof)"}
set ::repl::done [list eof $inputchan]
console_input_lost $inputchan "console_watchdog: console unavailable for '$inputchan' - closing input and finishing repl (eof)"
return
}
set console_watchdog_afterids($inputchan) [after $console_watchdog_ms [list [namespace current]::console_watchdog $inputchan]]
}
proc repl::console_input_lost {inputchan msg} {
#Shared terminal path for a dead/unusable console input channel (G-039/G-076),
#used by console_watchdog (polling rescue on defect-carrying tcl9 runtimes) and by
#repl_handler (runtimes carrying the upstream f10d91c2d3 fix deliver the dead
#console as a readable event instead).
#Close the channel (stops the tclWinConsole.c reader thread's error busy-loop)
#and finish the repl via the normal eof done-path. Do not attempt a console reopen:
#with the console dead, CONIN$ cannot be opened either (app-punkshell's eof
#handling makes the same discovery and exits cleanly).
variable console_watchdog_afterids
if {[info exists console_watchdog_afterids($inputchan)]} {
catch {after cancel $console_watchdog_afterids($inputchan)}
array unset console_watchdog_afterids $inputchan
}
catch {chan event $inputchan readable {}}
catch {chan close $inputchan}
catch {puts stderr "|repl> $msg"}
set ::repl::done [list eof $inputchan]
}
namespace eval repl::argdoc {
lappend PUNKARGS [list {
@id -id ::repl::console_get_size
@ -1852,7 +1864,18 @@ proc repl::repl_handler {inputchan readmore prompt_config} {
#According to DKF - -buffering option doesn't affect input channels
set rawmode 0
set waiting_needs_reader 0 ;#set when the 8.6-console waiting-chunks path skips its read on a drained channel - the readable handler must be armed (not the after-idle reinvoke) or the pending partial line could never complete
set original_input_conf [chan configure $inputchan] ;#whether repl is in line or raw mode - we restore the inputchan (stdin) state
if {[catch {chan configure $inputchan} original_input_conf]} {
#A runtime carrying the upstream tcl9 dead-console fix (f10d91c2d3) DELIVERS a
#dead console as a readable event, and this full option probe then errors
#(e.g 'couldn't read console mode: broken pipe'). Pre-fix runtimes never notify
#script level of a dead console (the console_watchdog poll covers those), so
#reaching here with a dead console requires the fixed driver. Terminal input
#loss - same teardown as the watchdog, no reopen attempt.
set in_repl_handler [list]
console_input_lost $inputchan "repl_handler: input channel unusable ($original_input_conf) - closing input and finishing repl (eof)"
return
}
#original_input_conf: whether repl is in line or raw mode - we restore the inputchan (stdin) state
if {[dict exists $original_input_conf -inputmode]} {
#review - when terminal is mintty and we switch to raw mode, the change isn't relected in chan configures '-inputmode' why?
@ -2923,11 +2946,19 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#to the selected console's channels (rputs maps stdout/stderr to conout/conerr).
#Interleaving between the two streams within a run is not preserved (single
#terminal stream; per-console err semantics are G-011).
#The vars are guarded: if the code-interp init aborted before installing the
#junction (e.g a package resolution failure) they don't exist - treat as no
#pending output rather than erroring here, which would leave the repl reader
#disabled and wedge the session (G-098 tkconsole diagnosis 2026-07-20).
set pending [thread::send $codethread {
interp eval code {
set _pendinglist [list $::codeinterp::console_pending_out $::codeinterp::console_pending_err]
set ::codeinterp::console_pending_out ""
set ::codeinterp::console_pending_err ""
if {[info exists ::codeinterp::console_pending_out]} {
set _pendinglist [list $::codeinterp::console_pending_out $::codeinterp::console_pending_err]
set ::codeinterp::console_pending_out ""
set ::codeinterp::console_pending_err ""
} else {
set _pendinglist [list "" ""]
}
set _pendinglist
}
}]
@ -3517,7 +3548,7 @@ namespace eval repl {
} %replthread_interp% [list $opt_callback_interp] {*}{
} %tmlist% [list [tcl::tm::list]] {*}{
} %autopath% [list $::auto_path] {*}{
} %lib_epoch% [list $::punk::libunknown::epoch] {*}{
} %lib_epoch% [list [punk::lib::libunknown_epoch_export]] {*}{
} %packageprefer% [list [package prefer]] {*}{
} %staticprefixes% [list [expr {[info exists ::punkboot::static_prefixes] ? $::punkboot::static_prefixes : ""}]] {*}{
} %staticpackages% [list [expr {[info exists ::punkboot::static_packages] ? $::punkboot::static_packages : ""}]] {*}{
@ -3923,7 +3954,9 @@ namespace eval repl {
catch {punk::lib::interp_sync_package_paths code}
code eval [list namespace eval ::punk::libunknown {}]
catch {
code eval [list set ::punk::libunknown::epoch $::punk::libunknown::epoch]
#reduced copy - 'added' short-circuit state is only sound in the donor
#interp (see punk::lib::libunknown_epoch_export)
code eval [list set ::punk::libunknown::epoch [punk::lib::libunknown_epoch_export]]
}
}
safebase {

Loading…
Cancel
Save