`corpus_items.mission_id` has existed since the table landed and nothing
could populate it. `POST /api/library/runs` now accepts `missionId`, which
is the seam the wizard needs: a mission-driven run is the same run, tagged.
`corpus::contributed()` answers the question a continuous mission has to
be able to answer — did THIS run add anything new. Because `record` never
reassigns mission_id on conflict, the mission that first found a source
keeps the credit, so a rerun cannot inflate its own count by re-recording
what an earlier run already held. The test asserts exactly that: two
missions see the same paper, the finder reports 1 and the rerun reports 0.
This is the check the 0030-0044 generation of continuous research did not
have. It could run weekly forever and every run looked like success.
The test also earned its FK: the first version attributed to a bare UUID
and the database refused it. Attribution to a mission that does not exist
is not attribution, so the test now seeds real mission rows.
400 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Closes the branch pile-up: a catalogue branch that only adds notes now
merges into main by itself, so the work is actually in the vault rather
than waiting in a branch nobody opened.
Additive-only is measured from the diff, not assumed from the mission
type. Three conditions, all required: the type declares additive_only,
the run verified, and `git diff --name-status base...branch` contains
only A entries. A research harvest that somehow rewrote a hand-written
note is refused by the same check that lets its new notes through —
which is the case the test pins down, asserting README.md on main is
byte-identical afterwards.
Renames and deletes count as non-additive. A rename is a delete plus an
add and the delete half can destroy hand-written work.
Unknown merge_policy values fail closed to Never. A typo must not grant
auto-merge.
The diff is taken against FETCH_HEAD, freshly fetched, using `...` so an
unrelated commit landing on main meanwhile is not misread as ours. A
conflicted merge aborts and leaves the branch for a human rather than
wedging the checkout for the next run.
merge_reason is always populated and surfaced in the API: a branch that
quietly did not merge is indistinguishable from one never delivered.
399 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
POST /api/library/runs harvests now; GET /api/library/items lists what
the library holds. Thin wrappers — the work stays in crate::library — so
a run can be started by a person, a schedule or the UI rather than only
from an integration test.
The response reports `healthy` explicitly rather than leaving a caller to
infer it from an empty `shelved` list. A quiet week and a broken run both
shelve zero papers, and collapsing those two is the exact ambiguity that
cost most of this week.
Failure reasons go to the log, not the response body: they can carry the
remote URL and raw git stderr.
AppState gains an optional blob store (the shelf), wired from the server
binary where storage is already constructed. Optional because AppState::new
is used by tests that never touch blobs; a route that needs it fails
loudly rather than the constructor demanding it everywhere.
393 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>