# Decisions One entry per decision that outlives the container it was made in. Newest last. A decision that lives only in an agent's context is lost when that container dies, which is what this file is for. --- ## P0 — the exporter, 2026-08-28 ### The exporter reads one authored file, and stamps its provenance into the output `export/` is derived and `authored/` is hand-written, and the natural reading of that is that the exporter never touches `authored/`. But a screen has to be *called* something, and the disc does not name its builds — the identification of build 5 as the main menu is HANDOFF Q2, **measured against a live capture**, not a field. Two ways to handle that: 1. the exporter emits `build_05.json` and the runtime renames it from `authored/screen_names.json`; 2. the exporter reads that map and writes `main_menu.json` directly. Chose **2**, with a condition: every name it applies carries `name_source: "authored"` and a `name_why` quoting the evidence, and `check` **rejects** an authored name with no `why`. The file that lands in `export/` is therefore still honest about which of its fields is a measurement — which is the property the derived/authored split exists to protect — while a human opening the tree sees `main_menu.json` rather than having to resolve a rename in their head. A build nobody has identified exports as `build_NN` with `name_source: "index"`, which is a locator and not a claim. This is the **only** authored input the exporter takes. Everything else in `authored/` is applied by the runtime over `export/`. ### Sprites are per screen, not a flat pool `main_menu` and `extras` both ship a `ptbase.t32` and they are different pictures. A flat `sprites/` directory would have silently collided; whichever screen exported second would have won, and the loser would have drawn the wrong background with no error anywhere. `sprites///.png`. ### The format is executable `sylpheed-export check --out export` validates a tree against `docs/FORMAT.md` with no disc in hand. It exists because "the export is correct" is otherwise an assertion, and because the P0 gate is *"validates against FORMAT.md"* — which is not a thing anyone can confirm by reading. It reads the tree the way Godot will: as a stranger, with no access to the disc, the decoders, or the exporter's internals. It deliberately does **not** check the export against the disc — that is what `sylpheed-cli screen render` is for, at P1. Checked that it bites, rather than assuming: five mutations of a valid `main_menu.json` — a broken `paint_order` permutation, a dangling `focus_sprite`, a reversed `buttons` list, a `#rrggbbaa` colour, an invented `name_source` — are each caught with a specific message. ### The highlight sprite pairs by name; `opt ` is exported but not believed FORMAT v1 said `focus_sprite` came from the element's `opt ` link. That reading was **measured and refuted** by the RE agent, and this export shows why plainly: on the main menu, `opt ` chains `ptloop01 → ptloop02 → ptbtn01` — two decorations and then a button. It is a linked list of something, and it is not focus. The highlight is paired by **sprite name** instead (`ptbtn01.t32` ↔ `ptbtn01f.t32`), which is HANDOFF's convention and holds for all 54 real pairs on the disc. It resolves all five main-menu buttons. The raw link is still exported as `opt_link`, renamed so that nothing downstream mistakes it for navigation, and so that whoever eventually decodes it has the data. Note this is 🟡 a naming convention, not a decoded field. It is authored in effect, and lives in the exporter only because it is a rule over disc data rather than a value we chose. ### The paint order is exported, not authored Q3 decoded it — a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index. So it is read in the exporter, per the contract's own rule for a decoded answer, and `paint_order` in `export/` is a derived field. `"paint_order"` is gone from `unresolved`; **`paint_order_ties` replaces it**, because the tie-break is still unknown and costs one element's blend on one screen. Where an element has no `T8aD` header the key comes from the decoders' table of keys **measured off the running game**. That is a different kind of fact, so it is labelled: `layer_source` is `"sprite"`, `"implied"` or `"none"`, and a consumer that needs to know whether a layer is read or measured can tell. ### Colours are exported as two fields with the byte order in the name There are two modulate colours and they multiply: `tint` is RGBA, `fade` is **ARGB** and its high byte is the alpha that ramps. v1's single `"#ffffffff"` could not carry both and silently discarded the ramping alpha. They are exported as `tint_rgba` and `fade_argb`, raw hex, byte order in the key — because getting it backwards is silent and looks like an art bug rather than a parse bug. ### `t` stays raw HANDOFF Q1 is answered — linear ramp, 2 units per rendered frame, working conversion 1 unit = 1/60 s — but that conversion is **measured off the running game, not read from the file**, and the finding itself flags the 27.6 present- frames/second measurement as the part worth re-testing. If the game turns out to present at 60 Hz, every duration halves. So `t` is exported exactly as the disc spells it, `keyframe_time_unit` stays in `unresolved`, and the conversion will live in one authored place at P2. One constant to change, in a file that says it is a decision. ### The final keyframe has no `t`, and `check` enforces that The disc has no time slot on the last keyframe of a group. A file that carries one there has invented it. `check` rejects it — this is the one place where the temptation to emit a plausible number is strongest and the resulting error is completely invisible.