From 74137cf644348475fcf35637d06c9a21b6de7c87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:49:26 +0900 Subject: [PATCH 01/14] test(zotero): specify delayed rollback reconciliation --- .../classification_rollback_reconciliation.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs diff --git a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs new file mode 100644 index 00000000..a55c6d92 --- /dev/null +++ b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs @@ -0,0 +1,94 @@ +use conceptweave_zotero::{ + ClassificationItemState, ClassificationRollbackOperation, ClassificationRollbackState, + ItemTag, reconcile_classification_rollback, +}; + +fn tag(value: &str) -> ItemTag { + ItemTag { + tag: value.into(), + tag_type: None, + } +} + +fn operation() -> ClassificationRollbackOperation { + ClassificationRollbackOperation { + server_id: "server-1".into(), + item_key: "ABCDEFGH".into(), + item_version: 10, + expected_collection_keys: vec!["classified".into()], + expected_tags: vec![tag("Classified")], + collection_keys: vec!["source".into()], + tags: vec![tag("Imported")], + } +} + +fn state( + item_version: u64, + collection_keys: Vec, + tags: Vec, +) -> ClassificationItemState { + ClassificationItemState { + server_id: "server-1".into(), + library_version: 99, + item_key: "ABCDEFGH".into(), + item_version, + collection_keys, + tags, + } +} + +#[test] +fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state() { + let operation = operation(); + let cases = [ + ( + state( + 11, + operation.collection_keys.clone(), + operation.tags.clone(), + ), + ClassificationRollbackState::Restored, + ), + ( + state( + 10, + operation.expected_collection_keys.clone(), + operation.expected_tags.clone(), + ), + ClassificationRollbackState::Unchanged, + ), + ( + state(11, vec!["other".into()], operation.tags.clone()), + ClassificationRollbackState::Indeterminate, + ), + ]; + + for (observed, expected) in cases { + assert_eq!( + reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(observed.clone())), + Ok(expected) + ); + } +} + +#[test] +fn later_reconciliation_preserves_read_failures_and_rejects_wrong_identity() { + let operation = operation(); + assert_eq!( + reconcile_classification_rollback(&operation, |_| Err::( + "read_failed" + )), + Err("read_failed") + ); + + let mut wrong_server = state( + 10, + operation.expected_collection_keys.clone(), + operation.expected_tags.clone(), + ); + wrong_server.server_id = "server-2".into(); + assert_eq!( + reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(wrong_server.clone())), + Ok(ClassificationRollbackState::Indeterminate) + ); +} From eb9e30f5c8dd3e158bef6da4095d62b9c42d39cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:50:54 +0900 Subject: [PATCH 02/14] test(zotero): require reconciliation evidence receipts --- .../classification_rollback_reconciliation.rs | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs index a55c6d92..67a3b88c 100644 --- a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs +++ b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs @@ -61,12 +61,37 @@ fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state state(11, vec!["other".into()], operation.tags.clone()), ClassificationRollbackState::Indeterminate, ), + ( + state( + 11, + operation.expected_collection_keys.clone(), + operation.expected_tags.clone(), + ), + ClassificationRollbackState::Indeterminate, + ), + ( + state( + 10, + operation.collection_keys.clone(), + operation.tags.clone(), + ), + ClassificationRollbackState::Indeterminate, + ), + ( + state(10, vec![" ".into()], operation.expected_tags.clone()), + ClassificationRollbackState::Indeterminate, + ), ]; for (observed, expected) in cases { + let receipt = + reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(observed.clone())); + assert_eq!(receipt.state, expected); + assert_eq!(receipt.operation, operation); + assert_eq!(receipt.observed_state, Some(observed)); assert_eq!( - reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(observed.clone())), - Ok(expected) + receipt.retry_operation, + (expected == ClassificationRollbackState::Unchanged).then(|| operation.clone()) ); } } @@ -74,12 +99,13 @@ fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state #[test] fn later_reconciliation_preserves_read_failures_and_rejects_wrong_identity() { let operation = operation(); - assert_eq!( - reconcile_classification_rollback(&operation, |_| Err::( - "read_failed" - )), - Err("read_failed") - ); + let unreadable = reconcile_classification_rollback(&operation, |_| { + Err::("read_failed") + }); + assert_eq!(unreadable.state, ClassificationRollbackState::Indeterminate); + assert_eq!(unreadable.operation, operation); + assert!(unreadable.observed_state.is_none()); + assert!(unreadable.retry_operation.is_none()); let mut wrong_server = state( 10, @@ -87,8 +113,39 @@ fn later_reconciliation_preserves_read_failures_and_rejects_wrong_identity() { operation.expected_tags.clone(), ); wrong_server.server_id = "server-2".into(); + let mismatched = + reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(wrong_server.clone())); assert_eq!( - reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(wrong_server.clone())), - Ok(ClassificationRollbackState::Indeterminate) + mismatched.state, + ClassificationRollbackState::Indeterminate ); + assert_eq!(mismatched.observed_state, Some(wrong_server)); + assert!(mismatched.retry_operation.is_none()); + + let mut wrong_item = state( + 10, + operation.expected_collection_keys.clone(), + operation.expected_tags.clone(), + ); + wrong_item.item_key = "BCDEFGHJ".into(); + assert_eq!( + reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(wrong_item)).state, + ClassificationRollbackState::Indeterminate + ); + + let mut invalid_operation = operation.clone(); + invalid_operation.expected_collection_keys.push(" ".into()); + let invalid = reconcile_classification_rollback( + &invalid_operation, + |_| -> Result { + panic!("invalid reconciliation evidence must fail before reading") + }, + ); + assert_eq!(invalid.state, ClassificationRollbackState::Indeterminate); + assert!(invalid.observed_state.is_none()); + assert!(invalid.retry_operation.is_none()); + + let serialized = serde_json::to_string(&invalid).unwrap(); + assert!(!serialized.to_ascii_lowercase().contains("api_key")); + assert!(!serialized.contains("read_failed")); } From b7a22c75e3d2a85c0aca03a6418eb8699478bd22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:51:29 +0900 Subject: [PATCH 03/14] feat(zotero): reconcile indeterminate rollbacks read only --- crates/conceptweave-zotero/src/lib.rs | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index a68b8ee7..c3c0755a 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -855,6 +855,31 @@ pub enum ClassificationRollbackOutcome { PartialFailure, } +/// Current state of one previously indeterminate rollback operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ClassificationRollbackState { + /// The restoration metadata is present at a newer item revision. + Restored, + /// The expected post-write metadata and item revision remain unchanged. + Unchanged, + /// The observed state proves neither safe outcome. + Indeterminate, +} + +/// Secret-free evidence from one delayed, read-only rollback reconciliation. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct ClassificationRollbackReconciliationReceipt { + /// State proven by the delayed observation. + pub state: ClassificationRollbackState, + /// Complete operation under reconciliation. + pub operation: ClassificationRollbackOperation, + /// Successfully observed state, absent when validation or reading failed. + pub observed_state: Option, + /// Operation eligible for the existing complete-preflight retry path. + pub retry_operation: Option, +} + /// Secret-free evidence for restored, failed, indeterminate, and pending inverse writes. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ClassificationRollbackReceipt { @@ -1775,6 +1800,43 @@ pub fn execute_classification_rollback_with_zotero10( ) } +/// Re-reads one indeterminate rollback operation without performing a write. +pub fn reconcile_classification_rollback( + operation: &ClassificationRollbackOperation, + read_item: impl FnOnce(&str) -> Result, +) -> ClassificationRollbackReconciliationReceipt { + let valid_operation = !operation.server_id.trim().is_empty() + && !operation.item_key.trim().is_empty() + && normalized_metadata( + &operation.expected_collection_keys, + &operation.expected_tags, + ) + .is_ok() + && normalized_metadata(&operation.collection_keys, &operation.tags).is_ok(); + let observed_state = valid_operation + .then(|| read_item(&operation.item_key).ok()) + .flatten(); + let state = observed_state + .as_ref() + .map(|observed| classify_rollback_state(observed, operation)) + .unwrap_or(ClassificationRollbackState::Indeterminate); + ClassificationRollbackReconciliationReceipt { + state, + operation: operation.clone(), + observed_state, + retry_operation: (state == ClassificationRollbackState::Unchanged) + .then(|| operation.clone()), + } +} + +/// Reconciles one rollback operation through the server-bound Zotero 10 adapter. +pub fn reconcile_classification_rollback_with_zotero10( + operation: &ClassificationRollbackOperation, + adapter: &Zotero10LocalAdapter, +) -> ClassificationRollbackReconciliationReceipt { + reconcile_classification_rollback(operation, |item_key| adapter.get_item(item_key)) +} + fn rollback_preflight_failure( operations: &[ClassificationRollbackOperation], failed_item_key: &str, @@ -1800,6 +1862,32 @@ fn matches_rollback_current( matches_rollback_current_at(state, state.library_version, operation) } +fn classify_rollback_state( + state: &ClassificationItemState, + operation: &ClassificationRollbackOperation, +) -> ClassificationRollbackState { + let Some((collections, tags)) = normalized_metadata(&state.collection_keys, &state.tags).ok() + else { + return ClassificationRollbackState::Indeterminate; + }; + if state.server_id != operation.server_id || state.item_key != operation.item_key { + return ClassificationRollbackState::Indeterminate; + } + if state.item_version == operation.item_version + && collections == operation.expected_collection_keys + && tags == operation.expected_tags + { + ClassificationRollbackState::Unchanged + } else if state.item_version > operation.item_version + && collections == operation.collection_keys + && tags == operation.tags + { + ClassificationRollbackState::Restored + } else { + ClassificationRollbackState::Indeterminate + } +} + fn matches_rollback_current_at( state: &ClassificationItemState, library_version: u64, From 7ce522a3028f03a0a06103a5df2cbb120a6019e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:52:19 +0900 Subject: [PATCH 04/14] test(zotero): cover adapter reconciliation boundary --- crates/conceptweave-zotero/src/lib.rs | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index c3c0755a..71206cd7 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -2778,6 +2778,41 @@ mod tests { assert_eq!(server.join().unwrap().len(), 4); } + #[test] + fn zotero10_adapter_reconciles_an_indeterminate_rollback_without_writing() { + let operation = ClassificationRollbackOperation { + server_id: "server-10".into(), + item_key: "ABCD2345".into(), + item_version: 43, + expected_collection_keys: vec!["CDEF4567".into()], + expected_tags: vec![ItemTag { + tag: "classified".into(), + tag_type: None, + }], + collection_keys: vec!["BCDE3456".into()], + tags: vec![ItemTag { + tag: "kept".into(), + tag_type: Some(1), + }], + }; + let item_body = r#"{"key":"ABCD2345","version":43,"data":{"itemType":"book","collections":["CDEF4567"],"tags":[{"tag":"classified"}]}}"#; + let responses = vec![ + Box::leak(library_response("server-10", 99).into_boxed_str()), + Box::leak( + raw_response(Some("server-10"), Some(43), item_body).into_boxed_str(), + ), + Box::leak(library_response("server-10", 99).into_boxed_str()), + ]; + let (base, server) = serve(responses); + + let receipt = + reconcile_classification_rollback_with_zotero10(&operation, &transport(base)); + + assert_eq!(receipt.state, ClassificationRollbackState::Unchanged); + assert_eq!(receipt.retry_operation, Some(operation)); + assert_eq!(server.join().unwrap().len(), 3); + } + #[test] fn zotero10_authorization_uses_exact_wire_contract_and_builds_adapter() { let body = r#"{"key":"0123456789abcdef0123456789abcdef","remember":true}"#; From b2120ad436c2a3ffb3e849990f1e59a9f730950c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:53:36 +0900 Subject: [PATCH 05/14] docs(zotero): record delayed rollback reconciliation --- CHANGELOG.md | 1 + docs/PRD.md | 2 +- docs/TRD.md | 2 +- docs/adr/0007-reviewed-zotero-write-plan.md | 2 +- docs/product-technical-gap-baseline.md | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8910d6fa..ac34db04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to ConceptWeave are documented here. - Fail-closed Draft -> Proposed -> Validated -> Reviewed -> Published lifecycle with explicit rejection and supersession. - Draft 2020-12 JSON Schema for the semantic-candidate public contract. - Standards and research doctoring covering stable W3C ontology standards, 2026 RDF/SHACL work in progress, Apache Ossie, and recent LLM ontology-engineering research. +- Read-only delayed reconciliation receipts for indeterminate Zotero rollback operations. ### Security diff --git a/docs/PRD.md b/docs/PRD.md index 1ee215b4..9b0576d1 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -62,7 +62,7 @@ For every connected duplicate component, accept externally verified steward deci Reviewed collection and tag changes default to a local dry-run plan. Each operation binds the authority receipt, server/library/item revisions, raw-snapshot digest, and complete before/after/rollback metadata. Execution-critical plan state is immutable outside the owner crate, so callers cannot turn a dry run into execution or alter validated operations. Zotero 9 execute requests fail closed. No plan contains credentials or permits `NeedsStewardReview`, source-record deletion, or attachment deletion. -For execute-mode plans, the runtime must preflight every item before the first write, stop at the first failed or unverifiable response, reconcile that item through the same server before declaring its state, and emit a secret-free receipt. The receipt identifies verified writes, the failed item, any indeterminate item, untouched items, and reverse-ordered rollback operations bound to the server identity, proven post-write item revision, and complete expected post-write metadata. Rollback must reject mixed-server evidence before reading, preflight every receipt item at one current library version before its first inverse write, consume the existing receipt order, advance the library precondition only from a verified response, and reconcile a failed response as restored, unchanged, or indeterminate. Only proven unchanged and untouched operations remain eligible for automatic retry; an indeterminate operation is retained separately with complete reconciliation evidence for an operator. A second use of consumed evidence must fail before writing. Cross-item atomicity is not claimed. +For execute-mode plans, the runtime must preflight every item before the first write, stop at the first failed or unverifiable response, reconcile that item through the same server before declaring its state, and emit a secret-free receipt. The receipt identifies verified writes, the failed item, any indeterminate item, untouched items, and reverse-ordered rollback operations bound to the server identity, proven post-write item revision, and complete expected post-write metadata. Rollback must reject mixed-server evidence before reading, preflight every receipt item at one current library version before its first inverse write, consume the existing receipt order, advance the library precondition only from a verified response, and reconcile a failed response as restored, unchanged, or indeterminate. Only proven unchanged and untouched operations remain eligible for automatic retry; an indeterminate operation is retained separately with complete reconciliation evidence for an operator. Delayed reconciliation performs one read and no write, preserves the observed state, ignores unrelated library-version advancement, and emits retry evidence only when the exact item revision and expected metadata remain unchanged. A second use of consumed evidence must fail before writing. Cross-item atomicity is not claimed. 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. diff --git a/docs/TRD.md b/docs/TRD.md index 89d93346..bfce6992 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -68,6 +68,6 @@ Duplicate review is independent of subject classification. A reviewed decision s Golden-set evaluation accepts only a governance receipt verified with the complete reviewed set by a caller-owned authorization boundary. Its library version, rule revision, canonical SHA-256 content digest, and every observed parent/child item-key/item-version identity must bind the classification report. The digest covers every raw Zotero item in canonical key order. Blank, duplicate, unknown, stale, content-mismatched, label-mismatched, or abstention-as-truth inputs fail closed. 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. Reviewed collection/tag changes can produce a pure local plan whose default mode is dry-run. The plan requires exact report and item preconditions, complete before/after/rollback arrays, externally verified authority, and preserved Zotero tag types; its fields are externally read-only after validation. Zotero 9 execute mode fails closed. The execution core makes no call in dry-run mode; otherwise it preflights every item before the first write, advances the library precondition only from verified state, stops on the first adapter or response failure, and re-reads that item through the same boundary. A proven applied state receives reverse-ordered rollback evidence containing server identity, post-write item revision, expected post-write metadata, and the complete restoration state even when the write response was lost; a state matching neither the before nor after contract is marked indeterminate. The generic rollback core rejects operations spanning server identities before any read, then reads every receipt item at one current library version and verifies that evidence before writing. It follows receipt order, advances the library version only after a verified inverse write, and on failure re-reads the item to classify restored, unchanged, or indeterminate state. Its secret-free receipt separates restored, failed, indeterminate, not-attempted, and remaining work. Automatic retry evidence includes a failed current operation only when it is proven unchanged; an indeterminate operation and its complete metadata are retained separately for operator reconciliation. Already consumed evidence fails preflight on reuse. +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. Reviewed collection/tag changes can produce a pure local plan whose default mode is dry-run. The plan requires exact report and item preconditions, complete before/after/rollback arrays, externally verified authority, and preserved Zotero tag types; its fields are externally read-only after validation. Zotero 9 execute mode fails closed. The execution core makes no call in dry-run mode; otherwise it preflights every item before the first write, advances the library precondition only from verified state, stops on the first adapter or response failure, and re-reads that item through the same boundary. A proven applied state receives reverse-ordered rollback evidence containing server identity, post-write item revision, expected post-write metadata, and the complete restoration state even when the write response was lost; a state matching neither the before nor after contract is marked indeterminate. The generic rollback core rejects operations spanning server identities before any read, then reads every receipt item at one current library version and verifies that evidence before writing. It follows receipt order, advances the library version only after a verified inverse write, and on failure re-reads the item to classify restored, unchanged, or indeterminate state. Its secret-free receipt separates restored, failed, indeterminate, not-attempted, and remaining work. Automatic retry evidence includes a failed current operation only when it is proven unchanged; an indeterminate operation and its complete metadata are retained separately for operator reconciliation. The delayed reconciliation boundary performs exactly one server-bound read and no write. It treats an exact item revision plus expected metadata as unchanged even if unrelated library changes advanced the library version, and treats restoration metadata as restored only at a newer item revision; every identity, metadata, or version ambiguity remains indeterminate. Already consumed evidence fails preflight on reuse. The Zotero 10+ transport is pinned to loopback, rejects redirects, and uses finite timeouts. A one-shot authorization POST to `/api/local/authorize` sends JSON `{ "appName": ... }`, `Content-Type: application/json`, and the expected `Zotero-Server-ID`. Application names must be nonblank and at most 128 bytes. Every authorization, read, and write response must repeat that exact server identity before its status is interpreted. A bounded `200 OK` authorization response contains a 32-byte visible-ASCII key plus the `remember` decision. A same-server `403` is classified as denial only when its bounded JSON body parses with `denied: true`; missing, malformed, oversized, or false denial evidence fails closed. `429` exposes only a safe integer `Retry-After` delta of at most one day. Neither condition retries or prompts again. The authorization wrapper is neither debug-printable nor serializable, keeps the key private, exposes only the remembered decision, and can be consumed into the existing adapter. Item responses remain capped at 1 MiB. Writes distinguish same-server `401` reauthorization from same-server `412` stale preconditions, while a different-server `412` on library, item, or write paths is a database switch; all errors remain static and secret-free. Narrow adapter functions reuse the generic write and rollback cores. Mock TCP evidence covers the wire contract, but no approved live Zotero 10 authorization, write, partial-failure, or rollback has been performed. diff --git a/docs/adr/0007-reviewed-zotero-write-plan.md b/docs/adr/0007-reviewed-zotero-write-plan.md index a2896ec1..9528c958 100644 --- a/docs/adr/0007-reviewed-zotero-write-plan.md +++ b/docs/adr/0007-reviewed-zotero-write-plan.md @@ -14,7 +14,7 @@ ConceptWeave builds a local-only `ClassificationWritePlan` from an externally ve Execute planning fails closed for Zotero versions below 10. The plan contains no API key and performs no network call. The execution core accepts caller-owned preflight and write functions, preflights the complete plan before the first mutation, and verifies server, library, item revision, collection, and typed-tag responses. After a failed or invalid write response, it reuses the same read boundary to distinguish unchanged, applied, and indeterminate state. A reconciled applied item receives a rollback operation; an unprovable state is named explicitly and requires operator reconciliation. -The authenticated Zotero 10+ adapter is a narrow loopback transport for those injected functions. A caller may supply credentials directly or perform one official `/api/local/authorize` request with a bounded nonblank application name and expected server identity. Every authorization, read, and write response must repeat that identity before status classification. Success returns an exact 32-character header-safe key plus the remembered decision; denial requires same-server bounded JSON with `denied: true`. The private authorization wrapper can only disclose the remembered decision or be consumed into the existing adapter; neither value is debuggable or serializable. Denial and rate limiting never trigger an automatic retry or repeated prompt, and only a bounded integer retry delay is retained. Writes distinguish an expired authorization from a matching-server stale precondition, while a different-server `412` invalidates the read/write partition as a database switch. Thin public adapter boundaries delegate to the generic write and rollback cores rather than creating parallel mutation logic. Rollback evidence binds the server, post-write item revision, complete expected current metadata, and complete restoration metadata. Before its first read, rollback rejects evidence spanning server identities; before its first write, it verifies every item at one current library version. It then follows the already reversed receipt order and advances that version only from a verified write. A failed or unverifiable response is re-read once and classified as restored, unchanged, or indeterminate. Only a proven unchanged current operation and untouched later operations remain eligible for automatic retry; an indeterminate operation retains the complete evidence required for operator reconciliation in a separate field. Reusing consumed evidence fails preflight before writing. Static errors and serializable receipts cannot echo a credential, response body, or URL. Cross-item transactionality is not claimed, and source records and attachments are never deleted. +The authenticated Zotero 10+ adapter is a narrow loopback transport for those injected functions. A caller may supply credentials directly or perform one official `/api/local/authorize` request with a bounded nonblank application name and expected server identity. Every authorization, read, and write response must repeat that identity before status classification. Success returns an exact 32-character header-safe key plus the remembered decision; denial requires same-server bounded JSON with `denied: true`. The private authorization wrapper can only disclose the remembered decision or be consumed into the existing adapter; neither value is debuggable or serializable. Denial and rate limiting never trigger an automatic retry or repeated prompt, and only a bounded integer retry delay is retained. Writes distinguish an expired authorization from a matching-server stale precondition, while a different-server `412` invalidates the read/write partition as a database switch. Thin public adapter boundaries delegate to the generic write and rollback cores rather than creating parallel mutation logic. Rollback evidence binds the server, post-write item revision, complete expected current metadata, and complete restoration metadata. Before its first read, rollback rejects evidence spanning server identities; before its first write, it verifies every item at one current library version. It then follows the already reversed receipt order and advances that version only from a verified write. A failed or unverifiable response is re-read once and classified as restored, unchanged, or indeterminate. Only a proven unchanged current operation and untouched later operations remain eligible for automatic retry; an indeterminate operation retains the complete evidence required for operator reconciliation in a separate field. A delayed reconciliation reads once without writing and records the current state. Library-level advancement alone is not item-change evidence: unchanged requires the exact item revision and expected metadata, while restored requires restoration metadata at a newer item revision. Reusing consumed evidence fails preflight before writing. Static errors and serializable receipts cannot echo a credential, response body, or URL. Cross-item transactionality is not claimed, and source records and attachments are never deleted. ## Consequences diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 4c203189..c361feb1 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -46,7 +46,7 @@ The successor authorization slice adds one-shot Zotero 10 Local API authorizatio 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 retains verified revisions plus an opaque snapshot digest so detached metrics remain attributable. Item and reviewer identities stay out of its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Connected duplicate components now produce a snapshot-bound local review manifest only after external steward verification; every operation retains all component source revisions and before/after/rollback canonical mappings while Zotero records remain unchanged. Reviewed collection/tag changes produce a default-dry-run plan bound to exact server, library, item, rule, digest, and complete metadata preconditions; externally read-only plan state prevents post-validation forgery, automatic-tag type is preserved, and Zotero 9 execute mode is rejected. The injected execution core calls nothing in dry-run mode, preflights every item before a write, stops at the first failure, reconciles a lost or invalid response with a same-boundary read, and emits rollback evidence for every item whose applied state is proven. The generic rollback executor rejects mixed-server evidence before reading, verifies all expected post-write states at one current library version before the first inverse write, follows the receipt's reverse order, advances only from verified writes, and stops with restored, failed, indeterminate, not-attempted, and remaining classifications. Unprovable state is reported as indeterminate with complete operation evidence retained separately and excluded from automatic retry until operator reconciliation; reuse after restoration fails before writing. A fixed-loopback Zotero 10 adapter supplies stable server-pinned reads and authenticated one-item writes with atomic library/item preconditions, complete collection/tag replacement, and bounded verified responses; thin wrappers reuse both generic executors. Mock fixtures verify these contracts and secret-free failures. Korean, Japanese, Chinese, Vietnamese, Spanish, German, and French ontology-alignment metadata now have explicit fail-closed abstention coverage alongside the existing English positive case; this is safety evidence, not translated classification support. No real precision/recall, duplicate merge, write, or rollback claim exists until a steward supplies reviewed local decisions and a production authorization adapter verifies them. AC6 still requires approved live Zotero 10 write, partial-failure, and rollback evidence. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. +The golden-set evaluation contract now records aggregate precision/recall numerators and denominators, requires an externally verified governance receipt bound to the complete item-key/item-version snapshot, rejects abstention as expected truth, and retains verified revisions plus an opaque snapshot digest so detached metrics remain attributable. Item and reviewer identities stay out of its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Connected duplicate components now produce a snapshot-bound local review manifest only after external steward verification; every operation retains all component source revisions and before/after/rollback canonical mappings while Zotero records remain unchanged. Reviewed collection/tag changes produce a default-dry-run plan bound to exact server, library, item, rule, digest, and complete metadata preconditions; externally read-only plan state prevents post-validation forgery, automatic-tag type is preserved, and Zotero 9 execute mode is rejected. The injected execution core calls nothing in dry-run mode, preflights every item before a write, stops at the first failure, reconciles a lost or invalid response with a same-boundary read, and emits rollback evidence for every item whose applied state is proven. The generic rollback executor rejects mixed-server evidence before reading, verifies all expected post-write states at one current library version before the first inverse write, follows the receipt's reverse order, advances only from verified writes, and stops with restored, failed, indeterminate, not-attempted, and remaining classifications. Unprovable state is reported as indeterminate with complete operation evidence retained separately and excluded from automatic retry until operator reconciliation. A later read-only reconciliation records the observed state, tolerates unrelated library-version advancement, and emits retry evidence only for an exact unchanged item; restoration metadata at a newer item revision proves current state but not causality. Reuse after restoration fails before writing. A fixed-loopback Zotero 10 adapter supplies stable server-pinned reads and authenticated one-item writes with atomic library/item preconditions, complete collection/tag replacement, and bounded verified responses; thin wrappers reuse both generic executors. Mock fixtures verify these contracts and secret-free failures. Korean, Japanese, Chinese, Vietnamese, Spanish, German, and French ontology-alignment metadata now have explicit fail-closed abstention coverage alongside the existing English positive case; this is safety evidence, not translated classification support. No real precision/recall, duplicate merge, write, or rollback claim exists until a steward supplies reviewed local decisions and a production authorization adapter verifies them. AC6 still requires approved live Zotero 10 write, partial-failure, and rollback evidence. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. 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 57b065c37ada7fb64334358c817899e79934073f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:53:48 +0900 Subject: [PATCH 06/14] style(zotero): format reconciliation contract --- crates/conceptweave-zotero/src/lib.rs | 7 ++----- .../tests/classification_rollback_reconciliation.rs | 9 +++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 71206cd7..ab246003 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -2798,15 +2798,12 @@ mod tests { let item_body = r#"{"key":"ABCD2345","version":43,"data":{"itemType":"book","collections":["CDEF4567"],"tags":[{"tag":"classified"}]}}"#; let responses = vec![ Box::leak(library_response("server-10", 99).into_boxed_str()), - Box::leak( - raw_response(Some("server-10"), Some(43), item_body).into_boxed_str(), - ), + Box::leak(raw_response(Some("server-10"), Some(43), item_body).into_boxed_str()), Box::leak(library_response("server-10", 99).into_boxed_str()), ]; let (base, server) = serve(responses); - let receipt = - reconcile_classification_rollback_with_zotero10(&operation, &transport(base)); + let receipt = reconcile_classification_rollback_with_zotero10(&operation, &transport(base)); assert_eq!(receipt.state, ClassificationRollbackState::Unchanged); assert_eq!(receipt.retry_operation, Some(operation)); diff --git a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs index 67a3b88c..43f43619 100644 --- a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs +++ b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs @@ -1,6 +1,6 @@ use conceptweave_zotero::{ - ClassificationItemState, ClassificationRollbackOperation, ClassificationRollbackState, - ItemTag, reconcile_classification_rollback, + ClassificationItemState, ClassificationRollbackOperation, ClassificationRollbackState, ItemTag, + reconcile_classification_rollback, }; fn tag(value: &str) -> ItemTag { @@ -115,10 +115,7 @@ fn later_reconciliation_preserves_read_failures_and_rejects_wrong_identity() { wrong_server.server_id = "server-2".into(); let mismatched = reconcile_classification_rollback(&operation, |_| Ok::<_, ()>(wrong_server.clone())); - assert_eq!( - mismatched.state, - ClassificationRollbackState::Indeterminate - ); + assert_eq!(mismatched.state, ClassificationRollbackState::Indeterminate); assert_eq!(mismatched.observed_state, Some(wrong_server)); assert!(mismatched.retry_operation.is_none()); From b3c24ab085693074a0c89d5005c78ba1ec008bbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:54:13 +0900 Subject: [PATCH 07/14] test(zotero): type reconciliation fixtures immutably --- 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 ab246003..e5439ad4 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -2796,7 +2796,7 @@ mod tests { }], }; let item_body = r#"{"key":"ABCD2345","version":43,"data":{"itemType":"book","collections":["CDEF4567"],"tags":[{"tag":"classified"}]}}"#; - let responses = vec![ + let responses: Vec<&'static str> = vec![ Box::leak(library_response("server-10", 99).into_boxed_str()), Box::leak(raw_response(Some("server-10"), Some(43), item_body).into_boxed_str()), Box::leak(library_response("server-10", 99).into_boxed_str()), From 6114b579f19a6fd316216549e4cf155bd0971352 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:55:08 +0900 Subject: [PATCH 08/14] test(zotero): cover reconciliation trust boundaries --- .../classification_rollback_reconciliation.rs | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs index 43f43619..c272100d 100644 --- a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs +++ b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs @@ -81,6 +81,22 @@ fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state state(10, vec![" ".into()], operation.expected_tags.clone()), ClassificationRollbackState::Indeterminate, ), + ( + state( + 10, + operation.expected_collection_keys.clone(), + vec![tag("Other")], + ), + ClassificationRollbackState::Indeterminate, + ), + ( + state( + 11, + operation.collection_keys.clone(), + vec![tag("Other")], + ), + ClassificationRollbackState::Indeterminate, + ), ]; for (observed, expected) in cases { @@ -130,19 +146,33 @@ fn later_reconciliation_preserves_read_failures_and_rejects_wrong_identity() { ClassificationRollbackState::Indeterminate ); - let mut invalid_operation = operation.clone(); - invalid_operation.expected_collection_keys.push(" ".into()); - let invalid = reconcile_classification_rollback( - &invalid_operation, - |_| -> Result { - panic!("invalid reconciliation evidence must fail before reading") - }, - ); - assert_eq!(invalid.state, ClassificationRollbackState::Indeterminate); - assert!(invalid.observed_state.is_none()); - assert!(invalid.retry_operation.is_none()); + let mut invalid_operations = Vec::new(); + let mut blank_server = operation.clone(); + blank_server.server_id = " ".into(); + invalid_operations.push(blank_server); + let mut blank_item = operation.clone(); + blank_item.item_key = " ".into(); + invalid_operations.push(blank_item); + let mut invalid_expected = operation.clone(); + invalid_expected.expected_collection_keys.push(" ".into()); + invalid_operations.push(invalid_expected); + let mut invalid_restoration = operation.clone(); + invalid_restoration.collection_keys.push(" ".into()); + invalid_operations.push(invalid_restoration); + + for invalid_operation in invalid_operations { + let invalid = reconcile_classification_rollback( + &invalid_operation, + |_| -> Result { + panic!("invalid reconciliation evidence must fail before reading") + }, + ); + assert_eq!(invalid.state, ClassificationRollbackState::Indeterminate); + assert!(invalid.observed_state.is_none()); + assert!(invalid.retry_operation.is_none()); - let serialized = serde_json::to_string(&invalid).unwrap(); - assert!(!serialized.to_ascii_lowercase().contains("api_key")); - assert!(!serialized.contains("read_failed")); + let serialized = serde_json::to_string(&invalid).unwrap(); + assert!(!serialized.to_ascii_lowercase().contains("api_key")); + assert!(!serialized.contains("read_failed")); + } } From 6d57abdd1e71106e13bbe91d8184c70c90eefffd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:55:20 +0900 Subject: [PATCH 09/14] style(zotero): format reconciliation tests --- .../tests/classification_rollback_reconciliation.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs index c272100d..24dda4ee 100644 --- a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs +++ b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs @@ -90,11 +90,7 @@ fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state ClassificationRollbackState::Indeterminate, ), ( - state( - 11, - operation.collection_keys.clone(), - vec![tag("Other")], - ), + state(11, operation.collection_keys.clone(), vec![tag("Other")]), ClassificationRollbackState::Indeterminate, ), ]; From 5084064960f2f5019de77d25c5238f8a2856c6fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 05:59:33 +0900 Subject: [PATCH 10/14] docs: retain Zotero version in rollback evidence --- docs/TRD.md | 2 +- docs/product-technical-gap-baseline.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index 9e1323fa..2f431362 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -68,7 +68,7 @@ Duplicate review is independent of subject classification. A reviewed decision s Golden-set evaluation accepts only a governance receipt verified with the complete reviewed set by a caller-owned authorization boundary. Its library version, rule revision, canonical SHA-256 content digest, and every observed parent/child item-key/item-version identity must bind the classification report. The digest covers every raw Zotero item in canonical key order. Blank, duplicate, unknown, stale, content-mismatched, label-mismatched, or abstention-as-truth inputs fail closed. 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. 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, library, rule, and snapshot 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 verified state, stops on the first adapter or response failure, and re-reads that item through the same boundary. A proven applied state receives reverse-ordered rollback evidence containing server identity, post-write item revision, expected post-write metadata, and the complete restoration state even when the write response was lost. An unexpected state stays indeterminate, but receives an inverse operation only when the same server/item and newer library/item versions prove a safe rollback target. The generic rollback core rejects operations spanning server identities before any read, then reads every receipt item at one current library version and verifies that evidence before writing. It follows receipt order, advances the library version only after a verified inverse write, and on failure re-reads the item to classify restored, unchanged, or indeterminate state. Its secret-free receipt separates restored, failed, indeterminate, not-attempted, and remaining work. Automatic retry evidence includes a failed current operation only when it is proven unchanged; an indeterminate operation and its complete metadata are retained separately for operator reconciliation. The delayed reconciliation boundary performs exactly one server-bound read and no write. It treats an exact item revision plus expected metadata as unchanged even if unrelated library changes advanced the library version, and treats restoration metadata as restored only at a newer item revision; every identity, metadata, or version ambiguity remains indeterminate. Already consumed evidence fails preflight on reuse. +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. 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, and snapshot 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 verified state, stops on the first adapter or response failure, and re-reads that item through the same boundary. A proven applied state receives reverse-ordered rollback evidence containing server identity, post-write item revision, expected post-write metadata, and the complete restoration state even when the write response was lost. An unexpected state stays indeterminate, but receives an inverse operation only when the same server/item and newer library/item versions prove a safe rollback target. The generic rollback core rejects operations spanning server identities before any read, then reads every receipt item at one current library version and verifies that evidence before writing. It follows receipt order, advances the library version only after a verified inverse write, and on failure re-reads the item to classify restored, unchanged, or indeterminate state. Its secret-free receipt separates restored, failed, indeterminate, not-attempted, and remaining work. Automatic retry evidence includes a failed current operation only when it is proven unchanged; an indeterminate operation and its complete metadata are retained separately for operator reconciliation. The delayed reconciliation boundary performs exactly one server-bound read and no write. It treats an exact item revision plus expected metadata as unchanged even if unrelated library changes advanced the library version, and treats restoration metadata as restored only at a newer item revision; every identity, metadata, or version ambiguity remains indeterminate. Already consumed evidence fails preflight on reuse. The Zotero 10+ transport is pinned to loopback, rejects redirects, and uses finite timeouts. A one-shot authorization POST to `/api/local/authorize` sends JSON `{ "appName": ... }`, `Content-Type: application/json`, and the expected `Zotero-Server-ID`. Application names must be nonblank and at most 128 bytes. Every authorization, read, and write response must repeat that exact server identity before its status is interpreted. A bounded `200 OK` authorization response contains a 32-byte visible-ASCII key plus the `remember` decision. A same-server `403` is classified as denial only when its bounded JSON body parses with `denied: true`; missing, malformed, oversized, or false denial evidence fails closed. `429` exposes only a safe integer `Retry-After` delta of at most one day. Neither condition retries or prompts again. The authorization wrapper is neither debug-printable nor serializable, keeps the key private, exposes only the remembered decision, and can be consumed into the existing adapter. Item responses remain capped at 1 MiB. Writes distinguish same-server `401` reauthorization from same-server `412` stale preconditions, while a different-server `412` on library, item, or write paths is a database switch; all errors remain static and secret-free. Narrow adapter functions reuse the generic write and rollback cores. Mock TCP evidence covers the wire contract, but no approved live Zotero 10 authorization, write, partial-failure, or rollback has been performed. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 201aaa7f..f1a547f4 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -46,7 +46,7 @@ The successor authorization slice adds one-shot Zotero 10 Local API authorizatio 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 retains verified revisions plus an opaque snapshot digest so detached metrics remain attributable. Item and reviewer identities stay out of its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Connected duplicate components now produce a snapshot-bound local review manifest only after external steward verification; every operation retains all component source revisions and before/after/rollback canonical mappings while Zotero records remain unchanged. Reviewed collection/tag changes produce a default-dry-run plan bound to exact server, library, item, rule, digest, and complete metadata preconditions; externally read-only plan state prevents post-validation forgery, automatic-tag type is preserved, and Zotero 9 execute mode is rejected. Every secret-free execution receipt retains the exact review, authority, server, library, rule, and snapshot coordinates. Dry-run calls nothing and reports all operations as not attempted. Execute mode preflights every item, stops at the first failure, and reconciles a lost or invalid response with a same-boundary read. Proven applied states receive rollback coordinates; identity- and version-confirmed unexpected mutations stay indeterminate while retaining a safe conditional inverse. Unprovable current-item state is reported as indeterminate without a false rollback claim. The generic rollback executor rejects mixed-server evidence before reading, verifies all expected post-write states at one current library version before the first inverse write, follows the receipt's reverse order, advances only from verified writes, and stops with restored, failed, indeterminate, not-attempted, and remaining classifications. Unprovable rollback state retains complete operation evidence separately and stays out of automatic retry until operator reconciliation. A later read-only reconciliation records the observed state, tolerates unrelated library-version advancement, and emits retry evidence only for an exact unchanged item; restoration metadata at a newer item revision proves current state but not causality. Reuse after restoration fails before writing. A fixed-loopback Zotero 10 adapter supplies stable server-pinned reads and authenticated one-item writes with atomic library/item preconditions, complete collection/tag replacement, and bounded verified responses; thin wrappers reuse both generic executors. Mock fixtures verify these contracts and secret-free failures. Korean, Japanese, Chinese, Vietnamese, Spanish, German, and French ontology-alignment metadata now have explicit fail-closed abstention coverage alongside the existing English positive case; this is safety evidence, not translated classification support. No real precision/recall, duplicate merge, write, or rollback claim exists until a steward supplies reviewed local decisions and a production authorization adapter verifies them. AC6 still requires approved live Zotero 10 write, partial-failure, and rollback evidence. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. +The golden-set evaluation contract now records aggregate precision/recall numerators and denominators, requires an externally verified governance receipt bound to the complete item-key/item-version snapshot, rejects abstention as expected truth, and retains verified revisions plus an opaque snapshot digest so detached metrics remain attributable. Item and reviewer identities stay out of its output. Successful classification reports also carry same-snapshot aggregate coverage, provenance, abstention, duplicate, disposition, and failure evidence. Connected duplicate components now produce a snapshot-bound local review manifest only after external steward verification; every operation retains all component source revisions and before/after/rollback canonical mappings while Zotero records remain unchanged. Reviewed collection/tag changes produce a default-dry-run plan bound to exact server, library, item, rule, digest, and complete metadata preconditions; externally read-only plan state prevents post-validation forgery, automatic-tag type is preserved, and Zotero 9 execute mode is rejected. Every secret-free execution receipt retains the exact review, authority, server, Zotero version, library, rule, and snapshot coordinates. Dry-run calls nothing and reports all operations as not attempted. Execute mode preflights every item, stops at the first failure, and reconciles a lost or invalid response with a same-boundary read. Proven applied states receive rollback coordinates; identity- and version-confirmed unexpected mutations stay indeterminate while retaining a safe conditional inverse. Unprovable current-item state is reported as indeterminate without a false rollback claim. The generic rollback executor rejects mixed-server evidence before reading, verifies all expected post-write states at one current library version before the first inverse write, follows the receipt's reverse order, advances only from verified writes, and stops with restored, failed, indeterminate, not-attempted, and remaining classifications. Unprovable rollback state retains complete operation evidence separately and stays out of automatic retry until operator reconciliation. A later read-only reconciliation records the observed state, tolerates unrelated library-version advancement, and emits retry evidence only for an exact unchanged item; restoration metadata at a newer item revision proves current state but not causality. Reuse after restoration fails before writing. A fixed-loopback Zotero 10 adapter supplies stable server-pinned reads and authenticated one-item writes with atomic library/item preconditions, complete collection/tag replacement, and bounded verified responses; thin wrappers reuse both generic executors. Mock fixtures verify these contracts and secret-free failures. Korean, Japanese, Chinese, Vietnamese, Spanish, German, and French ontology-alignment metadata now have explicit fail-closed abstention coverage alongside the existing English positive case; this is safety evidence, not translated classification support. No real precision/recall, duplicate merge, write, or rollback claim exists until a steward supplies reviewed local decisions and a production authorization adapter verifies them. AC6 still requires approved live Zotero 10 write, partial-failure, and rollback evidence. Multilingual rule expansion remains a later evidence-driven change and must not reduce abstention safety. A dedicated utility repository remains unnecessary until an independently released cross-product contract exists. 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 f2cf4ae16d4889406b8330a9c872c0c1b95537f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:14:36 +0900 Subject: [PATCH 11/14] test(zotero): reject delayed metadata as retry authority --- crates/conceptweave-zotero/src/lib.rs | 5 +++-- .../tests/classification_rollback_reconciliation.rs | 9 +++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 8cc47afc..79a9eddb 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -3129,8 +3129,9 @@ mod tests { let receipt = reconcile_classification_rollback_with_zotero10(&operation, &transport(base)); - assert_eq!(receipt.state, ClassificationRollbackState::Unchanged); - assert_eq!(receipt.retry_operation, Some(operation)); + assert_eq!(receipt.state, ClassificationRollbackState::Indeterminate); + assert!(receipt.retry_operation.is_none()); + assert_eq!(receipt.operation, operation); assert_eq!(server.join().unwrap().len(), 3); } diff --git a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs index 24dda4ee..26260ac8 100644 --- a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs +++ b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs @@ -47,7 +47,7 @@ fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state operation.collection_keys.clone(), operation.tags.clone(), ), - ClassificationRollbackState::Restored, + ClassificationRollbackState::Indeterminate, ), ( state( @@ -55,7 +55,7 @@ fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state operation.expected_collection_keys.clone(), operation.expected_tags.clone(), ), - ClassificationRollbackState::Unchanged, + ClassificationRollbackState::Indeterminate, ), ( state(11, vec!["other".into()], operation.tags.clone()), @@ -101,10 +101,7 @@ fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state assert_eq!(receipt.state, expected); assert_eq!(receipt.operation, operation); assert_eq!(receipt.observed_state, Some(observed)); - assert_eq!( - receipt.retry_operation, - (expected == ClassificationRollbackState::Unchanged).then(|| operation.clone()) - ); + assert!(receipt.retry_operation.is_none()); } } From f9c2c03f019c5ea3f27057a5dab69d43b19d5f33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:15:12 +0900 Subject: [PATCH 12/14] fix(zotero): keep delayed rollback observations nonauthoritative --- crates/conceptweave-zotero/src/lib.rs | 54 +++++++++------------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 79a9eddb..b298e499 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -909,13 +909,13 @@ pub enum ClassificationRollbackState { /// Secret-free evidence from one delayed, read-only rollback reconciliation. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ClassificationRollbackReconciliationReceipt { - /// State proven by the delayed observation. + /// Causal state remains indeterminate; metadata alone cannot settle the write. pub state: ClassificationRollbackState, /// Complete operation under reconciliation. pub operation: ClassificationRollbackOperation, /// Successfully observed state, absent when validation or reading failed. pub observed_state: Option, - /// Operation eligible for the existing complete-preflight retry path. + /// Legacy audit slot; this read-only observer never grants a retry operation. pub retry_operation: Option, } @@ -2012,16 +2012,11 @@ pub fn reconcile_classification_rollback( let observed_state = valid_operation .then(|| read_item(&operation.item_key).ok()) .flatten(); - let state = observed_state - .as_ref() - .map(|observed| classify_rollback_state(observed, operation)) - .unwrap_or(ClassificationRollbackState::Indeterminate); ClassificationRollbackReconciliationReceipt { - state, + state: ClassificationRollbackState::Indeterminate, operation: operation.clone(), observed_state, - retry_operation: (state == ClassificationRollbackState::Unchanged) - .then(|| operation.clone()), + retry_operation: None, } } @@ -2053,32 +2048,6 @@ fn rollback_preflight_failure( } } -fn classify_rollback_state( - state: &ClassificationItemState, - operation: &ClassificationRollbackOperation, -) -> ClassificationRollbackState { - let Some((collections, tags)) = normalized_metadata(&state.collection_keys, &state.tags).ok() - else { - return ClassificationRollbackState::Indeterminate; - }; - if state.server_id != operation.server_id || state.item_key != operation.item_key { - return ClassificationRollbackState::Indeterminate; - } - if state.item_version == operation.item_version - && collections == operation.expected_collection_keys - && tags == operation.expected_tags - { - ClassificationRollbackState::Unchanged - } else if state.item_version > operation.item_version - && collections == operation.collection_keys - && tags == operation.tags - { - ClassificationRollbackState::Restored - } else { - ClassificationRollbackState::Indeterminate - } -} - fn matches_rollback_current( state: &ClassificationItemState, operation: &ClassificationRollbackOperation, @@ -3132,7 +3101,20 @@ mod tests { assert_eq!(receipt.state, ClassificationRollbackState::Indeterminate); assert!(receipt.retry_operation.is_none()); assert_eq!(receipt.operation, operation); - assert_eq!(server.join().unwrap().len(), 3); + assert_eq!( + receipt.observed_state, + Some(ClassificationItemState { + server_id: operation.server_id, + library_version: 99, + item_key: operation.item_key, + item_version: 43, + collection_keys: operation.expected_collection_keys, + tags: operation.expected_tags, + }) + ); + let requests = server.join().unwrap(); + assert_eq!(requests.len(), 3); + assert!(requests.iter().all(|request| request.starts_with("GET "))); } #[test] From 7302e95d52225b6bcd4e6e18ce2e017bc610845f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:17:02 +0900 Subject: [PATCH 13/14] docs(zotero): clarify delayed observer compatibility vocabulary --- crates/conceptweave-zotero/src/lib.rs | 2 +- .../tests/classification_rollback_reconciliation.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index b298e499..17729a1e 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -894,7 +894,7 @@ pub enum ClassificationRollbackOutcome { PartialFailure, } -/// Current state of one previously indeterminate rollback operation. +/// Legacy state vocabulary; metadata-only reconciliation emits only `Indeterminate`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum ClassificationRollbackState { diff --git a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs index 26260ac8..18e11367 100644 --- a/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs +++ b/crates/conceptweave-zotero/tests/classification_rollback_reconciliation.rs @@ -38,7 +38,7 @@ fn state( } #[test] -fn later_reconciliation_distinguishes_restored_unchanged_and_indeterminate_state() { +fn later_reconciliation_preserves_metadata_without_settling_rollback() { let operation = operation(); let cases = [ ( From 425d8dfdf9cbef3c1c21dc21ba97e33f88033730 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:17:14 +0900 Subject: [PATCH 14/14] docs(research): record delayed observation repair and envelope gap --- docs/PRD.md | 2 +- docs/TRD.md | 2 +- docs/adr/0007-reviewed-zotero-write-plan.md | 2 +- docs/product-technical-gap-baseline.md | 34 +++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index ce37b3ab..febeb7f3 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -82,7 +82,7 @@ For execute-mode plans, the runtime must preflight every item before the first w Rollback must retain server-bound expected and restoration metadata, preflight every operation at one current library version, preserve receipt order and directly verified revision advancement. Uncertain original or inverse writes must not become successful recovery or retry authority. Failed-inverse inference is repaired locally: complete submitted requests and observations remain indeterminate. PR #20's operation-slice API still requires authoritative consumer integration preserving original-write scope before approved live use; an empty inverse list cannot establish that an unknown original write was recovered. -PR #21 retains one delayed read without writes and complete observed metadata. Its current metadata-only restored/unchanged and retry inference is an open repair finding; observations cannot establish causal completion, termination, or independent retry authority. +PR #21 retains validated delayed reads without writes and complete observed metadata. Metadata-only restored/unchanged and retry inference is removed in `f9c2c03`: the observer always retains indeterminate causal status and emits no retry operation. Eight metadata scenarios and the three-GET adapter fixture remain covered; the latter compares the entire observed state. Legacy enum variants and the optional retry field remain for contract compatibility, not as outputs or approval from this observer. Authoritative successor wrappers still must retain the complete prior rollback receipt, its exact submitted request, binding and untouched tail; an operation-only observation cannot replace that envelope or establish causal completion, termination, or independent retry authority. 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. diff --git a/docs/TRD.md b/docs/TRD.md index e2f21616..ea43705d 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -111,7 +111,7 @@ The report is local JSON and contains proposals rather than governance decisions PR #20 retains its rollback core and adapter: mixed-server rejection precedes reads; complete current-state checks precede inverse writes; only directly verified responses advance the library version. Every failed or invalid inverse response now remains indeterminate, retaining the complete operation, exact submitted request (including its library precondition), and optional complete readback. Matching restored or unchanged metadata does not prove causal completion or termination, and the failed inverse is absent from remaining work. Earlier directly verified restorations remain recorded; remaining operations are untouched only, not automatic retry authority. The operation-slice API still lacks original-write scope and independent authority; authoritative consumer adoption remains an open gate, including empty-slice and delayed-reconciliation handling. -PR #21 retains one delayed read without writes and complete observed metadata. Its current metadata-only restored/unchanged and retry inference is an open repair finding; observations cannot establish causal completion, termination, or independent retry authority. +PR #21 retains validated delayed reads without writes and complete observed metadata. Metadata-only restored/unchanged and retry inference is removed in `f9c2c03`: the observer always retains indeterminate causal status and emits no retry operation. Eight metadata scenarios and the three-GET adapter fixture remain covered; the latter compares the entire observed state. Legacy enum variants and the optional retry field remain for contract compatibility, not as outputs or approval from this observer. Authoritative successor wrappers still must retain the complete prior rollback receipt, its exact submitted request, binding and untouched tail; an operation-only observation cannot replace that envelope or establish causal completion, termination, or independent retry authority. The Zotero 10+ transport is pinned to loopback, rejects redirects, and uses finite timeouts. A one-shot authorization POST to `/api/local/authorize` sends JSON `{ "appName": ... }`, `Content-Type: application/json`, and the expected `Zotero-Server-ID`. Application names must be nonblank and at most 128 bytes. Every authorization, read, and write response must repeat that exact server identity before its status is interpreted. A bounded `200 OK` authorization response contains a 32-byte visible-ASCII key plus the `remember` decision. A same-server `403` is classified as denial only when its bounded JSON body parses with `denied: true`; missing, malformed, oversized, or false denial evidence fails closed. `429` exposes only a safe integer `Retry-After` delta of at most one day. Neither condition retries or prompts again. The authorization wrapper is neither debug-printable nor serializable, keeps the key private, exposes only the remembered decision, and can be consumed into the existing adapter. Item responses remain capped at 1 MiB. Writes distinguish same-server `401` reauthorization from same-server `412` stale preconditions, while a different-server `412` on library, item, or write paths is a database switch; all errors remain static and secret-free. Narrow adapter functions reuse the generic write and rollback cores. Mock TCP evidence covers the wire contract, but no approved live Zotero 10 authorization, write, partial-failure, or rollback has been performed. diff --git a/docs/adr/0007-reviewed-zotero-write-plan.md b/docs/adr/0007-reviewed-zotero-write-plan.md index cd5ddc4a..c616e359 100644 --- a/docs/adr/0007-reviewed-zotero-write-plan.md +++ b/docs/adr/0007-reviewed-zotero-write-plan.md @@ -20,7 +20,7 @@ Execute planning fails closed for Zotero versions below 10. The plan contains no The authenticated Zotero 10+ adapter is a narrow loopback transport for those injected functions. A caller may supply credentials directly or perform one official `/api/local/authorize` request with a bounded nonblank application name and expected server identity. Every authorization, read, and write response must repeat that identity before status classification. Success returns an exact 32-character header-safe key plus the remembered decision; denial requires same-server bounded JSON with `denied: true`. The private authorization wrapper can only disclose the remembered decision or be consumed into the existing adapter; neither value is debuggable or serializable. Denial and rate limiting never trigger an automatic retry or repeated prompt, and only a bounded integer retry delay is retained. Writes distinguish an expired authorization from a matching-server stale precondition, while a different-server `412` invalidates the read/write partition as a database switch. Thin public adapter boundaries delegate to the generic write and rollback cores rather than creating parallel mutation logic. Rollback evidence binds the server, post-write item revision, complete expected current metadata, and complete restoration metadata. Before its first read, rollback rejects evidence spanning server identities; before its first write, it verifies every item at one current library version. It then follows the already reversed receipt order and advances that version only from a verified write. A failed or unverifiable inverse response is re-read only as observation and always remains indeterminate. Its exact submitted request, full operation and optional observation are retained; matching metadata cannot prove completion or termination. Only untouched operations remain listed, without automatic retry authority. Public operation DTOs and empty operation slices are not complete original-write scope or independent approval; authoritative consumer wrappers must preserve those boundaries before live use. Reusing consumed evidence fails preflight before writing. Static errors and serializable receipts cannot echo a credential, response body, or URL. Cross-item transactionality is not claimed, and source records and attachments are never deleted. -PR #21 retains one delayed read without writes and complete observed metadata. Its current metadata-only restored/unchanged and retry inference is an open repair finding; observations cannot establish causal completion, termination, or independent retry authority. +PR #21 retains validated delayed reads without writes and complete observed metadata. Metadata-only restored/unchanged and retry inference is removed in `f9c2c03`: the observer always retains indeterminate causal status and emits no retry operation. Eight metadata scenarios and the three-GET adapter fixture remain covered; the latter compares the entire observed state. Legacy enum variants and the optional retry field remain for contract compatibility, not as outputs or approval from this observer. Authoritative successor wrappers still must retain the complete prior rollback receipt, its exact submitted request, binding and untouched tail; an operation-only observation cannot replace that envelope or establish causal completion, termination, or independent retry authority. ## Consequences diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e55d8112..205fddf8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -89,6 +89,40 @@ Remaining work: mandatory adoption by restoration, worksheet, duplicate and writ ## DDD fitness constraints +### PR #21 delayed observation authority repair (2026-09-06) + +Untouched `09c84e4cdb1393a5e450f5200b87f292eeea956f` passed 119 tests/22 suites. +Normal merge `ec2baac` preserves that delta and repaired PR #20 `000b37b`; +integration passed 142 tests/22 suites. Conflicts retain the delayed observer +while preserving parent forward/inverse uncertainty and complete request fields. + +Behavioral RED `f2cf4ae` compiled and failed one of two reconciliation tests: +metadata-only observation returned `Restored` instead of `Indeterminate`. +Owner fix `f9c2c03` retains local operation validation, the single callback read, +complete operation and complete observation, but never infers completion or +termination and never emits a retry operation. The obsolete state classifier is +removed. All eight metadata cases remain; the adapter test compares the complete +state and proves its three HTTP requests are GETs. Legacy enum variants and the +optional retry slot remain compatible, not authority-producing outputs. + +At `f9c2c03`, 142 tests/22 suites including three doctests, strict Clippy, +warnings-denied rustdoc, format/CI-contract/diff and unchanged coverage pass: +268/268 functions, 2360/2360 normalized regions and 400/400 normalized branches. +Raw LLVM remains 3077/3137 lines, 4616/4713 regions and 356/400 branches, not 100%. +Logs use `/tmp/conceptweave-pr21-causal-` with `red.log`, `final.log`, `clippy.log`, +`rustdoc.log`, `coverage.log`; doc/test-name-only follow-up uses `complete.log` +and `rustdoc-complete.log`. Independent review found no blocking issue. + +PRD/TRD/Proposed ADR retain the distinct downstream gap: opaque full-text wrappers +must retain the entire prior rollback receipt, exact submitted request, binding +and untouched tail. An operation-only observation cannot substitute that envelope. +No new approval or capability layer was introduced. Next verified successor is +PR #22 review context `7179d13b45d160682e4cce1473c145d465fe657b`. +No fresh visual evidence was collected; the latest native attempt encountered +the locked Mac. Historical 3,719 displayed items are not classification evidence. +Actual decisions/approvals stay 0/3,715 plus four pending sources. No real +authorization, mutation, recovery, protected merge or release occurred. + ### PR #20 rollback uncertainty repair (2026-09-06) Untouched `a03a7248c894a1e0765968ddf58514d98c517da3` passed 116 tests/21 suites.