Phase 4 of docs/chapter-uploads.md: the feature.
A review step rather than a submit button, and that is the one place this
departs visibly from the web app it mirrors. mantra.press can post twelve
files and redirect, because what it creates belongs to the uploader and the
uploader can fix it. Here each file becomes a request for the group's own
signature, so a filename standing in for a title, a file that decoded to
nothing, or a chapter too long to carry is wrong in something a quorum is
about to put its name to. Picking files produces a list; the list is what is
proposed.
Per-row problems, because a bad file should not cost the other eleven: not
text, too large, nothing to translate, no name, more chunks than a session
signs, more bytes than a proposal carries -- and a duplicate, which warns
rather than blocks. A book can repeat a page, but the common cause is picking
the same directory twice, and two chapters with the same text are two event
ids nothing later can merge.
Proposals go out sequentially, never concurrently: each writes a session and
publishes group events, and a group event that arrives a moment out of order
is dropped for good (docs/mls-skipped-keys.md). Indices are allocated once
from a freshly read base, so numbering does not depend on how fast the
sessions are written.
Partial success is the expected outcome and not an error. Nothing can retract
a proposal, so if the fourth fails the first three are real and the group can
still sign them: no rollback, no navigating away, the rows that went are
marked and the rest keep their reason, which is the only place a retry can be
aimed from. A pass that fully succeeds lands on the proposal list rather than
a signing screen -- at N there is no one session to watch.
Two shapes the writing settled. UploadReview holds the row building and the
problem decisions, which are pure functions of the files, the names typed
over them and the chapters the version already has; the view model keeps only
the orchestration, and the 13 tests need no view-model harness. And uiState
is a StateFlow, not `by mutableStateOf`: initiate() writes from
Dispatchers.IO after a primary-key lookup, which is exactly the few
milliseconds in which a write from another thread into a composition's
snapshot is silently discarded -- the bug that once left this app on
"Decrypting..." for ever.
Scaffolding into translations is not here yet; Phase 5 lifts it out of
AddChapterViewModel so one batch covers every chapter rather than one session
each. Until then an uploaded chapter reaches a translation through that
translation's own catch-up.
m3Audit's budgets all met.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>