From 2dbf635b99a23f8fb151e06bdb68a471a1f781e6 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 6 Aug 2026 03:26:58 +1000 Subject: [PATCH] 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 (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 --- AGENTS.md | 6 + CHANGELOG.md | 23 ++ punkproject.toml | 2 +- src/AGENTS.md | 2 +- src/make.tcl | 335 +++++++++++++++--- .../vendor/punk/project-0.1/src/make.tcl | 335 +++++++++++++++--- .../vendor/punk/basic/src/make.tcl | 335 +++++++++++++++--- .../vendor/punk/project-0.1/src/make.tcl | 335 +++++++++++++++--- .../shell/testsuites/punkexe/maketclhelp.test | 4 +- .../punkexe/maketclprojectversion.test | 284 +++++++++++++++ 10 files changed, 1426 insertions(+), 235 deletions(-) create mode 100644 src/tests/shell/testsuites/punkexe/maketclprojectversion.test diff --git a/AGENTS.md b/AGENTS.md index 8ab35ecf..120526c8 100644 --- a/AGENTS.md +++ b/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 ` 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 `. 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 27575773..c05ef865 100644 --- a/CHANGELOG.md +++ b/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 ` 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, diff --git a/punkproject.toml b/punkproject.toml index a2b87541..8eb45543 100644 --- a/punkproject.toml +++ b/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 - diff --git a/src/AGENTS.md b/src/AGENTS.md index 248188d4..0fdf04dd 100644 --- a/src/AGENTS.md +++ b/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 ` (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 diff --git a/src/make.tcl b/src/make.tcl index 2e8b3cf9..ae201003 100644 --- a/src/make.tcl +++ b/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 ?" \n append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to /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 ? ?kitname ...?" \n append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into /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 ?? ?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 ' src' or ' src shell'." \n \n + append h " -expect-projectversion (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 + kits; close running kit shells first [K5]; = 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 ' 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 ; 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 ' 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 ' 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 /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 + ' 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 ' 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 ' 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 diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl index 2e8b3cf9..ae201003 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/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 ?" \n append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to /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 ? ?kitname ...?" \n append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into /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 ?? ?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 ' src' or ' src shell'." \n \n + append h " -expect-projectversion (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 + kits; close running kit shells first [K5]; = 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 ' 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 ; 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 ' 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 ' 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 /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 + ' 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 ' 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 ' 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 diff --git a/src/project_layouts/vendor/punk/basic/src/make.tcl b/src/project_layouts/vendor/punk/basic/src/make.tcl index 2e8b3cf9..ae201003 100644 --- a/src/project_layouts/vendor/punk/basic/src/make.tcl +++ b/src/project_layouts/vendor/punk/basic/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 ?" \n append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to /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 ? ?kitname ...?" \n append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into /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 ?? ?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 ' src' or ' src shell'." \n \n + append h " -expect-projectversion (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 + kits; close running kit shells first [K5]; = 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 ' 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 ; 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 ' 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 ' 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 /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 + ' 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 ' 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 ' 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 diff --git a/src/project_layouts/vendor/punk/project-0.1/src/make.tcl b/src/project_layouts/vendor/punk/project-0.1/src/make.tcl index 2e8b3cf9..ae201003 100644 --- a/src/project_layouts/vendor/punk/project-0.1/src/make.tcl +++ b/src/project_layouts/vendor/punk/project-0.1/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 ?" \n append h " - consumer run from a clean checkout: mints the packages (modules + libs) then bakes kit/zipkit executables to /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 ? ?kitname ...?" \n append h " - assemble kit/zipkit executables from the promoted payload (src/vfs) and src/runtime runtimes into /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 ?? ?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 ' src' or ' src shell'." \n \n + append h " -expect-projectversion (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 + kits; close running kit shells first [K5]; = 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 ' 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 ; 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 ' 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 ' 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 /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 + ' 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 ' 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 ' 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 diff --git a/src/tests/shell/testsuites/punkexe/maketclhelp.test b/src/tests/shell/testsuites/punkexe/maketclhelp.test index 6c752393..16c618a1 100644 --- a/src/tests/shell/testsuites/punkexe/maketclhelp.test +++ b/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 \] \[-confirm \]} $out] + lappend result bakehousesyn [regexp {make\.tcl bakehouse \[-k\] \[-force\] \[-dirty-abort \] \[-expect-projectversion \] \[-confirm \]} $out] lappend result toolformsyn [regexp {make\.tcl tool build \[-test \] \[toolname\]\.\.\.} $out] lappend result bsformsyn [regexp {make\.tcl buildsuite build suitename \[driverarg\]\.\.\.} $out] set result diff --git a/src/tests/shell/testsuites/punkexe/maketclprojectversion.test b/src/tests/shell/testsuites/punkexe/maketclprojectversion.test new file mode 100644 index 00000000..596f2300 --- /dev/null +++ b/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 +#/bin/punk902z.exe then /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]]] + #/src/tests/shell/testsuites/punkexe -> 5 levels up to + 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 script 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 , + #output . + 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