Skip to content

Write a self-describing record beside every backup - #44

Merged
amrali-eg merged 1 commit into
masterfrom
feat/self-describing-backups
Aug 26, 2026
Merged

Write a self-describing record beside every backup#44
amrali-eg merged 1 commit into
masterfrom
feat/self-describing-backups

Conversation

@amrali-eg

@amrali-eg amrali-eg commented Aug 26, 2026

Copy link
Copy Markdown
Owner

The audit measured 99.2% of bad conversions as byte-recoverable — then found the qualifier that made the figure hollow: recovery needs the codec the file was read with, and that lived solely in the conversion report. A GUI user has no reason to keep one. "Recoverable" was true and unusable.

Every backup now carries a sidecar that answers, on its own, how to restore the original and reconstruct the conversion:

{
  "MetadataVersion": 1,
  "OriginalSha256": "cd20729b…",
  "BackupSha256": "cd20729b…",
  "DetectedEncoding": "windows-1252",
  "DetectedCodePage": 1252,
  "TargetEncoding": "utf-8",
  "TargetCodePage": 65001,
  "SourceTextSha256": "ded8dc8b…",
  "OutputTextSha256": "ded8dc8b…",
  "ECVersion": "3.7.0.0", "ConversionId": "", "ConversionTimestampUtc": ""
}

The code page is recorded because a name is not an identifier: cp949 and ks_c_5601-1987 are one encoding and only the number says so. This project already learned that lesson when comparing detector output by label scored spelling disagreements as detection errors.

Ordering is the part that matters

RecordConversion is invoked from inside Convert, after verification succeeds and before AtomicReplace. At that moment the backup exists, the conversion is proven, and the original is still in place — so a failure to record aborts with nothing lost and nothing needing recovery. Writing the record after installation would have reproduced the same gap in a new form.

The record is also read back after writing: a file that was written but cannot be parsed would leave a caller believing the conversion is reversible when it isn't.

Restore is verified, not inferred

RestoreAvailability distinguishes Available, BackupMissing, MetadataMissing, MetadataUnreadable, BackupCorrupted — because a .bak existing is not evidence anything can be recovered.

The backup is hashed and checked against both the recorded backup hash and the recorded original hash. Those are different questions: the first says the backup is the file described, the second says that file is the original.

A stale .bak from an earlier run is refused rather than recorded as this conversion's original.

Sidecar, not an alternate data stream

An ADS is lost by ordinary copying, archiving and cloud sync — exactly the operations that separate a backup from its origin. A sidecar survives them and can be read without EncodingChecker.

The sidecar is excluded from scanning alongside .bak, so a later run never treats its own output as input.

330 tests passing (was 320). Ten new tests, including one that reverses a real conversion using the sidecar alone.

🤖 Generated with Claude Code

An audit measured 99.2% of bad conversions as byte-recoverable, then noticed the
qualifier that made the figure hollow: recovery needs the codec the file was
read with, and that lived solely in the conversion report. A GUI user has no
reason to keep one. "Recoverable" was true and unusable.

Each backup now carries a sidecar answering, on its own, how the original is
restored and how the conversion can be reconstructed: original path, size and
SHA-256; the backup's SHA-256; the detected encoding *and its code page*; BOM
state; target encoding and code page; text hashes before and after; scalar
count; tool version, timestamp, conversion id; and a schema version.

The code page is there because a name is not an identifier. "cp949" and
"ks_c_5601-1987" are one encoding and only the number says so - a lesson this
project already learned when comparing detector output by label scored spelling
disagreements as errors.

Ordering is the part that matters. ConversionOptions.RecordConversion is invoked
from inside Convert, after verification succeeds and before AtomicReplace. The
backup exists, the conversion is proven, and the original is still in place, so
a failure to record aborts with nothing lost and nothing needing recovery.
Writing the record after installation would have reproduced the same gap in a
new form.

The record is also read back after writing, since a file that was written but
cannot be parsed would leave a caller believing the conversion is reversible
when it is not.

Restore availability is a verified state rather than an inference from a ".bak"
existing: Available, BackupMissing, MetadataMissing, MetadataUnreadable, or
BackupCorrupted. The backup is hashed and checked against both the recorded
backup hash and the recorded original hash - different questions, since the
first says the backup is the file described and the second says that file is the
original.

A sidecar is written only when a backup exists, since without one there is
nothing to restore from. A stale ".bak" from an earlier run is refused rather
than recorded as this conversion's original.

Chosen as a JSON sidecar over an NTFS alternate data stream: an ADS is lost by
ordinary copying, archiving and cloud sync, which are exactly the operations
that separate a backup from its origin.

330 tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@amrali-eg
amrali-eg merged commit 92f2efd into master Aug 26, 2026
1 check passed
@amrali-eg
amrali-eg deleted the feat/self-describing-backups branch August 26, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant