Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6193dff
test(zotero): require owner-only decision patch CLI
seonghobae Sep 4, 2026
fd7b295
feat(zotero): apply steward decision patches offline
seonghobae Sep 4, 2026
d5042b4
test(zotero): prove decision patch artifact safety
seonghobae Sep 4, 2026
cf660fd
docs(zotero): define offline decision patch workflow
seonghobae Sep 4, 2026
8f5dee8
style(zotero): format decision patch CLI tests
seonghobae Sep 4, 2026
123efcf
Merge PR #31 coverage-contract successor without force
seonghobae Sep 4, 2026
3610aee
Merge PR #31 parent-path contract successor without force
seonghobae Sep 4, 2026
587de6f
Merge PR #31 parent-path repair without force
seonghobae Sep 4, 2026
a51f912
Integrate concurrent PR #32 parent-path successor without force
seonghobae Sep 4, 2026
a914f0a
Merge repaired write receipt evidence into zotero-review-decision-cli
seonghobae Sep 4, 2026
becfb21
Merge write receipt fixture ownership repair into PR 32
seonghobae Sep 4, 2026
2534e17
Merge current receipt repair parent into zotero-review-decision-cli
seonghobae Sep 4, 2026
4947bdd
Merge remote-tracking branch 'origin/autoresearch/zotero-review-decis…
seonghobae Sep 4, 2026
09df447
Merge complete receipt binding coverage into PR 32
seonghobae Sep 4, 2026
a5ba0eb
merge(zotero): adopt current decision-patch parent
seonghobae Sep 4, 2026
0ac89b6
merge(zotero): adopt current decision-patch parent and gap baseline
seonghobae Sep 5, 2026
6f42521
merge(research): inherit verified source and proposal approval binding
seonghobae Sep 5, 2026
8c9eac5
test(research): adopt captured-source decision CLI fixtures
seonghobae Sep 5, 2026
492cbe5
merge(research): inherit canonical local transport repairs into PR #32
seonghobae Sep 5, 2026
2483d25
merge(research): inherit deterministic transport framing regression i…
seonghobae Sep 5, 2026
bd7669d
Merge verified private artifact boundary repair into PR 32
seonghobae Sep 5, 2026
f0bf02a
merge(zotero): propagate validated approval ordering through PR 32
seonghobae Sep 5, 2026
1e71172
merge(research): inherit bounded metadata reads into PR #32
seonghobae Sep 6, 2026
b7cae29
merge: inherit reviewed patch identity in offline CLI
seonghobae Sep 6, 2026
d3a2c7a
test(zotero): verify CLI binding refusal and valid output preservation
seonghobae Sep 6, 2026
a93b62a
docs(zotero): record thin decision CLI contract verification
seonghobae Sep 6, 2026
ea25437
docs(zotero): record decision CLI exact scope evidence
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ cargo +1.98.0 run --bin conceptweave-zotero -- /tmp/conceptweave-zotero-classifi

The command reads one stable library-version snapshot and creates a local, reviewable JSON report. On Unix, output is restricted to a new owner-only (`0600`) direct child of canonical `/tmp` or the system temporary directory; the CLI fails closed on other platforms. The command never changes Zotero records.

Apply a small steward-reviewed decision patch to a new worksheet without overwriting the current one:

```sh
cargo +1.98.0 run --bin conceptweave-zotero -- --apply-decision-patch /tmp/report.json /tmp/current-worksheet.json /tmp/patch.json /tmp/updated-worksheet.json
```

All three inputs must be separate owner-only files bound to the same snapshot and reviewed content. Older patches without content identity must be regenerated from the evidence actually reviewed; a new worksheet cannot renew an old patch. The output is a new owner-only file. This offline step neither changes Zotero nor grants governance approval.

If file-permission setup fails, the command stops before writing report content.
An empty file may remain; inspect it before removing it. The command does not
delete a pathname that another process may have replaced.
Expand Down
138 changes: 134 additions & 4 deletions crates/conceptweave-zotero/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]

use conceptweave_zotero::{
ClassificationReport, GoldenSetApproval, StewardReviewWorksheet,
assess_steward_review_progress, build_steward_review_worksheet, read_local_snapshot,
reviewed_golden_set_from_worksheet,
ClassificationReport, GoldenSetApproval, StewardDecisionPatch, StewardReviewWorksheet,
apply_steward_decision_patch, assess_steward_review_progress, build_steward_review_worksheet,
read_local_snapshot, reviewed_golden_set_from_worksheet,
};
use serde::de::DeserializeOwned;
use std::collections::BTreeSet;
Expand All @@ -13,7 +13,7 @@ use std::fs::{self, File, OpenOptions};
use std::io::{self, BufWriter, Read, Write};
use std::path::{Path, PathBuf};

const USAGE: &str = "usage: conceptweave-zotero /tmp/REPORT.json | --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json | --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json | --finalize /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/APPROVAL.json /tmp/GOLDEN.json";
const USAGE: &str = "usage: conceptweave-zotero /tmp/REPORT.json | --worksheet /tmp/REPORT.json /tmp/WORKSHEET.json | --review-progress /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/PROGRESS.json | --apply-decision-patch /tmp/REPORT.json /tmp/CURRENT_WORKSHEET.json /tmp/PATCH.json /tmp/UPDATED_WORKSHEET.json | --finalize /tmp/REPORT.json /tmp/WORKSHEET.json /tmp/APPROVAL.json /tmp/GOLDEN.json";
const MAX_ARTIFACT_BYTES: u64 = 16 * 1024 * 1024;

