# Enterprise migration: `@suaveplan/imessage-adapter` - **Change ID**: `enterprise-imessage-adapter` - **Status**: PROPOSED - **Capability**: `imessage-adapter` - **Target package**: `@suaveplan/imessage-adapter` at `0.1.0` - **Target path**: `packages/communication/messaging/imessage-adapter/` - **Design**: ./design.md - **Spec delta**: ./specs/imessage-adapter/spec.md - **Source on main**: `/mnt/s/web/suaveplan/packages/universal/imessage-adapter` (`~718 LOC`) - **Enterprise plan**: `/mnt/s/web/suaveplan/enterprise/imessage-adapter.md` ## Why `@suaveplan/imessage-adapter` is the macOS-side ingress for any genesis messaging surface that wants to read from Apple's Messages app. It polls the `~/Library/Messages/chat.db` SQLite database, decodes the Apple-epoch / nanosecond timestamps, resolves handle + chat + attachment joins, and emits `ParsedIMessage` payloads — and a downstream `InboundMessage`-shaped projection — that any inbound-message router can consume without coupling to Apple's schema. The upstream package (`v1.0.0`) is functional but not enterprise-grade: - `require("bun:sqlite")` is reached through an `any`-cast cast at every call site, defeating type-checking and blocking testability. - No structured error codes — every failure is a swallowed `console`-less `error` event with a bare `Error`. - No Zod boundary on config; no schema-registry hook. - No OpenTelemetry instrumentation — no counters for messages emitted, no histogram for poll duration, no error counter. - No `AbortSignal` propagation on `start()` — `stop()` only. - The internal listener registry duplicates `@suaveplan/events`'s `EventBus`; the package should ride the shared bus. - Tests don't exercise the watcher loop end-to-end against a real SQLite chat.db fixture — they only construct the adapter and assert on the un-initialised error path. This change rewrites the package on genesis at enterprise quality without removing a single export from the upstream surface, preserving the contract that any future consumer (e.g. an inbound-message router, the existing `@suaveplan/api-plugin-messaging` plugin family, or a standalone agent) can rely on. ## What A net-new package at `packages/communication/messaging/imessage-adapter/` at `0.1.0`, preserving the upstream public surface: - Classes: `IMessageAdapter`, `IMessageWatcher`, `IMessageParser` - Types: `IMessageRow`, `HandleRow`, `ChatRow`, `AttachmentRow`, `ParsedIMessage`, `ParsedAttachment`, `IMessageAdapterConfig`, `IMessageWatcherConfig`, `InboundMessage`, `MessageSourceAdapter`, `IMessageWatcherEvents` Enterprise additions on top: 1. **Structured errors via `defineCodes("IMESSAGE", [...])`** — 10 stable codes (`DB_OPEN_FAILED`, `DB_QUERY_FAILED`, `WATCHER_NOT_RUNNING`, `WATCHER_ALREADY_RUNNING`, `ADAPTER_NOT_INITIALIZED`, `INVALID_CONFIG`, `POLL_ABORTED`, `POLL_FAILED`, `ATTACHMENT_PARSE_FAILED`, `APPLE_DATE_PARSE_FAILED`), each reachable from a real call site and asserted in tests. 2. **Zod-validated config boundary** at `IMessageWatcherConfigSchema` and `IMessageAdapterConfigSchema`, used by the constructor; invalid config throws `IMessageInvalidConfigError`. 3. **Injectable `SqliteReader` seam** (structural type) replaces the `require("bun:sqlite") as any` cast. Production wiring uses `bun:sqlite` lazily; tests use a real in-process `bun:sqlite` database populated from a chat.db-shaped fixture (no mocks; rule 2). 4. **Injectable `Clock` + `Timers` + `IdGenerator`** for deterministic polling, last-rowid pointer management, and envelope IDs. 5. **OTel instrumentation** — counters `imessage.messages_emitted_total`, `imessage.poll_errors_total`, `imessage.attachments_emitted_total`; histogram `imessage.poll_duration_ms`. 6. **AbortSignal propagation** — `start({ signal })`, `poll({ signal })`, and the internal scheduler all honour cancellation; aborted polls throw `IMessagePollAbortedError`. 7. **Bus bridge** — `IMessageWatcher` exposes an `EventBus`-shaped surface in parallel with its legacy `on/off` API so downstream subscribers can compose with `@suaveplan/events` directly. The legacy `on/off` API is preserved for back-compat. 8. **Idempotent rowid pointer** — `lastRowId` is settable + readable, and the watcher refuses to re-emit messages with `ROWID <= lastRowId` so persisted-pointer consumers can resume safely. 9. **100/100/100/100 coverage** with real SQLite fixtures (no mocks), sibling `.md` ≥200 words per implementation file, root `README.md`. ## Impact ### Affected dependents | Package | Impact | Action | |---|---|---| | `@suaveplan/events` | New consumer | None — additive | | `@suaveplan/error` | New consumer | None — additive | | `@suaveplan/opentelemetry` (via `@opentelemetry/api`) | New consumer | None — additive | | `@suaveplan/types` | New consumer of `Clock`, `IdGenerator`, `LoggerLike` | None — additive | | (future) `@suaveplan/api-plugin-messaging` / inbound-message router | Will consume `IMessageAdapter` as a `MessageSourceAdapter` | No live consumer today; future package can depend on `^0.1.0` | No genesis package currently imports `@suaveplan/imessage-adapter`, so this change has zero blast radius beyond adding the package to the manifest + flipping the STATUS row. ## Out of scope Nothing. Per `feedback_no_deferrals_on_migration.md` and `AGENTS.md §2b`, no "phase 2" is permitted. The whole upstream surface plus the enterprise additions land in this change, feature-complete, with 100/100/100/100 coverage.