@ghostpaw/affinity 0.5.0 → 0.6.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/README.md +184 -9
- package/SKILL.md +43 -386
- package/dist/cli.js +2797 -894
- package/dist/index.cjs +1922 -613
- package/dist/index.d.cts +333 -55
- package/dist/index.d.ts +333 -55
- package/dist/index.js +1922 -613
- package/dist/mcp-server.js +2131 -819
- package/dist/mcp.cjs +1912 -707
- package/dist/mcp.d.cts +3 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +1913 -708
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +43 -386
- package/skills/affinity/references/identify-and-locate-contacts.md +9 -0
- package/skills/affinity/references/import-history-without-faking-evidence.md +25 -0
- package/skills/affinity/references/manage-promises-and-agreements.md +8 -0
- package/skills/affinity/references/manage-recurring-dates-and-reminders.md +8 -0
- package/skills/affinity/references/record-direct-evidence-well.md +18 -0
- package/skills/affinity/references/record-observations-and-referrals.md +10 -0
- package/skills/affinity/references/review-radar-progression-and-graph.md +13 -0
- package/skills/affinity/references/tools.md +489 -0
package/dist/index.js
CHANGED
|
@@ -4,13 +4,130 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
+
// src/events/init_events_tables.ts
|
|
8
|
+
function initEventsTables(db) {
|
|
9
|
+
db.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
type TEXT NOT NULL,
|
|
13
|
+
occurred_at INTEGER NOT NULL,
|
|
14
|
+
summary TEXT NOT NULL,
|
|
15
|
+
significance REAL NOT NULL DEFAULT 0,
|
|
16
|
+
moment_kind TEXT,
|
|
17
|
+
recurrence_kind TEXT,
|
|
18
|
+
anchor_month INTEGER,
|
|
19
|
+
anchor_day INTEGER,
|
|
20
|
+
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
21
|
+
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
22
|
+
created_at INTEGER NOT NULL,
|
|
23
|
+
updated_at INTEGER NOT NULL,
|
|
24
|
+
deleted_at INTEGER,
|
|
25
|
+
CHECK (length(trim(summary)) > 0),
|
|
26
|
+
CHECK (
|
|
27
|
+
type IN (
|
|
28
|
+
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
29
|
+
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
30
|
+
)
|
|
31
|
+
),
|
|
32
|
+
CHECK (
|
|
33
|
+
(type = 'date_anchor' AND (
|
|
34
|
+
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
35
|
+
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
36
|
+
))
|
|
37
|
+
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
38
|
+
),
|
|
39
|
+
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
40
|
+
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
41
|
+
)),
|
|
42
|
+
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
43
|
+
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
44
|
+
)),
|
|
45
|
+
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
46
|
+
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
db.exec(
|
|
50
|
+
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
51
|
+
);
|
|
52
|
+
db.exec(
|
|
53
|
+
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
54
|
+
);
|
|
55
|
+
const columns = db.prepare("PRAGMA table_info(events)").all();
|
|
56
|
+
if (!columns.some((c) => c.name === "declared_moment_kind"))
|
|
57
|
+
db.exec("ALTER TABLE events ADD COLUMN declared_moment_kind TEXT");
|
|
58
|
+
if (!columns.some((c) => c.name === "affects_bond"))
|
|
59
|
+
db.exec("ALTER TABLE events ADD COLUMN affects_bond INTEGER");
|
|
60
|
+
if (!columns.some((c) => c.name === "content"))
|
|
61
|
+
db.exec("ALTER TABLE events ADD COLUMN content TEXT");
|
|
62
|
+
if (!columns.some((c) => c.name === "external_source"))
|
|
63
|
+
db.exec("ALTER TABLE events ADD COLUMN external_source TEXT");
|
|
64
|
+
if (!columns.some((c) => c.name === "external_id"))
|
|
65
|
+
db.exec("ALTER TABLE events ADD COLUMN external_id TEXT");
|
|
66
|
+
if (!columns.some((c) => c.name === "channel"))
|
|
67
|
+
db.exec("ALTER TABLE events ADD COLUMN channel TEXT");
|
|
68
|
+
if (!columns.some((c) => c.name === "parent_source"))
|
|
69
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_source TEXT");
|
|
70
|
+
if (!columns.some((c) => c.name === "parent_ref_id"))
|
|
71
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_ref_id TEXT");
|
|
72
|
+
if (!columns.some((c) => c.name === "parent_event_id"))
|
|
73
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_event_id INTEGER");
|
|
74
|
+
if (!columns.some((c) => c.name === "thread_source"))
|
|
75
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_source TEXT");
|
|
76
|
+
if (!columns.some((c) => c.name === "thread_id"))
|
|
77
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_id TEXT");
|
|
78
|
+
if (!columns.some((c) => c.name === "provenance_json"))
|
|
79
|
+
db.exec("ALTER TABLE events ADD COLUMN provenance_json TEXT");
|
|
80
|
+
db.exec(
|
|
81
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external_ref ON events(external_source, external_id)"
|
|
82
|
+
);
|
|
83
|
+
if (!columns.some((column) => column.name === "occurred_on"))
|
|
84
|
+
db.exec("ALTER TABLE events ADD COLUMN occurred_on TEXT");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/with_transaction.ts
|
|
88
|
+
var transactionDepth = /* @__PURE__ */ new WeakMap();
|
|
89
|
+
function withTransaction(db, fn) {
|
|
90
|
+
const depth = transactionDepth.get(db) ?? 0;
|
|
91
|
+
const savepoint = `affinity_${String(depth)}`;
|
|
92
|
+
if (depth === 0) {
|
|
93
|
+
db.exec("BEGIN IMMEDIATE");
|
|
94
|
+
} else {
|
|
95
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
96
|
+
}
|
|
97
|
+
transactionDepth.set(db, depth + 1);
|
|
98
|
+
try {
|
|
99
|
+
const result = fn();
|
|
100
|
+
if (depth === 0) {
|
|
101
|
+
db.exec("COMMIT");
|
|
102
|
+
} else {
|
|
103
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
104
|
+
}
|
|
105
|
+
transactionDepth.set(db, depth);
|
|
106
|
+
return result;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
try {
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
db.exec("ROLLBACK");
|
|
111
|
+
} else {
|
|
112
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
113
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
transactionDepth.set(db, depth);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
7
122
|
// src/attributes/init_attributes_tables.ts
|
|
8
123
|
function initAttributesTables(db) {
|
|
124
|
+
initEventsTables(db);
|
|
9
125
|
db.exec(`
|
|
10
126
|
CREATE TABLE IF NOT EXISTS attributes (
|
|
11
127
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
128
|
contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
13
129
|
link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
130
|
+
event_id INTEGER REFERENCES events (id) ON DELETE CASCADE,
|
|
14
131
|
name TEXT NOT NULL,
|
|
15
132
|
value TEXT,
|
|
16
133
|
created_at INTEGER NOT NULL,
|
|
@@ -18,15 +135,35 @@ function initAttributesTables(db) {
|
|
|
18
135
|
deleted_at INTEGER,
|
|
19
136
|
CHECK (length(trim(name)) > 0),
|
|
20
137
|
CHECK (
|
|
21
|
-
(contact_id IS NOT NULL
|
|
22
|
-
OR
|
|
23
|
-
(contact_id IS NULL AND link_id IS NOT NULL)
|
|
138
|
+
(contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1
|
|
24
139
|
)
|
|
25
140
|
)
|
|
26
141
|
`);
|
|
142
|
+
const columns = db.prepare("PRAGMA table_info(attributes)").all();
|
|
143
|
+
if (!columns.some((c) => c.name === "event_id")) {
|
|
144
|
+
withTransaction(
|
|
145
|
+
db,
|
|
146
|
+
() => db.exec(`CREATE TABLE attributes_upgrade (
|
|
147
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
148
|
+
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
|
|
149
|
+
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
|
|
150
|
+
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
|
|
151
|
+
name TEXT NOT NULL CHECK(length(trim(name)) > 0), value TEXT,
|
|
152
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
153
|
+
CHECK ((contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1)
|
|
154
|
+
);
|
|
155
|
+
INSERT INTO attributes_upgrade (id,contact_id,link_id,name,value,created_at,updated_at,deleted_at)
|
|
156
|
+
SELECT id,contact_id,link_id,name,value,created_at,updated_at,deleted_at FROM attributes;
|
|
157
|
+
DROP TABLE attributes;
|
|
158
|
+
ALTER TABLE attributes_upgrade RENAME TO attributes;`)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
27
161
|
db.exec(
|
|
28
162
|
"CREATE INDEX IF NOT EXISTS idx_attributes_contact ON attributes(contact_id) WHERE deleted_at IS NULL"
|
|
29
163
|
);
|
|
164
|
+
db.exec(
|
|
165
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_attributes_event_name_live ON attributes(event_id,name) WHERE deleted_at IS NULL AND event_id IS NOT NULL"
|
|
166
|
+
);
|
|
30
167
|
db.exec(
|
|
31
168
|
"CREATE INDEX IF NOT EXISTS idx_attributes_link ON attributes(link_id) WHERE deleted_at IS NULL"
|
|
32
169
|
);
|
|
@@ -139,56 +276,6 @@ function initEventParticipantsTables(db) {
|
|
|
139
276
|
);
|
|
140
277
|
}
|
|
141
278
|
|
|
142
|
-
// src/events/init_events_tables.ts
|
|
143
|
-
function initEventsTables(db) {
|
|
144
|
-
db.exec(`
|
|
145
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
146
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
147
|
-
type TEXT NOT NULL,
|
|
148
|
-
occurred_at INTEGER NOT NULL,
|
|
149
|
-
summary TEXT NOT NULL,
|
|
150
|
-
significance REAL NOT NULL DEFAULT 0,
|
|
151
|
-
moment_kind TEXT,
|
|
152
|
-
recurrence_kind TEXT,
|
|
153
|
-
anchor_month INTEGER,
|
|
154
|
-
anchor_day INTEGER,
|
|
155
|
-
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
156
|
-
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
157
|
-
created_at INTEGER NOT NULL,
|
|
158
|
-
updated_at INTEGER NOT NULL,
|
|
159
|
-
deleted_at INTEGER,
|
|
160
|
-
CHECK (length(trim(summary)) > 0),
|
|
161
|
-
CHECK (
|
|
162
|
-
type IN (
|
|
163
|
-
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
164
|
-
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
165
|
-
)
|
|
166
|
-
),
|
|
167
|
-
CHECK (
|
|
168
|
-
(type = 'date_anchor' AND (
|
|
169
|
-
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
170
|
-
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
171
|
-
))
|
|
172
|
-
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
173
|
-
),
|
|
174
|
-
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
175
|
-
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
176
|
-
)),
|
|
177
|
-
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
178
|
-
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
179
|
-
)),
|
|
180
|
-
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
181
|
-
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
182
|
-
)
|
|
183
|
-
`);
|
|
184
|
-
db.exec(
|
|
185
|
-
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
186
|
-
);
|
|
187
|
-
db.exec(
|
|
188
|
-
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
279
|
// src/events/init_open_commitments_tables.ts
|
|
193
280
|
function initOpenCommitmentsTables(db) {
|
|
194
281
|
db.exec(`
|
|
@@ -208,6 +295,9 @@ function initOpenCommitmentsTables(db) {
|
|
|
208
295
|
)
|
|
209
296
|
)
|
|
210
297
|
`);
|
|
298
|
+
const columns = db.prepare("PRAGMA table_info(open_commitments)").all();
|
|
299
|
+
if (!columns.some((column) => column.name === "due_on"))
|
|
300
|
+
db.exec("ALTER TABLE open_commitments ADD COLUMN due_on TEXT");
|
|
211
301
|
}
|
|
212
302
|
|
|
213
303
|
// src/identities/init_identities_tables.ts
|
|
@@ -272,6 +362,21 @@ function initLinkEventEffectsTables(db) {
|
|
|
272
362
|
db.exec(
|
|
273
363
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
274
364
|
);
|
|
365
|
+
for (const name of ["participant_json", "provenance_json"])
|
|
366
|
+
if (!columns.some((c) => c.name === name))
|
|
367
|
+
db.exec(`ALTER TABLE link_event_effects ADD COLUMN ${name} TEXT`);
|
|
368
|
+
db.exec(
|
|
369
|
+
"CREATE TABLE IF NOT EXISTS event_link_baselines (link_id INTEGER PRIMARY KEY REFERENCES links(id) ON DELETE CASCADE, state_json TEXT NOT NULL)"
|
|
370
|
+
);
|
|
371
|
+
db.exec(
|
|
372
|
+
"CREATE TABLE IF NOT EXISTS event_link_state_changes (id INTEGER PRIMARY KEY AUTOINCREMENT, link_id INTEGER NOT NULL REFERENCES links(id) ON DELETE CASCADE, after_event_id INTEGER NOT NULL, state TEXT NOT NULL)"
|
|
373
|
+
);
|
|
374
|
+
db.exec(`INSERT OR IGNORE INTO event_link_baselines(link_id,state_json)
|
|
375
|
+
SELECT l.id,json_object('rank',l.rank-COALESCE(SUM(e.rank_delta),0),'affinity',l.affinity-COALESCE(SUM(e.affinity_delta),0),'trust',l.trust-COALESCE(SUM(e.trust_delta),0),'state','active','cadence_days',NULL)
|
|
376
|
+
FROM links l JOIN link_event_effects e ON e.link_id=l.id GROUP BY l.id`);
|
|
377
|
+
db.exec(
|
|
378
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id IN (SELECT id FROM events WHERE type IN ('promise', 'agreement'))"
|
|
379
|
+
);
|
|
275
380
|
db.exec(
|
|
276
381
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_moment ON link_event_effects(moment_kind, occurred_at DESC, impact_score DESC) WHERE moment_kind IS NOT NULL"
|
|
277
382
|
);
|
|
@@ -455,6 +560,7 @@ var AffinityError = class extends Error {
|
|
|
455
560
|
|
|
456
561
|
// src/lib/errors/affinity_conflict_error.ts
|
|
457
562
|
var AffinityConflictError = class extends AffinityError {
|
|
563
|
+
existingContactId;
|
|
458
564
|
code = "CONFLICT";
|
|
459
565
|
};
|
|
460
566
|
|
|
@@ -524,6 +630,9 @@ __export(read_exports, {
|
|
|
524
630
|
getAffinityChart: () => getAffinityChart,
|
|
525
631
|
getContactJournal: () => getContactJournal,
|
|
526
632
|
getContactProfile: () => getContactProfile,
|
|
633
|
+
getEventByExternalRef: () => getEventByExternalRef,
|
|
634
|
+
getEventById: () => getEventById,
|
|
635
|
+
getEventThread: () => getEventThread,
|
|
527
636
|
getLinkDetail: () => getLinkDetail,
|
|
528
637
|
getLinkTimeline: () => getLinkTimeline,
|
|
529
638
|
getLinksBetween: () => getLinksBetween,
|
|
@@ -539,7 +648,8 @@ __export(read_exports, {
|
|
|
539
648
|
listProgressionReadiness: () => listProgressionReadiness,
|
|
540
649
|
listRadar: () => listRadar,
|
|
541
650
|
listUpcomingDates: () => listUpcomingDates,
|
|
542
|
-
searchContacts: () => searchContacts
|
|
651
|
+
searchContacts: () => searchContacts,
|
|
652
|
+
searchEvents: () => searchEvents
|
|
543
653
|
});
|
|
544
654
|
|
|
545
655
|
// src/contacts/mappers.ts
|
|
@@ -727,6 +837,104 @@ function refreshContactRollup(db, contactId, now) {
|
|
|
727
837
|
).run(contactId, warnings.length, JSON.stringify(warnings), "{}", now);
|
|
728
838
|
}
|
|
729
839
|
|
|
840
|
+
// src/identities/mappers.ts
|
|
841
|
+
function mapIdentityRowToIdentityRecord(row) {
|
|
842
|
+
return {
|
|
843
|
+
id: row.id,
|
|
844
|
+
contactId: row.contact_id,
|
|
845
|
+
type: row.type,
|
|
846
|
+
value: row.value,
|
|
847
|
+
label: row.label,
|
|
848
|
+
verified: row.verified === 1,
|
|
849
|
+
verifiedAt: row.verified_at
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/identities/normalize.ts
|
|
854
|
+
var handleIdentityTypes = /* @__PURE__ */ new Set([
|
|
855
|
+
"handle",
|
|
856
|
+
"bluesky",
|
|
857
|
+
"x",
|
|
858
|
+
"twitter",
|
|
859
|
+
"mastodon",
|
|
860
|
+
"github",
|
|
861
|
+
"instagram",
|
|
862
|
+
"telegram",
|
|
863
|
+
"linkedin",
|
|
864
|
+
"facebook",
|
|
865
|
+
"youtube",
|
|
866
|
+
"tiktok",
|
|
867
|
+
"discord",
|
|
868
|
+
"slack"
|
|
869
|
+
]);
|
|
870
|
+
function normalizeIdentityValue(type, value) {
|
|
871
|
+
const v = value.trim();
|
|
872
|
+
return handleIdentityTypes.has(type.toLowerCase()) ? v.replace(/^@/, "").toLowerCase() : v;
|
|
873
|
+
}
|
|
874
|
+
function normalizeIdentityKey(type, value) {
|
|
875
|
+
let t = type.trim().toLowerCase();
|
|
876
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(t)) t = "did";
|
|
877
|
+
const v = normalizeIdentityValue(t, value).toLowerCase();
|
|
878
|
+
if (t.length === 0 || v.length === 0) {
|
|
879
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
880
|
+
}
|
|
881
|
+
return `${t}:${v}`;
|
|
882
|
+
}
|
|
883
|
+
function legacyIdentityKey(key) {
|
|
884
|
+
const colon = key.indexOf(":");
|
|
885
|
+
return handleIdentityTypes.has(key.slice(0, colon)) ? `${key.slice(0, colon + 1)}@${key.slice(colon + 1)}` : key;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/identities/queries.ts
|
|
889
|
+
function getIdentityRowById(db, id) {
|
|
890
|
+
const row = db.prepare(
|
|
891
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
892
|
+
created_at, updated_at, removed_at
|
|
893
|
+
FROM identities
|
|
894
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
895
|
+
).get(id);
|
|
896
|
+
if (row === void 0) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return row;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/identities/receipts.ts
|
|
903
|
+
function buildIdentityMutationReceipt(primary, partial) {
|
|
904
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
905
|
+
return {
|
|
906
|
+
primary,
|
|
907
|
+
created: normalized.created,
|
|
908
|
+
updated: normalized.updated,
|
|
909
|
+
archived: normalized.archived,
|
|
910
|
+
removed: normalized.removed,
|
|
911
|
+
affectedLinks: normalized.affectedLinks,
|
|
912
|
+
derivedEffects: normalized.derivedEffects
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/identities/validators.ts
|
|
917
|
+
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
918
|
+
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
919
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
920
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey) : db.prepare(
|
|
921
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL AND id != ?"
|
|
922
|
+
).get(
|
|
923
|
+
normalizedKey,
|
|
924
|
+
legacyIdentityKey(normalizedKey),
|
|
925
|
+
normalizedKey,
|
|
926
|
+
excludeIdentityId
|
|
927
|
+
);
|
|
928
|
+
if (row !== void 0) {
|
|
929
|
+
const owner = db.prepare(
|
|
930
|
+
"SELECT c.id, c.name FROM contacts c JOIN identities i ON i.contact_id = c.id WHERE (i.normalized_key IN (?, ?) OR (i.type='custom:atproto-did' AND ? = 'did:' || lower(i.value))) AND i.removed_at IS NULL"
|
|
931
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey);
|
|
932
|
+
throw new AffinityConflictError(
|
|
933
|
+
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
730
938
|
// src/resolve_now.ts
|
|
731
939
|
function resolveNow(now) {
|
|
732
940
|
if (now !== void 0) {
|
|
@@ -740,39 +948,140 @@ function resolveNow(now) {
|
|
|
740
948
|
return Date.now();
|
|
741
949
|
}
|
|
742
950
|
|
|
743
|
-
// src/
|
|
744
|
-
var
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
951
|
+
// src/identities/validate_identity_input.ts
|
|
952
|
+
var identityTypes = [
|
|
953
|
+
"did",
|
|
954
|
+
"atproto_did",
|
|
955
|
+
"email",
|
|
956
|
+
"phone",
|
|
957
|
+
"url",
|
|
958
|
+
"website",
|
|
959
|
+
"handle",
|
|
960
|
+
"x",
|
|
961
|
+
"twitter",
|
|
962
|
+
"linkedin",
|
|
963
|
+
"github",
|
|
964
|
+
"mastodon",
|
|
965
|
+
"bluesky",
|
|
966
|
+
"telegram",
|
|
967
|
+
"whatsapp",
|
|
968
|
+
"signal",
|
|
969
|
+
"matrix",
|
|
970
|
+
"discord",
|
|
971
|
+
"slack",
|
|
972
|
+
"instagram",
|
|
973
|
+
"facebook",
|
|
974
|
+
"youtube",
|
|
975
|
+
"tiktok",
|
|
976
|
+
"address",
|
|
977
|
+
"other",
|
|
978
|
+
"account_id",
|
|
979
|
+
"alias"
|
|
980
|
+
];
|
|
981
|
+
function validateIdentityInput(type, rawValue) {
|
|
982
|
+
const kind = type.toLowerCase();
|
|
983
|
+
const value = normalizeIdentityValue(kind, rawValue);
|
|
984
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(kind) && !/^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/.test(value))
|
|
985
|
+
throw new AffinityValidationError(
|
|
986
|
+
"did value must match did:<method>:<identifier> (for example did:plc:abc123)"
|
|
987
|
+
);
|
|
988
|
+
if (handleIdentityTypes.has(kind)) {
|
|
989
|
+
const pattern = kind === "bluesky" ? /^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/ : ["x", "twitter"].includes(kind) ? /^[a-z0-9_]{1,15}$/ : kind === "mastodon" ? /^[a-z0-9_.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+$/ : /^[a-z0-9_.-]+$/;
|
|
990
|
+
if (!pattern.test(value))
|
|
991
|
+
throw new AffinityValidationError(
|
|
992
|
+
`invalid ${kind === "handle" ? "" : `${kind} `}handle; use a handle without spaces or a URI${kind === "mastodon" ? ": user@host.tld" : kind === "bluesky" ? ": name.bsky.social" : " (leading @ is optional)"}`
|
|
993
|
+
);
|
|
752
994
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
995
|
+
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
996
|
+
throw new AffinityValidationError(
|
|
997
|
+
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
998
|
+
);
|
|
999
|
+
if (!value.trim())
|
|
1000
|
+
throw new AffinityValidationError("identity value must be non-empty");
|
|
1001
|
+
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
1002
|
+
throw new AffinityValidationError(
|
|
1003
|
+
"email value must match local@domain.tld"
|
|
1004
|
+
);
|
|
1005
|
+
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
1006
|
+
throw new AffinityValidationError(
|
|
1007
|
+
"phone value must contain at least 5 digits"
|
|
1008
|
+
);
|
|
1009
|
+
if (kind === "url" || kind === "website") {
|
|
764
1010
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
769
|
-
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
770
|
-
}
|
|
1011
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
1012
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
1013
|
+
throw new Error();
|
|
771
1014
|
} catch {
|
|
1015
|
+
throw new AffinityValidationError(
|
|
1016
|
+
"url/website value must be an http/https URL or bare domain"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/identities/add_identity.ts
|
|
1023
|
+
function addIdentity(db, contactId, input) {
|
|
1024
|
+
return withTransaction(db, () => {
|
|
1025
|
+
const contact = getContactRowById(db, contactId);
|
|
1026
|
+
if (!contact) {
|
|
1027
|
+
throw new AffinityNotFoundError("contact not found");
|
|
1028
|
+
}
|
|
1029
|
+
if (contact.lifecycle_state === "merged") {
|
|
1030
|
+
throw new AffinityStateError("merged contact is read-only");
|
|
1031
|
+
}
|
|
1032
|
+
const type = input.type.trim();
|
|
1033
|
+
const value = normalizeIdentityValue(type, input.value);
|
|
1034
|
+
if (type.length === 0 || value.length === 0) {
|
|
1035
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
1036
|
+
}
|
|
1037
|
+
validateIdentityInput(type, value);
|
|
1038
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
1039
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
1040
|
+
const nk = normalizeIdentityKey(type, value);
|
|
1041
|
+
if (input.ifExists === "return") {
|
|
1042
|
+
const existing = db.prepare(
|
|
1043
|
+
"SELECT id FROM identities WHERE contact_id=? AND (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
1044
|
+
).get(contactId, nk, legacyIdentityKey(nk), nk);
|
|
1045
|
+
if (existing) {
|
|
1046
|
+
const row2 = getIdentityRowById(db, Number(existing.id));
|
|
1047
|
+
if (row2)
|
|
1048
|
+
return buildIdentityMutationReceipt(
|
|
1049
|
+
mapIdentityRowToIdentityRecord(row2),
|
|
1050
|
+
{}
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
assertNoIdentityCollision(db, nk);
|
|
1055
|
+
const now = resolveNow(input.now);
|
|
1056
|
+
const verified = input.verified ? 1 : 0;
|
|
1057
|
+
const verifiedAt = verified === 1 ? now : null;
|
|
1058
|
+
const result = db.prepare(
|
|
1059
|
+
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
1060
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1061
|
+
).run(
|
|
1062
|
+
contactId,
|
|
1063
|
+
type,
|
|
1064
|
+
value,
|
|
1065
|
+
input.label ?? null,
|
|
1066
|
+
nk,
|
|
1067
|
+
verified,
|
|
1068
|
+
verifiedAt,
|
|
1069
|
+
now,
|
|
1070
|
+
now
|
|
1071
|
+
);
|
|
1072
|
+
const id = Number(result.lastInsertRowid);
|
|
1073
|
+
const row = getIdentityRowById(db, id);
|
|
1074
|
+
if (!row) {
|
|
1075
|
+
throw new AffinityInvariantError("inserted identity not found");
|
|
772
1076
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1077
|
+
refreshContactRollup(db, contactId, now);
|
|
1078
|
+
db.prepare(
|
|
1079
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
1080
|
+
).run(contactId, contactId);
|
|
1081
|
+
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
1082
|
+
created: [{ kind: "identity", id }]
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
776
1085
|
}
|
|
777
1086
|
|
|
778
1087
|
// src/contacts/create_contact.ts
|
|
@@ -799,6 +1108,34 @@ function createContact(db, input) {
|
|
|
799
1108
|
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
800
1109
|
);
|
|
801
1110
|
}
|
|
1111
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
1112
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
1113
|
+
const matches = (input.identities ?? []).flatMap((identity) => {
|
|
1114
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
1115
|
+
const key = normalizeIdentityKey(identity.type, identity.value);
|
|
1116
|
+
const row2 = db.prepare(
|
|
1117
|
+
`SELECT c.id, c.name FROM identities i JOIN contacts c ON c.id=i.contact_id WHERE i.removed_at IS NULL AND c.deleted_at IS NULL AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))`
|
|
1118
|
+
).get(key, legacyIdentityKey(key), key);
|
|
1119
|
+
return row2 ? [{ ...row2, identity }] : [];
|
|
1120
|
+
});
|
|
1121
|
+
const match = matches[0];
|
|
1122
|
+
if (match) {
|
|
1123
|
+
if (input.ifExists === "return") {
|
|
1124
|
+
const existing = getContactRowById(db, match.id);
|
|
1125
|
+
if (!existing)
|
|
1126
|
+
throw new AffinityInvariantError("identity owner not found");
|
|
1127
|
+
const primary2 = mapContactRowToContactListItem(existing);
|
|
1128
|
+
return Object.assign(buildContactMutationReceipt(primary2, {}), {
|
|
1129
|
+
outcome: "no_op",
|
|
1130
|
+
matchedIdentities: matches.map((m) => m.identity)
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const error = new AffinityConflictError(
|
|
1134
|
+
`identity ${normalizeIdentityKey(match.identity.type, match.identity.value).replace(":", " ")} already belongs to #${match.id} ${match.name}`
|
|
1135
|
+
);
|
|
1136
|
+
error.existingContactId = match.id;
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
802
1139
|
const now = resolveNow(input.now);
|
|
803
1140
|
if (input.bootstrapOwner) {
|
|
804
1141
|
const existing = db.prepare(
|
|
@@ -822,9 +1159,12 @@ function createContact(db, input) {
|
|
|
822
1159
|
}
|
|
823
1160
|
refreshContactRollup(db, id, now);
|
|
824
1161
|
const primary = mapContactRowToContactListItem(row);
|
|
825
|
-
|
|
1162
|
+
const receipt = buildContactMutationReceipt(primary, {
|
|
826
1163
|
created: [{ kind: "contact", id }]
|
|
827
1164
|
});
|
|
1165
|
+
for (const identity of input.identities ?? [])
|
|
1166
|
+
receipt.created.push(...addIdentity(db, id, identity).created);
|
|
1167
|
+
return receipt;
|
|
828
1168
|
});
|
|
829
1169
|
}
|
|
830
1170
|
|
|
@@ -847,6 +1187,100 @@ function resolveListLimitOffset(options) {
|
|
|
847
1187
|
return { limit, offset };
|
|
848
1188
|
}
|
|
849
1189
|
|
|
1190
|
+
// src/events/receipts.ts
|
|
1191
|
+
function buildEventMutationReceipt(primary, partial) {
|
|
1192
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
1193
|
+
return {
|
|
1194
|
+
primary,
|
|
1195
|
+
...partial?.warnings ? { warnings: partial.warnings } : {},
|
|
1196
|
+
created: normalized.created,
|
|
1197
|
+
updated: normalized.updated,
|
|
1198
|
+
archived: normalized.archived,
|
|
1199
|
+
removed: normalized.removed,
|
|
1200
|
+
affectedLinks: normalized.affectedLinks,
|
|
1201
|
+
derivedEffects: normalized.derivedEffects
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/events/event_metadata.ts
|
|
1206
|
+
function normalizeExternalRef(ref) {
|
|
1207
|
+
if (!ref || typeof ref.source !== "string" || typeof ref.id !== "string" || !ref.source.trim() || !ref.id.trim())
|
|
1208
|
+
throw new AffinityValidationError(
|
|
1209
|
+
"external reference source and id must be non-empty strings"
|
|
1210
|
+
);
|
|
1211
|
+
return { source: ref.source.trim().toLowerCase(), id: ref.id.trim() };
|
|
1212
|
+
}
|
|
1213
|
+
function deriveEventSummary(content) {
|
|
1214
|
+
const line = content.split(/\r?\n/).find((line2) => line2.trim())?.replace(/\s+/g, " ").trim() ?? "(no text)";
|
|
1215
|
+
return line.length > 140 ? `${line.slice(0, 139)}\u2026` : line;
|
|
1216
|
+
}
|
|
1217
|
+
function validateEventMetadata(input) {
|
|
1218
|
+
if (input.affectsBond !== void 0 && typeof input.affectsBond !== "boolean")
|
|
1219
|
+
throw new AffinityValidationError("affectsBond must be boolean");
|
|
1220
|
+
if (input.content !== void 0 && (typeof input.content !== "string" || Buffer.byteLength(input.content, "utf8") > 1048576))
|
|
1221
|
+
throw new AffinityValidationError(
|
|
1222
|
+
"content must be a string of at most 1 MB (1048576 UTF-8 bytes)"
|
|
1223
|
+
);
|
|
1224
|
+
for (const key of ["externalRef", "parentRef", "threadRef"])
|
|
1225
|
+
if (input[key] !== void 0) input[key] = normalizeExternalRef(input[key]);
|
|
1226
|
+
if (input.channel !== void 0) {
|
|
1227
|
+
if (typeof input.channel !== "string" || !input.channel.trim() || input.channel.trim().length > 40)
|
|
1228
|
+
throw new AffinityValidationError(
|
|
1229
|
+
"channel must be non-empty and at most 40 characters"
|
|
1230
|
+
);
|
|
1231
|
+
input.channel = input.channel.trim().toLowerCase();
|
|
1232
|
+
}
|
|
1233
|
+
if (input.parentEventId !== void 0 && (!Number.isSafeInteger(input.parentEventId) || input.parentEventId <= 0))
|
|
1234
|
+
throw new AffinityValidationError(
|
|
1235
|
+
"parentEventId must be a positive integer"
|
|
1236
|
+
);
|
|
1237
|
+
const moments = [
|
|
1238
|
+
"breakthrough",
|
|
1239
|
+
"rupture",
|
|
1240
|
+
"reconciliation",
|
|
1241
|
+
"milestone",
|
|
1242
|
+
"turning_point"
|
|
1243
|
+
];
|
|
1244
|
+
if (input.momentKind !== void 0 && !moments.includes(input.momentKind))
|
|
1245
|
+
throw new AffinityValidationError(
|
|
1246
|
+
`momentKind must be one of: ${moments.join(", ")}`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
function getEventById(db, eventId) {
|
|
1250
|
+
return db.prepare("SELECT 1 FROM events WHERE id=? AND deleted_at IS NULL").get(eventId) ? loadEventRecord(db, eventId) : null;
|
|
1251
|
+
}
|
|
1252
|
+
function getEventByExternalRef(db, source, id) {
|
|
1253
|
+
const ref = normalizeExternalRef({ source, id });
|
|
1254
|
+
const row = db.prepare(
|
|
1255
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
1256
|
+
).get(ref.source, ref.id);
|
|
1257
|
+
return row ? loadEventRecord(db, row.id) : null;
|
|
1258
|
+
}
|
|
1259
|
+
function existingEventReceipt(db, ref) {
|
|
1260
|
+
const event = ref && getEventByExternalRef(db, ref.source, ref.id);
|
|
1261
|
+
return event ? { ...buildEventMutationReceipt(event, {}), outcome: "no_op" } : null;
|
|
1262
|
+
}
|
|
1263
|
+
function enrichEventMetadata(db, record) {
|
|
1264
|
+
const bond = db.prepare("SELECT affects_bond FROM events WHERE id=?").get(record.id);
|
|
1265
|
+
record.affectsBond = bond?.affects_bond == null ? !(record.type === "observation" && record.significance <= 2) : Boolean(bond.affects_bond);
|
|
1266
|
+
if (record.parentEventId === void 0 && record.parentRef) {
|
|
1267
|
+
const row = db.prepare(
|
|
1268
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
1269
|
+
).get(record.parentRef.source, record.parentRef.id);
|
|
1270
|
+
if (row) record.parentEventId = row.id;
|
|
1271
|
+
}
|
|
1272
|
+
const attrs = db.prepare(
|
|
1273
|
+
"SELECT id, name, value FROM attributes WHERE event_id=? AND deleted_at IS NULL ORDER BY id"
|
|
1274
|
+
).all(record.id);
|
|
1275
|
+
if (attrs.length)
|
|
1276
|
+
record.attributes = attrs.map((a) => ({
|
|
1277
|
+
...a,
|
|
1278
|
+
contactId: null,
|
|
1279
|
+
linkId: null,
|
|
1280
|
+
eventId: record.id
|
|
1281
|
+
}));
|
|
1282
|
+
}
|
|
1283
|
+
|
|
850
1284
|
// src/events/validators.ts
|
|
851
1285
|
var PARTICIPANT_ROLES = /* @__PURE__ */ new Set([
|
|
852
1286
|
"actor",
|
|
@@ -889,8 +1323,11 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
889
1323
|
}
|
|
890
1324
|
function validateSocialEventInput(input) {
|
|
891
1325
|
assertFiniteTimestamp(input.occurredAt, "occurredAt");
|
|
892
|
-
|
|
893
|
-
if (summary.
|
|
1326
|
+
validateEventMetadata(input);
|
|
1327
|
+
if (input.summary !== void 0 && typeof input.summary !== "string")
|
|
1328
|
+
throw new AffinityValidationError("summary must be a string");
|
|
1329
|
+
const summary = input.summary?.trim();
|
|
1330
|
+
if (summary === "" || summary === void 0 && input.content === void 0) {
|
|
894
1331
|
throw new AffinityValidationError("summary must be non-empty");
|
|
895
1332
|
}
|
|
896
1333
|
const sig = input.significance;
|
|
@@ -954,11 +1391,25 @@ function mapEventRowToEventRecord(row, participants) {
|
|
|
954
1391
|
id: row.id,
|
|
955
1392
|
type: row.type,
|
|
956
1393
|
occurredAt: row.occurred_at,
|
|
957
|
-
|
|
1394
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
1395
|
+
summary: decodeEventText(row.summary),
|
|
958
1396
|
significance: row.significance,
|
|
959
1397
|
momentKind: row.moment_kind,
|
|
960
1398
|
participants
|
|
961
1399
|
};
|
|
1400
|
+
if (row.content != null) base.content = decodeEventText(row.content);
|
|
1401
|
+
if (row.channel != null) base.channel = row.channel;
|
|
1402
|
+
if (row.external_source != null && row.external_id != null)
|
|
1403
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
1404
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
1405
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
1406
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
1407
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
1408
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
1409
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
1410
|
+
base.dueAt = row.due_at ?? null;
|
|
1411
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
1412
|
+
}
|
|
962
1413
|
if (row.recurrence_kind !== null) {
|
|
963
1414
|
base.recurrenceKind = row.recurrence_kind;
|
|
964
1415
|
base.anchorMonth = row.anchor_month;
|
|
@@ -988,25 +1439,29 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
988
1439
|
targetRef,
|
|
989
1440
|
recurrenceKind,
|
|
990
1441
|
occursOn: row.occurs_on,
|
|
991
|
-
|
|
1442
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
1443
|
+
summary: decodeEventText(row.summary),
|
|
992
1444
|
significance: row.significance
|
|
993
1445
|
};
|
|
994
1446
|
}
|
|
1447
|
+
function decodeEventText(value) {
|
|
1448
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
1449
|
+
}
|
|
995
1450
|
|
|
996
1451
|
// src/events/queries.ts
|
|
997
1452
|
function getEventRowById(db, id) {
|
|
998
1453
|
const row = db.prepare(
|
|
999
|
-
`SELECT
|
|
1000
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
1001
|
-
anchor_contact_id, anchor_link_id,
|
|
1002
|
-
created_at, updated_at, deleted_at
|
|
1454
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
1003
1455
|
FROM events
|
|
1004
1456
|
WHERE id = ? AND deleted_at IS NULL`
|
|
1005
1457
|
).get(id);
|
|
1006
1458
|
if (row === void 0) {
|
|
1007
1459
|
return null;
|
|
1008
1460
|
}
|
|
1009
|
-
|
|
1461
|
+
const event = row;
|
|
1462
|
+
event.summary = decodeEventText(event.summary);
|
|
1463
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
1464
|
+
return event;
|
|
1010
1465
|
}
|
|
1011
1466
|
function findDuplicateDateAnchor(db, params) {
|
|
1012
1467
|
const exclude = params.excludeEventId;
|
|
@@ -1074,19 +1529,27 @@ function loadEventRecord(db, eventId) {
|
|
|
1074
1529
|
throw new AffinityInvariantError("event not found");
|
|
1075
1530
|
}
|
|
1076
1531
|
const participants = loadEventParticipantViews(db, eventId);
|
|
1077
|
-
|
|
1532
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
1533
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
1534
|
+
if (due) {
|
|
1535
|
+
record.dueAt = due.due_at;
|
|
1536
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
1537
|
+
}
|
|
1538
|
+
enrichEventMetadata(db, record);
|
|
1539
|
+
return record;
|
|
1078
1540
|
}
|
|
1079
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
1541
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
1080
1542
|
if (eventIds.length === 0) {
|
|
1081
1543
|
return [];
|
|
1082
1544
|
}
|
|
1083
1545
|
const ph = eventIds.map(() => "?").join(", ");
|
|
1084
1546
|
const eventRows = db.prepare(
|
|
1085
|
-
`SELECT id, type, occurred_at,
|
|
1547
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
1548
|
+
e.external_source,e.external_id,e.channel,e.parent_source,e.parent_ref_id,e.parent_event_id,e.thread_source,e.thread_id,
|
|
1086
1549
|
recurrence_kind, anchor_month, anchor_day,
|
|
1087
1550
|
anchor_contact_id, anchor_link_id,
|
|
1088
|
-
created_at, updated_at, deleted_at
|
|
1089
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
1551
|
+
e.created_at, e.updated_at, e.deleted_at, ${includeContent ? "CAST(e.content AS BLOB)" : "NULL"} AS content, CAST(e.summary AS BLOB) AS summary
|
|
1552
|
+
FROM events e LEFT JOIN open_commitments oc ON oc.event_id = e.id WHERE e.id IN (${ph}) AND e.deleted_at IS NULL`
|
|
1090
1553
|
).all(...eventIds);
|
|
1091
1554
|
const partRows = db.prepare(
|
|
1092
1555
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -1110,7 +1573,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
1110
1573
|
for (const id of eventIds) {
|
|
1111
1574
|
const row = rowById.get(id);
|
|
1112
1575
|
if (row) {
|
|
1113
|
-
|
|
1576
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
1577
|
+
enrichEventMetadata(db, record);
|
|
1578
|
+
out.push(record);
|
|
1114
1579
|
}
|
|
1115
1580
|
}
|
|
1116
1581
|
return out;
|
|
@@ -1169,6 +1634,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
1169
1634
|
clauses.push(`e.type IN (${ph})`);
|
|
1170
1635
|
params.push(...options.eventTypes);
|
|
1171
1636
|
}
|
|
1637
|
+
if (options?.channel !== void 0) {
|
|
1638
|
+
clauses.push("e.channel = ?");
|
|
1639
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1640
|
+
}
|
|
1172
1641
|
const where = clauses.join(" AND ");
|
|
1173
1642
|
const rows = db.prepare(
|
|
1174
1643
|
`SELECT e.id FROM events e
|
|
@@ -1179,8 +1648,75 @@ function getContactJournal(db, contactId, options) {
|
|
|
1179
1648
|
).all(...params, limit, offset);
|
|
1180
1649
|
return batchLoadEventRecords(
|
|
1181
1650
|
db,
|
|
1182
|
-
rows.map((r) => r.id)
|
|
1651
|
+
rows.map((r) => r.id),
|
|
1652
|
+
options?.includeContent ?? false
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/lib/parse_calendar_time.ts
|
|
1657
|
+
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
1658
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
1659
|
+
if (dueAt === null) return false;
|
|
1660
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
1661
|
+
return localCalendarDate(now) > dueOn;
|
|
1662
|
+
}
|
|
1663
|
+
return dueAt < now;
|
|
1664
|
+
}
|
|
1665
|
+
var OVERDUE_SQL = "(CASE WHEN oc.due_on IS NOT NULL AND oc.due_at = CAST(strftime('%s', oc.due_on) AS INTEGER) * 1000 + 86399999 THEN oc.due_on < ? ELSE oc.due_at < ? END)";
|
|
1666
|
+
function localCalendarDate(now) {
|
|
1667
|
+
const d = new Date(now);
|
|
1668
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1669
|
+
}
|
|
1670
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
1671
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
1672
|
+
throw new AffinityValidationError(
|
|
1673
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
1674
|
+
);
|
|
1675
|
+
const fail = () => {
|
|
1676
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1677
|
+
};
|
|
1678
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
1679
|
+
const timestamp2 = Number(value);
|
|
1680
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
1681
|
+
return fail();
|
|
1682
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
1683
|
+
}
|
|
1684
|
+
if (typeof value !== "string") return fail();
|
|
1685
|
+
let text = value.trim();
|
|
1686
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
1687
|
+
text = text.replace(" ", "T");
|
|
1688
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
1689
|
+
}
|
|
1690
|
+
let rounding = 0;
|
|
1691
|
+
text = text.replace(
|
|
1692
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
1693
|
+
(_, digits) => {
|
|
1694
|
+
const ms = Math.round(Number(`0.${digits}`) * 1e3);
|
|
1695
|
+
rounding = ms === 1e3 ? 1 : 0;
|
|
1696
|
+
return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
|
|
1697
|
+
}
|
|
1183
1698
|
);
|
|
1699
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
1700
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1701
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
1702
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
1703
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
1704
|
+
text = date.toISOString().slice(0, 10);
|
|
1705
|
+
}
|
|
1706
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
1707
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
1708
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
1709
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1710
|
+
text
|
|
1711
|
+
))
|
|
1712
|
+
return fail();
|
|
1713
|
+
const day = text.slice(0, 10);
|
|
1714
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
1715
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
1716
|
+
return fail();
|
|
1717
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
|
|
1718
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
1719
|
+
return { timestamp, day };
|
|
1184
1720
|
}
|
|
1185
1721
|
|
|
1186
1722
|
// src/events/list_open_commitments.ts
|
|
@@ -1238,9 +1774,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
1238
1774
|
clauses.push("oc.due_at <= ?");
|
|
1239
1775
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
1240
1776
|
}
|
|
1777
|
+
if (options?.channel !== void 0) {
|
|
1778
|
+
clauses.push("e.channel=?");
|
|
1779
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1780
|
+
}
|
|
1241
1781
|
const where = clauses.join(" AND ");
|
|
1242
1782
|
const rows = db.prepare(
|
|
1243
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
1783
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
1244
1784
|
e.type, e.summary
|
|
1245
1785
|
FROM open_commitments oc
|
|
1246
1786
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -1257,7 +1797,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
1257
1797
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
1258
1798
|
dueAt: r.due_at,
|
|
1259
1799
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
1260
|
-
|
|
1800
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
1801
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
1261
1802
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
1262
1803
|
}));
|
|
1263
1804
|
}
|
|
@@ -1291,8 +1832,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
1291
1832
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
1292
1833
|
}
|
|
1293
1834
|
function computeNextAnchorOccursOn(params) {
|
|
1294
|
-
const refStart =
|
|
1295
|
-
|
|
1835
|
+
const refStart = Date.parse(
|
|
1836
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
1837
|
+
);
|
|
1838
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
1296
1839
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
1297
1840
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
1298
1841
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -1352,19 +1895,6 @@ function loadActiveDatesForContact(db, contactId) {
|
|
|
1352
1895
|
return rows.map((row) => mapUpcomingRowToUpcomingDateRecord(row));
|
|
1353
1896
|
}
|
|
1354
1897
|
|
|
1355
|
-
// src/identities/mappers.ts
|
|
1356
|
-
function mapIdentityRowToIdentityRecord(row) {
|
|
1357
|
-
return {
|
|
1358
|
-
id: row.id,
|
|
1359
|
-
contactId: row.contact_id,
|
|
1360
|
-
type: row.type,
|
|
1361
|
-
value: row.value,
|
|
1362
|
-
label: row.label,
|
|
1363
|
-
verified: row.verified === 1,
|
|
1364
|
-
verifiedAt: row.verified_at
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
1898
|
// src/links/mappers.ts
|
|
1369
1899
|
function mapLinkRowToLinkListItem(row) {
|
|
1370
1900
|
const kind = row.kind;
|
|
@@ -1425,7 +1955,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1425
1955
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
1426
1956
|
LIMIT ?`
|
|
1427
1957
|
).all(contactId, contactId, topLinksLimit);
|
|
1428
|
-
const topLinks = linkRows.map((l) =>
|
|
1958
|
+
const topLinks = linkRows.map((l) => ({
|
|
1959
|
+
...mapLinkRowToLinkListItem(l),
|
|
1960
|
+
counterpartyName: getContactRowById(
|
|
1961
|
+
db,
|
|
1962
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1963
|
+
)?.name ?? ""
|
|
1964
|
+
}));
|
|
1429
1965
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
1430
1966
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1431
1967
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -1458,7 +1994,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
1458
1994
|
return {
|
|
1459
1995
|
...profile,
|
|
1460
1996
|
recentEvents: getContactJournal(db, contactId, {
|
|
1461
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1997
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1998
|
+
eventTypes: [
|
|
1999
|
+
"conversation",
|
|
2000
|
+
"activity",
|
|
2001
|
+
"gift",
|
|
2002
|
+
"support",
|
|
2003
|
+
"milestone",
|
|
2004
|
+
"observation",
|
|
2005
|
+
"conflict",
|
|
2006
|
+
"correction",
|
|
2007
|
+
"transaction",
|
|
2008
|
+
"promise",
|
|
2009
|
+
"agreement"
|
|
2010
|
+
]
|
|
1462
2011
|
}).map((event) => ({
|
|
1463
2012
|
...event,
|
|
1464
2013
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -1488,7 +2037,10 @@ function contactAttributeClauses(filters) {
|
|
|
1488
2037
|
const params = [];
|
|
1489
2038
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
1490
2039
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
1491
|
-
for (const filter of [
|
|
2040
|
+
for (const filter of [
|
|
2041
|
+
...attributes,
|
|
2042
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
2043
|
+
]) {
|
|
1492
2044
|
const separator = filter.indexOf("=");
|
|
1493
2045
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
1494
2046
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -1559,7 +2111,9 @@ function pairKey(leftContactId, rightContactId) {
|
|
|
1559
2111
|
function normalizeName(name) {
|
|
1560
2112
|
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
1561
2113
|
}
|
|
1562
|
-
function fuzzyNameScore(
|
|
2114
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
2115
|
+
const left = normalizeName(leftInput);
|
|
2116
|
+
const right = normalizeName(rightInput);
|
|
1563
2117
|
if (left === right) {
|
|
1564
2118
|
return 0.92;
|
|
1565
2119
|
}
|
|
@@ -1717,7 +2271,7 @@ function foldSearchText(value) {
|
|
|
1717
2271
|
|
|
1718
2272
|
// src/contacts/search_contacts.ts
|
|
1719
2273
|
function searchContacts(db, query, filters, options) {
|
|
1720
|
-
const q = foldSearchText(query.trim());
|
|
2274
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
1721
2275
|
if (!q) return [];
|
|
1722
2276
|
assertDefaultOrdering("searchContacts", options);
|
|
1723
2277
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -1737,6 +2291,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
1737
2291
|
const tokens = q.split(/\s+/);
|
|
1738
2292
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
1739
2293
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
2294
|
+
const suffix = digits.replace(/^0/, "");
|
|
1740
2295
|
const attributeFilters = contactAttributeClauses(filters);
|
|
1741
2296
|
const clauses = [
|
|
1742
2297
|
"deleted_at IS NULL",
|
|
@@ -1772,7 +2327,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
1772
2327
|
)
|
|
1773
2328
|
];
|
|
1774
2329
|
const phoneFields = phone ? values.filter(
|
|
1775
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
2330
|
+
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits) || field.source === "identity:phone" && suffix.length >= 6 && field.text.replace(/\D/g, "").endsWith(suffix)
|
|
1776
2331
|
) : [];
|
|
1777
2332
|
if (!phoneFields.length && !tokens.every(
|
|
1778
2333
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -1807,7 +2362,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
1807
2362
|
}
|
|
1808
2363
|
if (to === "merged") {
|
|
1809
2364
|
throw new AffinityInvariantError(
|
|
1810
|
-
"use
|
|
2365
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
1811
2366
|
);
|
|
1812
2367
|
}
|
|
1813
2368
|
if (from === to) {
|
|
@@ -1862,7 +2417,7 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
1862
2417
|
// src/events/persistence.ts
|
|
1863
2418
|
function insertJournalEvent(db, params) {
|
|
1864
2419
|
const now = resolveNow(params.now);
|
|
1865
|
-
const summary = params.summary
|
|
2420
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
1866
2421
|
const result = db.prepare(
|
|
1867
2422
|
`INSERT INTO events (
|
|
1868
2423
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -1879,6 +2434,33 @@ function insertJournalEvent(db, params) {
|
|
|
1879
2434
|
now
|
|
1880
2435
|
);
|
|
1881
2436
|
const eventId = Number(result.lastInsertRowid);
|
|
2437
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
2438
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
|
|
2439
|
+
eventId
|
|
2440
|
+
);
|
|
2441
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
2442
|
+
params.momentKind,
|
|
2443
|
+
eventId
|
|
2444
|
+
);
|
|
2445
|
+
db.prepare(
|
|
2446
|
+
"UPDATE events SET content=?, external_source=?, external_id=?, channel=?, parent_source=?, parent_ref_id=?, parent_event_id=?, thread_source=?, thread_id=?, provenance_json=? WHERE id=?"
|
|
2447
|
+
).run(
|
|
2448
|
+
params.content ?? null,
|
|
2449
|
+
params.externalRef?.source ?? null,
|
|
2450
|
+
params.externalRef?.id ?? null,
|
|
2451
|
+
params.channel ?? null,
|
|
2452
|
+
params.parentRef?.source ?? null,
|
|
2453
|
+
params.parentRef?.id ?? null,
|
|
2454
|
+
params.parentEventId ?? null,
|
|
2455
|
+
params.threadRef?.source ?? null,
|
|
2456
|
+
params.threadRef?.id ?? null,
|
|
2457
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
2458
|
+
eventId
|
|
2459
|
+
);
|
|
2460
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
2461
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
2462
|
+
eventId
|
|
2463
|
+
);
|
|
1882
2464
|
const insertParticipant = db.prepare(
|
|
1883
2465
|
`INSERT INTO event_participants (
|
|
1884
2466
|
event_id, contact_id, role, directionality
|
|
@@ -1921,6 +2503,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
1921
2503
|
now
|
|
1922
2504
|
);
|
|
1923
2505
|
const eventId = Number(result.lastInsertRowid);
|
|
2506
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
2507
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
2508
|
+
eventId
|
|
2509
|
+
);
|
|
1924
2510
|
const insertParticipant = db.prepare(
|
|
1925
2511
|
`INSERT INTO event_participants (
|
|
1926
2512
|
event_id, contact_id, role, directionality
|
|
@@ -1941,20 +2527,6 @@ function insertDateAnchorEvent(db, params) {
|
|
|
1941
2527
|
return eventId;
|
|
1942
2528
|
}
|
|
1943
2529
|
|
|
1944
|
-
// src/events/receipts.ts
|
|
1945
|
-
function buildEventMutationReceipt(primary, partial) {
|
|
1946
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
1947
|
-
return {
|
|
1948
|
-
primary,
|
|
1949
|
-
created: normalized.created,
|
|
1950
|
-
updated: normalized.updated,
|
|
1951
|
-
archived: normalized.archived,
|
|
1952
|
-
removed: normalized.removed,
|
|
1953
|
-
affectedLinks: normalized.affectedLinks,
|
|
1954
|
-
derivedEffects: normalized.derivedEffects
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
2530
|
// src/links/queries.ts
|
|
1959
2531
|
function getLinkRowById(db, id) {
|
|
1960
2532
|
const row = db.prepare(
|
|
@@ -2116,12 +2688,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
2116
2688
|
}
|
|
2117
2689
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
2118
2690
|
const params = [];
|
|
2119
|
-
const since = options?.since ??
|
|
2691
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
2120
2692
|
clauses.push("uo.occurs_on >= ?");
|
|
2121
2693
|
params.push(since);
|
|
2122
2694
|
let maxOccurs = options?.until;
|
|
2123
2695
|
if (filters?.horizonDays !== void 0) {
|
|
2124
|
-
const anchor =
|
|
2696
|
+
const anchor = since;
|
|
2125
2697
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
2126
2698
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
2127
2699
|
}
|
|
@@ -2659,6 +3231,10 @@ function listMoments(db, filters, options) {
|
|
|
2659
3231
|
clauses.push("le.link_id = ?");
|
|
2660
3232
|
params.push(filters.linkId);
|
|
2661
3233
|
}
|
|
3234
|
+
if (options?.channel !== void 0) {
|
|
3235
|
+
clauses.push("e.channel=?");
|
|
3236
|
+
params.push(options.channel.trim().toLowerCase());
|
|
3237
|
+
}
|
|
2662
3238
|
const where = clauses.join(" AND ");
|
|
2663
3239
|
return db.prepare(
|
|
2664
3240
|
`SELECT
|
|
@@ -2666,6 +3242,7 @@ function listMoments(db, filters, options) {
|
|
|
2666
3242
|
le.link_id,
|
|
2667
3243
|
le.moment_kind,
|
|
2668
3244
|
le.occurred_at,
|
|
3245
|
+
e.occurred_on,
|
|
2669
3246
|
e.summary,
|
|
2670
3247
|
le.impact_score
|
|
2671
3248
|
FROM link_event_effects le
|
|
@@ -2678,6 +3255,7 @@ function listMoments(db, filters, options) {
|
|
|
2678
3255
|
linkId: row.link_id,
|
|
2679
3256
|
momentKind: row.moment_kind,
|
|
2680
3257
|
occurredAt: row.occurred_at,
|
|
3258
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
2681
3259
|
summary: row.summary,
|
|
2682
3260
|
impactScore: row.impact_score
|
|
2683
3261
|
}));
|
|
@@ -2706,6 +3284,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2706
3284
|
clauses.push(`e.type IN (${ph})`);
|
|
2707
3285
|
params.push(...options.eventTypes);
|
|
2708
3286
|
}
|
|
3287
|
+
if (options?.channel !== void 0) {
|
|
3288
|
+
clauses.push("e.channel = ?");
|
|
3289
|
+
params.push(options.channel.trim().toLowerCase());
|
|
3290
|
+
}
|
|
2709
3291
|
const where = clauses.join(" AND ");
|
|
2710
3292
|
const rows = db.prepare(
|
|
2711
3293
|
`SELECT e.id
|
|
@@ -2717,7 +3299,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2717
3299
|
).all(...params, limit, offset);
|
|
2718
3300
|
return batchLoadEventRecords(
|
|
2719
3301
|
db,
|
|
2720
|
-
rows.map((r) => r.id)
|
|
3302
|
+
rows.map((r) => r.id),
|
|
3303
|
+
options?.includeContent ?? false
|
|
2721
3304
|
);
|
|
2722
3305
|
}
|
|
2723
3306
|
|
|
@@ -2791,26 +3374,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2791
3374
|
const latest = db.prepare(
|
|
2792
3375
|
`SELECT event_id, occurred_at
|
|
2793
3376
|
FROM link_event_effects
|
|
2794
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
3377
|
+
WHERE link_id = ? AND counts_for_recency = 1 AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))
|
|
2795
3378
|
ORDER BY occurred_at DESC, event_id DESC
|
|
2796
3379
|
LIMIT 1`
|
|
2797
|
-
).get(linkId);
|
|
3380
|
+
).get(linkId, now, localCalendarDate(now));
|
|
2798
3381
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
2799
3382
|
const counts = db.prepare(
|
|
2800
3383
|
`SELECT
|
|
2801
3384
|
COUNT(*) AS total_meaningful_events,
|
|
2802
3385
|
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
2803
3386
|
FROM link_event_effects
|
|
2804
|
-
WHERE link_id = ? AND occurred_at >=
|
|
2805
|
-
|
|
3387
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
3388
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
3389
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
2806
3390
|
const directionalityRows = db.prepare(
|
|
2807
3391
|
`SELECT ep.directionality
|
|
2808
3392
|
FROM link_event_effects le
|
|
2809
3393
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
2810
3394
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
2811
3395
|
AND ep.directionality IS NOT NULL
|
|
2812
|
-
AND le.occurred_at >=
|
|
2813
|
-
|
|
3396
|
+
AND le.occurred_at >= ?
|
|
3397
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
3398
|
+
).all(
|
|
3399
|
+
linkId,
|
|
3400
|
+
link.from_contact_id,
|
|
3401
|
+
trailingWindowStart,
|
|
3402
|
+
now,
|
|
3403
|
+
localCalendarDate(now)
|
|
3404
|
+
);
|
|
2814
3405
|
let outboundCount = 0;
|
|
2815
3406
|
let inboundCount = 0;
|
|
2816
3407
|
for (const row of directionalityRows) {
|
|
@@ -3129,21 +3720,6 @@ function listProgressionReadiness(db, filters, options) {
|
|
|
3129
3720
|
}
|
|
3130
3721
|
|
|
3131
3722
|
// src/links/list_radar.ts
|
|
3132
|
-
function deriveRadarReason(drift, recency, normalizedRank3) {
|
|
3133
|
-
if (drift > 0.5) {
|
|
3134
|
-
return "Significantly overdue for contact";
|
|
3135
|
-
}
|
|
3136
|
-
if (drift > 0.2) {
|
|
3137
|
-
return "Drifting \u2014 approaching overdue";
|
|
3138
|
-
}
|
|
3139
|
-
if (recency < 0.3 && normalizedRank3 > 0.3) {
|
|
3140
|
-
return "Important link losing recency";
|
|
3141
|
-
}
|
|
3142
|
-
if (normalizedRank3 > 0.5 && recency < 0.5) {
|
|
3143
|
-
return "High-rank link needs attention";
|
|
3144
|
-
}
|
|
3145
|
-
return "Routine check-in recommended";
|
|
3146
|
-
}
|
|
3147
3723
|
function listRadar(db, filters, options) {
|
|
3148
3724
|
assertDefaultOrdering("listRadar", options);
|
|
3149
3725
|
const ownerId = findOwnerContactId(db);
|
|
@@ -3151,7 +3727,10 @@ function listRadar(db, filters, options) {
|
|
|
3151
3727
|
return [];
|
|
3152
3728
|
}
|
|
3153
3729
|
const now = resolveNow(options?.now);
|
|
3154
|
-
const
|
|
3730
|
+
const today = localCalendarDate(now);
|
|
3731
|
+
const lastContact = `(SELECT MAX(le.occurred_at) FROM link_event_effects le JOIN events ev ON ev.id = le.event_id WHERE le.link_id = l.id AND le.counts_for_recency = 1 AND ev.deleted_at IS NULL AND (le.occurred_at <= ${now} OR ev.occurred_on <= '${today}'))`;
|
|
3732
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
3733
|
+
const overdue = `EXISTS (SELECT 1 FROM open_commitments oc JOIN events e ON e.id = oc.event_id JOIN event_participants ep ON ep.event_id = e.id WHERE ep.contact_id = CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END AND e.deleted_at IS NULL AND oc.resolved_at IS NULL AND (CASE WHEN oc.due_on IS NOT NULL AND oc.due_at = CAST(strftime('%s', oc.due_on) AS INTEGER) * 1000 + 86399999 THEN oc.due_on < '${today}' ELSE oc.due_at < ${now} END))`;
|
|
3155
3734
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
3156
3735
|
const clauses = [
|
|
3157
3736
|
"l.removed_at IS NULL",
|
|
@@ -3176,10 +3755,23 @@ function listRadar(db, filters, options) {
|
|
|
3176
3755
|
if (options?.includeObserved === false) {
|
|
3177
3756
|
clauses.push("l.kind != 'observed'");
|
|
3178
3757
|
}
|
|
3758
|
+
if (options?.includeDormant !== true)
|
|
3759
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
3760
|
+
if (filters?.includeLost !== true)
|
|
3761
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
3762
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
3763
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
3764
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
3765
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
3766
|
+
AND (ev.affects_bond=0 OR (ev.type='observation' AND ev.significance<=2) OR json_extract(ev.provenance_json, '$.ambient')=1 OR ep.directionality='owner_initiated' OR EXISTS (SELECT 1 FROM event_participants op WHERE op.event_id=ev.id AND op.contact_id=${ownerId} AND op.directionality='owner_initiated'))
|
|
3767
|
+
))`);
|
|
3179
3768
|
const where = clauses.join(" AND ");
|
|
3180
3769
|
const rows = db.prepare(
|
|
3181
3770
|
`SELECT
|
|
3182
3771
|
l.id,
|
|
3772
|
+
l.cadence_days,
|
|
3773
|
+
${lastContact} AS last_contact,
|
|
3774
|
+
${drift} AS drift,
|
|
3183
3775
|
${overdue} AS overdue_commitment,
|
|
3184
3776
|
l.from_contact_id,
|
|
3185
3777
|
l.to_contact_id,
|
|
@@ -3193,23 +3785,44 @@ function listRadar(db, filters, options) {
|
|
|
3193
3785
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
3194
3786
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
3195
3787
|
WHERE ${where}
|
|
3196
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3788
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3197
3789
|
LIMIT ? OFFSET ?`
|
|
3198
3790
|
).all(...params, limit, offset);
|
|
3199
|
-
return rows.map((row) =>
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3791
|
+
return rows.map((row) => {
|
|
3792
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
3793
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
3794
|
+
`SELECT e.summary, oc.due_at, oc.due_on FROM open_commitments oc JOIN events e ON e.id = oc.event_id JOIN event_participants ep ON ep.event_id = e.id WHERE ep.contact_id = ? AND e.deleted_at IS NULL AND oc.resolved_at IS NULL AND ${OVERDUE_SQL} ORDER BY oc.due_at LIMIT 1`
|
|
3795
|
+
).get(contactId, today, now) : void 0;
|
|
3796
|
+
const reasons = [
|
|
3797
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
3798
|
+
...row.drift ? ["drift"] : []
|
|
3799
|
+
];
|
|
3800
|
+
const snippets = [
|
|
3801
|
+
...promise ? [
|
|
3802
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
3803
|
+
] : [],
|
|
3804
|
+
...row.drift ? [
|
|
3805
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
3806
|
+
] : []
|
|
3807
|
+
];
|
|
3808
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
3809
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
3810
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
3811
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
3812
|
+
trust: row.trust ?? 0,
|
|
3813
|
+
rank: row.rank ?? 0
|
|
3814
|
+
});
|
|
3815
|
+
return {
|
|
3816
|
+
linkId: row.id,
|
|
3817
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
3818
|
+
reasons,
|
|
3819
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
3820
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
3821
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
3822
|
+
trust: row.trust ?? 0,
|
|
3823
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
3824
|
+
};
|
|
3825
|
+
});
|
|
3213
3826
|
}
|
|
3214
3827
|
|
|
3215
3828
|
// src/links/receipts.ts
|
|
@@ -3292,6 +3905,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
3292
3905
|
now,
|
|
3293
3906
|
linkId
|
|
3294
3907
|
);
|
|
3908
|
+
db.prepare(
|
|
3909
|
+
"INSERT INTO event_link_state_changes(link_id,after_event_id,state) SELECT ?, COALESCE(MAX(event_id),0), ? FROM link_event_effects WHERE link_id=?"
|
|
3910
|
+
).run(linkId, state, linkId);
|
|
3295
3911
|
refreshLinkRollup(db, linkId, now);
|
|
3296
3912
|
if (state === "archived" || row.state === "archived") {
|
|
3297
3913
|
refreshAllBridgeScores(db, now);
|
|
@@ -3366,6 +3982,8 @@ function reviseBond(db, linkId, bond, options) {
|
|
|
3366
3982
|
// src/links/seed_social_link.ts
|
|
3367
3983
|
function seedSocialLink(db, input) {
|
|
3368
3984
|
return withTransaction(db, () => {
|
|
3985
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
3986
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
3369
3987
|
if (!isRelationalLinkKind(input.kind)) {
|
|
3370
3988
|
throw new AffinityValidationError("invalid relational link kind");
|
|
3371
3989
|
}
|
|
@@ -3373,11 +3991,14 @@ function seedSocialLink(db, input) {
|
|
|
3373
3991
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
3374
3992
|
}
|
|
3375
3993
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
3376
|
-
|
|
3994
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
3377
3995
|
db,
|
|
3378
3996
|
input.fromContactId,
|
|
3379
3997
|
input.toContactId
|
|
3380
|
-
)
|
|
3998
|
+
);
|
|
3999
|
+
if (existing) {
|
|
4000
|
+
if (input.ifExists === "return")
|
|
4001
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
3381
4002
|
throw new AffinityConflictError(
|
|
3382
4003
|
"relational link already exists between these contacts"
|
|
3383
4004
|
);
|
|
@@ -4196,6 +4817,8 @@ function deriveStateAfter(params) {
|
|
|
4196
4817
|
return params.stateBefore;
|
|
4197
4818
|
}
|
|
4198
4819
|
function deriveMomentKind(params) {
|
|
4820
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
4821
|
+
return null;
|
|
4199
4822
|
if (params.rankAfter > params.rankBefore) {
|
|
4200
4823
|
return "breakthrough";
|
|
4201
4824
|
}
|
|
@@ -4225,6 +4848,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
4225
4848
|
if (!link || link.is_structural !== 0) {
|
|
4226
4849
|
throw new AffinityStateError("relational link not found");
|
|
4227
4850
|
}
|
|
4851
|
+
db.prepare(
|
|
4852
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
4853
|
+
).run(
|
|
4854
|
+
params.linkId,
|
|
4855
|
+
JSON.stringify({
|
|
4856
|
+
rank: link.rank ?? 0,
|
|
4857
|
+
affinity: link.affinity ?? 0,
|
|
4858
|
+
trust: link.trust ?? 0,
|
|
4859
|
+
state: link.state ?? "active",
|
|
4860
|
+
cadence_days: link.cadence_days
|
|
4861
|
+
})
|
|
4862
|
+
);
|
|
4228
4863
|
const stateBefore = link.state ?? "active";
|
|
4229
4864
|
const linkKind = link.kind;
|
|
4230
4865
|
const rankBefore = link.rank ?? 0;
|
|
@@ -4269,7 +4904,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
4269
4904
|
);
|
|
4270
4905
|
const weightedBase = base * massPenalty(
|
|
4271
4906
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
4272
|
-
) * salienceMultiplier;
|
|
4907
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
4273
4908
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
4274
4909
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
4275
4910
|
const affinityMass = clamp3(
|
|
@@ -4310,15 +4945,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
4310
4945
|
const latest = db.prepare(
|
|
4311
4946
|
`SELECT occurred_at
|
|
4312
4947
|
FROM link_event_effects
|
|
4313
|
-
WHERE link_id = ?
|
|
4948
|
+
WHERE link_id = ? AND counts_for_recency = 1 AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))
|
|
4314
4949
|
ORDER BY occurred_at DESC, event_id DESC
|
|
4315
4950
|
LIMIT 1`
|
|
4316
|
-
).get(params.linkId);
|
|
4951
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
4317
4952
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
4318
4953
|
1,
|
|
4319
4954
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
4320
4955
|
);
|
|
4321
|
-
const
|
|
4956
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
4957
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
4322
4958
|
kind: linkKind,
|
|
4323
4959
|
currentCadenceDays: cadenceBefore,
|
|
4324
4960
|
gapDays
|
|
@@ -4338,7 +4974,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
4338
4974
|
}
|
|
4339
4975
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
4340
4976
|
const trustDelta = trustAfter - trustBefore;
|
|
4341
|
-
const
|
|
4977
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
4978
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
4979
|
+
eventLabel
|
|
4980
|
+
);
|
|
4981
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
4342
4982
|
rankBefore,
|
|
4343
4983
|
rankAfter,
|
|
4344
4984
|
stateBefore,
|
|
@@ -4381,7 +5021,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
4381
5021
|
params.now,
|
|
4382
5022
|
params.now
|
|
4383
5023
|
);
|
|
4384
|
-
|
|
5024
|
+
db.prepare(
|
|
5025
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
5026
|
+
).run(
|
|
5027
|
+
JSON.stringify(params.participant),
|
|
5028
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
5029
|
+
params.eventId,
|
|
5030
|
+
params.linkId
|
|
5031
|
+
);
|
|
5032
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
4385
5033
|
db.prepare(
|
|
4386
5034
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4387
5035
|
).run(params.eventId, params.linkId);
|
|
@@ -4464,15 +5112,36 @@ function ensureRelationalLink(db, params) {
|
|
|
4464
5112
|
};
|
|
4465
5113
|
}
|
|
4466
5114
|
function applyJournalEventMechanics(db, params) {
|
|
5115
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
5116
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
5117
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
5118
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
4467
5119
|
const ownerId = requireOwnerContactId(db);
|
|
4468
|
-
const
|
|
5120
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
5121
|
+
const socialChannel = [
|
|
5122
|
+
"bluesky",
|
|
5123
|
+
"x",
|
|
5124
|
+
"twitter",
|
|
5125
|
+
"mastodon",
|
|
5126
|
+
"github",
|
|
5127
|
+
"instagram"
|
|
5128
|
+
].includes(String(bond?.channel));
|
|
5129
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
5130
|
+
const participants = params.participants.map(
|
|
5131
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
5132
|
+
);
|
|
5133
|
+
if (direction)
|
|
5134
|
+
db.prepare(
|
|
5135
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
5136
|
+
).run(direction, params.eventId);
|
|
5137
|
+
const participantIds = participants.map(
|
|
4469
5138
|
(participant) => participant.contactId
|
|
4470
5139
|
);
|
|
4471
5140
|
const hasOwner = participantIds.includes(ownerId);
|
|
4472
5141
|
const created = [];
|
|
4473
5142
|
const effectInputs = [];
|
|
4474
5143
|
if (hasOwner) {
|
|
4475
|
-
for (const participant of
|
|
5144
|
+
for (const participant of participants) {
|
|
4476
5145
|
if (participant.contactId === ownerId) {
|
|
4477
5146
|
continue;
|
|
4478
5147
|
}
|
|
@@ -4480,13 +5149,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4480
5149
|
fromContactId: ownerId,
|
|
4481
5150
|
toContactId: participant.contactId,
|
|
4482
5151
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
5152
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
4483
5153
|
now: params.now
|
|
4484
5154
|
});
|
|
4485
5155
|
created.push(...ensured.created);
|
|
4486
5156
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
4487
5157
|
}
|
|
4488
|
-
} else if (params.eventType === "observation" &&
|
|
4489
|
-
const [first, second] =
|
|
5158
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
5159
|
+
const [first, second] = participants;
|
|
4490
5160
|
if (first !== void 0 && second !== void 0) {
|
|
4491
5161
|
const a = Math.min(first.contactId, second.contactId);
|
|
4492
5162
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -4521,7 +5191,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4521
5191
|
eventType: params.eventType,
|
|
4522
5192
|
occurredAt: params.occurredAt,
|
|
4523
5193
|
significance: params.significance,
|
|
4524
|
-
participant:
|
|
5194
|
+
participant: {
|
|
5195
|
+
...input.participant,
|
|
5196
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
5197
|
+
},
|
|
4525
5198
|
provenance: params.provenance,
|
|
4526
5199
|
now: params.now
|
|
4527
5200
|
})
|
|
@@ -4545,6 +5218,8 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4545
5218
|
function recordCommitment(db, input) {
|
|
4546
5219
|
return withTransaction(db, () => {
|
|
4547
5220
|
validateSocialEventInput(input);
|
|
5221
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5222
|
+
if (existing) return existing;
|
|
4548
5223
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
4549
5224
|
throw new AffinityValidationError(
|
|
4550
5225
|
"commitmentType must be promise or agreement"
|
|
@@ -4559,11 +5234,12 @@ function recordCommitment(db, input) {
|
|
|
4559
5234
|
assertOwnerParticipates(ownerId, ids);
|
|
4560
5235
|
const now = resolveNow(input.now);
|
|
4561
5236
|
const eventId = insertJournalEvent(db, {
|
|
5237
|
+
...input,
|
|
4562
5238
|
type: input.commitmentType,
|
|
4563
5239
|
occurredAt: input.occurredAt,
|
|
4564
|
-
|
|
5240
|
+
occurredOn: input.occurredOn,
|
|
4565
5241
|
significance: input.significance,
|
|
4566
|
-
momentKind: null,
|
|
5242
|
+
momentKind: input.momentKind ?? null,
|
|
4567
5243
|
participants: input.participants,
|
|
4568
5244
|
now
|
|
4569
5245
|
});
|
|
@@ -4572,15 +5248,25 @@ function recordCommitment(db, input) {
|
|
|
4572
5248
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
4573
5249
|
) VALUES (?, ?, ?, ?, ?)`
|
|
4574
5250
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
5251
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
5252
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
5253
|
+
eventId
|
|
5254
|
+
);
|
|
4575
5255
|
const mechanics = applyJournalEventMechanics(db, {
|
|
4576
5256
|
eventId,
|
|
4577
5257
|
eventType: input.commitmentType,
|
|
4578
5258
|
occurredAt: input.occurredAt,
|
|
5259
|
+
occurredOn: input.occurredOn,
|
|
4579
5260
|
significance: input.significance,
|
|
4580
5261
|
participants: input.participants,
|
|
4581
5262
|
provenance: input.provenance,
|
|
4582
5263
|
now
|
|
4583
5264
|
});
|
|
5265
|
+
if (input.momentKind !== void 0)
|
|
5266
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5267
|
+
input.momentKind,
|
|
5268
|
+
eventId
|
|
5269
|
+
);
|
|
4584
5270
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
4585
5271
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4586
5272
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -4601,6 +5287,8 @@ var ALLOWED = /* @__PURE__ */ new Set([
|
|
|
4601
5287
|
function recordInteraction(db, input) {
|
|
4602
5288
|
return withTransaction(db, () => {
|
|
4603
5289
|
validateSocialEventInput(input);
|
|
5290
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5291
|
+
if (existing) return existing;
|
|
4604
5292
|
if (!ALLOWED.has(input.type)) {
|
|
4605
5293
|
throw new AffinityValidationError("invalid interaction event type");
|
|
4606
5294
|
}
|
|
@@ -4610,11 +5298,12 @@ function recordInteraction(db, input) {
|
|
|
4610
5298
|
assertOwnerParticipates(ownerId, ids);
|
|
4611
5299
|
const now = resolveNow(input.now);
|
|
4612
5300
|
const eventId = insertJournalEvent(db, {
|
|
5301
|
+
...input,
|
|
4613
5302
|
type: input.type,
|
|
4614
5303
|
occurredAt: input.occurredAt,
|
|
4615
|
-
|
|
5304
|
+
occurredOn: input.occurredOn,
|
|
4616
5305
|
significance: input.significance,
|
|
4617
|
-
momentKind: null,
|
|
5306
|
+
momentKind: input.momentKind ?? null,
|
|
4618
5307
|
participants: input.participants,
|
|
4619
5308
|
now
|
|
4620
5309
|
});
|
|
@@ -4622,12 +5311,24 @@ function recordInteraction(db, input) {
|
|
|
4622
5311
|
eventId,
|
|
4623
5312
|
eventType: input.type,
|
|
4624
5313
|
occurredAt: input.occurredAt,
|
|
5314
|
+
occurredOn: input.occurredOn,
|
|
4625
5315
|
significance: input.significance,
|
|
4626
5316
|
participants: input.participants,
|
|
4627
5317
|
provenance: input.provenance,
|
|
4628
5318
|
now
|
|
4629
5319
|
});
|
|
5320
|
+
if (input.momentKind !== void 0)
|
|
5321
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5322
|
+
input.momentKind,
|
|
5323
|
+
eventId
|
|
5324
|
+
);
|
|
4630
5325
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5326
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5327
|
+
{
|
|
5328
|
+
code: "future_event",
|
|
5329
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5330
|
+
}
|
|
5331
|
+
] : void 0,
|
|
4631
5332
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4632
5333
|
affectedLinks: mechanics.affectedLinks,
|
|
4633
5334
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4639,17 +5340,20 @@ function recordInteraction(db, input) {
|
|
|
4639
5340
|
function recordMilestone(db, input) {
|
|
4640
5341
|
return withTransaction(db, () => {
|
|
4641
5342
|
validateSocialEventInput(input);
|
|
5343
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5344
|
+
if (existing) return existing;
|
|
4642
5345
|
const ownerId = requireOwnerContactId(db);
|
|
4643
5346
|
const ids = input.participants.map((p) => p.contactId);
|
|
4644
5347
|
assertParticipantContactsLive(db, ids);
|
|
4645
5348
|
assertOwnerParticipates(ownerId, ids);
|
|
4646
5349
|
const now = resolveNow(input.now);
|
|
4647
5350
|
const eventId = insertJournalEvent(db, {
|
|
5351
|
+
...input,
|
|
4648
5352
|
type: "milestone",
|
|
4649
5353
|
occurredAt: input.occurredAt,
|
|
4650
|
-
|
|
5354
|
+
occurredOn: input.occurredOn,
|
|
4651
5355
|
significance: input.significance,
|
|
4652
|
-
momentKind: null,
|
|
5356
|
+
momentKind: input.momentKind ?? null,
|
|
4653
5357
|
participants: input.participants,
|
|
4654
5358
|
now
|
|
4655
5359
|
});
|
|
@@ -4657,12 +5361,24 @@ function recordMilestone(db, input) {
|
|
|
4657
5361
|
eventId,
|
|
4658
5362
|
eventType: "milestone",
|
|
4659
5363
|
occurredAt: input.occurredAt,
|
|
5364
|
+
occurredOn: input.occurredOn,
|
|
4660
5365
|
significance: input.significance,
|
|
4661
5366
|
participants: input.participants,
|
|
4662
5367
|
provenance: input.provenance,
|
|
4663
5368
|
now
|
|
4664
5369
|
});
|
|
5370
|
+
if (input.momentKind !== void 0)
|
|
5371
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5372
|
+
input.momentKind,
|
|
5373
|
+
eventId
|
|
5374
|
+
);
|
|
4665
5375
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5376
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5377
|
+
{
|
|
5378
|
+
code: "future_event",
|
|
5379
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5380
|
+
}
|
|
5381
|
+
] : void 0,
|
|
4666
5382
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4667
5383
|
affectedLinks: mechanics.affectedLinks,
|
|
4668
5384
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4674,15 +5390,18 @@ function recordMilestone(db, input) {
|
|
|
4674
5390
|
function recordObservation(db, input) {
|
|
4675
5391
|
return withTransaction(db, () => {
|
|
4676
5392
|
validateSocialEventInput(input);
|
|
5393
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5394
|
+
if (existing) return existing;
|
|
4677
5395
|
const ids = input.participants.map((p) => p.contactId);
|
|
4678
5396
|
assertParticipantContactsLive(db, ids);
|
|
4679
5397
|
const now = resolveNow(input.now);
|
|
4680
5398
|
const eventId = insertJournalEvent(db, {
|
|
5399
|
+
...input,
|
|
4681
5400
|
type: "observation",
|
|
4682
5401
|
occurredAt: input.occurredAt,
|
|
4683
|
-
|
|
5402
|
+
occurredOn: input.occurredOn,
|
|
4684
5403
|
significance: input.significance,
|
|
4685
|
-
momentKind: null,
|
|
5404
|
+
momentKind: input.momentKind ?? null,
|
|
4686
5405
|
participants: input.participants,
|
|
4687
5406
|
now
|
|
4688
5407
|
});
|
|
@@ -4690,12 +5409,24 @@ function recordObservation(db, input) {
|
|
|
4690
5409
|
eventId,
|
|
4691
5410
|
eventType: "observation",
|
|
4692
5411
|
occurredAt: input.occurredAt,
|
|
5412
|
+
occurredOn: input.occurredOn,
|
|
4693
5413
|
significance: input.significance,
|
|
4694
5414
|
participants: input.participants,
|
|
4695
5415
|
provenance: input.provenance,
|
|
4696
5416
|
now
|
|
4697
5417
|
});
|
|
5418
|
+
if (input.momentKind !== void 0)
|
|
5419
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5420
|
+
input.momentKind,
|
|
5421
|
+
eventId
|
|
5422
|
+
);
|
|
4698
5423
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5424
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5425
|
+
{
|
|
5426
|
+
code: "future_event",
|
|
5427
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5428
|
+
}
|
|
5429
|
+
] : void 0,
|
|
4699
5430
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4700
5431
|
affectedLinks: mechanics.affectedLinks,
|
|
4701
5432
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4707,17 +5438,20 @@ function recordObservation(db, input) {
|
|
|
4707
5438
|
function recordTransaction(db, input) {
|
|
4708
5439
|
return withTransaction(db, () => {
|
|
4709
5440
|
validateSocialEventInput(input);
|
|
5441
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5442
|
+
if (existing) return existing;
|
|
4710
5443
|
const ownerId = requireOwnerContactId(db);
|
|
4711
5444
|
const ids = input.participants.map((p) => p.contactId);
|
|
4712
5445
|
assertParticipantContactsLive(db, ids);
|
|
4713
5446
|
assertOwnerParticipates(ownerId, ids);
|
|
4714
5447
|
const now = resolveNow(input.now);
|
|
4715
5448
|
const eventId = insertJournalEvent(db, {
|
|
5449
|
+
...input,
|
|
4716
5450
|
type: "transaction",
|
|
4717
5451
|
occurredAt: input.occurredAt,
|
|
4718
|
-
|
|
5452
|
+
occurredOn: input.occurredOn,
|
|
4719
5453
|
significance: input.significance,
|
|
4720
|
-
momentKind: null,
|
|
5454
|
+
momentKind: input.momentKind ?? null,
|
|
4721
5455
|
participants: input.participants,
|
|
4722
5456
|
now
|
|
4723
5457
|
});
|
|
@@ -4725,12 +5459,24 @@ function recordTransaction(db, input) {
|
|
|
4725
5459
|
eventId,
|
|
4726
5460
|
eventType: "transaction",
|
|
4727
5461
|
occurredAt: input.occurredAt,
|
|
5462
|
+
occurredOn: input.occurredOn,
|
|
4728
5463
|
significance: input.significance,
|
|
4729
5464
|
participants: input.participants,
|
|
4730
5465
|
provenance: input.provenance,
|
|
4731
5466
|
now
|
|
4732
5467
|
});
|
|
5468
|
+
if (input.momentKind !== void 0)
|
|
5469
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5470
|
+
input.momentKind,
|
|
5471
|
+
eventId
|
|
5472
|
+
);
|
|
4733
5473
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5474
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5475
|
+
{
|
|
5476
|
+
code: "future_event",
|
|
5477
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5478
|
+
}
|
|
5479
|
+
] : void 0,
|
|
4734
5480
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4735
5481
|
affectedLinks: mechanics.affectedLinks,
|
|
4736
5482
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4803,6 +5549,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4803
5549
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
4804
5550
|
validateSocialEventInput({
|
|
4805
5551
|
occurredAt,
|
|
5552
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4806
5553
|
summary,
|
|
4807
5554
|
significance,
|
|
4808
5555
|
participants,
|
|
@@ -4819,6 +5566,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4819
5566
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4820
5567
|
type: eventType,
|
|
4821
5568
|
occurredAt,
|
|
5569
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4822
5570
|
summary,
|
|
4823
5571
|
significance,
|
|
4824
5572
|
momentKind: null,
|
|
@@ -4852,30 +5600,46 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4852
5600
|
});
|
|
4853
5601
|
}
|
|
4854
5602
|
|
|
4855
|
-
// src/contacts/
|
|
4856
|
-
function
|
|
4857
|
-
|
|
5603
|
+
// src/contacts/export_rows.ts
|
|
5604
|
+
function exportRows(db, sql, ...params) {
|
|
5605
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
4858
5606
|
const result = {};
|
|
4859
5607
|
for (const [key, value] of Object.entries(row)) {
|
|
4860
5608
|
const name = key.replace(
|
|
4861
5609
|
/_([a-z])/g,
|
|
4862
5610
|
(_, letter) => letter.toUpperCase()
|
|
4863
5611
|
);
|
|
4864
|
-
result[name] =
|
|
4865
|
-
|
|
5612
|
+
result[name] = [
|
|
5613
|
+
"affects_bond",
|
|
5614
|
+
"is_owner",
|
|
5615
|
+
"is_structural",
|
|
5616
|
+
"verified",
|
|
5617
|
+
"counts_for_recency",
|
|
5618
|
+
"manual"
|
|
5619
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
5620
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
4866
5621
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4867
5622
|
if (key.endsWith("_json") && typeof value === "string")
|
|
4868
5623
|
result[name] = JSON.parse(value);
|
|
4869
5624
|
}
|
|
4870
5625
|
return result;
|
|
4871
5626
|
});
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
// src/contacts/export_contact.ts
|
|
5630
|
+
function exportContact(db, id) {
|
|
5631
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
4872
5632
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4873
5633
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4874
5634
|
const events = query(
|
|
4875
|
-
"SELECT DISTINCT e
|
|
5635
|
+
"SELECT DISTINCT e.*, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
4876
5636
|
id
|
|
4877
5637
|
).map((event) => ({
|
|
4878
5638
|
...event,
|
|
5639
|
+
attributes: query(
|
|
5640
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5641
|
+
event.id
|
|
5642
|
+
),
|
|
4879
5643
|
participants: query(
|
|
4880
5644
|
"SELECT p.*, c.name AS contact_name FROM event_participants p JOIN contacts c ON c.id = p.contact_id WHERE event_id = ? ORDER BY p.contact_id",
|
|
4881
5645
|
event.id
|
|
@@ -4920,14 +5684,98 @@ function exportContact(db, id) {
|
|
|
4920
5684
|
// src/contacts/export_affinity.ts
|
|
4921
5685
|
function exportAffinity(db) {
|
|
4922
5686
|
const now = Date.now();
|
|
5687
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
5688
|
+
const events = exportRows(
|
|
5689
|
+
db,
|
|
5690
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
5691
|
+
).map((event) => ({
|
|
5692
|
+
...event,
|
|
5693
|
+
attributes: exportRows(
|
|
5694
|
+
db,
|
|
5695
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5696
|
+
event.id
|
|
5697
|
+
),
|
|
5698
|
+
participants: exportRows(
|
|
5699
|
+
db,
|
|
5700
|
+
"SELECT p.contact_id, p.role, c.name AS contact_name FROM event_participants p JOIN contacts c ON c.id = p.contact_id WHERE p.event_id = ? ORDER BY p.contact_id",
|
|
5701
|
+
event.id
|
|
5702
|
+
)
|
|
5703
|
+
}));
|
|
4923
5704
|
return {
|
|
4924
5705
|
version: 1,
|
|
4925
5706
|
exportedAt: now,
|
|
4926
5707
|
exportedAtIso: new Date(now).toISOString(),
|
|
4927
|
-
|
|
5708
|
+
events,
|
|
5709
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
5710
|
+
...record,
|
|
5711
|
+
eventIds: events2.map((event) => event.id),
|
|
5712
|
+
dateIds: dates.map((date) => date.id)
|
|
5713
|
+
}))
|
|
4928
5714
|
};
|
|
4929
5715
|
}
|
|
4930
5716
|
|
|
5717
|
+
// src/events/event_discovery.ts
|
|
5718
|
+
function searchEvents(db, query, options) {
|
|
5719
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5720
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
5721
|
+
if (!tokens.length) return [];
|
|
5722
|
+
const rows = db.prepare(
|
|
5723
|
+
"SELECT id,type,occurred_at,occurred_on,CAST(summary AS BLOB) AS summary,CAST(content AS BLOB) AS content,external_source,external_id,channel FROM events WHERE deleted_at IS NULL ORDER BY occurred_at DESC,id DESC"
|
|
5724
|
+
).all();
|
|
5725
|
+
return rows.flatMap((row) => {
|
|
5726
|
+
const fields = {
|
|
5727
|
+
summary: decodeEventText(row.summary),
|
|
5728
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
5729
|
+
source: row.external_source,
|
|
5730
|
+
id: row.external_id,
|
|
5731
|
+
channel: row.channel
|
|
5732
|
+
};
|
|
5733
|
+
const folded = Object.entries(fields).map(
|
|
5734
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
5735
|
+
);
|
|
5736
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
5737
|
+
return [];
|
|
5738
|
+
return [
|
|
5739
|
+
{
|
|
5740
|
+
kind: "event",
|
|
5741
|
+
id: row.id,
|
|
5742
|
+
type: row.type,
|
|
5743
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
5744
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
5745
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
5746
|
+
...row.external_source && row.external_id ? {
|
|
5747
|
+
externalRef: {
|
|
5748
|
+
source: row.external_source,
|
|
5749
|
+
id: row.external_id
|
|
5750
|
+
}
|
|
5751
|
+
} : {}
|
|
5752
|
+
}
|
|
5753
|
+
];
|
|
5754
|
+
}).slice(offset, offset + limit);
|
|
5755
|
+
}
|
|
5756
|
+
function getEventThread(db, source, id, options) {
|
|
5757
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5758
|
+
const root = getEventByExternalRef(db, source, id);
|
|
5759
|
+
if (!root) return [];
|
|
5760
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
5761
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
5762
|
+
WHERE e.deleted_at IS NULL AND (e.parent_event_id=p.id OR (e.parent_source=p.external_source AND e.parent_ref_id=p.external_id) OR (e.thread_source=? AND e.thread_id=?))
|
|
5763
|
+
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
5764
|
+
root.id,
|
|
5765
|
+
root.externalRef?.source ?? null,
|
|
5766
|
+
root.externalRef?.id ?? null,
|
|
5767
|
+
options?.channel ?? null,
|
|
5768
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
5769
|
+
limit,
|
|
5770
|
+
offset
|
|
5771
|
+
);
|
|
5772
|
+
return batchLoadEventRecords(
|
|
5773
|
+
db,
|
|
5774
|
+
rows.map((r) => r.id),
|
|
5775
|
+
options?.includeContent ?? false
|
|
5776
|
+
);
|
|
5777
|
+
}
|
|
5778
|
+
|
|
4931
5779
|
// src/soul.ts
|
|
4932
5780
|
var soul_exports = {};
|
|
4933
5781
|
__export(soul_exports, {
|
|
@@ -5134,6 +5982,15 @@ var identifyAndLocateContactsSkill = defineAffinitySkill({
|
|
|
5134
5982
|
description: "Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.",
|
|
5135
5983
|
content: `# Identify And Locate Contacts
|
|
5136
5984
|
|
|
5985
|
+
Handle identities normalize leading @ and case. Bluesky uses a domain handle,
|
|
5986
|
+
X/Twitter uses 1\u201315 letters/digits/underscores, Mastodon uses user@host.tld.
|
|
5987
|
+
Native did (alias atproto_did) validates did:<method>:<identifier>; legacy
|
|
5988
|
+
custom:atproto-did locators find the same identity. Keep old handles as identities.
|
|
5989
|
+
An owned identity makes contact creation fail atomically with conflict and
|
|
5990
|
+
existingContactId. input.ifExists="return" returns the matching contact and
|
|
5991
|
+
matchedIdentities without attaching any free identities. CLI: --if-exists return.
|
|
5992
|
+
Name-only possible duplicates still warn. Search @handle is always literal.
|
|
5993
|
+
|
|
5137
5994
|
Primary tools:
|
|
5138
5995
|
- \`search_affinity\`
|
|
5139
5996
|
- \`inspect_affinity_item\`
|
|
@@ -5191,6 +6048,31 @@ var importHistoryWithoutFakingEvidenceSkill = defineAffinitySkill({
|
|
|
5191
6048
|
description: "Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.",
|
|
5192
6049
|
content: `# Import History Without Faking Evidence
|
|
5193
6050
|
|
|
6051
|
+
For social imports, use native did identities (atproto_did is an alias; legacy
|
|
6052
|
+
custom:atproto-did locators remain equivalent). Create with input.ifExists="return"
|
|
6053
|
+
for safe retries: this attaches nothing to an existing contact. For handle changes,
|
|
6054
|
+
call manage_identity add separately with input.ifExists="return"; an identity already
|
|
6055
|
+
on that contact is a no-op, while another owner still conflicts. Seed links with
|
|
6056
|
+
input.ifExists="return" to reuse them without changing their state.
|
|
6057
|
+
Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
|
|
6058
|
+
{op:"event",kind:"interaction",input:{...}}. Per-line savepoints isolate failures;
|
|
6059
|
+
--stop-on-error stops early. Exit 2 identifies malformed JSON; other failures exit 1.
|
|
6060
|
+
Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
|
|
6061
|
+
Participant contact locators may use identities, including did, instead of ids.
|
|
6062
|
+
Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
|
|
6063
|
+
SQLite date-times without offsets mean UTC; microseconds round to milliseconds.
|
|
6064
|
+
Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
|
|
6065
|
+
The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
|
|
6066
|
+
show the mapping and verify an idempotent retry and byte-identical text.
|
|
6067
|
+
|
|
6068
|
+
For archived messages with known timestamps, record actual evidence with externalRef:
|
|
6069
|
+
{source, id} deduplicates retries across event families and commitments. Keep original
|
|
6070
|
+
message text in content, use channel and parentRef/threadRef for context, and keep
|
|
6071
|
+
summary short or let it derive from content. Content is limited to 1 MB UTF-8;
|
|
6072
|
+
keep larger raw archives in the source system. Inspect events by external reference,
|
|
6073
|
+
search scope "events", and use manage_event revise/delete to correct imports.
|
|
6074
|
+
See the tools guide for field details. Existing 0.5.x databases upgrade in place.
|
|
6075
|
+
|
|
5194
6076
|
Primary tools:
|
|
5195
6077
|
- \`manage_contact\`
|
|
5196
6078
|
- \`manage_identity\`
|
|
@@ -5247,6 +6129,14 @@ var managePromisesAndAgreementsSkill = defineAffinitySkill({
|
|
|
5247
6129
|
description: "Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.",
|
|
5248
6130
|
content: `# Manage Promises And Agreements
|
|
5249
6131
|
|
|
6132
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6133
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6134
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6135
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6136
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6137
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6138
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6139
|
+
|
|
5250
6140
|
Primary tools:
|
|
5251
6141
|
- \`manage_commitment\`
|
|
5252
6142
|
- \`review_affinity\`
|
|
@@ -5302,6 +6192,14 @@ var manageRecurringDatesAndRemindersSkill = defineAffinitySkill({
|
|
|
5302
6192
|
description: "Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.",
|
|
5303
6193
|
content: `# Manage Recurring Dates And Reminders
|
|
5304
6194
|
|
|
6195
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6196
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6197
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6198
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6199
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6200
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6201
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6202
|
+
|
|
5305
6203
|
Primary tools:
|
|
5306
6204
|
- \`manage_date_anchor\`
|
|
5307
6205
|
- \`review_affinity\`
|
|
@@ -5413,6 +6311,24 @@ var recordDirectEvidenceWellSkill = defineAffinitySkill({
|
|
|
5413
6311
|
description: "Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.",
|
|
5414
6312
|
content: `# Record Direct Evidence Well
|
|
5415
6313
|
|
|
6314
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6315
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d). Date-only inputs
|
|
6316
|
+
preserve the calendar day at UTC midnight (UTC end of day for dueAt).
|
|
6317
|
+
Keywords use the process local date. Read occurredOn/dueOn and review occursOn
|
|
6318
|
+
for calendar dates; *Iso fields remain UTC. Future events carry future_event
|
|
6319
|
+
warnings and do not count for recency, cadence or drift until their time and day
|
|
6320
|
+
arrive. Commitments and resolutions never reset contact recency or cadence.
|
|
6321
|
+
|
|
6322
|
+
Imported text and event maintenance:
|
|
6323
|
+
- Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
|
|
6324
|
+
- Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.
|
|
6325
|
+
- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.
|
|
6326
|
+
- Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
|
|
6327
|
+
- Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
|
|
6328
|
+
- Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.
|
|
6329
|
+
- Use manage_event delete with confirm: true to remove incorrect evidence and recompute affected relationship scores.
|
|
6330
|
+
- Send large payloads through whole-payload stdin or @file rather than argv. For example, affinity event record < payload.json or affinity call record_event - < payload.json.
|
|
6331
|
+
|
|
5416
6332
|
Primary tools:
|
|
5417
6333
|
- \`record_event\`
|
|
5418
6334
|
- \`inspect_affinity_item\`
|
|
@@ -5471,6 +6387,16 @@ var recordObservationsAndReferralsSkill = defineAffinitySkill({
|
|
|
5471
6387
|
description: "Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.",
|
|
5472
6388
|
content: `# Record Observations And Referrals
|
|
5473
6389
|
|
|
6390
|
+
Likes, reposts and follows are ambient observations, not milestones. Record them
|
|
6391
|
+
with affectsBond:false; observations with significance <=2 default to false.
|
|
6392
|
+
They stay in journals but do not change affinity or meaningful-contact recency.
|
|
6393
|
+
provenance.ambient:true also suppresses effects. Use explicit owner and other
|
|
6394
|
+
participants, actor/recipient roles, and the correct directionality.
|
|
6395
|
+
Unanswered owner_initiated evidence contributes much less and does not reset
|
|
6396
|
+
radar recency. A relationship with only outbound activity gets no routine check-in
|
|
6397
|
+
or drift prompt. other_initiated evidence counts fully. Observations and follow-like
|
|
6398
|
+
milestones never derive breakthroughs; low-significance milestones derive no moment.
|
|
6399
|
+
|
|
5474
6400
|
Primary tools:
|
|
5475
6401
|
- \`record_event\`
|
|
5476
6402
|
- \`review_affinity\`
|
|
@@ -5580,6 +6506,19 @@ var reviewRadarProgressionAndGraphSkill = defineAffinitySkill({
|
|
|
5580
6506
|
description: "Use the review surfaces to prioritize maintenance, understand progression readiness, and read the shape of the relationship graph without guessing from raw rows.",
|
|
5581
6507
|
content: `# Review Radar, Progression, And Graph
|
|
5582
6508
|
|
|
6509
|
+
List reviews accept limit (default 50, maximum 250) and offset. Results include
|
|
6510
|
+
total and hasMore; continue paging while hasMore is true. Event type array flags
|
|
6511
|
+
accept JSON arrays, comma lists, or repeated --event-types flags; invalid types
|
|
6512
|
+
report allowed values with exit 2. Use skill --short for a compact CLI reference.
|
|
6513
|
+
|
|
6514
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6515
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6516
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6517
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6518
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6519
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6520
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6521
|
+
|
|
5583
6522
|
Primary tools:
|
|
5584
6523
|
- \`review_affinity\`
|
|
5585
6524
|
- \`inspect_affinity_item\`
|
|
@@ -5680,6 +6619,9 @@ __export(tools_exports, {
|
|
|
5680
6619
|
manageDuplicateDismissalTool: () => manageDuplicateDismissalTool,
|
|
5681
6620
|
manageDuplicateDismissalToolHandler: () => manageDuplicateDismissalToolHandler,
|
|
5682
6621
|
manageDuplicateDismissalToolName: () => manageDuplicateDismissalToolName,
|
|
6622
|
+
manageEventTool: () => manageEventTool,
|
|
6623
|
+
manageEventToolHandler: () => manageEventToolHandler,
|
|
6624
|
+
manageEventToolName: () => manageEventToolName,
|
|
5683
6625
|
manageIdentityTool: () => manageIdentityTool,
|
|
5684
6626
|
manageIdentityToolHandler: () => manageIdentityToolHandler,
|
|
5685
6627
|
manageIdentityToolName: () => manageIdentityToolName,
|
|
@@ -5724,43 +6666,14 @@ __export(tools_exports, {
|
|
|
5724
6666
|
toolFailure: () => toolFailure,
|
|
5725
6667
|
toolNeedsClarification: () => toolNeedsClarification,
|
|
5726
6668
|
toolNoOp: () => toolNoOp,
|
|
5727
|
-
toolSuccess: () => toolSuccess,
|
|
5728
|
-
toolWarning: () => toolWarning,
|
|
5729
|
-
withToolHandling: () => withToolHandling
|
|
5730
|
-
});
|
|
5731
|
-
|
|
5732
|
-
// src/tools/parse_time_input.ts
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
const fail = () => {
|
|
5736
|
-
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
5737
|
-
};
|
|
5738
|
-
if (typeof value === "number")
|
|
5739
|
-
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
5740
|
-
if (typeof value !== "string") return fail();
|
|
5741
|
-
const text = value.trim();
|
|
5742
|
-
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
5743
|
-
let day;
|
|
5744
|
-
if (relative) {
|
|
5745
|
-
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
5746
|
-
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
5747
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
5748
|
-
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
5749
|
-
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
5750
|
-
return fail();
|
|
5751
|
-
} else {
|
|
5752
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
5753
|
-
text
|
|
5754
|
-
))
|
|
5755
|
-
return fail();
|
|
5756
|
-
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
5757
|
-
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
5758
|
-
return fail();
|
|
5759
|
-
const parsed = Date.parse(text);
|
|
5760
|
-
return Number.isFinite(parsed) ? parsed : fail();
|
|
5761
|
-
}
|
|
5762
|
-
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
5763
|
-
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
6669
|
+
toolSuccess: () => toolSuccess,
|
|
6670
|
+
toolWarning: () => toolWarning,
|
|
6671
|
+
withToolHandling: () => withToolHandling
|
|
6672
|
+
});
|
|
6673
|
+
|
|
6674
|
+
// src/tools/parse_time_input.ts
|
|
6675
|
+
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
6676
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
5764
6677
|
}
|
|
5765
6678
|
|
|
5766
6679
|
// src/tools/tool_metadata.ts
|
|
@@ -5844,6 +6757,39 @@ function oneOfSchema(schemas, description) {
|
|
|
5844
6757
|
return schema;
|
|
5845
6758
|
}
|
|
5846
6759
|
|
|
6760
|
+
// src/tools/event_metadata_schema.ts
|
|
6761
|
+
var externalRefSchema = objectSchema(
|
|
6762
|
+
{
|
|
6763
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
6764
|
+
id: stringSchema("Non-empty external id.")
|
|
6765
|
+
},
|
|
6766
|
+
["source", "id"]
|
|
6767
|
+
);
|
|
6768
|
+
var eventMetadataSchema = {
|
|
6769
|
+
affectsBond: booleanSchema(
|
|
6770
|
+
"Whether evidence affects the relationship; defaults false for observations with significance <=2."
|
|
6771
|
+
),
|
|
6772
|
+
content: stringSchema(
|
|
6773
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
6774
|
+
),
|
|
6775
|
+
externalRef: externalRefSchema,
|
|
6776
|
+
channel: stringSchema(
|
|
6777
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
6778
|
+
),
|
|
6779
|
+
parentRef: externalRefSchema,
|
|
6780
|
+
parentEventId: integerSchema("Parent event id."),
|
|
6781
|
+
threadRef: externalRefSchema,
|
|
6782
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
6783
|
+
"breakthrough",
|
|
6784
|
+
"rupture",
|
|
6785
|
+
"reconciliation",
|
|
6786
|
+
"milestone",
|
|
6787
|
+
"turning_point"
|
|
6788
|
+
]),
|
|
6789
|
+
occurredOn: stringSchema("Calendar day."),
|
|
6790
|
+
provenance: {}
|
|
6791
|
+
};
|
|
6792
|
+
|
|
5847
6793
|
// src/tools/tool_names.ts
|
|
5848
6794
|
var searchAffinityToolName = "search_affinity";
|
|
5849
6795
|
var reviewAffinityToolName = "review_affinity";
|
|
@@ -5857,6 +6803,7 @@ var recordEventToolName = "record_event";
|
|
|
5857
6803
|
var manageCommitmentToolName = "manage_commitment";
|
|
5858
6804
|
var manageDateAnchorToolName = "manage_date_anchor";
|
|
5859
6805
|
var manageAttributeToolName = "manage_attribute";
|
|
6806
|
+
var manageEventToolName = "manage_event";
|
|
5860
6807
|
|
|
5861
6808
|
// src/tools/tool_ref.ts
|
|
5862
6809
|
function contactState(value) {
|
|
@@ -5916,7 +6863,7 @@ function toEventEntityRef(value) {
|
|
|
5916
6863
|
return {
|
|
5917
6864
|
kind: "event",
|
|
5918
6865
|
id: value.id,
|
|
5919
|
-
title: value.summary,
|
|
6866
|
+
title: deriveEventSummary(value.summary),
|
|
5920
6867
|
subtitle: value.type,
|
|
5921
6868
|
state: value.type
|
|
5922
6869
|
};
|
|
@@ -5964,16 +6911,6 @@ function toReceiptEntityRefs(receipt) {
|
|
|
5964
6911
|
});
|
|
5965
6912
|
}
|
|
5966
6913
|
|
|
5967
|
-
// src/identities/normalize.ts
|
|
5968
|
-
function normalizeIdentityKey(type, value) {
|
|
5969
|
-
const t = type.trim().toLowerCase();
|
|
5970
|
-
const v = value.trim().toLowerCase();
|
|
5971
|
-
if (t.length === 0 || v.length === 0) {
|
|
5972
|
-
throw new AffinityValidationError("type and value must be non-empty");
|
|
5973
|
-
}
|
|
5974
|
-
return `${t}:${v}`;
|
|
5975
|
-
}
|
|
5976
|
-
|
|
5977
6914
|
// src/tools/tool_types.ts
|
|
5978
6915
|
function withOptionalFields(result, options) {
|
|
5979
6916
|
if (options.next && options.next.length > 0) {
|
|
@@ -6089,7 +7026,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
6089
7026
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
6090
7027
|
return {
|
|
6091
7028
|
ok: false,
|
|
6092
|
-
result: toolFailure("domain", "
|
|
7029
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
6093
7030
|
};
|
|
6094
7031
|
}
|
|
6095
7032
|
return { ok: true, value: row2 };
|
|
@@ -6114,9 +7051,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
6114
7051
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
6115
7052
|
WHERE i.removed_at IS NULL
|
|
6116
7053
|
AND c.deleted_at IS NULL
|
|
6117
|
-
AND i.normalized_key = ?
|
|
7054
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
6118
7055
|
LIMIT 2`
|
|
6119
|
-
).all(
|
|
7056
|
+
).all(
|
|
7057
|
+
normalizedKey,
|
|
7058
|
+
legacyIdentityKey(normalizedKey),
|
|
7059
|
+
normalizedKey
|
|
7060
|
+
);
|
|
6120
7061
|
if (row.length === 0) {
|
|
6121
7062
|
return {
|
|
6122
7063
|
ok: false,
|
|
@@ -6267,13 +7208,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
6267
7208
|
return { ok: true, value: matchedLink };
|
|
6268
7209
|
}
|
|
6269
7210
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
7211
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
7212
|
+
return error;
|
|
6270
7213
|
if (isAffinityError(error)) {
|
|
6271
|
-
|
|
7214
|
+
const result = toolFailure(
|
|
6272
7215
|
"domain",
|
|
6273
7216
|
affinityErrorCodeToToolCode(error),
|
|
6274
7217
|
summary,
|
|
6275
7218
|
error.message
|
|
6276
7219
|
);
|
|
7220
|
+
const id = error.existingContactId;
|
|
7221
|
+
if (id !== void 0) {
|
|
7222
|
+
result.data = { existingContactId: id };
|
|
7223
|
+
result.error.details = { existingContactId: id };
|
|
7224
|
+
}
|
|
7225
|
+
return result;
|
|
6277
7226
|
}
|
|
6278
7227
|
if (error instanceof Error) {
|
|
6279
7228
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -6312,27 +7261,32 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
6312
7261
|
}
|
|
6313
7262
|
|
|
6314
7263
|
// src/tools/validate_tool_input.ts
|
|
6315
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
7264
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
7265
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
7266
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
7267
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
7268
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
7269
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
6316
7270
|
if (schema.anyOf || schema.oneOf) {
|
|
6317
7271
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
6318
7272
|
return choices.some(
|
|
6319
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
7273
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
6320
7274
|
) ? null : `${path}: invalid value`;
|
|
6321
7275
|
}
|
|
6322
7276
|
if (schema.type === "object") {
|
|
6323
7277
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
6324
7278
|
return `${path}: missing input or expected object`;
|
|
6325
7279
|
const object = value;
|
|
6326
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
7280
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
6327
7281
|
for (const key of Object.keys(object)) {
|
|
6328
7282
|
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
6329
|
-
return key === "kind" && "name" in (schema.properties ?? {}) ? "kind cannot be changed after creation" : `${path}.${key}: unknown patch key; allowed keys: ${Object.keys(schema.properties ?? {}).join(", ")}`;
|
|
7283
|
+
return key === "kind" && "name" in (schema.properties ?? {}) ? "kind cannot be changed after creation" : `${path}.${key}: unknown ${path.endsWith(".patch") ? "patch key" : "key"}; allowed keys: ${Object.keys(schema.properties ?? {}).join(", ")}`;
|
|
6330
7284
|
}
|
|
6331
7285
|
}
|
|
6332
7286
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
6333
7287
|
return `${path}.patch: missing required field`;
|
|
6334
7288
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
6335
|
-
return `${path}.patch: unknown
|
|
7289
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
6336
7290
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
6337
7291
|
try {
|
|
6338
7292
|
return parseTimeInput(
|
|
@@ -6347,11 +7301,14 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6347
7301
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
6348
7302
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
6349
7303
|
try {
|
|
6350
|
-
|
|
7304
|
+
const parsed = parseCalendarTime(
|
|
6351
7305
|
object[key],
|
|
6352
7306
|
`${path}.${key}`,
|
|
6353
7307
|
referenceNow
|
|
6354
7308
|
);
|
|
7309
|
+
object[key] = parsed.timestamp;
|
|
7310
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
7311
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
6355
7312
|
} catch (error) {
|
|
6356
7313
|
return error.message;
|
|
6357
7314
|
}
|
|
@@ -6367,7 +7324,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6367
7324
|
child,
|
|
6368
7325
|
object[key],
|
|
6369
7326
|
`${path}.${key}`,
|
|
6370
|
-
referenceNow
|
|
7327
|
+
referenceNow,
|
|
7328
|
+
strict
|
|
6371
7329
|
);
|
|
6372
7330
|
if (error) return error;
|
|
6373
7331
|
}
|
|
@@ -6380,7 +7338,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6380
7338
|
schema.items ?? {},
|
|
6381
7339
|
item,
|
|
6382
7340
|
`${path}[${index}]`,
|
|
6383
|
-
injectedNow
|
|
7341
|
+
injectedNow,
|
|
7342
|
+
strict
|
|
6384
7343
|
);
|
|
6385
7344
|
if (error) return error;
|
|
6386
7345
|
}
|
|
@@ -6446,6 +7405,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
6446
7405
|
inputError
|
|
6447
7406
|
);
|
|
6448
7407
|
return withToolHandling(() => {
|
|
7408
|
+
if (input.kind === "event") {
|
|
7409
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
7410
|
+
return toolFailure(
|
|
7411
|
+
"protocol",
|
|
7412
|
+
"invalid_input",
|
|
7413
|
+
"Invalid event target.",
|
|
7414
|
+
"Provide exactly one of eventId or ref."
|
|
7415
|
+
);
|
|
7416
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
7417
|
+
return detail2 ? toolSuccess(
|
|
7418
|
+
`Loaded event #${detail2.id}.`,
|
|
7419
|
+
{ kind: "event", detail: detail2 },
|
|
7420
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
7421
|
+
) : toolFailure(
|
|
7422
|
+
"domain",
|
|
7423
|
+
"not_found",
|
|
7424
|
+
"Event not found.",
|
|
7425
|
+
"No event matches the target."
|
|
7426
|
+
);
|
|
7427
|
+
}
|
|
6449
7428
|
if (input.kind === "owner_profile") {
|
|
6450
7429
|
const detail2 = getOwnerProfile(
|
|
6451
7430
|
db,
|
|
@@ -6543,10 +7522,13 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
6543
7522
|
inputSchema: objectSchema(
|
|
6544
7523
|
{
|
|
6545
7524
|
kind: enumSchema("What to inspect.", [
|
|
7525
|
+
"event",
|
|
6546
7526
|
"owner_profile",
|
|
6547
7527
|
"contact_profile",
|
|
6548
7528
|
"link"
|
|
6549
7529
|
]),
|
|
7530
|
+
eventId: integerSchema("Exact event id."),
|
|
7531
|
+
ref: externalRefSchema,
|
|
6550
7532
|
contact: contactLocatorSchema(
|
|
6551
7533
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
6552
7534
|
),
|
|
@@ -6565,6 +7547,7 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
6565
7547
|
// src/attributes/helpers.ts
|
|
6566
7548
|
function clearedAttributePrimary(target) {
|
|
6567
7549
|
return {
|
|
7550
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
6568
7551
|
id: 0,
|
|
6569
7552
|
contactId: target.kind === "contact" ? target.id : null,
|
|
6570
7553
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -6576,6 +7559,7 @@ function clearedAttributePrimary(target) {
|
|
|
6576
7559
|
// src/attributes/mappers.ts
|
|
6577
7560
|
function mapAttributeRowToAttributeRecord(row) {
|
|
6578
7561
|
return {
|
|
7562
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
6579
7563
|
id: row.id,
|
|
6580
7564
|
contactId: row.contact_id,
|
|
6581
7565
|
linkId: row.link_id,
|
|
@@ -6585,49 +7569,26 @@ function mapAttributeRowToAttributeRecord(row) {
|
|
|
6585
7569
|
}
|
|
6586
7570
|
|
|
6587
7571
|
// src/attributes/queries.ts
|
|
7572
|
+
function attributeTargetSql(target) {
|
|
7573
|
+
return {
|
|
7574
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
7575
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
7576
|
+
};
|
|
7577
|
+
}
|
|
6588
7578
|
function getAttributeRowById(db, id) {
|
|
6589
|
-
|
|
6590
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
6591
|
-
FROM attributes WHERE id = ?`
|
|
6592
|
-
).get(id);
|
|
6593
|
-
if (row === void 0) {
|
|
6594
|
-
return null;
|
|
6595
|
-
}
|
|
6596
|
-
return row;
|
|
7579
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
6597
7580
|
}
|
|
6598
7581
|
function getLiveAttributeRow(db, target, name) {
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
WHERE contact_id = ? AND name = ? AND deleted_at IS NULL`
|
|
6604
|
-
).get(target.id, name);
|
|
6605
|
-
if (row2 === void 0) {
|
|
6606
|
-
return null;
|
|
6607
|
-
}
|
|
6608
|
-
return row2;
|
|
6609
|
-
}
|
|
6610
|
-
const row = db.prepare(
|
|
6611
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
6612
|
-
FROM attributes
|
|
6613
|
-
WHERE link_id = ? AND name = ? AND deleted_at IS NULL`
|
|
6614
|
-
).get(target.id, name);
|
|
6615
|
-
if (row === void 0) {
|
|
6616
|
-
return null;
|
|
6617
|
-
}
|
|
6618
|
-
return row;
|
|
7582
|
+
const t = attributeTargetSql(target);
|
|
7583
|
+
return db.prepare(
|
|
7584
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
7585
|
+
).get(t.id, name) ?? null;
|
|
6619
7586
|
}
|
|
6620
7587
|
function listLiveAttributeIdsForTarget(db, target) {
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
return rows2.map((row) => row.id);
|
|
6626
|
-
}
|
|
6627
|
-
const rows = db.prepare(
|
|
6628
|
-
"SELECT id FROM attributes WHERE link_id = ? AND deleted_at IS NULL ORDER BY id"
|
|
6629
|
-
).all(target.id);
|
|
6630
|
-
return rows.map((row) => row.id);
|
|
7588
|
+
const t = attributeTargetSql(target);
|
|
7589
|
+
return db.prepare(
|
|
7590
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
7591
|
+
).all(t.id).map((r) => r.id);
|
|
6631
7592
|
}
|
|
6632
7593
|
|
|
6633
7594
|
// src/attributes/receipts.ts
|
|
@@ -6649,6 +7610,8 @@ function normalizeAttributeValue(value) {
|
|
|
6649
7610
|
if (value === null) {
|
|
6650
7611
|
return null;
|
|
6651
7612
|
}
|
|
7613
|
+
if (typeof value !== "string")
|
|
7614
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
6652
7615
|
const trimmed = value.trim();
|
|
6653
7616
|
return trimmed.length === 0 ? null : trimmed;
|
|
6654
7617
|
}
|
|
@@ -6677,6 +7640,11 @@ function validateAttributeEntries(entries) {
|
|
|
6677
7640
|
return out;
|
|
6678
7641
|
}
|
|
6679
7642
|
function assertAttributeTargetWritable(db, target) {
|
|
7643
|
+
if (target.kind === "event") {
|
|
7644
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
7645
|
+
throw new AffinityNotFoundError("event not found");
|
|
7646
|
+
return;
|
|
7647
|
+
}
|
|
6680
7648
|
if (target.kind === "contact") {
|
|
6681
7649
|
const contact = getContactRowById(db, target.id);
|
|
6682
7650
|
if (!contact) {
|
|
@@ -6712,26 +7680,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
6712
7680
|
}
|
|
6713
7681
|
const created = [];
|
|
6714
7682
|
let lastId = 0;
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
created.push({ kind: "attribute", id: lastId });
|
|
6724
|
-
}
|
|
6725
|
-
} else {
|
|
6726
|
-
const insert = db.prepare(
|
|
6727
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
6728
|
-
VALUES (NULL, ?, ?, ?, ?, ?)`
|
|
6729
|
-
);
|
|
6730
|
-
for (const e of normalized) {
|
|
6731
|
-
const r = insert.run(target.id, e.name, e.value, now, now);
|
|
6732
|
-
lastId = Number(r.lastInsertRowid);
|
|
6733
|
-
created.push({ kind: "attribute", id: lastId });
|
|
6734
|
-
}
|
|
7683
|
+
const t = attributeTargetSql(target);
|
|
7684
|
+
const insert = db.prepare(
|
|
7685
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
7686
|
+
);
|
|
7687
|
+
for (const e of normalized) {
|
|
7688
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
7689
|
+
lastId = Number(r.lastInsertRowid);
|
|
7690
|
+
created.push({ kind: "attribute", id: lastId });
|
|
6735
7691
|
}
|
|
6736
7692
|
if (normalized.length === 0) {
|
|
6737
7693
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -6776,27 +7732,10 @@ function setAttribute(db, target, name, value) {
|
|
|
6776
7732
|
}
|
|
6777
7733
|
);
|
|
6778
7734
|
}
|
|
6779
|
-
|
|
6780
|
-
const r2 = db.prepare(
|
|
6781
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
6782
|
-
VALUES (?, NULL, ?, ?, ?, ?)`
|
|
6783
|
-
).run(target.id, key, stored, now, now);
|
|
6784
|
-
const id2 = Number(r2.lastInsertRowid);
|
|
6785
|
-
const row2 = getAttributeRowById(db, id2);
|
|
6786
|
-
if (!row2) {
|
|
6787
|
-
throw new AffinityInvariantError("attribute row missing after insert");
|
|
6788
|
-
}
|
|
6789
|
-
return buildAttributeMutationReceipt(
|
|
6790
|
-
mapAttributeRowToAttributeRecord(row2),
|
|
6791
|
-
{
|
|
6792
|
-
created: [{ kind: "attribute", id: id2 }]
|
|
6793
|
-
}
|
|
6794
|
-
);
|
|
6795
|
-
}
|
|
7735
|
+
const t = attributeTargetSql(target);
|
|
6796
7736
|
const r = db.prepare(
|
|
6797
|
-
`INSERT INTO attributes (
|
|
6798
|
-
|
|
6799
|
-
).run(target.id, key, stored, now, now);
|
|
7737
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
7738
|
+
).run(t.id, key, stored, now, now);
|
|
6800
7739
|
const id = Number(r.lastInsertRowid);
|
|
6801
7740
|
const row = getAttributeRowById(db, id);
|
|
6802
7741
|
if (!row) {
|
|
@@ -6852,14 +7791,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
6852
7791
|
affectedLinks: receipt.affectedLinks,
|
|
6853
7792
|
derivedEffects: receipt.derivedEffects
|
|
6854
7793
|
};
|
|
6855
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
7794
|
+
const summary = changedCount === 0 ? receipt.primary && typeof receipt.primary === "object" && "externalRef" in receipt.primary && "id" in receipt.primary ? `Event already recorded (#${receipt.primary.id}, ${receipt.primary.externalRef.source} ${receipt.primary.externalRef.id}).` : `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
6856
7795
|
if (changedCount === 0) {
|
|
6857
7796
|
return toolNoOp(summary, data, {
|
|
6858
7797
|
entities: toReceiptEntityRefs(receipt)
|
|
6859
7798
|
});
|
|
6860
7799
|
}
|
|
6861
7800
|
return toolSuccess(summary, data, {
|
|
6862
|
-
entities: toReceiptEntityRefs(receipt)
|
|
7801
|
+
entities: toReceiptEntityRefs(receipt),
|
|
7802
|
+
warnings: receipt.warnings
|
|
6863
7803
|
});
|
|
6864
7804
|
}
|
|
6865
7805
|
var PLURALS = { identity: "identities" };
|
|
@@ -6916,6 +7856,7 @@ function linkLocatorSchema2(description) {
|
|
|
6916
7856
|
};
|
|
6917
7857
|
}
|
|
6918
7858
|
function resolveAttributeTarget(db, target) {
|
|
7859
|
+
if (target.kind === "event") return { kind: "ok", target };
|
|
6919
7860
|
if (target.kind === "contact") {
|
|
6920
7861
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
6921
7862
|
if (!contact.ok) {
|
|
@@ -6975,7 +7916,7 @@ function manageAttributeToolHandler(db, rawInput) {
|
|
|
6975
7916
|
}
|
|
6976
7917
|
var manageAttributeTool = defineAffinityTool({
|
|
6977
7918
|
name: manageAttributeToolName,
|
|
6978
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
7919
|
+
description: "Set, unset, or replace custom metadata attributes on contacts, links and events. For emails, phones, handles, and URLs use manage_identity instead \u2014 those are routing keys, not attributes.",
|
|
6979
7920
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
6980
7921
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
6981
7922
|
sideEffects: "writes_state",
|
|
@@ -6996,7 +7937,8 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
6996
7937
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
6997
7938
|
target: objectSchema(
|
|
6998
7939
|
{
|
|
6999
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
7940
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
7941
|
+
eventId: integerSchema("Target event id."),
|
|
7000
7942
|
contact: contactLocatorSchema2(
|
|
7001
7943
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
7002
7944
|
),
|
|
@@ -7026,7 +7968,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
7026
7968
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
7027
7969
|
},
|
|
7028
7970
|
["action"],
|
|
7029
|
-
"Set, unset, or replace attributes on contacts and
|
|
7971
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
7030
7972
|
),
|
|
7031
7973
|
handler: manageAttributeToolHandler
|
|
7032
7974
|
});
|
|
@@ -7058,7 +8000,13 @@ function prepareParticipants(db, raw) {
|
|
|
7058
8000
|
}
|
|
7059
8001
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
7060
8002
|
const input = structuredClone(rawInput);
|
|
7061
|
-
const inputError = validateToolInput(
|
|
8003
|
+
const inputError = validateToolInput(
|
|
8004
|
+
manageCommitmentTool.inputSchema,
|
|
8005
|
+
input,
|
|
8006
|
+
"input",
|
|
8007
|
+
Date.now(),
|
|
8008
|
+
true
|
|
8009
|
+
);
|
|
7062
8010
|
if (inputError)
|
|
7063
8011
|
return toolFailure(
|
|
7064
8012
|
"protocol",
|
|
@@ -7088,6 +8036,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
7088
8036
|
"commitment"
|
|
7089
8037
|
);
|
|
7090
8038
|
}
|
|
8039
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
8040
|
+
return toolFailure(
|
|
8041
|
+
"protocol",
|
|
8042
|
+
"invalid_input",
|
|
8043
|
+
"Missing commitment.",
|
|
8044
|
+
"commitmentEventId is required for resolve."
|
|
8045
|
+
);
|
|
8046
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
8047
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
8048
|
+
return toolFailure(
|
|
8049
|
+
"domain",
|
|
8050
|
+
"invalid_input",
|
|
8051
|
+
"Invalid commitment.",
|
|
8052
|
+
"not a commitment event"
|
|
8053
|
+
);
|
|
7091
8054
|
return mutationToolResult(
|
|
7092
8055
|
"resolve",
|
|
7093
8056
|
resolveCommitment(
|
|
@@ -7126,15 +8089,21 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7126
8089
|
"promise",
|
|
7127
8090
|
"agreement"
|
|
7128
8091
|
]),
|
|
8092
|
+
...eventMetadataSchema,
|
|
7129
8093
|
occurredAt: integerSchema(
|
|
7130
8094
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
7131
8095
|
),
|
|
7132
8096
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
7133
8097
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
7134
8098
|
dueAt: integerSchema("Optional due timestamp."),
|
|
8099
|
+
dueOn: stringSchema("Due calendar day."),
|
|
7135
8100
|
participants: arraySchema(
|
|
7136
8101
|
objectSchema(
|
|
7137
8102
|
{
|
|
8103
|
+
contact: objectSchema(
|
|
8104
|
+
{ contactId: integerSchema("Contact id.") },
|
|
8105
|
+
["contactId"]
|
|
8106
|
+
),
|
|
7138
8107
|
contactId: integerSchema(
|
|
7139
8108
|
"Participant contact id. The owner contact MUST be included."
|
|
7140
8109
|
),
|
|
@@ -7147,13 +8116,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7147
8116
|
),
|
|
7148
8117
|
now: integerSchema("Optional timestamp override.")
|
|
7149
8118
|
},
|
|
7150
|
-
[
|
|
7151
|
-
"commitmentType",
|
|
7152
|
-
"occurredAt",
|
|
7153
|
-
"summary",
|
|
7154
|
-
"significance",
|
|
7155
|
-
"participants"
|
|
7156
|
-
],
|
|
8119
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
7157
8120
|
"Commitment payload. Required when action=record."
|
|
7158
8121
|
),
|
|
7159
8122
|
commitmentEventId: integerSchema(
|
|
@@ -7168,6 +8131,19 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7168
8131
|
now: integerSchema("Optional timestamp."),
|
|
7169
8132
|
resolvingEvent: objectSchema(
|
|
7170
8133
|
{
|
|
8134
|
+
occurredOn: stringSchema("Calendar day."),
|
|
8135
|
+
provenance: {},
|
|
8136
|
+
participants: arraySchema(
|
|
8137
|
+
objectSchema(
|
|
8138
|
+
{
|
|
8139
|
+
contactId: integerSchema("Contact id."),
|
|
8140
|
+
role: stringSchema("Role."),
|
|
8141
|
+
directionality: stringSchema("Directionality.")
|
|
8142
|
+
},
|
|
8143
|
+
["contactId", "role"]
|
|
8144
|
+
),
|
|
8145
|
+
"Resolving participants."
|
|
8146
|
+
),
|
|
7171
8147
|
type: stringSchema("Optional resolving event type."),
|
|
7172
8148
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
7173
8149
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -7180,227 +8156,75 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7180
8156
|
},
|
|
7181
8157
|
[],
|
|
7182
8158
|
"Optional metadata."
|
|
7183
|
-
)
|
|
7184
|
-
},
|
|
7185
|
-
["action"],
|
|
7186
|
-
"Record or resolve one commitment."
|
|
7187
|
-
),
|
|
7188
|
-
handler: manageCommitmentToolHandler
|
|
7189
|
-
});
|
|
7190
|
-
|
|
7191
|
-
// src/contacts/delete_contact.ts
|
|
7192
|
-
function deleteContact(db, locatorId, options) {
|
|
7193
|
-
if (options?.confirm !== true)
|
|
7194
|
-
throw new AffinityValidationError(
|
|
7195
|
-
"delete is permanent; pass confirm: true"
|
|
7196
|
-
);
|
|
7197
|
-
return withTransaction(db, () => {
|
|
7198
|
-
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
7199
|
-
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
7200
|
-
if (contact.is_owner)
|
|
7201
|
-
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
7202
|
-
const tables = [
|
|
7203
|
-
"contacts",
|
|
7204
|
-
"identities",
|
|
7205
|
-
"attributes",
|
|
7206
|
-
"links",
|
|
7207
|
-
"events",
|
|
7208
|
-
"event_participants",
|
|
7209
|
-
"open_commitments",
|
|
7210
|
-
"contact_merges",
|
|
7211
|
-
"dismissed_duplicates",
|
|
7212
|
-
"link_event_effects",
|
|
7213
|
-
"upcoming_occurrences",
|
|
7214
|
-
"link_rollups",
|
|
7215
|
-
"contact_rollups"
|
|
7216
|
-
];
|
|
7217
|
-
const before = new Map(
|
|
7218
|
-
tables.map((table) => [
|
|
7219
|
-
table,
|
|
7220
|
-
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
7221
|
-
])
|
|
7222
|
-
);
|
|
7223
|
-
db.prepare(
|
|
7224
|
-
"DELETE FROM events WHERE id IN (SELECT p.event_id FROM event_participants p WHERE p.contact_id = ? AND NOT EXISTS (SELECT 1 FROM event_participants q JOIN contacts c ON c.id = q.contact_id WHERE q.event_id = p.event_id AND q.contact_id != ? AND c.is_owner = 0))"
|
|
7225
|
-
).run(locatorId, locatorId);
|
|
7226
|
-
db.prepare(
|
|
7227
|
-
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
7228
|
-
).run(locatorId, locatorId);
|
|
7229
|
-
db.prepare(
|
|
7230
|
-
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7231
|
-
).run(locatorId, locatorId);
|
|
7232
|
-
db.prepare(
|
|
7233
|
-
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
7234
|
-
).run(locatorId);
|
|
7235
|
-
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
7236
|
-
const now = Date.now();
|
|
7237
|
-
const affectedLinks = db.prepare(
|
|
7238
|
-
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
7239
|
-
).all().map((row) => row.id);
|
|
7240
|
-
refreshAllBridgeScores(db, now);
|
|
7241
|
-
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
7242
|
-
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
7243
|
-
refreshContactRollup(db, Number(row.id), now);
|
|
7244
|
-
const removed = tables.map((table) => {
|
|
7245
|
-
const remaining = new Set(
|
|
7246
|
-
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
7247
|
-
);
|
|
7248
|
-
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
7249
|
-
return { entity: table, count: ids.length, ids };
|
|
7250
|
-
}).filter((item) => item.count > 0);
|
|
7251
|
-
return { contactId: locatorId, removed, affectedLinks };
|
|
7252
|
-
});
|
|
7253
|
-
}
|
|
7254
|
-
|
|
7255
|
-
// src/identities/queries.ts
|
|
7256
|
-
function getIdentityRowById(db, id) {
|
|
7257
|
-
const row = db.prepare(
|
|
7258
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
7259
|
-
created_at, updated_at, removed_at
|
|
7260
|
-
FROM identities
|
|
7261
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
7262
|
-
).get(id);
|
|
7263
|
-
if (row === void 0) {
|
|
7264
|
-
return null;
|
|
7265
|
-
}
|
|
7266
|
-
return row;
|
|
7267
|
-
}
|
|
7268
|
-
|
|
7269
|
-
// src/identities/receipts.ts
|
|
7270
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
7271
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
7272
|
-
return {
|
|
7273
|
-
primary,
|
|
7274
|
-
created: normalized.created,
|
|
7275
|
-
updated: normalized.updated,
|
|
7276
|
-
archived: normalized.archived,
|
|
7277
|
-
removed: normalized.removed,
|
|
7278
|
-
affectedLinks: normalized.affectedLinks,
|
|
7279
|
-
derivedEffects: normalized.derivedEffects
|
|
7280
|
-
};
|
|
7281
|
-
}
|
|
7282
|
-
|
|
7283
|
-
// src/identities/validators.ts
|
|
7284
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
7285
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
7286
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7287
|
-
).get(normalizedKey) : db.prepare(
|
|
7288
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
7289
|
-
).get(normalizedKey, excludeIdentityId);
|
|
7290
|
-
if (row !== void 0) {
|
|
7291
|
-
const owner = db.prepare(
|
|
7292
|
-
"SELECT c.id, c.name FROM contacts c JOIN identities i ON i.contact_id = c.id WHERE i.normalized_key = ? AND i.removed_at IS NULL"
|
|
7293
|
-
).get(normalizedKey);
|
|
7294
|
-
throw new AffinityConflictError(
|
|
7295
|
-
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
7296
|
-
);
|
|
7297
|
-
}
|
|
7298
|
-
}
|
|
7299
|
-
|
|
7300
|
-
// src/identities/validate_identity_input.ts
|
|
7301
|
-
var identityTypes = [
|
|
7302
|
-
"email",
|
|
7303
|
-
"phone",
|
|
7304
|
-
"url",
|
|
7305
|
-
"website",
|
|
7306
|
-
"handle",
|
|
7307
|
-
"x",
|
|
7308
|
-
"twitter",
|
|
7309
|
-
"linkedin",
|
|
7310
|
-
"github",
|
|
7311
|
-
"mastodon",
|
|
7312
|
-
"bluesky",
|
|
7313
|
-
"telegram",
|
|
7314
|
-
"whatsapp",
|
|
7315
|
-
"signal",
|
|
7316
|
-
"matrix",
|
|
7317
|
-
"discord",
|
|
7318
|
-
"slack",
|
|
7319
|
-
"instagram",
|
|
7320
|
-
"facebook",
|
|
7321
|
-
"youtube",
|
|
7322
|
-
"tiktok",
|
|
7323
|
-
"address",
|
|
7324
|
-
"other",
|
|
7325
|
-
"account_id",
|
|
7326
|
-
"alias"
|
|
7327
|
-
];
|
|
7328
|
-
function validateIdentityInput(type, value) {
|
|
7329
|
-
const kind = type.toLowerCase();
|
|
7330
|
-
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
7331
|
-
throw new AffinityValidationError(
|
|
7332
|
-
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
7333
|
-
);
|
|
7334
|
-
if (!value.trim())
|
|
7335
|
-
throw new AffinityValidationError("identity value must be non-empty");
|
|
7336
|
-
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
7337
|
-
throw new AffinityValidationError(
|
|
7338
|
-
"email value must match local@domain.tld"
|
|
7339
|
-
);
|
|
7340
|
-
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
7341
|
-
throw new AffinityValidationError(
|
|
7342
|
-
"phone value must contain at least 5 digits"
|
|
7343
|
-
);
|
|
7344
|
-
if (kind === "url" || kind === "website") {
|
|
7345
|
-
try {
|
|
7346
|
-
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
7347
|
-
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
7348
|
-
throw new Error();
|
|
7349
|
-
} catch {
|
|
7350
|
-
throw new AffinityValidationError(
|
|
7351
|
-
"url/website value must be an http/https URL or bare domain"
|
|
7352
|
-
);
|
|
7353
|
-
}
|
|
7354
|
-
}
|
|
7355
|
-
}
|
|
8159
|
+
)
|
|
8160
|
+
},
|
|
8161
|
+
["action"],
|
|
8162
|
+
"Record or resolve one commitment."
|
|
8163
|
+
),
|
|
8164
|
+
handler: manageCommitmentToolHandler
|
|
8165
|
+
});
|
|
7356
8166
|
|
|
7357
|
-
// src/
|
|
7358
|
-
function
|
|
8167
|
+
// src/contacts/delete_contact.ts
|
|
8168
|
+
function deleteContact(db, locatorId, options) {
|
|
8169
|
+
if (options?.confirm !== true)
|
|
8170
|
+
throw new AffinityValidationError(
|
|
8171
|
+
"delete is permanent; pass confirm: true"
|
|
8172
|
+
);
|
|
7359
8173
|
return withTransaction(db, () => {
|
|
7360
|
-
const contact =
|
|
7361
|
-
if (!contact)
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
value,
|
|
7385
|
-
input.label ?? null,
|
|
7386
|
-
nk,
|
|
7387
|
-
verified,
|
|
7388
|
-
verifiedAt,
|
|
7389
|
-
now,
|
|
7390
|
-
now
|
|
8174
|
+
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
8175
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
8176
|
+
if (contact.is_owner)
|
|
8177
|
+
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
8178
|
+
const tables = [
|
|
8179
|
+
"contacts",
|
|
8180
|
+
"identities",
|
|
8181
|
+
"attributes",
|
|
8182
|
+
"links",
|
|
8183
|
+
"events",
|
|
8184
|
+
"event_participants",
|
|
8185
|
+
"open_commitments",
|
|
8186
|
+
"contact_merges",
|
|
8187
|
+
"dismissed_duplicates",
|
|
8188
|
+
"link_event_effects",
|
|
8189
|
+
"upcoming_occurrences",
|
|
8190
|
+
"link_rollups",
|
|
8191
|
+
"contact_rollups"
|
|
8192
|
+
];
|
|
8193
|
+
const before = new Map(
|
|
8194
|
+
tables.map((table) => [
|
|
8195
|
+
table,
|
|
8196
|
+
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
8197
|
+
])
|
|
7391
8198
|
);
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
8199
|
+
db.prepare(
|
|
8200
|
+
"DELETE FROM events WHERE id IN (SELECT p.event_id FROM event_participants p WHERE p.contact_id = ? AND NOT EXISTS (SELECT 1 FROM event_participants q JOIN contacts c ON c.id = q.contact_id WHERE q.event_id = p.event_id AND q.contact_id != ? AND c.is_owner = 0))"
|
|
8201
|
+
).run(locatorId, locatorId);
|
|
8202
|
+
db.prepare(
|
|
8203
|
+
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
8204
|
+
).run(locatorId, locatorId);
|
|
7398
8205
|
db.prepare(
|
|
7399
8206
|
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7400
|
-
).run(
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
8207
|
+
).run(locatorId, locatorId);
|
|
8208
|
+
db.prepare(
|
|
8209
|
+
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
8210
|
+
).run(locatorId);
|
|
8211
|
+
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
8212
|
+
const now = Date.now();
|
|
8213
|
+
const affectedLinks = db.prepare(
|
|
8214
|
+
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
8215
|
+
).all().map((row) => row.id);
|
|
8216
|
+
refreshAllBridgeScores(db, now);
|
|
8217
|
+
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
8218
|
+
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
8219
|
+
refreshContactRollup(db, Number(row.id), now);
|
|
8220
|
+
const removed = tables.map((table) => {
|
|
8221
|
+
const remaining = new Set(
|
|
8222
|
+
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
8223
|
+
);
|
|
8224
|
+
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
8225
|
+
return { entity: table, count: ids.length, ids };
|
|
8226
|
+
}).filter((item) => item.count > 0);
|
|
8227
|
+
return { contactId: locatorId, removed, affectedLinks };
|
|
7404
8228
|
});
|
|
7405
8229
|
}
|
|
7406
8230
|
|
|
@@ -7408,27 +8232,20 @@ function addIdentity(db, contactId, input) {
|
|
|
7408
8232
|
function createContactToolResult(db, input) {
|
|
7409
8233
|
return withTransaction(db, () => {
|
|
7410
8234
|
const receipt = createContact(db, input);
|
|
7411
|
-
const identityOwners = /* @__PURE__ */ new Set();
|
|
7412
|
-
for (const identity of input.identities ?? []) {
|
|
7413
|
-
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
7414
|
-
const existing = db.prepare(
|
|
7415
|
-
"SELECT contact_id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7416
|
-
).get(normalizeIdentityKey(identity.type, identity.value));
|
|
7417
|
-
if (existing) {
|
|
7418
|
-
if (Number(existing.contact_id) !== receipt.primary.id)
|
|
7419
|
-
identityOwners.add(Number(existing.contact_id));
|
|
7420
|
-
continue;
|
|
7421
|
-
}
|
|
7422
|
-
const added = addIdentity(db, receipt.primary.id, identity);
|
|
7423
|
-
receipt.created.push(...added.created);
|
|
7424
|
-
}
|
|
7425
8235
|
const result = mutationToolResult("create", receipt, "contact");
|
|
8236
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
8237
|
+
result.outcome = "no_op";
|
|
8238
|
+
Object.assign(result.data, {
|
|
8239
|
+
matchedIdentities: receipt.matchedIdentities
|
|
8240
|
+
});
|
|
8241
|
+
return result;
|
|
8242
|
+
}
|
|
7426
8243
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
7427
8244
|
const warnings = db.prepare(
|
|
7428
8245
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
7429
8246
|
).all(receipt.primary.id).flatMap((row) => {
|
|
7430
|
-
const score =
|
|
7431
|
-
return score
|
|
8247
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
8248
|
+
return score <= 0 ? [] : [
|
|
7432
8249
|
{
|
|
7433
8250
|
code: "possible_duplicate",
|
|
7434
8251
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -7511,6 +8328,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
7511
8328
|
return contact.result;
|
|
7512
8329
|
}
|
|
7513
8330
|
if (input.action === "delete") {
|
|
8331
|
+
if (contact.value.is_owner)
|
|
8332
|
+
return toolFailure(
|
|
8333
|
+
"domain",
|
|
8334
|
+
"conflict",
|
|
8335
|
+
"Cannot delete owner.",
|
|
8336
|
+
"The owner contact cannot be deleted"
|
|
8337
|
+
);
|
|
7514
8338
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
7515
8339
|
return toolSuccess("Permanently deleted contact.", {
|
|
7516
8340
|
action: "delete",
|
|
@@ -7584,7 +8408,11 @@ var manageContactTool = defineAffinityTool({
|
|
|
7584
8408
|
},
|
|
7585
8409
|
["type", "value"]
|
|
7586
8410
|
),
|
|
7587
|
-
"Optional identities to attach.
|
|
8411
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
8412
|
+
),
|
|
8413
|
+
ifExists: enumSchema(
|
|
8414
|
+
"Identity collision policy; return attaches nothing.",
|
|
8415
|
+
["error", "return"]
|
|
7588
8416
|
),
|
|
7589
8417
|
name: stringSchema("Contact name."),
|
|
7590
8418
|
kind: enumSchema("Contact kind.", [
|
|
@@ -8003,7 +8831,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
8003
8831
|
});
|
|
8004
8832
|
}
|
|
8005
8833
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
8006
|
-
const nextValue =
|
|
8834
|
+
const nextValue = normalizeIdentityValue(
|
|
8835
|
+
nextType,
|
|
8836
|
+
patch.value ?? row.value
|
|
8837
|
+
);
|
|
8007
8838
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
8008
8839
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
8009
8840
|
}
|
|
@@ -8062,6 +8893,10 @@ function contactLocatorSchema6(description) {
|
|
|
8062
8893
|
contactId: integerSchema("Exact contact id."),
|
|
8063
8894
|
identity: objectSchema(
|
|
8064
8895
|
{
|
|
8896
|
+
ifExists: enumSchema(
|
|
8897
|
+
"Return an existing identity on this contact without changes.",
|
|
8898
|
+
["error", "return"]
|
|
8899
|
+
),
|
|
8065
8900
|
type: stringSchema(
|
|
8066
8901
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8067
8902
|
),
|
|
@@ -8158,6 +8993,10 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
8158
8993
|
),
|
|
8159
8994
|
input: objectSchema(
|
|
8160
8995
|
{
|
|
8996
|
+
ifExists: enumSchema(
|
|
8997
|
+
"Return an existing identity on this contact without changes.",
|
|
8998
|
+
["error", "return"]
|
|
8999
|
+
),
|
|
8161
9000
|
type: stringSchema(
|
|
8162
9001
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8163
9002
|
),
|
|
@@ -8492,11 +9331,13 @@ var mergeContactsTool = defineAffinityTool({
|
|
|
8492
9331
|
});
|
|
8493
9332
|
|
|
8494
9333
|
// src/tools/record_event_tool.ts
|
|
8495
|
-
function normalizeParticipant2(p) {
|
|
9334
|
+
function normalizeParticipant2(db, p) {
|
|
8496
9335
|
let contactId = p.contactId;
|
|
8497
9336
|
if (contactId === void 0 && p.contact != null) {
|
|
8498
9337
|
const loc = p.contact;
|
|
8499
|
-
|
|
9338
|
+
const resolved = resolveContactLocator(db, loc);
|
|
9339
|
+
if (!resolved.ok) throw resolved.result;
|
|
9340
|
+
contactId = resolved.value.id;
|
|
8500
9341
|
}
|
|
8501
9342
|
const result = {
|
|
8502
9343
|
contactId,
|
|
@@ -8514,12 +9355,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
8514
9355
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
8515
9356
|
}
|
|
8516
9357
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
8517
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
9358
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
8518
9359
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
8519
9360
|
}
|
|
8520
9361
|
function recordEventToolHandler(db, rawInput) {
|
|
8521
9362
|
const input = structuredClone(rawInput);
|
|
8522
|
-
const inputError = validateToolInput(
|
|
9363
|
+
const inputError = validateToolInput(
|
|
9364
|
+
recordEventTool.inputSchema,
|
|
9365
|
+
input,
|
|
9366
|
+
"input",
|
|
9367
|
+
Date.now(),
|
|
9368
|
+
true
|
|
9369
|
+
);
|
|
8523
9370
|
if (inputError)
|
|
8524
9371
|
return toolFailure(
|
|
8525
9372
|
"protocol",
|
|
@@ -8599,6 +9446,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
8599
9446
|
"correction"
|
|
8600
9447
|
]
|
|
8601
9448
|
),
|
|
9449
|
+
...eventMetadataSchema,
|
|
8602
9450
|
occurredAt: integerSchema(
|
|
8603
9451
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
8604
9452
|
),
|
|
@@ -8607,6 +9455,19 @@ var recordEventTool = defineAffinityTool({
|
|
|
8607
9455
|
participants: arraySchema(
|
|
8608
9456
|
objectSchema(
|
|
8609
9457
|
{
|
|
9458
|
+
contact: objectSchema(
|
|
9459
|
+
{
|
|
9460
|
+
contactId: integerSchema("Contact id."),
|
|
9461
|
+
identity: objectSchema(
|
|
9462
|
+
{
|
|
9463
|
+
type: stringSchema("Identity type."),
|
|
9464
|
+
value: stringSchema("Identity value.")
|
|
9465
|
+
},
|
|
9466
|
+
["type", "value"]
|
|
9467
|
+
)
|
|
9468
|
+
},
|
|
9469
|
+
[]
|
|
9470
|
+
),
|
|
8610
9471
|
contactId: integerSchema(
|
|
8611
9472
|
"Participant contact id. The owner contact MUST be included."
|
|
8612
9473
|
),
|
|
@@ -8630,7 +9491,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
8630
9491
|
),
|
|
8631
9492
|
now: integerSchema("Optional timestamp override.")
|
|
8632
9493
|
},
|
|
8633
|
-
["occurredAt", "
|
|
9494
|
+
["occurredAt", "significance", "participants"],
|
|
8634
9495
|
"Event payload. Always required. Interaction events also require `type`."
|
|
8635
9496
|
)
|
|
8636
9497
|
},
|
|
@@ -8669,13 +9530,40 @@ function enrichReviewItem(db, item) {
|
|
|
8669
9530
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8670
9531
|
}
|
|
8671
9532
|
if (Array.isArray(item.participants)) {
|
|
8672
|
-
result.
|
|
8673
|
-
(p) =>
|
|
9533
|
+
result.participants = item.participants.map(
|
|
9534
|
+
(p) => ({
|
|
9535
|
+
id: p.contactId,
|
|
9536
|
+
name: name(p.contactId),
|
|
9537
|
+
role: p.role
|
|
9538
|
+
})
|
|
8674
9539
|
);
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
9540
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
9541
|
+
result[key] = void 0;
|
|
9542
|
+
}
|
|
9543
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
9544
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
9545
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
9546
|
+
if (result.title === result.summary) result.title = void 0;
|
|
9547
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
9548
|
+
for (const key of [
|
|
9549
|
+
"affinity",
|
|
9550
|
+
"trust",
|
|
9551
|
+
"rank",
|
|
9552
|
+
"recencyScore",
|
|
9553
|
+
"driftPriority",
|
|
9554
|
+
"readinessScore",
|
|
9555
|
+
"normalizedRank",
|
|
9556
|
+
"reciprocityScore",
|
|
9557
|
+
"radarScore",
|
|
9558
|
+
"subtitle",
|
|
9559
|
+
"snippet",
|
|
9560
|
+
"targetRef",
|
|
9561
|
+
"occursOnDate"
|
|
9562
|
+
])
|
|
9563
|
+
result[key] = void 0;
|
|
9564
|
+
} else if (item.kind === "link") {
|
|
9565
|
+
result.recommendedReason = void 0;
|
|
9566
|
+
result.snippet = void 0;
|
|
8679
9567
|
}
|
|
8680
9568
|
if (typeof item.leftContactId === "number")
|
|
8681
9569
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -8704,6 +9592,8 @@ function summarizeCount(count, singular) {
|
|
|
8704
9592
|
// src/tools/review_affinity_tool.ts
|
|
8705
9593
|
function buildReadOptions(input) {
|
|
8706
9594
|
return {
|
|
9595
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
9596
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
8707
9597
|
...input.now === void 0 ? {} : { now: input.now },
|
|
8708
9598
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
8709
9599
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -8840,7 +9730,7 @@ function momentItem(moment) {
|
|
|
8840
9730
|
id: moment.eventId,
|
|
8841
9731
|
title: moment.summary,
|
|
8842
9732
|
subtitle: moment.momentKind,
|
|
8843
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
9733
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8844
9734
|
};
|
|
8845
9735
|
}
|
|
8846
9736
|
function commitmentItem(commitment) {
|
|
@@ -8850,7 +9740,7 @@ function commitmentItem(commitment) {
|
|
|
8850
9740
|
id: commitment.eventId,
|
|
8851
9741
|
title: commitment.summary,
|
|
8852
9742
|
subtitle: commitment.type,
|
|
8853
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
9743
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8854
9744
|
};
|
|
8855
9745
|
}
|
|
8856
9746
|
function radarItem(radar) {
|
|
@@ -8866,6 +9756,7 @@ function radarItem(radar) {
|
|
|
8866
9756
|
function upcomingDateItem(date) {
|
|
8867
9757
|
return {
|
|
8868
9758
|
...date,
|
|
9759
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
8869
9760
|
occursAt: date.occursOn,
|
|
8870
9761
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
8871
9762
|
kind: date.targetRef.kind,
|
|
@@ -8903,6 +9794,9 @@ function compactFilters(input) {
|
|
|
8903
9794
|
);
|
|
8904
9795
|
}
|
|
8905
9796
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
9797
|
+
return reviewPage(db, rawInput, false);
|
|
9798
|
+
}
|
|
9799
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
8906
9800
|
const input = structuredClone(rawInput);
|
|
8907
9801
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8908
9802
|
if (inputError)
|
|
@@ -8917,6 +9811,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8917
9811
|
if (invalid) {
|
|
8918
9812
|
return invalid;
|
|
8919
9813
|
}
|
|
9814
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
9815
|
+
const limit = input.limit ?? 50;
|
|
9816
|
+
const offset = input.offset ?? 0;
|
|
9817
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
9818
|
+
return toolFailure(
|
|
9819
|
+
"protocol",
|
|
9820
|
+
"invalid_input",
|
|
9821
|
+
"Invalid pagination.",
|
|
9822
|
+
"limit must be 1..250; offset must be non-negative"
|
|
9823
|
+
);
|
|
9824
|
+
const all = [];
|
|
9825
|
+
let page;
|
|
9826
|
+
do {
|
|
9827
|
+
page = reviewPage(
|
|
9828
|
+
db,
|
|
9829
|
+
{ ...input, limit: 250, offset: all.length },
|
|
9830
|
+
true
|
|
9831
|
+
);
|
|
9832
|
+
if (!page.ok) return page;
|
|
9833
|
+
all.push(...page.data.items);
|
|
9834
|
+
} while (page.data.items.length === 250);
|
|
9835
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9836
|
+
return toolSuccess(
|
|
9837
|
+
summarizeCount(items2.length, "item"),
|
|
9838
|
+
{
|
|
9839
|
+
view: input.view,
|
|
9840
|
+
items: items2,
|
|
9841
|
+
count: items2.length,
|
|
9842
|
+
total: all.length,
|
|
9843
|
+
hasMore: offset + items2.length < all.length,
|
|
9844
|
+
appliedFilters: compactFilters(input)
|
|
9845
|
+
},
|
|
9846
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
9847
|
+
);
|
|
9848
|
+
}
|
|
8920
9849
|
const options = buildReadOptions(input);
|
|
8921
9850
|
let items = [];
|
|
8922
9851
|
let chart;
|
|
@@ -8948,6 +9877,19 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8948
9877
|
options
|
|
8949
9878
|
).map(duplicateCandidateItem);
|
|
8950
9879
|
break;
|
|
9880
|
+
case "events.thread": {
|
|
9881
|
+
if (!input.ref)
|
|
9882
|
+
return toolFailure(
|
|
9883
|
+
"protocol",
|
|
9884
|
+
"invalid_input",
|
|
9885
|
+
"Missing thread reference.",
|
|
9886
|
+
"ref is required for events.thread."
|
|
9887
|
+
);
|
|
9888
|
+
items = getEventThread(db, input.ref.source, input.ref.id, options).map(
|
|
9889
|
+
toEventListToolItem
|
|
9890
|
+
);
|
|
9891
|
+
break;
|
|
9892
|
+
}
|
|
8951
9893
|
case "events.contact_journal": {
|
|
8952
9894
|
const locator = input.contact;
|
|
8953
9895
|
if (locator === void 0) {
|
|
@@ -9089,6 +10031,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9089
10031
|
items = listRadar(
|
|
9090
10032
|
db,
|
|
9091
10033
|
{
|
|
10034
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
9092
10035
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
9093
10036
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
9094
10037
|
},
|
|
@@ -9138,6 +10081,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9138
10081
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
9139
10082
|
break;
|
|
9140
10083
|
}
|
|
10084
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
10085
|
+
items = items.map((item) => {
|
|
10086
|
+
const content = getEventById(
|
|
10087
|
+
db,
|
|
10088
|
+
Number(item.eventId ?? item.id)
|
|
10089
|
+
)?.content;
|
|
10090
|
+
return content === void 0 ? item : { ...item, content };
|
|
10091
|
+
});
|
|
9141
10092
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
9142
10093
|
return toolSuccess(
|
|
9143
10094
|
summarizeCount(items.length, "item"),
|
|
@@ -9149,7 +10100,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9149
10100
|
...chart === void 0 ? {} : { chart }
|
|
9150
10101
|
},
|
|
9151
10102
|
{
|
|
9152
|
-
entities: items,
|
|
10103
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
9153
10104
|
warnings: items.length === 0 ? [
|
|
9154
10105
|
toolWarning(
|
|
9155
10106
|
"empty_result",
|
|
@@ -9201,9 +10152,15 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9201
10152
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
9202
10153
|
inputSchema: objectSchema(
|
|
9203
10154
|
{
|
|
10155
|
+
ref: externalRefSchema,
|
|
10156
|
+
includeContent: booleanSchema(
|
|
10157
|
+
"Include verbatim content in event rows; defaults to false."
|
|
10158
|
+
),
|
|
10159
|
+
channel: stringSchema("Filter events by channel."),
|
|
9204
10160
|
view: enumSchema("Which review surface to return.", [
|
|
9205
10161
|
"contacts.list",
|
|
9206
10162
|
"contacts.duplicates",
|
|
10163
|
+
"events.thread",
|
|
9207
10164
|
"events.contact_journal",
|
|
9208
10165
|
"events.link_timeline",
|
|
9209
10166
|
"events.moments",
|
|
@@ -9258,7 +10215,20 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9258
10215
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
9259
10216
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
9260
10217
|
eventTypes: arraySchema(
|
|
9261
|
-
|
|
10218
|
+
enumSchema("Event type.", [
|
|
10219
|
+
"conversation",
|
|
10220
|
+
"activity",
|
|
10221
|
+
"gift",
|
|
10222
|
+
"support",
|
|
10223
|
+
"milestone",
|
|
10224
|
+
"observation",
|
|
10225
|
+
"conflict",
|
|
10226
|
+
"correction",
|
|
10227
|
+
"transaction",
|
|
10228
|
+
"promise",
|
|
10229
|
+
"agreement",
|
|
10230
|
+
"date_anchor"
|
|
10231
|
+
]),
|
|
9262
10232
|
"Optional event-type filter."
|
|
9263
10233
|
),
|
|
9264
10234
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -9298,6 +10268,9 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9298
10268
|
|
|
9299
10269
|
// src/tools/search_affinity_tool.ts
|
|
9300
10270
|
function searchAffinityToolHandler(db, rawInput) {
|
|
10271
|
+
return searchPage(db, rawInput, false);
|
|
10272
|
+
}
|
|
10273
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
9301
10274
|
const input = structuredClone(rawInput);
|
|
9302
10275
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9303
10276
|
if (inputError)
|
|
@@ -9308,11 +10281,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9308
10281
|
inputError
|
|
9309
10282
|
);
|
|
9310
10283
|
return withToolHandling(() => {
|
|
10284
|
+
if (!pageOnly) {
|
|
10285
|
+
const limit = input.limit ?? 50;
|
|
10286
|
+
const offset = input.offset ?? 0;
|
|
10287
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
10288
|
+
return toolFailure(
|
|
10289
|
+
"protocol",
|
|
10290
|
+
"invalid_input",
|
|
10291
|
+
"Invalid pagination.",
|
|
10292
|
+
"limit must be 1..250; offset must be non-negative"
|
|
10293
|
+
);
|
|
10294
|
+
const all = [];
|
|
10295
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
10296
|
+
for (const scope of scopes) {
|
|
10297
|
+
let page;
|
|
10298
|
+
let nextOffset = 0;
|
|
10299
|
+
do {
|
|
10300
|
+
page = searchPage(
|
|
10301
|
+
db,
|
|
10302
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
10303
|
+
true
|
|
10304
|
+
);
|
|
10305
|
+
if (!page.ok) return page;
|
|
10306
|
+
all.push(...page.data.items);
|
|
10307
|
+
nextOffset += page.data.count;
|
|
10308
|
+
} while (page.data.count === 250);
|
|
10309
|
+
}
|
|
10310
|
+
const items2 = all.slice(offset, offset + limit);
|
|
10311
|
+
return toolSuccess(
|
|
10312
|
+
summarizeCount(
|
|
10313
|
+
items2.length,
|
|
10314
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10315
|
+
),
|
|
10316
|
+
{
|
|
10317
|
+
query: input.query,
|
|
10318
|
+
items: items2,
|
|
10319
|
+
count: items2.length,
|
|
10320
|
+
total: all.length,
|
|
10321
|
+
hasMore: offset + items2.length < all.length
|
|
10322
|
+
},
|
|
10323
|
+
{ entities: items2 }
|
|
10324
|
+
);
|
|
10325
|
+
}
|
|
9311
10326
|
const options = {
|
|
9312
10327
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
9313
10328
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
9314
10329
|
};
|
|
9315
|
-
const rows = searchContacts(
|
|
10330
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
9316
10331
|
db,
|
|
9317
10332
|
input.query,
|
|
9318
10333
|
{
|
|
@@ -9326,9 +10341,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9326
10341
|
},
|
|
9327
10342
|
options
|
|
9328
10343
|
);
|
|
9329
|
-
const items =
|
|
10344
|
+
const items = [
|
|
10345
|
+
...rows.map(toContactListToolItem),
|
|
10346
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
10347
|
+
];
|
|
9330
10348
|
return toolSuccess(
|
|
9331
|
-
summarizeCount(
|
|
10349
|
+
summarizeCount(
|
|
10350
|
+
items.length,
|
|
10351
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10352
|
+
),
|
|
9332
10353
|
{
|
|
9333
10354
|
query: input.query,
|
|
9334
10355
|
items,
|
|
@@ -9364,6 +10385,11 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
9364
10385
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
9365
10386
|
inputSchema: objectSchema(
|
|
9366
10387
|
{
|
|
10388
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
10389
|
+
"contacts",
|
|
10390
|
+
"events",
|
|
10391
|
+
"all"
|
|
10392
|
+
]),
|
|
9367
10393
|
query: stringSchema("Free-text or natural-key query."),
|
|
9368
10394
|
kind: enumSchema("Optional contact kind filter.", [
|
|
9369
10395
|
"human",
|
|
@@ -9402,6 +10428,268 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
9402
10428
|
handler: searchAffinityToolHandler
|
|
9403
10429
|
});
|
|
9404
10430
|
|
|
10431
|
+
// src/events/event_replay.ts
|
|
10432
|
+
function replayEventLinks(db, linkIds, now) {
|
|
10433
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
10434
|
+
for (const linkId of linkIds) {
|
|
10435
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
10436
|
+
if (!saved) continue;
|
|
10437
|
+
const baseline = JSON.parse(saved.state_json);
|
|
10438
|
+
const effects = db.prepare(
|
|
10439
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
10440
|
+
).all(linkId);
|
|
10441
|
+
const overrides = db.prepare(
|
|
10442
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
10443
|
+
).all(linkId);
|
|
10444
|
+
let overrideIndex = 0;
|
|
10445
|
+
const applyOverrides = (beforeId) => {
|
|
10446
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
10447
|
+
const override = overrides[overrideIndex];
|
|
10448
|
+
if (override)
|
|
10449
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
10450
|
+
override.state,
|
|
10451
|
+
linkId
|
|
10452
|
+
);
|
|
10453
|
+
overrideIndex++;
|
|
10454
|
+
}
|
|
10455
|
+
};
|
|
10456
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
10457
|
+
db.prepare(
|
|
10458
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
10459
|
+
).run(
|
|
10460
|
+
baseline.rank,
|
|
10461
|
+
baseline.affinity,
|
|
10462
|
+
baseline.trust,
|
|
10463
|
+
baseline.state,
|
|
10464
|
+
baseline.cadence_days,
|
|
10465
|
+
now,
|
|
10466
|
+
linkId
|
|
10467
|
+
);
|
|
10468
|
+
for (const effect of effects) {
|
|
10469
|
+
eventIds.add(effect.event_id);
|
|
10470
|
+
applyOverrides(effect.event_id);
|
|
10471
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
10472
|
+
continue;
|
|
10473
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
10474
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
10475
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
10476
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
10477
|
+
applyEventEffectToLink(db, {
|
|
10478
|
+
linkId,
|
|
10479
|
+
eventId: event.id,
|
|
10480
|
+
eventType: event.type,
|
|
10481
|
+
occurredAt: event.occurredAt,
|
|
10482
|
+
significance: event.significance,
|
|
10483
|
+
participant,
|
|
10484
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
10485
|
+
now: effect.created_at
|
|
10486
|
+
});
|
|
10487
|
+
}
|
|
10488
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
10489
|
+
refreshLinkRollup(db, linkId, now);
|
|
10490
|
+
}
|
|
10491
|
+
for (const eventId of eventIds)
|
|
10492
|
+
db.prepare(
|
|
10493
|
+
"UPDATE events SET moment_kind=COALESCE(declared_moment_kind,(SELECT moment_kind FROM link_event_effects WHERE event_id=events.id AND moment_kind IS NOT NULL ORDER BY link_id LIMIT 1)) WHERE id=?"
|
|
10494
|
+
).run(eventId);
|
|
10495
|
+
refreshAllBridgeScores(db, now);
|
|
10496
|
+
}
|
|
10497
|
+
|
|
10498
|
+
// src/events/manage_event.ts
|
|
10499
|
+
function requireEventTarget(db, target) {
|
|
10500
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
10501
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
10502
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
10503
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
10504
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
10505
|
+
return event;
|
|
10506
|
+
}
|
|
10507
|
+
function affected(db, id) {
|
|
10508
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
10509
|
+
}
|
|
10510
|
+
function reviseEvent(db, target, patch) {
|
|
10511
|
+
return withTransaction(db, () => {
|
|
10512
|
+
const allowed = [
|
|
10513
|
+
"summary",
|
|
10514
|
+
"content",
|
|
10515
|
+
"occurredAt",
|
|
10516
|
+
"significance",
|
|
10517
|
+
"channel",
|
|
10518
|
+
"type",
|
|
10519
|
+
"externalRef"
|
|
10520
|
+
];
|
|
10521
|
+
for (const key of Object.keys(patch))
|
|
10522
|
+
if (!allowed.includes(key))
|
|
10523
|
+
throw new AffinityValidationError(
|
|
10524
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
10525
|
+
);
|
|
10526
|
+
const event = requireEventTarget(db, target);
|
|
10527
|
+
const next = { ...event, ...patch };
|
|
10528
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
10529
|
+
validateEventMetadata(metadata);
|
|
10530
|
+
Object.assign(next, metadata);
|
|
10531
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
10532
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
10533
|
+
if (patch.occurredAt !== void 0)
|
|
10534
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
10535
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
10536
|
+
throw new AffinityValidationError(
|
|
10537
|
+
"significance must be an integer from 1 to 10"
|
|
10538
|
+
);
|
|
10539
|
+
const interactions = [
|
|
10540
|
+
"conversation",
|
|
10541
|
+
"activity",
|
|
10542
|
+
"gift",
|
|
10543
|
+
"support",
|
|
10544
|
+
"conflict",
|
|
10545
|
+
"correction"
|
|
10546
|
+
];
|
|
10547
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
10548
|
+
throw new AffinityValidationError(
|
|
10549
|
+
"type must stay within the same event kind"
|
|
10550
|
+
);
|
|
10551
|
+
if (patch.externalRef && event.externalRef)
|
|
10552
|
+
throw new AffinityConflictError(
|
|
10553
|
+
"externalRef can only be set when absent"
|
|
10554
|
+
);
|
|
10555
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
10556
|
+
throw new AffinityConflictError(
|
|
10557
|
+
"externalRef already belongs to an event"
|
|
10558
|
+
);
|
|
10559
|
+
const now = Date.now();
|
|
10560
|
+
const links = affected(db, event.id);
|
|
10561
|
+
db.prepare(
|
|
10562
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
10563
|
+
).run(
|
|
10564
|
+
next.summary.trim(),
|
|
10565
|
+
next.content ?? null,
|
|
10566
|
+
next.occurredAt,
|
|
10567
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
10568
|
+
next.significance,
|
|
10569
|
+
next.channel ?? null,
|
|
10570
|
+
next.type,
|
|
10571
|
+
next.externalRef?.source ?? null,
|
|
10572
|
+
next.externalRef?.id ?? null,
|
|
10573
|
+
now,
|
|
10574
|
+
event.id
|
|
10575
|
+
);
|
|
10576
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
10577
|
+
db.prepare(
|
|
10578
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
10579
|
+
).run(patch.type, event.id);
|
|
10580
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
10581
|
+
replayEventLinks(db, links, now);
|
|
10582
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
10583
|
+
updated: [{ kind: "event", id: event.id }],
|
|
10584
|
+
affectedLinks: links
|
|
10585
|
+
});
|
|
10586
|
+
});
|
|
10587
|
+
}
|
|
10588
|
+
function deleteEvent(db, target, options) {
|
|
10589
|
+
return withTransaction(db, () => {
|
|
10590
|
+
if (options?.confirm !== true)
|
|
10591
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
10592
|
+
const event = requireEventTarget(db, target);
|
|
10593
|
+
const links = affected(db, event.id);
|
|
10594
|
+
for (const table of [
|
|
10595
|
+
"event_participants",
|
|
10596
|
+
"attributes",
|
|
10597
|
+
"open_commitments",
|
|
10598
|
+
"upcoming_occurrences",
|
|
10599
|
+
"link_event_effects"
|
|
10600
|
+
])
|
|
10601
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
10602
|
+
db.prepare(
|
|
10603
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
10604
|
+
).run(event.id);
|
|
10605
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
10606
|
+
replayEventLinks(db, links, Date.now());
|
|
10607
|
+
return buildEventMutationReceipt(event, {
|
|
10608
|
+
removed: [{ kind: "event", id: event.id }],
|
|
10609
|
+
affectedLinks: links
|
|
10610
|
+
});
|
|
10611
|
+
});
|
|
10612
|
+
}
|
|
10613
|
+
|
|
10614
|
+
// src/tools/manage_event_tool.ts
|
|
10615
|
+
function manageEventToolHandler(db, raw) {
|
|
10616
|
+
const input = structuredClone(raw);
|
|
10617
|
+
const error = validateToolInput(
|
|
10618
|
+
manageEventTool.inputSchema,
|
|
10619
|
+
input,
|
|
10620
|
+
"input",
|
|
10621
|
+
Date.now(),
|
|
10622
|
+
true
|
|
10623
|
+
);
|
|
10624
|
+
if (error)
|
|
10625
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
10626
|
+
return withToolHandling(() => {
|
|
10627
|
+
const target = {
|
|
10628
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
10629
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
10630
|
+
};
|
|
10631
|
+
return mutationToolResult(
|
|
10632
|
+
input.action,
|
|
10633
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
10634
|
+
"event"
|
|
10635
|
+
);
|
|
10636
|
+
}, "Event management failed.");
|
|
10637
|
+
}
|
|
10638
|
+
var manageEventTool = defineAffinityTool({
|
|
10639
|
+
name: manageEventToolName,
|
|
10640
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
10641
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
10642
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
10643
|
+
sideEffects: "writes_state",
|
|
10644
|
+
readOnly: false,
|
|
10645
|
+
supportsClarification: false,
|
|
10646
|
+
targetKinds: ["event"],
|
|
10647
|
+
inputDescriptions: {
|
|
10648
|
+
action: "revise or delete",
|
|
10649
|
+
eventId: "Exact event id",
|
|
10650
|
+
ref: "External event reference",
|
|
10651
|
+
patch: "Fields to revise",
|
|
10652
|
+
confirm: "Required true for deletion"
|
|
10653
|
+
},
|
|
10654
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
10655
|
+
inputSchema: objectSchema(
|
|
10656
|
+
{
|
|
10657
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
10658
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
10659
|
+
ref: externalRefSchema,
|
|
10660
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
10661
|
+
patch: objectSchema(
|
|
10662
|
+
{
|
|
10663
|
+
summary: stringSchema("Short non-empty label."),
|
|
10664
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
10665
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
10666
|
+
significance: integerSchema("Significance 1..10."),
|
|
10667
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
10668
|
+
type: enumSchema("Type within the same event kind.", [
|
|
10669
|
+
"conversation",
|
|
10670
|
+
"activity",
|
|
10671
|
+
"gift",
|
|
10672
|
+
"support",
|
|
10673
|
+
"conflict",
|
|
10674
|
+
"correction",
|
|
10675
|
+
"observation",
|
|
10676
|
+
"milestone",
|
|
10677
|
+
"transaction",
|
|
10678
|
+
"promise",
|
|
10679
|
+
"agreement",
|
|
10680
|
+
"date_anchor"
|
|
10681
|
+
]),
|
|
10682
|
+
externalRef: externalRefSchema
|
|
10683
|
+
},
|
|
10684
|
+
[],
|
|
10685
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
10686
|
+
)
|
|
10687
|
+
},
|
|
10688
|
+
["action"]
|
|
10689
|
+
),
|
|
10690
|
+
handler: manageEventToolHandler
|
|
10691
|
+
});
|
|
10692
|
+
|
|
9405
10693
|
// src/tools/tool_registry.ts
|
|
9406
10694
|
var affinityTools = [
|
|
9407
10695
|
searchAffinityTool,
|
|
@@ -9415,7 +10703,8 @@ var affinityTools = [
|
|
|
9415
10703
|
recordEventTool,
|
|
9416
10704
|
manageCommitmentTool,
|
|
9417
10705
|
manageDateAnchorTool,
|
|
9418
|
-
manageAttributeTool
|
|
10706
|
+
manageAttributeTool,
|
|
10707
|
+
manageEventTool
|
|
9419
10708
|
];
|
|
9420
10709
|
function listAffinityToolDefinitions() {
|
|
9421
10710
|
return [...affinityTools];
|
|
@@ -9426,6 +10715,24 @@ function getAffinityToolByName(name) {
|
|
|
9426
10715
|
|
|
9427
10716
|
// src/tools/tool_mapping.ts
|
|
9428
10717
|
var affinityToolMappings = [
|
|
10718
|
+
{
|
|
10719
|
+
source: "getEventById",
|
|
10720
|
+
tool: inspectAffinityItemToolName,
|
|
10721
|
+
action: "event"
|
|
10722
|
+
},
|
|
10723
|
+
{
|
|
10724
|
+
source: "getEventByExternalRef",
|
|
10725
|
+
tool: inspectAffinityItemToolName,
|
|
10726
|
+
action: "event"
|
|
10727
|
+
},
|
|
10728
|
+
{
|
|
10729
|
+
source: "getEventThread",
|
|
10730
|
+
tool: reviewAffinityToolName,
|
|
10731
|
+
view: "events.thread"
|
|
10732
|
+
},
|
|
10733
|
+
{ source: "searchEvents", tool: searchAffinityToolName },
|
|
10734
|
+
{ source: "reviseEvent", tool: "manage_event", action: "revise" },
|
|
10735
|
+
{ source: "deleteEvent", tool: "manage_event", action: "delete" },
|
|
9429
10736
|
{ source: "exportContact", tool: manageContactToolName, action: "export" },
|
|
9430
10737
|
{ source: "exportAffinity", tool: manageContactToolName, action: "export" },
|
|
9431
10738
|
{ source: "deleteContact", tool: manageContactToolName, action: "delete" },
|
|
@@ -9616,6 +10923,7 @@ __export(write_exports, {
|
|
|
9616
10923
|
addIdentity: () => addIdentity,
|
|
9617
10924
|
createContact: () => createContact,
|
|
9618
10925
|
deleteContact: () => deleteContact,
|
|
10926
|
+
deleteEvent: () => deleteEvent,
|
|
9619
10927
|
dismissDuplicate: () => dismissDuplicate,
|
|
9620
10928
|
mergeContacts: () => mergeContacts,
|
|
9621
10929
|
overrideLinkState: () => overrideLinkState,
|
|
@@ -9632,6 +10940,7 @@ __export(write_exports, {
|
|
|
9632
10940
|
reviseBond: () => reviseBond,
|
|
9633
10941
|
reviseContact: () => reviseContact,
|
|
9634
10942
|
reviseDateAnchor: () => reviseDateAnchor,
|
|
10943
|
+
reviseEvent: () => reviseEvent,
|
|
9635
10944
|
reviseIdentity: () => reviseIdentity,
|
|
9636
10945
|
seedSocialLink: () => seedSocialLink,
|
|
9637
10946
|
setAttribute: () => setAttribute,
|