From f8600f355c8954e04396034e887a58f056cbdde7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:35:59 +0900 Subject: [PATCH 01/16] experiment: add classification audit summary Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/src/lib.rs | 50 ++++++++++++++++++- .../tests/golden_set_evaluation.rs | 26 ++++++++++ docs/PRD.md | 2 + docs/TRD.md | 2 + docs/adr/0006-zotero-research-intake.md | 2 + docs/product-technical-gap-baseline.md | 2 +- 6 files changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 0e9f239b..a21cd157 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -170,6 +170,29 @@ pub struct ClassificationReport { pub classified_items: Vec, /// Reversible DOI/title duplicate candidates. pub duplicate_candidates: Vec, + /// Aggregate completeness evidence for this successful snapshot. + pub audit_summary: ClassificationAudit, +} + +/// Aggregate-only evidence that a successful report covers its input and proposals. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct ClassificationAudit { + /// Records captured from the immutable snapshot. + pub snapshot_item_count: usize, + /// Top-level bibliographic records eligible for classification. + pub bibliographic_item_count: usize, + /// Eligible records with exactly one proposed disposition. + pub proposed_disposition_count: usize, + /// Proposals retaining required item and classifier provenance. + pub provenance_complete_count: usize, + /// Proposals routed to steward review. + pub abstention_count: usize, + /// Reversible duplicate identity groups. + pub duplicate_candidate_count: usize, + /// Reader or classifier failures; successful reports always record zero. + pub failure_count: usize, + /// Proposal totals by disposition. + pub disposition_counts: BTreeMap, } /// One steward-reviewed expected disposition in a local golden set. @@ -574,12 +597,36 @@ pub fn classify_snapshot( let children = child_index(&items); let bibliographic: Vec<&ZoteroItem> = items.iter().filter(|item| is_bibliographic(item)).collect(); + let bibliographic_item_count = bibliographic.len(); let duplicate_candidates = duplicate_candidates(&bibliographic); - let classified_items = bibliographic + let classified_items: Vec = bibliographic .into_iter() .map(|item| classify_item(item, children.get(&item.key).cloned().unwrap_or_default())) .collect(); + let mut disposition_counts = BTreeMap::new(); + for item in &classified_items { + *disposition_counts + .entry(item.proposed_disposition) + .or_insert(0) += 1; + } + let audit_summary = ClassificationAudit { + snapshot_item_count: items.len(), + bibliographic_item_count, + proposed_disposition_count: classified_items.len(), + provenance_complete_count: classified_items + .iter() + .filter(|item| !item.item_key.trim().is_empty() && item.item_version > 0) + .count(), + abstention_count: classified_items + .iter() + .filter(|item| item.proposed_disposition == Disposition::NeedsStewardReview) + .count(), + duplicate_candidate_count: duplicate_candidates.len(), + failure_count: 0, + disposition_counts, + }; + ClassificationReport { zotero_version, api_version: None, @@ -590,6 +637,7 @@ pub fn classify_snapshot( observed_item_count: items.len(), classified_items, duplicate_candidates, + audit_summary, } } diff --git a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs index 70ce1709..a33161f0 100644 --- a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs +++ b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs @@ -44,6 +44,20 @@ fn golden(labels: Vec) -> ReviewedGoldenSet { #[test] fn reviewed_golden_set_reports_count_based_precision_and_recall_evidence() { let report = report(); + assert_eq!(report.audit_summary.snapshot_item_count, 3); + assert_eq!(report.audit_summary.bibliographic_item_count, 3); + assert_eq!(report.audit_summary.proposed_disposition_count, 3); + assert_eq!(report.audit_summary.provenance_complete_count, 3); + assert_eq!(report.audit_summary.abstention_count, 1); + assert_eq!(report.audit_summary.failure_count, 0); + assert_eq!( + report + .audit_summary + .disposition_counts + .values() + .sum::(), + 3 + ); let evaluation = evaluate_reviewed_golden_set( &report, &golden(vec![ @@ -153,4 +167,16 @@ fn reviewed_golden_set_rejects_stale_unknown_and_duplicate_labels() { ] { assert!(error.to_string().contains(fragment)); } + + let mut incomplete = item("A", "ontology learning"); + incomplete.version = 0; + let incomplete_report = classify_snapshot("9.0.6".into(), None, 42, vec![incomplete]); + assert_eq!(incomplete_report.audit_summary.provenance_complete_count, 0); + let blank_key_report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item(" ", "ontology learning")], + ); + assert_eq!(blank_key_report.audit_summary.provenance_complete_count, 0); } diff --git a/docs/PRD.md b/docs/PRD.md index a31916ee..bd22ac98 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -60,6 +60,8 @@ Read one immutable Zotero Local API library-version snapshot and propose exactly Evaluate classifier quality only against a version-bound, steward-reviewed local golden set. Evaluation emits aggregate counts for exact matches, abstentions, and per-disposition true-positive/predicted/expected totals; it must not copy Zotero keys or bibliographic text into the result. +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 Relational schema snapshot -> observed tables/columns/foreign keys -> concept/relation/dimension/measure/mapping candidates -> evidence-bound validation report -> reviewable proposal package. diff --git a/docs/TRD.md b/docs/TRD.md index 40f32201..803b8ab4 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -65,4 +65,6 @@ Every top-level bibliographic record receives exactly one proposed disposition. Golden-set evaluation accepts only a non-empty review receipt whose library version and rule revision exactly match the classification report. Blank, duplicate, or unknown item keys and duplicate report identities fail closed. The output contains no item keys or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. +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. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. + The report is local JSON and contains proposals rather than governance decisions. CLI output is restricted to a new direct child of canonical `/tmp` or the operating system temporary directory; relative paths, nested paths, existing paths, and symlinks are rejected, and create-new file semantics prevent overwrite/path-swap writes. Zotero 9 writes are unsupported; no mutation path exists in this slice. A future Zotero 10+ writer requires a separate reviewed change with a Local API key, stable server identity, fresh item/library version preconditions, item-by-item before/after receipts, and rollback evidence. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index 18aeba36..5eccabb4 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -21,6 +21,8 @@ Matched metadata values are copied into the local-only evidence receipt for repl Classifier quality is measured only against local steward-reviewed labels bound to the same library and rule revisions. Evaluation returns aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys and bibliographic text are deliberately omitted from evaluation output. Missing, stale, unknown, or duplicate review identities fail closed. +Every successful report includes an aggregate audit summary computed from the same captured snapshot. Partial reads never produce a report, so successful output explicitly records zero failures alongside snapshot, proposal, provenance, abstention, duplicate, and per-disposition totals. + 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 a0165ff9..eea84641 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -44,7 +44,7 @@ Protected central source is `.github/main@c31d2e5471fc5daf9d72ff67cde6a8874b736d Local evidence on 2026-09-04 showed Zotero 9.0.6, Local API v3/schema 42, library version 12341, 8,326 total items, and 3,719 top-level items. The corrected read-only run observed all 8,326 records at that single version and classified all 3,715 top-level bibliographic records; four top-level note/attachment/annotation records were correctly excluded. It proposed 56 adjacent-evidence records, 1 semantic-consumption bridge, and 3,658 steward-review abstentions, linked children for 3,287 records, and surfaced 49 reversible duplicate groups (18 DOI, 31 title). No live record matched multiple specific disposition families; the tested conflict path still abstains fail-closed. Token-boundary matching prevents strings such as `knowledge` from becoming false OWL evidence. These are local aggregate observations, not reviewed truth or applied Zotero changes. The report stays outside the repository. -The golden-set evaluation contract now records aggregate precision/recall numerators and denominators, rejects stale or malformed review sets, and omits item identities from its output. Synthetic fixtures verify the contract; no real precision/recall claim exists until a steward supplies reviewed local labels. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. Zotero write-back remains blocked by the installed v9 capability; a Zotero 10+ change must satisfy ADR 0006 preconditions. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. +The golden-set evaluation contract now records aggregate precision/recall numerators and denominators, rejects stale or malformed review sets, and omits item identities from its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Synthetic fixtures verify both contracts; no real precision/recall claim exists until a steward supplies reviewed local labels. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. Zotero write-back remains blocked by the installed v9 capability; a Zotero 10+ change must satisfy ADR 0006 preconditions. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. 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. 2. **Ontology discovery** — deterministic term/concept/taxonomy/non-taxonomic-relation candidate generation with exact source receipts and abstention for unsupported semantics. From a520107b5f72d2dad52303cc1593b3f492b00d0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:43:29 +0900 Subject: [PATCH 02/16] experiment: bind golden evaluation to verified snapshot Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/src/lib.rs | 84 +++++++++++++--- .../tests/golden_set_evaluation.rs | 97 +++++++++++++++---- docs/PRD.md | 2 +- docs/TRD.md | 2 +- docs/adr/0006-zotero-research-intake.md | 2 +- docs/product-technical-gap-baseline.md | 2 +- 6 files changed, 151 insertions(+), 38 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index a21cd157..2205b539 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -217,14 +217,34 @@ impl GoldenLabel { /// Version-bound steward labels that remain outside the repository. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct ReviewedGoldenSet { - /// Opaque review receipt identifier. - pub review_id: String, + /// Approval receipt verified by the caller's governance boundary. + pub approval: GoldenSetApproval, + /// Item-level expected dispositions. + pub labels: Vec, +} + +/// One item revision in the exact reviewed classification snapshot. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub struct SnapshotItemRevision { + /// Stable Zotero item key. + pub item_key: String, + /// Item revision observed during review. + pub item_version: u64, +} + +/// Governance receipt binding a steward approval to one exact classifier input. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct GoldenSetApproval { + /// Opaque receipt identifier. + pub receipt_id: String, + /// Stable reviewer subject understood by the governance verifier. + pub reviewer_subject: String, /// Zotero library version reviewed by the steward. pub library_version: u64, /// Classifier rule revision whose proposals were reviewed. pub rule_revision: String, - /// Item-level expected dispositions. - pub labels: Vec, + /// Complete sorted item-revision identity of the reviewed report. + pub snapshot_items: Vec, } /// Integer evidence from which precision and recall can be calculated exactly. @@ -260,6 +280,10 @@ pub enum EvaluationError { InvalidReview, /// The golden set was reviewed against another library or rule revision. SnapshotMismatch, + /// The caller's governance boundary did not verify the approval receipt. + UnverifiedApproval, + /// Abstention cannot be used as steward-approved semantic truth. + InvalidExpectedDisposition, /// A reviewed key is absent from the classification report. UnknownItem, /// A reviewed key occurs more than once. @@ -271,6 +295,10 @@ impl fmt::Display for EvaluationError { formatter.write_str(match self { Self::InvalidReview => "golden-set review metadata or labels are invalid", Self::SnapshotMismatch => "golden set does not match the report snapshot", + Self::UnverifiedApproval => "golden-set approval receipt is unverified", + Self::InvalidExpectedDisposition => { + "steward truth cannot use the classifier abstention disposition" + } Self::UnknownItem => "golden set contains an item absent from the report", Self::DuplicateItem => "golden set contains a duplicate item", }) @@ -280,18 +308,46 @@ impl fmt::Display for EvaluationError { impl std::error::Error for EvaluationError {} /// Evaluates reviewed labels without copying item identities into the result. -pub fn evaluate_reviewed_golden_set( +pub fn evaluate_reviewed_golden_set( report: &ClassificationReport, golden: &ReviewedGoldenSet, -) -> Result { - if golden.review_id.trim().is_empty() + verify_approval: F, +) -> Result +where + F: FnOnce(&GoldenSetApproval) -> bool, +{ + if golden.approval.receipt_id.trim().is_empty() + || golden.approval.reviewer_subject.trim().is_empty() || golden.labels.is_empty() - || golden.rule_revision.trim().is_empty() + || golden.approval.rule_revision.trim().is_empty() { return Err(EvaluationError::InvalidReview); } - if golden.library_version != report.library_version - || golden.rule_revision != report.rule_revision + if !verify_approval(&golden.approval) { + return Err(EvaluationError::UnverifiedApproval); + } + let report_snapshot = report + .classified_items + .iter() + .map(|item| SnapshotItemRevision { + item_key: item.item_key.clone(), + item_version: item.item_version, + }) + .collect::>(); + let approved_snapshot = golden + .approval + .snapshot_items + .iter() + .cloned() + .collect::>(); + if report_snapshot.len() != report.classified_items.len() + || approved_snapshot.len() != golden.approval.snapshot_items.len() + { + return Err(EvaluationError::InvalidReview); + } + if golden.approval.library_version != report.library_version + || golden.approval.rule_revision != report.rule_revision + || approved_snapshot != report_snapshot { return Err(EvaluationError::SnapshotMismatch); } @@ -301,9 +357,6 @@ pub fn evaluate_reviewed_golden_set( .iter() .map(|item| (item.item_key.as_str(), item.proposed_disposition)) .collect::>(); - if classified.len() != report.classified_items.len() { - return Err(EvaluationError::InvalidReview); - } let mut seen = BTreeSet::new(); let mut correct_count = 0; let mut abstention_count = 0; @@ -313,6 +366,9 @@ pub fn evaluate_reviewed_golden_set( if label.item_key.trim().is_empty() { return Err(EvaluationError::InvalidReview); } + if label.expected_disposition == Disposition::NeedsStewardReview { + return Err(EvaluationError::InvalidExpectedDisposition); + } if !seen.insert(label.item_key.as_str()) { return Err(EvaluationError::DuplicateItem); } @@ -335,7 +391,7 @@ pub fn evaluate_reviewed_golden_set( } Ok(GoldenSetEvaluation { - review_id: golden.review_id.clone(), + review_id: golden.approval.receipt_id.clone(), reviewed_count: golden.labels.len(), correct_count, abstention_count, diff --git a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs index a33161f0..d73ec192 100644 --- a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs +++ b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs @@ -1,6 +1,6 @@ use conceptweave_zotero::{ - Disposition, EvaluationError, GoldenLabel, ItemData, ReviewedGoldenSet, ZoteroItem, - classify_snapshot, evaluate_reviewed_golden_set, + Disposition, EvaluationError, GoldenLabel, GoldenSetApproval, ItemData, ReviewedGoldenSet, + SnapshotItemRevision, ZoteroItem, classify_snapshot, evaluate_reviewed_golden_set, }; fn item(key: &str, title: &str) -> ZoteroItem { @@ -34,13 +34,27 @@ fn report() -> conceptweave_zotero::ClassificationReport { fn golden(labels: Vec) -> ReviewedGoldenSet { ReviewedGoldenSet { - review_id: "synthetic-review-1".into(), - library_version: 42, - rule_revision: "ontology-research-v2".into(), + approval: GoldenSetApproval { + receipt_id: "synthetic-review-1".into(), + reviewer_subject: "synthetic-steward".into(), + library_version: 42, + rule_revision: "ontology-research-v2".into(), + snapshot_items: ["A", "B", "C"] + .into_iter() + .map(|item_key| SnapshotItemRevision { + item_key: item_key.into(), + item_version: 1, + }) + .collect(), + }, labels, } } +fn verify_synthetic_approval(approval: &GoldenSetApproval) -> bool { + approval.receipt_id == "synthetic-review-1" && approval.reviewer_subject == "synthetic-steward" +} + #[test] fn reviewed_golden_set_reports_count_based_precision_and_recall_evidence() { let report = report(); @@ -65,6 +79,7 @@ fn reviewed_golden_set_reports_count_based_precision_and_recall_evidence() { GoldenLabel::new("B", Disposition::AlignmentVersioning), GoldenLabel::new("C", Disposition::Generation), ]), + verify_synthetic_approval, ) .unwrap(); @@ -91,44 +106,82 @@ fn reviewed_golden_set_rejects_stale_unknown_and_duplicate_labels() { let report = report(); assert_eq!( - evaluate_reviewed_golden_set(&report, &golden(vec![])), + evaluate_reviewed_golden_set(&report, &golden(vec![]), verify_synthetic_approval), Err(EvaluationError::InvalidReview) ); let mut blank = golden(vec![GoldenLabel::new(" ", Disposition::Generation)]); - blank.review_id.clear(); + blank.approval.receipt_id.clear(); + assert_eq!( + evaluate_reviewed_golden_set(&report, &blank, verify_synthetic_approval), + Err(EvaluationError::InvalidReview) + ); + blank.approval.receipt_id = "synthetic-review-1".into(); assert_eq!( - evaluate_reviewed_golden_set(&report, &blank), + evaluate_reviewed_golden_set(&report, &blank, verify_synthetic_approval), Err(EvaluationError::InvalidReview) ); - blank.review_id = "synthetic-review-1".into(); + blank.approval.reviewer_subject.clear(); assert_eq!( - evaluate_reviewed_golden_set(&report, &blank), + evaluate_reviewed_golden_set(&report, &blank, verify_synthetic_approval), Err(EvaluationError::InvalidReview) ); let mut missing_revision = golden(vec![GoldenLabel::new("A", Disposition::Generation)]); - missing_revision.rule_revision.clear(); + missing_revision.approval.rule_revision.clear(); assert_eq!( - evaluate_reviewed_golden_set(&report, &missing_revision), + evaluate_reviewed_golden_set(&report, &missing_revision, verify_synthetic_approval), Err(EvaluationError::InvalidReview) ); let mut stale = golden(vec![GoldenLabel::new("A", Disposition::Generation)]); - stale.library_version += 1; + stale.approval.library_version += 1; assert_eq!( - evaluate_reviewed_golden_set(&report, &stale), + evaluate_reviewed_golden_set(&report, &stale, verify_synthetic_approval), Err(EvaluationError::SnapshotMismatch) ); - stale.library_version = report.library_version; - stale.rule_revision = "older-rules".into(); + stale.approval.library_version = report.library_version; + stale.approval.rule_revision = "older-rules".into(); assert_eq!( - evaluate_reviewed_golden_set(&report, &stale), + evaluate_reviewed_golden_set(&report, &stale, verify_synthetic_approval), Err(EvaluationError::SnapshotMismatch) ); + stale.approval.rule_revision = report.rule_revision.into(); + stale.approval.snapshot_items[0].item_version += 1; + assert_eq!( + evaluate_reviewed_golden_set(&report, &stale, verify_synthetic_approval), + Err(EvaluationError::SnapshotMismatch) + ); + let mut duplicate_snapshot = golden(vec![GoldenLabel::new("A", Disposition::Generation)]); + duplicate_snapshot + .approval + .snapshot_items + .push(duplicate_snapshot.approval.snapshot_items[0].clone()); + assert_eq!( + evaluate_reviewed_golden_set(&report, &duplicate_snapshot, verify_synthetic_approval), + Err(EvaluationError::InvalidReview) + ); + + assert_eq!( + evaluate_reviewed_golden_set( + &report, + &golden(vec![GoldenLabel::new("A", Disposition::Generation)]), + |_| false + ), + Err(EvaluationError::UnverifiedApproval) + ); + assert_eq!( + evaluate_reviewed_golden_set( + &report, + &golden(vec![GoldenLabel::new("A", Disposition::NeedsStewardReview)]), + verify_synthetic_approval, + ), + Err(EvaluationError::InvalidExpectedDisposition) + ); assert_eq!( evaluate_reviewed_golden_set( &report, - &golden(vec![GoldenLabel::new("missing", Disposition::Generation)]) + &golden(vec![GoldenLabel::new("missing", Disposition::Generation)]), + verify_synthetic_approval, ), Err(EvaluationError::UnknownItem) ); @@ -138,7 +191,8 @@ fn reviewed_golden_set_rejects_stale_unknown_and_duplicate_labels() { &golden(vec![ GoldenLabel::new("A", Disposition::Generation), GoldenLabel::new("A", Disposition::Generation), - ]) + ]), + verify_synthetic_approval, ), Err(EvaluationError::DuplicateItem) ); @@ -155,13 +209,16 @@ fn reviewed_golden_set_rejects_stale_unknown_and_duplicate_labels() { assert_eq!( evaluate_reviewed_golden_set( &duplicate_report, - &golden(vec![GoldenLabel::new("A", Disposition::Generation)]) + &golden(vec![GoldenLabel::new("A", Disposition::Generation)]), + verify_synthetic_approval, ), Err(EvaluationError::InvalidReview) ); for (error, fragment) in [ (EvaluationError::InvalidReview, "invalid"), (EvaluationError::SnapshotMismatch, "snapshot"), + (EvaluationError::UnverifiedApproval, "unverified"), + (EvaluationError::InvalidExpectedDisposition, "abstention"), (EvaluationError::UnknownItem, "absent"), (EvaluationError::DuplicateItem, "duplicate"), ] { diff --git a/docs/PRD.md b/docs/PRD.md index bd22ac98..ac191f3c 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -58,7 +58,7 @@ All LLM-backed induction uses `contextual-orchestrator`. Model output is untrust Read one immutable Zotero Local API library-version snapshot and propose exactly one research disposition for every top-level bibliographic item. Each proposal retains the item key/version, exact matched metadata values, rule revision, linked child records, and any model receipt. Weak evidence and evidence that matches multiple specific disposition families must abstain into steward review. Duplicate DOI/title identities are review candidates only: intake never merges, deletes, or silently mutates Zotero records. -Evaluate classifier quality only against a version-bound, steward-reviewed local golden set. Evaluation emits aggregate counts for exact matches, abstentions, and per-disposition true-positive/predicted/expected totals; it must not copy Zotero keys or bibliographic text into the result. +Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and bound to the complete Zotero item-key/item-version snapshot. Abstention is a prediction outcome, never an approved truth label. Evaluation emits 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. 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 803b8ab4..d8e7857d 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -63,7 +63,7 @@ Evaluation must separate extraction recall, semantic correctness, structural cor Every top-level bibliographic record receives exactly one proposed disposition. `NeedsStewardReview` also records a deterministic abstention reason so missing classification metadata, vocabulary unsupported by the current deterministic rules, and present-but-unmatched metadata are distinguishable. DOI duplicate identity normalization treats bare DOI values, `doi:`, `doi.org`, and legacy `dx.doi.org` resolver forms as the same identity when their normalized DOI is equal. -Golden-set evaluation accepts only a non-empty review receipt whose library version and rule revision exactly match the classification report. Blank, duplicate, or unknown item keys and duplicate report identities fail closed. The output contains no item keys or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. +Golden-set evaluation accepts only a governance receipt verified by a caller-owned authorization boundary. Its library version, rule revision, and complete sorted item-key/item-version identity must exactly match the classification report. Blank, duplicate, unknown, stale, or abstention-as-truth labels fail closed. The output contains no item keys, reviewer identity, or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. Production authorization remains Keyverse/governance-owned; this crate validates the binding and does not mint or self-verify 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. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index 5eccabb4..73bfafe3 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -19,7 +19,7 @@ The adapter links child records, emits exactly one deterministic proposed dispos Matched metadata values are copied into the local-only evidence receipt for replay. This is necessary for abstract-only matches because a later Zotero revision cannot reconstruct the exact text used for an earlier proposal from item key/version alone. DOI/title matches remain reversible duplicate candidates, including legacy `dx.doi.org` resolver forms. -Classifier quality is measured only against local steward-reviewed labels bound to the same library and rule revisions. Evaluation returns aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys and bibliographic text are deliberately omitted from evaluation output. Missing, stale, unknown, or duplicate review identities fail closed. +Classifier quality is measured only against local steward-reviewed labels whose governance receipt is verified outside this crate and bound to the exact library version, rule revision, and complete item-key/item-version snapshot. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Evaluation returns aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys, reviewer identity, and bibliographic text are deliberately omitted from evaluation output. Missing, stale, unverified, unknown, duplicate, or invalid review identities fail closed. Every successful report includes an aggregate audit summary computed from the same captured snapshot. Partial reads never produce a report, so successful output explicitly records zero failures alongside snapshot, proposal, provenance, abstention, duplicate, and per-disposition totals. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index eea84641..b4047b29 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -44,7 +44,7 @@ Protected central source is `.github/main@c31d2e5471fc5daf9d72ff67cde6a8874b736d Local evidence on 2026-09-04 showed Zotero 9.0.6, Local API v3/schema 42, library version 12341, 8,326 total items, and 3,719 top-level items. The corrected read-only run observed all 8,326 records at that single version and classified all 3,715 top-level bibliographic records; four top-level note/attachment/annotation records were correctly excluded. It proposed 56 adjacent-evidence records, 1 semantic-consumption bridge, and 3,658 steward-review abstentions, linked children for 3,287 records, and surfaced 49 reversible duplicate groups (18 DOI, 31 title). No live record matched multiple specific disposition families; the tested conflict path still abstains fail-closed. Token-boundary matching prevents strings such as `knowledge` from becoming false OWL evidence. These are local aggregate observations, not reviewed truth or applied Zotero changes. The report stays outside the repository. -The golden-set evaluation contract now records aggregate precision/recall numerators and denominators, rejects stale or malformed review sets, and omits item identities from its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Synthetic fixtures verify both contracts; no real precision/recall claim exists until a steward supplies reviewed local labels. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. Zotero write-back remains blocked by the installed v9 capability; a Zotero 10+ change must satisfy ADR 0006 preconditions. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. +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 omits item and reviewer identities from its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Synthetic fixtures verify both contracts; no real precision/recall claim exists until a steward supplies reviewed local labels and the production authorization adapter verifies the receipt. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. Zotero write-back remains blocked by the installed v9 capability; a Zotero 10+ change must satisfy ADR 0006 preconditions. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. 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. 2. **Ontology discovery** — deterministic term/concept/taxonomy/non-taxonomic-relation candidate generation with exact source receipts and abstention for unsupported semantics. From eda4d7a28c8944035fcfb803578e324b651c51ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:48:44 +0900 Subject: [PATCH 03/16] fix: retain golden snapshot evaluation identity Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/src/lib.rs | 12 ++++++++++++ .../tests/golden_set_evaluation.rs | 11 +++++++++++ docs/PRD.md | 2 +- docs/TRD.md | 2 +- docs/adr/0006-zotero-research-intake.md | 2 +- docs/product-technical-gap-baseline.md | 2 +- 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 2205b539..a17e8674 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -243,6 +243,8 @@ pub struct GoldenSetApproval { pub library_version: u64, /// Classifier rule revision whose proposals were reviewed. pub rule_revision: String, + /// Immutable digest over the approved snapshot, verified by the caller. + pub snapshot_digest: String, /// Complete sorted item-revision identity of the reviewed report. pub snapshot_items: Vec, } @@ -263,6 +265,12 @@ pub struct DispositionEvaluation { pub struct GoldenSetEvaluation { /// Opaque review receipt identifier. pub review_id: String, + /// Zotero library revision bound to the verified receipt. + pub library_version: u64, + /// Classifier revision bound to the verified receipt. + pub rule_revision: String, + /// Opaque immutable snapshot digest from the verified receipt. + pub snapshot_digest: String, /// Number of steward-reviewed items. pub reviewed_count: usize, /// Number of exact disposition matches. @@ -320,6 +328,7 @@ where || golden.approval.reviewer_subject.trim().is_empty() || golden.labels.is_empty() || golden.approval.rule_revision.trim().is_empty() + || golden.approval.snapshot_digest.trim().is_empty() { return Err(EvaluationError::InvalidReview); } @@ -392,6 +401,9 @@ where Ok(GoldenSetEvaluation { review_id: golden.approval.receipt_id.clone(), + library_version: golden.approval.library_version, + rule_revision: golden.approval.rule_revision.clone(), + snapshot_digest: golden.approval.snapshot_digest.clone(), reviewed_count: golden.labels.len(), correct_count, abstention_count, diff --git a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs index d73ec192..82887592 100644 --- a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs +++ b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs @@ -39,6 +39,7 @@ fn golden(labels: Vec) -> ReviewedGoldenSet { reviewer_subject: "synthetic-steward".into(), library_version: 42, rule_revision: "ontology-research-v2".into(), + snapshot_digest: "sha256:synthetic-snapshot".into(), snapshot_items: ["A", "B", "C"] .into_iter() .map(|item_key| SnapshotItemRevision { @@ -84,6 +85,9 @@ fn reviewed_golden_set_reports_count_based_precision_and_recall_evidence() { .unwrap(); assert_eq!(evaluation.review_id, "synthetic-review-1"); + assert_eq!(evaluation.library_version, 42); + assert_eq!(evaluation.rule_revision, "ontology-research-v2"); + assert_eq!(evaluation.snapshot_digest, "sha256:synthetic-snapshot"); assert_eq!(evaluation.reviewed_count, 3); assert_eq!(evaluation.correct_count, 1); assert_eq!(evaluation.abstention_count, 1); @@ -99,6 +103,7 @@ fn reviewed_golden_set_reports_count_based_precision_and_recall_evidence() { let serialized = serde_json::to_value(&evaluation).unwrap(); assert!(serialized.get("labels").is_none()); assert!(serialized.get("item_key").is_none()); + assert!(serialized.get("reviewer_subject").is_none()); } #[test] @@ -131,6 +136,12 @@ fn reviewed_golden_set_rejects_stale_unknown_and_duplicate_labels() { evaluate_reviewed_golden_set(&report, &missing_revision, verify_synthetic_approval), Err(EvaluationError::InvalidReview) ); + missing_revision.approval.rule_revision = "ontology-research-v2".into(); + missing_revision.approval.snapshot_digest.clear(); + assert_eq!( + evaluate_reviewed_golden_set(&report, &missing_revision, verify_synthetic_approval), + Err(EvaluationError::InvalidReview) + ); let mut stale = golden(vec![GoldenLabel::new("A", Disposition::Generation)]); stale.approval.library_version += 1; diff --git a/docs/PRD.md b/docs/PRD.md index ac191f3c..97b59865 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -58,7 +58,7 @@ All LLM-backed induction uses `contextual-orchestrator`. Model output is untrust Read one immutable Zotero Local API library-version snapshot and propose exactly one research disposition for every top-level bibliographic item. Each proposal retains the item key/version, exact matched metadata values, rule revision, linked child records, and any model receipt. Weak evidence and evidence that matches multiple specific disposition families must abstain into steward review. Duplicate DOI/title identities are review candidates only: intake never merges, deletes, or silently mutates Zotero records. -Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and bound to the complete Zotero item-key/item-version snapshot. Abstention is a prediction outcome, never an approved truth label. Evaluation emits 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. +Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and bound to the complete Zotero item-key/item-version snapshot. 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. 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 d8e7857d..5160be6e 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -63,7 +63,7 @@ Evaluation must separate extraction recall, semantic correctness, structural cor Every top-level bibliographic record receives exactly one proposed disposition. `NeedsStewardReview` also records a deterministic abstention reason so missing classification metadata, vocabulary unsupported by the current deterministic rules, and present-but-unmatched metadata are distinguishable. DOI duplicate identity normalization treats bare DOI values, `doi:`, `doi.org`, and legacy `dx.doi.org` resolver forms as the same identity when their normalized DOI is equal. -Golden-set evaluation accepts only a governance receipt verified by a caller-owned authorization boundary. Its library version, rule revision, and complete sorted item-key/item-version identity must exactly match the classification report. Blank, duplicate, unknown, stale, or abstention-as-truth labels fail closed. The output contains no item keys, reviewer identity, or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. Production authorization remains Keyverse/governance-owned; this crate validates the binding and does not mint or self-verify authority. +Golden-set evaluation accepts only a governance receipt verified by a caller-owned authorization boundary. Its library version, rule revision, immutable snapshot digest, and complete sorted item-key/item-version identity must bind the classification report. Blank, duplicate, unknown, stale, or abstention-as-truth labels fail closed. The output retains the verified library revision, rule revision, and opaque snapshot digest so stored metrics remain attributable, but contains no item keys, reviewer identity, or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. Production authorization remains Keyverse/governance-owned; this crate validates the item-revision binding and does not mint or self-verify 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. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index 73bfafe3..a49d1b78 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -19,7 +19,7 @@ The adapter links child records, emits exactly one deterministic proposed dispos Matched metadata values are copied into the local-only evidence receipt for replay. This is necessary for abstract-only matches because a later Zotero revision cannot reconstruct the exact text used for an earlier proposal from item key/version alone. DOI/title matches remain reversible duplicate candidates, including legacy `dx.doi.org` resolver forms. -Classifier quality is measured only against local steward-reviewed labels whose governance receipt is verified outside this crate and bound to the exact library version, rule revision, and complete item-key/item-version snapshot. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Evaluation returns aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys, reviewer identity, and bibliographic text are deliberately omitted from evaluation output. Missing, stale, unverified, unknown, duplicate, or invalid review identities fail closed. +Classifier quality is measured only against local steward-reviewed labels whose governance receipt is verified outside this crate and bound to the exact library version, rule revision, immutable snapshot digest, and complete item-key/item-version snapshot. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys, reviewer identity, and bibliographic text are deliberately omitted from evaluation output. Missing, stale, unverified, unknown, duplicate, or invalid review identities fail closed. Every successful report includes an aggregate audit summary computed from the same captured snapshot. Partial reads never produce a report, so successful output explicitly records zero failures alongside snapshot, proposal, provenance, abstention, duplicate, and per-disposition totals. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b4047b29..d0e89c97 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -44,7 +44,7 @@ Protected central source is `.github/main@c31d2e5471fc5daf9d72ff67cde6a8874b736d Local evidence on 2026-09-04 showed Zotero 9.0.6, Local API v3/schema 42, library version 12341, 8,326 total items, and 3,719 top-level items. The corrected read-only run observed all 8,326 records at that single version and classified all 3,715 top-level bibliographic records; four top-level note/attachment/annotation records were correctly excluded. It proposed 56 adjacent-evidence records, 1 semantic-consumption bridge, and 3,658 steward-review abstentions, linked children for 3,287 records, and surfaced 49 reversible duplicate groups (18 DOI, 31 title). No live record matched multiple specific disposition families; the tested conflict path still abstains fail-closed. Token-boundary matching prevents strings such as `knowledge` from becoming false OWL evidence. These are local aggregate observations, not reviewed truth or applied Zotero changes. The report stays outside the repository. -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 omits item and reviewer identities from its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Synthetic fixtures verify both contracts; no real precision/recall claim exists until a steward supplies reviewed local labels and the production authorization adapter verifies the receipt. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. Zotero write-back remains blocked by the installed v9 capability; a Zotero 10+ change must satisfy ADR 0006 preconditions. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. +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. Synthetic fixtures verify both contracts; no real precision/recall claim exists until a steward supplies reviewed local labels and the production authorization adapter verifies the receipt. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. Zotero write-back remains blocked by the installed v9 capability; a Zotero 10+ change must satisfy ADR 0006 preconditions. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. 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. 2. **Ontology discovery** — deterministic term/concept/taxonomy/non-taxonomic-relation candidate generation with exact source receipts and abstention for unsupported semantics. From 50c2eb1c0e4f481700d56f49b4f379e873bdbc39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:55:44 +0900 Subject: [PATCH 04/16] test(research): preserve Zotero 9 zero-version provenance --- .../tests/provenance_version_contract.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 crates/conceptweave-zotero/tests/provenance_version_contract.rs diff --git a/crates/conceptweave-zotero/tests/provenance_version_contract.rs b/crates/conceptweave-zotero/tests/provenance_version_contract.rs new file mode 100644 index 00000000..ad45882a --- /dev/null +++ b/crates/conceptweave-zotero/tests/provenance_version_contract.rs @@ -0,0 +1,28 @@ +use conceptweave_zotero::{ItemData, ZoteroItem, classify_snapshot}; + +#[test] +fn zotero_nine_zero_item_version_is_still_a_valid_provenance_coordinate() { + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![ZoteroItem { + key: "UNSYNCED1".into(), + version: 0, + 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![], + }, + }], + ); + + assert_eq!( + report.audit_summary.provenance_complete_count, 1, + "Zotero 9 may report version 0 for never-synced items; zero is a valid observed version, not missing provenance" + ); +} From 35a18fdfcb5c4539eccf4a86a846d6e1c0756b07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:54:50 +0900 Subject: [PATCH 05/16] fix: bind golden approval to report content Signed-off-by: Seongho Bae --- Cargo.lock | 84 +++++++++++++++++++ crates/conceptweave-zotero/Cargo.toml | 1 + crates/conceptweave-zotero/src/lib.rs | 24 ++++++ .../tests/golden_set_evaluation.rs | 7 +- .../tests/snapshot_content_binding.rs | 8 +- docs/PRD.md | 2 +- docs/TRD.md | 2 +- docs/adr/0006-zotero-research-intake.md | 2 +- 8 files changed, 118 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd4cfe80..ad534722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,27 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "conceptweave-domain" version = "0.1.0" @@ -24,9 +39,49 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "sha2", "ureq", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "http" version = "1.5.0" @@ -49,6 +104,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + [[package]] name = "log" version = "0.4.34" @@ -128,6 +189,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "syn" version = "3.0.4" @@ -139,6 +211,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -176,6 +254,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/conceptweave-zotero/Cargo.toml b/crates/conceptweave-zotero/Cargo.toml index 7f78f15f..3f492bbe 100644 --- a/crates/conceptweave-zotero/Cargo.toml +++ b/crates/conceptweave-zotero/Cargo.toml @@ -10,6 +10,7 @@ description = "Read-only Zotero research classification for ConceptWeave" [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" ureq = { version = "3", default-features = false } [lib] diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index a17e8674..42c18a39 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -4,6 +4,7 @@ //! Deterministic, read-only classification of a Zotero library snapshot. use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::time::Duration; @@ -249,6 +250,28 @@ pub struct GoldenSetApproval { pub snapshot_items: Vec, } +/// Computes the canonical content identity verified by a golden-set approval. +pub fn classification_snapshot_digest(report: &ClassificationReport) -> String { + let classified_items = report + .classified_items + .iter() + .map(|item| (item.item_key.as_str(), item)) + .collect::>(); + let canonical_snapshot = serde_json::to_vec(&( + &report.zotero_version, + report.api_version, + report.schema_version, + &report.server_id, + report.library_version, + report.rule_revision, + report.observed_item_count, + classified_items, + &report.duplicate_candidates, + )) + .expect("classification reports contain only JSON-compatible values"); + format!("sha256:{:x}", Sha256::digest(canonical_snapshot)) +} + /// Integer evidence from which precision and recall can be calculated exactly. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)] pub struct DispositionEvaluation { @@ -356,6 +379,7 @@ where } if golden.approval.library_version != report.library_version || golden.approval.rule_revision != report.rule_revision + || golden.approval.snapshot_digest != classification_snapshot_digest(report) || approved_snapshot != report_snapshot { return Err(EvaluationError::SnapshotMismatch); diff --git a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs index 82887592..de22a33f 100644 --- a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs +++ b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs @@ -1,6 +1,7 @@ use conceptweave_zotero::{ Disposition, EvaluationError, GoldenLabel, GoldenSetApproval, ItemData, ReviewedGoldenSet, - SnapshotItemRevision, ZoteroItem, classify_snapshot, evaluate_reviewed_golden_set, + SnapshotItemRevision, ZoteroItem, classification_snapshot_digest, classify_snapshot, + evaluate_reviewed_golden_set, }; fn item(key: &str, title: &str) -> ZoteroItem { @@ -39,7 +40,7 @@ fn golden(labels: Vec) -> ReviewedGoldenSet { reviewer_subject: "synthetic-steward".into(), library_version: 42, rule_revision: "ontology-research-v2".into(), - snapshot_digest: "sha256:synthetic-snapshot".into(), + snapshot_digest: classification_snapshot_digest(&report()), snapshot_items: ["A", "B", "C"] .into_iter() .map(|item_key| SnapshotItemRevision { @@ -87,7 +88,7 @@ fn reviewed_golden_set_reports_count_based_precision_and_recall_evidence() { assert_eq!(evaluation.review_id, "synthetic-review-1"); assert_eq!(evaluation.library_version, 42); assert_eq!(evaluation.rule_revision, "ontology-research-v2"); - assert_eq!(evaluation.snapshot_digest, "sha256:synthetic-snapshot"); + assert!(evaluation.snapshot_digest.starts_with("sha256:")); assert_eq!(evaluation.reviewed_count, 3); assert_eq!(evaluation.correct_count, 1); assert_eq!(evaluation.abstention_count, 1); diff --git a/crates/conceptweave-zotero/tests/snapshot_content_binding.rs b/crates/conceptweave-zotero/tests/snapshot_content_binding.rs index 7c4c72a9..efe76a1f 100644 --- a/crates/conceptweave-zotero/tests/snapshot_content_binding.rs +++ b/crates/conceptweave-zotero/tests/snapshot_content_binding.rs @@ -21,12 +21,8 @@ fn item(title: &str) -> ZoteroItem { #[test] fn golden_approval_rejects_same_revision_coordinates_with_changed_snapshot_content() { - let changed_report = classify_snapshot( - "9.0.6".into(), - None, - 42, - vec![item("ontology evaluation")], - ); + let changed_report = + classify_snapshot("9.0.6".into(), None, 42, vec![item("ontology evaluation")]); let golden = ReviewedGoldenSet { approval: GoldenSetApproval { receipt_id: "review-original-snapshot".into(), diff --git a/docs/PRD.md b/docs/PRD.md index 01b0a2ad..40de5db4 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -58,7 +58,7 @@ All LLM-backed induction uses `contextual-orchestrator`. Model output is untrust Read one immutable Zotero Local API library-version snapshot and propose exactly one research disposition for every top-level bibliographic item. Each proposal retains the item key/version, exact matched metadata values, rule revision, linked child records, and any model receipt. Weak evidence and evidence that matches multiple specific disposition families must abstain into steward review. Duplicate DOI/title identities are review candidates only: intake never merges, deletes, or silently mutates Zotero records. -Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and bound to the complete Zotero item-key/item-version snapshot. 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. +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. 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 f4ebc4e2..c55466c2 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -63,7 +63,7 @@ Evaluation must separate extraction recall, semantic correctness, structural cor Every top-level bibliographic record receives exactly one proposed disposition. `NeedsStewardReview` also records a deterministic abstention reason so missing classification metadata, vocabulary unsupported by the current deterministic rules, and present-but-unmatched metadata are distinguishable. DOI duplicate identity normalization treats bare DOI values, `doi:`, `doi.org`, and legacy `dx.doi.org` resolver forms as the same identity when their normalized DOI is equal. -Golden-set evaluation accepts only a governance receipt verified by a caller-owned authorization boundary. Its library version, rule revision, immutable snapshot digest, and complete sorted item-key/item-version identity must bind the classification report. Blank, duplicate, unknown, stale, or abstention-as-truth labels fail closed. The output retains the verified library revision, rule revision, and opaque snapshot digest so stored metrics remain attributable, but contains no item keys, reviewer identity, or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. Production authorization remains Keyverse/governance-owned; this crate validates the item-revision binding and does not mint or self-verify authority. +Golden-set evaluation accepts only a governance receipt verified by a caller-owned authorization boundary. Its library version, rule revision, canonical SHA-256 content digest, and complete sorted item-key/item-version identity must bind the classification report. The digest covers report contract versions, source identity, observed count, classified item content and evidence in key order, and duplicate candidates. Blank, duplicate, unknown, stale, content-mismatched, or abstention-as-truth labels fail closed. The output retains the verified library revision, rule revision, and opaque snapshot digest so stored metrics remain attributable, but contains no item keys, reviewer identity, or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. Production authorization remains Keyverse/governance-owned; this crate validates the content binding and does not mint or self-verify 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. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. The report is local JSON and contains proposals rather than governance decisions. CLI output is restricted to a new direct child of canonical `/tmp` or the operating system temporary directory; relative paths, nested paths, existing paths, and symlinks are rejected, and create-new file semantics prevent overwrite/path-swap writes. Zotero 9 writes are unsupported; no mutation path exists in this slice. A future Zotero 10+ writer requires a separate reviewed change with a Local API key, stable server identity, fresh item/library version preconditions, item-by-item before/after receipts, and rollback evidence. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index bffd544f..803437fd 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -19,7 +19,7 @@ The adapter links child records, emits exactly one deterministic proposed dispos Matched metadata values are copied into the local-only evidence receipt for replay. This is necessary for abstract-only matches because a later Zotero revision cannot reconstruct the exact text used for an earlier proposal from item key/version alone. DOI/title matches remain reversible duplicate candidates, including legacy `dx.doi.org` resolver forms. -Classifier quality is measured only against local steward-reviewed labels whose governance receipt is verified outside this crate and bound to the exact library version, rule revision, immutable snapshot digest, and complete item-key/item-version snapshot. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys, reviewer identity, and bibliographic text are deliberately omitted from evaluation output. Missing, stale, unverified, unknown, duplicate, or invalid review identities fail closed. +Classifier quality is measured only against local steward-reviewed labels whose governance receipt is verified outside this crate and bound to the exact library version, rule revision, canonical SHA-256 report-content digest, and complete item-key/item-version snapshot. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys, reviewer identity, and bibliographic text are deliberately omitted from evaluation output. Missing, stale, content-mismatched, unverified, unknown, duplicate, or invalid review identities fail closed. Every successful report includes an aggregate audit summary computed from the same captured snapshot. Partial reads never produce a report, so successful output explicitly records zero failures alongside snapshot, proposal, provenance, abstention, duplicate, and per-disposition totals. 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. From 9f6a18af90d86873e2aabef724b78e7d48e38d7a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:00:07 +0900 Subject: [PATCH 06/16] fix: preserve zero-version Zotero provenance Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/src/lib.rs | 2 +- crates/conceptweave-zotero/tests/golden_set_evaluation.rs | 4 ---- docs/TRD.md | 2 +- docs/adr/0006-zotero-research-intake.md | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 42c18a39..6cc872b8 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -708,7 +708,7 @@ pub fn classify_snapshot( proposed_disposition_count: classified_items.len(), provenance_complete_count: classified_items .iter() - .filter(|item| !item.item_key.trim().is_empty() && item.item_version > 0) + .filter(|item| !item.item_key.trim().is_empty()) .count(), abstention_count: classified_items .iter() diff --git a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs index de22a33f..9952d840 100644 --- a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs +++ b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs @@ -237,10 +237,6 @@ fn reviewed_golden_set_rejects_stale_unknown_and_duplicate_labels() { assert!(error.to_string().contains(fragment)); } - let mut incomplete = item("A", "ontology learning"); - incomplete.version = 0; - let incomplete_report = classify_snapshot("9.0.6".into(), None, 42, vec![incomplete]); - assert_eq!(incomplete_report.audit_summary.provenance_complete_count, 0); let blank_key_report = classify_snapshot( "9.0.6".into(), None, diff --git a/docs/TRD.md b/docs/TRD.md index c55466c2..62dae3d4 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -64,6 +64,6 @@ Evaluation must separate extraction recall, semantic correctness, structural cor Every top-level bibliographic record receives exactly one proposed disposition. `NeedsStewardReview` also records a deterministic abstention reason so missing classification metadata, vocabulary unsupported by the current deterministic rules, and present-but-unmatched metadata are distinguishable. DOI duplicate identity normalization treats bare DOI values, `doi:`, `doi.org`, and legacy `dx.doi.org` resolver forms as the same identity when their normalized DOI is equal. Golden-set evaluation accepts only a governance receipt verified by a caller-owned authorization boundary. Its library version, rule revision, canonical SHA-256 content digest, and complete sorted item-key/item-version identity must bind the classification report. The digest covers report contract versions, source identity, observed count, classified item content and evidence in key order, and duplicate candidates. Blank, duplicate, unknown, stale, content-mismatched, or abstention-as-truth labels fail closed. The output retains the verified library revision, rule revision, and opaque snapshot digest so stored metrics remain attributable, but contains no item keys, reviewer identity, or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. Production authorization remains Keyverse/governance-owned; this crate validates the content binding and does not mint or self-verify 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. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. +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 report is local JSON and contains proposals rather than governance decisions. CLI output is restricted to a new direct child of canonical `/tmp` or the operating system temporary directory; relative paths, nested paths, existing paths, and symlinks are rejected, and create-new file semantics prevent overwrite/path-swap writes. Zotero 9 writes are unsupported; no mutation path exists in this slice. A future Zotero 10+ writer requires a separate reviewed change with a Local API key, stable server identity, fresh item/library version preconditions, item-by-item before/after receipts, and rollback evidence. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index 803437fd..e2e0228b 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -20,7 +20,7 @@ The adapter links child records, emits exactly one deterministic proposed dispos Matched metadata values are copied into the local-only evidence receipt for replay. This is necessary for abstract-only matches because a later Zotero revision cannot reconstruct the exact text used for an earlier proposal from item key/version alone. DOI/title matches remain reversible duplicate candidates, including legacy `dx.doi.org` resolver forms. Classifier quality is measured only against local steward-reviewed labels whose governance receipt is verified outside this crate and bound to the exact library version, rule revision, canonical SHA-256 report-content digest, and complete item-key/item-version snapshot. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys, reviewer identity, and bibliographic text are deliberately omitted from evaluation output. Missing, stale, content-mismatched, unverified, unknown, duplicate, or invalid review identities fail closed. -Every successful report includes an aggregate audit summary computed from the same captured snapshot. Partial reads never produce a report, so successful output explicitly records zero failures alongside snapshot, proposal, provenance, abstention, duplicate, and per-disposition totals. +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. 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. From 6d881158e804197878525b7f99933953f2ac9ec7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:05:41 +0900 Subject: [PATCH 07/16] fix: bind golden review to complete snapshot Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/src/lib.rs | 54 ++++++++++--------- .../tests/golden_set_evaluation.rs | 5 +- .../tests/golden_set_integrity_contract.rs | 5 +- docs/TRD.md | 2 +- docs/adr/0006-zotero-research-intake.md | 2 +- 5 files changed, 36 insertions(+), 32 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 6cc872b8..d7a96328 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -167,6 +167,10 @@ pub struct ClassificationReport { pub rule_revision: &'static str, /// Number of items read, including child notes and attachments. pub observed_item_count: usize, + /// Complete item-revision identity of every observed record. + pub snapshot_items: Vec, + /// Canonical SHA-256 digest of every observed raw Zotero item. + pub snapshot_digest: String, /// One proposal for every top-level bibliographic item. pub classified_items: Vec, /// Reversible DOI/title duplicate candidates. @@ -252,24 +256,7 @@ pub struct GoldenSetApproval { /// Computes the canonical content identity verified by a golden-set approval. pub fn classification_snapshot_digest(report: &ClassificationReport) -> String { - let classified_items = report - .classified_items - .iter() - .map(|item| (item.item_key.as_str(), item)) - .collect::>(); - let canonical_snapshot = serde_json::to_vec(&( - &report.zotero_version, - report.api_version, - report.schema_version, - &report.server_id, - report.library_version, - report.rule_revision, - report.observed_item_count, - classified_items, - &report.duplicate_candidates, - )) - .expect("classification reports contain only JSON-compatible values"); - format!("sha256:{:x}", Sha256::digest(canonical_snapshot)) + report.snapshot_digest.clone() } /// Integer evidence from which precision and recall can be calculated exactly. @@ -345,7 +332,7 @@ pub fn evaluate_reviewed_golden_set( verify_approval: F, ) -> Result where - F: FnOnce(&GoldenSetApproval) -> bool, + F: FnOnce(&ReviewedGoldenSet) -> bool, { if golden.approval.receipt_id.trim().is_empty() || golden.approval.reviewer_subject.trim().is_empty() @@ -355,16 +342,18 @@ where { return Err(EvaluationError::InvalidReview); } - if !verify_approval(&golden.approval) { + if !verify_approval(golden) { return Err(EvaluationError::UnverifiedApproval); } let report_snapshot = report - .classified_items + .snapshot_items .iter() - .map(|item| SnapshotItemRevision { - item_key: item.item_key.clone(), - item_version: item.item_version, - }) + .cloned() + .collect::>(); + let report_keys = report + .snapshot_items + .iter() + .map(|item| item.item_key.as_str()) .collect::>(); let approved_snapshot = golden .approval @@ -372,7 +361,8 @@ where .iter() .cloned() .collect::>(); - if report_snapshot.len() != report.classified_items.len() + if report_snapshot.len() != report.snapshot_items.len() + || report_keys.len() != report.snapshot_items.len() || approved_snapshot.len() != golden.approval.snapshot_items.len() { return Err(EvaluationError::InvalidReview); @@ -686,6 +676,16 @@ pub fn classify_snapshot( mut items: Vec, ) -> ClassificationReport { items.sort_by(|left, right| left.key.cmp(&right.key)); + let snapshot_items = items + .iter() + .map(|item| SnapshotItemRevision { + item_key: item.key.clone(), + item_version: item.version, + }) + .collect(); + let snapshot_bytes = serde_json::to_vec(&items) + .expect("Zotero snapshot items contain only JSON-compatible values"); + let snapshot_digest = format!("sha256:{:x}", Sha256::digest(snapshot_bytes)); let children = child_index(&items); let bibliographic: Vec<&ZoteroItem> = items.iter().filter(|item| is_bibliographic(item)).collect(); @@ -727,6 +727,8 @@ pub fn classify_snapshot( library_version, rule_revision: RULE_REVISION, observed_item_count: items.len(), + snapshot_items, + snapshot_digest, classified_items, duplicate_candidates, audit_summary, diff --git a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs index 9952d840..e416abe3 100644 --- a/crates/conceptweave-zotero/tests/golden_set_evaluation.rs +++ b/crates/conceptweave-zotero/tests/golden_set_evaluation.rs @@ -53,8 +53,9 @@ fn golden(labels: Vec) -> ReviewedGoldenSet { } } -fn verify_synthetic_approval(approval: &GoldenSetApproval) -> bool { - approval.receipt_id == "synthetic-review-1" && approval.reviewer_subject == "synthetic-steward" +fn verify_synthetic_approval(golden: &ReviewedGoldenSet) -> bool { + golden.approval.receipt_id == "synthetic-review-1" + && golden.approval.reviewer_subject == "synthetic-steward" } #[test] diff --git a/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs b/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs index 8bc6d504..dc0615a0 100644 --- a/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs +++ b/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs @@ -104,8 +104,9 @@ fn verified_snapshot_receipt_cannot_authorize_mutated_steward_labels() { }; assert_eq!( - evaluate_reviewed_golden_set(&report, &golden, |receipt| { - receipt.receipt_id == "approved-review" + evaluate_reviewed_golden_set(&report, &golden, |reviewed_set| { + reviewed_set.approval.receipt_id == "approved-review" + && reviewed_set.labels == vec![GoldenLabel::new("A", Disposition::Generation)] }), Err(EvaluationError::UnverifiedApproval), "approval verification must bind the reviewed labels as well as the snapshot receipt" diff --git a/docs/TRD.md b/docs/TRD.md index 62dae3d4..5990ae08 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -63,7 +63,7 @@ Evaluation must separate extraction recall, semantic correctness, structural cor Every top-level bibliographic record receives exactly one proposed disposition. `NeedsStewardReview` also records a deterministic abstention reason so missing classification metadata, vocabulary unsupported by the current deterministic rules, and present-but-unmatched metadata are distinguishable. DOI duplicate identity normalization treats bare DOI values, `doi:`, `doi.org`, and legacy `dx.doi.org` resolver forms as the same identity when their normalized DOI is equal. -Golden-set evaluation accepts only a governance receipt verified by a caller-owned authorization boundary. Its library version, rule revision, canonical SHA-256 content digest, and complete sorted item-key/item-version identity must bind the classification report. The digest covers report contract versions, source identity, observed count, classified item content and evidence in key order, and duplicate candidates. Blank, duplicate, unknown, stale, content-mismatched, or abstention-as-truth labels fail closed. The output retains the verified library revision, rule revision, and opaque snapshot digest so stored metrics remain attributable, but contains no item keys, reviewer identity, or bibliographic text; per-disposition `true_positive`, `predicted`, and `expected` counts retain exact precision/recall numerators and denominators without floating-point rounding. Production authorization remains Keyverse/governance-owned; this crate validates the content binding and does not mint or self-verify authority. +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. 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 report is local JSON and contains proposals rather than governance decisions. CLI output is restricted to a new direct child of canonical `/tmp` or the operating system temporary directory; relative paths, nested paths, existing paths, and symlinks are rejected, and create-new file semantics prevent overwrite/path-swap writes. Zotero 9 writes are unsupported; no mutation path exists in this slice. A future Zotero 10+ writer requires a separate reviewed change with a Local API key, stable server identity, fresh item/library version preconditions, item-by-item before/after receipts, and rollback evidence. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index e2e0228b..eb474eb7 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -19,7 +19,7 @@ The adapter links child records, emits exactly one deterministic proposed dispos Matched metadata values are copied into the local-only evidence receipt for replay. This is necessary for abstract-only matches because a later Zotero revision cannot reconstruct the exact text used for an earlier proposal from item key/version alone. DOI/title matches remain reversible duplicate candidates, including legacy `dx.doi.org` resolver forms. -Classifier quality is measured only against local steward-reviewed labels whose governance receipt is verified outside this crate and bound to the exact library version, rule revision, canonical SHA-256 report-content digest, and complete item-key/item-version snapshot. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence for precision, recall, exact matches, and abstentions; Zotero keys, reviewer identity, and bibliographic text are deliberately omitted from evaluation output. Missing, stale, content-mismatched, unverified, unknown, duplicate, or invalid review identities fail closed. +Classifier quality is measured only against local steward-reviewed labels whose complete 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. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence; Zotero keys, reviewer identity, and bibliographic text are omitted. Missing, stale, content- or label-mismatched, unverified, unknown, duplicate, or invalid review identities fail closed. 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. 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. From b836db910a07cb3c3fc3fb68140d70bcd8d6c6c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:23:38 +0900 Subject: [PATCH 08/16] test(zotero): require linked child provenance identity --- .../tests/provenance_version_contract.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/conceptweave-zotero/tests/provenance_version_contract.rs b/crates/conceptweave-zotero/tests/provenance_version_contract.rs index ad45882a..60954c2a 100644 --- a/crates/conceptweave-zotero/tests/provenance_version_contract.rs +++ b/crates/conceptweave-zotero/tests/provenance_version_contract.rs @@ -26,3 +26,45 @@ fn zotero_nine_zero_item_version_is_still_a_valid_provenance_coordinate() { "Zotero 9 may report version 0 for never-synced items; zero is a valid observed version, not missing provenance" ); } + +#[test] +fn provenance_completeness_requires_stable_linked_child_identity() { + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![ + ZoteroItem { + key: "PARENT01".into(), + version: 4, + data: ItemData { + item_type: "journalArticle".into(), + title: "ontology learning".into(), + abstract_note: String::new(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + }, + ZoteroItem { + key: String::new(), + version: 0, + data: ItemData { + item_type: "note".into(), + title: String::new(), + abstract_note: String::new(), + doi: String::new(), + parent_item: "PARENT01".into(), + collections: vec![], + tags: vec![], + }, + }, + ], + ); + + assert_eq!( + report.audit_summary.provenance_complete_count, 0, + "a proposal with a linked child lacking a stable Zotero key is not provenance-complete" + ); +} From c28fccaa592d6cbb0dcf9d86d0558bdb28d43842 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:25:40 +0900 Subject: [PATCH 09/16] fix(zotero): audit linked child provenance --- crates/conceptweave-zotero/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index d7a96328..da36f9cc 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -708,7 +708,13 @@ pub fn classify_snapshot( proposed_disposition_count: classified_items.len(), provenance_complete_count: classified_items .iter() - .filter(|item| !item.item_key.trim().is_empty()) + .filter(|item| { + !item.item_key.trim().is_empty() + && item + .child_item_keys + .iter() + .all(|child_key| !child_key.trim().is_empty()) + }) .count(), abstention_count: classified_items .iter() From 082710e8c9a0e37ddae528e305cd75ab3574d0bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:51:02 +0900 Subject: [PATCH 10/16] test(research): adopt captured-source fixture contract Signed-off-by: Seongho Bae --- .../conceptweave-zotero/tests/provenance_version_contract.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/conceptweave-zotero/tests/provenance_version_contract.rs b/crates/conceptweave-zotero/tests/provenance_version_contract.rs index 60954c2a..dfe0730e 100644 --- a/crates/conceptweave-zotero/tests/provenance_version_contract.rs +++ b/crates/conceptweave-zotero/tests/provenance_version_contract.rs @@ -7,6 +7,7 @@ fn zotero_nine_zero_item_version_is_still_a_valid_provenance_coordinate() { None, 42, vec![ZoteroItem { + source_record: None, key: "UNSYNCED1".into(), version: 0, data: ItemData { @@ -35,6 +36,7 @@ fn provenance_completeness_requires_stable_linked_child_identity() { 42, vec![ ZoteroItem { + source_record: None, key: "PARENT01".into(), version: 4, data: ItemData { @@ -48,6 +50,7 @@ fn provenance_completeness_requires_stable_linked_child_identity() { }, }, ZoteroItem { + source_record: None, key: String::new(), version: 0, data: ItemData { From ebdd85272dcfce9acef4336a469894a231b912be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:24:19 +0900 Subject: [PATCH 11/16] test: reject forged audit and ambiguous provenance totals --- .../tests/golden_set_integrity_contract.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs b/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs index 7ffa4f32..401c8d9d 100644 --- a/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs +++ b/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs @@ -6,6 +6,58 @@ use conceptweave_zotero::{ }; use sha2::{Digest, Sha256}; +#[test] +fn derived_audit_mutation_fails_before_governance() { + for mutation in 0..8 { + let mut report = scope_report(); + let audit = &mut report.audit_summary; + match mutation { + 0 => audit.snapshot_item_count += 1, + 1 => audit.bibliographic_item_count += 1, + 2 => audit.proposed_disposition_count += 1, + 3 => audit.provenance_complete_count += 1, + 4 => audit.abstention_count += 1, + 5 => audit.duplicate_candidate_count += 1, + 6 => audit.failure_count += 1, + _ => audit.disposition_counts.clear(), + } + let golden = scope_golden(&report); + assert_eq!( + evaluate_reviewed_golden_set(&report, &golden, |_| panic!( + "forged audit reached governance" + )), + Err(EvaluationError::InvalidReview), + "audit mutation {mutation}" + ); + } +} + +#[test] +fn ambiguous_source_coordinates_never_count_as_complete_provenance() { + for items in [ + vec![ + bibliographic("A", 1, "ontology learning"), + bibliographic("A", 2, "ontology learning"), + ], + vec![ + bibliographic("A", 1, "ontology learning"), + child_note("C", 1, "A"), + child_note("C", 2, "A"), + ], + vec![ + bibliographic("A", 1, "ontology learning"), + child_note("A", 1, ""), + ], + ] { + let report = classify_snapshot("10.0.1".into(), None, 42, items); + assert_eq!(report.audit_summary.provenance_complete_count, 0); + assert_eq!( + validate_classification_report(&report), + Err(EvaluationError::InvalidReview) + ); + } +} + #[test] fn legacy_proposal_receipt_is_rejected_without_calling_governance() { let report = scope_report(); From cb4c06ba7fbf59c11b9cd45a0d53826a795a9873 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:26:27 +0900 Subject: [PATCH 12/16] fix: recompute audit evidence and exclude ambiguous identities --- crates/conceptweave-zotero/src/lib.rs | 81 ++++++++++++++++++--------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 8dc8a49a..e64ee10d 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -456,6 +456,15 @@ pub fn validate_classification_report( return Err(invalid); } } + if report.audit_summary + != classification_audit( + &report.snapshot_items, + &report.classified_items, + report.duplicate_candidates.len(), + ) + { + return Err(invalid); + } let mut reported_pending = report.pending_source_item_keys.clone(); reported_pending.sort(); // Equal partition size and one successful removal per record prove completeness. @@ -887,7 +896,6 @@ pub fn classify_snapshot( let children = child_index(&items); let bibliographic: Vec<&ZoteroItem> = items.iter().filter(|item| is_bibliographic(item)).collect(); - let bibliographic_item_count = bibliographic.len(); let duplicate_candidates = duplicate_candidates(&bibliographic); let classified_items: Vec = bibliographic .into_iter() @@ -901,50 +909,69 @@ pub fn classify_snapshot( let pending_source_item_keys = pending_source_keys(&classified_items, &unclassified_items, children); + let audit_summary = classification_audit( + &snapshot_items, + &classified_items, + duplicate_candidates.len(), + ); + + ClassificationReport { + zotero_version, + api_version: None, + schema_version: None, + server_id, + library_version, + rule_revision: RULE_REVISION, + observed_item_count, + snapshot_items, + snapshot_digest, + classified_items, + unclassified_items, + pending_source_item_keys, + duplicate_candidates, + audit_summary, + } +} + +fn classification_audit( + snapshot_items: &[SnapshotItemRevision], + classified_items: &[ClassifiedItem], + duplicate_candidate_count: usize, +) -> ClassificationAudit { + let mut identity_counts = BTreeMap::new(); + for item in snapshot_items { + *identity_counts + .entry(item.item_key.as_str()) + .or_insert(0usize) += 1; + } let mut disposition_counts = BTreeMap::new(); - for item in &classified_items { + for item in classified_items { *disposition_counts .entry(item.proposed_disposition) .or_insert(0) += 1; } - let audit_summary = ClassificationAudit { - snapshot_item_count: observed_item_count, - bibliographic_item_count, + ClassificationAudit { + snapshot_item_count: snapshot_items.len(), + bibliographic_item_count: classified_items.len(), proposed_disposition_count: classified_items.len(), provenance_complete_count: classified_items .iter() .filter(|item| { !item.item_key.trim().is_empty() - && item - .child_item_keys - .iter() - .all(|child_key| !child_key.trim().is_empty()) + && identity_counts.get(item.item_key.as_str()) == Some(&1) + && item.child_item_keys.iter().all(|child_key| { + !child_key.trim().is_empty() + && identity_counts.get(child_key.as_str()) == Some(&1) + }) }) .count(), abstention_count: classified_items .iter() .filter(|item| item.proposed_disposition == Disposition::NeedsStewardReview) .count(), - duplicate_candidate_count: duplicate_candidates.len(), + duplicate_candidate_count, failure_count: 0, disposition_counts, - }; - - ClassificationReport { - zotero_version, - api_version: None, - schema_version: None, - server_id, - library_version, - rule_revision: RULE_REVISION, - observed_item_count, - snapshot_items, - snapshot_digest, - classified_items, - unclassified_items, - pending_source_item_keys, - duplicate_candidates, - audit_summary, } } From 935e035d6d865490ae024e7384eafca779f96e8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:27:05 +0900 Subject: [PATCH 13/16] fix: preserve owned snapshot vector at audit boundary --- crates/conceptweave-zotero/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index e64ee10d..b17c8608 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -879,7 +879,7 @@ pub fn classify_snapshot( mut items: Vec, ) -> ClassificationReport { items.sort_by(|left, right| left.key.cmp(&right.key)); - let snapshot_items = items + let snapshot_items: Vec<_> = items .iter() .map(|item| SnapshotItemRevision { item_key: item.key.clone(), From 23178a9768aa216692d77918d56357ae1269535c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:29:14 +0900 Subject: [PATCH 14/16] test: retain approval attack coverage with coherent audit counts --- .../tests/golden_set_integrity_contract.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs b/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs index 401c8d9d..7399394a 100644 --- a/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs +++ b/crates/conceptweave-zotero/tests/golden_set_integrity_contract.rs @@ -275,6 +275,8 @@ fn approved_snapshot_cannot_authorize_a_prediction_changed_to_match_the_label() ); report.classified_items[0].proposed_disposition = Disposition::AlignmentVersioning; + report.audit_summary.disposition_counts = + std::collections::BTreeMap::from([(Disposition::AlignmentVersioning, 1)]); let verifier_called = std::cell::Cell::new(false); assert_eq!( evaluate_reviewed_golden_set(&report, &golden, |candidate| { @@ -301,6 +303,8 @@ fn rewriting_the_proposal_digest_cannot_reuse_an_independent_approval() { }; let approved_golden = golden.clone(); report.classified_items[0].proposed_disposition = Disposition::AlignmentVersioning; + report.audit_summary.disposition_counts = + std::collections::BTreeMap::from([(Disposition::AlignmentVersioning, 1)]); golden.approval.proposal_digest = classification_proposal_digest(&report); let imported_golden = serde_json::from_slice::(&serde_json::to_vec(&golden).unwrap()).unwrap(); From bec7e316e7d2ed97562bec1988b44fa7c4a37218 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:32:06 +0900 Subject: [PATCH 15/16] docs: record derived audit repair and fresh visual evidence --- docs/adr/0006-zotero-research-intake.md | 8 ++++++++ docs/product-technical-gap-baseline.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index b147120a..69299e6e 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -73,6 +73,14 @@ GREEN source `f6735b585022aac1c8ceff86c150d9b64fd77ec2` passed 68 tests across 1 ## Alternatives considered +### September 6 derived audit repair (Proposed) + +Live PR #11 findings [3934799129](https://github.com/ContextualWisdomLab/ConceptWeave/pull/11#discussion_r3934799129) and [3934994550](https://github.com/ContextualWisdomLab/ConceptWeave/pull/11#discussion_r3934994550) were rechecked after source-scope integration. Source identity must not absorb derived audit fields, but accepting arbitrary audit values alongside a verified report can still misstate completeness. RED `ebdd852` proved both forged audit reaching governance and duplicate source keys counted as complete provenance. + +We extracted the existing aggregate computation and reuse it during report construction and shared admission. A parent and every linked child must each have one nonblank identity in the complete snapshot to count as provenance-complete. Audit fields are recomputed before independent verification; zero item revisions remain valid. This adds an O(n log n) identity-count pass and rejects inconsistent audit reports, without changing the raw source digest or minting approval. Copying a second audit implementation or mixing counters into source identity was rejected. Duplicate candidate semantics remain owned by the subsequent duplicate boundary; comparing its count is not candidate authentication. + +`cb4c06b` exposed a slice-inference compilation error, repaired by the explicit owned vector in `935e035`. Existing prediction-tampering tests then needed coherent attacker-controlled counters to reach their original gates; `23178a9` retains their original mismatch and unverified-approval expectations. All 75 workspace tests passed. Full coverage, hosted checks, descendant adoption and protection-compliant merge remain separate requirements. + The follow-up `8ccb0d5b3d7705786b6c40c3bcf5a10ff32046d9` removes duplicate evaluator identity checks subsumed by the entry validator. Equal partition lengths plus one successful removal per unique coordinate prove no leftover source; testing impossible duplicate branches would require bypassing the real entry boundary. Existing malformed-report cases remain, and `d1344c7` adds legacy-v1 rejection and empty/orphan/cycle/blank-identity regressions. The unchanged pinned coverage gate now passes all normalized owned regions and branches; raw instantiated gaps remain explicitly reported in the Gap baseline. No coverage exclusion, dependency or authority service was added. - First-match classification was rejected because FR-9 requires ambiguous evidence to abstain rather than acquire an arbitrary priority-based disposition. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0d167f12..2623c6f7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -56,6 +56,10 @@ Protected central source is `.github/main@b5efbc2762e472e4a380b0503b1f050f76fbb0 ## September 6 source-scope admission checkpoint +PR #11 local successor checkpoint `23178a9768aa216692d77918d56357ae1269535c` normally inherits #10 `fdf8b8d70c05bcb76c55cb6336c9bf31b5e42ce4` while preserving prior #11 `1dc032598b41a35d52c09d8690c871e07365d7e3`. The stable isolated baseline passed 60 tests/15 suites; an earlier run contaminated by merge timing is invalid evidence. RED `ebdd852` reproduced forged derived audit and ambiguous provenance totals. Extracted audit computation now counts unique parent/child identities and is recomputed before governance. Final local tests passed 75/15 suites including two doctests; independent integrity review passed 17 tests with no new regression. Strict Clippy passed at unchanged runtime `935e035`. Pinned coverage is still running; no inherited coverage claim or remote PR update is made here. + +Residual duplicate-owner gap: changing duplicate candidates and their matching audit count is not bound by the v2 proposal receipt; derived count consistency does not authenticate duplicate proposals. No duplicate merge/write authority is granted. Fresh native screenshot and accessibility state both showed 3,719 items in the library view, with list rows and attachment icons rendered. The earlier Mac-lock limitation is no longer current. No screenshot or bibliographic identities are committed; this view check does not establish full-library reclassification. + Follow-up runtime `8ccb0d5b3d7705786b6c40c3bcf5a10ff32046d9` adds explicit legacy receipt/empty/orphan/cycle/blank-identity regressions and removes only checks proven redundant after shared admission. Local workspace tests: 71/14 unfiltered suites including two doctests; strict Clippy, warnings-denied rustdoc, release build, format and diff checks passed. Independent read-only review reran 15 integrity tests and found no actionable defect. Unchanged pinned coverage gate passed: functions 136/136, normalized owned regions 1,037/1,037 and branches 174/174. Raw lines 1,448/1,451, regions 2,268/2,279 and branches 169/174 remain below 100%; no raw full-coverage claim is made. Logs: `/tmp/conceptweave-admission-coverage-baseline.log`, `/tmp/conceptweave-admission-coverage-green.log`, `/tmp/conceptweave-admission-boundary-tests.log`. PR #10 source `f6735b585022aac1c8ceff86c150d9b64fd77ec2` repairs evaluation admission and independently approved scope binding after normal integration of producer `51c7df6d03f072449422fd58ca24b2f9d6026f07`. RED `3f2cf55` failed three new integrity tests; GREEN passed 68 tests/14 unfiltered suites and strict Clippy. See the Proposed [ADR 0006 amendment](adr/0006-zotero-research-intake.md). This local result does not establish hosted GREEN, protection-compliant merge, release, downstream adoption or full coverage. From 6dff8c2ee42cfeb7bf8688c1f7e95989b61be266 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:33:05 +0900 Subject: [PATCH 16/16] docs: record terminal audit coverage result --- docs/product-technical-gap-baseline.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2623c6f7..49eb6b43 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -58,6 +58,8 @@ Protected central source is `.github/main@b5efbc2762e472e4a380b0503b1f050f76fbb0 PR #11 local successor checkpoint `23178a9768aa216692d77918d56357ae1269535c` normally inherits #10 `fdf8b8d70c05bcb76c55cb6336c9bf31b5e42ce4` while preserving prior #11 `1dc032598b41a35d52c09d8690c871e07365d7e3`. The stable isolated baseline passed 60 tests/15 suites; an earlier run contaminated by merge timing is invalid evidence. RED `ebdd852` reproduced forged derived audit and ambiguous provenance totals. Extracted audit computation now counts unique parent/child identities and is recomputed before governance. Final local tests passed 75/15 suites including two doctests; independent integrity review passed 17 tests with no new regression. Strict Clippy passed at unchanged runtime `935e035`. Pinned coverage is still running; no inherited coverage claim or remote PR update is made here. +The pinned coverage run subsequently finished with exit 0: 140/140 functions, 1,101/1,101 normalized owned regions, 182/182 normalized branches. Raw lines 1,502/1,505, regions 2,332/2,343 and branches 177/182 remain below 100%. The coverage script and exclusions are unchanged; the earlier pending sentence records chronology, not current execution state. + Residual duplicate-owner gap: changing duplicate candidates and their matching audit count is not bound by the v2 proposal receipt; derived count consistency does not authenticate duplicate proposals. No duplicate merge/write authority is granted. Fresh native screenshot and accessibility state both showed 3,719 items in the library view, with list rows and attachment icons rendered. The earlier Mac-lock limitation is no longer current. No screenshot or bibliographic identities are committed; this view check does not establish full-library reclassification. Follow-up runtime `8ccb0d5b3d7705786b6c40c3bcf5a10ff32046d9` adds explicit legacy receipt/empty/orphan/cycle/blank-identity regressions and removes only checks proven redundant after shared admission. Local workspace tests: 71/14 unfiltered suites including two doctests; strict Clippy, warnings-denied rustdoc, release build, format and diff checks passed. Independent read-only review reran 15 integrity tests and found no actionable defect. Unchanged pinned coverage gate passed: functions 136/136, normalized owned regions 1,037/1,037 and branches 174/174. Raw lines 1,448/1,451, regions 2,268/2,279 and branches 169/174 remain below 100%; no raw full-coverage claim is made. Logs: `/tmp/conceptweave-admission-coverage-baseline.log`, `/tmp/conceptweave-admission-coverage-green.log`, `/tmp/conceptweave-admission-boundary-tests.log`.