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.
- checksums.yaml +4 -4
- data/README.md +129 -2
- data/bin/abbu +158 -6
- data/bin/abbu-mcp +36 -0
- data/docs/ABBU.md +28 -0
- data/docs/ALTERNATE_CALENDAR_EVIDENCE.md +82 -0
- data/docs/CHANGELOG.md +98 -0
- data/docs/CONTRIBUTING.md +3 -0
- data/docs/FUZZY_MATCHING.md +72 -0
- data/docs/HISTORY_EVIDENCE.md +98 -0
- data/docs/ICALENDAR.md +101 -0
- data/docs/MACHINE_JSON.md +128 -0
- data/docs/MCP.md +148 -0
- data/docs/MERGE_PLANS.md +78 -0
- data/docs/MY_CARD_EVIDENCE.md +92 -0
- data/docs/PORTABLE_SQLITE.md +142 -0
- data/docs/RELEASING.md +76 -68
- data/docs/SNAPSHOT_DIFF.md +74 -0
- data/docs/SNAPSHOT_HISTORY.md +93 -0
- data/docs/STREAMING.md +73 -0
- data/docs/TODO.md +4 -4
- data/docs/WRITER_DECISION.md +90 -0
- data/lib/abbu/archive.rb +14 -0
- data/lib/abbu/exporters/csv_exporter.rb +7 -0
- data/lib/abbu/exporters/icalendar_encoding.rb +35 -0
- data/lib/abbu/exporters/icalendar_event.rb +61 -0
- data/lib/abbu/exporters/icalendar_exporter.rb +86 -0
- data/lib/abbu/exporters/json_exporter.rb +2 -2
- data/lib/abbu/exporters/jsonl_exporter.rb +23 -0
- data/lib/abbu/exporters/sqlite_exporter.rb +85 -0
- data/lib/abbu/exporters/sqlite_rows.rb +113 -0
- data/lib/abbu/exporters/sqlite_schema.rb +76 -0
- data/lib/abbu/exporters/vcard_exporter.rb +10 -11
- data/lib/abbu/exporters/vcard_photo.rb +50 -0
- data/lib/abbu/history_tracker.rb +96 -0
- data/lib/abbu/live_store.rb +11 -0
- data/lib/abbu/machine_command.rb +159 -0
- data/lib/abbu/machine_output.rb +52 -0
- data/lib/abbu/mcp/read_session.rb +57 -0
- data/lib/abbu/mcp/server.rb +100 -0
- data/lib/abbu/mcp.rb +11 -0
- data/lib/abbu/merge_plan.rb +132 -0
- data/lib/abbu/merge_preview.rb +35 -0
- data/lib/abbu/merge_values.rb +26 -0
- data/lib/abbu/parsers/plist_parser.rb +9 -0
- data/lib/abbu/parsers/sqlite_parser.rb +25 -0
- data/lib/abbu/snapshot_diff.rb +125 -0
- data/lib/abbu/snapshot_history.rb +66 -0
- data/lib/abbu/utils/fuzzy_matcher.rb +116 -0
- data/lib/abbu/version.rb +1 -1
- data/lib/abbu.rb +9 -0
- data/sig/fuzzy_matcher.rbs +22 -0
- data/sig/icalendar_exporter.rbs +13 -0
- data/sig/machine_output.rbs +23 -0
- data/sig/mcp.rbs +19 -0
- data/sig/merge_plan.rbs +16 -0
- data/sig/snapshot_diff.rbs +8 -0
- data/sig/snapshot_history.rbs +10 -0
- data/sig/sqlite_exporter.rbs +10 -0
- data/sig/streaming.rbs +27 -0
- data/sig/vcard_exporter.rbs +1 -1
- metadata +46 -2
data/docs/MERGE_PLANS.md
ADDED
|
@@ -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
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
- workflow
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|