feat(library): attribute a run to a mission, and prove what it contributed
`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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9de2cf34e4
commit
ad89ef94cd
@@ -291,6 +291,35 @@ pub async fn unseen(
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many NEW sources a mission contributed.
|
||||||
|
///
|
||||||
|
/// The verification predicate for a continuous research mission. `record`
|
||||||
|
/// never reassigns `mission_id` on conflict, so the first mission to find a
|
||||||
|
/// source keeps the credit and a rerun cannot inflate its own count by
|
||||||
|
/// re-recording what an earlier run already had.
|
||||||
|
///
|
||||||
|
/// A mission whose answer is zero produced nothing, whatever its transcript
|
||||||
|
/// says — which is the check the 0030-0044 generation of this feature lacked.
|
||||||
|
pub async fn contributed(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
corpus_id: &str,
|
||||||
|
mission_id: Uuid,
|
||||||
|
) -> Result<i64, String> {
|
||||||
|
let row: (i64,) = sqlx::query_as(
|
||||||
|
"SELECT count(*) FROM corpus_items
|
||||||
|
WHERE workspace_id = $1 AND corpus_id = $2 AND mission_id = $3
|
||||||
|
AND kind = 'source'",
|
||||||
|
)
|
||||||
|
.bind(workspace_id)
|
||||||
|
.bind(corpus_id)
|
||||||
|
.bind(mission_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("contributed({mission_id}): {e}"))?;
|
||||||
|
Ok(row.0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Index every note in a checkout. Idempotent by construction.
|
/// Index every note in a checkout. Idempotent by construction.
|
||||||
pub async fn index_vault(
|
pub async fn index_vault(
|
||||||
pool: &sqlx::PgPool,
|
pool: &sqlx::PgPool,
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ pub struct RunRequest {
|
|||||||
/// library can otherwise pull hundreds of PDFs in one go.
|
/// library can otherwise pull hundreds of PDFs in one go.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub per_topic: Option<usize>,
|
pub per_topic: Option<usize>,
|
||||||
|
/// Attribute this run to a mission, so the mission can later be asked
|
||||||
|
/// what it contributed. `corpus_items.mission_id` has existed since the
|
||||||
|
/// table landed; without this field nothing could ever populate it.
|
||||||
|
#[serde(default, rename = "missionId")]
|
||||||
|
pub mission_id: Option<uuid::Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -80,7 +85,7 @@ pub async fn run(
|
|||||||
&work_root,
|
&work_root,
|
||||||
&topics,
|
&topics,
|
||||||
per_topic,
|
per_topic,
|
||||||
None,
|
req.mission_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|||||||
@@ -19,6 +19,24 @@ async fn workspace(pool: &sqlx::PgPool) -> Uuid {
|
|||||||
ws
|
ws
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A real mission row. `corpus_items.mission_id` has a foreign key, which is
|
||||||
|
/// deliberate: attribution to a mission that does not exist is not
|
||||||
|
/// attribution. The first version of the test below used a bare UUID and was
|
||||||
|
/// correctly rejected.
|
||||||
|
async fn mission(pool: &sqlx::PgPool, ws: Uuid) -> Uuid {
|
||||||
|
let id = Uuid::now_v7();
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO missions (id, workspace_id, title, template_kind, schedule, status, config)
|
||||||
|
VALUES ($1,$2,'library','research_only','{}'::jsonb,'running','{}'::jsonb)",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.bind(ws)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
fn seed_vault(root: &std::path::Path) {
|
fn seed_vault(root: &std::path::Path) {
|
||||||
std::fs::create_dir_all(root.join("50 APESS 2026/Lectures")).unwrap();
|
std::fs::create_dir_all(root.join("50 APESS 2026/Lectures")).unwrap();
|
||||||
std::fs::create_dir_all(root.join("Repos")).unwrap();
|
std::fs::create_dir_all(root.join("Repos")).unwrap();
|
||||||
@@ -226,3 +244,43 @@ async fn live_arxiv_search_and_fetch() {
|
|||||||
assert!(pdf.starts_with(b"%PDF"));
|
assert!(pdf.starts_with(b"%PDF"));
|
||||||
assert!(pdf.len() > 10_000, "suspiciously small pdf: {}", pdf.len());
|
assert!(pdf.len() > 10_000, "suspiciously small pdf: {}", pdf.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A rerun must not be able to claim credit for work an earlier run did.
|
||||||
|
///
|
||||||
|
/// This is the verification predicate for a continuous mission: "did THIS run
|
||||||
|
/// contribute anything new". If a rerun could re-record an existing source
|
||||||
|
/// under its own mission id, every run would report success forever — the
|
||||||
|
/// failure that killed the 0030-0044 generation of this feature.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_rerun_cannot_claim_an_earlier_missions_work() {
|
||||||
|
let pool = cm_testkit::test_pool().await;
|
||||||
|
let ws = workspace(&pool).await;
|
||||||
|
let first_mission = mission(&pool, ws).await;
|
||||||
|
let second_mission = mission(&pool, ws).await;
|
||||||
|
|
||||||
|
corpus::record(
|
||||||
|
&pool, ws, "lib", "source", "arxiv:2401.55555",
|
||||||
|
Some("Paper"), None, None, "h1", Some(first_mission),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The second mission sees the same paper and re-records it.
|
||||||
|
corpus::record(
|
||||||
|
&pool, ws, "lib", "source", "arxiv:2401.55555",
|
||||||
|
Some("Paper"), None, None, "h2", Some(second_mission),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
corpus::contributed(&pool, ws, "lib", first_mission).await.unwrap(),
|
||||||
|
1,
|
||||||
|
"the finder keeps the credit"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
corpus::contributed(&pool, ws, "lib", second_mission).await.unwrap(),
|
||||||
|
0,
|
||||||
|
"a rerun that found nothing new must report zero, not one"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user