The doc comment beside `a1`..`a5` promised that an out-of-range value marks
THAT entry skipped and never the batch, while the fields sat in the batch
schema and answered 422 for all five hundred. The words were right and the
code was not, and a phone draining a year of local history lost the year to
one row.
The five now parse per entry, the same way the day context does; a bad row
reports `invalid_dimensions` and the rest of the batch lands.
The split buys the tolerance and costs a second copy of "0 to 10", so a guard
offers the same value to both schemas and makes them agree at both ends of the
scale and one step outside each.
- /api/sync/changes carries a1 to a5 and the day context under the same keys
the write path accepts, so a client reads back exactly what it can post
- BulkMoodEntry accepts both, all optional; a1 stays server-derived from the
five-point label, so a build that sends only a label still writes a complete
row and needs no change to stay correct
- the batch parses each entry's context on its own: one row carrying a value
the vocabulary does not name is skipped and the rest of the batch lands
- docs/api/openapi.yaml regenerated alongside
- moodContextSchema derives every enum from the vocabulary, so a value it does
not name answers 422 through returnAllZodIssues rather than being dropped
- the context is written in the same transaction as the entry and its links; a
failure rolls the entry back rather than leaving a client retrying a 5xx
into a duplicate
- an entry with nothing filled in stores no row, and a context that says
nothing removes the one that was there
- the create, list, detail and edit reads answer the context with its lists
decoded and its note decrypted; the ciphertext never leaves the server