You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

15 KiB

G-135 Asset provenance records: a verifiable master-to-derived icon chain with a platform-neutral generator

Status: achieved 2026-07-28 Scope: src/assets/logo/ (per-asset *.assetorigin.toml sidecars, masters and generated icons); src/assets/logo/make-ico.ps1 (replaced in place by a Tcl generator, then removed); scriptlib/developer/ (provenance checker, sibling to goals_lint and architecture_lint); checker home revisited if it becomes a bake step (a punk:: module rather than scriptlib, which make.tcl must not depend on); src/runtime/punkshell.ico and src/vfs/_vfscommon.vfs/punkshell.ico (recorded as derived copies, not relocated) Goal: Any committed icon in the tree can state what it was derived from and be checked against that claim on any platform, and regenerating one from its master needs no Windows-only tooling - while an icon carrying no record stays a fully supported and silent case. Acceptance: the <asset-filename>.assetorigin.toml sidecar format is documented with a schema key, the recorded asset bound by the sidecar's own filename (suffix stripped, same directory) and the source reference relative to that directory, files ending in the suffix being records, never assets; a reader tolerates a missing sidecar, unknown keys and an unrecognised schema version without erroring; a checker written in plain Tcl - no rasterizer, no external binary, runnable under any punkshell runtime - classifies each asset it examines as exactly one of unrecorded (no sidecar), verified, artifact-absent (a sidecar whose paired asset is gone), source-absent, replaced or stale per the state table in this file, where verified requires every hash the sidecar records to match, so a sidecar carrying only the artifact hash is verified on that alone, demonstrably distinguishing stale (source changed since the artifact was generated) from replaced (artifact changed since recording) and from artifact-absent on constructed fixtures, and is advisory only, never failing a build, in the manner of the G-133 payload checks; the icon generator is reimplemented with no Windows-only component (System.Drawing in particular), regenerating every generated .ico under src/assets/logo/ byte-identically to the current make-ico.ps1 under the invocation profile that produced it before that script is removed, and (re)writing the sidecar of each artifact it produces - artifact and source hashes plus the invocation options - so regeneration cannot leave a stale record; and sidecars exist and verify for every generated artifact under src/assets/logo/, alternative/, web/ and project-default/.

Context

Before 2026-07-27 the tree held several byte-identical punk1.ico files with nothing above them. An .ico is a compiled artifact - a container of pre-rasterized bitmaps - so there was no file anyone could edit, no recorded way to produce a new one, and no way to verify that any copy was what someone intended.

src/assets/logo/ (91fa500e) introduced a vector master and a generator, establishing three tiers: the SVG master, the generated .ico, and the build-consumed copies (src/runtime/punkshell.ico, _vfscommon.vfs, mkzipfix.vfs, punk8_statictwapi.vfs). What is still missing is any record that ties the tiers together. Nothing states that src/runtime/punkshell.ico came from punk-mark.svg, so one specific failure is undetectable: someone edits the SVG, forgets to regenerate, and the committed .ico silently stops depicting the master.

Design-time and bake-time are separate problems, and conflating them is what makes the manual copy into build trees feel wrong when it is not. G-057's acceptance requires that "a fresh checkout that fetches a runtime and bakes produces iconed kits with no build toolchain installed" - a rasterizer IS a build toolchain, so SVG-to-raster can never happen at bake time on any platform, including Windows. The .ico must remain a committed artifact. What is missing is therefore not automation but a recorded, checkable link.

The generator's platform tie is narrower than it appears. pwsh and rsvg-convert are both cross-platform; the entire Windows dependency is one line - Add-Type -AssemblyName System.Drawing, used solely to decode a PNG into raw BGRA pixels, which throws on non-Windows under .NET 6+. Tcl core ships zlib, so PNG decode is inflate plus undoing the five row filters, and the ICO container assembly downstream is pure binary format.

