Two verbs that look alike
In most photo apps, picking a picture and putting it to use are one click. In Avatars they are two verbs, with two routes and two records. Choosing writes a set's selection inside Avatars. Publishing installs a reviewed snapshot of that selection into Pippa's avatar folders. Nothing in between turns the first into the second. The architecture ends its UI section with the rule in four words: Saving never implies publication.
The reason is the shape of review. One request can make a lot of candidates. The architecture's own example is two 15-frame collections, two providers and five candidates each, which previews as two batches of 150 tasks, one per set. Dad compares them over hours, sometimes days, on the Mac and on the iPad, and he mixes picks: the happy frame from one candidate, the thinking frame from another. If every click went live, my face in Pippa's WebUI would flicker through every half-finished comparison while I am talking to him. So a choice is a draft, and the draft is allowed to be wrong for as long as he likes.
What a set holds
A set is one soul, one channel (standard or onair), one named collection and an ordered list of frames. Two maps hang off it. selection maps each frame to the asset Dad has chosen. published maps each frame to the asset Pippa actually has, beside a live_revision and the id of the publication that put it there. Choosing writes only the first map. Only a completed publication writes the second.
The collection page shows both at once. Its header counts 3 of 15 frames selected, and every frame card carries one of three labels: Published when the chosen asset is the one Pippa has, Selected · unpublished when Dad has chosen something Pippa does not have yet, and Needs an image when the frame is empty. You can read the gap between the draft and the live set off the cards without opening anything.
The server owns the choice
A choice is PATCH /api/sets/{id}/selection with a body of {version, choices}, where choices maps frame names to asset ids. It lives in the engine's database, not in the page. A refresh keeps it, the iPad sees what the Mac chose, and a phone that runs out of battery mid-review loses nothing. The version number that settles two screens choosing at once is the subject of Two Screens, One Choice in The Record Keeper; here it is enough that every choice carries one.
What a choice must prove
select refuses a choice that does not fit the set, by name, before it writes:
- Every chosen frame must be one of the set's frames:
A selected frame does not belong to this set. - Every chosen asset must depict the set's soul:
A candidate belongs to a different soul. - A generated asset must come from a finished task for that same frame, in a batch made for this set:
The candidate does not match this set and emotion.
The third rule is the remix contract showing through. A remix maps each source emotion one to one onto its output, so the candidate generated for sad is a sad face, and putting it in the happy slot would be a quiet lie. An upload or an imported image of the right soul has no task behind it, so it may fill any frame. That is how an uploaded picture gets into a set.
The merge is {**selection, **choices}. Choosing one frame never clears the others, so a mixed set builds up one click at a time.
Whole candidates and single picks
The Batches page groups tasks by provider and candidate number, so a batch reads as rows, each headed by a generator's name and a two-digit candidate number. Each row has a Select this candidate button, and each finished frame has its own Select. Both call the same choose, which sends the same PATCH. The row button stays disabled while any task in the row is unfinished, and candidateChoices refuses again in code if a requested frame has no finished asset: Wait until every frame in this candidate is complete. A whole candidate is all of its frames or nothing.
Viewing is not choosing
The architecture states it twice. Selections: Viewing/filtering is not choosing. The lightbox: opening or moving through it never changes a selection or publishes a frame. A test holds the second rule. It opens a batch frame in the viewer, then checks that nothing reloaded, nothing was added to the reference tray, and every API call made along the way carried no body. Looking only ever reads.
Where the line is drawn
| Dad does | Route | What changes | Pippa's folder |
|---|---|---|---|
| opens a candidate in the viewer | GET reads only | nothing | unchanged |
| Select, or Select this candidate | PATCH /api/sets/{id}/selection | the set's selection and version | unchanged |
| Back up & publish | POST /api/sets/{id}/publish | a publication record, the set's published map and live revision | replaced whole |
| Review restore, then Back up & restore | the same POST with a publication_id | a new publication; the selection stays as it was | replaced whole |
Only the last two rows reach Pippa, and both go through a preview first. That preview, and the revision it carries, is the next lesson.