From 43562803e55b950355fa1f6dd05636b12736c69e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:34:40 +0900 Subject: [PATCH 01/16] test(zotero): require snapshot-bound review worksheet --- .../tests/steward_review_worksheet.rs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 crates/conceptweave-zotero/tests/steward_review_worksheet.rs diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs new file mode 100644 index 00000000..0dc25cea --- /dev/null +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -0,0 +1,71 @@ +use conceptweave_zotero::{ + ItemData, WorksheetError, ZoteroItem, build_steward_review_worksheet, classify_snapshot, +}; + +fn item(key: &str, title: &str) -> ZoteroItem { + ZoteroItem { + key: key.into(), + version: 7, + data: ItemData { + item_type: "book".into(), + title: title.into(), + abstract_note: "sensitive abstract".into(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + } +} + +#[test] +fn worksheet_is_snapshot_bound_complete_and_contains_no_bibliographic_text() { + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("B", "unmatched title"), item("A", "ontology learning")], + ); + + let worksheet = build_steward_review_worksheet(&report).unwrap(); + + assert_eq!(worksheet.library_version, 42); + assert_eq!(worksheet.rule_revision, report.rule_revision); + assert_eq!(worksheet.snapshot_digest, report.snapshot_digest); + assert_eq!(worksheet.snapshot_items, report.snapshot_items); + assert_eq!( + worksheet + .decisions + .iter() + .map(|decision| decision.item_key.as_str()) + .collect::>(), + ["A", "B"] + ); + assert!( + worksheet + .decisions + .iter() + .all(|decision| decision.reviewed_disposition.is_none()) + ); + let serialized = serde_json::to_value(&worksheet).unwrap(); + let serialized = serialized.to_string(); + assert!(!serialized.contains("unmatched title")); + assert!(!serialized.contains("sensitive abstract")); + assert!(!serialized.contains("field_values")); +} + +#[test] +fn worksheet_rejects_a_report_with_duplicate_source_identity() { + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "ontology learning"), item("A", "ontology learning")], + ); + + assert_eq!( + build_steward_review_worksheet(&report), + Err(WorksheetError::InvalidReport) + ); + assert!(WorksheetError::InvalidReport.to_string().contains("invalid")); +} From cfc33213d4f06b92877b2ac3fa78bf23e433600c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:35:13 +0900 Subject: [PATCH 02/16] feat(zotero): build complete steward review worksheet --- crates/conceptweave-zotero/src/lib.rs | 99 ++++++++++++++++++- .../tests/steward_review_worksheet.rs | 11 ++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index fb4c5880..404dddc9 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -96,7 +96,7 @@ pub enum Disposition { } /// Deterministic reason that a bibliographic item requires steward review. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum AbstentionReason { /// Title, abstract, and tags contain no classification metadata. @@ -1030,6 +1030,103 @@ pub struct ClassificationAudit { pub disposition_counts: BTreeMap, } +/// One editable local steward decision without duplicated bibliographic text. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct StewardReviewDecision { + /// Stable Zotero item key used to join the sensitive classification report. + pub item_key: String, + /// Exact item revision observed in the classified snapshot. + pub item_version: u64, + /// Deterministic proposal supplied for comparison, never as approval. + pub proposed_disposition: Disposition, + /// Deterministic abstention reason when the proposal requires review. + pub abstention_reason: Option, + /// Steward decision to fill; abstention is rejected by completion evaluation. + pub reviewed_disposition: Option, +} + +/// Snapshot-bound local worksheet for one decision per bibliographic item. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct StewardReviewWorksheet { + /// Zotero library revision observed with the source snapshot. + pub library_version: u64, + /// Classifier revision that produced the proposals. + pub rule_revision: String, + /// Canonical content digest of the complete raw snapshot. + pub snapshot_digest: String, + /// Complete parent and child item-revision coordinates. + pub snapshot_items: Vec, + /// Deterministically ordered editable decisions for bibliographic items. + pub decisions: Vec, +} + +/// A classification report cannot safely produce a review worksheet. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WorksheetError { + /// Report identity or coverage is incomplete, duplicated, or inconsistent. + InvalidReport, +} + +impl fmt::Display for WorksheetError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("classification report is invalid for steward review") + } +} + +impl std::error::Error for WorksheetError {} + +/// Builds a complete local worksheet without copying titles, abstracts, or evidence. +pub fn build_steward_review_worksheet( + report: &ClassificationReport, +) -> Result { + if report.rule_revision.trim().is_empty() + || report.snapshot_digest.trim().is_empty() + || report.snapshot_items.len() != report.observed_item_count + || report.classified_items.len() != report.audit_summary.bibliographic_item_count + || report.classified_items.len() != report.audit_summary.proposed_disposition_count + { + return Err(WorksheetError::InvalidReport); + } + + let mut snapshot_versions = BTreeMap::new(); + for item in &report.snapshot_items { + if item.item_key.trim().is_empty() + || snapshot_versions + .insert(item.item_key.as_str(), item.item_version) + .is_some() + { + return Err(WorksheetError::InvalidReport); + } + } + + let mut decision_keys = BTreeSet::new(); + let mut decisions = Vec::with_capacity(report.classified_items.len()); + for item in &report.classified_items { + if item.item_key.trim().is_empty() + || !decision_keys.insert(item.item_key.as_str()) + || snapshot_versions.get(item.item_key.as_str()) != Some(&item.item_version) + { + return Err(WorksheetError::InvalidReport); + } + decisions.push(StewardReviewDecision { + item_key: item.item_key.clone(), + item_version: item.item_version, + proposed_disposition: item.proposed_disposition, + abstention_reason: item.abstention_reason, + reviewed_disposition: None, + }); + } + decisions.sort_by(|left, right| left.item_key.cmp(&right.item_key)); + + Ok(StewardReviewWorksheet { + library_version: report.library_version, + rule_revision: report.rule_revision.into(), + snapshot_digest: report.snapshot_digest.clone(), + snapshot_items: report.snapshot_items.clone(), + decisions, + }) +} + /// One steward-reviewed expected disposition in a local golden set. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct GoldenLabel { diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index 0dc25cea..c76c6dbe 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -60,12 +60,19 @@ fn worksheet_rejects_a_report_with_duplicate_source_identity() { "9.0.6".into(), None, 42, - vec![item("A", "ontology learning"), item("A", "ontology learning")], + vec![ + item("A", "ontology learning"), + item("A", "ontology learning"), + ], ); assert_eq!( build_steward_review_worksheet(&report), Err(WorksheetError::InvalidReport) ); - assert!(WorksheetError::InvalidReport.to_string().contains("invalid")); + assert!( + WorksheetError::InvalidReport + .to_string() + .contains("invalid") + ); } From b2722499b821c25d0e9b14283d10408a5cfbf726 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:36:37 +0900 Subject: [PATCH 03/16] docs(zotero): define steward review worksheet --- CHANGELOG.md | 1 + docs/PRD.md | 2 ++ docs/TRD.md | 2 ++ docs/UML.md | 1 + docs/adr/0006-zotero-research-intake.md | 2 ++ docs/product-technical-gap-baseline.md | 2 ++ 6 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c05128bd..6bd95ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to ConceptWeave are documented here. - Minimal, nonduplicated local abstract context for Zotero items that require steward classification. - Owner-only file permissions for sensitive local Zotero classification reports. - 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. ### Security diff --git a/docs/PRD.md b/docs/PRD.md index 561699de..6c102e85 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -67,6 +67,8 @@ For execute-mode plans, the runtime must preflight every item before the first w The Zotero 10+ adapter can accept a caller-owned API key and server identity at runtime or consume one successful, user-approved Local API authorization. Authorization sends one bounded application name and the expected server identity to the fixed loopback endpoint; only a same-server bounded response that explicitly reports denial is classified as the user's decision. Denial and rate limiting return immediately without another prompt or automatic retry. The private 32-character key is neither serializable nor printable. Authorization, read, and write responses bind to the expected server before status classification; writes name expired authorization and matching-server stale preconditions separately. Thin public execution boundaries connect the adapter to the reviewed write and rollback cores without duplicating mutation logic. Synthetic transport evidence does not satisfy AC6's approved live Zotero 10 authorization, write, and rollback requirement. 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. + Every successful classification report includes aggregate evidence for snapshot coverage, proposal coverage, provenance completeness, abstentions, duplicate candidates, disposition totals, and zero unreported failures. ## 6. First vertical slice diff --git a/docs/TRD.md b/docs/TRD.md index 7f1453f6..966ce0c6 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -68,6 +68,8 @@ Duplicate review is independent of subject classification. A reviewed decision s Golden-set evaluation accepts only a governance receipt verified with the complete reviewed set by a caller-owned authorization boundary. Its library version, rule revision, canonical SHA-256 content digest, and every observed parent/child item-key/item-version identity must bind the classification report. The digest covers every raw Zotero item in canonical key order. Blank, duplicate, unknown, stale, content-mismatched, label-mismatched, or abstention-as-truth inputs fail closed before the external approval verifier is called, so an invalid local set cannot consume approval authority. The full-reclassification evaluator checks label cardinality before that boundary and additionally requires the reviewed label count to equal the unique classified bibliographic-item count; because the base evaluator rejects blank, duplicate, and unknown keys, equality proves complete coverage. A sampled golden set can measure quality but cannot satisfy this completion gate. The output retains the verified library version, rule revision, and opaque snapshot digest, but contains no item keys, reviewer identity, or bibliographic text. Production authorization remains Keyverse/governance-owned; this crate passes the complete reviewed labels to that boundary instead of minting authority. 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. 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, or proposal counts. It deliberately omits bibliographic text and matched evidence; stewards consult the owner-only report by item key. + 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. The report is local JSON and contains proposals rather than governance decisions. On supported Unix platforms, CLI output is restricted to a new owner-readable/writable (`0600`) direct child of canonical `/tmp` or the operating system temporary directory; exact permissions are restored after umask application, and other platforms fail closed. Relative paths, nested paths, existing paths, and symlinks are rejected, and create-new file semantics prevent overwrite/path-swap writes. Reviewed collection/tag changes can produce a pure local plan whose default mode is dry-run. The plan requires exact report and item preconditions, complete before/after/rollback arrays, externally verified authority, and preserved Zotero tag types; its fields are externally read-only after validation. Zotero 9 execute mode fails closed. The execution core makes no call in dry-run mode; otherwise it preflights every item before the first write, advances the library precondition only from verified state, stops on the first adapter or response failure, and re-reads that item through the same boundary. A proven applied state receives reverse-ordered rollback evidence containing server identity, post-write item revision, expected post-write metadata, and the complete restoration state even when the write response was lost; a state matching neither the before nor after contract is marked indeterminate. The generic rollback core rejects operations spanning server identities before any read, then reads every receipt item at one current library version and verifies that evidence before writing. It follows receipt order, advances the library version only after a verified inverse write, and on failure re-reads the item to classify restored, unchanged, or indeterminate state. Its secret-free receipt separates restored, failed, indeterminate, not-attempted, and remaining work. Automatic retry evidence includes a failed current operation only when it is proven unchanged; an indeterminate operation and its complete metadata are retained separately for operator reconciliation. The delayed reconciliation boundary performs exactly one server-bound read and no write. It treats an exact item revision plus expected metadata as unchanged even if unrelated library changes advanced the library version, and treats restoration metadata as restored only at a newer item revision; every identity, metadata, or version ambiguity remains indeterminate. Already consumed evidence fails preflight on reuse. diff --git a/docs/UML.md b/docs/UML.md index bcdafb81..8b14ad73 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -54,6 +54,7 @@ sequenceDiagram end Intake->>Intake: classify or abstain; link children; find duplicate candidates Intake->>Report: write proposals and evidence + Intake->>Report: derive snapshot-bound decision worksheet without bibliographic text Report->>Steward: review dispositions and merge candidates Steward->>Intake: verified labels for every bibliographic item Intake-->>Steward: aggregate completion evidence or incomplete-review failure diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index 7621db9f..7f6d5add 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -24,6 +24,8 @@ Duplicate candidates become canonical references only through externally verifie Classifier quality is measured only against local steward-reviewed labels whose reviewed set is verified outside this crate and bound to the exact library version, rule revision, canonical SHA-256 raw-snapshot digest, and every observed parent/child item-key/item-version coordinate. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Sampled labels may measure classifier quality, but a full-reclassification completion result requires exactly one approved label for every classified bibliographic item. Cardinality, snapshot, key, disposition, and duplicate checks run before the external approval verifier so invalid local input cannot consume approval authority. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence; Zotero keys, reviewer identity, and bibliographic text are omitted. Missing, incomplete, stale, content- or label-mismatched, unverified, unknown, duplicate, or invalid review identities fail closed at the applicable completion boundary. Every successful report includes an aggregate audit summary computed from the same captured snapshot. Zotero 9 item version zero is preserved as a valid never-synced source coordinate, not treated as missing provenance. Partial reads never produce a report, so successful output explicitly records zero failures alongside snapshot, proposal, provenance, abstention, duplicate, and per-disposition totals. +To make full review executable without copying sensitive text again, ConceptWeave derives a local worksheet from the validated report. The worksheet carries the exact snapshot binding, complete parent/child revision coordinates, deterministic proposal and abstention reason, plus one empty steward-decision slot per bibliographic item in item-key order. Titles, abstracts, tags, collections, and matched evidence remain only in the owner-only report. Invalid report identity or coverage fails worksheet construction. + The reader fails closed above 50,000 items or 256 MiB of cumulative response bodies, while retaining the 8 MiB per-page bound, finite request timeouts, redirect denial, total-count checks, snapshot-version checks, and duplicate-key detection. Pagination, consistency, resource-budget, and provider-contract behavior are separated from the narrow `ureq` transport so deterministic tests exercise the production reader core rather than excluding the entire reader from coverage. Report output is restricted to a new direct child of the operating system temporary directory. Relative paths, nested paths, existing files, and symlinks are rejected before write; the file is opened with create-new semantics so a path swap cannot cause repository or arbitrary-file overwrite. The buffered writer is explicitly flushed and a final filesystem error fails the command. Reports stay local and are never committed. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2ffa86d2..f2dd0b03 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -50,6 +50,8 @@ The 3,658-item abstention queue now preserves each nonempty abstract exactly onc The golden-set evaluation contract now records aggregate precision/recall numerators and denominators, requires an externally verified governance receipt bound to the complete item-key/item-version snapshot, rejects abstention as expected truth, and retains verified revisions plus an opaque snapshot digest so detached metrics remain attributable. Item and reviewer identities stay out of its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Connected duplicate components now produce a snapshot-bound local review manifest only after external steward verification; every operation retains all component source revisions and before/after/rollback canonical mappings while Zotero records remain unchanged. Reviewed collection/tag changes produce a default-dry-run plan bound to exact server, library, item, rule, digest, and complete metadata preconditions; externally read-only plan state prevents post-validation forgery, automatic-tag type is preserved, and Zotero 9 execute mode is rejected. The injected execution core calls nothing in dry-run mode, preflights every item before a write, stops at the first failure, reconciles a lost or invalid response with a same-boundary read, and emits rollback evidence for every item whose applied state is proven. The generic rollback executor rejects mixed-server evidence before reading, verifies all expected post-write states at one current library version before the first inverse write, follows the receipt's reverse order, advances only from verified writes, and stops with restored, failed, indeterminate, not-attempted, and remaining classifications. Unprovable state is reported as indeterminate with complete operation evidence retained separately and excluded from automatic retry until operator reconciliation. A later read-only reconciliation records the observed state, tolerates unrelated library-version advancement, and emits retry evidence only for an exact unchanged item; restoration metadata at a newer item revision proves current state but not causality. Reuse after restoration fails before writing. A fixed-loopback Zotero 10 adapter supplies stable server-pinned reads and authenticated one-item writes with atomic library/item preconditions, complete collection/tag replacement, and bounded verified responses; thin wrappers reuse both generic executors. Mock fixtures verify these contracts and secret-free failures. Korean, Japanese, Chinese, Vietnamese, Spanish, German, and French ontology-alignment metadata now have explicit fail-closed abstention coverage alongside the existing English positive case; this is safety evidence, not translated classification support. No real precision/recall, duplicate merge, write, or rollback claim exists until a steward supplies reviewed local decisions and a production authorization adapter verifies them. AC6 still requires approved live Zotero 10 write, partial-failure, and rollback evidence. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. +The steward workload now has a deterministic local worksheet contract rather than an informal report-editing step. 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 separate owner-only report. This stays inside ConceptWeave because no independent cross-product review utility contract exists. + 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 3a9dc1f0fd1081d8ca7f0c2b932128f060680c44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:38:31 +0900 Subject: [PATCH 04/16] test(zotero): cover invalid worksheet reports --- .../tests/steward_review_worksheet.rs | 85 ++++++++++++++++++- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index c76c6dbe..d9dfd8f9 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -18,14 +18,18 @@ fn item(key: &str, title: &str) -> ZoteroItem { } } -#[test] -fn worksheet_is_snapshot_bound_complete_and_contains_no_bibliographic_text() { - let report = classify_snapshot( +fn report() -> conceptweave_zotero::ClassificationReport { + classify_snapshot( "9.0.6".into(), None, 42, vec![item("B", "unmatched title"), item("A", "ontology learning")], - ); + ) +} + +#[test] +fn worksheet_is_snapshot_bound_complete_and_contains_no_bibliographic_text() { + let report = report(); let worksheet = build_steward_review_worksheet(&report).unwrap(); @@ -54,6 +58,79 @@ fn worksheet_is_snapshot_bound_complete_and_contains_no_bibliographic_text() { assert!(!serialized.contains("field_values")); } +#[test] +fn worksheet_rejects_each_inconsistent_report_coordinate() { + let mut invalid = report(); + invalid.rule_revision = ""; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.snapshot_digest.clear(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.observed_item_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.bibliographic_item_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.proposed_disposition_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.snapshot_items[0].item_key.clear(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.snapshot_items[1].item_key = invalid.snapshot_items[0].item_key.clone(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[0].item_key.clear(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[1].item_key = invalid.classified_items[0].item_key.clone(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[0].item_version += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); +} + #[test] fn worksheet_rejects_a_report_with_duplicate_source_identity() { let report = classify_snapshot( From f34cf2421e2f069586db148c5930ffcd0f20e890 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:40:42 +0900 Subject: [PATCH 05/16] fix(zotero): reject contradictory review reports --- crates/conceptweave-zotero/src/lib.rs | 32 ++++++++++ .../tests/steward_review_worksheet.rs | 64 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 404dddc9..9c87e61f 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1079,11 +1079,41 @@ impl std::error::Error for WorksheetError {} pub fn build_steward_review_worksheet( report: &ClassificationReport, ) -> Result { + let disposition_counts = report.classified_items.iter().fold( + BTreeMap::new(), + |mut counts, item| { + *counts.entry(item.proposed_disposition).or_insert(0) += 1; + counts + }, + ); + let provenance_complete_count = report + .classified_items + .iter() + .filter(|item| { + !item.item_key.trim().is_empty() + && item + .child_item_keys + .iter() + .all(|child_key| !child_key.trim().is_empty()) + }) + .count(); + let abstention_count = report + .classified_items + .iter() + .filter(|item| item.proposed_disposition == Disposition::NeedsStewardReview) + .count(); if report.rule_revision.trim().is_empty() || report.snapshot_digest.trim().is_empty() || report.snapshot_items.len() != report.observed_item_count + || report.audit_summary.snapshot_item_count != report.observed_item_count || report.classified_items.len() != report.audit_summary.bibliographic_item_count || report.classified_items.len() != report.audit_summary.proposed_disposition_count + || report.audit_summary.provenance_complete_count != provenance_complete_count + || provenance_complete_count != report.classified_items.len() + || report.audit_summary.abstention_count != abstention_count + || report.audit_summary.duplicate_candidate_count != report.duplicate_candidates.len() + || report.audit_summary.failure_count != 0 + || report.audit_summary.disposition_counts != disposition_counts { return Err(WorksheetError::InvalidReport); } @@ -1105,6 +1135,8 @@ pub fn build_steward_review_worksheet( if item.item_key.trim().is_empty() || !decision_keys.insert(item.item_key.as_str()) || snapshot_versions.get(item.item_key.as_str()) != Some(&item.item_version) + || (item.proposed_disposition == Disposition::NeedsStewardReview) + != item.abstention_reason.is_some() { return Err(WorksheetError::InvalidReport); } diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index d9dfd8f9..1e1fb109 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -95,6 +95,55 @@ fn worksheet_rejects_each_inconsistent_report_coordinate() { Err(WorksheetError::InvalidReport) ); + let mut invalid = report(); + invalid.audit_summary.snapshot_item_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.provenance_complete_count -= 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.abstention_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.duplicate_candidate_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.failure_count = 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.disposition_counts.clear(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[0].child_item_keys = vec![String::new()]; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + let mut invalid = report(); invalid.snapshot_items[0].item_key.clear(); assert_eq!( @@ -129,6 +178,21 @@ fn worksheet_rejects_each_inconsistent_report_coordinate() { build_steward_review_worksheet(&invalid), Err(WorksheetError::InvalidReport) ); + + let mut invalid = report(); + invalid.classified_items[0].abstention_reason = None; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[1].abstention_reason = + Some(conceptweave_zotero::AbstentionReason::NoDeterministicRuleMatch); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); } #[test] From c401f5ed9fbcfbcd96d7697a050229d3b67f06c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:40:53 +0900 Subject: [PATCH 06/16] style(zotero): format worksheet validation --- crates/conceptweave-zotero/src/lib.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 9c87e61f..76721647 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1079,13 +1079,14 @@ impl std::error::Error for WorksheetError {} pub fn build_steward_review_worksheet( report: &ClassificationReport, ) -> Result { - let disposition_counts = report.classified_items.iter().fold( - BTreeMap::new(), - |mut counts, item| { - *counts.entry(item.proposed_disposition).or_insert(0) += 1; - counts - }, - ); + let disposition_counts = + report + .classified_items + .iter() + .fold(BTreeMap::new(), |mut counts, item| { + *counts.entry(item.proposed_disposition).or_insert(0) += 1; + counts + }); let provenance_complete_count = report .classified_items .iter() From 6564e29563e7732c9fb4297981b60b60f31f73d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:41:34 +0900 Subject: [PATCH 07/16] test(zotero): select worksheet cases by disposition --- .../tests/steward_review_worksheet.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index 1e1fb109..7a8d55c3 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -180,15 +180,24 @@ fn worksheet_rejects_each_inconsistent_report_coordinate() { ); let mut invalid = report(); - invalid.classified_items[0].abstention_reason = None; + invalid + .classified_items + .iter_mut() + .find(|item| item.proposed_disposition == Disposition::NeedsStewardReview) + .unwrap() + .abstention_reason = None; assert_eq!( build_steward_review_worksheet(&invalid), Err(WorksheetError::InvalidReport) ); let mut invalid = report(); - invalid.classified_items[1].abstention_reason = - Some(conceptweave_zotero::AbstentionReason::NoDeterministicRuleMatch); + invalid + .classified_items + .iter_mut() + .find(|item| item.proposed_disposition != Disposition::NeedsStewardReview) + .unwrap() + .abstention_reason = Some(conceptweave_zotero::AbstentionReason::NoDeterministicRuleMatch); assert_eq!( build_steward_review_worksheet(&invalid), Err(WorksheetError::InvalidReport) From cef73745c008e3191fbc0c22bc625e813fbf369b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:41:57 +0900 Subject: [PATCH 08/16] fix(zotero): import worksheet disposition contract --- crates/conceptweave-zotero/tests/steward_review_worksheet.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index 7a8d55c3..9c248350 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -1,5 +1,6 @@ use conceptweave_zotero::{ - ItemData, WorksheetError, ZoteroItem, build_steward_review_worksheet, classify_snapshot, + Disposition, ItemData, WorksheetError, ZoteroItem, build_steward_review_worksheet, + classify_snapshot, }; fn item(key: &str, title: &str) -> ZoteroItem { From eeb0797a0f5d2f06f457dbd90cc40bd8842a41bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:42:37 +0900 Subject: [PATCH 09/16] test(zotero): exercise incomplete worksheet provenance --- crates/conceptweave-zotero/tests/steward_review_worksheet.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index 9c248350..40fb2756 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -140,6 +140,7 @@ fn worksheet_rejects_each_inconsistent_report_coordinate() { let mut invalid = report(); invalid.classified_items[0].child_item_keys = vec![String::new()]; + invalid.audit_summary.provenance_complete_count -= 1; assert_eq!( build_steward_review_worksheet(&invalid), Err(WorksheetError::InvalidReport) @@ -161,6 +162,7 @@ fn worksheet_rejects_each_inconsistent_report_coordinate() { let mut invalid = report(); invalid.classified_items[0].item_key.clear(); + invalid.audit_summary.provenance_complete_count -= 1; assert_eq!( build_steward_review_worksheet(&invalid), Err(WorksheetError::InvalidReport) From 79228e35633c4fe873ed0f01ff658983aa138eec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:43:16 +0900 Subject: [PATCH 10/16] refactor(zotero): remove unreachable worksheet guard --- crates/conceptweave-zotero/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 76721647..03a63b81 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1133,8 +1133,7 @@ pub fn build_steward_review_worksheet( let mut decision_keys = BTreeSet::new(); let mut decisions = Vec::with_capacity(report.classified_items.len()); for item in &report.classified_items { - if item.item_key.trim().is_empty() - || !decision_keys.insert(item.item_key.as_str()) + if !decision_keys.insert(item.item_key.as_str()) || snapshot_versions.get(item.item_key.as_str()) != Some(&item.item_version) || (item.proposed_disposition == Disposition::NeedsStewardReview) != item.abstention_reason.is_some() From b92bc57bccd97612ddf7f8851c9b98fc3c683eed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:00:23 +0900 Subject: [PATCH 11/16] test(research): adopt captured-source worksheet fixture Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/tests/steward_review_worksheet.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index 40fb2756..19a5050e 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -5,6 +5,7 @@ use conceptweave_zotero::{ fn item(key: &str, title: &str) -> ZoteroItem { ZoteroItem { + source_record: None, key: key.into(), version: 7, data: ItemData { From 900038e93ccc56531a70d565917ef92d195c2180 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:49:21 +0900 Subject: [PATCH 12/16] test(research): reproduce worksheet source inventory admission gaps --- .../tests/steward_review_worksheet.rs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index 19a5050e..660b6709 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -29,6 +29,59 @@ fn report() -> conceptweave_zotero::ClassificationReport { ) } +#[test] +fn worksheet_preserves_complete_inventory_without_requiring_source_resolution() { + for parent_key in ["", "missing", "source", "A"] { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + source_item.data.parent_item = parent_key.into(); + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "ontology learning"), source_item], + ); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + assert_eq!(worksheet.snapshot_items, report.snapshot_items); + assert_eq!(worksheet.decisions.len(), 1); + assert_eq!(worksheet.decisions[0].reviewed_disposition, None); + } +} + +#[test] +fn worksheet_rejects_omitted_retained_source_inventory() { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + let mut report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "ontology learning"), source_item], + ); + report.unclassified_items.clear(); + assert_eq!( + build_steward_review_worksheet(&report), + Err(WorksheetError::InvalidReport) + ); +} + +#[test] +fn worksheet_rejects_hidden_pending_sources() { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + let mut report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "ontology learning"), source_item], + ); + report.pending_source_item_keys.clear(); + assert_eq!( + build_steward_review_worksheet(&report), + Err(WorksheetError::InvalidReport) + ); +} + #[test] fn worksheet_is_snapshot_bound_complete_and_contains_no_bibliographic_text() { let report = report(); From 5b54d062aa605bf454f7886e768469601b2dd10f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:50:02 +0900 Subject: [PATCH 13/16] fix(research): reuse complete report validation for blank worksheets --- crates/conceptweave-zotero/src/lib.rs | 58 ++------------------------- 1 file changed, 4 insertions(+), 54 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 6f739fc1..57e50393 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1155,64 +1155,14 @@ impl std::error::Error for WorksheetError {} pub fn build_steward_review_worksheet( report: &ClassificationReport, ) -> Result { - let disposition_counts = - report - .classified_items - .iter() - .fold(BTreeMap::new(), |mut counts, item| { - *counts.entry(item.proposed_disposition).or_insert(0) += 1; - counts - }); - let provenance_complete_count = report - .classified_items - .iter() - .filter(|item| { - !item.item_key.trim().is_empty() - && item - .child_item_keys - .iter() - .all(|child_key| !child_key.trim().is_empty()) - }) - .count(); - let abstention_count = report - .classified_items - .iter() - .filter(|item| item.proposed_disposition == Disposition::NeedsStewardReview) - .count(); - if report.rule_revision.trim().is_empty() - || report.snapshot_digest.trim().is_empty() - || report.snapshot_items.len() != report.observed_item_count - || report.audit_summary.snapshot_item_count != report.observed_item_count - || report.classified_items.len() != report.audit_summary.bibliographic_item_count - || report.classified_items.len() != report.audit_summary.proposed_disposition_count - || report.audit_summary.provenance_complete_count != provenance_complete_count - || provenance_complete_count != report.classified_items.len() - || report.audit_summary.abstention_count != abstention_count - || report.audit_summary.duplicate_candidate_count != report.duplicate_candidates.len() - || report.audit_summary.failure_count != 0 - || report.audit_summary.disposition_counts != disposition_counts - { + validate_classification_report(report).map_err(|_| WorksheetError::InvalidReport)?; + if report.rule_revision.trim().is_empty() || report.snapshot_digest.trim().is_empty() { return Err(WorksheetError::InvalidReport); } - - let mut snapshot_versions = BTreeMap::new(); - for item in &report.snapshot_items { - if item.item_key.trim().is_empty() - || snapshot_versions - .insert(item.item_key.as_str(), item.item_version) - .is_some() - { - return Err(WorksheetError::InvalidReport); - } - } - - let mut decision_keys = BTreeSet::new(); let mut decisions = Vec::with_capacity(report.classified_items.len()); for item in &report.classified_items { - if !decision_keys.insert(item.item_key.as_str()) - || snapshot_versions.get(item.item_key.as_str()) != Some(&item.item_version) - || (item.proposed_disposition == Disposition::NeedsStewardReview) - != item.abstention_reason.is_some() + if (item.proposed_disposition == Disposition::NeedsStewardReview) + != item.abstention_reason.is_some() { return Err(WorksheetError::InvalidReport); } From 30aa0914cb287ebaa676da2ba708fdc27a166c1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:51:18 +0900 Subject: [PATCH 14/16] test(research): reproduce unbound worksheet content identity --- .../tests/steward_review_worksheet.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs index 660b6709..143edfa7 100644 --- a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -29,6 +29,43 @@ fn report() -> conceptweave_zotero::ClassificationReport { ) } +#[test] +fn worksheet_identity_binds_review_context_and_retained_source_metadata() { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + let mut report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "unmatched"), source_item], + ); + let original = build_steward_review_worksheet(&report).unwrap(); + report.unclassified_items[0].data.title.push_str(" changed"); + let source_changed = build_steward_review_worksheet(&report).unwrap(); + assert_ne!(original, source_changed); + report.classified_items[0].review_abstract_note = Some("changed review context".into()); + let context_changed = build_steward_review_worksheet(&report).unwrap(); + assert_ne!(source_changed, context_changed); + let serialized = serde_json::to_value(&context_changed).unwrap(); + assert_eq!( + serialized["proposal_digest"], + conceptweave_zotero::classification_proposal_digest(&report) + ); +} + +#[test] +fn worksheet_cannot_deserialize_without_proposal_binding() { + let worksheet = build_steward_review_worksheet(&report()).unwrap(); + let mut serialized = serde_json::to_value(&worksheet).unwrap(); + serialized + .as_object_mut() + .unwrap() + .remove("proposal_digest"); + assert!( + serde_json::from_value::(serialized).is_err() + ); +} + #[test] fn worksheet_preserves_complete_inventory_without_requiring_source_resolution() { for parent_key in ["", "missing", "source", "A"] { From 97046c78094167303e1ecbcdd6c90af195295d57 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:51:43 +0900 Subject: [PATCH 15/16] fix(research): bind blank worksheet to complete proposal scope --- crates/conceptweave-zotero/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 57e50393..4059586e 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1130,6 +1130,9 @@ pub struct StewardReviewWorksheet { pub rule_revision: String, /// Canonical content digest of the complete raw snapshot. pub snapshot_digest: String, + /// Versioned identity of proposals, review context and retained source scope. + /// Older worksheets require regeneration; this binding is not approval. + pub proposal_digest: String, /// Complete parent and child item-revision coordinates. pub snapshot_items: Vec, /// Deterministically ordered editable decisions for bibliographic items. @@ -1152,6 +1155,9 @@ impl fmt::Display for WorksheetError { impl std::error::Error for WorksheetError {} /// Builds a complete local worksheet without copying titles, abstracts, or evidence. +/// +/// Complete inventory validation precedes projection. Valid unresolved sources +/// may be reviewed, but worksheet creation never proves completed review. pub fn build_steward_review_worksheet( report: &ClassificationReport, ) -> Result { @@ -1180,6 +1186,7 @@ pub fn build_steward_review_worksheet( library_version: report.library_version, rule_revision: report.rule_revision.into(), snapshot_digest: report.snapshot_digest.clone(), + proposal_digest: classification_proposal_digest(report), snapshot_items: report.snapshot_items.clone(), decisions, }) From 51631fbf711b403a40f3b9fafa2ec3958d54ceaf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:53:36 +0900 Subject: [PATCH 16/16] docs(research): record worksheet scope evidence and downstream adoption --- docs/PRD.md | 2 +- docs/TRD.md | 2 +- docs/adr/0006-zotero-research-intake.md | 4 ++++ docs/product-technical-gap-baseline.md | 10 ++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index 1bbda55e..f8963801 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -86,7 +86,7 @@ PR #21 retains validated delayed reads without writes and complete observed meta The Zotero 10+ adapter can accept a caller-owned API key and server identity at runtime or consume one successful, user-approved Local API authorization. Authorization sends one bounded application name and the expected server identity to the fixed loopback endpoint; only a same-server bounded response that explicitly reports denial is classified as the user's decision. Denial and rate limiting return immediately without another prompt or automatic retry. The private 32-character key is neither serializable nor printable. Authorization, read, and write responses bind to the expected server before status classification; writes name expired authorization and matching-server stale preconditions separately. Thin public execution boundaries connect the adapter to the reviewed write and rollback cores without duplicating mutation logic. Synthetic transport evidence does not satisfy AC6's approved live Zotero 10 authorization, write, and rollback requirement. -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. +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. 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 0b9691ec..37033816 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -109,7 +109,7 @@ Provider deserialization captures each complete JSON object before projecting me The complete metadata-review evaluator rejects unequal label cardinality or nonempty `pending_source_item_keys` with `IncompleteReview` before governance. The shared evaluator then recomputes the complete inventory and pending ancestry, so clearing pending keys and rewriting the proposal digest still fails local validation. Because shared validation rejects blank, duplicate, and unknown keys, equal cardinality proves bibliographic label coverage. Sampled evaluation still supports pending sources; completion does not prove a Zotero mutation or full-text approval. 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. 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, or proposal counts. It deliberately omits bibliographic text and matched evidence; stewards consult the owner-only report by item key. +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 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 b299a0e8..7d92be5f 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -26,6 +26,10 @@ Every successful report includes an aggregate audit summary computed from the sa To make full review executable without copying sensitive text again, ConceptWeave derives a local worksheet from the validated report. The worksheet carries the exact snapshot binding, complete parent/child revision coordinates, deterministic proposal and abstention reason, plus one empty steward-decision slot per bibliographic item in item-key order. Titles, abstracts, tags, collections, and matched evidence remain only in the owner-only report. Invalid report identity or coverage fails worksheet construction. +Proposed worksheet source-scope amendment: in the context of continuing review across saved local worksheets, facing removed retained records or changed context at unchanged item revisions, we decided for the existing shared report validator and required v2 proposal digest, and against a duplicate worksheet audit or coordinate-only identity, to preserve complete source scope and distinguish changed review material, accepting a breaking serialized-field requirement and downstream comparison work. RED `900038e` reproduces omitted inventory and hidden pending keys; `5b54d06` removes the divergent checks in favor of the shared validator. RED `30aa091` reproduces equal worksheets for changed content and successful loading without binding; `97046c7` reuses the existing scope hash. Valid standalone, orphan and cyclic evidence still permits blank worksheet creation, because preparation is not completion. No source metadata is copied into a new artifact. + +Older worksheets require regeneration, not a fabricated digest or approval. The later progress, patch and finalization owners must compare the worksheet's digest against the report, including rejecting blank or locally rewritten values, before external approval verification. Those later consumers have not yet adopted this field at this checkpoint. A valid hash proves identity rather than authorship or authorization, and no metadata worksheet downcast grants full-text or Zotero write authority. Keep this amendment Proposed until protected owner/consumer and release evidence is available; the Gap baseline records exact tests and remaining live work. + The reader fails closed above 50,000 items or 256 MiB of cumulative response bodies, while retaining the 8 MiB per-page bound, finite request timeouts, redirect denial, total-count checks, snapshot-version checks, and duplicate-key detection. Pagination, consistency, resource-budget, and provider-contract behavior are separated from the narrow `ureq` transport so deterministic tests exercise the production reader core rather than excluding the entire reader from coverage. In the context of reading every bibliographic source before classification, facing individually timely pages that can cumulatively hold a run open for days, we decided for a five-minute monotonic admission/completion budget in the existing reader and against rejecting legitimate short pages or adding another transport, to bound accepted work without excluding papers, accepting that an already-started request or classification computation can finish after the limit before its result is rejected. This is an application read limit, not a model timeout, hard process-cancellation deadline, wall-clock/suspend guarantee or atomic snapshot claim. Each page is checked before fetch and after return, and the complete report is checked before return. The stdlib clock has a private deterministic test seam; public APIs, provider timeouts and data/byte ceilings are unchanged. The [deadline doctoring](../doctoring/zotero_metadata_deadline.md) records the original review, RED/GREEN, alternatives and exact verification. This amendment remains Proposed and grants no Zotero mutation authority. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 10ddb3ef..18a8a441 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 +### PR25 worksheet admission and identity repair + +Original PR25 `c6b4c17e931951a2e1d4ea79ac79363f6306a5bf` passed 126 tests/24 suites. Normal merge `4a1a3bb` retains it and PR24 `35c57ca4510a65cf48069285d78b95cf47db65ba`; integrated tests passed 153/24. RED `900038e` compiled with 4 passing and 2 failing tests: omitted retained inventory and hidden pending keys still produced worksheets. `5b54d06` reuses shared report validation and removes 54 lines of divergent audit/coordinate checks. Valid standalone, orphan, cyclic and attached source metadata remains reviewable with blank decisions. Completion admission remains distinct. + +RED `30aa091` compiled with 6 passing and 2 failing tests: changed retained metadata produced an equal worksheet and missing proposal binding deserialized successfully. Final source `97046c7` adds a required worksheet `proposal_digest` from the existing v2 hash; source/context changes produce different identities and missing binding fails loading. No new hash, library or authority issuer was introduced. Independent read-only review found no additional production regression. Required downstream repair: compare worksheet/report binding before progress, patch and finalization, including blank or locally rewritten digest rejection; never backfill older worksheets or infer full-text authority. + +Final local tests are 158/24 suites including three doctests, strict Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks pass. Unchanged pinned coverage passes 282/282 functions, 2,468/2,468 normalized regions and 414/414 normalized branches. Raw coverage remains 3,232/3,293 lines, 4,858/4,960 regions and 369/414 branches, not 100%. Logs: `/tmp/conceptweave-pr25-{baseline,integration,source-red,binding-red,final,clippy-final,rustdoc-final,coverage-final}.log`. + +The root runtime and later consumers have not adopted this repair. Actual decisions and independent approvals remain 0/3,715 plus four unresolved standalone sources. No real Zotero record, classification label or approval was created. Native Visual Inspection was attempted again but the Mac is locked, so there is no new screenshot evidence. PRD/TRD and Proposed ADR 0006 record scope, compatibility and remaining adoption. Local verification is not hosted current-head GREEN, protected approval/merge or release. + ### PR24 pending-source completion repair Final source `5b2282a` passes strict all-target Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks. The unchanged pinned coverage gate passes 279/279 functions, 2,420/2,420 normalized regions and 408/408 normalized branches. Raw coverage remains 3,198/3,259 lines, 4,810/4,912 regions and 363/408 branches, not 100%. Logs: `/tmp/conceptweave-pr24-{clippy,rustdoc,coverage}-verified.log`. No predecessor or later-head coverage is attributed to this source.