diff --git a/README.md b/README.md index 9d12d77..50838ac 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ cargo +1.98.0 run --bin conceptweave-zotero -- /tmp/conceptweave-zotero-classifi The command reads one stable library-version snapshot and creates a local, reviewable JSON report. On Unix, output is restricted to a new owner-only (`0600`) direct child of canonical `/tmp` or the system temporary directory; the CLI fails closed on other platforms. The command never changes Zotero records. +Apply a small steward-reviewed decision patch to a new worksheet without overwriting the current one: + +```sh +cargo +1.98.0 run --bin conceptweave-zotero -- --apply-decision-patch /tmp/report.json /tmp/current-worksheet.json /tmp/patch.json /tmp/updated-worksheet.json +``` + +All three inputs must be separate owner-only files bound to the same snapshot and reviewed content. Older patches without content identity must be regenerated from the evidence actually reviewed; a new worksheet cannot renew an old patch. The output is a new owner-only file. This offline step neither changes Zotero nor grants governance approval. + If file-permission setup fails, the command stops before writing report content. An empty file may remain; inspect it before removing it. The command does not delete a pathname that another process may have replaced. diff --git a/crates/conceptweave-zotero/src/main.rs b/crates/conceptweave-zotero/src/main.rs index e016e8e..390f9d3 100644 --- a/crates/conceptweave-zotero/src/main.rs +++ b/crates/conceptweave-zotero/src/main.rs @@ -2,9 +2,9 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] use conceptweave_zotero::{ - ClassificationReport, GoldenSetApproval, StewardReviewWorksheet, - assess_steward_review_progress, build_steward_review_worksheet, read_local_snapshot, - reviewed_golden_set_from_worksheet, + ClassificationReport, GoldenSetApproval, StewardDecisionPatch, StewardReviewWorksheet, + apply_steward_decision_patch, assess_steward_review_progress, build_steward_review_worksheet, + read_local_snapshot, reviewed_golden_set_from_worksheet, }; use serde::de::DeserializeOwned; use std::collections::BTreeSet; @@ -13,7 +13,7 @@ use std::fs::{self, File, OpenOptions}; use std::io::{self, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; -const USAGE: &str = "usage: conceptweave-zotero /tmp/REPORT.json | --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json | --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json | --finalize /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/APPROVAL.json /tmp/GOLDEN.json"; +const USAGE: &str = "usage: conceptweave-zotero /tmp/REPORT.json | --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json | --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json | --apply-decision-patch /tmp/REPORT.json /tmp/CURRENT_WORKSHEET.json /tmp/PATCH.json /tmp/UPDATED_WORKSHEET.json | --finalize /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/APPROVAL.json /tmp/GOLDEN.json"; const MAX_ARTIFACT_BYTES: u64 = 16 * 1024 * 1024; #[derive(Debug, PartialEq, Eq)] @@ -28,6 +28,12 @@ enum OutputRequest { worksheet: String, output: String, }, + ApplyDecisionPatch { + report: String, + worksheet: String, + patch: String, + output: String, + }, Finalize { report: String, worksheet: String, @@ -73,6 +79,36 @@ where worksheet, output, } + } else if first == "--apply-decision-patch" { + let report = args + .next() + .ok_or("--apply-decision-patch requires four artifact paths")?; + let worksheet = args + .next() + .ok_or("--apply-decision-patch requires four artifact paths")?; + let patch = args + .next() + .ok_or("--apply-decision-patch requires four artifact paths")?; + let output = args + .next() + .ok_or("--apply-decision-patch requires four artifact paths")?; + if BTreeSet::from([ + report.as_str(), + worksheet.as_str(), + patch.as_str(), + output.as_str(), + ]) + .len() + != 4 + { + return Err("decision patch artifact paths must differ"); + } + OutputRequest::ApplyDecisionPatch { + report, + worksheet, + patch, + output, + } } else if first == "--finalize" { let report = args .next() @@ -411,6 +447,30 @@ fn main() -> Result<(), Box> { let progress = assess_steward_review_progress(&report, &worksheet)?; write_private_output(&output, &serde_json::to_vec_pretty(&progress)?)?; } + OutputRequest::ApplyDecisionPatch { + report, + worksheet, + patch, + output, + } => { + let output = validate_output_path(&output)?; + let (report, report_identity): (ClassificationReport, _) = + read_private_json(&report).map_err(|error| label_input("report", error))?; + let (worksheet, worksheet_identity): (StewardReviewWorksheet, _) = + read_private_json(&worksheet).map_err(|error| label_input("worksheet", error))?; + let (patch, patch_identity): (StewardDecisionPatch, _) = + read_private_json(&patch).map_err(|error| label_input("decision patch", error))?; + if BTreeSet::from([report_identity, worksheet_identity, patch_identity]).len() != 3 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "decision patch inputs must be distinct files", + ) + .into()); + } + let updated = apply_steward_decision_patch(&report, &worksheet, &patch)?; + let content = serde_json::to_vec_pretty(&updated)?; + write_private_output(&output, &content)?; + } OutputRequest::Finalize { report, worksheet, @@ -587,6 +647,76 @@ mod tests { ); } + #[test] + fn apply_decision_patch_mode_requires_four_distinct_artifact_paths() { + let report = "/tmp/report.json"; + let worksheet = "/tmp/worksheet.json"; + let patch = "/tmp/patch.json"; + let output = "/tmp/updated-worksheet.json"; + assert_eq!( + parse_output_request(vec![ + "--apply-decision-patch", + report, + worksheet, + patch, + output + ]), + Ok(OutputRequest::ApplyDecisionPatch { + report: report.to_owned(), + worksheet: worksheet.to_owned(), + patch: patch.to_owned(), + output: output.to_owned(), + }) + ); + assert!(parse_output_request(vec!["--apply-decision-patch"]).is_err()); + assert!(parse_output_request(vec!["--apply-decision-patch", report]).is_err()); + assert!(parse_output_request(vec!["--apply-decision-patch", report, worksheet]).is_err()); + assert!( + parse_output_request(vec!["--apply-decision-patch", report, worksheet, patch]).is_err() + ); + assert!( + parse_output_request(vec![ + "--apply-decision-patch", + report, + worksheet, + patch, + report + ]) + .is_err() + ); + assert!( + parse_output_request(vec![ + "--apply-decision-patch", + report, + worksheet, + worksheet, + output + ]) + .is_err() + ); + assert!( + parse_output_request(vec![ + "--apply-decision-patch", + report, + report, + patch, + output + ]) + .is_err() + ); + assert!( + parse_output_request(vec![ + "--apply-decision-patch", + report, + worksheet, + patch, + output, + "extra", + ]) + .is_err() + ); + } + #[cfg(unix)] #[test] fn private_json_input_is_owner_only_regular_bounded_and_valid() { diff --git a/crates/conceptweave-zotero/tests/finalization_artifact_identity.rs b/crates/conceptweave-zotero/tests/finalization_artifact_identity.rs index ecd4950..c1cda8b 100644 --- a/crates/conceptweave-zotero/tests/finalization_artifact_identity.rs +++ b/crates/conceptweave-zotero/tests/finalization_artifact_identity.rs @@ -100,6 +100,16 @@ fn artifact_commands_reject_distinct_path_spellings_for_one_input_file() { ]) .status() .unwrap(); + let patch_status = Command::new(env!("CARGO_BIN_EXE_conceptweave-zotero")) + .args([ + "--apply-decision-patch", + &report_path, + &worksheet_path, + &approval_path, + output.to_str().unwrap(), + ]) + .status() + .unwrap(); let _ = fs::remove_file(&input); let _ = fs::remove_file(&output); @@ -111,4 +121,8 @@ fn artifact_commands_reject_distinct_path_spellings_for_one_input_file() { !progress_status.success(), "progress must reject two path spellings that resolve to one input artifact" ); + assert!( + !patch_status.success(), + "decision patching must reject path spellings that resolve to one input artifact" + ); } diff --git a/crates/conceptweave-zotero/tests/steward_decision_cli.rs b/crates/conceptweave-zotero/tests/steward_decision_cli.rs new file mode 100644 index 0000000..bf43710 --- /dev/null +++ b/crates/conceptweave-zotero/tests/steward_decision_cli.rs @@ -0,0 +1,237 @@ +#![cfg(unix)] + +use conceptweave_zotero::{ + Disposition, ItemData, StewardDecisionPatch, StewardDecisionUpdate, StewardReviewWorksheet, + ZoteroItem, build_steward_review_worksheet, classify_snapshot, +}; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +#[test] +fn decision_patch_cli_writes_one_owner_only_updated_worksheet() { + let mut report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![ZoteroItem { + source_record: None, + key: "ITEM".into(), + version: 7, + data: ItemData { + item_type: "book".into(), + title: "ontology learning".into(), + abstract_note: String::new(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + }], + ); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let patch = StewardDecisionPatch { + proposal_digest: worksheet.proposal_digest.clone(), + library_version: report.library_version, + rule_revision: report.rule_revision.clone(), + snapshot_digest: report.snapshot_digest.clone(), + decisions: vec![StewardDecisionUpdate { + item_key: "ITEM".into(), + item_version: 7, + reviewed_disposition: Disposition::AlignmentVersioning, + }], + }; + + let report_path = private_input("decision-cli-report", &report); + let worksheet_path = private_input("decision-cli-worksheet", &worksheet); + let patch_path = private_input("decision-cli-patch", &patch); + let output_path = temp_path("decision-cli-output"); + let replay_path = temp_path("decision-cli-replay"); + let _ = fs::remove_file(&output_path); + let _ = fs::remove_file(&replay_path); + + let status = Command::new(env!("CARGO_BIN_EXE_conceptweave-zotero")) + .args([ + "--apply-decision-patch", + report_path.to_str().unwrap(), + worksheet_path.to_str().unwrap(), + patch_path.to_str().unwrap(), + output_path.to_str().unwrap(), + ]) + .status() + .unwrap(); + + assert!(status.success()); + let updated: StewardReviewWorksheet = + serde_json::from_slice(&fs::read(&output_path).unwrap()).unwrap(); + assert_eq!( + updated.decisions[0].reviewed_disposition, + Some(Disposition::AlignmentVersioning) + ); + assert_eq!( + fs::metadata(&output_path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + + let replay_status = Command::new(env!("CARGO_BIN_EXE_conceptweave-zotero")) + .args([ + "--apply-decision-patch", + report_path.to_str().unwrap(), + output_path.to_str().unwrap(), + patch_path.to_str().unwrap(), + replay_path.to_str().unwrap(), + ]) + .status() + .unwrap(); + assert!(replay_status.success()); + assert_eq!( + fs::read(&replay_path).unwrap(), + fs::read(&output_path).unwrap() + ); + + let original_output = fs::read(&output_path).unwrap(); + assert!(!run_patch(&report_path, &worksheet_path, &patch_path, &output_path).success()); + assert_eq!(fs::read(&output_path).unwrap(), original_output); + + let rejected_output = temp_path("decision-binding-rejected"); + assert!(!rejected_output.exists()); + for missing in [true, false] { + let mut value = serde_json::to_value(&patch).unwrap(); + if missing { + value.as_object_mut().unwrap().remove("proposal_digest"); + } else { + value["proposal_digest"] = serde_json::json!(""); + } + let invalid_path = private_input("decision-binding-invalid", &value); + assert!( + !run_patch( + &report_path, + &worksheet_path, + &invalid_path, + &rejected_output + ) + .success() + ); + assert!(!rejected_output.exists()); + fs::remove_file(invalid_path).unwrap(); + } + + report.classified_items[0] + .title + .push_str(" changed review context"); + let current = build_steward_review_worksheet(&report).unwrap(); + let current_report_path = private_input("decision-binding-current-report", &report); + let current_worksheet_path = private_input("decision-binding-current-worksheet", ¤t); + let original_patch = fs::read(&patch_path).unwrap(); + let current_bytes = fs::read(¤t_worksheet_path).unwrap(); + assert!( + !run_patch( + ¤t_report_path, + ¤t_worksheet_path, + &patch_path, + &rejected_output + ) + .success() + ); + assert!(!rejected_output.exists()); + assert_eq!(fs::read(&patch_path).unwrap(), original_patch); + assert_eq!(fs::read(¤t_worksheet_path).unwrap(), current_bytes); + fs::remove_file(current_report_path).unwrap(); + fs::remove_file(current_worksheet_path).unwrap(); + + for path in [ + report_path, + worksheet_path, + patch_path, + output_path, + replay_path, + ] { + fs::remove_file(path).unwrap(); + } +} + +#[test] +fn decision_patch_cli_never_overwrites_or_emits_invalid_work() { + let report = classify_snapshot( + "9.0.6".into(), + None, + 42, + vec![ZoteroItem { + source_record: None, + key: "ITEM".into(), + version: 7, + data: ItemData { + item_type: "book".into(), + title: "ontology learning".into(), + abstract_note: String::new(), + doi: String::new(), + parent_item: String::new(), + collections: vec![], + tags: vec![], + }, + }], + ); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let invalid_patch = StewardDecisionPatch { + proposal_digest: worksheet.proposal_digest.clone(), + library_version: report.library_version + 1, + rule_revision: report.rule_revision.clone(), + snapshot_digest: report.snapshot_digest.clone(), + decisions: vec![StewardDecisionUpdate { + item_key: "ITEM".into(), + item_version: 7, + reviewed_disposition: Disposition::OutOfScope, + }], + }; + let report_path = private_input("decision-invalid-report", &report); + let worksheet_path = private_input("decision-invalid-worksheet", &worksheet); + let patch_path = private_input("decision-invalid-patch", &invalid_patch); + let absent_output = temp_path("decision-invalid-output"); + let existing_output = temp_path("decision-existing-output"); + let _ = fs::remove_file(&absent_output); + let _ = fs::remove_file(&existing_output); + + assert!(!run_patch(&report_path, &worksheet_path, &patch_path, &absent_output).success()); + assert!(!absent_output.exists()); + + fs::write(&existing_output, b"preserve me").unwrap(); + assert!(!run_patch(&report_path, &worksheet_path, &patch_path, &existing_output).success()); + assert_eq!(fs::read(&existing_output).unwrap(), b"preserve me"); + + for path in [report_path, worksheet_path, patch_path, existing_output] { + fs::remove_file(path).unwrap(); + } +} + +fn run_patch( + report: &std::path::Path, + worksheet: &std::path::Path, + patch: &std::path::Path, + output: &std::path::Path, +) -> std::process::ExitStatus { + Command::new(env!("CARGO_BIN_EXE_conceptweave-zotero")) + .args([ + "--apply-decision-patch", + report.to_str().unwrap(), + worksheet.to_str().unwrap(), + patch.to_str().unwrap(), + output.to_str().unwrap(), + ]) + .status() + .unwrap() +} + +fn private_input(name: &str, value: &impl serde::Serialize) -> std::path::PathBuf { + let path = temp_path(name); + let _ = fs::remove_file(&path); + fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap(); + path +} + +fn temp_path(name: &str) -> std::path::PathBuf { + std::env::temp_dir().join(format!( + "conceptweave-zotero-{}-{name}.json", + std::process::id() + )) +} diff --git a/docs/PRD.md b/docs/PRD.md index 4684708..5f47731 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -93,6 +93,7 @@ During the human review campaign, operators must be able to validate a partially 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 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. +The offline CLI must read the saved report, current worksheet, and decision patch as three distinct owner-only file identities and create a separate updated worksheet. It must never overwrite the current worksheet, reread Zotero, or emit output after invalid input. 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 f355cd2..0ac6f39 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -121,6 +121,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` 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. +`conceptweave-zotero --apply-decision-patch /tmp/REPORT.json /tmp/CURRENT_WORKSHEET.json /tmp/PATCH.json /tmp/UPDATED_WORKSHEET.json` exposes that contract through the existing private artifact boundary. Four textual paths and all three opened input device/inode identities must differ. Inputs are bounded, regular, single-link, exact `0600`, opened through the checked canonical parent with no-follow/nonblocking flags. The required patch proposal binding is deserialized and passed unchanged to the owner validator, never synthesized by the CLI. Validation failure leaves no output; existing output is preserved even for a valid patch. Serialization precedes create-new output, while a later write failure may retain private partial files without cleanup or retry. Identical replay may create another equal worksheet. No Zotero read or authority verifier runs. 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 2291bf8..a41a7f7 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -57,6 +57,8 @@ The follow-up premortem identified a separate FIFO replacement: `O_NOFOLLOW` doe 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 offline CLI consumes the saved report, current worksheet, and patch as distinct owner-only file identities and emits only a separate create-new worksheet. Reusing the existing private artifact boundary keeps the command local, bounded, and fail-closed without introducing a second review service or repository. The CLI never rereads Zotero, overwrites the source worksheet, or verifies approval. + The reader fails closed above 50,000 items or 256 MiB of cumulative response bodies, while retaining the 8 MiB per-page bound, finite request timeouts, redirect denial, total-count checks, snapshot-version checks, and duplicate-key detection. Pagination, consistency, resource-budget, and provider-contract behavior are separated from the narrow `ureq` transport so deterministic tests exercise the production reader core rather than excluding the entire reader from coverage. In the context of reading every bibliographic source before classification, facing individually timely pages that can cumulatively hold a run open for days, we decided for a five-minute monotonic admission/completion budget in the existing reader and against rejecting legitimate short pages or adding another transport, to bound accepted work without excluding papers, accepting that an already-started request or classification computation can finish after the limit before its result is rejected. This is an application read limit, not a model timeout, hard process-cancellation deadline, wall-clock/suspend guarantee or atomic snapshot claim. Each page is checked before fetch and after return, and the complete report is checked before return. The stdlib clock has a private deterministic test seam; public APIs, provider timeouts and data/byte ceilings are unchanged. The [deadline doctoring](../doctoring/zotero_metadata_deadline.md) records the original review, RED/GREEN, alternatives and exact verification. This amendment remains Proposed and grants no Zotero mutation authority. @@ -122,6 +124,12 @@ GREEN source `f6735b585022aac1c8ceff86c150d9b64fd77ec2` passed 68 tests across 1 ## Alternatives considered +### Proposed decision CLI integration amendment — September 7 + +PR32 exposes the patch owner as an offline command. Normal merge `b7cae29` retains the original CLI delta and PR31's required proposal binding with prior private-file protections. The existing arm reads three distinct private inputs, delegates the typed patch unchanged to the canonical validator, serializes the returned worksheet and creates a new output. We retain that implementation rather than add another validator or infer a patch digest in the CLI. Two synthetic fixture constructors adopt the required binding; no production source is copied or new dependency introduced. + +Test commit `d3a2c7a` extends the real binary contract: a valid patch cannot overwrite existing output; missing and blank binding produce no output; an old patch fails against changed report content even with a fresh valid worksheet; the rejected operation preserves patch and worksheet bytes. Successful owner-only output and identical replay remain covered. These are inherited-owner GREEN checks, not a newly reproduced CLI defect or actual steward review. Rejected alternatives are duplicate CLI validation, automatic legacy binding repair and cleanup of failed paths. Local output remains unverified metadata preparation; failures after writing starts may retain partial private files. Root and subsequent command consumers must inherit the binding and private-open repairs before release. Protected checks and independent approval remain separate. + ### 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. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1b4956a..897e99e 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 PR32 decision CLI continuity verification + +Original PR32 `1e711728d83efc1e60fc3d43ba0c67c467dd6a43` passed 161 tests/34 suites. Normal merge `b7cae29` retains it and PR31 `eaf248afd33dcac477daf1e3a31d79d47c5cbb69`; two synthetic patch constructors adopt required proposal identity, and integrated tests pass 210/34. The thin apply arm remains unchanged: private distinct inputs go to the canonical validator without binding backfill; output is serialized before create-new writing. No additional production defect or new RED is claimed. + +Test `d3a2c7a` extends actual binary execution: valid-patch nonoverwrite, missing/blank binding no-output rejection, stale patch against regenerated current worksheet rejection, and unchanged patch/worksheet bytes. Existing valid `0600` output and equal replay remain. Independent bounded review found no additional defect. Final source passes 210 tests/34 suites including three doctests, strict Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks. Unchanged coverage passes 332/332 reported functions, 2,997/2,997 normalized regions and 548/548 normalized branches. Raw 4,060/4,122 lines, 6,228/6,335 regions and 503/548 branches are not 100%. Logs: `/tmp/conceptweave-pr32-{baseline,integrated,verified,clippy,rustdoc,coverage}.log`. + +README/TRD/Proposed ADR0006 distinguish current-content binding, no legacy backfill, pre-write rejection and possible retained partial files after write failure. Root/later consumers still require cascade adoption. Authentic decisions and independent approvals remain 0/3,715 plus four unresolved sources; synthetic unit data is not real research. Native Visual Inspection was retried, but the Mac is locked and no fresh screenshot exists. Keep Draft; no Zotero mutation, hosted GREEN, protected merge or release occurred. + ### 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.