Context

A validation verdict is evidence about one exact text, checked against one exact schema, at one moment. A tool that shows the verdict and then lets it go has checked the configuration but kept no evidence that it did.

Pyxel Config Lab validates Pyxel configurations in the browser, against the schema of a pinned Pyxel release (3.0.2) and the Lab's own checks of what Pyxel refuses at load or stops on at run time. Nothing leaves the visitor's device. Until this decision, the verdict lived in one panel that is rewritten as the visitor types, and three buttons took a configuration away: Copy YAML, Share Link and Files Needed.

This record captures the decision to keep every verdict, and to make taking a configuration away an act on a recorded verdict rather than on whatever the editor holds.

Problem

Each of the three buttons acted on the editor's text at the moment of the click, and carried the text alone: not the verdict, not the schema it was judged against, not the Pyxel version. Once the panel moved on, nothing could show what the Lab had said about a configuration, or about which exact text.

That gap stays invisible until the record is needed. A run fails a week later, a result is questioned, or the Lab moves to a newer Pyxel schema and the same text now gets a different verdict. "The Lab said it was fine" can no longer be shown, only remembered. A check that leaves no record is, a week later, indistinguishable from a check that never ran.

Pyxel itself does not close the gap. When Pyxel runs from the command line, it copies the configuration into the output folder and appends a version footer, so the copy no longer matches the original byte for byte. When it runs from a notebook through pyxel.run_mode, it keeps no copy at all. Nothing in Pyxel hashes a configuration, its input files or its outputs.

We need a clear boundary between checking a configuration and taking it away as checked.

Decision

Every press of Validate with Pyxel will append one block to a hash-chained ledger in the browser, whatever the verdict, and Copy YAML, Share Link and Files Needed will act only on recorded blocks.

A block holds the editor's text exactly as it was, never reformatted, and its SHA-256. It holds the verdict and its messages in plain words, the files the configuration references (by path only), the Pyxel version and the SHA-256 of the bundled schema the verdict came from, the time from the computer's clock, and the hash of the block before it. Its own hash is SHA-256 over all of that, so a change to any part of an old block shows at every link after it.

Block 0                  Block 1                  Block 2
previous: 64 zeros   ←   previous: hash 0     ←   previous: hash 1
config + verdict         config + verdict         config + verdict
versions + files         versions + files         versions + files
hash 0                   hash 1                   hash 2

Failed validations are recorded as fully as passing ones. A ledger of passes only would show where a configuration ended up and hide the path it took: the first error, the fix, the second check.

A block can be copied, shared as a link, checked for the files it needs, or opened back into the editor to reproduce it. The chain is verified when the Lab opens, when the ledger opens and after every new block. A mismatch is named by block number, and never repaired, reordered or deleted on the visitor's behalf.

When the browser's storage is blocked, the ledger lasts for the tab and says so. When storage is full, the new validation is not recorded and the Lab says so, rather than dropping an older block to make room. Clearing the history is a deliberate act, with a confirmation.

This establishes a boundary between a check and its export: what leaves the Lab is always a block with a verdict recorded beside it, rather than whatever text the editor happens to hold.

Alternatives Considered

Keep the Export Buttons as They Were

Rejected.

They worked, and that was the problem: each carried text with nothing about how it had been judged. Adding a verdict line to the copied text would change the YAML on the way out, which the Lab never does.

Record Only Passing Validations

Rejected.

A history of successes is a record with the failures edited out, and the failures carry the information: which rule fired, what changed, how long the fix took. Recording only passes would also make the ledger's silence about a configuration ambiguous. It could mean never checked, or checked and failed.

A Plain History List Without Hashes

Rejected.

A list can be edited by a browser extension, a developer console, or a future version of the Lab migrating its own storage, and the edit would be invisible. SHA-256 is built into every current browser, so the cost of making edits detectable is close to nothing.

A Distributed Blockchain With Consensus

Rejected.

Consensus solves agreement between parties who do not trust each other. Here there is one person and one browser, with nobody to agree with. A network of nodes would also send configurations off the device, which the Lab has never done and its privacy notice promises it will not.

The ledger keeps the part of a blockchain that does the work here, the hash links, and leaves out the layer that solves a different problem. That is also why the Lab can honestly call it a Blockchain Ledger: it is a chain of blocks, each sealed to the one before, without the network.

Anchor the Chain to a Public Ledger

Deferred.

Publishing each chain's latest hash to a public chain would make even a full rewrite of the history detectable. It would also cost money per anchor, add a network dependency, and publish when someone was working. That trade only pays once ledgers are shared between people who need to trust each other's history.

Hash the Referenced Files in the Browser

Rejected for this boundary.

A configuration names its input files by path, and a web page cannot read a path on the visitor's disk. Asking for every file to be picked by hand would turn a check into an upload ritual. Files are listed by path and by the model that reads them, as Files Needed always did, and hashing their contents belongs where they live: beside Pyxel, on the machine that runs it.

Consequences

Positive

  • Every verdict is tied to the exact text, schema hash and Pyxel version it was computed against.

  • What is copied or shared is always a recorded block, so the sharer can show what the Lab said about it and when.

  • The path from the first error to a passing configuration is kept, not only the result.

  • An edit to a stored block is detected and named by block number, rather than silently changing the record.

  • Blocks recorded under one Pyxel schema can be told apart from blocks recorded under the next, which a future upgrade check can build on.

  • Nothing new leaves the device, and no new dependency is added: hashing uses the browser's own cryptography, or the Lab's own tested SHA-256 where the browser offers none.

  • Share links keep their format, so every link made before the ledger still opens.

