16 KiB
G-171 Routine builds never write tracked paths - explicit commands own VCS-visible changes
Status: proposed Scope: src/make.tcl (bake/bakehouse/vfslibs - VFSPAYLOAD + PACKAGES phases, kit assembly, new explicit materialize/tidy commands); src/make.tcl sync_layouts phase (the mint-side tracked-path write) with its targets src/project_layouts/ and src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/ plus the workflow_text TERMINOLOGY/DIAGRAM 1b stage-verb assignment; src/vfs/ (payload declarations, _targets staging generalization, README.md + AGENTS.md precedence spec); src/_bake (assembly staging); bin/packages (lib-tier consumption); .gitignore + .fossil-settings/ignore-glob (staging ignore coverage); src/tests/shell/testsuites/punkexe/ (kill-probe + precedence characterization) Goal: routine build operations (mint, bake/bakehouse/vfslibs and every phase they run) write only to VCS-ignored locations - declared payloads materialize into ignored staging and merge-time layering enacts drop-in-wins precedence identically on every machine - so an interrupted, buggy, or stale-tool bake cannot modify, delete, or shadow tracked files and git/fossil status stays truthful during and after any build; deliberate writes into tracked trees (materializing a fetched/built package into a .vfs folder as drop-in content, retiring superseded committed files, refreshing the thin-layout store) remain available only as explicit separately-invoked commands whose output is an ordinary reviewable VCS change, with help and docs steering to the declaration-based route by default. Acceptance: from a clean tree, a full bake/bakehouse run - including runs killed at arbitrary mid-run points (kill-probe characterization) - leaves git status and fossil changes reporting nothing outside ignore-covered paths; the VFSPAYLOAD/PACKAGES phases self-check their write destinations and abort before writing any tracked path; a hand-dropped file shadowing a declared payload entry survives assembly with drop-in-wins precedence on a records-free fresh clone (characterized, with the unified regime including _targets documented in src/vfs/README.md); the explicit materialize/tidy command(s) exist and are documented as the sole sanctioned tracked-tree write path, and no bake/bakehouse phase invokes them; a bake -hold run stops after assembly leaving the resolved per-kit images inspectable in staging, and a subsequent plain bake completes to kits identical to a single uninterrupted bake - staged assembly is reused only under digest-confirmed input currency, and stale or hand-modified held state is redone, never shipped; the thin-layout sync writes no tracked path from a mint or bakehouse run - refreshing the layout store and its modpod payload is an explicitly-invoked command producing an ordinary reviewable VCS change, the mint commands detect and report a stale store instead of silently refreshing it (the modules mint packs the tracked modpod tree as it stands), and make.tcl's stage vocabulary names the movement - the layout store documented as the third VCS-tracked consumption tree beside src/bootsupport and src/vfs/_vfscommon.vfs.
Context
The kit merge already runs entirely in ignored staging (src/_bake). The only step writing tracked paths is payload materialization: G-115's declarations (achieved 2026-07-31 - see goals/archive/G-115-declarative-vfs-composition.md) automate filling the same .vfs folders that were previously hand-assembled, so machine writes land in tracked space as an evolutionary artifact, not an architectural necessity.
Four structural problems follow. (1) Drop-in-wins is enacted in place via punkcheck records that are machine-local and VCS-ignored, so the authored-vs-installed distinction does not travel between machines - replace=true exists to wipe rather than reason, which is exactly the operation that strands trees when a bake dies. (2) Two precedence regimes coexist: in-folder materialization preserves drop-ins, while _targets staging (G-127, achieved - see goals/archive/G-127-crosstarget-vfs-bake.md) merges OVER the per-kit .vfs. (3) Any bake failure mid-materialization leaves tracked files modified/deleted - uninterpretable for a non-developer, and the reason three separate mid-bake-status traps are recorded in project memory. (4) The MINT side carries the same class: the sync_layouts phase of modules/libs/packages/bakehouse copies src/make.tcl, src/build.tcl, the root .gitignore and the bootsupport manifests into the tracked layout store, then the store into the templates modpod source tree - tracked writes issued by a routine build and owned by no explicit command. On a clean checkout they are no-ops, which is why the class went unnoticed; with -dirty-abort 0 a bakehouse mutates tracked layout files from uncommitted source, while its own help states it does NOT run the promotion gates because "payload promotion is a confirm-gated, committed act".
Incident evidence 2026-08-06: on a consumer-shaped machine (reset --hard + pull + fetch + bake), a stale untracked bin/punkzip.exe (pre-2.4.0) received the v2.4.0 -b flag, wrote a misdirected 90MB archive to src/-b, the kit-container step failed, and the died bake stranded half-replaced tracked trees across the declared kits - a wall of git modifications/deletions with no user-facing recovery story. Under this goal that whole class strands only ignored staging: recovery = rerun.
Vocabulary finding 2026-08-06 (surfaced by the BAKE SUMMARY work, whose mint run synced the layout copies and prompted the question "does the mint terminology cover this?"). make.tcl's stage verbs (G-155, achieved 2026-08-02 - see goals/archive/G-155-mint-stage-vocabulary.md) define mint as version-stamping magic-version sources into the projectroot modules*/lib*/ trees, and promote as propagation into a VCS-tracked consumption tree. The layout sync stamps nothing and lands nowhere near modules*/lib*/, so it is not mint by make.tcl's own definition; its target IS a tracked consumption tree - the src/project_layouts/ store that 'dev project.new' generates from (G-087, achieved - see goals/archive/G-087-thin-project-layouts.md) - so it has promotion's shape, and the release sequence already commits it in step (9) beside the two promotion targets. There are three such trees and only this one has no named command, no gate and no TERMINOLOGY entry: 'sync' appears there purely as a MECHANISM word, so the movement it performs was never assigned a stage and inherited mint by accident of which subcommands call it. The store->modpod copy is the one genuinely mint-internal half
- input preparation ordered deliberately BEFORE the source-modules mint that packs #modpod-* trees (verified 2026-08-06: a modules run's freshly packed templates-0.2.0.tm carries the same run's make.tcl, so there is no one-run lag) - and under this goal it moves with the rest into the explicit command, leaving the mint to pack the tracked modpod tree as it stands.
Approach
- Generalize the G-127 staging pattern: ALL declared payload entries (not just %platform% forms) materialize into ignored staging; the tracked .vfs folder is never a materialization target of any routine phase.
- Unify precedence at merge time: staging merged first, authored .vfs merged over it (drop-in wins), supersedes/replace expressed as merge-time masks; record the deliberate flip of the current _targets-over-.vfs order; document in src/vfs/README.md; characterize.
- Explicit commands own tracked-tree writes: a tidy command retiring superseded committed files (report-then-act), and a materialize command that deliberately lands a fetched/built package into a .vfs folder as drop-in content for the developer/builder persona (user direction 2026-08-06) - both producing ordinary reviewable VCS changes, with help text steering to declarations by default.
- Defense in depth: the phases enumerate write destinations and abort before touching any tracked path.
- Kill-probe characterization: clean tree in, clean tree out, at arbitrary kill points.
- Optional interim hardening (independent value if migration is staged): temp-sibling + atomic-swap for the current in-folder materialization, removing the stranded-mid-replace mode before the full move lands.
- Hold point: 'bake -hold' stops after assembly (post payload-staging + merge + per-target selection, pre container/deploy) with the resolved per-kit images in staging for inspection; a subsequent plain bake completes. Deliberate holds and interrupted bakes unify: any completing run re-verifies staged assembly against per-kit input digests (the G-162 machinery, write-side) and reuses only what is confirmed current. Held staging is machine-owned - inspection is sanctioned, hand-edits are not, and a digest mismatch means redo, not ship. bakelist surfaces held state alongside its freshness verdicts. The project-version gate applies to both invocations.
- DOX revision: src/vfs/AGENTS.md + README.md - the authored .vfs remains the drop-in surface; the resolved view is src/_bake (optionally a resolved-view report); revise the "operative assembly area" language accordingly.
- Coordinate with G-162: digest inputs become authored .vfs + staging (or declarations + tier); the digest infrastructure serves both bakelist's read-side verdicts and hold/resume's reuse decision (Approach 7); record the decision in both files.
- The mint-side write (Context (4)): an explicit command owns the thin-layout sync - refreshing the layout store from src/make.tcl + src/build.tcl + the root .gitignore + the bootsupport manifests, then the store->modpod payload - and modules/libs/packages/bakehouse stop invoking it, detecting and REPORTING a stale store instead (the same shape as the existing stale_bootsupport gate, so the developer learns exactly when the step is due). make.tcl's own vocabulary is corrected in the same work: TERMINOLOGY names the layout store as the third VCS-tracked consumption tree beside src/bootsupport and src/vfs/_vfscommon.vfs - widening 'promote' from what MOVES (minted packages) to where it LANDS (a tracked tree a downstream consumer boots, ships or is generated from) - DIAGRAM 1b marks the store->modpod copy as mint input preparation rather than layout update, and the bakehouse help's "does NOT run the promotion gates" claim becomes true.
Alternatives considered
- Generalized per-kit ignore lines (punkdeclare pattern for every kit) - workable interim, rejected as endpoint: machine writes still land inside tracked folders, ignore bookkeeping grows per kit, authored-vs-machine mixing persists, and supersedes still mutates tracked files from inside bake.
- Hardening only (step 6 atomic swap) - rejected as endpoint: shrinks the window but any bake bug can still mutate tracked files; the class survives.
- Requiring committed payload removal first - rejected as sequencing: that is G-004's own arc (artifact coverage per package); this goal works with committed payloads still present and decouples their eventual removal from bake behaviour.
Notes
- Related: G-004 - parent hygiene arc; write-side complement: bake stops being the maintainer of committed payload copies, so G-004's per-package removals decouple from bake behaviour. No dependency on G-004's completion.
- Related: G-157 - sibling commit-policy goal; shares the working-modes-stay- first-class posture (uncommitted drop-ins and derived-project permissiveness remain untouched). Its Scope also edits the store->modpod sync exclusion list (bin/sdx.kit), which this goal's explicit layout command inherits.
- Related: G-156 - its Scope re-seeds renamed identities through the same sync channels this goal takes ownership of (src/project_layouts + the templates modpod tree); whichever lands first, the other consumes the changed ownership of that channel.
- Related: G-162 - input-freshness digests consume payload trees; the staging move changes the digest input set, and the per-kit digest machinery is shared with the hold/resume seam (Approach 7, 9).
- Related: G-065 - declarative vendoring is the source-side sibling surface; its sync command is the same explicit-command pattern, and its audit's undeclared-drop-in reporting complements an authored-only .vfs.
- Related: G-141 - consumption-driven libfetch selection reads the same *.vfs.toml declarations; unaffected by the write-location move.
- Related: G-147 - fetch-side sibling from the same 2026-08-06 evidence (content-keyed tier tree currency).
- Related: G-006 - consent posture untouched: this goal moves write locations only; every network-consent gate stays as-is.
- Related (archived): G-115 (achieved 2026-07-31 - see goals/archive/G-115-declarative-vfs-composition.md) - the declarative surface consumed here; this goal deliberately revises its "folder remains the operative assembly area" stance: the authored folder stays the drop-in surface, machine-written content leaves it.
- Related (archived): G-127 (achieved - see goals/archive/G-127-crosstarget-vfs-bake.md) - the ignored-staging + merge mechanism generalized here, and the origin of the second precedence regime this goal unifies.
- Related (archived): G-155 (achieved 2026-08-02, archived) - the _bake/_mint stage-workdir vocabulary this staging extends.
- Tool-level fixes from the 2026-08-06 incident (punk::zip accelerator version handshake before emitting v2.4.0-only flags; punkzip strict rejection of unknown dash-flags) harden the trigger and are deliberately outside this contract - this goal removes the blast radius, not the trigger.
- Overlap survey 2026-08-06 (goals_xref paths over src/vfs, src/make.tcl, src/_bake, bin/packages): related in substance - G-004, G-141, G-065, G-162, plus policy/evidence siblings G-157, G-147, G-006 added conceptually; surveyed and judged not related in substance: G-089, G-114, G-131 (boot/_config surface), G-019 (payload content trimming), G-020 (Scope names a lib_tcl9 path only), G-116 (tcltls payload constituent - its removal stays G-004-era work).
- Follow-on candidate recorded 2026-08-08 (user-directed; becomes a Follow-on line at the flip): dirty-bake provenance indication. A bake from an uncommitted tree records nothing durable today - the stderr PROVENANCE-WARNING is the only trace (punkkit-stamp deliberately carries no VCS/time fields: the G-025 no-time-of-bake DECISION (achieved 2026-08-02 - see goals/archive/G-025-exe-selfreport.md) and the make.tcl no-volatile-fields stamp comment; identical inputs must yield identical bytes). Homes assessed 2026-08-08: (a) near-term - a tree_state = "clean@" | "dirty" field in the advisory kit sidecar (the stamp comment's own designated home for volatile facts, punkbin sidecar precedent; writefile_ifchanged keeps it byte-stable per state), surfaced by bakelist, the BAKE SUMMARY block and ' buildinfo' (a provenance notion beside G-025's stamp/live/governing: attested / unattested / unknown); (b) endpoint - a publication-side content-addressed manifest (kit sha1 -> producing commit) written by an explicit post-step-(9) command in the punkbin publication pattern, where ABSENCE of an entry IS the no-provenance indication - a dirty build cannot be trusted to attest itself, and this goal's records-free-fresh-clone reproducibility (Approach 1/2/5) is what upgrades rebake-and-compare into the manifest's verification step, so (b) sequences AFTER this goal's landing. REJECTED homes: the stamp/payload (determinism principle - phantom staleness across history, no truthful value in the sanctioned step-(6)-(8) dirty window) and the artifact filename (breaks name-keyed deploy/ledger machinery, retroactively false mid-sequence).
- Approach 10 message-shape template updated 2026-08-08 (git
b729ce07/ fossil 55a54d8b, project 0.65.1): the stale_bootsupport gate it cites now emits via a shared helper, punkboot::lib::bootsupport_stale_warning - mechanism-first wording that names the launch directory, contrasts src/-vs-elsewhere module precedence, special-cases the repair commands, and prints the resolved src path as a copy-pasteable fix line. The layout-store staleness report should generalize that helper (parameterize the subject tree, stale list and steer-to command) rather than hand-roll a second block. Semantics stay distinct by design: bootsupport staleness is a version-aware abort/prompt (the stale copies are the code executing make.tcl itself), while the layout-store report is report-and-proceed (the mint packs the tracked modpod tree as it stands - the lag is the deliberate, reported state).