# G-094 punkcheck single record lifecycle: OO installtrack as sole implementation with a persistence policy Status: achieved 2026-07-21 Scope: src/modules/punkcheck-999999.0a1.0.tm (installtrack/installevent lifecycle unification, installfile_* retirement, punkcheck::install as OO consumer); src/modules/punk/mix/cli-999999.0a1.0.tm + src/make.tcl (consumers verified unchanged); src/tests/modules/punkcheck/testsuites/punkcheck/ (characterization + new equivalence/staleness coverage) Goal: The installtrack/installevent OO layer is the only implementation of the .punkcheck record lifecycle (targetset init / add-source / started / end), with an explicit per-event persistence policy - eager per-operation read-modify-write (current OO behaviour) and deferred event-scoped flush (current batch behaviour, QUERY's no-save being the in-object precedent) - punkcheck::install rebuilt as a tree-walking consumer of that layer in deferred mode, the zero-caller legacy pipeline (installfile_begin / installfile_finished_install / installfile_skipped_install) retired to error-with-pointer shims, and every .punkcheck save flowing through a single chokepoint. Acceptance: characterization suites install.test and installtrack.test pass (pins flipped only where this goal's contract deliberately changes behaviour, each flip recorded); a new test pins field equivalence between batch-installed and OO-installed records (including -targets_cksums, which the legacy proc pipeline never stored); a new regression test covers o_record_list coherence - two sequential events on one installtrack instance, with targetset writes in the first event, lose nothing at the second start_event; grep of src/modules, src/make.tcl and src/project_layouts finds no live caller of the retired trio outside shims and tests; a full make.tcl build completes with skip/copy decisions unchanged against pre-refactor .punkcheck state (no spurious recopies of unchanged sources). ## Context 2026-07-19 module review found three parallel implementations of the record lifecycle, with drift: - **Legacy proc pipeline** (installfile_begin/started/finished/skipped + start_installer_event): frozen at an earlier design - INSTALL-only (no MODIFY/DELETE/VIRTUAL/QUERY), no FAILED status, no -note, and installfile_finished_install stores no post-install target cksums, so its records can never serve `-overwrite synced-targets`. Zero live callers of begin/finished/skipped anywhere (punk::mix::cli's calls are commented out next to their OO replacements); they are kept alive only by installfile_help and the characterization tests. - **OO layer** (installtrack/installevent): targetset_started, targetset_addsource and targetset_addsource_virtual delegate down to the legacy procs, while targetset_init and targetset_end are parallel rewrites of installfile_begin and finished/skipped with the extensions above - a half-inversion of the desired layering (procs should be the facade, the OO lifecycle the substrate). - **Batch punkcheck::install**: bypasses both - uses start_installer_event and the metadata fetcher but inlines its own begin and finalize so it can defer all persistence to one end-of-run save, independently re-growing -targets_cksums. Its in-memory recordset handling is the deferred persistence mode this goal makes first-class. Latent single-process clobber: start_event saves the whole constructor-era o_record_list, which targetset operations never refresh (they re-load from file into locals) - a second start_event on one instance would discard every FILEINFO update the first event's targetsets wrote. Unexercised-path evidence: installtrack get_recordlist reads a misspelled variable ($o_recordlist) and targetset_dict calls a nonexistent punk::records_as_target_dict - both would error if ever called. Performance shape that motivates the policy knob rather than "just save eagerly everywhere": eager OO costs three full parse+prettyprint rewrites of a MB-scale file per installed target (modules/.punkcheck is ~1.9MB), which is why the batch path defers; deferring must be a first-class mode of the one lifecycle, not an ad-hoc bypass with its own record code. ## Approach - Persistence policy selected per event (eager | deferred); in deferred mode the installtrack's in-memory recordset is authoritative between flushes, so the o_record_list coherence fix is a precondition, not a side quest. - installfile_add_source_and_fetch_metadata stays as the shared pure engine - it is the one correctly-layered piece (record transform + filesystem metadata fetch, no saves), used by all three current paths. - start_installer_event's role is subsumed by installtrack construction + start_event; the surviving convenience surface for callers is punkcheck::install / install_tm_files / install_non_tm_files as thin OO consumers. - Single save chokepoint: all writes to a .punkcheck file (event start, eager targetset ops, deferred flush) route through one proc/method - this is the attachment point G-095 locks onto. ## Alternatives considered - Making the legacy procs true facades that construct transient event objects while preserving their dict-passing contract - rejected: zero live callers make the reconstitution ceremony pointless; error-with-pointer shims are honest and cheaper. - Locking/concurrency first, refactor later - rejected: the safety measures would have to be implemented and kept in sync across three lifecycle implementations; sequencing the unification first means G-095 lands once. ## Notes - Prerequisite for G-095 (concurrent-writer safety) - that goal's lock, atomic-rename and merge-on-flush measures assume this goal's single save chokepoint. - Version-skew during rollout: bootsupport punkcheck (0.4.0) and the dev module can both write the same tree's .punkcheck files (make.tcl runs bootsupport's copy; a dev shell runs the built one) - re-vendor bootsupport promptly after landing, and sequence this refactor before any on-disk protocol change from G-095. - G-087 (archived - goals/archive/G-087-thin-project-layouts.md) recorded a punkcheck MAX_PATH candidate (deep relative source chains exceed Windows path limits, unnormalized) - natural rider on this goal's load/save consolidation. Reconfirmed 2026-07-19 during the stage-4 verification: project generation into a deep directory fails reading the layout store manifests through a long ../ chain. - The characterization suites were written expressly to lock behaviour "before any refactoring" - this is that refactoring. - punkcheck::cli (status/paths) is a read-only consumer and stays untouched. ## Progress - 2026-07-21 landed in full (punkcheck 0.5.0; punk::mix::cli 0.5.1 comment-only cleanup; bootsupport re-vendored same day): installtrack/installevent as the sole lifecycle implementation; per-event persistence policy (start_event ?-persistence eager|deferred?; deferred = in-memory working recordset, persisted by an explicit $installer flush; QUERY never persisted in either mode); punkcheck::install rebuilt as a tree-walking OO consumer in deferred mode; legacy proc pipeline (installfile_begin/started_install/finished_install/ skipped_install AND start_installer_event) retired to error-with-pointer shims with installfile_help rewritten for the OO lifecycle; single save chokepoint = installtrack method save_working_recordset (the G-095 attachment point); dead targetset class removed. - Acceptance verification (2026-07-21, native Tcl 9.0.3 tclsh, Windows 11): - install.test 35/35, installtrack.test 29/29. One pin flipped, recorded in-file: the full-lifecycle test's `$event end` + `$installer end_event` double-end now correctly errors, because ending clears the installtrack's active-event slot (the mechanism that makes sequential events on one instance possible). No real consumer double-ends (cli/make.tcl/commandsets all call `$event end` once - verified by grep). - lifecycle.test (new, 14 tests): batch/OO field equivalence for INSTALL-RECORD and SOURCE records incl equal -targets_cksums; o_record_list coherence (event-1 targetset writes survive the second start_event; event ts_begin preserved through flush); deferred no-write-until-flush and unflushed-discard semantics; QUERY leaves no on-disk FILEINFO; all five retirement shims error with pointer; get_recordlist and targetset_dict fixes. - Retired-trio grep over src/modules, src/make.tcl, src/project_layouts: only the module's own shims match. - Full build: `make.tcl modules` x2 plus a full `make.tcl project` under the re-vendored bootsupport punkcheck 0.5.0 complete exit 0 with skip/copy decisions unchanged against pre-refactor .punkcheck state - copied: 0 on every batch phase (modules, modules_tcl8/9, libs, vfs syncs, layout syncs), kit installs skip 'no change detected'; the sole punk905 kit rebuild traces to the newly promoted tclsh905 runtime (a genuinely changed source from the parallel G-102 session, not a spurious recopy). - Additional latent fixes surfaced by the unification (beyond the two named in Context): installtrack as_record wrote -name with a trailing space via a stray escaped-space backslash, breaking by-name installer lookup after any flush/save_installer_record; event reconstruction in the installtrack constructor passed raw record fields so -tsbegin/-tsend fell to defaults (a flush would have rewritten prior event history timestamps with 'now' and empty ends). Both fixed and pinned by lifecycle.test. - Deliberate contract deltas beyond the headline unification (all recorded in the punkcheck 0.5.0 buildversion changelog): QUERY targetset_started now sets -metadata_us like other operations; all-targets batch installs now store -targets_cksums (previously only non-all-targets copies did - enables later synced-targets use); batch install records gain -metadata_us/-ts_start_transfer/-transfer_us/-cksum_us (field parity with OO records); recordlist::extract_or_create_fileset_record no longer prints to stdout (isnew returned; targetset_init reports via debugchannel, and punkcheck::install preserves its progresschannel-gated announcement); an explicitly empty -punkcheck-folder now errors early instead of failing later in path relativization; -punkcheck-records is accepted-but-ignored (pre-G-094 behaviour already replaced it at depth 0); the installedsourcechanged copy print no longer emits a duplicate '(cksum time)' using the copy timing. - Observed during verification (G-095 evidence): one transient save failure (targetset_init -> save_working_recordset traceback) on the first post-re-vendor build run, with a second session concurrently active in the same checkout; not reproducible - two subsequent full runs clean, exit 0. Exactly the concurrent-writer exposure G-095's locking/atomic-rename measures target, now funnelled through the single chokepoint.