Browse Source

Project-version bake gate - bump-before-bake machine-enforced (punkshell 0.56.1)

The project version is a bake input (kit stamp punkkit-stamp.toml
project_version + versioned-scheme output names), but nothing enforced the
step (2) bump -> step (8) bake order; a 2026-08-06 session baked at 0.55.1,
bumped to 0.56.0 and paid a full 11-kit re-bake. Every other release-sequence
edge already had a mechanical gate (provenance, bootsupport staleness, boot
precondition) - this was the un-gated one.

- ::punkboot::get_projectversion_status: shared staleness/consistency
  computation (projectversion advisory, check report, gate); states
  ok/overdue/bump_uncommitted/unavailable.
- project-version gate on bake/bakehouse/bin: a pending-looking bump (src/
  commits since punkproject.toml last changed, none staged) prompts y/N on a
  terminal and aborts fast non-interactively unless -expect-projectversion
  asserts the intended version. -confirm 0 deliberately does not waive the
  gate. A staged-but-uncommitted bump passes (bake reads the working-tree
  value); degraded bootsupport reduces the gate to a NOTE.
- -expect-projectversion <ver> (bake/bakehouse/bin argdocs + degraded scan):
  assertion verified in every state - a mismatch with punkproject.toml always
  aborts before any kit work; unavailable status aborts a supplied assertion
  as unverifiable rather than silently losing the requested strictness.
- projectversion overdue warning now states the consequence and prints the
  ready-made assertion command; check reports the gate state and current
  staleness verdict; workflow text ([K2] bake-input + gate, [K6] no-waiver
  exception, steps (2)/(8)) and gethelp/HELPTEXTS/OPT_CONFIRM document it;
  root AGENTS.md gains "The version is a bake input (bump BEFORE bake)" +
  an Enforcement gate paragraph; src/AGENTS.md closeout bullet extended.
- maketclprojectversion.test: piped characterization (8 tests) - check
  report, advisory read-only, and git-fixture verdicts for pending-abort,
  assert-match proceed, assert-mismatch abort, bump-staged proceed, no-git
  NOTE. maketclhelp.test bakehouse-synopsis pin updated for the new flag.
- Thin-layout + templates-modpod make.tcl copies synced via make.tcl modules.

Claude-Session: https://claude.ai/code/session_01EukjNTv2r5msFgHzWgaCdS
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
2dbf635b99
  1. 6
      AGENTS.md
  2. 23
      CHANGELOG.md
  3. 2
      punkproject.toml
  4. 2
      src/AGENTS.md
  5. 335
      src/make.tcl
  6. 335
      src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl
  7. 335
      src/project_layouts/vendor/punk/basic/src/make.tcl
  8. 335
      src/project_layouts/vendor/punk/project-0.1/src/make.tcl
  9. 4
      src/tests/shell/testsuites/punkexe/maketclhelp.test
  10. 284
      src/tests/shell/testsuites/punkexe/maketclprojectversion.test

6
AGENTS.md

@ -160,6 +160,10 @@ The `make.tcl` command interface is part of the product surface: new subcommands
Every version bump must add a corresponding entry to `CHANGELOG.md` at the repo root. The latest `## [X.Y.Z]` header in that file must match the `version` field in `punkproject.toml`. Entries are newest-first, one bullet per notable change.
### The version is a bake input (bump BEFORE bake)
`make.tcl bake` reads `punkproject.toml` at bake time: the version is stamped into every kit's `punkkit-stamp.toml` (`project_version`, surfaced by `<kit> buildinfo`) and derives the `versioned`-scheme output names. The bump therefore belongs BEFORE the bake — release-sequence step (2) vs step (8) in `make.tcl workflow` is a data dependency, not a convention. Bumping after a bake mis-stamps every kit and costs a full re-bake to correct. Do not invert the documented order on a speculative concern without falsifying the concern first (the 2026-08-06 inversion feared `versioned`-scheme name churn that `bakelist` already showed to be moot — `default=no` entries are skipped by a bare bake).
### Enforcement
`tclsh src/make.tcl projectversion` is a read-only check (never aborts, only warns) that:
@ -168,6 +172,8 @@ Every version bump must add a corresponding entry to `CHANGELOG.md` at the repo
The check is advisory — it catches forgotten bumps, not hard errors. Agents should run it during closeout for any `src/` change that ships behaviour, alongside existing `make.tcl` verification.
The same computation gates the kit-assembling commands (`bake`, `bakehouse`, `bin`) as the **project-version gate** (2026-08-06): when a bump looks pending (`src/` commits since `punkproject.toml` last changed, none staged), an interactive terminal run is prompted (y/N, default No) and a non-interactive run aborts fast unless the intended version is asserted with `-expect-projectversion <ver>`. A supplied assertion is verified in every state — a mismatch with `punkproject.toml` always aborts before any kit work. `-confirm 0` deliberately does NOT waive this gate: agent runs pass `-confirm 0` habitually, and the gate exists to interrupt habit with a checked claim (a version to assert, not a boolean to wave through). A staged-but-uncommitted bump passes the gate — bake reads the working-tree value. `tclsh src/make.tcl check` reports the gate state and the current staleness verdict.
### Relationship to module versions
The project version is fully independent of module versions. A module bump (even a major one) does not force a project bump unless the module's change is itself user-visible at the shell level. Conversely, a project bump does not require bumping any module.

23
CHANGELOG.md

