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/CHANGELOG.md CHANGED
@@ -11,6 +11,185 @@ Versioning follows [Semantic Versioning](https://semver.org/).
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.8.1] - 2026-10-01
15
+
16
+ ### Fixed
17
+
18
+ - Package verification reuses Bundler-installed dependency paths in CI while
19
+ loading the built ABBU gem from an isolated installation. CI now exercises
20
+ this check before a release tag is created.
21
+
22
+ ## [0.8.0] - 2026-10-01
23
+
24
+ This release includes the previously unreleased 0.5.0–0.7.0 development milestones.
25
+
26
+ ### Added — vCard fidelity
27
+
28
+ - vCard fidelity layer with raw-label-preserving grouped repeated properties,
29
+ public exporter RBS, and deterministic SQLite/plist-to-vCard regression tests.
30
+
31
+ ### Changed — vCard fidelity
32
+
33
+ - vCard output now uses CRLF, escaped TEXT/structured components, UTF-8-safe
34
+ 75-octet folding, and URI-specific encoding. Consumers must unfold and decode
35
+ rather than parse ungrouped literal output lines.
36
+ - Custom labels use grouped `X-ABLABEL` rather than arbitrary TYPE parameters;
37
+ only standard exact ASCII labels become TYPE. Removed invented anniversary
38
+ preference and unlabeled-address HOME. Existing method signatures remain.
39
+ - Invalid text/control bytes and unsafe service tokens/schemes fail before
40
+ output rather than emitting malformed records. Apple-specific support limits
41
+ and migration guidance are explicit; no Contacts import certification claimed.
42
+
43
+ ### Added — groups
44
+
45
+ - First-class observed groups scoped by file provenance and SQLite record key,
46
+ with source/input enumeration, reverse membership lookup, and chainable
47
+ `Query#in_group` filtering without merging same-name groups.
48
+ - Lossless `Contact#group_memberships` evidence alongside unchanged legacy group
49
+ labels and contact exports; immutable group snapshots and public RBS.
50
+ - JSON-only `--groups` listing for archives and read-only live stores, preserving
51
+ raw names, nulls, diagnostics, and explicit unsupported empty/plist-group boundaries.
52
+
53
+ ### Added — sources
54
+
55
+ - Read-only source containers through `Archive#sources` and `LiveStore#sources`,
56
+ preserving raw file provenance, unknown providers, queryable contacts, and
57
+ observed group membership labels without conflating source-local identifiers.
58
+ - JSON-only `--sources` listing for archive/live inputs, including empty sources;
59
+ public RBS, immutable metadata and deterministic source/file ordering.
60
+
61
+ ### Added — timestamp queries
62
+
63
+ - Chainable created/modified-since and half-open timestamp range queries with
64
+ timezone-explicit ISO 8601 bounds, missing-timestamp exclusion, and Query RBS.
65
+ - Archive CLI timestamp filters with existing TSV/JSON results and exit contracts.
66
+
67
+ ## [0.4.0] - 2026-09-30
68
+
69
+ ### Fixed
70
+
71
+ - Live CLI input uses distinct `--live` auto-discovery and `--live-path PATH` forms;
72
+ mixed modes and unexpected live positional arguments are rejected.
73
+
74
+ - Deduplication recognizes international phone prefixes from trimmed raw input, not
75
+ punctuation-stripped digits, keeping national-looking `(001)` values source-local.
76
+
77
+ - Image extraction exclusively creates destination files and reports `destination_exists`
78
+ rather than overwriting existing files, symlinks, or hard links. Output directory aliases
79
+ are resolved before extraction; extraction diagnostic privacy is documented.
80
+
81
+ ### Added
82
+
83
+ - Explicit, read-only live Contacts access through `Abbu.open_live` and
84
+ `abbu --live` / `abbu --live-path PATH`, with root and `Sources/*` database discovery
85
+ - Actionable live-store errors for missing databases, unsupported automatic
86
+ discovery, and macOS Full Disk Access restrictions
87
+ - Platform-independent synthetic coverage for live-store discovery, source
88
+ provenance, SQLite read-only enforcement, and CLI behavior
89
+ - Provenance-aware `Utils::Deduplicator#matches` suggestions with normalized email,
90
+ international/source-local phone, Unicode name, and organization evidence
91
+ - Match confidence, ambiguity status, original source records, and raw evidence, plus an
92
+ explicit callable merge-policy boundary that never silently collapses contacts
93
+
94
+ - `Archive#extract_images` and `Abbu::ImageExtractor` for copying contact photos to a
95
+ caller-selected directory with structured missing, unreadable, and unsupported-image
96
+ diagnostics
97
+ - `--extract-images DIR` CLI support with content-aware JPEG, PNG, GIF, and HEIC extension
98
+ selection and safe, Unicode-preserving, collision-resistant filenames
99
+ - Source-local resolution for duplicate image stems in nested account directories without
100
+ guessing when provenance cannot disambiguate candidates
101
+
102
+ - Structured tolerant-parsing diagnostics with strict API/CLI mode for corrupt
103
+ plist records, missing optional SQLite data, and unresolved image references
104
+ - Missing optional SQLite tables emit one non-PII diagnostic per database and
105
+ table instead of repeating schema-level warnings for every contact
106
+ - Evidence-safe SQLite schema diagnostics through `Archive#schema_report` and
107
+ `abbu <archive> --schema`, including unknown tables/columns, absent recognized
108
+ schema elements, and owner/contact-style relationship candidates
109
+ - Deterministic schema-variation coverage for missing optional tables, unknown
110
+ contact-linked tables, and column drift
111
+ - Normalized Apple standard labels with the original source value preserved as
112
+ `raw_label` on labeled contact values
113
+ - Chainable `Abbu::Query` and `Archive#where` APIs for contact filtering
114
+ - Exact normalized email and phone lookup that returns all matches across sources
115
+ - Case-insensitive partial name and email search through Ruby and tab-separated CLI output
116
+ - Stable `--json` search output using the regular contact JSON schema
117
+ - Contact creation and modification timestamps from optional SQLite `ZCREATIONDATE` and `ZMODIFICATIONDATE` columns
118
+ - Provenance metadata identifying each contact's source database or plist and its location within the ABBU bundle
119
+ - Creation, modification, and source metadata in JSON exports
120
+ - Repository constitution and focused local skills for ABBU format evidence,
121
+ Ruby gem development, testing, and Sheriff-gated releases
122
+ - Canonical `bin/spec` and `bin/package` workflows, with CI reusing project-local
123
+ `bin/spec` and `bin/lint` instead of duplicating their commands
124
+
125
+ ### Changed
126
+
127
+ - 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
128
+ - SQLite parsing now tolerates absent established email, phone, and postal-address
129
+ tables and returns empty collections while retaining the variation in schema diagnostics;
130
+ unexpected column drift and other SQL errors on present tables continue to surface
131
+ - vCard anniversary export now prefers the original `raw_label` so Apple and
132
+ custom source representations survive parse-and-export round trips
133
+ - Minimum supported Ruby and RuboCop target are now 3.3; CI covers Ruby 3.3,
134
+ 3.4, and 4.0, with Ruby 3.3 as the designated lint/tooling job
135
+ - Agent guidance is consolidated in `AGENTS.md`; the redundant `CLAUDE.md` has
136
+ been removed
137
+ - Gem packaging now includes only the public `bin/abbu` executable instead of
138
+ repository-only developer commands
139
+
140
+ ## [0.3.0] - 2026-09-29
141
+
142
+ ### Added
143
+
144
+ - `Contact#image_uri` and `Contact#image_path` accessors
145
+ - `Parsers::SqliteParser` extracts `ZIMAGEURI` from `ZABCDRECORD`
146
+ - `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
147
+ - `Archive#contacts` automatically resolves `image_path` for any contact that has a non-nil `image_uri`
148
+ - CSV export: new `ImagePath` column (last column)
149
+ - JSON export: `image_uri` and `image_path` fields (omitted via `.compact` when blank)
150
+ - vCard export: `PHOTO;VALUE=URI:file://<absolute path>` line, emitted when `image_path` is present
151
+ - `spec/fixtures/TestContacts.abbu/Images/stan-photo.jpg` stub image, regenerated by `spec/support/fixture_generator.rb`
152
+
153
+ ### Notes
154
+
155
+ - PlistParser does not extract images — legacy `.abcdp` contacts typically embed image data inline, which is a separate extraction path
156
+ - vCard `PHOTO` references the absolute bundle path; base64 embedding and the `--extract-images` CLI flag are tracked for follow-up
157
+
158
+ ### Fixed
159
+
160
+ - vCard `PHOTO` file URIs now percent-encode spaces, reserved characters, and non-ASCII bytes in image paths
161
+ - `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.
162
+
163
+ ## [0.2.0] - 2026-05-03
164
+
165
+ ### Added
166
+
167
+ - `Parsers::PlistParser` — full implementation of legacy `.abcdp` plist contact parsing, replacing the previous stub
168
+ - `PlistParser::FIELD_MAP` constant for flat-field mapping
169
+ - Multi-value field extraction for emails, phones, addresses, URLs, notes, related names, and social profiles
170
+ - `Archive` recursively scans `**/*.abcdp` across the entire bundle tree
171
+ - `plist` gem (~> 3.7) runtime dependency
172
+ - Plist fixture files (`spec/fixtures/PlistContacts.abbu/`) for integration testing
173
+ - `middle_name` attribute on `Contact`; `ZMIDDLENAME` / `Middle` plist key
174
+ - `dates` attribute on `Contact` (array of `{ year:, month:, day:, label: }` hashes)
175
+ - `birthday`, `anniversary`, and `lunar_birthday` accessor methods derived from the dates list
176
+ - `instant_messages` attribute (AIM, Jabber, Skype, etc.); `ZABCDMESSAGINGADDRESS` / `InstantMessage` key
177
+ - `verification_code` attribute on `Contact`; `ZVERIFICATIONCODE` column / `VerificationCode` plist key
178
+ - `phonetic_middle_name` attribute; `ZPHONETICMIDDLENAME` column / `PhoneticMiddle` plist key
179
+ - `ZABCDDATECOMPONENTS` parsing (year/month/day split columns)
180
+ - `BDAY`, `X-LUNAR-BDAY`, `X-ABDATE`, `X-ABLABEL` vCard fields
181
+ - `IMPP` vCard field for instant messaging
182
+ - Birthday, anniversary, lunar birthday, instant messages, and verification code in CSV and JSON exports
183
+ - Plist fixture for testing all the above (`spec/fixtures/PlistContacts.abbu/`)
184
+
185
+ ### Changed
186
+
187
+ - `Contact#full_name` now includes middle name (`Honorable Stan The Man "Stretch" Carver II`)
188
+ - vCard `N` field includes middle name component
189
+ - `SqliteParser::RECORD_FIELD_MAP` extended with the new column → attr mappings
190
+ - `JsonExporter#contact_hash` includes all new fields; blank fields are dropped via `.compact`
191
+ - `CsvExporter` extended-field section now exports lunar birthday and verification code
192
+
14
193
  ## [0.1.2] - 2026-04-26
15
194
 
16
195
  ### Added
data/docs/CONTRIBUTING.md CHANGED
@@ -23,19 +23,41 @@ 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
+ Keep the [format compatibility matrix](FORMAT_COMPATIBILITY.md) passing when
30
+ changing parsers, discovery, models or exporters. Add evidence-backed synthetic
31
+ variations without replacing legacy fixtures. Run the focused matrix with
32
+ `bin/spec spec/abbu/archive_compatibility_spec.rb`; the full suite remains
33
+ authoritative for the 100% coverage gate.
34
+
29
35
  ## Linting
30
36
 
31
37
  ```bash
32
- mise exec -- bundle exec rubocop
38
+ bin/lint
33
39
  mise exec -- bundle exec rubocop -a # autocorrect
34
40
  ```
35
41
 
42
+ ## Packaging
43
+
44
+ ```bash
45
+ bin/package
46
+ ```
47
+
48
+ This builds the current gem, verifies its metadata, installs it into an isolated
49
+ gem home, and loads that installed copy. A successful package check is evidence
50
+ only; it does not authorize a release tag or RubyGems publication.
51
+
52
+ Run `bundle install` through the project toolchain first. Package verification
53
+ installs ABBU without resolving dependencies again, then loads that installed
54
+ copy using Bundler's dependency search paths. Missing runtime dependencies still
55
+ fail the load check. Only ABBU is isolated, not the dependency set. The CI Ruby
56
+ matrix runs this same command with its Bundler-managed installation paths.
57
+
36
58
  ## Pull Request Guidelines
37
59
 
38
- - Base branch: `master`
60
+ - Base branch: `main`
39
61
  - Commit style: [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `chore:`)
40
62
  - All specs must pass and coverage must remain at 100%
41
63
  - RuboCop must pass with no offenses
@@ -0,0 +1,97 @@
1
+ <!-- docs/FORMAT_COMPATIBILITY.md -->
2
+
3
+ # ABBU Compatibility Regression Matrix
4
+
5
+ Legacy support is a tested behavior, not a filename/version guess. The suite
6
+ must keep the older supported shapes passing as newer fields and layouts are
7
+ added. This matrix is run by ordinary `bin/spec` and therefore by every current
8
+ CI Ruby job (3.3, 3.4, 4.0); it needs no macOS services or personal contacts.
9
+
10
+ ## Evidence and scope
11
+
12
+ All profiles below are **synthetic**, with **unknown macOS and Contacts build**.
13
+ Their evidence is the repository's existing XML fixture keys, SQLite fixture
14
+ generator, and parser schema-variance tests. They are not collected exports from
15
+ eight Apple releases. In particular, `v1` and `v99` are filename-dispatch probes
16
+ using the known schema; they do not establish support for real schemas with
17
+ those version numbers. Do not report the Ruby CI matrix as a macOS matrix.
18
+
19
+ `spec/support/compatibility_fixtures.rb` rebuilds each profile inside a fresh
20
+ temporary directory. It reuses only the non-destructive schema/seed methods of
21
+ the existing fixture generator, not its bundle-reset command. No committed
22
+ binary is regenerated or overwritten by these tests.
23
+
24
+ ## Always-on end-to-end profiles
25
+
26
+ | Profile | Structure / variation | Expected boundary |
27
+ | --- | --- | --- |
28
+ | `xml_records` | XML `.abcdp` under `Records/` | Plist fallback, raw labels, absent SQLite-only timestamps/groups |
29
+ | `xml_nested` | XML records below `Sources/Équipe/Records/` | Recursive archive discovery and source provenance |
30
+ | `sqlite_sparse` | Synthetic `AddressBook-v1.abcddb`; absent timestamp/image/name-extension columns and most relationship tables | Preserve core contact/email; nil absent fields, empty collections, tolerant diagnostics; strict mode raises |
31
+ | `sqlite_root` | Root `AddressBook-v22.abcddb` with all currently queried relationship tables | Strict-mode success, timestamps, original group membership |
32
+ | `sqlite_source_only` | Database only under `Sources/Équipe/` | Root database is not required |
33
+ | `sqlite_empty_root` | Empty root plus two source databases with colliding contact/group keys | Empty source remains listed; every source's contact/group remains distinct |
34
+ | `sqlite_mixed_schemas` | Sparse root plus complete nested database, different filename suffixes | Optional-table state is file-local, not cached globally across schemas |
35
+ | `mixed_formats` | Both SQLite and XML records | Preserve current SQLite-first policy; plist records are intentionally not combined |
36
+
37
+ Each profile independently asserts expected record counts and file provenance,
38
+ raw versus normalized email labels, query results, timestamp availability, group
39
+ traversal, source enumeration, and strict/tolerant outcomes. JSON retains labels
40
+ and provenance; CSV and vCard retain the core email/contact counts. Export tests
41
+ compare SHA-256 of every input file before and after parsing/exporting and detect
42
+ new or missing files as well as changed bytes. These archive tests do not claim
43
+ byte-for-byte immutability of live WAL shared-memory state.
44
+
45
+ Additional matrix regressions prevent an unknown required SQLite schema from
46
+ silently falling back to XML, and keep valid XML siblings recoverable when one
47
+ record is malformed. They test public `Abbu.open` behavior without parser mocks.
48
+ The matrix does not freeze vCard whitespace or property spelling; the dedicated
49
+ exporter tests own those contracts.
50
+
51
+ ## Complementary existing coverage
52
+
53
+ | Variation | Regression location |
54
+ | --- | --- |
55
+ | Full relational fields; absent/invalid timestamps; missing tables vs missing columns | `spec/abbu/parsers/sqlite_parser_spec.rb` |
56
+ | Rich/minimal XML; malformed records; dates; raw anniversary labels | `spec/abbu/parsers/plist_parser_spec.rb` |
57
+ | Empty/multiple files, source collisions, SQLite-first precedence | `spec/abbu/source_spec.rb` |
58
+ | Same-name groups, duplicate joins, null/Unicode labels | `spec/abbu/group_spec.rb` |
59
+ | Duplicate image stems, source-local resolution, missing images | `spec/abbu/utils/image_resolver_spec.rb`, `spec/abbu/archive_spec.rb` |
60
+ | JPEG/PNG/GIF/HEIC signatures, unknown bytes, safe extraction | `spec/abbu/image_extractor_spec.rb` |
61
+ | WAL visibility, concurrent commits and cache limits | `spec/abbu/live_store_wal_spec.rb` |
62
+ | Unknown tables/columns and schema drift without inferred mappings | `spec/abbu/schema_inspector_spec.rb` |
63
+
64
+ ## Unverified historical boundaries
65
+
66
+ There is no release-attributed real-archive corpus in this repository. Binary
67
+ plist `.abcdp`, alternate private table/column/entity mappings, and exports from
68
+ specific historical macOS/Contacts builds are **not certified by this matrix**.
69
+ XML support must not be generalized to every plist encoding. Existing recovery
70
+ tests likewise do not prove all corruption forms recover safely.
71
+
72
+ To add a genuine historical variation:
73
+
74
+ 1. Record the export's macOS version, Contacts build, export method and storage
75
+ encoding; mark any unavailable metadata unknown.
76
+ 2. Inspect the relevant behavior privately. Never commit a real address book,
77
+ photo, account identifier, or unsanitized schema/data dump.
78
+ 3. Reproduce only the necessary structure with deterministic invented data,
79
+ recording the observed keys/columns/relationships and source evidence.
80
+ 4. Add a named matrix profile or focused regression with explicit expected
81
+ fields, provenance, raw evidence, exports, diagnostics and strict behavior.
82
+ 5. Fix supported behavior only after the regression demonstrates the gap. Keep
83
+ older fixtures/tests; do not replace them with the newest schema. Unknown
84
+ formats should remain explicit gaps, not guessed adapters or silent fallback.
85
+ 6. Record the evidence and limits in [ABBU.md](ABBU.md) and this matrix, and run
86
+ the full suite/lint/CI before review.
87
+
88
+ This test-only expansion changes neither runtime behavior nor the gem version.
89
+ Future behavior fixes use patch bumps; newly completed features use minor bumps.
90
+
91
+ Return to [README](../README.md), [format evidence](ABBU.md), or
92
+ [contributing](CONTRIBUTING.md).
93
+
94
+ —
95
+ Stan Carver II
96
+ Made in Texas 🤠
97
+ https://stancarver.com
data/docs/RELEASING.md ADDED
@@ -0,0 +1,75 @@
1
+ <!-- docs/RELEASING.md -->
2
+
3
+ # RubyGems Trusted Publishing
4
+
5
+ ABBU publishes RubyGems releases from GitHub Actions using RubyGems.org Trusted
6
+ Publishing. The release workflow intentionally contains no long-lived RubyGems
7
+ API key.
8
+
9
+ Workflow-level permissions default to `{}`. The release job grants only
10
+ `contents: read` for checkout/source inspection and `id-token: write` for OIDC.
11
+ It has no GitHub repository write permission; the version tag must already exist.
12
+
13
+ ## One-time RubyGems.org setup
14
+
15
+ As a RubyGems owner for `abbu`, configure a Trusted Publisher with:
16
+
17
+ - GitHub repository owner: `scarver2`
18
+ - GitHub repository: `abbu`
19
+ - workflow filename: `release.yml`
20
+ - GitHub environment: `release`
21
+
22
+ Create the GitHub environment named `release` with `scarver2` as a required
23
+ reviewer and administrator bypass disabled. Only tags matching `v*` may deploy.
24
+ The Sheriff may approve a release they initiated; approval is still explicit.
25
+ Verify these settings before the first automated publication. Merely naming an
26
+ environment in YAML does not protect it.
27
+
28
+ Official RubyGems guidance:
29
+ https://guides.rubygems.org/trusted-publishing/
30
+
31
+ ## Release flow
32
+
33
+ 1. Prepare and review the release on `main`.
34
+ 2. Run the canonical spec, lint, and package gates.
35
+ 3. Obtain explicit Sheriff authorization for the specific version.
36
+ 4. Push only the authorized `vMAJOR.MINOR.PATCH` tag.
37
+ 5. GitHub Actions runs `.github/workflows/release.yml`.
38
+ 6. The Sheriff reviews the exact tag target and approves the `release` environment.
39
+ The workflow requires a stable version tag reachable from `origin/main`, checks
40
+ that it matches `Abbu::VERSION`, and reruns the package gates before publishing
41
+ with `rubygems/release-gem@v1` using OIDC.
42
+ 7. Verify the RubyGems release and provenance against the accepted source.
43
+
44
+ The workflow does not bump versions, create tags, or grant release authority.
45
+
46
+ For repeatable candidate verification, run:
47
+
48
+ ```bash
49
+ SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD) bin/package
50
+ ```
51
+
52
+ The workflow sets that same value for
53
+ both verification and the action's `bundle exec rake release` build. Compare the
54
+ published gem with the accepted source; never move a release tag or republish a
55
+ version to recover from a failed run. A failed run after upload requires checking
56
+ RubyGems before retrying. Do not replay the already published `v0.4.0` tag.
57
+
58
+ Normal PR CI remains the Ruby 3.3/3.4/4.0 review gate; the release job repeats
59
+ spec/lint/package checks on the Ruby 3.3 compatibility floor. Local/manual pushes
60
+ are not the default path. No API key secret is needed, and Trusted Publisher
61
+ registration is not itself permission to publish a new version.
62
+
63
+ ## Repository standard
64
+
65
+ Use this pattern by default for our other GitHub-hosted RubyGems: OIDC Trusted
66
+ Publishing, a protected `release` environment, exact version-tag verification,
67
+ canonical repository checks, and no long-lived RubyGems publishing secret.
68
+
69
+ See [AGENTS.md](../AGENTS.md), [contribution workflows](CONTRIBUTING.md), and
70
+ [README](../README.md). This infrastructure PR does not authorize a release.
71
+
72
+ —
73
+ Stan Carver II
74
+ Made in Texas 🤠
75
+ https://stancarver.com
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,82 @@ 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
+ The [API stability gate](API_STABILITY.md) defines the evidence required below;
170
+ its existence does not mean those checks are complete or authorize a 1.0 release.
171
+
172
+ - [ ] Complete the API-stability checklist and acceptance evidence before promising stability
173
+ - [ ] Comprehensive public API documentation
174
+ - [ ] Benchmarks for large archives (10k+ contacts)
175
+ - [ ] Optional external CRM adapters (Printavo, HubSpot, generic webhook/API), separately scoped
176
+
177
+ See [README](../README.md) and [CHANGELOG](CHANGELOG.md) for current behavior
178
+ and historical releases.
179
+
180
+ —
170
181
  Stan Carver II
171
182
  Made in Texas 🤠
172
183
  https://stancarver.com
data/lib/abbu/archive.rb CHANGED
@@ -2,26 +2,76 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'pathname'
5
- require_relative 'parsers/sqlite_parser'
5
+ require_relative 'diagnostic'
6
+ require_relative 'image_extractor'
7
+ require_relative 'parse_error'
6
8
  require_relative 'parsers/plist_parser'
9
+ require_relative 'parsers/sqlite_parser'
10
+ require_relative 'query'
11
+ require_relative 'schema_inspector'
12
+ require_relative 'source_catalog'
13
+ require_relative 'utils/image_resolver'
7
14
 
8
15
  module Abbu
9
16
  class Archive
10
- attr_reader :path
17
+ attr_reader :diagnostics, :path
11
18
 
12
- def initialize(path)
19
+ def initialize(path, strict: false)
13
20
  @path = Pathname.new(path)
21
+ @strict = strict
22
+ @diagnostics = []
14
23
  validate!
15
24
  end
16
25
 
17
26
  def contacts
18
- parser.contacts
27
+ @contacts ||= parser.contacts.tap { |cs| attach_images(cs) }
28
+ end
29
+
30
+ def query
31
+ Query.new(contacts)
32
+ end
33
+
34
+ def where(criteria)
35
+ query.where(criteria)
36
+ end
37
+
38
+ def search(term)
39
+ query.search(term)
40
+ end
41
+
42
+ def find_by_email(email)
43
+ query.find_by_email(email)
44
+ end
45
+
46
+ def find_by_phone(phone)
47
+ query.find_by_phone(phone)
19
48
  end
20
49
 
21
50
  def sqlite?
22
51
  db_paths.any?
23
52
  end
24
53
 
54
+ def extract_images(output_dir)
55
+ ImageExtractor.new(contacts).extract(output_dir)
56
+ end
57
+
58
+ def schema_report
59
+ SchemaInspector.new(db_paths, root_path: @path).report
60
+ end
61
+
62
+ # Enumerate only the files selected by this archive's existing parser mode.
63
+ def sources
64
+ @sources ||= SourceCatalog.new(sqlite? ? db_paths : plist_paths, root_path: @path, contacts: contacts).sources
65
+ end
66
+
67
+ def groups
68
+ @groups ||= sources.flat_map(&:groups).freeze
69
+ end
70
+
71
+ def groups_for(contact)
72
+ groups.select { |group| group.include?(contact) }
73
+ end
74
+
25
75
  private
26
76
 
27
77
  def validate!
@@ -39,10 +89,34 @@ module Abbu
39
89
 
40
90
  def parser
41
91
  if sqlite?
42
- Parsers::SqliteParser.new(db_paths)
92
+ Parsers::SqliteParser.new(db_paths, root_path: @path, diagnostics: diagnostics, strict: @strict)
43
93
  else
44
- Parsers::PlistParser.new(plist_paths)
94
+ Parsers::PlistParser.new(plist_paths, root_path: @path, diagnostics: diagnostics, strict: @strict)
45
95
  end
46
96
  end
97
+
98
+ def attach_images(contacts)
99
+ return if contacts.empty?
100
+
101
+ resolver = Utils::ImageResolver.new(@path)
102
+ contacts.each do |contact|
103
+ next unless contact.image_uri
104
+
105
+ contact.image_path = resolver.resolve(contact.image_uri, source: contact.source)
106
+ record_missing_image(contact) unless contact.image_path
107
+ end
108
+ end
109
+
110
+ def record_missing_image(contact)
111
+ diagnostic = Diagnostic.new(
112
+ category: :missing_image,
113
+ message: 'Referenced contact image was not found',
114
+ parser: :archive,
115
+ source: contact.source&.fetch(:path, @path.to_s) || @path.to_s,
116
+ context: {}
117
+ )
118
+ diagnostics << diagnostic
119
+ raise ParseError, diagnostic if @strict
120
+ end
47
121
  end
48
122
  end