ADR-016: The firmware and TFT sources are verified through a registered composition
Date: 2026-08-06
Status: Accepted
Context
v0.5.0 puts two bodies of code on the development host that have never been there. The firmware
application gains a run loop, a time policy, an edge to action mapping, and a diagnostic emitter, and
frontends/tft gains role resolution, frame composition, clipping, a procedural fallback, and a pure
button filter. All of it is decision code, and all of it is written to be a function of its inputs so
that a host test can assert it. The ESP-IDF backends underneath it are the thin half, and only a
device can confirm those.
Nothing runs a hosted test today unless a composition selects it. config/compositions.json declares
the apps and frontends, tools/compositions.py resolves a selection to one composition, and the root
CMakeLists.txt configures the selected composition’s app and frontend and nothing else. Components
are the exception, because they belong to no app and no frontend and are configured by every
composition.
apps/firmware has lived outside that model since v0.3.0. It exists as pet_firmware_app, it is
built by the ESP-IDF build, and no hosted composition selects it. gate.py records the situation in
firmware_includes, a helper whose comment states that the firmware and board headers deliberately
sit outside hosted compositions and are added to the header check by hand.
Leaving the new sources in that position would put the release’s testable decisions in the one place the gate does not reach, which is the opposite of what the release is designed for.
Decision
The firmware application and the TFT frontend are registered in config/compositions.json like any
other app and frontend, and the hosted gate covers them through the resulting composition.
The registry gains the app firmware with target pet_firmware_app and the frontend tft with
target pet_frontend_tft. composition_name derives the name firmware-tft from the app and the
frontend, so the identifier is produced by the registry rather than chosen. The hosted composition
and the embedded build are the same app and the same frontend, and the only difference between them
is whether PET_FRONTEND_TFT_ESP_IDF selects the ESP-IDF backends. The hosted composition leaves
them deselected.
The claim boundary is recorded where claims are checked rather than in the identifier. The support
matrix in architecture section 7.2 and the release evidence both state that
firmware-tft is hosted build and test evidence for the firmware orchestration boundary and the
common TFT sources, and that it is not a device runtime claim.
Consequences
The gate covers the release’s decision code. The strict gate, the include boundaries, the header
self-containment check, the sanitiser runs, and the audit gate all reach the firmware application and
the common TFT sources through an ordinary composition selection, and ./gate.py audit --all-compositions widens from two compositions to three.
Three pieces of tooling move with it, and they are named in
architecture section 7.3 so that no implementing PBI meets them by
surprise. The firmware_includes helper in gate.py describes a situation that ends when the
composition exists, so the helper and its comment are reconciled rather than left to drift. The
embedded_build.subjects set in config/verification.json names no frontend today and grows to
carry the first one. The same file forbids the display dependency family in the embedded build, and
that family matches the esp_lcd prefix the display backend is added to use, so the rule is narrowed
to the sources that must still be free of it rather than removed.
The composition carries a cost the other two do not. It builds an application whose only real host is a board, so a maintainer running the full gate pays for a composition they cannot run as a product. That is the price of asserting the decisions before a device sees them, and it is smaller than discovering a composition mistake through a flashed image.
A reader could take a passing firmware-tft gate as evidence that the firmware works on hardware.
The name cannot prevent that, so the support matrix and the evidence file carry the boundary, and the
device rows stay separate from it.
Alternatives considered
Configure frontends/tft unconditionally in every composition, the way components/ is
configured. Rejected because the root CMakeLists.txt deliberately configures only the selected
composition’s frontend. A frontend outside that rule would be a second kind of frontend with no
registry entry and no selection, and the registry would stop describing what a build contains.
Add a suffix such as firmware-tft-hosted to distinguish the hosted build from the device
build. Rejected because the name is derived by composition_name as app-frontend, and a suffix
would require inventing an app or a frontend that does not exist. The two builds are also the same
app and the same frontend, so a second name would describe a second composition that is not there.
The distinction is a claim rather than an identity, and claims are recorded in the support matrix.
Register the app and leave the frontend out, testing the TFT sources through a test-only target. Rejected because a target reachable only by the test suite is outside the composition model that the gate, the presets, and the project command all read, so it would be verified by a path no other frontend uses.
Leave the firmware and TFT sources hosted-untested and rely on the ESP-IDF build. Rejected because a cross build proves that code compiles and links, and this release’s value is in decisions that a compile cannot check. It would also make every rendering and filtering mistake a flash cycle away from discovery.
References
- ADR-010, the build selection pattern the backends follow
- Architecture for v0.5.0, sections 5.10, 7.2, and 7.3
- Epics,
E30andE33 - PBIs,
PBI-105andPBI-113 - Milestones, release
v0.5.0