Three kinds (dictionary, font, texture) of replica sync now visibly
duplicate each other; this PR extracts the shared shape now that the
abstraction is well-validated, per the plan's "extract only after the
second kind validates" guidance.
- New `services/sync/adapters/_helpers.ts` — `unwrap` field-envelope,
`singleFileFilenameFromManifest`, `singleFileBinaryEnumerator`, and
a default `computeId` for kinds with `record.contentId`. Wired into
font + texture adapters (dictionary stays bespoke — multi-file
enumeration and a different identity recipe).
- `useReplicaPull.ts` collapses the three near-identical
`buildXPullDeps` (~80 lines each) into a single
`buildReplicaPullDeps<T>` factory plus three small `ReplicaPullConfig`
records (~12 lines each). Dispatch stays a typed `switch` to keep
the generic record type sound under contravariance.
- New `services/sync/migrateLegacy.ts` — `migrateLegacyReplicas<T>`
helper that owns the rehash-flat-path → `<bundleDir>/<filename>`
migration. `migrateLegacyFonts` and `migrateLegacyTextures` are now
thin per-kind configs, ~15 lines each (down from ~60).
Net: +143 / -291 across 5 files plus 2 new helpers. No behavior
change; 3933 tests still pass.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>