# OpenSpec Change: enterprise-macos-contacts - **Change ID**: enterprise-macos-contacts - **Package**: `@suaveplan/macos-contacts` - **Category**: universal/platform - **Target path**: `universal/platform/macos-contacts` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/macos-contacts.md` --- ## Summary Deliver `@suaveplan/macos-contacts` on the genesis branch from scratch at enterprise quality. The package provides a macOS Contacts.app bridge via AppleScript and the `osascript` subprocess, enabling programmatic reading, searching, and writing of contacts without native bindings. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all contact field types, OTel instrumentation on every I/O call, `AbortSignal` on all async operations, injectable `ProcessRunner` and `Clock` for testability, retry with backoff on AppleScript failures, and property-based tests for data-parsing logic. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `MacosContactsError`; stable error codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` injectable interfaces | | `@suaveplan/logger` (`workspace:^`) | Structured logging with OTel trace correlation | | `@suaveplan/retry` (`workspace:^`) | Retry with exponential backoff for `osascript` subprocess calls | | `zod` (`catalog:forms`) | Zod schemas for contact types and options | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters on contact read/write/search | | `tslib` (`catalog:build`) | Runtime helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`). --- ## Public API surface ### Core types ```ts interface MacosContact { readonly id: string readonly firstName?: string readonly lastName?: string readonly fullName: string readonly emails: ReadonlyArray<{ label: string; value: string }> readonly phones: ReadonlyArray<{ label: string; value: string }> readonly organization?: string readonly jobTitle?: string readonly note?: string readonly birthday?: string readonly addresses: ReadonlyArray readonly groups: ReadonlyArray readonly imageData?: string readonly creationDate?: string readonly modificationDate?: string } interface MacosAddress { label: string street?: string city?: string state?: string zip?: string country?: string countryCode?: string } interface MacosGroup { readonly id: string readonly name: string } ``` ### Client ```ts function createContactsClient(options?: ContactsClientOptions): ContactsClient interface ContactsClient { listContacts(options?: ListContactsOptions, signal?: AbortSignal): Promise getContact(id: string, signal?: AbortSignal): Promise searchContacts(query: string, options?: SearchContactsOptions, signal?: AbortSignal): Promise createContact(data: CreateContactData, signal?: AbortSignal): Promise updateContact(id: string, data: Partial, signal?: AbortSignal): Promise deleteContact(id: string, signal?: AbortSignal): Promise listGroups(signal?: AbortSignal): Promise addToGroup(contactId: string, groupName: string, signal?: AbortSignal): Promise removeFromGroup(contactId: string, groupName: string, signal?: AbortSignal): Promise } interface ContactsClientOptions { logger?: LoggerLike clock?: Clock processRunner?: ProcessRunner retryOptions?: RetryOptions } interface ListContactsOptions { groupName?: string limit?: number offset?: number } interface SearchContactsOptions { fields?: Array<'name' | 'email' | 'phone' | 'organization'> limit?: number } type CreateContactData = { firstName?: string lastName?: string emails?: Array<{ label: string; value: string }> phones?: Array<{ label: string; value: string }> organization?: string jobTitle?: string note?: string addresses?: Array } ``` ### Process runner abstraction ```ts interface ProcessRunner { run(script: string, signal?: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number }> } function createOsascriptRunner(options?: OsascriptRunnerOptions): ProcessRunner interface OsascriptRunnerOptions { timeout?: number } ``` ### Error codes ```ts type MacosContactsErrorCode = | 'CONTACTS_PERMISSION_DENIED' | 'CONTACTS_NOT_FOUND' | 'CONTACTS_SCRIPT_FAILED' | 'CONTACTS_PARSE_FAILED' | 'CONTACTS_CREATE_FAILED' | 'CONTACTS_UPDATE_FAILED' | 'CONTACTS_DELETE_FAILED' | 'CONTACTS_ABORT' function registerMacosContactsErrorCodes(): void ``` ### Zod schemas ```ts MacosContactSchema MacosAddressSchema MacosGroupSchema CreateContactDataSchema ContactsClientOptionsSchema ListContactsOptionsSchema SearchContactsOptionsSchema ``` --- ## Genesis additions 1. **`ProcessRunner` injection** — decouples `osascript` execution for testing without mocks 2. **Structured `MacosContactsError`** extending `SuaveplanError` with stable `CONTACTS.*` codes 3. **Full Zod schemas** for every public type 4. **Retry via `@suaveplan/retry`** on `ProcessRunner.run` failures 5. **`AbortSignal` threading** on all async operations 6. **OTel instrumentation** — spans on `listContacts`, `searchContacts`, `createContact`, `updateContact`, `deleteContact`; histogram on `osascript` execution duration 7. **Property-based tests** for contact parsing, search filtering, address normalization --- ## File layout ```text universal/platform/macos-contacts/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts types/ common.ts common.md core/ contacts-client/ contacts-client.ts contacts-client.md contacts-client.types.ts contacts-client.test.ts index.ts process-runner/ process-runner.ts process-runner.md process-runner.test.ts index.ts scripts/ list-contacts.applescript get-contact.applescript search-contacts.applescript create-contact.applescript update-contact.applescript delete-contact.applescript list-groups.applescript utils/ parse-contact.ts parse-contact.md parse-contact.test.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Platform constraint**: This package requires macOS with Contacts.app and AppleScript available. Tests that call `osascript` are guarded by a platform check and skipped on non-macOS hosts. **Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/logger`, `@suaveplan/retry` must be published to Verdaccio before this package can be built.