From 63a0905cd2fd5faee5409598bd15ddc9f3a1b3e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:57:00 +0900 Subject: [PATCH 01/17] test(zotero): require worksheet finalization contract --- .../tests/steward_review_finalization.rs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 crates/conceptweave-zotero/tests/steward_review_finalization.rs diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs new file mode 100644 index 00000000..923596ad --- /dev/null +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -0,0 +1,102 @@ +use conceptweave_zotero::{ + Disposition, EvaluationError, GoldenSetApproval, ItemData, ZoteroItem, + build_steward_review_worksheet, classify_snapshot, reviewed_golden_set_from_worksheet, +}; + +fn report() -> conceptweave_zotero::ClassificationReport { + classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![ + ZoteroItem { + key: "B".into(), + version: 8, + data: ItemData { + item_type: "book".into(), + title: "unknown vocabulary".into(), + abstract_note: String::new(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + }, + ZoteroItem { + key: "A".into(), + version: 7, + data: ItemData { + item_type: "book".into(), + title: "ontology learning".into(), + abstract_note: String::new(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + }, + ], + ) +} + +fn approval( + worksheet: &conceptweave_zotero::StewardReviewWorksheet, +) -> GoldenSetApproval { + GoldenSetApproval { + receipt_id: "review-receipt".into(), + reviewer_subject: "steward-subject".into(), + library_version: worksheet.library_version, + rule_revision: worksheet.rule_revision.clone(), + snapshot_digest: worksheet.snapshot_digest.clone(), + snapshot_items: worksheet.snapshot_items.clone(), + } +} + +#[test] +fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { + let mut worksheet = build_steward_review_worksheet(&report()).unwrap(); + for decision in &mut worksheet.decisions { + decision.reviewed_disposition = Some(Disposition::Generation); + } + + let golden = reviewed_golden_set_from_worksheet(&worksheet, approval(&worksheet)).unwrap(); + + assert_eq!(golden.labels.len(), 2); + assert_eq!(golden.labels[0].item_key, "A"); + assert_eq!(golden.labels[1].item_key, "B"); + assert_eq!(golden.approval.snapshot_items, worksheet.snapshot_items); +} + +#[test] +fn finalization_rejects_incomplete_invalid_or_mismatched_review() { + let worksheet = build_steward_review_worksheet(&report()).unwrap(); + assert_eq!( + reviewed_golden_set_from_worksheet(&worksheet, approval(&worksheet)), + Err(EvaluationError::IncompleteReview) + ); + + let mut invalid = worksheet.clone(); + invalid.decisions[0].reviewed_disposition = Some(Disposition::NeedsStewardReview); + invalid.decisions[1].reviewed_disposition = Some(Disposition::Generation); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidExpectedDisposition) + ); + + let mut complete = worksheet.clone(); + for decision in &mut complete.decisions { + decision.reviewed_disposition = Some(Disposition::Generation); + } + let mut mismatched = approval(&complete); + mismatched.snapshot_digest.push_str("-changed"); + assert_eq!( + reviewed_golden_set_from_worksheet(&complete, mismatched), + Err(EvaluationError::SnapshotMismatch) + ); + + complete.decisions[1].item_key = complete.decisions[0].item_key.clone(); + assert_eq!( + reviewed_golden_set_from_worksheet(&complete, approval(&complete)), + Err(EvaluationError::DuplicateItem) + ); +} From be54e98abfe6e71a4b57f5d7b2761db9c93bb348 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:57:25 +0900 Subject: [PATCH 02/17] feat(zotero): finalize reviewed steward worksheets --- crates/conceptweave-zotero/src/lib.rs | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 03a63b81..fbc5c654 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1213,6 +1213,71 @@ pub struct GoldenSetApproval { pub snapshot_items: Vec, } +/// Converts a fully decided local worksheet into the input for approval verification. +pub fn reviewed_golden_set_from_worksheet( + worksheet: &StewardReviewWorksheet, + approval: GoldenSetApproval, +) -> Result { + if approval.receipt_id.trim().is_empty() + || approval.reviewer_subject.trim().is_empty() + || worksheet.rule_revision.trim().is_empty() + || worksheet.snapshot_digest.trim().is_empty() + { + return Err(EvaluationError::InvalidReview); + } + if approval.library_version != worksheet.library_version + || approval.rule_revision != worksheet.rule_revision + || approval.snapshot_digest != worksheet.snapshot_digest + || approval.snapshot_items != worksheet.snapshot_items + { + return Err(EvaluationError::SnapshotMismatch); + } + + let mut snapshot_versions = BTreeMap::new(); + for item in &worksheet.snapshot_items { + if item.item_key.trim().is_empty() + || snapshot_versions + .insert(item.item_key.as_str(), item.item_version) + .is_some() + { + return Err(EvaluationError::InvalidReview); + } + } + + let mut decision_keys = BTreeSet::new(); + let mut labels = Vec::with_capacity(worksheet.decisions.len()); + for decision in &worksheet.decisions { + if decision.item_key.trim().is_empty() + || snapshot_versions.get(decision.item_key.as_str()) != Some(&decision.item_version) + { + return Err(EvaluationError::InvalidReview); + } + if !decision_keys.insert(decision.item_key.as_str()) { + return Err(EvaluationError::DuplicateItem); + } + if (decision.proposed_disposition == Disposition::NeedsStewardReview) + != decision.abstention_reason.is_some() + { + return Err(EvaluationError::InvalidReview); + } + let expected_disposition = decision + .reviewed_disposition + .ok_or(EvaluationError::IncompleteReview)?; + if expected_disposition == Disposition::NeedsStewardReview { + return Err(EvaluationError::InvalidExpectedDisposition); + } + labels.push(GoldenLabel::new( + decision.item_key.clone(), + expected_disposition, + )); + } + if labels.is_empty() { + return Err(EvaluationError::IncompleteReview); + } + + Ok(ReviewedGoldenSet { approval, labels }) +} + /// Computes the canonical content identity verified by a golden-set approval. pub fn classification_snapshot_digest(report: &ClassificationReport) -> String { report.snapshot_digest.clone() From 9992dfa4400798859977a13ebb25cb23c4839478 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:57:38 +0900 Subject: [PATCH 03/17] test(zotero): preserve duplicate decision coordinates --- crates/conceptweave-zotero/tests/steward_review_finalization.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index 923596ad..adc93106 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -95,6 +95,7 @@ fn finalization_rejects_incomplete_invalid_or_mismatched_review() { ); complete.decisions[1].item_key = complete.decisions[0].item_key.clone(); + complete.decisions[1].item_version = complete.decisions[0].item_version; assert_eq!( reviewed_golden_set_from_worksheet(&complete, approval(&complete)), Err(EvaluationError::DuplicateItem) From f3d076e8f789d577afbd82756a7a631e25b0a1ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:58:00 +0900 Subject: [PATCH 04/17] docs(zotero): define worksheet finalization boundary --- CHANGELOG.md | 1 + docs/PRD.md | 1 + docs/TRD.md | 1 + docs/product-technical-gap-baseline.md | 2 ++ 4 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be1e970..1d7ae9e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to ConceptWeave are documented here. - A complete-review evaluator that rejects partial steward labels as full reclassification evidence. - A snapshot-bound steward worksheet with one blank decision per bibliographic item and no duplicated bibliographic text. - An explicit `--worksheet` CLI mode that writes the live worksheet with owner-only report protections. +- Fail-closed conversion from a fully decided worksheet to the existing externally verified golden-set boundary. ### Security diff --git a/docs/PRD.md b/docs/PRD.md index 6c102e85..62884c9f 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -68,6 +68,7 @@ The Zotero 10+ adapter can accept a caller-owned API key and server identity at Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and bound to the canonical SHA-256 digest of the complete Zotero classification report plus its item-key/item-version coordinates. Abstention is a prediction outcome, never an approved truth label. Evaluation emits the verified library revision, rule revision, opaque snapshot digest, and aggregate counts for exact matches, abstentions, and per-disposition true-positive/predicted/expected totals; it must not copy Zotero keys, reviewer identity, or bibliographic text into the result. A full-reclassification completion result additionally requires exactly one non-abstention steward label for every top-level bibliographic item; a sampled golden set remains valid for quality measurement but cannot prove completion. A local steward worksheet must bind the library version, rule revision, complete raw-snapshot digest, every observed parent/child item revision, and one editable decision slot per classified bibliographic item. It repeats item identity, proposal, and abstention reason only; titles, abstracts, tags, collections, and matched evidence remain in the separate sensitive report. Invalid or duplicate report identity cannot produce a worksheet. +After every decision is filled, worksheet finalization must verify the governance receipt coordinates, unique item identities and revisions, proposal/abstention consistency, and non-abstention truth labels before producing a reviewed golden set. Missing decisions remain incomplete and cannot reach external approval verification. Every successful classification report includes aggregate evidence for snapshot coverage, proposal coverage, provenance completeness, abstentions, duplicate candidates, disposition totals, and zero unreported failures. diff --git a/docs/TRD.md b/docs/TRD.md index 8727a212..463521a9 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -69,6 +69,7 @@ Golden-set evaluation accepts only a governance receipt verified with the comple A successful classification report carries an `audit_summary` whose snapshot, bibliographic, proposed-disposition, provenance-complete, abstention, duplicate-candidate, failure, and per-disposition counts are derived from the same in-memory immutable snapshot. Zotero item version zero remains a valid observed coordinate for never-synced Zotero 9 records; provenance completeness rejects a missing item key rather than inventing a positive-only version invariant. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. The review worksheet is a deterministic item-key-ordered projection of the report. `conceptweave-zotero --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json` reads one live snapshot and creates both owner-only outputs as a bound pair; failed output removes partial files so the same paths can be retried. It binds the library version, rule revision, raw-snapshot digest, complete parent/child item coordinates, item proposal, abstention reason, and an initially empty decision for every bibliographic item. Construction rejects blank or duplicate snapshot identity, mismatched item revisions, and inconsistent observed, bibliographic, proposal, provenance, abstention, duplicate, failure, or disposition counts. It deliberately omits bibliographic text and matched evidence; stewards consult the paired owner-only report by item key. +The finalization function consumes the filled worksheet plus a governance approval receipt. It rejects blank authority metadata, coordinate drift, duplicate or unknown decision keys, missing decisions, abstention as approved truth, and tampered proposal/reason pairs. Its output reuses the existing reviewed-golden-set evaluator; finalization itself does not verify external authority. The local report can contain titles, tags, matched metadata, and abstention abstracts. It is sensitive steward-review material, remains outside the repository, and is not a publication artifact. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 3e92da7e..c7dc359f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -52,6 +52,8 @@ The golden-set evaluation contract now records aggregate precision/recall numera The steward workload now has a deterministic local worksheet contract rather than an informal report-editing step. The existing CLI exports the report and worksheet from one live snapshot with `--worksheet`, preserving the owner-only file boundary and deleting incomplete output on failure. It binds library/rule/digest plus all parent and child revisions, emits one blank decision for each of the 3,715 bibliographic items in item-key order, and repeats only the proposal and abstention reason. Titles, abstracts, tags, collections, and matched evidence remain in the paired owner-only report. This stays inside ConceptWeave because no independent cross-product review utility contract exists. +Filled worksheets now have a fail-closed conversion into the existing reviewed golden-set contract. The conversion checks exact approval coordinates, complete unique decision identity, snapshot membership and item revision, proposal/abstention consistency, and rejects missing or abstention truth labels before the external authority verifier can run. The live completion KPI remains 0/3,715 until real steward decisions and an externally verified approval receipt exist. + The completion KPI is now executable: a sampled golden set may measure classifier quality, but it cannot prove that the library was fully reclassified. The full-review boundary returns aggregate completion evidence only when externally verified, snapshot-bound, non-abstention labels cover every unique classified bibliographic item exactly once. The current live baseline therefore remains incomplete at 0/3,715 approved labels rather than treating 57 deterministic proposals or a future sample as steward truth. 1. **Concrete Source Observation adapter** — maintained Rust PostgreSQL driver behind `conceptweave-source-port`; adapter-local credential resolution; explicit read-only mode; statement timeout, cancellation, row/byte/concurrency budgets; complete immutable snapshot or fail closed; deterministic replay against a frozen anonymized GRC-shaped fixture. From 03ced904abee1c182123b0666331ab7f1b1282b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:58:14 +0900 Subject: [PATCH 05/17] style(zotero): format finalization tests --- .../conceptweave-zotero/tests/steward_review_finalization.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index adc93106..79f7dba1 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -39,9 +39,7 @@ fn report() -> conceptweave_zotero::ClassificationReport { ) } -fn approval( - worksheet: &conceptweave_zotero::StewardReviewWorksheet, -) -> GoldenSetApproval { +fn approval(worksheet: &conceptweave_zotero::StewardReviewWorksheet) -> GoldenSetApproval { GoldenSetApproval { receipt_id: "review-receipt".into(), reviewer_subject: "steward-subject".into(), From fbbba10b0a4609b82548d55b25d1e3523d44fd59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:59:25 +0900 Subject: [PATCH 06/17] test(zotero): cover finalization trust boundary --- .../tests/steward_review_finalization.rs | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index 79f7dba1..6a2714a4 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -50,12 +50,17 @@ fn approval(worksheet: &conceptweave_zotero::StewardReviewWorksheet) -> GoldenSe } } -#[test] -fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { +fn complete_worksheet() -> conceptweave_zotero::StewardReviewWorksheet { let mut worksheet = build_steward_review_worksheet(&report()).unwrap(); for decision in &mut worksheet.decisions { decision.reviewed_disposition = Some(Disposition::Generation); } + worksheet +} + +#[test] +fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { + let worksheet = complete_worksheet(); let golden = reviewed_golden_set_from_worksheet(&worksheet, approval(&worksheet)).unwrap(); @@ -65,6 +70,103 @@ fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { assert_eq!(golden.approval.snapshot_items, worksheet.snapshot_items); } +#[test] +fn finalization_rejects_each_invalid_identity_coordinate() { + let worksheet = complete_worksheet(); + + let mut invalid_approval = approval(&worksheet); + invalid_approval.receipt_id.clear(); + assert_eq!( + reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + Err(EvaluationError::InvalidReview) + ); + let mut invalid_approval = approval(&worksheet); + invalid_approval.reviewer_subject.clear(); + assert_eq!( + reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + Err(EvaluationError::InvalidReview) + ); + + let mut invalid = worksheet.clone(); + invalid.rule_revision.clear(); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + let mut invalid = worksheet.clone(); + invalid.snapshot_digest.clear(); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + + let mut invalid_approval = approval(&worksheet); + invalid_approval.library_version += 1; + assert_eq!( + reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + Err(EvaluationError::SnapshotMismatch) + ); + let mut invalid_approval = approval(&worksheet); + invalid_approval.rule_revision.push_str("-changed"); + assert_eq!( + reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + Err(EvaluationError::SnapshotMismatch) + ); + let mut invalid_approval = approval(&worksheet); + invalid_approval.snapshot_items.pop(); + assert_eq!( + reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + Err(EvaluationError::SnapshotMismatch) + ); + + let mut invalid = worksheet.clone(); + invalid.snapshot_items[0].item_key.clear(); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + let mut invalid = worksheet.clone(); + invalid.snapshot_items[1] = invalid.snapshot_items[0].clone(); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + + let mut invalid = worksheet.clone(); + invalid.decisions[0].item_key.clear(); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + let mut invalid = worksheet.clone(); + invalid.decisions[0].item_version += 1; + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + + let mut invalid = worksheet.clone(); + invalid.decisions[0].abstention_reason = + Some(conceptweave_zotero::AbstentionReason::NoDeterministicRuleMatch); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + let mut invalid = worksheet.clone(); + invalid.decisions[1].abstention_reason = None; + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); + + let mut invalid = worksheet.clone(); + invalid.decisions.clear(); + assert_eq!( + reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + Err(EvaluationError::IncompleteReview) + ); +} + #[test] fn finalization_rejects_incomplete_invalid_or_mismatched_review() { let worksheet = build_steward_review_worksheet(&report()).unwrap(); From 0f709e62665fc7a42e3e5f54373d69b72c20b46b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:01:47 +0900 Subject: [PATCH 07/17] fix(zotero): bind finalization to classification report --- crates/conceptweave-zotero/src/lib.rs | 38 +++++++-------- .../tests/steward_review_finalization.rs | 46 +++++++++---------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index fbc5c654..142d1b87 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1215,9 +1215,14 @@ pub struct GoldenSetApproval { /// Converts a fully decided local worksheet into the input for approval verification. pub fn reviewed_golden_set_from_worksheet( + report: &ClassificationReport, worksheet: &StewardReviewWorksheet, approval: GoldenSetApproval, ) -> Result { + let expected = build_steward_review_worksheet(report).map_err(|_| EvaluationError::InvalidReview)?; + if worksheet.decisions.len() != expected.decisions.len() { + return Err(EvaluationError::IncompleteReview); + } if approval.receipt_id.trim().is_empty() || approval.reviewer_subject.trim().is_empty() || worksheet.rule_revision.trim().is_empty() @@ -1232,31 +1237,20 @@ pub fn reviewed_golden_set_from_worksheet( { return Err(EvaluationError::SnapshotMismatch); } - - let mut snapshot_versions = BTreeMap::new(); - for item in &worksheet.snapshot_items { - if item.item_key.trim().is_empty() - || snapshot_versions - .insert(item.item_key.as_str(), item.item_version) - .is_some() - { - return Err(EvaluationError::InvalidReview); - } + if worksheet.library_version != expected.library_version + || worksheet.rule_revision != expected.rule_revision + || worksheet.snapshot_digest != expected.snapshot_digest + || worksheet.snapshot_items != expected.snapshot_items + { + return Err(EvaluationError::SnapshotMismatch); } - let mut decision_keys = BTreeSet::new(); let mut labels = Vec::with_capacity(worksheet.decisions.len()); - for decision in &worksheet.decisions { - if decision.item_key.trim().is_empty() - || snapshot_versions.get(decision.item_key.as_str()) != Some(&decision.item_version) - { - return Err(EvaluationError::InvalidReview); - } - if !decision_keys.insert(decision.item_key.as_str()) { - return Err(EvaluationError::DuplicateItem); - } - if (decision.proposed_disposition == Disposition::NeedsStewardReview) - != decision.abstention_reason.is_some() + for (decision, expected_decision) in worksheet.decisions.iter().zip(expected.decisions) { + if decision.item_key != expected_decision.item_key + || decision.item_version != expected_decision.item_version + || decision.proposed_disposition != expected_decision.proposed_disposition + || decision.abstention_reason != expected_decision.abstention_reason { return Err(EvaluationError::InvalidReview); } diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index 6a2714a4..2faf5f37 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -62,7 +62,7 @@ fn complete_worksheet() -> conceptweave_zotero::StewardReviewWorksheet { fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { let worksheet = complete_worksheet(); - let golden = reviewed_golden_set_from_worksheet(&worksheet, approval(&worksheet)).unwrap(); + let golden = reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&worksheet)).unwrap(); assert_eq!(golden.labels.len(), 2); assert_eq!(golden.labels[0].item_key, "A"); @@ -77,71 +77,71 @@ fn finalization_rejects_each_invalid_identity_coordinate() { let mut invalid_approval = approval(&worksheet); invalid_approval.receipt_id.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::InvalidReview) ); let mut invalid_approval = approval(&worksheet); invalid_approval.reviewer_subject.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.rule_revision.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.snapshot_digest.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid_approval = approval(&worksheet); invalid_approval.library_version += 1; assert_eq!( - reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::SnapshotMismatch) ); let mut invalid_approval = approval(&worksheet); invalid_approval.rule_revision.push_str("-changed"); assert_eq!( - reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::SnapshotMismatch) ); let mut invalid_approval = approval(&worksheet); invalid_approval.snapshot_items.pop(); assert_eq!( - reviewed_golden_set_from_worksheet(&worksheet, invalid_approval), + reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.snapshot_items[0].item_key.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), - Err(EvaluationError::InvalidReview) + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.snapshot_items[1] = invalid.snapshot_items[0].clone(); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), - Err(EvaluationError::InvalidReview) + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.decisions[0].item_key.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.decisions[0].item_version += 1; assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidReview) ); @@ -149,20 +149,20 @@ fn finalization_rejects_each_invalid_identity_coordinate() { invalid.decisions[0].abstention_reason = Some(conceptweave_zotero::AbstentionReason::NoDeterministicRuleMatch); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.decisions[1].abstention_reason = None; assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); - invalid.decisions.clear(); + invalid.decisions.pop(); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::IncompleteReview) ); } @@ -171,7 +171,7 @@ fn finalization_rejects_each_invalid_identity_coordinate() { fn finalization_rejects_incomplete_invalid_or_mismatched_review() { let worksheet = build_steward_review_worksheet(&report()).unwrap(); assert_eq!( - reviewed_golden_set_from_worksheet(&worksheet, approval(&worksheet)), + reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&worksheet)), Err(EvaluationError::IncompleteReview) ); @@ -179,7 +179,7 @@ fn finalization_rejects_incomplete_invalid_or_mismatched_review() { invalid.decisions[0].reviewed_disposition = Some(Disposition::NeedsStewardReview); invalid.decisions[1].reviewed_disposition = Some(Disposition::Generation); assert_eq!( - reviewed_golden_set_from_worksheet(&invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidExpectedDisposition) ); @@ -190,14 +190,14 @@ fn finalization_rejects_incomplete_invalid_or_mismatched_review() { let mut mismatched = approval(&complete); mismatched.snapshot_digest.push_str("-changed"); assert_eq!( - reviewed_golden_set_from_worksheet(&complete, mismatched), + reviewed_golden_set_from_worksheet(&report(), &complete, mismatched), Err(EvaluationError::SnapshotMismatch) ); complete.decisions[1].item_key = complete.decisions[0].item_key.clone(); complete.decisions[1].item_version = complete.decisions[0].item_version; assert_eq!( - reviewed_golden_set_from_worksheet(&complete, approval(&complete)), - Err(EvaluationError::DuplicateItem) + reviewed_golden_set_from_worksheet(&report(), &complete, approval(&complete)), + Err(EvaluationError::InvalidReview) ); } From 9b577ec7e28e0e64d0f91a92819226129dd1ef09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:02:02 +0900 Subject: [PATCH 08/17] style(zotero): format report-bound finalization --- crates/conceptweave-zotero/src/lib.rs | 3 ++- .../conceptweave-zotero/tests/steward_review_finalization.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 142d1b87..adb8d9d6 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1219,7 +1219,8 @@ pub fn reviewed_golden_set_from_worksheet( worksheet: &StewardReviewWorksheet, approval: GoldenSetApproval, ) -> Result { - let expected = build_steward_review_worksheet(report).map_err(|_| EvaluationError::InvalidReview)?; + let expected = + build_steward_review_worksheet(report).map_err(|_| EvaluationError::InvalidReview)?; if worksheet.decisions.len() != expected.decisions.len() { return Err(EvaluationError::IncompleteReview); } diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index 2faf5f37..e896b54c 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -62,7 +62,8 @@ fn complete_worksheet() -> conceptweave_zotero::StewardReviewWorksheet { fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { let worksheet = complete_worksheet(); - let golden = reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&worksheet)).unwrap(); + let golden = + reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&worksheet)).unwrap(); assert_eq!(golden.labels.len(), 2); assert_eq!(golden.labels[0].item_key, "A"); From b88dc92d09b5e081029dc39a8088bd6c235c9fc4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:03:08 +0900 Subject: [PATCH 09/17] test(zotero): cover report-bound finalization --- .../tests/steward_review_finalization.rs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index e896b54c..13c40dae 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -75,6 +75,17 @@ fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { fn finalization_rejects_each_invalid_identity_coordinate() { let worksheet = complete_worksheet(); + let mut invalid_report = report(); + invalid_report.rule_revision = ""; + assert_eq!( + reviewed_golden_set_from_worksheet( + &invalid_report, + &worksheet, + approval(&worksheet) + ), + Err(EvaluationError::InvalidReview) + ); + let mut invalid_approval = approval(&worksheet); invalid_approval.receipt_id.clear(); assert_eq!( @@ -133,6 +144,25 @@ fn finalization_rejects_each_invalid_identity_coordinate() { Err(EvaluationError::SnapshotMismatch) ); + let mut invalid = worksheet.clone(); + invalid.library_version += 1; + assert_eq!( + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + Err(EvaluationError::SnapshotMismatch) + ); + let mut invalid = worksheet.clone(); + invalid.rule_revision.push_str("-changed"); + assert_eq!( + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + Err(EvaluationError::SnapshotMismatch) + ); + let mut invalid = worksheet.clone(); + invalid.snapshot_digest.push_str("-changed"); + assert_eq!( + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + Err(EvaluationError::SnapshotMismatch) + ); + let mut invalid = worksheet.clone(); invalid.decisions[0].item_key.clear(); assert_eq!( @@ -145,6 +175,12 @@ fn finalization_rejects_each_invalid_identity_coordinate() { reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::InvalidReview) ); + let mut invalid = worksheet.clone(); + invalid.decisions[0].proposed_disposition = Disposition::AlignmentVersioning; + assert_eq!( + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + Err(EvaluationError::InvalidReview) + ); let mut invalid = worksheet.clone(); invalid.decisions[0].abstention_reason = @@ -166,6 +202,30 @@ fn finalization_rejects_each_invalid_identity_coordinate() { reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), Err(EvaluationError::IncompleteReview) ); + + let empty_report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![ZoteroItem { + key: "NOTE".into(), + version: 1, + data: ItemData { + item_type: "note".into(), + title: String::new(), + abstract_note: String::new(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + }], + ); + let empty = build_steward_review_worksheet(&empty_report).unwrap(); + assert_eq!( + reviewed_golden_set_from_worksheet(&empty_report, &empty, approval(&empty)), + Err(EvaluationError::IncompleteReview) + ); } #[test] From 7ca97b4104f0a61f09502455187382490f3f44c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:03:28 +0900 Subject: [PATCH 10/17] style(zotero): format finalization coverage --- .../tests/steward_review_finalization.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index 13c40dae..c368ca79 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -78,11 +78,7 @@ fn finalization_rejects_each_invalid_identity_coordinate() { let mut invalid_report = report(); invalid_report.rule_revision = ""; assert_eq!( - reviewed_golden_set_from_worksheet( - &invalid_report, - &worksheet, - approval(&worksheet) - ), + reviewed_golden_set_from_worksheet(&invalid_report, &worksheet, approval(&worksheet)), Err(EvaluationError::InvalidReview) ); From 4718073e36f80ea2dfe111232425d9f5127fb809 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:01:52 +0900 Subject: [PATCH 11/17] test(research): reproduce finalization proposal approval reuse Signed-off-by: Seongho Bae --- .../tests/steward_review_finalization.rs | 112 +++++++++++++----- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index c368ca79..3243eab2 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -1,6 +1,7 @@ use conceptweave_zotero::{ Disposition, EvaluationError, GoldenSetApproval, ItemData, ZoteroItem, - build_steward_review_worksheet, classify_snapshot, reviewed_golden_set_from_worksheet, + build_steward_review_worksheet, classification_proposal_digest, classify_snapshot, + reviewed_golden_set_from_worksheet, }; fn report() -> conceptweave_zotero::ClassificationReport { @@ -10,6 +11,7 @@ fn report() -> conceptweave_zotero::ClassificationReport { 42, vec![ ZoteroItem { + source_record: None, key: "B".into(), version: 8, data: ItemData { @@ -23,6 +25,7 @@ fn report() -> conceptweave_zotero::ClassificationReport { }, }, ZoteroItem { + source_record: None, key: "A".into(), version: 7, data: ItemData { @@ -39,13 +42,17 @@ fn report() -> conceptweave_zotero::ClassificationReport { ) } -fn approval(worksheet: &conceptweave_zotero::StewardReviewWorksheet) -> GoldenSetApproval { +fn approval( + report: &conceptweave_zotero::ClassificationReport, + worksheet: &conceptweave_zotero::StewardReviewWorksheet, +) -> GoldenSetApproval { GoldenSetApproval { receipt_id: "review-receipt".into(), reviewer_subject: "steward-subject".into(), library_version: worksheet.library_version, rule_revision: worksheet.rule_revision.clone(), snapshot_digest: worksheet.snapshot_digest.clone(), + proposal_digest: classification_proposal_digest(report), snapshot_items: worksheet.snapshot_items.clone(), } } @@ -63,7 +70,8 @@ fn complete_worksheet_becomes_a_snapshot_bound_golden_set() { let worksheet = complete_worksheet(); let golden = - reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&worksheet)).unwrap(); + reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&report(), &worksheet)) + .unwrap(); assert_eq!(golden.labels.len(), 2); assert_eq!(golden.labels[0].item_key, "A"); @@ -78,17 +86,21 @@ fn finalization_rejects_each_invalid_identity_coordinate() { let mut invalid_report = report(); invalid_report.rule_revision = ""; assert_eq!( - reviewed_golden_set_from_worksheet(&invalid_report, &worksheet, approval(&worksheet)), + reviewed_golden_set_from_worksheet( + &invalid_report, + &worksheet, + approval(&report(), &worksheet) + ), Err(EvaluationError::InvalidReview) ); - let mut invalid_approval = approval(&worksheet); + let mut invalid_approval = approval(&report(), &worksheet); invalid_approval.receipt_id.clear(); assert_eq!( reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::InvalidReview) ); - let mut invalid_approval = approval(&worksheet); + let mut invalid_approval = approval(&report(), &worksheet); invalid_approval.reviewer_subject.clear(); assert_eq!( reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), @@ -98,29 +110,29 @@ fn finalization_rejects_each_invalid_identity_coordinate() { let mut invalid = worksheet.clone(); invalid.rule_revision.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.snapshot_digest.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidReview) ); - let mut invalid_approval = approval(&worksheet); + let mut invalid_approval = approval(&report(), &worksheet); invalid_approval.library_version += 1; assert_eq!( reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::SnapshotMismatch) ); - let mut invalid_approval = approval(&worksheet); + let mut invalid_approval = approval(&report(), &worksheet); invalid_approval.rule_revision.push_str("-changed"); assert_eq!( reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), Err(EvaluationError::SnapshotMismatch) ); - let mut invalid_approval = approval(&worksheet); + let mut invalid_approval = approval(&report(), &worksheet); invalid_approval.snapshot_items.pop(); assert_eq!( reviewed_golden_set_from_worksheet(&report(), &worksheet, invalid_approval), @@ -130,51 +142,51 @@ fn finalization_rejects_each_invalid_identity_coordinate() { let mut invalid = worksheet.clone(); invalid.snapshot_items[0].item_key.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.snapshot_items[1] = invalid.snapshot_items[0].clone(); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.library_version += 1; assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.rule_revision.push_str("-changed"); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.snapshot_digest.push_str("-changed"); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::SnapshotMismatch) ); let mut invalid = worksheet.clone(); invalid.decisions[0].item_key.clear(); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.decisions[0].item_version += 1; assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.decisions[0].proposed_disposition = Disposition::AlignmentVersioning; assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidReview) ); @@ -182,20 +194,20 @@ fn finalization_rejects_each_invalid_identity_coordinate() { invalid.decisions[0].abstention_reason = Some(conceptweave_zotero::AbstentionReason::NoDeterministicRuleMatch); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.decisions[1].abstention_reason = None; assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidReview) ); let mut invalid = worksheet.clone(); invalid.decisions.pop(); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::IncompleteReview) ); @@ -204,6 +216,7 @@ fn finalization_rejects_each_invalid_identity_coordinate() { None, 42, vec![ZoteroItem { + source_record: None, key: "NOTE".into(), version: 1, data: ItemData { @@ -219,7 +232,7 @@ fn finalization_rejects_each_invalid_identity_coordinate() { ); let empty = build_steward_review_worksheet(&empty_report).unwrap(); assert_eq!( - reviewed_golden_set_from_worksheet(&empty_report, &empty, approval(&empty)), + reviewed_golden_set_from_worksheet(&empty_report, &empty, approval(&empty_report, &empty)), Err(EvaluationError::IncompleteReview) ); } @@ -228,7 +241,7 @@ fn finalization_rejects_each_invalid_identity_coordinate() { fn finalization_rejects_incomplete_invalid_or_mismatched_review() { let worksheet = build_steward_review_worksheet(&report()).unwrap(); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&worksheet)), + reviewed_golden_set_from_worksheet(&report(), &worksheet, approval(&report(), &worksheet)), Err(EvaluationError::IncompleteReview) ); @@ -236,7 +249,7 @@ fn finalization_rejects_incomplete_invalid_or_mismatched_review() { invalid.decisions[0].reviewed_disposition = Some(Disposition::NeedsStewardReview); invalid.decisions[1].reviewed_disposition = Some(Disposition::Generation); assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&invalid)), + reviewed_golden_set_from_worksheet(&report(), &invalid, approval(&report(), &invalid)), Err(EvaluationError::InvalidExpectedDisposition) ); @@ -244,7 +257,7 @@ fn finalization_rejects_incomplete_invalid_or_mismatched_review() { for decision in &mut complete.decisions { decision.reviewed_disposition = Some(Disposition::Generation); } - let mut mismatched = approval(&complete); + let mut mismatched = approval(&report(), &complete); mismatched.snapshot_digest.push_str("-changed"); assert_eq!( reviewed_golden_set_from_worksheet(&report(), &complete, mismatched), @@ -254,7 +267,52 @@ fn finalization_rejects_incomplete_invalid_or_mismatched_review() { complete.decisions[1].item_key = complete.decisions[0].item_key.clone(); complete.decisions[1].item_version = complete.decisions[0].item_version; assert_eq!( - reviewed_golden_set_from_worksheet(&report(), &complete, approval(&complete)), + reviewed_golden_set_from_worksheet(&report(), &complete, approval(&report(), &complete)), Err(EvaluationError::InvalidReview) ); } + +#[test] +fn finalization_rejects_changed_review_evidence_under_the_original_approval() { + for alter_title in [false, true] { + let mut report = report(); + let worksheet = complete_worksheet(); + let original_approval = approval(&report, &worksheet); + let golden = + reviewed_golden_set_from_worksheet(&report, &worksheet, original_approval.clone()) + .unwrap(); + assert_eq!(golden.approval, original_approval); + + if alter_title { + report.classified_items[0].title.push_str(" changed"); + } else { + report.classified_items[0].evidence.field_values.clear(); + } + let mut rebuilt_worksheet = build_steward_review_worksheet(&report).unwrap(); + for decision in &mut rebuilt_worksheet.decisions { + decision.reviewed_disposition = Some(Disposition::Generation); + } + assert_eq!( + reviewed_golden_set_from_worksheet(&report, &rebuilt_worksheet, original_approval), + Err(EvaluationError::SnapshotMismatch) + ); + } +} + +#[test] +fn finalization_rejects_missing_or_replaced_proposal_binding() { + let report = report(); + let worksheet = complete_worksheet(); + for (digest, expected) in [ + ("", EvaluationError::InvalidReview), + (" ", EvaluationError::InvalidReview), + ("sha256:replaced", EvaluationError::SnapshotMismatch), + ] { + let mut receipt = approval(&report, &worksheet); + receipt.proposal_digest = digest.into(); + assert_eq!( + reviewed_golden_set_from_worksheet(&report, &worksheet, receipt), + Err(expected) + ); + } +} From 61fee4c3acada36355fbaaa0020828f3a7081555 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:02:18 +0900 Subject: [PATCH 12/17] fix(research): validate proposal approval before worksheet finalization Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/src/lib.rs | 5 +++++ docs/TRD.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 78e64009..a89b01c2 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1263,6 +1263,9 @@ pub struct GoldenSetApproval { } /// Converts a fully decided local worksheet into the input for approval verification. +/// +/// The supplied approval must already bind the current complete proposal records. +/// This function validates that binding without creating or renewing authority. pub fn reviewed_golden_set_from_worksheet( report: &ClassificationReport, worksheet: &StewardReviewWorksheet, @@ -1275,6 +1278,7 @@ pub fn reviewed_golden_set_from_worksheet( } if approval.receipt_id.trim().is_empty() || approval.reviewer_subject.trim().is_empty() + || approval.proposal_digest.trim().is_empty() || worksheet.rule_revision.trim().is_empty() || worksheet.snapshot_digest.trim().is_empty() { @@ -1283,6 +1287,7 @@ pub fn reviewed_golden_set_from_worksheet( if approval.library_version != worksheet.library_version || approval.rule_revision != worksheet.rule_revision || approval.snapshot_digest != worksheet.snapshot_digest + || approval.proposal_digest != classification_proposal_digest(report) || approval.snapshot_items != worksheet.snapshot_items { return Err(EvaluationError::SnapshotMismatch); diff --git a/docs/TRD.md b/docs/TRD.md index 68ec94a1..074dff89 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -75,7 +75,7 @@ The full-reclassification evaluator checks label cardinality before that boundar A successful classification report carries an `audit_summary` whose snapshot, bibliographic, proposed-disposition, provenance-complete, abstention, duplicate-candidate, failure, and per-disposition counts are derived from the same in-memory immutable snapshot. Zotero item version zero remains a valid observed coordinate for never-synced Zotero 9 records; provenance completeness rejects a missing item key rather than inventing a positive-only version invariant. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. The review worksheet is a deterministic item-key-ordered projection of the report. `conceptweave-zotero --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json` reads one live snapshot and creates both owner-only outputs as a bound pair; failed output removes partial files so the same paths can be retried. It binds the library version, rule revision, raw-snapshot digest, complete parent/child item coordinates, item proposal, abstention reason, and an initially empty decision for every bibliographic item. Construction rejects blank or duplicate snapshot identity, mismatched item revisions, and inconsistent observed, bibliographic, proposal, provenance, abstention, duplicate, failure, or disposition counts. It deliberately omits bibliographic text and matched evidence; stewards consult the paired owner-only report by item key. -The finalization function consumes the filled worksheet plus a governance approval receipt. It rejects blank authority metadata, coordinate drift, duplicate or unknown decision keys, missing decisions, abstention as approved truth, and tampered proposal/reason pairs. Its output reuses the existing reviewed-golden-set evaluator; finalization itself does not verify external authority. +The finalization function consumes the filled worksheet plus a governance approval receipt. It rejects blank authority metadata, missing or mismatched proposal bindings, coordinate drift, duplicate or unknown decision keys, missing decisions, abstention as approved truth, and tampered proposal/reason pairs. It recomputes the complete current proposal digest, including title and evidence fields omitted from the worksheet, and compares it with the supplied approval without replacing that approval. Its output reuses the existing reviewed-golden-set evaluator; finalization itself does not verify external authority. The local report can contain titles, tags, matched metadata, and abstention abstracts. It is sensitive steward-review material, remains outside the repository, and is not a publication artifact. From f02631e3228063781c669b26f4cc7c6aaaaa36d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:10:47 +0900 Subject: [PATCH 13/17] test(research): reproduce stale worksheet finalization admission --- .../tests/steward_review_finalization.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index 3243eab2..5f4e6125 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -299,6 +299,41 @@ fn finalization_rejects_changed_review_evidence_under_the_original_approval() { } } +#[test] +fn finalization_rejects_stale_worksheet_even_with_current_approval_coordinates() { + for changed_field in 0..3 { + let mut report = report(); + let worksheet = complete_worksheet(); + match changed_field { + 0 => report.classified_items[0].title.push_str(" changed"), + 1 => report.classified_items[0].evidence.field_values.clear(), + _ => report.classified_items[0].review_abstract_note = Some("changed context".into()), + } + let current_receipt = approval(&report, &worksheet); + assert_eq!( + reviewed_golden_set_from_worksheet(&report, &worksheet, current_receipt), + Err(EvaluationError::SnapshotMismatch) + ); + } +} + +#[test] +fn finalization_rejects_blank_or_replaced_worksheet_binding() { + let report = report(); + for (binding, expected) in [ + ("", EvaluationError::InvalidReview), + (" ", EvaluationError::InvalidReview), + ("sha256:replaced", EvaluationError::SnapshotMismatch), + ] { + let mut worksheet = complete_worksheet(); + worksheet.proposal_digest = binding.into(); + assert_eq!( + reviewed_golden_set_from_worksheet(&report, &worksheet, approval(&report, &worksheet)), + Err(expected) + ); + } +} + #[test] fn finalization_rejects_missing_or_replaced_proposal_binding() { let report = report(); From d44b9fee42ba5173a5dedcc9ca9d4118f9c3896c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:11:17 +0900 Subject: [PATCH 14/17] fix(research): require worksheet content continuity during finalization --- crates/conceptweave-zotero/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index d8c1d229..65732590 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1267,6 +1267,7 @@ pub fn reviewed_golden_set_from_worksheet( || approval.proposal_digest.trim().is_empty() || worksheet.rule_revision.trim().is_empty() || worksheet.snapshot_digest.trim().is_empty() + || worksheet.proposal_digest.trim().is_empty() { return Err(EvaluationError::InvalidReview); } @@ -1281,6 +1282,7 @@ pub fn reviewed_golden_set_from_worksheet( if worksheet.library_version != expected.library_version || worksheet.rule_revision != expected.rule_revision || worksheet.snapshot_digest != expected.snapshot_digest + || worksheet.proposal_digest != expected.proposal_digest || worksheet.snapshot_items != expected.snapshot_items { return Err(EvaluationError::SnapshotMismatch); From e90a02beb7f64bbc2536c3ffa0a6795d87664114 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:12:03 +0900 Subject: [PATCH 15/17] test(research): separate local finalization from independent completion authority --- .../tests/steward_review_finalization.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_finalization.rs b/crates/conceptweave-zotero/tests/steward_review_finalization.rs index 5f4e6125..27e6a057 100644 --- a/crates/conceptweave-zotero/tests/steward_review_finalization.rs +++ b/crates/conceptweave-zotero/tests/steward_review_finalization.rs @@ -299,6 +299,57 @@ fn finalization_rejects_changed_review_evidence_under_the_original_approval() { } } +#[test] +fn locally_rebound_finalization_does_not_renew_independent_approval() { + let mut report = report(); + let mut worksheet = complete_worksheet(); + let issued_set = + reviewed_golden_set_from_worksheet(&report, &worksheet, approval(&report, &worksheet)) + .unwrap(); + assert!( + conceptweave_zotero::evaluate_reviewed_golden_set(&report, &issued_set, |value| value + == &issued_set) + .is_ok() + ); + report.classified_items[0].title.push_str(" changed"); + worksheet.proposal_digest = classification_proposal_digest(&report); + let rebound = + reviewed_golden_set_from_worksheet(&report, &worksheet, approval(&report, &worksheet)) + .unwrap(); + assert_eq!( + conceptweave_zotero::evaluate_reviewed_golden_set(&report, &rebound, |value| value + == &issued_set), + Err(EvaluationError::UnverifiedApproval) + ); +} + +#[test] +fn pending_source_conversion_does_not_prove_complete_review() { + let items = [("A", "book"), ("source", "attachment")] + .into_iter() + .map(|(key, item_type)| { + serde_json::from_value::(serde_json::json!({ + "key": key, "version": 7, + "data": {"itemType": item_type, "title": "synthetic ontology learning"} + })) + .unwrap() + }) + .collect(); + let report = classify_snapshot("9.0.6".into(), None, 42, items); + let mut worksheet = build_steward_review_worksheet(&report).unwrap(); + worksheet.decisions[0].reviewed_disposition = Some(Disposition::Generation); + let local_set = + reviewed_golden_set_from_worksheet(&report, &worksheet, approval(&report, &worksheet)) + .unwrap(); + assert_eq!(report.pending_source_item_keys, ["source"]); + assert_eq!( + conceptweave_zotero::evaluate_complete_reviewed_classification(&report, &local_set, |_| { + panic!("pending scope must not contact governance") + }), + Err(EvaluationError::IncompleteReview) + ); +} + #[test] fn finalization_rejects_stale_worksheet_even_with_current_approval_coordinates() { for changed_field in 0..3 { From da2556be631f61d9a88f53b218f1b22ddb2c4500 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:13:34 +0900 Subject: [PATCH 16/17] docs(research): distinguish finalization continuity from independent approval --- crates/conceptweave-zotero/src/lib.rs | 3 +++ docs/PRD.md | 1 + docs/TRD.md | 1 + docs/adr/0006-zotero-research-intake.md | 2 ++ 4 files changed, 7 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 65732590..a11258ec 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1252,6 +1252,9 @@ pub struct GoldenSetApproval { /// /// The supplied approval must already bind the current complete proposal records. /// This function validates that binding without creating or renewing authority. +/// The worksheet must independently match the current proposal identity; changing +/// approval coordinates cannot refresh an older worksheet. Successful conversion +/// is still unverified input, not complete-library, full-text or write authority. pub fn reviewed_golden_set_from_worksheet( report: &ClassificationReport, worksheet: &StewardReviewWorksheet, diff --git a/docs/PRD.md b/docs/PRD.md index ec7b0586..1f51bb52 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -88,6 +88,7 @@ The Zotero 10+ adapter can accept a caller-owned API key and server identity at A local steward worksheet must bind the library version, rule revision, complete raw-snapshot digest, current proposal-and-retained-source digest, every observed parent/child item revision, and one blank decision slot per classified bibliographic item. It repeats item identity, proposal, and abstention reason only; titles, abstracts, tags, collections, and matched evidence remain in the separate sensitive report. Shared inventory validation rejects omitted source records, hidden pending relationships and inconsistent identity before construction. Valid unresolved sources do not prevent starting review, but prevent claiming completion. Old worksheets without the content binding require regeneration, never automatic approval backfill. After every decision is filled, worksheet finalization must verify the governance receipt coordinates, unique item identities and revisions, proposal/abstention consistency, and non-abstention truth labels before producing a reviewed golden set. Missing decisions remain incomplete and cannot reach external approval verification. +The worksheet's own required content identity must match the current report independently of the supplied receipt. Blank identity is invalid; a stale or replaced identity is a snapshot mismatch. Conversion only prepares input for independent verification. Unresolved sources can remain in locally prepared review data, but prevent whole-library completion; refreshing local digests cannot renew an independently issued approval. Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and binds both the complete source/classifier-input snapshot and every current proposal field, in addition to the item-key/item-version coordinates. Same-version changes to unmodeled provider metadata, absent/default fields, classifier inputs, predictions or supporting evidence must invalidate the corresponding binding. Evaluation recomputes proposal identity before contacting governance; a locally changed digest cannot renew an approval. Legacy unbound approvals require reissuance, never automatic backfill. Abstention is a prediction outcome, never an approved truth label. Evaluation emits the verified library revision, rule revision, opaque snapshot and proposal digests, and aggregate counts for exact matches, abstentions, and per-disposition true-positive/predicted/expected totals; it must not copy Zotero keys, reviewer identity, or bibliographic text into the result. diff --git a/docs/TRD.md b/docs/TRD.md index 044c5207..8df75fb6 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -112,6 +112,7 @@ A successful classification report carries an `audit_summary` whose snapshot, bi `conceptweave-zotero --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json` validates both canonical destinations and rejects aliases before reading one live snapshot. Both artifacts are built and serialized before the first file write. Writes are sequential, not an atomic publication. Any write failure propagates without pathname deletion or implicit buffer-flush retry. A complete report and an empty/partial worksheet may remain; that is not a completed pair. Inspect retained owner-only files and use new output paths for another capture. Never automatically overwrite or infer approval from a surviving artifact. A successful flush is not a crash-durability guarantee. The review worksheet is a deterministic item-key-ordered projection of the report. It binds library/rule revisions, raw-snapshot digest, required `proposal_digest` from the existing v2 scope hash, complete item coordinates, proposal, abstention reason, and one initially empty decision per bibliographic item. Construction reuses `validate_classification_report`, mapped to `WorksheetError::InvalidReport`, before worksheet-specific nonblank identity and abstention checks. This avoids a second drifting audit implementation while admitting structurally valid pending source evidence. It deliberately omits bibliographic text and matched evidence; stewards consult the owner-only report by item key. Missing proposal binding fails deserialization. Subsequent progress, application and finalization owners must compare this field to the recomputed report binding; present-but-blank or rewritten values are not authority. Legacy worksheets must be regenerated, and independent approval remains separate. The finalization function consumes the filled worksheet plus a governance approval receipt. It rejects blank authority metadata, missing or mismatched proposal bindings, coordinate drift, duplicate or unknown decision keys, missing decisions, abstention as approved truth, and tampered proposal/reason pairs. It recomputes the complete current proposal digest, including title and evidence fields omitted from the worksheet, and compares it with the supplied approval without replacing that approval. Its output reuses the existing reviewed-golden-set evaluator; finalization itself does not verify external authority. +It also rejects a blank worksheet `proposal_digest` as `InvalidReview` and compares that field with the freshly built expected worksheet as `SnapshotMismatch`, retaining existing cardinality and approval error precedence. The converter does not accept an updated receipt as proof that old decisions reviewed changed content. A caller can construct self-consistent unverified data, so the evaluator still authenticates the entire reviewed set against independent evidence. Pending sources are admitted for preparation but rejected by complete evaluation before governance is contacted. Later extracted worksheet validators must preserve this comparison. The local report can contain titles, tags, matched metadata, and abstention abstracts. It is sensitive steward-review material, remains outside the repository, and is not a publication artifact. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index ecb7082e..931021cb 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -5,6 +5,8 @@ ## Context +Proposed September 7 finalization amendment: when carrying completed metadata decisions between saved artifacts, changed report evidence plus freshly supplied receipt coordinates must not refresh a stale worksheet. We choose two checks in the existing converter—nonblank worksheet proposal identity and equality with the recomputed expected worksheet—rather than a second approval mechanism or automatically rebinding old decisions. RED `f02631e` reproduces both stale-content and blank/replaced-binding admission; `d44b9fe` closes them while preserving prior error precedence. The cost is explicit worksheet regeneration/review after changed source context. Self-consistent local artifacts remain unverified: `e90a02b` demonstrates that locally rewritten digests still fail independent original-receipt verification and that pending sources prevent complete evaluation even after local conversion. This decision grants neither full-text nor Zotero write authority. Later worksheet comparators must inherit the binding check, and protected/runtime evidence remains outstanding. + Proposed September 7 export-failure amendment: when exporting sensitive report/worksheet pairs, a failed write may race with pathname replacement, and destroying a buffered writer may write pending bytes after failure. We choose to preserve artifacts, disassemble the existing buffer without flushing, and propagate the original error, rejecting pathname cleanup or an automatic retry. This extends the existing private-creation policy to the common output writer and the pair's second-file failure. The cost is retained empty/partial files and operator inspection; sequential export is not a transaction or crash-durable publication. Both canonical destinations must differ before the Local API read, including aliases such as `/tmp` and `/private/tmp`. RED `68575a6` proves replacement deletion and implicit drop-flush; `ab391b2` removes both behaviors. RED `fb5b5e5` proves alias collision; `e928858` rejects it before snapshot capture. Later CLI owners must inherit this implementation instead of restoring cleanup. No Zotero mutation or approval authority follows from local output. CWL needs a reproducible inventory of ontology research without turning keyword matches into authoritative library organization. The current desktop is Zotero 9.0.6, whose Local API supports reads but not writes. Zotero documents Local API writes only for Zotero 10+, where they require user-granted authorization and `Zotero-Server-ID`; this slice therefore has no mutation capability. The library is mutable while pagination is in progress, duplicate metadata does not prove that two records should be merged, and the local report contains titles and item keys that must not be written into the repository. From fd5ef23c1c23fa36ef106400e15c9438eaa5cd41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:16:02 +0900 Subject: [PATCH 17/17] docs(research): record finalization binding RED and exact verification --- docs/product-technical-gap-baseline.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 31aba87c..864e14c3 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,6 +6,16 @@ This file records code-current product and technical gaps. Exact PR/check/run co ## September 6 source inventory checkpoint +### September 7 PR27 finalization continuity repair + +Final source `da2556b` passes 172 tests/25 suites including three doctests, strict all-target Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks. The unchanged pinned coverage gate passes 295/295 functions, 2,611/2,611 normalized regions and 468/468 normalized branches. Raw coverage remains 3,436/3,498 lines, 5,214/5,321 regions and 423/468 branches, not 100%. Logs: `/tmp/conceptweave-pr27-verified.log` and `/tmp/conceptweave-pr27-{clippy,rustdoc,coverage}-verified.log`. Both baseline and integration finished before their successors were edited; the final documentation commit receives its own full verification. + +Original PR27 `3df0c124f390797bacaba8ffdf229f502b0e9bf3` passed 133 tests/25 suites. Ordinary merge `5fa34b4` retains it and PR26 `7ad6386de13e19bb57fc4519141ac67c7b8bf92b`; integrated tests passed 168/25. RED `f02631e` compiled with 5 passing and 2 failing tests: blank/replaced worksheet bindings and stale worksheet decisions with a current report/receipt were admitted. `d44b9fe` adds two existing-boundary checks: blank worksheet binding is `InvalidReview`; mismatch with the recomputed expected worksheet is `SnapshotMismatch`. Prior cardinality and approval error precedence is preserved. + +`e90a02b` verifies the distinction between local conversion and independent authority: locally rewriting both digests can produce self-consistent input, but the original independently verified whole-set receipt still rejects it. Valid pending-source conversion is retained for preparation, while complete evaluation refuses it before governance. Tests cover changed title, evidence and review context. Independent read-only review found no additional production finding; `da2556b` documents this boundary in the public API, PRD, TRD and Proposed ADR0006. Final exact-source verification is recorded below. + +This does not complete the library campaign: actual decisions and independent approvals remain 0/3,715 plus four unresolved standalone sources. No real metadata, labels or approval was generated by these synthetic tests. Root and later extracted worksheet validators still need the repair; next PR28 report roundtrip must retain exact binding and source scope. Visual Inspection was attempted but the Mac remains locked, so no new screen evidence is claimed. Local verification is not hosted GREEN, independent protected approval, protected merge, release, full-text authority or Zotero mutation. + ### September 7 PR26 private export repair Final source `58ff5985890d5a0b4aaadaa1f8d604e1bc96a1e2` passes 163 tests/24 suites including three doctests, strict Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks. The unchanged pinned coverage gate passes 294/294 functions, 2,536/2,536 normalized regions and 422/422 normalized branches. Raw coverage remains 3,382/3,444 lines, 5,139/5,246 regions and 377/422 branches, not 100%. Logs: `/tmp/conceptweave-pr26-verified.log` and `/tmp/conceptweave-pr26-{clippy,rustdoc,coverage}-verified.log`. The earlier coverage failure is retained in `/tmp/conceptweave-pr26-coverage.log`; it is not a successful checkpoint.