Initial Commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user