@ -5,6 +5,29 @@ 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.56.1] - 2026-08-06
- `make.tcl` project-version bake gate: the kit-assembling subcommands (`bake`,
`bakehouse`, `bin`) now treat the project version as the bake input it is
(stamped into every kit's `punkkit-stamp.toml` and used for versioned-scheme
output names). A new `-expect-projectversion <ver>` flag asserts the version
a bake is intended to ship - a mismatch with `punkproject.toml` aborts before
any kit work. When a bump looks pending (src/ commits since
`punkproject.toml` last changed, none staged), an interactive run is prompted
(y/N) and a non-interactive run aborts fast unless the version is asserted;
`-confirm 0` deliberately does not waive the gate (agent runs pass it
habitually - the gate interrupts habit with a checked claim). A
staged-but-uncommitted bump passes (bake reads the working-tree value);
degraded bootsupport reduces the gate to a NOTE, but a supplied assertion
then aborts as unverifiable. Motivated by the 2026-08-06 bake-then-bump
inversion, which stamped 0.55.1 into 11 kits and cost a full re-bake.
- `make.tcl projectversion` overdue warning now states the consequence and
prints the ready-made assertion command; `make.tcl check` reports the gate
state and current staleness verdict; the workflow text ([K2]/[K6],
release-sequence steps (2)/(8)) and the help surfaces document the ordering
and the flag. Root and src `AGENTS.md` "Project Versioning" guidance updated
to state the bake-input dependency and the bump-BEFORE-bake order.
## [0.56.0] - 2026-08-06
- Coherent-set promotion + bake (the G-169 and G-170 follow-on promotion lines,

2
punkproject.toml

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

2
src/AGENTS.md

@ -96,7 +96,7 @@ Recovery after a wrong path guess:
- When touching VFS payloads, describe regeneration steps in durable docs if the workflow changes.
- When a tool summary reports cleanup issues such as `git diff --check` whitespace errors, verify against exact command output and exit code before running secondary scans or reporting the issue.
- `make.tcl` performs version-aware bootsupport staleness detection: major bumps abort, minor bumps prompt (y/N; `-confirm 0` proceeds without prompting, non-interactive stdin without it aborts fast), patch bumps warn-and-proceed. See `src/bootsupport/AGENTS.md` "Bootsupport Staleness Handling" for the full contract and the version-bump discipline agents must follow.
- Project-level version bumps are governed by the root `AGENTS.md` "Project Versioning" section. Any `src/` change that ships user-visible shell behaviour requires a `punkproject.toml` version bump and a `CHANGELOG.md` entry as part of the DOX closeout pass. Run `tclsh src/make.tcl projectversion` to verify.
- Project-level version bumps are governed by the root `AGENTS.md` "Project Versioning" section. Any `src/` change that ships user-visible shell behaviour requires a `punkproject.toml` version bump and a `CHANGELOG.md` entry as part of the DOX closeout pass. Run `tclsh src/make.tcl projectversion` to verify. The project version is a BAKE INPUT (stamped into every kit's `punkkit-stamp.toml` as `project_version` and used for `versioned`-scheme output names), so the bump comes BEFORE any bake - bumping after mis-stamps every kit and costs a full re-bake. `bake`/`bakehouse`/`bin` hold the project-version gate: with a pending-looking bump, non-interactive runs abort fast unless the intended version is asserted with `-expect-projectversion <ver>` (a mismatched assertion always aborts; `-confirm 0` deliberately does not waive the gate - agent runs pass `-confirm 0` habitually).
- Exit-path work, qualified by current upstream reality (recorded 2026-07): Tcl core's exit/finalization sequence is not a documented, guaranteed order, and Tcl 9.0.x (buildsuite target 9.0.5) still carries the busy-thread finalize crash (tcl ticket e55a589d2b - partial fix is trunk-only). The responsible maintainer's stated position (2026-05/07) is that a complete fix between non-cooperating threads is not expected. The rules below match THAT reality and are not permanent policy - re-evaluate them as Tcl core finalization work lands: (a) do real cleanup while interps are fully alive - never rely on exit handlers or Tcl_Finalize ordering to finish business (the app-level ordered teardown this rule exists to protect is described in root `ARCHITECTURE.md` "Process exit and shutdown"); (b) treat any extension registering exit/thread-exit handlers (the tcludp UDP_ExitProc class, see G-036) as suspect until audited for what it closes and which threads are still alive when it fires; (c) at/after exit, assume channels are already dead (per Exit.3, I/O vanishes) and that thread-local storage may already be finalized - exit-path code must not allocate Tcl_Objs, perform channel I/O, or touch other threads' state.
## Verification

335
src/make.tcl

@ -2589,16 +2589,20 @@ proc ::punkboot::punkboot_gethelp {args} {
append h "" \n
append h " $scriptname help or just $scriptname" \n
append h " - This help." \n \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0?" \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0? ?-expect-projectversion <ver>?" \n
append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to <projectdir>/bin" \n
append h " - refuses uncommitted src by default (-dirty-abort defaults ON: the bakehouse bakes from the committed recipe; pass -dirty-abort 0 to override)" \n
append h " - holds the project-version gate as for bake (see the -expect-projectversion flag below)" \n
append h " - the optional -k flag will terminate running processes matching the executable being baked (if applicable)" \n
append h " - does NOT run the promotion gates (bootsupport, vfscommonupdate) - on a clean checkout they are already satisfied by the committed tree" \n \n
append h " $scriptname bake ?-k? ?-force? ?kitname ...?" \n
append h " $scriptname bake ?-k? ?-force? ?-expect-projectversion <ver>? ?kitname ...?" \n
append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into <projectdir>/bin" \n
append h " - includes the vfslibs phase; does not re-mint modules/libs and does not run the promotion gates" \n
append h " - with kitname arguments, bakes and deploys only the named configured kits (unknown names error before any bake)" \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n
append h " - project-version gate: the project version is a BAKE INPUT (kit stamp + versioned-scheme output names) - a" \n
append h " pending-looking bump (src/ commits since punkproject.toml last changed, none staged) prompts on a terminal" \n
append h " and aborts a non-interactive run unless -expect-projectversion asserts the intended version" \n
append h " $scriptname bakelist ?kitname ...?" \n
append h " - list the kit outputs configured in src/runtime/mapvfs.toml: name, kit type, runtime (with presence)," \n
append h " vfs folder and deployed state (bin copy absent/current/stale/nobake vs the src/_bake product)" \n
@ -2640,7 +2644,8 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " $scriptname shell" \n
append h " - run the punk shell using bootsupport libraries." \n
append h " $scriptname projectversion" \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n
append h " (the same computation feeds the project-version gate on bake/bakehouse/bin - see -expect-projectversion)" \n \n
append h " $scriptname workflow" \n
append h " - print an ASCII data-flow overview of the release workflow (edit -> mint -> promote -> bake) incl. the TERMINOLOGY key" \n \n
append h " $scriptname buildsuite list|info|build ?<suitename>? ?driver-args ...?" \n
@ -2671,6 +2676,12 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " Warnings carry a plain PROVENANCE-WARNING: prefix (greppable in redirected output) and are recapped at the end of the run." \n
append h " Use '$scriptname check' to see the current provenance status. To evaluate uncommitted source without minting or baking," \n
append h " use '<builtexe> src' or '<builtexe> src shell'." \n \n
append h " -expect-projectversion <ver> (bake, bakehouse, bin)" \n
append h " - assert the punkproject.toml project version this bake is intended to ship (the version is a BAKE INPUT:" \n
append h " stamped into every kit + used for versioned-scheme output names). A mismatch aborts before any kit work;" \n
append h " a match satisfies the project-version gate when a bump looks pending (a conscious no-bump bake)." \n
append h " Without it, that pending state prompts on a terminal and aborts fast non-interactively." \n
append h " -confirm 0 does NOT waive this gate. '$scriptname projectversion' shows the check standalone." \n \n
append h "" \n
append h [punkboot_availability_note]
return $h
@ -2736,13 +2747,15 @@ RELEASE SEQUENCE (the developer order that satisfies the staleness + provenance
(1) edit src/modules/foo-999999.0a1.0.tm code change [K1]
(2) bump src/modules/foo-buildversion.txt module version + changelog comment
punkproject.toml + CHANGELOG.md project version (if shipped behaviour) [K2]
punkproject.toml + CHANGELOG.md project version (a bake input) [K2]
(3) test tclsh src/tests/runtests.tcl ... runs against src/ trees (if present)
(4) commit source changes (git/fossil) clean tree -> committed provenance [K3]
(5) mint tclsh src/make.tcl modules -confirm 0 (or 'packages' = modules + libs)
(6) promote tclsh src/make.tcl bootsupport -confirm 0 run twice: 2nd pass must copy 0 files [K4]
(7) promote tclsh src/make.tcl vfscommonupdate -confirm 0
(8) bake tclsh src/make.tcl bake -confirm 0 kits; close running kit shells first [K5]
(8) bake tclsh src/make.tcl bake -confirm 0 -expect-projectversion <ver>
kits; close running kit shells first [K5]; <ver> = the step (2) project version -
the project-version gate aborts flagless non-interactive bakes on a pending bump [K2]
(9) commit tracked output trees src/bootsupport, thin-layout script/
manifest/gitignore.in payload copies
(src/project_layouts +
@ -2906,6 +2919,15 @@ KEY / NOTES
[K2] Project version (punkproject.toml [project] version + matching CHANGELOG.md
entry) is change-driven: bump when the change ships user-visible behaviour.
It is also a BAKE INPUT - bake reads punkproject.toml into every kit's
punkkit-stamp.toml (project_version, surfaced by '<kit> buildinfo') and
derives versioned-scheme output names from it - so the bump belongs BEFORE
the bake: bumping afterwards mis-stamps every kit and costs a full re-bake.
bake/bakehouse/bin hold a project-version gate: when src/ has commits since
punkproject.toml last changed and no bump is staged, a terminal run prompts
(y/N) and a non-interactive run aborts fast unless the intended version is
asserted with -expect-projectversion <ver>; a mismatched assertion always
aborts. -confirm 0 does NOT waive this gate (see [K6]).
Advisory check: tclsh src/make.tcl projectversion
[K3] Every output target root gets a .punkcheck file recording what was installed
@ -2929,7 +2951,9 @@ KEY / NOTES
[K6] All confirmation prompts follow -confirm: unattended/agent runs must pass
-confirm 0 (non-interactive stdin aborts fast at prompts; piping 'y' is
retired). 'make.tcl help <subcommand>' shows per-subcommand usage.
retired). EXCEPTION: the [K2] project-version gate is deliberately NOT
waived by -confirm 0 - assert with -expect-projectversion, or bump first.
'make.tcl help <subcommand>' shows per-subcommand usage.
[K7] Testing hooks along the way:
- src/tests/runtests.tcl exercises the src/ trees directly (dev modules).
@ -3144,7 +3168,11 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Deliberately does NOT run the promotion gates (bootsupport,
vfscommonupdate): payload promotion is a confirm-gated, committed
act - on a clean checkout the gates are already satisfied by the
committed tree."
committed tree.
Holds the project-version gate as for bake: a pending-looking
project-version bump prompts on a terminal and aborts a
non-interactive run unless -expect-projectversion asserts the
intended version (-confirm 0 does not waive the gate)."
packages
"
Mint (stamp real versions, pack modpods; plain copy where no
@ -3186,7 +3214,14 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Does not rebuild modules/libs and does not run the promotion gates -
minted modules reach the kit payload only via 'make.tcl
vfscommonupdate' (committed for provenance). Consumer full path from
a clean checkout: 'make.tcl bakehouse'."
a clean checkout: 'make.tcl bakehouse'.
The project version is a BAKE INPUT (stamped into every kit's
punkkit-stamp.toml and used for versioned-scheme output names):
when a bump looks pending (src/ commits since punkproject.toml
last changed, none staged) the project-version gate prompts on a
terminal and aborts a non-interactive run unless
-expect-projectversion asserts the intended version. -confirm 0
does not waive the gate; a mismatched assertion always aborts."
bakelist
"
List the kit outputs configured in src/runtime/mapvfs.toml (the
@ -3260,7 +3295,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bin
"
Install executables from src/bin to <projectdir>/bin, then check vfs
folders and bake kit/zipkit executables as for the bake subcommand."
folders and bake kit/zipkit executables as for the bake subcommand.
Holds the project-version gate as for bake (see 'make.tcl help
bake' and the -expect-projectversion flag)."
vendorupdate
"
Update the src/vendormodules based on src/vendormodules/include_modules.config.
@ -3298,7 +3335,10 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
projectversion
"
Advisory check: verify CHANGELOG.md matches punkproject.toml and warn
if src/ has changes since the last project-version bump."
if src/ has changes since the last project-version bump.
The same computation feeds the project-version gate on
bake/bakehouse/bin (the project version is a bake input - see
'make.tcl help bake' and -expect-projectversion)."
workflow
"
Print a plain-text (ASCII diagram) overview of the release
@ -3411,6 +3451,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bootsupport minor-version staleness gate -> proceed
vfscommonupdate REPLACE confirmation -> proceed
kit source/target type mismatch -> skip baking that kit
project-version pending-bump gate -> abort unless
-expect-projectversion asserts the version (never waived
by -confirm 0)
Note: Tcl 8.6 lacks the terminal probe (-inputmode) so stdin is assumed
interactive there - piped runs under 8.6 should always pass -confirm
explicitly."
@ -3449,6 +3492,25 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
(packages -> test -> bootsupport/vfscommonupdate -> commit -> bake).
Pass -dirty-abort 0 to proceed anyway with PROVENANCE-WARNINGs."
}
#project-version bake gate assertion (2026-08-06) - see the gate block
#beside the producing-commands dirty-src check. Deliberately its own
#acknowledgement channel: agent runs pass -confirm 0 habitually, so a
#gate waived by -confirm 0 would be bypassed by reflex.
variable OPT_EXPECTPROJECTVERSION {
-expect-projectversion -type string -default "" -help\
"Assert the punkproject.toml project version this bake is intended
to ship. The project version is a BAKE INPUT - stamped into every
kit (punkkit-stamp.toml project_version, surfaced by
'<kit> buildinfo') and used for versioned-scheme output names -
so an assertion that does not match punkproject.toml aborts
before any kit work. Supplying a matching assertion also
satisfies the project-version gate when a bump looks pending
(src/ commits since punkproject.toml last changed, none staged):
the bake then proceeds as a conscious no-bump decision. Without
it, that pending state prompts (y/N) on an interactive terminal
and aborts fast otherwise. -confirm 0 does NOT waive this gate.
'make.tcl projectversion' shows the same check standalone."
}
#libfetch fragments (G-139): consent is keyed to SERVER TRUST (the G-123
#posture) - the canonical punkbin origin needs no acknowledgement, any
#other origin needs the explicit flag. Never an interactive prompt.
@ -3519,6 +3581,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT_ON}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3565,6 +3628,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max -1
kitname -type string -optional 1 -multiple 1${$KITNAME_CHOICEPART} -help -&
@ -3607,6 +3671,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
@opts
${$OPT_FORCEKILL}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3916,6 +3981,7 @@ set ::punkboot::opt_confirm 1
set ::punkboot::opt_serverurl ""
set ::punkboot::opt_trust_server 0
set ::punkboot::opt_force 0
set ::punkboot::opt_expect_projectversion "" ;#project-version bake gate assertion (2026-08-06)
set ::punkboot::opt_kitnames [list] ;#G-121: requested kit names for bake/bakelist (empty = all configured)
if {$::punkboot::punkargs_ok} {
@ -3991,7 +4057,7 @@ if {$::punkboot::punkargs_ok} {
}
set ::punkboot::command $subcommand
set _opts [dict get $argd opts]
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force} {
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force -expect-projectversion opt_expect_projectversion} {
if {[dict exists $_opts $_optname]} {
set ::punkboot::$_var [dict get $_opts $_optname]
}
@ -4034,6 +4100,10 @@ if {$::punkboot::punkargs_ok} {
set ::punkboot::opt_serverurl [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-expect-projectversion {
set ::punkboot::opt_expect_projectversion [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-confirm {
set v [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
@ -4388,6 +4458,62 @@ 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]]
}
#Project-version status - the shared computation behind the 'projectversion' advisory
#subcommand and the project-version bake gate (2026-08-06). The project version is a
#BAKE INPUT: the kit stamp (punkkit-stamp.toml project_version - G-025) and the
#versioned-scheme output names both read punkproject.toml at bake time, so baking from
#a tree whose bump is still pending mis-stamps every kit and costs a full re-bake to
#correct (the 0.55.1/0.56.0 incident this gate exists to prevent).
#Returns a dict:
# available 0|1 - punkboot::utils loadable (version fields readable)
# pp_version punkproject.toml project version, "" if unreadable
# cl_version latest CHANGELOG.md version header, "" if none found
# staleness ok - no src/ commits since punkproject.toml last changed
# overdue - src/ commits since then and no version change staged
# bump_uncommitted - working-tree punkproject.toml version differs from
# HEAD's (bump done, commit pending)
# unavailable - punkboot::utils, git or the file history not queryable
# src_commits commit count behind overdue/bump_uncommitted (0 otherwise)
# src_commitlines up to 5 --oneline lines of those commits
# head_version HEAD's punkproject.toml version, "" if unknown
proc ::punkboot::get_projectversion_status {projectroot} {
set status [dict create available 0 pp_version "" cl_version "" staleness unavailable src_commits 0 src_commitlines [list] head_version ""]
if {[catch {package require punkboot::utils}]} {
return $status
}
dict set status available 1
catch {dict set status pp_version [::punkboot::utils::read_punkproject_version [file join $projectroot punkproject.toml]]}
catch {dict set status cl_version [::punkboot::utils::read_changelog_latest_version [file join $projectroot CHANGELOG.md]]}
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {![string length $pp_commit]} {
return $status ;#no git/no history - staleness stays 'unavailable'
}
if {[catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
return $status
}
set src_changes [string trim $src_raw]
if {![string length $src_changes]} {
dict set status staleness ok
return $status
}
set commitlines [split $src_changes \n]
dict set status src_commits [llength $commitlines]
dict set status src_commitlines [lrange $commitlines 0 4]
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
dict set status head_version $head_version
if {[string length $head_version] && [string length [dict get $status pp_version]] && [dict get $status pp_version] ne $head_version} {
dict set status staleness bump_uncommitted
} else {
dict set status staleness overdue
}
return $status
}
#Replace a file that may be MEMORY-MAPPED by another process (or by this one).
#
#On windows an ordinary 'file copy -force' onto a mapped file is refused: a zipfs mount
@ -4637,6 +4763,82 @@ if {$::punkboot::command in {bakehouse packages modules libs bake vfslibs bin bo
}
}
#Project-version bake gate (2026-08-06, user-directed after the 0.55.1 kit mis-stamp):
#the kit-assembling commands read punkproject.toml at bake time (kit stamp + versioned-
#scheme names - see get_projectversion_status), so a version bump still pending at bake
#time silently mis-stamps every kit. When src/ has commits since punkproject.toml last
#changed and no bump is staged, the gate requires a conscious decision:
# interactive terminal + -confirm 1 : y/N prompt (default N)
# non-interactive OR -confirm 0 : abort fast UNLESS -expect-projectversion asserts
# the version this bake is intended to ship. -confirm 0 deliberately does NOT waive
# the gate: agent runs pass -confirm 0 habitually, and a gate their habitual flags
# waive is no gate. The assertion is a version, not a boolean, so satisfying it
# requires stating a checkable claim about punkproject.toml rather than a -force.
#A supplied -expect-projectversion is verified in EVERY staleness state - a mismatch
#with punkproject.toml always aborts before any kit work. Degradations mirror the
#dirty-src check above: status unavailable -> NOTE and proceed flagless, but a supplied
#assertion aborts as unverifiable rather than silently losing the requested strictness.
if {$::punkboot::command in {bakehouse bake bin}} {
set _pv [::punkboot::get_projectversion_status $projectroot]
set _pv_expect $::punkboot::opt_expect_projectversion
set _pv_current [dict get $_pv pp_version]
if {$_pv_expect ne ""} {
if {![dict get $_pv available] || ![string length $_pv_current]} {
puts stderr "-aborted- (-expect-projectversion $_pv_expect supplied but the punkproject.toml version is unreadable (punkboot::utils not loadable from bootsupport, or file missing/unparseable) - cannot verify the assertion)"
exit 1
}
if {$_pv_expect ne $_pv_current} {
puts stderr "make.tcl $::punkboot::command: -expect-projectversion MISMATCH: asserted $_pv_expect but punkproject.toml says $_pv_current"
puts stderr " The project version is a BAKE INPUT (kit stamp + versioned-scheme output names)."
puts stderr " Either bump punkproject.toml + CHANGELOG.md to $_pv_expect (root AGENTS.md 'Project"
puts stderr " Versioning'), or re-assert the version this bake should actually ship."
puts stderr "-aborted- (project-version assertion mismatch - nothing built, nothing deployed)"
exit 1
}
puts stdout "project-version gate: OK (asserted -expect-projectversion $_pv_expect matches punkproject.toml)"
if {[dict get $_pv staleness] eq "overdue"} {
puts stdout " NOTE: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed - proceeding on the assertion that this bake intentionally ships $_pv_expect"
}
} else {
switch -- [dict get $_pv staleness] {
overdue {
::punkboot::print_bake_warnings [list "project-version bump may be PENDING: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed (version $_pv_current)"]
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
foreach _pvline [dict get $_pv src_commitlines] {
puts stderr " $_pvline"
}
if {!$::punkboot::opt_confirm || ![::punkboot::lib::stdin_is_interactive]} {
set ::punkboot::bake_warnings_pending [list] ;#aborting adjacent to the warning - no recap needed
puts stderr " Resolve before baking:"
puts stderr " bump: edit punkproject.toml + CHANGELOG.md (root AGENTS.md 'Project Versioning'), commit, re-run"
puts stderr " assert: make.tcl $::punkboot::command -confirm 0 -expect-projectversion $_pv_current (this bake intentionally ships $_pv_current)"
puts stderr " ('make.tcl projectversion' shows this check standalone; -confirm 0 does not waive this gate)"
puts stderr "-aborted- (project-version bump appears pending and no -expect-projectversion assertion supplied - nothing built, nothing deployed)"
exit 1
}
if {![::punkboot::lib::bootsupport_prompt_yesno "PROJECT-VERSION: baking now stamps version $_pv_current into every kit; a later bump costs a full re-bake to correct."]} {
set ::punkboot::bake_warnings_pending [list]
puts stderr "-aborted- by user (bump punkproject.toml + CHANGELOG.md first, or assert intent with -expect-projectversion)"
exit 1
}
puts stderr "-proceeding at project version $_pv_current at user's request- (warning recapped at end of run)"
}
bump_uncommitted {
puts stdout "project-version gate: OK (punkproject.toml bumped [dict get $_pv head_version] -> $_pv_current, commit pending - bake reads the working-tree value)"
}
ok {
#quiet - no src/ commits since the version last changed
}
default {
puts stdout "NOTE: project-version gate skipped ([expr {[dict get $_pv available] ? {git history not queryable} : {punkboot::utils not loadable from bootsupport}}]) - continuing without it"
}
}
}
unset -nocomplain _pv _pv_expect _pv_current _pvline
}
if {$::punkboot::command eq "check"} {
set sep [string repeat - 75]
puts stdout $sep
@ -4830,6 +5032,28 @@ if {$::punkboot::command eq "check"} {
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'."
}
# Project-version bake gate status - pending-bump detection for the kit-assembling commands
puts stdout $sep
set _pv [::punkboot::get_projectversion_status $projectroot]
if {[dict get $_pv available]} {
puts stdout "project-version bake gate: ACTIVE"
puts stdout " the project version is a BAKE INPUT (kit stamp punkkit-stamp.toml project_version + versioned-"
puts stdout " scheme output names), so bake/bakehouse/bin verify a supplied -expect-projectversion against"
puts stdout " punkproject.toml (mismatch always aborts) and, when src/ has commits since the version last"
puts stdout " changed with no bump staged, prompt on a terminal / abort fast non-interactively unless the"
puts stdout " version is asserted. -confirm 0 does not waive the gate. Advisory twin: 'make.tcl projectversion'."
switch -- [dict get $_pv staleness] {
ok { puts stdout " current state: OK (no src/ commits since punkproject.toml version [dict get $_pv pp_version] last changed)" }
overdue { puts stdout " current state: bump may be PENDING ([dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed at version [dict get $_pv pp_version]) - flagless non-interactive bakes abort" }
bump_uncommitted { puts stdout " current state: bump staged ([dict get $_pv head_version] -> [dict get $_pv pp_version], commit pending) - bakes proceed reading the working-tree value" }
default { puts stdout " current state: staleness unknown (git history not queryable) - bakes proceed with a NOTE" }
}
} else {
puts stdout "project-version bake gate: UNAVAILABLE (punkboot::utils not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot detect a pending bump - run 'make.tcl modules' then"
puts stdout " 'make.tcl bootsupport'. A supplied -expect-projectversion still aborts as unverifiable."
}
unset -nocomplain _pv
# Payload/target consistency checks (G-133) - advisory arch scan + smoke-require probe
puts stdout $sep
#availability probe only - the path deliberately does not exist
@ -4973,24 +5197,26 @@ if {$::punkboot::command eq "info"} {
}
if {$::punkboot::command eq "projectversion"} {
package require punkboot::utils
set sep [string repeat - 75]
puts stdout $sep
puts stdout "project version check"
puts stdout $sep
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set src_folder [file join $projectroot src]
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set pp_version [::punkboot::utils::read_punkproject_version $pp_file]
set cl_version [::punkboot::utils::read_changelog_latest_version $cl_file]
#shared computation with the project-version bake gate on bake/bakehouse/bin
set pvstatus [::punkboot::get_projectversion_status $projectroot]
set pp_version [dict get $pvstatus pp_version]
set cl_version [dict get $pvstatus cl_version]
puts stdout "punkproject.toml version : [expr {[string length $pp_version] ? $pp_version : "(not found)"}]"
puts stdout "CHANGELOG.md version : [expr {[string length $cl_version] ? $cl_version : "(not found)"}]"
# 1. Consistency: CHANGELOG latest header must match punkproject.toml version.
if {![string length $pp_version]} {
if {![dict get $pvstatus available]} {
puts stderr "WARNING: punkboot::utils not loadable from bootsupport - version fields unreadable"
} elseif {![string length $pp_version]} {
puts stderr "WARNING: could not read version from $pp_file"
} elseif {![string length $cl_version]} {
puts stderr "WARNING: no '## \[X.Y.Z\]' version header found in $cl_file"
@ -5007,48 +5233,41 @@ if {$::punkboot::command eq "projectversion"} {
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {[string length $pp_commit]} {
if {![catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {
if {$i >= 5} break
puts stderr " $line"
incr i
}
switch -- [dict get $pvstatus staleness] {
bump_uncommitted {
set n [dict get $pvstatus src_commits]
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ([dict get $pvstatus head_version] -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
}
overdue {
set n [dict get $pvstatus src_commits]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
puts stderr " bake/bakehouse/bin hold a project-version gate in this state: non-interactive runs abort"
puts stderr " unless the intended version is asserted, e.g for a deliberate no-bump bake:"
puts stderr " make.tcl bake -confirm 0 -expect-projectversion $pp_version"
}
ok {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
}
default {
if {[dict get $pvstatus available]} {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
} else {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
puts stdout "staleness: skipped (punkboot::utils not loadable from bootsupport)"
}
} else {
puts stdout "staleness: skipped (git query failed)"
}
} else {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
}
puts stdout $sep

335
src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -2589,16 +2589,20 @@ proc ::punkboot::punkboot_gethelp {args} {
append h "" \n
append h " $scriptname help or just $scriptname" \n
append h " - This help." \n \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0?" \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0? ?-expect-projectversion <ver>?" \n
append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to <projectdir>/bin" \n
append h " - refuses uncommitted src by default (-dirty-abort defaults ON: the bakehouse bakes from the committed recipe; pass -dirty-abort 0 to override)" \n
append h " - holds the project-version gate as for bake (see the -expect-projectversion flag below)" \n
append h " - the optional -k flag will terminate running processes matching the executable being baked (if applicable)" \n
append h " - does NOT run the promotion gates (bootsupport, vfscommonupdate) - on a clean checkout they are already satisfied by the committed tree" \n \n
append h " $scriptname bake ?-k? ?-force? ?kitname ...?" \n
append h " $scriptname bake ?-k? ?-force? ?-expect-projectversion <ver>? ?kitname ...?" \n
append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into <projectdir>/bin" \n
append h " - includes the vfslibs phase; does not re-mint modules/libs and does not run the promotion gates" \n
append h " - with kitname arguments, bakes and deploys only the named configured kits (unknown names error before any bake)" \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n
append h " - project-version gate: the project version is a BAKE INPUT (kit stamp + versioned-scheme output names) - a" \n
append h " pending-looking bump (src/ commits since punkproject.toml last changed, none staged) prompts on a terminal" \n
append h " and aborts a non-interactive run unless -expect-projectversion asserts the intended version" \n
append h " $scriptname bakelist ?kitname ...?" \n
append h " - list the kit outputs configured in src/runtime/mapvfs.toml: name, kit type, runtime (with presence)," \n
append h " vfs folder and deployed state (bin copy absent/current/stale/nobake vs the src/_bake product)" \n
@ -2640,7 +2644,8 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " $scriptname shell" \n
append h " - run the punk shell using bootsupport libraries." \n
append h " $scriptname projectversion" \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n
append h " (the same computation feeds the project-version gate on bake/bakehouse/bin - see -expect-projectversion)" \n \n
append h " $scriptname workflow" \n
append h " - print an ASCII data-flow overview of the release workflow (edit -> mint -> promote -> bake) incl. the TERMINOLOGY key" \n \n
append h " $scriptname buildsuite list|info|build ?<suitename>? ?driver-args ...?" \n
@ -2671,6 +2676,12 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " Warnings carry a plain PROVENANCE-WARNING: prefix (greppable in redirected output) and are recapped at the end of the run." \n
append h " Use '$scriptname check' to see the current provenance status. To evaluate uncommitted source without minting or baking," \n
append h " use '<builtexe> src' or '<builtexe> src shell'." \n \n
append h " -expect-projectversion <ver> (bake, bakehouse, bin)" \n
append h " - assert the punkproject.toml project version this bake is intended to ship (the version is a BAKE INPUT:" \n
append h " stamped into every kit + used for versioned-scheme output names). A mismatch aborts before any kit work;" \n
append h " a match satisfies the project-version gate when a bump looks pending (a conscious no-bump bake)." \n
append h " Without it, that pending state prompts on a terminal and aborts fast non-interactively." \n
append h " -confirm 0 does NOT waive this gate. '$scriptname projectversion' shows the check standalone." \n \n
append h "" \n
append h [punkboot_availability_note]
return $h
@ -2736,13 +2747,15 @@ RELEASE SEQUENCE (the developer order that satisfies the staleness + provenance
(1) edit src/modules/foo-999999.0a1.0.tm code change [K1]
(2) bump src/modules/foo-buildversion.txt module version + changelog comment
punkproject.toml + CHANGELOG.md project version (if shipped behaviour) [K2]
punkproject.toml + CHANGELOG.md project version (a bake input) [K2]
(3) test tclsh src/tests/runtests.tcl ... runs against src/ trees (if present)
(4) commit source changes (git/fossil) clean tree -> committed provenance [K3]
(5) mint tclsh src/make.tcl modules -confirm 0 (or 'packages' = modules + libs)
(6) promote tclsh src/make.tcl bootsupport -confirm 0 run twice: 2nd pass must copy 0 files [K4]
(7) promote tclsh src/make.tcl vfscommonupdate -confirm 0
(8) bake tclsh src/make.tcl bake -confirm 0 kits; close running kit shells first [K5]
(8) bake tclsh src/make.tcl bake -confirm 0 -expect-projectversion <ver>
kits; close running kit shells first [K5]; <ver> = the step (2) project version -
the project-version gate aborts flagless non-interactive bakes on a pending bump [K2]
(9) commit tracked output trees src/bootsupport, thin-layout script/
manifest/gitignore.in payload copies
(src/project_layouts +
@ -2906,6 +2919,15 @@ KEY / NOTES
[K2] Project version (punkproject.toml [project] version + matching CHANGELOG.md
entry) is change-driven: bump when the change ships user-visible behaviour.
It is also a BAKE INPUT - bake reads punkproject.toml into every kit's
punkkit-stamp.toml (project_version, surfaced by '<kit> buildinfo') and
derives versioned-scheme output names from it - so the bump belongs BEFORE
the bake: bumping afterwards mis-stamps every kit and costs a full re-bake.
bake/bakehouse/bin hold a project-version gate: when src/ has commits since
punkproject.toml last changed and no bump is staged, a terminal run prompts
(y/N) and a non-interactive run aborts fast unless the intended version is
asserted with -expect-projectversion <ver>; a mismatched assertion always
aborts. -confirm 0 does NOT waive this gate (see [K6]).
Advisory check: tclsh src/make.tcl projectversion
[K3] Every output target root gets a .punkcheck file recording what was installed
@ -2929,7 +2951,9 @@ KEY / NOTES
[K6] All confirmation prompts follow -confirm: unattended/agent runs must pass
-confirm 0 (non-interactive stdin aborts fast at prompts; piping 'y' is
retired). 'make.tcl help <subcommand>' shows per-subcommand usage.
retired). EXCEPTION: the [K2] project-version gate is deliberately NOT
waived by -confirm 0 - assert with -expect-projectversion, or bump first.
'make.tcl help <subcommand>' shows per-subcommand usage.
[K7] Testing hooks along the way:
- src/tests/runtests.tcl exercises the src/ trees directly (dev modules).
@ -3144,7 +3168,11 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Deliberately does NOT run the promotion gates (bootsupport,
vfscommonupdate): payload promotion is a confirm-gated, committed
act - on a clean checkout the gates are already satisfied by the
committed tree."
committed tree.
Holds the project-version gate as for bake: a pending-looking
project-version bump prompts on a terminal and aborts a
non-interactive run unless -expect-projectversion asserts the
intended version (-confirm 0 does not waive the gate)."
packages
"
Mint (stamp real versions, pack modpods; plain copy where no
@ -3186,7 +3214,14 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Does not rebuild modules/libs and does not run the promotion gates -
minted modules reach the kit payload only via 'make.tcl
vfscommonupdate' (committed for provenance). Consumer full path from
a clean checkout: 'make.tcl bakehouse'."
a clean checkout: 'make.tcl bakehouse'.
The project version is a BAKE INPUT (stamped into every kit's
punkkit-stamp.toml and used for versioned-scheme output names):
when a bump looks pending (src/ commits since punkproject.toml
last changed, none staged) the project-version gate prompts on a
terminal and aborts a non-interactive run unless
-expect-projectversion asserts the intended version. -confirm 0
does not waive the gate; a mismatched assertion always aborts."
bakelist
"
List the kit outputs configured in src/runtime/mapvfs.toml (the
@ -3260,7 +3295,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bin
"
Install executables from src/bin to <projectdir>/bin, then check vfs
folders and bake kit/zipkit executables as for the bake subcommand."
folders and bake kit/zipkit executables as for the bake subcommand.
Holds the project-version gate as for bake (see 'make.tcl help
bake' and the -expect-projectversion flag)."
vendorupdate
"
Update the src/vendormodules based on src/vendormodules/include_modules.config.
@ -3298,7 +3335,10 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
projectversion
"
Advisory check: verify CHANGELOG.md matches punkproject.toml and warn
if src/ has changes since the last project-version bump."
if src/ has changes since the last project-version bump.
The same computation feeds the project-version gate on
bake/bakehouse/bin (the project version is a bake input - see
'make.tcl help bake' and -expect-projectversion)."
workflow
"
Print a plain-text (ASCII diagram) overview of the release
@ -3411,6 +3451,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bootsupport minor-version staleness gate -> proceed
vfscommonupdate REPLACE confirmation -> proceed
kit source/target type mismatch -> skip baking that kit
project-version pending-bump gate -> abort unless
-expect-projectversion asserts the version (never waived
by -confirm 0)
Note: Tcl 8.6 lacks the terminal probe (-inputmode) so stdin is assumed
interactive there - piped runs under 8.6 should always pass -confirm
explicitly."
@ -3449,6 +3492,25 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
(packages -> test -> bootsupport/vfscommonupdate -> commit -> bake).
Pass -dirty-abort 0 to proceed anyway with PROVENANCE-WARNINGs."
}
#project-version bake gate assertion (2026-08-06) - see the gate block
#beside the producing-commands dirty-src check. Deliberately its own
#acknowledgement channel: agent runs pass -confirm 0 habitually, so a
#gate waived by -confirm 0 would be bypassed by reflex.
variable OPT_EXPECTPROJECTVERSION {
-expect-projectversion -type string -default "" -help\
"Assert the punkproject.toml project version this bake is intended
to ship. The project version is a BAKE INPUT - stamped into every
kit (punkkit-stamp.toml project_version, surfaced by
'<kit> buildinfo') and used for versioned-scheme output names -
so an assertion that does not match punkproject.toml aborts
before any kit work. Supplying a matching assertion also
satisfies the project-version gate when a bump looks pending
(src/ commits since punkproject.toml last changed, none staged):
the bake then proceeds as a conscious no-bump decision. Without
it, that pending state prompts (y/N) on an interactive terminal
and aborts fast otherwise. -confirm 0 does NOT waive this gate.
'make.tcl projectversion' shows the same check standalone."
}
#libfetch fragments (G-139): consent is keyed to SERVER TRUST (the G-123
#posture) - the canonical punkbin origin needs no acknowledgement, any
#other origin needs the explicit flag. Never an interactive prompt.
@ -3519,6 +3581,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT_ON}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3565,6 +3628,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max -1
kitname -type string -optional 1 -multiple 1${$KITNAME_CHOICEPART} -help -&
@ -3607,6 +3671,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
@opts
${$OPT_FORCEKILL}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3916,6 +3981,7 @@ set ::punkboot::opt_confirm 1
set ::punkboot::opt_serverurl ""
set ::punkboot::opt_trust_server 0
set ::punkboot::opt_force 0
set ::punkboot::opt_expect_projectversion "" ;#project-version bake gate assertion (2026-08-06)
set ::punkboot::opt_kitnames [list] ;#G-121: requested kit names for bake/bakelist (empty = all configured)
if {$::punkboot::punkargs_ok} {
@ -3991,7 +4057,7 @@ if {$::punkboot::punkargs_ok} {
}
set ::punkboot::command $subcommand
set _opts [dict get $argd opts]
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force} {
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force -expect-projectversion opt_expect_projectversion} {
if {[dict exists $_opts $_optname]} {
set ::punkboot::$_var [dict get $_opts $_optname]
}
@ -4034,6 +4100,10 @@ if {$::punkboot::punkargs_ok} {
set ::punkboot::opt_serverurl [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-expect-projectversion {
set ::punkboot::opt_expect_projectversion [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-confirm {
set v [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
@ -4388,6 +4458,62 @@ 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]]
}
#Project-version status - the shared computation behind the 'projectversion' advisory
#subcommand and the project-version bake gate (2026-08-06). The project version is a
#BAKE INPUT: the kit stamp (punkkit-stamp.toml project_version - G-025) and the
#versioned-scheme output names both read punkproject.toml at bake time, so baking from
#a tree whose bump is still pending mis-stamps every kit and costs a full re-bake to
#correct (the 0.55.1/0.56.0 incident this gate exists to prevent).
#Returns a dict:
# available 0|1 - punkboot::utils loadable (version fields readable)
# pp_version punkproject.toml project version, "" if unreadable
# cl_version latest CHANGELOG.md version header, "" if none found
# staleness ok - no src/ commits since punkproject.toml last changed
# overdue - src/ commits since then and no version change staged
# bump_uncommitted - working-tree punkproject.toml version differs from
# HEAD's (bump done, commit pending)
# unavailable - punkboot::utils, git or the file history not queryable
# src_commits commit count behind overdue/bump_uncommitted (0 otherwise)
# src_commitlines up to 5 --oneline lines of those commits
# head_version HEAD's punkproject.toml version, "" if unknown
proc ::punkboot::get_projectversion_status {projectroot} {
set status [dict create available 0 pp_version "" cl_version "" staleness unavailable src_commits 0 src_commitlines [list] head_version ""]
if {[catch {package require punkboot::utils}]} {
return $status
}
dict set status available 1
catch {dict set status pp_version [::punkboot::utils::read_punkproject_version [file join $projectroot punkproject.toml]]}
catch {dict set status cl_version [::punkboot::utils::read_changelog_latest_version [file join $projectroot CHANGELOG.md]]}
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {![string length $pp_commit]} {
return $status ;#no git/no history - staleness stays 'unavailable'
}
if {[catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
return $status
}
set src_changes [string trim $src_raw]
if {![string length $src_changes]} {
dict set status staleness ok
return $status
}
set commitlines [split $src_changes \n]
dict set status src_commits [llength $commitlines]
dict set status src_commitlines [lrange $commitlines 0 4]
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
dict set status head_version $head_version
if {[string length $head_version] && [string length [dict get $status pp_version]] && [dict get $status pp_version] ne $head_version} {
dict set status staleness bump_uncommitted
} else {
dict set status staleness overdue
}
return $status
}
#Replace a file that may be MEMORY-MAPPED by another process (or by this one).
#
#On windows an ordinary 'file copy -force' onto a mapped file is refused: a zipfs mount
@ -4637,6 +4763,82 @@ if {$::punkboot::command in {bakehouse packages modules libs bake vfslibs bin bo
}
}
#Project-version bake gate (2026-08-06, user-directed after the 0.55.1 kit mis-stamp):
#the kit-assembling commands read punkproject.toml at bake time (kit stamp + versioned-
#scheme names - see get_projectversion_status), so a version bump still pending at bake
#time silently mis-stamps every kit. When src/ has commits since punkproject.toml last
#changed and no bump is staged, the gate requires a conscious decision:
# interactive terminal + -confirm 1 : y/N prompt (default N)
# non-interactive OR -confirm 0 : abort fast UNLESS -expect-projectversion asserts
# the version this bake is intended to ship. -confirm 0 deliberately does NOT waive
# the gate: agent runs pass -confirm 0 habitually, and a gate their habitual flags
# waive is no gate. The assertion is a version, not a boolean, so satisfying it
# requires stating a checkable claim about punkproject.toml rather than a -force.
#A supplied -expect-projectversion is verified in EVERY staleness state - a mismatch
#with punkproject.toml always aborts before any kit work. Degradations mirror the
#dirty-src check above: status unavailable -> NOTE and proceed flagless, but a supplied
#assertion aborts as unverifiable rather than silently losing the requested strictness.
if {$::punkboot::command in {bakehouse bake bin}} {
set _pv [::punkboot::get_projectversion_status $projectroot]
set _pv_expect $::punkboot::opt_expect_projectversion
set _pv_current [dict get $_pv pp_version]
if {$_pv_expect ne ""} {
if {![dict get $_pv available] || ![string length $_pv_current]} {
puts stderr "-aborted- (-expect-projectversion $_pv_expect supplied but the punkproject.toml version is unreadable (punkboot::utils not loadable from bootsupport, or file missing/unparseable) - cannot verify the assertion)"
exit 1
}
if {$_pv_expect ne $_pv_current} {
puts stderr "make.tcl $::punkboot::command: -expect-projectversion MISMATCH: asserted $_pv_expect but punkproject.toml says $_pv_current"
puts stderr " The project version is a BAKE INPUT (kit stamp + versioned-scheme output names)."
puts stderr " Either bump punkproject.toml + CHANGELOG.md to $_pv_expect (root AGENTS.md 'Project"
puts stderr " Versioning'), or re-assert the version this bake should actually ship."
puts stderr "-aborted- (project-version assertion mismatch - nothing built, nothing deployed)"
exit 1
}
puts stdout "project-version gate: OK (asserted -expect-projectversion $_pv_expect matches punkproject.toml)"
if {[dict get $_pv staleness] eq "overdue"} {
puts stdout " NOTE: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed - proceeding on the assertion that this bake intentionally ships $_pv_expect"
}
} else {
switch -- [dict get $_pv staleness] {
overdue {
::punkboot::print_bake_warnings [list "project-version bump may be PENDING: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed (version $_pv_current)"]
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
foreach _pvline [dict get $_pv src_commitlines] {
puts stderr " $_pvline"
}
if {!$::punkboot::opt_confirm || ![::punkboot::lib::stdin_is_interactive]} {
set ::punkboot::bake_warnings_pending [list] ;#aborting adjacent to the warning - no recap needed
puts stderr " Resolve before baking:"
puts stderr " bump: edit punkproject.toml + CHANGELOG.md (root AGENTS.md 'Project Versioning'), commit, re-run"
puts stderr " assert: make.tcl $::punkboot::command -confirm 0 -expect-projectversion $_pv_current (this bake intentionally ships $_pv_current)"
puts stderr " ('make.tcl projectversion' shows this check standalone; -confirm 0 does not waive this gate)"
puts stderr "-aborted- (project-version bump appears pending and no -expect-projectversion assertion supplied - nothing built, nothing deployed)"
exit 1
}
if {![::punkboot::lib::bootsupport_prompt_yesno "PROJECT-VERSION: baking now stamps version $_pv_current into every kit; a later bump costs a full re-bake to correct."]} {
set ::punkboot::bake_warnings_pending [list]
puts stderr "-aborted- by user (bump punkproject.toml + CHANGELOG.md first, or assert intent with -expect-projectversion)"
exit 1
}
puts stderr "-proceeding at project version $_pv_current at user's request- (warning recapped at end of run)"
}
bump_uncommitted {
puts stdout "project-version gate: OK (punkproject.toml bumped [dict get $_pv head_version] -> $_pv_current, commit pending - bake reads the working-tree value)"
}
ok {
#quiet - no src/ commits since the version last changed
}
default {
puts stdout "NOTE: project-version gate skipped ([expr {[dict get $_pv available] ? {git history not queryable} : {punkboot::utils not loadable from bootsupport}}]) - continuing without it"
}
}
}
unset -nocomplain _pv _pv_expect _pv_current _pvline
}
if {$::punkboot::command eq "check"} {
set sep [string repeat - 75]
puts stdout $sep
@ -4830,6 +5032,28 @@ if {$::punkboot::command eq "check"} {
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'."
}
# Project-version bake gate status - pending-bump detection for the kit-assembling commands
puts stdout $sep
set _pv [::punkboot::get_projectversion_status $projectroot]
if {[dict get $_pv available]} {
puts stdout "project-version bake gate: ACTIVE"
puts stdout " the project version is a BAKE INPUT (kit stamp punkkit-stamp.toml project_version + versioned-"
puts stdout " scheme output names), so bake/bakehouse/bin verify a supplied -expect-projectversion against"
puts stdout " punkproject.toml (mismatch always aborts) and, when src/ has commits since the version last"
puts stdout " changed with no bump staged, prompt on a terminal / abort fast non-interactively unless the"
puts stdout " version is asserted. -confirm 0 does not waive the gate. Advisory twin: 'make.tcl projectversion'."
switch -- [dict get $_pv staleness] {
ok { puts stdout " current state: OK (no src/ commits since punkproject.toml version [dict get $_pv pp_version] last changed)" }
overdue { puts stdout " current state: bump may be PENDING ([dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed at version [dict get $_pv pp_version]) - flagless non-interactive bakes abort" }
bump_uncommitted { puts stdout " current state: bump staged ([dict get $_pv head_version] -> [dict get $_pv pp_version], commit pending) - bakes proceed reading the working-tree value" }
default { puts stdout " current state: staleness unknown (git history not queryable) - bakes proceed with a NOTE" }
}
} else {
puts stdout "project-version bake gate: UNAVAILABLE (punkboot::utils not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot detect a pending bump - run 'make.tcl modules' then"
puts stdout " 'make.tcl bootsupport'. A supplied -expect-projectversion still aborts as unverifiable."
}
unset -nocomplain _pv
# Payload/target consistency checks (G-133) - advisory arch scan + smoke-require probe
puts stdout $sep
#availability probe only - the path deliberately does not exist
@ -4973,24 +5197,26 @@ if {$::punkboot::command eq "info"} {
}
if {$::punkboot::command eq "projectversion"} {
package require punkboot::utils
set sep [string repeat - 75]
puts stdout $sep
puts stdout "project version check"
puts stdout $sep
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set src_folder [file join $projectroot src]
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set pp_version [::punkboot::utils::read_punkproject_version $pp_file]
set cl_version [::punkboot::utils::read_changelog_latest_version $cl_file]
#shared computation with the project-version bake gate on bake/bakehouse/bin
set pvstatus [::punkboot::get_projectversion_status $projectroot]
set pp_version [dict get $pvstatus pp_version]
set cl_version [dict get $pvstatus cl_version]
puts stdout "punkproject.toml version : [expr {[string length $pp_version] ? $pp_version : "(not found)"}]"
puts stdout "CHANGELOG.md version : [expr {[string length $cl_version] ? $cl_version : "(not found)"}]"
# 1. Consistency: CHANGELOG latest header must match punkproject.toml version.
if {![string length $pp_version]} {
if {![dict get $pvstatus available]} {
puts stderr "WARNING: punkboot::utils not loadable from bootsupport - version fields unreadable"
} elseif {![string length $pp_version]} {
puts stderr "WARNING: could not read version from $pp_file"
} elseif {![string length $cl_version]} {
puts stderr "WARNING: no '## \[X.Y.Z\]' version header found in $cl_file"
@ -5007,48 +5233,41 @@ if {$::punkboot::command eq "projectversion"} {
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {[string length $pp_commit]} {
if {![catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {
if {$i >= 5} break
puts stderr " $line"
incr i
}
switch -- [dict get $pvstatus staleness] {
bump_uncommitted {
set n [dict get $pvstatus src_commits]
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ([dict get $pvstatus head_version] -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
}
overdue {
set n [dict get $pvstatus src_commits]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
puts stderr " bake/bakehouse/bin hold a project-version gate in this state: non-interactive runs abort"
puts stderr " unless the intended version is asserted, e.g for a deliberate no-bump bake:"
puts stderr " make.tcl bake -confirm 0 -expect-projectversion $pp_version"
}
ok {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
}
default {
if {[dict get $pvstatus available]} {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
} else {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
puts stdout "staleness: skipped (punkboot::utils not loadable from bootsupport)"
}
} else {
puts stdout "staleness: skipped (git query failed)"
}
} else {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
}
puts stdout $sep

335
src/project_layouts/vendor/punk/basic/src/make.tcl vendored

@ -2589,16 +2589,20 @@ proc ::punkboot::punkboot_gethelp {args} {
append h "" \n
append h " $scriptname help or just $scriptname" \n
append h " - This help." \n \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0?" \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0? ?-expect-projectversion <ver>?" \n
append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to <projectdir>/bin" \n
append h " - refuses uncommitted src by default (-dirty-abort defaults ON: the bakehouse bakes from the committed recipe; pass -dirty-abort 0 to override)" \n
append h " - holds the project-version gate as for bake (see the -expect-projectversion flag below)" \n
append h " - the optional -k flag will terminate running processes matching the executable being baked (if applicable)" \n
append h " - does NOT run the promotion gates (bootsupport, vfscommonupdate) - on a clean checkout they are already satisfied by the committed tree" \n \n
append h " $scriptname bake ?-k? ?-force? ?kitname ...?" \n
append h " $scriptname bake ?-k? ?-force? ?-expect-projectversion <ver>? ?kitname ...?" \n
append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into <projectdir>/bin" \n
append h " - includes the vfslibs phase; does not re-mint modules/libs and does not run the promotion gates" \n
append h " - with kitname arguments, bakes and deploys only the named configured kits (unknown names error before any bake)" \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n
append h " - project-version gate: the project version is a BAKE INPUT (kit stamp + versioned-scheme output names) - a" \n
append h " pending-looking bump (src/ commits since punkproject.toml last changed, none staged) prompts on a terminal" \n
append h " and aborts a non-interactive run unless -expect-projectversion asserts the intended version" \n
append h " $scriptname bakelist ?kitname ...?" \n
append h " - list the kit outputs configured in src/runtime/mapvfs.toml: name, kit type, runtime (with presence)," \n
append h " vfs folder and deployed state (bin copy absent/current/stale/nobake vs the src/_bake product)" \n
@ -2640,7 +2644,8 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " $scriptname shell" \n
append h " - run the punk shell using bootsupport libraries." \n
append h " $scriptname projectversion" \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n
append h " (the same computation feeds the project-version gate on bake/bakehouse/bin - see -expect-projectversion)" \n \n
append h " $scriptname workflow" \n
append h " - print an ASCII data-flow overview of the release workflow (edit -> mint -> promote -> bake) incl. the TERMINOLOGY key" \n \n
append h " $scriptname buildsuite list|info|build ?<suitename>? ?driver-args ...?" \n
@ -2671,6 +2676,12 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " Warnings carry a plain PROVENANCE-WARNING: prefix (greppable in redirected output) and are recapped at the end of the run." \n
append h " Use '$scriptname check' to see the current provenance status. To evaluate uncommitted source without minting or baking," \n
append h " use '<builtexe> src' or '<builtexe> src shell'." \n \n
append h " -expect-projectversion <ver> (bake, bakehouse, bin)" \n
append h " - assert the punkproject.toml project version this bake is intended to ship (the version is a BAKE INPUT:" \n
append h " stamped into every kit + used for versioned-scheme output names). A mismatch aborts before any kit work;" \n
append h " a match satisfies the project-version gate when a bump looks pending (a conscious no-bump bake)." \n
append h " Without it, that pending state prompts on a terminal and aborts fast non-interactively." \n
append h " -confirm 0 does NOT waive this gate. '$scriptname projectversion' shows the check standalone." \n \n
append h "" \n
append h [punkboot_availability_note]
return $h
@ -2736,13 +2747,15 @@ RELEASE SEQUENCE (the developer order that satisfies the staleness + provenance
(1) edit src/modules/foo-999999.0a1.0.tm code change [K1]
(2) bump src/modules/foo-buildversion.txt module version + changelog comment
punkproject.toml + CHANGELOG.md project version (if shipped behaviour) [K2]
punkproject.toml + CHANGELOG.md project version (a bake input) [K2]
(3) test tclsh src/tests/runtests.tcl ... runs against src/ trees (if present)
(4) commit source changes (git/fossil) clean tree -> committed provenance [K3]
(5) mint tclsh src/make.tcl modules -confirm 0 (or 'packages' = modules + libs)
(6) promote tclsh src/make.tcl bootsupport -confirm 0 run twice: 2nd pass must copy 0 files [K4]
(7) promote tclsh src/make.tcl vfscommonupdate -confirm 0
(8) bake tclsh src/make.tcl bake -confirm 0 kits; close running kit shells first [K5]
(8) bake tclsh src/make.tcl bake -confirm 0 -expect-projectversion <ver>
kits; close running kit shells first [K5]; <ver> = the step (2) project version -
the project-version gate aborts flagless non-interactive bakes on a pending bump [K2]
(9) commit tracked output trees src/bootsupport, thin-layout script/
manifest/gitignore.in payload copies
(src/project_layouts +
@ -2906,6 +2919,15 @@ KEY / NOTES
[K2] Project version (punkproject.toml [project] version + matching CHANGELOG.md
entry) is change-driven: bump when the change ships user-visible behaviour.
It is also a BAKE INPUT - bake reads punkproject.toml into every kit's
punkkit-stamp.toml (project_version, surfaced by '<kit> buildinfo') and
derives versioned-scheme output names from it - so the bump belongs BEFORE
the bake: bumping afterwards mis-stamps every kit and costs a full re-bake.
bake/bakehouse/bin hold a project-version gate: when src/ has commits since
punkproject.toml last changed and no bump is staged, a terminal run prompts
(y/N) and a non-interactive run aborts fast unless the intended version is
asserted with -expect-projectversion <ver>; a mismatched assertion always
aborts. -confirm 0 does NOT waive this gate (see [K6]).
Advisory check: tclsh src/make.tcl projectversion
[K3] Every output target root gets a .punkcheck file recording what was installed
@ -2929,7 +2951,9 @@ KEY / NOTES
[K6] All confirmation prompts follow -confirm: unattended/agent runs must pass
-confirm 0 (non-interactive stdin aborts fast at prompts; piping 'y' is
retired). 'make.tcl help <subcommand>' shows per-subcommand usage.
retired). EXCEPTION: the [K2] project-version gate is deliberately NOT
waived by -confirm 0 - assert with -expect-projectversion, or bump first.
'make.tcl help <subcommand>' shows per-subcommand usage.
[K7] Testing hooks along the way:
- src/tests/runtests.tcl exercises the src/ trees directly (dev modules).
@ -3144,7 +3168,11 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Deliberately does NOT run the promotion gates (bootsupport,
vfscommonupdate): payload promotion is a confirm-gated, committed
act - on a clean checkout the gates are already satisfied by the
committed tree."
committed tree.
Holds the project-version gate as for bake: a pending-looking
project-version bump prompts on a terminal and aborts a
non-interactive run unless -expect-projectversion asserts the
intended version (-confirm 0 does not waive the gate)."
packages
"
Mint (stamp real versions, pack modpods; plain copy where no
@ -3186,7 +3214,14 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Does not rebuild modules/libs and does not run the promotion gates -
minted modules reach the kit payload only via 'make.tcl
vfscommonupdate' (committed for provenance). Consumer full path from
a clean checkout: 'make.tcl bakehouse'."
a clean checkout: 'make.tcl bakehouse'.
The project version is a BAKE INPUT (stamped into every kit's
punkkit-stamp.toml and used for versioned-scheme output names):
when a bump looks pending (src/ commits since punkproject.toml
last changed, none staged) the project-version gate prompts on a
terminal and aborts a non-interactive run unless
-expect-projectversion asserts the intended version. -confirm 0
does not waive the gate; a mismatched assertion always aborts."
bakelist
"
List the kit outputs configured in src/runtime/mapvfs.toml (the
@ -3260,7 +3295,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bin
"
Install executables from src/bin to <projectdir>/bin, then check vfs
folders and bake kit/zipkit executables as for the bake subcommand."
folders and bake kit/zipkit executables as for the bake subcommand.
Holds the project-version gate as for bake (see 'make.tcl help
bake' and the -expect-projectversion flag)."
vendorupdate
"
Update the src/vendormodules based on src/vendormodules/include_modules.config.
@ -3298,7 +3335,10 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
projectversion
"
Advisory check: verify CHANGELOG.md matches punkproject.toml and warn
if src/ has changes since the last project-version bump."
if src/ has changes since the last project-version bump.
The same computation feeds the project-version gate on
bake/bakehouse/bin (the project version is a bake input - see
'make.tcl help bake' and -expect-projectversion)."
workflow
"
Print a plain-text (ASCII diagram) overview of the release
@ -3411,6 +3451,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bootsupport minor-version staleness gate -> proceed
vfscommonupdate REPLACE confirmation -> proceed
kit source/target type mismatch -> skip baking that kit
project-version pending-bump gate -> abort unless
-expect-projectversion asserts the version (never waived
by -confirm 0)
Note: Tcl 8.6 lacks the terminal probe (-inputmode) so stdin is assumed
interactive there - piped runs under 8.6 should always pass -confirm
explicitly."
@ -3449,6 +3492,25 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
(packages -> test -> bootsupport/vfscommonupdate -> commit -> bake).
Pass -dirty-abort 0 to proceed anyway with PROVENANCE-WARNINGs."
}
#project-version bake gate assertion (2026-08-06) - see the gate block
#beside the producing-commands dirty-src check. Deliberately its own
#acknowledgement channel: agent runs pass -confirm 0 habitually, so a
#gate waived by -confirm 0 would be bypassed by reflex.
variable OPT_EXPECTPROJECTVERSION {
-expect-projectversion -type string -default "" -help\
"Assert the punkproject.toml project version this bake is intended
to ship. The project version is a BAKE INPUT - stamped into every
kit (punkkit-stamp.toml project_version, surfaced by
'<kit> buildinfo') and used for versioned-scheme output names -
so an assertion that does not match punkproject.toml aborts
before any kit work. Supplying a matching assertion also
satisfies the project-version gate when a bump looks pending
(src/ commits since punkproject.toml last changed, none staged):
the bake then proceeds as a conscious no-bump decision. Without
it, that pending state prompts (y/N) on an interactive terminal
and aborts fast otherwise. -confirm 0 does NOT waive this gate.
'make.tcl projectversion' shows the same check standalone."
}
#libfetch fragments (G-139): consent is keyed to SERVER TRUST (the G-123
#posture) - the canonical punkbin origin needs no acknowledgement, any
#other origin needs the explicit flag. Never an interactive prompt.
@ -3519,6 +3581,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT_ON}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3565,6 +3628,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max -1
kitname -type string -optional 1 -multiple 1${$KITNAME_CHOICEPART} -help -&
@ -3607,6 +3671,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
@opts
${$OPT_FORCEKILL}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3916,6 +3981,7 @@ set ::punkboot::opt_confirm 1
set ::punkboot::opt_serverurl ""
set ::punkboot::opt_trust_server 0
set ::punkboot::opt_force 0
set ::punkboot::opt_expect_projectversion "" ;#project-version bake gate assertion (2026-08-06)
set ::punkboot::opt_kitnames [list] ;#G-121: requested kit names for bake/bakelist (empty = all configured)
if {$::punkboot::punkargs_ok} {
@ -3991,7 +4057,7 @@ if {$::punkboot::punkargs_ok} {
}
set ::punkboot::command $subcommand
set _opts [dict get $argd opts]
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force} {
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force -expect-projectversion opt_expect_projectversion} {
if {[dict exists $_opts $_optname]} {
set ::punkboot::$_var [dict get $_opts $_optname]
}
@ -4034,6 +4100,10 @@ if {$::punkboot::punkargs_ok} {
set ::punkboot::opt_serverurl [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-expect-projectversion {
set ::punkboot::opt_expect_projectversion [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-confirm {
set v [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
@ -4388,6 +4458,62 @@ 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]]
}
#Project-version status - the shared computation behind the 'projectversion' advisory
#subcommand and the project-version bake gate (2026-08-06). The project version is a
#BAKE INPUT: the kit stamp (punkkit-stamp.toml project_version - G-025) and the
#versioned-scheme output names both read punkproject.toml at bake time, so baking from
#a tree whose bump is still pending mis-stamps every kit and costs a full re-bake to
#correct (the 0.55.1/0.56.0 incident this gate exists to prevent).
#Returns a dict:
# available 0|1 - punkboot::utils loadable (version fields readable)
# pp_version punkproject.toml project version, "" if unreadable
# cl_version latest CHANGELOG.md version header, "" if none found
# staleness ok - no src/ commits since punkproject.toml last changed
# overdue - src/ commits since then and no version change staged
# bump_uncommitted - working-tree punkproject.toml version differs from
# HEAD's (bump done, commit pending)
# unavailable - punkboot::utils, git or the file history not queryable
# src_commits commit count behind overdue/bump_uncommitted (0 otherwise)
# src_commitlines up to 5 --oneline lines of those commits
# head_version HEAD's punkproject.toml version, "" if unknown
proc ::punkboot::get_projectversion_status {projectroot} {
set status [dict create available 0 pp_version "" cl_version "" staleness unavailable src_commits 0 src_commitlines [list] head_version ""]
if {[catch {package require punkboot::utils}]} {
return $status
}
dict set status available 1
catch {dict set status pp_version [::punkboot::utils::read_punkproject_version [file join $projectroot punkproject.toml]]}
catch {dict set status cl_version [::punkboot::utils::read_changelog_latest_version [file join $projectroot CHANGELOG.md]]}
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {![string length $pp_commit]} {
return $status ;#no git/no history - staleness stays 'unavailable'
}
if {[catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
return $status
}
set src_changes [string trim $src_raw]
if {![string length $src_changes]} {
dict set status staleness ok
return $status
}
set commitlines [split $src_changes \n]
dict set status src_commits [llength $commitlines]
dict set status src_commitlines [lrange $commitlines 0 4]
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
dict set status head_version $head_version
if {[string length $head_version] && [string length [dict get $status pp_version]] && [dict get $status pp_version] ne $head_version} {
dict set status staleness bump_uncommitted
} else {
dict set status staleness overdue
}
return $status
}
#Replace a file that may be MEMORY-MAPPED by another process (or by this one).
#
#On windows an ordinary 'file copy -force' onto a mapped file is refused: a zipfs mount
@ -4637,6 +4763,82 @@ if {$::punkboot::command in {bakehouse packages modules libs bake vfslibs bin bo
}
}
#Project-version bake gate (2026-08-06, user-directed after the 0.55.1 kit mis-stamp):
#the kit-assembling commands read punkproject.toml at bake time (kit stamp + versioned-
#scheme names - see get_projectversion_status), so a version bump still pending at bake
#time silently mis-stamps every kit. When src/ has commits since punkproject.toml last
#changed and no bump is staged, the gate requires a conscious decision:
# interactive terminal + -confirm 1 : y/N prompt (default N)
# non-interactive OR -confirm 0 : abort fast UNLESS -expect-projectversion asserts
# the version this bake is intended to ship. -confirm 0 deliberately does NOT waive
# the gate: agent runs pass -confirm 0 habitually, and a gate their habitual flags
# waive is no gate. The assertion is a version, not a boolean, so satisfying it
# requires stating a checkable claim about punkproject.toml rather than a -force.
#A supplied -expect-projectversion is verified in EVERY staleness state - a mismatch
#with punkproject.toml always aborts before any kit work. Degradations mirror the
#dirty-src check above: status unavailable -> NOTE and proceed flagless, but a supplied
#assertion aborts as unverifiable rather than silently losing the requested strictness.
if {$::punkboot::command in {bakehouse bake bin}} {
set _pv [::punkboot::get_projectversion_status $projectroot]
set _pv_expect $::punkboot::opt_expect_projectversion
set _pv_current [dict get $_pv pp_version]
if {$_pv_expect ne ""} {
if {![dict get $_pv available] || ![string length $_pv_current]} {
puts stderr "-aborted- (-expect-projectversion $_pv_expect supplied but the punkproject.toml version is unreadable (punkboot::utils not loadable from bootsupport, or file missing/unparseable) - cannot verify the assertion)"
exit 1
}
if {$_pv_expect ne $_pv_current} {
puts stderr "make.tcl $::punkboot::command: -expect-projectversion MISMATCH: asserted $_pv_expect but punkproject.toml says $_pv_current"
puts stderr " The project version is a BAKE INPUT (kit stamp + versioned-scheme output names)."
puts stderr " Either bump punkproject.toml + CHANGELOG.md to $_pv_expect (root AGENTS.md 'Project"
puts stderr " Versioning'), or re-assert the version this bake should actually ship."
puts stderr "-aborted- (project-version assertion mismatch - nothing built, nothing deployed)"
exit 1
}
puts stdout "project-version gate: OK (asserted -expect-projectversion $_pv_expect matches punkproject.toml)"
if {[dict get $_pv staleness] eq "overdue"} {
puts stdout " NOTE: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed - proceeding on the assertion that this bake intentionally ships $_pv_expect"
}
} else {
switch -- [dict get $_pv staleness] {
overdue {
::punkboot::print_bake_warnings [list "project-version bump may be PENDING: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed (version $_pv_current)"]
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
foreach _pvline [dict get $_pv src_commitlines] {
puts stderr " $_pvline"
}
if {!$::punkboot::opt_confirm || ![::punkboot::lib::stdin_is_interactive]} {
set ::punkboot::bake_warnings_pending [list] ;#aborting adjacent to the warning - no recap needed
puts stderr " Resolve before baking:"
puts stderr " bump: edit punkproject.toml + CHANGELOG.md (root AGENTS.md 'Project Versioning'), commit, re-run"
puts stderr " assert: make.tcl $::punkboot::command -confirm 0 -expect-projectversion $_pv_current (this bake intentionally ships $_pv_current)"
puts stderr " ('make.tcl projectversion' shows this check standalone; -confirm 0 does not waive this gate)"
puts stderr "-aborted- (project-version bump appears pending and no -expect-projectversion assertion supplied - nothing built, nothing deployed)"
exit 1
}
if {![::punkboot::lib::bootsupport_prompt_yesno "PROJECT-VERSION: baking now stamps version $_pv_current into every kit; a later bump costs a full re-bake to correct."]} {
set ::punkboot::bake_warnings_pending [list]
puts stderr "-aborted- by user (bump punkproject.toml + CHANGELOG.md first, or assert intent with -expect-projectversion)"
exit 1
}
puts stderr "-proceeding at project version $_pv_current at user's request- (warning recapped at end of run)"
}
bump_uncommitted {
puts stdout "project-version gate: OK (punkproject.toml bumped [dict get $_pv head_version] -> $_pv_current, commit pending - bake reads the working-tree value)"
}
ok {
#quiet - no src/ commits since the version last changed
}
default {
puts stdout "NOTE: project-version gate skipped ([expr {[dict get $_pv available] ? {git history not queryable} : {punkboot::utils not loadable from bootsupport}}]) - continuing without it"
}
}
}
unset -nocomplain _pv _pv_expect _pv_current _pvline
}
if {$::punkboot::command eq "check"} {
set sep [string repeat - 75]
puts stdout $sep
@ -4830,6 +5032,28 @@ if {$::punkboot::command eq "check"} {
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'."
}
# Project-version bake gate status - pending-bump detection for the kit-assembling commands
puts stdout $sep
set _pv [::punkboot::get_projectversion_status $projectroot]
if {[dict get $_pv available]} {
puts stdout "project-version bake gate: ACTIVE"
puts stdout " the project version is a BAKE INPUT (kit stamp punkkit-stamp.toml project_version + versioned-"
puts stdout " scheme output names), so bake/bakehouse/bin verify a supplied -expect-projectversion against"
puts stdout " punkproject.toml (mismatch always aborts) and, when src/ has commits since the version last"
puts stdout " changed with no bump staged, prompt on a terminal / abort fast non-interactively unless the"
puts stdout " version is asserted. -confirm 0 does not waive the gate. Advisory twin: 'make.tcl projectversion'."
switch -- [dict get $_pv staleness] {
ok { puts stdout " current state: OK (no src/ commits since punkproject.toml version [dict get $_pv pp_version] last changed)" }
overdue { puts stdout " current state: bump may be PENDING ([dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed at version [dict get $_pv pp_version]) - flagless non-interactive bakes abort" }
bump_uncommitted { puts stdout " current state: bump staged ([dict get $_pv head_version] -> [dict get $_pv pp_version], commit pending) - bakes proceed reading the working-tree value" }
default { puts stdout " current state: staleness unknown (git history not queryable) - bakes proceed with a NOTE" }
}
} else {
puts stdout "project-version bake gate: UNAVAILABLE (punkboot::utils not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot detect a pending bump - run 'make.tcl modules' then"
puts stdout " 'make.tcl bootsupport'. A supplied -expect-projectversion still aborts as unverifiable."
}
unset -nocomplain _pv
# Payload/target consistency checks (G-133) - advisory arch scan + smoke-require probe
puts stdout $sep
#availability probe only - the path deliberately does not exist
@ -4973,24 +5197,26 @@ if {$::punkboot::command eq "info"} {
}
if {$::punkboot::command eq "projectversion"} {
package require punkboot::utils
set sep [string repeat - 75]
puts stdout $sep
puts stdout "project version check"
puts stdout $sep
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set src_folder [file join $projectroot src]
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set pp_version [::punkboot::utils::read_punkproject_version $pp_file]
set cl_version [::punkboot::utils::read_changelog_latest_version $cl_file]
#shared computation with the project-version bake gate on bake/bakehouse/bin
set pvstatus [::punkboot::get_projectversion_status $projectroot]
set pp_version [dict get $pvstatus pp_version]
set cl_version [dict get $pvstatus cl_version]
puts stdout "punkproject.toml version : [expr {[string length $pp_version] ? $pp_version : "(not found)"}]"
puts stdout "CHANGELOG.md version : [expr {[string length $cl_version] ? $cl_version : "(not found)"}]"
# 1. Consistency: CHANGELOG latest header must match punkproject.toml version.
if {![string length $pp_version]} {
if {![dict get $pvstatus available]} {
puts stderr "WARNING: punkboot::utils not loadable from bootsupport - version fields unreadable"
} elseif {![string length $pp_version]} {
puts stderr "WARNING: could not read version from $pp_file"
} elseif {![string length $cl_version]} {
puts stderr "WARNING: no '## \[X.Y.Z\]' version header found in $cl_file"
@ -5007,48 +5233,41 @@ if {$::punkboot::command eq "projectversion"} {
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {[string length $pp_commit]} {
if {![catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {
if {$i >= 5} break
puts stderr " $line"
incr i
}
switch -- [dict get $pvstatus staleness] {
bump_uncommitted {
set n [dict get $pvstatus src_commits]
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ([dict get $pvstatus head_version] -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
}
overdue {
set n [dict get $pvstatus src_commits]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
puts stderr " bake/bakehouse/bin hold a project-version gate in this state: non-interactive runs abort"
puts stderr " unless the intended version is asserted, e.g for a deliberate no-bump bake:"
puts stderr " make.tcl bake -confirm 0 -expect-projectversion $pp_version"
}
ok {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
}
default {
if {[dict get $pvstatus available]} {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
} else {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
puts stdout "staleness: skipped (punkboot::utils not loadable from bootsupport)"
}
} else {
puts stdout "staleness: skipped (git query failed)"
}
} else {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
}
puts stdout $sep

335
src/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -2589,16 +2589,20 @@ proc ::punkboot::punkboot_gethelp {args} {
append h "" \n
append h " $scriptname help or just $scriptname" \n
append h " - This help." \n \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0?" \n
append h " $scriptname bakehouse ?-k? ?-dirty-abort 1|0? ?-expect-projectversion <ver>?" \n
append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to <projectdir>/bin" \n
append h " - refuses uncommitted src by default (-dirty-abort defaults ON: the bakehouse bakes from the committed recipe; pass -dirty-abort 0 to override)" \n
append h " - holds the project-version gate as for bake (see the -expect-projectversion flag below)" \n
append h " - the optional -k flag will terminate running processes matching the executable being baked (if applicable)" \n
append h " - does NOT run the promotion gates (bootsupport, vfscommonupdate) - on a clean checkout they are already satisfied by the committed tree" \n \n
append h " $scriptname bake ?-k? ?-force? ?kitname ...?" \n
append h " $scriptname bake ?-k? ?-force? ?-expect-projectversion <ver>? ?kitname ...?" \n
append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into <projectdir>/bin" \n
append h " - includes the vfslibs phase; does not re-mint modules/libs and does not run the promotion gates" \n
append h " - with kitname arguments, bakes and deploys only the named configured kits (unknown names error before any bake)" \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n \n
append h " - -force re-assembles kits whose inputs are unchanged (bypasses the 'no change detected' skip)" \n
append h " - project-version gate: the project version is a BAKE INPUT (kit stamp + versioned-scheme output names) - a" \n
append h " pending-looking bump (src/ commits since punkproject.toml last changed, none staged) prompts on a terminal" \n
append h " and aborts a non-interactive run unless -expect-projectversion asserts the intended version" \n
append h " $scriptname bakelist ?kitname ...?" \n
append h " - list the kit outputs configured in src/runtime/mapvfs.toml: name, kit type, runtime (with presence)," \n
append h " vfs folder and deployed state (bin copy absent/current/stale/nobake vs the src/_bake product)" \n
@ -2640,7 +2644,8 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " $scriptname shell" \n
append h " - run the punk shell using bootsupport libraries." \n
append h " $scriptname projectversion" \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n \n
append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n
append h " (the same computation feeds the project-version gate on bake/bakehouse/bin - see -expect-projectversion)" \n \n
append h " $scriptname workflow" \n
append h " - print an ASCII data-flow overview of the release workflow (edit -> mint -> promote -> bake) incl. the TERMINOLOGY key" \n \n
append h " $scriptname buildsuite list|info|build ?<suitename>? ?driver-args ...?" \n
@ -2671,6 +2676,12 @@ proc ::punkboot::punkboot_gethelp {args} {
append h " Warnings carry a plain PROVENANCE-WARNING: prefix (greppable in redirected output) and are recapped at the end of the run." \n
append h " Use '$scriptname check' to see the current provenance status. To evaluate uncommitted source without minting or baking," \n
append h " use '<builtexe> src' or '<builtexe> src shell'." \n \n
append h " -expect-projectversion <ver> (bake, bakehouse, bin)" \n
append h " - assert the punkproject.toml project version this bake is intended to ship (the version is a BAKE INPUT:" \n
append h " stamped into every kit + used for versioned-scheme output names). A mismatch aborts before any kit work;" \n
append h " a match satisfies the project-version gate when a bump looks pending (a conscious no-bump bake)." \n
append h " Without it, that pending state prompts on a terminal and aborts fast non-interactively." \n
append h " -confirm 0 does NOT waive this gate. '$scriptname projectversion' shows the check standalone." \n \n
append h "" \n
append h [punkboot_availability_note]
return $h
@ -2736,13 +2747,15 @@ RELEASE SEQUENCE (the developer order that satisfies the staleness + provenance
(1) edit src/modules/foo-999999.0a1.0.tm code change [K1]
(2) bump src/modules/foo-buildversion.txt module version + changelog comment
punkproject.toml + CHANGELOG.md project version (if shipped behaviour) [K2]
punkproject.toml + CHANGELOG.md project version (a bake input) [K2]
(3) test tclsh src/tests/runtests.tcl ... runs against src/ trees (if present)
(4) commit source changes (git/fossil) clean tree -> committed provenance [K3]
(5) mint tclsh src/make.tcl modules -confirm 0 (or 'packages' = modules + libs)
(6) promote tclsh src/make.tcl bootsupport -confirm 0 run twice: 2nd pass must copy 0 files [K4]
(7) promote tclsh src/make.tcl vfscommonupdate -confirm 0
(8) bake tclsh src/make.tcl bake -confirm 0 kits; close running kit shells first [K5]
(8) bake tclsh src/make.tcl bake -confirm 0 -expect-projectversion <ver>
kits; close running kit shells first [K5]; <ver> = the step (2) project version -
the project-version gate aborts flagless non-interactive bakes on a pending bump [K2]
(9) commit tracked output trees src/bootsupport, thin-layout script/
manifest/gitignore.in payload copies
(src/project_layouts +
@ -2906,6 +2919,15 @@ KEY / NOTES
[K2] Project version (punkproject.toml [project] version + matching CHANGELOG.md
entry) is change-driven: bump when the change ships user-visible behaviour.
It is also a BAKE INPUT - bake reads punkproject.toml into every kit's
punkkit-stamp.toml (project_version, surfaced by '<kit> buildinfo') and
derives versioned-scheme output names from it - so the bump belongs BEFORE
the bake: bumping afterwards mis-stamps every kit and costs a full re-bake.
bake/bakehouse/bin hold a project-version gate: when src/ has commits since
punkproject.toml last changed and no bump is staged, a terminal run prompts
(y/N) and a non-interactive run aborts fast unless the intended version is
asserted with -expect-projectversion <ver>; a mismatched assertion always
aborts. -confirm 0 does NOT waive this gate (see [K6]).
Advisory check: tclsh src/make.tcl projectversion
[K3] Every output target root gets a .punkcheck file recording what was installed
@ -2929,7 +2951,9 @@ KEY / NOTES
[K6] All confirmation prompts follow -confirm: unattended/agent runs must pass
-confirm 0 (non-interactive stdin aborts fast at prompts; piping 'y' is
retired). 'make.tcl help <subcommand>' shows per-subcommand usage.
retired). EXCEPTION: the [K2] project-version gate is deliberately NOT
waived by -confirm 0 - assert with -expect-projectversion, or bump first.
'make.tcl help <subcommand>' shows per-subcommand usage.
[K7] Testing hooks along the way:
- src/tests/runtests.tcl exercises the src/ trees directly (dev modules).
@ -3144,7 +3168,11 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Deliberately does NOT run the promotion gates (bootsupport,
vfscommonupdate): payload promotion is a confirm-gated, committed
act - on a clean checkout the gates are already satisfied by the
committed tree."
committed tree.
Holds the project-version gate as for bake: a pending-looking
project-version bump prompts on a terminal and aborts a
non-interactive run unless -expect-projectversion asserts the
intended version (-confirm 0 does not waive the gate)."
packages
"
Mint (stamp real versions, pack modpods; plain copy where no
@ -3186,7 +3214,14 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
Does not rebuild modules/libs and does not run the promotion gates -
minted modules reach the kit payload only via 'make.tcl
vfscommonupdate' (committed for provenance). Consumer full path from
a clean checkout: 'make.tcl bakehouse'."
a clean checkout: 'make.tcl bakehouse'.
The project version is a BAKE INPUT (stamped into every kit's
punkkit-stamp.toml and used for versioned-scheme output names):
when a bump looks pending (src/ commits since punkproject.toml
last changed, none staged) the project-version gate prompts on a
terminal and aborts a non-interactive run unless
-expect-projectversion asserts the intended version. -confirm 0
does not waive the gate; a mismatched assertion always aborts."
bakelist
"
List the kit outputs configured in src/runtime/mapvfs.toml (the
@ -3260,7 +3295,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bin
"
Install executables from src/bin to <projectdir>/bin, then check vfs
folders and bake kit/zipkit executables as for the bake subcommand."
folders and bake kit/zipkit executables as for the bake subcommand.
Holds the project-version gate as for bake (see 'make.tcl help
bake' and the -expect-projectversion flag)."
vendorupdate
"
Update the src/vendormodules based on src/vendormodules/include_modules.config.
@ -3298,7 +3335,10 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
projectversion
"
Advisory check: verify CHANGELOG.md matches punkproject.toml and warn
if src/ has changes since the last project-version bump."
if src/ has changes since the last project-version bump.
The same computation feeds the project-version gate on
bake/bakehouse/bin (the project version is a bake input - see
'make.tcl help bake' and -expect-projectversion)."
workflow
"
Print a plain-text (ASCII diagram) overview of the release
@ -3411,6 +3451,9 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
bootsupport minor-version staleness gate -> proceed
vfscommonupdate REPLACE confirmation -> proceed
kit source/target type mismatch -> skip baking that kit
project-version pending-bump gate -> abort unless
-expect-projectversion asserts the version (never waived
by -confirm 0)
Note: Tcl 8.6 lacks the terminal probe (-inputmode) so stdin is assumed
interactive there - piped runs under 8.6 should always pass -confirm
explicitly."
@ -3449,6 +3492,25 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
(packages -> test -> bootsupport/vfscommonupdate -> commit -> bake).
Pass -dirty-abort 0 to proceed anyway with PROVENANCE-WARNINGs."
}
#project-version bake gate assertion (2026-08-06) - see the gate block
#beside the producing-commands dirty-src check. Deliberately its own
#acknowledgement channel: agent runs pass -confirm 0 habitually, so a
#gate waived by -confirm 0 would be bypassed by reflex.
variable OPT_EXPECTPROJECTVERSION {
-expect-projectversion -type string -default "" -help\
"Assert the punkproject.toml project version this bake is intended
to ship. The project version is a BAKE INPUT - stamped into every
kit (punkkit-stamp.toml project_version, surfaced by
'<kit> buildinfo') and used for versioned-scheme output names -
so an assertion that does not match punkproject.toml aborts
before any kit work. Supplying a matching assertion also
satisfies the project-version gate when a bump looks pending
(src/ commits since punkproject.toml last changed, none staged):
the bake then proceeds as a conscious no-bump decision. Without
it, that pending state prompts (y/N) on an interactive terminal
and aborts fast otherwise. -confirm 0 does NOT waive this gate.
'make.tcl projectversion' shows the same check standalone."
}
#libfetch fragments (G-139): consent is keyed to SERVER TRUST (the G-123
#posture) - the canonical punkbin origin needs no acknowledgement, any
#other origin needs the explicit flag. Never an interactive prompt.
@ -3519,6 +3581,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT_ON}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3565,6 +3628,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
${$OPT_FORCEKILL}
${$OPT_FORCE}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max -1
kitname -type string -optional 1 -multiple 1${$KITNAME_CHOICEPART} -help -&
@ -3607,6 +3671,7 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO
@opts
${$OPT_FORCEKILL}
${$OPT_DIRTYABORT}
${$OPT_EXPECTPROJECTVERSION}
${$OPT_CONFIRM}
@values -min 0 -max 0
}
@ -3916,6 +3981,7 @@ set ::punkboot::opt_confirm 1
set ::punkboot::opt_serverurl ""
set ::punkboot::opt_trust_server 0
set ::punkboot::opt_force 0
set ::punkboot::opt_expect_projectversion "" ;#project-version bake gate assertion (2026-08-06)
set ::punkboot::opt_kitnames [list] ;#G-121: requested kit names for bake/bakelist (empty = all configured)
if {$::punkboot::punkargs_ok} {
@ -3991,7 +4057,7 @@ if {$::punkboot::punkargs_ok} {
}
set ::punkboot::command $subcommand
set _opts [dict get $argd opts]
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force} {
foreach {_optname _var} {-k opt_forcekill -dirty-abort opt_dirty_abort -confirm opt_confirm -serverurl opt_serverurl -trust-server opt_trust_server -force opt_force -expect-projectversion opt_expect_projectversion} {
if {[dict exists $_opts $_optname]} {
set ::punkboot::$_var [dict get $_opts $_optname]
}
@ -4034,6 +4100,10 @@ if {$::punkboot::punkargs_ok} {
set ::punkboot::opt_serverurl [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-expect-projectversion {
set ::punkboot::opt_expect_projectversion [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
}
-confirm {
set v [lindex $argsleft 0]
set argsleft [lrange $argsleft 1 end]
@ -4388,6 +4458,62 @@ 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]]
}
#Project-version status - the shared computation behind the 'projectversion' advisory
#subcommand and the project-version bake gate (2026-08-06). The project version is a
#BAKE INPUT: the kit stamp (punkkit-stamp.toml project_version - G-025) and the
#versioned-scheme output names both read punkproject.toml at bake time, so baking from
#a tree whose bump is still pending mis-stamps every kit and costs a full re-bake to
#correct (the 0.55.1/0.56.0 incident this gate exists to prevent).
#Returns a dict:
# available 0|1 - punkboot::utils loadable (version fields readable)
# pp_version punkproject.toml project version, "" if unreadable
# cl_version latest CHANGELOG.md version header, "" if none found
# staleness ok - no src/ commits since punkproject.toml last changed
# overdue - src/ commits since then and no version change staged
# bump_uncommitted - working-tree punkproject.toml version differs from
# HEAD's (bump done, commit pending)
# unavailable - punkboot::utils, git or the file history not queryable
# src_commits commit count behind overdue/bump_uncommitted (0 otherwise)
# src_commitlines up to 5 --oneline lines of those commits
# head_version HEAD's punkproject.toml version, "" if unknown
proc ::punkboot::get_projectversion_status {projectroot} {
set status [dict create available 0 pp_version "" cl_version "" staleness unavailable src_commits 0 src_commitlines [list] head_version ""]
if {[catch {package require punkboot::utils}]} {
return $status
}
dict set status available 1
catch {dict set status pp_version [::punkboot::utils::read_punkproject_version [file join $projectroot punkproject.toml]]}
catch {dict set status cl_version [::punkboot::utils::read_changelog_latest_version [file join $projectroot CHANGELOG.md]]}
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {![string length $pp_commit]} {
return $status ;#no git/no history - staleness stays 'unavailable'
}
if {[catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
return $status
}
set src_changes [string trim $src_raw]
if {![string length $src_changes]} {
dict set status staleness ok
return $status
}
set commitlines [split $src_changes \n]
dict set status src_commits [llength $commitlines]
dict set status src_commitlines [lrange $commitlines 0 4]
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
dict set status head_version $head_version
if {[string length $head_version] && [string length [dict get $status pp_version]] && [dict get $status pp_version] ne $head_version} {
dict set status staleness bump_uncommitted
} else {
dict set status staleness overdue
}
return $status
}
#Replace a file that may be MEMORY-MAPPED by another process (or by this one).
#
#On windows an ordinary 'file copy -force' onto a mapped file is refused: a zipfs mount
@ -4637,6 +4763,82 @@ if {$::punkboot::command in {bakehouse packages modules libs bake vfslibs bin bo
}
}
#Project-version bake gate (2026-08-06, user-directed after the 0.55.1 kit mis-stamp):
#the kit-assembling commands read punkproject.toml at bake time (kit stamp + versioned-
#scheme names - see get_projectversion_status), so a version bump still pending at bake
#time silently mis-stamps every kit. When src/ has commits since punkproject.toml last
#changed and no bump is staged, the gate requires a conscious decision:
# interactive terminal + -confirm 1 : y/N prompt (default N)
# non-interactive OR -confirm 0 : abort fast UNLESS -expect-projectversion asserts
# the version this bake is intended to ship. -confirm 0 deliberately does NOT waive
# the gate: agent runs pass -confirm 0 habitually, and a gate their habitual flags
# waive is no gate. The assertion is a version, not a boolean, so satisfying it
# requires stating a checkable claim about punkproject.toml rather than a -force.
#A supplied -expect-projectversion is verified in EVERY staleness state - a mismatch
#with punkproject.toml always aborts before any kit work. Degradations mirror the
#dirty-src check above: status unavailable -> NOTE and proceed flagless, but a supplied
#assertion aborts as unverifiable rather than silently losing the requested strictness.
if {$::punkboot::command in {bakehouse bake bin}} {
set _pv [::punkboot::get_projectversion_status $projectroot]
set _pv_expect $::punkboot::opt_expect_projectversion
set _pv_current [dict get $_pv pp_version]
if {$_pv_expect ne ""} {
if {![dict get $_pv available] || ![string length $_pv_current]} {
puts stderr "-aborted- (-expect-projectversion $_pv_expect supplied but the punkproject.toml version is unreadable (punkboot::utils not loadable from bootsupport, or file missing/unparseable) - cannot verify the assertion)"
exit 1
}
if {$_pv_expect ne $_pv_current} {
puts stderr "make.tcl $::punkboot::command: -expect-projectversion MISMATCH: asserted $_pv_expect but punkproject.toml says $_pv_current"
puts stderr " The project version is a BAKE INPUT (kit stamp + versioned-scheme output names)."
puts stderr " Either bump punkproject.toml + CHANGELOG.md to $_pv_expect (root AGENTS.md 'Project"
puts stderr " Versioning'), or re-assert the version this bake should actually ship."
puts stderr "-aborted- (project-version assertion mismatch - nothing built, nothing deployed)"
exit 1
}
puts stdout "project-version gate: OK (asserted -expect-projectversion $_pv_expect matches punkproject.toml)"
if {[dict get $_pv staleness] eq "overdue"} {
puts stdout " NOTE: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed - proceeding on the assertion that this bake intentionally ships $_pv_expect"
}
} else {
switch -- [dict get $_pv staleness] {
overdue {
::punkboot::print_bake_warnings [list "project-version bump may be PENDING: [dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed (version $_pv_current)"]
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
foreach _pvline [dict get $_pv src_commitlines] {
puts stderr " $_pvline"
}
if {!$::punkboot::opt_confirm || ![::punkboot::lib::stdin_is_interactive]} {
set ::punkboot::bake_warnings_pending [list] ;#aborting adjacent to the warning - no recap needed
puts stderr " Resolve before baking:"
puts stderr " bump: edit punkproject.toml + CHANGELOG.md (root AGENTS.md 'Project Versioning'), commit, re-run"
puts stderr " assert: make.tcl $::punkboot::command -confirm 0 -expect-projectversion $_pv_current (this bake intentionally ships $_pv_current)"
puts stderr " ('make.tcl projectversion' shows this check standalone; -confirm 0 does not waive this gate)"
puts stderr "-aborted- (project-version bump appears pending and no -expect-projectversion assertion supplied - nothing built, nothing deployed)"
exit 1
}
if {![::punkboot::lib::bootsupport_prompt_yesno "PROJECT-VERSION: baking now stamps version $_pv_current into every kit; a later bump costs a full re-bake to correct."]} {
set ::punkboot::bake_warnings_pending [list]
puts stderr "-aborted- by user (bump punkproject.toml + CHANGELOG.md first, or assert intent with -expect-projectversion)"
exit 1
}
puts stderr "-proceeding at project version $_pv_current at user's request- (warning recapped at end of run)"
}
bump_uncommitted {
puts stdout "project-version gate: OK (punkproject.toml bumped [dict get $_pv head_version] -> $_pv_current, commit pending - bake reads the working-tree value)"
}
ok {
#quiet - no src/ commits since the version last changed
}
default {
puts stdout "NOTE: project-version gate skipped ([expr {[dict get $_pv available] ? {git history not queryable} : {punkboot::utils not loadable from bootsupport}}]) - continuing without it"
}
}
}
unset -nocomplain _pv _pv_expect _pv_current _pvline
}
if {$::punkboot::command eq "check"} {
set sep [string repeat - 75]
puts stdout $sep
@ -4830,6 +5032,28 @@ if {$::punkboot::command eq "check"} {
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'."
}
# Project-version bake gate status - pending-bump detection for the kit-assembling commands
puts stdout $sep
set _pv [::punkboot::get_projectversion_status $projectroot]
if {[dict get $_pv available]} {
puts stdout "project-version bake gate: ACTIVE"
puts stdout " the project version is a BAKE INPUT (kit stamp punkkit-stamp.toml project_version + versioned-"
puts stdout " scheme output names), so bake/bakehouse/bin verify a supplied -expect-projectversion against"
puts stdout " punkproject.toml (mismatch always aborts) and, when src/ has commits since the version last"
puts stdout " changed with no bump staged, prompt on a terminal / abort fast non-interactively unless the"
puts stdout " version is asserted. -confirm 0 does not waive the gate. Advisory twin: 'make.tcl projectversion'."
switch -- [dict get $_pv staleness] {
ok { puts stdout " current state: OK (no src/ commits since punkproject.toml version [dict get $_pv pp_version] last changed)" }
overdue { puts stdout " current state: bump may be PENDING ([dict get $_pv src_commits] src/ commit(s) since punkproject.toml last changed at version [dict get $_pv pp_version]) - flagless non-interactive bakes abort" }
bump_uncommitted { puts stdout " current state: bump staged ([dict get $_pv head_version] -> [dict get $_pv pp_version], commit pending) - bakes proceed reading the working-tree value" }
default { puts stdout " current state: staleness unknown (git history not queryable) - bakes proceed with a NOTE" }
}
} else {
puts stdout "project-version bake gate: UNAVAILABLE (punkboot::utils not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot detect a pending bump - run 'make.tcl modules' then"
puts stdout " 'make.tcl bootsupport'. A supplied -expect-projectversion still aborts as unverifiable."
}
unset -nocomplain _pv
# Payload/target consistency checks (G-133) - advisory arch scan + smoke-require probe
puts stdout $sep
#availability probe only - the path deliberately does not exist
@ -4973,24 +5197,26 @@ if {$::punkboot::command eq "info"} {
}
if {$::punkboot::command eq "projectversion"} {
package require punkboot::utils
set sep [string repeat - 75]
puts stdout $sep
puts stdout "project version check"
puts stdout $sep
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set src_folder [file join $projectroot src]
set pp_file [file join $projectroot punkproject.toml]
set cl_file [file join $projectroot CHANGELOG.md]
set pp_version [::punkboot::utils::read_punkproject_version $pp_file]
set cl_version [::punkboot::utils::read_changelog_latest_version $cl_file]
#shared computation with the project-version bake gate on bake/bakehouse/bin
set pvstatus [::punkboot::get_projectversion_status $projectroot]
set pp_version [dict get $pvstatus pp_version]
set cl_version [dict get $pvstatus cl_version]
puts stdout "punkproject.toml version : [expr {[string length $pp_version] ? $pp_version : "(not found)"}]"
puts stdout "CHANGELOG.md version : [expr {[string length $cl_version] ? $cl_version : "(not found)"}]"
# 1. Consistency: CHANGELOG latest header must match punkproject.toml version.
if {![string length $pp_version]} {
if {![dict get $pvstatus available]} {
puts stderr "WARNING: punkboot::utils not loadable from bootsupport - version fields unreadable"
} elseif {![string length $pp_version]} {
puts stderr "WARNING: could not read version from $pp_file"
} elseif {![string length $cl_version]} {
puts stderr "WARNING: no '## \[X.Y.Z\]' version header found in $cl_file"
@ -5007,48 +5233,41 @@ if {$::punkboot::command eq "projectversion"} {
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
if {![catch {exec git -C $projectroot log -1 --format=%H -- punkproject.toml} pp_raw]} {
set pp_commit [string trim $pp_raw]
}
if {[string length $pp_commit]} {
if {![catch {exec git -C $projectroot log --oneline ${pp_commit}..HEAD -- src/} src_raw]} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {
if {$i >= 5} break
puts stderr " $line"
incr i
}
switch -- [dict get $pvstatus staleness] {
bump_uncommitted {
set n [dict get $pvstatus src_commits]
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ([dict get $pvstatus head_version] -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
}
overdue {
set n [dict get $pvstatus src_commits]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
puts stderr " (showing first 5 commits below)"
foreach line [dict get $pvstatus src_commitlines] {
puts stderr " $line"
}
puts stderr " The project version is a BAKE INPUT: bake stamps it into every kit (punkkit-stamp.toml"
puts stderr " project_version, surfaced by '<kit> buildinfo') and derives versioned-scheme output names"
puts stderr " from it. Baking before the bump mis-stamps every kit; correction costs a full re-bake."
puts stderr " bake/bakehouse/bin hold a project-version gate in this state: non-interactive runs abort"
puts stderr " unless the intended version is asserted, e.g for a deliberate no-bump bake:"
puts stderr " make.tcl bake -confirm 0 -expect-projectversion $pp_version"
}
ok {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
}
default {
if {[dict get $pvstatus available]} {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
} else {
puts stdout "staleness: OK (no src/ changes since last punkproject.toml commit)"
puts stdout "staleness: skipped (punkboot::utils not loadable from bootsupport)"
}
} else {
puts stdout "staleness: skipped (git query failed)"
}
} else {
puts stdout "staleness: skipped (punkproject.toml has no git history or git unavailable)"
}
puts stdout $sep

4
src/tests/shell/testsuites/punkexe/maketclhelp.test

@ -322,6 +322,8 @@ namespace eval ::testspace {
#grouped in a single column (2-column deferred until table cell wrapping)
#updated 2026-08-04 (agent, G-165) - bake/bakehouse gained the -force flag
#(bypass the no-change skip), so the pinned bakehouse synopsis carries [-force]
#updated 2026-08-06 (agent, project-version gate) - bake/bakehouse/bin gained
#-expect-projectversion, so the pinned synopsis carries it before [-confirm]
test maketcl_help_toplevel_overview {bare 'make.tcl help' renders the rich top-level overview: ESC-free, per-subcommand synopsis lines incl. the tool forms} -constraints {tclshavailable} -body {
set r [maketcl_run {help}]
set out [dict get $r output]
@ -329,7 +331,7 @@ namespace eval ::testspace {
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode]
lappend result esc [esc_count $out]
lappend result subcmd [regexp {subcommand} $out]
lappend result bakehousesyn [regexp {make\.tcl bakehouse \[-k\] \[-force\] \[-dirty-abort <bool>\] \[-confirm <bool>\]} $out]
lappend result bakehousesyn [regexp {make\.tcl bakehouse \[-k\] \[-force\] \[-dirty-abort <bool>\] \[-expect-projectversion <string>\] \[-confirm <bool>\]} $out]
lappend result toolformsyn [regexp {make\.tcl tool build \[-test <bool>\] \[toolname\]\.\.\.} $out]
lappend result bsformsyn [regexp {make\.tcl buildsuite build suitename \[driverarg\]\.\.\.} $out]
set result

284
src/tests/shell/testsuites/punkexe/maketclprojectversion.test

@ -0,0 +1,284 @@
package require tcltest
#Piped characterization of the make.tcl project-version bake gate (added 2026-08-06).
#The project version is a bake input: punkkit-stamp.toml project_version and the
#versioned-scheme output names are read from punkproject.toml at bake time, so the
#kit-assembling subcommands refuse to proceed with a non-interactive stdin when a
#bump looks pending - src/ commits since punkproject.toml last changed, none staged -
#unless -expect-projectversion asserts the intended version, and always refuse a
#mismatched assertion. -confirm 0 deliberately does not waive the gate.
#Runs make.tcl under the built punk executable's 'script' subcommand with output
#captured through a pipe and stdin half-closed - a non-interactive stdin by
#construction, exactly the population the gate exists to stop.
# - the 'check' and 'projectversion' pins run against this WORKING TREE
# - the gate VERDICT pins run against throwaway fixture projects under tcltest
# temporaryDirectory, each carrying a copy of the working-tree make.tcl and of
# src/bootsupport so punkboot::utils resolves. The git fixture is built at
# version 0.1.0 with one src/ commit after the last punkproject.toml commit -
# the pending-bump state; the no-git fixture exercises the documented NOTE
# degradation. Downstream-of-gate outcomes in fixtures - no runtimes to bake,
# no VCS for the bake workdir - are deliberately not pinned: the pins assert
# only the gate's own contract.
#Target executable resolved from env(PUNK_SHELL_TEST_EXE), else
#<projectroot>/bin/punk902z.exe then <projectroot>/bin/punkshell902. Skipped
#(constraint punkexeavailable) if none found; fixture tests additionally need git
#on PATH (constraint gitavailable).
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]}]
testConstraint gitavailable [expr {[llength [auto_execok git]] > 0}]
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 <somemaketcl> <subcommand...> with output captured through a
#pipe (stdin half-closed for immediate EOF - the gate must see a non-interactive
#stdin). scriptpath defaults to the working tree's src/make.tcl; the fixture tests
#pass their fixture's copy. Returns dict: timedout 0|1, exitcode <int|empty>,
#output <combined stdout+stderr>.
proc maketcl_run {cmdargs {scriptpath ""}} {
variable runstate
variable maketcl_run_timeout_ms
variable punkexe
variable maketcl
if {$scriptpath eq ""} {
set scriptpath $maketcl
}
array unset runstate
set runstate(output) ""
set runstate(done) ""
set chan [open |[list $punkexe script $scriptpath {*}$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)]
}
proc fixture_git {base args} {
exec git -C $base -c user.name=fixture -c user.email=fixture@test -c commit.gpgsign=false -c init.defaultBranch=main {*}$args 2>@1
}
#Minimal punk-project-shaped tree carrying a copy of the working-tree make.tcl and
#of src/bootsupport. withgit builds the pending-bump state: version 0.1.0
#committed, then one src/ commit after it.
proc fixture_new {dirtail withgit} {
variable projectroot
set base [file normalize [makeDirectory $dirtail]]
file mkdir [file join $base src modules] [file join $base src lib] [file join $base lib] [file join $base modules]
file copy [file join $projectroot src make.tcl] [file join $base src make.tcl]
file copy [file join $projectroot src bootsupport] [file join $base src bootsupport]
set fd [open [file join $base punkproject.toml] w]
puts $fd "\[project\]"
puts $fd "name = \"fixtureproj\""
puts $fd "version = \"0.1.0\""
close $fd
set fd [open [file join $base CHANGELOG.md] w]
puts $fd "# Changelog"
puts $fd ""
puts $fd "## \[0.1.0\] - 2026-08-06"
puts $fd "- initial"
close $fd
if {$withgit} {
fixture_git $base init -q -b main
fixture_git $base add -A
fixture_git $base commit -q -m "initial 0.1.0"
set fd [open [file join $base src change.tcl] w]
puts $fd "puts change"
close $fd
fixture_git $base add -A
fixture_git $base commit -q -m "src change"
}
return $base
}
variable fixgit ""
if {[testConstraint punkexeavailable] && [testConstraint gitavailable]} {
set fixgit [fixture_new maketclpvgate_git 1]
}
variable fixnogit ""
if {[testConstraint punkexeavailable]} {
set fixnogit [fixture_new maketclpvgate_nogit 0]
}
proc has {out needle} {
expr {[string first $needle $out] >= 0}
}
#added 2026-08-06 (agent, project-version gate)
test maketcl_check_pvgate_status {make.tcl check reports the project-version bake 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 {^project-version bake gate: (ACTIVE|UNAVAILABLE)} $out]
set result
}\
-result {timedout 0 exitcode 0 esc 0 line 1}
#added 2026-08-06 (agent, project-version gate)
test maketcl_check_pvgate_active_contract {with a current bootsupport snapshot the gate is ACTIVE, states the bake-input relationship, the no-waiver rule and a current state}\
-constraints {punkexeavailable} -body {
set r [maketcl_run {check}]
set out [dict get $r output]
set result [list]
lappend result active [regexp -line {^project-version bake gate: ACTIVE\s*$} $out]
lappend result bakeinput [has $out "BAKE INPUT"]
lappend result nowaiver [has $out "-confirm 0 does not waive the gate"]
lappend result state [regexp -line {^ current state: } $out]
set result
}\
-result {active 1 bakeinput 1 nowaiver 1 state 1}
#added 2026-08-06 (agent, project-version gate)
test maketcl_projectversion_advisory_read_only {make.tcl projectversion stays a read-only exit-0 advisory, ESC-free}\
-constraints {punkexeavailable} -body {
set r [maketcl_run {projectversion}]
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 header [has $out "project version check"]
lappend result ppline [has $out "punkproject.toml version :"]
set result
}\
-result {timedout 0 exitcode 0 esc 0 header 1 ppline 1}
#added 2026-08-06 (agent, project-version gate)
test maketcl_pvgate_pending_flagless_aborts {pending-bump fixture: a flagless non-interactive bake aborts fast, naming the consequence and the assertion escape hatch}\
-constraints {punkexeavailable gitavailable} -body {
variable fixgit
set r [maketcl_run {bake -confirm 0} [file join $fixgit src make.tcl]]
set out [dict get $r output]
set result [list]
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode]
lappend result pending [has $out "BAKE-WARNING: project-version bump may be PENDING"]
lappend result consequence [has $out "BAKE INPUT"]
lappend result hint [has $out "-expect-projectversion 0.1.0"]
lappend result aborted [has $out "-aborted- (project-version bump appears pending"]
set result
}\
-result {timedout 0 exitcode 1 pending 1 consequence 1 hint 1 aborted 1}
#added 2026-08-06 (agent, project-version gate)
test maketcl_pvgate_assert_match_proceeds {pending-bump fixture: a matching -expect-projectversion satisfies the gate and the bake proceeds past it}\
-constraints {punkexeavailable gitavailable} -body {
variable fixgit
set r [maketcl_run {bake -confirm 0 -expect-projectversion 0.1.0} [file join $fixgit src make.tcl]]
set out [dict get $r output]
set result [list]
lappend result timedout [dict get $r timedout]
lappend result gateok [has $out "project-version gate: OK (asserted -expect-projectversion 0.1.0"]
lappend result note [has $out "proceeding on the assertion"]
lappend result gateabort [has $out "-aborted- (project-version"]
set result
}\
-result {timedout 0 gateok 1 note 1 gateabort 0}
#added 2026-08-06 (agent, project-version gate)
test maketcl_pvgate_assert_mismatch_aborts {a mismatched -expect-projectversion aborts before any kit work}\
-constraints {punkexeavailable gitavailable} -body {
variable fixgit
set r [maketcl_run {bake -confirm 0 -expect-projectversion 9.9.9} [file join $fixgit src make.tcl]]
set out [dict get $r output]
set result [list]
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode]
lappend result mismatch [has $out "-expect-projectversion MISMATCH: asserted 9.9.9 but punkproject.toml says 0.1.0"]
lappend result aborted [has $out "-aborted- (project-version assertion mismatch"]
set result
}\
-result {timedout 0 exitcode 1 mismatch 1 aborted 1}
#added 2026-08-06 (agent, project-version gate)
test maketcl_pvgate_bump_staged_proceeds {a staged-but-uncommitted bump passes the gate - bake reads the working-tree value}\
-constraints {punkexeavailable gitavailable} -body {
variable fixgit
set fd [open [file join $fixgit punkproject.toml] w]
puts $fd "\[project\]"
puts $fd "name = \"fixtureproj\""
puts $fd "version = \"0.2.0\""
close $fd
set r [maketcl_run {bake -confirm 0} [file join $fixgit src make.tcl]]
set out [dict get $r output]
set result [list]
lappend result timedout [dict get $r timedout]
lappend result staged [has $out "bumped 0.1.0 -> 0.2.0, commit pending"]
lappend result gateabort [has $out "-aborted- (project-version"]
set result
}\
-result {timedout 0 staged 1 gateabort 0}
#added 2026-08-06 (agent, project-version gate)
test maketcl_pvgate_nogit_note_degradation {no-VCS fixture: the gate degrades to the documented NOTE and does not block}\
-constraints {punkexeavailable} -body {
variable fixnogit
set r [maketcl_run {bake -confirm 0} [file join $fixnogit src make.tcl]]
set out [dict get $r output]
set result [list]
lappend result timedout [dict get $r timedout]
lappend result note [has $out "NOTE: project-version gate skipped"]
lappend result gateabort [has $out "-aborted- (project-version"]
set result
}\
-result {timedout 0 note 1 gateabort 0}
#fixture dirs are makeDirectory-registered; pre-delete defensively with catch
#(git object files are read-only on windows) so cleanupTests has nothing left
#to trip on
variable fixgit
variable fixnogit
if {$fixgit ne ""} {catch {file delete -force $fixgit}}
if {$fixnogit ne ""} {catch {file delete -force $fixnogit}}
cleanupTests
}
namespace delete ::testspace
Loading…
Cancel
Save