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.
@@ -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