Browse Source
The project version is a bake input (kit stamp punkkit-stamp.toml project_version + versioned-scheme output names), but nothing enforced the step (2) bump -> step (8) bake order; a 2026-08-06 session baked at 0.55.1, bumped to 0.56.0 and paid a full 11-kit re-bake. Every other release-sequence edge already had a mechanical gate (provenance, bootsupport staleness, boot precondition) - this was the un-gated one. - ::punkboot::get_projectversion_status: shared staleness/consistency computation (projectversion advisory, check report, gate); states ok/overdue/bump_uncommitted/unavailable. - project-version gate on bake/bakehouse/bin: a pending-looking bump (src/ commits since punkproject.toml last changed, none staged) prompts y/N on a terminal and aborts fast non-interactively unless -expect-projectversion asserts the intended version. -confirm 0 deliberately does not waive the gate. A staged-but-uncommitted bump passes (bake reads the working-tree value); degraded bootsupport reduces the gate to a NOTE. - -expect-projectversion <ver> (bake/bakehouse/bin argdocs + degraded scan): assertion verified in every state - a mismatch with punkproject.toml always aborts before any kit work; unavailable status aborts a supplied assertion as unverifiable rather than silently losing the requested strictness. - projectversion overdue warning now states the consequence and prints the ready-made assertion command; check reports the gate state and current staleness verdict; workflow text ([K2] bake-input + gate, [K6] no-waiver exception, steps (2)/(8)) and gethelp/HELPTEXTS/OPT_CONFIRM document it; root AGENTS.md gains "The version is a bake input (bump BEFORE bake)" + an Enforcement gate paragraph; src/AGENTS.md closeout bullet extended. - maketclprojectversion.test: piped characterization (8 tests) - check report, advisory read-only, and git-fixture verdicts for pending-abort, assert-match proceed, assert-mismatch abort, bump-staged proceed, no-git NOTE. maketclhelp.test bakehouse-synopsis pin updated for the new flag. - Thin-layout + templates-modpod make.tcl copies synced via make.tcl modules. Claude-Session: https://claude.ai/code/session_01EukjNTv2r5msFgHzWgaCdS Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
10 changed files with 1426 additions and 235 deletions
@ -0,0 +1,284 @@
|
||||
package require tcltest |
||||
|
||||
#Piped characterization of the make.tcl project-version bake gate (added 2026-08-06). |
||||
#The project version is a bake input: punkkit-stamp.toml project_version and the |
||||
#versioned-scheme output names are read from punkproject.toml at bake time, so the |
||||
#kit-assembling subcommands refuse to proceed with a non-interactive stdin when a |
||||
#bump looks pending - src/ commits since punkproject.toml last changed, none staged - |
||||
#unless -expect-projectversion asserts the intended version, and always refuse a |
||||
#mismatched assertion. -confirm 0 deliberately does not waive the gate. |
||||
#Runs make.tcl under the built punk executable's 'script' subcommand with output |
||||
#captured through a pipe and stdin half-closed - a non-interactive stdin by |
||||
#construction, exactly the population the gate exists to stop. |
||||
# - the 'check' and 'projectversion' pins run against this WORKING TREE |
||||
# - the gate VERDICT pins run against throwaway fixture projects under tcltest |
||||
# temporaryDirectory, each carrying a copy of the working-tree make.tcl and of |
||||
# src/bootsupport so punkboot::utils resolves. The git fixture is built at |
||||
# version 0.1.0 with one src/ commit after the last punkproject.toml commit - |
||||
# the pending-bump state; the no-git fixture exercises the documented NOTE |
||||
# degradation. Downstream-of-gate outcomes in fixtures - no runtimes to bake, |
||||
# no VCS for the bake workdir - are deliberately not pinned: the pins assert |
||||
# only the gate's own contract. |
||||
#Target executable resolved from env(PUNK_SHELL_TEST_EXE), else |
||||
#<projectroot>/bin/punk902z.exe then <projectroot>/bin/punkshell902. Skipped |
||||
#(constraint punkexeavailable) if none found; fixture tests additionally need git |
||||
#on PATH (constraint gitavailable). |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
|
||||
variable testdir [file dirname [file normalize [info script]]] |
||||
#<projectroot>/src/tests/shell/testsuites/punkexe -> 5 levels up to <projectroot> |
||||
variable projectroot [file normalize [file join $testdir .. .. .. .. ..]] |
||||
variable maketcl [file join $projectroot src make.tcl] |
||||
|
||||
variable punkexe "" |
||||
if {[info exists ::env(PUNK_SHELL_TEST_EXE)] && $::env(PUNK_SHELL_TEST_EXE) ne ""} { |
||||
set punkexe [file normalize $::env(PUNK_SHELL_TEST_EXE)] |
||||
} else { |
||||
foreach candidate [list [file join $projectroot bin punk902z.exe] [file join $projectroot bin punkshell902]] { |
||||
if {[file exists $candidate]} { |
||||
set punkexe $candidate |
||||
break |
||||
} |
||||
} |
||||
} |
||||
testConstraint punkexeavailable [expr {$punkexe ne "" && [file exists $punkexe]}] |
||||
testConstraint gitavailable [expr {[llength [auto_execok git]] > 0}] |
||||
|
||||
variable maketcl_run_timeout_ms 60000 |
||||
|
||||
variable runstate |
||||
array set runstate {} |
||||
|
||||
proc maketcl_run_read {chan} { |
||||
variable runstate |
||||
append runstate(output) [read $chan] |
||||
if {[chan eof $chan]} { |
||||
chan event $chan readable {} |
||||
set runstate(done) eof |
||||
} |
||||
} |
||||
|
||||
#Run <punkexe> script <somemaketcl> <subcommand...> with output captured through a |
||||
#pipe (stdin half-closed for immediate EOF - the gate must see a non-interactive |
||||
#stdin). scriptpath defaults to the working tree's src/make.tcl; the fixture tests |
||||
#pass their fixture's copy. Returns dict: timedout 0|1, exitcode <int|empty>, |
||||
#output <combined stdout+stderr>. |
||||
proc maketcl_run {cmdargs {scriptpath ""}} { |
||||
variable runstate |
||||
variable maketcl_run_timeout_ms |
||||
variable punkexe |
||||
variable maketcl |
||||
if {$scriptpath eq ""} { |
||||
set scriptpath $maketcl |
||||
} |
||||
array unset runstate |
||||
set runstate(output) "" |
||||
set runstate(done) "" |
||||
|
||||
set chan [open |[list $punkexe script $scriptpath {*}$cmdargs 2>@1] r+] |
||||
chan configure $chan -blocking 0 -translation binary |
||||
catch {chan close $chan write} ;#no stdin for the child - immediate EOF |
||||
set timerid [after $maketcl_run_timeout_ms [list set [namespace current]::runstate(done) timeout]] |
||||
chan event $chan readable [list [namespace current]::maketcl_run_read $chan] |
||||
while {$runstate(done) eq ""} { |
||||
vwait [namespace current]::runstate(done) |
||||
} |
||||
after cancel $timerid |
||||
set timedout [expr {$runstate(done) eq "timeout"}] |
||||
set exitcode "" |
||||
if {$timedout} { |
||||
catch {exec {*}[auto_execok taskkill] /F /T /PID [lindex [pid $chan] 0]} |
||||
catch {chan close $chan} |
||||
} else { |
||||
chan configure $chan -blocking 1 |
||||
if {[catch {chan close $chan} errdata errdict]} { |
||||
set exitcode [lindex [dict get $errdict -errorcode] end] |
||||
} else { |
||||
set exitcode 0 |
||||
} |
||||
} |
||||
return [dict create timedout $timedout exitcode $exitcode output $runstate(output)] |
||||
} |
||||
|
||||
proc fixture_git {base args} { |
||||
exec git -C $base -c user.name=fixture -c user.email=fixture@test -c commit.gpgsign=false -c init.defaultBranch=main {*}$args 2>@1 |
||||
} |
||||
|
||||
#Minimal punk-project-shaped tree carrying a copy of the working-tree make.tcl and |
||||
#of src/bootsupport. withgit builds the pending-bump state: version 0.1.0 |
||||
#committed, then one src/ commit after it. |
||||
proc fixture_new {dirtail withgit} { |
||||
variable projectroot |
||||
set base [file normalize [makeDirectory $dirtail]] |
||||
file mkdir [file join $base src modules] [file join $base src lib] [file join $base lib] [file join $base modules] |
||||
file copy [file join $projectroot src make.tcl] [file join $base src make.tcl] |
||||
file copy [file join $projectroot src bootsupport] [file join $base src bootsupport] |
||||
set fd [open [file join $base punkproject.toml] w] |
||||
puts $fd "\[project\]" |
||||
puts $fd "name = \"fixtureproj\"" |
||||
puts $fd "version = \"0.1.0\"" |
||||
close $fd |
||||
set fd [open [file join $base CHANGELOG.md] w] |
||||
puts $fd "# Changelog" |
||||
puts $fd "" |
||||
puts $fd "## \[0.1.0\] - 2026-08-06" |
||||
puts $fd "- initial" |
||||
close $fd |
||||
if {$withgit} { |
||||
fixture_git $base init -q -b main |
||||
fixture_git $base add -A |
||||
fixture_git $base commit -q -m "initial 0.1.0" |
||||
set fd [open [file join $base src change.tcl] w] |
||||
puts $fd "puts change" |
||||
close $fd |
||||
fixture_git $base add -A |
||||
fixture_git $base commit -q -m "src change" |
||||
} |
||||
return $base |
||||
} |
||||
|
||||
variable fixgit "" |
||||
if {[testConstraint punkexeavailable] && [testConstraint gitavailable]} { |
||||
set fixgit [fixture_new maketclpvgate_git 1] |
||||
} |
||||
variable fixnogit "" |
||||
if {[testConstraint punkexeavailable]} { |
||||
set fixnogit [fixture_new maketclpvgate_nogit 0] |
||||
} |
||||
|
||||
proc has {out needle} { |
||||
expr {[string first $needle $out] >= 0} |
||||
} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_check_pvgate_status {make.tcl check reports the project-version bake gate with one of the two documented statuses, ESC-free}\ |
||||
-constraints {punkexeavailable} -body { |
||||
set r [maketcl_run {check}] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] |
||||
lappend result esc [regexp -all {\x1b} $out] |
||||
lappend result line [regexp -line {^project-version bake gate: (ACTIVE|UNAVAILABLE)} $out] |
||||
set result |
||||
}\ |
||||
-result {timedout 0 exitcode 0 esc 0 line 1} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_check_pvgate_active_contract {with a current bootsupport snapshot the gate is ACTIVE, states the bake-input relationship, the no-waiver rule and a current state}\ |
||||
-constraints {punkexeavailable} -body { |
||||
set r [maketcl_run {check}] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result active [regexp -line {^project-version bake gate: ACTIVE\s*$} $out] |
||||
lappend result bakeinput [has $out "BAKE INPUT"] |
||||
lappend result nowaiver [has $out "-confirm 0 does not waive the gate"] |
||||
lappend result state [regexp -line {^ current state: } $out] |
||||
set result |
||||
}\ |
||||
-result {active 1 bakeinput 1 nowaiver 1 state 1} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_projectversion_advisory_read_only {make.tcl projectversion stays a read-only exit-0 advisory, ESC-free}\ |
||||
-constraints {punkexeavailable} -body { |
||||
set r [maketcl_run {projectversion}] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] |
||||
lappend result esc [regexp -all {\x1b} $out] |
||||
lappend result header [has $out "project version check"] |
||||
lappend result ppline [has $out "punkproject.toml version :"] |
||||
set result |
||||
}\ |
||||
-result {timedout 0 exitcode 0 esc 0 header 1 ppline 1} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_pvgate_pending_flagless_aborts {pending-bump fixture: a flagless non-interactive bake aborts fast, naming the consequence and the assertion escape hatch}\ |
||||
-constraints {punkexeavailable gitavailable} -body { |
||||
variable fixgit |
||||
set r [maketcl_run {bake -confirm 0} [file join $fixgit src make.tcl]] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] |
||||
lappend result pending [has $out "BAKE-WARNING: project-version bump may be PENDING"] |
||||
lappend result consequence [has $out "BAKE INPUT"] |
||||
lappend result hint [has $out "-expect-projectversion 0.1.0"] |
||||
lappend result aborted [has $out "-aborted- (project-version bump appears pending"] |
||||
set result |
||||
}\ |
||||
-result {timedout 0 exitcode 1 pending 1 consequence 1 hint 1 aborted 1} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_pvgate_assert_match_proceeds {pending-bump fixture: a matching -expect-projectversion satisfies the gate and the bake proceeds past it}\ |
||||
-constraints {punkexeavailable gitavailable} -body { |
||||
variable fixgit |
||||
set r [maketcl_run {bake -confirm 0 -expect-projectversion 0.1.0} [file join $fixgit src make.tcl]] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result timedout [dict get $r timedout] |
||||
lappend result gateok [has $out "project-version gate: OK (asserted -expect-projectversion 0.1.0"] |
||||
lappend result note [has $out "proceeding on the assertion"] |
||||
lappend result gateabort [has $out "-aborted- (project-version"] |
||||
set result |
||||
}\ |
||||
-result {timedout 0 gateok 1 note 1 gateabort 0} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_pvgate_assert_mismatch_aborts {a mismatched -expect-projectversion aborts before any kit work}\ |
||||
-constraints {punkexeavailable gitavailable} -body { |
||||
variable fixgit |
||||
set r [maketcl_run {bake -confirm 0 -expect-projectversion 9.9.9} [file join $fixgit src make.tcl]] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] |
||||
lappend result mismatch [has $out "-expect-projectversion MISMATCH: asserted 9.9.9 but punkproject.toml says 0.1.0"] |
||||
lappend result aborted [has $out "-aborted- (project-version assertion mismatch"] |
||||
set result |
||||
}\ |
||||
-result {timedout 0 exitcode 1 mismatch 1 aborted 1} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_pvgate_bump_staged_proceeds {a staged-but-uncommitted bump passes the gate - bake reads the working-tree value}\ |
||||
-constraints {punkexeavailable gitavailable} -body { |
||||
variable fixgit |
||||
set fd [open [file join $fixgit punkproject.toml] w] |
||||
puts $fd "\[project\]" |
||||
puts $fd "name = \"fixtureproj\"" |
||||
puts $fd "version = \"0.2.0\"" |
||||
close $fd |
||||
set r [maketcl_run {bake -confirm 0} [file join $fixgit src make.tcl]] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result timedout [dict get $r timedout] |
||||
lappend result staged [has $out "bumped 0.1.0 -> 0.2.0, commit pending"] |
||||
lappend result gateabort [has $out "-aborted- (project-version"] |
||||
set result |
||||
}\ |
||||
-result {timedout 0 staged 1 gateabort 0} |
||||
|
||||
#added 2026-08-06 (agent, project-version gate) |
||||
test maketcl_pvgate_nogit_note_degradation {no-VCS fixture: the gate degrades to the documented NOTE and does not block}\ |
||||
-constraints {punkexeavailable} -body { |
||||
variable fixnogit |
||||
set r [maketcl_run {bake -confirm 0} [file join $fixnogit src make.tcl]] |
||||
set out [dict get $r output] |
||||
set result [list] |
||||
lappend result timedout [dict get $r timedout] |
||||
lappend result note [has $out "NOTE: project-version gate skipped"] |
||||
lappend result gateabort [has $out "-aborted- (project-version"] |
||||
set result |
||||
}\ |
||||
-result {timedout 0 note 1 gateabort 0} |
||||
|
||||
#fixture dirs are makeDirectory-registered; pre-delete defensively with catch |
||||
#(git object files are read-only on windows) so cleanupTests has nothing left |
||||
#to trip on |
||||
variable fixgit |
||||
variable fixnogit |
||||
if {$fixgit ne ""} {catch {file delete -force $fixgit}} |
||||
if {$fixnogit ne ""} {catch {file delete -force $fixnogit}} |
||||
|
||||
cleanupTests |
||||
} |
||||
namespace delete ::testspace |
||||
Loading…
Reference in new issue