From 05c1cc3c8d2d207afe22257b6ac2e81171ea1f3a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:30:02 +0900 Subject: [PATCH 01/15] test(zotero): bind incremental steward decisions --- .../tests/steward_decision_patch.rs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 crates/conceptweave-zotero/tests/steward_decision_patch.rs diff --git a/crates/conceptweave-zotero/tests/steward_decision_patch.rs b/crates/conceptweave-zotero/tests/steward_decision_patch.rs new file mode 100644 index 00000000..1c6fc875 --- /dev/null +++ b/crates/conceptweave-zotero/tests/steward_decision_patch.rs @@ -0,0 +1,118 @@ +use conceptweave_zotero::{ + Disposition, ItemData, StewardDecisionPatch, StewardDecisionUpdate, WorksheetError, ZoteroItem, + apply_steward_decision_patch, build_steward_review_worksheet, classify_snapshot, +}; + +fn item(key: &str, title: &str) -> ZoteroItem { + ZoteroItem { + key: key.into(), + version: 7, + data: ItemData { + item_type: "book".into(), + title: title.into(), + abstract_note: String::new(), + 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("A", "ontology learning"), item("B", "unmatched")], + ) +} + +fn patch(item_key: &str, item_version: u64, disposition: Disposition) -> StewardDecisionPatch { + let report = report(); + StewardDecisionPatch { + library_version: report.library_version, + rule_revision: report.rule_revision, + snapshot_digest: report.snapshot_digest, + decisions: vec![StewardDecisionUpdate { + item_key: item_key.into(), + item_version, + reviewed_disposition: disposition, + }], + } +} + +#[test] +fn decision_patch_is_snapshot_bound_idempotent_and_non_overwriting() { + let report = report(); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let update = patch("A", 7, Disposition::AlignmentVersioning); + + let updated = apply_steward_decision_patch(&report, &worksheet, &update).unwrap(); + assert_eq!( + updated.decisions[0].reviewed_disposition, + Some(Disposition::AlignmentVersioning) + ); + assert_eq!( + apply_steward_decision_patch(&report, &updated, &update).unwrap(), + updated + ); + + let conflicting = patch("A", 7, Disposition::OutOfScope); + assert_eq!( + apply_steward_decision_patch(&report, &updated, &conflicting), + Err(WorksheetError::InvalidReport) + ); +} + +#[test] +fn decision_patch_rejects_invalid_identity_and_truth() { + let report = report(); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + + let mut invalid = patch("A", 7, Disposition::AlignmentVersioning); + invalid.library_version += 1; + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = patch("A", 7, Disposition::AlignmentVersioning); + invalid.rule_revision.clear(); + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &invalid), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid = patch("A", 7, Disposition::AlignmentVersioning); + invalid.snapshot_digest.clear(); + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &invalid), + Err(WorksheetError::InvalidReport) + ); + + for invalid in [ + patch("UNKNOWN", 7, Disposition::OutOfScope), + patch("A", 8, Disposition::OutOfScope), + patch("A", 7, Disposition::NeedsStewardReview), + ] { + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &invalid), + Err(WorksheetError::InvalidReport) + ); + } + + let mut duplicate = patch("A", 7, Disposition::AlignmentVersioning); + duplicate.decisions.push(duplicate.decisions[0].clone()); + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &duplicate), + Err(WorksheetError::InvalidReport) + ); + + let mut empty = patch("A", 7, Disposition::AlignmentVersioning); + empty.decisions.clear(); + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &empty), + Err(WorksheetError::InvalidReport) + ); +} From 96eb3655b84baf8b482635fe10014acc0f8b9f1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:30:38 +0900 Subject: [PATCH 02/15] feat(zotero): apply snapshot-bound decision patches --- crates/conceptweave-zotero/src/lib.rs | 76 +++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 31252f4c..f5010c38 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1079,6 +1079,30 @@ pub struct StewardReviewProgress { pub complete: bool, } +/// One snapshot-bound steward decision supplied without rewriting a worksheet by hand. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct StewardDecisionUpdate { + /// Stable Zotero item key of the reviewed bibliographic item. + pub item_key: String, + /// Exact item revision reviewed by the steward. + pub item_version: u64, + /// Human-reviewed truth label; abstention is not valid truth. + pub reviewed_disposition: Disposition, +} + +/// A bounded set of local steward decisions for one immutable classification snapshot. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct StewardDecisionPatch { + /// Zotero library revision shared with the report and worksheet. + pub library_version: u64, + /// Classifier revision whose proposals were reviewed. + pub rule_revision: String, + /// Canonical digest of the complete raw snapshot. + pub snapshot_digest: String, + /// Unique item-revision decisions to apply atomically. + pub decisions: Vec, +} + /// A classification report cannot safely produce a review worksheet. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorksheetError { @@ -1236,6 +1260,58 @@ pub fn assess_steward_review_progress( }) } +/// Applies one snapshot-bound decision patch without overwriting conflicting review work. +pub fn apply_steward_decision_patch( + report: &ClassificationReport, + worksheet: &StewardReviewWorksheet, + patch: &StewardDecisionPatch, +) -> Result { + let expected = build_steward_review_worksheet(report)?; + validate_steward_review_worksheet_against(&expected, worksheet)?; + if patch.library_version != expected.library_version + || patch.rule_revision != expected.rule_revision + || patch.snapshot_digest != expected.snapshot_digest + || patch.decisions.is_empty() + { + return Err(WorksheetError::InvalidReport); + } + + let mut updates = BTreeMap::new(); + for decision in &patch.decisions { + if decision.item_key.trim().is_empty() + || decision.reviewed_disposition == Disposition::NeedsStewardReview + || updates + .insert( + decision.item_key.as_str(), + (decision.item_version, decision.reviewed_disposition), + ) + .is_some() + { + return Err(WorksheetError::InvalidReport); + } + } + + let mut updated = worksheet.clone(); + for decision in &mut updated.decisions { + let Some((item_version, reviewed_disposition)) = updates.remove(decision.item_key.as_str()) + else { + continue; + }; + if item_version != decision.item_version + || decision + .reviewed_disposition + .is_some_and(|existing| existing != reviewed_disposition) + { + return Err(WorksheetError::InvalidReport); + } + decision.reviewed_disposition = Some(reviewed_disposition); + } + if !updates.is_empty() { + return Err(WorksheetError::InvalidReport); + } + Ok(updated) +} + fn validate_steward_review_worksheet_against( expected: &StewardReviewWorksheet, worksheet: &StewardReviewWorksheet, From e8f6e843d716418a5d9e8c0bf09a8732dc3d8f28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:31:21 +0900 Subject: [PATCH 03/15] docs(zotero): define incremental review patches --- docs/PRD.md | 2 ++ docs/TRD.md | 2 ++ docs/adr/0006-zotero-research-intake.md | 2 ++ docs/product-technical-gap-baseline.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/docs/PRD.md b/docs/PRD.md index 0e6c6698..80c55510 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -72,6 +72,8 @@ After every decision is filled, worksheet finalization must verify the governanc Operators must be able to finalize the saved report, completed worksheet, and approval receipt offline without rereading mutable Zotero state. Every input must be a distinct owner-only file identity, not merely a differently spelled path, and the new golden-set output must use a separate path; invalid, oversized, linked, or shared inputs fail closed. During the human review campaign, operators must be able to validate a partially completed worksheet against its original report and persist aggregate progress without an approval receipt. Progress reports only total, decided, and remaining counts plus a syntactic-completion flag; it never suggests labels or claims correctness, approval, or publication authority. An empty campaign is not complete. +Operators must be able to accumulate small steward-reviewed decision sets into the canonical worksheet without hand-merging the complete JSON document. Each decision patch binds the original library version, classifier revision, snapshot digest, item key, and item revision. Empty, duplicate, unknown, stale, or abstention decisions fail atomically. Reapplying the same decision is idempotent; a different decision cannot overwrite existing review work. Applying a patch does not confer approval or record publication authority. + Every successful classification report includes aggregate evidence for snapshot coverage, proposal coverage, provenance completeness, abstentions, duplicate candidates, disposition totals, and zero unreported failures. ## 6. First vertical slice diff --git a/docs/TRD.md b/docs/TRD.md index b0e44e93..b53d780a 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -74,6 +74,8 @@ The owner-only report uses owned JSON values and supports lossless deserializati `conceptweave-zotero --finalize /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/APPROVAL.json /tmp/GOLDEN.json` performs that offline transition. All four path arguments must differ, and the three opened inputs must also have distinct Unix device/inode identities so alternate spellings cannot collapse artifacts. Inputs must be regular, single-link files with exact `0600` permissions, no larger than 16 MiB, and direct children of a canonical system temporary directory; the golden set uses the existing create-new `0600` output boundary. Finalization reads no Zotero state and applies the existing report-bound worksheet and approval validation before writing. `conceptweave-zotero --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json` reuses the same private input/output boundary and canonical worksheet comparison for an incremental checkpoint. Its report and worksheet paths and opened Unix device/inode identities must differ. It accepts blank decisions, counts only explicit non-abstention steward decisions, rejects missing, extra, reordered, shifted, or tampered decisions, and emits only the library/rule/digest coordinates, total, decided, remaining, and `complete`. `complete` requires a nonempty fully decided worksheet and is coverage evidence only; no authority verifier or Zotero call runs. +`apply_steward_decision_patch` first rebuilds the canonical worksheet from the saved report and validates the complete current worksheet. It then validates one nonempty patch against the same library version, rule revision, and snapshot digest; every update must name one unique canonical item key at its exact revision and supply a non-abstention disposition. Updates apply to a clone, so any duplicate, unknown, stale, or conflicting decision rejects the whole patch without partial state. An identical existing decision is accepted idempotently. This library contract is the owner boundary for a later private CLI; no new artifact or approval authority is implied yet. + The local report can contain titles, tags, matched metadata, and abstention abstracts. It is sensitive steward-review material, remains outside the repository, and is not a publication artifact. The report is local JSON and contains proposals rather than governance decisions. On supported Unix platforms, CLI output is restricted to a new owner-readable/writable (`0600`) direct child of canonical `/tmp` or the operating system temporary directory; exact permissions are restored after umask application, and other platforms fail closed. Relative paths, nested paths, existing paths, and symlinks are rejected, and create-new file semantics prevent overwrite/path-swap writes. Reviewed collection/tag changes can produce a pure local plan whose default mode is dry-run. The plan requires exact report and item preconditions, complete before/after/rollback arrays, externally verified authority, and preserved Zotero tag types; its fields are externally read-only after validation. Zotero 9 execute mode fails closed. The execution core makes no call in dry-run mode; otherwise it preflights every item before the first write, advances the library precondition only from verified state, stops on the first adapter or response failure, and re-reads that item through the same boundary. A proven applied state receives reverse-ordered rollback evidence containing server identity, post-write item revision, expected post-write metadata, and the complete restoration state even when the write response was lost; a state matching neither the before nor after contract is marked indeterminate. The generic rollback core rejects operations spanning server identities before any read, then reads every receipt item at one current library version and verifies that evidence before writing. It follows receipt order, advances the library version only after a verified inverse write, and on failure re-reads the item to classify restored, unchanged, or indeterminate state. Its secret-free receipt separates restored, failed, indeterminate, not-attempted, and remaining work. Automatic retry evidence includes a failed current operation only when it is proven unchanged; an indeterminate operation and its complete metadata are retained separately for operator reconciliation. The delayed reconciliation boundary performs exactly one server-bound read and no write. It treats an exact item revision plus expected metadata as unchanged even if unrelated library changes advanced the library version, and treats restoration metadata as restored only at a newer item revision; every identity, metadata, or version ambiguity remains indeterminate. Already consumed evidence fails preflight on reuse. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index f5eee294..3d54b375 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -32,6 +32,8 @@ The CLI finalizes an original report, completed worksheet, and approval receipt The same offline boundary may emit an aggregate progress checkpoint for a partial worksheet. The checkpoint revalidates every immutable coordinate and proposal field, counts only human-supplied non-abstention decisions, contains no item or reviewer identity, and treats zero required decisions as incomplete. It is operational coverage evidence, not an approval receipt or semantic-quality result. +Incremental steward work is integrated by a snapshot-bound decision patch rather than editing or merging the complete worksheet structure. The patch carries only library/rule/digest coordinates and unique item key/version/disposition updates. The canonical report and current worksheet are revalidated first; applying to a clone makes invalid or conflicting batches atomic failures, while an identical replay is idempotent. We reject direct in-place mutation and last-writer-wins merging because either can silently discard concurrent review work. The patch remains local review input and does not mint governance authority. + The reader fails closed above 50,000 items or 256 MiB of cumulative response bodies, while retaining the 8 MiB per-page bound, finite request timeouts, redirect denial, total-count checks, snapshot-version checks, and duplicate-key detection. Pagination, consistency, resource-budget, and provider-contract behavior are separated from the narrow `ureq` transport so deterministic tests exercise the production reader core rather than excluding the entire reader from coverage. Report output is restricted to a new direct child of the operating system temporary directory. Relative paths, nested paths, existing files, and symlinks are rejected before write; the file is opened with create-new semantics so a path swap cannot cause repository or arbitrary-file overwrite. The buffered writer is explicitly flushed and a final filesystem error fails the command. Reports stay local and are never committed. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 83a53110..e3795db4 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -60,6 +60,8 @@ The paired owner-only report is losslessly deserializable: its rule revision and The steward campaign now has an offline progress checkpoint. It revalidates a partial worksheet against the original saved report, rejects immutable-field drift, reordering, missing or extra rows, and abstention as reviewed truth, then writes only snapshot coordinates and aggregate total/decided/remaining counts through the existing owner-only artifact boundary. It suggests no labels, invokes no authority verifier, and treats an empty workload as incomplete. This creates a separate unverified worksheet-review coverage measure (`decided / 3,715`) for campaign operations; it does **not** change or satisfy the externally approved-label completion measure (`approved / 3,715`). The remaining Gap is human completion of all 3,715 decisions followed by an externally verified receipt; a utility repository is still unwarranted because the workflow has no independent cross-product consumer. +The first campaign-integration slice now applies small snapshot-bound decision patches to the canonical worksheet. It revalidates the complete report and worksheet, applies updates atomically to a clone, permits identical replay, and rejects empty, duplicate, unknown, stale, abstention, or conflicting decisions. This removes unsafe manual whole-file merging without creating another aggregate or repository. No CLI has adopted the function and no live steward decision has been applied, so both unverified worksheet coverage and externally approved completion remain 0/3,715; the next Gap is the owner-only CLI path and real steward input. + On 2026-09-05, the exact PR #30 implementation produced a fresh owner-only report/worksheet pair from Zotero 9.0.6 library version 12341 and schema version 42 after parent-coordinate validation was repaired. The immutable snapshot digest was `sha256:c49b08066c4526e520a5f85416543ea20a620a06170e1e15f563088f6bc9e162`; the report observed 8,326 records, retained 4,607 child-to-parent coordinates, and produced 3,715/3,715 proposals with 3,715/3,715 complete provenance coordinates, 3,658 abstentions, 56 adjacent-evidence proposals, one semantic-consumption-bridge proposal, 49 duplicate candidates, and zero read failures. The real progress command validated the pair and returned unverified worksheet coverage of 0 decided, 3,715 remaining, and `complete=false`. The new private report, worksheet, and progress artifact SHA-256 values were respectively `ff13383b88f89fcef94d2f2d7284838b268fb871bed78c75ce5b53bfab2138a8`, `ad32c8352cb7d84ac3bdcd3a60c975f61e2e19adc3a8294d4c680360071e752b`, and `ac79c719037aca2d67bb4b0ea7e84babd8a701506a7d2ec274139d260328f524`; all three files were created with mode `0600`. The earlier report/worksheet hashes are superseded because those artifacts lacked parent coordinates. This proves a replayable campaign start, not steward correctness or approval; Zotero remained read-only. Externally approved labels remain independently 0/3,715. A sampled golden set may measure classifier quality, but it cannot prove that the library was fully reclassified. The full-review boundary returns aggregate completion evidence only when externally verified, snapshot-bound, non-abstention labels cover every unique classified bibliographic item exactly once. The current live baseline therefore remains incomplete at 0/3,715 approved labels rather than treating 57 deterministic proposals, unverified worksheet coverage, or a future sample as steward truth. From 42236d84fe6e0fe696558e216e350c37602e2f5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:32:38 +0900 Subject: [PATCH 04/15] test(zotero): cover decision patch rejection paths --- .../tests/steward_decision_patch.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_decision_patch.rs b/crates/conceptweave-zotero/tests/steward_decision_patch.rs index 1c6fc875..dd18723f 100644 --- a/crates/conceptweave-zotero/tests/steward_decision_patch.rs +++ b/crates/conceptweave-zotero/tests/steward_decision_patch.rs @@ -70,6 +70,28 @@ fn decision_patch_rejects_invalid_identity_and_truth() { let report = report(); let worksheet = build_steward_review_worksheet(&report).unwrap(); + let mut invalid_report = report.clone(); + invalid_report.rule_revision.clear(); + assert_eq!( + apply_steward_decision_patch( + &invalid_report, + &worksheet, + &patch("A", 7, Disposition::AlignmentVersioning) + ), + Err(WorksheetError::InvalidReport) + ); + + let mut invalid_worksheet = worksheet.clone(); + invalid_worksheet.decisions[0].item_version += 1; + assert_eq!( + apply_steward_decision_patch( + &report, + &invalid_worksheet, + &patch("A", 7, Disposition::AlignmentVersioning) + ), + Err(WorksheetError::InvalidReport) + ); + let mut invalid = patch("A", 7, Disposition::AlignmentVersioning); invalid.library_version += 1; assert_eq!( @@ -92,6 +114,7 @@ fn decision_patch_rejects_invalid_identity_and_truth() { ); for invalid in [ + patch(" ", 7, Disposition::OutOfScope), patch("UNKNOWN", 7, Disposition::OutOfScope), patch("A", 8, Disposition::OutOfScope), patch("A", 7, Disposition::NeedsStewardReview), From b0795240b561368e7b0452a58f0b51837c0de895 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:32:54 +0900 Subject: [PATCH 05/15] test(zotero): build independent invalid report --- crates/conceptweave-zotero/tests/steward_decision_patch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/tests/steward_decision_patch.rs b/crates/conceptweave-zotero/tests/steward_decision_patch.rs index dd18723f..3f9d578c 100644 --- a/crates/conceptweave-zotero/tests/steward_decision_patch.rs +++ b/crates/conceptweave-zotero/tests/steward_decision_patch.rs @@ -70,7 +70,7 @@ fn decision_patch_rejects_invalid_identity_and_truth() { let report = report(); let worksheet = build_steward_review_worksheet(&report).unwrap(); - let mut invalid_report = report.clone(); + let mut invalid_report = crate::report(); invalid_report.rule_revision.clear(); assert_eq!( apply_steward_decision_patch( From 15d44f3435deec32a03d405e22f37b5365f4cd37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:35:25 +0900 Subject: [PATCH 06/15] test(zotero): reject preexisting review abstention --- .../tests/steward_decision_patch.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_decision_patch.rs b/crates/conceptweave-zotero/tests/steward_decision_patch.rs index 3f9d578c..4bdbc765 100644 --- a/crates/conceptweave-zotero/tests/steward_decision_patch.rs +++ b/crates/conceptweave-zotero/tests/steward_decision_patch.rs @@ -92,6 +92,17 @@ fn decision_patch_rejects_invalid_identity_and_truth() { Err(WorksheetError::InvalidReport) ); + let mut abstaining_worksheet = worksheet.clone(); + abstaining_worksheet.decisions[1].reviewed_disposition = Some(Disposition::NeedsStewardReview); + assert_eq!( + apply_steward_decision_patch( + &report, + &abstaining_worksheet, + &patch("A", 7, Disposition::AlignmentVersioning) + ), + Err(WorksheetError::InvalidReport) + ); + let mut invalid = patch("A", 7, Disposition::AlignmentVersioning); invalid.library_version += 1; assert_eq!( From 963841b800c03535266f5f44c13e02ecf85f8b29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:35:58 +0900 Subject: [PATCH 07/15] fix(zotero): reject invalid review state before patching --- crates/conceptweave-zotero/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index f5010c38..09c548e1 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1268,7 +1268,11 @@ pub fn apply_steward_decision_patch( ) -> Result { let expected = build_steward_review_worksheet(report)?; validate_steward_review_worksheet_against(&expected, worksheet)?; - if patch.library_version != expected.library_version + if worksheet + .decisions + .iter() + .any(|decision| decision.reviewed_disposition == Some(Disposition::NeedsStewardReview)) + || patch.library_version != expected.library_version || patch.rule_revision != expected.rule_revision || patch.snapshot_digest != expected.snapshot_digest || patch.decisions.is_empty() From 4f454045378a15173d7499c8a87d93f7560b59fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:37:41 +0900 Subject: [PATCH 08/15] docs(zotero): state patch abstention invariant --- 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 b53d780a..0a7d4e58 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -74,7 +74,7 @@ The owner-only report uses owned JSON values and supports lossless deserializati `conceptweave-zotero --finalize /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/APPROVAL.json /tmp/GOLDEN.json` performs that offline transition. All four path arguments must differ, and the three opened inputs must also have distinct Unix device/inode identities so alternate spellings cannot collapse artifacts. Inputs must be regular, single-link files with exact `0600` permissions, no larger than 16 MiB, and direct children of a canonical system temporary directory; the golden set uses the existing create-new `0600` output boundary. Finalization reads no Zotero state and applies the existing report-bound worksheet and approval validation before writing. `conceptweave-zotero --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json` reuses the same private input/output boundary and canonical worksheet comparison for an incremental checkpoint. Its report and worksheet paths and opened Unix device/inode identities must differ. It accepts blank decisions, counts only explicit non-abstention steward decisions, rejects missing, extra, reordered, shifted, or tampered decisions, and emits only the library/rule/digest coordinates, total, decided, remaining, and `complete`. `complete` requires a nonempty fully decided worksheet and is coverage evidence only; no authority verifier or Zotero call runs. -`apply_steward_decision_patch` first rebuilds the canonical worksheet from the saved report and validates the complete current worksheet. It then validates one nonempty patch against the same library version, rule revision, and snapshot digest; every update must name one unique canonical item key at its exact revision and supply a non-abstention disposition. Updates apply to a clone, so any duplicate, unknown, stale, or conflicting decision rejects the whole patch without partial state. An identical existing decision is accepted idempotently. This library contract is the owner boundary for a later private CLI; no new artifact or approval authority is implied yet. +`apply_steward_decision_patch` first rebuilds the canonical worksheet from the saved report and validates the complete current worksheet, including rejection of any pre-existing reviewed abstention. It then validates one nonempty patch against the same library version, rule revision, and snapshot digest; every update must name one unique canonical item key at its exact revision and supply a non-abstention disposition. Updates apply to a clone, so any duplicate, unknown, stale, or conflicting decision rejects the whole patch without partial state. An identical existing decision is accepted idempotently. This library contract is the owner boundary for a later private CLI; no new artifact or approval authority is implied yet. The local report can contain titles, tags, matched metadata, and abstention abstracts. It is sensitive steward-review material, remains outside the repository, and is not a publication artifact. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e3795db4..ed730495 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -60,7 +60,7 @@ The paired owner-only report is losslessly deserializable: its rule revision and The steward campaign now has an offline progress checkpoint. It revalidates a partial worksheet against the original saved report, rejects immutable-field drift, reordering, missing or extra rows, and abstention as reviewed truth, then writes only snapshot coordinates and aggregate total/decided/remaining counts through the existing owner-only artifact boundary. It suggests no labels, invokes no authority verifier, and treats an empty workload as incomplete. This creates a separate unverified worksheet-review coverage measure (`decided / 3,715`) for campaign operations; it does **not** change or satisfy the externally approved-label completion measure (`approved / 3,715`). The remaining Gap is human completion of all 3,715 decisions followed by an externally verified receipt; a utility repository is still unwarranted because the workflow has no independent cross-product consumer. -The first campaign-integration slice now applies small snapshot-bound decision patches to the canonical worksheet. It revalidates the complete report and worksheet, applies updates atomically to a clone, permits identical replay, and rejects empty, duplicate, unknown, stale, abstention, or conflicting decisions. This removes unsafe manual whole-file merging without creating another aggregate or repository. No CLI has adopted the function and no live steward decision has been applied, so both unverified worksheet coverage and externally approved completion remain 0/3,715; the next Gap is the owner-only CLI path and real steward input. +The first campaign-integration slice now applies small snapshot-bound decision patches to the canonical worksheet. It revalidates the complete report and worksheet, rejects both pre-existing reviewed abstentions and abstentions in the incoming patch, applies updates atomically to a clone, permits identical replay, and rejects empty, duplicate, unknown, stale, or conflicting decisions. This removes unsafe manual whole-file merging without creating another aggregate or repository. No CLI has adopted the function and no live steward decision has been applied, so both unverified worksheet coverage and externally approved completion remain 0/3,715; the next Gap is the owner-only CLI path and real steward input. On 2026-09-05, the exact PR #30 implementation produced a fresh owner-only report/worksheet pair from Zotero 9.0.6 library version 12341 and schema version 42 after parent-coordinate validation was repaired. The immutable snapshot digest was `sha256:c49b08066c4526e520a5f85416543ea20a620a06170e1e15f563088f6bc9e162`; the report observed 8,326 records, retained 4,607 child-to-parent coordinates, and produced 3,715/3,715 proposals with 3,715/3,715 complete provenance coordinates, 3,658 abstentions, 56 adjacent-evidence proposals, one semantic-consumption-bridge proposal, 49 duplicate candidates, and zero read failures. The real progress command validated the pair and returned unverified worksheet coverage of 0 decided, 3,715 remaining, and `complete=false`. The new private report, worksheet, and progress artifact SHA-256 values were respectively `ff13383b88f89fcef94d2f2d7284838b268fb871bed78c75ce5b53bfab2138a8`, `ad32c8352cb7d84ac3bdcd3a60c975f61e2e19adc3a8294d4c680360071e752b`, and `ac79c719037aca2d67bb4b0ea7e84babd8a701506a7d2ec274139d260328f524`; all three files were created with mode `0600`. The earlier report/worksheet hashes are superseded because those artifacts lacked parent coordinates. This proves a replayable campaign start, not steward correctness or approval; Zotero remained read-only. Externally approved labels remain independently 0/3,715. From 1ca8a798b6a183c3dd823f416e396d6a4bb6149a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 05:48:39 +0900 Subject: [PATCH 09/15] test: preserve owned rule revision fixture --- .../tests/classification_write_receipt_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs b/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs index 0a1b2416..70c055fc 100644 --- a/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs +++ b/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs @@ -54,7 +54,7 @@ fn reviewed(report: &conceptweave_zotero::ClassificationReport) -> ReviewedClass server_id: report.server_id.clone(), zotero_version: report.zotero_version.clone(), library_version: report.library_version, - rule_revision: report.rule_revision.into(), + rule_revision: report.rule_revision.clone(), snapshot_digest: report.snapshot_digest.clone(), snapshot_items: report.snapshot_items.clone(), changes: vec![ From 58da926561b5bc8e991bad95eed55835af15590f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:10:46 +0900 Subject: [PATCH 10/15] test: preserve complete receipt binding coverage --- .../classification_write_receipt_contract.rs | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs b/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs index 70c055fc..efcfeb15 100644 --- a/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs +++ b/crates/conceptweave-zotero/tests/classification_write_receipt_contract.rs @@ -99,6 +99,19 @@ fn preflight_state( } } +fn assert_receipt_binding( + receipt: &conceptweave_zotero::ClassificationWriteReceipt, + report: &conceptweave_zotero::ClassificationReport, +) { + assert_eq!(receipt.review_id, "review-1"); + assert_eq!(receipt.authority_receipt, "authority-1"); + assert_eq!(receipt.server_id.as_deref(), Some("server-1")); + assert_eq!(receipt.zotero_version, report.zotero_version); + assert_eq!(receipt.library_version, 42); + assert_eq!(receipt.rule_revision, report.rule_revision); + assert_eq!(receipt.snapshot_digest, report.snapshot_digest); +} + #[test] fn every_receipt_binds_to_the_reviewed_plan_coordinates() { let report = classification_report(); @@ -111,12 +124,42 @@ fn every_receipt_binds_to_the_reviewed_plan_coordinates() { |_| -> Result { panic!("dry-run must not write") }, ); - assert_eq!(receipt.review_id, "review-1"); - assert_eq!(receipt.authority_receipt, "authority-1"); - assert_eq!(receipt.server_id.as_deref(), Some("server-1")); - assert_eq!(receipt.library_version, 42); - assert_eq!(receipt.rule_revision, report.rule_revision); - assert_eq!(receipt.snapshot_digest, report.snapshot_digest); + assert_receipt_binding(&receipt, &report); +} + +#[test] +fn applied_and_preflight_failure_receipts_bind_to_the_reviewed_plan() { + let report = classification_report(); + let plan = + build_classification_write_plan(&report, &reviewed(&report), WriteMode::Execute, |_| true) + .unwrap(); + let preflight_failure = execute_classification_write_plan( + &plan, + |_| Err::(()), + |_| -> Result { panic!("preflight failure must not write") }, + ); + assert_eq!( + preflight_failure.outcome, + ClassificationWriteOutcome::PreflightFailure + ); + assert_receipt_binding(&preflight_failure, &report); + + let applied = execute_classification_write_plan( + &plan, + |item_key| Ok::<_, ()>(preflight_state(&plan, item_key)), + |request| { + Ok::<_, ()>(ClassificationItemState { + server_id: request.server_id.clone(), + library_version: request.library_version + 1, + item_key: request.item_key.clone(), + item_version: request.item_version + 1, + collection_keys: request.collection_keys.clone(), + tags: request.tags.clone(), + }) + }, + ); + assert_eq!(applied.outcome, ClassificationWriteOutcome::Applied); + assert_receipt_binding(&applied, &report); } #[test] @@ -166,6 +209,7 @@ fn confirmed_unexpected_mutation_retains_known_inverse_rollback() { ); assert_eq!(receipt.outcome, ClassificationWriteOutcome::PartialFailure); + assert_receipt_binding(&receipt, &report); assert_eq!(receipt.failed_item_key.as_deref(), Some("A")); assert_eq!(receipt.indeterminate_item_key.as_deref(), Some("A")); assert_eq!(receipt.rollback_operations.len(), 1); From 65b6472f398878ff52d8a39469e7f7afcb81a348 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:08:17 +0900 Subject: [PATCH 11/15] test(research): adopt captured-source decision patch fixture Signed-off-by: Seongho Bae --- crates/conceptweave-zotero/tests/steward_decision_patch.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/conceptweave-zotero/tests/steward_decision_patch.rs b/crates/conceptweave-zotero/tests/steward_decision_patch.rs index 4bdbc765..180c19ed 100644 --- a/crates/conceptweave-zotero/tests/steward_decision_patch.rs +++ b/crates/conceptweave-zotero/tests/steward_decision_patch.rs @@ -5,6 +5,7 @@ use conceptweave_zotero::{ fn item(key: &str, title: &str) -> ZoteroItem { ZoteroItem { + source_record: None, key: key.into(), version: 7, data: ItemData { From 7208d80ee2fe3920f25ecd5a669781620698095d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:52:24 +0900 Subject: [PATCH 12/15] test(zotero): expose stale decision patch content admission --- .../tests/steward_decision_patch.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/conceptweave-zotero/tests/steward_decision_patch.rs b/crates/conceptweave-zotero/tests/steward_decision_patch.rs index 180c19ed..ba769e75 100644 --- a/crates/conceptweave-zotero/tests/steward_decision_patch.rs +++ b/crates/conceptweave-zotero/tests/steward_decision_patch.rs @@ -66,6 +66,45 @@ fn decision_patch_is_snapshot_bound_idempotent_and_non_overwriting() { ); } +#[test] +fn stale_patch_cannot_use_a_fresh_worksheet_for_changed_content() { + let update = patch("A", 7, Disposition::AlignmentVersioning); + let mut report = report(); + report.classified_items[0] + .title + .push_str(" changed evidence"); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &update), + Err(WorksheetError::InvalidReport) + ); +} + +#[test] +fn patch_deserialization_requires_content_binding() { + let mut value = serde_json::to_value(patch("A", 7, Disposition::AlignmentVersioning)).unwrap(); + value.as_object_mut().unwrap().remove("proposal_digest"); + assert!(serde_json::from_value::(value).is_err()); +} + +#[test] +fn late_invalid_update_preserves_the_original_worksheet() { + let report = report(); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let before = worksheet.clone(); + let mut update = patch("A", 7, Disposition::Generation); + update.decisions.push(StewardDecisionUpdate { + item_key: "UNKNOWN".into(), + item_version: 7, + reviewed_disposition: Disposition::OutOfScope, + }); + assert_eq!( + apply_steward_decision_patch(&report, &worksheet, &update), + Err(WorksheetError::InvalidReport) + ); + assert_eq!(worksheet, before); +} + #[test] fn decision_patch_rejects_invalid_identity_and_truth() { let report = report(); From 89bb941f3e4d5a1db147a58d7d6b8ddce822b6cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:52:58 +0900 Subject: [PATCH 13/15] fix(zotero): require reviewed content identity in decision patches --- crates/conceptweave-zotero/src/lib.rs | 3 +++ crates/conceptweave-zotero/tests/steward_decision_patch.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 54edbdd8..fcdc0ee3 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -1183,6 +1183,8 @@ pub struct StewardDecisionPatch { pub rule_revision: String, /// Canonical digest of the complete raw snapshot. pub snapshot_digest: String, + /// Required opaque identity of the proposals and retained metadata reviewed for this patch. + pub proposal_digest: String, /// Unique item-revision decisions to apply atomically. pub decisions: Vec, } @@ -1292,6 +1294,7 @@ pub fn apply_steward_decision_patch( || patch.library_version != expected.library_version || patch.rule_revision != expected.rule_revision || patch.snapshot_digest != expected.snapshot_digest + || patch.proposal_digest != expected.proposal_digest || patch.decisions.is_empty() { return Err(WorksheetError::InvalidReport); diff --git a/crates/conceptweave-zotero/tests/steward_decision_patch.rs b/crates/conceptweave-zotero/tests/steward_decision_patch.rs index ba769e75..ce927ea6 100644 --- a/crates/conceptweave-zotero/tests/steward_decision_patch.rs +++ b/crates/conceptweave-zotero/tests/steward_decision_patch.rs @@ -32,6 +32,7 @@ fn report() -> conceptweave_zotero::ClassificationReport { fn patch(item_key: &str, item_version: u64, disposition: Disposition) -> StewardDecisionPatch { let report = report(); StewardDecisionPatch { + proposal_digest: conceptweave_zotero::classification_proposal_digest(&report), library_version: report.library_version, rule_revision: report.rule_revision, snapshot_digest: report.snapshot_digest, From fde6a5eb906e27e42613987832799eb70c6e7bee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:53:49 +0900 Subject: [PATCH 14/15] docs(zotero): trace independent decision patch content binding --- docs/PRD.md | 2 +- docs/TRD.md | 2 +- docs/adr/0006-zotero-research-intake.md | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index 0403c2aa..4684708c 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -92,7 +92,7 @@ Operators must be able to finalize the saved report, completed worksheet, and ap During the human review campaign, operators must be able to validate a partially completed worksheet against its original report and persist aggregate progress without an approval receipt. Progress binds current proposal and retained metadata identity and reports bibliographic total, decided and remaining counts alongside unresolved source count. Local preparation is complete only for a nonempty fully decided worksheet with no unresolved sources. Filled paper decisions do not hide pending attachments, notes or disconnected ancestry. Progress never claims correctness, independent approval, applied reclassification or publication authority. An empty campaign is not complete. The worksheet's own required content identity must match the current report independently of the supplied receipt. Blank identity is invalid; a stale or replaced identity is a snapshot mismatch. Conversion only prepares input for independent verification. Unresolved sources can remain in locally prepared review data, but prevent whole-library completion; refreshing local digests cannot renew an independently issued approval. -Operators must be able to accumulate small steward-reviewed decision sets into the canonical worksheet without hand-merging the complete JSON document. Each decision patch binds the original library version, classifier revision, snapshot digest, item key, and item revision. Empty, duplicate, unknown, stale, or abstention decisions fail atomically. Reapplying the same decision is idempotent; a different decision cannot overwrite existing review work. Applying a patch does not confer approval or record publication authority. +Operators must be able to accumulate small steward-reviewed decision sets without hand-merging the complete worksheet. Each patch binds the original library version, classifier revision, snapshot and proposal/retained-content digests, item key and item revision. Regenerating a worksheet after content changes cannot make an older patch valid. Missing content binding requires a new review-bound patch, never automatic backfill. Empty, duplicate, unknown, stale or abstention decisions fail atomically. Identical replay is idempotent; conflicting decisions cannot overwrite review work. Applying a patch does not confer independent approval, full-text review provenance or publication authority. Evaluate classifier quality only against a steward-reviewed local golden set whose governance receipt is externally verified and binds both the complete source/classifier-input snapshot and every current proposal field, in addition to the item-key/item-version coordinates. Same-version changes to unmodeled provider metadata, absent/default fields, classifier inputs, predictions or supporting evidence must invalidate the corresponding binding. Evaluation recomputes proposal identity before contacting governance; a locally changed digest cannot renew an approval. Legacy unbound approvals require reissuance, never automatic backfill. Abstention is a prediction outcome, never an approved truth label. Evaluation emits the verified library revision, rule revision, opaque snapshot and proposal digests, and aggregate counts for exact matches, abstentions, and per-disposition true-positive/predicted/expected totals; it must not copy Zotero keys, reviewer identity, or bibliographic text into the result. diff --git a/docs/TRD.md b/docs/TRD.md index 0a7ca03d..f355cd2e 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -120,7 +120,7 @@ The owner-only report uses owned JSON values and supports lossless deserializati Offline input admission pins the checked canonical parent plus file name for metadata and opening. Unix opening refuses final-component symlinks with `O_NOFOLLOW` and uses `O_NONBLOCK` so a raced FIFO cannot wait for a writer before device/inode validation rejects the replacement; a pre-open pathname check alone is insufficient. Regular-file reads remain bounded as before. Paired export retains the shared canonical-destination check before capture and serializes both artifacts before writing. Failures may leave private partial files and never trigger pathname cleanup or implicit buffer-flush retry. Finalized metadata remains unverified until the independent whole-set approval boundary succeeds. -`apply_steward_decision_patch` first rebuilds the canonical worksheet from the saved report and validates the complete current worksheet, including rejection of any pre-existing reviewed abstention. It then validates one nonempty patch against the same library version, rule revision, and snapshot digest; every update must name one unique canonical item key at its exact revision and supply a non-abstention disposition. Updates apply to a clone, so any duplicate, unknown, stale, or conflicting decision rejects the whole patch without partial state. An identical existing decision is accepted idempotently. This library contract is the owner boundary for a later private CLI; no new artifact or approval authority is implied yet. +`apply_steward_decision_patch` rebuilds the canonical worksheet and validates the complete current worksheet, including rejection of pre-existing reviewed abstention. It validates a nonempty patch against library version, rule revision, snapshot digest and its own required `proposal_digest`; a fresh worksheet cannot renew an older patch's reviewed content. Missing binding fails deserialization; blank or stale binding fails before updates, without backfill. Every update names a unique canonical item key and exact revision with a non-abstention disposition. Updates apply to a clone, so duplicate, unknown, stale or conflicting decisions reject the whole batch without partial state. Identical replay remains idempotent. This is the owner boundary for a later private CLI, not independent approval or full-text/write authority. The local report can contain titles, tags, matched metadata, and abstention abstracts. It is sensitive steward-review material, remains outside the repository, and is not a publication artifact. diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index fc486aac..2291bf85 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -122,6 +122,12 @@ GREEN source `f6735b585022aac1c8ceff86c150d9b64fd77ec2` passed 68 tests across 1 ## Alternatives considered +### Proposed decision-patch content identity amendment — September 7 + +PR31 accumulates local decisions without overwriting conflicts. Normal merge `ef0ce43` preserves the original patch delta and PR30's content-bound worksheet/progress contract. A valid current worksheet alone does not prove that an older patch reviewed the current content: RED `7208d80` compiled with three passing and two failing tests, accepting both an unbound serialized patch and an old patch after changed report context plus fresh worksheet generation. `89bb941` adds a required patch `proposal_digest` and compares it with the existing recomputed identity before any updates. No new hash or authority issuer is added. Missing fields fail loading; blank/stale values cannot equal the expected digest. + +We reject copying the current digest into submitted patches because that would erase the distinction between reviewed and changed content. We reject deriving patch authority from the worksheet because they are separate inputs. Required identity preserves that boundary at the cost of regenerating legacy patches from genuinely reviewed current evidence. Existing clone-before-update behavior remains: a valid first update followed by an unknown update leaves the input worksheet unchanged, identical replay succeeds and conflicting decisions fail. A caller can still construct self-consistent unverified local labels; independent whole-set approval remains mandatory and no metadata patch becomes full-text or Zotero write authority. Later patch CLI/consumer owners must inherit the required field, with protected review and release still pending. + ### Proposed review-progress scope amendment — September 7 PR30 exposes incremental local progress and extracts shared worksheet comparison. Normal merge `7251238` inherits repaired source identity, finalization and private input boundaries while preserving the child's coverage improvements. The extracted comparator omitted the required proposal digest, allowing old worksheet progress after changed content. RED `4099434` compiled with one passing and two failing tests: blank binding was admitted and pending/content identity fields were missing. `bee32f4` adds one shared digest comparison and includes the opaque proposal binding and pending count in existing aggregate output. `b2b0ef4` verifies one filled paper decision plus an unresolved source stays incomplete, preserving bibliographic counts and excluding the private source key. From eaf248afd33dcac477daf1e3a31d79d47c5cbb69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:54:36 +0900 Subject: [PATCH 15/15] docs(zotero): record decision patch content verification --- docs/product-technical-gap-baseline.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 4b8c346e..1b4956a7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,6 +6,14 @@ This file records code-current product and technical gaps. Exact PR/check/run co ## September 6 source inventory checkpoint +### September 7 PR31 decision-patch content repair + +Original PR31 `61072a70f7ec5a1fbd0b477430aacb8e770aa109` passed 158 tests/33 suites. Normal merge `ef0ce43` retains it and PR30 `ee1ac9925c5287e9f10c2e9581b7cf513b170bd7`; integrated tests passed 204/33. RED `7208d80` compiled with three passing and two failing tests: an old patch applied after changed report context and fresh worksheet generation, and serialized patches without content binding loaded. The valid-first/unknown-later batch already preserved the original worksheet. `89bb941` adds required patch proposal identity and compares it before updates, reusing the existing digest. Missing fields fail loading, stale/blank identities cannot equal the recomputed expected identity, and no automatic backfill is permitted. Independent bounded review found no additional defect; atomic failure, same-label idempotency and conflicting-decision rejection remain. + +Final source `89bb941` passes 207 tests/33 suites including three doctests, strict Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks. Unchanged coverage passes 331/331 reported functions, 2,965/2,965 normalized regions and 544/544 normalized branches. Raw 3,971/4,033 lines, 6,125/6,232 regions and 499/544 branches are not 100%. Logs: `/tmp/conceptweave-pr31-{baseline,integrated,binding-red,verified,clippy,rustdoc,coverage}.log`. PRD/TRD/Proposed ADR0006 trace the separate report/worksheet/patch identities and legacy regeneration requirement. + +Root and later patch consumers must inherit this required binding alongside the prior source, progress and FIFO repairs. Actual decisions and independent approvals remain 0/3,715 plus four unresolved standalone sources. Synthetic fixture labels are not real review evidence. Native Visual Inspection was retried, but the Mac is locked; no fresh screenshot is claimed. Keep Draft. No real Zotero write, hosted GREEN, protected merge or release occurred. + ### September 7 PR30 content-bound progress repair Original PR30 `a11e889d1680ab4d91f3565e3debf7ed0f10ba23` passed 156 tests/32 suites. Normal merge `7251238` retains it and PR29 `e21f14fbb4954762ffc97521af9a6cdd9982c630`; integrated tests passed 200/32. Child private-helper coverage improvements remain, combined with parent FIFO/source/approval/output safety. RED `4099434` compiled with one passing and two failing progress tests: blank proposal binding was accepted and pending/content identity fields were absent. `bee32f4` compares the existing digest in the shared worksheet validator and adds opaque proposal identity plus pending count to aggregate progress. `b2b0ef4` proves filled bibliographic slots do not hide an unresolved standalone source. Independent bounded review found no collateral finalization error-precedence or privacy regression.