Browse Source

make.tcl: dirty-src provenance gate for build/promotion commands (G-026 direction)

Build/promotion commands (project packages modules libs vfs vfslibs bin
bootsupport vfscommonupdate) now warn when src/ has uncommitted fossil/git
changes - artifacts built from dirty src have no committed provenance.
Warn-only by default; new -dirty-abort flag makes the check aborting.
With '<builtexe> src' available for evaluating uncommitted source directly,
building is the promotion step this treats it as.

- punkboot::utils 0.2.0: vcs_dirty_warnings gains optional scope arg so only
  changes under a subpath (src) count; unscoped vendorupdate call unchanged.
- Warnings print with a plain column-0 PROVENANCE-WARNING: token (greppable
  in redirected output) + ANSI colour, shared with vendorupdate's dirty
  source-project check, and are recapped at end-of-run via a wrapped ::exit
  so they survive scrolling chatty build output.
- Interactive terminal runs (stdin -inputmode probe, tcl 8.7+/9) get a 3s
  ctrl-c grace countdown before a dirty build proceeds; piped/agent/CI runs
  pay no delay.
- 'make.tcl check' reports src provenance status and what the build
  commands would do.
- Guarded require: stale/missing punkboot::utils snapshot degrades the check
  to a skip notice, but -dirty-abort then aborts rather than silently losing
  the requested strictness.

Project 0.10.2 -> 0.10.3.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
1fa2988a3f
  1. 5
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 2
      src/AGENTS.md
  4. 120
      src/make.tcl
  5. 48
      src/modules/punkboot/utils-999999.0a1.0.tm
  6. 3
      src/modules/punkboot/utils-buildversion.txt

5
CHANGELOG.md

