From 0027d7d9c7dc4f7d83853599b8ba2f6ce63d41d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:19:23 +0900 Subject: [PATCH 01/18] test: require capture-bound private full-text review context --- .../src/full_text_capture.rs | 13 ++++ .../src/full_text_capture_tests.rs | 64 +++++++++++++++++++ crates/conceptweave-zotero/src/lib.rs | 3 +- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/src/full_text_capture.rs b/crates/conceptweave-zotero/src/full_text_capture.rs index c7220849..7112a5d3 100644 --- a/crates/conceptweave-zotero/src/full_text_capture.rs +++ b/crates/conceptweave-zotero/src/full_text_capture.rs @@ -72,6 +72,19 @@ impl fmt::Display for FullTextError { impl std::error::Error for FullTextError {} +/// Builds a bounded private evidence view for the next pending review rows. +/// +/// The returned JSON is not a decision patch or an approval. It must not replace +/// the original report or worksheet, and it is rejected by legacy apply commands. +pub fn build_full_text_review_json( + _report: &ClassificationReport, + _worksheet: &crate::StewardReviewWorksheet, + _capture: &FullTextCapture, + _limit: usize, +) -> Result, FullTextError> { + Err(INVALID_EVIDENCE) +} + /// Reads every full-text manifest entry through the fixed loopback API. /// /// The report remains unchanged. Missing content is retained explicitly, while diff --git a/crates/conceptweave-zotero/src/full_text_capture_tests.rs b/crates/conceptweave-zotero/src/full_text_capture_tests.rs index d3ce7cdf..10bad93f 100644 --- a/crates/conceptweave-zotero/src/full_text_capture_tests.rs +++ b/crates/conceptweave-zotero/src/full_text_capture_tests.rs @@ -1,6 +1,70 @@ use super::*; use crate::{ZoteroItem, classify_snapshot}; +#[test] +fn review_view_binds_exact_capture_and_keeps_missing_parents_without_decisions() { + let report = report_fixture(); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let before_report = serde_json::to_vec(&report).unwrap(); + let before_worksheet = worksheet.clone(); + let capture = capture_with(&report, 4096, &mut |request_path, _| { + Ok(response_fixture(request_path)) + }) + .unwrap(); + let bytes = build_full_text_review_json(&report, &worksheet, &capture, 2).unwrap(); + let view: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(view["view_kind"], "full_text_review_view_v1"); + assert_eq!(view["capture_digest"], capture.capture_digest); + assert_eq!( + view["metadata_report_digest"], + capture.capture_evidence.metadata_report_digest + ); + assert_eq!(view["bibliographic_item_count"], 2); + assert_eq!(view["review_batch"]["remaining_count"], 2); + assert_eq!( + view["review_batch"]["decisions"].as_array().unwrap().len(), + 2 + ); + assert_eq!( + view["attachment_evidence"]["ABCD2345"] + .as_array() + .unwrap() + .len(), + 2 + ); + assert_eq!( + view["attachment_evidence"]["DEFG5678"], + serde_json::json!([]) + ); + assert_eq!( + view["attachment_evidence"]["ABCD2345"][0]["item_key"], + "BCDE3456" + ); + assert_eq!( + view["attachment_evidence"]["ABCD2345"][0]["content_response"]["body"], + r#"{"content":"fixture text 한글","indexedPages":2,"totalPages":2,"providerExtra":{"retained":true}}"# + ); + assert_eq!( + view["attachment_evidence"]["ABCD2345"][1]["content_response"]["status"], + 404 + ); + assert!( + view["review_batch"]["decisions"] + .as_array() + .unwrap() + .iter() + .all(|row| row["reviewed_disposition"].is_null()) + ); + assert!(serde_json::from_slice::(&bytes).is_err()); + assert!(serde_json::from_slice::(&bytes).is_err()); + assert_eq!(serde_json::to_vec(&report).unwrap(), before_report); + assert_eq!(worksheet, before_worksheet); + assert_eq!( + build_full_text_review_json(&report, &worksheet, &capture, 2).unwrap(), + bytes + ); +} + fn report_fixture() -> ClassificationReport { let items: Vec = serde_json::from_value(serde_json::json!([ {"key":"ABCD2345","version":2,"data":{"itemType":"journalArticle","title":"fixture paper"}}, diff --git a/crates/conceptweave-zotero/src/lib.rs b/crates/conceptweave-zotero/src/lib.rs index 58218af1..607c9bfd 100644 --- a/crates/conceptweave-zotero/src/lib.rs +++ b/crates/conceptweave-zotero/src/lib.rs @@ -12,7 +12,8 @@ use std::time::Duration; mod full_text_capture; pub use full_text_capture::{ - FullTextCapture, FullTextError, read_local_full_text, verify_full_text_capture, + FullTextCapture, FullTextError, build_full_text_review_json, read_local_full_text, + verify_full_text_capture, }; /// Classification rule revision recorded in every report. From e426c2b40f3071833345716d66e8a0eefb93e5c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:20:31 +0900 Subject: [PATCH 02/18] feat: build bounded capture-bound full-text review views --- .../src/full_text_capture.rs | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/crates/conceptweave-zotero/src/full_text_capture.rs b/crates/conceptweave-zotero/src/full_text_capture.rs index 7112a5d3..12f3b469 100644 --- a/crates/conceptweave-zotero/src/full_text_capture.rs +++ b/crates/conceptweave-zotero/src/full_text_capture.rs @@ -77,12 +77,69 @@ impl std::error::Error for FullTextError {} /// The returned JSON is not a decision patch or an approval. It must not replace /// the original report or worksheet, and it is rejected by legacy apply commands. pub fn build_full_text_review_json( - _report: &ClassificationReport, - _worksheet: &crate::StewardReviewWorksheet, - _capture: &FullTextCapture, - _limit: usize, + report: &ClassificationReport, + worksheet: &crate::StewardReviewWorksheet, + capture: &FullTextCapture, + limit: usize, ) -> Result, FullTextError> { - Err(INVALID_EVIDENCE) + #[derive(Serialize)] + struct AttachmentEvidence<'a> { + item_key: &'a str, + metadata_version: Option, + content_response: &'a CapturedResponse, + } + #[derive(Serialize)] + struct ReviewView<'a> { + view_kind: &'static str, + capture_digest: &'a str, + metadata_report_digest: &'a str, + proposal_digest: String, + bibliographic_item_count: usize, + review_batch: crate::StewardReviewBatch, + attachment_evidence: BTreeMap>>, + } + + let review_batch = crate::build_steward_review_batch(report, worksheet, limit) + .map_err(|_| FullTextError("full-text review requires a valid pending batch"))?; + verify_full_text_capture(capture, report)?; + let parent_by_key: BTreeMap<_, _> = report + .snapshot_items + .iter() + .map(|item| (item.item_key.as_str(), item.parent_item_key.as_deref())) + .collect(); + let mut attachment_evidence: BTreeMap<_, Vec<_>> = review_batch + .decisions + .iter() + .map(|decision| (decision.item_key.clone(), Vec::new())) + .collect(); + for record in &capture.capture_evidence.records { + if let Some(rows) = parent_by_key[record.item_key.as_str()] + .and_then(|parent| attachment_evidence.get_mut(parent)) + { + rows.push(AttachmentEvidence { + item_key: &record.item_key, + metadata_version: record.metadata_response.version, + content_response: &record.content_response, + }); + } + } + let view = ReviewView { + view_kind: "full_text_review_view_v1", + capture_digest: &capture.capture_digest, + metadata_report_digest: &capture.capture_evidence.metadata_report_digest, + proposal_digest: crate::classification_proposal_digest(report), + bibliographic_item_count: report.classified_items.len(), + review_batch, + attachment_evidence, + }; + // A fixed slice bounds serialization itself, including JSON escaping. + let mut bytes = vec![0; 16 * 1024 * 1024]; + let mut writer = std::io::Cursor::new(bytes.as_mut_slice()); + serde_json::to_writer(&mut writer, &view) + .map_err(|_| FullTextError("full-text review exceeds the 16 MiB output limit"))?; + let length = writer.position() as usize; + bytes.truncate(length); + Ok(bytes) } /// Reads every full-text manifest entry through the fixed loopback API. From 3fb930bfb540e1c62615f2cb8392c348db85048c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:23:42 +0900 Subject: [PATCH 03/18] test: preserve full-text review selection and resource boundaries --- .../src/full_text_capture_tests.rs | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/crates/conceptweave-zotero/src/full_text_capture_tests.rs b/crates/conceptweave-zotero/src/full_text_capture_tests.rs index 10bad93f..53c15bdd 100644 --- a/crates/conceptweave-zotero/src/full_text_capture_tests.rs +++ b/crates/conceptweave-zotero/src/full_text_capture_tests.rs @@ -65,6 +65,121 @@ fn review_view_binds_exact_capture_and_keeps_missing_parents_without_decisions() ); } +#[test] +fn review_view_selects_only_pending_parents_and_never_copies_unrelated_text() { + let report = report_fixture(); + let mut worksheet = build_steward_review_worksheet(&report).unwrap(); + let capture = capture_with(&report, 4096, &mut |request_path, _| { + Ok(response_fixture(request_path)) + }) + .unwrap(); + let first: serde_json::Value = serde_json::from_slice( + &build_full_text_review_json(&report, &worksheet, &capture, 1).unwrap(), + ) + .unwrap(); + assert_eq!( + first["review_batch"]["decisions"].as_array().unwrap().len(), + 1 + ); + assert!(first["attachment_evidence"].get("DEFG5678").is_none()); + worksheet.decisions[0].reviewed_disposition = Some(crate::Disposition::OutOfScope); + let next = build_full_text_review_json(&report, &worksheet, &capture, 2).unwrap(); + let view: serde_json::Value = serde_json::from_slice(&next).unwrap(); + assert_eq!(view["review_batch"]["remaining_count"], 1); + assert_eq!(view["bibliographic_item_count"], 2); + assert_eq!( + view["attachment_evidence"], + serde_json::json!({"DEFG5678":[]}) + ); + assert!(!String::from_utf8(next).unwrap().contains("fixture text")); +} + +#[test] +fn review_view_rejects_changed_capture_report_and_invalid_pending_work() { + let report = report_fixture(); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let capture = capture_with(&report, 4096, &mut |request_path, _| { + Ok(response_fixture(request_path)) + }) + .unwrap(); + for field in ["body", "status", "version"] { + let mut saved = serde_json::to_value(&capture).unwrap(); + saved["capture_evidence"]["records"][0]["content_response"][field] = if field == "body" { + serde_json::json!("private changed text") + } else { + serde_json::json!(999) + }; + let changed: FullTextCapture = serde_json::from_value(saved).unwrap(); + assert!(build_full_text_review_json(&report, &worksheet, &changed, 2).is_err()); + } + let mut changed_report = report_fixture(); + changed_report.classified_items[0].title = "different report evidence".into(); + assert!(build_full_text_review_json(&changed_report, &worksheet, &capture, 2).is_err()); + for limit in [0, 101] { + assert!(build_full_text_review_json(&report, &worksheet, &capture, limit).is_err()); + } + let mut invalid = worksheet.clone(); + invalid.decisions[0].reviewed_disposition = Some(crate::Disposition::NeedsStewardReview); + assert!(build_full_text_review_json(&report, &invalid, &capture, 2).is_err()); + for decision in &mut invalid.decisions { + decision.reviewed_disposition = Some(crate::Disposition::OutOfScope); + } + assert!(build_full_text_review_json(&report, &invalid, &capture, 2).is_err()); +} + +#[test] +fn review_view_retains_empty_partial_and_unknown_provider_evidence() { + for body in [ + r#"{"content":""}"#, + r#"{"content":"partial text","indexedPages":1,"totalPages":2}"#, + r#"{"content":"unknown completeness","providerExtra":{"original":true}}"#, + ] { + let report = report_fixture(); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let capture = capture_with(&report, 4096, &mut |request_path, _| { + let mut response = response_fixture(request_path); + if request_path == "items/BCDE3456/fulltext" { + response.body = body.into(); + } + Ok(response) + }) + .unwrap(); + let view: serde_json::Value = serde_json::from_slice( + &build_full_text_review_json(&report, &worksheet, &capture, 2).unwrap(), + ) + .unwrap(); + assert_eq!( + view["attachment_evidence"]["ABCD2345"][0]["content_response"]["body"], + body + ); + assert_eq!(view["bibliographic_item_count"], 2); + } +} + +#[test] +fn review_view_rejects_json_expansion_without_truncating_valid_captured_text() { + let report = report_fixture(); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + let capture = capture_with(&report, MAX_SNAPSHOT_BYTES, &mut |request_path, _| { + let mut response = response_fixture(request_path); + if request_path.ends_with("/fulltext") { + response.status = 200; + response.version = Some(if request_path.contains("BCDE3456") { + 12403 + } else { + 0 + }); + response.body = serde_json::json!({"content":"\"".repeat(3 * 1024 * 1024)}).to_string(); + } + Ok(response) + }) + .unwrap(); + verify_full_text_capture(&capture, &report).unwrap(); + let error = build_full_text_review_json(&report, &worksheet, &capture, 1).unwrap_err(); + assert!(error.to_string().contains("16 MiB output limit")); + assert_eq!(worksheet.decisions.len(), 2); +} + fn report_fixture() -> ClassificationReport { let items: Vec = serde_json::from_value(serde_json::json!([ {"key":"ABCD2345","version":2,"data":{"itemType":"journalArticle","title":"fixture paper"}}, From fcba2367dd8623ee7cb4408bd76c76e5d304ce73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:27:29 +0900 Subject: [PATCH 04/18] test: verify distinct text parents and source version spaces --- .../src/full_text_capture_tests.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/conceptweave-zotero/src/full_text_capture_tests.rs b/crates/conceptweave-zotero/src/full_text_capture_tests.rs index 53c15bdd..4047b48e 100644 --- a/crates/conceptweave-zotero/src/full_text_capture_tests.rs +++ b/crates/conceptweave-zotero/src/full_text_capture_tests.rs @@ -15,6 +15,10 @@ fn review_view_binds_exact_capture_and_keeps_missing_parents_without_decisions() let view: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(view["view_kind"], "full_text_review_view_v1"); assert_eq!(view["capture_digest"], capture.capture_digest); + assert_eq!( + view["proposal_digest"], + crate::classification_proposal_digest(&report) + ); assert_eq!( view["metadata_report_digest"], capture.capture_evidence.metadata_report_digest @@ -40,6 +44,19 @@ fn review_view_binds_exact_capture_and_keeps_missing_parents_without_decisions() view["attachment_evidence"]["ABCD2345"][0]["item_key"], "BCDE3456" ); + assert_eq!( + view["attachment_evidence"]["ABCD2345"][0]["metadata_version"], + 1 + ); + assert_eq!( + view["attachment_evidence"]["ABCD2345"][0]["content_response"]["version"], + 12403 + ); + assert_eq!( + view["attachment_evidence"]["ABCD2345"][1]["metadata_version"], + 0 + ); + assert!(view["attachment_evidence"]["ABCD2345"][1]["content_response"]["version"].is_null()); assert_eq!( view["attachment_evidence"]["ABCD2345"][0]["content_response"]["body"], r#"{"content":"fixture text 한글","indexedPages":2,"totalPages":2,"providerExtra":{"retained":true}}"# @@ -94,6 +111,77 @@ fn review_view_selects_only_pending_parents_and_never_copies_unrelated_text() { assert!(!String::from_utf8(next).unwrap().contains("fixture text")); } +#[test] +fn review_view_separates_two_text_parents_and_excludes_standalone_attachments() { + let source: Vec = serde_json::from_value(serde_json::json!([ + {"key":"ABCD2345","version":2,"data":{"itemType":"book","title":"first paper"}}, + {"key":"BCDE3456","version":1,"data":{"itemType":"attachment","parentItem":"ABCD2345"}}, + {"key":"DEFG5678","version":2,"data":{"itemType":"book","title":"second paper"}}, + {"key":"EFGH6789","version":1,"data":{"itemType":"attachment","parentItem":"DEFG5678"}}, + {"key":"FGHI789A","version":1,"data":{"itemType":"attachment"}} + ])) + .unwrap(); + let mut report = classify_snapshot("10.0.1".into(), Some("fixture-server".into()), 2, source); + report.api_version = Some(3); + report.schema_version = Some(44); + let mut worksheet = build_steward_review_worksheet(&report).unwrap(); + let capture = capture_with(&report, 4096, &mut |request_path, _| { + Ok(match request_path { + "fulltext?since=0" => CapturedResponse {status:200,version:None, + body:r#"{"BCDE3456":12403,"EFGH6789":9,"FGHI789A":0}"#.into()}, + "items/EFGH6789" => CapturedResponse {status:200,version:Some(1), + body:r#"{"key":"EFGH6789","version":1,"data":{"itemType":"attachment","parentItem":"DEFG5678"}}"#.into()}, + "items/EFGH6789/fulltext" => CapturedResponse {status:200,version:Some(9), + body:r#"{"content":"second parent evidence"}"#.into()}, + "items/FGHI789A" => CapturedResponse {status:200,version:Some(1), + body:r#"{"key":"FGHI789A","version":1,"data":{"itemType":"attachment"}}"#.into()}, + "items/FGHI789A/fulltext" => CapturedResponse {status:200,version:Some(0), + body:r#"{"content":"standalone evidence"}"#.into()}, + _ => response_fixture(request_path), + }) + }).unwrap(); + let both: serde_json::Value = serde_json::from_slice( + &build_full_text_review_json(&report, &worksheet, &capture, 2).unwrap(), + ) + .unwrap(); + assert_eq!(both["bibliographic_item_count"], 2); + assert_eq!( + both["attachment_evidence"]["ABCD2345"] + .as_array() + .unwrap() + .len(), + 1 + ); + assert_eq!( + both["attachment_evidence"]["DEFG5678"] + .as_array() + .unwrap() + .len(), + 1 + ); + assert_eq!( + both["attachment_evidence"]["DEFG5678"][0]["item_key"], + "EFGH6789" + ); + assert_eq!( + both["attachment_evidence"]["DEFG5678"][0]["content_response"]["version"], + 9 + ); + let first = + String::from_utf8(build_full_text_review_json(&report, &worksheet, &capture, 1).unwrap()) + .unwrap(); + assert!(first.contains("fixture text")); + assert!(!first.contains("second parent evidence")); + assert!(!first.contains("standalone evidence")); + worksheet.decisions[0].reviewed_disposition = Some(crate::Disposition::OutOfScope); + let second = + String::from_utf8(build_full_text_review_json(&report, &worksheet, &capture, 1).unwrap()) + .unwrap(); + assert!(second.contains("second parent evidence")); + assert!(!second.contains("fixture text")); + assert!(!second.contains("standalone evidence")); +} + #[test] fn review_view_rejects_changed_capture_report_and_invalid_pending_work() { let report = report_fixture(); From 25a1005205d04ffd2815a96539180942da8ae685 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:29:23 +0900 Subject: [PATCH 05/18] test(zotero): reproduce missing private full-text review CLI --- crates/conceptweave-zotero/src/main.rs | 53 +++ .../tests/full_text_review_cli.rs | 339 ++++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 crates/conceptweave-zotero/tests/full_text_review_cli.rs diff --git a/crates/conceptweave-zotero/src/main.rs b/crates/conceptweave-zotero/src/main.rs index c42126b8..48c95e40 100644 --- a/crates/conceptweave-zotero/src/main.rs +++ b/crates/conceptweave-zotero/src/main.rs @@ -637,6 +637,59 @@ fn main() -> Result<(), Box> { mod tests { use super::*; + #[test] + fn full_text_review_mode_requires_five_distinct_bounded_arguments() { + assert!( + parse_output_request([ + "--full-text-review", + "/tmp/report.json", + "/tmp/worksheet.json", + "/tmp/capture.json", + "2", + "/tmp/view.json", + ]) + .is_ok() + ); + for length in 1..6 { + let arguments = ["--full-text-review", "r", "w", "c", "1", "o"]; + assert!(parse_output_request(arguments[..length].iter().copied()).is_err()); + } + for limit in [ + "", + "0", + "101", + " 1", + "+1", + "-1", + "one", + "999999999999999999999999999", + ] { + assert!( + parse_output_request(["--full-text-review", "r", "w", "c", limit, "o"]).is_err() + ); + } + for paths in [ + ["r", "r", "c", "o"], + ["r", "w", "r", "o"], + ["r", "w", "c", "r"], + ] { + assert!( + parse_output_request([ + "--full-text-review", + paths[0], + paths[1], + paths[2], + "1", + paths[3] + ]) + .is_err() + ); + } + assert!( + parse_output_request(["--full-text-review", "r", "w", "c", "1", "o", "extra"]).is_err() + ); + } + #[test] fn full_text_mode_requires_two_distinct_artifact_paths() { let report = "/tmp/report.json"; diff --git a/crates/conceptweave-zotero/tests/full_text_review_cli.rs b/crates/conceptweave-zotero/tests/full_text_review_cli.rs new file mode 100644 index 00000000..cdd4b08f --- /dev/null +++ b/crates/conceptweave-zotero/tests/full_text_review_cli.rs @@ -0,0 +1,339 @@ +#![cfg(unix)] + +use conceptweave_zotero::{ + ClassificationReport, FullTextCapture, StewardReviewWorksheet, ZoteroItem, + build_steward_review_worksheet, classify_snapshot, verify_full_text_capture, +}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt, symlink}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +#[test] +fn full_text_review_cli_preserves_private_evidence_without_becoming_a_decision() { + let inputs = FixtureFiles::new("positive"); + let output_path = inputs.path("view"); + let command = inputs.run("2", &output_path); + assert!(command.status.success(), "{:?}", command.stderr); + assert!(command.stdout.is_empty()); + let output: serde_json::Value = + serde_json::from_slice(&fs::read(&output_path).unwrap()).unwrap(); + assert_eq!(output["view_kind"], "full_text_review_view_v1"); + assert_eq!(output["bibliographic_item_count"], 2); + assert_eq!( + output["review_batch"]["decisions"] + .as_array() + .unwrap() + .len(), + 2 + ); + assert_eq!( + output["attachment_evidence"]["CDEF4567"], + serde_json::json!([]) + ); + assert_eq!( + output["attachment_evidence"]["ABCD2345"][0]["content_response"]["body"], + r#"{"content":"synthetic private text","providerExtra":true}"# + ); + assert!(output.get("decisions").is_none()); + assert_eq!( + fs::metadata(&output_path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + + let original = fs::read(&inputs.worksheet_path).unwrap(); + for apply_mode in ["--apply-review-batch", "--apply-decision-patch"] { + let rejected_path = inputs.path(apply_mode); + let command = Command::new(env!("CARGO_BIN_EXE_conceptweave-zotero")) + .arg(apply_mode) + .args([ + &inputs.report_path, + &inputs.worksheet_path, + &output_path, + &rejected_path, + ]) + .output() + .unwrap(); + assert!(!command.status.success()); + assert!(!rejected_path.exists()); + assert_eq!(fs::read(&inputs.worksheet_path).unwrap(), original); + } + let again = inputs.run("2", &output_path); + assert!(!again.status.success()); + assert_eq!( + serde_json::from_slice::(&fs::read(&output_path).unwrap()).unwrap(), + output + ); + fs::remove_file(output_path).unwrap(); +} + +#[test] +fn full_text_review_cli_rejects_invalid_or_oversized_capture_without_echoing_content() { + let inputs = FixtureFiles::new("invalid"); + let output_path = inputs.path("view"); + let original = fs::read(&inputs.worksheet_path).unwrap(); + for content in [ + b"{\"synthetic-private-sentinel\":true}".as_slice(), + b"{\"capture_digest\":\"synthetic-private-sentinel\",", + b"\xff", + ] { + fs::write(&inputs.capture_path, content).unwrap(); + let command = inputs.run("1", &output_path); + assert!(!command.status.success()); + assert!(!output_path.exists()); + assert!(command.stdout.is_empty()); + let stderr = String::from_utf8(command.stderr).unwrap(); + assert!( + stderr.contains("full-text capture input is invalid"), + "{stderr}" + ); + assert!(!stderr.contains("synthetic-private-sentinel")); + } + let file = OpenOptions::new() + .write(true) + .open(&inputs.capture_path) + .unwrap(); + file.set_len(512 * 1024 * 1024 + 1).unwrap(); + drop(file); + let command = inputs.run("1", &output_path); + assert!(!command.status.success()); + assert!(!output_path.exists()); + assert!( + String::from_utf8(command.stderr) + .unwrap() + .contains("full-text capture input exceeds the file size limit") + ); + assert_eq!(fs::read(&inputs.worksheet_path).unwrap(), original); +} + +#[test] +fn full_text_review_cli_rejects_missing_alias_and_unsafe_files() { + let inputs = FixtureFiles::new("unsafe"); + let output_path = inputs.path("view"); + let saved = inputs.path("saved"); + fs::rename(&inputs.capture_path, &saved).unwrap(); + assert!(!inputs.run("1", &output_path).status.success()); + symlink(&saved, &inputs.capture_path).unwrap(); + assert!(!inputs.run("1", &output_path).status.success()); + fs::remove_file(&inputs.capture_path).unwrap(); + fs::hard_link(&saved, &inputs.capture_path).unwrap(); + assert!(!inputs.run("1", &output_path).status.success()); + fs::remove_file(&inputs.capture_path).unwrap(); + fs::rename(&saved, &inputs.capture_path).unwrap(); + fs::set_permissions(&inputs.capture_path, fs::Permissions::from_mode(0o644)).unwrap(); + assert!(!inputs.run("1", &output_path).status.success()); + fs::set_permissions(&inputs.capture_path, fs::Permissions::from_mode(0o600)).unwrap(); + for capture_path in [&inputs.report_path, &inputs.worksheet_path, &output_path] { + let command = run( + &inputs.report_path, + &inputs.worksheet_path, + capture_path, + "1", + &output_path, + ); + assert!(!command.status.success()); + } + // Different path spellings that resolve to one inode must still fail closed. + let alias = inputs + .report_path + .parent() + .unwrap() + .join(".") + .join(inputs.report_path.file_name().unwrap()); + assert!( + !run( + &inputs.report_path, + &inputs.worksheet_path, + &alias, + "1", + &output_path + ) + .status + .success() + ); + assert!( + !run( + &inputs.report_path, + &inputs.worksheet_path, + Path::new("relative.json"), + "1", + &output_path + ) + .status + .success() + ); + assert!(!output_path.exists()); +} + +#[test] +fn full_text_review_cli_capture_file_budget_is_separate_from_metadata_budget() { + let inputs = FixtureFiles::new("budgets"); + let output_path = inputs.path("view"); + // Legal JSON whitespace makes the file exceed 16 MiB without a large text value. + let mut file = OpenOptions::new() + .append(true) + .open(&inputs.capture_path) + .unwrap(); + let padding = [b' '; 8192]; + for _ in 0..2048 { + file.write_all(&padding).unwrap(); + } + drop(file); + assert!(inputs.run("1", &output_path).status.success()); + fs::remove_file(&output_path).unwrap(); + for metadata_path in [&inputs.report_path, &inputs.worksheet_path] { + let file = OpenOptions::new().write(true).open(metadata_path).unwrap(); + file.set_len(16 * 1024 * 1024 + 1).unwrap(); + drop(file); + assert!(!inputs.run("1", &output_path).status.success()); + assert!(!output_path.exists()); + } +} + +struct FixtureFiles { + name: &'static str, + report_path: PathBuf, + worksheet_path: PathBuf, + capture_path: PathBuf, +} + +impl FixtureFiles { + fn new(name: &'static str) -> Self { + let (report, worksheet, capture) = synthetic_fixture(); + let files = Self { + name, + report_path: temp_path(name, "report"), + worksheet_path: temp_path(name, "worksheet"), + capture_path: temp_path(name, "capture"), + }; + for (path, bytes) in [ + (&files.report_path, serde_json::to_vec(&report).unwrap()), + ( + &files.worksheet_path, + serde_json::to_vec(&worksheet).unwrap(), + ), + (&files.capture_path, serde_json::to_vec(&capture).unwrap()), + ] { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .mode(0o600) + .open(path) + .unwrap(); + file.write_all(&bytes).unwrap(); + } + files + } + fn path(&self, suffix: &str) -> PathBuf { + temp_path(self.name, suffix) + } + fn run(&self, limit: &str, output: &Path) -> Output { + run( + &self.report_path, + &self.worksheet_path, + &self.capture_path, + limit, + output, + ) + } +} + +impl Drop for FixtureFiles { + fn drop(&mut self) { + for path in [&self.report_path, &self.worksheet_path, &self.capture_path] { + let _ = fs::remove_file(path); + } + } +} + +fn run(report: &Path, worksheet: &Path, capture: &Path, limit: &str, output: &Path) -> Output { + Command::new(env!("CARGO_BIN_EXE_conceptweave-zotero")) + .arg("--full-text-review") + .args([report, worksheet, capture]) + .arg(limit) + .arg(output) + .output() + .unwrap() +} + +fn temp_path(name: &str, suffix: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "conceptweave-fulltext-review-{}-{name}-{suffix}.json", + std::process::id() + )) +} + +fn synthetic_fixture() -> ( + ClassificationReport, + StewardReviewWorksheet, + FullTextCapture, +) { + let items: Vec = serde_json::from_value(serde_json::json!([ + {"key":"ABCD2345","version":2,"data":{"itemType":"journalArticle","title":"synthetic paper"}}, + {"key":"BCDE3456","version":1,"data":{"itemType":"attachment","parentItem":"ABCD2345"}}, + {"key":"CDEF4567","version":2,"data":{"itemType":"book","title":"synthetic unattached paper"}} + ])).unwrap(); + let mut report = classify_snapshot("10.0.1".into(), Some("synthetic-server".into()), 2, items); + report.api_version = Some(3); + report.schema_version = Some(44); + let worksheet = build_steward_review_worksheet(&report).unwrap(); + // Field order is the capture wire format, not a real provider observation. + let evidence = format!( + concat!( + "{{\"capture_kind\":\"non_atomic_fulltext_sweep_v1\",", + "\"metadata_report_digest\":\"{}\",\"metadata_snapshot_digest\":\"{}\",", + "\"bibliographic_item_count\":2,\"started_unix_ms\":0,\"finished_unix_ms\":1,", + "\"library_before\":{},\"manifest_before\":{},\"records\":[{{", + "\"item_key\":\"BCDE3456\",\"metadata_response\":{},\"content_response\":{}}}],", + "\"manifest_after\":{},\"library_after\":{}}}" + ), + json_digest(&report), + report.snapshot_digest, + response(200, Some(2), "[]"), + response(200, None, r#"{"BCDE3456":3}"#), + response( + 200, + Some(1), + r#"{"key":"BCDE3456","version":1,"data":{"itemType":"attachment","parentItem":"ABCD2345"}}"# + ), + response( + 200, + Some(3), + r#"{"content":"synthetic private text","providerExtra":true}"# + ), + response(200, None, r#"{"BCDE3456":3}"#), + response(200, Some(2), "[]"), + ); + let capture: FullTextCapture = serde_json::from_str(&format!( + "{{\"capture_digest\":\"sha256:{:x}\",\"capture_evidence\":{evidence}}}", + Sha256::digest(evidence.as_bytes()) + )) + .unwrap(); + verify_full_text_capture(&capture, &report).unwrap(); + (report, worksheet, capture) +} + +fn response(status: u16, version: Option, body: &str) -> String { + #[derive(Serialize)] + struct Response<'a> { + status: u16, + version: Option, + body: &'a str, + } + serde_json::to_string(&Response { + status, + version, + body, + }) + .unwrap() +} + +fn json_digest(value: &impl Serialize) -> String { + format!( + "sha256:{:x}", + Sha256::digest(serde_json::to_vec(value).unwrap()) + ) +} From db1d2b9344dc8dc8e92626e99c4770ac4c16903d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:30:15 +0900 Subject: [PATCH 06/18] docs: define private full-text review view and approval boundary --- AGENTS.md | 1 + ARCHITECTURE.md | 2 ++ CHANGELOG.md | 2 ++ CLAUDE.md | 2 ++ THREAT_MODEL.md | 2 ++ crates/conceptweave-zotero/src/full_text_capture.rs | 8 ++++++++ docs/CONTEXT_MAP.md | 2 ++ docs/PRD.md | 2 ++ docs/TRD.md | 6 ++++++ docs/UBIQUITOUS_LANGUAGE.md | 1 + docs/UML.md | 7 +++++++ docs/adr/0006-zotero-research-intake.md | 10 ++++++++++ 12 files changed, 45 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 156d85ff..8983336e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ ConceptWeave owns automatic, evidence-bound **Semantic Model Engineering**. Do n - New database objects, when introduced, use descriptive two-or-more-word `snake_case` names and 3NF by default. - Preserve source evidence, truth status, and publication state separately. - Keep Zotero full-text captures separate from metadata reports and approval receipts; restored captures require bounded verification, and local HTTP continuity is not peer authentication. +- Full-text review views are read-only evidence; never strip their outer binding into a metadata-only patch and claim full-text decision or approval provenance. - Published semantic truth is immutable; correction uses supersession/new release. - Public Rust APIs require beginner-readable documentation. - Owned production coverage target is 100% line/function/region/branch where tooling exposes it. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c1e6721b..3d38bb37 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -24,6 +24,8 @@ flowchart LR Research Intake's Zotero adapter retains optional full-text observations in a separate private artifact bound to the original metadata report. It remains inside ConceptWeave: acquisition is supporting evidence work, not a publication authority or another research system of record. Provider version counters remain opaque at this Anti-Corruption Layer; downstream classification and review must explicitly adopt new content under fresh evidence bindings. See [ADR 0006](docs/adr/0006-zotero-research-intake.md). +Its Full-Text Review View reuses capture verification and canonical pending selection to make exact text inspectable without changing earlier proposals. This bounded read projection is not an aggregate or a decision-application API; the metadata-only approval chain does not acquire full-text provenance from it. + | Context | Type | Owns | Does not own | | --- | --- | --- | --- | | Source Observation | Supporting | immutable observations, parser receipts, evidence locations | source-system business truth | diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b751cd..6c96d907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to ConceptWeave are documented here. ### Added +- Private inspection of pending papers alongside their saved text, preserving missing material and leaving previous reports and decisions unchanged. + - Private, replayable paper-text capture for later research review, preserving unavailable material and leaving earlier reports and approvals unchanged. - Full-library research-source audit separating available text, incomplete indexing and missing material from reviewed classification; no paper is excluded because its abstract or text is unavailable. diff --git a/CLAUDE.md b/CLAUDE.md index 76caecc3..0cfb8135 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,3 +7,5 @@ ConceptWeave's core invariant is: **inference is not authority**. Every generate Keep domain logic in bounded domain modules, LLM/provider logic behind ports/adapters, and source/consumer systems independent. Prefer deterministic validation and explicit abstention over plausible unsupported output. Zotero source capture must not alter the metadata report or renew its approval. Preserve private-file protections and the full bibliographic denominator, including missing and partial text. + +The separate full-text review view does not authorize decisions or writes. Keep its evidence binding intact; existing metadata-only apply/finalization cannot establish full-text-reviewed approval. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 4b3df9ef..42f9ed6f 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -75,4 +75,6 @@ PR #30 commit `9733d28` reproduced the inode-preserving final-component symlink ## Release gate +The optional full-text review view must not be confused with the metadata-only decision or approval artifacts. A required versioned outer envelope and strict legacy parsers prevent silent direct application; an intentional caller can still strip fields, so no full-text-reviewed approval is claimed. The complete capture is reverified before selecting text, unrelated parent text is excluded, and output serialization itself is bounded. The larger serialized-capture input ceiling is separate from the unchanged report/worksheet limit. Source text is untrusted data, never instructions, and source content must not be included in input-error diagnostics. + A capability is not release-ready while a valid security finding lacks a deterministic test or equivalent machine-verifiable contract, while required exact-head checks are non-terminal, or while the implemented transport cannot satisfy the advertised security claim. Documentation must describe residual risk without upgrading provider guarantees by inference. diff --git a/crates/conceptweave-zotero/src/full_text_capture.rs b/crates/conceptweave-zotero/src/full_text_capture.rs index 12f3b469..92e6ddc3 100644 --- a/crates/conceptweave-zotero/src/full_text_capture.rs +++ b/crates/conceptweave-zotero/src/full_text_capture.rs @@ -74,6 +74,14 @@ impl std::error::Error for FullTextError {} /// Builds a bounded private evidence view for the next pending review rows. /// +/// This verifies the complete capture against the unchanged report, then selects +/// the same 1–100 pending rows as the metadata review command. Each selected paper +/// keeps an attachment list, even when no text was captured. Exact response bodies +/// preserve missing, empty and partially indexed content without interpreting it. +/// The serialized output is limited to 16 MiB, including JSON escaping; an +/// oversized view fails without truncation. Callers must separately bound private +/// capture-file deserialization before passing the restored capture here. +/// /// The returned JSON is not a decision patch or an approval. It must not replace /// the original report or worksheet, and it is rejected by legacy apply commands. pub fn build_full_text_review_json( diff --git a/docs/CONTEXT_MAP.md b/docs/CONTEXT_MAP.md index 86bc97f4..0a9ea806 100644 --- a/docs/CONTEXT_MAP.md +++ b/docs/CONTEXT_MAP.md @@ -10,6 +10,8 @@ Research Intake also owns the optional private Full-Text Capture bound to a metadata report. It preserves provider observations while rejecting mixed-origin counters as a reliable incremental cursor. This is an adapter responsibility, not a new bounded context, shared catalog or approval owner; downstream proposal adoption remains a separate evidence/review transition. +The Full-Text Review View is an in-context read projection over that verified capture and the current pending worksheet. It introduces no new aggregate or authority owner. Governance cannot accept it through the existing metadata-only patch path; full-text decision provenance needs a subsequent explicit contract. + ## External relationships - Zotero Local API -> research evidence intake: **Anti-Corruption Layer into Semantic Discovery**. Zotero remains the bibliographic system of record; ConceptWeave consumes a version-pinned snapshot and emits proposal evidence. Execute-mode metadata changes cross only a caller-owned authenticated adapter after complete preflight; ConceptWeave retains no API key and records verified item-level outcomes and rollback coordinates. Item metadata, attachments, collection/tag truth, and write authority remain in Zotero. No Zotero record becomes semantic authority without ConceptWeave validation/review/publication. diff --git a/docs/PRD.md b/docs/PRD.md index 518460cc..3c3c7013 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -60,6 +60,8 @@ Read one immutable Zotero Local API library-version snapshot and propose exactly Full-text enrichment must distinguish listed attachments, returned nonempty text, complete or partial indexing, and reviewed meaning. Missing abstracts or unavailable text never remove papers from the campaign denominator or prove irrelevance. Newly retrieved text requires its own immutable evidence capture and renewed review of any changed proposal; it cannot silently replace evidence beneath an earlier approval. The [full-text audit](doctoring/zotero_fulltext_contract_audit.md) establishes availability only. A separate proposed local capture now preserves the observed text for later review, with missing material still visible. Retained text is neither completed classification nor approved meaning. +Stewards must be able to inspect saved text alongside the next pending review rows without rereading Zotero or rewriting the report. A separate private evidence view binds the capture, original report and unchanged proposals, includes only those rows' attachment content, and retains missing and partial material explicitly. If the complete view exceeds its size limit, generation fails instead of truncating papers or text. Generating or reading this view does not fill decisions. Existing metadata-only apply commands must reject it; a later full-text decision path must carry its reviewed evidence identity into finalization and independently verified approval. + For every connected duplicate component, accept externally verified steward decisions selecting one component-level canonical item. Produce a local-only manifest that binds the decisions to the raw snapshot, its complete item-key/item-version coordinates, and exact duplicate-candidate membership, and records every component source revision plus before, after, and rollback canonical-key mappings. Classification preserves every Zotero source record. 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. diff --git a/docs/TRD.md b/docs/TRD.md index 2e64d85b..9fe1cfa0 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -69,6 +69,12 @@ The artifact records the complete metadata-report digest, metadata snapshot dige Capture limits are 8 MiB per body, 256 MiB cumulative body bytes, the existing 50,000-item ceiling and a five-minute monotonic admission/completion budget. Request timeouts retain the local adapter's 30-second global / 2-second connect / 10-second response and body bounds; these are not model timeouts. A request already admitted can finish after the sweep deadline, but no late result is accepted. The writer streams JSON into a create-new `0600` temp file and removes a failed partial write. Raw response bytes are bounded separately from serialized file size, which can expand through JSON escaping. The 16 MiB private review-input reader is only used for the metadata report; it is not advertised as a large-capture reader. In-memory replay callers must bound private file deserialization separately. The capture remains a proposed local capability pending protected integration and review, with no change to proposal/decision/approval counts. +The proposed offline `--full-text-review /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/CAPTURE.json LIMIT /tmp/VIEW.json` reads the original report, current worksheet and retained capture as distinct owner-only file identities. The capture has a separate 512 MiB serialized-file admission ceiling and buffered bounded deserialization; report and worksheet retain their existing 16 MiB ceiling. This file ceiling is not the capture's 256 MiB cumulative response-body budget and does not imply every JSON-escaped valid capture will fit. Existing canonical-parent, no-follow, single-link, exact-`0600` checks and create-new output semantics remain required. + +`build_full_text_review_json` reuses canonical pending-batch selection and verifies the complete capture against the report before projecting any text. Its non-flattened, Serialize-only `full_text_review_view_v1` envelope records the capture digest, metadata-report digest, current proposal digest, full bibliographic denominator, nested pending batch and attachment evidence keyed by the selected bibliographic parent. Every selected parent has an entry, including an empty list when the capture has no associated attachment. Only verified direct-parent attachments enter that entry; unrelated or standalone attachments remain in the original capture and are not copied into the view. Metadata item versions and opaque content versions remain separate. Raw content response bodies preserve 404, empty text, partial/unknown index counters and unknown provider fields without inferring relevance or completeness. + +Output serialization uses a fixed 16 MiB byte slice, so JSON escaping cannot silently grow the buffer; overflow fails before file creation and never emits a truncated view. Original artifacts and proposal/decision counts remain unchanged. The envelope cannot deserialize as `StewardReviewBatch` or `StewardDecisionPatch`; no conversion API is provided. Deliberately extracting its nested metadata batch does not preserve full-text review provenance. Carrying a review-context digest through decision application, worksheet finalization and a freshly verified approval remains a required follow-up, not a capability of this read-only view. + `conceptweave-zotero` reads only the loopback Local API with at most 100 records per page, an 8 MiB page-body limit, a 50,000-item whole-snapshot limit, a 256 MiB cumulative body limit, redirects disabled, and finite connect/response/body/global timeouts. Every request pins `Zotero-API-Version: 3`; every response must report API version 3, while its schema version is recorded and must remain stable across the snapshot. Before another request is issued, exhausted whole-snapshot budgets fail closed. Before a parsed page is accumulated, checked item-count and byte arithmetic must remain within both the advertised total and the configured whole-snapshot budgets. `Total-Results`, `Last-Modified-Version`, Zotero version, and server identity must remain identical across all pages; contract drift, malformed JSON, an empty intermediate page, duplicate keys, or an oversized response fails the run. Every top-level bibliographic record receives exactly one proposed disposition. `NeedsStewardReview` also records a deterministic abstention reason so missing classification metadata, vocabulary unsupported by the current deterministic rules, and present-but-unmatched metadata are distinguishable. Its nonempty abstract is retained exactly once in the local report: an abstract that triggered conflicting rules remains in matched evidence, while other abstention abstracts use the review-only field. Non-abstained items omit that field. DOI duplicate identity normalization treats bare DOI values, `doi:`, `doi.org`, and legacy `dx.doi.org` resolver forms as the same identity when their normalized DOI is equal. diff --git a/docs/UBIQUITOUS_LANGUAGE.md b/docs/UBIQUITOUS_LANGUAGE.md index c59a9b74..fdd347ed 100644 --- a/docs/UBIQUITOUS_LANGUAGE.md +++ b/docs/UBIQUITOUS_LANGUAGE.md @@ -4,6 +4,7 @@ | --- | --- | | Source Snapshot | Immutable revision of source evidence observed by ConceptWeave. | | Full-Text Capture | Separate private record of exact text/metadata responses, missing results and read interval, bound to an earlier metadata report; not an atomic Source Snapshot or an Authority Receipt. | +| Full-Text Review View | Bounded private projection of verified captured text beside current pending metadata decisions; keeps missing evidence visible and is neither an applicable decision patch nor an Authority Receipt. | | Observation | Deterministically extracted fact from a Source Snapshot. | | Evidence Reference | Stable source identity, digest, and location supporting a candidate. | | Semantic Candidate | Evidence-bound proposal for a concept, relation, constraint, dimension, measure, or physical mapping. | diff --git a/docs/UML.md b/docs/UML.md index 28d5ba38..97fa9a5f 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -68,6 +68,13 @@ sequenceDiagram Note over Report,Capture: non-atomic observation; no changed proposal or approval end Intake->>Report: derive snapshot-bound decision worksheet without bibliographic text + opt inspect retained text for pending rows + Report->>Intake: original report and current worksheet + Capture->>Intake: separately bounded private capture + Intake->>Intake: verify capture binding and select canonical pending rows + Intake-->>Steward: create-new bounded evidence view with missing text visible + Note over Intake,Steward: read-only view; legacy apply commands reject it + end Report->>Steward: review dispositions and merge candidates Steward->>Intake: save partially completed worksheet Intake->>Report: validate original binding; emit aggregate progress only diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index 94918420..ae94bd4c 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -47,6 +47,16 @@ No dedicated utility repository or Zotero mutation path is created. A future Zot ## Consequences +### 2026-09-05 private full-text review view amendment (Proposed) + +In the context of stewards inspecting retained text for pending papers, facing a metadata-only decision and approval chain that cannot retain a later capture's identity, we decided for a separate verified, bounded evidence view and against enriching the earlier report or silently converting the view into a legacy decision patch, to preserve source and review boundaries while making the saved text inspectable, accepting that full-text decision application and approval binding still require a subsequent contract change. + +The view combines the original report, current pending worksheet and exact capture through existing validation, without new proposals or authority. It keeps capture/report/proposal digests distinct, includes only selected parents' exact attachment content/status/version observations, and keeps all bibliographic items in the campaign denominator. A missing attachment list, 404 response, empty content or incomplete counter is evidence for review, never a reason to invent a label or exclude a paper. Standalone or unrelated attachment text is not duplicated into another paper's view. + +We choose a non-flattened, Serialize-only envelope so the existing strict batch and patch parsers reject it. Copying its nested metadata batch manually cannot substantiate full-text-reviewed decisions; the next decision-application contract must retain review-context identity through worksheet/finalization and require fresh external approval. Regenerating all proposals was rejected because this step changes access to evidence, not predictions. New domain aggregates, a database, an assignment service and a Utility Repository were rejected because there is still one local Research Intake consumer. + +The larger capture reader preserves the existing private-file identity checks and adds a separate 512 MiB file ceiling without raising the 16 MiB report/worksheet limit. A fixed-slice standard-library writer bounds the complete output to 16 MiB, including escaping, without a custom writer abstraction. Positive consequences are repeatable evidence inspection and no full-capture-sized serialization buffer. Negative consequences are another sensitive retained file, a bounded output buffer, and possible rejection of an otherwise valid large capture or review view. Overflow must be reported without truncation; the original capture remains available and papers remain pending. No UI, model call, publication, approved write or protected release is implied. Status remains Proposed. + ### 2026-09-05 full-text evidence amendment (Proposed) In the context of reviewing papers with missing abstracts, facing a full-text API whose observed versions mix sync and local writes and whose list lacks the documented version header, we decided for separately captured, content-bound full-text observations and against treating attachment listings or unchanged version counters as complete snapshot evidence, to preserve review provenance and the full campaign denominator, accepting another capture/verification step and no current claim of atomic full-text enrichment. From 045031cf09058da967c4f45e6820ace8daa68cc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:36:13 +0900 Subject: [PATCH 07/18] feat(zotero): add bounded private full-text review CLI --- .../src/full_text_review_cli_reader_tests.rs | 70 +++++++++ crates/conceptweave-zotero/src/main.rs | 136 ++++++++++++++++-- .../tests/full_text_review_cli.rs | 2 + 3 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs diff --git a/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs b/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs new file mode 100644 index 00000000..0df5ec29 --- /dev/null +++ b/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs @@ -0,0 +1,70 @@ +use super::*; + +#[test] +fn capture_reader_accepts_exact_budget_without_truncating_trailing_bytes() { + let bytes = b"{\"synthetic\":true} "; + let length = bytes.len() as u64; + let parsed: serde_json::Value = + read_bounded_capture(&mut bytes.as_slice(), length, length).unwrap(); + assert_eq!(parsed, serde_json::json!({"synthetic":true})); + let parsed: serde_json::Value = + read_bounded_capture(&mut bytes.as_slice(), length, length + 1).unwrap(); + assert_eq!(parsed, serde_json::json!({"synthetic":true})); +} + +#[test] +fn capture_reader_rejects_advertised_oversize_growth_and_shrink() { + let bytes = b"{} "; + for (advertised, limit, expected) in [ + (6, 5, "full-text capture input exceeds the file size limit"), + ( + 4, + 4, + "full-text capture input grew beyond the file size limit", + ), + (4, 5, "full-text capture input changed size while reading"), + (6, 6, "full-text capture input changed size while reading"), + ] { + let error = + read_bounded_capture::(&mut bytes.as_slice(), advertised, limit) + .unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + assert_eq!(error.to_string(), expected); + } +} + +#[test] +fn capture_reader_sanitizes_json_and_io_failures() { + for bytes in [ + b"{\"private-sentinel\":".as_slice(), + b"{} private-sentinel", + b"\xff", + b"", + ] { + let error = + read_bounded_capture::(&mut bytes.as_ref(), bytes.len() as u64, 64) + .unwrap_err(); + assert_eq!(error.to_string(), "full-text capture input is invalid"); + } + struct FailedReader; + impl Read for FailedReader { + fn read(&mut self, _: &mut [u8]) -> io::Result { + Err(io::Error::other("private-sentinel")) + } + } + let error = read_bounded_capture::(&mut FailedReader, 0, 64).unwrap_err(); + assert_eq!(error.to_string(), "full-text capture input is invalid"); +} + +#[test] +fn private_capture_reader_reuses_regular_owner_only_file_boundary() { + let file_path = tests::unique_temp_path("streaming-capture-reader"); + let file = create_report_file(&file_path).unwrap(); + drop(file); + let error = match read_private_capture(file_path.to_str().unwrap()) { + Ok(_) => panic!("empty capture must be rejected"), + Err(error) => error, + }; + assert_eq!(error.to_string(), "full-text capture input is invalid"); + fs::remove_file(file_path).unwrap(); +} diff --git a/crates/conceptweave-zotero/src/main.rs b/crates/conceptweave-zotero/src/main.rs index 48c95e40..11cf5ba6 100644 --- a/crates/conceptweave-zotero/src/main.rs +++ b/crates/conceptweave-zotero/src/main.rs @@ -2,21 +2,23 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] use conceptweave_zotero::{ - ClassificationReport, GoldenSetApproval, MAX_REVIEW_BATCH_ITEMS, StewardDecisionPatch, - StewardReviewBatch, StewardReviewWorksheet, apply_steward_decision_patch, - assess_steward_review_progress, build_steward_review_batch, build_steward_review_worksheet, - decision_patch_from_review_batch, read_local_full_text, read_local_snapshot, - reviewed_golden_set_from_worksheet, + ClassificationReport, FullTextCapture, GoldenSetApproval, MAX_REVIEW_BATCH_ITEMS, + StewardDecisionPatch, StewardReviewBatch, StewardReviewWorksheet, apply_steward_decision_patch, + assess_steward_review_progress, build_full_text_review_json, build_steward_review_batch, + build_steward_review_worksheet, decision_patch_from_review_batch, read_local_full_text, + read_local_snapshot, reviewed_golden_set_from_worksheet, }; use serde::de::DeserializeOwned; use std::collections::BTreeSet; use std::env; use std::fs::{self, File, OpenOptions}; -use std::io::{self, BufWriter, Read, Write}; +use std::io::{self, BufReader, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; -const USAGE: &str = "usage: conceptweave-zotero /tmp/REPORT.json | --capture-full-text /tmp/REPORT.json /tmp/CAPTURE.json | --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json | --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json | --review-batch /tmp/REPORT.json /tmp/CURRENT_WORKSHEET.json LIMIT /tmp/BATCH.json | --apply-review-batch /tmp/REPORT.json /tmp/CURRENT_WORKSHEET.json /tmp/COMPLETED_BATCH.json /tmp/UPDATED_WORKSHEET.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 USAGE: &str = "usage: conceptweave-zotero /tmp/REPORT.json | --capture-full-text /tmp/REPORT.json /tmp/CAPTURE.json | --full-text-review /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/CAPTURE.json LIMIT /tmp/VIEW.json | --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json | --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json | --review-batch /tmp/REPORT.json /tmp/CURRENT_WORKSHEET.json LIMIT /tmp/BATCH.json | --apply-review-batch /tmp/REPORT.json /tmp/CURRENT_WORKSHEET.json /tmp/COMPLETED_BATCH.json /tmp/UPDATED_WORKSHEET.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; +// JSON escaping and envelope bytes are separate from the capture's raw-body budget. +const MAX_CAPTURE_FILE_BYTES: u64 = 512 * 1024 * 1024; #[derive(Debug, PartialEq, Eq)] enum OutputRequest { @@ -25,6 +27,13 @@ enum OutputRequest { report: String, output: String, }, + FullTextReview { + report: String, + worksheet: String, + capture: String, + limit: usize, + output: String, + }, Worksheet { report: String, worksheet: String, @@ -79,6 +88,41 @@ where return Err("full-text report and output paths must differ"); } OutputRequest::FullTextCapture { report, output } + } else if first == "--full-text-review" { + let report = args + .next() + .ok_or("full-text review mode requires a report path")?; + let worksheet = args + .next() + .ok_or("full-text review mode requires a worksheet path")?; + let capture = args + .next() + .ok_or("full-text review mode requires a capture path")?; + let limit = args + .next() + .ok_or("full-text review mode requires a limit")?; + let output = args + .next() + .ok_or("full-text review mode requires an output path")?; + if limit.is_empty() || !limit.bytes().all(|byte| byte.is_ascii_digit()) { + return Err("full-text review limit must be an unsigned decimal integer"); + } + let limit: usize = limit + .parse() + .map_err(|_| "full-text review limit is out of range")?; + if !(1..=MAX_REVIEW_BATCH_ITEMS).contains(&limit) { + return Err("full-text review limit must be between 1 and 100"); + } + if BTreeSet::from([&report, &worksheet, &capture, &output]).len() != 4 { + return Err("full-text review paths must be distinct"); + } + OutputRequest::FullTextReview { + report, + worksheet, + capture, + limit, + output, + } } else if first == "--worksheet" { let report = args .next() @@ -246,6 +290,19 @@ struct ArtifactIdentity { /// Opens, validates, bounds, and deserializes one owner-only review artifact. fn read_private_json(raw: &str) -> io::Result<(T, ArtifactIdentity)> { + read_private_input(raw, |file, length| read_bounded_json(file, length)) +} + +fn read_private_capture(raw: &str) -> io::Result<(FullTextCapture, ArtifactIdentity)> { + read_private_input(raw, |file, length| { + read_bounded_capture(file, length, MAX_CAPTURE_FILE_BYTES) + }) +} + +fn read_private_input( + raw: &str, + parse: impl FnOnce(&mut File, u64) -> io::Result, +) -> io::Result<(T, ArtifactIdentity)> { let path = PathBuf::from(raw); if !path.is_absolute() { return Err(io::Error::new( @@ -277,7 +334,7 @@ fn read_private_json(raw: &str) -> io::Result<(T, ArtifactI let (file, opened_metadata) = open_with_metadata(&validated_path)?; #[cfg(not(unix))] { - let _ = (path_metadata, opened_metadata, file); + let _ = (path_metadata, opened_metadata, file, parse); return Err(io::Error::new( io::ErrorKind::Unsupported, "private review input requires a Unix platform", @@ -287,7 +344,7 @@ fn read_private_json(raw: &str) -> io::Result<(T, ArtifactI let identity = validate_opened_identity(&path_metadata, &opened_metadata)?; #[cfg(unix)] { - let parsed = read_bounded_json(&mut { file }, opened_metadata.len())?; + let parsed = parse(&mut { file }, opened_metadata.len())?; Ok((parsed, identity)) } } @@ -353,6 +410,42 @@ fn read_bounded_json( .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)) } +/// Parses capture JSON through a fixed buffer and rejects size drift or trailing data. +fn read_bounded_capture( + reader: &mut dyn Read, + advertised_len: u64, + file_limit: u64, +) -> io::Result { + if advertised_len > file_limit { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "full-text capture input exceeds the file size limit", + )); + } + let mut bounded = reader.take(file_limit + 1); + // from_reader checks the complete stream, including trailing whitespace. + let parsed = serde_json::from_reader(BufReader::new(&mut bounded)); + if bounded.limit() == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "full-text capture input grew beyond the file size limit", + )); + } + let parsed = parsed.map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "full-text capture input is invalid", + ) + })?; + if file_limit + 1 - bounded.limit() != advertised_len { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "full-text capture input changed size while reading", + )); + } + Ok(parsed) +} + /// Preserves an input error kind while naming the rejected artifact. fn label_input(name: &str, error: io::Error) -> io::Error { io::Error::new(error.kind(), format!("{name}: {error}")) @@ -482,6 +575,26 @@ fn create_report_file_with( /// Reads one Zotero snapshot and writes its sensitive local proposal report. fn main() -> Result<(), Box> { match parse_output_request(env::args().skip(1))? { + OutputRequest::FullTextReview { + report, + worksheet, + capture, + limit, + output, + } => { + let output_path = 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 (capture, capture_identity) = + read_private_capture(&capture).map_err(|error| label_input("capture", error))?; + if BTreeSet::from([report_identity, worksheet_identity, capture_identity]).len() != 3 { + return Err("full-text review inputs must be distinct files".into()); + } + let content = build_full_text_review_json(&report, &worksheet, &capture, limit)?; + write_private_output(&output_path, &content)?; + } OutputRequest::FullTextCapture { report, output } => { let output = validate_output_path(&output)?; let (report, _): (ClassificationReport, _) = @@ -633,6 +746,9 @@ fn main() -> Result<(), Box> { Ok(()) } +#[cfg(test)] +mod full_text_review_cli_reader_tests; + #[cfg(test)] mod tests { use super::*; @@ -1122,7 +1238,7 @@ mod tests { fs::remove_file(read_only).unwrap(); } - fn unique_temp_path(suffix: &str) -> PathBuf { + pub(super) fn unique_temp_path(suffix: &str) -> PathBuf { env::temp_dir().join(format!( "conceptweave-zotero-{}-{suffix}.json", std::process::id() diff --git a/crates/conceptweave-zotero/tests/full_text_review_cli.rs b/crates/conceptweave-zotero/tests/full_text_review_cli.rs index cdd4b08f..cb419e03 100644 --- a/crates/conceptweave-zotero/tests/full_text_review_cli.rs +++ b/crates/conceptweave-zotero/tests/full_text_review_cli.rs @@ -185,11 +185,13 @@ fn full_text_review_cli_capture_file_budget_is_separate_from_metadata_budget() { assert!(inputs.run("1", &output_path).status.success()); fs::remove_file(&output_path).unwrap(); for metadata_path in [&inputs.report_path, &inputs.worksheet_path] { + let original = fs::read(metadata_path).unwrap(); let file = OpenOptions::new().write(true).open(metadata_path).unwrap(); file.set_len(16 * 1024 * 1024 + 1).unwrap(); drop(file); assert!(!inputs.run("1", &output_path).status.success()); assert!(!output_path.exists()); + fs::write(metadata_path, original).unwrap(); } } From ed308bdc914fd18e408b835026b270eb9aebaf7c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:38:22 +0900 Subject: [PATCH 08/18] test(zotero): keep streaming reader checks warning-free --- .../src/full_text_review_cli_reader_tests.rs | 7 +++---- crates/conceptweave-zotero/src/main.rs | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs b/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs index 0df5ec29..b3343df6 100644 --- a/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs +++ b/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs @@ -35,15 +35,14 @@ fn capture_reader_rejects_advertised_oversize_growth_and_shrink() { #[test] fn capture_reader_sanitizes_json_and_io_failures() { - for bytes in [ + for mut bytes in [ b"{\"private-sentinel\":".as_slice(), b"{} private-sentinel", b"\xff", b"", ] { - let error = - read_bounded_capture::(&mut bytes.as_ref(), bytes.len() as u64, 64) - .unwrap_err(); + let length = bytes.len() as u64; + let error = read_bounded_capture::(&mut bytes, length, 64).unwrap_err(); assert_eq!(error.to_string(), "full-text capture input is invalid"); } struct FailedReader; diff --git a/crates/conceptweave-zotero/src/main.rs b/crates/conceptweave-zotero/src/main.rs index 11cf5ba6..9ebfa917 100644 --- a/crates/conceptweave-zotero/src/main.rs +++ b/crates/conceptweave-zotero/src/main.rs @@ -293,12 +293,14 @@ fn read_private_json(raw: &str) -> io::Result<(T, ArtifactI read_private_input(raw, |file, length| read_bounded_json(file, length)) } +/// Restores a private capture without allocating a second full-file byte buffer. fn read_private_capture(raw: &str) -> io::Result<(FullTextCapture, ArtifactIdentity)> { read_private_input(raw, |file, length| { read_bounded_capture(file, length, MAX_CAPTURE_FILE_BYTES) }) } +/// Shares the validated, single-open file boundary across bounded artifact parsers. fn read_private_input( raw: &str, parse: impl FnOnce(&mut File, u64) -> io::Result, From 26a3e6a4cbc88f97316d852fed5f4e1fb6e1510f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:40:42 +0900 Subject: [PATCH 09/18] test(zotero): verify streaming read ceiling without unreachable arm --- .../src/full_text_review_cli_reader_tests.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs b/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs index b3343df6..e8c55ce7 100644 --- a/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs +++ b/crates/conceptweave-zotero/src/full_text_review_cli_reader_tests.rs @@ -31,6 +31,9 @@ fn capture_reader_rejects_advertised_oversize_growth_and_shrink() { assert_eq!(error.kind(), io::ErrorKind::InvalidData); assert_eq!(error.to_string(), expected); } + let mut stream = io::Cursor::new(b"{} "); + assert!(read_bounded_capture::(&mut stream, 4, 4).is_err()); + assert_eq!(stream.position(), 5); } #[test] @@ -60,10 +63,9 @@ fn private_capture_reader_reuses_regular_owner_only_file_boundary() { let file_path = tests::unique_temp_path("streaming-capture-reader"); let file = create_report_file(&file_path).unwrap(); drop(file); - let error = match read_private_capture(file_path.to_str().unwrap()) { - Ok(_) => panic!("empty capture must be rejected"), - Err(error) => error, - }; + let error = read_private_capture(file_path.to_str().unwrap()) + .err() + .unwrap(); assert_eq!(error.to_string(), "full-text capture input is invalid"); fs::remove_file(file_path).unwrap(); } From a1b4939a43eabb78f1493b25644c594d31893163 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:53:19 +0900 Subject: [PATCH 10/18] docs: record offline full-text review evidence and live review gates --- .../zotero_fulltext_contract_audit.md | 12 ++++- .../zotero_fulltext_review_evidence.json | 52 +++++++++++++++++++ docs/product-technical-gap-baseline.md | 30 +++++++++-- 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 docs/doctoring/zotero_fulltext_review_evidence.json diff --git a/docs/doctoring/zotero_fulltext_contract_audit.md b/docs/doctoring/zotero_fulltext_contract_audit.md index 526e6775..e8108cd1 100644 --- a/docs/doctoring/zotero_fulltext_contract_audit.md +++ b/docs/doctoring/zotero_fulltext_contract_audit.md @@ -95,7 +95,17 @@ The capture retains 3,432 successful content responses, 41 missing responses and Response bodies total 232,366,711 bytes, including attachment metadata absent from the earlier sweep. The encoded file is 235,602,798 bytes, a new single-link `0600` file outside the repository. The production verifier checked report binding, response bounds, structure, parent/item revision and observed content versions before writing. A separate saved-file audit checked permissions, the complete artifact/evidence/report digests, parent mapping, counts and unchanged report hash without printing identities or source text. Synthetic tests independently exercise Rust deserialization/replay; the saved-file audit does not add an authority verifier. The public JSON retains only aggregate counts, times, limits and digests. No private file is published here. -The original metadata report remains byte-identical. Retained-text coverage improves from zero replayable full-text artifacts to 3,203/3,715 parents; new text-bound proposals, authentic decisions and approved labels remain zero. Follow-up work must present the captured evidence under a new proposal/review binding, preserve missing/partial coverage, and prove a released contextual-orchestrator integration before model assistance. The Zotero version-space defect remains upstream; a consumer capture does not repair it. +The original metadata report remains byte-identical. Retained-text coverage improves from zero replayable full-text artifacts to 3,203/3,715 parents; new text-bound proposals, authentic decisions and approved labels remain zero. The next section records the separate read-only review view. Follow-up decision and approval work must retain its exact context, preserve missing/partial coverage, and prove a released contextual-orchestrator integration before model assistance. The Zotero version-space defect remains upstream; a consumer capture does not repair it. + +### Follow-up: offline inspection of the first 25 pending papers + +The [review-view evidence](zotero_fulltext_review_evidence.json) is bound to `54383eae2e83863c4cb72ee00f16cd504ff66151`. The release-profile Rust executable restored the existing private report, worksheet and 235,602,798-byte capture without another provider request. It validated the complete capture before selecting the canonical 25 pending rows. Those rows match the earlier metadata-only batch exactly; 21 have nonempty retained text and four have none. All 21 copied content responses are HTTP 200, with 16 complete and five unknown index-counter results under the predicate above. These are this batch's counts, not a new full-library or abstract-missing-subgroup measurement. + +The create-new single-link `0600` view is 1,590,742 bytes. A separate local audit recomputed capture, complete-report and proposal digests, compared every selected parent/revision/content response and confirmed the original report, worksheet, capture and batch hashes remained unchanged. The command took 1.60 seconds with maximum resident memory 288,014,336 bytes and peak memory footprint 286,966,336 bytes. The saved-file audit is outside that command measurement; neither constitutes a load benchmark. The public record includes aggregates only, never bibliography, item identities or raw text. + +The view retains the original metadata proposal digest and a separate capture binding. Unchanged predictions do not require a fresh proposal merely to display additional evidence. Its outer versioned envelope is intentionally not accepted by either existing decision-application command. Full-text evidence binding through decision application, worksheet history, finalization and external approval remains unimplemented; stripping the envelope does not preserve that provenance. Inspectable captured-text rows improve from zero to 25 while remaining decisions and externally approved labels stay 3,715 and zero respectively. + +The implementation reuses the canonical pending selector and capture verifier, borrows retained responses during projection, and writes into the standard library's fixed-size slice-backed cursor. It adds no service, dependency, mutable report fields or duplicate security helper. The separately bounded buffered capture reader uses pinned `serde_json` 1.0.151; installed source confirms whole-stream completion checks and the caller separately checks the actual byte count. Metadata and output remain bounded at 16 MiB, capture files at 512 MiB, and raw response bodies at the existing 256 MiB cumulative bound. High JSON escaping can exceed a file/output ceiling without exceeding the raw-body ceiling; such input fails without truncation. The Proposed [ADR 0006 amendment](../adr/0006-zotero-research-intake.md) records alternatives, consequences and the outstanding approval contract. ## Consumer transport and replay root-cause repairs diff --git a/docs/doctoring/zotero_fulltext_review_evidence.json b/docs/doctoring/zotero_fulltext_review_evidence.json new file mode 100644 index 00000000..449b0c6e --- /dev/null +++ b/docs/doctoring/zotero_fulltext_review_evidence.json @@ -0,0 +1,52 @@ +{ + "observation_kind": "private_offline_fulltext_review_view", + "observed_at": "2026-09-05T10:45:01.627Z", + "source_commit": "54383eae2e83863c4cb72ee00f16cd504ff66151", + "view_kind": "full_text_review_view_v1", + "command_elapsed_seconds": 1.60, + "maximum_resident_set_bytes": 288014336, + "peak_memory_footprint_bytes": 286966336, + "view_file_bytes": 1590742, + "view_file_mode": "0600", + "view_file_links": 1, + "view_file_sha256": "e99a1963f3b5d7adfb62070785f2b69f1d9efd1d4882d365a5ca6f6b8d70f34a", + "capture_file_sha256": "56d385398c8da559aa597a4e3783d946638855bba19ac808ce81d917bf06f94d", + "capture_digest": "sha256:429d98dc90e172b4f0bb4e3e1c493feb33b61664793d02a53d8183fb76f76a50", + "metadata_report_digest": "sha256:3c7b83647ed3b567584cea6a784e40c97c67c9c5aac5623fbf15b3a10cfaee52", + "proposal_digest": "sha256:f3b524e0ff30ea851fd1529335f6637954e1e79c1ea230b0bb69bdc4302d246c", + "bibliographic_denominator": 3715, + "review_view_rows": 25, + "remaining_decisions": 3715, + "attachment_count": 21, + "content_response_counts": { "200": 21 }, + "nonempty_text_parent_count": 21, + "missing_nonempty_text_parent_count": 4, + "empty_content_count": 0, + "index_complete_count": 16, + "index_partial_count": 0, + "index_unknown_count": 5, + "limits": { + "metadata_file_bytes": 16777216, + "capture_file_bytes": 536870912, + "review_output_bytes": 16777216, + "maximum_review_rows": 100 + }, + "verification": { + "production_capture_verifier_before_projection": true, + "independent_saved_json_capture_report_proposal_digests": true, + "exact_parent_versions_and_content_verified": true, + "canonical_original_batch_identical": true, + "original_report_worksheet_capture_batch_unchanged": true, + "raw_text_retained_privately": true, + "raw_text_committed": false, + "review_context_bound_approval_contract": false, + "atomic_provider_snapshot": false, + "authenticated_provider": false + }, + "new_text_bound_proposals": 0, + "steward_decisions": 0, + "approved_labels": 0, + "zotero_requests": 0, + "model_requests": 0, + "source_mutations": 0 +} diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index f48a2e2f..a0902963 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -11,10 +11,10 @@ Protected/default `main` remains `f4f440dd58c77d7cd90dff8a1eb2eeb9a9940425`; onl The active roots observed immediately before this baseline refresh are: 1. Foundation PR #1 — exact head `b538470c963e6524ddc0c3f652a46a4fc8265150`, Draft/open. The current Foundation makes Product CI draft-aware while keeping Ready/non-Draft quality requirements intact. Product CI still cannot materialize from protected `main` because that branch does not yet contain `.github/workflows/product.yml`. -2. Product-CI bootstrap PR #35 — exact head `a31ae0c2df920f2794f7ddb456795b04797ab472`, open/non-Draft. It adds the pull-request form of Product CI and removes no-op closed/converted-to-Draft triggers. Scope detection and review admission have executed successfully, while CodeQL, Semgrep, Noema, Strix, Trivy and Scorecard remain queued. The workflow-only diff skips Dependency Review and OSV; these skips do not prove Foundation's dependency-changing checks. No independent approval exists for this head. +2. Product-CI bootstrap PR #35 — exact head `a31ae0c2df920f2794f7ddb456795b04797ab472`, open/non-Draft. It adds the pull-request form of Product CI and removes no-op closed/converted-to-Draft triggers. At 10:46–10:48 UTC, Semgrep, Noema execution, admission, Trivy and Scorecard had completed successfully, coverage was queued and Strix running. CodeQL's new red dispatch job handed off to a queued successor; this is not a completed scan verdict. Noema submitted CHANGES_REQUESTED for an unsupported-flag claim contradicted by pinned Cargo help and official documentation, as detailed below. The workflow-only diff skips Dependency Review and OSV; these skips do not prove Foundation's dependency-changing checks. No independent approval exists for this head. 3. Client Consumption PR #5 — exact head `fcf36c8a99f015b963c9f812787df127ac2e2f9e`, Draft/open. The current source retains language-neutral semantic-release admission, integrity, compatibility, diff/resolution and supersession validation. Previously valid review findings are source-repaired, but current protected evidence remains independently required. 4. Source Observation PR #6 — exact head `c362a73403b6bda2cc0e94de913e39f3139d6205`, Draft/open. Its independent owner preserves registry denial before an authorized-request-only adapter boundary. The counted regression now verifies zero adapter/source/snapshot executions on denial and one of each for an authorized control, retaining the existing denial result. This audit checked source and formatting, not that branch's runtime or coverage. The new submitted repair report is COMMENTED, not approval; current-head Actions/check-runs were absent. The concrete bounded read-only PostgreSQL adapter remains absent. -5. Zotero Research Classification root PR #9 — exact head `a2a84884f67dcac6f6892c958d55450aea6d6c88`, Draft/open. A minimal owner backport reproduces and repairs proxy inheritance and exact-byte-limit rejection without bringing later full-text features backward. Integrity root #10 was `e7d4e59f1b55b5954c5f8436527bc96e7ef2fb13`; all 23 descendants inherited that earlier repair through ordinary merges. The transport cascade now reaches review-batch PR #34 at `b0119a57047e7b1fe5ddfbbf4b973de0f15de172`, preserving its original `2e6448e896e65562ebeee2fd339dec64d9fdf6e5` and every intermediate delta. [Full-text capture PR #36](https://github.com/ContextualWisdomLab/ConceptWeave/pull/36) integrates that parent at locally verified merge `75da75cf01704d9aae47f1e5573e3bbe3fb42bb0`; its subsequent documentation head must be refreshed separately. Before that push, the remote remained Draft at `e19d95f42c0f745cb428133ee7c4a15043e76744`, with only an explicitly skipped CodeRabbit Draft review and no submitted review or hosted Product verification. The stack remains proposal/review oriented and does not elevate local classifier output to semantic authority. +5. Zotero Research Classification root PR #9 — exact head `a2a84884f67dcac6f6892c958d55450aea6d6c88`, Draft/open. A minimal owner backport reproduces and repairs proxy inheritance and exact-byte-limit rejection without bringing later full-text features backward. Integrity root #10 was `e7d4e59f1b55b5954c5f8436527bc96e7ef2fb13`; all 23 descendants inherited that earlier repair through ordinary merges. The transport cascade now reaches review-batch PR #34 at `b0119a57047e7b1fe5ddfbbf4b973de0f15de172`, preserving its original `2e6448e896e65562ebeee2fd339dec64d9fdf6e5` and every intermediate delta. [Full-text capture PR #36](https://github.com/ContextualWisdomLab/ConceptWeave/pull/36) integrates that parent at locally verified merge `75da75cf01704d9aae47f1e5573e3bbe3fb42bb0`; the 10:46–10:48 UTC audit confirmed its remote documentation head `1e7d23c91116d84a455b6e6e5a6fb00a5e004c04` and actual named base `b0119a5`. It remains Draft, with an explicitly skipped CodeRabbit review and no hosted Product verification or approval. The new private-review-view work below is a child delta, not a replacement or closure of this prerequisite. The stack remains proposal/review oriented and does not elevate local classifier output to semantic authority. Predecessor reviews/checks never transfer to successor heads. No force-push, destructive rebase, self-approval, fail-open scanner substitution or routine administrator bypass is acceptance evidence. @@ -28,7 +28,7 @@ Predecessor reviews/checks never transfer to successor heads. No force-push, des | Client Consumption | ACTIVE_CHILD | Offline Published+Authoritative admission, compatibility, exact resolution/diff, canonical digest verification, detached artifact verification and explicit supersession validation exist. Current exact-head protected evidence and prerequisite integration remain outstanding. | | Quality gate | ACTIVE_PR | Rust 1.98.0, unsafe forbidden, public docs required, exact checkout, fmt, Clippy, tests, rustdoc, owned 100% coverage, Draft-2020-12 schema fixtures, lock freshness and clean-tree checks. Every head movement requires fresh exact-head evidence. | | Security / dependency review | CONSUMER_REVALIDATION_PENDING | The earlier public non-fork exact-range HTTP 403 was traced to an uninitialized repository dependency graph, not to a retryable central workflow defect. `.github#1873` was closed unmerged after enabling Dependabot vulnerability alerts initialized affected graphs and the same exact comparison returned HTTP 200. The hard gate remains fail closed; a current ConceptWeave head must still execute the pinned Dependency Review action successfully before acceptance. | -| Review / runner admission | PENDING_CURRENT_CHECKS | #35's scope/admission jobs have executed successfully, while scanner and model-review jobs remain queued. The active rules require one approving review, resolved review threads and seven central workflows. Queueing is not a reason to stop repository-owned work. | +| Review / runner admission | PENDING_CURRENT_CHECKS | #35 has several completed execution checks but an unresolved CHANGES_REQUESTED review, a queued CodeQL successor/coverage and running Strix. The active rules require one approving review, resolved review threads and seven central workflows. Execution success does not imply review approval; queueing is not a reason to stop repository-owned work. | | Zotero Research Intake | CAMPAIGN_INCOMPLETE | PRD FR-9 and ADRs 0006/0007 have executable local proposal, review, dry-run and recovery contracts. Saved-snapshot proposals cover 3,715/3,715 bibliographic items; unverified steward decisions and externally approved labels both remain 0/3,715. See the campaign evidence below. | | Standards / research | REPAIRED_PENDING_CI | Doctoring remains bound to authoritative standards/primary research and exact implementation contracts; hosted exact-head evidence remains independently required after head changes. | | Release | NOT_STARTED | No immutable ConceptWeave release exists. Version/CHANGELOG/tag/package/semantic_release/SBOM/provenance/reproducibility/rollback are required on the exact protected release head. | @@ -43,7 +43,7 @@ Acceptance remains stricter than the RCA. HTTP 200 availability alone is not GRE ## Central control-plane evidence -Protected central source is `.github/main@8aea81323d93e90c79b71d7718de2798919fa1df` at this snapshot. This is evidence only, not a mutable ConceptWeave dependency. Five commits after the prior `6d7fbebec8aec31d88a30a36e71ca5b3925d241d` checkpoint repair admission coverage and remove two echo-only review jobs, alongside governance-documentation updates; this does not renew evidence for already-created consumer runs. +The current central branch ref is `.github/main@7fcada597d5b79bdb14445f24322b2c9f6ed4b19`, refreshed at 10:46–10:48 UTC. This is evidence only, not a mutable ConceptWeave dependency or a new audit of central protection. The earlier `8aea81323d93e90c79b71d7718de2798919fa1df` checkpoint followed admission-coverage and echo-only-review-job repairs; its nine-commit range to the current ref changes governance documents while preserving the previously read master-context and product-directive blobs. Already-created consumer runs retain their own exact workflow revisions. - The current central source includes queue/admission and changed-scope/review-runtime repairs already integrated through ordinary protected history. - `.github#1873@41935494aa234eb458f1cc08f006daaa278b9760` is closed/unmerged because repository dependency-graph initialization, not its retry/sleep source delta, was the verified root cause of the observed public-repository 403. @@ -51,6 +51,10 @@ Protected central source is `.github/main@8aea81323d93e90c79b71d7718de2798919fa1 At 2026-09-05 09:47 UTC, Foundation #1's two CodeQL failures in [run 33937211620](https://github.com/ContextualWisdomLab/ConceptWeave/actions/runs/33937211620) were verified runner-release handoffs, not observed scan findings. Both dispatches succeeded but their terminal verdicts remained pending. Exact-head successor runs [33958339895](https://github.com/ContextualWisdomLab/.github/actions/runs/33958339895) and [33958340068](https://github.com/ContextualWisdomLab/.github/actions/runs/33958340068) were queued, bound to ConceptWeave `b538470c963e6524ddc0c3f652a46a4fc8265150` and central run source `7fcada597d5b79bdb14445f24322b2c9f6ed4b19`. This audit did not independently reverify that source's branch protection. The originating job promises an exact-job rerun after the terminal verdict; no manual retry, scanner substitution or new repair was justified by the red status alone. +The 10:46–10:48 UTC paginated audit still found 30 open PRs, 29 Draft, unchanged heads and 36 existing unresolved threads. Since 10:14 UTC it found one new submitted review, no new/updated thread comments and one new terminal failure. Foundation's same two successors remain queued at attempt one. #35's 10:35:40 CodeQL failure likewise records successful dispatch with a pending verdict; its exact-head successor [33961083940](https://github.com/ContextualWisdomLab/.github/actions/runs/33961083940) is queued. No scan finding, manual retry or weakening is inferred from these handoffs. ConceptWeave's active organization [ruleset 18156473](https://github.com/ContextualWisdomLab/ConceptWeave/rules/18156473) requires one approval, stale-review dismissal, thread resolution and seven central workflows, and protects against deletion/non-fast-forward updates. A classic-protection endpoint 404 does not negate these effective rules. + +At 10:42:56 UTC, [Noema review 5120903874](https://github.com/ContextualWisdomLab/ConceptWeave/pull/35#pullrequestreview-5120903874) requested changes on #35's exact head, claiming `cargo generate-lockfile --locked` is unsupported and reporting adversarial confirmation. Fresh `cargo +1.98.0 generate-lockfile --help` exits zero and lists the flag; [Cargo's official command documentation](https://doc.rust-lang.org/cargo/commands/cargo-generate-lockfile.html#manifest-options) also documents its lock-preserving failure behavior. The consumer guard and subsequent tracked/unchanged-lockfile checks are preserved. An [evidence-based reassessment request](https://github.com/ContextualWisdomLab/ConceptWeave/pull/35#issuecomment-5551270771) was posted without dismissing the review or supplying approval. The central owner received this specimen to investigate whether model assertions were represented as executed verification; no owner diagnosis or repair is claimed yet. Flag support alone does not prove a hosted Product run or all dependency-freshness behavior. + ## Zotero research campaign evidence ### Repaired current snapshot and source verification @@ -90,7 +94,7 @@ Replayable retained-text coverage has progressed from 0 to 3,203/3,715 parents. Committed regressions repaired inherited environment proxies, exact-byte-limit rejection and replay checks occurring after digest allocation; clock fault injection verifies late/invalid-clock failures without changing the deadline. Final source verification at `733425df01511d894277fb8682e070f3dde03689` passed 173 tests across 37 suites including documentation tests, strict Clippy, formatting, rustdoc with warnings denied, the CI contract and the existing coverage gate. Coverage is 347/347 functions, 3,710/3,710 source-normalized regions and 674/674 source-normalized branch outcomes. Raw LLVM totals remain 4,159/4,255 lines, 6,129/6,274 regions and 603/674 branch outcomes; those are not 100%. The only source delta after the live run is a writer type alias resolving strict Clippy's complexity finding without changing runtime behavior. Hosted checks and independent protected approval remain separate gates. -Next: revalidate current-head protected gates after the completed transport cascade below; present retained text under new proposal/review bindings with partial/missing coverage; continue the 61 remaining repository capability audits and the upstream version-contract repair. A released contextual-orchestrator integration artifact remains unverified at the audited protected owner head, so source documentation alone does not authorize model-provider bypass. No new utility repository is justified by this one intake seam. +Next: revalidate current-head protected gates after the completed transport cascade below; carry the new read-only view's exact context into a separately verified decision and approval contract without stripping its capture binding; continue the 61 remaining repository capability audits and the upstream version-contract repair. Unchanged predictions do not require a new proposal run merely to display retained evidence. A released contextual-orchestrator integration artifact remains unverified at the audited protected owner head, so source documentation alone does not authorize model-provider bypass. No new utility repository is justified by this one intake seam. ### Canonical transport repair and released-owner audit @@ -104,6 +108,22 @@ Root integration `75da75cf01704d9aae47f1e5573e3bbe3fb42bb0` retains one identica The [released-orchestration audit](doctoring/zotero_fulltext_contract_audit.md#released-orchestration-evidence) verified no qualifying artifact or deployed gateway in the inspected channels. Its 66 deployment records include eight successes, all Provider catalog sync, not proof of a serving gateway. Existing CO [PR #1030](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/1030) owns release work and its active owner confirmed that artifact/schema/deployed-version evidence is still pending. The handoff requests that evidence without duplicate release machinery or provider bypass. New text-bound proposals and approved labels remain zero. +A 10:48 UTC CO task snapshot exposed a terminal selected-model capacity error without new release evidence. One continuation prompt kept the same task/model settings and prior scope; the subsequent compact snapshot returned active/in-progress. The CO task then clarified that its single-writer scope is integration PRs #1067/#1074, not release PR #1030, and will locate the existing release owner's exact evidence. Its reported `47ae9d65` integration change and ongoing tests are not an immutable artifact, schema digest or deployed-gateway receipt. This is a task handoff report, not independently reverified PR state. No new task, paid-model substitution or duplicate release implementation was created. + +### Capture-bound private review view + +Integrated source `54383eae2e83863c4cb72ee00f16cd504ff66151` adds an offline, read-only view of the next pending papers and their retained text. Committed core RED `0027d7d` fails one selected test against the intentional stub; GREEN `e426c2b` reuses the existing canonical batch and complete capture verifier. A prior incorrectly filtered command selected zero tests and is not RED evidence. Boundary tests `3fb930b` and `fcba236` cover changed bindings, direct-parent selection, missing/empty/partial/unknown responses, separate metadata/content versions, standalone exclusion and escaping beyond the fixed 16 MiB output. No text is silently truncated or unselected parent copied. + +CLI RED `25a1005` precedes implementation `045031c`, warning correction `ed308bd` and test-only `26a3e6a`. Ordinary merges `b86fe86` and `54383ea` preserve both development histories. The CLI shares the existing canonical-parent, single-open `O_NOFOLLOW`, identity, single-link, exact-`0600` and create-new output boundary. Metadata inputs retain their 16 MiB limit; the capture uses buffered deserialization under a separate 512 MiB file bound, including exact-boundary, size-drift, trailing-data and static-error tests. That file ceiling is not the capture's 256 MiB response-body budget and can reject an otherwise valid high-escaping capture. Both legacy decision-application modes reject the new outer view; copying out its nested metadata batch would discard full-text provenance and is not an approved conversion. + +The [saved-file evidence](doctoring/zotero_fulltext_review_evidence.json) records one real offline invocation using the original private repaired report, worksheet and full-text capture, with no Zotero or model requests. It produced 25 pending rows, 21 with nonempty text and four without it; the latter remain visible. The 21 attached responses were HTTP 200, with 16 complete and five unknown index counters under the documented predicate. The new single-link `0600` file is 1,590,742 bytes with SHA-256 `e99a1963f3b5d7adfb62070785f2b69f1d9efd1d4882d365a5ca6f6b8d70f34a`. Command elapsed time was 1.60 seconds, maximum resident memory 288,014,336 bytes and peak memory footprint 286,966,336 bytes. This is one observed local run, not a latency SLO or a load benchmark. + +An independent aggregate-only audit recomputed the saved capture, complete report and proposal digests; compared every selected parent, attachment revision, content version and raw response body with the original capture; and proved the nested batch equals the earlier metadata batch. Original report, worksheet, capture and batch file hashes remain unchanged. Inspectable captured-text review rows progressed from zero to 25; this is an evidence-access measure, not classification progress. The full denominator and remaining decisions stay 3,715, lifecycle capability metric stays 25, and new text-bound proposals, authentic steward decisions and independently approved labels stay zero. + +At `54383ea`, 201 workspace tests across 38 unfiltered suites, including three doctests, pass; the nested subprocess result is not counted twice. Strict Clippy, formatting, rustdoc with warnings denied, CI contract and the existing coverage gate pass. Functions are 359/359, source-normalized regions 3,982/3,982 and branch outcomes 692/692. Raw LLVM lines remain 4,322/4,430, regions 6,314/6,500 and branches 612/692, not 100%. An earlier CLI-only coverage run exposed an unreachable test panic arm, repaired without exclusions, and core cases already covered by the integrated newer tests. Independent documentation and integrated source reviews found no actionable regression; neither is independent protected approval. + +PRD FR-9, TRD, the Proposed ADR 0006 amendment, Context Map, Ubiquitous Language, sequence diagram, architecture, threat model and contributor rules preserve the same boundary. The next product gap is exact full-text review-context identity through decision application, worksheet history, finalization and fresh external approval. The existing approval contract has no capture digest; a readable file does not close that gap. Hosted current-head checks, prerequisite protected merges and released orchestration evidence remain independent requirements. + ### Historical pre-repair Zotero 10 transition The following schema-44 observations were captured before integrity repair and are retained for provenance, not reused as current approval inputs. From 6b9f2ef85f220f0b1d1676e5d1983c6d0db5b67d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:56:08 +0900 Subject: [PATCH 11/18] docs: distinguish review rows and confirmed owner scope --- docs/doctoring/zotero_fulltext_contract_audit.md | 4 ++-- docs/product-technical-gap-baseline.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/zotero_fulltext_contract_audit.md b/docs/doctoring/zotero_fulltext_contract_audit.md index e8108cd1..a91c473a 100644 --- a/docs/doctoring/zotero_fulltext_contract_audit.md +++ b/docs/doctoring/zotero_fulltext_contract_audit.md @@ -83,7 +83,7 @@ The 2026-09-05 08:58 UTC owner audit verified public MIT source at protected `co GitHub does contain deployment records: 66 observed records had 57 failure, one queued and eight success states. All eight successes resolve to Provider catalog sync. The latest successful deployment `6276208512` binds `2e414d15ba58f28597751b625a8a2f00fc9fadcf` to [run 33934725405](https://github.com/ContextualWisdomLab/contextual-orchestrator/actions/runs/33934725405); its job refreshes a PostgreSQL-backed provider catalog, stops its containers and supplies no environment URL. An environment named production does not establish a running model gateway. No model request or credential inspection was performed in this audit. -The existing owner [PR #1030](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/1030), Draft at `f753f453ce4fc3dbc612bb9bdbb8db4cbfd93c16`, already owns immutable release work under ADR 0129. Its owner confirmed that release artifacts, schema digest and gateway deployed-version evidence are still unproved; this audit requested those results there rather than duplicating release machinery. At the subsequent check, both branch and Git-ref endpoints still returned `a080297d2546bb61e89520d637cabc202db331ec`, while PR #1030's base object returned `2e414d15ba58f28597751b625a8a2f00fc9fadcf`. The PR base observation is not substituted for the default-branch ref. +The existing owner [PR #1030](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/1030), Draft at `f753f453ce4fc3dbc612bb9bdbb8db4cbfd93c16`, already owns immutable release work under ADR 0129. The contacted CO integration task reported release artifacts, schema digest and gateway deployed-version evidence as unproved. It later clarified that its single-writer scope is #1067/#1074, not #1030, and will identify the existing release owner's exact evidence. Confirmation from that actual release owner remains pending; no duplicate release machinery was requested. At the subsequent branch check, both branch and Git-ref endpoints still returned `a080297d2546bb61e89520d637cabc202db331ec`, while PR #1030's base object returned `2e414d15ba58f28597751b625a8a2f00fc9fadcf`. The PR base observation is not substituted for the default-branch ref. Admission requires an immutable owner artifact and schema digest, protected-source provenance, an identified deployed gateway version and exact-consumer contract evidence. Until then, no model-assisted proposal is generated through copied source, a temporary branch or a direct provider. Catalog-sync success, source documentation and a Draft release PR cannot satisfy this gate. Review labels must not be invented to compensate for unavailable model assistance. @@ -103,7 +103,7 @@ The [review-view evidence](zotero_fulltext_review_evidence.json) is bound to `54 The create-new single-link `0600` view is 1,590,742 bytes. A separate local audit recomputed capture, complete-report and proposal digests, compared every selected parent/revision/content response and confirmed the original report, worksheet, capture and batch hashes remained unchanged. The command took 1.60 seconds with maximum resident memory 288,014,336 bytes and peak memory footprint 286,966,336 bytes. The saved-file audit is outside that command measurement; neither constitutes a load benchmark. The public record includes aggregates only, never bibliography, item identities or raw text. -The view retains the original metadata proposal digest and a separate capture binding. Unchanged predictions do not require a fresh proposal merely to display additional evidence. Its outer versioned envelope is intentionally not accepted by either existing decision-application command. Full-text evidence binding through decision application, worksheet history, finalization and external approval remains unimplemented; stripping the envelope does not preserve that provenance. Inspectable captured-text rows improve from zero to 25 while remaining decisions and externally approved labels stay 3,715 and zero respectively. +The view retains the original metadata proposal digest and a separate capture binding. Unchanged predictions do not require a fresh proposal merely to display additional evidence. Its outer versioned envelope is intentionally not accepted by either existing decision-application command. Full-text evidence binding through decision application, worksheet history, finalization and external approval remains unimplemented; stripping the envelope does not preserve that provenance. Pending rows with an evidence view improve from zero to 25, including 21 with nonempty captured text and four without it, while remaining decisions and externally approved labels stay 3,715 and zero respectively. The implementation reuses the canonical pending selector and capture verifier, borrows retained responses during projection, and writes into the standard library's fixed-size slice-backed cursor. It adds no service, dependency, mutable report fields or duplicate security helper. The separately bounded buffered capture reader uses pinned `serde_json` 1.0.151; installed source confirms whole-stream completion checks and the caller separately checks the actual byte count. Metadata and output remain bounded at 16 MiB, capture files at 512 MiB, and raw response bodies at the existing 256 MiB cumulative bound. High JSON escaping can exceed a file/output ceiling without exceeding the raw-body ceiling; such input fails without truncation. The Proposed [ADR 0006 amendment](../adr/0006-zotero-research-intake.md) records alternatives, consequences and the outstanding approval contract. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a0902963..c2642c14 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -106,7 +106,7 @@ The ordered non-force cascade completed through #34 `b0119a57047e7b1fe5ddfbbf4b9 Root integration `75da75cf01704d9aae47f1e5573e3bbe3fb42bb0` retains one identical shared reader at its original metadata-owner location, all three transport regression modules and the richer request parser with the EOF guard. Full-text capture, replay, CLI and proxy-isolation source remain unchanged from its first parent. Independent source review found no actionable merge finding; this is not approval. Root verification passed 186 workspace tests across 37 unfiltered suites, including three doctests; the isolated subprocess invocation is not counted twice. Strict Clippy, formatting, rustdoc with warnings denied, CI contract and existing coverage gate passed. Functions are 347/347, source-normalized regions 3,710/3,710 and branch outcomes 674/674. Raw LLVM totals remain 4,159/4,255 lines, 6,129/6,274 regions and 603/674 branches, not 100%. No coverage exclusion, dependency, real Zotero request, credential use, classification decision or approval was added by this integration. -The [released-orchestration audit](doctoring/zotero_fulltext_contract_audit.md#released-orchestration-evidence) verified no qualifying artifact or deployed gateway in the inspected channels. Its 66 deployment records include eight successes, all Provider catalog sync, not proof of a serving gateway. Existing CO [PR #1030](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/1030) owns release work and its active owner confirmed that artifact/schema/deployed-version evidence is still pending. The handoff requests that evidence without duplicate release machinery or provider bypass. New text-bound proposals and approved labels remain zero. +The [released-orchestration audit](doctoring/zotero_fulltext_contract_audit.md#released-orchestration-evidence) verified no qualifying artifact or deployed gateway in the inspected channels. Its 66 deployment records include eight successes, all Provider catalog sync, not proof of a serving gateway. Existing CO [PR #1030](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/1030) owns release work. The contacted CO integration task reported artifact/schema/deployed-version evidence as pending; it is not the #1030 writer, and confirmation from that actual release owner remains outstanding. The handoff requests that evidence without duplicate release machinery or provider bypass. New text-bound proposals and approved labels remain zero. A 10:48 UTC CO task snapshot exposed a terminal selected-model capacity error without new release evidence. One continuation prompt kept the same task/model settings and prior scope; the subsequent compact snapshot returned active/in-progress. The CO task then clarified that its single-writer scope is integration PRs #1067/#1074, not release PR #1030, and will locate the existing release owner's exact evidence. Its reported `47ae9d65` integration change and ongoing tests are not an immutable artifact, schema digest or deployed-gateway receipt. This is a task handoff report, not independently reverified PR state. No new task, paid-model substitution or duplicate release implementation was created. @@ -118,7 +118,7 @@ CLI RED `25a1005` precedes implementation `045031c`, warning correction `ed308bd The [saved-file evidence](doctoring/zotero_fulltext_review_evidence.json) records one real offline invocation using the original private repaired report, worksheet and full-text capture, with no Zotero or model requests. It produced 25 pending rows, 21 with nonempty text and four without it; the latter remain visible. The 21 attached responses were HTTP 200, with 16 complete and five unknown index counters under the documented predicate. The new single-link `0600` file is 1,590,742 bytes with SHA-256 `e99a1963f3b5d7adfb62070785f2b69f1d9efd1d4882d365a5ca6f6b8d70f34a`. Command elapsed time was 1.60 seconds, maximum resident memory 288,014,336 bytes and peak memory footprint 286,966,336 bytes. This is one observed local run, not a latency SLO or a load benchmark. -An independent aggregate-only audit recomputed the saved capture, complete report and proposal digests; compared every selected parent, attachment revision, content version and raw response body with the original capture; and proved the nested batch equals the earlier metadata batch. Original report, worksheet, capture and batch file hashes remain unchanged. Inspectable captured-text review rows progressed from zero to 25; this is an evidence-access measure, not classification progress. The full denominator and remaining decisions stay 3,715, lifecycle capability metric stays 25, and new text-bound proposals, authentic steward decisions and independently approved labels stay zero. +An independent aggregate-only audit recomputed the saved capture, complete report and proposal digests; compared every selected parent, attachment revision, content version and raw response body with the original capture; and proved the nested batch equals the earlier metadata batch. Original report, worksheet, capture and batch file hashes remain unchanged. Pending rows with an evidence view progressed from zero to 25, including 21 with nonempty captured text and four without it; this is an evidence-access measure, not classification progress. The full denominator and remaining decisions stay 3,715, lifecycle capability metric stays 25, and new text-bound proposals, authentic steward decisions and independently approved labels stay zero. At `54383ea`, 201 workspace tests across 38 unfiltered suites, including three doctests, pass; the nested subprocess result is not counted twice. Strict Clippy, formatting, rustdoc with warnings denied, CI contract and the existing coverage gate pass. Functions are 359/359, source-normalized regions 3,982/3,982 and branch outcomes 692/692. Raw LLVM lines remain 4,322/4,430, regions 6,314/6,500 and branches 612/692, not 100%. An earlier CLI-only coverage run exposed an unreachable test panic arm, repaired without exclusions, and core cases already covered by the integrated newer tests. Independent documentation and integrated source reviews found no actionable regression; neither is independent protected approval. From bdeaa19fe6281563dbb7a1387db43aadd1f5c5a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:57:00 +0900 Subject: [PATCH 12/18] docs: link private review view successor PR --- docs/product-technical-gap-baseline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c2642c14..d23874ea 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -112,7 +112,7 @@ A 10:48 UTC CO task snapshot exposed a terminal selected-model capacity error wi ### Capture-bound private review view -Integrated source `54383eae2e83863c4cb72ee00f16cd504ff66151` adds an offline, read-only view of the next pending papers and their retained text. Committed core RED `0027d7d` fails one selected test against the intentional stub; GREEN `e426c2b` reuses the existing canonical batch and complete capture verifier. A prior incorrectly filtered command selected zero tests and is not RED evidence. Boundary tests `3fb930b` and `fcba236` cover changed bindings, direct-parent selection, missing/empty/partial/unknown responses, separate metadata/content versions, standalone exclusion and escaping beyond the fixed 16 MiB output. No text is silently truncated or unselected parent copied. +[Private review view PR #37](https://github.com/ContextualWisdomLab/ConceptWeave/pull/37) is an open Draft child of #36. Its integrated source `54383eae2e83863c4cb72ee00f16cd504ff66151` adds an offline, read-only view of the next pending papers and their retained text. Committed core RED `0027d7d` fails one selected test against the intentional stub; GREEN `e426c2b` reuses the existing canonical batch and complete capture verifier. A prior incorrectly filtered command selected zero tests and is not RED evidence. Boundary tests `3fb930b` and `fcba236` cover changed bindings, direct-parent selection, missing/empty/partial/unknown responses, separate metadata/content versions, standalone exclusion and escaping beyond the fixed 16 MiB output. No text is silently truncated or unselected parent copied. CLI RED `25a1005` precedes implementation `045031c`, warning correction `ed308bd` and test-only `26a3e6a`. Ordinary merges `b86fe86` and `54383ea` preserve both development histories. The CLI shares the existing canonical-parent, single-open `O_NOFOLLOW`, identity, single-link, exact-`0600` and create-new output boundary. Metadata inputs retain their 16 MiB limit; the capture uses buffered deserialization under a separate 512 MiB file bound, including exact-boundary, size-drift, trailing-data and static-error tests. That file ceiling is not the capture's 256 MiB response-body budget and can reject an otherwise valid high-escaping capture. Both legacy decision-application modes reject the new outer view; copying out its nested metadata batch would discard full-text provenance and is not an approved conversion. From cd830ffd9955b79ebe1e5931d1d1c1012c7d02f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:22:21 +0900 Subject: [PATCH 13/18] test: preserve source scope in full text review views --- .../src/full_text_capture_tests.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/conceptweave-zotero/src/full_text_capture_tests.rs b/crates/conceptweave-zotero/src/full_text_capture_tests.rs index dfa393b6..15b125d0 100644 --- a/crates/conceptweave-zotero/src/full_text_capture_tests.rs +++ b/crates/conceptweave-zotero/src/full_text_capture_tests.rs @@ -145,6 +145,15 @@ fn review_view_separates_two_text_parents_and_excludes_standalone_attachments() ) .unwrap(); assert_eq!(both["bibliographic_item_count"], 2); + assert_eq!(both["review_batch"]["pending_source_count"], 1); + assert_eq!( + both["review_batch"]["proposal_digest"], + both["proposal_digest"] + ); + assert_eq!(both["proposal_digest"], worksheet.proposal_digest); + assert_eq!(report.pending_source_item_keys, ["FGHI789A"]); + assert!(!both.to_string().contains("standalone evidence")); + assert!(!both.to_string().contains("FGHI789A")); assert_eq!( both["attachment_evidence"]["ABCD2345"] .as_array() @@ -180,6 +189,7 @@ fn review_view_separates_two_text_parents_and_excludes_standalone_attachments() assert!(second.contains("second parent evidence")); assert!(!second.contains("fixture text")); assert!(!second.contains("standalone evidence")); + assert_eq!(report.pending_source_item_keys, ["FGHI789A"]); } #[test] @@ -203,6 +213,15 @@ fn review_view_rejects_changed_capture_report_and_invalid_pending_work() { let mut changed_report = report_fixture(); changed_report.classified_items[0].title = "different report evidence".into(); assert!(build_full_text_review_json(&changed_report, &worksheet, &capture, 2).is_err()); + let mut changed_report = report_fixture(); + changed_report.unclassified_items[0].data.title = Some("changed retained source".into()); + let fresh_worksheet = build_steward_review_worksheet(&changed_report).unwrap(); + assert_eq!(fresh_worksheet.snapshot_digest, worksheet.snapshot_digest); + assert_ne!(fresh_worksheet.proposal_digest, worksheet.proposal_digest); + assert!(build_full_text_review_json(&changed_report, &fresh_worksheet, &capture, 2).is_err()); + let mut unbound = worksheet.clone(); + unbound.proposal_digest.clear(); + assert!(build_full_text_review_json(&report, &unbound, &capture, 2).is_err()); for limit in [0, 101] { assert!(build_full_text_review_json(&report, &worksheet, &capture, limit).is_err()); } From d53475f4bdb302db724f899a24b0dbfeb47e3e26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:23:56 +0900 Subject: [PATCH 14/18] docs: record proposed full text view scope continuity --- docs/adr/0006-zotero-research-intake.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/adr/0006-zotero-research-intake.md b/docs/adr/0006-zotero-research-intake.md index e1c897fd..f31acacc 100644 --- a/docs/adr/0006-zotero-research-intake.md +++ b/docs/adr/0006-zotero-research-intake.md @@ -176,6 +176,14 @@ PR30 exposes incremental local progress and extracts shared worksheet comparison We reject snapshot-only identity because restored proposal and retained metadata can change under that coordinate. We reject treating nonbibliographic sources as paper decisions because their disposition is not inferred truth. We report both scopes, require zero pending sources for local completion, and retain separate approval and write gates. No new digest, source-text export or authority mechanism is added. Existing early finalization checks preserve error precedence. The stricter completion meaning corrects consumers that equated filled slots with complete source scope; later consumers must inherit it before claiming preparation completion. Protected review and release remain pending. +### September 7 pending full-text view source scope (Proposed) + +PR37's read-only evidence view must preserve the source scope inherited from PR36 without turning viewing into a decision or approval. Normal merge `c4d40f7` preserves the child view and private-reader refactor together with complete retained-source validation, nonblocking opens and failure-safe buffered output. The PRD conflict is resolved by retaining both view requirements and duplicate-governance requirements. + +The existing view builder first validates the worksheet through the shared batch builder and then verifies the capture against the complete report. We retain this sequence and its existing digests. A new validator or hash would duplicate the owner boundary without adding authority. Tests `cd830ff` check nested proposal identity, unresolved source count and exclusion of standalone text from paper rows. A fresh worksheet for changed retained metadata must still reject an old capture even when the raw snapshot identity is unchanged. Blank worksheet binding also fails closed. + +The consequence is that unresolved sources remain visible as a count but cannot be silently classified as papers. View generation remains read-only and does not resolve their relationships. Full-text application and finalization must retain the outer evidence envelope; metadata-only downcasting is not full-text review. This local repair is proposed pending downstream adoption, independent review, hosted verification and protected merge. It neither issues approval nor writes Zotero. + ### September 6 duplicate source-scope admission (Proposed) PR #12 already binds exact candidate membership and complete item revisions in `ReviewedDuplicateMergeSet`; the prior audit-owner concern about unbound duplicate authority therefore does not describe this consumer. Its real remaining gap was that retained metadata and inventory were absent from its receipt, and external verification preceded local decision checks. RED `4656d6b` reproduced missing legacy scope binding, malformed inventory accepted, and altered standalone evidence reaching governance. From 21fd6484c14eef539ac86eed6547a7a0d3ae560d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:26:35 +0900 Subject: [PATCH 15/18] test: match retained title fixture to source contract --- crates/conceptweave-zotero/src/full_text_capture_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/conceptweave-zotero/src/full_text_capture_tests.rs b/crates/conceptweave-zotero/src/full_text_capture_tests.rs index 15b125d0..8e7d8526 100644 --- a/crates/conceptweave-zotero/src/full_text_capture_tests.rs +++ b/crates/conceptweave-zotero/src/full_text_capture_tests.rs @@ -214,7 +214,7 @@ fn review_view_rejects_changed_capture_report_and_invalid_pending_work() { changed_report.classified_items[0].title = "different report evidence".into(); assert!(build_full_text_review_json(&changed_report, &worksheet, &capture, 2).is_err()); let mut changed_report = report_fixture(); - changed_report.unclassified_items[0].data.title = Some("changed retained source".into()); + changed_report.unclassified_items[0].data.title = "changed retained source".into(); let fresh_worksheet = build_steward_review_worksheet(&changed_report).unwrap(); assert_eq!(fresh_worksheet.snapshot_digest, worksheet.snapshot_digest); assert_ne!(fresh_worksheet.proposal_digest, worksheet.proposal_digest); From 975988b64e19c5814090140d3603fbe5b4a8e9b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:28:56 +0900 Subject: [PATCH 16/18] docs: checkpoint pending review view 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 648ab6bd..549bd53b 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 PR37 source continuity verification in progress + +Normal merge `c4d40f7` retains PR37's pending full-text view and PR36's source inventory and private-file protections. Tests `cd830ff` add nested proposal/pending-source identity and stale capture rejection against a fresh worksheet. Independent review found a test-only `String`/`Option` mismatch; the failed compilation is retained in `/tmp/conceptweave-pr37-verified.log`, and `21fd648` corrects it without claiming a production RED. Proposed ADR0006 `d53475f` records the reuse decision and downstream envelope requirement. + +The prerequisite subsequently advanced to `3ab00417c229aeae59709f8980c79d5339687893` through manifest-version experiments and reversion. Its tree is exactly the same as `aca72e7` (`01067eb790e9e9f55395a0ddd6fb930dae685bcb`); another normal merge preserves that history. Workspace integration and corrected-source tests are still running in `/tmp/conceptweave-pr37-integrated.log` and `/tmp/conceptweave-pr37-final.log`. No final test count, coverage result or hosted verification is claimed at this checkpoint. Counts from earlier checkpoints include filtered subprocess results; final reporting must separate unfiltered suite totals. + +Actual decisions and independent approvals remain 0/3,715 with four unresolved sources. Native Visual Inspection remains incomplete because the Mac is locked. No fresh screenshot, real Zotero write, protected merge or release exists. Root and later consumers still require adoption. PR37 remains OPEN Draft; local source must finish verification before push and exact-head readback. + ### September 7 PR36 full-text source continuity verification Original PR36 `d3f991dcc1b746afed7c36f315e8937c39390c5e` passed 202 tests/39 suites. Normal merge `707f2f2` preserves its private full-text capture/proxy-isolation delta and PR34 `9eb89b8d4e751c34e640261f4883381571c83f25`. Existing capture admission delegates to worksheet construction, so shared inventory and parent consistency checks are inherited before any request. Existing whole-report hashing includes retained metadata and pending keys. No duplicate validator or hash was added. Initial integration failed compilation because two parent test callbacks required mutable borrows under PR36's writer signature; `c7052d9` repairs those calls and adds source-scope regressions. This is not a new production RED claim. From 2e93bf880047cbe3cb236fe7ca9f7579558a9c1b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:41:20 +0900 Subject: [PATCH 17/18] docs: record successful native library visual inspection --- docs/product-technical-gap-baseline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 549bd53b..6a55fcac 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -12,7 +12,7 @@ Normal merge `c4d40f7` retains PR37's pending full-text view and PR36's source i The prerequisite subsequently advanced to `3ab00417c229aeae59709f8980c79d5339687893` through manifest-version experiments and reversion. Its tree is exactly the same as `aca72e7` (`01067eb790e9e9f55395a0ddd6fb930dae685bcb`); another normal merge preserves that history. Workspace integration and corrected-source tests are still running in `/tmp/conceptweave-pr37-integrated.log` and `/tmp/conceptweave-pr37-final.log`. No final test count, coverage result or hosted verification is claimed at this checkpoint. Counts from earlier checkpoints include filtered subprocess results; final reporting must separate unfiltered suite totals. -Actual decisions and independent approvals remain 0/3,715 with four unresolved sources. Native Visual Inspection remains incomplete because the Mac is locked. No fresh screenshot, real Zotero write, protected merge or release exists. Root and later consumers still require adoption. PR37 remains OPEN Draft; local source must finish verification before push and exact-head readback. +Actual decisions and independent approvals remain 0/3,715 with four unresolved sources. Native Visual Inspection was initially blocked by the Mac lock; the September 7 continuation successfully retrieved both the Zotero accessibility state and an actual window screenshot. The library view displays 3,719 items, collection/tag panes, attachment indicators and an unselected detail pane. Long collection names, titles and creators are visibly ellipsized in the current window width; this screenshot alone does not establish full-title readability, paper count or full-text review. The screenshot remains in the private task, not a public repository artifact. No click, edit, classification, approval or Zotero write was performed during inspection. Root and later consumers still require adoption. PR37 remains OPEN Draft; local source must finish verification before push and exact-head readback. Protected merge and release remain unproven. ### September 7 PR36 full-text source continuity verification From 01395a0506ff80bc68ff0345e7c717d73af31c17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:43:19 +0900 Subject: [PATCH 18/18] docs: record verified review view source scope --- docs/product-technical-gap-baseline.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6a55fcac..344b2489 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,13 +6,13 @@ This file records code-current product and technical gaps. Exact PR/check/run co ## September 6 source inventory checkpoint -### September 7 PR37 source continuity verification in progress +### September 7 PR37 source continuity verification Normal merge `c4d40f7` retains PR37's pending full-text view and PR36's source inventory and private-file protections. Tests `cd830ff` add nested proposal/pending-source identity and stale capture rejection against a fresh worksheet. Independent review found a test-only `String`/`Option` mismatch; the failed compilation is retained in `/tmp/conceptweave-pr37-verified.log`, and `21fd648` corrects it without claiming a production RED. Proposed ADR0006 `d53475f` records the reuse decision and downstream envelope requirement. -The prerequisite subsequently advanced to `3ab00417c229aeae59709f8980c79d5339687893` through manifest-version experiments and reversion. Its tree is exactly the same as `aca72e7` (`01067eb790e9e9f55395a0ddd6fb930dae685bcb`); another normal merge preserves that history. Workspace integration and corrected-source tests are still running in `/tmp/conceptweave-pr37-integrated.log` and `/tmp/conceptweave-pr37-final.log`. No final test count, coverage result or hosted verification is claimed at this checkpoint. Counts from earlier checkpoints include filtered subprocess results; final reporting must separate unfiltered suite totals. +The prerequisite subsequently advanced to `3ab00417c229aeae59709f8980c79d5339687893` through manifest-version experiments and reversion. Its tree is exactly the same as `aca72e7` (`01067eb790e9e9f55395a0ddd6fb930dae685bcb`); normal merge `7569a1a` preserves that history. Baseline passed 216 tests/39 unfiltered suites; integration and corrected source `21fd648` each passed 268/39 including three doctests. Filtered subprocess summaries are not counted twice. Strict Clippy, warnings-denied rustdoc, formatting, CI contract and diff checks pass. Unchanged pinned coverage passes 386/386 functions, 3,960/3,960 normalized regions and 674/674 normalized branches. Raw LLVM remains 4,794/4,878 lines, 7,249/7,411 regions and 617/674 branches, not 100%. Logs `/tmp/conceptweave-pr37-{baseline,integrated,final,clippy,rustdoc,coverage}.log` are terminal success; `verified` is the earlier failed test compilation. Subsequent commits change documentation only. No hosted verification is claimed. -Actual decisions and independent approvals remain 0/3,715 with four unresolved sources. Native Visual Inspection was initially blocked by the Mac lock; the September 7 continuation successfully retrieved both the Zotero accessibility state and an actual window screenshot. The library view displays 3,719 items, collection/tag panes, attachment indicators and an unselected detail pane. Long collection names, titles and creators are visibly ellipsized in the current window width; this screenshot alone does not establish full-title readability, paper count or full-text review. The screenshot remains in the private task, not a public repository artifact. No click, edit, classification, approval or Zotero write was performed during inspection. Root and later consumers still require adoption. PR37 remains OPEN Draft; local source must finish verification before push and exact-head readback. Protected merge and release remain unproven. +Actual decisions and independent approvals remain 0/3,715 with four unresolved sources. Native Visual Inspection was initially blocked by the Mac lock; the September 7 continuation successfully retrieved both the Zotero accessibility state and an actual window screenshot. The library view displays 3,719 items, collection/tag panes, attachment indicators and an unselected detail pane. Long collection names, titles and creators are visibly ellipsized in the current window width; this screenshot alone does not establish full-title readability, paper count or full-text review. The screenshot remains in the private task, not a public repository artifact. No click, edit, classification, approval or Zotero write was performed during inspection. Root and later consumers still require adoption. Keep PR37 OPEN Draft; normal push and exact-head readback remain separate from independent hosted checks, protected merge and release. ### September 7 PR36 full-text source continuity verification