abbu 0.2.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +281 -8
  3. data/bin/abbu +230 -53
  4. data/docs/ABBU.md +369 -7
  5. data/docs/API_STABILITY.md +189 -0
  6. data/docs/CHANGELOG.md +179 -0
  7. data/docs/CONTRIBUTING.md +25 -3
  8. data/docs/FORMAT_COMPATIBILITY.md +97 -0
  9. data/docs/RELEASING.md +75 -0
  10. data/docs/TODO.md +73 -62
  11. data/lib/abbu/archive.rb +80 -6
  12. data/lib/abbu/contact.rb +6 -3
  13. data/lib/abbu/diagnostic.rb +27 -0
  14. data/lib/abbu/exporters/csv_exporter.rb +2 -2
  15. data/lib/abbu/exporters/json_exporter.rb +7 -1
  16. data/lib/abbu/exporters/vcard_document.rb +54 -0
  17. data/lib/abbu/exporters/vcard_encoding.rb +54 -0
  18. data/lib/abbu/exporters/vcard_exporter.rb +44 -27
  19. data/lib/abbu/group.rb +28 -0
  20. data/lib/abbu/group_catalog.rb +32 -0
  21. data/lib/abbu/image_extractor.rb +134 -0
  22. data/lib/abbu/live_store.rb +117 -0
  23. data/lib/abbu/parse_error.rb +13 -0
  24. data/lib/abbu/parsers/plist_parser.rb +41 -11
  25. data/lib/abbu/parsers/sqlite_parser.rb +139 -65
  26. data/lib/abbu/query.rb +92 -0
  27. data/lib/abbu/schema_inspector.rb +133 -0
  28. data/lib/abbu/source.rb +53 -0
  29. data/lib/abbu/source_catalog.rb +32 -0
  30. data/lib/abbu/timestamp_range.rb +40 -0
  31. data/lib/abbu/utils/contact_identity.rb +97 -0
  32. data/lib/abbu/utils/deduplicator.rb +130 -2
  33. data/lib/abbu/utils/image_resolver.rb +59 -0
  34. data/lib/abbu/utils/label_normalizer.rb +15 -0
  35. data/lib/abbu/utils/source_descriptor.rb +35 -0
  36. data/lib/abbu/version.rb +1 -1
  37. data/lib/abbu.rb +15 -2
  38. data/sig/group.rbs +36 -0
  39. data/sig/query.rbs +21 -0
  40. data/sig/source.rbs +25 -0
  41. data/sig/vcard_exporter.rbs +10 -0
  42. data/tasks/abbu.rake +1 -1
  43. metadata +33 -15
  44. data/bin/cleanse +0 -7
  45. data/bin/console +0 -10
  46. data/bin/dev +0 -7
  47. data/bin/lint +0 -7
  48. data/bin/outdated +0 -7
  49. data/bin/test +0 -7
data/docs/ABBU.md CHANGED
@@ -9,9 +9,34 @@
9
9
  They are **not** a single file format — they are a macOS "package" (a directory bundle that Finder
10
10
  presents as a single file). This means you can inspect the contents with `ls` or `open -a Finder`.
11
11
 
12
+ ## Evidence Standard
13
+
14
+ Apple does not publish a stable specification for every internal Contacts
15
+ archive schema represented by `.abbu` bundles. This document therefore
16
+ distinguishes observed repository-fixture behavior from documented Apple APIs
17
+ and from hypotheses that still require verification.
18
+
19
+ Never infer Apple Contacts storage semantics merely from Core Data table or
20
+ column names. Require observed fixture evidence, Apple documentation where
21
+ available, or reproducible verification, and record consequential discoveries
22
+ in this document.
23
+
24
+ For each new schema, relationship, source layout, image convention, or version
25
+ variation:
26
+
27
+ 1. record the macOS or Contacts version when known;
28
+ 2. identify the synthetic fixture, SQLite query, plist key path, file evidence,
29
+ or Apple documentation supporting the conclusion;
30
+ 3. add a deterministic regression fixture and spec; and
31
+ 4. label unresolved interpretations as hypotheses rather than format guarantees.
32
+
33
+ Real address-book exports contain sensitive personal data. Use them only for
34
+ local verification, sanitize the observed behavior into deterministic synthetic
35
+ fixtures, and never commit the original contacts, photos, or account identifiers.
36
+
12
37
  ## Structure
13
38
 
14
- Typical contents of a `.abbu` bundle:
39
+ The supported synthetic fixtures and observed exports use layouts such as:
15
40
 
16
41
  ```text
17
42
  Contacts.abbu/
@@ -36,12 +61,16 @@ Contacts.abbu/
36
61
 
37
62
  ### 1. SQLite (modern macOS)
38
63
 
39
- Newer macOS versions store the address book in a single SQLite database:
64
+ Supported modern fixtures contain one or more SQLite databases named:
40
65
 
41
66
  ```
42
67
  AddressBook-v22.abcddb
