abbu 0.2.0 → 0.4.0

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.
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,194 @@ 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
+ ### Opt-in live Contacts stores
129
+
130
+ The CLI uses `--live` only for auto-discovery and `--live-path PATH` for an explicit
131
+ store. These forms are mutually exclusive and accept no positional archive/path arguments.
132
+ Option ordering does not change input selection. Synthetic WAL-mode and concurrent-writer
133
+ validation remains a follow-up; read-only handles do not establish snapshot consistency
134
+ across an actively changing Contacts store.
135
+
136
+ `Abbu.open_live` and the CLI's live modes can read an AddressBook directory
137
+ without first exporting an `.abbu` archive. This mode is deliberately separate from
138
+ `Abbu.open`: archive validation and plist fallback do not apply to a live store.
139
+
140
+ When no path is supplied on macOS, ABBU checks the observed per-user location at
141
+ `~/Library/Application Support/AddressBook`. Callers may instead provide a directory,
142
+ which keeps automation and tests independent of the host platform and user account.
143
+ ABBU discovers databases directly under that directory and one level below
144
+ `Sources/<identifier>/`, preserving the same root/source provenance used for archives.
145
+ Only files matching the observed `AddressBook-v*.abcddb` shape are candidates; this
146
+ filename pattern is discovery evidence, not a guarantee of stable Apple semantics.
147
+
148
+ Every live-store database is opened using SQLite's read-only mode. ABBU has no live-store
149
+ write API and never creates, updates, or deletes Contacts data. macOS privacy controls may
150
+ deny access even when the path exists. In that case ABBU raises
151
+ `Abbu::LiveStore::PermissionError` with instructions to grant the calling terminal or
152
+ application Full Disk Access in **System Settings → Privacy & Security → Full Disk
153
+ Access**. Missing directories and databases raise `Abbu::LiveStore::NotFoundError`, and
154
+ automatic discovery without a caller-supplied path raises
155
+ `Abbu::LiveStore::UnsupportedPlatformError` outside macOS.
156
+
157
+ The repository verifies live-store behavior only with deterministic synthetic SQLite
158
+ fixtures. It does not inspect or commit a developer's real Contacts store.
159
+ ### Provenance-aware identity evidence
160
+
161
+ ABBU treats deduplication as a suggestion boundary rather than proof that two records are
162
+ the same person. `Utils::Deduplicator#matches` compares normalized email, phone, name, and
163
+ organization signals while returning both original contacts, both source records, raw
164
+ evidence, normalized comparison values, confidence, and ambiguity status.
165
+
166
+ Email comparison trims surrounding whitespace and applies Unicode-aware case folding.
167
+ Names and organizations use Unicode NFKC normalization, case folding, and whitespace or
168
+ punctuation normalization without transliterating distinct characters. Explicit `+` and
169
+ `00` phone forms are compared as international numbers. The trimmed raw value must start
170
+ with a literal ASCII `+` or contiguous `00`; punctuation removal never establishes an
171
+ international prefix. For example, `(001) 512-555-0100` remains national-format evidence.
172
+ National-format numbers remain
173
+ source-local evidence because ABBU has no country or numbering-plan evidence with which
174
+ to infer a global identity.
175
+
176
+ SQLite primary keys, source identifiers, private Apple link identifiers, and image stems
177
+ are not treated as global contact identifiers. The repository fixtures do not establish
178
+ such semantics. Competing candidates and weak name/organization or source-local phone
179
+ matches remain ambiguous, and no contact is merged unless the caller supplies an explicit
180
+ merge policy.
181
+
182
+ ### Image resolution and extraction
183
+
184
+ The synthetic SQLite fixture demonstrates a `ZIMAGEURI` value whose stem matches a file
185
+ under an `Images/` directory. Resolution covers the root bundle and nested
186
+ `Sources/<identifier>/Images/` directories. When duplicate stems exist, ABBU uses the
187
+ contact's database provenance to select only an image beside that database; it does not
188
+ guess when the available evidence remains ambiguous.
189
+
190
+ `Archive#extract_images(output_dir)` and the CLI `--extract-images DIR` copy resolved
191
+ images without changing `Contact#image_uri`, `Contact#image_path`, or `Contact#source`.
192
+ Exported filenames combine a sanitized contact name, the original image identifier, and
193
+ a stable provenance digest. Path separators, control characters, and reserved filename
194
+ characters cannot create subdirectories or traverse outside the selected output directory.
195
+
196
+ The selected output directory (including symlinked parents) is resolved to its canonical
197
+ directory before copying; callers must control that directory and prevent concurrent
198
+ directory replacement. Each image is created exclusively with owner-only permissions.
199
+ Existing destinations are never overwritten, including regular files, hard links, and
200
+ symlinks (even dangling ones). These collisions produce a `destination_exists` extraction
201
+ diagnostic and no successful file record; other images continue. Repeating extraction
202
+ into the same directory therefore reports collisions instead of replacing earlier output.
203
+ Directory creation/resolution failures raise filesystem errors before extraction begins.
204
+
205
+ Extraction diagnostics are a separate API from `archive.diagnostics`: they may contain
206
+ contact names, raw image identifiers, source paths, and filesystem error details. Treat
207
+ them and the CLI's image warnings as sensitive contact data, not safe-to-publish logs.
208
+
209
+ ABBU recognizes JPEG, PNG, GIF, and common HEIF/HEIC-compatible brands from file
210
+ signatures and chooses the exported extension from those bytes rather than the source
211
+ extension. Unknown content is reported as a diagnostic instead of being relabeled. HEIC
212
+ data is copied unchanged; ABBU does not transcode it.
213
+
214
+ No repository fixture currently demonstrates a reliable Apple thumbnail-versus-full-size
215
+ naming or selection rule. ABBU therefore exports the image resolved by the observed
216
+ `ZIMAGEURI` relationship and does not infer size semantics from filenames or directories.
217
+
218
+ ### Recovery and diagnostics
219
+
220
+ By default, ABBU recovers from malformed individual plist records, missing
221
+ optional SQLite relationship tables, and unresolved image references. Each
222
+ recovery appends an `Abbu::Diagnostic` to `archive.diagnostics` with a category,
223
+ parser, source path, non-PII context, and a stable message. Required contact
224
+ schema failures still raise because no evidence-backed contact record can be
225
+ recovered safely.
226
+
227
+ An absent optional SQLite table produces one diagnostic per database and table,
228
+ regardless of contact count. ABBU does not place record identifiers or raw image
229
+ references in these schema- and image-level diagnostic contexts.
230
+
231
+ Pass `strict: true` to `Abbu.open` or `--strict` to the CLI to raise
232
+ `Abbu::ParseError` on the first recoverable condition. The CLI prints a
233
+ diagnostic summary to standard error so exported data on standard output remains
234
+ pipeable.
235
+
236
+ ### Schema diagnostics
237
+
238
+ `Archive#schema_report` and `abbu Contacts.abbu --schema` inspect every discovered
239
+ SQLite database and return deterministic schema metadata. Reports identify recognized
240
+ and unrecognized tables and columns, recognized items that are absent, declared SQLite
241
+ types, primary-key and nullability metadata, source provenance, and exact owner/contact-
242
+ style column names that may represent contact links.
243
+
244
+ These reports are research evidence, not parser mappings. In particular, a
245
+ `contact_link_candidate` flag records only an exact column-name shape such as `ZOWNER`,
246
+ `ZCONTACT`, or `Z_CONTACT`; it does not claim a foreign-key target or assign Apple
247
+ Contacts semantics. Unknown tables and columns must be reproduced in a sanitized fixture
248
+ or supported by documentation before ABBU uses them to populate contacts.
249
+
250
+ Missing recognized tables and columns remain visible as diagnostic observations. The
251
+ parser tolerates absent established email, phone, and postal-address tables by returning
252
+ empty collections, while the schema report preserves the absence for compatibility
253
+ research. If one of those tables exists but lacks an expected column, parsing raises the
254
+ SQLite schema error instead of silently treating the contact as having no corresponding
255
+ data. The core `ZABCDRECORD` table remains required for contact parsing.
256
+
257
+ ### Labeled values
258
+
259
+ The synthetic SQLite and plist fixtures include both custom labels and Apple's
260
+ observed standard-label wrapper, such as `_$!<Work>!$_`. ABBU exposes the
261
+ human-facing value as `label` (`Work`) and preserves the exact stored value as
262
+ `raw_label`. Custom, blank, malformed, Unicode, and already-normalized labels
263
+ are not otherwise rewritten. Direct plist keys such as `Birthday` have no
264
+ stored label, so their normalized label is derived from the key and
265
+ `raw_label` is `nil`.
266
+
267
+ Normalization applies to email addresses, phone numbers, postal addresses,
268
+ URLs, related names, date components, and instant-message handles. JSON keeps
269
+ both values. Human-facing CSV uses normalized labels, while vCard anniversary
270
+ labels prefer `raw_label` so Apple label wrappers and custom source values
271
+ survive parse → model → interchange export.
80
272
 
