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
data/docs/CHANGELOG.md CHANGED
@@ -11,6 +11,104 @@ Versioning follows [Semantic Versioning](https://semver.org/).
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ### Release Tooling
15
+
16
+ - Browser-initiated GitHub releases accept an exact approved version and commit,
17
+ verify the supported Ruby matrix, and create immutable tags with the built-in
18
+ GitHub token before OIDC publication. Protected Sheriff approvals remain required.
19
+
20
+ ## [0.18.0] - 2026-10-03
21
+
22
+ This release includes the previously unreleased 0.12.0–0.17.0 development milestones.
23
+
24
+ ### Added — optional MCP adapter
25
+
26
+ - Optional `abbu-mcp` stdio executable and `require 'abbu/mcp'` adapter, using
27
+ the separately installed official MCP Ruby SDK. Core `abbu` has no MCP runtime
28
+ dependency and never loads the SDK automatically.
29
+ - Five read-only tools for bounded contact queries, statistics, sources/groups
30
+ and explicit diagnostics, bound to one operator-selected archive or live path.
31
+ No arbitrary path tool argument, file export, Contacts mutation, default
32
+ contact logging, or unevidenced My Card detection.
33
+
34
+ ### Added — snapshot history
35
+
36
+ - Streaming snapshot-history transitions with bounded absent-contact retention,
37
+ per-analysis timelines, explicit ambiguity breaks and observed reappearances.
38
+ Ruby ordered-list API and directory JSON Lines CLI do not infer edit events.
39
+
40
+ ### Added — streaming
41
+
42
+ - `Archive#each_contact` and `LiveStore#each_contact` enumerate contacts without
43
+ populating the cached array; SQLite readers close on block termination/errors.
44
+ - Incremental CSV/vCard `write_to(io)`, JSONL export, and opt-in CLI `--stream`.
45
+ Late failures can leave partial output. Existing buffered exports are unchanged.
46
+ - Reproducible synthetic 10,000-contact benchmark and documented memory bounds.
47
+
48
+ ### Added — portable SQLite
49
+
50
+ - Portable normalized SQLite exporter and `--format sqlite --output FILE`, with
51
+ explicit schema version, relational contact/source/group/multivalue/date data,
52
+ raw-label and primitive-value evidence, transactional staging, and atomic no-overwrite publication.
53
+ Existing exports and source stores remain unchanged; this is not an Apple writer.
54
+
55
+ ### Added — merge planning
56
+
57
+ - Immutable merge-plan previews with explicit union, timestamp, source and
58
+ completeness policies. Conflicts and original provenance remain reviewable;
59
+ materialization requires an explicit policy and never writes source stores.
60
+ `--merge-preview` provides a JSON-only preview, never an apply operation.
61
+ - Reject merge options in machine JSON, snapshot-diff, and calendar combinations
62
+ before input reads; machine-mode conflicts retain structured JSON errors.
63
+
64
+ ### Added — snapshot comparison
65
+
66
+ - Identity-evidence snapshot comparison through `SnapshotDiff` and `--diff`,
67
+ with raw field changes, provenance, explicit ambiguity and stable JSON output.
68
+ - Snapshot JSON uses the machine dispatcher and structured error contract;
69
+ conflicting machine operations and calendar metadata fail before input reads.
70
+
71
+ ### Added — iCalendar
72
+
73
+ - Deterministic iCalendar birthday/anniversary reminders through a Ruby exporter
74
+ and `--format icalendar`. Explicit recurrence year, revision time, and calendar
75
+ namespace avoid fabricated metadata. Unknown years remain unknown; invalid and
76
+ alternate-calendar dates are diagnosed and omitted. Existing exports are unchanged.
77
+ - Machine JSON rejects calendar metadata before opening inputs, rather than
78
+ silently ignoring export-only options.
79
+
80
+ ## [0.11.0] - 2026-10-02
81
+
82
+ ### Added — machine JSON
83
+
84
+ - First-class `--json` for contacts, statistics, schema, sources/groups, duplicate
85
+ groups, identity matches, and image extraction. New `--diagnostics` and
86
+ `--matches` are JSON-only operations. Public `JsonExporter#payload` and
87
+ machine serializers reuse the unchanged contact JSON representation.
88
+ - Documented machine stdout, stderr, exit codes, privacy, and SemVer contract.
89
+
90
+ ### Changed — machine JSON
91
+
92
+ - JSON-mode errors now emit a single `{ "error": { "code", "message" } }`
93
+ document instead of empty stdout or a Ruby exception trace. Invalid inputs
94
+ and conflicting options exit 2; unavailable live inputs and filesystem errors
95
+ exit 1. JSON callers must check exit status before treating stdout as data.
96
+ - Bare `--json` lists contacts. JSON combinations are validated before reading
97
+ or writing, rather than silently prioritizing one operation. Human modes and
98
+ successful existing contact/source/group/schema JSON shapes are unchanged.
99
+
100
+ ### Added — fuzzy matching
101
+
102
+ - Opt-in bounded, explainable fuzzy name suggestions using Unicode-preserving
103
+ Levenshtein distance. Exact identity evidence retains precedence; fuzzy-only
104
+ suggestions remain ambiguous and never merge contacts automatically.
105
+
106
+ ### Added — embedded photos
107
+
108
+ - Opt-in embedded JPEG/PNG/GIF vCard photos via `photo_mode: :embedded` and
109
+ `--photo-mode embedded`. URI mode remains the default. Missing, unreadable,
110
+ or unsupported image evidence fails before output; HEIC embedding is deferred.
111
+
14
112
  ## [0.8.1] - 2026-10-01
15
113
 
16
114
  ### Fixed
data/docs/CONTRIBUTING.md CHANGED
@@ -57,6 +57,9 @@ matrix runs this same command with its Bundler-managed installation paths.
57
57
 
58
58
  ## Pull Request Guidelines
59
59
 
60
+ For authorized publication, use the [GitHub-hosted release runbook](RELEASING.md).
61
+ It documents exact-commit dispatch, environment approvals, and safe recovery.
62
+
60
63
  - Base branch: `main`
61
64
  - Commit style: [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `chore:`)
62
65
  - All specs must pass and coverage must remain at 100%
@@ -0,0 +1,72 @@
1
+ <!-- docs/FUZZY_MATCHING.md -->
2
+
3
+ # Explainable Fuzzy Name Matching
4
+
5
+ `Abbu::Utils::FuzzyMatcher.new(contact, candidates, threshold: 0.85).matches`
6
+ returns existing `Deduplicator::Match` objects in candidate order. The original
7
+ anchor object is skipped. Existing `Deduplicator#matches` remains unchanged.
8
+ Callers should supply distinct candidate objects. This API suggests comparisons;
9
+ it never selects a winner, collapses records or modifies source evidence.
10
+
11
+ ## Algorithm And Evidence Boundary
12
+
13
+ ABBU uses normalized Levenshtein edit distance over Unicode codepoints:
14
+ `similarity = 1 - distance / maximum_name_length`. This local, dependency-free
15
+ algorithm exposes an integer edit count. Unlike prefix-boosted metrics, it adds
16
+ no assumption that a matching prefix is more identifying. It is an ABBU
17
+ convention, not an Apple Contacts storage or identity rule.
18
+
19
+ Names use NFC, downcasing, trimming and whitespace collapse only. Accents,
20
+ punctuation, scripts and compatibility characters remain distinct; no ASCII
21
+ transliteration, nickname expansion, initial expansion or token reordering is
22
+ performed. Canonically equivalent Unicode spellings compare equally. Original
23
+ names remain in evidence and in the contacts. Empty names supply no fuzzy signal.
24
+
25
+ Thresholds are inclusive, finite numeric values in `[0, 1]`. Comparison uses the
26
+ unrounded similarity. Each qualifying fuzzy signal exposes `algorithm`,
27
+ `distance`, `similarity`, both normalized names, both raw names and `contribution`.
28
+ Threshold 1 requires equality after the documented normalization, not raw equality.
29
+ Even exact normalized names alone remain ambiguous.
30
+
31
+ ## Exact Evidence And Scores
32
+
33
+ Each pair is also checked by the existing provenance-aware exact matcher. Its
34
+ qualifying score and status take precedence; fuzzy evidence then contributes
35
+ zero, so similarity cannot inflate exact confidence. Exact signal contributions
36
+ follow existing weights, counted once per type and capped in evidence order at
37
+ the total score. Otherwise the fuzzy-only score is `0.15 * similarity`, rounded
38
+ to four decimals, with low confidence and ambiguous status. Scores are ranking
39
+ heuristics, never probabilities. Names are not global identifiers.
40
+
41
+ Two or more qualifying exact candidates for the anchor make every returned
42
+ suggestion ambiguous. Fuzzy suggestions alone do not demote a single exact
43
+ candidate. Candidate-to-candidate relationships are not evaluated; scope and
44
+ candidate selection remain the caller's responsibility. `Match#merge` still
45
+ requires an explicit caller-provided policy; review remains necessary.
46
+
47
+ ## Resource And Privacy Boundaries
48
+
49
+ Defaults are 10,000 candidates and 128 normalized codepoints per name. Both
50
+ `max_candidates:` and `max_name_length:` require positive integers. Exceeding a
51
+ bound raises `ArgumentError`, never silent truncation. Candidate enumeration
52
+ reads at most limit+1 elements; names are validated during `matches`. Results
53
+ are buffered. Callers raising limits accept the additional cost.
54
+
55
+ For N candidates and bounded name length L, edit-distance work is O(N × L²)
56
+ with O(L) distance-row memory. Exact comparison also traverses contact identifier
57
+ collections, so these bounds do not bound arbitrary email/phone arrays. The
58
+ deterministic 1,000-candidate synthetic spec exercises this baseline without a
59
+ flaky wall-clock assertion or personal Contacts data. This is not a benchmark
60
+ claim for arbitrary archive sizes or real-world identity accuracy.
61
+
62
+ Returned contacts, raw names, exact evidence and source paths are sensitive;
63
+ do not publish match results as sanitized diagnostics. Parsing, exporters and
64
+ existing JSON contracts are unchanged. No private Apple schema assumptions or
65
+ new runtime dependencies are introduced.
66
+
67
+ [README](../README.md) · [Contributing](CONTRIBUTING.md)
68
+
69
+ —
70
+ Stan Carver II
71
+ Made in Texas 🤠
72
+ https://stancarver.com
@@ -0,0 +1,98 @@
1
+ <!-- docs/HISTORY_EVIDENCE.md -->
2
+
3
+ # Contacts History: Evidence Before Events
4
+
5
+ Research date: 2026-10-01. Issue [#35](https://github.com/scarver2/abbu/issues/35).
6
+ Decision proposed for review: do not expose private save/edit-history fields
7
+ without reproducible storage evidence. This is a research boundary, not a
8
+ completed history parser or a claim that such records cannot exist.
9
+
10
+ ## Three Different Questions
11
+
12
+ 1. What timestamp does a record expose? ABBU already reads observed optional
13
+ creation/modification fields; these are not a sequence of edits.
14
+ 2. What changed between two observed snapshots? Future snapshot comparison can
15
+ describe differences, not infer actions or exact times between observations.
16
+ 3. What events does a supported history provider report? This requires its own
17
+ evidence and semantics; it must not replace the original record timestamps.
18
+
19
+ No timestamp alone proves a human edit, an author, or an interaction with the
20
+ Contacts UI. Sync, migration and normalization are alternative hypotheses to
21
+ test, not asserted explanations for a given record.
22
+
23
+ ## Primary Evidence
24
+
25
+ Apple documents [CNChangeHistoryFetchRequest](https://developer.apple.com/documentation/contacts/cnchangehistoryfetchrequest)
26
+ as a framework query for change history. Its overview says changes are coalesced
27
+ to eliminate redundant adds, updates and deletes. Therefore even this documented
28
+ surface must not be described as a complete keystroke or immutable audit log.
29
+
30
+ The documented [starting token](https://developer.apple.com/documentation/contacts/cnchangehistoryfetchrequest/startingtoken)
31
+ is opaque. A nil starting token requests a reset event and then add events for
32
+ current contacts/groups; those add events are not proof of original creation
33
+ times. Tokens are not dates, monotonically meaningful integers, or archive keys.
34
+
35
+ Apple's [unification option](https://developer.apple.com/documentation/contacts/cnchangehistoryfetchrequest/shouldunifyresults)
36
+ distinguishes unified from individual history. A future adapter must preserve
37
+ which mode it requested rather than silently treating the outputs identically.
38
+
39
+ Sources inspected on the research date. These documents do not specify private
40
+ SQLite history table layouts or mappings into an exported ABBU. No native API
41
+ calls, private live-store reads or personal database experiments were performed.
42
+
43
+ ## Repository Evidence Audit
44
+
45
+ At accepted main `77eaeda`, the SQLite parser reads `ZCREATIONDATE` and
46
+ `ZMODIFICATIONDATE` as optional record timestamps. Synthetic fixture generation
47
+ and timestamp regressions exercise those fields. The schema inspector reports
48
+ unknown structure without interpreting it. The repository has no verified
49
+ save-history mapping with attributed macOS/Contacts build and controlled event
50
+ sequence. See [format evidence](ABBU.md) and the [compatibility matrix](FORMAT_COMPATIBILITY.md).
51
+
52
+ A table named like history, transaction, save or change does not establish
53
+ event semantics. Neither file modification times nor WAL page changes identify
54
+ the person who edited a contact. Third-party reverse engineering may supply
55
+ candidate observations, but is not an accepted mapping without reproduction.
56
+
57
+ ## Required Experiment Before Implementation
58
+
59
+ - Use disposable, unsynced accounts and invented contacts, with explicit
60
+ experiment approval. Record exact OS/Contacts builds and capture method.
61
+ - Establish a known initial state, then perform one controlled operation per
62
+ observation: add, scalar edit, labeled-value edit, group membership change,
63
+ delete, no-op save, and reopen. Separately test sync/migration if in scope.
64
+ - Capture original bytes and compare candidate records without modifying the
65
+ source store. Record ordering, clock/time-zone assumptions, transaction
66
+ grouping, retention/reset behavior and missing events. Do not guess epochs.
67
+ - Confirm relationships under duplicate names, source-local ID collisions,
68
+ linked/unified contacts, and deletion of the referenced record. Keep old
69
+ schema cases. Unknown layouts must remain unsupported, not empty history.
70
+ - Independently reproduce any external observation. Preserve raw values beside
71
+ proposed interpretation and document counterexamples and alternative causes.
72
+
73
+ ## Future Public Contract
74
+
75
+ History would be optional provenance evidence, never replacement `created_at`
76
+ or `modified_at`. Require explicit source/snapshot attribution, raw event data,
77
+ evidenced timestamp interpretation, ordering limits and unknown fields.
78
+ Distinguish unsupported backend, unavailable/permission failure, reset/truncated
79
+ history, and genuinely empty results from a capable provider. Never imply a
80
+ human actor without independent evidence. Define stable JSON and RBS only after
81
+ the source model is justified; no speculative API is shipped here.
82
+
83
+ Do not log contact values, identifiers, tokens or filesystem paths by default.
84
+ Archived evidence and tokens remain sensitive. A framework adapter would be a
85
+ separately reviewed, opt-in macOS boundary, not a mandatory portable-gem
86
+ dependency or a query against the current user's store when opening an archive.
87
+
88
+ The research outcome is insufficient evidence for a private parser. #35 can
89
+ close on this scoped boundary under the development-train directive; reopen
90
+ with a reproducible fixture/event sequence. #43/#44 snapshot work remains
91
+ independently useful but must not advertise unseen historical events.
92
+
93
+ Return to [README](../README.md), [roadmap](TODO.md), or [format evidence](ABBU.md).
94
+
95
+ —
96
+ Stan Carver II
97
+ Made in Texas 🤠
98
+ https://stancarver.com
data/docs/ICALENDAR.md ADDED
@@ -0,0 +1,101 @@
1
+ <!-- docs/ICALENDAR.md -->
2
+
3
+ # iCalendar Reminders
4
+
5
+ `Abbu::Exporters::IcalendarExporter` consumes the public `Contact#birthday` and
6
+ `Contact#anniversary` fields, not private Apple tables. It emits one all-day,
7
+ transparent yearly reminder per supported field. Arbitrary `Contact#dates`
8
+ entries are not classified as birthdays or anniversaries by this exporter.
9
+ It does not create alarms, calculate ages, change contacts, or write source stores.
10
+
11
+ ## Explicit Metadata
12
+
13
+ The constructor requires `year:` (Integer 1–9996), `generated_at:` (Time whose UTC
14
+ year is 1–9999), and a nonempty `calendar_id:` String. The latter is a caller-owned
15
+ unique namespace: choose a unique UUID or identifier under a domain you control,
16
+ and reuse it only for the same logical ordered input. The timestamp is the real
17
+ export revision time, converted to UTC seconds; subsecond precision is omitted.
18
+ No wall clock, random identifier, source timestamp, or fabricated epoch is used.
19
+
20
+ `to_ical` returns UTF-8 text; `to_file(path)` writes identical bytes;
21
+ `to_stdout` prints those bytes. Same ordered contacts and metadata produce the
22
+ same output. Every call rebuilds `diagnostics`, a frozen array of frozen hashes
23
+ with `category`, zero-based `contact_index`, and `field`. Categories are
24
+ `invalid_date` and `unsupported_calendar`. These omit names, paths and date values.
25
+
26
+ ## Date Policy
27
+
28
+ An absent year (`nil`) or integer zero is unknown. Other year/month/day values
29
+ must be integers and form a valid proleptic Gregorian date, with known years in
30
+ 1–9999. Unknown years use a leap-year validation reference internally; that
31
+ reference is never exported as a birth year. Invalid values are diagnosed and
32
+ omitted, not repaired. No private Apple sentinel year is guessed.
33
+
34
+ `DTSTART` is the first valid occurrence at or after the caller's year and, when
35
+ known, the source year. February 29 advances to the next leap year if needed;
36
+ yearly recurrence skips non-leap years, never silently shifts to February 28 or
37
+ March 1. `DURATION:P1D` avoids cross-year end-date overflow. All events contain
38
+ an explanatory DESCRIPTION and `X-ABBU-YEAR-UNKNOWN:TRUE` or `FALSE`. Known source
39
+ years appear in DESCRIPTION; unknown source years never gain an asserted age or
40
+ original year. Calendar software may display recurrence starts as dates; these
41
+ are reminders, not birthday-field synchronization or Apple round-trip backups.
42
+
43
+ `lunar_birthday` is always diagnosed and omitted. A supplied `calendar` key on a
44
+ date permits only absent/nil, `'gregorian'`, or `:gregorian`; anything else is
45
+ diagnosed and omitted. Existing parsers do not expose every private calendar
46
+ attribute, so absence of an attribute is not proof of original Apple calendar
47
+ semantics. This is an explicit Gregorian convention over the current public
48
+ normalized fields, not alternate-calendar conversion or expanded parse support.
49
+
50
+ SUMMARY preserves `raw_label` when present (including blank), otherwise `label`,
51
+ otherwise Birthday/Anniversary. Custom and wrapped labels remain exact after
52
+ TEXT decoding; the Contact and its evidence are never rewritten.
53
+
54
+ ## Wire And Identity Contract
55
+
56
+ The standards basis is [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545):
57
+ content lines and UTF-8 in §3.1, TEXT escaping in §3.3.11, DATE in §3.3.4,
58
+ yearly recurrence in §3.3.10, all-day VEVENT in §3.6.1, UID in §3.8.4.7, and
59
+ DTSTAMP in §3.8.7.2. Output uses CRLF, escaped backslashes/commas/semicolons and
60
+ newlines, and UTF-8-safe folding at 75 octets including continuation whitespace.
61
+ Invalid text/control characters raise before opening an output file.
62
+
63
+ UIDs hash the namespace, zero-based input position, field kind, source-relative
64
+ path, and full name. Duplicate contacts remain separate. These are generated
65
+ reminder identifiers, not Apple IDs or proof of contact identity. Reordering,
66
+ inserting contacts, changing names/provenance, or changing namespace changes
67
+ UIDs; this is not a persistent synchronization feed. Hashes are not anonymization.
68
+ No source paths or contact identifiers are exported in clear text, but names,
69
+ labels, source years, dates and correlatable identifiers are sensitive.
70
+
71
+ With no supported events, output is the empty string, not an invalid componentless
72
+ VCALENDAR. Tolerant omissions can therefore produce no bytes. Inspect diagnostics.
73
+ `strict:`/`--strict` belongs to parsing only; it does not alter exporter omissions.
74
+ Existing output files are overwritten on successful export, even empty output;
75
+ file writing is buffered but not atomic/no-clobber. Use a separate destination
76
+ outside the source bundle. Filesystem failures can leave partial output.
77
+
78
+ ## CLI
79
+
80
+ Use `--format icalendar`, `--calendar-year`, `--calendar-stamp` (full ISO 8601
81
+ timestamp with timezone), and `--calendar-id`. Archive and explicit live-store
82
+ input are supported. Only output selection and parser strictness accompany
83
+ calendar export; conflicting operations are rejected. Stdout contains only
84
+ calendar bytes (or none); omission diagnostics go to stderr. Success including
85
+ omissions exits 0, invalid metadata/conflicts exit 2. Existing input/access
86
+ failure behavior is unchanged. The exporter adds no runtime dependency.
87
+
88
+ ## Evidence And Limits
89
+
90
+ `spec/support/calendar_fixtures.rb` reproduces previously supported plist keys
91
+ and SQLite date columns using synthetic data, not a particular macOS build.
92
+ API and CLI specs exercise byte preservation, unknown-year/leap policies,
93
+ Unicode folding, raw labels, malformed input, and deterministic output. No live
94
+ personal store is inspected. No calendar-client import certification is claimed.
95
+
96
+ [README](../README.md) · [ABBU evidence](ABBU.md)
97
+
98
+ —
99
+ Stan Carver II
100
+ Made in Texas 🤠
101
+ https://stancarver.com
@@ -0,0 +1,128 @@
1
+ <!-- docs/MACHINE_JSON.md -->
2
+
3
+ # Machine JSON Contract
4
+
5
+ `--json` selects one structured operation with no human chatter on stdout.
6
+ Every handled success, no-match, validation, or input/output failure emits one
7
+ UTF-8 JSON document followed by a newline. This is buffered JSON, not streaming
8
+ JSON Lines. Consumers must inspect exit status before interpreting the document.
9
+ Broken output pipes and unexpected programmer errors are not guaranteed a JSON
10
+ response. Human CLI modes and `--format json` without `--json` remain unchanged.
11
+
12
+ ## Operations
13
+
14
+ | Options | Success document | Empty result |
15
+ | --- | --- | --- |
16
+ | `--json` (optionally `--format json`) | Existing contact JSON array | `[]`, status 0 |
17
+ | `--search TERM --json`, `--email ADDRESS --json`, `--phone NUMBER --json` | Existing contact JSON array | `[]`, status 1 |
18
+ | Timestamp bounds plus `--json` | Existing contact JSON array | `[]`, status 1 |
19
+ | `--stats --json` | Integer `total_contacts`, `with_email`, `with_phone` | All zero, status 0 |
20
+ | `--schema --json` | Existing `schema_report` object | Empty `databases`, status 0 |
21
+ | `--sources --json` | Existing source metadata array | `[]`, status 0 |
22
+ | `--groups --json` | Existing group metadata array | `[]`, status 0 |
23
+ | `--dedupe --json` | Array of `{email, contacts}` | `[]`, status 0 |
24
+ | `--matches` (optional `--json`) | Identity suggestion array | `[]`, status 0 |
25
+ | `--diagnostics` (optional `--json`) | Parser diagnostic array after parsing | `[]`, status 0 |
26
+ | `--extract-images DIR --json` | `{files, diagnostics}` | Two empty arrays, status 0 |
27
+ | `--diff PATH --json` | [Snapshot comparison](SNAPSHOT_DIFF.md), schema version 1 | Empty category arrays, status 0 |
28
+ | `--version --json`, `--help --json` | `{version}` or `{help}` strings | Not applicable |
29
+
30
+ Only one operation is allowed, except timestamp bounds can accompany one search.
31
+ `--output`, photo mode, or a non-JSON export format conflict with machine mode.
32
+ `--format json` may accompany bare `--json`, but not another operation. Validation
33
+ precedes input reads and image copying. Help/version terminate immediately as
34
+ before. Supply exactly one archive path, or `--live`/`--live-path PATH` with no
35
+ positional arguments. Live schema/search/extraction remain unsupported; live
36
+ stats, contacts, sources/groups, diagnostics, duplicates and matches are supported.
37
+ No live-store snapshot guarantee is added. `--strict` retains fail-fast parsing.
38
+
39
+ ## Schemas And Meaning
40
+
41
+ Contact objects are exactly `JsonExporter#payload`: absent optional scalar
42
+ fields are omitted, existing empty collections remain arrays, provenance and
43
+ raw labels survive, and timestamps use the existing ISO 8601 representation.
44
+ This method is now public for callers needing Ruby hashes without serializing
45
+ and reparsing JSON. Source/group metadata and schema reports retain their
46
+ previous shapes. Do not depend on whitespace or JSON object key ordering.
47
+
48
+ Duplicate `email` is the existing first-email hash used by `Deduplicator#duplicates`,
49
+ not a newly normalized string. `contacts` contains the full contact objects.
50
+ `--matches` instead uses `Deduplicator#matches`: each object has full `left` and
51
+ `right` contacts, two `sources` (possibly null), `evidence` containing `type`,
52
+ `normalized`, `left_raw`, `right_raw`, numeric `score`, string `confidence`, and
53
+ string `status`. These are suggestions, not identity truth; no merge is performed.
54
+ Weak or competing candidates retain the existing ambiguity status.
55
+
56
+ Parser diagnostics contain `category`, `message`, `parser`, `source`, `context`.
57
+ With `--diagnostics` they appear on stdout only. For other machine operations,
58
+ nonfatal parser diagnostics stay on stderr as text and never corrupt stdout.
59
+ Schema inspection alone does not parse contacts or force parser diagnostics.
60
+
61
+ Image results contain `files` and extraction `diagnostics`. Each file includes
62
+ the full `contact`, raw `image_uri`, absolute string `source_path` and output
63
+ `path`, and `media_type`. Extraction diagnostics use the existing `code`,
64
+ `contact_name`, `image_uri`, and optional `source_path`/`detail`. Partial copies
65
+ and collisions are reported in that array with status 0, as in existing
66
+ extraction; callers must inspect diagnostics to require complete extraction.
67
+ Directory creation failures instead produce an error and status 1. Source
68
+ archives remain read-only; extraction creates separate files without overwriting.
69
+
70
+ ## Errors And Exit Codes
71
+
72
+ | Status | Meaning in machine mode |
73
+ | --- | --- |
74
+ | 0 | Operation completed (possibly with nonfatal diagnostics) |
75
+ | 1 | Search had no matches (`[]`), or live input/filesystem failure (`error` object) |
76
+ | 2 | Invalid options/input, conflicting operations, invalid timestamp, or parse failure |
77
+
78
+ Errors have exactly the shape `{"error":{"code":"invalid_input","message":"..."}}`.
79
+ Codes are `invalid_option` for option parsing, `invalid_input` for validation or
80
+ parse failures, and `input_output_error` for live access/filesystem errors.
81
+ Messages aid humans; branch on the code and exit status, not message wording.
82
+ Parser diagnostics can still appear on stderr after a strict failure.
83
+
84
+ Migration from 0.9: JSON failures now produce this document rather than empty
85
+ stdout; previously ignored operation combinations are rejected. Bare `--json`
86
+ now lists contacts. Search no-match status and successful legacy JSON shapes are
87
+ unchanged. Human CLI error behavior is not normalized by this change.
88
+
89
+ ## Automation And Privacy
90
+
91
+ ```bash
92
+ abbu Contacts.abbu --stats --json >stats.json 2>diagnostics.log
93
+ abbu Contacts.abbu --diagnostics >parser-diagnostics.json
94
+ abbu Contacts.abbu --matches >identity-suggestions.json 2>diagnostics.log
95
+ abbu Contacts.abbu --sources --json | jq '.[].contact_count'
96
+
97
+ # Do not let jq's status hide ABBU's failure; inspect the producer first.
98
+ if abbu Contacts.abbu --email person@example.test --json >result.json 2>diagnostics.log; then
99
+ jq '.[].name' result.json
100
+ else
101
+ status=$?
102
+ printf 'ABBU status: %s\n' "$status" >&2
103
+ jq . result.json
104
+ fi
105
+ ```
106
+
107
+ All outputs are sensitive. Full contact/identity/image payloads expose contact
108
+ values, raw labels, identifiers, photo paths and provenance; image extraction
109
+ also writes photos. Parser diagnostics deliberately retain the existing limited
110
+ contexts, but paths and messages can identify local accounts. Schema/source/group
111
+ reports are not sanitized logs. Error messages may contain caller paths or input.
112
+ JSON does not grant permission to send any of this to a remote agent, store it in
113
+ public CI artifacts, or publish logs. Use controlled destinations and permissions.
114
+ No Apple-private semantics are inferred by these serializers.
115
+
116
+ JSON schemas are public API under the repository's pre-1.0 SemVer policy.
117
+ Breaking shape/type/meaning changes require explicit migration notes and a minor
118
+ version; compatible bug fixes use a patch. Future diff/history or optional MCP
119
+ adapters should consume these public representations without changing established
120
+ operations or imposing an MCP runtime dependency on ABBU core.
121
+
122
+ See [README](../README.md), [API stability](API_STABILITY.md), and
123
+ [format evidence](ABBU.md).
124
+
125
+ —
126
+ Stan Carver II
127
+ Made in Texas 🤠
128
+ https://stancarver.com
data/docs/MCP.md ADDED
@@ -0,0 +1,148 @@
1
+ <!-- docs/MCP.md -->
2
+
3
+ # Optional Read-Only MCP Adapter
4
+
5
+ The adapter connects a trusted local MCP host to one explicitly selected ABBU
6
+ archive or live-store directory. It consumes public ABBU APIs and the
7
+ [machine JSON contract](MACHINE_JSON.md); it never reads parser internals or
8
+ defines new Apple storage semantics.
9
+
10
+ ## Dependency And Execution Boundary
11
+
12
+ The adapter ships as an opt-in executable/require path inside ABBU rather than a
13
+ second companion gem. This keeps its version, public payloads and tests together
14
+ without a second release pipeline. The official `mcp` gem is **not** a runtime
15
+ dependency of `abbu`, and `require 'abbu'` does not load it. Only adapter users
16
+ install/declare it. Repository development/test uses the SDK for real protocol
17
+ integration tests, not a mock protocol implementation.
18
+
19
+ ```ruby
20
+ # Application Gemfile (once this ABBU development version is released)
21
+ gem 'abbu', '~> 0.18.0'
22
+ gem 'mcp', '~> 1.6.1'
23
+ ```
24
+
25
+ ```bash
26
+ abbu-mcp --help
27
+ abbu-mcp /absolute/path/Contacts.abbu
28
+ abbu-mcp --live-path /absolute/path/AddressBook --strict
29
+ ```
30
+
31
+ Use the same Ruby/Bundler environment for both gems. From this repository:
32
+ `mise exec -- bundle exec ruby bin/abbu-mcp /absolute/path/Contacts.abbu`.
33
+ The executable uses Ruby directly, as does packaged `abbu`; it does not source
34
+ development-only shell helpers that are absent from the installed gem.
35
+ Configure an MCP host's stdio subprocess with that command/argument array.
36
+ Do not put shell expansion or credentials in arguments. There is no HTTP server,
37
+ network listener, remote transport or automatically discovered live store.
38
+
39
+ Missing SDK, invalid startup configuration or unavailable input exits 2 with a
40
+ generic stderr message, no path/exception dump. `--help` prints usage and exits
41
+ without opening input. Once serving, stdout is exclusively SDK JSON-RPC frames.
42
+
43
+ ## Tools
44
+
45
+ | Tool | Arguments | `structuredContent.data` |
46
+ | --- | --- | --- |
47
+ | `abbu_query` | `mode`, `value`, optional `offset`, `limit` | `{contacts, offset, limit, has_more}` |
48
+ | `abbu_stats` | None | `{total_contacts, with_email, with_phone}` |
49
+ | `abbu_sources` | None | Existing source metadata array |
50
+ | `abbu_groups` | None | Existing group metadata array |
51
+ | `abbu_diagnostics` | None | Existing parser diagnostic array |
52
+
53
+ Query modes are `search` (partial name/email), `email` and `phone` (existing exact
54
+ normalized lookup), and `modified_since` (timezone-explicit ISO 8601 lower bound).
55
+ Every query requires a string `value`. Empty search retains public Query semantics:
56
+ no matches. Results keep parser ordering and are never automatically deduplicated.
57
+ Offset defaults to 0, limit to 100; maximum limit is 500. `has_more` explicitly
58
+ indicates remaining matches. These are payload bounds, not streaming parsing or
59
+ an archive size limit: the input and query still eagerly materialize contacts.
60
+
61
+ Each successful tool result includes the same JSON envelope in text content and
62
+ structured content. Output schemas specify the envelope, array/object types,
63
+ query pagination fields and integer statistics; nested contact/source/group
64
+ objects retain their existing ABBU contracts. Consumers must accept optional
65
+ contact keys. No-match query results are successful with `contacts: []`.
66
+
67
+ Tool schemas reject undeclared arguments, so a caller cannot substitute another
68
+ path. The operator chooses the input before server startup. My Card is omitted
69
+ until evidence establishes it. Image extraction, export, merging, writing,
70
+ tagging, releases, shell execution and arbitrary filesystem access have no tool.
71
+
72
+ Output validation remains enabled. In the pinned SDK 1.6.1,
73
+ [`validate_tool_call_result!`](https://github.com/modelcontextprotocol/ruby-sdk/blob/v1.6.1/lib/mcp/server.rb#L1539-L1545)
74
+ validates successful results against the declared `{data: ...}` schema and
75
+ deliberately skips that success schema for `isError: true`. Adapter failures use
76
+ the separately documented `{error: ...}` envelope. Real stdio regressions exercise
77
+ both semantic invalid-query and strict-parser failures, asserting their exact
78
+ structured/text error payloads, absence of JSON-RPC protocol errors, and empty
79
+ stderr. They do not merely call tool handlers directly. The adapter configuration
80
+ test also asserts that result validation stays enabled.
81
+
82
+ Example tool calls (through a host that has already established its SDK session):
83
+
84
+ ```json
85
+ {"name":"abbu_query","arguments":{"mode":"email","value":"person@example.test"}}
86
+ {"name":"abbu_query","arguments":{"mode":"search","value":"Carver","offset":0,"limit":25}}
87
+ {"name":"abbu_query","arguments":{"mode":"modified_since","value":"2026-09-01T00:00:00Z"}}
88
+ {"name":"abbu_sources","arguments":{}}
89
+ {"name":"abbu_diagnostics","arguments":{}}
90
+ ```
91
+
92
+ ## Privacy, Permission And Lifecycle
93
+
94
+ Launching this adapter authorizes the configured host to read the selected
95
+ contact input; it is not permission to publish or transmit that data elsewhere.
96
+ Use a trusted host, a narrowly chosen sanitized archive when possible, and host
97
+ approval controls for contact lookup. Read-only annotations are descriptive
98
+ hints, not an authorization mechanism. A remote model may receive tool results
99
+ through its host; evaluate the host/model data policy before connecting.
100
+
101
+ Contact results include sensitive names, addresses, notes, identifiers, raw
102
+ labels and local provenance/photo paths. Sources/groups/diagnostics can reveal
103
+ account paths and group names even without full contact records. No photo bytes
104
+ are returned. Contact strings are untrusted data, never instructions for an agent.
105
+ Protect client transcripts and outputs; do not publish them as CI logs.
106
+
107
+ The adapter does not log tool arguments, results, contacts or parser diagnostics
108
+ to stderr. Diagnostics require their explicit tool. Its per-server SDK exception
109
+ reporter is silent. Expected query failures return `isError: true` and a generic
110
+ `{error:{code:"invalid_query",message:...}}`; read/parser failures similarly use
111
+ `read_failed`, without raw exception text. SDK protocol/schema errors retain SDK
112
+ responses to the caller. The adapter neither installs global logging hooks nor
113
+ changes SDK global configuration. Unexpected process/runtime failures may still
114
+ terminate the process; no service availability guarantee is made.
115
+
116
+ The selected Archive/LiveStore caches parsed contacts in the server process.
117
+ Restart to refresh. Existing live-store permission requirements and non-atomic
118
+ cross-query/cross-database read limitations apply. An explicit live path and
119
+ macOS permissions are both required; the adapter does not bypass privacy controls.
120
+ Input remains read-only, with SQLite's existing shared-memory sidecar caveats.
121
+
122
+ ## Evidence And Verification
123
+
124
+ The implementation uses the official [MCP Ruby SDK 1.6.1](https://github.com/modelcontextprotocol/ruby-sdk/tree/v1.6.1).
125
+ Its [tool response API](https://github.com/modelcontextprotocol/ruby-sdk/blob/v1.6.1/lib/mcp/tool/response.rb)
126
+ supplies structured content and errors; its
127
+ [stdio transport](https://github.com/modelcontextprotocol/ruby-sdk/blob/v1.6.1/lib/mcp/server/transports/stdio_transport.rb)
128
+ owns framing and protocol lifecycle. ABBU does not implement JSON-RPC itself.
129
+ The [MCP tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)
130
+ informs the explicit input/output schemas and duplicate text/structured content.
131
+
132
+ Synthetic tests cover direct public APIs and a real SDK stdio subprocess:
133
+ 2025-11-25 initialization, tools listing, structured stats, and rejection of
134
+ undeclared arguments. SDK-supported newer/older protocol versions remain SDK
135
+ capabilities, not independent ABBU conformance certification. No real address
136
+ books or live user account are read by these tests. Run canonical `bin/spec`,
137
+ `bin/lint`, `bin/package`, and `mise exec -- rbs -I sig validate`.
138
+
139
+ The adapter is pre-1.0 public API and follows ABBU SemVer. Publication remains
140
+ Sheriff-gated; installing a development branch is not a published release.
141
+
142
+ See [README](../README.md), [format evidence](ABBU.md), and
143
+ [API stability](API_STABILITY.md).
144
+
145
+ —
146
+ Stan Carver II
147
+ Made in Texas 🤠
148
+ https://stancarver.com