feat(diff): detailed before/after metadata diff — surface + JPEG (#22) #119
No reviewers
Labels
No labels
bug
documentation
duplicate
e-copy
e-features
e-mobile
enhancement
f-coverage
f-forensic
f-perf
f-privacy
forensic
good first issue
help wanted
infra
invalid
phase-a
phase-b
phase-c
phase-d
phase-e
phase-f
phase-g
phase-h
priority-1
priority-2
priority-3
privacy
question
v5
v6
video-hardening
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: forgejo_admin/exifcleaner-web#119
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/metadata-diff"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #22.
Summary
Phase 1 of issue #22 — the per-file expandable before/after metadata diff. Surface (types, plumbing, renderer component) + JPEG enumeration. Other formats (PDF/PNG/Office/Video) return
metadataItems: []and will fill in via per-format follow-up PRs as planned in the spec.User-visible: JPEG rows now show a chevron + clickable diff panel listing what was removed (red strikethrough), modified (green
before → after), and kept (neutral grey badge — Orientation + ICC profile preservation). Diff is collapsed by default; single-row-expanded semantics fromAppContext.expandedRowId. Mobile stacks to single column at< 420px.Scope
MetadataItemdiscriminated union (removed/modified/kept),StripResult.metadataItems,ProcessOutcome+WasmApi.processextension,FileEntry.metadataItems,UPDATE_FILE_METADATAaction,MetadataDiffExpansioncomponent with BEM CSS + dark mode tokens, 6 new i18n keys (English placeholders for the other 24 locales),isExpandablegate tightened so non-JPEGCompleterows stay non-expandable.47° 36' 22.3" Nfrom triplet + ref). MakerNote emitted as one opaque item with vendor signature detection (Nikon/Canon/Sony/Olympus/Fuji/Panasonic/Pentax/Leica). XMP/ICC/JFIF/Comment dropped segments emit blob items. Kept items for Orientation (whenpreserveOrientation: true) and ICC profile (whenpreserveColorProfile: true).Spec + plan
docs/superpowers/specs/2026-05-14-issue-22-metadata-diff-design.mddocs/superpowers/plans/2026-05-14-issue-22-metadata-diff.mdWhat's deferred (per spec §11)
Verification
Quality gates (all green):
yarn typecheckpassyarn lintpassyarn test— 341/341 pass (31 test files)yarn check:depspass (no circular)yarn build:webpassyarn build:web:standalonepassE2E:
yarn test:e2e:web— 105/105 pass, 17 skipped (1 flakyweb-mobile-iosservice-worker precache test passed cleanly on rerun; unrelated to diff)yarn test:e2e:standalone— 7/7 pass (standalone-desktop+standalone-mobile)Forensic gate (per
format-strategy-workflow.md§3 /privacy-invariants.md§3):Re-ran
tools/forensic/jpeg.tsagainst the Phase 1 JPEG strategy with exiftool 12.76. Zero sentinel survival across every recovery technique — rawstrings | grep,exiftool -a -G1 -stag enumeration, and the in-process APP*/COM marker walker all return[]on the default strip (15 b output) and onpreserveOrientation=true(51 b output, single 34-byte APP1 carrying only the Orientation entry). Output sizes, segment counts, sentinel-survival counts, and theExifTool-marker check reproduce the 2026-05-07 baseline atdocs/forensic/jpeg.mdbyte-for-byte. Phase 1 changes are observation-only on the strip path — the walker emits aMetadataItem[]describing what it saw while dropping the same bytes it always dropped; no segment-drop policy or marker-keep policy moved.Screenshots
Desktop, collapsed (chevron visible on JPEG row):
Desktop, expanded (JFIF + EXIF + GPS groups with strikethrough removed values):
Mobile (390 × 844, iPhone UA), expanded — single-column stack at
< 420px:Test plan
removeditems; click collapsespreserveOrientation: truein settings; Orientation row renders askeptwith thepreservedbadgeGenerated with Claude Code
Pure render component for the per-file before/after metadata diff: groups items by source (first-seen ordering), kept items first within each group, with per-action row styling (red strikethrough for removed, green arrow target for modified, neutral with `preserved` badge for kept). Long values truncate with a `title` tooltip; mobile stacks to a single column under 420px. The diff i18n keys (added in Task 9) carry `{count}` placeholders. Interpolation runs locally in the component via `.replace("{count}", ...)`, mirroring the `ErrorExpansion.tsx` pattern, because the live `useI18n()` `t` signature is `(key: string) => string` and does not interpolate vars. Tests use `react-dom/server.renderToStaticMarkup` for assertions on the rendered HTML — no DOM environment, no new dev deps. Vitest include pattern extended to `.test.{ts,tsx}` so the component test file is picked up. Tokens added under `[data-theme="dark"]` (matching the existing codebase pattern) rather than `@media (prefers-color-scheme: dark)`. The existing surface/border/text-secondary tokens are reused for the group header chrome.Splits the EXIF walker and tag dictionary out of `jpeg_strategy.ts` into shared modules under `src/infrastructure/wasm/exif/` and `src/domain/exif/`, so future format strategies (PNG eXIf, HEIC Exif, native TIFF) can consume the same parser without copying the nine-hundred-LOC body of the JPEG strategy. The new layout splits along a clean DDD seam: - `src/domain/exif/ifd_tag_names.ts` — GENERATED from `exiftool -listx` via `scripts/generate_exif_tags.mjs` (idempotent, no deps, regenerate with `yarn generate:exif-tags`). Four numeric dictionaries: IFD0 (242), ExifIFD (357), GPS (32), InteropIFD (5). - `src/domain/exif/ifd_value_formatters.ts` — hand-rolled, primitive-typed (no IfdEntry dependency). - `src/infrastructure/wasm/exif/ifd_entry.ts` — `IfdEntry` interface + TIFF type constants. - `src/infrastructure/wasm/exif/ifd_readers.ts` — endian-aware readers, `describeUnknown`, `formatByteSize`. - `src/infrastructure/wasm/exif/ifd_decoders.ts` — assembles `{ name, format }` per tag; small `IFD0_NAME_OVERRIDES` / `EXIF_SUBIFD_NAME_OVERRIDES` preserve Exif-spec names where ExifTool ships its own alias (0x9204 ExposureBiasValue vs ExposureCompensation; 0x927c MakerNote vs the 91 vendor aliases; 0x0201/0x0202 JPEGInterchangeFormat[Length] vs ThumbnailOffset/Length). - `src/infrastructure/wasm/exif/ifd_walker.ts` — `parseIfd`, `enumerateExifSegment` (APP1 path with "Exif\0\0" prefix), `enumerateExifTiff` (prefix-less path for PNG eXIf / HEIC). `jpeg_strategy.ts` drops from ~1629 to ~495 LOC; it now contains only the JPEG segment walker, drop policy, orientation-only APP1 synthesis, and JFIF/ICC/Comment helpers. Zero behaviour change — all 343 existing tests pass unmodified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>