Approach

  • The record is OPTIONAL by construction. Absence means unrecorded - silent, never a warning. A record is an opt-in claim, and nothing may read it to decide whether a build proceeds. Most projects live in this state permanently.

  • Per-asset sidecar records: <asset-filename>.assetorigin.toml beside the asset it describes. The pairing is the filename - no path key - so create, replace and remove are always the file pair, with no shared record to edit; the pair survives wholesale directory copies (the property G-136 depends on) and single-asset copies alike; and overlay merges compose correctly by construction (merge_over's same-path replacement carries asset and sidecar together). Files ending in .assetorigin.toml are records, never assets.

  • Every field except the artifact hash is optional. A sidecar carrying only a hash means "I know this file, I do not know where it came from" - which serves an owner who drops in their own .ico but still wants change detection.

  • Unknown keys and unrecognised schema versions are skipped, not errors, so a future writer cannot break an older reader.

  • Six states, evaluated in table order with the first match winning (which is what makes "exactly one" true by construction), policy left to the consumer rather than baked into the format:

    Order State Condition
    1 unrecorded no sidecar for the file
    2 artifact-absent a sidecar exists but its paired asset does not
    3 replaced asset present, hash differs from the sidecar (regardless of source state)
    4 source-absent asset matches, a recorded source is not present in the tree
    5 stale asset matches, recorded source present but its hash differs
    6 verified asset matches, and every other hash the sidecar records also matches

    Hash comparisons apply only to hashes actually recorded - a claim with no hash contributes only a presence check, so a source named without a source hash can never produce stale; drift on unhashed claims is the record's own limitation, not the checker's. stale is the point of the whole goal; it is distinguishable from replaced, which means the opposite thing and deserves the opposite response, and from artifact-absent, which means the record itself has gone stale and needs repair.

  • A role field (pe-resource | web | generic) makes an existing hazard machine-checkable rather than a documentation footnote: web/favicon.ico is all-PNG, which is exactly what GDI+ and tklib's ico package cannot read, so a future embedding step can refuse anything marked role = "web".

  • Generation and verification are separable and get different treatment. Verification must be portable and dependency-free, because it is what runs everywhere - including inside a bake and inside a derived project. Generation needs a rasterizer and runs only when artwork changes, so it may depend on rsvg-convert while still carrying no Windows-only component.

Alternatives considered

  • PROVENANCE.md prose, the src/tools/punkzip convention - rejected: human prose is not machine-checkable, and the failure this goal targets is silent drift, which needs hashes.
  • punkorigin.toml, G-027's generated-project carrier - rejected: that answers project lineage ("this project came from punkshell layout X"), a different question from asset derivation. Keep them separate.
  • One record file per directory carrying an [[asset]] array - the original draft shape - rejected at the 2026-07-28 pre-activation review: entries are shared state (every writer does a read-modify-write - the punkcheck lesson: G-094 + G-095, both achieved 2026-07-21, archived at goals/archive/G-094-punkcheck-single-lifecycle.md and goals/archive/G-095-punkcheck-concurrent-writer-safety.md), a record at a .vfs root is clobbered file-level by overlay merges (a kit-tree record would replace the common one wholesale, entries and all), dangling entries are invisible in listings, and copying a single asset out of a directory loses its record. Accepted in exchange: web/ carries one sidecar per generated raster, and per-directory facts (format documentation, cross-asset notes) live in AGENTS.md rather than in the record file.
  • A fauxlink-carried record (toml body per the fauxlink extension convention, source path encoded in the name) - rejected: the filename would encode a mutable fact, so master renames invalidate sidecar filenames, and overlay pairing would need a nominal-keyed composition invariant in merge_over and every other composer; the ls-visible derivation edge does not pay for that. The nominal-keyed idea itself (at most one fauxlink per effective nominal per directory, last layer wins) is recorded as generally useful fauxlink hardening independent of this goal - the tree already satisfies it (71 fauxlinks, no same-directory nominal collisions, surveyed 2026-07-28).
  • Making the .ico a build output regenerated at bake time - rejected: G-057 requires bakes to succeed with no build toolchain installed.

Notes

  • Related: G-057 - consumer. Its per-kit icon sidecar is specified to carry "source .ico identity and provenance" in a documented plain-text form; these records are the natural thing it reads to populate those fields, which closes the gap where its provenance otherwise stops one tier short of the actual source. Its Scope names src/runtime/punkshell.ico as the project default, which this goal reframes as a recorded derived copy rather than the origin.
  • Related: G-128 - sibling surface. Portable PE resource stamping shares this goal's motivation that icon handling must not be Windows-bound.
  • The advisory posture the Acceptance borrows is G-133's (achieved 2026-07-27, archived at goals/archive/G-133-bake-payload-consistency-checks.md): its bake payload checks recap as BUILD-WARNINGs and never fail the build.
  • Generator and checker have different homes on purpose. The generator operates on the files beside it, runs only when artwork changes, and is already documented in src/assets/logo/AGENTS.md, so it is replaced in place. The checker is a lint in the shape of goals_lint.tcl and architecture_lint.tcl and belongs in scriptlib/developer/ - unless it becomes a bake step, in which case it must move to a punk:: module, because scriptlib/ is user-only territory that make.tcl must not depend on. That decision is deliberately deferred to the work.
  • Open workflow question, deliberately kept out of the Acceptance: whether the checker runs during make.tcl bake or only as an explicit verification subcommand. stale is a design-time mistake rather than a build-time one, which argues for a subcommand plus an opportunistic check only when a bake actually consumes an icon. Unresolved at achievement: the checker shipped as a standalone scriptlib tool only; bake integration, if ever wanted, moves it to a punk:: module per Scope (make.tcl must not depend on scriptlib).
  • Override behaviour in baked kits (recorded 2026-07-28): a kit .vfs that overrides a common asset without shipping its own sidecar produces a kit where the travelled common sidecar classifies the override as replaced - truthful and intended. An overrider that wants verified inside the built artifact ships its own sidecar beside the override; merge_over carries the pair by same-path replacement with no new machinery.
  • The sidecar shape eases the checker's parsing posture: each record is one small flat toml table, so a self-contained subset reader stays defensible if the tomlish dependency is unwanted in the scriptlib context. RESOLVED at implementation (2026-07-28): the checker embeds a self-contained flat-toml subset reader (root table only, stops at the first table header) and its own sha256, as does the generator - no tomlish, no tcllib; each script runs standalone under stock tclsh 8.6+.
  • Open at pre-activation review, resolved 2026-07-28 for one of the four build-consumed punkshell.ico copies: src/runtime/punkshell.ico carries a hand-authored derived-copy sidecar (hash equals source_hash, source ../assets/logo/punk-mark.ico) - it is a passive file no build step copies onward, so nothing ships. Still open (user call): sidecars for the _vfscommon.vfs, mkzipfix.vfs and punk8_statictwapi.vfs copies - a sidecar committed in _vfscommon.vfs ships inside every baked kit (the per-asset shape makes that collision-free; the shipping intent itself is the open call). Those three copies stay deliberately unrecorded (silent, fully supported) until the user decides.

Progress

Achieved 2026-07-28, all work landed that day in three increments (activation bcf6625d; generator + sidecars + ps1 removal + format docs f529f44e; checker de5a27a4). Verification evidence:

  • Byte-identity (generator clause): make-ico.tcl all -check regenerates all seven manifest artifacts (punk-mark.ico, alternative/punk-mark-solid.ico and project-default/icon.ico on the 16/24/32/48/64/128-DIB + 256-PNG profile; web/favicon.ico 16/32/48 all-PNG; web rasters 180/192/512 from icon-opaque.svg) byte-identical to the make-ico.ps1-era committed files, under both mingw tclsh 8.6.11 and native Tcl 9.0.3 with rsvg-convert 2.62.0 (the version that produced the committed artifacts) - proven BEFORE make-ico.ps1 was removed in the same change-set. rsvg-convert determinism separately confirmed by matching the PNG payloads extracted from committed icos against fresh renders.
  • Embedded sha256 validated against the three NIST vectors (empty string, "abc", the 448-bit vector) and cross-checked against PowerShell Get-FileHash on real artifacts. Identical implementation embedded in generator and checker; each script standalone.
  • Checker state model: selftest constructs 13 fixtures demonstrating all six states, the rule-3 precedence (replaced wins regardless of source state, proven with a missing source) and every tolerance rule (unrecognised schema ignored; unknown keys skipped; root table ends at the first table header; hash-only record verifies on the artifact hash alone; source named without source_hash is presence-only; unrecognised hash-algorithm prefix treated as not recorded; record without a usable artifact hash ignored) - 16/16 PASS under mingw tclsh 8.6.11, native Tcl 9.0.3 and the punk91 kit runtime (the "runnable under any punkshell runtime" clause).
  • Live tree: assetorigin_check.tcl check reports 8 records, 8 verified (the seven generated artifacts plus the src/runtime/punkshell.ico derived-copy record); default scan (src minus buildsuites, scriptlib, goals) 2.2s.
  • favicon source pinned empirically: committed payloads byte-match renders of both web/icon.svg and punk-mark.svg (identical output at 16/32/48); recorded as web/icon.svg - same directory, the primary web icon.

Remaining manual items: none for this goal's acceptance. The open user call on vfs-copy sidecars (kit shipping) is carried in Notes above; the bake-vs-subcommand workflow question likewise, both deliberately outside the Acceptance.