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