Browse Source

punkcheck 0.6.1: installtrack self-heals damaged persisted event history

Field failure (2026-07-27, second dev machine): 'make.tcl bake
punk9_beta' died in the vfslibs phase with
  eventcollection ... key '$eventid' already exists in collection
because that machine's src/vfs/.punkcheck predates punkcheck 0.3.2.
The 0.3.2 note fixed the WRITER (a braced template wrote the literal
strings '$eventid' etc into event headers) but files written before it
keep the damage, and the modern installtrack constructor - which
reloads persisted events into a collection keyed by -id - collided on
the duplicate literals and aborted the whole bake.

- punkcheck 0.6.1: the constructor reload now self-heals instead of
  aborting: an EVENT record with a missing or duplicate -id is
  reconstructed under a synthesized unique id ('damaged-id-...') with
  a stderr warning naming the file and the offending id. Synthesized
  ids match no INSTALL record's -eventid reference, so healed events
  age out through normal -keep_events pruning. Reload also tolerates
  missing -source/-targets keys.
- oolib 0.1.4: the collection duplicate-key error no longer misnames
  the object as 'col_processors' (stray copy-paste in the generic
  class).
- regression test installtrack_damaged_event_history_selfheal seeds a
  real two-event file, rewrites both -id values to the braced literal
  {$eventid} (as the damaged files carry it - the record loader
  evaluates records, so only a braced literal survives as the string),
  and asserts the reload succeeds with both events kept under distinct
  keys (one healed). Suite: 30/30.
- before/after proof against the released artifacts: punkcheck 0.6.0
  (exact-pinned) reproduces the field abort on the crafted file;
  0.6.1 heals it. Bootsupport promoted to punkcheck-0.6.1 + oolib-0.1.4
  (superseded copies pruned), so make.tcl bakes get the fix.

Remedy already applied on the affected machine (file deletion); with
this fix, remaining old .punkcheck files anywhere self-heal with a
warning instead of failing builds.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 3 days ago
parent
commit
91d3594570
  1. 4
      src/bootsupport/modules/oolib-0.1.4.tm
  2. BIN
      src/bootsupport/modules/punk/mix/templates-0.2.0.tm
  3. 31
      src/bootsupport/modules/punkcheck-0.6.1.tm
  4. 2
      src/modules/oolib-999999.0a1.0.tm
  5. 3
      src/modules/oolib-buildversion.txt
  6. 23
      src/modules/punkcheck-999999.0a1.0.tm
  7. 3
      src/modules/punkcheck-buildversion.txt
  8. 50
      src/tests/modules/punkcheck/testsuites/punkcheck/installtrack.test

4
src/bootsupport/modules/oolib-0.1.3.tm → src/bootsupport/modules/oolib-0.1.4.tm

