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/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
|
-
|
|
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
|
-
|
|
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: `
|
|
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 (
|
|
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
|
|
105
|
+
## v0.3.0 — Image Resolution (Released)
|
|
106
106
|
|
|
107
|
-
- [
|
|
108
|
-
- [
|
|
109
|
-
- [
|
|
110
|
-
- [
|
|
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 '
|
|
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
|