@ -5,6 +5,11 @@ 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.10.3] - 2026-07-11
- make.tcl build/promotion commands (`project`, `packages`, `modules`, `libs`, `vfs`, `vfslibs`, `bin`, `bootsupport`, `vfscommonupdate`) now warn when `src/` has uncommitted fossil/git changes — artifacts built from dirty src have no committed provenance (G-026 direction). Warn-only by default; a new `-dirty-abort` flag makes the check aborting. Dirt outside `src/` is ignored (`punkboot::utils::vcs_dirty_warnings` gained an optional scope argument, utils 0.2.0). With `<builtexe> src` / `<builtexe> src shell` available for evaluating uncommitted source directly, building is the promotion step the warning treats it as.
- Provenance warnings (dirty-src gate and vendorupdate's dirty source-project check) are presented with a plain column-0 `PROVENANCE-WARNING:` token for automated discovery in redirected output plus ANSI colour for humans, and are recapped at the very end of the run (via a wrapped `::exit`) so they survive scrolling chatty build output. Interactive terminal runs (Tcl 8.7+/9 stdin `-inputmode` probe) additionally get a 3-second ctrl-c grace countdown before a dirty build proceeds; piped/agent/CI runs pay no delay. `make.tcl check` now reports src provenance status and what the build/promotion commands would do.
## [0.10.2] - 2026-07-11
- punk::libunknown bumped 0.1 → 0.2.0 (file renamed `libunknown-0.2.0.tm`), retroactively versioning the register_all_tm and source_pkgindex additions and adopting full `major.minor.patch` form. Verified nothing requires it by exact version or filename: punk_main.tcl and punk::repl glob `libunknown-*.tm` picking the highest by vcompare (the dev copy now also outversions the stale bootsupport/project.vfs `0.1` copies deterministically), bootsupport's include_modules.config lists it by name only. The module header now carries a version-history block in lieu of a buildversion.txt, and `src/modules/AGENTS.md` documents the manual-versioning bump mechanics for agents (same Patch/Minor/Major rules as buildversion-tracked modules).

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.10.2"
version = "0.10.3"

2
src/AGENTS.md

@ -64,6 +64,8 @@ Recovery after a wrong path guess:
- Use `tclsh src/make.tcl libs` to build just the library packages.
- Use `tclsh src/make.tcl packages` to build both modules and libraries.
- Use `tclsh src/make.tcl vendorupdate` to refresh vendormodules from config. It warns (non-fatal) for each source project whose fossil/git checkout is dirty - vendored artifacts built from a dirty tree have no committed provenance; commit in the source project to clear the warning (enforcement policy tracked by goal G-026).
- All build/promotion commands (`project`, `packages`, `modules`, `libs`, `vfs`, `vfslibs`, `bin`, `bootsupport`, `vfscommonupdate`) warn similarly when this project's own `src/` has uncommitted fossil/git changes (dirt outside `src/` is ignored). Warn-only by default; pass `-dirty-abort` to make the check aborting. For evaluating uncommitted source without a build, prefer `<builtexe> src` / `<builtexe> src shell`. The check is `punkboot::utils::vcs_dirty_warnings` (optional scope argument) loaded guardedly from bootsupport - if the snapshot is stale/missing the check degrades to a skip notice (but `-dirty-abort` then aborts rather than silently losing the requested strictness).
- Provenance warnings (dirty-src gate + vendorupdate source-project check) print with a plain column-0 `PROVENANCE-WARNING:` token (grep for it in captured build output) and are recapped at the end of the run via make.tcl's wrapped `::exit`. Interactive terminal runs get a 3-second ctrl-c grace countdown before a dirty build proceeds; piped/agent runs skip it. `tclsh src/make.tcl check` reports the current src provenance status and what the build commands would do.
- Use `tclsh src/make.tcl vfscommonupdate` to rebuild `_vfscommon.vfs`.
- Uncommitted `make.tcl`-generated outputs may be batched into one commit, regardless of how many `make.tcl` invocations produced them. This covers punkcheck-managed build outputs that are VCS-tracked: `src/bootsupport/`, `_vfscommon.vfs/modules` + `/lib`, and declared per-kit `*.vfs/lib_tcl<N>/<pkg>` subfolders. (Root `modules/`, `lib/`, `modules_tcl<N>/`, `lib_tcl<N>/` are gitignored and never committed.) Each module's old→new version rename is atomic; a regeneration reflects one build-output refresh, not independent hand-edits. This is a carve-out from generic "split aggressively" commit guidance for punkcheck-managed build outputs only — it does not cover user-curated VFS content (`_config/`, per-kit `*.vfs/` outside declared `lib_tcl<N>` subfolders, `_aside/`, `mkzipfix.vfs`, `_vfscommon.vfs/doc`), which commits separately per its own concerns.
- Use `punk make.tcl project` or `punk902z make.tcl project` inside Punk shell when building binaries through Punk.

120
src/make.tcl

@ -20,7 +20,7 @@ namespace eval ::punkboot {
variable scriptfolder [file normalize [file dirname [info script]]]
variable foldername [file tail $scriptfolder]
variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list]
variable non_help_flags [list -k]
variable non_help_flags [list -k -dirty-abort]
variable help_flags [list -help --help /? -h]
variable known_commands [list project modules libs packages vfs vfslibs bin info check shell vendorupdate bootsupport vfscommonupdate projectversion]
}
@ -1364,6 +1364,13 @@ proc ::punkboot::punkboot_gethelp {args} {
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 " Flags:" \n
append h " -dirty-abort" \n
append h " - abort build/promotion commands (project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate) when src/ has" \n
append h " uncommitted VCS changes. Default is warn-only: artifacts built from dirty src have no committed provenance." \n
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 building," \n
append h " use '<builtexe> src' or '<builtexe> src shell'." \n \n
append h "" \n
if {[llength [dict get $pkg_availability missing]] || [llength [dict get $pkg_availability broken]]} {
set has_recommended 0
@ -1604,6 +1611,98 @@ if {![string length [set projectroot [punk::repo::find_project $scriptfolder]]]}
set sourcefolder $projectroot/src
set binfolder $projectroot/bin
# ----------------------------------------
# Provenance-warning presentation + dirty-src check for build/promotion commands (goal G-026 direction).
# Build/promotion commands stamp versions onto src content and propagate it into trees other
# things consume (root modules/, bootsupport snapshots, vfs payloads, kits/zipkits).
# Artifacts built from uncommitted src have no committed provenance - warn by default,
# abort if the -dirty-abort flag was given. To evaluate uncommitted source without
# building, use '<builtexe> src' / '<builtexe> src shell' instead.
# Guarded require: a stale or missing punkboot::utils bootsupport snapshot must never
# brick the make.tcl commands used to repair it - the check degrades to a skip notice
# (but -dirty-abort still aborts rather than silently skipping the requested strictness).
#Emit provenance warnings with a plain column-0 token for automated discovery (grep PROVENANCE-WARNING
#in redirected output) and ANSI colour for humans. Lines are also accumulated in
#::punkboot::provenance_warnings_pending unless -norecap is given, so the wrapped ::exit below can
#recap them at the tail of the output - build progress output is chatty and the original warning
#position may scroll out of sight or even out of scrollback.
set ::punkboot::provenance_warnings_pending [list]
proc ::punkboot::print_provenance_warnings {warninglines args} {
global A
if {![array size A]} {punkboot::define_global_ansi}
foreach w $warninglines {
regsub {^WARNING: } $w "" w
puts stderr "PROVENANCE-WARNING: $A(BAD)$w$A(RST)"
if {"-norecap" ni $args} {
lappend ::punkboot::provenance_warnings_pending $w
}
}
flush stderr
}
#Availability probe + scoped dirty-src check, shared by the build/promotion gate below and the
#'check' command report. Returns {available warninglist}.
proc ::punkboot::get_src_provenance_warnings {projectroot label} {
set available [expr {\
![catch {package require punkboot::utils}]\
&& [llength [info commands ::punkboot::utils::vcs_dirty_warnings]]\
&& "scope" in [info args ::punkboot::utils::vcs_dirty_warnings]\
}]
if {!$available} {
return [list 0 [list]]
}
set checked_vcs_roots [dict create]
return [list 1 [::punkboot::utils::vcs_dirty_warnings $projectroot checked_vcs_roots $label src]]
}
#Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter
#which of make.tcl's many exit points a command takes.
if {![llength [info commands ::punkboot::exit_original]]} {
rename ::exit ::punkboot::exit_original
proc ::exit {{returnCode 0}} {
if {[info exists ::punkboot::provenance_warnings_pending] && [llength $::punkboot::provenance_warnings_pending]} {
puts stderr ""
puts stderr "PROVENANCE-WARNING: recap of warning(s) emitted earlier in this run:"
::punkboot::print_provenance_warnings $::punkboot::provenance_warnings_pending -norecap
}
::punkboot::exit_original $returnCode
}
}
if {$::punkboot::command in {project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate}} {
set dirty_abort [expr {[lsearch $::argv -dirty-abort] >= 0}]
lassign [::punkboot::get_src_provenance_warnings $projectroot "make.tcl $::punkboot::command"] have_scoped_dirty_check dirty_warnings
if {$have_scoped_dirty_check} {
if {[llength $dirty_warnings]} {
::punkboot::print_provenance_warnings $dirty_warnings
puts stderr " To evaluate uncommitted source without building, use '<builtexe> src' or '<builtexe> src shell'."
if {$dirty_abort} {
set ::punkboot::provenance_warnings_pending [list] ;#no recap needed - aborting adjacent to the warning
puts stderr "-aborted- (-dirty-abort given and src has uncommitted changes - commit first, or rerun without -dirty-abort to build with a warning)"
exit 1
}
#Grace period for an attentive human to ctrl-c - only when stdin is a terminal
#(-inputmode is only supported on terminal channels, Tcl 8.7+/9; pipes/redirects/8.6 skip the delay
# so agent/CI runs pay nothing)
if {![catch {chan configure stdin -inputmode}]} {
puts -nonewline stderr " proceeding despite dirty src - ctrl-c now to abort "
flush stderr
foreach tick {3 2 1} {
puts -nonewline stderr "..$tick"
flush stderr
after 1000
}
puts stderr ""
}
}
} else {
if {$dirty_abort} {
puts stderr "-aborted- (-dirty-abort given but the dirty-src provenance check is unavailable: punkboot::utils vcs_dirty_warnings with scope support not loadable from bootsupport)"
exit 1
}
puts stderr "NOTE: dirty-src provenance check unavailable (punkboot::utils vcs_dirty_warnings with scope support not loadable from bootsupport) - continuing without it"
}
}
if {$::punkboot::command eq "check"} {
set sep [string repeat - 75]
puts stdout $sep
@ -1738,6 +1837,21 @@ if {$::punkboot::command eq "check"} {
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
}
# Dirty-src provenance status - what the build/promotion commands would do
puts stdout $sep
lassign [::punkboot::get_src_provenance_warnings $projectroot "make.tcl check"] _prov_available _prov_warnings
if {!$_prov_available} {
puts stdout "src provenance: check unavailable (punkboot::utils vcs_dirty_warnings with scope support not loadable from bootsupport)"
puts stdout " build/promotion commands will proceed with a NOTE; -dirty-abort would abort as unverifiable."
} elseif {![llength $_prov_warnings]} {
puts stdout "src provenance: OK (no uncommitted fossil/git changes under src/)"
puts stdout " build/promotion commands (project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate) will proceed without provenance warnings."
} else {
::punkboot::print_provenance_warnings $_prov_warnings -norecap
puts stdout " build/promotion commands (project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate) will WARN as above and proceed."
puts stdout " With -dirty-abort they would abort. To evaluate uncommitted source without building, use '<builtexe> src' or '<builtexe> src shell'."
}
puts stdout $sep
exit 0
}
@ -2040,9 +2154,7 @@ if {$::punkboot::command eq "vendorupdate"} {
set normsrclocation [file normalize $srclocation]
if {![dict exists $checked_source_paths $normsrclocation]} {
dict set checked_source_paths $normsrclocation 1
foreach w [::punkboot::utils::vcs_dirty_warnings $normsrclocation checked_vcs_roots vendorupdate] {
puts stderr $w
}
::punkboot::print_provenance_warnings [::punkboot::utils::vcs_dirty_warnings $normsrclocation checked_vcs_roots vendorupdate]
}
}
#puts stdout "$relpath $module $module_subpath $srclocation"

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

@ -134,6 +134,10 @@ namespace eval punkboot::utils {
used to report each VCS root at most once across calls.
label, if supplied, is prefixed into each warning to name
the calling operation (e.g. 'vendorupdate').
scope, if supplied, is a subpath relative to path; only
uncommitted changes under that subpath are counted (e.g.
scope 'src' warns about dirty src/ while ignoring dirt
elsewhere in the checkout).
Returns an empty list if the path is clean, unversioned,
missing, already reported, or state cannot be determined."
@ -144,9 +148,11 @@ namespace eval punkboot::utils {
"Name of a dict variable in the caller's scope for dedupe across calls"
label -type string -optional 1 -default "" -help\
"Operation name prefixed into warnings"
scope -type string -optional 1 -default "" -help\
"Subpath relative to path limiting which changes count; empty = whole checkout"
}]
}
proc vcs_dirty_warnings {path checkedrootsvar {label ""}} {
proc vcs_dirty_warnings {path checkedrootsvar {label ""} {scope ""}} {
upvar 1 $checkedrootsvar checkedroots
if {![info exists checkedroots]} {set checkedroots [dict create]}
if {$label ne ""} {set label "$label "}
@ -155,6 +161,12 @@ namespace eval punkboot::utils {
if {![file isdirectory $dir]} {
return $warnings ;#missing path - not this proc's business to report
}
set scopeabs ""
set scopedesc "" ;#included in warning text when scoped
if {$scope ne ""} {
set scopeabs [file normalize [file join $dir $scope]]
set scopedesc " under [string trimright $scope /]/"
}
set fossilroot ""
set gitroot ""
while {1} {
@ -171,8 +183,8 @@ namespace eval punkboot::utils {
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot,$scope] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot,$scope 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
@ -180,19 +192,37 @@ namespace eval punkboot::utils {
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
} else {
set flines [list]
foreach line [split $fchanges \n] {
set line [string trim $line]
if {$line eq ""} {continue}
if {$scopeabs ne ""} {
#fossil changes lines are '<STATUS> <path-relative-to-checkout-root>'
if {![regexp {^\S+\s+(.*)$} $line _ relfile]} {continue}
set normfile [file normalize [file join $fossilroot $relfile]]
if {$normfile ne $scopeabs && ![string match "${scopeabs}/*" $normfile]} {continue}
}
lappend flines $line
}
if {[llength $flines]} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes${scopedesc} ([llength $flines] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot,$scope] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot,$scope 1
set gitpathargs [list]
if {$scopeabs ne ""} {
set gitpathargs [list -- $scopeabs]
}
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain {*}$gitpathargs]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes${scopedesc} ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings

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

@ -1,5 +1,6 @@
0.1.1
0.2.0
#First line must be a semantic version number
#all other lines are ignored.
#0.2.0 - vcs_dirty_warnings: optional scope arg limiting which uncommitted changes count (e.g scope src for make.tcl build/promotion dirty-src gate)
#0.1.1 - added vcs_dirty_warnings (dirty fossil/git checkout provenance warnings; used by make.tcl vendorupdate, shared with bootsupport update per goal G-026)
#0.1.0 - initial: parse_punkproject_version, read_punkproject_version, read_changelog_latest_version extracted from make.tcl inline procs

Loading…
Cancel
Save