feat(rules): event-dispatch rule engine + tag-actions migration
Lands plan 3d7d9239 stages 1-3.
Stage 1 — rule registry. New `rules` table (schema/v1.json) backs an
event-dispatch surface: each row pairs an event topic + JSON match
predicate with a script id and optional `display` block. Pure-SQL
CRUD lives in rules.ts; tile-ipc exposes list/register/unregister
plus a cross-feature list-affordances. Manifest contributions reconcile
on feature install/load with content-hash ids for stability across
reboots. A drift test asserts no manifest declares both `resident:true`
and a non-empty `rules:[...]`.
Stage 2 — dispatcher. rule-engine.ts subscribes to every concrete
topic referenced by an enabled rule; on a fire, evaluates the
predicate (operators: $eq/$ne/$in/$exists/$startsWith/$endsWith/
$contains/$and/$or/$not, dotted paths, structural object equality,
unknown-op throws) and publishes `scripts:execute:<scriptId>` so
each script can lazy-load via `lazyEvents` on its own topic. Disabled
rules filter at fire time; recursion caps at 8 with `rules:loop-detected`
telemetry. Wildcard rules (`page:*`) are persisted but not yet
subscribed — concrete topics only for now (matcher is tested for
forward compat).
Stage 3 — tag-actions migration (big bang). Pairs are no longer
imperative settings serving `tag-actions:get-all`; each pair is one
source='user' rule with `topic: 'affordance:click'`,
`scriptId: 'tag-swap'`, `match` + `config` + `display`. tag-actions
drops `resident: true`; the old fat background.js is replaced by a
tiny lazy bg tile that wakes on `scripts:execute:tag-swap` and runs
the swap against the datastore. home.js does pair CRUD via
api.rules.register/unregister and migrates from legacy
`feature_settings[tag-actions:data].pairs` on first load. The
shared affordance lib (app/lib/tag-action-affordances.js) reads
api.rules.listAffordances and publishes `affordance:click` on toggle
click. Consumer manifests (lists/search/groups/tags/pagestream) drop
the legacy `tag-actions:*` topics and add `affordance:click` +
`rules:changed` + the `rules` capability. Playwright spec rewritten
to seed a tag-swap rule from the bg window and assert end-to-end.
Tests: 124 unit tests green (rules: 23, rule-engine: 39, drift: 1,
manifest: 42, tile-ipc/gate/feature-installer/registry: 60, plus the
existing trusted-builtin/features-strict suites). tsc clean.