234 lines
6.8 KiB
Markdown
234 lines
6.8 KiB
Markdown
# 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<MacosAddress>
|
|
readonly groups: ReadonlyArray<string>
|
|
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<MacosContact[]>
|
|
getContact(id: string, signal?: AbortSignal): Promise<MacosContact | null>
|
|
searchContacts(query: string, options?: SearchContactsOptions, signal?: AbortSignal): Promise<MacosContact[]>
|
|
createContact(data: CreateContactData, signal?: AbortSignal): Promise<MacosContact>
|
|
updateContact(id: string, data: Partial<CreateContactData>, signal?: AbortSignal): Promise<MacosContact>
|
|
deleteContact(id: string, signal?: AbortSignal): Promise<void>
|
|
listGroups(signal?: AbortSignal): Promise<MacosGroup[]>
|
|
addToGroup(contactId: string, groupName: string, signal?: AbortSignal): Promise<void>
|
|
removeFromGroup(contactId: string, groupName: string, signal?: AbortSignal): Promise<void>
|
|
}
|
|
|
|
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<MacosAddress>
|
|
}
|
|
```
|
|
|
|
### 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.
|