-
Notifications
You must be signed in to change notification settings - Fork 0
feat(research): add Zotero classification audit evidence #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/zotero-golden-set-evaluation
Are you sure you want to change the base?
Changes from all commits
f8600f3
a520107
eda4d7a
31e2447
6f2b988
50c2eb1
35a18fd
f17acfb
9f6a18a
393c514
f509ebf
6d88115
551e6cb
b836db9
c28fcca
e0ec7da
472dd34
cb365fb
1dd81bc
082710e
11d158b
1dc0325
39c487a
ebdd852
cb4c06b
935e035
23178a9
bec7e31
6dff8c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -223,6 +223,29 @@ pub struct ClassificationReport { | |
| pub pending_source_item_keys: Vec<String>, | ||
| /// Reversible DOI/title duplicate candidates. | ||
| pub duplicate_candidates: Vec<DuplicateCandidate>, | ||
| /// 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<Disposition, usize>, | ||
| } | ||
|
|
||
| /// One steward-reviewed expected disposition in a local golden set. | ||
|
|
@@ -433,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. | ||
|
|
@@ -847,7 +879,7 @@ pub fn classify_snapshot( | |
| mut items: Vec<ZoteroItem>, | ||
| ) -> 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(), | ||
|
|
@@ -865,7 +897,7 @@ pub fn classify_snapshot( | |
| let bibliographic: Vec<&ZoteroItem> = | ||
| items.iter().filter(|item| is_bibliographic(item)).collect(); | ||
| let duplicate_candidates = duplicate_candidates(&bibliographic); | ||
| let classified_items: Vec<_> = bibliographic | ||
| let classified_items: Vec<ClassifiedItem> = bibliographic | ||
| .into_iter() | ||
| .map(|item| classify_item(item, children.get(&item.key).cloned().unwrap_or_default())) | ||
| .collect(); | ||
|
|
@@ -877,6 +909,12 @@ 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, | ||
|
|
@@ -891,6 +929,49 @@ pub fn classify_snapshot( | |
| 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 { | ||
| *disposition_counts | ||
| .entry(item.proposed_disposition) | ||
| .or_insert(0) += 1; | ||
| } | ||
| 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() | ||
| && 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(), | ||
|
Comment on lines
+957
to
+967
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a bibliographic item has a linked child with a blank key, AGENTS.md reference: AGENTS.md:L18-L18 Useful? React with 👍 / 👎. |
||
| abstention_count: classified_items | ||
| .iter() | ||
| .filter(|item| item.proposed_disposition == Disposition::NeedsStewardReview) | ||
| .count(), | ||
| duplicate_candidate_count, | ||
| failure_count: 0, | ||
| disposition_counts, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| 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 { | ||
| source_record: None, | ||
| 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" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn provenance_completeness_requires_stable_linked_child_identity() { | ||
| let report = classify_snapshot( | ||
| "9.0.6".into(), | ||
| None, | ||
| 42, | ||
| vec![ | ||
| ZoteroItem { | ||
| source_record: None, | ||
| 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 { | ||
| source_record: None, | ||
| 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" | ||
| ); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.