@auxiora/contacts 1.0.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.
package/src/context.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { Contact } from './types.js';
2
+ import type { ContactGraph } from './graph.js';
3
+
4
+ export class ContextRecall {
5
+ private graph: ContactGraph;
6
+
7
+ constructor(graph: ContactGraph) {
8
+ this.graph = graph;
9
+ }
10
+
11
+ getContext(emailOrName: string): { contact: Contact; relationshipSummary: string } | null {
12
+ let contact = this.graph.findByEmail(emailOrName);
13
+ if (!contact) {
14
+ const byName = this.graph.findByName(emailOrName);
15
+ if (byName.length > 0) {
16
+ contact = byName[0];
17
+ }
18
+ }
19
+ if (!contact) return null;
20
+
21
+ const rel = contact.relationship;
22
+ const recencyText = isFinite(rel.recency)
23
+ ? `${Math.round(rel.recency)} days ago`
24
+ : 'never';
25
+ const relationshipSummary = `Strong relationship (score ${rel.strength.toFixed(2)}). Context: ${rel.context}. Last interaction: ${recencyText}.`;
26
+
27
+ return { contact, relationshipSummary };
28
+ }
29
+
30
+ whoIs(query: string): string {
31
+ const result = this.getContext(query);
32
+ if (!result) return `No contact found for "${query}".`;
33
+
34
+ const { contact, relationshipSummary } = result;
35
+ const email = contact.emails[0] ?? 'no email';
36
+ const company = contact.company ? ` at ${contact.company}` : '';
37
+ const title = contact.jobTitle ? ` - ${contact.jobTitle}` : '';
38
+ const notes = contact.notes?.length ? ` Notes: ${contact.notes.join('; ')}` : '';
39
+
40
+ return `${contact.displayName} (${email})${company}${title}. ${relationshipSummary}${notes}`;
41
+ }
42
+
43
+ getUpcomingBirthdays(contacts: Contact[], withinDays = 30): Contact[] {
44
+ const now = new Date();
45
+ const currentYear = now.getFullYear();
46
+
47
+ return contacts.filter(contact => {
48
+ if (!contact.birthday) return false;
49
+
50
+ const bday = new Date(contact.birthday);
51
+ // Set birthday to current year
52
+ const thisYearBday = new Date(currentYear, bday.getMonth(), bday.getDate());
53
+
54
+ // If birthday already passed this year, check next year
55
+ if (thisYearBday.getTime() < now.getTime()) {
56
+ thisYearBday.setFullYear(currentYear + 1);
57
+ }
58
+
59
+ const diffMs = thisYearBday.getTime() - now.getTime();
60
+ const diffDays = diffMs / (24 * 60 * 60 * 1000);
61
+ return diffDays >= 0 && diffDays <= withinDays;
62
+ });
63
+ }
64
+ }
package/src/dedup.ts ADDED
@@ -0,0 +1,61 @@
1
+ import type { Contact } from './types.js';
2
+
3
+ export class ContactDeduplicator {
4
+ similarity(a: Contact, b: Contact): number {
5
+ let score = 0;
6
+
7
+ // Email overlap: any shared email → 0.9 minimum
8
+ const aEmails = new Set(a.emails.map(e => e.toLowerCase()));
9
+ const hasSharedEmail = b.emails.some(e => aEmails.has(e.toLowerCase()));
10
+ if (hasSharedEmail) {
11
+ score = Math.max(score, 0.9);
12
+ }
13
+
14
+ // Name similarity
15
+ const nameScore = this.nameSimilarity(a.displayName, b.displayName);
16
+ score = Math.max(score, nameScore);
17
+
18
+ // Company match adds 0.1
19
+ if (a.company && b.company && a.company.toLowerCase() === b.company.toLowerCase()) {
20
+ score = Math.min(1, score + 0.1);
21
+ }
22
+
23
+ return score;
24
+ }
25
+
26
+ findDuplicates(
27
+ contacts: Contact[],
28
+ threshold = 0.7,
29
+ ): Array<{ contact1: Contact; contact2: Contact; similarity: number }> {
30
+ const results: Array<{ contact1: Contact; contact2: Contact; similarity: number }> = [];
31
+
32
+ for (let i = 0; i < contacts.length; i++) {
33
+ for (let j = i + 1; j < contacts.length; j++) {
34
+ const sim = this.similarity(contacts[i], contacts[j]);
35
+ if (sim >= threshold) {
36
+ results.push({ contact1: contacts[i], contact2: contacts[j], similarity: sim });
37
+ }
38
+ }
39
+ }
40
+
41
+ return results;
42
+ }
43
+
44
+ private nameSimilarity(a: string, b: string): number {
45
+ const la = a.toLowerCase();
46
+ const lb = b.toLowerCase();
47
+
48
+ if (la === lb) return 1.0;
49
+ if (la.includes(lb) || lb.includes(la)) return 0.7;
50
+
51
+ // Character overlap ratio
52
+ const setA = new Set(la);
53
+ const setB = new Set(lb);
54
+ let overlap = 0;
55
+ for (const ch of setA) {
56
+ if (setB.has(ch)) overlap++;
57
+ }
58
+ const union = new Set([...setA, ...setB]).size;
59
+ return union > 0 ? overlap / union : 0;
60
+ }
61
+ }
package/src/graph.ts ADDED
@@ -0,0 +1,115 @@
1
+ import { nanoid } from 'nanoid';
2
+ import type { Contact, ContactGraphConfig } from './types.js';
3
+
4
+ export class ContactGraph {
5
+ private contacts: Map<string, Contact> = new Map();
6
+ private config: ContactGraphConfig;
7
+
8
+ constructor(config?: ContactGraphConfig) {
9
+ this.config = {
10
+ mergeThreshold: config?.mergeThreshold ?? 0.8,
11
+ decayDays: config?.decayDays ?? 90,
12
+ };
13
+ }
14
+
15
+ addContact(input: Omit<Contact, 'id' | 'relationship'>): Contact {
16
+ const contact: Contact = {
17
+ ...input,
18
+ id: nanoid(),
19
+ relationship: {
20
+ strength: 0,
21
+ frequency: 0,
22
+ recency: Infinity,
23
+ context: 'unknown',
24
+ },
25
+ };
26
+ this.contacts.set(contact.id, contact);
27
+ return contact;
28
+ }
29
+
30
+ getById(id: string): Contact | undefined {
31
+ return this.contacts.get(id);
32
+ }
33
+
34
+ findByEmail(email: string): Contact | undefined {
35
+ const lower = email.toLowerCase();
36
+ for (const contact of this.contacts.values()) {
37
+ if (contact.emails.some(e => e.toLowerCase() === lower)) {
38
+ return contact;
39
+ }
40
+ }
41
+ return undefined;
42
+ }
43
+
44
+ findByName(name: string): Contact[] {
45
+ const lower = name.toLowerCase();
46
+ const results: Contact[] = [];
47
+ for (const contact of this.contacts.values()) {
48
+ if (contact.displayName.toLowerCase().includes(lower)) {
49
+ results.push(contact);
50
+ }
51
+ }
52
+ return results;
53
+ }
54
+
55
+ search(query: string): Contact[] {
56
+ const lower = query.toLowerCase();
57
+ const results: Contact[] = [];
58
+ for (const contact of this.contacts.values()) {
59
+ if (
60
+ contact.displayName.toLowerCase().includes(lower) ||
61
+ contact.emails.some(e => e.toLowerCase().includes(lower)) ||
62
+ contact.company?.toLowerCase().includes(lower) ||
63
+ contact.tags?.some(t => t.toLowerCase().includes(lower))
64
+ ) {
65
+ results.push(contact);
66
+ }
67
+ }
68
+ return results;
69
+ }
70
+
71
+ merge(id1: string, id2: string): Contact {
72
+ const c1 = this.contacts.get(id1);
73
+ const c2 = this.contacts.get(id2);
74
+ if (!c1 || !c2) {
75
+ throw new Error(`Contact not found: ${!c1 ? id1 : id2}`);
76
+ }
77
+
78
+ const mergedEmails = [...new Set([...c1.emails, ...c2.emails])];
79
+ const mergedSources = [...c1.sources, ...c2.sources];
80
+ const mergedNotes = [...(c1.notes ?? []), ...(c2.notes ?? [])];
81
+ const mergedTags = [...new Set([...(c1.tags ?? []), ...(c2.tags ?? [])])];
82
+
83
+ const merged: Contact = {
84
+ ...c1,
85
+ emails: mergedEmails,
86
+ sources: mergedSources,
87
+ notes: mergedNotes.length > 0 ? mergedNotes : undefined,
88
+ tags: mergedTags.length > 0 ? mergedTags : undefined,
89
+ };
90
+
91
+ this.contacts.set(id1, merged);
92
+ this.contacts.delete(id2);
93
+ return merged;
94
+ }
95
+
96
+ update(id: string, updates: Partial<Omit<Contact, 'id'>>): Contact | undefined {
97
+ const contact = this.contacts.get(id);
98
+ if (!contact) return undefined;
99
+ const updated = { ...contact, ...updates, id: contact.id };
100
+ this.contacts.set(id, updated);
101
+ return updated;
102
+ }
103
+
104
+ remove(id: string): boolean {
105
+ return this.contacts.delete(id);
106
+ }
107
+
108
+ getAll(): Contact[] {
109
+ return [...this.contacts.values()];
110
+ }
111
+
112
+ count(): number {
113
+ return this.contacts.size;
114
+ }
115
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { ContactGraph } from './graph.js';
2
+ export { RelationshipScorer } from './relationship.js';
3
+ export { ContactDeduplicator } from './dedup.js';
4
+ export { ContextRecall } from './context.js';
5
+ export type {
6
+ Contact,
7
+ ContactSource,
8
+ RelationshipScore,
9
+ Interaction,
10
+ ContactGraphConfig,
11
+ } from './types.js';
@@ -0,0 +1,59 @@
1
+ import type { Contact, Interaction, RelationshipScore } from './types.js';
2
+
3
+ export class RelationshipScorer {
4
+ private decayDays: number;
5
+
6
+ constructor(config?: { decayDays?: number }) {
7
+ this.decayDays = config?.decayDays ?? 90;
8
+ }
9
+
10
+ score(interactions: Interaction[]): RelationshipScore {
11
+ if (interactions.length === 0) {
12
+ return { strength: 0, frequency: 0, recency: Infinity, context: 'unknown' };
13
+ }
14
+
15
+ const now = Date.now();
16
+ const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
17
+
18
+ // Recency: days since most recent interaction
19
+ const mostRecent = Math.max(...interactions.map(i => i.timestamp));
20
+ const recency = (now - mostRecent) / (24 * 60 * 60 * 1000);
21
+
22
+ // Frequency: interactions in last 30 days
23
+ const recentInteractions = interactions.filter(
24
+ i => now - i.timestamp <= thirtyDaysMs,
25
+ );
26
+ const frequency = recentInteractions.length;
27
+
28
+ // Strength: weighted formula, clamped to [0, 1]
29
+ const recencyFactor = Math.max(0, 1 - recency / this.decayDays);
30
+ const frequencyFactor = Math.min(frequency / 10, 1);
31
+ const strength = Math.max(0, Math.min(1, 0.4 * recencyFactor + 0.6 * frequencyFactor));
32
+
33
+ // Context: infer from interaction types
34
+ const context = this.inferContext(interactions);
35
+
36
+ return { strength, frequency, recency, context };
37
+ }
38
+
39
+ updateRelationship(contact: Contact, interactions: Interaction[]): Contact {
40
+ return { ...contact, relationship: this.score(interactions) };
41
+ }
42
+
43
+ private inferContext(interactions: Interaction[]): string {
44
+ const counts: Record<string, number> = {};
45
+ for (const interaction of interactions) {
46
+ counts[interaction.type] = (counts[interaction.type] ?? 0) + 1;
47
+ }
48
+
49
+ const total = interactions.length;
50
+ const emailRatio = (counts['email'] ?? 0) / total;
51
+ const messageRatio = (counts['message'] ?? 0) / total;
52
+ const meetingRatio = (counts['meeting'] ?? 0) / total;
53
+
54
+ if (emailRatio > 0.5) return 'professional';
55
+ if (messageRatio > 0.5) return 'personal';
56
+ if (meetingRatio > 0.5) return 'colleague';
57
+ return 'mixed';
58
+ }
59
+ }
package/src/types.ts ADDED
@@ -0,0 +1,39 @@
1
+ export interface Contact {
2
+ id: string;
3
+ displayName: string;
4
+ emails: string[];
5
+ phones?: string[];
6
+ company?: string;
7
+ jobTitle?: string;
8
+ birthday?: string; // ISO date
9
+ sources: ContactSource[];
10
+ relationship: RelationshipScore;
11
+ lastInteraction?: number; // timestamp
12
+ notes?: string[];
13
+ tags?: string[];
14
+ }
15
+
16
+ export interface ContactSource {
17
+ type: 'email' | 'calendar' | 'social' | 'channel' | 'manual';
18
+ sourceId: string;
19
+ importedAt: number;
20
+ }
21
+
22
+ export interface RelationshipScore {
23
+ strength: number; // 0-1
24
+ frequency: number; // interactions per 30 days
25
+ recency: number; // days since last interaction
26
+ context: string; // e.g. "colleague", "client", "friend"
27
+ }
28
+
29
+ export interface Interaction {
30
+ contactId: string;
31
+ type: 'email' | 'meeting' | 'message' | 'social';
32
+ timestamp: number;
33
+ summary?: string;
34
+ }
35
+
36
+ export interface ContactGraphConfig {
37
+ mergeThreshold?: number; // similarity threshold for auto-merge, default 0.8
38
+ decayDays?: number; // relationship decay period, default 90
39
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ContactGraph } from '../src/graph.js';
3
+ import { ContextRecall } from '../src/context.js';
4
+ import type { Contact } from '../src/types.js';
5
+
6
+ function setupGraph() {
7
+ const graph = new ContactGraph();
8
+ const contact = graph.addContact({
9
+ displayName: 'Alice Smith',
10
+ emails: ['alice@example.com'],
11
+ sources: [{ type: 'manual', sourceId: 'test', importedAt: Date.now() }],
12
+ company: 'Acme Corp',
13
+ jobTitle: 'Engineer',
14
+ notes: ['Met at conference'],
15
+ birthday: getBirthdaySoon(),
16
+ });
17
+ // Manually update relationship for testing
18
+ graph.update(contact.id, {
19
+ relationship: { strength: 0.85, frequency: 5, recency: 3, context: 'colleague' },
20
+ });
21
+ return { graph, contact: graph.getById(contact.id)! };
22
+ }
23
+
24
+ function getBirthdaySoon(): string {
25
+ const d = new Date();
26
+ d.setDate(d.getDate() + 5);
27
+ return `${d.getFullYear() - 30}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
28
+ }
29
+
30
+ describe('ContextRecall', () => {
31
+ it('getContext finds by email', () => {
32
+ const { graph } = setupGraph();
33
+ const recall = new ContextRecall(graph);
34
+ const result = recall.getContext('alice@example.com');
35
+ expect(result).not.toBeNull();
36
+ expect(result!.contact.displayName).toBe('Alice Smith');
37
+ expect(result!.relationshipSummary).toContain('0.85');
38
+ });
39
+
40
+ it('getContext finds by name', () => {
41
+ const { graph } = setupGraph();
42
+ const recall = new ContextRecall(graph);
43
+ const result = recall.getContext('Alice');
44
+ expect(result).not.toBeNull();
45
+ expect(result!.contact.emails).toContain('alice@example.com');
46
+ });
47
+
48
+ it('getContext returns null for unknown', () => {
49
+ const { graph } = setupGraph();
50
+ const recall = new ContextRecall(graph);
51
+ expect(recall.getContext('nobody@nowhere.com')).toBeNull();
52
+ });
53
+
54
+ it('whoIs returns formatted string', () => {
55
+ const { graph } = setupGraph();
56
+ const recall = new ContextRecall(graph);
57
+ const result = recall.whoIs('Alice');
58
+ expect(result).toContain('Alice Smith');
59
+ expect(result).toContain('alice@example.com');
60
+ expect(result).toContain('Acme Corp');
61
+ expect(result).toContain('Engineer');
62
+ expect(result).toContain('Met at conference');
63
+ });
64
+
65
+ it('getUpcomingBirthdays filters correctly', () => {
66
+ const { graph, contact } = setupGraph();
67
+ const recall = new ContextRecall(graph);
68
+ const allContacts = graph.getAll();
69
+
70
+ const upcoming = recall.getUpcomingBirthdays(allContacts, 10);
71
+ expect(upcoming).toHaveLength(1);
72
+ expect(upcoming[0].id).toBe(contact.id);
73
+
74
+ // Birthday 5 days away should not appear in 2-day window
75
+ const none = recall.getUpcomingBirthdays(allContacts, 2);
76
+ expect(none).toHaveLength(0);
77
+ });
78
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ContactDeduplicator } from '../src/dedup.js';
3
+ import type { Contact } from '../src/types.js';
4
+
5
+ function makeContact(overrides: Partial<Contact> = {}): Contact {
6
+ return {
7
+ id: 'test-id',
8
+ displayName: 'Test User',
9
+ emails: ['test@example.com'],
10
+ sources: [{ type: 'manual', sourceId: 'test', importedAt: Date.now() }],
11
+ relationship: { strength: 0, frequency: 0, recency: Infinity, context: 'unknown' },
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ describe('ContactDeduplicator', () => {
17
+ const dedup = new ContactDeduplicator();
18
+
19
+ it('same email produces similarity >= 0.9', () => {
20
+ const a = makeContact({ id: 'a', displayName: 'Alice', emails: ['shared@example.com'] });
21
+ const b = makeContact({ id: 'b', displayName: 'Bob', emails: ['shared@example.com'] });
22
+ expect(dedup.similarity(a, b)).toBeGreaterThanOrEqual(0.9);
23
+ });
24
+
25
+ it('same name produces high similarity', () => {
26
+ const a = makeContact({ id: 'a', displayName: 'Alice Smith', emails: ['a@test.com'] });
27
+ const b = makeContact({ id: 'b', displayName: 'Alice Smith', emails: ['b@test.com'] });
28
+ expect(dedup.similarity(a, b)).toBe(1.0);
29
+ });
30
+
31
+ it('no overlap produces low similarity', () => {
32
+ const a = makeContact({ id: 'a', displayName: 'Alice', emails: ['alice@test.com'] });
33
+ const b = makeContact({ id: 'b', displayName: 'Zyx', emails: ['zyx@other.com'] });
34
+ expect(dedup.similarity(a, b)).toBeLessThan(0.5);
35
+ });
36
+
37
+ it('findDuplicates returns pairs above threshold', () => {
38
+ const contacts = [
39
+ makeContact({ id: 'a', displayName: 'Alice', emails: ['alice@test.com'] }),
40
+ makeContact({ id: 'b', displayName: 'Alice', emails: ['alice@test.com'] }),
41
+ ];
42
+ const pairs = dedup.findDuplicates(contacts, 0.7);
43
+ expect(pairs.length).toBeGreaterThanOrEqual(1);
44
+ expect(pairs[0].similarity).toBeGreaterThanOrEqual(0.7);
45
+ });
46
+
47
+ it('findDuplicates ignores below threshold', () => {
48
+ const contacts = [
49
+ makeContact({ id: 'a', displayName: 'Alice', emails: ['alice@test.com'] }),
50
+ makeContact({ id: 'b', displayName: 'Zyx', emails: ['zyx@other.com'] }),
51
+ ];
52
+ const pairs = dedup.findDuplicates(contacts, 0.9);
53
+ expect(pairs).toHaveLength(0);
54
+ });
55
+
56
+ it('company match adds to score', () => {
57
+ const a = makeContact({
58
+ id: 'a', displayName: 'Alice Smith', emails: ['a@test.com'], company: 'Acme',
59
+ });
60
+ const b = makeContact({
61
+ id: 'b', displayName: 'Al Smith', emails: ['b@test.com'], company: 'Acme',
62
+ });
63
+ const withCompany = dedup.similarity(a, b);
64
+
65
+ const c = makeContact({
66
+ id: 'c', displayName: 'Alice Smith', emails: ['a@test.com'],
67
+ });
68
+ const d = makeContact({
69
+ id: 'd', displayName: 'Al Smith', emails: ['b@test.com'],
70
+ });
71
+ const withoutCompany = dedup.similarity(c, d);
72
+
73
+ expect(withCompany).toBeGreaterThan(withoutCompany);
74
+ });
75
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ContactGraph } from '../src/graph.js';
3
+
4
+ function makeInput(overrides: Partial<Parameters<ContactGraph['addContact']>[0]> = {}) {
5
+ return {
6
+ displayName: 'Alice Smith',
7
+ emails: ['alice@example.com'],
8
+ sources: [{ type: 'manual' as const, sourceId: 'test', importedAt: Date.now() }],
9
+ ...overrides,
10
+ };
11
+ }
12
+
13
+ describe('ContactGraph', () => {
14
+ it('addContact generates ID', () => {
15
+ const graph = new ContactGraph();
16
+ const contact = graph.addContact(makeInput());
17
+ expect(contact.id).toBeDefined();
18
+ expect(typeof contact.id).toBe('string');
19
+ expect(contact.id.length).toBeGreaterThan(0);
20
+ });
21
+
22
+ it('addContact sets default relationship', () => {
23
+ const graph = new ContactGraph();
24
+ const contact = graph.addContact(makeInput());
25
+ expect(contact.relationship).toEqual({
26
+ strength: 0,
27
+ frequency: 0,
28
+ recency: Infinity,
29
+ context: 'unknown',
30
+ });
31
+ });
32
+
33
+ it('findByEmail finds correct contact', () => {
34
+ const graph = new ContactGraph();
35
+ graph.addContact(makeInput());
36
+ const found = graph.findByEmail('alice@example.com');
37
+ expect(found).toBeDefined();
38
+ expect(found!.displayName).toBe('Alice Smith');
39
+ });
40
+
41
+ it('findByEmail returns undefined for unknown', () => {
42
+ const graph = new ContactGraph();
43
+ graph.addContact(makeInput());
44
+ expect(graph.findByEmail('unknown@example.com')).toBeUndefined();
45
+ });
46
+
47
+ it('findByName case-insensitive match', () => {
48
+ const graph = new ContactGraph();
49
+ graph.addContact(makeInput());
50
+ const results = graph.findByName('alice');
51
+ expect(results).toHaveLength(1);
52
+ expect(results[0].displayName).toBe('Alice Smith');
53
+ });
54
+
55
+ it('search matches across fields', () => {
56
+ const graph = new ContactGraph();
57
+ graph.addContact(makeInput({ company: 'Acme Corp', tags: ['vip'] }));
58
+ expect(graph.search('acme')).toHaveLength(1);
59
+ expect(graph.search('vip')).toHaveLength(1);
60
+ expect(graph.search('alice')).toHaveLength(1);
61
+ expect(graph.search('example.com')).toHaveLength(1);
62
+ });
63
+
64
+ it('merge combines emails', () => {
65
+ const graph = new ContactGraph();
66
+ const c1 = graph.addContact(makeInput());
67
+ const c2 = graph.addContact(makeInput({
68
+ displayName: 'Alice S.',
69
+ emails: ['alice2@example.com'],
70
+ }));
71
+ const merged = graph.merge(c1.id, c2.id);
72
+ expect(merged.emails).toContain('alice@example.com');
73
+ expect(merged.emails).toContain('alice2@example.com');
74
+ expect(merged.displayName).toBe('Alice Smith');
75
+ });
76
+
77
+ it('merge removes second contact', () => {
78
+ const graph = new ContactGraph();
79
+ const c1 = graph.addContact(makeInput());
80
+ const c2 = graph.addContact(makeInput({
81
+ displayName: 'Alice S.',
82
+ emails: ['alice2@example.com'],
83
+ }));
84
+ graph.merge(c1.id, c2.id);
85
+ expect(graph.getById(c2.id)).toBeUndefined();
86
+ expect(graph.count()).toBe(1);
87
+ });
88
+
89
+ it('remove deletes contact', () => {
90
+ const graph = new ContactGraph();
91
+ const c = graph.addContact(makeInput());
92
+ expect(graph.remove(c.id)).toBe(true);
93
+ expect(graph.getById(c.id)).toBeUndefined();
94
+ });
95
+
96
+ it('count returns correct number', () => {
97
+ const graph = new ContactGraph();
98
+ expect(graph.count()).toBe(0);
99
+ graph.addContact(makeInput());
100
+ expect(graph.count()).toBe(1);
101
+ graph.addContact(makeInput({ displayName: 'Bob', emails: ['bob@example.com'] }));
102
+ expect(graph.count()).toBe(2);
103
+ });
104
+ });