Files
suaveplan/openspec/changes/exif-completion/tasks.md
2026-05-27 23:29:53 +01:00

158 lines
12 KiB
Markdown

# Tasks — EXIF completion (MakerNotes / ICC / GPS / Apple bplist / vendor ciphers)
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was claimed but never landed. The MakerNotes / ICC / GPS gaps closed here were prematurely ticked in `openspec/archive/2026-04-19-enterprise-exif/tasks.md` lines 37, 38, 43; that's the failure mode this checklist guards against.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/packages/universal/exif/src/{makernotes,icc,gps}` end-to-end (no dedicated `enterprise/exif.md` exists)
- [ ] 1.2 Re-read `openspec/archive/2026-04-19-enterprise-exif/tasks.md` and `specs/exif/spec.md` to confirm what was claimed but not delivered
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 0a (parity), 13 (verification), 14 (archival), 14a (affected dependents)
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`; the **Why** section explicitly cites the archive discrepancy (lines 37, 38, 43 of the archived `tasks.md`)
- [ ] 1.5 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
- [ ] 1.6 Draft `specs/exif/spec.md` from `openspec/templates/spec-delta.md` covering all six ADDED Requirements
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
- [ ] 1.8 `openspec change validate exif-completion` exits with no errors
## 2. MakerNotes registry + decoders
- [ ] 2.1 Define `MakerNoteDecoder` interface in `packages/image/exif/src/makernotes/types.ts`
- [ ] 2.2 Port the dispatcher `makernotes/dispatch/dispatch.ts` (L1-L78) into `packages/image/exif/src/makernotes/dispatch/dispatch.ts`
- [ ] 2.3 Port the 18 vendor decoders (one file each):
- `apple/apple.ts` (L1-L126)
- `canon/canon.ts` (L1-L139)
- `casio/casio.ts` (L1-L6) — vendor-detection stub
- `fujifilm/fujifilm.ts` (L1-L130)
- `kodak/kodak.ts` (L1-L6) — vendor-detection stub
- `leica/leica.ts` (L1-L6) — vendor-detection stub
- `minolta/minolta.ts` (L1-L6) — vendor-detection stub
- `nikon/nikon.ts` (L1-L155)
- `olympus/olympus.ts` (L1-L129)
- `panasonic/panasonic.ts` (L1-L133)
- `pentax/pentax.ts` (L1-L140)
- `ricoh/ricoh.ts` (L1-L6) — vendor-detection stub
- `samsung/samsung.ts` (L1-L9)
- `sigma/sigma.ts` (L1-L6) — vendor-detection stub
- `sony/sony.ts` (L1-L14)
- `generic/generic.ts` (L1-L13)
- [ ] 2.4 `decodeMakerNotes(buffer, byteOrder, makeTag, modelTag?)` dispatcher — case-insensitive Make-prefix match, falls through to `generic`
- [ ] 2.5 Add `EXIF.MAKERNOTE_VENDOR_UNKNOWN`, `EXIF.MAKERNOTE_DECRYPT_FAILED`, `EXIF.MAKERNOTE_INVALID_HEADER` to `error-codes.ts`
- [ ] 2.6 Sibling `.md` for each implementation file (≥200 words, basic + advanced examples)
## 3. Vendor ciphers
- [ ] 3.1 Port `decryptNikonMakerNote` from `makernotes/nikon-cipher/nikon-cipher.ts` (L1-L84) into `packages/image/exif/src/makernotes/nikon-cipher/nikon-cipher.ts`
- [ ] 3.2 Port `decryptSonyMakerNote` from `makernotes/sony-cipher/sony-cipher.ts` (L1-L29) into `packages/image/exif/src/makernotes/sony-cipher/sony-cipher.ts`
- [ ] 3.3 Wire ciphers into the Nikon and Sony decoders; both decoders accept an injectable cipher function (default = the bundled one) for testability
- [ ] 3.4 KAT vector tests against legacy implementation
- [ ] 3.5 Sibling `.md`
## 4. Apple bplist parser
- [ ] 4.1 Port `parseBplist` from `makernotes/apple-bplist/apple-bplist.ts` (L1-L170) into `packages/image/exif/src/makernotes/apple-bplist/apple-bplist.ts`
- [ ] 4.2 Define typed value tree: `BplistValue = { type: 'dict' | 'array' | 'string' | 'integer' | 'real' | 'boolean' | 'date' | 'data' | 'uid' | 'null'; value: … }`
- [ ] 4.3 Add `EXIF.BPLIST_VERSION_UNSUPPORTED`, `EXIF.BPLIST_TRAILER_INVALID`, `EXIF.BPLIST_OBJECT_REF_OUT_OF_RANGE` to `error-codes.ts`
- [ ] 4.4 Wire `parseBplist` into the Apple decoder for HEIC + iPhone-shot JPEG
- [ ] 4.5 Sibling `.md`
## 5. ICC profile module
- [ ] 5.1 Port `icc/header/header.ts` (L1-L121), `icc/read/read.ts` (L1-L165), `icc/write/write.ts` (L1-L187), `icc/validate/validate.ts` (L1-L37), `icc/convert/convert.ts` (L1-L51), `icc/tag-table/tag-table.ts` (L1-L63) into `packages/image/exif/src/icc/{header,read,write,validate,convert,tag-table}/`
- [ ] 5.2 Define `IccProfile`, `IccHeader`, `IccTagEntry`, `IccProfileClass`, `IccColorSpace`, `IccRenderingIntent`, `IccTagSignature` types in `src/icc/types.ts`
- [ ] 5.3 Public API: `parseIccProfile`, `writeIccProfile`, `validateIccProfile`, plus convert helpers `classifyProfile`, `isSRGB`, `isAdobeRGB`, `isDisplayP3`, `profileDescription`
- [ ] 5.4 Add `EXIF.ICC_HEADER_TOO_SHORT`, `EXIF.ICC_PROFILE_TOO_SHORT`, `EXIF.ICC_TAG_OFFSET_OUT_OF_RANGE`, `EXIF.ICC_INVALID_SIGNATURE`, `EXIF.ICC_VALIDATION_FAILED` to `error-codes.ts`
- [ ] 5.5 Wire ICC into `readMetadata`: when an ICC profile segment is present in JPEG (`APP2 / ICC_PROFILE\0`) or PNG (`iCCP` chunk), populate `metadata.icc`
- [ ] 5.6 Add `includeIcc?: boolean` to `ReadOptions` (default `true`)
- [ ] 5.7 Sibling `.md` for each implementation file
## 6. GPS module
- [ ] 6.1 Port `gps/convert/convert.ts` (L1-L41) into `packages/image/exif/src/gps/convert/convert.ts`
- [ ] 6.2 Port `gps/timestamp/timestamp.ts` (L1-L38) into `packages/image/exif/src/gps/timestamp/timestamp.ts`
- [ ] 6.3 Public API: `gpsCoordinatesToDecimal`, `decimalToGpsCoordinates`, `parseGpsTimestamp`, `gpsAltitudeToMeters`
- [ ] 6.4 Add `EXIF.GPS_INVALID_REF`, `EXIF.GPS_OUT_OF_RANGE` to `error-codes.ts`
- [ ] 6.5 Sibling `.md`
## 7. Public surface
- [ ] 7.1 `package.json` `exports` map gains `./makernotes`, `./icc`, `./gps`, `./bplist` subpaths
- [ ] 7.2 Root `index.ts` re-exports `decodeMakerNotes`, `parseIccProfile`/`writeIccProfile`/`validateIccProfile`, GPS helpers, and `parseBplist`
- [ ] 7.3 `ReadOptions` gains `includeMakerNote?: boolean` and `includeIcc?: boolean` (both default `true`)
- [ ] 7.4 `ImageMetadata` gains optional `icc?: IccProfile`
- [ ] 7.5 `readMetadata` decodes MakerNote when `Make` tag present; falls through to raw bytes when `includeMakerNote: false` (back-compat)
## 8. Tests
- [ ] 8.1 `src/makernotes/dispatch/dispatch.test.ts` — vendor selection by Make tag; fall-through to generic
- [ ] 8.2 `src/makernotes/dispatch/dispatch.spec-conformance.test.ts` — `MakerNoteDecoder` interface pinned for every vendor; `MAKERNOTE_VENDOR_UNKNOWN` raised in strict mode
- [ ] 8.3 `src/makernotes/<vendor>/<vendor>.test.ts` for all 18 vendors — happy path against fixture
- [ ] 8.4 `src/makernotes/nikon-cipher/nikon-cipher.test.ts` — KAT vectors
- [ ] 8.5 `src/makernotes/sony-cipher/sony-cipher.test.ts` — KAT vectors
- [ ] 8.6 `src/makernotes/apple-bplist/apple-bplist.test.ts` — bplist00 v0/v1 fixtures
- [ ] 8.7 `src/makernotes/apple-bplist/apple-bplist.spec-conformance.test.ts` — `BPLIST_VERSION_UNSUPPORTED`, `BPLIST_TRAILER_INVALID`, `BPLIST_OBJECT_REF_OUT_OF_RANGE` paths
- [ ] 8.8 `src/icc/read/read.test.ts` — sRGB and Display-P3 fixtures
- [ ] 8.9 `src/icc/write/write.test.ts` — round-trip byte-equal
- [ ] 8.10 `src/icc/validate/validate.test.ts` — valid + 3 truncation variants
- [ ] 8.11 `src/icc/convert/convert.test.ts` — `classifyProfile`, `isSRGB`, `isAdobeRGB`, `isDisplayP3`
- [ ] 8.12 `src/icc/header/header.spec-conformance.test.ts` — `ICC_HEADER_TOO_SHORT`
- [ ] 8.13 `src/gps/convert/convert.test.ts` — round-trip decimal ↔ DMS within 1e-9; altitude positive vs negative ref
- [ ] 8.14 `src/gps/convert/convert.spec-conformance.test.ts` — `GPS_INVALID_REF`, `GPS_OUT_OF_RANGE`
- [ ] 8.15 `src/gps/timestamp/timestamp.test.ts` — date+time → Date; midnight/midday boundaries
- [ ] 8.16 `tests/dep-graph.test.ts` — subpath exports `./makernotes`, `./icc`, `./gps`, `./bplist` resolve
- [ ] 8.17 Every `it(...)` carries `{ timeout: 5000 }`
## 9. Documentation
- [ ] 9.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
- [ ] 9.2 README links every co-located `.md` under `## Modules`
- [ ] 9.3 Update `CLAUDE.md` `exif` package listing to reflect the restored surface (MakerNotes for 18 vendors, ICC read/write/validate/convert, GPS, Apple bplist, Nikon + Sony ciphers)
## 10. Pre-archive verification (the 5-gate triple)
- [ ] 10.1 `bun run check` — Biome 0/0/0 across the monorepo
- [ ] 10.2 `bunx turbo lint typecheck test --filter=@suaveplan/exif` — green
- [ ] 10.3 `bunx turbo test:coverage --filter=@suaveplan/exif` — 100/100/100/100
- [ ] 10.4 `bun scripts/openspec-conformance.ts @suaveplan/exif` — exit 0, no `error` findings
- [ ] 10.5 `bunx turbo build --filter=@suaveplan/exif` — green
## 11. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 11.1 Generics threaded; `expectTypeOf` assertions on `MakerNoteData`, `IccProfile`, `GpsCoordinate`, `BplistValue` discriminated unions
- [ ] 11.2 Zod schemas wired at every declared boundary (`MakerNoteData`, `IccProfile`, GPS inputs, `ReadOptions`)
- [ ] 11.3 Every `EXIF.*` code added in §2.5 / 3 / 4.3 / 5.4 / 6.4 thrown by at least one production path; no orphans
- [ ] 11.4 Strict-mode behaviors tested both on and off (e.g. `includeMakerNote: true` vs `false`; `includeIcc: true` vs `false`)
- [ ] 11.5 OTel histograms emitted for `exif.makernote.decode_ms`, `exif.icc.parse_ms`, `exif.gps.convert_ms`, asserted with a real in-memory `MeterProvider`
- [ ] 11.6 Lifecycle dedup tested: same image decoded twice produces identical output (deterministic; ciphers seeded from input only, no module-scope state)
- [ ] 11.7 No retroactive tick — every box above reflects work done in this change
## 12. Archive
- [ ] 12.1 Fold spec delta into `openspec/specs/exif/spec.md` (creating any missing requirements; existing requirements claimed in 2026-04-19 archive but never delivered are now backed by code)
- [ ] 12.2 Move `openspec/changes/exif-completion/` → `openspec/archive/<yyyy-mm-dd>-exif-completion/`
- [ ] 12.3 Add a note to `openspec/archive/2026-04-19-enterprise-exif/tasks.md` (or to the archive's `proposal.md`) pointing at this change as the actual delivery of §7.1, §7.2, §8.1
- [ ] 12.4 Refresh `MANIFEST.md` if the package's exports list materially changed
- [ ] 12.5 Update `STATUS.md` if the package was previously listed as Pending
## Audit Note (2026-05-02)
Audit verdict: **DEFERRED — 0/78 tasks evidenced in code**.
Evidence reviewed:
- `packages/image/exif/src/` has `binary/`, `core/`, `exif/`, `format/`, `ifd/`, `iptc/`, `jpeg/`, `xmp/`, `errors/`, `types.ts`. No `makernotes/`, no `icc/`, no `gps/`, no `bplist/`.
- The 18 vendor MakerNote decoders, the dispatcher, the Nikon + Sony ciphers, the Apple bplist parser, the ICC profile module (header/read/write/validate/convert/tag-table), and the GPS module are all absent.
- None of the new `EXIF.*` error codes (MAKERNOTE_*, BPLIST_*, ICC_*, GPS_*) appear in `errors/exif-error.ts`.
Missing pieces:
1. `makernotes/` directory with dispatcher + 18 vendor decoders + Nikon/Sony cipher pair + Apple bplist parser.
2. `icc/` module (six files: header, read, write, validate, convert, tag-table) wired into `readMetadata` for JPEG `APP2` and PNG `iCCP`.
3. `gps/` module (DMS↔decimal conversion + timestamp parsing) plus `ReadOptions` flag additions and the corresponding error codes.
Decision: leave change open. The MakerNotes block alone is 18+ vendor files with KAT cipher tests, plus the ICC and GPS modules — substantial implementation work flagged by the prior audit (entries 37/38/43 of `2026-04-19-enterprise-exif/tasks.md` were ticked but never delivered).