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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71bd4536cf0e22f9bf9177d5715f4210a84a3b50bcab562266fe4d71c24d831a
|
|
4
|
+
data.tar.gz: 1bce6e9c3022f814f7ae789c232e8f0d62fe17999cca109cdba16825cea5a7f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 378d928ea9d82de3f5ddb54e8f4d9b61e8b14a9cea61225248394e4d665bc8a5c7263a8292af2089acb879b6a0d600dd532fa9a288c5025b36de9c000fab6107
|
|
7
|
+
data.tar.gz: 573617afdbedd7af5a415dd08f0fb1b2c330d8ccc1bc4d798f0a9febc38eb455b83e292a4c3f25dd30728770fe56a0b97cb65c1acd1e9a13e59df7be43377042
|
data/README.md
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
# abbu
|
|
4
4
|
|
|
5
|
-
Read
|
|
5
|
+
Read-only Apple Contacts toolkit for `.abbu` archives and opt-in live macOS stores.
|
|
6
|
+
Development version 0.8.0 strengthens vCard serialization alongside source-scoped groups, timestamp
|
|
7
|
+
queries, diagnostics, identity evidence, image extraction, and CSV/JSON/vCard export.
|
|
8
|
+
The public API remains pre-1.0.
|
|
6
9
|
|
|
7
10
|
## Features
|
|
8
11
|
|
|
9
12
|
- Parse ABBU (Apple Contacts export) bundles
|
|
13
|
+
- Opt-in, read-only access to a local macOS Contacts store
|
|
10
14
|
- SQLite-backed contact extraction (modern macOS)
|
|
11
15
|
- Legacy plist `.abcdp` parsing (older macOS)
|
|
12
|
-
-
|
|
16
|
+
- Evidence-backed Apple Contacts fields: names, nicknames, prefix/suffix, job title, department, phonetics, pronouns, and more
|
|
13
17
|
- Rich relational data: addresses, URLs, notes, related names, social profiles
|
|
14
18
|
- Export to CSV, JSON, vCard 3.0
|
|
15
19
|
- CLI + Ruby API
|
|
16
|
-
-
|
|
20
|
+
- Source provenance and creation/modification timestamps
|
|
21
|
+
- Lossless raw labels alongside human-friendly normalization
|
|
22
|
+
- Schema introspection, tolerant diagnostics, and strict mode
|
|
23
|
+
- Chainable archive queries, exact identifier lookup, and TSV/JSON CLI search
|
|
24
|
+
- Provenance-aware duplicate suggestions without automatic merging
|
|
25
|
+
- Safe photo extraction with content detection and no destination overwrites
|
|
17
26
|
|
|
18
27
|
## Installation
|
|
19
28
|
|
|
@@ -36,13 +45,192 @@ require "abbu"
|
|
|
36
45
|
|
|
37
46
|
archive = Abbu.open("Contacts.abbu")
|
|
38
47
|
contacts = archive.contacts
|
|
48
|
+
schema = archive.schema_report # Evidence-only SQLite schema diagnostics
|
|
39
49
|
|
|
40
50
|
contacts.first.full_name # => "Honorable Stan \"Stretch\" Carver II"
|
|
41
|
-
contacts.first.emails # => [{ address: "stan@example.com", label: "Work" }]
|
|
42
|
-
contacts.first.phones # => [{ number: "555-1234", label: "Mobile" }]
|
|
51
|
+
contacts.first.emails # => [{ address: "stan@example.com", label: "Work", raw_label: "_$!<Work>!$_" }]
|
|
52
|
+
contacts.first.phones # => [{ number: "555-1234", label: "Mobile", raw_label: "Mobile" }]
|
|
43
53
|
contacts.first.job_title # => "Engineer"
|
|
54
|
+
|
|
55
|
+
# Copy resolved photos using safe, content-derived filenames.
|
|
56
|
+
result = archive.extract_images("exported-photos")
|
|
57
|
+
result.files # copied-file metadata, including source_path and media_type
|
|
58
|
+
result.diagnostics # image errors or destination_exists; may contain contact identifiers
|
|
59
|
+
|
|
60
|
+
# Recover safe records and inspect non-fatal data loss.
|
|
61
|
+
archive.diagnostics.each { |diagnostic| warn diagnostic.to_h }
|
|
62
|
+
|
|
63
|
+
# Or fail on the first corrupt/unsupported optional input.
|
|
64
|
+
strict_contacts = Abbu.open("Contacts.abbu", strict: true).contacts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Live-store access is a separate, explicit API and never changes `Abbu.open` archive
|
|
68
|
+
validation:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Auto-discover the current macOS user's AddressBook directory
|
|
72
|
+
live_contacts = Abbu.open_live.contacts
|
|
73
|
+
|
|
74
|
+
# Or supply a directory for automation and platform-independent testing
|
|
75
|
+
live_contacts = Abbu.open_live("/path/to/AddressBook").contacts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Live databases are opened with SQLite's read-only mode. The process may require
|
|
79
|
+
Full Disk Access under **System Settings → Privacy & Security → Full Disk Access**.
|
|
80
|
+
Live inputs expose parser `diagnostics` and accept `strict: true` (CLI `--strict`).
|
|
81
|
+
The live CLI supports source/group listing, stats, deduplication, and exports; archive-only search, schema,
|
|
82
|
+
and image-extraction options are rejected explicitly.
|
|
83
|
+
|
|
84
|
+
Live access does not write Contacts databases. Synthetic WAL tests verify committed
|
|
85
|
+
data visibility and read-only access, but a read is **not an atomic snapshot**:
|
|
86
|
+
separate contact/relationship queries can observe different commits, and databases
|
|
87
|
+
are read independently. A LiveStore caches its first contact result; reopen it to
|
|
88
|
+
refresh. See [live-store consistency evidence](docs/ABBU.md#wal-and-concurrent-writer-evidence).
|
|
89
|
+
|
|
90
|
+
Labeled values expose a normalized `label` for display and retain the source
|
|
91
|
+
value in `raw_label`. For example, `_$!<Mobile>!$_` becomes `Mobile` while the
|
|
92
|
+
original wrapper remains available in `raw_label`.
|
|
93
|
+
|
|
94
|
+
### Search and identifier lookup
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Exact lookup normalizes email case/whitespace and phone punctuation.
|
|
98
|
+
archive.find_by_email("STAN@EXAMPLE.COM").each { |contact| puts contact.full_name }
|
|
99
|
+
archive.find_by_phone("(555) 123-4567").each { |contact| puts contact.full_name }
|
|
100
|
+
|
|
101
|
+
# Name and email search is case-insensitive and can be chained with `where`.
|
|
102
|
+
archive.where(company: "Acme Corp").search("stan").each do |contact|
|
|
103
|
+
puts [contact.full_name, contact.source[:relative_path]].join("\t")
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Lookup methods return every match as an `Abbu::Query`; they never silently pick
|
|
108
|
+
one contact when the same identifier appears in multiple sources. Returned
|
|
109
|
+
contacts retain their parser-provided source provenance.
|
|
110
|
+
|
|
111
|
+
### Timestamp queries
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
archive.query.modified_since('2026-09-01T00:00:00Z').search('stan')
|
|
115
|
+
archive.query.date_range(:created_at, since: '2026-09-01T00:00:00Z',
|
|
116
|
+
before: '2026-10-01T00:00:00Z')
|
|
117
|
+
# Also works with explicitly opened live data:
|
|
118
|
+
Abbu::Query.new(Abbu.open_live.contacts).created_since(Time.utc(2026, 9, 1))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Bounds accept `Time` or full ISO 8601 strings with seconds and an explicit `Z`
|
|
122
|
+
or numeric offset. Start (`since`) is inclusive; end (`before`) is exclusive.
|
|
123
|
+
At least one bound is required, and start must precede end. Invalid bounds raise
|
|
124
|
+
`ArgumentError`, including on empty queries. Missing timestamps never match.
|
|
125
|
+
Filters preserve source records and compose with other Query criteria. These are
|
|
126
|
+
observed storage timestamps, not proof of human edits or a complete change feed;
|
|
127
|
+
deletions and changes without timestamps cannot be discovered this way.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
abbu Contacts.abbu --modified-since 2026-09-01T00:00:00Z --json
|
|
131
|
+
abbu Contacts.abbu --created-since 2026-09-01T00:00:00Z --created-before 2026-10-01T00:00:00Z --search stan --json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
All four `--created-since`, `--created-before`, `--modified-since`, and
|
|
135
|
+
`--modified-before` filters can be combined (AND). They use existing search
|
|
136
|
+
TSV/JSON output: status 0 for matches, 1 for no matches, and 2 for invalid bounds
|
|
137
|
+
or incompatible output options. Diagnostics stay on stderr. CLI timestamp queries
|
|
138
|
+
are archive-only, like existing search; use the Ruby Query API for live inputs.
|
|
139
|
+
Do not combine timestamp queries with export, stats, schema, dedupe, or extraction.
|
|
140
|
+
|
|
141
|
+
### Sources
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
sources = archive.sources # Frozen Array<Abbu::Source>, sorted by relative_path
|
|
145
|
+
source = sources.first
|
|
146
|
+
source.relative_path # "." for root, or "Sources/<observed identifier>"
|
|
147
|
+
source.identifier # Raw directory identifier, or nil for root
|
|
148
|
+
source.provider # nil: never inferred from paths or identifier spelling
|
|
149
|
+
source.files # Frozen file-level provenance descriptors
|
|
150
|
+
source.contacts.search('stan') # Query over original contacts, no automatic deduplication
|
|
151
|
+
source.group_names # Sorted unique observed membership labels, not group identities
|
|
152
|
+
source.to_h # Metadata, files, contact_count, group_names; no contact payload
|
|
153
|
+
|
|
154
|
+
Abbu.open_live('/path/to/AddressBook').sources # Same API, read-only connections
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Sources are observed input containers, not verified Apple account identities.
|
|
158
|
+
Existing `contact.source` hashes are unchanged. Membership is based on the exact
|
|
159
|
+
file provenance path; repeated contact IDs and filenames in different sources
|
|
160
|
+
never merge. Files from the same observed source directory form one container.
|
|
161
|
+
An empty discovered database still appears; an empty archive returns `[]`.
|
|
162
|
+
Plist records outside `Sources/` form the root container. Archives retain their
|
|
163
|
+
existing SQLite-first parser selection: ignored plist files are not listed.
|
|
164
|
+
|
|
165
|
+
Source metadata, file descriptors, membership arrays, and group-name strings are
|
|
166
|
+
frozen snapshots. Contacts remain the original mutable objects. `sources` eagerly
|
|
167
|
+
parses contacts, honors diagnostics/strict mode, and is cached; reopen the input to
|
|
168
|
+
refresh. It does not add live-store atomic snapshot guarantees. Group names cover
|
|
169
|
+
observed contact memberships only, not empty groups or same-name group identity.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
abbu Contacts.abbu --sources
|
|
173
|
+
abbu --live-path /path/to/AddressBook --sources --json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`--sources` always emits a JSON array to stdout; `--json` is optional. Each object
|
|
177
|
+
has `path`, `relative_path`, `kind`, `identifier`, `provider`, `files`,
|
|
178
|
+
`contact_count`, and `group_names`; unknown `provider` and root `identifier` are
|
|
179
|
+
explicit JSON nulls. `files` uses the existing four-key contact provenance schema.
|
|
180
|
+
Source/file ordering is by relative path; contact order is parser order within
|
|
181
|
+
those sorted files. Listings exit 0 even when empty. Conflicting operations or
|
|
182
|
+
strict parse failures exit 2, live input/access failures exit 1, and diagnostics
|
|
183
|
+
stay on stderr. Only `--strict` and `--json` may accompany this operation besides
|
|
184
|
+
input selection. Paths, raw identifiers, and group names may be sensitive;
|
|
185
|
+
do not publish source listings as sanitized logs.
|
|
186
|
+
|
|
187
|
+
### Groups
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
group = archive.groups.first # Frozen Array<Abbu::Group>
|
|
191
|
+
group.record_id # Observed SQLite group key, local to group.source[:path]
|
|
192
|
+
group.name # Original name, including whitespace/Unicode; may be nil
|
|
193
|
+
group.source # Frozen file-level provenance, including source identifier
|
|
194
|
+
group.contacts.search('stan') # Query over the original mutable contacts
|
|
195
|
+
archive.groups_for(archive.contacts.first) # Reverse lookup, without changing Contact#groups
|
|
196
|
+
archive.sources.first.groups # Same objects, scoped to that source
|
|
197
|
+
archive.query.where(company: 'Acme Corp').in_group(group)
|
|
198
|
+
Abbu.open_live('/path/to/AddressBook').groups # Same API, read-only
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Groups are identified by database path plus observed record key, never by name.
|
|
202
|
+
Same-name groups within a file and repeated keys across files/sources remain
|
|
203
|
+
distinct. `Contact#groups` remains the original array of labels (including duplicate
|
|
204
|
+
or null names); `Contact#group_memberships` additionally retains `{ record_id:, name: }`
|
|
205
|
+
for each observed join row, including duplicates. Group contacts list each original
|
|
206
|
+
contact once. Existing contact JSON/CSV/vCard output is unchanged.
|
|
207
|
+
|
|
208
|
+
Only groups reached by supported contact membership joins are listed. Empty or
|
|
209
|
+
unreferenced groups, dangling joins, and legacy plist group relationships are not
|
|
210
|
+
enumerated; no semantics are inferred for them. Missing optional membership tables
|
|
211
|
+
retain tolerant diagnostics and strict-mode errors. Ordering is source relative
|
|
212
|
+
path, file relative path, then numeric record key; contacts retain parser order.
|
|
213
|
+
|
|
214
|
+
Metadata and membership snapshots are frozen when sources/groups are first built;
|
|
215
|
+
contacts themselves remain mutable. Later edits to contact labels/evidence do not
|
|
216
|
+
rewrite these snapshots. `groups_for` and `Query#in_group` use original contact
|
|
217
|
+
object identity: a contact freshly parsed by another input instance does not
|
|
218
|
+
belong to this snapshot, even for the same path. Reopen to refresh; live reads
|
|
219
|
+
retain the consistency limitations described above.
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
abbu Contacts.abbu --groups
|
|
223
|
+
abbu --live-path /path/to/AddressBook --groups --json
|
|
44
224
|
```
|
|
45
225
|
|
|
226
|
+
`--groups` emits a JSON array of `record_id` (integer), `name` (string or null),
|
|
227
|
+
`source` (the four-key file provenance hash), and `contact_count` (integer).
|
|
228
|
+
`Group#to_h` uses the same schema. Names are never normalized. Empty results exit 0;
|
|
229
|
+
strict failures and conflicting operations exit 2; live access failures exit 1.
|
|
230
|
+
Only input selection, `--strict`, and optional `--json` may accompany this mode;
|
|
231
|
+
`--sources` and `--groups` cannot be combined. Diagnostics stay on stderr.
|
|
232
|
+
Group names, IDs and source paths may be sensitive; this is not sanitized logging.
|
|
233
|
+
|
|
46
234
|
### Export
|
|
47
235
|
|
|
48
236
|
```ruby
|
|
@@ -56,6 +244,34 @@ Abbu::Exporters::JsonExporter.new(archive.contacts).to_file("contacts.json")
|
|
|
56
244
|
Abbu::Exporters::VcardExporter.new(archive.contacts).to_file("contacts.vcf")
|
|
57
245
|
```
|
|
58
246
|
|
|
247
|
+
vCard output uses UTF-8, CRLF endings (including the final line), escaped TEXT
|
|
248
|
+
values, and folding at no more than 75 bytes without splitting UTF-8 characters.
|
|
249
|
+
Repeated fields retain order. `N` and `ADR` escape each component independently.
|
|
250
|
+
Text newline variants become vCard `\\n`; URL/IM URI values use percent encoding
|
|
251
|
+
instead of TEXT escaping. Empty contact input emits no bytes.
|
|
252
|
+
|
|
253
|
+
Labeled emails, phones, addresses, URLs, IM handles, and anniversaries pair with
|
|
254
|
+
`itemN.X-ABLABEL` using the same per-card `itemN` group. The original `raw_label`
|
|
255
|
+
wins over the display `label`, including empty strings. Custom labels no longer
|
|
256
|
+
become arbitrary `TYPE` parameters. Only exact ASCII standard type names are
|
|
257
|
+
recognized, case-insensitively; email/phone defaults are `INTERNET`/`VOICE`.
|
|
258
|
+
For example, `Mobile` is preserved as a label, not guessed to mean `CELL`.
|
|
259
|
+
No preference is invented for anniversaries. Existing photo file URIs remain;
|
|
260
|
+
embedded photos are separate work.
|
|
261
|
+
|
|
262
|
+
Migration from 0.7: consumers must unfold CRLF continuations before parsing,
|
|
263
|
+
decode TEXT escapes, and accept grouped property names rather than matching
|
|
264
|
+
literal lines such as `EMAIL;TYPE=Work`. The exporter signatures are unchanged.
|
|
265
|
+
Unsupported controls, invalid UTF-8, unsafe social-service parameter tokens,
|
|
266
|
+
or invalid IM service schemes fail explicitly before writing output; export
|
|
267
|
+
does not sanitize or mutate Contact evidence. Other encoding conversions may
|
|
268
|
+
raise Ruby encoding errors. Existing files are overwritten on a successful
|
|
269
|
+
`to_file`, as before; this is not an atomic/no-clobber writer.
|
|
270
|
+
|
|
271
|
+
See [vCard evidence and compatibility limits](docs/ABBU.md#vcard-serialization-evidence)
|
|
272
|
+
for the standards basis and Apple-specific audit. Synthetic round-trip tests
|
|
273
|
+
are not proof of import fidelity in every Apple Contacts release.
|
|
274
|
+
|
|
59
275
|
### Duplicate Detection
|
|
60
276
|
|
|
61
277
|
```ruby
|
|
@@ -66,6 +282,23 @@ dupes.each do |email, contacts|
|
|
|
66
282
|
end
|
|
67
283
|
```
|
|
68
284
|
|
|
285
|
+
For provenance-aware suggestions, use `#matches`. Results preserve both contacts,
|
|
286
|
+
their source records, raw and normalized evidence, confidence, and ambiguity:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
matches = Abbu::Utils::Deduplicator.new(archive.contacts).matches
|
|
290
|
+
matches.each do |match|
|
|
291
|
+
puts "#{match.confidence}: #{match.left.full_name} / #{match.right.full_name}"
|
|
292
|
+
pp match.sources
|
|
293
|
+
pp match.evidence
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Matching never mutates or collapses contacts. Merging requires a caller policy:
|
|
297
|
+
merged = matches.first.merge(policy: ->(left, right, evidence:) {
|
|
298
|
+
MyContactMerge.call(left, right, evidence: evidence)
|
|
299
|
+
})
|
|
300
|
+
```
|
|
301
|
+
|
|
69
302
|
## CLI
|
|
70
303
|
|
|
71
304
|
```bash
|
|
@@ -78,13 +311,41 @@ abbu Contacts.abbu -f json | jq .
|
|
|
78
311
|
# vCard export
|
|
79
312
|
abbu Contacts.abbu -f vcard -o contacts.vcf
|
|
80
313
|
|
|
314
|
+
# Copy contact photos to a selected directory
|
|
315
|
+
abbu Contacts.abbu --extract-images exported-photos
|
|
316
|
+
|
|
81
317
|
# Stats
|
|
82
318
|
abbu Contacts.abbu --stats
|
|
83
319
|
|
|
84
320
|
# Find duplicates
|
|
85
321
|
abbu Contacts.abbu --dedupe
|
|
322
|
+
|
|
323
|
+
# Read the current macOS user's live Contacts store
|
|
324
|
+
abbu --live --stats
|
|
325
|
+
|
|
326
|
+
# Read a caller-supplied AddressBook directory
|
|
327
|
+
abbu --live-path /path/to/AddressBook -f json
|
|
328
|
+
|
|
329
|
+
# Fail on the first corrupt or unsupported optional record/table.
|
|
330
|
+
abbu Contacts.abbu --stats --strict
|
|
331
|
+
|
|
332
|
+
# Inspect each SQLite schema without inferring undocumented semantics
|
|
333
|
+
abbu Contacts.abbu --schema
|
|
334
|
+
|
|
335
|
+
# Tab-separated search output: name, emails, phones, source-relative path
|
|
336
|
+
abbu Contacts.abbu --search stan
|
|
337
|
+
abbu Contacts.abbu --email stan@example.com
|
|
338
|
+
abbu Contacts.abbu --phone '(555) 123-4567'
|
|
339
|
+
|
|
340
|
+
# Stable structured search output using the regular contact JSON schema
|
|
341
|
+
abbu Contacts.abbu --search stan --json | jq .
|
|
86
342
|
```
|
|
87
343
|
|
|
344
|
+
CLI search defaults to tab-separated output and exits successfully when at least
|
|
345
|
+
one contact matches. A search with no matches exits with status 1; TSV mode emits
|
|
346
|
+
no output, while `--json` emits a valid empty array. This makes both modes
|
|
347
|
+
suitable for shell conditionals, pipelines, and agent integrations.
|
|
348
|
+
|
|
88
349
|
## Rake Tasks
|
|
89
350
|
|
|
90
351
|
```ruby
|
|
@@ -103,17 +364,29 @@ rake abbu:stats[Contacts.abbu]
|
|
|
103
364
|
See [`docs/ABBU.md`](docs/ABBU.md) for a full explanation of the archive structure,
|
|
104
365
|
SQLite table schema, and format history.
|
|
105
366
|
|
|
367
|
+
The [compatibility regression matrix](docs/FORMAT_COMPATIBILITY.md) protects
|
|
368
|
+
legacy XML and varied SQLite layouts in every CI run, and distinguishes tested
|
|
369
|
+
synthetic shapes from unverified macOS/Contacts releases.
|
|
370
|
+
|
|
106
371
|
## Roadmap
|
|
107
372
|
|
|
108
373
|
See [`docs/TODO.md`](docs/TODO.md) for the full release schedule and feature checklist.
|
|
374
|
+
The [pre-1.0 API stability gate](docs/API_STABILITY.md) inventories supported
|
|
375
|
+
surfaces, evidence gaps, compatibility policy, and required release-readiness checks.
|
|
376
|
+
|
|
377
|
+
## Ruby Compatibility
|
|
378
|
+
|
|
379
|
+
`abbu` supports Ruby 3.3 and newer. CI exercises Ruby 3.3, 3.4, and 4.0;
|
|
380
|
+
Ruby 3.3 is the compatibility floor and designated lint/tooling job.
|
|
109
381
|
|
|
110
382
|
## Development
|
|
111
383
|
|
|
112
384
|
```bash
|
|
113
385
|
mise exec -- bundle install
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
386
|
+
bin/dev # Guard feedback loop
|
|
387
|
+
bin/spec # RSpec with the 100% coverage gate
|
|
388
|
+
bin/lint # RuboCop
|
|
389
|
+
bin/package # build and verify the gem in isolation
|
|
117
390
|
```
|
|
118
391
|
|
|
119
392
|
## Contributing
|