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.
- checksums.yaml +4 -4
- data/README.md +281 -8
- data/bin/abbu +230 -53
- data/docs/ABBU.md +369 -7
- data/docs/API_STABILITY.md +189 -0
- data/docs/CHANGELOG.md +179 -0
- data/docs/CONTRIBUTING.md +25 -3
- data/docs/FORMAT_COMPATIBILITY.md +97 -0
- data/docs/RELEASING.md +75 -0
- data/docs/TODO.md +73 -62
- data/lib/abbu/archive.rb +80 -6
- data/lib/abbu/contact.rb +6 -3
- data/lib/abbu/diagnostic.rb +27 -0
- data/lib/abbu/exporters/csv_exporter.rb +2 -2
- data/lib/abbu/exporters/json_exporter.rb +7 -1
- data/lib/abbu/exporters/vcard_document.rb +54 -0
- data/lib/abbu/exporters/vcard_encoding.rb +54 -0
- data/lib/abbu/exporters/vcard_exporter.rb +44 -27
- data/lib/abbu/group.rb +28 -0
- data/lib/abbu/group_catalog.rb +32 -0
- data/lib/abbu/image_extractor.rb +134 -0
- data/lib/abbu/live_store.rb +117 -0
- data/lib/abbu/parse_error.rb +13 -0
- data/lib/abbu/parsers/plist_parser.rb +41 -11
- data/lib/abbu/parsers/sqlite_parser.rb +139 -65
- data/lib/abbu/query.rb +92 -0
- data/lib/abbu/schema_inspector.rb +133 -0
- data/lib/abbu/source.rb +53 -0
- data/lib/abbu/source_catalog.rb +32 -0
- data/lib/abbu/timestamp_range.rb +40 -0
- data/lib/abbu/utils/contact_identity.rb +97 -0
- data/lib/abbu/utils/deduplicator.rb +130 -2
- data/lib/abbu/utils/image_resolver.rb +59 -0
- data/lib/abbu/utils/label_normalizer.rb +15 -0
- data/lib/abbu/utils/source_descriptor.rb +35 -0
- data/lib/abbu/version.rb +1 -1
- data/lib/abbu.rb +15 -2
- data/sig/group.rbs +36 -0
- data/sig/query.rbs +21 -0
- data/sig/source.rbs +25 -0
- data/sig/vcard_exporter.rbs +10 -0
- data/tasks/abbu.rake +1 -1
- metadata +33 -15
- data/bin/cleanse +0 -7
- data/bin/console +0 -10
- data/bin/dev +0 -7
- data/bin/lint +0 -7
- data/bin/outdated +0 -7
- 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
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|