abbu 0.8.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +129 -2
  3. data/bin/abbu +158 -6
  4. data/bin/abbu-mcp +36 -0
  5. data/docs/ABBU.md +28 -0
  6. data/docs/ALTERNATE_CALENDAR_EVIDENCE.md +82 -0
  7. data/docs/CHANGELOG.md +98 -0
  8. data/docs/CONTRIBUTING.md +3 -0
  9. data/docs/FUZZY_MATCHING.md +72 -0
  10. data/docs/HISTORY_EVIDENCE.md +98 -0
  11. data/docs/ICALENDAR.md +101 -0
  12. data/docs/MACHINE_JSON.md +128 -0
  13. data/docs/MCP.md +148 -0
  14. data/docs/MERGE_PLANS.md +78 -0
  15. data/docs/MY_CARD_EVIDENCE.md +92 -0
  16. data/docs/PORTABLE_SQLITE.md +142 -0
  17. data/docs/RELEASING.md +76 -68
  18. data/docs/SNAPSHOT_DIFF.md +74 -0
  19. data/docs/SNAPSHOT_HISTORY.md +93 -0
  20. data/docs/STREAMING.md +73 -0
  21. data/docs/TODO.md +4 -4
  22. data/docs/WRITER_DECISION.md +90 -0
  23. data/lib/abbu/archive.rb +14 -0
  24. data/lib/abbu/exporters/csv_exporter.rb +7 -0
  25. data/lib/abbu/exporters/icalendar_encoding.rb +35 -0
  26. data/lib/abbu/exporters/icalendar_event.rb +61 -0
  27. data/lib/abbu/exporters/icalendar_exporter.rb +86 -0
  28. data/lib/abbu/exporters/json_exporter.rb +2 -2
  29. data/lib/abbu/exporters/jsonl_exporter.rb +23 -0
  30. data/lib/abbu/exporters/sqlite_exporter.rb +85 -0
  31. data/lib/abbu/exporters/sqlite_rows.rb +113 -0
  32. data/lib/abbu/exporters/sqlite_schema.rb +76 -0
  33. data/lib/abbu/exporters/vcard_exporter.rb +10 -11
  34. data/lib/abbu/exporters/vcard_photo.rb +50 -0
  35. data/lib/abbu/history_tracker.rb +96 -0
  36. data/lib/abbu/live_store.rb +11 -0
  37. data/lib/abbu/machine_command.rb +159 -0
  38. data/lib/abbu/machine_output.rb +52 -0
  39. data/lib/abbu/mcp/read_session.rb +57 -0
  40. data/lib/abbu/mcp/server.rb +100 -0
  41. data/lib/abbu/mcp.rb +11 -0
  42. data/lib/abbu/merge_plan.rb +132 -0
  43. data/lib/abbu/merge_preview.rb +35 -0
  44. data/lib/abbu/merge_values.rb +26 -0
  45. data/lib/abbu/parsers/plist_parser.rb +9 -0
  46. data/lib/abbu/parsers/sqlite_parser.rb +25 -0
  47. data/lib/abbu/snapshot_diff.rb +125 -0
  48. data/lib/abbu/snapshot_history.rb +66 -0
  49. data/lib/abbu/utils/fuzzy_matcher.rb +116 -0
  50. data/lib/abbu/version.rb +1 -1
  51. data/lib/abbu.rb +9 -0
  52. data/sig/fuzzy_matcher.rbs +22 -0
  53. data/sig/icalendar_exporter.rbs +13 -0
  54. data/sig/machine_output.rbs +23 -0
  55. data/sig/mcp.rbs +19 -0
  56. data/sig/merge_plan.rbs +16 -0
  57. data/sig/snapshot_diff.rbs +8 -0
  58. data/sig/snapshot_history.rbs +10 -0
  59. data/sig/sqlite_exporter.rbs +10 -0
  60. data/sig/streaming.rbs +27 -0
  61. data/sig/vcard_exporter.rbs +1 -1
  62. metadata +46 -2
