abbu 0.4.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 +166 -5
- data/bin/abbu +54 -8
- data/docs/ABBU.md +146 -3
- data/docs/API_STABILITY.md +189 -0
- data/docs/CHANGELOG.md +53 -0
- data/docs/CONTRIBUTING.md +12 -0
- data/docs/FORMAT_COMPATIBILITY.md +97 -0
- data/docs/RELEASING.md +75 -0
- data/docs/TODO.md +3 -0
- data/lib/abbu/archive.rb +14 -0
- data/lib/abbu/contact.rb +3 -2
- 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 +30 -31
- data/lib/abbu/group.rb +28 -0
- data/lib/abbu/group_catalog.rb +32 -0
- data/lib/abbu/live_store.rb +13 -0
- data/lib/abbu/parsers/sqlite_parser.rb +6 -3
- data/lib/abbu/query.rb +19 -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/version.rb +1 -1
- data/lib/abbu.rb +1 -0
- 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
- metadata +16 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<!-- docs/API_STABILITY.md -->
|
|
2
|
+
|
|
3
|
+
# Pre-1.0 API Stability Gate
|
|
4
|
+
|
|
5
|
+
This is the acceptance checklist for [issue #47](https://github.com/scarver2/abbu/issues/47),
|
|
6
|
+
not a declaration that ABBU is stable or authorization to release 1.0. The baseline
|
|
7
|
+
inventory below describes the accepted 0.4.0 interfaces. Proposed features join
|
|
8
|
+
this inventory only after review and integration.
|
|
9
|
+
|
|
10
|
+
## Public surface inventory
|
|
11
|
+
|
|
12
|
+
The following are consumer-facing contracts even while the gem is pre-1.0:
|
|
13
|
+
|
|
14
|
+
| Surface | Contract to document and test before 1.0 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| Entry points | `Abbu.open(path, strict:)`, `Abbu.open_live(path = nil, strict:)`, `Abbu::VERSION` |
|
|
17
|
+
| `Archive` | Construction, `path`, `contacts`, `diagnostics`, `sqlite?`, `query`, `where`, `search`, `find_by_email`, `find_by_phone`, `schema_report`, `extract_images` |
|
|
18
|
+
| `LiveStore` | Construction, `path`, `contacts`, `diagnostics`, `database_paths`, `.default_path`; discovery, caching, read-only and permission boundaries |
|
|
19
|
+
| `Contact` | Mutable accessors listed below, collection defaults, nil behavior, `full_name`, `to_s`, `inspect`; sensitive-data implications of inspection |
|
|
20
|
+
| `Query` | Construction from contacts, Enumerable/`each`, `to_a`, `where`, `search`, exact email/phone lookup; ordering, defensive arrays, original contact identity |
|
|
21
|
+
| Exporters | `CsvExporter`, `JsonExporter`, `VcardExporter`: construction, `to_file`, `to_stdout`, output encoding and file behavior |
|
|
22
|
+
| Diagnostics | `Diagnostic` fields and `to_h`; `ParseError#diagnostic`; tolerant recovery versus strict failure |
|
|
23
|
+
| Identity suggestions | `Utils::Deduplicator#duplicates`, `#matches`/`#identity_matches`; `Match` readers, `sources`, `ambiguous?`, explicit `merge(policy:)`; `MergePolicyRequired` |
|
|
24
|
+
| Image extraction | `ImageExtractor#extract` and `Result#files`/`#diagnostics`, also exposed through Archive; no-overwrite behavior and privacy-sensitive metadata |
|
|
25
|
+
| Schema evidence | `SchemaInspector#report`, also exposed through Archive; deterministic reports, no inferred Apple semantics |
|
|
26
|
+
| CLI and tasks | `bin/abbu` flags, stdout/stderr/exit codes; `abbu:export`, `abbu:dedupe`, `abbu:stats` task arguments |
|
|
27
|
+
|
|
28
|
+
Contact accessor inventory:
|
|
29
|
+
|
|
30
|
+
- Names: `first_name`, `middle_name`, `last_name`, `nickname`, `prefix`, `suffix`,
|
|
31
|
+
`maiden_name`, `phonetic_first_name`, `phonetic_middle_name`, `phonetic_last_name`.
|
|
32
|
+
- Organization: `company`, `job_title`, `department`, `phonetic_company`.
|
|
33
|
+
- Collections: `emails`, `phones`, `addresses`, `groups`, `urls`, `notes`,
|
|
34
|
+
`related_names`, `social_profiles`, `dates`, `instant_messages`.
|
|
35
|
+
- Other observed fields: `pronouns`, `ringtone`, `texttone`, `birthday`,
|
|
36
|
+
`anniversary`, `verification_code`, `lunar_birthday`, `image_uri`, `image_path`,
|
|
37
|
+
`created_at`, `modified_at`, `source`.
|
|
38
|
+
|
|
39
|
+
Record the keys, value types, raw evidence, optionality, ordering, and source/version
|
|
40
|
+
limits for every collection and hash. A Ruby accessor does not guarantee that every
|
|
41
|
+
parser or exporter supports the field. Contact fields and exported JSON are not
|
|
42
|
+
identical schemas: the current JSON exporter omits nil top-level values, retains
|
|
43
|
+
empty collections, serializes timestamps as ISO 8601, and adds the display `name`.
|
|
44
|
+
Do not generate a JSON schema simply by enumerating Contact accessors.
|
|
45
|
+
|
|
46
|
+
### Source extension (0.6.0 development)
|
|
47
|
+
|
|
48
|
+
Add `Archive#sources`, `LiveStore#sources`, and `Source` construction/readers,
|
|
49
|
+
`provider`, and `to_h` to the public inventory. Source JSON and `--sources` are
|
|
50
|
+
public contracts; see the [source API](../README.md#sources). `SourceCatalog`
|
|
51
|
+
is an internal adapter, not an extension point. File/container provenance must
|
|
52
|
+
remain distinct, and provider inference is unsupported. Contacts remain mutable
|
|
53
|
+
even though source metadata and membership snapshots are frozen.
|
|
54
|
+
|
|
55
|
+
## Internal and experimental boundaries
|
|
56
|
+
|
|
57
|
+
The 0.8.0 development vCard fidelity extension retains the exporter signatures
|
|
58
|
+
and adds their RBS contract. Wire output now uses CRLF, escapes/folding and grouped
|
|
59
|
+
raw labels; custom TYPE injection and fabricated preferences are removed. See
|
|
60
|
+
the [export migration](../README.md#export) and [evidence audit](ABBU.md#vcard-serialization-evidence).
|
|
61
|
+
`VcardDocument` and `VcardEncoding` are internal serialization helpers, not
|
|
62
|
+
public extension points. No claim of a complete Apple import round-trip is made.
|
|
63
|
+
|
|
64
|
+
The 0.7.0 development extension adds `Group` construction/readers, `include?`,
|
|
65
|
+
`to_h`, `Contact#group_memberships`, `Source#groups`, input `groups`, source/input
|
|
66
|
+
`groups_for`, `Query#in_group`, and `--groups`. See the [group API](../README.md#groups)
|
|
67
|
+
for snapshot identity, JSON schema, ordering, and unsupported group boundaries.
|
|
68
|
+
`GroupCatalog` is internal. Existing `Contact#groups` and contact exporter schemas
|
|
69
|
+
are unchanged; raw membership keys are available in the model and group listings.
|
|
70
|
+
|
|
71
|
+
Parser SQL/plist mappings, image-resolution heuristics, identity weights,
|
|
72
|
+
schema-inspection constants, private helpers, and `Utils::ContactIdentity` are
|
|
73
|
+
implementation details, not supported extension points. Prefer the entry points
|
|
74
|
+
and public results above over calling `Parsers::*` directly. Being a reachable
|
|
75
|
+
Ruby constant is not sufficient evidence of a supported extension contract.
|
|
76
|
+
Audit previously documented direct usage before hiding or removing any such API;
|
|
77
|
+
this classification does not authorize an immediate compatibility break.
|
|
78
|
+
|
|
79
|
+
Pre-1.0 does not mean disposable: documented behavior remains review-sensitive.
|
|
80
|
+
Live consistency, identity scoring/thresholds, private Apple schema coverage,
|
|
81
|
+
alternate-calendar interpretation, and Apple-specific vCard conventions remain
|
|
82
|
+
evidence-limited. Match scores are suggestions, not probabilities or proof of
|
|
83
|
+
identity. Raw labels/provenance must survive normalization and interchange.
|
|
84
|
+
|
|
85
|
+
## CLI and machine-output compatibility
|
|
86
|
+
|
|
87
|
+
The 0.4.0 CLI inventory is `--format` (`csv`, `json`, `vcard`), `--output`,
|
|
88
|
+
`--extract-images`, `--stats`, `--dedupe`, `--search`, `--email`, `--phone`,
|
|
89
|
+
`--json`, `--schema`, `--strict`, `--live`, `--live-path`, `--version`, and `--help`,
|
|
90
|
+
including documented short forms. Search returns status 0 for matches and 1 for
|
|
91
|
+
no matches; JSON no-match output is `[]`. Strict parser failures return 2.
|
|
92
|
+
Live input failures return 1. Other usage/error combinations need a complete
|
|
93
|
+
matrix before 1.0; do not claim that every existing path already follows one
|
|
94
|
+
unified exit-code scheme.
|
|
95
|
+
|
|
96
|
+
Treat JSON keys, types, omission/null behavior, ordering promises, diagnostic codes,
|
|
97
|
+
CLI flags, and exit statuses as public API. Renames/removals/type changes need
|
|
98
|
+
explicit compatibility review and migration guidance. Machine-mode stdout must
|
|
99
|
+
contain only the requested payload; warnings and errors belong on stderr. Clients
|
|
100
|
+
must not parse human presentation text. [Issue #39](https://github.com/scarver2/abbu/issues/39)
|
|
101
|
+
tracks remaining structured output and schema work. Freeze and version the schema
|
|
102
|
+
contract before 1.0; an unimplemented schema version must not be advertised today.
|
|
103
|
+
|
|
104
|
+
CSV headers/order and vCard version, CRLF, escaping, folding, Unicode, repeated
|
|
105
|
+
properties, raw labels, and photo behavior also need golden/round-trip coverage.
|
|
106
|
+
Portable SQLite and iCalendar are proposed formats, not current guarantees.
|
|
107
|
+
|
|
108
|
+
## Deprecation and versioning policy
|
|
109
|
+
|
|
110
|
+
For an intentional public change, document the old behavior, replacement, affected
|
|
111
|
+
consumers, migration example, and planned removal version before removal. Keep a
|
|
112
|
+
replacement available for at least one published minor release during pre-1.0;
|
|
113
|
+
once 1.x is stable, incompatible removals wait for a major release. Security or
|
|
114
|
+
data-loss emergencies require an explicitly approved, documented exception.
|
|
115
|
+
Runtime warnings, when appropriate, must be controllable and never pollute JSON
|
|
116
|
+
stdout or disclose contact values. No warning machinery is claimed to exist yet.
|
|
117
|
+
|
|
118
|
+
Follow Stan's release grouping rule: completed features receive minor bumps and
|
|
119
|
+
bug-fix-only releases receive patch bumps. A patch must not silently remove a
|
|
120
|
+
documented contract. Review incompatible pre-1.0 changes explicitly rather than
|
|
121
|
+
hiding them under a bug-fix label. Version bumps, green CI, and completed checklists
|
|
122
|
+
do not authorize tagging, merging, or publication; see [releasing](RELEASING.md).
|
|
123
|
+
|
|
124
|
+
## Required evidence before 1.0
|
|
125
|
+
|
|
126
|
+
- [ ] Audit and approve this inventory against the exact candidate commit, including
|
|
127
|
+
new features integrated since 0.4.0; identify supported constructors and errors.
|
|
128
|
+
- [ ] Publish YARD/API documentation for every supported public method and result:
|
|
129
|
+
arguments, returns, errors, mutation/caching, I/O, privacy, and runnable examples.
|
|
130
|
+
- [ ] Keep public RBS contracts synchronized with implementation and validate them;
|
|
131
|
+
maintain tests for both documented success and failure behavior.
|
|
132
|
+
- [ ] Freeze CLI/JSON schemas and exit-code matrix with empty/error/Unicode fixtures;
|
|
133
|
+
document migration and deprecation policy in release notes.
|
|
134
|
+
- [ ] Verify parser → model → interchange preservation of raw labels, timestamps,
|
|
135
|
+
source evidence and images, including malformed and unsupported cases.
|
|
136
|
+
- [ ] Run supported Ruby CI (currently 3.3, 3.4, 4.0), 100% full-suite line coverage,
|
|
137
|
+
lint, package inspection and isolated install against the candidate.
|
|
138
|
+
- [ ] Publish an evidence matrix: Ruby/SQLite versions, OS, Contacts build, storage
|
|
139
|
+
layout, archive/live mode, tested fields, fixture provenance and known gaps.
|
|
140
|
+
Synthetic fixtures with unknown Apple versions are not macOS certification.
|
|
141
|
+
- [ ] Complete the benchmark below and explicitly disposition performance gaps.
|
|
142
|
+
- [ ] Review sensitive-data exposure in exports, diagnostics, exception messages,
|
|
143
|
+
`inspect`, absolute paths, image extraction, permissions, and future agent tools.
|
|
144
|
+
Do not log real contacts, photos, verification codes, or provider identifiers in CI.
|
|
145
|
+
- [ ] Disposition each remaining backlog item as required, deferred, or unsupported
|
|
146
|
+
with rationale; do not substitute feature count for a stability decision.
|
|
147
|
+
- [ ] Obtain Deputy exact-head review and separate Sheriff authorization for any
|
|
148
|
+
1.0 tag/publication. This checklist itself does not authorize either.
|
|
149
|
+
|
|
150
|
+
## 10k+ performance acceptance
|
|
151
|
+
|
|
152
|
+
Before 1.0, provide a deterministic, non-personal 10,000-contact benchmark with
|
|
153
|
+
multiple sources, multivalues, Unicode, missing optional data, and a repeatable seed.
|
|
154
|
+
Measure archive and live-style synthetic parsing, query, CSV/JSON/vCard export,
|
|
155
|
+
and identity suggestions separately. Include a 100,000-contact scaling run for
|
|
156
|
+
streaming work in [#40](https://github.com/scarver2/abbu/issues/40).
|
|
157
|
+
|
|
158
|
+
Record exact commit, Ruby/SQLite versions, hardware/OS, fixture recipe, command,
|
|
159
|
+
wall time, peak RSS, output count/checksum, warmup, and at least three measured
|
|
160
|
+
runs. Report cold versus cached reads separately. Proposed gate: no unexplained
|
|
161
|
+
greater-than-20% median time or peak-RSS regression against the recorded baseline
|
|
162
|
+
on the same runner; any exception requires reviewer rationale. Establish absolute
|
|
163
|
+
budgets from measured evidence before approving 1.0, not invented timings here.
|
|
164
|
+
Streaming must demonstrate bounded contact-memory overhead at both scales;
|
|
165
|
+
materializing `contacts` is not a streaming benchmark. The current all-pairs
|
|
166
|
+
identity matcher needs its own scaling assessment, not a linear-memory promise.
|
|
167
|
+
|
|
168
|
+
## Linked gaps and adoption
|
|
169
|
+
|
|
170
|
+
See the [complete backlog](TODO.md#pre-10-backlog--not-included-in-040).
|
|
171
|
+
Important dependencies include WAL evidence [#28](https://github.com/scarver2/abbu/issues/28),
|
|
172
|
+
vCard fidelity/photos [#29](https://github.com/scarver2/abbu/issues/29) /
|
|
173
|
+
[#30](https://github.com/scarver2/abbu/issues/30), sources/groups
|
|
174
|
+
[#31](https://github.com/scarver2/abbu/issues/31) / [#32](https://github.com/scarver2/abbu/issues/32),
|
|
175
|
+
My Card/history/calendar research [#33](https://github.com/scarver2/abbu/issues/33) /
|
|
176
|
+
[#35](https://github.com/scarver2/abbu/issues/35) / [#36](https://github.com/scarver2/abbu/issues/36),
|
|
177
|
+
machine output [#39](https://github.com/scarver2/abbu/issues/39), streaming
|
|
178
|
+
[#40](https://github.com/scarver2/abbu/issues/40), and safe-writer research
|
|
179
|
+
[#46](https://github.com/scarver2/abbu/issues/46). Deferral may be acceptable if the
|
|
180
|
+
released support boundary is explicit. Never infer unsupported Apple semantics
|
|
181
|
+
to check off a gate.
|
|
182
|
+
|
|
183
|
+
Return to the [README](../README.md), [format evidence](ABBU.md), or
|
|
184
|
+
[contribution workflow](CONTRIBUTING.md).
|
|
185
|
+
|
|
186
|
+
—
|
|
187
|
+
Stan Carver II
|
|
188
|
+
Made in Texas 🤠
|
|
189
|
+
https://stancarver.com
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,59 @@ 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
|
+
|
|
14
67
|
## [0.4.0] - 2026-09-30
|
|
15
68
|
|
|
16
69
|
### Fixed
|
data/docs/CONTRIBUTING.md
CHANGED
|
@@ -26,6 +26,12 @@ This runs RSpec and RuboCop automatically on file changes.
|
|
|
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
|
|
@@ -43,6 +49,12 @@ This builds the current gem, verifies its metadata, installs it into an isolated
|
|
|
43
49
|
gem home, and loads that installed copy. A successful package check is evidence
|
|
44
50
|
only; it does not authorize a release tag or RubyGems publication.
|
|
45
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
|
+
|
|
46
58
|
## Pull Request Guidelines
|
|
47
59
|
|
|
48
60
|
- Base branch: `main`
|
|
@@ -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
|
@@ -166,6 +166,9 @@ Contacts databases is not part of the roadmap.
|
|
|
166
166
|
|
|
167
167
|
## v1.0.0 — Future Stable API
|
|
168
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
|
+
|
|
169
172
|
- [ ] Complete the API-stability checklist and acceptance evidence before promising stability
|
|
170
173
|
- [ ] Comprehensive public API documentation
|
|
171
174
|
- [ ] Benchmarks for large archives (10k+ contacts)
|
data/lib/abbu/archive.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative 'parsers/plist_parser'
|
|
|
9
9
|
require_relative 'parsers/sqlite_parser'
|
|
10
10
|
require_relative 'query'
|
|
11
11
|
require_relative 'schema_inspector'
|
|
12
|
+
require_relative 'source_catalog'
|
|
12
13
|
require_relative 'utils/image_resolver'
|
|
13
14
|
|
|
14
15
|
module Abbu
|
|
@@ -58,6 +59,19 @@ module Abbu
|
|
|
58
59
|
SchemaInspector.new(db_paths, root_path: @path).report
|
|
59
60
|
end
|
|
60
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
|
+
|
|
61
75
|
private
|
|
62
76
|
|
|
63
77
|
def validate!
|
data/lib/abbu/contact.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
module Abbu
|
|
5
5
|
class Contact
|
|
6
6
|
attr_accessor :first_name, :middle_name, :last_name, :emails,
|
|
7
|
-
:phones, :company, :addresses, :groups, :nickname,
|
|
7
|
+
:phones, :company, :addresses, :groups, :group_memberships, :nickname,
|
|
8
8
|
:prefix, :suffix, :job_title, :department, :maiden_name,
|
|
9
9
|
:phonetic_first_name, :phonetic_middle_name, :phonetic_last_name,
|
|
10
10
|
:phonetic_company, :pronouns, :ringtone, :texttone,
|
|
@@ -14,11 +14,12 @@ module Abbu
|
|
|
14
14
|
:image_uri, :image_path,
|
|
15
15
|
:created_at, :modified_at, :source
|
|
16
16
|
|
|
17
|
-
def initialize
|
|
17
|
+
def initialize # rubocop:disable Metrics/MethodLength
|
|
18
18
|
@emails = []
|
|
19
19
|
@phones = []
|
|
20
20
|
@addresses = []
|
|
21
21
|
@groups = []
|
|
22
|
+
@group_memberships = []
|
|
22
23
|
@urls = []
|
|
23
24
|
@notes = []
|
|
24
25
|
@related_names = []
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# lib/abbu/exporters/vcard_document.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'vcard_encoding'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
module Exporters
|
|
8
|
+
# Per-card grouping avoids ambiguous associations between repeated labels.
|
|
9
|
+
class VcardDocument
|
|
10
|
+
TYPES = {
|
|
11
|
+
'EMAIL' => %w[INTERNET X400 PREF],
|
|
12
|
+
'TEL' => %w[HOME WORK PREF VOICE FAX MSG CELL PAGER BBS MODEM CAR ISDN VIDEO PCS],
|
|
13
|
+
'ADR' => %w[DOM INTL POSTAL PARCEL HOME WORK PREF],
|
|
14
|
+
'IMPP' => %w[PERSONAL BUSINESS HOME WORK MOBILE PREF]
|
|
15
|
+
}.transform_values(&:freeze).freeze
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@lines = ['BEGIN:VCARD', 'VERSION:3.0']
|
|
19
|
+
@group_number = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def <<(line)
|
|
23
|
+
@lines << line
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def labeled(property, value, entry, default_type: nil)
|
|
27
|
+
type = standard_type(property, entry[:label]) || default_type
|
|
28
|
+
header = type ? "#{property};TYPE=#{type}" : property
|
|
29
|
+
label = entry[:raw_label] || entry[:label]
|
|
30
|
+
return self << "#{header}:#{value}" if label.nil?
|
|
31
|
+
|
|
32
|
+
@group_number += 1
|
|
33
|
+
self << "item#{@group_number}.#{header}:#{value}"
|
|
34
|
+
self << "item#{@group_number}.X-ABLABEL:#{VcardEncoding.text(label)}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
lines = (@lines + ['END:VCARD']).map { |line| VcardEncoding.fold(line) }
|
|
39
|
+
"#{lines.join("\r\n")}\r\n"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def standard_type(property, label)
|
|
45
|
+
return unless label.to_s.match?(/\A[A-Za-z0-9-]+\z/)
|
|
46
|
+
|
|
47
|
+
type = label.upcase
|
|
48
|
+
return 'INTERNET,PREF' if property == 'EMAIL' && type == 'PREF'
|
|
49
|
+
|
|
50
|
+
type if TYPES.fetch(property, []).include?(type)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# lib/abbu/exporters/vcard_encoding.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
module Exporters
|
|
8
|
+
# Internal wire encoding; URI values and TEXT values have different grammars.
|
|
9
|
+
module VcardEncoding
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def text(value)
|
|
13
|
+
utf8(value).gsub(/\\|;|,|\r\n|\r|\n/) do |character|
|
|
14
|
+
character.match?(/[\r\n]/) ? '\\n' : "\\#{character}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def structured(values)
|
|
19
|
+
values.map { |value| text(value) }.join(';')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def uri(value)
|
|
23
|
+
URI::DEFAULT_PARSER.escape(utf8(value), %r{[^A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]})
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def token(value)
|
|
27
|
+
string = utf8(value)
|
|
28
|
+
raise ArgumentError, 'vCard parameter must be an ASCII token' unless string.match?(/\A[A-Za-z0-9-]+\z/)
|
|
29
|
+
|
|
30
|
+
string
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fold(line)
|
|
34
|
+
parts = [+'']
|
|
35
|
+
utf8(line).each_char do |character|
|
|
36
|
+
parts << +' ' if parts.last.bytesize + character.bytesize > 75
|
|
37
|
+
parts.last << character
|
|
38
|
+
end
|
|
39
|
+
parts.join("\r\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def utf8(value)
|
|
43
|
+
string = value.to_s.encode(Encoding::UTF_8)
|
|
44
|
+
raise ArgumentError, 'vCard text has invalid encoding' unless string.valid_encoding?
|
|
45
|
+
if string.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/)
|
|
46
|
+
raise ArgumentError,
|
|
47
|
+
'vCard text contains an unsupported control character'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
string
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|