From fbf7748ffefe684346533baae025a4f9342bfb56 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 17 Jul 2026 00:05:04 +0900 Subject: [PATCH] feat: allow evidence-bound fallback date review --- ...-reviewed-fallback-date-evidence-design.md | 45 +++++++++++++++++++ src-tauri/src/cloud.rs | 6 +++ src-tauri/src/cloud_transfer.rs | 23 +++++++++- src/lib/CloudArchive.svelte | 10 +++-- 4 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 docs/superpowers/specs/2026-07-16-reviewed-fallback-date-evidence-design.md diff --git a/docs/superpowers/specs/2026-07-16-reviewed-fallback-date-evidence-design.md b/docs/superpowers/specs/2026-07-16-reviewed-fallback-date-evidence-design.md new file mode 100644 index 000000000..983e5757d --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-reviewed-fallback-date-evidence-design.md @@ -0,0 +1,45 @@ +# Reviewed fallback production-date evidence + +## Problem + +DiskSage already ranks production-time evidence as embedded metadata, explicit filename date, +filesystem creation time, then modification time. The copy gate nevertheless rejected every +candidate that lacked an embedded, high-confidence production date, even after an operator had +approved the exact evidence shown in the review UI. That made the fallback ranking unusable for +opaque archives and older files while adding no extra protection after an evidence-bound review. + +## Decision + +Embedded, high-confidence production time remains the only path that can pass the date gate +without a review decision. A lower-ranked date may pass only when all of the following are true: + +1. The planner marks the candidate as requiring review. +2. The append-only decision is valid and has disposition `approved`. +3. The decision candidate fingerprint and review fingerprint match the rebuilt candidate. +4. The normal planner, path, provider, destination, metadata-fingerprint, and copy gates pass. + +Held, absent, invalid, mismatched, or stale decisions do not waive the embedded-date gate. The +headless CLI does not accept or load review decisions, so it cannot use fallback dates to copy. + +Embedded dates below high confidence receive an explicit +`embedded-production-date-confidence-not-high` review reason. Filename and filesystem fallbacks +already receive `production-date-not-from-embedded-metadata`. + +## Safety invariants + +- A filename date is never trusted automatically. +- An approval is bound to the source, destination, provider, file identity, selected date, + confidence, review reasons, and all displayed metadata evidence. +- Replanning occurs before the decision is stored and again before copying. +- Approval changes only review/date eligibility. It cannot bypass a planner block, unsafe path, + provider mismatch, changed fingerprint, destination collision, copy verification, or provider + synchronization proof. +- The source deletion API remains absent. Local eviction still requires an immutable copy receipt + and provider-native synchronization evidence. + +## Verification + +- Rust unit tests cover approved, absent, and held fallback-date decisions. +- Planner tests cover medium-confidence embedded dates becoming review-required. +- Svelte type checking verifies that the UI mirrors the backend gate. +- Existing stale/tampered decision and provider synchronization tests remain authoritative. diff --git a/src-tauri/src/cloud.rs b/src-tauri/src/cloud.rs index 8b255169b..6b3a3d69e 100644 --- a/src-tauri/src/cloud.rs +++ b/src-tauri/src/cloud.rs @@ -1706,6 +1706,8 @@ pub fn plan_cloud_archive( } if !production_time_source.starts_with("embedded:") { review_reasons.push("production-date-not-from-embedded-metadata".into()); + } else if production_time_confidence != "high" { + review_reasons.push("embedded-production-date-confidence-not-high".into()); } let embedded_dates: BTreeSet<&str> = lineage_metadata .evidence @@ -2617,6 +2619,10 @@ mod tests { "embedded:unknown" ); assert_eq!(report.candidates[0].production_time_confidence, "medium"); + assert!(report.candidates[0].requires_review); + assert!(report.candidates[0] + .review_reasons + .contains(&"embedded-production-date-confidence-not-high".to_string())); } #[test] diff --git a/src-tauri/src/cloud_transfer.rs b/src-tauri/src/cloud_transfer.rs index abdd146c5..88aa469e7 100644 --- a/src-tauri/src/cloud_transfer.rs +++ b/src-tauri/src/cloud_transfer.rs @@ -112,6 +112,7 @@ pub fn candidate_blockers_with_review( let destination = Path::new(&candidate.dst); let root = Path::new(&cloud_root.path); let mut blockers = Vec::new(); + let mut exact_review_approved = false; if candidate.review_fingerprint.len() != 64 || !candidate @@ -140,13 +141,17 @@ pub fn candidate_blockers_with_review( Some(decision) if decision.disposition == CloudReviewDisposition::Held => { blockers.push("review-held".into()); } - Some(_) => {} + Some(_) => exact_review_approved = true, } } if candidate.blocked_reason.is_some() { blockers.push("planner-blocked".into()); } - if !embedded_high_confidence(candidate) { + // Embedded, high-confidence production time remains the only evidence that can pass without + // an operator decision. Lower-ranked evidence (explicit filename date, filesystem creation, + // then modification) may enter the copy-only phase only when an approval is bound to the + // exact candidate evidence and destination above. The headless CLI never supplies a decision. + if !embedded_high_confidence(candidate) && !exact_review_approved { blockers.push("embedded-high-confidence-date-required".into()); } if candidate.metadata_fingerprint.trim().is_empty() { @@ -876,7 +881,21 @@ mod tests { ) .unwrap(); assert!(candidate_blockers_with_review(&reviewed, &root(), Some(&filename_approval)) + .is_empty()); + + assert!(candidate_blockers_with_review(&reviewed, &root(), None) .contains(&"embedded-high-confidence-date-required".to_string())); + + let filename_hold = crate::cloud_review::create_decision( + &reviewed, + CloudReviewDisposition::Held, + 13, + ) + .unwrap(); + let held_blockers = + candidate_blockers_with_review(&reviewed, &root(), Some(&filename_hold)); + assert!(held_blockers.contains(&"review-held".to_string())); + assert!(held_blockers.contains(&"embedded-high-confidence-date-required".to_string())); } #[test] diff --git a/src/lib/CloudArchive.svelte b/src/lib/CloudArchive.svelte index 29a7b4bd2..bfa53561e 100644 --- a/src/lib/CloudArchive.svelte +++ b/src/lib/CloudArchive.svelte @@ -60,10 +60,12 @@ function copyEligible(candidate: api.CloudCandidate): boolean { const decision = matchingReviewDecision(candidate); - return candidate.blocked_reason === null - && (!candidate.requires_review || decision?.disposition === "approved") - && candidate.production_time_confidence === "high" + const exactApproval = decision?.disposition === "approved"; + const embeddedHighConfidence = candidate.production_time_confidence === "high" && candidate.production_time_source.startsWith("embedded:"); + return candidate.blocked_reason === null + && (!candidate.requires_review || exactApproval) + && (embeddedHighConfidence || exactApproval); } function reviewDecision(candidate: api.CloudCandidate): api.CloudReviewDecision | null { @@ -300,7 +302,7 @@ 충돌 제외 잠재 회수 {fmtBytes(report.potentially_reclaimable_bytes)}

- 복사는 내부 메타데이터가 고신뢰이고, 검토 사유가 있으면 현재 증거에 결박된 명시적 승인이 있는 후보만 가능합니다. 원본 삭제 기능은 제공하지 않으며, 업로드 증거가 확인되어도 허가 정보만 표시합니다. + 내장 고신뢰 생산일은 자동 자격을 얻습니다. 파일명·파일시스템 등 보조 생산일은 현재 메타데이터와 목적지에 결박된 명시적 승인이 있어야만 복사할 수 있습니다. 원본 삭제 기능은 제공하지 않으며, 업로드 증거가 확인되어도 허가 정보만 표시합니다.

{#if copied}