abbu 0.1.2 → 0.4.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 +127 -17
- data/bin/abbu +185 -54
- data/docs/ABBU.md +226 -7
- data/docs/CHANGELOG.md +193 -0
- data/docs/CONTRIBUTING.md +61 -0
- data/docs/TODO.md +180 -0
- data/lib/abbu/archive.rb +68 -8
- data/lib/abbu/contact.rb +13 -4
- data/lib/abbu/diagnostic.rb +27 -0
- data/lib/abbu/exporters/csv_exporter.rb +26 -12
- data/lib/abbu/exporters/json_exporter.rb +17 -1
- data/lib/abbu/exporters/vcard_exporter.rb +91 -7
- data/lib/abbu/image_extractor.rb +134 -0
- data/lib/abbu/live_store.rb +104 -0
- data/lib/abbu/parse_error.rb +13 -0
- data/lib/abbu/parsers/plist_parser.rb +172 -4
- data/lib/abbu/parsers/sqlite_parser.rb +158 -55
- data/lib/abbu/query.rb +73 -0
- data/lib/abbu/schema_inspector.rb +133 -0
- data/lib/abbu/utils/contact_identity.rb +97 -0
- data/lib/abbu/utils/deduplicator.rb +130 -2
- data/lib/abbu/utils/image_resolver.rb +59 -0
- data/lib/abbu/utils/label_normalizer.rb +15 -0
- data/lib/abbu/utils/source_descriptor.rb +35 -0
- data/lib/abbu/version.rb +1 -1
- data/lib/abbu.rb +14 -2
- data/tasks/abbu.rake +1 -1
- metadata +50 -16
- data/CHANGELOG.md +0 -67
- data/bin/cleanse +0 -7
- data/bin/console +0 -10
- data/bin/dev +0 -7
- data/bin/lint +0 -7
- data/bin/outdated +0 -7
- data/bin/test +0 -7
|
@@ -1,9 +1,61 @@
|
|
|
1
1
|
# lib/abbu/utils/deduplicator.rb
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative 'contact_identity'
|
|
5
|
+
|
|
4
6
|
module Abbu
|
|
5
7
|
module Utils
|
|
6
8
|
class Deduplicator
|
|
9
|
+
class MergePolicyRequired < ArgumentError; end
|
|
10
|
+
|
|
11
|
+
class Match
|
|
12
|
+
attr_reader :left, :right, :evidence, :score, :confidence, :status
|
|
13
|
+
|
|
14
|
+
def initialize(left:, right:, evidence:, score:, status:)
|
|
15
|
+
@left = left
|
|
16
|
+
@right = right
|
|
17
|
+
@evidence = evidence.freeze
|
|
18
|
+
@score = score
|
|
19
|
+
@confidence = confidence_for(score)
|
|
20
|
+
@status = status
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def sources
|
|
25
|
+
[left.source, right.source].freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ambiguous?
|
|
29
|
+
status == :ambiguous
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def merge(policy: nil)
|
|
33
|
+
raise MergePolicyRequired, 'an explicit callable merge policy is required' unless policy.respond_to?(:call)
|
|
34
|
+
|
|
35
|
+
policy.call(left, right, evidence: evidence)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def confidence_for(value)
|
|
41
|
+
return :high if value >= 0.8
|
|
42
|
+
return :medium if value >= 0.6
|
|
43
|
+
|
|
44
|
+
:low
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
WEIGHTS = {
|
|
49
|
+
email: 0.8,
|
|
50
|
+
international_phone: 0.65,
|
|
51
|
+
source_local_phone: 0.45,
|
|
52
|
+
name: 0.25,
|
|
53
|
+
organization: 0.15
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
MINIMUM_SCORE = 0.4
|
|
57
|
+
PROBABLE_SCORE = 0.6
|
|
58
|
+
|
|
7
59
|
def initialize(contacts)
|
|
8
60
|
@contacts = contacts
|
|
9
61
|
end
|
|
@@ -11,8 +63,84 @@ module Abbu
|
|
|
11
63
|
def duplicates
|
|
12
64
|
@contacts
|
|
13
65
|
.group_by { |c| c.emails.first }
|
|
14
|
-
.reject
|
|
15
|
-
.select
|
|
66
|
+
.reject { |k, _| k.nil? }
|
|
67
|
+
.select { |_, v| v.size > 1 }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def matches
|
|
71
|
+
identities = @contacts.map { |contact| ContactIdentity.new(contact) }
|
|
72
|
+
candidates = identities.combination(2).filter_map { |left, right| candidate(left, right) }
|
|
73
|
+
ambiguous_contacts = multiply_matched_contacts(candidates)
|
|
74
|
+
|
|
75
|
+
candidates.map do |candidate|
|
|
76
|
+
build_match(candidate, ambiguous_contacts)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
alias identity_matches matches
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def candidate(left, right)
|
|
85
|
+
evidence = evidence_for(left, right)
|
|
86
|
+
score = evidence.map { |item| item[:type] }.uniq.sum { |type| WEIGHTS.fetch(type) }.clamp(0.0, 1.0).round(2)
|
|
87
|
+
return if score < MINIMUM_SCORE
|
|
88
|
+
|
|
89
|
+
{ left: left.contact, right: right.contact, evidence: evidence, score: score }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def evidence_for(left, right)
|
|
93
|
+
signal_pairs(left, right).flat_map do |left_signals, right_signals, same_source|
|
|
94
|
+
shared_evidence(left_signals, right_signals, same_source: same_source)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def signal_pairs(left, right)
|
|
99
|
+
[
|
|
100
|
+
[left.emails, right.emails, false],
|
|
101
|
+
[left.phones, right.phones, left.same_source?(right)],
|
|
102
|
+
[[left.name].compact, [right.name].compact, false],
|
|
103
|
+
[[left.organization].compact, [right.organization].compact, false]
|
|
104
|
+
]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def shared_evidence(left_signals, right_signals, same_source: false)
|
|
108
|
+
left_signals.filter_map do |left|
|
|
109
|
+
right = right_signals.find { |candidate| comparable?(left, candidate, same_source) }
|
|
110
|
+
evidence_record(left, right) if right
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def comparable?(left, right, same_source)
|
|
115
|
+
return false unless left.type == right.type && left.normalized == right.normalized
|
|
116
|
+
return same_source if left.type == :source_local_phone
|
|
117
|
+
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def evidence_record(left, right)
|
|
122
|
+
{
|
|
123
|
+
type: left.type,
|
|
124
|
+
normalized: left.normalized,
|
|
125
|
+
left_raw: left.raw,
|
|
126
|
+
right_raw: right.raw
|
|
127
|
+
}.freeze
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def multiply_matched_contacts(candidates)
|
|
131
|
+
counts = Hash.new(0).compare_by_identity
|
|
132
|
+
candidates.each do |candidate|
|
|
133
|
+
counts[candidate[:left]] += 1
|
|
134
|
+
counts[candidate[:right]] += 1
|
|
135
|
+
end
|
|
136
|
+
counts.select { |_, count| count > 1 }.keys
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def build_match(candidate, ambiguous_contacts)
|
|
140
|
+
ambiguous = candidate[:score] < PROBABLE_SCORE ||
|
|
141
|
+
ambiguous_contacts.include?(candidate[:left]) ||
|
|
142
|
+
ambiguous_contacts.include?(candidate[:right])
|
|
143
|
+
Match.new(**candidate, status: ambiguous ? :ambiguous : :probable)
|
|
16
144
|
end
|
|
17
145
|
end
|
|
18
146
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# lib/abbu/utils/image_resolver.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
module Utils
|
|
8
|
+
class ImageResolver
|
|
9
|
+
EXTENSIONS = %w[jpg jpeg png heic].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(bundle_path)
|
|
12
|
+
@bundle_path = Pathname.new(bundle_path).expand_path
|
|
13
|
+
@index = build_index
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def resolve(image_uri, source: nil)
|
|
17
|
+
return nil if image_uri.nil? || image_uri.to_s.empty?
|
|
18
|
+
|
|
19
|
+
candidates = @index.fetch(image_uri.to_s, [])
|
|
20
|
+
return candidates.first if candidates.one?
|
|
21
|
+
return if candidates.empty?
|
|
22
|
+
|
|
23
|
+
source_local_candidate(candidates, source)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def each_image(&)
|
|
27
|
+
return enum_for(:each_image) unless block_given?
|
|
28
|
+
|
|
29
|
+
@index.each_value { |files| files.each(&) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_index
|
|
35
|
+
image_files.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |file, hash|
|
|
36
|
+
stem = file.basename('.*').to_s
|
|
37
|
+
hash[stem] << file
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def image_files
|
|
42
|
+
@bundle_path.glob('**/Images/*').select { |f| f.file? && image_extension?(f) }.sort
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def image_extension?(file)
|
|
46
|
+
ext = file.extname.downcase.delete_prefix('.')
|
|
47
|
+
EXTENSIONS.include?(ext)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def source_local_candidate(candidates, source)
|
|
51
|
+
return unless source
|
|
52
|
+
|
|
53
|
+
database = @bundle_path.join(source.fetch(:relative_path, ''))
|
|
54
|
+
local_images = database.dirname.join('Images').expand_path
|
|
55
|
+
candidates.find { |candidate| candidate.dirname.expand_path == local_images }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# lib/abbu/utils/label_normalizer.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Abbu
|
|
5
|
+
module Utils
|
|
6
|
+
class LabelNormalizer
|
|
7
|
+
APPLE_STANDARD_LABEL = /\A_\$!<(?<label>.+)>!\$_\z/
|
|
8
|
+
|
|
9
|
+
def self.normalize(label)
|
|
10
|
+
match = label&.match(APPLE_STANDARD_LABEL)
|
|
11
|
+
match ? match[:label] : label
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# lib/abbu/utils/source_descriptor.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
module Utils
|
|
8
|
+
class SourceDescriptor
|
|
9
|
+
def initialize(path, root_path: nil)
|
|
10
|
+
@path = Pathname.new(path).expand_path
|
|
11
|
+
@root_path = Pathname.new(root_path).expand_path if root_path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
relative_path = relative_path_for(@path)
|
|
16
|
+
parts = Pathname.new(relative_path).each_filename.to_a
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
path: @path.to_s,
|
|
20
|
+
relative_path: relative_path,
|
|
21
|
+
kind: parts.first == 'Sources' ? 'source' : 'root',
|
|
22
|
+
identifier: parts.first == 'Sources' ? parts[1] : nil
|
|
23
|
+
}.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def relative_path_for(path)
|
|
29
|
+
return path.basename.to_s unless @root_path
|
|
30
|
+
|
|
31
|
+
path.relative_path_from(@root_path).to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/abbu/version.rb
CHANGED
data/lib/abbu.rb
CHANGED
|
@@ -3,16 +3,28 @@
|
|
|
3
3
|
|
|
4
4
|
require_relative 'abbu/version'
|
|
5
5
|
require_relative 'abbu/contact'
|
|
6
|
+
require_relative 'abbu/diagnostic'
|
|
7
|
+
require_relative 'abbu/parse_error'
|
|
6
8
|
require_relative 'abbu/archive'
|
|
9
|
+
require_relative 'abbu/live_store'
|
|
10
|
+
require_relative 'abbu/query'
|
|
11
|
+
require_relative 'abbu/schema_inspector'
|
|
7
12
|
require_relative 'abbu/parsers/sqlite_parser'
|
|
8
13
|
require_relative 'abbu/parsers/plist_parser'
|
|
9
14
|
require_relative 'abbu/exporters/csv_exporter'
|
|
10
15
|
require_relative 'abbu/exporters/json_exporter'
|
|
11
16
|
require_relative 'abbu/exporters/vcard_exporter'
|
|
17
|
+
require_relative 'abbu/image_extractor'
|
|
18
|
+
require_relative 'abbu/utils/contact_identity'
|
|
12
19
|
require_relative 'abbu/utils/deduplicator'
|
|
20
|
+
require_relative 'abbu/utils/label_normalizer'
|
|
13
21
|
|
|
14
22
|
module Abbu
|
|
15
|
-
def self.open(path)
|
|
16
|
-
Archive.new(path)
|
|
23
|
+
def self.open(path, strict: false)
|
|
24
|
+
Archive.new(path, strict: strict)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.open_live(path = nil, strict: false)
|
|
28
|
+
LiveStore.new(path, strict: strict)
|
|
17
29
|
end
|
|
18
30
|
end
|
data/tasks/abbu.rake
CHANGED
|
@@ -12,7 +12,7 @@ namespace :abbu do
|
|
|
12
12
|
CSV.open('contacts.csv', 'w') do |csv|
|
|
13
13
|
csv << %w[Name Email Phone Company]
|
|
14
14
|
archive.contacts.each do |c|
|
|
15
|
-
csv << [c.full_name, c.emails.first, c.phones.first, c.company]
|
|
15
|
+
csv << [c.full_name, c.emails.first&.dig(:address), c.phones.first&.dig(:number), c.company]
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: abbu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stan Carver II
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sqlite3
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: plist
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.7'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.7'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: guard
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -108,6 +122,20 @@ dependencies:
|
|
|
108
122
|
- - "~>"
|
|
109
123
|
- !ruby/object:Gem::Version
|
|
110
124
|
version: '3.13'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: ruby-lsp
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0.1'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0.1'
|
|
111
139
|
- !ruby/object:Gem::Dependency
|
|
112
140
|
name: rubocop
|
|
113
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -178,9 +206,9 @@ dependencies:
|
|
|
178
206
|
- - "~>"
|
|
179
207
|
- !ruby/object:Gem::Version
|
|
180
208
|
version: '0.22'
|
|
181
|
-
description:
|
|
182
|
-
|
|
183
|
-
|
|
209
|
+
description: Read SQLite and legacy plist .abbu archives or opt into read-only live
|
|
210
|
+
macOS Contacts access. Query contacts, inspect schemas and diagnostics, compare
|
|
211
|
+
identity evidence, extract images, and export CSV, JSON, or vCard.
|
|
184
212
|
email:
|
|
185
213
|
- stan@a1webconsulting.com
|
|
186
214
|
executables:
|
|
@@ -188,17 +216,13 @@ executables:
|
|
|
188
216
|
extensions: []
|
|
189
217
|
extra_rdoc_files: []
|
|
190
218
|
files:
|
|
191
|
-
- CHANGELOG.md
|
|
192
219
|
- LICENSE
|
|
193
220
|
- README.md
|
|
194
221
|
- bin/abbu
|
|
195
|
-
- bin/cleanse
|
|
196
|
-
- bin/console
|
|
197
|
-
- bin/dev
|
|
198
|
-
- bin/lint
|
|
199
|
-
- bin/outdated
|
|
200
|
-
- bin/test
|
|
201
222
|
- docs/ABBU.md
|
|
223
|
+
- docs/CHANGELOG.md
|
|
224
|
+
- docs/CONTRIBUTING.md
|
|
225
|
+
- docs/TODO.md
|
|
202
226
|
- examples/deduplicate_contacts.rb
|
|
203
227
|
- examples/export_to_api.rb
|
|
204
228
|
- examples/export_to_csv.rb
|
|
@@ -209,12 +233,22 @@ files:
|
|
|
209
233
|
- lib/abbu.rb
|
|
210
234
|
- lib/abbu/archive.rb
|
|
211
235
|
- lib/abbu/contact.rb
|
|
236
|
+
- lib/abbu/diagnostic.rb
|
|
212
237
|
- lib/abbu/exporters/csv_exporter.rb
|
|
213
238
|
- lib/abbu/exporters/json_exporter.rb
|
|
214
239
|
- lib/abbu/exporters/vcard_exporter.rb
|
|
240
|
+
- lib/abbu/image_extractor.rb
|
|
241
|
+
- lib/abbu/live_store.rb
|
|
242
|
+
- lib/abbu/parse_error.rb
|
|
215
243
|
- lib/abbu/parsers/plist_parser.rb
|
|
216
244
|
- lib/abbu/parsers/sqlite_parser.rb
|
|
245
|
+
- lib/abbu/query.rb
|
|
246
|
+
- lib/abbu/schema_inspector.rb
|
|
247
|
+
- lib/abbu/utils/contact_identity.rb
|
|
217
248
|
- lib/abbu/utils/deduplicator.rb
|
|
249
|
+
- lib/abbu/utils/image_resolver.rb
|
|
250
|
+
- lib/abbu/utils/label_normalizer.rb
|
|
251
|
+
- lib/abbu/utils/source_descriptor.rb
|
|
218
252
|
- lib/abbu/version.rb
|
|
219
253
|
- tasks/abbu.rake
|
|
220
254
|
homepage: https://github.com/scarver2/abbu
|
|
@@ -224,7 +258,7 @@ metadata:
|
|
|
224
258
|
allowed_push_host: https://rubygems.org
|
|
225
259
|
homepage_uri: https://github.com/scarver2/abbu
|
|
226
260
|
source_code_uri: https://github.com/scarver2/abbu
|
|
227
|
-
changelog_uri: https://github.com/scarver2/abbu/blob/
|
|
261
|
+
changelog_uri: https://github.com/scarver2/abbu/blob/main/docs/CHANGELOG.md
|
|
228
262
|
rubygems_mfa_required: 'true'
|
|
229
263
|
post_install_message:
|
|
230
264
|
rdoc_options: []
|
|
@@ -234,15 +268,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
234
268
|
requirements:
|
|
235
269
|
- - ">="
|
|
236
270
|
- !ruby/object:Gem::Version
|
|
237
|
-
version: '3.
|
|
271
|
+
version: '3.3'
|
|
238
272
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
239
273
|
requirements:
|
|
240
274
|
- - ">="
|
|
241
275
|
- !ruby/object:Gem::Version
|
|
242
276
|
version: '0'
|
|
243
277
|
requirements: []
|
|
244
|
-
rubygems_version: 3.
|
|
278
|
+
rubygems_version: 3.5.22
|
|
245
279
|
signing_key:
|
|
246
280
|
specification_version: 4
|
|
247
|
-
summary: Read
|
|
281
|
+
summary: Read-only Apple Contacts toolkit for archives and live macOS stores.
|
|
248
282
|
test_files: []
|
data/CHANGELOG.md
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
<!-- CHANGELOG.md -->
|
|
2
|
-
|
|
3
|
-
# Changelog
|
|
4
|
-
|
|
5
|
-
All notable changes to `abbu` are documented here.
|
|
6
|
-
|
|
7
|
-
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
8
|
-
Versioning follows [Semantic Versioning](https://semver.org/).
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## [Unreleased]
|
|
13
|
-
|
|
14
|
-
## [0.1.2] - 2026-04-26
|
|
15
|
-
|
|
16
|
-
### Added
|
|
17
|
-
|
|
18
|
-
- Full Apple Contacts schema support: job title, department, maiden name, phonetic names, pronouns, ringtone, texttone
|
|
19
|
-
- Relational table parsing: URLs, notes, related names (family/business), social profiles (Twitter, etc.)
|
|
20
|
-
- Nickname, prefix, and suffix fields with smart `full_name` formatting
|
|
21
|
-
- Hash-based email/phone data preserving custom labels (e.g. "Direct Line", "Work")
|
|
22
|
-
- Address, group, URL, notes, related names, and social profiles in CSV export
|
|
23
|
-
- Comprehensive JSON export with all contact fields
|
|
24
|
-
- vCard 3.0 export with ADR, URL, NICKNAME, TITLE, NOTE, X-SOCIALPROFILE
|
|
25
|
-
- `rubocop-rspec` plugin integration
|
|
26
|
-
- 100% line coverage across all 44 specs
|
|
27
|
-
|
|
28
|
-
### Changed
|
|
29
|
-
|
|
30
|
-
- Refactored `SqliteParser` to use `RECORD_FIELD_MAP` constant for maintainability
|
|
31
|
-
- Refactored `CsvExporter` into `core_fields`/`extended_fields` for cleaner ABC metrics
|
|
32
|
-
- All specs comply with rubocop-rspec conventions
|
|
33
|
-
|
|
34
|
-
## [0.1.1] - 2026-04-23
|
|
35
|
-
|
|
36
|
-
### Fixed
|
|
37
|
-
|
|
38
|
-
- `require 'pathname'` missing in `archive.rb` causing `NameError` in isolation
|
|
39
|
-
- Added regression guard spec for file require isolation
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## [0.1.0] - 2026-04-12
|
|
43
|
-
|
|
44
|
-
### Added
|
|
45
|
-
|
|
46
|
-
- `Abbu.open(path)` entry point returning an `Archive`
|
|
47
|
-
- `Archive#contacts` — reads contacts from SQLite or falls back to plist stub
|
|
48
|
-
- `Archive#sqlite?` — detects modern `.abcddb` bundles
|
|
49
|
-
- `Contact` object with `first_name`, `last_name`, `emails`, `phones`, `company`, `full_name`
|
|
50
|
-
- `Parsers::SqliteParser` — queries `ZABCDRECORD`, `ZABCDEMAILADDRESS`, `ZABCDPHONENUMBER`
|
|
51
|
-
- `Parsers::PlistParser` — stub with warning (legacy `.abcdp` support in v0.2)
|
|
52
|
-
- `Exporters::CsvExporter` — `to_file` and `to_stdout`
|
|
53
|
-
- `Exporters::JsonExporter` — `to_file` and `to_stdout`
|
|
54
|
-
- `Exporters::VcardExporter` — `to_file` and `to_stdout` (vCard 3.0)
|
|
55
|
-
- `Utils::Deduplicator` — groups contacts by first email, returns duplicates hash
|
|
56
|
-
- `bin/abbu` CLI with `--format`, `--output`, `--stats`, `--dedupe`, `--version`
|
|
57
|
-
- Rake tasks: `abbu:export`, `abbu:dedupe`, `abbu:stats`
|
|
58
|
-
- Example scripts: CSV, JSON, vCard, API, CRM sync, stats, dedupe
|
|
59
|
-
- `docs/ABBU.md` — file format reference
|
|
60
|
-
- RSpec test suite with 100% coverage target
|
|
61
|
-
- Guard + RuboCop DX loop
|
|
62
|
-
- GitHub Actions CI (Ruby 3.2 + 3.3)
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
Stan Carver II
|
|
66
|
-
Made in Texas 🤠
|
|
67
|
-
https://stancarver.com
|
data/bin/cleanse
DELETED
data/bin/console
DELETED
data/bin/dev
DELETED
data/bin/lint
DELETED
data/bin/outdated
DELETED