Skip to content

feat(porter): manifest content hashes + import preview diff, driven validate - #80

Merged
markmybytes merged 2 commits into
mainfrom
feat/preview-diff-manifest-hashes
Nov 5, 2026
Merged

markmybytes merged 2 commits into
mainfrom
feat/preview-diff-manifest-hashes

Conversation

@markmybytes

Copy link
Copy Markdown
Owner

Summary

Import preview now tells you whether the ZIP's settings/data actually differ from what's on disk before you import — and export stamps the content hashes that make it possible.

Backend (pkg/porter)

  • Manifest hashes (hash.go): toZip computes a per-entry sha1 while packing (single io.MultiWriter pass) and writes manifest.json last, once the digest of entries is known:
    • hashes.settings — sha1 of the conf/setting.json entry bytes
    • hashes.data — folded digest over conf/data.db + drivers/**: frames <byte-len-of-arcname>:<arcname>:<sha1hex>\n sorted byte-wise; empty fold stays absent (omitempty)
    • The fold recipe is pinned cross-repo by hash_golden_test.go (mirrors it-claws tests/test_hashes.py; CJK/Cyrillic arcname exercises the byte-length prefix). If it fails, the recipe drifted and must be reported — not silently fixed.
  • Import preview diff (porter.go): zipScan (cheap, central directory + manifest, no filesystem) is shared by the validate and import paths; buildPreview augments it with two TargetDiff rows (settings/data): incoming/local present, counts, sizes, identical.
    • Graceful degradation, one invariant: legacy manifests, hashes absent, or local read failures → available=false, the row is simply hidden. Only cancellation errors out (sentinel errImportCanceled, shared with the extract paths; errors.Is-checked in dataDiff).
  • Validate runs as a job (progress.go, new tick in hash.go): local hashing reads every byte of the local driver set, which previously ran job-less and froze the UI on large packs. ValidateZip now follows the same job lifecycle as Export/DownloadAndValidate; localDataHashes is two-phase (stat-only listing → hash pass) reporting byte progress per file and honoring Abort mid-walk.
  • DownloadAndValidate now removes the temp ZIP on any post-download failure (added compare paths could previously leak it).

Frontend

  • Import preview modal shows an identical/changed/new badge + incoming/local facts per category (porter.vue); rows hidden when available is false.
  • File-validate button goes through ProgressModal (new validate expose sharing one runPreviewJob driver with download): stepper shows Init → Compare → Complete with a live byte-progress bar and a working Cancel.
  • compare step key added to ProgressStepper; i18n keys added in en.json + zh_Hant_HK.json (only existing keys reused otherwise). Dead unavailable diff state removed.

Known limits (intentional)

  • sha1 is fine here: hardware-SHA-NI accelerated, IO-bound workload, and required by the pinned cross-repo recipe.
  • Manifest hashes at validation time are advisory (preview-diff only); entry contents are not verified at import. Local filesystem is case-insensitive on Windows, so a locally-renamed Drivers\ directory vice zip drivers/ shows "changed" — preview-only cosmetics.

Tests

  • go test ./pkg/... — full suite green on Windows; 5 new porter tests for fold/progress/abort/semantics (diff_internal_test.go, expanded porter_test.go, hash_golden_test.go)
  • vue-tsc --noEmit clean, eslint . clean (3 pre-existing unrelated warnings), 121/121 vitest pass
  • Go suite adds: manifest-last ordering, legacy manifest (no hashes), hash-present-entry-missing, hash/entry disagreement, cancel propagation, data-fold read-error, running-job reject, tick progress/abort

Backups now carry SHA-1 digests of their content in the manifest
(hashes: {settings, data}; sha1 computed during export streaming,
manifest written last), and the import preview shows a per-target
diff between the machine's current content and the backup: settings
and data (db + drivers folded) each get an identical/changed/new
state with sizes and counts.

- pkg/porter: hash fold recipe (UTF-8 byte-length framing, frame-string
  sort, last-wins arcname dedupe, empty keys omitted), ValidateZip
  split into a ZIP-only core (imports never re-hash the disk) plus a
  synchronous live-FS compare that degrades to "unavailable" on read
  errors; legacy backups without hashes hide the diff rows.
- it-claws (separate repo, mirrored there): the same recipe writes
  hashes when building exports; cross-repo parity is pinned by a
  golden test on a CJK-arcname fixture.
- frontend: diff rows on the preview checklist, `compare` step in the
  download stepper, re-entrancy guard on validate, en + zh_Hant_HK
  i18n; bindings regenerated.

Verified: go test ./pkg/... green, frontend type-check/lint/vitest
(121) green, wails generate clean.
- ValidateZip follows the same job lifecycle as Export/DownloadAndValidate
  so the frontend can poll it; previously local hashing ran job-less and
  the UI appeared frozen on large driver sets.
- localDataHashes splits into a stat-only listing pass and a hashing pass,
  reporting byte progress per file (tick) and honoring Abort mid-walk;
  buildPreview/dataDiff propagate only the cancel sentinel, local read
  failures still hide the row (both settings and data).
- DownloadAndValidate removes the temp ZIP on any post-download failure
  (temp file leaked on the new compare error paths before this).
- frontend shares one runPreviewJob driver for download/validate exposes
  (was a verbatim 25-line clone), validate wired through ProgressModal,
  dead 'unavailable' DiffState removed, fold-key invariant documented in
  action.go, stale step-list comment in JobSnapshot dropped.
- drop committed session handoff doc.
@markmybytes
markmybytes force-pushed the feat/preview-diff-manifest-hashes branch from 868df24 to 4540f06 Compare November 5, 2026 13:38
@markmybytes
markmybytes merged commit 457914a into main Nov 5, 2026
2 checks passed
@markmybytes
markmybytes deleted the feat/preview-diff-manifest-hashes branch November 5, 2026 13:39
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