@ -136,7 +136,7 @@ namespace eval oolib {
error "[self object] collection key must not be an integer. Use another structure if integer keys required"
}
if {[dict exists $o_data $key]} {
error "[self object] col_processors object error: key '$key' already exists in collection"
error "[self object] collection error: key '$key' already exists in collection"
}
dict set o_data $key $value
return [expr {[dict size $o_data] - 1}] ;#return index of item
@ -196,5 +196,5 @@ namespace eval oolib {
package provide oolib [namespace eval oolib {
variable version
set version 0.1.3
set version 0.1.4
}]

BIN
src/bootsupport/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

31
src/bootsupport/modules/punkcheck-0.6.0.tm → src/bootsupport/modules/punkcheck-0.6.1.tm

@ -910,14 +910,31 @@ namespace eval punkcheck {
#(the record keys are -ts_begin/-ts_end - passing the raw record gave the constructor's
# -tsbegin/-tsend defaults, so reconstructed events took 'now' as begin and empty end,
# and any later flush/save_installer_record rewrote prior event history with those values)
set eid [punkcheck::dict_getwithdefault $e -id ""]
#self-heal damaged persisted history rather than abort the caller's operation:
#files written by punkcheck < 0.3.2 carry EVENT records whose -id (and
#-source/-targets/-config) hold the LITERAL strings of the old writer's braced
#template ('$eventid' etc - the 0.3.2 buildversion note fixed the writer, but
#files keep the damage) and any two such records collide on the duplicate key
#here. History is advisory - reconstruct the record under a synthesized unique
#id and warn naming the file, instead of failing e.g a whole bake's vfslibs
#phase. Synthesized ids match no INSTALL record's -eventid reference, so
#healed events age out through normal -keep_events pruning.
if {$eid eq "" || $eid in [my events keys]} {
set healed_id "damaged-id-[my events count]-[clock microseconds]"
set shown [string range $eid 0 60]
set why [expr {$eid eq "" ? "missing" : "duplicates an earlier event"}]
puts stderr "punkcheck: WARNING damaged event history in '$o_checkfile': event -id '$shown' $why - keeping record under synthesized id '$healed_id' (safe to ignore; delete the file to reset history)"
set eid $healed_id
}
set eargs [list]
lappend eargs -id [punkcheck::dict_getwithdefault $e -id ""]
lappend eargs -id $eid
lappend eargs -tsbegin [punkcheck::dict_getwithdefault $e -ts_begin ""]
lappend eargs -tsend [punkcheck::dict_getwithdefault $e -ts_end ""]
lappend eargs -types [punkcheck::dict_getwithdefault $e -types {}]
lappend eargs -config [punkcheck::dict_getwithdefault $e -config {}]
set eobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] [dict get $e -source] [dict get $e -targets] {*}$eargs]
$o_events add $eobj [dict get $e -id]
set eobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] [punkcheck::dict_getwithdefault $e -source ""] [punkcheck::dict_getwithdefault $e -targets ""] {*}$eargs]
$o_events add $eobj $eid
}
}
@ -1426,7 +1443,7 @@ namespace eval punkcheck {
#The -changed flag is computed by comparing -value against the matching virtual SOURCE in the last completed
#install record (no filesystem access) - so virtual sources participate in targetset_source_changes like file sources.
#Typical use: recording the resolved build version of a module target, so records identify which product of an
#unchanging source fileset (e.g <module>-0.6.0.tm + <module>-buildversion.txt) the target represents.
#unchanging source fileset (e.g <module>-0.6.1.tm + <module>-buildversion.txt) the target represents.
proc installsource_add_virtual {file_record id value} {
if {![lib::is_file_record_inprogress $file_record]} {
error "installsource_add_virtual error: bad file_record - expected FILEINFO with last body element *-INPROGRESS ($file_record)"
@ -2069,11 +2086,11 @@ namespace eval punkcheck {
#FILEINFO -targets jjjetc-0.1.0.tm -keep_installrecords 2 -keep_skipped 1 -keep_inprogress 2 {
# INSTALL-RECORD -tsiso 2023-09-20T07:30:30 -ts 1695159030266610 -installer punk::mix::cli::build_modules_from_source_to_base -metadata_us 18426 -ts_start_transfer 1695159030285036 -transfer_us 10194 -elapsed_us 28620 {
# SOURCE -type file -path ../src/modules/jjjetc-buildversion.txt -cksum c7c71839c36b3d21c8370fed106192fcd659eca9 -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 1 -metadata_us 3423
# SOURCE -type file -path ../src/modules/jjjetc-0.6.0.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 1 -metadata_us 3413
# SOURCE -type file -path ../src/modules/jjjetc-0.6.1.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 1 -metadata_us 3413
# }
# INSTALL-SKIPPED -tsiso 2023-09-20T08:14:26 -ts 1695161666087880 -installer punk::mix::cli::build_modules_from_source_to_base -elapsed_us 18914 {
# SOURCE -type file -path ../src/modules/jjjetc-buildversion.txt -cksum c7c71839c36b3d21c8370fed106192fcd659eca9 -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 0 -metadata_us 3435
# SOURCE -type file -path ../src/modules/jjjetc-0.6.0.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 0 -metadata_us 3338
# SOURCE -type file -path ../src/modules/jjjetc-0.6.1.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 0 -metadata_us 3338
# }
#}
@ -2778,6 +2795,6 @@ namespace eval ::punk::args::register {
package provide punkcheck [namespace eval punkcheck {
set pkg punkcheck
variable version
set version 0.6.0
set version 0.6.1
}]
return

2
src/modules/oolib-999999.0a1.0.tm

@ -136,7 +136,7 @@ namespace eval oolib {
error "[self object] collection key must not be an integer. Use another structure if integer keys required"
}
if {[dict exists $o_data $key]} {
error "[self object] col_processors object error: key '$key' already exists in collection"
error "[self object] collection error: key '$key' already exists in collection"
}
dict set o_data $key $value
return [expr {[dict size $o_data] - 1}] ;#return index of item

3
src/modules/oolib-buildversion.txt

@ -1,3 +1,4 @@
0.1.3
0.1.4
#First line must be a tm version number
#all other lines are ignored.
#0.1.4 - collection add duplicate-key error message no longer misnames the object as 'col_processors' (stray copy-paste in the generic collection class)

23
src/modules/punkcheck-999999.0a1.0.tm

@ -910,14 +910,31 @@ namespace eval punkcheck {
#(the record keys are -ts_begin/-ts_end - passing the raw record gave the constructor's
# -tsbegin/-tsend defaults, so reconstructed events took 'now' as begin and empty end,
# and any later flush/save_installer_record rewrote prior event history with those values)
set eid [punkcheck::dict_getwithdefault $e -id ""]
#self-heal damaged persisted history rather than abort the caller's operation:
#files written by punkcheck < 0.3.2 carry EVENT records whose -id (and
#-source/-targets/-config) hold the LITERAL strings of the old writer's braced
#template ('$eventid' etc - the 0.3.2 buildversion note fixed the writer, but
#files keep the damage) and any two such records collide on the duplicate key
#here. History is advisory - reconstruct the record under a synthesized unique
#id and warn naming the file, instead of failing e.g a whole bake's vfslibs
#phase. Synthesized ids match no INSTALL record's -eventid reference, so
#healed events age out through normal -keep_events pruning.
if {$eid eq "" || $eid in [my events keys]} {
set healed_id "damaged-id-[my events count]-[clock microseconds]"
set shown [string range $eid 0 60]
set why [expr {$eid eq "" ? "missing" : "duplicates an earlier event"}]
puts stderr "punkcheck: WARNING damaged event history in '$o_checkfile': event -id '$shown' $why - keeping record under synthesized id '$healed_id' (safe to ignore; delete the file to reset history)"
set eid $healed_id
}
set eargs [list]
lappend eargs -id [punkcheck::dict_getwithdefault $e -id ""]
lappend eargs -id $eid
lappend eargs -tsbegin [punkcheck::dict_getwithdefault $e -ts_begin ""]
lappend eargs -tsend [punkcheck::dict_getwithdefault $e -ts_end ""]
lappend eargs -types [punkcheck::dict_getwithdefault $e -types {}]
lappend eargs -config [punkcheck::dict_getwithdefault $e -config {}]
set eobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] [dict get $e -source] [dict get $e -targets] {*}$eargs]
$o_events add $eobj [dict get $e -id]
set eobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] [punkcheck::dict_getwithdefault $e -source ""] [punkcheck::dict_getwithdefault $e -targets ""] {*}$eargs]
$o_events add $eobj $eid
}
}

3
src/modules/punkcheck-buildversion.txt

@ -1,6 +1,7 @@
0.6.0
0.6.1
#First line must be a semantic version number
#all other lines are ignored.
#0.6.1 - installtrack constructor self-heals damaged persisted event history instead of aborting the caller: EVENT records with a missing or duplicate -id (notably the literal '$eventid' records written by punkcheck < 0.3.2 - the 0.3.2 note fixed the writer but existing files keep the damage, and the modern keyed event reload collided on them, killing e.g a whole bake's vfslibs phase; first field hit 2026-07-27 on a machine whose src/vfs/.punkcheck predated 0.3.2) are reconstructed under synthesized unique ids ('damaged-id-...') with a stderr warning naming the file. Synthesized ids match no INSTALL record's -eventid reference, so healed events age out through normal -keep_events pruning. Event reload also tolerates missing -source/-targets keys. Regression test: src/tests/modules/punkcheck/testsuites/punkcheck/installtrack.test (installtrack_damaged_event_history_selfheal).
#0.6.0 - G-095 concurrent-writer safety: saves are write-temp-then-atomic-rename (bounded retry for Windows sharing violations) so readers never see torn content, and load_records_from_file retries transient open denials during a writer's replace window (parse errors stay strict). New punkcheck::lock namespace: advisory sibling lockfile <punkcheckfile>.lock (open WRONLY CREAT EXCL, holder pid/host/installer/timestamp contents, channel held open, in-process refcounted re-acquisition) serializes whole installer events - acquired at start_event, released at event end / installtrack destroy; flushes and chokepoint saves outside an event take a transient lock; contention retries with backoff to -timeout (env PUNKCHECK_LOCK_TIMEOUT) then errors naming the holder (errorcode PUNKCHECK LOCK TIMEOUT); stale locks break on provably-dead holder pid (twapi/kill -0, capability-gated) or age (-staleage / PUNKCHECK_LOCK_STALEAGE). Deferred flushes merge own records (INSTALLER by -name, touched FILEINFO by -targets) into freshly-loaded file state instead of wholesale overwrite; an mtime/size tripwire warns when a non-protocol writer touched the file between load and save. Duplicate-INSTALLER recovery degrades to a clean actionable error when stdin is non-interactive (new punkcheck::lib::stdin_is_interactive; twapi GetConsoleMode on the real stdin handle / unix -inputmode / test -t 0), and the interactive prompt gains the synced-targets deletion-wedge caveat. default_excludefiletail_core adds .punkcheck.* so lock/temp siblings are never treated as installable content.
#0.5.0 - G-094 single record lifecycle: installtrack/installevent OO layer is the sole implementation (targetset init/add-source/started/end) with a per-event persistence policy (start_event ?-persistence eager|deferred?; deferred = in-memory working recordset + $installer flush, the mode punkcheck::install now uses as a tree-walking OO consumer). Legacy proc pipeline (start_installer_event, installfile_begin/started_install/finished_install/skipped_install) retired to error-with-pointer shims. All saves flow through one chokepoint (installtrack save_working_recordset - G-095 attachment point). Fixes: start_event refreshes the working recordset (sequential events no longer clobber prior targetset writes; end/end_event now clears the active event so sequential events work), installtrack as_record no longer writes -name with a trailing space, event reconstruction maps -ts_begin/-ts_end (flush no longer rewrites prior event history timestamps), get_recordlist misspelled variable, targetset_dict called a nonexistent helper. punkcheck::install records now carry -metadata_us/-ts_start_transfer/-transfer_us/-cksum_us like OO-installed records, and all-targets mode now stores -targets_cksums. recordlist::extract_or_create_fileset_record no longer prints to stdout (isnew returned; targetset_init reports via debugchannel). Dead targetset class removed.
#0.4.0 - cross-root sources: installfile_add_source_and_fetch_metadata now records sources that share no common root with the punkcheck folder (e.g //zipfs:/ module-mounted sources installing to the filesystem) as independent absolute paths via the cksum helpers' empty-base mode, instead of erroring 'don't share a common root'. Enables punkcheck::install from module-carried (vfs-mounted) layout payloads - G-087 stage 3 bare-kit project generation.

