Skip to content

Commit 3779648

Browse files
authored
Merge pull request #34 from BitmatchApp/audit/history-export-claims-20261017
Clarify verification status in History and CSV exports
2 parents 422fb7b + 3b7287a commit 3779648

6 files changed

Lines changed: 68 additions & 18 deletions

File tree

‎BitMatchTests/LocalTransferJournalTests.swift‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,17 +660,24 @@ struct LocalTransferJournalTests {
660660
let id = try journal.enqueue(sourceURL: f.source, destinationURLs: [f.destination], verificationMode: .quick,
661661
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
662662
try journal.markRunning(id: id)
663-
try journal.finish(id: id, results: [ResultRow(path: "file", status: "✅ Copied", size: 1, checksum: nil, destination: "Backup")],
663+
try journal.finish(id: id, results: [ResultRow(path: "file", status: "✅ Match", size: 1, checksum: nil, destination: "Backup")],
664664
summary: "Done", hadIssues: false)
665665
let record = try #require(journal.records.first)
666+
#expect(record.results.first?.status == "✅ Match")
666667
let csv = try TransferHistoryDocument(record: record, asCSV: true)
667668
let csvText = String(decoding: csv.data, as: UTF8.self)
668669
#expect(csvText.contains("verification,transfer_state,transfer_summary"))
669670
#expect(csvText.contains("\"Quick\",\"issues\""))
670671
#expect(csvText.contains("Quick mode only compares file sizes"))
672+
#expect(csvText.contains("reused,recorded_status"))
673+
#expect(csvText.contains("\"Copied · contents not verified\""))
674+
#expect(csvText.contains("\"✅ Match\""))
671675
let json = try TransferHistoryDocument(record: record, asCSV: false)
672676
let payload = try #require(JSONSerialization.jsonObject(with: json.data) as? [String: Any])
673677
#expect(payload["state"] as? String == "issues")
678+
let exportedRow = try #require((payload["results"] as? [[String: Any]])?.first)
679+
#expect(exportedRow["status"] as? String == "Copied · contents not verified")
680+
#expect(exportedRow["recordedStatus"] as? String == "✅ Match")
674681
#expect(payload["source"] as? String == f.source.path)
675682
#expect(payload["bookmark"] == nil)
676683
#expect(!String(decoding: json.data, as: UTF8.self).contains(record.source.bookmark.base64EncodedString()))
@@ -696,6 +703,7 @@ struct LocalTransferJournalTests {
696703
#expect(payload["clientName"] as? String == "Studio")
697704
#expect(payload["ascMHLRequested"] as? Bool == true)
698705
#expect((payload["results"] as? [[String: Any]])?.count == 1)
706+
#expect((payload["results"] as? [[String: Any]])?.first?["recordedStatus"] == nil)
699707

700708
let csv = try TransferHistoryDocument(record: record, asCSV: true)
701709
let csvText = String(decoding: csv.data, as: UTF8.self)

‎BitMatchTests/ReportEvidenceBytesTests.swift‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,18 @@ final class ReportEvidenceBytesTests: XCTestCase {
206206
XCTAssertTrue(csv.contains("Source Rereads Avoided,0"), csv)
207207
}
208208

209+
func testCSVDoesNotCallAnUnverifiedCopyVerifiedBecauseItHasAHash() throws {
210+
let row = ResultRow(path: "/card/clip.mov", status: ResultOutcome.copiedUnverified.statusText,
211+
size: 10, checksum: "source-only-hash", destination: "Backup")
212+
let csv = try ReportExporter.makeEnhancedCSV(
213+
results: [row], started: Date(), duration: 1, filesPerSecond: 1,
214+
photographerContext: nil, prefs: ReportPrefs(verificationMode: .standard)
215+
)
216+
let dataRow = try XCTUnwrap(csv.components(separatedBy: "\n").dropFirst().first)
217+
XCTAssertTrue(dataRow.contains("✅ Copied"), dataRow)
218+
XCTAssertFalse(dataRow.contains("Verified"), dataRow)
219+
}
220+
209221
func testJSONUsesPhaseDurationSecondKeysAndQuickOmitsVerify() throws {
210222
var prefs = ReportPrefs()
211223
prefs.verificationMode = .quick

‎Packages/BitMatchEngine/Sources/BitMatchEngine/Evidence/EvidenceWriter.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ public enum EvidenceWriter: Sendable {
526526
let camera = project?.camera ?? ""
527527
let card = project?.card ?? ""
528528
let packagePath = project?.packagePath ?? ""
529-
let verified = isMatchStatus(result.status) && result.checksum?.isEmpty == false && prefs?.verificationMode != .quick
529+
let verified = result.isVerifiedStatus && result.checksum?.isEmpty == false && prefs?.verificationMode != .quick
530530
let details = verified ? "Verified" : result.status
531531

532532
// Per-file times are not measured; the column stays for existing

‎Shared/Core/Models/TransferLibraryPresentation.swift‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import Foundation
22
import BitMatchEngine
33

4+
/// Keep older Quick rows' original status in the journal, but present their
5+
/// successful copy as unverified everywhere a person reads it.
6+
enum TransferHistoryResultPresentation {
7+
static func statusText(for row: ResultRow, mode: VerificationMode) -> String {
8+
if mode == .quick && row.isSuccessStatus {
9+
return "Copied · contents not verified"
10+
}
11+
return row.status
12+
}
13+
}
14+
415
/// What History shows for each journal record, and which records raise the
516
/// "review in History" banner on Mac, iPad and iPhone.
617
/// Pure values, so every platform shows the same state the same way.

‎Shared/Core/Services/TransferHistoryDocument.swift‎

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ struct TransferHistoryDocument: FileDocument {
1818
results: record.results, includeModePlan: record.plannedFileCounts != nil)
1919
if asCSV {
2020
func quote(_ value: String) -> String { "\"" + value.replacingOccurrences(of: "\"", with: "\"\"") + "\"" }
21-
let header = "source,destination,status,bytes,checksum,verification,transfer_state,transfer_summary,project,asc_mhl,client,reused\n"
21+
let header = "source,destination,status,bytes,checksum,verification,transfer_state,transfer_summary,project,asc_mhl,client,reused,recorded_status\n"
2222
let rows = record.results.map { row in
23-
[row.path, row.destinationPath ?? row.destination ?? "", row.status, String(row.size), row.checksum ?? "",
23+
[row.path, row.destinationPath ?? row.destination ?? "",
24+
TransferHistoryResultPresentation.statusText(for: row, mode: record.verificationMode),
25+
String(row.size), row.checksum ?? "",
2426
record.verificationMode.rawValue, record.state.rawValue, record.summary,
2527
record.reportSettings.projectName, record.generateASCMHL ? "requested" : "not requested",
26-
record.reportSettings.clientName, row.wasReused ? "true" : "false"].map(quote).joined(separator: ",")
28+
record.reportSettings.clientName, row.wasReused ? "true" : "false", row.status].map(quote).joined(separator: ",")
2729
}
2830
func pair(_ key: String, _ value: String) -> String { quote(key) + "," + quote(value) }
2931
func detail(_ value: String) -> String {
@@ -80,7 +82,7 @@ struct TransferHistoryDocument: FileDocument {
8082
let copyDurationSeconds: TimeInterval?
8183
let verifyDurationSeconds: TimeInterval?
8284
let performanceTelemetry: TransferPerformanceTelemetry?
83-
let results: [ResultRow]
85+
let results: [HistoryResult]
8486
}
8587
let encoder = JSONEncoder()
8688
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
@@ -96,7 +98,35 @@ struct TransferHistoryDocument: FileDocument {
9698
copyDurationSeconds: record.copyDurationSeconds,
9799
verifyDurationSeconds: record.verifyDurationSeconds,
98100
performanceTelemetry: record.performanceTelemetry,
99-
results: record.results))
101+
results: record.results.map { HistoryResult(row: $0, mode: record.verificationMode) }))
100102
}
101103
}
102104
}
105+
106+
/// Export the interpreted status without changing the saved journal row.
107+
/// `recordedStatus` retains old wording when it could overstate verification.
108+
private struct HistoryResult: Encodable {
109+
let id: UUID
110+
let path: String
111+
let status: String
112+
let recordedStatus: String?
113+
let size: Int64
114+
let checksum: String?
115+
let destination: String?
116+
let destinationPath: String?
117+
let clipIntegrity: ClipIntegrityFinding?
118+
let wasReused: Bool?
119+
120+
init(row: ResultRow, mode: VerificationMode) {
121+
id = row.id
122+
path = row.path
123+
status = TransferHistoryResultPresentation.statusText(for: row, mode: mode)
124+
recordedStatus = status == row.status ? nil : row.status
125+
size = row.size
126+
checksum = row.checksum
127+
destination = row.destination
128+
destinationPath = row.destinationPath
129+
clipIntegrity = row.clipIntegrity
130+
wasReused = row.wasReused ? true : nil
131+
}
132+
}

‎Shared/Views/TransferLibraryView.swift‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,17 +353,6 @@ struct TransferLibraryView: View {
353353
}
354354
}
355355

356-
/// History keeps the original row status in exports, but Quick's older
357-
/// "Match" wording must not be presented as content verification on screen.
358-
enum TransferHistoryResultPresentation {
359-
static func statusText(for row: ResultRow, mode: VerificationMode) -> String {
360-
if mode == .quick && row.isSuccessStatus {
361-
return "Copied · contents not verified"
362-
}
363-
return row.status
364-
}
365-
}
366-
367356
/// Attaches `.searchable` only when it makes sense, so the field never
368357
/// appears empty over an empty Queue and never grabs focus on its own.
369358
private struct OptionalSearchable: ViewModifier {

0 commit comments

Comments
 (0)