#[derive(Debug, PartialEq, Eq)]
Expand All @@ -28,6 +28,12 @@ enum OutputRequest {
worksheet: String,
output: String,
},
ApplyDecisionPatch {
report: String,
worksheet: String,
patch: String,
output: String,
},
Finalize {
report: String,
worksheet: String,
Expand Down Expand Up @@ -73,6 +79,36 @@ where
worksheet,
output,
}
} else if first == "--apply-decision-patch" {
let report = args
.next()
.ok_or("--apply-decision-patch requires four artifact paths")?;
let worksheet = args
.next()
.ok_or("--apply-decision-patch requires four artifact paths")?;
let patch = args
.next()
.ok_or("--apply-decision-patch requires four artifact paths")?;
let output = args
.next()
.ok_or("--apply-decision-patch requires four artifact paths")?;
if BTreeSet::from([
report.as_str(),
worksheet.as_str(),
patch.as_str(),
output.as_str(),
])
.len()
!= 4
{
return Err("decision patch artifact paths must differ");
}
OutputRequest::ApplyDecisionPatch {
report,
worksheet,
patch,
output,
}
} else if first == "--finalize" {
let report = args
.next()
Expand Down Expand Up @@ -411,6 +447,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let progress = assess_steward_review_progress(&report, &worksheet)?;
write_private_output(&output, &serde_json::to_vec_pretty(&progress)?)?;
}
OutputRequest::ApplyDecisionPatch {
report,
worksheet,
patch,
output,
} => {
let output = validate_output_path(&output)?;
let (report, report_identity): (ClassificationReport, _) =
read_private_json(&report).map_err(|error| label_input("report", error))?;
let (worksheet, worksheet_identity): (StewardReviewWorksheet, _) =
read_private_json(&worksheet).map_err(|error| label_input("worksheet", error))?;
let (patch, patch_identity): (StewardDecisionPatch, _) =
read_private_json(&patch).map_err(|error| label_input("decision patch", error))?;
if BTreeSet::from([report_identity, worksheet_identity, patch_identity]).len() != 3 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"decision patch inputs must be distinct files",
)
.into());
}
let updated = apply_steward_decision_patch(&report, &worksheet, &patch)?;
let content = serde_json::to_vec_pretty(&updated)?;
write_private_output(&output, &content)?;
}
OutputRequest::Finalize {
report,
worksheet,
Expand Down Expand Up @@ -587,6 +647,76 @@ mod tests {
);
}

#[test]
fn apply_decision_patch_mode_requires_four_distinct_artifact_paths() {
let report = "/tmp/report.json";
let worksheet = "/tmp/worksheet.json";
let patch = "/tmp/patch.json";
let output = "/tmp/updated-worksheet.json";
assert_eq!(
parse_output_request(vec![
"--apply-decision-patch",
report,
worksheet,
patch,
output
]),
Ok(OutputRequest::ApplyDecisionPatch {
report: report.to_owned(),
worksheet: worksheet.to_owned(),
patch: patch.to_owned(),
output: output.to_owned(),
})
);
assert!(parse_output_request(vec!["--apply-decision-patch"]).is_err());
assert!(parse_output_request(vec!["--apply-decision-patch", report]).is_err());
assert!(parse_output_request(vec!["--apply-decision-patch", report, worksheet]).is_err());
assert!(
parse_output_request(vec!["--apply-decision-patch", report, worksheet, patch]).is_err()
);
assert!(
parse_output_request(vec![
"--apply-decision-patch",
report,
worksheet,
patch,
report
])
.is_err()
);
assert!(
parse_output_request(vec![
"--apply-decision-patch",
report,
worksheet,
worksheet,
output
])
.is_err()
);
assert!(
parse_output_request(vec![
"--apply-decision-patch",
report,
report,
patch,
output
])
.is_err()
);
assert!(
parse_output_request(vec![
"--apply-decision-patch",
report,
worksheet,
patch,
output,
"extra",
])
.is_err()
);
}

#[cfg(unix)]
#[test]
fn private_json_input_is_owner_only_regular_bounded_and_valid() {
Expand Down
14 changes: 14 additions & 0 deletions crates/conceptweave-zotero/tests/finalization_artifact_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ fn artifact_commands_reject_distinct_path_spellings_for_one_input_file() {
])
.status()
.unwrap();
let patch_status = Command::new(env!("CARGO_BIN_EXE_conceptweave-zotero"))
.args([
"--apply-decision-patch",
&report_path,
&worksheet_path,
&approval_path,
output.to_str().unwrap(),
])
.status()
.unwrap();

let _ = fs::remove_file(&input);
let _ = fs::remove_file(&output);
Expand All @@ -111,4 +121,8 @@ fn artifact_commands_reject_distinct_path_spellings_for_one_input_file() {
!progress_status.success(),
"progress must reject two path spellings that resolve to one input artifact"
);
assert!(
!patch_status.success(),
"decision patching must reject path spellings that resolve to one input artifact"
);
}
Loading