Trade-offs

  • The chain is tamper-evident, not tamper-proof. Anyone with access to the browser can rebuild the whole chain with fresh hashes. It catches accidental and partial edits, not a deliberate rewrite of everything.

  • Times come from the computer's clock, which the visitor controls. A block shows when the computer said it was recorded, not proof of when it was.

  • A verdict records what the Lab checked, not what Pyxel ran. A passing block means the Lab's checks found nothing, and only a run in Pyxel proves the rest. The ledger has to say so wherever it shows a pass.

  • Files are recorded by path only. A file's contents can change without any block noticing.

  • The ledger lives in one browser profile on one device. A private window, another browser or cleared site data starts a new chain, and inside the Jupyter extension the frame may keep a ledger of its own.

  • Configurations stay in the browser's storage until the history is cleared. On a shared computer, the next person can read them.

  • Taking a configuration away now takes one more step: validate first, then copy or share from the block. That step is the decision rather than a side effect, but it is still a step.

  • Two tabs recording at the same instant could fork the chain. The Lab reads the stored chain again before each new block, and a fork that still happens is reported as a mismatch rather than merged silently.

This decision is implemented in Pyxel Config Lab, and the work below provides concrete evidence of applying the principle in practice. The gap it closes was confirmed in Pyxel's own source first, at Pyxel master 4eee160 (13 October 2026) and ESA's Pyxel MCP server at 5f78392.

Implementation Evidence

Pyxel Config Lab
A browser tool for building and validating Pyxel configurations, running entirely on the visitor's device. The Blockchain Ledger sits under the editor as one dropdown, in place of the three export buttons.

https://pyxel-config-lab-ede25c.gitlab.io

The Ledger Module
The chain itself: a canonical serialization with sorted keys, SHA-256 from the browser's Web Crypto (with a small built-in implementation, tested against Node's, for pages served without HTTPS), blocks that carry the previous block's hash, and a verifier that names the first block that no longer matches.

https://gitlab.com/dobybaxter127/pyxel-config-lab/-/blob/main/scripts/ledger.js

Tests
The tests build a chain, change one character of an old block's configuration, verdict or link, and check that verification names that block. They check the Lab's hashes against Node's own SHA-256, check that a failed validation is recorded as fully as a passing one, and check that full storage refuses the new block while keeping every old one.

https://gitlab.com/dobybaxter127/pyxel-config-lab/-/blob/main/tests/ledger.test.js

Documentation
The Lab's README describes what a block holds, what the ledger proves and what it does not, with a diagram of the chain.

https://gitlab.com/dobybaxter127/pyxel-config-lab/-/blob/main/README.md

Upstream Evidence

The Configuration Copy in Pyxel
copy_config_file copies the YAML into the run folder and appends a # Pyxel version footer to the copy. It is called only when Pyxel runs from the command line.

https://gitlab.com/esa/pyxel/-/blob/master/pyxel/configuration/configuration.py

Pyxel Issue #728: Save YAML File for Each Running Mode
Open. The configuration is saved to the output folder only from the command line, not when pyxel.run_mode is used, which is how notebooks run Pyxel.

https://gitlab.com/esa/pyxel/-/issues/728

No Hashing in a Run
A search of Pyxel's source for hashing, checksums and provenance finds one fixed MD5, used to check a test image Pyxel downloads. Configurations, input files and outputs are never hashed. Pyxel stamps its version and running mode into its results, with no hash of what produced them.

https://gitlab.com/esa/pyxel

ESA's Pyxel MCP Server
Stamps the Pyxel version on every answer and asks notebook authors to keep the YAML path, version and run mode visible. It records no hashes.

https://gitlab.com/esa/pyxel-mcp

The Share Link Checksum in Pyxel Config Lab
Share links already carry their text's length and a 32-bit checksum, so a link cut short by an email or chat app opens as damaged instead of as part of a configuration. The ledger carries the same idea from a link in transit to a history at rest, with SHA-256, because the risk it guards against is an edit rather than damage on the way.

https://gitlab.com/dobybaxter127/pyxel-config-lab/-/blob/main/scripts/permalink.js

Relationship to the Broader Engineering Approach

This ADR is part of a broader engineering principle:

Invalid states should be made difficult or impossible to reach the execution layer.

ADR-001 applies that principle to Pyxel Config Lab directly: configuration is validated before it runs. This record is the next step for the same tool. ADR-001 decides that a configuration must be checked; this decision makes the check leave evidence.

Where ADR-001 places the boundary between configuration and execution, ADR-010 places it between write paths and stored state, and ADR-012 places it between generation and presentation, this decision places it between a check and its export. The move is the same in each case: what cannot be assumed is settled at a named boundary, and everything past that boundary may rely on it.

The distinctive difficulty here is that the risk is not a wrong input but a missing record. A failed check announces itself. A check that left no trace announces nothing, and the absence is discovered only at the moment the evidence is needed. A verdict on screen is a claim about the present. A verdict in a chain is evidence about the past.

Configuration → guarded before execution
Writes        → guarded before storage
Generation    → guarded before presentation
Verdicts      → chained before they are taken away

Decision Outcome

The ledger is considered an intentional architectural boundary.

Future work on the Lab should preserve this principle where practical:

Edit → Validate → Block (text, verdict, versions, hash) → Copy, Share or Files Needed from the block

rather than:

Edit → Copy

This ADR should be revisited if ledgers start being shared between people, at which point anchoring the chain or collecting signed confirmations from independent reproductions becomes worth its cost. It should also be revisited if a run receipt beside Pyxel begins hashing input files and outputs, in which case the browser's blocks and those receipts should share one format, or if browser storage limits are reached in real use.