81
273
  ### 2. Plist / `.abcdp` (legacy macOS)
82
274
 
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.
275
+ Older macOS versions stored contacts as separate plist files under `Records/`.
276
+ The repository fixtures demonstrate dictionaries that the plist parser
277
+ normalizes into the same contact model used by the SQLite parser. Additional
278
+ plist keys or layouts require fixture evidence before they are treated as
279
+ supported semantics.
280
+
281
+ ## Repository Evidence
282
+
283
+ - `spec/fixtures/TestContacts.abbu/` exercises the supported synthetic SQLite,
284
+ nested source, and image-resolution behavior.
285
+ - `spec/fixtures/PlistContacts.abbu/` exercises the supported synthetic legacy
286
+ plist behavior.
287
+ - `spec/fixtures/identity_cases.yml` contains deterministic synthetic cross-source and
288
+ Unicode near-collision identity evidence.
289
+ - `spec/support/fixture_generator.rb` is the reproducible source for generated
290
+ SQLite fixture structure and data.
291
+ - `spec/abbu/schema_inspector_spec.rb` builds deterministic temporary SQLite
292
+ schemas for missing tables, unknown contact-link candidates, and column drift.
293
+
294
+ These fixtures prove only the variations they contain. Table names, column
295
+ names, entity numbers, UUIDs, and directory names alone are not sufficient
296
+ evidence for new behavior.
86
297
 
