ADR-015: The canonical manifest grammar has one reader

Date: 2026-08-05

Status: Accepted

Context

assets/pet/manifest.petasset is the authored source that maps images, animations, and presentation roles. Today one program reads it. components/manifest parses the text in C at run time into a PetManifest, and components/assets and components/presentation consume that structure.

v0.5.0 adds a second reader. tools/assets/pack.py is a build-time converter that compiles the same canonical manifest and its images into the embedded asset pack. It cannot call the existing C parser without a compiled host tool, and producing one during an ESP-IDF cross compile would put a host build inside a cross build and would end the converter’s standard-library-only property. The converter therefore reads the grammar itself.

The result is one grammar with two implementations in two languages. Nothing checks that they agree. A change to the manifest format, such as a new section or a new field on a frame, has to be made twice, and a disagreement surfaces only if both sides happen to be tested for it.

ADR-007 decided that parsing stays in components/manifest. It decided that when one reader existed and no build-time consumer was planned.

Decision

The canonical manifest grammar has one reader, and that reader is the build-time converter.

The converter gains a second output. Alongside the pack it emits a generated C source that constructs a PetManifest carrying paths, which hosted builds compile in place of a run-time parse. The embedded build continues to take its presentation structure from the pack.

components/manifest keeps manifest.h, which owns the data model, the bounded limits, and the status vocabulary that both paths already share. manifest.c and its grammar tests are removed with the run-time parse they serve.

pet_assets_load stops reading and parsing manifest text. It keeps the responsibility that only a run time can hold, which is confirming that every image the manifest declares can actually be opened by the configured reader.

v0.5.0 does not implement this. It carries one requirement only, recorded in PBI-118: the converter’s grammar reading is an isolated Python module with its own tests, separate from pack encoding, so that the later collapse moves a module rather than rewriting a program. A later release owns the generated source, the components/assets contract change, and the removal.

Consequences

The grammar lives in one place, in Python, at build time. A manifest the grammar rejects fails the build instead of the run, which is a stronger guarantee than run-time validation rather than a weaker one, because a rejected manifest can no longer reach a user. ADR-007 already recorded that the manifest is a project-owned contract and not a user modding interface, so no untrusted input path is given up.

The components/assets contract narrows when the change lands. PetAssetsConfig loses the scratch text storage the parse borrowed, and PET_ASSETS_MANIFEST_NAME loses its run-time meaning. The fallback path loses its invalid-manifest trigger on the desktop and keeps its missing-image and rejected-pack triggers, so the fallback test surface moves rather than disappearing. That contract work is the larger half of the change and is the reason it is not carried by v0.5.0.

Hosted builds gain a build dependency on the converter. Python is already a hard prerequisite for the project command and every gate, so the dependency is new to the build graph rather than to the project.

The desktop keeps loading canonical PNG files and moves onto no pack pixel encoding. Only the source of its presentation structure changes.

v0.5.0 carries the duplication deliberately. Following the register practice, the limitation is opened by the PBI that creates it rather than in advance, and the first change to the manifest grammar is the signal that the deferred work is due.

Alternatives considered

Keep both implementations permanently. Rejected because the cost is unbounded and lands on whoever changes the grammar next, and because no compiler, linker, or gate step can check that two readers in two languages still agree.

Call the C parser from Python through ctypes, cffi, or a compiled host tool. Rejected because the converter also runs inside the ESP-IDF cross build, which would then have to produce a host binary during a cross compile. It would also make the converter depend on a compiled artefact and end the standard-library-only property that arch-v0.5.0 section 5.7 records.

Move the desktop onto the pack. Rejected because the pack’s pixel encoding is chosen against a measured panel, so the reference host would inherit a device decision. v0.5.0 excludes this for the same reason.

Emit an intermediate JSON or text artefact for the desktop to parse in C. Rejected because it moves the grammar rather than removing it, and because it needs the C JSON dependency that ADR-007 already declined.

Make the change inside v0.5.0. Rejected because that release already carries the first device bring-up in the project’s history, and the components/assets contract change would be a second structural change to the same component in the same release.

References