Browse Source

G-125 achieved: a kit that cannot boot is not deployed (punkshell 0.27.0)

The kit machinery already knew when it had failed to extract anything from a source
runtime, and under G-122 that notice became a recapped BUILD-WARNING - but the build
still assembled an artifact, DELETED the deployed kit and copied the new one over it,
closing running instances of the kit first. Field-observed 2026-07-26: an msys2-hosted
bake of punk91 produced a 49,501,792-byte bin/punk91.exe that failed at startup with
"application-specific initialization failed: Cannot find a usable init.tcl", by which
point the previously working kit was already gone. G-124 removed the particular cause;
this removes the class. A build tool should not make a working executable worse.

A bake now refuses any kit whose merged .vfs supplies no tcl library: the kit is listed
under FAILED KITS with a reason naming the cause and NOTHING is written - no
src/_build/<kit>, no deploy, and the previously deployed bin/<kit> is left byte-identical.
The gate sits immediately after the _vfscommon.vfs + <kit>.vfs merges and before the
kit-type assembly switch, the last point at which no build product exists; a refusal
appends to failed_kits, ends the punkcheck event FAILED and continues, so the punkcheck
records stay consistent with what is on disk. The merged src/_build/<kit>.vfs tree is
deliberately left behind - it is the evidence for the refusal.

It gates on the BOOT PRECONDITION, not on the extraction step having run. Some .vfs
folders legitimately supply their own tcl library (src/vfs/punk8_statictwapi.vfs,
punk9test.vfs and mkzipfix.vfs do today), and those must keep building - so the check
reads the merged tree. It is structural and executes nothing, which also makes it valid
for cross-target kits this host could never run, and costs 0.63ms per kit (measured over
200 iterations against the real src/_build/punk905.exe.vfs), so it runs unconditionally.

The predicate is punkboot::utils::vfs_boot_library_report (0.2.0 -> 0.3.0), called through
a guarded require modelled on the existing get_src_provenance_warnings: a stale or missing
bootsupport snapshot degrades the gate to a NOTE rather than failing every kit. Both
branches were observed live - before the bootsupport propagation 'make.tcl check' reported
UNAVAILABLE, after it ACTIVE. Scope was extended at activation (user-approved) to put the
predicate in that module rather than private to the build script, because a gate that
cannot be exercised cannot be characterized, and punkboot::utils is already where
make.tcl's provenance gate helper lives.

What qualifies as a tcl library: tcl_library/init.tcl (zipfs-attached kits) or
lib/tcl<major>.<minor>/init.tcl (starkit-style), AND at least one companion file beside it
(tm.tcl, package.tcl, auto.tcl, clock.tcl, history.tcl, word.tcl) or an encoding/
directory. The companion test is load-bearing: every punkshell kit carries
lib/BWidget1.10.1/init.tcl, so a check that looked for any init.tcl anywhere would pass a
kit with no tcl library at all. lib/tcllib2.0 is correctly not matched by the lib/tcl[0-9]*
glob. Verified against all 10 assembled trees in src/_build - both conventions are
represented (tcl_library for punk902z/punk905/punk91/punk9_beta/punk9bi_beta/punkmagic,
lib/tcl8.6 for punk86/punkbi/punksys, lib/tcl9.0 for punkshell902).

The no-extraction BUILD-WARNING was reworded from "will not initialise unless src/vfs/<x>
supplies one" - a consequence the build then ignored - to "will FAIL the boot-precondition
gate unless src/vfs/<x> supplies one". It stays a warning rather than becoming the failure,
because a .vfs that supplies its own library needs no extraction.

Verified 2026-07-27 end-to-end against a temporary fixture kit (a mapvfs entry pointing a
zip kit at a payload-free runtime, plus a vfs with no tcl library), with sentinel files
standing in for a previously deployed kit and a previous build product. Across the failing
run bin/punkgatefixture.exe (sha1 2a0700eb...) and src/_build/punkgatefixture.exe (sha1
12c50567...) were byte-identical with unchanged mtimes, and the kit was reported under
FAILED KITS with the cause named. Adding tcl_library/{init.tcl,tm.tcl} to that same fixture
vfs - extraction still yielding nothing - made it build and deploy normally, which is the
other half of the criterion. The fixture was removed afterwards and mapvfs.config restored
byte-identical to its backup. Real-kit bake through the gate: punk905 builds, deploys and
boots.