@@ -0,0 +1,78 @@
1
+ <!-- docs/MERGE_PLANS.md -->
2
+
3
+ # Safe Merge Plans
4
+
5
+ `Abbu::MergePlan.new(left, right, policy: nil, source: nil)` creates a detached,
6
+ frozen review artifact. A plan does not assert that the records identify the
7
+ same person. `to_h` exposes policy, sources, both complete input snapshots,
8
+ per-field selected values and alternatives, conflicts, reasons and a
9
+ `materializable` flag. The caller remains responsible for identity decisions.
10
+
11
+ ## Explicit Policies
12
+
13
+ All policies union multivalues by exact Ruby equality, retaining left order then
14
+ unseen right values. Raw labels are never normalized or discarded. Same address
15
+ with different raw labels remains two entries. Arrays include emails, phones,
16
+ addresses, groups, URLs, notes, related names, social profiles, dates and IMs.
17
+
18
+ - No policy: preview only; conflicting scalars remain unresolved.
19
+ - `union_multivalues`: materialize only when scalars have no unresolved conflict.
20
+ - `prefer_newer`: prefer the input with the later observed `modified_at` Time.
21
+ Missing/non-Time/equal values do not choose a winner. This is caller policy,
22
+ not proof of human edit history or field-level freshness.
23
+ - `prefer_source`: `source:` must equal the file-level `relative_path` of exactly
24
+ one input; source directory/provider names are not identity shortcuts.
25
+ - `prefer_more_complete`: prefer the input with more populated scalar fields
26
+ and nonempty multivalue collections, one point per field; timestamps and images
27
+ are excluded. Ties do not choose a winner. This is a heuristic, not truth.
28
+
29
+ Nonconflicting scalar evidence fills missing values regardless of preferred
30
+ record. Empty strings are considered absent; whitespace-only strings are not
31
+ stripped. Conflicts keep both alternatives even when a policy selects a value.
32
+ Image URI and path are one atomic selection, never a cross-record combination.
33
+ No implicit field normalization, date conversion or image copying occurs.
34
+
35
+ `materialize` requires an explicit policy and rejects unresolved conflicts. It
36
+ returns a fresh mutable Contact, independent of the frozen plan and originals.
37
+ Its `source` is nil and `group_memberships` is empty: a derived record cannot
38
+ claim a single original file or merge source-local group keys. Original sources
39
+ and full group membership evidence remain in `to_h[:inputs]`; keep the plan with
40
+ the derived contact for provenance. Display group labels are unioned normally.
41
+ Existing contact exporter schemas remain unchanged.
42
+
43
+ Snapshots accept nil, booleans, symbols, numbers, strings, Time/Date, arrays and
44
+ hashes; unsupported caller objects raise rather than being frozen in place.
45
+ Plans are eager in-memory artifacts, not streaming stores.
46
+
47
+ ## CLI Preview And Privacy
48
+
49
+ `abbu Contacts.abbu --merge-preview` emits a JSON array of plans for the existing
50
+ exact identity engine's candidate pairs. Optional `--merge-policy POLICY` and
51
+ `--prefer-source PATH` choose preview preferences. `--strict` and live input
52
+ selection are supported; other operations, including `--json`, are rejected.
53
+ JSON is intrinsic to this preview. Empty previews exit 0; argument or strict
54
+ parse errors exit 2; existing live input errors retain exit 1. Diagnostics remain
55
+ on stderr. Policies validate even when there are no candidates. A source selector
56
+ that does not uniquely identify one member of a candidate pair fails explicitly.
57
+
58
+ Merge options cannot accompany machine operations (`--json`, `--matches`, or
59
+ `--diagnostics`), snapshot diff, or calendar exports/metadata. Machine-mode
60
+ conflicts produce the existing structured `invalid_input` error with status 2;
61
+ they never silently fall back to contact output. Operation conflicts and orphaned
62
+ policy/source options are rejected before reading inputs.
63
+
64
+ There is no CLI apply, file output or source mutation path. Exact matching is
65
+ currently all-pairs and results/plans are buffered: scope inputs in the Ruby API
66
+ for large datasets. A plan contains sensitive raw values, labels, image paths,
67
+ identifiers and source paths; it is not redacted telemetry. It should be stored
68
+ and transported with the same protection as the original address book.
69
+
70
+ This feature uses existing Contact evidence only and asserts no new Apple
71
+ storage semantics. Legacy parser and exporter contracts remain unchanged.
72
+
73
+ [README](../README.md) · [Contributing](CONTRIBUTING.md)
74
+
75
+ —
76
+ Stan Carver II
77
+ Made in Texas 🤠
78
+ https://stancarver.com
@@ -0,0 +1,92 @@
1
+ <!-- docs/MY_CARD_EVIDENCE.md -->
2
+
3
+ # My Card: Evidence Boundary
4
+
5
+ Research date: 2026-10-01. Issue: [#33](https://github.com/scarver2/abbu/issues/33).
6
+ Proposed decision, effective when merged: private-store evidence is insufficient
7
+ for ABBU to identify My Card. No detector, `me` method or `--me` flag is added.
8
+
9
+ ## What Is Documented
10
+
11
+ Apple's [My Card guide](https://support.apple.com/guide/contacts/set-up-your-my-card-adrb3525ca49/mac)
12
+ describes selecting a different card as My Card and sharing/export controls.
13
+ Designation is not equivalent to the first contact, the local account name,
14
+ an email domain, or the most complete record.
15
+
16
+ Apple's public [CNContactStore API](https://developer.apple.com/documentation/contacts/cncontactstore)
17
+ includes `unifiedMeContactWithKeys(toFetch:)`. This is a supported framework
18
+ surface, not documentation of an ABBU table, column, key, or archive mapping.
19
+ Its existence prevents the stronger, incorrect conclusion that macOS has no
20
+ supported way to retrieve My Card.
21
+
22
+ The [unified contact API](https://developer.apple.com/documentation/contacts/cncontactstore/unifiedcontact%28withidentifier%3Akeystofetch%3A%29)
23
+ also documents that unification can return a different identifier than requested.
24
+ Do not assume framework identifiers equal file-local SQLite record keys, or
25
+ that a unified live contact identifies one historical archive record.
26
+
27
+ These primary sources were inspected on the research date. No framework call,
28
+ permission prompt, real Contacts access, or import experiment was performed.
29
+
30
+ ## Repository Audit
31
+
32
+ The current parser models, fixtures and documentation contain no verified
33
+ My Card designation mapping. The existing [compatibility matrix](FORMAT_COMPATIBILITY.md)
34
+ tests synthetic read behavior without attributed macOS/Contacts builds. It
35
+ does not prove an archived designation is present, absent, unique or retained
36
+ across account changes. Group/source provenance is not identity designation.
37
+
38
+ Audit scope: `lib/`, `spec/` and `docs/`, including parser field mappings and
39
+ the synthetic fixture corpus at accepted main `77eaeda`. This is an audit of
40
+ this repository's evidence, not an exhaustive claim about every Apple schema.
41
+ Third-party findings and suggestive column names would be hypotheses pending
42
+ independent, sanitized reproduction.
43
+
44
+ ## Agent-Safe Outcome
45
+
46
+ Current archive and direct read-only live-store inputs are **unsupported for
47
+ My Card identification**, not “no My Card exists.” Consumers must not substitute
48
+ the first record, logged-in user's name, matching email, image, or source folder.
49
+ There is no new JSON shape or misleading always-nil API in this docs-only change.
50
+
51
+ A future implementation must distinguish these states in a reviewed public
52
+ contract before coding:
53
+
54
+ | State | Required meaning |
55
+ | --- | --- |
56
+ | Unsupported | The input/backend has no verified designation capability. |
57
+ | Unavailable | A capable backend could not retrieve designation, including permission errors; not proof of absence. |
58
+ | Absent | A verified capable backend explicitly reports no designation. |
59
+ | Found | Exactly one evidenced designation with provenance; never heuristic confidence. |
60
+ | Ambiguous | Multiple unresolved designations; preserve candidates without selecting a winner. |
61
+
62
+ State names here are design requirements, not shipped enum names. Structured
63
+ results must be deterministic and preserve ambiguity. Do not emit contact
64
+ names, paths, identifiers, or values in diagnostic logs by default. Returning
65
+ the designated contact is itself sensitive and requires explicit user intent.
66
+
67
+ ## Reopening Gate
68
+
69
+ For archive support, use disposable local accounts and invented contacts with
70
+ known designation changes. Record macOS and Contacts builds, export settings,
71
+ source membership, before/after evidence, and the verified relationship from
72
+ designation to record. Test zero/one/multiple/unsupported inputs, same-name and
73
+ same-email decoys, cross-source collisions, and legacy XML separately. Keep
74
+ unrelated fields constant so a correlated change is not assumed causal.
75
+
76
+ For a public-framework adapter, propose a separate opt-in macOS boundary with
77
+ permission handling and no mandatory native dependency for the portable gem.
78
+ Do not silently query the current user's Contacts while opening an unrelated
79
+ archive. Prove any identifier mapping independently; preserve framework
80
+ unification provenance rather than inventing one archive source.
81
+
82
+ Either path needs sanitized reproducible fixtures, exact-version scope, raw
83
+ evidence preservation, RBS/API/CLI contracts, regression coverage and review.
84
+ This issue permits an insufficient-evidence conclusion; no runtime feature or
85
+ version bump is claimed. Reopen with concrete evidence, not a guessed mapping.
86
+
87
+ Return to [README](../README.md), [roadmap](TODO.md), or [format evidence](ABBU.md).
88
+
89
+ —
90
+ Stan Carver II
91
+ Made in Texas 🤠
92
+ https://stancarver.com
@@ -0,0 +1,142 @@
1
+ <!-- docs/PORTABLE_SQLITE.md -->
2
+
3
+ # Portable Normalized SQLite
4
+
5
+ `Abbu::Exporters::SqliteExporter.new(contacts).to_file(path)` creates a separate
6
+ SQLite artifact from an `Enumerable<Abbu::Contact>`. It returns the canonical
7
+ destination path as a String. It has no stdout or source-writing API. The CLI is
8
+ `abbu Contacts.abbu --format sqlite --output contacts.sqlite`; explicit live-store
9
+ input is also supported, without adding snapshot guarantees.
10
+
11
+ This is an embedded/local export, not a server database. SQLite is deliberately
12
+ used for portability; no PostgreSQL service or Rails dependency is appropriate.
13
+ Schema definitions use fixed ABBU-owned SQL identifiers through the existing
14
+ sqlite3 library, and every contact value is a bound parameter. No user text is
15
+ interpolated into SQL identifiers or expressions. No new dependency is added.
16
+
17
+ ## Schema Version 1
18
+
19
+ `SqliteExporter::SCHEMA_VERSION`, `PRAGMA user_version`, and the single `metadata`
20
+ row all report 1. Metadata also records `generator_version` (the gem version),
21
+ not a wall-clock timestamp. A schema change needs explicit versioning; readers
22
+ must reject unknown versions rather than assume compatibility. There is no
23
+ in-place migration command; recreate derived artifacts from retained inputs.
24
+
25
+ | Table | Queryable facts and relationships |
26
+ | --- | --- |
27
+ | `metadata` | `schema_version`, `generator_version` |
28
+ | `contacts` | Sequential `id`, nullable `source_id`, all supported flat name/organization/phonetic/pronoun/tone/verification/image fields, `created_at`, `modified_at`, flat-field `evidence_json` |
29
+ | `sources` | File-level observed `path`, `relative_path`, `kind`, `identifier`, complete `evidence_json` |
30
+ | `emails` | `contact_id`, `position`, `address`, `label`, `raw_label`, `evidence_json` |
31
+ | `phones` | Same identity/order columns, `number`, labels, evidence |
32
+ | `addresses` | Same identity/order columns, `street`, `city`, `state`, `zip`, `country`, labels, evidence |
33
+ | `urls` | Same identity/order columns, `url`, labels, evidence |
34
+ | `related_names` | Same identity/order columns, `name`, labels, evidence |
35
+ | `social_profiles` | Same identity/order columns, `service`, `username`, evidence |
36
+ | `instant_messages` | Same identity/order columns, `address`, `service`, labels, evidence |
37
+ | `notes`, `group_labels` | `contact_id`, `position`, nullable display `value`, original `evidence_json`, including duplicates |
38
+ | `dates` | `contact_id`, `field`, `position`, `year`, `month`, `day`, labels, evidence |
39
+ | `groups` | Sequential `id`, nullable `source_id`, observed `record_id`, raw `name`, evidence |
40
+ | `group_memberships` | `contact_id`, `position`, `group_id`, retaining duplicate observations |
41
+
42
+ Collection primary keys preserve contact-local order. Foreign keys link children
43
+ to contacts, contacts/groups to source files, and memberships to groups. Indexes
44
+ support source and reverse group lookup. Foreign keys are enabled during export;
45
+ downstream writers must enable them on their own connections. Derived IDs are
46
+ 1-based insertion IDs, not Apple identities; collection positions are zero-based.
47
+
48
+ For example, ordinary SQL clients can query `contacts JOIN emails ON
49
+ contacts.id = emails.contact_id` without knowing any Apple table name. Contact
50
+ name components remain separate, rather than hiding queryable facts in JSON.
51
+ `evidence_json` duplicates each structured entry's complete public hash, including
52
+ unknown extra keys, nulls, raw labels and nested primitive data. It distinguishes
53
+ missing keys from explicit nulls where fixed relational columns cannot. Hash keys
54
+ are canonically sorted; array order is preserved. Source date/calendar evidence
55
+ is copied without interpreting it or fabricating missing years. Flat fields,
56
+ notes and group-label values also retain original JSON primitives in evidence:
57
+ their TEXT columns are query/display conveniences, not authoritative original
58
+ types. For example, a plist integer name/note remains an integer in evidence
59
+ even though its text column is a string. Pathname objects are represented by
60
+ their path strings; arbitrary Ruby object serialization is not promised.
61
+
62
+ `dates.field` distinguishes `birthday`, `anniversary`, `lunar_birthday`, and
63
+ `dates`. Direct fields and collection entries are intentionally both retained;
64
+ they are not deduplicated. Dates are stored as observed components, not validated
65
+ or converted into a guessed calendar. Timestamps use ISO 8601 with nine fractional
66
+ digits and their original offset; absent timestamps remain NULL. Image URI/path
67
+ references are text only: no image files are read or embedded.
68
+
69
+ ## Sources And Groups
70
+
71
+ Sources are file-provenance hashes observed on the supplied contacts, deduplicated
72
+ by complete canonical evidence. They are not inferred account/provider identities.
73
+ Empty sources and unreferenced groups cannot be discovered from the contact input
74
+ and are not fabricated. For those catalogs, retain the original archive or use
75
+ the public Source/Group listing APIs separately.
76
+
77
+ Groups deduplicate only identical membership evidence within the same complete
78
+ source descriptor. Repeated keys across sources remain separate. Conflicting names
79
+ or extra evidence yield separate observations, not silent reconciliation. With no
80
+ source descriptor, groups remain contact-local. `group_labels` independently keeps
81
+ the complete `Contact#groups` array; names are not treated as group identities.
82
+
83
+ ## Determinism And Round-Trip Evaluation
84
+
85
+ The exporter materializes the contact enumerable; it is not a constant-memory
86
+ streaming implementation. It iterates contacts and collections in supplied order,
87
+ and assigns IDs in first-occurrence order. Equivalent ordered input and generator
88
+ version produce identical relational contents; tests additionally establish byte
89
+ equality with the same SQLite library. Byte identity across SQLite versions is
90
+ not promised. Reordering input or changing generator version changes the artifact.
91
+
92
+ Regression tests reconstruct normalized collections and dates from the persisted
93
+ rows/evidence, compare flat fields and timestamps, and preserve duplicate/null
94
+ labels and membership observations. Both legacy XML and SQLite sources are tested.
95
+ This evaluates the normalized interchange boundary only: no public re-import API,
96
+ private Apple schema writer, Apple import certification, account identity mapping,
97
+ or byte-for-byte original archive restoration is provided. JSON primitive evidence
98
+ is preserved; arbitrary custom Ruby object graphs are outside the contact contract.
99
+
100
+ ## Publication And Source Safety
101
+
102
+ The destination parent must already exist. A temporary SQLite file is created
103
+ in that resolved directory with mode 0600. Data insertion uses a transaction;
104
+ the completed database is closed before an atomic same-filesystem hard link
105
+ publishes the final name. This operation fails if any destination already exists,
106
+ including a dangling symlink or a file created concurrently. Temporary files are
107
+ removed after success or failure. Empty input still creates a valid versioned
108
+ database with no contacts.
109
+
110
+ No destination is overwritten. Destinations beneath `.abbu`-suffixed directories
111
+ are rejected even for empty input; observed source-bundle aliases are also resolved
112
+ and rejected when the real directory has a different name. Callers must control the
113
+ destination directory and prevent concurrent ancestor replacement; this is not an
114
+ adversarial filesystem sandbox. Filesystems without hard-link support fail safely,
115
+ without an unsafe overwrite fallback. No cross-filesystem atomicity or power-loss
116
+ durability guarantee is claimed. Source parsing retains existing read-only/live
117
+ consistency limitations; exporting does not repair or checkpoint source databases.
118
+
119
+ ## Security And CLI Failures
120
+
121
+ This user-requested portable/queryable export is deliberately **plaintext**, an
122
+ exception to encrypted application database storage—not an encrypted database or
123
+ multi-tenant service. It contains confidential names, contact values, notes,
124
+ verification-code fields, and absolute provenance/image paths. Owner-only mode is
125
+ not encryption. Keep it on encrypted storage, protect backups separately, minimize
126
+ retention, and do not publish it in logs, issues, analytics, or test fixtures.
127
+ Use only synthetic data in tests. Exporting does not contact external services.
128
+
129
+ CLI success produces no stdout and exits 0; omission/parser diagnostics retain
130
+ their existing stderr behavior. `--format sqlite` requires an output file and
131
+ rejects conflicting operations. Filesystem, SQLite, and JSON generation failures
132
+ exit 2 with a class-only export error rather than contact payloads. Input/access
133
+ failures retain the existing CLI contract. The Ruby API raises the underlying
134
+ exception, which callers should treat as potentially sensitive. No release action
135
+ is authorized by a successful package/export check.
136
+
137
+ [README](../README.md) · [ABBU evidence](ABBU.md)
138
+
139
+ —
140
+ Stan Carver II
141
+ Made in Texas 🤠
142
+ https://stancarver.com
data/docs/RELEASING.md CHANGED
@@ -1,73 +1,81 @@
1
1
  <!-- docs/RELEASING.md -->
2
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.
3
+ # GitHub-Hosted Releases
4
+
5
+ [Back to README](../README.md) · [Contributing](CONTRIBUTING.md)
6
+
7
+ Release automation executes Sheriff authorization; green checks do not grant it.
8
+ Never dispatch a publication run or approve its environment without approval for
9
+ the exact version and commit. No local personal access token or RubyGems API key
10
+ is required for the browser-driven path.
11
+
12
+ ## One-Time Configuration
13
+
14
+ - RubyGems Trusted Publisher: owner `scarver2`, repository `abbu`, workflow
15
+ `release.yml`, environment `release`.
16
+ - GitHub `release` environment: required reviewer `scarver2`, administrator
17
+ bypass disabled, exact branch `main` and tags `v*` allowed. If the Sheriff
18
+ starts and approves the run, self-review prevention must remain unchecked.
19
+ - Permit the workflow's job-scoped permissions. Do not broaden the repository's
20
+ default token permissions or add a long-lived publishing secret.
21
+ - Any tag ruleset must permit the authorized Actions tag-creation job. If it
22
+ blocks creation, stop for owner review; do not bypass protection or add a PAT.
23
+
24
+ ## Release From The Browser
25
+
26
+ 1. Merge reviewed release changes and a dated changelog. Verify accepted CI.
27
+ 2. Open **Actions → Release gem → Run workflow**, selecting branch **main**.
28
+ 3. Enter the approved numeric version (without `v`) and its complete lowercase
29
+ 40-character commit SHA. An earlier accepted commit is allowed; a branch name
30
+ or abbreviated SHA is not. The workflow itself must be run from `main`.
31
+ 4. The run displays the version/SHA and verifies ancestry, version, dated
32
+ changelog, and any existing tag. Specs, lint and package checks run on the
33
+ exact candidate across Ruby 3.3, 3.4 and 4.0 before a tag can be created.
34
+ 5. Approve **Authorize and tag exact candidate** in the protected environment.
35
+ It creates only `vVERSION`, never moves a tag, and accepts a matching existing
36
+ tag for recovery. Check the run's version and SHA before approving.
37
+ 6. Approve **Publish approved gem** if GitHub requests a second environment
38
+ review. Separate jobs intentionally isolate Contents-write from OIDC rights.
39
+ Publishing checks the tag again, reruns specs/lint/package, and uses RubyGems
40
+ Trusted Publishing. The publishing job has only Contents-read and OIDC-write.
41
+ 7. Verify the published version on RubyGems and inspect its provenance and run
42
+ result. The action waits for registry propagation; the run summary records
43
+ the version and source commit. This workflow does not create a GitHub Release.
44
+
45
+ The retained external `v*` tag-push path runs the same gates when the tagged
46
+ commit contains this workflow. Older tagged commits use their historical
47
+ workflow; manual dispatch from current `main` can release an older accepted
48
+ candidate without changing that candidate. Newly created Actions tags do **not**
49
+ trigger another push workflow: this run proceeds directly to publishing.
50
+
51
+ ## Safe Checks And Recovery
52
+
53
+ `bin/release-check VERSION FULL_COMMIT_SHA` is read-only. Run in a clean checkout
54
+ after fetching `origin/main` and tags; it checks those local references and does
55
+ not fetch, create tags, or publish. The workflow fetches immediately before it.
56
+ This command checks source invariants, not tests or publication authority.
57
+
58
+ For a reproducible local package comparison, use
59
+ `SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD) bin/package` in the accepted
60
+ checkout. The workflow uses the candidate commit timestamp for verification and
61
+ the publishing action's build. Do not replay previously published tags, including
62
+ `v0.4.0`.
63
+
64
+ If a job fails, inspect its logs. An existing matching tag permits retry after
65
+ fixing infrastructure, but a conflicting tag is a hard stop. Never delete or
66
+ move a release tag. If RubyGems already has that version, verify its provenance
67
+ before doing anything else: rerunning publication is not an idempotent no-op.
68
+ There is no automatic yank, overwrite, version increment, or rollback.
69
+
70
+ The first workflow PR must be merged before the Run workflow button appears.
71
+ That merge does not authorize releasing its commit. Reconfirm the exact candidate
72
+ if it differs from the Sheriff's previously approved SHA. Workflow-only tooling
73
+ does not change the gem's public version.
74
+
75
+ ## References
76
+
77
+ - [GitHub token event behavior](https://docs.github.com/en/actions/concepts/security/github_token)
78
+ - [RubyGems Trusted Publishing](https://guides.rubygems.org/trusted-publishing/)
71
79
 
72
80
  —
73
81
  Stan Carver II
@@ -0,0 +1,74 @@
1
+ <!-- docs/SNAPSHOT_DIFF.md -->
2
+
3
+ # Snapshot Comparison
4
+
5
+ `Abbu::SnapshotDiff.new(before, after).to_h` compares two Archive/LiveStore
6
+ objects or ordered contact Enumerables. `abbu Before.abbu --diff After.abbu
7
+ --json` emits the same versioned JSON object. Omit `--json` for count summaries.
8
+ The first CLI input may use explicit read-only live input selection; the second
9
+ is an archive. `--strict` applies to both. The command exits 0 for a completed
10
+ comparison even when differences or ambiguities exist, and 2 for argument/parse
11
+ errors (live-store errors retain exit 1). Diagnostics remain on stderr.
12
+ JSON mode follows the [machine error contract](MACHINE_JSON.md): one structured
13
+ error document, with filesystem/access failures exiting 1. Only input selection,
14
+ `--strict`, and optional `--json` may accompany `--diff`; other operations,
15
+ export formats, output paths, and calendar metadata are rejected before reading
16
+ either input. Comparison-input diagnostics are summarized on stderr.
17
+
18
+ ## Identity Boundary
19
+
20
+ Comparison uses the existing exact/provenance-aware identity engine, not Apple
21
+ record keys or fuzzy matching. Every cross-snapshot pair is evaluated. Only
22
+ one-to-one candidates at the engine's probable threshold become comparisons.
23
+ Competing candidates and weaker qualifying evidence remain `ambiguous`; their
24
+ records are excluded from added/removed so ambiguity is not presented as loss.
25
+ No contact is merged or modified. A probable match is still an evidence-based
26
+ suggestion, not a declaration of identity truth.
27
+
28
+ No qualifying evidence means unmatched: an email/name/phone change that removes
29
+ all qualifying evidence may appear as removed plus added. A name alone does not
30
+ establish continuity. Source moves remain visible through provenance changes.
31
+ Identical nameless/no-signal records are not silently paired by array position.
32
+
33
+ ## JSON Contract (Schema Version 1)
34
+
35
+ Top-level keys: `schema_version`, `added`, `removed`, `changed`, `unchanged`,
36
+ `ambiguous`. Unmatched entries contain side-local `index` and a full `contact`
37
+ snapshot. Compared/ambiguous entries contain `before_index`, `after_index`,
38
+ `score`, exact-engine `evidence`, and full `before`/`after` snapshots. Compared
39
+ entries additionally contain `fields`: changed field names mapped to original
40
+ `before` and `after` values. Unchanged comparisons have empty `fields`.
41
+
42
+ Snapshots cover the explicit `SnapshotDiff::FIELDS` public contact attributes,
43
+ including raw labels, all multivalues, group membership, source, image paths and
44
+ timestamps. Nulls are retained. Array order is significant; reordering is a
45
+ reported change, not an inferred semantic equivalence. Paths become strings;
46
+ times use ISO 8601 with nine fractional digits. Image bytes are not compared:
47
+ matching paths do not prove identical photos. No unknown private columns are
48
+ reconstructed. The detached result is deeply frozen; source contacts remain
49
+ mutable and unchanged.
50
+
51
+ Ordering follows the supplied input order and cross-product traversal. Results
52
+ are deterministic for equivalent ordered inputs, not canonically sorted across
53
+ permutations. Indices are positions in that comparison, not persistent IDs.
54
+ No dates of edits, authors or events between snapshots are inferred.
55
+
56
+ ## Resource and Privacy Limits
57
+
58
+ Both inputs are materialized. The O(before × after) candidate pass is capped at
59
+ 100,000 pairs by default; API callers can explicitly set positive `max_pairs`.
60
+ Oversized comparisons fail before pair evaluation. This is not streaming or an
61
+ enterprise-scale benchmark claim. Live input is read through its existing
62
+ read-only API and consistency limitations, not an atomic historical snapshot.
63
+
64
+ JSON intentionally includes full contact values, raw identity evidence, paths,
65
+ source identifiers and verification codes already held by the supplied models.
66
+ Treat stdout/results as sensitive; do not send them to untrusted logs or agents.
67
+ Human summaries disclose counts only. Original source archives are never written.
68
+
69
+ Return to [README](../README.md), [format evidence](ABBU.md), or [API stability](API_STABILITY.md).
70
+
71
+ —
72
+ Stan Carver II
73
+ Made in Texas 🤠
74
+ https://stancarver.com
@@ -0,0 +1,93 @@
1
+ <!-- docs/SNAPSHOT_HISTORY.md -->
2
+
3
+ # Snapshot History
4
+
5
+ `Abbu::SnapshotHistory.new(paths, strict: false, retention: 2, limits: {}).each`
6
+ streams one transition per supplied archive path. Explicit list order is
7
+ authoritative. `from_directory(path)` selects only immediate directory children
8
+ whose names end in `.abbu`, sorted lexically. Directory names are ordering
9
+ labels, not verified capture times; choose sortable names or supply an ordered
10
+ list. Duplicate expanded paths are rejected; symlink aliases are not resolved.
11
+
12
+ History uses the public [SnapshotDiff](SNAPSHOT_DIFF.md) contract. Only unique
13
+ probable exact-evidence matches connect observations. Raw labels, source paths,
14
+ all modeled field changes and ambiguity are retained; fuzzy matching is not
15
+ identity. Each connected timeline records first/last observed snapshot references
16
+ and a numeric ID local to this analysis. Re-enumeration starts a fresh analysis.
17
+
18
+ ## Continuity And Gaps
19
+
20
+ - `first_seen`: first observation in this analysis or after previous evidence
21
+ was forgotten. It does **not** prove that the contact was newly created.
22
+ - `observed`: an adjacent unique probable comparison. `changes` may be empty.
23
+ - `absent`: previously observed contact has no qualifying candidate in this
24
+ snapshot. Reported once per absence, not proof of deletion from Apple Contacts.
25
+ - `reappeared`: unique probable match after an observed gap within retention.
26
+ This means observed again; it does not prove deletion/recreation between files.
27
+ - `ambiguous_start`: competing/weak evidence breaks prior continuity and starts
28
+ explicitly flagged new timelines. The ambiguous pairs identify prior timeline
29
+ IDs. Broken old timelines are not kept in the dormant pool for later reconnection.
30
+
31
+ `retention` is the nonnegative maximum number of absent snapshots to retain a
32
+ contact for possible reappearance matching. Default 2 permits up to two empty
33
+ observations between appearances; 0 discards an absent record immediately.
34
+ After expiry, a return receives a new ID and `first_seen`. The analysis cannot
35
+ distinguish a genuinely new record from forgotten identity evidence. A later
36
+ unambiguous link from an ambiguous-start timeline never repairs its earlier break.
37
+
38
+ Matching includes current and retained dormant candidates together. Competing
39
+ evidence cannot silently select the active or most recent one. This is an
40
+ observation stream, not a complete contact-index database; consumers can group
41
+ observations by ID to build a per-contact index. Do not coalesce IDs by names.
42
+
43
+ ## JSON Lines Contract
44
+
45
+ `abbu snapshots-directory --history [--strict]` emits one JSON object per line,
46
+ with `schema_version: 1`, `snapshot`, `observations`, `absent`, `ambiguous` and
47
+ `diagnostics`. `snapshot` has zero-based `index` and expanded `path`.
48
+ Observations expose `timeline_id`, `first_seen`, `last_seen`, `event`, full
49
+ `contact`, field `changes`, `compared_with` (previous observed snapshot or null)
50
+ and `match` (score/evidence or null). Absences carry timeline metadata and
51
+ `observed_at`. Ambiguous pairs retain SnapshotDiff before/after payloads plus
52
+ `before_timeline_id`; their indices refer to that transition's comparison pool,
53
+ not permanent contact identifiers. Observation order follows parser order.
54
+
55
+ Other CLI modes (including `--json` and live input) are rejected. No snapshots
56
+ emits no lines and exits 0. Argument/parse failures exit 2 through existing CLI
57
+ handling. Diagnostics are included in each transition, not hidden or printed as
58
+ non-JSON stdout. A later failure may leave earlier valid JSON lines: consumers
59
+ must check exit status and must not label a partial stream complete.
60
+
61
+ Combining history with machine selectors (`--json`, `--matches`, or
62
+ `--diagnostics`) returns the machine contract's structured `invalid_input` error
63
+ before opening inputs. History already emits JSON Lines; these selectors do not
64
+ convert it to a single JSON document. Export and other operation options are
65
+ also rejected before opening snapshots or creating output files.
66
+
67
+ ## Bounded Strategy And Privacy
68
+
69
+ Default `limits: { snapshots: 100, contacts: 10_000, pairs: 100_000 }` bounds path
70
+ count, current-plus-retained contact count and each comparison's cross-product.
71
+ Unknown keys, nonpositive limits and overflows raise explicitly, never truncate.
72
+ The snapshot count is checked before loading; contact/pool bounds are checked
73
+ after the parser has materialized a single archive. Thus this is not a bound on
74
+ individual archive parsing, contact field sizes or directory listing size.
75
+
76
+ The reader holds the current archive, bounded current/dormant records and one
77
+ detached transition, not all archives. Consuming `each` incrementally avoids
78
+ accumulating all results; calling `to_a` intentionally retains them. Large sets
79
+ can raise configured limits explicitly, accepting the resulting memory/work cost.
80
+ Synthetic tests cover deterministic order, expiry, reappearance, ambiguity and
81
+ bound failures; no real-world performance or historical completeness is claimed.
82
+
83
+ Output includes complete personal contact data, labels, source identifiers,
84
+ image paths, verification codes and diagnostic locations. Treat streams as
85
+ sensitive backups, not telemetry. Source archives remain read-only. No edit
86
+ authors, real event timestamps or events between snapshots are reconstructed.
87
+
88
+ [README](../README.md) · [Snapshot Diff](SNAPSHOT_DIFF.md)
89
+
90
+ —
91
+ Stan Carver II
92
+ Made in Texas 🤠
93
+ https://stancarver.com