diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c41e2..895cbef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ All notable changes to ConceptWeave are documented here. - Minimal, nonduplicated local abstract context for Zotero items that require steward classification. - Owner-only file permissions for sensitive local Zotero classification reports. - A complete-review evaluator that rejects partial steward labels as full reclassification evidence. +- A snapshot-bound steward worksheet with one blank decision per bibliographic item and no duplicated bibliographic text. ### Security diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 79c1bc1..4059586 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -131,7 +131,7 @@ pub enum Disposition { } /// Deterministic reason that a bibliographic item requires steward review. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum AbstentionReason { /// Title, abstract, and tags contain no classification metadata. @@ -1106,6 +1106,92 @@ pub struct ClassificationAudit { pub disposition_counts: BTreeMap, } +/// One editable local steward decision without duplicated bibliographic text. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct StewardReviewDecision { + /// Stable Zotero item key used to join the sensitive classification report. + pub item_key: String, + /// Exact item revision observed in the classified snapshot. + pub item_version: u64, + /// Deterministic proposal supplied for comparison, never as approval. + pub proposed_disposition: Disposition, + /// Deterministic abstention reason when the proposal requires review. + pub abstention_reason: Option, + /// Steward decision to fill; abstention is rejected by completion evaluation. + pub reviewed_disposition: Option, +} + +/// Snapshot-bound local worksheet for one decision per bibliographic item. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct StewardReviewWorksheet { + /// Zotero library revision observed with the source snapshot. + pub library_version: u64, + /// Classifier revision that produced the proposals. + pub rule_revision: String, + /// Canonical content digest of the complete raw snapshot. + pub snapshot_digest: String, + /// Versioned identity of proposals, review context and retained source scope. + /// Older worksheets require regeneration; this binding is not approval. + pub proposal_digest: String, + /// Complete parent and child item-revision coordinates. + pub snapshot_items: Vec, + /// Deterministically ordered editable decisions for bibliographic items. + pub decisions: Vec, +} + +/// A classification report cannot safely produce a review worksheet. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WorksheetError { + /// Report identity or coverage is incomplete, duplicated, or inconsistent. + InvalidReport, +} + +impl fmt::Display for WorksheetError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("classification report is invalid for steward review") + } +} + +impl std::error::Error for WorksheetError {} + +/// Builds a complete local worksheet without copying titles, abstracts, or evidence. +/// +/// Complete inventory validation precedes projection. Valid unresolved sources +/// may be reviewed, but worksheet creation never proves completed review. +pub fn build_steward_review_worksheet( + report: &ClassificationReport, +) -> Result { + validate_classification_report(report).map_err(|_| WorksheetError::InvalidReport)?; + if report.rule_revision.trim().is_empty() || report.snapshot_digest.trim().is_empty() { + return Err(WorksheetError::InvalidReport); + } + let mut decisions = Vec::with_capacity(report.classified_items.len()); + for item in &report.classified_items { + if (item.proposed_disposition == Disposition::NeedsStewardReview) + != item.abstention_reason.is_some() + { + return Err(WorksheetError::InvalidReport); + } + decisions.push(StewardReviewDecision { + item_key: item.item_key.clone(), + item_version: item.item_version, + proposed_disposition: item.proposed_disposition, + abstention_reason: item.abstention_reason, + reviewed_disposition: None, + }); + } + decisions.sort_by(|left, right| left.item_key.cmp(&right.item_key)); + + Ok(StewardReviewWorksheet { + library_version: report.library_version, + rule_revision: report.rule_revision.into(), + snapshot_digest: report.snapshot_digest.clone(), + proposal_digest: classification_proposal_digest(report), + snapshot_items: report.snapshot_items.clone(), + decisions, + }) +} + /// One steward-reviewed expected disposition in a local golden set. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct GoldenLabel { diff --git a/crates/conceptweave-zotero/tests/steward_review_worksheet.rs b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs new file mode 100644 index 0000000..143edfa --- /dev/null +++ b/crates/conceptweave-zotero/tests/steward_review_worksheet.rs @@ -0,0 +1,322 @@ +use conceptweave_zotero::{ + Disposition, ItemData, WorksheetError, ZoteroItem, build_steward_review_worksheet, + classify_snapshot, +}; + +fn item(key: &str, title: &str) -> ZoteroItem { + ZoteroItem { + source_record: None, + key: key.into(), + version: 7, + data: ItemData { + item_type: "book".into(), + title: title.into(), + abstract_note: "sensitive abstract".into(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + } +} + +fn report() -> conceptweave_zotero::ClassificationReport { + classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("B", "unmatched title"), item("A", "ontology learning")], + ) +} + +#[test] +fn worksheet_identity_binds_review_context_and_retained_source_metadata() { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + let mut report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "unmatched"), source_item], + ); + let original = build_steward_review_worksheet(&report).unwrap(); + report.unclassified_items[0].data.title.push_str(" changed"); + let source_changed = build_steward_review_worksheet(&report).unwrap(); + assert_ne!(original, source_changed); + report.classified_items[0].review_abstract_note = Some("changed review context".into()); + let context_changed = build_steward_review_worksheet(&report).unwrap(); + assert_ne!(source_changed, context_changed); + let serialized = serde_json::to_value(&context_changed).unwrap(); + assert_eq!( + serialized["proposal_digest"], + conceptweave_zotero::classification_proposal_digest(&report) + ); +} + +#[test] +fn worksheet_cannot_deserialize_without_proposal_binding() { + let worksheet = build_steward_review_worksheet(&report()).unwrap(); + let mut serialized = serde_json::to_value(&worksheet).unwrap(); + serialized + .as_object_mut() + .unwrap() + .remove("proposal_digest"); + assert!( + serde_json::from_value::(serialized).is_err() + ); +} + +#[test] +fn worksheet_preserves_complete_inventory_without_requiring_source_resolution() { + for parent_key in ["", "missing", "source", "A"] { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + source_item.data.parent_item = parent_key.into(); + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "ontology learning"), source_item], + ); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + assert_eq!(worksheet.snapshot_items, report.snapshot_items); + assert_eq!(worksheet.decisions.len(), 1); + assert_eq!(worksheet.decisions[0].reviewed_disposition, None); + } +} + +#[test] +fn worksheet_rejects_omitted_retained_source_inventory() { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + let mut report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "ontology learning"), source_item], + ); + report.unclassified_items.clear(); + assert_eq!( + build_steward_review_worksheet(&report), + Err(WorksheetError::InvalidReport) + ); +} + +#[test] +fn worksheet_rejects_hidden_pending_sources() { + let mut source_item = item("source", "synthetic source"); + source_item.data.item_type = "attachment".into(); + let mut report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![item("A", "ontology learning"), source_item], + ); + report.pending_source_item_keys.clear(); + assert_eq!( + build_steward_review_worksheet(&report), + Err(WorksheetError::InvalidReport) + ); +} + +#[test] +fn worksheet_is_snapshot_bound_complete_and_contains_no_bibliographic_text() { + let report = report(); + + let worksheet = build_steward_review_worksheet(&report).unwrap(); + + assert_eq!(worksheet.library_version, 42); + assert_eq!(worksheet.rule_revision, report.rule_revision); + assert_eq!(worksheet.snapshot_digest, report.snapshot_digest); + assert_eq!(worksheet.snapshot_items, report.snapshot_items); + assert_eq!( + worksheet + .decisions + .iter() + .map(|decision| decision.item_key.as_str()) + .collect::>(), + ["A", "B"] + ); + assert!( + worksheet + .decisions + .iter() + .all(|decision| decision.reviewed_disposition.is_none()) + ); + let serialized = serde_json::to_value(&worksheet).unwrap(); + let serialized = serialized.to_string(); + assert!(!serialized.contains("unmatched title")); + assert!(!serialized.contains("sensitive abstract")); + assert!(!serialized.contains("field_values")); +} + +#[test] +fn worksheet_rejects_each_inconsistent_report_coordinate() { + let mut invalid = report(); + invalid.rule_revision = ""; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.snapshot_digest.clear(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.observed_item_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.bibliographic_item_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.proposed_disposition_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.snapshot_item_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.provenance_complete_count -= 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.abstention_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.duplicate_candidate_count += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.failure_count = 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.audit_summary.disposition_counts.clear(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[0].child_item_keys = vec![String::new()]; + invalid.audit_summary.provenance_complete_count -= 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.snapshot_items[0].item_key.clear(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.snapshot_items[1].item_key = invalid.snapshot_items[0].item_key.clone(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[0].item_key.clear(); + invalid.audit_summary.provenance_complete_count -= 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[1].item_key = invalid.classified_items[0].item_key.clone(); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid.classified_items[0].item_version += 1; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid + .classified_items + .iter_mut() + .find(|item| item.proposed_disposition == Disposition::NeedsStewardReview) + .unwrap() + .abstention_reason = None; + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = report(); + invalid + .classified_items + .iter_mut() + .find(|item| item.proposed_disposition != Disposition::NeedsStewardReview) + .unwrap() + .abstention_reason = Some(conceptweave_zotero::AbstentionReason::NoDeterministicRuleMatch); + assert_eq!( + build_steward_review_worksheet(&invalid), + Err(WorksheetError::InvalidReport) + ); +} + +#[test] +fn worksheet_rejects_a_report_with_duplicate_source_identity() { + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![ + item("A", "ontology learning"), + item("A", "ontology learning"), + ], + ); + + assert_eq!( + build_steward_review_worksheet(&report), + Err(WorksheetError::InvalidReport) + ); + assert!( + WorksheetError::InvalidReport + .to_string() + .contains("invalid") + ); +} diff --git a/docs/PRD.md b/docs/PRD.md index f1212f6..f896380 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -86,6 +86,8 @@ PR #21 retains validated delayed reads without writes and complete observed meta The Zotero 10+ adapter can accept a caller-owned API key and server identity at runtime or consume one successful, user-approved Local API authorization. Authorization sends one bounded application name and the expected server identity to the fixed loopback endpoint; only a same-server bounded response that explicitly reports denial is classified as the user's decision. Denial and rate limiting return immediately without another prompt or automatic retry. The private 32-character key is neither serializable nor printable. Authorization, read, and write responses bind to the expected server before status classification; writes name expired authorization and matching-server stale preconditions separately. Thin public execution boundaries connect the adapter to the reviewed write and rollback cores without duplicating mutation logic. Synthetic transport evidence does not satisfy AC6's approved live Zotero 10 authorization, write, and rollback requirement. +A local steward worksheet must bind the library version, rule revision, complete raw-snapshot digest, current proposal-and-retained-source digest, every observed parent/child item revision, and one blank decision slot per classified bibliographic item. It repeats item identity, proposal, and abstention reason only; titles, abstracts, tags, collections, and matched evidence remain in the separate sensitive report. Shared inventory validation rejects omitted source records, hidden pending relationships and inconsistent identity before construction. Valid unresolved sources do not prevent starting review, but prevent claiming completion. Old worksheets without the content binding require regeneration, never automatic approval backfill. + Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and binds both the complete source/classifier-input snapshot and every current proposal field, in addition to the item-key/item-version coordinates. Same-version changes to unmodeled provider metadata, absent/default fields, classifier inputs, predictions or supporting evidence must invalidate the corresponding binding. Evaluation recomputes proposal identity before contacting governance; a locally changed digest cannot renew an approval. Legacy unbound approvals require reissuance, never automatic backfill. Abstention is a prediction outcome, never an approved truth label. Evaluation emits the verified library revision, rule revision, opaque snapshot and proposal digests, and aggregate counts for exact matches, abstentions, and per-disposition true-positive/predicted/expected totals; it must not copy Zotero keys, reviewer identity, or bibliographic text into the result. A complete metadata-review result additionally requires exactly one non-abstention steward label for every top-level bibliographic item and no unresolved source records. Standalone sources, orphan trees and disconnected cycles must be resolved before completion; clearing their reported list cannot bypass inventory validation. A sampled golden set remains valid for quality measurement but cannot prove completion. Neither result proves full-text approval or an applied Zotero reclassification. diff --git a/docs/TRD.md b/docs/TRD.md index 3080f01..3703381 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -109,6 +109,8 @@ Provider deserialization captures each complete JSON object before projecting me The complete metadata-review evaluator rejects unequal label cardinality or nonempty `pending_source_item_keys` with `IncompleteReview` before governance. The shared evaluator then recomputes the complete inventory and pending ancestry, so clearing pending keys and rewriting the proposal digest still fails local validation. Because shared validation rejects blank, duplicate, and unknown keys, equal cardinality proves bibliographic label coverage. Sampled evaluation still supports pending sources; completion does not prove a Zotero mutation or full-text approval. A successful classification report carries an `audit_summary` whose snapshot, bibliographic, proposed-disposition, provenance-complete, abstention, duplicate-candidate, failure, and per-disposition counts are derived from the same in-memory immutable snapshot. Zotero item version zero remains a valid observed coordinate for never-synced Zotero 9 records; provenance completeness rejects a missing item key rather than inventing a positive-only version invariant. Reader failures return an error instead of a partial report; therefore a returned report records `failure_count=0` rather than hiding partial failures. +The review worksheet is a deterministic item-key-ordered projection of the report. It binds library/rule revisions, raw-snapshot digest, required `proposal_digest` from the existing v2 scope hash, complete item coordinates, proposal, abstention reason, and one initially empty decision per bibliographic item. Construction reuses `validate_classification_report`, mapped to `WorksheetError::InvalidReport`, before worksheet-specific nonblank identity and abstention checks. This avoids a second drifting audit implementation while admitting structurally valid pending source evidence. It deliberately omits bibliographic text and matched evidence; stewards consult the owner-only report by item key. Missing proposal binding fails deserialization. Subsequent progress, application and finalization owners must compare this field to the recomputed report binding; present-but-blank or rewritten values are not authority. Legacy worksheets must be regenerated, and independent approval remains separate. + The local report can contain titles, tags, matched metadata, and abstention abstracts. It is sensitive steward-review material, remains outside the repository, and is not a publication artifact. The report is local JSON and contains proposals rather than governance decisions. On supported Unix platforms, CLI output is restricted to a new owner-readable/writable (`0600`) direct child of canonical `/tmp` or the operating system temporary directory; exact permissions are restored after umask application, and other platforms fail closed. Relative paths, nested paths, existing paths, and symlinks are rejected, and create-new file semantics prevent overwrite/path-swap writes. Reviewed collection/tag changes can produce a pure local plan whose default mode is dry-run. The plan requires exact report and item preconditions, complete before/after/rollback arrays, externally verified authority, and preserved Zotero tag types; its fields are externally read-only after validation. Zotero 9 execute mode fails closed. Every receipt copies the plan's review, authority, server, Zotero version, library, rule, snapshot and proposal coordinates; dry-run reports every operation as not attempted and makes no Local API call. Execute mode preflights every item before the first write, advances the library precondition only from a directly verified write response, stops on the first adapter or response failure, and re-reads that item through the same boundary as observation only. Failed writes remain indeterminate regardless of observed metadata; no inverse is issued for them. Prior directly verified operations retain their inverse coordinates. The API key remains adapter-owned and absent from serializable structures. diff --git a/docs/UML.md b/docs/UML.md index e20f31b..9a03998 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -57,6 +57,7 @@ sequenceDiagram Intake->>Intake: retain excluded metadata; traverse parent links from bibliographic roots Intake->>Report: write proposals, complete inventory and unresolved source keys Note over Report,Steward: Pending sources prevent a whole-library completion claim; inventory is not approval + Intake->>Report: derive snapshot-bound decision worksheet without bibliographic text Report->>Steward: review dispositions and merge candidates Steward->>Intake: verified labels for every bibliographic item Steward->>Intake: reviewed labels and independently issued receipt diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index 72fd8ba..7d92be5 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -24,6 +24,12 @@ Duplicate candidates become canonical references only through externally verifie Classifier quality is measured only against local steward-reviewed labels whose reviewed set is verified outside this crate and bound to the exact library version, rule revision, canonical SHA-256 raw-snapshot digest, and every observed parent/child item-key/item-version coordinate. `NeedsStewardReview` is an abstention prediction and cannot be approved truth. Sampled labels may measure classifier quality, but a full-reclassification completion result requires exactly one approved label for every classified bibliographic item. Cardinality, snapshot, key, disposition, and duplicate checks run before the external approval verifier so invalid local input cannot consume approval authority. Evaluation returns the verified revisions and opaque digest with aggregate integer evidence; Zotero keys, reviewer identity, and bibliographic text are omitted. Missing, incomplete, stale, content- or label-mismatched, unverified, unknown, duplicate, or invalid review identities fail closed at the applicable completion boundary. Every successful report includes an aggregate audit summary computed from the same captured snapshot. Zotero 9 item version zero is preserved as a valid never-synced source coordinate, not treated as missing provenance. Partial reads never produce a report, so successful output explicitly records zero failures alongside snapshot, proposal, provenance, abstention, duplicate, and per-disposition totals. +To make full review executable without copying sensitive text again, ConceptWeave derives a local worksheet from the validated report. The worksheet carries the exact snapshot binding, complete parent/child revision coordinates, deterministic proposal and abstention reason, plus one empty steward-decision slot per bibliographic item in item-key order. Titles, abstracts, tags, collections, and matched evidence remain only in the owner-only report. Invalid report identity or coverage fails worksheet construction. + +Proposed worksheet source-scope amendment: in the context of continuing review across saved local worksheets, facing removed retained records or changed context at unchanged item revisions, we decided for the existing shared report validator and required v2 proposal digest, and against a duplicate worksheet audit or coordinate-only identity, to preserve complete source scope and distinguish changed review material, accepting a breaking serialized-field requirement and downstream comparison work. RED `900038e` reproduces omitted inventory and hidden pending keys; `5b54d06` removes the divergent checks in favor of the shared validator. RED `30aa091` reproduces equal worksheets for changed content and successful loading without binding; `97046c7` reuses the existing scope hash. Valid standalone, orphan and cyclic evidence still permits blank worksheet creation, because preparation is not completion. No source metadata is copied into a new artifact. + +Older worksheets require regeneration, not a fabricated digest or approval. The later progress, patch and finalization owners must compare the worksheet's digest against the report, including rejecting blank or locally rewritten values, before external approval verification. Those later consumers have not yet adopted this field at this checkpoint. A valid hash proves identity rather than authorship or authorization, and no metadata worksheet downcast grants full-text or Zotero write authority. Keep this amendment Proposed until protected owner/consumer and release evidence is available; the Gap baseline records exact tests and remaining live work. + The reader fails closed above 50,000 items or 256 MiB of cumulative response bodies, while retaining the 8 MiB per-page bound, finite request timeouts, redirect denial, total-count checks, snapshot-version checks, and duplicate-key detection. Pagination, consistency, resource-budget, and provider-contract behavior are separated from the narrow `ureq` transport so deterministic tests exercise the production reader core rather than excluding the entire reader from coverage. In the context of reading every bibliographic source before classification, facing individually timely pages that can cumulatively hold a run open for days, we decided for a five-minute monotonic admission/completion budget in the existing reader and against rejecting legitimate short pages or adding another transport, to bound accepted work without excluding papers, accepting that an already-started request or classification computation can finish after the limit before its result is rejected. This is an application read limit, not a model timeout, hard process-cancellation deadline, wall-clock/suspend guarantee or atomic snapshot claim. Each page is checked before fetch and after return, and the complete report is checked before return. The stdlib clock has a private deterministic test seam; public APIs, provider timeouts and data/byte ceilings are unchanged. The [deadline doctoring](../doctoring/zotero_metadata_deadline.md) records the original review, RED/GREEN, alternatives and exact verification. This amendment remains Proposed and grants no Zotero mutation authority. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 10ddb3e..18a8a44 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,6 +6,16 @@ This file records code-current product and technical gaps. Exact PR/check/run co ## September 6 source inventory checkpoint +### PR25 worksheet admission and identity repair + +Original PR25 `c6b4c17e931951a2e1d4ea79ac79363f6306a5bf` passed 126 tests/24 suites. Normal merge `4a1a3bb` retains it and PR24 `35c57ca4510a65cf48069285d78b95cf47db65ba`; integrated tests passed 153/24. RED `900038e` compiled with 4 passing and 2 failing tests: omitted retained inventory and hidden pending keys still produced worksheets. `5b54d06` reuses shared report validation and removes 54 lines of divergent audit/coordinate checks. Valid standalone, orphan, cyclic and attached source metadata remains reviewable with blank decisions. Completion admission remains distinct. + +RED `30aa091` compiled with 6 passing and 2 failing tests: changed retained metadata produced an equal worksheet and missing proposal binding deserialized successfully. Final source `97046c7` adds a required worksheet `proposal_digest` from the existing v2 hash; source/context changes produce different identities and missing binding fails loading. No new hash, library or authority issuer was introduced. Independent read-only review found no additional production regression. Required downstream repair: compare worksheet/report binding before progress, patch and finalization, including blank or locally rewritten digest rejection; never backfill older worksheets or infer full-text authority. + +Final local tests are 158/24 suites including three doctests, strict Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks pass. Unchanged pinned coverage passes 282/282 functions, 2,468/2,468 normalized regions and 414/414 normalized branches. Raw coverage remains 3,232/3,293 lines, 4,858/4,960 regions and 369/414 branches, not 100%. Logs: `/tmp/conceptweave-pr25-{baseline,integration,source-red,binding-red,final,clippy-final,rustdoc-final,coverage-final}.log`. + +The root runtime and later consumers have not adopted this repair. Actual decisions and independent approvals remain 0/3,715 plus four unresolved standalone sources. No real Zotero record, classification label or approval was created. Native Visual Inspection was attempted again but the Mac is locked, so there is no new screenshot evidence. PRD/TRD and Proposed ADR 0006 record scope, compatibility and remaining adoption. Local verification is not hosted current-head GREEN, protected approval/merge or release. + ### PR24 pending-source completion repair Final source `5b2282a` passes strict all-target Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks. The unchanged pinned coverage gate passes 279/279 functions, 2,420/2,420 normalized regions and 408/408 normalized branches. Raw coverage remains 3,198/3,259 lines, 4,810/4,912 regions and 363/408 branches, not 100%. Logs: `/tmp/conceptweave-pr24-{clippy,rustdoc,coverage}-verified.log`. No predecessor or later-head coverage is attributed to this source.