feat(tiles): tile-lifecycle FSM + manifest-cache scaffold (Phase A)
Policy + scaffold for the tile lifecycle rewrite laid out in
docs/tile-lifecycle-fsm.md. No callers wired yet — later phases will
route existing tile-launcher / window-open / cmd-dispatch paths
through this.
- `backend/electron/tile-fsm.ts` — pure transition table. No
electron/node imports. `validateTransition(from, trigger)` returns
`{ok, to | error}`. Source of truth for the 7 states (unregistered,
registered, loading, ready, visible, unloading, crashed) and 11
triggers. Policy helpers `isDispatchable` / `acceptsDynamicRegistration`
encode the doc's "dispatch requires ready" and "dynamic registration
only in ready/visible" invariants.
- `backend/electron/tile-lifecycle.ts` — side-effectful enforcement
engine. Owns the authoritative `tileId:entryId → state` map.
`transition(tileId, entryId, trigger)` is the ONLY entry point for
state mutation; no back-door setter (`setStateUnchecked` was
considered and removed — tests walk the FSM like production would).
Transitions to `unregistered` delete the record so uninstall/install
cycles don't leak. Keys are JSON-encoded `[tileId, entryId]` so
featureIds containing colons (atproto handles) round-trip cleanly.
`resetForTests()` throws outside `NODE_ENV=test`.
- `backend/electron/manifest-cache.ts` — SQLite cache for parsed
manifest declarations (commands, nouns, shortcuts, lazyEvents,
resolved capability grant). Stubbed install + boot-discovery write
paths land in Phase B; read APIs (`listAllCommands` /
`listAllShortcuts` / `listAllLazyEvents` / `list` / `get`) are
ready for Phase C to consume. Named `manifest-cache` (not
`feature-registry`) to keep the boundary with the existing
install-metadata module clean.
- `manifest_cache` table DDL added to `datastore.ts`. Idempotent
CREATE IF NOT EXISTS, includes `disabledAt` so the Features pane
can toggle without loss of cache. Desktop-local only (not in the
synced v1.json schema).
- 38 unit tests in `tests/unit/tile-fsm.test.js` (every allowed
transition + every disallowed combination + reachability) and
`tests/unit/tile-lifecycle.test.js` (happy path, crash recovery,
dispatch gating, map-leak prevention, colon-tolerant keys,
production guard). Test script sets `NODE_ENV=test` so
`resetForTests()` passes its guard.
No runtime behavior change. Phase B wires manifest_cache writes on
install / boot discovery; Phase C swaps cmd panel & shortcut registry
from pubsub subscriptions to cache reads; Phase D unifies window
creation; Phase E adds load-on-dispatch.