114 lines
5.5 KiB
Markdown
114 lines
5.5 KiB
Markdown
# 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.
|