43
68
  ```
44
69
 
70
+ The following tables and mappings are exercised by the repository's generated
71
+ SQLite fixture and parser specs. Their names alone are not evidence that the
72
+ same semantics apply to every macOS version.
73
+
45
74
  Key tables:
46
75
 
47
76
  | Table | Purpose |
@@ -77,12 +106,337 @@ Notable columns in `ZABCDRECORD`:
77
106
  | `ZPRONOUNS` | Pronouns |
78
107
  | `ZRINGTONE` | Ringtone |
79
108
  | `ZTEXTTONE` | Text tone |
109
+ | `ZCREATIONDATE` | Optional record creation timestamp |
110
+ | `ZMODIFICATIONDATE` | Optional record modification timestamp |
111
+
112
+ ### Timestamp and source provenance
113
+
114
+ Observed Contacts databases may include `ZCREATIONDATE` and `ZMODIFICATIONDATE` on
115
+ `ZABCDRECORD`. ABBU interprets numeric values in those columns as Apple absolute time:
116
+ seconds since 2001-01-01 00:00:00 UTC. The columns are optional because exported schemas
117
+ vary across macOS releases and account providers; when either column is absent or invalid,
118
+ the corresponding `Contact` value is `nil`.
119
+
120
+ These values describe timestamps stored on the record. They must not be interpreted as
121
+ proof of a user-initiated creation or edit, because syncing and migration can also affect
122
+ them.
123
+
124
+ Every parsed contact includes source provenance with the absolute source path, its path
125
+ relative to the `.abbu` root, and whether it came from the root bundle or a database under
126
+ `Sources/<identifier>/`. Legacy plist contacts receive the same file-level provenance.
127
+
128
+ ### Source container evidence
129
+
130
+ `Archive#sources` and `LiveStore#sources` group the existing `SourceDescriptor`
131
+ file evidence into root (`.`) and observed `Sources/<identifier>` containers.
132
+ This is ABBU's grouping convention over input paths, not discovery of Apple's
133
+ private account/container tables. It introduces no new storage-column inference.
134
+ Every selected input file appears, including databases with no contacts; files
135
+ in the same observed container are retained individually. Legacy plist files
136
+ outside `Sources/` belong to root. Parser selection and discovery scope are unchanged.
137
+
138
+ `Source#identifier` preserves the observed directory spelling, while `provider`
139
+ remains nil even for a directory named `iCloud`. Container paths are local identities
140
+ within that opened input, not globally stable IDs across moved archives or snapshots.
141
+ Contacts link through their original file-level `source[:path]`; the source hash
142
+ is neither replaced nor enriched with speculative provider metadata. `group_names`
143
+ reports distinct membership strings only; the group API below preserves identity.
144
+
145
+ `spec/abbu/source_spec.rb` copies the existing deterministic root database into
146
+ root, `Sources/iCloud`, and `Sources/Équipe`, including two database filenames in
147
+ one container and colliding contact IDs across containers. These fixtures verify
148
+ grouping, provenance preservation and unknown-provider behavior, not an Apple
149
+ account schema. Source listing parses/caches contacts and retains the existing
150
+ strict/tolerant diagnostics and live consistency limitations below.
151
+
152
+ ### Group membership evidence
153
+
154
+ The existing synthetic fixture joins `Z_ABCDCONTACTGROUP.Z_GROUP` to
155
+ `ZABCDRECORD.Z_PK` and filters memberships by `Z_CONTACT`. The group parser now
156
+ retains that joined key alongside the exact `ZFIRSTNAME` value instead of discarding
157
+ the key. `Contact#group_memberships` preserves every returned join row; the existing
158
+ `Contact#groups` label array and exporters are unchanged. No label normalization
159
+ or inference from other column names is introduced.
160
+
161
+ `Archive#groups`, `LiveStore#groups`, and `Source#groups` aggregate this evidence by
162
+ absolute database path and record key. Keys are file-local, not global Apple IDs.
163
+ `Group#source` is file provenance; its `identifier` connects to the observed source
164
+ container. Group contacts retain original object identities with repeated joins
165
+ collapsed only in the navigable contact set, not in raw membership evidence.
166
+ Reverse lookup uses `groups_for(contact)` on the input or source; `Query#in_group`
167
+ intersects the caller's contacts with that snapshot without name-based matching.
168
+
169
+ `spec/abbu/group_spec.rb` extends the deterministic fixture with duplicate joins,
170
+ same-name/different-key groups, null and Unicode names, and repeated keys in root
171
+ and multiple files under `Sources/Équipe`. These establish collision boundaries
172
+ and name preservation through model and JSON output, not a new Apple schema.
173
+ No Apple version is asserted for this synthetic variation.
174
+
175
+ Only joined groups with parsed contacts are represented. Empty/unreferenced rows,
176
+ dangling joins, and plist group relationships remain unsupported; their semantics
177
+ need separate evidence. Missing membership tables retain existing diagnostics and
178
+ strict-mode behavior. Building groups adds no database reads beyond contact parsing
179
+ and makes no stronger live snapshot guarantee. JSON listings expose raw names and
180
+ file paths and must be treated as sensitive.
181
+
182
+ ### Opt-in live Contacts stores
183
+
184
+ The CLI uses `--live` only for auto-discovery and `--live-path PATH` for an explicit
185
+ store. These forms are mutually exclusive and accept no positional archive/path arguments.
186
+ Option ordering does not change input selection. Read-only handles do not establish
187
+ snapshot consistency across an actively changing Contacts store.
188
+
189
+ `Abbu.open_live` and the CLI's live modes can read an AddressBook directory
190
+ without first exporting an `.abbu` archive. This mode is deliberately separate from
191
+ `Abbu.open`: archive validation and plist fallback do not apply to a live store.
192
+
193
+ When no path is supplied on macOS, ABBU checks the observed per-user location at
194
+ `~/Library/Application Support/AddressBook`. Callers may instead provide a directory,
195
+ which keeps automation and tests independent of the host platform and user account.
196
+ ABBU discovers databases directly under that directory and one level below
197
+ `Sources/<identifier>/`, preserving the same root/source provenance used for archives.
198
+ Only files matching the observed `AddressBook-v*.abcddb` shape are candidates; this
199
+ filename pattern is discovery evidence, not a guarantee of stable Apple semantics.
200
+
201
+ Every live-store database is opened using SQLite's read-only mode. ABBU has no live-store
202
+ write API and never creates, updates, or deletes Contacts data. macOS privacy controls may
203
+ deny access even when the path exists. In that case ABBU raises
204
+ `Abbu::LiveStore::PermissionError` with instructions to grant the calling terminal or
205
+ application Full Disk Access in **System Settings → Privacy & Security → Full Disk
206
+ Access**. Missing directories and databases raise `Abbu::LiveStore::NotFoundError`, and
207
+ automatic discovery without a caller-supplied path raises
208
+ `Abbu::LiveStore::UnsupportedPlatformError` outside macOS.
209
+
210
+ The repository verifies live-store behavior only with deterministic synthetic SQLite
211
+ fixtures. It does not inspect or commit a developer's real Contacts store.
212
+
213
+ ### WAL and concurrent-writer evidence
214
+
215
+ `spec/abbu/live_store_wal_spec.rb` copies the existing synthetic root database into
216
+ a temporary directory and opens a separate writer connection in WAL mode. No
217
+ real Contacts data or macOS privacy permission is required. The tests deliberately
218
+ keep the writer open, disable its automatic checkpoint, and interleave operations
219
+ at known boundaries instead of using sleeps or timing races.
220
+
221
+ The fixture demonstrates that ABBU reads committed WAL changes while the writer
222
+ remains connected. Its database and WAL bytes stay unchanged across the read,
223
+ all ABBU connections use `readonly: true`, and traced statements contain no writes
224
+ or checkpoint requests. An uncommitted writer transaction is not visible. After
225
+ commit, a new LiveStore sees the new value; an existing store retains its cached
226
+ contacts.
227
+
228
+ There is an important consistency limit: the parser does not enclose all queries
229
+ in a read transaction. A commit between the contact-row SELECT and an email SELECT
230
+ can produce an old name with a new email from the same database. SQLite's snapshot
231
+ isolation applies within a read transaction, not across ABBU's independent
232
+ statements. Multiple database files are also read independently; there is no
233
+ cross-database snapshot guarantee. These tests characterize existing behavior,
234
+ not a new snapshot API or a guarantee for every Contacts/SQLite version.
235
+
236
+ SQLite uses `-wal` and `-shm` sidecars for WAL operation. Read-only database access
237
+ does not promise that shared-memory lock/index state is byte-for-byte unchanged;
238
+ the tests intentionally do not make that claim. Do not remove sidecars, checkpoint
239
+ a live Contacts database, or copy only its main file to try to obtain a snapshot.
240
+ Use an independently verified consistent export/backup for snapshot-sensitive work.
241
+ Missing/inaccessible sidecar and filesystem-lock behavior remain deployment-specific
242
+ limitations, not behavior established by the writable temporary fixture.
243
+
244
+ References: [SQLite WAL](https://www.sqlite.org/wal.html) and
245
+ [SQLite isolation](https://www.sqlite.org/isolation.html).
246
+
247
+ ### Provenance-aware identity evidence
248
+
249
+ ABBU treats deduplication as a suggestion boundary rather than proof that two records are
250
+ the same person. `Utils::Deduplicator#matches` compares normalized email, phone, name, and
251
+ organization signals while returning both original contacts, both source records, raw
252
+ evidence, normalized comparison values, confidence, and ambiguity status.
253
+
254
+ Email comparison trims surrounding whitespace and applies Unicode-aware case folding.
255
+ Names and organizations use Unicode NFKC normalization, case folding, and whitespace or
256
+ punctuation normalization without transliterating distinct characters. Explicit `+` and
257
+ `00` phone forms are compared as international numbers. The trimmed raw value must start
258
+ with a literal ASCII `+` or contiguous `00`; punctuation removal never establishes an
259
+ international prefix. For example, `(001) 512-555-0100` remains national-format evidence.
260
+ National-format numbers remain
261
+ source-local evidence because ABBU has no country or numbering-plan evidence with which
262
+ to infer a global identity.
263
+
264
+ SQLite primary keys, source identifiers, private Apple link identifiers, and image stems
265
+ are not treated as global contact identifiers. The repository fixtures do not establish
266
+ such semantics. Competing candidates and weak name/organization or source-local phone
267
+ matches remain ambiguous, and no contact is merged unless the caller supplies an explicit
268
+ merge policy.
269
+
270
+ ### Image resolution and extraction
271
+
272
+ The synthetic SQLite fixture demonstrates a `ZIMAGEURI` value whose stem matches a file
273
+ under an `Images/` directory. Resolution covers the root bundle and nested
274
+ `Sources/<identifier>/Images/` directories. When duplicate stems exist, ABBU uses the
275
+ contact's database provenance to select only an image beside that database; it does not
276
+ guess when the available evidence remains ambiguous.
277
+
278
+ `Archive#extract_images(output_dir)` and the CLI `--extract-images DIR` copy resolved
279
+ images without changing `Contact#image_uri`, `Contact#image_path`, or `Contact#source`.
280
+ Exported filenames combine a sanitized contact name, the original image identifier, and
281
+ a stable provenance digest. Path separators, control characters, and reserved filename
282
+ characters cannot create subdirectories or traverse outside the selected output directory.
283
+
284
+ The selected output directory (including symlinked parents) is resolved to its canonical
285
+ directory before copying; callers must control that directory and prevent concurrent
286
+ directory replacement. Each image is created exclusively with owner-only permissions.
287
+ Existing destinations are never overwritten, including regular files, hard links, and
288
+ symlinks (even dangling ones). These collisions produce a `destination_exists` extraction
289
+ diagnostic and no successful file record; other images continue. Repeating extraction
290
+ into the same directory therefore reports collisions instead of replacing earlier output.
291
+ Directory creation/resolution failures raise filesystem errors before extraction begins.
292
+
293
+ Extraction diagnostics are a separate API from `archive.diagnostics`: they may contain
294
+ contact names, raw image identifiers, source paths, and filesystem error details. Treat
295
+ them and the CLI's image warnings as sensitive contact data, not safe-to-publish logs.
296
+
297
+ ABBU recognizes JPEG, PNG, GIF, and common HEIF/HEIC-compatible brands from file
298
+ signatures and chooses the exported extension from those bytes rather than the source
299
+ extension. Unknown content is reported as a diagnostic instead of being relabeled. HEIC
300
+ data is copied unchanged; ABBU does not transcode it.
301
+
302
+ No repository fixture currently demonstrates a reliable Apple thumbnail-versus-full-size
303
+ naming or selection rule. ABBU therefore exports the image resolved by the observed
304
+ `ZIMAGEURI` relationship and does not infer size semantics from filenames or directories.
305
+
306
+ ### Recovery and diagnostics
307
+
308
+ By default, ABBU recovers from malformed individual plist records, missing
309
+ optional SQLite relationship tables, and unresolved image references. Each
310
+ recovery appends an `Abbu::Diagnostic` to `archive.diagnostics` with a category,
311
+ parser, source path, non-PII context, and a stable message. Required contact
312
+ schema failures still raise because no evidence-backed contact record can be
313
+ recovered safely.
314
+
315
+ An absent optional SQLite table produces one diagnostic per database and table,
316
+ regardless of contact count. ABBU does not place record identifiers or raw image
317
+ references in these schema- and image-level diagnostic contexts.
318
+
319
+ Pass `strict: true` to `Abbu.open` or `--strict` to the CLI to raise
320
+ `Abbu::ParseError` on the first recoverable condition. The CLI prints a
321
+ diagnostic summary to standard error so exported data on standard output remains
322
+ pipeable.
323
+
324
+ ### Schema diagnostics
325
+
326
+ `Archive#schema_report` and `abbu Contacts.abbu --schema` inspect every discovered
327
+ SQLite database and return deterministic schema metadata. Reports identify recognized
328
+ and unrecognized tables and columns, recognized items that are absent, declared SQLite
329
+ types, primary-key and nullability metadata, source provenance, and exact owner/contact-
330
+ style column names that may represent contact links.
331
+
332
+ These reports are research evidence, not parser mappings. In particular, a
333
+ `contact_link_candidate` flag records only an exact column-name shape such as `ZOWNER`,
334
+ `ZCONTACT`, or `Z_CONTACT`; it does not claim a foreign-key target or assign Apple
335
+ Contacts semantics. Unknown tables and columns must be reproduced in a sanitized fixture
336
+ or supported by documentation before ABBU uses them to populate contacts.
337
+
338
+ Missing recognized tables and columns remain visible as diagnostic observations. The
339
+ parser tolerates absent established email, phone, and postal-address tables by returning
340
+ empty collections, while the schema report preserves the absence for compatibility
341
+ research. If one of those tables exists but lacks an expected column, parsing raises the
342
+ SQLite schema error instead of silently treating the contact as having no corresponding
343
+ data. The core `ZABCDRECORD` table remains required for contact parsing.
344
+
345
+ ### Labeled values
346
+
347
+ The synthetic SQLite and plist fixtures include both custom labels and Apple's
348
+ observed standard-label wrapper, such as `_$!<Work>!$_`. ABBU exposes the
349
+ human-facing value as `label` (`Work`) and preserves the exact stored value as
350
+ `raw_label`. Custom, blank, malformed, Unicode, and already-normalized labels
351
+ are not otherwise rewritten. Direct plist keys such as `Birthday` have no
352
+ stored label, so their normalized label is derived from the key and
353
+ `raw_label` is `nil`.
354
+
355
+ Normalization applies to email addresses, phone numbers, postal addresses,
356
+ URLs, related names, date components, and instant-message handles. JSON keeps
357
+ both values. Human-facing CSV uses normalized labels, while vCard anniversary
358
+ labels prefer `raw_label` so Apple label wrappers and custom source values
359
+ survive parse → model → interchange export.
80
360
 
81
361
  ### 2. Plist / `.abcdp` (legacy macOS)
82
362
 
83
- Older macOS versions stored each contact as a separate binary plist file under `Records/`.
84
- Each file is a serialised `ABPerson` dictionary. The `abbu` gem currently stubs this parser
85
- and returns an empty array with a warning.
363
+ Older macOS versions stored contacts as separate plist files under `Records/`.
364
+ The repository fixtures demonstrate dictionaries that the plist parser
365
+ normalizes into the same contact model used by the SQLite parser. Additional
366
+ plist keys or layouts require fixture evidence before they are treated as
367
+ supported semantics.
368
+
369
+ ### vCard serialization evidence
370
+
371
+ The 0.8.0 exporter implements TEXT escaping and structured components from
372
+ [RFC 2426 §§2.3–2.6](https://www.rfc-editor.org/rfc/rfc2426), and CRLF/grouping
373
+ and unfolding from [RFC 2425 §5.8.1](https://www.rfc-editor.org/rfc/rfc2425).
374
+ ABBU folds conservatively at 75 **octets**, including the continuation space,
375
+ without splitting a UTF-8 code point. Escaping happens before folding; decoding
376
+ must unfold first. Commas, semicolons and backslashes are escaped in TEXT;
377
+ CRLF, bare CR and LF become the logical newline escape. This preserves logical
378
+ text, not the original newline byte convention. Input Contact values are unchanged.
379
+ URI properties use percent encoding, preserving existing escapes and URI
380
+ delimiters rather than applying TEXT rules. This is not a general URI validator.
381
+
382
+ `spec/abbu/exporters/vcard_exporter_fidelity_spec.rb` contains deterministic
383
+ inline wire fixtures and an independent fixture-only decoder. It exercises
384
+ repeated properties, injection-shaped input, structured names/addresses,
385
+ multiline notes, UTF-8 folding boundaries, per-card group numbering, and both
386
+ SQLite/plist → Contact → vCard label preservation. The temporary SQLite fixture
387
+ only varies an already supported `ZLABEL`; the plist fixture uses the existing
388
+ `Email.values[].label` mapping. Neither adds guessed Apple storage semantics.
389
+
390
+ The reviewed extension audit is deliberately bounded:
391
+
392
+ | Surface | Implemented rule / evidence limit |
393
+ | --- | --- |
394
+ | `itemN`, `X-ABLABEL` | Standard group syntax pairs repeated properties with ABBU's existing raw-label extension. Labels remain exact after TEXT decoding; numbering is local to each card, not a stored Apple ID. No Apple import validation is claimed. |
395
+ | `X-ABDATE` | Existing anniversary mapping retained, now grouped with its label. Removed the fabricated `type=pref`. Other date collections remain unsupported by this exporter. |
396
+ | `TYPE`, `PREF` | Only exact ASCII names from RFC 2426's EMAIL/TEL/ADR lists and RFC 4770's IMPP list are recognized from display labels. No whitespace trimming, Unicode folding, custom-label tokenization, or Mobile→CELL guess. Raw labels remain separately available. Explicit `PREF` is recognized; no priority is inferred from row order. |
397
+ | `UID` | Not generated: SQLite keys and source paths are not demonstrated global contact identifiers. |
398
+ | `IMPP` | URI-valued property per [RFC 4770](https://www.rfc-editor.org/rfc/rfc4770). Existing service-to-scheme convention retained with scheme syntax checks and address encoding. A syntactically valid scheme does not prove service interoperability; absent service retains legacy `unknown:`. |
399
+ | `X-SOCIALPROFILE` | Existing service/username extension retained; parameter service must be an ASCII token and username is escaped TEXT. No provider URL or Apple import semantics are inferred. |
400
+ | `ADR` | Seven components, each independently escaped. No new PO box/extended-address storage mapping. Missing label no longer fabricates HOME. |
401
+ | Partial/lunar dates, phonetic names, verification code | Existing extensions retained, not certified as standard vCard 3.0 date forms or Apple alternate-calendar semantics. |
402
+ | `PHOTO` | Existing local file URI retained; portable embedding belongs to #30. |
403
+
404
+ Unsafe parameter values and control characters fail rather than create extra
405
+ properties or silently discard evidence. Serialization completes before file
406
+ opening/stdout emission, so validation failures produce no partial export and
407
+ leave an existing output file untouched. Filesystem failures after opening can
408
+ still leave partial files. Exports contain sensitive contact values and photo
409
+ paths; callers must choose appropriate destinations and permissions.
410
+
411
+ Email preference uses `TYPE=INTERNET,PREF`, retaining the default address type
412
+ as required by RFC 2426's email parameter grammar; TEL includes the standard
413
+ `PCS` token. Unknown extension/registered type names are preserved as labels,
414
+ not asserted to be registered by ABBU's deliberately bounded built-in list.
415
+
416
+ These are standards-backed serialization guarantees and synthetic regression
417
+ observations, not a full-fidelity ABBU backup or certification against a specific
418
+ macOS/Contacts build. A sanitized real Apple export/import corpus remains a
419
+ separate compatibility gate before broader claims.
420
+
421
+ ## Repository Evidence
422
+
423
+ - [Compatibility regression matrix](FORMAT_COMPATIBILITY.md): always-on XML,
424
+ sparse/complete SQLite, root/source/mixed layout profiles and their explicit
425
+ historical evidence gaps. Filename suffixes are not macOS version guarantees.
426
+ - `spec/fixtures/TestContacts.abbu/` exercises the supported synthetic SQLite,
427
+ nested source, and image-resolution behavior.
428
+ - `spec/fixtures/PlistContacts.abbu/` exercises the supported synthetic legacy
429
+ plist behavior.
430
+ - `spec/fixtures/identity_cases.yml` contains deterministic synthetic cross-source and
431
+ Unicode near-collision identity evidence.
432
+ - `spec/support/fixture_generator.rb` is the reproducible source for generated
433
+ SQLite fixture structure and data.
434
+ - `spec/abbu/schema_inspector_spec.rb` builds deterministic temporary SQLite
435
+ schemas for missing tables, unknown contact-link candidates, and column drift.
436
+
437
+ These fixtures prove only the variations they contain. Table names, column
438
+ names, entity numbers, UUIDs, and directory names alone are not sufficient
439
+ evidence for new behavior.
86
440
 
87
441
  ## Export Steps
88
442
 
@@ -96,9 +450,17 @@ The "Contacts Archive" option produces a `.abbu` bundle.
96
450
 
97
451
  ## References
98
452
 
99
- - [Apple Contacts Framework (private)](https://developer.apple.com/documentation/contacts)
453
+ - [Apple Contacts framework](https://developer.apple.com/documentation/contacts)
454
+ - [iQueryContacts forensic schema notes](https://github.com/MetadataForensics/iQueryContacts)
455
+ - [Observed Contacts timestamp epoch](https://apple.stackexchange.com/questions/115551/how-to-sort-contacts-by-creation-date-or-modification-date-in-ios-contacts-or-os/229313)
456
+ - [LifeOS Apple Contacts timestamp conversion](https://github.com/nbramia/LifeOS/blob/main/scripts/apple_data_export.py)
100
457
  - [SQLite3 gem](https://github.com/sparklemotion/sqlite3-ruby)
101
- - macOS `AddressBook.framework` private headers (reverse-engineered)
458
+ - [macos-ts live Contacts reader](https://github.com/evantahler/macos-ts)
459
+ - Repository fixtures and regression specs listed above
460
+
461
+ Apple's public Contacts framework documents application-facing concepts, not a
462
+ stable `.abbu` storage contract. Private framework names and Core Data names are
463
+ not normative references.
102
464
 
103
465
  ---
104
466
  Stan Carver II
@@ -0,0 +1,189 @@
1
+ <!-- docs/API_STABILITY.md -->
2
+
3
+ # Pre-1.0 API Stability Gate
4
+
5
+ This is the acceptance checklist for [issue #47](https://github.com/scarver2/abbu/issues/47),
6
+ not a declaration that ABBU is stable or authorization to release 1.0. The baseline
7
+ inventory below describes the accepted 0.4.0 interfaces. Proposed features join
8
+ this inventory only after review and integration.
9
+
10
+ ## Public surface inventory
11
+
12
+ The following are consumer-facing contracts even while the gem is pre-1.0:
13
+
14
+ | Surface | Contract to document and test before 1.0 |
15
+ | --- | --- |
16
+ | Entry points | `Abbu.open(path, strict:)`, `Abbu.open_live(path = nil, strict:)`, `Abbu::VERSION` |
17
+ | `Archive` | Construction, `path`, `contacts`, `diagnostics`, `sqlite?`, `query`, `where`, `search`, `find_by_email`, `find_by_phone`, `schema_report`, `extract_images` |
18
+ | `LiveStore` | Construction, `path`, `contacts`, `diagnostics`, `database_paths`, `.default_path`; discovery, caching, read-only and permission boundaries |
19
+ | `Contact` | Mutable accessors listed below, collection defaults, nil behavior, `full_name`, `to_s`, `inspect`; sensitive-data implications of inspection |
20
+ | `Query` | Construction from contacts, Enumerable/`each`, `to_a`, `where`, `search`, exact email/phone lookup; ordering, defensive arrays, original contact identity |
21
+ | Exporters | `CsvExporter`, `JsonExporter`, `VcardExporter`: construction, `to_file`, `to_stdout`, output encoding and file behavior |
22
+ | Diagnostics | `Diagnostic` fields and `to_h`; `ParseError#diagnostic`; tolerant recovery versus strict failure |
23
+ | Identity suggestions | `Utils::Deduplicator#duplicates`, `#matches`/`#identity_matches`; `Match` readers, `sources`, `ambiguous?`, explicit `merge(policy:)`; `MergePolicyRequired` |
24
+ | Image extraction | `ImageExtractor#extract` and `Result#files`/`#diagnostics`, also exposed through Archive; no-overwrite behavior and privacy-sensitive metadata |
25
+ | Schema evidence | `SchemaInspector#report`, also exposed through Archive; deterministic reports, no inferred Apple semantics |
26
+ | CLI and tasks | `bin/abbu` flags, stdout/stderr/exit codes; `abbu:export`, `abbu:dedupe`, `abbu:stats` task arguments |
27
+
28
+ Contact accessor inventory:
29
+
30
+ - Names: `first_name`, `middle_name`, `last_name`, `nickname`, `prefix`, `suffix`,
31
+ `maiden_name`, `phonetic_first_name`, `phonetic_middle_name`, `phonetic_last_name`.
32
+ - Organization: `company`, `job_title`, `department`, `phonetic_company`.
33
+ - Collections: `emails`, `phones`, `addresses`, `groups`, `urls`, `notes`,
34
+ `related_names`, `social_profiles`, `dates`, `instant_messages`.
35
+ - Other observed fields: `pronouns`, `ringtone`, `texttone`, `birthday`,
36
+ `anniversary`, `verification_code`, `lunar_birthday`, `image_uri`, `image_path`,
37
+ `created_at`, `modified_at`, `source`.
38
+
39
+ Record the keys, value types, raw evidence, optionality, ordering, and source/version
40
+ limits for every collection and hash. A Ruby accessor does not guarantee that every
41
+ parser or exporter supports the field. Contact fields and exported JSON are not
42
+ identical schemas: the current JSON exporter omits nil top-level values, retains
43
+ empty collections, serializes timestamps as ISO 8601, and adds the display `name`.
44
+ Do not generate a JSON schema simply by enumerating Contact accessors.
45
+
46
+ ### Source extension (0.6.0 development)
47
+
48
+ Add `Archive#sources`, `LiveStore#sources`, and `Source` construction/readers,
49
+ `provider`, and `to_h` to the public inventory. Source JSON and `--sources` are
50
+ public contracts; see the [source API](../README.md#sources). `SourceCatalog`
51
+ is an internal adapter, not an extension point. File/container provenance must
52
+ remain distinct, and provider inference is unsupported. Contacts remain mutable
53
+ even though source metadata and membership snapshots are frozen.
54
+
55
+ ## Internal and experimental boundaries
56
+
57
+ The 0.8.0 development vCard fidelity extension retains the exporter signatures
58
+ and adds their RBS contract. Wire output now uses CRLF, escapes/folding and grouped
59
+ raw labels; custom TYPE injection and fabricated preferences are removed. See
60
+ the [export migration](../README.md#export) and [evidence audit](ABBU.md#vcard-serialization-evidence).
61
+ `VcardDocument` and `VcardEncoding` are internal serialization helpers, not
62
+ public extension points. No claim of a complete Apple import round-trip is made.
63
+
64
+ The 0.7.0 development extension adds `Group` construction/readers, `include?`,
65
+ `to_h`, `Contact#group_memberships`, `Source#groups`, input `groups`, source/input
66
+ `groups_for`, `Query#in_group`, and `--groups`. See the [group API](../README.md#groups)
67
+ for snapshot identity, JSON schema, ordering, and unsupported group boundaries.
68
+ `GroupCatalog` is internal. Existing `Contact#groups` and contact exporter schemas
69
+ are unchanged; raw membership keys are available in the model and group listings.
70
+
71
+ Parser SQL/plist mappings, image-resolution heuristics, identity weights,
72
+ schema-inspection constants, private helpers, and `Utils::ContactIdentity` are
73
+ implementation details, not supported extension points. Prefer the entry points
74
+ and public results above over calling `Parsers::*` directly. Being a reachable
75
+ Ruby constant is not sufficient evidence of a supported extension contract.
76
+ Audit previously documented direct usage before hiding or removing any such API;
77
+ this classification does not authorize an immediate compatibility break.
78
+
79
+ Pre-1.0 does not mean disposable: documented behavior remains review-sensitive.
80
+ Live consistency, identity scoring/thresholds, private Apple schema coverage,
81
+ alternate-calendar interpretation, and Apple-specific vCard conventions remain
82
+ evidence-limited. Match scores are suggestions, not probabilities or proof of
83
+ identity. Raw labels/provenance must survive normalization and interchange.
84
+
85
+ ## CLI and machine-output compatibility
86
+
87
+ The 0.4.0 CLI inventory is `--format` (`csv`, `json`, `vcard`), `--output`,
88
+ `--extract-images`, `--stats`, `--dedupe`, `--search`, `--email`, `--phone`,
89
+ `--json`, `--schema`, `--strict`, `--live`, `--live-path`, `--version`, and `--help`,
90
+ including documented short forms. Search returns status 0 for matches and 1 for
91
+ no matches; JSON no-match output is `[]`. Strict parser failures return 2.
92
+ Live input failures return 1. Other usage/error combinations need a complete
93
+ matrix before 1.0; do not claim that every existing path already follows one
94
+ unified exit-code scheme.
95
+
96
+ Treat JSON keys, types, omission/null behavior, ordering promises, diagnostic codes,
97
+ CLI flags, and exit statuses as public API. Renames/removals/type changes need
98
+ explicit compatibility review and migration guidance. Machine-mode stdout must
99
+ contain only the requested payload; warnings and errors belong on stderr. Clients
100
+ must not parse human presentation text. [Issue #39](https://github.com/scarver2/abbu/issues/39)
101
+ tracks remaining structured output and schema work. Freeze and version the schema
102
+ contract before 1.0; an unimplemented schema version must not be advertised today.
103
+
104
+ CSV headers/order and vCard version, CRLF, escaping, folding, Unicode, repeated
105
+ properties, raw labels, and photo behavior also need golden/round-trip coverage.
106
+ Portable SQLite and iCalendar are proposed formats, not current guarantees.
107
+
108
+ ## Deprecation and versioning policy
109
+
110
+ For an intentional public change, document the old behavior, replacement, affected
111
+ consumers, migration example, and planned removal version before removal. Keep a
112
+ replacement available for at least one published minor release during pre-1.0;
113
+ once 1.x is stable, incompatible removals wait for a major release. Security or
114
+ data-loss emergencies require an explicitly approved, documented exception.
115
+ Runtime warnings, when appropriate, must be controllable and never pollute JSON
116
+ stdout or disclose contact values. No warning machinery is claimed to exist yet.
117
+
118
+ Follow Stan's release grouping rule: completed features receive minor bumps and
119
+ bug-fix-only releases receive patch bumps. A patch must not silently remove a
120
+ documented contract. Review incompatible pre-1.0 changes explicitly rather than
121
+ hiding them under a bug-fix label. Version bumps, green CI, and completed checklists
122
+ do not authorize tagging, merging, or publication; see [releasing](RELEASING.md).
123
+
124
+ ## Required evidence before 1.0
125
+
126
+ - [ ] Audit and approve this inventory against the exact candidate commit, including
127
+ new features integrated since 0.4.0; identify supported constructors and errors.
128
+ - [ ] Publish YARD/API documentation for every supported public method and result:
129
+ arguments, returns, errors, mutation/caching, I/O, privacy, and runnable examples.
130
+ - [ ] Keep public RBS contracts synchronized with implementation and validate them;
131
+ maintain tests for both documented success and failure behavior.
132
+ - [ ] Freeze CLI/JSON schemas and exit-code matrix with empty/error/Unicode fixtures;
133
+ document migration and deprecation policy in release notes.
134
+ - [ ] Verify parser → model → interchange preservation of raw labels, timestamps,
135
+ source evidence and images, including malformed and unsupported cases.
136
+ - [ ] Run supported Ruby CI (currently 3.3, 3.4, 4.0), 100% full-suite line coverage,
137
+ lint, package inspection and isolated install against the candidate.
138
+ - [ ] Publish an evidence matrix: Ruby/SQLite versions, OS, Contacts build, storage
139
+ layout, archive/live mode, tested fields, fixture provenance and known gaps.
140
+ Synthetic fixtures with unknown Apple versions are not macOS certification.
141
+ - [ ] Complete the benchmark below and explicitly disposition performance gaps.
142
+ - [ ] Review sensitive-data exposure in exports, diagnostics, exception messages,
143
+ `inspect`, absolute paths, image extraction, permissions, and future agent tools.
144
+ Do not log real contacts, photos, verification codes, or provider identifiers in CI.
145
+ - [ ] Disposition each remaining backlog item as required, deferred, or unsupported
146
+ with rationale; do not substitute feature count for a stability decision.
147
+ - [ ] Obtain Deputy exact-head review and separate Sheriff authorization for any
148
+ 1.0 tag/publication. This checklist itself does not authorize either.
149
+
150
+ ## 10k+ performance acceptance
151
+
152
+ Before 1.0, provide a deterministic, non-personal 10,000-contact benchmark with
153
+ multiple sources, multivalues, Unicode, missing optional data, and a repeatable seed.
154
+ Measure archive and live-style synthetic parsing, query, CSV/JSON/vCard export,
155
+ and identity suggestions separately. Include a 100,000-contact scaling run for
156
+ streaming work in [#40](https://github.com/scarver2/abbu/issues/40).
157
+
158
+ Record exact commit, Ruby/SQLite versions, hardware/OS, fixture recipe, command,
159
+ wall time, peak RSS, output count/checksum, warmup, and at least three measured
160
+ runs. Report cold versus cached reads separately. Proposed gate: no unexplained
161
+ greater-than-20% median time or peak-RSS regression against the recorded baseline
162
+ on the same runner; any exception requires reviewer rationale. Establish absolute
163
+ budgets from measured evidence before approving 1.0, not invented timings here.
164
+ Streaming must demonstrate bounded contact-memory overhead at both scales;
165
+ materializing `contacts` is not a streaming benchmark. The current all-pairs
166
+ identity matcher needs its own scaling assessment, not a linear-memory promise.
167
+
168
+ ## Linked gaps and adoption
169
+
170
+ See the [complete backlog](TODO.md#pre-10-backlog--not-included-in-040).
171
+ Important dependencies include WAL evidence [#28](https://github.com/scarver2/abbu/issues/28),
172
+ vCard fidelity/photos [#29](https://github.com/scarver2/abbu/issues/29) /
173
+ [#30](https://github.com/scarver2/abbu/issues/30), sources/groups
174
+ [#31](https://github.com/scarver2/abbu/issues/31) / [#32](https://github.com/scarver2/abbu/issues/32),
175
+ My Card/history/calendar research [#33](https://github.com/scarver2/abbu/issues/33) /
176
+ [#35](https://github.com/scarver2/abbu/issues/35) / [#36](https://github.com/scarver2/abbu/issues/36),
177
+ machine output [#39](https://github.com/scarver2/abbu/issues/39), streaming
178
+ [#40](https://github.com/scarver2/abbu/issues/40), and safe-writer research
179
+ [#46](https://github.com/scarver2/abbu/issues/46). Deferral may be acceptable if the
180
+ released support boundary is explicit. Never infer unsupported Apple semantics
181
+ to check off a gate.
182
+
183
+ Return to the [README](../README.md), [format evidence](ABBU.md), or
184
+ [contribution workflow](CONTRIBUTING.md).
185
+
186
+ —
187
+ Stan Carver II
188
+ Made in Texas 🤠
189
+ https://stancarver.com