50
src/tests/modules/punkcheck/testsuites/punkcheck/installtrack.test

@ -613,5 +613,55 @@ namespace eval ::testspace {
file delete -force [dict get $ws root]
}\
-result 0
# --- installtrack: reload self-heals damaged persisted event history ---
# Files written by punkcheck < 0.3.2 carry EVENT records whose -id is the
# LITERAL string {$eventid} (the 0.3.2 buildversion note fixed the writer;
# existing files keep the damage). Two such records collide when the modern
# constructor reloads events keyed by -id - which killed whole make.tcl
# bakes (vfslibs phase) on checkouts with old .punkcheck files. The
# constructor must reconstruct such records under synthesized unique ids
# (with a stderr warning) rather than error. Added 2026-07-27 (agent,
# punkcheck 0.6.1).
test installtrack_damaged_event_history_selfheal {reload of a file with duplicate literal \$eventid EVENT records self-heals instead of erroring}\
-setup {
set ws [make_workspace]
set punkcheck_file [dict get $ws punkcheck_file]
#seed two completed events so the persisted INSTALLER body carries two EVENT records
foreach step {one two} {
set installer [punkcheck::installtrack new "test" $punkcheck_file]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event [list -install_step $step]]
$event end
$installer destroy
}
#simulate the pre-0.3.2 damage: every persisted event -id becomes the
#literal string $eventid - BRACED, as the damaged files carry it (the
#record loader evaluates records, so only a braced literal survives
#as the string; the -eventid keys of INSTALL records are a different
#token and are not touched by the \s-id\s pattern)
set fd [open $punkcheck_file r]
set data [read $fd]
close $fd
regsub -all {(\s-id\s+)\S+} $data {\1{$eventid}} data
set fd [open $punkcheck_file w]
puts -nonewline $fd $data
close $fd
}\
-body {
set result [list]
set installer [punkcheck::installtrack new "test" $punkcheck_file]
lappend result reloaded_events [$installer events count]
set keys [$installer events keys]
lappend result distinct_keys [expr {[llength $keys] == [llength [lsort -unique $keys]]}]
lappend result healed_count [llength [lsearch -all -glob $keys damaged-id-*]]
lappend result literal_kept [expr {{$eventid} in $keys}]
$installer destroy
set result
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {reloaded_events 2 distinct_keys 1 healed_count 1 literal_kept 1}
}
tcltest::cleanupTests ;#needed to produce test summary line.
Loading…
Cancel
Save