experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

+44 -14
+44 -14
backend/electron/tile-loader.ts
··· 18 18 import { FeatureRegistry, type FeatureRegistryEntry } from './feature-registry.js'; 19 19 import { 20 20 parseManifestFile, 21 - detectManifestVersion, 22 21 type TileManifestV2, 23 22 type ParsedManifest, 24 23 } from './tile-manifest.js'; 25 24 import { type AnnotatedExtension, loadV2Tile, type TileCompatConfig } from './tile-compat.js'; 26 25 import { type ExtensionManifest } from './extensions.js'; 26 + import * as manifestCache from './manifest-cache.js'; 27 + import { DEBUG } from './config.js'; 27 28 28 29 // ─── Types ────────────────────────────────────────────────────────── 29 30 ··· 78 79 } 79 80 80 81 /** 82 + * Resolve a feature's manifest, preferring manifest_cache over disk. 83 + * 84 + * Phase C: the cache is the authoritative source. Disk fallback covers 85 + * two legitimate cases — v1 manifests (which the cache deliberately 86 + * doesn't store) and cache drift (e.g. ephemeral profile whose cache 87 + * was cleared mid-session between registry seed and feature load). 88 + * 89 + * Returning `null` means both paths failed; caller surfaces an error. 90 + */ 91 + function loadParsedManifest( 92 + entry: FeatureRegistryEntry, 93 + manifestPath: string, 94 + ): ParsedManifest | null { 95 + try { 96 + const cached = manifestCache.get(entry.id); 97 + if (cached && cached.manifest) { 98 + // The cache stores the full parsed v2 manifest JSON; it IS both 99 + // the validated `v2` view and the `raw` view as far as downstream 100 + // consumers care (they only touch v2 fields when version is 'v2'). 101 + return { 102 + version: 'v2', 103 + v2: cached.manifest, 104 + raw: cached.manifest as unknown as Record<string, unknown>, 105 + }; 106 + } 107 + } catch (err) { 108 + DEBUG && console.warn(`[tile-loader] manifest-cache read failed for ${entry.id}, falling back to disk:`, err); 109 + } 110 + 111 + if (!fs.existsSync(manifestPath)) return null; 112 + return parseManifestFile(manifestPath); 113 + } 114 + 115 + /** 81 116 * Load a single feature from a registry entry 82 117 */ 83 118 function loadSingleFeature( ··· 96 131 97 132 const manifestPath = path.join(entry.path, 'manifest.json'); 98 133 99 - // Check the feature directory still exists 100 - if (!fs.existsSync(manifestPath)) { 101 - return { 102 - id: entry.id, 103 - loaded: false, 104 - manifestVersion: 'v2', 105 - error: `Manifest not found at ${manifestPath}`, 106 - }; 107 - } 108 - 109 - // Parse manifest 110 - const parsed = parseManifestFile(manifestPath); 134 + // Cache-first read. feature-startup seeds manifest_cache from the 135 + // registry at boot (Phase B), so every v2 feature that went through 136 + // the installer is here. A miss means either a v1 feature (not 137 + // cached — we don't extract v1 decls) or drift (cache cleared mid- 138 + // session, disk-only manifest). Fall back to the disk parse so we 139 + // don't hard-fail in either case. 140 + const parsed = loadParsedManifest(entry, manifestPath); 111 141 if (!parsed) { 112 142 return { 113 143 id: entry.id, 114 144 loaded: false, 115 145 manifestVersion: 'v2', 116 - error: 'Failed to parse manifest', 146 + error: `Manifest not available for ${entry.id} (cache miss + disk parse failed)`, 117 147 }; 118 148 } 119 149