feat(tiles): tile-loader reads from manifest_cache (Phase C)
Phase C of the tile-lifecycle rewrite. Phase B seeded the cache; this
commit flips tile-loader.ts over to read from it as the primary source
of truth at boot. Disk parse becomes the fallback, not the default.
- `tile-loader.ts`: new `loadParsedManifest(entry, manifestPath)`
helper. Tries `manifestCache.get(entry.id)` first; on a hit we
construct a `ParsedManifest` directly from the cached
TileManifestV2. On a miss (or cache-read throw), falls back to
`parseManifestFile(manifestPath)` so:
- v1 manifests still load (cache deliberately doesn't store v1)
- drift scenarios (ephemeral profile whose cache was cleared
between registry seed and feature load) still recover
- first-boot-with-no-cache still works
Cache-read failure logs only in DEBUG mode — not an error, just a
heads-up that the primary path missed.
- Drop unused `detectManifestVersion` import. `loadParsedManifest`
either returns a pre-validated cached v2 or delegates to
`parseManifestFile`, which does its own version detection.
No runtime behavior change when the cache is populated — the cached
manifest is the exact same object `parseManifestFile` would have
returned for a v2 feature. The diff is structural: the hot read path
is now SQLite-backed instead of synchronous disk I/O per feature on
every boot.
Scope choice: cmd panel / shortcut registry / lazyEvents interceptors
still consume their data via pubsub from `tile-lazy.ts`'s
`registerLazyTile` path. That stays unchanged because `registerLazyTile`
now receives its manifest from the cache-sourced `loadParsedManifest`,
so the cache is effectively driving stub registration. Fully swapping
the consumers to read the cache directly (bypassing pubsub) is
deferred — it's entangled with Phase E (load-on-dispatch) for the
shortcut case, and better landed together.
2250/2250 unit tests pass.