87
298
  ## Export Steps
88
299
 
@@ -96,9 +307,17 @@ The "Contacts Archive" option produces a `.abbu` bundle.
96
307
 
97
308
  ## References
98
309
 
99
- - [Apple Contacts Framework (private)](https://developer.apple.com/documentation/contacts)
310
+ - [Apple Contacts framework](https://developer.apple.com/documentation/contacts)
311
+ - [iQueryContacts forensic schema notes](https://github.com/MetadataForensics/iQueryContacts)
312
+ - [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)
313
+ - [LifeOS Apple Contacts timestamp conversion](https://github.com/nbramia/LifeOS/blob/main/scripts/apple_data_export.py)
100
314
  - [SQLite3 gem](https://github.com/sparklemotion/sqlite3-ruby)
101
- - macOS `AddressBook.framework` private headers (reverse-engineered)
315
+ - [macos-ts live Contacts reader](https://github.com/evantahler/macos-ts)
316
+ - Repository fixtures and regression specs listed above
317
+
318
+ Apple's public Contacts framework documents application-facing concepts, not a
319
+ stable `.abbu` storage contract. Private framework names and Core Data names are
320
+ not normative references.
102
321
 
103
322
  ---
104
323
  Stan Carver II
data/docs/CHANGELOG.md CHANGED
@@ -11,6 +11,132 @@ Versioning follows [Semantic Versioning](https://semver.org/).
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.4.0] - 2026-09-30
15
+
16
+ ### Fixed
17
+
18
+ - Live CLI input uses distinct `--live` auto-discovery and `--live-path PATH` forms;
19
+ mixed modes and unexpected live positional arguments are rejected.
20
+
21
+ - Deduplication recognizes international phone prefixes from trimmed raw input, not
22
+ punctuation-stripped digits, keeping national-looking `(001)` values source-local.
23
+
24
+ - Image extraction exclusively creates destination files and reports `destination_exists`
25
+ rather than overwriting existing files, symlinks, or hard links. Output directory aliases
26
+ are resolved before extraction; extraction diagnostic privacy is documented.
27
+
28
+ ### Added
29
+
30
+ - Explicit, read-only live Contacts access through `Abbu.open_live` and
31
+ `abbu --live` / `abbu --live-path PATH`, with root and `Sources/*` database discovery
32
+ - Actionable live-store errors for missing databases, unsupported automatic
33
+ discovery, and macOS Full Disk Access restrictions
34
+ - Platform-independent synthetic coverage for live-store discovery, source
35
+ provenance, SQLite read-only enforcement, and CLI behavior
36
+ - Provenance-aware `Utils::Deduplicator#matches` suggestions with normalized email,
37
+ international/source-local phone, Unicode name, and organization evidence
38
+ - Match confidence, ambiguity status, original source records, and raw evidence, plus an
39
+ explicit callable merge-policy boundary that never silently collapses contacts
40
+
41
+ - `Archive#extract_images` and `Abbu::ImageExtractor` for copying contact photos to a
42
+ caller-selected directory with structured missing, unreadable, and unsupported-image
43
+ diagnostics
44
+ - `--extract-images DIR` CLI support with content-aware JPEG, PNG, GIF, and HEIC extension
45
+ selection and safe, Unicode-preserving, collision-resistant filenames
46
+ - Source-local resolution for duplicate image stems in nested account directories without
47
+ guessing when provenance cannot disambiguate candidates
48
+
49
+ - Structured tolerant-parsing diagnostics with strict API/CLI mode for corrupt
50
+ plist records, missing optional SQLite data, and unresolved image references
51
+ - Missing optional SQLite tables emit one non-PII diagnostic per database and
52
+ table instead of repeating schema-level warnings for every contact
53
+ - Evidence-safe SQLite schema diagnostics through `Archive#schema_report` and
54
+ `abbu <archive> --schema`, including unknown tables/columns, absent recognized
55
+ schema elements, and owner/contact-style relationship candidates
56
+ - Deterministic schema-variation coverage for missing optional tables, unknown
57
+ contact-linked tables, and column drift
58
+ - Normalized Apple standard labels with the original source value preserved as
59
+ `raw_label` on labeled contact values
60
+ - Chainable `Abbu::Query` and `Archive#where` APIs for contact filtering
61
+ - Exact normalized email and phone lookup that returns all matches across sources
62
+ - Case-insensitive partial name and email search through Ruby and tab-separated CLI output
63
+ - Stable `--json` search output using the regular contact JSON schema
64
+ - Contact creation and modification timestamps from optional SQLite `ZCREATIONDATE` and `ZMODIFICATIONDATE` columns
65
+ - Provenance metadata identifying each contact's source database or plist and its location within the ABBU bundle
66
+ - Creation, modification, and source metadata in JSON exports
67
+ - Repository constitution and focused local skills for ABBU format evidence,
68
+ Ruby gem development, testing, and Sheriff-gated releases
69
+ - Canonical `bin/spec` and `bin/package` workflows, with CI reusing project-local
70
+ `bin/spec` and `bin/lint` instead of duplicating their commands
71
+
72
+ ### Changed
73
+
74
+ - CI now runs the Ruby matrix once for pull requests and on pushes to canonical `main`, avoiding duplicate feature-branch push and pull-request runs
75
+ - SQLite parsing now tolerates absent established email, phone, and postal-address
76
+ tables and returns empty collections while retaining the variation in schema diagnostics;
77
+ unexpected column drift and other SQL errors on present tables continue to surface
78
+ - vCard anniversary export now prefers the original `raw_label` so Apple and
79
+ custom source representations survive parse-and-export round trips
80
+ - Minimum supported Ruby and RuboCop target are now 3.3; CI covers Ruby 3.3,
81
+ 3.4, and 4.0, with Ruby 3.3 as the designated lint/tooling job
82
+ - Agent guidance is consolidated in `AGENTS.md`; the redundant `CLAUDE.md` has
83
+ been removed
84
+ - Gem packaging now includes only the public `bin/abbu` executable instead of
85
+ repository-only developer commands
86
+
87
+ ## [0.3.0] - 2026-09-29
88
+
89
+ ### Added
90
+
91
+ - `Contact#image_uri` and `Contact#image_path` accessors
92
+ - `Parsers::SqliteParser` extracts `ZIMAGEURI` from `ZABCDRECORD`
93
+ - `Utils::ImageResolver` builds an index of every image file under the bundle's `**/Images/` directories (jpg, jpeg, png, heic — case-insensitive) and resolves a contact's `image_uri` to a `Pathname` within the bundle
94
+ - `Archive#contacts` automatically resolves `image_path` for any contact that has a non-nil `image_uri`
95
+ - CSV export: new `ImagePath` column (last column)
96
+ - JSON export: `image_uri` and `image_path` fields (omitted via `.compact` when blank)
97
+ - vCard export: `PHOTO;VALUE=URI:file://<absolute path>` line, emitted when `image_path` is present
98
+ - `spec/fixtures/TestContacts.abbu/Images/stan-photo.jpg` stub image, regenerated by `spec/support/fixture_generator.rb`
99
+
100
+ ### Notes
101
+
102
+ - PlistParser does not extract images — legacy `.abcdp` contacts typically embed image data inline, which is a separate extraction path
103
+ - vCard `PHOTO` references the absolute bundle path; base64 embedding and the `--extract-images` CLI flag are tracked for follow-up
104
+
105
+ ### Fixed
106
+
107
+ - vCard `PHOTO` file URIs now percent-encode spaces, reserved characters, and non-ASCII bytes in image paths
108
+ - `abbu:export` Rake task wrote hash literals (e.g. `{:address=>"…", :label=>"…"}`) into the `Email` and `Phone` CSV columns. Now correctly extracts the first address and number from each contact's multi-value field.
109
+
110
+ ## [0.2.0] - 2026-05-03
111
+
112
+ ### Added
113
+
114
+ - `Parsers::PlistParser` — full implementation of legacy `.abcdp` plist contact parsing, replacing the previous stub
115
+ - `PlistParser::FIELD_MAP` constant for flat-field mapping
116
+ - Multi-value field extraction for emails, phones, addresses, URLs, notes, related names, and social profiles
117
+ - `Archive` recursively scans `**/*.abcdp` across the entire bundle tree
118
+ - `plist` gem (~> 3.7) runtime dependency
119
+ - Plist fixture files (`spec/fixtures/PlistContacts.abbu/`) for integration testing
120
+ - `middle_name` attribute on `Contact`; `ZMIDDLENAME` / `Middle` plist key
121
+ - `dates` attribute on `Contact` (array of `{ year:, month:, day:, label: }` hashes)
122
+ - `birthday`, `anniversary`, and `lunar_birthday` accessor methods derived from the dates list
123
+ - `instant_messages` attribute (AIM, Jabber, Skype, etc.); `ZABCDMESSAGINGADDRESS` / `InstantMessage` key
124
+ - `verification_code` attribute on `Contact`; `ZVERIFICATIONCODE` column / `VerificationCode` plist key
125
+ - `phonetic_middle_name` attribute; `ZPHONETICMIDDLENAME` column / `PhoneticMiddle` plist key
126
+ - `ZABCDDATECOMPONENTS` parsing (year/month/day split columns)
127
+ - `BDAY`, `X-LUNAR-BDAY`, `X-ABDATE`, `X-ABLABEL` vCard fields
128
+ - `IMPP` vCard field for instant messaging
129
+ - Birthday, anniversary, lunar birthday, instant messages, and verification code in CSV and JSON exports
130
+ - Plist fixture for testing all the above (`spec/fixtures/PlistContacts.abbu/`)
131
+
132
+ ### Changed
133
+
134
+ - `Contact#full_name` now includes middle name (`Honorable Stan The Man "Stretch" Carver II`)
135
+ - vCard `N` field includes middle name component
136
+ - `SqliteParser::RECORD_FIELD_MAP` extended with the new column → attr mappings
137
+ - `JsonExporter#contact_hash` includes all new fields; blank fields are dropped via `.compact`
138
+ - `CsvExporter` extended-field section now exports lunar birthday and verification code
139
+
14
140
  ## [0.1.2] - 2026-04-26
15
141
 
16
142
  ### Added
data/docs/CONTRIBUTING.md CHANGED
@@ -23,19 +23,29 @@ This runs RSpec and RuboCop automatically on file changes.
23
23
  ## Running Tests
24
24
 
25
25
  ```bash
26
- mise exec -- bundle exec rspec
26
+ bin/spec
27
27
  ```
28
28
 
29
29
  ## Linting
30
30
 
31
31
  ```bash
32
- mise exec -- bundle exec rubocop
32
+ bin/lint
33
33
  mise exec -- bundle exec rubocop -a # autocorrect
34
34
  ```
35
35
 
36
+ ## Packaging
37
+
38
+ ```bash
39
+ bin/package
40
+ ```
41
+
42
+ This builds the current gem, verifies its metadata, installs it into an isolated
43
+ gem home, and loads that installed copy. A successful package check is evidence
44
+ only; it does not authorize a release tag or RubyGems publication.
45
+
36
46
  ## Pull Request Guidelines
37
47
 
38
- - Base branch: `master`
48
+ - Base branch: `main`
39
49
  - Commit style: [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `chore:`)
40
50
  - All specs must pass and coverage must remain at 100%
41
51
  - RuboCop must pass with no offenses
data/docs/TODO.md CHANGED
@@ -52,7 +52,7 @@ Feature checklist organized by release version.
52
52
 
53
53
  ---
54
54
 
55
- ## v0.2.0 — Plist Parser (In Progress)
55
+ ## v0.2.0 — Plist Parser (Released)
56
56
 
57
57
  - [x] `PlistParser` — parse legacy `.abcdp` plist contact files
58
58
  - [x] Full field extraction matching SqliteParser output shape
@@ -102,71 +102,79 @@ Feature checklist organized by release version.
102
102
 
103
103
  ---
104
104
 
105
- ## v0.3.0 — Image Extraction
105
+ ## v0.3.0 — Image Resolution (Released)
106
106
 
107
- - [ ] Extract contact photos from `Images/` directory
108
- - [ ] Map image UUIDs to contacts via `ZIMAGEURI` or `ZHASIMAGE`
109
- - [ ] `Contact#image_path` accessor
110
- - [ ] CLI: `--extract-images` flag to export photos alongside contacts
111
- - [ ] Support JPEG, PNG, HEIC formats
112
- - [ ] Thumbnail vs. full-size image handling
113
-
114
- ---
115
-
116
- ## v0.4.0 — Fuzzy Deduplication
117
-
118
- - [ ] Levenshtein distance matching for name-based deduplication
119
- - [ ] Phone number normalization (strip formatting, compare digits)
120
- - [ ] Configurable similarity thresholds
121
- - [ ] `Deduplicator#fuzzy_duplicates` method
122
- - [ ] CLI: `--dedupe --fuzzy` flag
123
- - [ ] Merge suggestions output (side-by-side diff)
124
-
125
- ---
126
-
127
- ## v0.5.0 — Merge Engine
128
-
129
- - [ ] `Contact#merge(other)` — combine two contacts preserving all data
130
- - [ ] Conflict resolution strategies (keep-first, keep-last, keep-both)
131
- - [ ] `Archive#deduplicate!` — in-place merge with backup
132
- - [ ] CLI: `--merge` interactive mode
133
- - [ ] Export merged results to new `.abbu` bundle
134
-
135
- ---
136
-
137
- ## v0.6.0 — Filtering & Querying
138
-
139
- - [ ] `Archive#where(field: value)` query API
140
- - [ ] Filter by region (state, city, country)
141
- - [ ] Filter by group membership
142
- - [ ] Filter by date range (created, modified)
143
- - [ ] CLI: `--filter` flag with key=value syntax
144
- - [ ] Chainable query interface
145
-
146
- ---
147
-
148
- ## v0.7.0 — Write Support
149
-
150
- - [ ] Create new `.abbu` bundles from Contact objects
151
- - [ ] Write SQLite databases with correct schema
152
- - [ ] Write `.abcdp` plist files
153
- - [ ] Round-trip: read → modify → write
154
- - [ ] `Archive#save(path)` method
155
-
156
- ---
157
-
158
- ## v1.0.0 — Sync Adapters & Stable API
159
-
160
- - [ ] Adapter interface for external CRM sync
161
- - [ ] Printavo adapter
162
- - [ ] HubSpot adapter
163
- - [ ] Generic webhook/API adapter
164
- - [ ] Stable public API guarantee
165
- - [ ] Comprehensive API documentation (YARD)
166
- - [ ] Performance benchmarks for large archives (10k+ contacts)
107
+ - [x] Resolve contact photos through observed `ZIMAGEURI` relationships
108
+ - [x] `Contact#image_uri` and `Contact#image_path` accessors
109
+ - [x] Root and nested source image discovery
110
+ - [x] CSV / JSON image paths and vCard PHOTO file URI references
167
111
 
168
112
  ---
169
113
 
114
+ ## v0.4.0 — Read-only Contacts Toolkit
115
+
116
+ Implemented in the accepted #19–#25 tranche and earlier prerequisites; publication
117
+ is gated by [release issue #27](https://github.com/scarver2/abbu/issues/27),
118
+ Deputy exact-head review, and Sheriff release authority.
119
+
120
+ - [x] Evidence-safe schema introspection and `--schema`
121
+ - [x] Lossless Apple label normalization with `raw_label` and anniversary vCard fidelity
122
+ - [x] Creation/modification timestamps and source provenance
123
+ - [x] Tolerant structured diagnostics, per-database/table deduplication, and strict mode
124
+ - [x] Provenance-aware identity suggestions, phone comparison, and explicit merge-policy boundary
125
+ - [x] Source-aware duplicate image-stem resolution
126
+ - [x] First-class image extraction with content detection, safe filenames, and no destination overwrites
127
+ - [x] Chainable query API, exact email/phone lookup, and partial name/email search
128
+ - [x] TSV and stable JSON CLI search output
129
+ - [x] Explicit read-only live macOS Contacts input through `--live` / `--live-path PATH`
130
+ - [x] Ruby 3.3 minimum and lint/tooling gate; Ruby 3.4 and 4.0 CI
131
+ - [x] Pull-request CI plus canonical-main pushes, without duplicate feature-branch runs
132
+ - [x] `AGENTS.md`, local skills, and canonical spec/lint/package workflows
133
+
134
+ ## Pre-1.0 Backlog — Not Included in 0.4.0
135
+
136
+ These remain future work; no implementation is authorized by release preparation.
137
+ Version assignments below 1.0 are intentionally deferred until scopes are accepted.
138
+
139
+ - [ ] [#28](https://github.com/scarver2/abbu/issues/28): synthetic WAL, sidecar, and concurrent-writer validation
140
+ - [ ] [#29](https://github.com/scarver2/abbu/issues/29): stronger Apple-compatible vCard fidelity and round trips
141
+ - [ ] [#30](https://github.com/scarver2/abbu/issues/30): embedded vCard photos (currently file URI references)
142
+ - [ ] [#31](https://github.com/scarver2/abbu/issues/31): first-class read-only source objects
143
+ - [ ] [#32](https://github.com/scarver2/abbu/issues/32): first-class groups and membership queries
144
+ - [ ] [#33](https://github.com/scarver2/abbu/issues/33): evidence-backed My Card identification
145
+ - [ ] [#34](https://github.com/scarver2/abbu/issues/34): modified-since and date-range queries
146
+ - [ ] [#35](https://github.com/scarver2/abbu/issues/35): evidence-backed save/edit history
147
+ - [ ] [#36](https://github.com/scarver2/abbu/issues/36): alternate-calendar and lunar metadata research
148
+ - [ ] [#37](https://github.com/scarver2/abbu/issues/37): explainable fuzzy names and configurable thresholds
149
+ - [ ] [#38](https://github.com/scarver2/abbu/issues/38): merge plans, side-by-side evidence, and safe built-in policies
150
+ - [ ] [#39](https://github.com/scarver2/abbu/issues/39): broader machine-readable JSON CLI contract
151
+ - [ ] [#40](https://github.com/scarver2/abbu/issues/40): streaming contact iteration/export and large-store performance
152
+ - [ ] [#41](https://github.com/scarver2/abbu/issues/41): portable SQLite export
153
+ - [ ] [#42](https://github.com/scarver2/abbu/issues/42): birthday/anniversary iCalendar export
154
+ - [ ] [#43](https://github.com/scarver2/abbu/issues/43): identity-aware snapshot diffs
155
+ - [ ] [#44](https://github.com/scarver2/abbu/issues/44): history across snapshot directories
156
+ - [ ] [#45](https://github.com/scarver2/abbu/issues/45): optional MCP/agent adapter over read-only interfaces
157
+ - [ ] [#46](https://github.com/scarver2/abbu/issues/46): evidence-backed new-ABBU writer research
158
+ - [ ] [#47](https://github.com/scarver2/abbu/issues/47): public API stability checklist
159
+ - [ ] Thumbnail/full-size image selection after reproducible format evidence
160
+ - [ ] Region filtering and an explicit CLI filter grammar
161
+
162
+ Previous in-place `Archive#deduplicate!` / source-archive mutation proposals are
163
+ superseded. Merge plans must preserve originals; any future writer targets a new
164
+ output bundle and requires evidence-backed round-trip validation. Writing live
165
+ Contacts databases is not part of the roadmap.
166
+
167
+ ## v1.0.0 — Future Stable API
168
+
169
+ - [ ] Complete the API-stability checklist and acceptance evidence before promising stability
170
+ - [ ] Comprehensive public API documentation
171
+ - [ ] Benchmarks for large archives (10k+ contacts)
172
+ - [ ] Optional external CRM adapters (Printavo, HubSpot, generic webhook/API), separately scoped
173
+
174
+ See [README](../README.md) and [CHANGELOG](CHANGELOG.md) for current behavior
175
+ and historical releases.
176
+
177
+ —
170
178
  Stan Carver II
171
179
  Made in Texas 🤠
172
180
  https://stancarver.com