Characterization: 12 tests in modules/punkboot/utils/testsuites/utils/bootlibrary.test
(both conventions, the companion-file requirement and its BWidget decoy, near-miss
reporting, missing/empty trees, and a sweep asserting every assembled src/_build/*.vfs
tree still passes so the gate cannot fail kits that boot today) and 2 in
shell/testsuites/punkexe/maketclbootgate.test (the ACTIVE/UNAVAILABLE report, ESC-free per
the G-113 piped policy). Existing punkexe suites pass unchanged.

Documented in src/AGENTS.md with the remedy when a kit is refused, in ARCHITECTURE.md
beside the provenance gates, and in the embedded 'make.tcl workflow' data flow as key note
K10 (verified under the default and PUNKBOOT_PLAIN=1 paths, within the 100-column budget).

The achieved flip archives the entry and the detail file, and sweeps the live tier: G-127
(its output location inherits the never-written guarantee as long as the gate stays
upstream of the artifact write, and its payload/target mismatch reporting should share this
vocabulary), G-028 (the sibling "why bin/<kit> was not updated" surface in the same deploy
step) and G-101 (a new container type must land its library where the gate looks, or extend
the location list).

Assisted-by: harness=claude; primary-model=claude-opus-5[1m]; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
b7c632b670
  1. 1
      ARCHITECTURE.md
  2. 20
      CHANGELOG.md
  3. 4
      GOALS-archive.md
  4. 3
      GOALS.md
  5. 5
      goals/G-028-file-locker-identification.md
  6. 8
      goals/G-101-tcl86-kit-container-strategy.md
  7. 83
      goals/G-125-unbootable-kit-deploy-gate.md
  8. 10
      goals/G-127-crosstarget-vfs-bake.md
  9. 157
      goals/archive/G-125-unbootable-kit-deploy-gate.md
  10. 2
      punkproject.toml
  11. 1
      src/AGENTS.md
  12. 91
      src/make.tcl
  13. 99
      src/modules/punkboot/utils-999999.0a1.0.tm
  14. 3
      src/modules/punkboot/utils-buildversion.txt
  15. 1
      src/tests/modules/AGENTS.md
  16. 166
      src/tests/modules/punkboot/utils/testsuites/utils/bootlibrary.test
  17. 1
      src/tests/shell/AGENTS.md
  18. 124
      src/tests/shell/testsuites/punkexe/maketclbootgate.test

1
ARCHITECTURE.md

@ -84,6 +84,7 @@ Two layers with a deliberate dependency direction (the class never depends on th
- **Host vs target platform (G-122).** The kit surfaces separate what the driving tclsh IS from what a bake is FOR. Target-keyed: the `bin/runtime/<tier>` store a runtime is read from, `.exe` suffixing of runtimes and kit outputs, presence checks, and the pre-deploy process sweep's tooling (`tasklist`/`taskkill` vs `ps`/`kill`, skipped when the target's processes cannot exist on this host). Host-keyed: copy commands, path handling, filesystem case rules, prompts. The default target is the host canon except for cygwin-family hosts (msys2/cygwin-runtime tclsh - `tcl_platform(platform)` `unix` on windows), which target `win32-x86_64`; a `mapvfs.config` entry may declare its own target and is then addressed in that platform's tier. `make.tcl check` prints the derivation. Zip-type kits assemble without zipfs in the driving tcl (raw-runtime split + `punk::zip::mkzip` + concatenation), and since G-124 they EXTRACT without it too: `punk::zip` reads a zip - plain, or attached to an executable under either offset convention - with stock Tcl only, so a bake needs neither zipfs nor tcllib to carry a runtime's `tcl_library` into the kit. Sources: `src/AGENTS.md`, `src/runtime/AGENTS.md`, `src/modules/punk/platform-999999.0a1.0.tm`, `src/modules/punk/zip-999999.0a1.0.tm`.
- **punkcheck** records every install/delete as events in per-folder `.punkcheck` directories - the basis for skip/copy change detection, superseded-module pruning and provenance. Single OO record lifecycle (G-094, achieved) with atomic saves and advisory event-scoped locking for concurrent writers (G-095, achieved).
- **Provenance gates.** Build/promotion commands warn on uncommitted `src/` changes (column-0 `PROVENANCE-WARNING:` token, `-dirty-abort` for strict mode); `vendorupdate` warns for dirty source-project checkouts.
- **Boot-precondition gate (G-125).** A bake refuses a kit whose merged `.vfs` supplies no tcl library rather than deploying an artifact that cannot initialise: the kit lands in `FAILED KITS` and nothing is written, so the previously deployed `bin/<kit>` survives. Structural and non-executing (so cross-target kits are covered), reading the merged tree rather than the extraction outcome. The predicate is `punkboot::utils::vfs_boot_library_report`, called through the same guarded require as the provenance check so a stale bootsupport snapshot degrades it to a notice; `make.tcl check` reports ACTIVE/UNAVAILABLE. Sources: `src/AGENTS.md`, `src/modules/punkboot/utils-999999.0a1.0.tm`.
- **Buildsuites and the kit family.** `src/buildsuites/suite_tcl90/` builds Tcl/Tk/tcllib from source with a pinned zig toolchain and produces the runtime kit family (plain / punk / bi) plus artifact metadata (G-096-G-117 era: see archived goals G-096, G-098, G-102, G-103, G-107); artifacts publish to the punkbin repo that `bin/punk-runtime.cmd` fetches from. `src/buildsuites/suite_tcl86/` is the 8.6 sibling (G-099 + G-100, both achieved): a Tcl 8.6 windows runtime (static + dynamic shells, `tcl86t.dll`, on-disk lib tree - no zipfs, so no self-contained kit) with thread 2.8 + tclvfs + Tk 8.6 + tklib + tcllib(+tcllibc critcl accelerators) companions, gated against the 8.6 core/thread/tclvfs testsuites with tcllib/tklib on a record tier and an opt-in `test-tk`; punkshell's own runtests is censused on that runtime at parity with a same-day native-8.6 baseline. Suite child shells scrub `TCL<major>_<minor>_TM_PATH` as well as TCLLIBPATH/TCL_LIBRARY/TK_LIBRARY - without it a census measures the machine's module trees. The 8.6 kit container strategy remains in-flux - see "In-flux areas".
- **Project generation.** `dev project.new` composes thin layouts from `src/project_layouts/` (overlay chain with `.anti` deletion markers and `name@base` derivation - G-087, achieved) and injects bootsupport modules from the generating shell at generation time.
- **Workflow overview.** `tclsh src/make.tcl workflow` prints the embedded ASCII data-flow overview of the build/release pipeline, with its own update contract in `src/AGENTS.md`. This section deliberately summarises rather than copies it.

20
CHANGELOG.md

@ -5,6 +5,26 @@ 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.27.0] - 2026-07-27
- A kit that cannot boot is no longer deployed (G-125). `make.tcl bake` now checks each
kit's merged `.vfs` for a tcl library before building it; a kit with none is listed under
`FAILED KITS` with a reason naming the cause and NOTHING is written - no
`src/_build/<kit>`, no deploy, and the previously deployed `bin/<kit>` is left
byte-identical. Previously the build warned and carried on, deleting the working kit and
replacing it with an artifact that died at startup with
"application-specific initialization failed: Cannot find a usable init.tcl".
- The gate tests the boot precondition, not whether extraction ran: a `.vfs` that supplies
its own tcl library builds and deploys exactly as before. The check is structural and
executes nothing, so it covers cross-target kits this host could never run, and it is
cheap enough to run for every kit on every bake.
- `tclsh src/make.tcl check` reports the gate as ACTIVE or UNAVAILABLE, and
`tclsh src/make.tcl workflow` shows it in the kit data flow (key note K10). A stale
bootsupport snapshot degrades the gate to a NOTE rather than failing every kit - the same
guarded-require treatment the dirty-src provenance check already had.
- New `punkboot::utils::vfs_boot_library_report` (0.2.0 -> 0.3.0) is the predicate behind
the gate, with its own test suite over directory fixtures.
## [0.26.0] - 2026-07-26
- punk::zip can READ a zip archive using only stock Tcl - no zipfs, no vfs::zip, no tcllib

4
GOALS-archive.md

@ -202,3 +202,7 @@ Acceptance: an msys/cygwin-runtime tclsh driving bakelist/bake on windows report
### G-124 [achieved 2026-07-26] Dependency-free zip reading in punk::zip: archives, runtime-prefixed executables, and a scripter-facing introspection surface → detail: goals/archive/G-124-punkzip-reader.md
Scope: src/modules/punk/zip-999999.0a1.0.tm (reader + introspection procs, base-offset derivation factored out of extract_preamble for shared use, buildversion); src/make.tcl (zipfs-less kit extraction path as first consumer - the zipfile::decode requirement is removed, not made optional); src/tests/modules/punk/zip/ (new testsuite - mkzip round-trip, prefixed-archive cases, introspection, unsupported-input behaviour); bootsupport + src/vfs/_vfscommon.vfs copies via established sync channels
Acceptance: punk::zip extracts to a target directory from (a) a plain zip, (b) an executable-prefixed archive with archive-relative offsets and (c) one with file-relative offsets - bin/runtime/win32-x86_64/tclsh90b4_piperepl.exe is the recorded (c) case, on which the current split-then-tcllib path fails with "Bad zip file. Bad closure." - producing CRC-verified byte-identical members with directory entries materialized as directories, on a tclsh with no zipfs, no vfs::zip and no tcllib reachable (the interpreter used is recorded here); an introspection call lists members without extracting, returning per-entry name, size, compressed size, method, mtime, crc and directory-vs-file, over the same three input shapes, and both surfaces carry punk::args definitions with worked examples so 'i punk::zip::<proc>' documents them; unsupported inputs (zip64, encrypted, unknown compression method) fail naming the reason rather than emitting partial or garbled output; make.tcl's zipfs-less kit extraction consumes the new surface and NO zipfile::decode requirement remains anywhere in the shipped tree, verified by a tree grep, and demonstrated by an 8.6 zipfs-less bake of a zip kit on a tcllib-less tclsh (msys2 /usr/bin/tclsh8.6 as reference host) producing an artifact that boots with its tcl_library present; a new src/tests/modules/punk/zip/ suite covers a mkzip -> read round trip asserting names, content, directory-vs-file classification and stored attributes - recording the answer to the standing zipfs dir-misidentification question noted in punk::mix::cli and src/vfs/mkzipfix.vfs - plus the three prefixed-archive cases, the introspection output, and the unsupported-input failures; punk::zip buildversion minor-bumped with its changelog line.
### G-125 [achieved 2026-07-27] A kit that cannot boot is not deployed: extraction failure gates the bake instead of warning past it → detail: goals/archive/G-125-unbootable-kit-deploy-gate.md
Scope: src/make.tcl (kit extraction outcome handling, deploy step gating, the no-extraction BUILD-WARNING as the current behaviour being replaced); src/modules/punkboot/utils-999999.0a1.0.tm (the boot-precondition predicate, called guardedly from make.tcl as vcs_dirty_warnings is); src/tests/modules/punkboot/utils/ (predicate characterization over directory fixtures); src/tests/shell/testsuites/punkexe/ (characterization of the make.tcl-side gate reporting); src/AGENTS.md (bake failure-mode documentation)
Acceptance: when extraction from the source runtime yields nothing and the kit's .vfs does not itself supply a bootable tcl library, the kit is recorded in FAILED KITS with a reason naming the cause and NEITHER src/_build nor bin receives a new artifact - the previously deployed kit is left untouched, verified by mtime and byte comparison across such a run; a kit whose .vfs legitimately supplies its own tcl library still builds and deploys, so the gate tests for the boot precondition rather than for the extraction step having run; the check is cheap enough to run on every kit and executes no artifact; the condition and its remedy are documented in src/AGENTS.md; existing punkexe suites pass unchanged and the gate itself is characterized.

3
GOALS.md

@ -390,9 +390,6 @@ Scope: src/scriptapps/punk-runtime.ps1 + src/scriptapps/bin/punk-runtime.bash +
Detail: goals/G-123-thirdparty-runtime-tiers.md
### G-125 [proposed] A kit that cannot boot is not deployed: extraction failure gates the bake instead of warning past it
Scope: src/make.tcl (kit extraction outcome handling, deploy step gating, the no-extraction BUILD-WARNING as the current behaviour being replaced); src/tests/shell/testsuites/punkexe/ (characterization of the gate); src/AGENTS.md (bake failure-mode documentation)
Detail: goals/G-125-unbootable-kit-deploy-gate.md
### G-126 [proposed] punkzip as a vendored zip accelerator: zig 0.16 port, reproducible build into bin/, punk::zip fast path
Scope: src/tools/punkzip/ (vendored punkzip source tree - root name settled in the work - with provenance and licence records); src/make.tcl (tool build step); bin/tools/zig* (pinned toolchain as consumed); bin/punkzip.exe (untracked build output); src/modules/punk/zip-999999.0a1.0.tm (accelerator detection + fast path behind the G-124 pure-Tcl floor); src/tests/modules/punk/zip/ (parity suite - accelerated and pure-Tcl paths must agree); upstream maintained checkout c:/repo/jn/zig/punkzip (re-vendor source, read-only from this repo)

5
goals/G-028-file-locker-identification.md

@ -59,6 +59,11 @@ the kind of file this fails on.
## Notes
- Related: G-125 (achieved 2026-07-27 - see goals/archive/G-125-unbootable-kit-deploy-gate.md) -
the sibling refusal in the same deploy step, and a precedent for its shape: a kit that
fails the boot-precondition gate is reported under FAILED KITS with a reason naming the
cause and nothing is written. This goal names the PROCESS holding a target the build
cannot replace; both are "why bin/<kit> was not updated" answers and should read alike.
- Related: G-023 (versioned binary deploys are the recurring customer),
G-020 (same optionally-available twapi/cffi pattern for platform features),
punkboot::utils chicken-and-egg ordering rules in src/bootsupport/AGENTS.md

8
goals/G-101-tcl86-kit-container-strategy.md

@ -45,6 +45,14 @@ unblocked; activation is the user's call.
## Notes
- Related: G-125 (achieved 2026-07-27 - see goals/archive/G-125-unbootable-kit-deploy-gate.md) -
a new container type is exactly where an extraction path can silently produce nothing,
and the bake now refuses such a kit instead of deploying it. Concretely for this goal:
whichever container is chosen must land a tcl library in the merged vfs at
`tcl_library/` or `lib/tcl<major>.<minor>/` (with a companion file beside init.tcl), or
the gate's location list in `punkboot::utils::vfs_boot_library_report` needs extending
to recognise the new shape - the check is the place where "this container produced a
bootable kit" is asserted.
- Related: G-124 (achieved 2026-07-26 - see goals/archive/G-124-punkzip-reader.md) - an 8.6
container has no zipfs by definition, so whichever container this goal picks needs a
reader that works without it. That reader now exists and is verified on 8.6 with no

83
goals/G-125-unbootable-kit-deploy-gate.md

@ -1,83 +0,0 @@
# G-125 A kit that cannot boot is not deployed
Status: proposed
Scope: src/make.tcl (kit extraction outcome handling, deploy step gating, the no-extraction BUILD-WARNING as the current behaviour being replaced); src/tests/shell/testsuites/punkexe/ (characterization of the gate); src/AGENTS.md (bake failure-mode documentation)
Goal: a bake that could not lift a runtime's own payload (its tcl_library) into the kit fails that kit instead of building and deploying an artifact that cannot boot - so the worst outcome of a missing extraction capability is a red kit, never a silently replaced working executable.
Acceptance: when extraction from the source runtime yields nothing and the kit's .vfs does not itself supply a bootable tcl library, the kit is recorded in FAILED KITS with a reason naming the cause and NEITHER src/_build nor bin receives a new artifact - the previously deployed kit is left untouched, verified by mtime and byte comparison across such a run; a kit whose .vfs legitimately supplies its own tcl library still builds and deploys, so the gate tests for the boot precondition rather than for the extraction step having run; the check is cheap enough to run on every kit and executes no artifact; the condition and its remedy are documented in src/AGENTS.md; existing punkexe suites pass unchanged and the gate itself is characterized.
## Context
The kit machinery already knows when it failed to extract anything from a source runtime.
Under G-122 (achieved - see goals/archive/G-122-host-target-platform-split.md) that
notice was promoted from an inline stderr block to a recapped `BUILD-WARNING:` naming the
kit and what was tried - but the build still proceeds, assembles an artifact, DELETES the
deployed kit and copies the new one over it. The pre-deploy process sweep will even close
running instances of the kit first.
Field-observed 2026-07-26: an msys2-hosted bake of `punk91` produced a 49,501,792-byte
`bin/punk91.exe` that failed at startup with
application-specific initialization failed: Cannot find a usable init.tcl
because the host's tclsh had no tcllib and therefore could not extract the runtime's own
zip. The previously working kit had already been replaced by then. G-124 removes the
particular cause, but not the class: a runtime with no attached payload at all, an
extraction that fails for any other reason, or a future container type reaches the same
state, and the build's response is still "warn and deploy".
The existing precedent in the same code path is `vfs_startup_script_warning`, which warns
about a missing `main.tcl` and continues - deliberately, because a kit without a startup
script is legal. A kit without a resolvable tcl library is not in the same category: it
cannot initialise at all.
## Approach
- Gate on the BOOT PRECONDITION, not on the extraction step having run. Some .vfs folders
legitimately supply their own tcl library (the merged `targetvfs` is what matters), so
the check inspects the assembled tree rather than the extraction outcome - a runtime
that needed no extraction must keep building.
- Fail the kit the same way every other failure in that loop does (append to
`failed_kits` with a reason, end the punkcheck event FAILED) BEFORE the artifact is
renamed into place, so neither `src/_build/<kit>` nor `bin/<kit>` is touched and the
punkcheck records stay consistent with what is on disk.
- Keep it cheap and non-executing: a structural check of the merged vfs, no artifact
launch, so it can run for every kit on every bake.
- The recapped BUILD-WARNING wording changes from "will not initialise unless the vfs
supplies one" to a failure reason, since the gate now enforces what the warning
described.
## Alternatives considered
- Keep warning and deploying, on the grounds that the developer sees the recap - rejected:
the recap arrives after the deployed kit has already been replaced, and the failure
surfaces later as an unexplained broken shell. A build tool should not make a working
executable worse.
- Boot the freshly built artifact as the check - rejected: it cannot work for
cross-target kits (G-122 makes those routine), costs a process launch per kit, and a
kit that boots into an interactive shell is awkward to probe safely.
- Emit to `src/_build` but refuse only the deploy - rejected as a half-measure that
leaves a punkcheck record claiming a good build product; a later run would consider it
current.
## Notes
- Related: G-124 (achieved 2026-07-26 - see goals/archive/G-124-punkzip-reader.md) - removed
the most common cause: a tcllib-less tclsh can now read a runtime's attached zip, and a
bake from msys2's tclsh8.6 carries `tcl_library` into the kit instead of warning past it.
This gate is independent of that and remains valuable: the class survives (a runtime with
no attached payload, an extraction that fails for any other reason, a future container
type), and the build's response to it is still "warn and deploy". Note for the gate's
design: the reader now RAISES with a named reason (encrypted, zip64, unknown compression
method, crc mismatch, offsets that do not describe the file) where the old path returned
an opaque tcllib error, so the gate has a usable failure reason to report.
- Related: G-122 (achieved) - promoted the no-extraction notice to a recapped
BUILD-WARNING and recorded the field incident that motivates this goal; see
goals/archive/G-122-host-target-platform-split.md.
- Related: G-101 - a new 8.6 container type is exactly the situation where an extraction
path can silently produce nothing; the gate is what keeps that honest.
- Related: G-028 - sibling failure-reporting surface in the same deploy step (that goal
names the process holding a target the build cannot replace; this one refuses to
replace a target with something unbootable).
- Overlap survey also considered G-057 (icon embedding rides the same wrap steps) and
G-115 (declarative .vfs composition decides what the vfs supplies) - neither is a
dependency: this gate reads whatever the merged tree contains, however it was composed.

10
goals/G-127-crosstarget-vfs-bake.md

@ -89,6 +89,16 @@ exists locally (empty but for sha1sums.txt) and punkbin upstream carries
- Depends on: G-122 (achieved) - the input half; this is the output half of the same
split, and retires the TODO that goal left standing.
- Related: G-125 (achieved 2026-07-27 - see goals/archive/G-125-unbootable-kit-deploy-gate.md;
pointer pushed at its archive sweep, where the pair were each other's only recorded
bridge). It added a boot-precondition gate that refuses a kit BEFORE any build product
is written - immediately after the vfs merges, before the kit-type assembly switch. Two
consequences for this goal: whatever output location it settles on for non-native kits
(bin/kits/<platform>/) inherits the never-written guarantee for free as long as the gate
stays upstream of the artifact write, and the payload/target mismatch reporting this
goal adds should read as one vocabulary with the gate's refusal rather than a second,
differently-worded failure surface. The gate is deliberately non-executing precisely so
it covers cross-target kits this host could never run.
- Related: G-115 - owns the payload DECLARATION FORMAT and is currently written without
any platform axis (zero mentions of platform or target). Whichever of the two lands
second adopts the other: this goal supplies the axis, that goal supplies the format.

157
goals/archive/G-125-unbootable-kit-deploy-gate.md

@ -0,0 +1,157 @@
# G-125 A kit that cannot boot is not deployed
Status: achieved 2026-07-27
Scope: src/make.tcl (kit extraction outcome handling, deploy step gating, the no-extraction BUILD-WARNING as the current behaviour being replaced); src/modules/punkboot/utils-999999.0a1.0.tm (the boot-precondition predicate, called guardedly from make.tcl as vcs_dirty_warnings is); src/tests/modules/punkboot/utils/ (predicate characterization over directory fixtures); src/tests/shell/testsuites/punkexe/ (characterization of the make.tcl-side gate reporting); src/AGENTS.md (bake failure-mode documentation)
Goal: a bake that could not lift a runtime's own payload (its tcl_library) into the kit fails that kit instead of building and deploying an artifact that cannot boot - so the worst outcome of a missing extraction capability is a red kit, never a silently replaced working executable.
Acceptance: when extraction from the source runtime yields nothing and the kit's .vfs does not itself supply a bootable tcl library, the kit is recorded in FAILED KITS with a reason naming the cause and NEITHER src/_build nor bin receives a new artifact - the previously deployed kit is left untouched, verified by mtime and byte comparison across such a run; a kit whose .vfs legitimately supplies its own tcl library still builds and deploys, so the gate tests for the boot precondition rather than for the extraction step having run; the check is cheap enough to run on every kit and executes no artifact; the condition and its remedy are documented in src/AGENTS.md; existing punkexe suites pass unchanged and the gate itself is characterized.
## Context
The kit machinery already knows when it failed to extract anything from a source runtime.
Under G-122 (achieved - see goals/archive/G-122-host-target-platform-split.md) that
notice was promoted from an inline stderr block to a recapped `BUILD-WARNING:` naming the
kit and what was tried - but the build still proceeds, assembles an artifact, DELETES the
deployed kit and copies the new one over it. The pre-deploy process sweep will even close
running instances of the kit first.
Field-observed 2026-07-26: an msys2-hosted bake of `punk91` produced a 49,501,792-byte
`bin/punk91.exe` that failed at startup with
application-specific initialization failed: Cannot find a usable init.tcl
because the host's tclsh had no tcllib and therefore could not extract the runtime's own
zip. The previously working kit had already been replaced by then. G-124 removes the
particular cause, but not the class: a runtime with no attached payload at all, an
extraction that fails for any other reason, or a future container type reaches the same
state, and the build's response is still "warn and deploy".
The existing precedent in the same code path is `vfs_startup_script_warning`, which warns
about a missing `main.tcl` and continues - deliberately, because a kit without a startup
script is legal. A kit without a resolvable tcl library is not in the same category: it
cannot initialise at all.
## Approach
- Gate on the BOOT PRECONDITION, not on the extraction step having run. Some .vfs folders
legitimately supply their own tcl library (the merged `targetvfs` is what matters), so
the check inspects the assembled tree rather than the extraction outcome - a runtime
that needed no extraction must keep building.
- Fail the kit the same way every other failure in that loop does (append to
`failed_kits` with a reason, end the punkcheck event FAILED) BEFORE the artifact is
renamed into place, so neither `src/_build/<kit>` nor `bin/<kit>` is touched and the
punkcheck records stay consistent with what is on disk.
- Keep it cheap and non-executing: a structural check of the merged vfs, no artifact
launch, so it can run for every kit on every bake.
- The predicate lives in `punkboot::utils` and make.tcl calls it through a guarded require,
exactly as it already does for `vcs_dirty_warnings` - Scope extended at activation
(2026-07-27, user-approved) because a predicate private to the build script cannot be
characterized, and that module is the established home for make.tcl's gate helpers.
- The recapped BUILD-WARNING wording changes from "will not initialise unless the vfs
supplies one" to a failure reason, since the gate now enforces what the warning
described.
## Alternatives considered
- Keep warning and deploying, on the grounds that the developer sees the recap - rejected:
the recap arrives after the deployed kit has already been replaced, and the failure
surfaces later as an unexplained broken shell. A build tool should not make a working
executable worse.
- Boot the freshly built artifact as the check - rejected: it cannot work for
cross-target kits (G-122 makes those routine), costs a process launch per kit, and a
kit that boots into an interactive shell is awkward to probe safely.
- Emit to `src/_build` but refuse only the deploy - rejected as a half-measure that
leaves a punkcheck record claiming a good build product; a later run would consider it
current.
## Notes
- Related: G-124 (achieved 2026-07-26 - see goals/archive/G-124-punkzip-reader.md) - removed
the most common cause: a tcllib-less tclsh can now read a runtime's attached zip, and a
bake from msys2's tclsh8.6 carries `tcl_library` into the kit instead of warning past it.
This gate is independent of that and remains valuable: the class survives (a runtime with
no attached payload, an extraction that fails for any other reason, a future container
type), and the build's response to it is still "warn and deploy". Note for the gate's
design: the reader now RAISES with a named reason (encrypted, zip64, unknown compression
method, crc mismatch, offsets that do not describe the file) where the old path returned
an opaque tcllib error, so the gate has a usable failure reason to report.
- Related: G-122 (achieved) - promoted the no-extraction notice to a recapped
BUILD-WARNING and recorded the field incident that motivates this goal; see
goals/archive/G-122-host-target-platform-split.md.
- Related: G-127 (recorded at activation 2026-07-26 - drafted after this goal, and the
activation-freshness survey's top unlinked pair on shared paths: src/make.tcl deploy
step, src/tests/shell/testsuites/punkexe/, src/AGENTS.md). It moves non-native kits to
bin/kits/<platform>/ and adds its own payload/target mismatch reporting, so this gate
must refuse before whichever output location that goal settles on, and the two failure
reports should read as one vocabulary rather than two.
- Related: G-101 - a new 8.6 container type is exactly the situation where an extraction
path can silently produce nothing; the gate is what keeps that honest.
- Related: G-028 - sibling failure-reporting surface in the same deploy step (that goal
names the process holding a target the build cannot replace; this one refuses to
replace a target with something unbootable).
- Overlap survey also considered G-057 (icon embedding rides the same wrap steps) and
G-115 (declarative .vfs composition decides what the vfs supplies) - neither is a
dependency: this gate reads whatever the merged tree contains, however it was composed.
## Progress
2026-07-27 - implemented; every Acceptance clause verified. Detail:
- `punkboot::utils` 0.2.0 -> 0.3.0 gains `vfs_boot_library_report {vfsfolder}`, returning
`ok` / `locations` / `rejected` / `checked` / `reason`. make.tcl calls it through
`::punkboot::get_vfs_boot_library_report`, a guarded require modelled on the existing
`get_src_provenance_warnings`, so a stale bootsupport snapshot degrades the gate to a
NOTE rather than failing every kit. Both branches were observed live: before the
bootsupport propagation `make.tcl check` reported UNAVAILABLE, after it ACTIVE.
- The gate sits in the kit loop immediately after the `_vfscommon.vfs` + `<kit>.vfs`
merges and before the kit-type assembly switch - the last point at which no build
product exists. A refusal appends to `failed_kits`, ends the punkcheck event FAILED and
`continue`s, so `src/_build/<kit>` and `bin/<kit>` are never touched and the punkcheck
records stay consistent with what is on disk. Note the merged `src/_build/<kit>.vfs`
tree IS left behind (it is the evidence for the refusal); only the artifact is refused.
- What qualifies as a tcl library: `tcl_library/init.tcl` (zipfs-attached kits) or
`lib/tcl<major>.<minor>/init.tcl` (starkit-style), AND at least one companion file
(`tm.tcl`, `package.tcl`, `auto.tcl`, `clock.tcl`, `history.tcl`, `word.tcl`) or an
`encoding/` directory beside it. The companion test is load-bearing: every punkshell kit
carries `lib/BWidget1.10.1/init.tcl` and `lib/BWidget1.9.16/init.tcl`, so a check that
looked for any `init.tcl` would pass a kit with no tcl library at all. `lib/tcllib2.0`
is correctly not matched by the `lib/tcl[0-9]*` glob.
- The no-extraction BUILD-WARNING was reworded from "will not initialise unless
src/vfs/<x> supplies one" (which described a consequence the build then ignored) to
"will FAIL the boot-precondition gate unless src/vfs/<x> supplies one". It stays a
warning rather than becoming the failure, because a .vfs that supplies its own library
needs no extraction - the gate decides, on the merged tree.
### Verification records (2026-07-27)
- Predicate over the 10 assembled kit trees in `src/_build`: all pass, and the two
conventions are both represented (tcl_library for punk902z/punk905/punk91/punk9_beta/
punk9bi_beta/punkmagic, lib/tcl8.6 for punk86/punkbi/punksys, lib/tcl9.0 for
punkshell902). Of the source `.vfs` folders three legitimately supply their own library
(mkzipfix.vfs, punk8_statictwapi.vfs, punk9test.vfs) - the case the gate must not break.
- Gate fired end-to-end against a temporary fixture kit (a mapvfs entry pointing a zip kit
at a payload-free runtime plus a vfs with no tcl library), with sentinel files standing
in for a previously deployed kit and a previous build product:
`bin/punkgatefixture.exe` sha1 2a0700ebfad097e34f70f6d779c8c1c8f2ba7dba and
`src/_build/punkgatefixture.exe` sha1 12c505679ae7cc8fe44527eb4e8f34fa2659894d were
byte-identical with unchanged mtimes across the failing run, and the kit was reported
under FAILED KITS with the cause named. Adding `tcl_library/{init.tcl,tm.tcl}` to that
same fixture vfs - extraction still yielding nothing - made the kit build and deploy
normally (INSTALLED KITS: 1), which is the "tests the precondition, not the extraction
step" half of the criterion. The fixture (mapvfs entry, vfs folder, fake runtime,
sentinels, build leftovers) was removed afterwards and mapvfs.config restored
byte-identical to its backup.
- Real-kit bake through the gate: `bake -confirm 0 punk905` builds, deploys and boots
(tcl_library at //zipfs:/app/tcl_library, punkboot::utils 0.3.0 present in the kit).
- `src/tests/modules/punkboot/utils/testsuites/utils/bootlibrary.test` 12/12;
`src/tests/shell/testsuites/punkexe/maketclbootgate.test` 2/2.
- `make.tcl workflow` renders the gate in the kit data flow with key note K10, verified
under the default and PUNKBOOT_PLAIN=1 paths, max line width within the 100 budget.
- Cost: 0.63ms per kit measured over 200 iterations against the real
`src/_build/punk905.exe.vfs` tree (a `glob` plus a handful of `file isfile` tests, no
recursion, nothing executed) - negligible beside a bake that takes minutes, which is
what lets it run for every kit unconditionally.
- Full source-tree suite after the change: 1161 tests, 1138 passed, 20 skipped, 3 failed.
One is `core/tcl exec-14.3`, the documented pre-existing baseline. The other two are in
`modules/argparsingtest/` and belong to unrelated work in progress in the working tree
at the time of the run (that module, its buildversion and both of its test files were
being edited concurrently); nothing in this change reaches arg-parsing benchmarks.

2
punkproject.toml

@ -1,4 +1,4 @@
[project]
name = "punkshell"
version = "0.26.0"
version = "0.27.0"
license = "BSD-2-Clause"

1
src/AGENTS.md

@ -71,6 +71,7 @@ Recovery after a wrong path guess:
- `tclsh src/make.tcl workflow` prints an embedded ASCII data-flow overview of the build/release workflow (release-ready command sequence, source→outputs folder flow, kit assembly, keyed notes). The text lives in `::punkboot::workflow_text` in `src/make.tcl` - embedded rather than a data file so it travels with the make.tcl copies seeded into generated projects. **Update contract (agents):** whenever build data flow changes - a make.tcl subcommand added/removed/repurposed, a source or output folder added or rerouted, a propagation target added, a gate (staleness/provenance/confirm) or deploy behaviour changed - update the workflow text in the same change-set. Keep it plain ASCII, max line width 100, and preserve the RELEASE SEQUENCE / DIAGRAM / KEY-NOTES / MAINTENANCE structure; verify by running `tclsh src/make.tcl workflow` (and once under `PUNKBOOT_PLAIN=1`). Adding/changing a subcommand also means: SUMMARIES + HELPTEXTS + SUBOPTS (+ SUBGROUPS) in `::punkboot::argdoc`, `known_commands`, the plain `punkboot_gethelp` text, and a project-version bump (make.tcl interface is product surface per root AGENTS.md). Layout make.tcl copies pick the change up via `make.tcl bootsupport` - do not hand-sync them.
- make.tcl subcommands and flags are punk::args-declared (G-030): `tclsh src/make.tcl help ?subcommand?` or `<subcommand> -help` shows tabled usage; invalid arguments produce punk::args usage errors. The dispatch degrades to plain scan/help when the bootsupport punk::args (or its rendering stack) is stale or unavailable; `PUNKBOOT_PLAIN=1` forces the degraded mode for troubleshooting. The `::punkboot::argdoc` definitions are constructed (string-built) and use the G-045 authoring mechanisms — block-form help values + `@normalize`, explicit `@form -synopsis` — see src/modules/AGENTS.md "punk::args definition authoring ergonomics" before editing them or authoring similar definitions.
- Uncommitted `make.tcl`-generated outputs may be batched into one commit, regardless of how many `make.tcl` invocations produced them. This covers punkcheck-managed build outputs that are VCS-tracked: `src/bootsupport/`, `_vfscommon.vfs/modules` + `/lib`, declared per-kit `*.vfs/lib_tcl<N>/<pkg>` subfolders, the thin-layout sync copies (`src/project_layouts/vendor/punk/<layout>/src/{make.tcl,build.tcl}` + bootsupport manifest copies), and the module-shipped layout payload under `src/modules/punk/mix/#modpod-templates-*/templates/project_layouts/` (G-087: synced from `src/project_layouts` - never hand-edit the modpod copy). (Root `modules/`, `lib/`, `modules_tcl<N>/`, `lib_tcl<N>/` are gitignored and never committed.) Each module's old→new version rename is atomic; a regeneration reflects one build-output refresh, not independent hand-edits. This is a carve-out from generic "split aggressively" commit guidance for punkcheck-managed build outputs only — it does not cover user-curated VFS content (`_config/`, per-kit `*.vfs/` outside declared `lib_tcl<N>` subfolders, `_aside/`, `mkzipfix.vfs`, `_vfscommon.vfs/doc`), which commits separately per its own concerns.
- **Boot-precondition gate (G-125).** A bake refuses any kit whose merged `.vfs` supplies no tcl library: the kit is listed under `FAILED KITS` with a reason naming the cause, and NOTHING is written - no `src/_build/<kit>`, no deploy, and the previously deployed `bin/<kit>` is left byte-identical. This is deliberately a failure rather than a warning: the deploy step deletes the old kit before copying the new one, so the pre-gate behaviour replaced a working shell with an artifact that died at startup with `application-specific initialization failed: Cannot find a usable init.tcl`. The check is structural and non-executing (so it covers cross-target kits too): `tcl_library/init.tcl` or `lib/tcl<major>.<minor>/init.tcl`, plus one companion file a real Tcl library carries beside it (`tm.tcl`, `package.tcl`, `auto.tcl`, `clock.tcl`, `history.tcl`, `word.tcl`) or an `encoding/` directory - without that second test the `lib/BWidget1.10.1/init.tcl` every punkshell kit carries would answer for a tcl library it cannot provide. It reads the MERGED tree rather than asking whether extraction ran, so a `.vfs` that legitimately supplies its own tcl library (`src/vfs/punk8_statictwapi.vfs`, `src/vfs/punk9test.vfs`) builds and deploys unchanged. **Remedy when a kit is refused:** the usual cause is that the runtime's own payload could not be extracted - read the recapped `BUILD-WARNING` naming what was tried, then either fix extraction (a driving tclsh with no zipfs needs punk::zip >= 0.2.0 in bootsupport - see the extraction bullet above), point the kit at a runtime that carries a payload, or have the kit's `.vfs` supply a tcl library itself. The predicate is `punkboot::utils::vfs_boot_library_report` called through a guarded require, exactly as the dirty-src provenance check is: a stale or missing bootsupport snapshot degrades the gate to a `NOTE` rather than failing every kit. `tclsh src/make.tcl check` reports whether the gate is `ACTIVE` or `UNAVAILABLE`.
- Kit bakes consume the punk-runtime WORKING COPIES under `bin/runtime/<platform>/`: `make.tcl bake`/`bakehouse` emit a `BUILD-WARNING:` (recapped at end of run) when a wrapped runtime's beside-toml revision is older than an `-r<N>` artifact present in the same folder - the forgot-to-switch-back guard for deliberate `punk-runtime use <old-rN>` testing excursions. Heed it before trusting freshly baked family kits; `bin/punk-runtime.cmd use <artifact-r<N>>` (`.exe` suffix optional) re-materializes the working copy.
- `tclsh src/make.tcl bakelist ?kitname ...?` (G-121) reports the kit outputs configured in `src/runtime/mapvfs.config`: kit name, kit type, runtime (with presence in the runtime store), vfs folder, and the deployed state of `bin/<kit>` vs the `src/_build` build product (`current|stale|absent|nobuild`), with anomalies (`runtime=missing`, `vfs=missing`, `rtrev=r<cur><r<max>` materialization staleness) and the cross-target marker `target=<platform>` in a trailing notes column. A nonexistent store tier is flagged loudly per tier actually referenced (header `(FOLDER MISSING)` for the default tier + stderr derivation; bake's no-runtimes exit self-diagnoses the same way). Name arguments filter the report and add a per-kit detail block (resolved store tier, target and provenance of the target, paths/sizes/mtimes). `make.tcl bake ?kitname ...?` bakes and deploys only the named kits - other kits' `_build`/`bin` artifacts and punkcheck records are untouched and the vfslibs phase narrows to the named kits' vfs folders; an unknown name errors before any build, listing the configured names; flags go before kit names (`make.tcl bake -confirm 0 punk91`). Bare `bake` processes all configured kits as before. Both surfaces consume the shared parsed-mapping helpers (`::punkboot::lib::mapvfs_*`) rather than the file format, so the G-024 config-format work swaps the reader underneath. Piped characterization: `src/tests/shell/testsuites/punkexe/maketclbakelist.test`.
- **Host vs target platform (G-122).** Everything a bake EMITS is keyed by the artifact's TARGET platform, never by the driving tclsh's personality: the `bin/runtime/<tier>` store the runtime is read from, `.exe` suffixing of runtime files and kit outputs, the presence checks behind `runtime=missing`, and whether the pre-deploy process sweep uses `tasklist`/`taskkill` or `ps`/`kill`. Host semantics - copy commands, path handling, filesystem case rules, prompts - stay keyed to the host. The default target is the host's own platform canon, EXCEPT a cygwin-family host (an msys2/cygwin-runtime tclsh, which reports `tcl_platform(platform)` `unix` on windows and canonizes as `msys-x86_64`/`cygwin-x86_64`), which targets `win32-x86_64`: such a host now drives the identical kit set, names and store addressing as a native tclsh. `tclsh src/make.tcl check` prints the derivation on one `platform (G-122): host=... target=... store=... exe-suffix=... process-tooling=...` line. A mapvfs entry may declare its own target platform (4th element - see `src/runtime/AGENTS.md`), which is then read from that platform's tier, named with that platform's executable convention, and skipped by the process sweep (its processes are not visible to this host). Traps this closes, all field-verified 2026-07-26: msys `ps` cannot see a natively-launched kit (only `tasklist` can), and msys2 rewrites arguments that look like absolute posix paths when spawning a native windows program - so `taskkill /PID <n>` arrived as `taskkill C:/<msysroot>/PID <n>`; native-windows command lines therefore go through `::punkboot::exec_nativeargs` (sets `MSYS2_ARG_CONV_EXCL=*`, inert elsewhere). Also note a cygwin-family tclsh is a POSIX Tcl: name the script the way that shell spells paths (`tclsh /c/repo/.../src/make.tcl`, not `C:/repo/...`, which it resolves relative to the cwd) - make.tcl diagnoses the windows-spelling case. Piped characterization: `src/tests/shell/testsuites/punkexe/maketclplatform.test` (the cygwin-host tests self-gate on finding and probing a real msys/cygwin tclsh; `PUNK_MSYS_TCLSH` names one explicitly).

91
src/make.tcl

@ -2281,6 +2281,12 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase)
| copy common, then merge overlay over it
v
src/_build/<kit>.exe.vfs
|
| boot-precondition gate: merged vfs must supply a tcl
| library (tcl_library/ or lib/tcl<M>.<m>/) [K10]
v
(gate) --> refused: kit listed under FAILED KITS, nothing
| built, nothing deployed, bin/<kit> untouched
|
| zip image appended to the runtime exe: tcl::zipfs::mkimg,
| or (driving tcl without zipfs, e.g 8.6) raw-runtime split
@ -2354,6 +2360,21 @@ KEY / NOTES
'make.tcl check' prints the derivation; 'make.tcl bakelist' shows per-kit
targets, with a target= note on rows that are not on the default target.
[K10] BOOT-PRECONDITION gate (G-125). A kit whose merged vfs has no tcl library
cannot initialise at all ('Cannot find a usable init.tcl'), so the bake
refuses it: the kit is listed under FAILED KITS and NOTHING is written -
no src/_build/<kit>, no deploy, and the previously deployed bin/<kit> is
left exactly as it was. This is deliberately not a warning: the deploy step
deletes the old kit before copying the new one, so warning-and-proceeding
replaced a working shell with one that could not start. The check is
structural (tcl_library/init.tcl or lib/tcl<major>.<minor>/init.tcl, plus a
companion file so a package's own init.tcl cannot answer for a tcl library)
and runs on the MERGED tree, not on whether extraction ran - a .vfs that
supplies its own tcl library builds and deploys as normal. Nothing is
executed, so cross-target kits are covered too. Usual cause: the runtime's
own payload could not be extracted - see the BUILD-WARNING naming what was
tried. 'make.tcl check' reports whether the gate is ACTIVE.
MAINTENANCE (agents take note)
----------------------------------------------------------------------------
This text is embedded in make.tcl (::punkboot::workflow_text). When the build
@ -3264,6 +3285,21 @@ proc ::punkboot::get_src_provenance_warnings {projectroot label} {
set checked_vcs_roots [dict create]
return [list 1 [::punkboot::utils::vcs_dirty_warnings $projectroot checked_vcs_roots $label src]]
}
#Availability probe + boot-precondition check of an assembled kit vfs (G-125), shared by the
#kit gate in the bake loop and the 'check' command report. Same guarded-require treatment as
#the provenance check above: a stale or missing punkboot::utils snapshot must not brick the
#commands used to repair it, so an unavailable check degrades to a notice rather than failing
#every kit. Returns {available report}.
proc ::punkboot::get_vfs_boot_library_report {vfsfolder} {
set available [expr {\
![catch {package require punkboot::utils}]\
&& [llength [info commands ::punkboot::utils::vfs_boot_library_report]]\
}]
if {!$available} {
return [list 0 [dict create]]
}
return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]]
}
#Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter
#which of make.tcl's many exit points a command takes.
if {![llength [info commands ::punkboot::exit_original]]} {
@ -3506,6 +3542,19 @@ if {$::punkboot::command eq "check"} {
puts stdout " build/promotion commands (project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate) will WARN as above and proceed."
puts stdout " With -dirty-abort they would abort. To evaluate uncommitted source without building, use '<builtexe> src' or '<builtexe> src shell'."
}
# Kit boot-precondition gate status - whether a bake can refuse an unbootable kit
puts stdout $sep
#availability probe only - the path deliberately does not exist
lassign [::punkboot::get_vfs_boot_library_report [file join $projectroot __punkboot_bootgate_probe__]] _boot_available _boot_report
if {$_boot_available} {
puts stdout "boot-precondition gate (G-125): ACTIVE"
puts stdout " bake checks each kit's merged vfs for a tcl library (tcl_library/ or lib/tcl<major>.<minor>/) before"
puts stdout " building it. A kit with none is reported under FAILED KITS and is neither built nor deployed, so a"
puts stdout " previously deployed kit is never replaced by an artifact that cannot initialise."
} else {
puts stdout "boot-precondition gate (G-125): UNAVAILABLE (punkboot::utils vfs_boot_library_report not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot refuse an unbootable kit - run 'make.tcl modules' then 'make.tcl bootsupport'."
}
puts stdout $sep
exit 0
}
@ -6047,13 +6096,15 @@ foreach vfstail $vfs_tails {
}
} ;#end while !$extraction_done && [llength $extraction_trylist]
set extraction_tried "$target_kit_type[expr {[llength $source_kit_caps] ? " + runtime caps $source_kit_caps" : ""}]"
if {!$extraction_done} {
#TODO: if not extracted - use a default tcl_library for patchlevel and platform?
#Recapped BUILD-WARNING (not just an inline note): the build continues
#and produces an artifact, but unless the .vfs itself supplies a
#tcl_library that artifact cannot boot - and this line would otherwise
#scroll away behind thousands of merge lines.
::punkboot::print_build_warnings [list "No extraction done from runtime $runtime_fullname (tried: $target_kit_type[expr {[llength $source_kit_caps] ? " + runtime caps $source_kit_caps" : ""}]) - kit $targetkit gets no tcl_library from the runtime and will not initialise unless src/vfs/$vfstail supplies one"]
#Recapped BUILD-WARNING (not just an inline note): this line would
#otherwise scroll away behind thousands of merge lines. It is a warning
#rather than the failure itself because a .vfs that supplies its own tcl
#library needs no extraction - the G-125 gate below decides, on the
#merged tree, whether this kit can initialise.
::punkboot::print_build_warnings [list "No extraction done from runtime $runtime_fullname (tried: $extraction_tried) - kit $targetkit gets no tcl library from the runtime, so it will FAIL the boot-precondition gate unless src/vfs/$vfstail supplies one"]
file mkdir $targetvfs
}
@ -6111,6 +6162,36 @@ foreach vfstail $vfs_tails {
set sourcevfs [file join $sourcefolder vfs $vfstail]
merge_over $sourcevfs $targetvfs
#G-125 boot-precondition gate. A kit with no tcl library cannot initialise
#at all - 'Cannot find a usable init.tcl' - so refuse it HERE: before any
#build product is written, and long before the deploy step deletes the
#previously working bin/<kit> and copies the new one over it. The check is
#on the MERGED tree rather than on whether extraction ran, because a .vfs
#that supplies its own tcl library is legitimate and must keep building
#(src/vfs/punk8_statictwapi.vfs and punk9test.vfs are such folders today).
#Structural and non-executing, so it also covers cross-target kits, which
#this host could never run.
lassign [::punkboot::get_vfs_boot_library_report $targetvfs] bootcheck_available bootcheck
if {!$bootcheck_available} {
puts stderr "NOTE: kit boot-precondition check unavailable (punkboot::utils vfs_boot_library_report not loadable from bootsupport) - continuing without it"
} elseif {![dict get $bootcheck ok]} {
if {$extraction_done} {
#extract_kit_type is only recorded by some of the extraction branches
set extractedas [expr {$extract_kit_type ne "" ? " (as $extract_kit_type)" : ""}]
set gatecause "extraction from runtime $runtime_fullname$extractedas ran but supplied no tcl library, and src/vfs/$vfstail does not supply one either"
} else {
set gatecause "nothing was extracted from runtime $runtime_fullname (tried: $extraction_tried) and src/vfs/$vfstail does not supply a tcl library"
}
set failmsg "kit cannot initialise - $gatecause. Checked [join [dict get $bootcheck checked] { and }] in the merged vfs; [dict get $bootcheck reason]. NOT BUILT and NOT DEPLOYED: any previously deployed $targetkit is untouched"
puts stderr "boot-precondition gate FAILED for kit $targetkit - refusing to build an artifact that cannot initialise"
puts stderr " $gatecause"
lappend failed_kits [list kit $targetkit reason $failmsg]
$vfs_event targetset_end FAILED
$vfs_event destroy
$vfs_installer destroy
continue
}
set wrapvfs $targetvfs
switch -- $target_kit_type {
zip {

99
src/modules/punkboot/utils-999999.0a1.0.tm

@ -23,7 +23,7 @@ package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_boot_library_report
namespace eval argdoc {
@ -227,6 +227,103 @@ namespace eval punkboot::utils {
}
return $warnings
}
#Companion files of a real Tcl library directory. init.tcl sources these, so a
#directory holding init.tcl with none of them beside it is a package's own init
#script (lib/BWidget1.10.1/init.tcl and friends), not a tcl library.
variable boot_library_companions {tm.tcl package.tcl auto.tcl clock.tcl history.tcl word.tcl}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_boot_library_report
@cmd -name "::punkboot::utils::vfs_boot_library_report"\
-summary\
"Report whether an assembled vfs tree can supply a bootable tcl library"\
-help\
"Structural check of a merged .vfs tree for the precondition a kit
needs to initialise at all: a Tcl library the runtime can find. Reads
the directory only - nothing is executed, decompressed or launched, so
a caller such as make.tcl can run it for every kit on every bake and
for a cross-target kit it could never run itself.
Two conventions are recognised, both observed in this project's kits:
tcl_library/ zipfs-attached kits (tclZipfs looks for
<mount>/tcl_library/init.tcl)
lib/tcl<M>.<m>/ starkit/tclkit-style kits (lib/tcl8.6,
lib/tcl9.0)
A candidate directory qualifies only when it holds init.tcl AND at
least one companion file a real Tcl library carries beside it
(tm.tcl, package.tcl, auto.tcl, clock.tcl, history.tcl, word.tcl) or
an encoding/ subdirectory. Without that second test a package's own
init script - lib/BWidget1.10.1/init.tcl, present in every punkshell
kit - would answer for a tcl library it cannot provide.
Returned dict keys:
ok 1 when at least one qualifying library was found
locations qualifying directories, relative to vfsfolder
rejected candidate directories that held init.tcl but no
companion, relative to vfsfolder - the near-misses
worth naming when a gate fires unexpectedly
checked the location patterns examined, relative to vfsfolder
reason why ok is 0; empty string when ok is 1"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the assembled vfs tree to inspect"
}]
}
proc vfs_boot_library_report {vfsfolder} {
variable boot_library_companions
set checked [list tcl_library lib/tcl<M>.<m>]
set report [dict create ok 0 locations {} rejected {} checked $checked reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report reason "no such directory: $vfsfolder"
return $report
}
#candidates are the two conventions only - a full tree walk would find every
#package's init.tcl and cost more than the check is worth
set candidates [list [file join $vfsfolder tcl_library]]
foreach dir [lsort [glob -nocomplain -type d -directory [file join $vfsfolder lib] -- {tcl[0-9]*}]] {
lappend candidates $dir
}
set locations [list]
set rejected [list]
foreach dir $candidates {
if {![file isfile [file join $dir init.tcl]]} {
continue
}
set qualifies 0
foreach companion $boot_library_companions {
if {[file isfile [file join $dir $companion]]} {
set qualifies 1
break
}
}
if {!$qualifies && [file isdirectory [file join $dir encoding]]} {
set qualifies 1
}
set rel [string range $dir [expr {[string length $vfsfolder] + 1}] end]
if {$qualifies} {
lappend locations $rel
} else {
lappend rejected $rel
}
}
if {[llength $locations]} {
dict set report ok 1
dict set report locations $locations
dict set report rejected $rejected
return $report
}
dict set report rejected $rejected
if {[llength $rejected]} {
dict set report reason "no tcl library in [file tail $vfsfolder]: [join $rejected {, }] hold init.tcl but none of the companion files a tcl library carries beside it ([join $boot_library_companions {, }]) nor an encoding/ directory"
} else {
dict set report reason "no tcl library in [file tail $vfsfolder]: neither tcl_library/init.tcl nor lib/tcl<major>.<minor>/init.tcl is present"
}
return $report
}
}
namespace eval ::punk::args::register {

3
src/modules/punkboot/utils-buildversion.txt

@ -1,6 +1,7 @@
0.2.0
0.3.0
#First line must be a semantic version number
#all other lines are ignored.
#0.3.0 - added vfs_boot_library_report (structural boot-precondition check of an assembled .vfs tree; backs make.tcl's G-125 unbootable-kit deploy gate)
#0.2.0 - vcs_dirty_warnings: optional scope arg limiting which uncommitted changes count (e.g scope src for make.tcl build/promotion dirty-src gate)
#0.1.1 - added vcs_dirty_warnings (dirty fossil/git checkout provenance warnings; used by make.tcl vendorupdate, shared with bootsupport update per goal G-026)
#0.1.0 - initial: parse_punkproject_version, read_punkproject_version, read_changelog_latest_version extracted from make.tcl inline procs

1
src/tests/modules/AGENTS.md

@ -38,6 +38,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
## Child DOX Index
- `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 build-helper module. `utils.test` (punkproject.toml/CHANGELOG version parsing), `vcsdirty.test` (dirty fossil/git provenance warnings behind the build 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/_build/*.vfs` tree still passes so the gate cannot fail kits that boot today). All three are pure fixture tests - no build is run; the make.tcl side of the gate is pinned separately in `shell/testsuites/punkexe/maketclbootgate.test`
- `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)

166
src/tests/modules/punkboot/utils/testsuites/utils/bootlibrary.test

@ -0,0 +1,166 @@
# -*- tcl -*-
# Tests for punkboot::utils::vfs_boot_library_report - the structural boot-precondition
# check behind make.tcl's G-125 unbootable-kit deploy gate:
# - both tcl-library conventions this project's kits use (tcl_library/ for zipfs-attached
# kits, lib/tcl<major>.<minor>/ for starkit-style kits)
# - the companion-file requirement that keeps a package's own init.tcl (every punkshell
# kit carries lib/BWidget1.10.1/init.tcl) from answering for a tcl library
# - near-miss reporting, missing/empty trees, and the real built vfs trees in src/_build
# when a build has been run on this checkout
# Directory fixtures only - the check never executes or launches anything, which is what
# makes it usable for cross-target kits.
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkboot/utils/*** bootlibrary.test
package require tcltest
package require punkboot::utils
#added 2026-07-27 (agent, G-125)
namespace eval ::testspace {
namespace import ::tcltest::*
variable BASE [makeDirectory g125_bootlibrary]
#<projectroot>/src/tests/modules/punkboot/utils/testsuites/utils -> 7 levels up
variable projectroot [file normalize [file join [file dirname [info script]] .. .. .. .. .. .. ..]]
variable buildfolder [file join $projectroot src _build]
testConstraint builtvfsavailable [expr {[llength [glob -nocomplain -type d -directory $buildfolder *.vfs]] > 0}]
proc mkfile {path} {
file mkdir [file dirname $path]
set fd [open $path w]
puts -nonewline $fd "#fixture\n"
close $fd
}
#build a fresh fixture tree under BASE and return its path
proc mktree {name args} {
variable BASE
set root [file join $BASE $name]
file delete -force $root
file mkdir $root
foreach rel $args {
if {[string index $rel end] eq "/"} {
file mkdir [file join $root [string trimright $rel /]]
} else {
mkfile [file join $root $rel]
}
}
return $root
}
proc report {root} {
return [punkboot::utils::vfs_boot_library_report $root]
}
variable common {
set result [list]
}
# -- --- --- the two conventions --- --- --
test bootlib_zipfs_convention {a zipfs-style tcl_library with a companion file qualifies}\
-setup $common -body {
set r [report [mktree zipfs tcl_library/init.tcl tcl_library/tm.tcl main.tcl]]
lappend result [dict get $r ok] [dict get $r locations] [dict get $r rejected] [dict get $r reason]
}\
-result {1 tcl_library {} {}}
test bootlib_starkit_convention {a starkit-style lib/tcl<major>.<minor> with a companion file qualifies}\
-setup $common -body {
set r [report [mktree starkit lib/tcl8.6/init.tcl lib/tcl8.6/package.tcl main.tcl]]
lappend result [dict get $r ok] [dict get $r locations]
}\
-result {1 lib/tcl8.6}
test bootlib_encoding_directory_qualifies {an encoding/ subdirectory beside init.tcl is accepted in place of a companion file}\
-setup $common -body {
set r [report [mktree encdir tcl_library/init.tcl tcl_library/encoding/]]
lappend result [dict get $r ok] [dict get $r locations]
}\
-result {1 tcl_library}
test bootlib_both_conventions {a tree carrying both conventions reports both, tcl_library first}\
-setup $common -body {
set r [report [mktree both tcl_library/init.tcl tcl_library/tm.tcl lib/tcl9.0/init.tcl lib/tcl9.0/auto.tcl]]
lappend result [dict get $r ok] [dict get $r locations]
}\
-result {1 {tcl_library lib/tcl9.0}}
test bootlib_reports_checked_locations {the report names the locations it examined whether or not it found one}\
-setup $common -body {
lappend result [dict get [report [mktree checked1 main.tcl]] checked]
lappend result [dict get [report [mktree checked2 tcl_library/init.tcl tcl_library/tm.tcl]] checked]
}\
-result {{tcl_library lib/tcl<M>.<m>} {tcl_library lib/tcl<M>.<m>}}
# -- --- --- what must NOT qualify --- --- --
test bootlib_package_init_decoy {a package's own init.tcl under lib/ does not answer for a tcl library}\
-setup $common -body {
#every punkshell kit carries these - they are the reason the check cannot
#simply look for an init.tcl anywhere in the tree
set r [report [mktree decoy lib/BWidget1.10.1/init.tcl lib/BWidget1.9.16/init.tcl main.tcl modules/]]
lappend result [dict get $r ok] [dict get $r locations] [dict get $r rejected]
lappend result [string match "*neither tcl_library/init.tcl nor lib/tcl<major>.<minor>/init.tcl*" [dict get $r reason]]
}\
-result {0 {} {} 1}
test bootlib_tcllib_is_not_a_tcl_library {lib/tcllib2.0 is not matched by the lib/tcl<major>.<minor> convention}\
-setup $common -body {
set r [report [mktree tcllib lib/tcllib2.0/init.tcl lib/tcllib2.0/tm.tcl]]
lappend result [dict get $r ok] [dict get $r rejected]
}\
-result {0 {}}
test bootlib_near_miss_is_named {init.tcl in the right place with no companion is rejected and reported as a near miss}\
-setup $common -body {
set r [report [mktree nearmiss tcl_library/init.tcl]]
lappend result [dict get $r ok] [dict get $r locations] [dict get $r rejected]
lappend result [string match "*hold init.tcl but none of the companion files*" [dict get $r reason]]
}\
-result {0 {} tcl_library 1}
test bootlib_empty_tree {a tree with no tcl library at all is refused with a reason}\
-setup $common -body {
set r [report [mktree emptytree]]
lappend result [dict get $r ok] [expr {[dict get $r reason] ne ""}]
}\
-result {0 1}
test bootlib_missing_directory {a nonexistent folder is refused by name rather than erroring}\
-setup $common -body {
variable BASE
set r [report [file join $BASE no_such_vfs]]
lappend result [dict get $r ok] [string match "no such directory:*" [dict get $r reason]]
}\
-result {0 1}
test bootlib_does_not_execute_or_write {the check writes nothing into the tree it inspects}\
-setup $common -body {
set root [mktree readonlycheck tcl_library/init.tcl tcl_library/tm.tcl main.tcl]
set before [lsort [glob -nocomplain -directory $root -tails *]]
report $root
set after [lsort [glob -nocomplain -directory $root -tails *]]
lappend result [expr {$before eq $after}] $before
}\
-result {1 {main.tcl tcl_library}}
# -- --- --- the trees the gate will actually see --- --- --
test bootlib_real_built_vfs_trees {every assembled kit vfs in src/_build passes the gate - the check must not fail kits that boot today}\
-constraints {builtvfsavailable} -setup $common -body {
variable buildfolder
set failures [list]
set checked 0
foreach d [lsort [glob -nocomplain -type d -directory $buildfolder *.vfs]] {
incr checked
set r [report $d]
if {![dict get $r ok]} {
lappend failures [file tail $d]
}
}
lappend result [expr {$checked > 0}] $failures
}\
-result {1 {}}
cleanupTests
}

1
src/tests/shell/AGENTS.md

@ -17,6 +17,7 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac
- `testsuites/punkexe/` targets a built punk executable: resolved from `env(PUNK_SHELL_TEST_EXE)`, else `<projectroot>/bin/punk902z.exe`, else `<projectroot>/bin/punkshell902`; tests auto-skip via the `punkexeavailable` constraint when none is found.
- Tests that spawn the built executable must be hang-proof: use the event-loop `punk_run` pattern (pipe stdin, half-close for EOF, timeout then force-kill) rather than plain `exec`, because known failure modes leave the child waiting on a reopened console.
- Three punkexe suites drive the WORKING TREE's `src/make.tcl` through the built executable's `script` subcommand with output captured through a pipe, pinning its reporting surfaces rather than running builds: `maketclcolour.test` (G-113 piped colour policy), `maketclbakelist.test` (G-121 kit matrix report + the selective-bake no-build guard) and `maketclplatform.test` (G-122 host/target platform split - the `check` derivation line, per-kit store tier/target, and cross-target artifact naming). Their per-kit row expectations are characterization of the current `src/runtime/mapvfs.config` matrix, so a deliberate config change legitimately updates them. `maketclplatform.test` additionally carries self-gating cygwin-host tests: they discover an msys2/cygwin-runtime tclsh (`env(PUNK_MSYS_TCLSH)`, else the usual msys2/cygwin roots) and PROBE it - a candidate only counts when it really reports `tcl_platform(platform)` `unix` with a cygwin-family os string - then assert that host derives the same win32 target, store and kit set as the native run. Such a tclsh is a POSIX Tcl: pass it a `/c/...`-spelled script path, never `C:/...`.
- `testsuites/punkexe/maketclbootgate.test` (G-125) pins the make.tcl side of the boot-precondition gate through the same piped `script` harness: `check` reports the gate as `ACTIVE` or `UNAVAILABLE` (the documented degradation when the bootsupport `punkboot::utils` snapshot lacks `vfs_boot_library_report`), names both tcl-library conventions, and states the never-deployed guarantee - ESC-free per the G-113 piped policy. The gate's VERDICT is characterized against directory fixtures in `modules/punkboot/utils/testsuites/utils/bootlibrary.test`; the predicate lives in `punkboot::utils` precisely so it is testable without running a bake.
- `testsuites/punkexe/shellexit.test` guards piped-stdin termination behaviour of the built executable (no `invalid thread handle` on shutdown races, no console-reopen hang on eof/error paths, exitcode propagation); do not weaken these tests - a red result means a regression in punk::repl shutdown or app-punkshell eof handling.
- `testsuites/punkexe/shellnavns.test` proves the namespace-navigation session contract end-to-end over a piped 'src'-mode session (dev modules - deliberate, so current punk::repl/nav source is exercised without a rebuild): n/ sets ns_current, the current namespace continues into `subshell punk` and back out (currently via the documented first-subshell shared-code-interp asymmetry - the SHARED marker pins it; the repl::start seeding template for genuinely separate interps is proven in modules/punk/repl nscurrent.test), the punk::repl 0.5.2 stray-namespace fix holds live, and the session exits cleanly. KNOWN GAP pinned there: piped submissions evaluate at :: despite ns_current (interactive and module-level inscope behaviour differ) - investigate alongside G-038.
- `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk.

124
src/tests/shell/testsuites/punkexe/maketclbootgate.test

@ -0,0 +1,124 @@
package require tcltest
#Piped characterization of the make.tcl side of the G-125 boot-precondition gate: a bake
#must refuse a kit whose merged vfs supplies no tcl library, rather than replacing a
#working bin/<kit> with an artifact that cannot initialise. Runs the WORKING TREE's
#src/make.tcl under the built punk executable's 'script' subcommand with output captured
#through a pipe, and pins:
# - 'check' reports the gate as ACTIVE when punkboot::utils in bootsupport carries
# vfs_boot_library_report, naming both tcl-library conventions and the never-deployed
# guarantee; the UNAVAILABLE branch is the documented degradation when the bootsupport
# snapshot is stale (same guarded-require treatment as the dirty-src provenance check)
# - the report stays ESC-free under the G-113 piped colour policy
#The gate's VERDICT - which trees pass and which are refused - is characterized directly
#against directory fixtures in modules/punkboot/utils/testsuites/utils/bootlibrary.test;
#the predicate lives in punkboot::utils precisely so it can be tested without a bake.
#
#Target executable resolved from env(PUNK_SHELL_TEST_EXE), else <projectroot>/bin/punk902z.exe
#then <projectroot>/bin/punkshell902. Skipped (constraint punkexeavailable) if none found.
namespace eval ::testspace {
namespace import ::tcltest::*
variable testdir [file dirname [file normalize [info script]]]
#<projectroot>/src/tests/shell/testsuites/punkexe -> 5 levels up to <projectroot>
variable projectroot [file normalize [file join $testdir .. .. .. .. ..]]
variable maketcl [file join $projectroot src make.tcl]
variable punkexe ""
if {[info exists ::env(PUNK_SHELL_TEST_EXE)] && $::env(PUNK_SHELL_TEST_EXE) ne ""} {
set punkexe [file normalize $::env(PUNK_SHELL_TEST_EXE)]
} else {
foreach candidate [list [file join $projectroot bin punk902z.exe] [file join $projectroot bin punkshell902]] {
if {[file exists $candidate]} {
set punkexe $candidate
break
}
}
}
testConstraint punkexeavailable [expr {$punkexe ne "" && [file exists $punkexe]}]
variable maketcl_run_timeout_ms 60000
variable runstate
array set runstate {}
proc maketcl_run_read {chan} {
variable runstate
append runstate(output) [read $chan]
if {[chan eof $chan]} {
chan event $chan readable {}
set runstate(done) eof
}
}
#Run <punkexe> script src/make.tcl <subcommand...> with output captured through a
#pipe (stdin half-closed for immediate EOF - make.tcl must never wait on stdin for
#these subcommands). Returns dict: timedout 0|1, exitcode <int|"">, output
#<combined stdout+stderr>.
proc maketcl_run {cmdargs} {
variable runstate
variable maketcl_run_timeout_ms
variable punkexe
variable maketcl
array unset runstate
set runstate(output) ""
set runstate(done) ""
set chan [open |[list $punkexe script $maketcl {*}$cmdargs 2>@1] r+]
chan configure $chan -blocking 0 -translation binary
catch {chan close $chan write} ;#no stdin for the child - immediate EOF
set timerid [after $maketcl_run_timeout_ms [list set [namespace current]::runstate(done) timeout]]
chan event $chan readable [list [namespace current]::maketcl_run_read $chan]
while {$runstate(done) eq ""} {
vwait [namespace current]::runstate(done)
}
after cancel $timerid
set timedout [expr {$runstate(done) eq "timeout"}]
set exitcode ""
if {$timedout} {
catch {exec {*}[auto_execok taskkill] /F /T /PID [lindex [pid $chan] 0]}
catch {chan close $chan}
} else {
chan configure $chan -blocking 1
if {[catch {chan close $chan} errdata errdict]} {
set exitcode [lindex [dict get $errdict -errorcode] end]
} else {
set exitcode 0
}
}
return [dict create timedout $timedout exitcode $exitcode output $runstate(output)]
}
#added 2026-07-27 (agent, G-125)
test maketcl_check_bootgate_status {make.tcl check reports the boot-precondition gate with one of the two documented statuses, ESC-free}\
-constraints {punkexeavailable} -body {
set r [maketcl_run {check}]
set out [dict get $r output]
set result [list]
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode]
lappend result esc [regexp -all {\x1b} $out]
lappend result line [regexp -line {^boot-precondition gate \(G-125\): (ACTIVE|UNAVAILABLE)} $out]
set result
}\
-result {timedout 0 exitcode 0 esc 0 line 1}
#added 2026-07-27 (agent, G-125)
test maketcl_check_bootgate_active_contract {with a current bootsupport snapshot the gate is ACTIVE and states both conventions and the never-deployed guarantee}\
-constraints {punkexeavailable} -body {
set r [maketcl_run {check}]
set out [dict get $r output]
set result [list]
lappend result active [regexp -line {^boot-precondition gate \(G-125\): ACTIVE\s*$} $out]
#the two tcl-library conventions the gate accepts
lappend result conventions [regexp {tcl_library/ or lib/tcl<major>\.<minor>/} $out]
#the contract that motivates the goal: refused kits are neither built nor deployed
lappend result failedkits [regexp {FAILED KITS} $out]
lappend result neverreplaced [regexp {never replaced by an artifact that cannot initialise} $out]
set result
}\
-result {active 1 conventions 1 failedkits 1 neverreplaced 1}
cleanupTests
}
namespace delete ::testspace
Loading…
Cancel
Save