@ghostpaw/affinity 0.5.0 → 0.6.1
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 +197 -9
- package/SKILL.md +43 -386
- package/dist/cli.js +2881 -914
- package/dist/index.cjs +1957 -605
- package/dist/index.d.cts +340 -55
- package/dist/index.d.ts +340 -55
- package/dist/index.js +1957 -605
- package/dist/mcp-server.js +2162 -791
- package/dist/mcp.cjs +1962 -701
- package/dist/mcp.d.cts +3 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +1963 -702
- 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 +11 -0
- package/skills/affinity/references/review-radar-progression-and-graph.md +13 -0
- package/skills/affinity/references/tools.md +490 -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,27 @@ 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.provenance_json != null)
|
|
1403
|
+
base.provenance = JSON.parse(row.provenance_json);
|
|
1404
|
+
if (row.content != null) base.content = decodeEventText(row.content);
|
|
1405
|
+
if (row.channel != null) base.channel = row.channel;
|
|
1406
|
+
if (row.external_source != null && row.external_id != null)
|
|
1407
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
1408
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
1409
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
1410
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
1411
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
1412
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
1413
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
1414
|
+
base.dueAt = row.due_at ?? null;
|
|
1415
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
1416
|
+
}
|
|
964
1417
|
if (row.recurrence_kind !== null) {
|
|
965
1418
|
base.recurrenceKind = row.recurrence_kind;
|
|
966
1419
|
base.anchorMonth = row.anchor_month;
|
|
@@ -990,25 +1443,29 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
990
1443
|
targetRef,
|
|
991
1444
|
recurrenceKind,
|
|
992
1445
|
occursOn: row.occurs_on,
|
|
993
|
-
|
|
1446
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
1447
|
+
summary: decodeEventText(row.summary),
|
|
994
1448
|
significance: row.significance
|
|
995
1449
|
};
|
|
996
1450
|
}
|
|
1451
|
+
function decodeEventText(value) {
|
|
1452
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
1453
|
+
}
|
|
997
1454
|
|
|
998
1455
|
// src/events/queries.ts
|
|
999
1456
|
function getEventRowById(db, id) {
|
|
1000
1457
|
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
|
|
1458
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
1005
1459
|
FROM events
|
|
1006
1460
|
WHERE id = ? AND deleted_at IS NULL`
|
|
1007
1461
|
).get(id);
|
|
1008
1462
|
if (row === void 0) {
|
|
1009
1463
|
return null;
|
|
1010
1464
|
}
|
|
1011
|
-
|
|
1465
|
+
const event = row;
|
|
1466
|
+
event.summary = decodeEventText(event.summary);
|
|
1467
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
1468
|
+
return event;
|
|
1012
1469
|
}
|
|
1013
1470
|
function findDuplicateDateAnchor(db, params) {
|
|
1014
1471
|
const exclude = params.excludeEventId;
|
|
@@ -1076,19 +1533,27 @@ function loadEventRecord(db, eventId) {
|
|
|
1076
1533
|
throw new AffinityInvariantError("event not found");
|
|
1077
1534
|
}
|
|
1078
1535
|
const participants = loadEventParticipantViews(db, eventId);
|
|
1079
|
-
|
|
1536
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
1537
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
1538
|
+
if (due) {
|
|
1539
|
+
record.dueAt = due.due_at;
|
|
1540
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
1541
|
+
}
|
|
1542
|
+
enrichEventMetadata(db, record);
|
|
1543
|
+
return record;
|
|
1080
1544
|
}
|
|
1081
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
1545
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
1082
1546
|
if (eventIds.length === 0) {
|
|
1083
1547
|
return [];
|
|
1084
1548
|
}
|
|
1085
1549
|
const ph = eventIds.map(() => "?").join(", ");
|
|
1086
1550
|
const eventRows = db.prepare(
|
|
1087
|
-
`SELECT id, type, occurred_at,
|
|
1551
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
1552
|
+
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
1553
|
recurrence_kind, anchor_month, anchor_day,
|
|
1089
1554
|
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`
|
|
1555
|
+
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
|
|
1556
|
+
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
1557
|
).all(...eventIds);
|
|
1093
1558
|
const partRows = db.prepare(
|
|
1094
1559
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -1112,7 +1577,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
1112
1577
|
for (const id of eventIds) {
|
|
1113
1578
|
const row = rowById.get(id);
|
|
1114
1579
|
if (row) {
|
|
1115
|
-
|
|
1580
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
1581
|
+
enrichEventMetadata(db, record);
|
|
1582
|
+
out.push(record);
|
|
1116
1583
|
}
|
|
1117
1584
|
}
|
|
1118
1585
|
return out;
|
|
@@ -1171,6 +1638,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
1171
1638
|
clauses.push(`e.type IN (${ph})`);
|
|
1172
1639
|
params.push(...options.eventTypes);
|
|
1173
1640
|
}
|
|
1641
|
+
if (options?.channel !== void 0) {
|
|
1642
|
+
clauses.push("e.channel = ?");
|
|
1643
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1644
|
+
}
|
|
1174
1645
|
const where = clauses.join(" AND ");
|
|
1175
1646
|
const rows = db.prepare(
|
|
1176
1647
|
`SELECT e.id FROM events e
|
|
@@ -1181,8 +1652,70 @@ function getContactJournal(db, contactId, options) {
|
|
|
1181
1652
|
).all(...params, limit, offset);
|
|
1182
1653
|
return batchLoadEventRecords(
|
|
1183
1654
|
db,
|
|
1184
|
-
rows.map((r) => r.id)
|
|
1655
|
+
rows.map((r) => r.id),
|
|
1656
|
+
options?.includeContent ?? false
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/lib/parse_calendar_time.ts
|
|
1661
|
+
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
1662
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
1663
|
+
if (dueAt === null) return false;
|
|
1664
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
1665
|
+
return localCalendarDate(now) > dueOn;
|
|
1666
|
+
}
|
|
1667
|
+
return dueAt < now;
|
|
1668
|
+
}
|
|
1669
|
+
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)";
|
|
1670
|
+
function localCalendarDate(now) {
|
|
1671
|
+
const d = new Date(now);
|
|
1672
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1673
|
+
}
|
|
1674
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
1675
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
1676
|
+
throw new AffinityValidationError(
|
|
1677
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
1678
|
+
);
|
|
1679
|
+
const fail = () => {
|
|
1680
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1681
|
+
};
|
|
1682
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
1683
|
+
const timestamp2 = Number(value);
|
|
1684
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
1685
|
+
return fail();
|
|
1686
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
1687
|
+
}
|
|
1688
|
+
if (typeof value !== "string") return fail();
|
|
1689
|
+
let text = value.trim();
|
|
1690
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
1691
|
+
text = text.replace(" ", "T");
|
|
1692
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
1693
|
+
}
|
|
1694
|
+
text = text.replace(
|
|
1695
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
1696
|
+
(_, digits) => `.${digits.slice(0, 3)}`
|
|
1185
1697
|
);
|
|
1698
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
1699
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1700
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
1701
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
1702
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
1703
|
+
text = date.toISOString().slice(0, 10);
|
|
1704
|
+
}
|
|
1705
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
1706
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
1707
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
1708
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1709
|
+
text
|
|
1710
|
+
))
|
|
1711
|
+
return fail();
|
|
1712
|
+
const day = text.slice(0, 10);
|
|
1713
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
1714
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
1715
|
+
return fail();
|
|
1716
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
|
|
1717
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
1718
|
+
return { timestamp, day };
|
|
1186
1719
|
}
|
|
1187
1720
|
|
|
1188
1721
|
// src/events/list_open_commitments.ts
|
|
@@ -1240,9 +1773,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
1240
1773
|
clauses.push("oc.due_at <= ?");
|
|
1241
1774
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
1242
1775
|
}
|
|
1776
|
+
if (options?.channel !== void 0) {
|
|
1777
|
+
clauses.push("e.channel=?");
|
|
1778
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1779
|
+
}
|
|
1243
1780
|
const where = clauses.join(" AND ");
|
|
1244
1781
|
const rows = db.prepare(
|
|
1245
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
1782
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
1246
1783
|
e.type, e.summary
|
|
1247
1784
|
FROM open_commitments oc
|
|
1248
1785
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -1259,7 +1796,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
1259
1796
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
1260
1797
|
dueAt: r.due_at,
|
|
1261
1798
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
1262
|
-
|
|
1799
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
1800
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
1263
1801
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
1264
1802
|
}));
|
|
1265
1803
|
}
|
|
@@ -1293,8 +1831,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
1293
1831
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
1294
1832
|
}
|
|
1295
1833
|
function computeNextAnchorOccursOn(params) {
|
|
1296
|
-
const refStart =
|
|
1297
|
-
|
|
1834
|
+
const refStart = Date.parse(
|
|
1835
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
1836
|
+
);
|
|
1837
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
1298
1838
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
1299
1839
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
1300
1840
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -1354,19 +1894,6 @@ function loadActiveDatesForContact(db, contactId) {
|
|
|
1354
1894
|
return rows.map((row) => mapUpcomingRowToUpcomingDateRecord(row));
|
|
1355
1895
|
}
|
|
1356
1896
|
|
|
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
1897
|
// src/links/mappers.ts
|
|
1371
1898
|
function mapLinkRowToLinkListItem(row) {
|
|
1372
1899
|
const kind = row.kind;
|
|
@@ -1427,7 +1954,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1427
1954
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
1428
1955
|
LIMIT ?`
|
|
1429
1956
|
).all(contactId, contactId, topLinksLimit);
|
|
1430
|
-
const topLinks = linkRows.map((l) =>
|
|
1957
|
+
const topLinks = linkRows.map((l) => ({
|
|
1958
|
+
...mapLinkRowToLinkListItem(l),
|
|
1959
|
+
counterpartyName: getContactRowById(
|
|
1960
|
+
db,
|
|
1961
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1962
|
+
)?.name ?? ""
|
|
1963
|
+
}));
|
|
1431
1964
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
1432
1965
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1433
1966
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -1460,7 +1993,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
1460
1993
|
return {
|
|
1461
1994
|
...profile,
|
|
1462
1995
|
recentEvents: getContactJournal(db, contactId, {
|
|
1463
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1996
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1997
|
+
eventTypes: [
|
|
1998
|
+
"conversation",
|
|
1999
|
+
"activity",
|
|
2000
|
+
"gift",
|
|
2001
|
+
"support",
|
|
2002
|
+
"milestone",
|
|
2003
|
+
"observation",
|
|
2004
|
+
"conflict",
|
|
2005
|
+
"correction",
|
|
2006
|
+
"transaction",
|
|
2007
|
+
"promise",
|
|
2008
|
+
"agreement"
|
|
2009
|
+
]
|
|
1464
2010
|
}).map((event) => ({
|
|
1465
2011
|
...event,
|
|
1466
2012
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -1490,7 +2036,10 @@ function contactAttributeClauses(filters) {
|
|
|
1490
2036
|
const params = [];
|
|
1491
2037
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
1492
2038
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
1493
|
-
for (const filter of [
|
|
2039
|
+
for (const filter of [
|
|
2040
|
+
...attributes,
|
|
2041
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
2042
|
+
]) {
|
|
1494
2043
|
const separator = filter.indexOf("=");
|
|
1495
2044
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
1496
2045
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -1561,7 +2110,9 @@ function pairKey(leftContactId, rightContactId) {
|
|
|
1561
2110
|
function normalizeName(name) {
|
|
1562
2111
|
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
1563
2112
|
}
|
|
1564
|
-
function fuzzyNameScore(
|
|
2113
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
2114
|
+
const left = normalizeName(leftInput);
|
|
2115
|
+
const right = normalizeName(rightInput);
|
|
1565
2116
|
if (left === right) {
|
|
1566
2117
|
return 0.92;
|
|
1567
2118
|
}
|
|
@@ -1719,7 +2270,7 @@ function foldSearchText(value) {
|
|
|
1719
2270
|
|
|
1720
2271
|
// src/contacts/search_contacts.ts
|
|
1721
2272
|
function searchContacts(db, query, filters, options) {
|
|
1722
|
-
const q = foldSearchText(query.trim());
|
|
2273
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
1723
2274
|
if (!q) return [];
|
|
1724
2275
|
assertDefaultOrdering("searchContacts", options);
|
|
1725
2276
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -1739,6 +2290,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
1739
2290
|
const tokens = q.split(/\s+/);
|
|
1740
2291
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
1741
2292
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
2293
|
+
const suffix = digits.replace(/^0/, "");
|
|
1742
2294
|
const attributeFilters = contactAttributeClauses(filters);
|
|
1743
2295
|
const clauses = [
|
|
1744
2296
|
"deleted_at IS NULL",
|
|
@@ -1774,7 +2326,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
1774
2326
|
)
|
|
1775
2327
|
];
|
|
1776
2328
|
const phoneFields = phone ? values.filter(
|
|
1777
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
2329
|
+
(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
2330
|
) : [];
|
|
1779
2331
|
if (!phoneFields.length && !tokens.every(
|
|
1780
2332
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -1809,7 +2361,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
1809
2361
|
}
|
|
1810
2362
|
if (to === "merged") {
|
|
1811
2363
|
throw new AffinityInvariantError(
|
|
1812
|
-
"use
|
|
2364
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
1813
2365
|
);
|
|
1814
2366
|
}
|
|
1815
2367
|
if (from === to) {
|
|
@@ -1864,7 +2416,7 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
1864
2416
|
// src/events/persistence.ts
|
|
1865
2417
|
function insertJournalEvent(db, params) {
|
|
1866
2418
|
const now = resolveNow(params.now);
|
|
1867
|
-
const summary = params.summary
|
|
2419
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
1868
2420
|
const result = db.prepare(
|
|
1869
2421
|
`INSERT INTO events (
|
|
1870
2422
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -1881,6 +2433,35 @@ function insertJournalEvent(db, params) {
|
|
|
1881
2433
|
now
|
|
1882
2434
|
);
|
|
1883
2435
|
const eventId = Number(result.lastInsertRowid);
|
|
2436
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
2437
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 || ["activity", "milestone", "observation"].includes(params.type) && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
2438
|
+
summary
|
|
2439
|
+
) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
|
|
2440
|
+
eventId
|
|
2441
|
+
);
|
|
2442
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
2443
|
+
params.momentKind,
|
|
2444
|
+
eventId
|
|
2445
|
+
);
|
|
2446
|
+
db.prepare(
|
|
2447
|
+
"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=?"
|
|
2448
|
+
).run(
|
|
2449
|
+
params.content ?? null,
|
|
2450
|
+
params.externalRef?.source ?? null,
|
|
2451
|
+
params.externalRef?.id ?? null,
|
|
2452
|
+
params.channel ?? null,
|
|
2453
|
+
params.parentRef?.source ?? null,
|
|
2454
|
+
params.parentRef?.id ?? null,
|
|
2455
|
+
params.parentEventId ?? null,
|
|
2456
|
+
params.threadRef?.source ?? null,
|
|
2457
|
+
params.threadRef?.id ?? null,
|
|
2458
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
2459
|
+
eventId
|
|
2460
|
+
);
|
|
2461
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
2462
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
2463
|
+
eventId
|
|
2464
|
+
);
|
|
1884
2465
|
const insertParticipant = db.prepare(
|
|
1885
2466
|
`INSERT INTO event_participants (
|
|
1886
2467
|
event_id, contact_id, role, directionality
|
|
@@ -1923,6 +2504,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
1923
2504
|
now
|
|
1924
2505
|
);
|
|
1925
2506
|
const eventId = Number(result.lastInsertRowid);
|
|
2507
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
2508
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
2509
|
+
eventId
|
|
2510
|
+
);
|
|
1926
2511
|
const insertParticipant = db.prepare(
|
|
1927
2512
|
`INSERT INTO event_participants (
|
|
1928
2513
|
event_id, contact_id, role, directionality
|
|
@@ -1943,20 +2528,6 @@ function insertDateAnchorEvent(db, params) {
|
|
|
1943
2528
|
return eventId;
|
|
1944
2529
|
}
|
|
1945
2530
|
|
|
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
2531
|
// src/links/queries.ts
|
|
1961
2532
|
function getLinkRowById(db, id) {
|
|
1962
2533
|
const row = db.prepare(
|
|
@@ -2118,12 +2689,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
2118
2689
|
}
|
|
2119
2690
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
2120
2691
|
const params = [];
|
|
2121
|
-
const since = options?.since ??
|
|
2692
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
2122
2693
|
clauses.push("uo.occurs_on >= ?");
|
|
2123
2694
|
params.push(since);
|
|
2124
2695
|
let maxOccurs = options?.until;
|
|
2125
2696
|
if (filters?.horizonDays !== void 0) {
|
|
2126
|
-
const anchor =
|
|
2697
|
+
const anchor = since;
|
|
2127
2698
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
2128
2699
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
2129
2700
|
}
|
|
@@ -2661,6 +3232,10 @@ function listMoments(db, filters, options) {
|
|
|
2661
3232
|
clauses.push("le.link_id = ?");
|
|
2662
3233
|
params.push(filters.linkId);
|
|
2663
3234
|
}
|
|
3235
|
+
if (options?.channel !== void 0) {
|
|
3236
|
+
clauses.push("e.channel=?");
|
|
3237
|
+
params.push(options.channel.trim().toLowerCase());
|
|
3238
|
+
}
|
|
2664
3239
|
const where = clauses.join(" AND ");
|
|
2665
3240
|
return db.prepare(
|
|
2666
3241
|
`SELECT
|
|
@@ -2668,6 +3243,7 @@ function listMoments(db, filters, options) {
|
|
|
2668
3243
|
le.link_id,
|
|
2669
3244
|
le.moment_kind,
|
|
2670
3245
|
le.occurred_at,
|
|
3246
|
+
e.occurred_on,
|
|
2671
3247
|
e.summary,
|
|
2672
3248
|
le.impact_score
|
|
2673
3249
|
FROM link_event_effects le
|
|
@@ -2680,6 +3256,7 @@ function listMoments(db, filters, options) {
|
|
|
2680
3256
|
linkId: row.link_id,
|
|
2681
3257
|
momentKind: row.moment_kind,
|
|
2682
3258
|
occurredAt: row.occurred_at,
|
|
3259
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
2683
3260
|
summary: row.summary,
|
|
2684
3261
|
impactScore: row.impact_score
|
|
2685
3262
|
}));
|
|
@@ -2708,6 +3285,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2708
3285
|
clauses.push(`e.type IN (${ph})`);
|
|
2709
3286
|
params.push(...options.eventTypes);
|
|
2710
3287
|
}
|
|
3288
|
+
if (options?.channel !== void 0) {
|
|
3289
|
+
clauses.push("e.channel = ?");
|
|
3290
|
+
params.push(options.channel.trim().toLowerCase());
|
|
3291
|
+
}
|
|
2711
3292
|
const where = clauses.join(" AND ");
|
|
2712
3293
|
const rows = db.prepare(
|
|
2713
3294
|
`SELECT e.id
|
|
@@ -2719,7 +3300,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2719
3300
|
).all(...params, limit, offset);
|
|
2720
3301
|
return batchLoadEventRecords(
|
|
2721
3302
|
db,
|
|
2722
|
-
rows.map((r) => r.id)
|
|
3303
|
+
rows.map((r) => r.id),
|
|
3304
|
+
options?.includeContent ?? false
|
|
2723
3305
|
);
|
|
2724
3306
|
}
|
|
2725
3307
|
|
|
@@ -2793,26 +3375,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2793
3375
|
const latest = db.prepare(
|
|
2794
3376
|
`SELECT event_id, occurred_at
|
|
2795
3377
|
FROM link_event_effects
|
|
2796
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
3378
|
+
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
3379
|
ORDER BY occurred_at DESC, event_id DESC
|
|
2798
3380
|
LIMIT 1`
|
|
2799
|
-
).get(linkId);
|
|
3381
|
+
).get(linkId, now, localCalendarDate(now));
|
|
2800
3382
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
2801
3383
|
const counts = db.prepare(
|
|
2802
3384
|
`SELECT
|
|
2803
3385
|
COUNT(*) AS total_meaningful_events,
|
|
2804
3386
|
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
3387
|
FROM link_event_effects
|
|
2806
|
-
WHERE link_id = ? AND occurred_at >=
|
|
2807
|
-
|
|
3388
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
3389
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
3390
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
2808
3391
|
const directionalityRows = db.prepare(
|
|
2809
3392
|
`SELECT ep.directionality
|
|
2810
3393
|
FROM link_event_effects le
|
|
2811
3394
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
2812
3395
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
2813
3396
|
AND ep.directionality IS NOT NULL
|
|
2814
|
-
AND le.occurred_at >=
|
|
2815
|
-
|
|
3397
|
+
AND le.occurred_at >= ?
|
|
3398
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
3399
|
+
).all(
|
|
3400
|
+
linkId,
|
|
3401
|
+
link.from_contact_id,
|
|
3402
|
+
trailingWindowStart,
|
|
3403
|
+
now,
|
|
3404
|
+
localCalendarDate(now)
|
|
3405
|
+
);
|
|
2816
3406
|
let outboundCount = 0;
|
|
2817
3407
|
let inboundCount = 0;
|
|
2818
3408
|
for (const row of directionalityRows) {
|
|
@@ -3131,21 +3721,6 @@ function listProgressionReadiness(db, filters, options) {
|
|
|
3131
3721
|
}
|
|
3132
3722
|
|
|
3133
3723
|
// 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
3724
|
function listRadar(db, filters, options) {
|
|
3150
3725
|
assertDefaultOrdering("listRadar", options);
|
|
3151
3726
|
const ownerId = findOwnerContactId(db);
|
|
@@ -3153,7 +3728,10 @@ function listRadar(db, filters, options) {
|
|
|
3153
3728
|
return [];
|
|
3154
3729
|
}
|
|
3155
3730
|
const now = resolveNow(options?.now);
|
|
3156
|
-
const
|
|
3731
|
+
const today = localCalendarDate(now);
|
|
3732
|
+
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}'))`;
|
|
3733
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
3734
|
+
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
3735
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
3158
3736
|
const clauses = [
|
|
3159
3737
|
"l.removed_at IS NULL",
|
|
@@ -3178,10 +3756,23 @@ function listRadar(db, filters, options) {
|
|
|
3178
3756
|
if (options?.includeObserved === false) {
|
|
3179
3757
|
clauses.push("l.kind != 'observed'");
|
|
3180
3758
|
}
|
|
3759
|
+
if (options?.includeDormant !== true)
|
|
3760
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
3761
|
+
if (filters?.includeLost !== true)
|
|
3762
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
3763
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
3764
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
3765
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
3766
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
3767
|
+
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'))
|
|
3768
|
+
))`);
|
|
3181
3769
|
const where = clauses.join(" AND ");
|
|
3182
3770
|
const rows = db.prepare(
|
|
3183
3771
|
`SELECT
|
|
3184
3772
|
l.id,
|
|
3773
|
+
l.cadence_days,
|
|
3774
|
+
${lastContact} AS last_contact,
|
|
3775
|
+
${drift} AS drift,
|
|
3185
3776
|
${overdue} AS overdue_commitment,
|
|
3186
3777
|
l.from_contact_id,
|
|
3187
3778
|
l.to_contact_id,
|
|
@@ -3195,23 +3786,44 @@ function listRadar(db, filters, options) {
|
|
|
3195
3786
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
3196
3787
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
3197
3788
|
WHERE ${where}
|
|
3198
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3789
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3199
3790
|
LIMIT ? OFFSET ?`
|
|
3200
3791
|
).all(...params, limit, offset);
|
|
3201
|
-
return rows.map((row) =>
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3792
|
+
return rows.map((row) => {
|
|
3793
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
3794
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
3795
|
+
`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`
|
|
3796
|
+
).get(contactId, today, now) : void 0;
|
|
3797
|
+
const reasons = [
|
|
3798
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
3799
|
+
...row.drift ? ["drift"] : []
|
|
3800
|
+
];
|
|
3801
|
+
const snippets = [
|
|
3802
|
+
...promise ? [
|
|
3803
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
3804
|
+
] : [],
|
|
3805
|
+
...row.drift ? [
|
|
3806
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
3807
|
+
] : []
|
|
3808
|
+
];
|
|
3809
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
3810
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
3811
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
3812
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
3813
|
+
trust: row.trust ?? 0,
|
|
3814
|
+
rank: row.rank ?? 0
|
|
3815
|
+
});
|
|
3816
|
+
return {
|
|
3817
|
+
linkId: row.id,
|
|
3818
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
3819
|
+
reasons,
|
|
3820
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
3821
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
3822
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
3823
|
+
trust: row.trust ?? 0,
|
|
3824
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
3825
|
+
};
|
|
3826
|
+
});
|
|
3215
3827
|
}
|
|
3216
3828
|
|
|
3217
3829
|
// src/links/receipts.ts
|
|
@@ -3294,6 +3906,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
3294
3906
|
now,
|
|
3295
3907
|
linkId
|
|
3296
3908
|
);
|
|
3909
|
+
db.prepare(
|
|
3910
|
+
"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=?"
|
|
3911
|
+
).run(linkId, state, linkId);
|
|
3297
3912
|
refreshLinkRollup(db, linkId, now);
|
|
3298
3913
|
if (state === "archived" || row.state === "archived") {
|
|
3299
3914
|
refreshAllBridgeScores(db, now);
|
|
@@ -3368,6 +3983,8 @@ function reviseBond(db, linkId, bond, options) {
|
|
|
3368
3983
|
// src/links/seed_social_link.ts
|
|
3369
3984
|
function seedSocialLink(db, input) {
|
|
3370
3985
|
return withTransaction(db, () => {
|
|
3986
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
3987
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
3371
3988
|
if (!isRelationalLinkKind(input.kind)) {
|
|
3372
3989
|
throw new AffinityValidationError("invalid relational link kind");
|
|
3373
3990
|
}
|
|
@@ -3375,11 +3992,14 @@ function seedSocialLink(db, input) {
|
|
|
3375
3992
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
3376
3993
|
}
|
|
3377
3994
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
3378
|
-
|
|
3995
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
3379
3996
|
db,
|
|
3380
3997
|
input.fromContactId,
|
|
3381
3998
|
input.toContactId
|
|
3382
|
-
)
|
|
3999
|
+
);
|
|
4000
|
+
if (existing) {
|
|
4001
|
+
if (input.ifExists === "return")
|
|
4002
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
3383
4003
|
throw new AffinityConflictError(
|
|
3384
4004
|
"relational link already exists between these contacts"
|
|
3385
4005
|
);
|
|
@@ -4198,6 +4818,8 @@ function deriveStateAfter(params) {
|
|
|
4198
4818
|
return params.stateBefore;
|
|
4199
4819
|
}
|
|
4200
4820
|
function deriveMomentKind(params) {
|
|
4821
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
4822
|
+
return null;
|
|
4201
4823
|
if (params.rankAfter > params.rankBefore) {
|
|
4202
4824
|
return "breakthrough";
|
|
4203
4825
|
}
|
|
@@ -4227,6 +4849,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
4227
4849
|
if (!link || link.is_structural !== 0) {
|
|
4228
4850
|
throw new AffinityStateError("relational link not found");
|
|
4229
4851
|
}
|
|
4852
|
+
db.prepare(
|
|
4853
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
4854
|
+
).run(
|
|
4855
|
+
params.linkId,
|
|
4856
|
+
JSON.stringify({
|
|
4857
|
+
rank: link.rank ?? 0,
|
|
4858
|
+
affinity: link.affinity ?? 0,
|
|
4859
|
+
trust: link.trust ?? 0,
|
|
4860
|
+
state: link.state ?? "active",
|
|
4861
|
+
cadence_days: link.cadence_days
|
|
4862
|
+
})
|
|
4863
|
+
);
|
|
4230
4864
|
const stateBefore = link.state ?? "active";
|
|
4231
4865
|
const linkKind = link.kind;
|
|
4232
4866
|
const rankBefore = link.rank ?? 0;
|
|
@@ -4271,7 +4905,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
4271
4905
|
);
|
|
4272
4906
|
const weightedBase = base * massPenalty(
|
|
4273
4907
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
4274
|
-
) * salienceMultiplier;
|
|
4908
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
4275
4909
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
4276
4910
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
4277
4911
|
const affinityMass = clamp3(
|
|
@@ -4312,15 +4946,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
4312
4946
|
const latest = db.prepare(
|
|
4313
4947
|
`SELECT occurred_at
|
|
4314
4948
|
FROM link_event_effects
|
|
4315
|
-
WHERE link_id = ?
|
|
4949
|
+
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
4950
|
ORDER BY occurred_at DESC, event_id DESC
|
|
4317
4951
|
LIMIT 1`
|
|
4318
|
-
).get(params.linkId);
|
|
4952
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
4319
4953
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
4320
4954
|
1,
|
|
4321
4955
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
4322
4956
|
);
|
|
4323
|
-
const
|
|
4957
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
4958
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
4324
4959
|
kind: linkKind,
|
|
4325
4960
|
currentCadenceDays: cadenceBefore,
|
|
4326
4961
|
gapDays
|
|
@@ -4340,7 +4975,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
4340
4975
|
}
|
|
4341
4976
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
4342
4977
|
const trustDelta = trustAfter - trustBefore;
|
|
4343
|
-
const
|
|
4978
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
4979
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
4980
|
+
eventLabel
|
|
4981
|
+
);
|
|
4982
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
4344
4983
|
rankBefore,
|
|
4345
4984
|
rankAfter,
|
|
4346
4985
|
stateBefore,
|
|
@@ -4383,7 +5022,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
4383
5022
|
params.now,
|
|
4384
5023
|
params.now
|
|
4385
5024
|
);
|
|
4386
|
-
|
|
5025
|
+
db.prepare(
|
|
5026
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
5027
|
+
).run(
|
|
5028
|
+
JSON.stringify(params.participant),
|
|
5029
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
5030
|
+
params.eventId,
|
|
5031
|
+
params.linkId
|
|
5032
|
+
);
|
|
5033
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
4387
5034
|
db.prepare(
|
|
4388
5035
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4389
5036
|
).run(params.eventId, params.linkId);
|
|
@@ -4466,15 +5113,36 @@ function ensureRelationalLink(db, params) {
|
|
|
4466
5113
|
};
|
|
4467
5114
|
}
|
|
4468
5115
|
function applyJournalEventMechanics(db, params) {
|
|
5116
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
5117
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
5118
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
5119
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
4469
5120
|
const ownerId = requireOwnerContactId(db);
|
|
4470
|
-
const
|
|
5121
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
5122
|
+
const socialChannel = [
|
|
5123
|
+
"bluesky",
|
|
5124
|
+
"x",
|
|
5125
|
+
"twitter",
|
|
5126
|
+
"mastodon",
|
|
5127
|
+
"github",
|
|
5128
|
+
"instagram"
|
|
5129
|
+
].includes(String(bond?.channel));
|
|
5130
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
5131
|
+
const participants = params.participants.map(
|
|
5132
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
5133
|
+
);
|
|
5134
|
+
if (direction)
|
|
5135
|
+
db.prepare(
|
|
5136
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
5137
|
+
).run(direction, params.eventId);
|
|
5138
|
+
const participantIds = participants.map(
|
|
4471
5139
|
(participant) => participant.contactId
|
|
4472
5140
|
);
|
|
4473
5141
|
const hasOwner = participantIds.includes(ownerId);
|
|
4474
5142
|
const created = [];
|
|
4475
5143
|
const effectInputs = [];
|
|
4476
5144
|
if (hasOwner) {
|
|
4477
|
-
for (const participant of
|
|
5145
|
+
for (const participant of participants) {
|
|
4478
5146
|
if (participant.contactId === ownerId) {
|
|
4479
5147
|
continue;
|
|
4480
5148
|
}
|
|
@@ -4482,13 +5150,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4482
5150
|
fromContactId: ownerId,
|
|
4483
5151
|
toContactId: participant.contactId,
|
|
4484
5152
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
5153
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
4485
5154
|
now: params.now
|
|
4486
5155
|
});
|
|
4487
5156
|
created.push(...ensured.created);
|
|
4488
5157
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
4489
5158
|
}
|
|
4490
|
-
} else if (params.eventType === "observation" &&
|
|
4491
|
-
const [first, second] =
|
|
5159
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
5160
|
+
const [first, second] = participants;
|
|
4492
5161
|
if (first !== void 0 && second !== void 0) {
|
|
4493
5162
|
const a = Math.min(first.contactId, second.contactId);
|
|
4494
5163
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -4523,7 +5192,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4523
5192
|
eventType: params.eventType,
|
|
4524
5193
|
occurredAt: params.occurredAt,
|
|
4525
5194
|
significance: params.significance,
|
|
4526
|
-
participant:
|
|
5195
|
+
participant: {
|
|
5196
|
+
...input.participant,
|
|
5197
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
5198
|
+
},
|
|
4527
5199
|
provenance: params.provenance,
|
|
4528
5200
|
now: params.now
|
|
4529
5201
|
})
|
|
@@ -4547,6 +5219,8 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4547
5219
|
function recordCommitment(db, input) {
|
|
4548
5220
|
return withTransaction(db, () => {
|
|
4549
5221
|
validateSocialEventInput(input);
|
|
5222
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5223
|
+
if (existing) return existing;
|
|
4550
5224
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
4551
5225
|
throw new AffinityValidationError(
|
|
4552
5226
|
"commitmentType must be promise or agreement"
|
|
@@ -4561,11 +5235,12 @@ function recordCommitment(db, input) {
|
|
|
4561
5235
|
assertOwnerParticipates(ownerId, ids);
|
|
4562
5236
|
const now = resolveNow(input.now);
|
|
4563
5237
|
const eventId = insertJournalEvent(db, {
|
|
5238
|
+
...input,
|
|
4564
5239
|
type: input.commitmentType,
|
|
4565
5240
|
occurredAt: input.occurredAt,
|
|
4566
|
-
|
|
5241
|
+
occurredOn: input.occurredOn,
|
|
4567
5242
|
significance: input.significance,
|
|
4568
|
-
momentKind: null,
|
|
5243
|
+
momentKind: input.momentKind ?? null,
|
|
4569
5244
|
participants: input.participants,
|
|
4570
5245
|
now
|
|
4571
5246
|
});
|
|
@@ -4574,15 +5249,25 @@ function recordCommitment(db, input) {
|
|
|
4574
5249
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
4575
5250
|
) VALUES (?, ?, ?, ?, ?)`
|
|
4576
5251
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
5252
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
5253
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
5254
|
+
eventId
|
|
5255
|
+
);
|
|
4577
5256
|
const mechanics = applyJournalEventMechanics(db, {
|
|
4578
5257
|
eventId,
|
|
4579
5258
|
eventType: input.commitmentType,
|
|
4580
5259
|
occurredAt: input.occurredAt,
|
|
5260
|
+
occurredOn: input.occurredOn,
|
|
4581
5261
|
significance: input.significance,
|
|
4582
5262
|
participants: input.participants,
|
|
4583
5263
|
provenance: input.provenance,
|
|
4584
5264
|
now
|
|
4585
5265
|
});
|
|
5266
|
+
if (input.momentKind !== void 0)
|
|
5267
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5268
|
+
input.momentKind,
|
|
5269
|
+
eventId
|
|
5270
|
+
);
|
|
4586
5271
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
4587
5272
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4588
5273
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -4603,6 +5288,8 @@ var ALLOWED = /* @__PURE__ */ new Set([
|
|
|
4603
5288
|
function recordInteraction(db, input) {
|
|
4604
5289
|
return withTransaction(db, () => {
|
|
4605
5290
|
validateSocialEventInput(input);
|
|
5291
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5292
|
+
if (existing) return existing;
|
|
4606
5293
|
if (!ALLOWED.has(input.type)) {
|
|
4607
5294
|
throw new AffinityValidationError("invalid interaction event type");
|
|
4608
5295
|
}
|
|
@@ -4612,11 +5299,12 @@ function recordInteraction(db, input) {
|
|
|
4612
5299
|
assertOwnerParticipates(ownerId, ids);
|
|
4613
5300
|
const now = resolveNow(input.now);
|
|
4614
5301
|
const eventId = insertJournalEvent(db, {
|
|
5302
|
+
...input,
|
|
4615
5303
|
type: input.type,
|
|
4616
5304
|
occurredAt: input.occurredAt,
|
|
4617
|
-
|
|
5305
|
+
occurredOn: input.occurredOn,
|
|
4618
5306
|
significance: input.significance,
|
|
4619
|
-
momentKind: null,
|
|
5307
|
+
momentKind: input.momentKind ?? null,
|
|
4620
5308
|
participants: input.participants,
|
|
4621
5309
|
now
|
|
4622
5310
|
});
|
|
@@ -4624,12 +5312,24 @@ function recordInteraction(db, input) {
|
|
|
4624
5312
|
eventId,
|
|
4625
5313
|
eventType: input.type,
|
|
4626
5314
|
occurredAt: input.occurredAt,
|
|
5315
|
+
occurredOn: input.occurredOn,
|
|
4627
5316
|
significance: input.significance,
|
|
4628
5317
|
participants: input.participants,
|
|
4629
5318
|
provenance: input.provenance,
|
|
4630
5319
|
now
|
|
4631
5320
|
});
|
|
5321
|
+
if (input.momentKind !== void 0)
|
|
5322
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5323
|
+
input.momentKind,
|
|
5324
|
+
eventId
|
|
5325
|
+
);
|
|
4632
5326
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5327
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5328
|
+
{
|
|
5329
|
+
code: "future_event",
|
|
5330
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5331
|
+
}
|
|
5332
|
+
] : void 0,
|
|
4633
5333
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4634
5334
|
affectedLinks: mechanics.affectedLinks,
|
|
4635
5335
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4641,17 +5341,20 @@ function recordInteraction(db, input) {
|
|
|
4641
5341
|
function recordMilestone(db, input) {
|
|
4642
5342
|
return withTransaction(db, () => {
|
|
4643
5343
|
validateSocialEventInput(input);
|
|
5344
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5345
|
+
if (existing) return existing;
|
|
4644
5346
|
const ownerId = requireOwnerContactId(db);
|
|
4645
5347
|
const ids = input.participants.map((p) => p.contactId);
|
|
4646
5348
|
assertParticipantContactsLive(db, ids);
|
|
4647
5349
|
assertOwnerParticipates(ownerId, ids);
|
|
4648
5350
|
const now = resolveNow(input.now);
|
|
4649
5351
|
const eventId = insertJournalEvent(db, {
|
|
5352
|
+
...input,
|
|
4650
5353
|
type: "milestone",
|
|
4651
5354
|
occurredAt: input.occurredAt,
|
|
4652
|
-
|
|
5355
|
+
occurredOn: input.occurredOn,
|
|
4653
5356
|
significance: input.significance,
|
|
4654
|
-
momentKind: null,
|
|
5357
|
+
momentKind: input.momentKind ?? null,
|
|
4655
5358
|
participants: input.participants,
|
|
4656
5359
|
now
|
|
4657
5360
|
});
|
|
@@ -4659,12 +5362,24 @@ function recordMilestone(db, input) {
|
|
|
4659
5362
|
eventId,
|
|
4660
5363
|
eventType: "milestone",
|
|
4661
5364
|
occurredAt: input.occurredAt,
|
|
5365
|
+
occurredOn: input.occurredOn,
|
|
4662
5366
|
significance: input.significance,
|
|
4663
5367
|
participants: input.participants,
|
|
4664
5368
|
provenance: input.provenance,
|
|
4665
5369
|
now
|
|
4666
5370
|
});
|
|
5371
|
+
if (input.momentKind !== void 0)
|
|
5372
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5373
|
+
input.momentKind,
|
|
5374
|
+
eventId
|
|
5375
|
+
);
|
|
4667
5376
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5377
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5378
|
+
{
|
|
5379
|
+
code: "future_event",
|
|
5380
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5381
|
+
}
|
|
5382
|
+
] : void 0,
|
|
4668
5383
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4669
5384
|
affectedLinks: mechanics.affectedLinks,
|
|
4670
5385
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4676,15 +5391,18 @@ function recordMilestone(db, input) {
|
|
|
4676
5391
|
function recordObservation(db, input) {
|
|
4677
5392
|
return withTransaction(db, () => {
|
|
4678
5393
|
validateSocialEventInput(input);
|
|
5394
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5395
|
+
if (existing) return existing;
|
|
4679
5396
|
const ids = input.participants.map((p) => p.contactId);
|
|
4680
5397
|
assertParticipantContactsLive(db, ids);
|
|
4681
5398
|
const now = resolveNow(input.now);
|
|
4682
5399
|
const eventId = insertJournalEvent(db, {
|
|
5400
|
+
...input,
|
|
4683
5401
|
type: "observation",
|
|
4684
5402
|
occurredAt: input.occurredAt,
|
|
4685
|
-
|
|
5403
|
+
occurredOn: input.occurredOn,
|
|
4686
5404
|
significance: input.significance,
|
|
4687
|
-
momentKind: null,
|
|
5405
|
+
momentKind: input.momentKind ?? null,
|
|
4688
5406
|
participants: input.participants,
|
|
4689
5407
|
now
|
|
4690
5408
|
});
|
|
@@ -4692,12 +5410,24 @@ function recordObservation(db, input) {
|
|
|
4692
5410
|
eventId,
|
|
4693
5411
|
eventType: "observation",
|
|
4694
5412
|
occurredAt: input.occurredAt,
|
|
5413
|
+
occurredOn: input.occurredOn,
|
|
4695
5414
|
significance: input.significance,
|
|
4696
5415
|
participants: input.participants,
|
|
4697
5416
|
provenance: input.provenance,
|
|
4698
5417
|
now
|
|
4699
5418
|
});
|
|
5419
|
+
if (input.momentKind !== void 0)
|
|
5420
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5421
|
+
input.momentKind,
|
|
5422
|
+
eventId
|
|
5423
|
+
);
|
|
4700
5424
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5425
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5426
|
+
{
|
|
5427
|
+
code: "future_event",
|
|
5428
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5429
|
+
}
|
|
5430
|
+
] : void 0,
|
|
4701
5431
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4702
5432
|
affectedLinks: mechanics.affectedLinks,
|
|
4703
5433
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4709,17 +5439,20 @@ function recordObservation(db, input) {
|
|
|
4709
5439
|
function recordTransaction(db, input) {
|
|
4710
5440
|
return withTransaction(db, () => {
|
|
4711
5441
|
validateSocialEventInput(input);
|
|
5442
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5443
|
+
if (existing) return existing;
|
|
4712
5444
|
const ownerId = requireOwnerContactId(db);
|
|
4713
5445
|
const ids = input.participants.map((p) => p.contactId);
|
|
4714
5446
|
assertParticipantContactsLive(db, ids);
|
|
4715
5447
|
assertOwnerParticipates(ownerId, ids);
|
|
4716
5448
|
const now = resolveNow(input.now);
|
|
4717
5449
|
const eventId = insertJournalEvent(db, {
|
|
5450
|
+
...input,
|
|
4718
5451
|
type: "transaction",
|
|
4719
5452
|
occurredAt: input.occurredAt,
|
|
4720
|
-
|
|
5453
|
+
occurredOn: input.occurredOn,
|
|
4721
5454
|
significance: input.significance,
|
|
4722
|
-
momentKind: null,
|
|
5455
|
+
momentKind: input.momentKind ?? null,
|
|
4723
5456
|
participants: input.participants,
|
|
4724
5457
|
now
|
|
4725
5458
|
});
|
|
@@ -4727,12 +5460,24 @@ function recordTransaction(db, input) {
|
|
|
4727
5460
|
eventId,
|
|
4728
5461
|
eventType: "transaction",
|
|
4729
5462
|
occurredAt: input.occurredAt,
|
|
5463
|
+
occurredOn: input.occurredOn,
|
|
4730
5464
|
significance: input.significance,
|
|
4731
5465
|
participants: input.participants,
|
|
4732
5466
|
provenance: input.provenance,
|
|
4733
5467
|
now
|
|
4734
5468
|
});
|
|
5469
|
+
if (input.momentKind !== void 0)
|
|
5470
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5471
|
+
input.momentKind,
|
|
5472
|
+
eventId
|
|
5473
|
+
);
|
|
4735
5474
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5475
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5476
|
+
{
|
|
5477
|
+
code: "future_event",
|
|
5478
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5479
|
+
}
|
|
5480
|
+
] : void 0,
|
|
4736
5481
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4737
5482
|
affectedLinks: mechanics.affectedLinks,
|
|
4738
5483
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4805,6 +5550,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4805
5550
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
4806
5551
|
validateSocialEventInput({
|
|
4807
5552
|
occurredAt,
|
|
5553
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4808
5554
|
summary,
|
|
4809
5555
|
significance,
|
|
4810
5556
|
participants,
|
|
@@ -4821,6 +5567,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4821
5567
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4822
5568
|
type: eventType,
|
|
4823
5569
|
occurredAt,
|
|
5570
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4824
5571
|
summary,
|
|
4825
5572
|
significance,
|
|
4826
5573
|
momentKind: null,
|
|
@@ -4854,30 +5601,46 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4854
5601
|
});
|
|
4855
5602
|
}
|
|
4856
5603
|
|
|
4857
|
-
// src/contacts/
|
|
4858
|
-
function
|
|
4859
|
-
|
|
5604
|
+
// src/contacts/export_rows.ts
|
|
5605
|
+
function exportRows(db, sql, ...params) {
|
|
5606
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
4860
5607
|
const result = {};
|
|
4861
5608
|
for (const [key, value] of Object.entries(row)) {
|
|
4862
5609
|
const name = key.replace(
|
|
4863
5610
|
/_([a-z])/g,
|
|
4864
5611
|
(_, letter) => letter.toUpperCase()
|
|
4865
5612
|
);
|
|
4866
|
-
result[name] =
|
|
4867
|
-
|
|
5613
|
+
result[name] = [
|
|
5614
|
+
"affects_bond",
|
|
5615
|
+
"is_owner",
|
|
5616
|
+
"is_structural",
|
|
5617
|
+
"verified",
|
|
5618
|
+
"counts_for_recency",
|
|
5619
|
+
"manual"
|
|
5620
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
5621
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
4868
5622
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4869
5623
|
if (key.endsWith("_json") && typeof value === "string")
|
|
4870
5624
|
result[name] = JSON.parse(value);
|
|
4871
5625
|
}
|
|
4872
5626
|
return result;
|
|
4873
5627
|
});
|
|
5628
|
+
}
|
|
5629
|
+
|
|
5630
|
+
// src/contacts/export_contact.ts
|
|
5631
|
+
function exportContact(db, id) {
|
|
5632
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
4874
5633
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4875
5634
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4876
5635
|
const events = query(
|
|
4877
|
-
"SELECT DISTINCT e
|
|
5636
|
+
"SELECT DISTINCT e.*, COALESCE(e.parent_event_id, (SELECT parent.id FROM events parent WHERE parent.external_source=e.parent_source AND parent.external_id=e.parent_ref_id AND parent.deleted_at IS NULL)) AS parent_event_id, 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
5637
|
id
|
|
4879
5638
|
).map((event) => ({
|
|
4880
5639
|
...event,
|
|
5640
|
+
attributes: query(
|
|
5641
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5642
|
+
event.id
|
|
5643
|
+
),
|
|
4881
5644
|
participants: query(
|
|
4882
5645
|
"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
5646
|
event.id
|
|
@@ -4922,14 +5685,100 @@ function exportContact(db, id) {
|
|
|
4922
5685
|
// src/contacts/export_affinity.ts
|
|
4923
5686
|
function exportAffinity(db) {
|
|
4924
5687
|
const now = Date.now();
|
|
5688
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
5689
|
+
const events = exportRows(
|
|
5690
|
+
db,
|
|
5691
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
5692
|
+
).map((event) => ({
|
|
5693
|
+
...event,
|
|
5694
|
+
attributes: exportRows(
|
|
5695
|
+
db,
|
|
5696
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5697
|
+
event.id
|
|
5698
|
+
),
|
|
5699
|
+
participants: exportRows(
|
|
5700
|
+
db,
|
|
5701
|
+
"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",
|
|
5702
|
+
event.id
|
|
5703
|
+
)
|
|
5704
|
+
}));
|
|
4925
5705
|
return {
|
|
4926
5706
|
version: 1,
|
|
4927
5707
|
exportedAt: now,
|
|
4928
5708
|
exportedAtIso: new Date(now).toISOString(),
|
|
4929
|
-
|
|
5709
|
+
events,
|
|
5710
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
5711
|
+
...record,
|
|
5712
|
+
eventIds: events2.map((event) => event.id),
|
|
5713
|
+
dateIds: dates.map((date) => date.id)
|
|
5714
|
+
}))
|
|
4930
5715
|
};
|
|
4931
5716
|
}
|
|
4932
5717
|
|
|
5718
|
+
// src/events/event_discovery.ts
|
|
5719
|
+
function searchEvents(db, query, options) {
|
|
5720
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5721
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
5722
|
+
if (!tokens.length) return [];
|
|
5723
|
+
const rows = db.prepare(
|
|
5724
|
+
"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"
|
|
5725
|
+
).all();
|
|
5726
|
+
return rows.flatMap((row) => {
|
|
5727
|
+
const fields = {
|
|
5728
|
+
summary: decodeEventText(row.summary),
|
|
5729
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
5730
|
+
source: row.external_source,
|
|
5731
|
+
id: row.external_id,
|
|
5732
|
+
channel: row.channel
|
|
5733
|
+
};
|
|
5734
|
+
const folded = Object.entries(fields).map(
|
|
5735
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
5736
|
+
);
|
|
5737
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
5738
|
+
return [];
|
|
5739
|
+
return [
|
|
5740
|
+
{
|
|
5741
|
+
kind: "event",
|
|
5742
|
+
id: row.id,
|
|
5743
|
+
type: row.type,
|
|
5744
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
5745
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
5746
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
5747
|
+
...row.external_source && row.external_id ? {
|
|
5748
|
+
externalRef: {
|
|
5749
|
+
source: row.external_source,
|
|
5750
|
+
id: row.external_id
|
|
5751
|
+
}
|
|
5752
|
+
} : {}
|
|
5753
|
+
}
|
|
5754
|
+
];
|
|
5755
|
+
}).slice(offset, offset + limit);
|
|
5756
|
+
}
|
|
5757
|
+
function getEventThread(db, source, id, options) {
|
|
5758
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5759
|
+
const root = getEventByExternalRef(db, source, id);
|
|
5760
|
+
if (!root) return [];
|
|
5761
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
5762
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
5763
|
+
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=?))
|
|
5764
|
+
) 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=?) AND (? IS NULL OR e.type IN (SELECT value FROM json_each(?))) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
5765
|
+
root.id,
|
|
5766
|
+
root.externalRef?.source ?? null,
|
|
5767
|
+
root.externalRef?.id ?? null,
|
|
5768
|
+
options?.channel ?? null,
|
|
5769
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
5770
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5771
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5772
|
+
limit,
|
|
5773
|
+
offset
|
|
5774
|
+
);
|
|
5775
|
+
return batchLoadEventRecords(
|
|
5776
|
+
db,
|
|
5777
|
+
rows.map((r) => r.id),
|
|
5778
|
+
options?.includeContent ?? false
|
|
5779
|
+
);
|
|
5780
|
+
}
|
|
5781
|
+
|
|
4933
5782
|
// src/soul.ts
|
|
4934
5783
|
var soul_exports = {};
|
|
4935
5784
|
__export(soul_exports, {
|
|
@@ -5136,6 +5985,15 @@ var identifyAndLocateContactsSkill = defineAffinitySkill({
|
|
|
5136
5985
|
description: "Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.",
|
|
5137
5986
|
content: `# Identify And Locate Contacts
|
|
5138
5987
|
|
|
5988
|
+
Handle identities normalize leading @ and case. Bluesky uses a domain handle,
|
|
5989
|
+
X/Twitter uses 1\u201315 letters/digits/underscores, Mastodon uses user@host.tld.
|
|
5990
|
+
Native did (alias atproto_did) validates did:<method>:<identifier>; legacy
|
|
5991
|
+
custom:atproto-did locators find the same identity. Keep old handles as identities.
|
|
5992
|
+
An owned identity makes contact creation fail atomically with conflict and
|
|
5993
|
+
existingContactId. input.ifExists="return" returns the matching contact and
|
|
5994
|
+
matchedIdentities without attaching any free identities. CLI: --if-exists return.
|
|
5995
|
+
Name-only possible duplicates still warn. Search @handle is always literal.
|
|
5996
|
+
|
|
5139
5997
|
Primary tools:
|
|
5140
5998
|
- \`search_affinity\`
|
|
5141
5999
|
- \`inspect_affinity_item\`
|
|
@@ -5193,6 +6051,31 @@ var importHistoryWithoutFakingEvidenceSkill = defineAffinitySkill({
|
|
|
5193
6051
|
description: "Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.",
|
|
5194
6052
|
content: `# Import History Without Faking Evidence
|
|
5195
6053
|
|
|
6054
|
+
For social imports, use native did identities (atproto_did is an alias; legacy
|
|
6055
|
+
custom:atproto-did locators remain equivalent). Create with input.ifExists="return"
|
|
6056
|
+
for safe retries: this attaches nothing to an existing contact. For handle changes,
|
|
6057
|
+
call manage_identity add separately with input.ifExists="return"; an identity already
|
|
6058
|
+
on that contact is a no-op, while another owner still conflicts. Seed links with
|
|
6059
|
+
input.ifExists="return" to reuse them without changing their state.
|
|
6060
|
+
Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
|
|
6061
|
+
{op:"event",kind:"interaction",input:{...}}. Per-line savepoints isolate failures;
|
|
6062
|
+
--stop-on-error stops early. Exit 2 identifies malformed JSON; other failures exit 1.
|
|
6063
|
+
Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
|
|
6064
|
+
Participant contact locators may use identities, including did, instead of ids.
|
|
6065
|
+
Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
|
|
6066
|
+
SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
|
|
6067
|
+
Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
|
|
6068
|
+
The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
|
|
6069
|
+
show the mapping and verify an idempotent retry and byte-identical text.
|
|
6070
|
+
|
|
6071
|
+
For archived messages with known timestamps, record actual evidence with externalRef:
|
|
6072
|
+
{source, id} deduplicates retries across event families and commitments. Keep original
|
|
6073
|
+
message text in content, use channel and parentRef/threadRef for context, and keep
|
|
6074
|
+
summary short or let it derive from content. Content is limited to 1 MB UTF-8;
|
|
6075
|
+
keep larger raw archives in the source system. Inspect events by external reference,
|
|
6076
|
+
search scope "events", and use manage_event revise/delete to correct imports.
|
|
6077
|
+
See the tools guide for field details. Existing 0.5.x databases upgrade in place.
|
|
6078
|
+
|
|
5196
6079
|
Primary tools:
|
|
5197
6080
|
- \`manage_contact\`
|
|
5198
6081
|
- \`manage_identity\`
|
|
@@ -5249,6 +6132,14 @@ var managePromisesAndAgreementsSkill = defineAffinitySkill({
|
|
|
5249
6132
|
description: "Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.",
|
|
5250
6133
|
content: `# Manage Promises And Agreements
|
|
5251
6134
|
|
|
6135
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6136
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6137
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6138
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6139
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6140
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6141
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6142
|
+
|
|
5252
6143
|
Primary tools:
|
|
5253
6144
|
- \`manage_commitment\`
|
|
5254
6145
|
- \`review_affinity\`
|
|
@@ -5304,6 +6195,14 @@ var manageRecurringDatesAndRemindersSkill = defineAffinitySkill({
|
|
|
5304
6195
|
description: "Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.",
|
|
5305
6196
|
content: `# Manage Recurring Dates And Reminders
|
|
5306
6197
|
|
|
6198
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6199
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6200
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6201
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6202
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6203
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6204
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6205
|
+
|
|
5307
6206
|
Primary tools:
|
|
5308
6207
|
- \`manage_date_anchor\`
|
|
5309
6208
|
- \`review_affinity\`
|
|
@@ -5415,6 +6314,24 @@ var recordDirectEvidenceWellSkill = defineAffinitySkill({
|
|
|
5415
6314
|
description: "Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.",
|
|
5416
6315
|
content: `# Record Direct Evidence Well
|
|
5417
6316
|
|
|
6317
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6318
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d). Date-only inputs
|
|
6319
|
+
preserve the calendar day at UTC midnight (UTC end of day for dueAt).
|
|
6320
|
+
Keywords use the process local date. Read occurredOn/dueOn and review occursOn
|
|
6321
|
+
for calendar dates; *Iso fields remain UTC. Future events carry future_event
|
|
6322
|
+
warnings and do not count for recency, cadence or drift until their time and day
|
|
6323
|
+
arrive. Commitments and resolutions never reset contact recency or cadence.
|
|
6324
|
+
|
|
6325
|
+
Imported text and event maintenance:
|
|
6326
|
+
- Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
|
|
6327
|
+
- 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.
|
|
6328
|
+
- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId} or {kind: "event", ref: {source, id}}. Unchanged values return no_op.
|
|
6329
|
+
- Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
|
|
6330
|
+
- Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
|
|
6331
|
+
- 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.
|
|
6332
|
+
- Use manage_event delete with confirm: true to remove incorrect evidence and recompute affected relationship scores.
|
|
6333
|
+
- 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.
|
|
6334
|
+
|
|
5418
6335
|
Primary tools:
|
|
5419
6336
|
- \`record_event\`
|
|
5420
6337
|
- \`inspect_affinity_item\`
|
|
@@ -5473,6 +6390,17 @@ var recordObservationsAndReferralsSkill = defineAffinitySkill({
|
|
|
5473
6390
|
description: "Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.",
|
|
5474
6391
|
content: `# Record Observations And Referrals
|
|
5475
6392
|
|
|
6393
|
+
Likes, reposts and follows are ambient observations, not milestones. Record them
|
|
6394
|
+
with affectsBond:false. Like/follow/repost labels default to false across families;
|
|
6395
|
+
explicit true opts in. Observations with significance <=2 also default to false.
|
|
6396
|
+
They stay in journals but do not change affinity or meaningful-contact recency.
|
|
6397
|
+
provenance.ambient:true also suppresses effects. Use explicit owner and other
|
|
6398
|
+
participants, actor/recipient roles, and the correct directionality.
|
|
6399
|
+
Unanswered owner_initiated evidence contributes much less and does not reset
|
|
6400
|
+
radar recency. A relationship with only outbound activity gets no routine check-in
|
|
6401
|
+
or drift prompt. other_initiated evidence counts fully. Observations and follow-like
|
|
6402
|
+
milestones never derive breakthroughs; low-significance milestones derive no moment.
|
|
6403
|
+
|
|
5476
6404
|
Primary tools:
|
|
5477
6405
|
- \`record_event\`
|
|
5478
6406
|
- \`review_affinity\`
|
|
@@ -5582,6 +6510,19 @@ var reviewRadarProgressionAndGraphSkill = defineAffinitySkill({
|
|
|
5582
6510
|
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
6511
|
content: `# Review Radar, Progression, And Graph
|
|
5584
6512
|
|
|
6513
|
+
List reviews accept limit (default 50, maximum 250) and offset. Results include
|
|
6514
|
+
total and hasMore; continue paging while hasMore is true. Event type array flags
|
|
6515
|
+
accept JSON arrays, comma lists, or repeated --event-types flags; invalid types
|
|
6516
|
+
report allowed values with exit 2. Use skill --short for a compact CLI reference.
|
|
6517
|
+
|
|
6518
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6519
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6520
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6521
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6522
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6523
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6524
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6525
|
+
|
|
5585
6526
|
Primary tools:
|
|
5586
6527
|
- \`review_affinity\`
|
|
5587
6528
|
- \`inspect_affinity_item\`
|
|
@@ -5682,6 +6623,9 @@ __export(tools_exports, {
|
|
|
5682
6623
|
manageDuplicateDismissalTool: () => manageDuplicateDismissalTool,
|
|
5683
6624
|
manageDuplicateDismissalToolHandler: () => manageDuplicateDismissalToolHandler,
|
|
5684
6625
|
manageDuplicateDismissalToolName: () => manageDuplicateDismissalToolName,
|
|
6626
|
+
manageEventTool: () => manageEventTool,
|
|
6627
|
+
manageEventToolHandler: () => manageEventToolHandler,
|
|
6628
|
+
manageEventToolName: () => manageEventToolName,
|
|
5685
6629
|
manageIdentityTool: () => manageIdentityTool,
|
|
5686
6630
|
manageIdentityToolHandler: () => manageIdentityToolHandler,
|
|
5687
6631
|
manageIdentityToolName: () => manageIdentityToolName,
|
|
@@ -5732,37 +6676,8 @@ __export(tools_exports, {
|
|
|
5732
6676
|
});
|
|
5733
6677
|
|
|
5734
6678
|
// src/tools/parse_time_input.ts
|
|
5735
|
-
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, today, tomorrow, yesterday, +3d or -2d";
|
|
5736
6679
|
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
5737
|
-
|
|
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();
|
|
6680
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
5766
6681
|
}
|
|
5767
6682
|
|
|
5768
6683
|
// src/tools/tool_metadata.ts
|
|
@@ -5846,6 +6761,39 @@ function oneOfSchema(schemas, description) {
|
|
|
5846
6761
|
return schema;
|
|
5847
6762
|
}
|
|
5848
6763
|
|
|
6764
|
+
// src/tools/event_metadata_schema.ts
|
|
6765
|
+
var externalRefSchema = objectSchema(
|
|
6766
|
+
{
|
|
6767
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
6768
|
+
id: stringSchema("Non-empty external id.")
|
|
6769
|
+
},
|
|
6770
|
+
["source", "id"]
|
|
6771
|
+
);
|
|
6772
|
+
var eventMetadataSchema = {
|
|
6773
|
+
affectsBond: booleanSchema(
|
|
6774
|
+
"Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
|
|
6775
|
+
),
|
|
6776
|
+
content: stringSchema(
|
|
6777
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
6778
|
+
),
|
|
6779
|
+
externalRef: externalRefSchema,
|
|
6780
|
+
channel: stringSchema(
|
|
6781
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
6782
|
+
),
|
|
6783
|
+
parentRef: externalRefSchema,
|
|
6784
|
+
parentEventId: integerSchema("Parent event id."),
|
|
6785
|
+
threadRef: externalRefSchema,
|
|
6786
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
6787
|
+
"breakthrough",
|
|
6788
|
+
"rupture",
|
|
6789
|
+
"reconciliation",
|
|
6790
|
+
"milestone",
|
|
6791
|
+
"turning_point"
|
|
6792
|
+
]),
|
|
6793
|
+
occurredOn: stringSchema("Calendar day."),
|
|
6794
|
+
provenance: {}
|
|
6795
|
+
};
|
|
6796
|
+
|
|
5849
6797
|
// src/tools/tool_names.ts
|
|
5850
6798
|
var searchAffinityToolName = "search_affinity";
|
|
5851
6799
|
var reviewAffinityToolName = "review_affinity";
|
|
@@ -5859,6 +6807,7 @@ var recordEventToolName = "record_event";
|
|
|
5859
6807
|
var manageCommitmentToolName = "manage_commitment";
|
|
5860
6808
|
var manageDateAnchorToolName = "manage_date_anchor";
|
|
5861
6809
|
var manageAttributeToolName = "manage_attribute";
|
|
6810
|
+
var manageEventToolName = "manage_event";
|
|
5862
6811
|
|
|
5863
6812
|
// src/tools/tool_ref.ts
|
|
5864
6813
|
function contactState(value) {
|
|
@@ -5918,7 +6867,7 @@ function toEventEntityRef(value) {
|
|
|
5918
6867
|
return {
|
|
5919
6868
|
kind: "event",
|
|
5920
6869
|
id: value.id,
|
|
5921
|
-
title: value.summary,
|
|
6870
|
+
title: deriveEventSummary(value.summary),
|
|
5922
6871
|
subtitle: value.type,
|
|
5923
6872
|
state: value.type
|
|
5924
6873
|
};
|
|
@@ -5966,16 +6915,6 @@ function toReceiptEntityRefs(receipt) {
|
|
|
5966
6915
|
});
|
|
5967
6916
|
}
|
|
5968
6917
|
|
|
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
6918
|
// src/tools/tool_types.ts
|
|
5980
6919
|
function withOptionalFields(result, options) {
|
|
5981
6920
|
if (options.next && options.next.length > 0) {
|
|
@@ -6091,7 +7030,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
6091
7030
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
6092
7031
|
return {
|
|
6093
7032
|
ok: false,
|
|
6094
|
-
result: toolFailure("domain", "
|
|
7033
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
6095
7034
|
};
|
|
6096
7035
|
}
|
|
6097
7036
|
return { ok: true, value: row2 };
|
|
@@ -6116,9 +7055,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
6116
7055
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
6117
7056
|
WHERE i.removed_at IS NULL
|
|
6118
7057
|
AND c.deleted_at IS NULL
|
|
6119
|
-
AND i.normalized_key = ?
|
|
7058
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
6120
7059
|
LIMIT 2`
|
|
6121
|
-
).all(
|
|
7060
|
+
).all(
|
|
7061
|
+
normalizedKey,
|
|
7062
|
+
legacyIdentityKey(normalizedKey),
|
|
7063
|
+
normalizedKey
|
|
7064
|
+
);
|
|
6122
7065
|
if (row.length === 0) {
|
|
6123
7066
|
return {
|
|
6124
7067
|
ok: false,
|
|
@@ -6269,13 +7212,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
6269
7212
|
return { ok: true, value: matchedLink };
|
|
6270
7213
|
}
|
|
6271
7214
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
7215
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
7216
|
+
return error;
|
|
6272
7217
|
if (isAffinityError(error)) {
|
|
6273
|
-
|
|
7218
|
+
const result = toolFailure(
|
|
6274
7219
|
"domain",
|
|
6275
7220
|
affinityErrorCodeToToolCode(error),
|
|
6276
7221
|
summary,
|
|
6277
7222
|
error.message
|
|
6278
7223
|
);
|
|
7224
|
+
const id = error.existingContactId;
|
|
7225
|
+
if (id !== void 0) {
|
|
7226
|
+
result.data = { existingContactId: id };
|
|
7227
|
+
result.error.details = { existingContactId: id };
|
|
7228
|
+
}
|
|
7229
|
+
return result;
|
|
6279
7230
|
}
|
|
6280
7231
|
if (error instanceof Error) {
|
|
6281
7232
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -6314,27 +7265,32 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
6314
7265
|
}
|
|
6315
7266
|
|
|
6316
7267
|
// src/tools/validate_tool_input.ts
|
|
6317
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
7268
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
7269
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
7270
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
7271
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
7272
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
7273
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
6318
7274
|
if (schema.anyOf || schema.oneOf) {
|
|
6319
7275
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
6320
7276
|
return choices.some(
|
|
6321
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
7277
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
6322
7278
|
) ? null : `${path}: invalid value`;
|
|
6323
7279
|
}
|
|
6324
7280
|
if (schema.type === "object") {
|
|
6325
7281
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
6326
7282
|
return `${path}: missing input or expected object`;
|
|
6327
7283
|
const object = value;
|
|
6328
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
7284
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
6329
7285
|
for (const key of Object.keys(object)) {
|
|
6330
7286
|
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(", ")}`;
|
|
7287
|
+
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
7288
|
}
|
|
6333
7289
|
}
|
|
6334
7290
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
6335
7291
|
return `${path}.patch: missing required field`;
|
|
6336
7292
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
6337
|
-
return `${path}.patch: unknown
|
|
7293
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
6338
7294
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
6339
7295
|
try {
|
|
6340
7296
|
return parseTimeInput(
|
|
@@ -6349,11 +7305,21 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6349
7305
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
6350
7306
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
6351
7307
|
try {
|
|
6352
|
-
|
|
7308
|
+
const parsed = parseCalendarTime(
|
|
6353
7309
|
object[key],
|
|
6354
7310
|
`${path}.${key}`,
|
|
6355
7311
|
referenceNow
|
|
6356
7312
|
);
|
|
7313
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
|
|
7314
|
+
const provenance = object.provenance;
|
|
7315
|
+
object.provenance = {
|
|
7316
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
7317
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
|
|
7318
|
+
};
|
|
7319
|
+
}
|
|
7320
|
+
object[key] = parsed.timestamp;
|
|
7321
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
7322
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
6357
7323
|
} catch (error) {
|
|
6358
7324
|
return error.message;
|
|
6359
7325
|
}
|
|
@@ -6368,8 +7334,9 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6368
7334
|
const error = validateToolInput(
|
|
6369
7335
|
child,
|
|
6370
7336
|
object[key],
|
|
6371
|
-
`${path}.${key}`,
|
|
6372
|
-
referenceNow
|
|
7337
|
+
key === "input" && path === "input" ? path : `${path}.${key}`,
|
|
7338
|
+
referenceNow,
|
|
7339
|
+
strict
|
|
6373
7340
|
);
|
|
6374
7341
|
if (error) return error;
|
|
6375
7342
|
}
|
|
@@ -6382,7 +7349,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6382
7349
|
schema.items ?? {},
|
|
6383
7350
|
item,
|
|
6384
7351
|
`${path}[${index}]`,
|
|
6385
|
-
injectedNow
|
|
7352
|
+
injectedNow,
|
|
7353
|
+
strict
|
|
6386
7354
|
);
|
|
6387
7355
|
if (error) return error;
|
|
6388
7356
|
}
|
|
@@ -6448,6 +7416,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
6448
7416
|
inputError
|
|
6449
7417
|
);
|
|
6450
7418
|
return withToolHandling(() => {
|
|
7419
|
+
if (input.kind === "event") {
|
|
7420
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
7421
|
+
return toolFailure(
|
|
7422
|
+
"protocol",
|
|
7423
|
+
"invalid_input",
|
|
7424
|
+
"Invalid event target.",
|
|
7425
|
+
"Provide exactly one of eventId or ref."
|
|
7426
|
+
);
|
|
7427
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
7428
|
+
return detail2 ? toolSuccess(
|
|
7429
|
+
`Loaded event #${detail2.id}.`,
|
|
7430
|
+
{ kind: "event", detail: detail2 },
|
|
7431
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
7432
|
+
) : toolFailure(
|
|
7433
|
+
"domain",
|
|
7434
|
+
"not_found",
|
|
7435
|
+
"Event not found.",
|
|
7436
|
+
"No event matches the target."
|
|
7437
|
+
);
|
|
7438
|
+
}
|
|
6451
7439
|
if (input.kind === "owner_profile") {
|
|
6452
7440
|
const detail2 = getOwnerProfile(
|
|
6453
7441
|
db,
|
|
@@ -6545,10 +7533,13 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
6545
7533
|
inputSchema: objectSchema(
|
|
6546
7534
|
{
|
|
6547
7535
|
kind: enumSchema("What to inspect.", [
|
|
7536
|
+
"event",
|
|
6548
7537
|
"owner_profile",
|
|
6549
7538
|
"contact_profile",
|
|
6550
7539
|
"link"
|
|
6551
7540
|
]),
|
|
7541
|
+
eventId: integerSchema("Exact event id."),
|
|
7542
|
+
ref: externalRefSchema,
|
|
6552
7543
|
contact: contactLocatorSchema(
|
|
6553
7544
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
6554
7545
|
),
|
|
@@ -6567,6 +7558,7 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
6567
7558
|
// src/attributes/helpers.ts
|
|
6568
7559
|
function clearedAttributePrimary(target) {
|
|
6569
7560
|
return {
|
|
7561
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
6570
7562
|
id: 0,
|
|
6571
7563
|
contactId: target.kind === "contact" ? target.id : null,
|
|
6572
7564
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -6578,6 +7570,7 @@ function clearedAttributePrimary(target) {
|
|
|
6578
7570
|
// src/attributes/mappers.ts
|
|
6579
7571
|
function mapAttributeRowToAttributeRecord(row) {
|
|
6580
7572
|
return {
|
|
7573
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
6581
7574
|
id: row.id,
|
|
6582
7575
|
contactId: row.contact_id,
|
|
6583
7576
|
linkId: row.link_id,
|
|
@@ -6587,49 +7580,26 @@ function mapAttributeRowToAttributeRecord(row) {
|
|
|
6587
7580
|
}
|
|
6588
7581
|
|
|
6589
7582
|
// src/attributes/queries.ts
|
|
7583
|
+
function attributeTargetSql(target) {
|
|
7584
|
+
return {
|
|
7585
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
7586
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
7587
|
+
};
|
|
7588
|
+
}
|
|
6590
7589
|
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;
|
|
7590
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
6599
7591
|
}
|
|
6600
7592
|
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;
|
|
7593
|
+
const t = attributeTargetSql(target);
|
|
7594
|
+
return db.prepare(
|
|
7595
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
7596
|
+
).get(t.id, name) ?? null;
|
|
6621
7597
|
}
|
|
6622
7598
|
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);
|
|
7599
|
+
const t = attributeTargetSql(target);
|
|
7600
|
+
return db.prepare(
|
|
7601
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
7602
|
+
).all(t.id).map((r) => r.id);
|
|
6633
7603
|
}
|
|
6634
7604
|
|
|
6635
7605
|
// src/attributes/receipts.ts
|
|
@@ -6651,6 +7621,8 @@ function normalizeAttributeValue(value) {
|
|
|
6651
7621
|
if (value === null) {
|
|
6652
7622
|
return null;
|
|
6653
7623
|
}
|
|
7624
|
+
if (typeof value !== "string")
|
|
7625
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
6654
7626
|
const trimmed = value.trim();
|
|
6655
7627
|
return trimmed.length === 0 ? null : trimmed;
|
|
6656
7628
|
}
|
|
@@ -6679,6 +7651,11 @@ function validateAttributeEntries(entries) {
|
|
|
6679
7651
|
return out;
|
|
6680
7652
|
}
|
|
6681
7653
|
function assertAttributeTargetWritable(db, target) {
|
|
7654
|
+
if (target.kind === "event") {
|
|
7655
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
7656
|
+
throw new AffinityNotFoundError("event not found");
|
|
7657
|
+
return;
|
|
7658
|
+
}
|
|
6682
7659
|
if (target.kind === "contact") {
|
|
6683
7660
|
const contact = getContactRowById(db, target.id);
|
|
6684
7661
|
if (!contact) {
|
|
@@ -6714,26 +7691,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
6714
7691
|
}
|
|
6715
7692
|
const created = [];
|
|
6716
7693
|
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
|
-
}
|
|
7694
|
+
const t = attributeTargetSql(target);
|
|
7695
|
+
const insert = db.prepare(
|
|
7696
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
7697
|
+
);
|
|
7698
|
+
for (const e of normalized) {
|
|
7699
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
7700
|
+
lastId = Number(r.lastInsertRowid);
|
|
7701
|
+
created.push({ kind: "attribute", id: lastId });
|
|
6737
7702
|
}
|
|
6738
7703
|
if (normalized.length === 0) {
|
|
6739
7704
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -6763,6 +7728,11 @@ function setAttribute(db, target, name, value) {
|
|
|
6763
7728
|
const stored = normalizeAttributeValue(value);
|
|
6764
7729
|
const now = resolveNow();
|
|
6765
7730
|
const existing = getLiveAttributeRow(db, target, key);
|
|
7731
|
+
if (existing?.value === stored)
|
|
7732
|
+
return buildAttributeMutationReceipt(
|
|
7733
|
+
mapAttributeRowToAttributeRecord(existing),
|
|
7734
|
+
{}
|
|
7735
|
+
);
|
|
6766
7736
|
if (existing) {
|
|
6767
7737
|
db.prepare(
|
|
6768
7738
|
"UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
|
|
@@ -6778,27 +7748,10 @@ function setAttribute(db, target, name, value) {
|
|
|
6778
7748
|
}
|
|
6779
7749
|
);
|
|
6780
7750
|
}
|
|
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
|
-
}
|
|
7751
|
+
const t = attributeTargetSql(target);
|
|
6798
7752
|
const r = db.prepare(
|
|
6799
|
-
`INSERT INTO attributes (
|
|
6800
|
-
|
|
6801
|
-
).run(target.id, key, stored, now, now);
|
|
7753
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
7754
|
+
).run(t.id, key, stored, now, now);
|
|
6802
7755
|
const id = Number(r.lastInsertRowid);
|
|
6803
7756
|
const row = getAttributeRowById(db, id);
|
|
6804
7757
|
if (!row) {
|
|
@@ -6854,14 +7807,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
6854
7807
|
affectedLinks: receipt.affectedLinks,
|
|
6855
7808
|
derivedEffects: receipt.derivedEffects
|
|
6856
7809
|
};
|
|
6857
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
7810
|
+
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
7811
|
if (changedCount === 0) {
|
|
6859
7812
|
return toolNoOp(summary, data, {
|
|
6860
7813
|
entities: toReceiptEntityRefs(receipt)
|
|
6861
7814
|
});
|
|
6862
7815
|
}
|
|
6863
7816
|
return toolSuccess(summary, data, {
|
|
6864
|
-
entities: toReceiptEntityRefs(receipt)
|
|
7817
|
+
entities: toReceiptEntityRefs(receipt),
|
|
7818
|
+
warnings: receipt.warnings
|
|
6865
7819
|
});
|
|
6866
7820
|
}
|
|
6867
7821
|
var PLURALS = { identity: "identities" };
|
|
@@ -6918,6 +7872,30 @@ function linkLocatorSchema2(description) {
|
|
|
6918
7872
|
};
|
|
6919
7873
|
}
|
|
6920
7874
|
function resolveAttributeTarget(db, target) {
|
|
7875
|
+
if (target.kind === "event") {
|
|
7876
|
+
if (target.eventId === void 0 === (target.ref === void 0))
|
|
7877
|
+
return {
|
|
7878
|
+
kind: "error",
|
|
7879
|
+
result: toolFailure(
|
|
7880
|
+
"protocol",
|
|
7881
|
+
"invalid_input",
|
|
7882
|
+
"Invalid event target.",
|
|
7883
|
+
"Provide exactly one of target.eventId or target.ref."
|
|
7884
|
+
)
|
|
7885
|
+
};
|
|
7886
|
+
const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
|
|
7887
|
+
if (eventId === void 0)
|
|
7888
|
+
return {
|
|
7889
|
+
kind: "error",
|
|
7890
|
+
result: toolFailure(
|
|
7891
|
+
"domain",
|
|
7892
|
+
"not_found",
|
|
7893
|
+
"Event not found.",
|
|
7894
|
+
"No event matches target.ref."
|
|
7895
|
+
)
|
|
7896
|
+
};
|
|
7897
|
+
return { kind: "ok", target: { kind: "event", eventId } };
|
|
7898
|
+
}
|
|
6921
7899
|
if (target.kind === "contact") {
|
|
6922
7900
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
6923
7901
|
if (!contact.ok) {
|
|
@@ -6977,7 +7955,7 @@ function manageAttributeToolHandler(db, rawInput) {
|
|
|
6977
7955
|
}
|
|
6978
7956
|
var manageAttributeTool = defineAffinityTool({
|
|
6979
7957
|
name: manageAttributeToolName,
|
|
6980
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
7958
|
+
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
7959
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
6982
7960
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
6983
7961
|
sideEffects: "writes_state",
|
|
@@ -6998,7 +7976,9 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
6998
7976
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
6999
7977
|
target: objectSchema(
|
|
7000
7978
|
{
|
|
7001
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
7979
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
7980
|
+
eventId: integerSchema("Target event id; provide eventId or ref."),
|
|
7981
|
+
ref: externalRefSchema,
|
|
7002
7982
|
contact: contactLocatorSchema2(
|
|
7003
7983
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
7004
7984
|
),
|
|
@@ -7028,7 +8008,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
7028
8008
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
7029
8009
|
},
|
|
7030
8010
|
["action"],
|
|
7031
|
-
"Set, unset, or replace attributes on contacts and
|
|
8011
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
7032
8012
|
),
|
|
7033
8013
|
handler: manageAttributeToolHandler
|
|
7034
8014
|
});
|
|
@@ -7060,7 +8040,13 @@ function prepareParticipants(db, raw) {
|
|
|
7060
8040
|
}
|
|
7061
8041
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
7062
8042
|
const input = structuredClone(rawInput);
|
|
7063
|
-
const inputError = validateToolInput(
|
|
8043
|
+
const inputError = validateToolInput(
|
|
8044
|
+
manageCommitmentTool.inputSchema,
|
|
8045
|
+
input,
|
|
8046
|
+
"input",
|
|
8047
|
+
Date.now(),
|
|
8048
|
+
true
|
|
8049
|
+
);
|
|
7064
8050
|
if (inputError)
|
|
7065
8051
|
return toolFailure(
|
|
7066
8052
|
"protocol",
|
|
@@ -7090,6 +8076,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
7090
8076
|
"commitment"
|
|
7091
8077
|
);
|
|
7092
8078
|
}
|
|
8079
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
8080
|
+
return toolFailure(
|
|
8081
|
+
"protocol",
|
|
8082
|
+
"invalid_input",
|
|
8083
|
+
"Missing commitment.",
|
|
8084
|
+
"commitmentEventId is required for resolve."
|
|
8085
|
+
);
|
|
8086
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
8087
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
8088
|
+
return toolFailure(
|
|
8089
|
+
"domain",
|
|
8090
|
+
"invalid_input",
|
|
8091
|
+
"Invalid commitment.",
|
|
8092
|
+
"not a commitment event"
|
|
8093
|
+
);
|
|
7093
8094
|
return mutationToolResult(
|
|
7094
8095
|
"resolve",
|
|
7095
8096
|
resolveCommitment(
|
|
@@ -7128,15 +8129,21 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7128
8129
|
"promise",
|
|
7129
8130
|
"agreement"
|
|
7130
8131
|
]),
|
|
8132
|
+
...eventMetadataSchema,
|
|
7131
8133
|
occurredAt: integerSchema(
|
|
7132
8134
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
7133
8135
|
),
|
|
7134
8136
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
7135
8137
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
7136
8138
|
dueAt: integerSchema("Optional due timestamp."),
|
|
8139
|
+
dueOn: stringSchema("Due calendar day."),
|
|
7137
8140
|
participants: arraySchema(
|
|
7138
8141
|
objectSchema(
|
|
7139
8142
|
{
|
|
8143
|
+
contact: objectSchema(
|
|
8144
|
+
{ contactId: integerSchema("Contact id.") },
|
|
8145
|
+
["contactId"]
|
|
8146
|
+
),
|
|
7140
8147
|
contactId: integerSchema(
|
|
7141
8148
|
"Participant contact id. The owner contact MUST be included."
|
|
7142
8149
|
),
|
|
@@ -7149,13 +8156,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7149
8156
|
),
|
|
7150
8157
|
now: integerSchema("Optional timestamp override.")
|
|
7151
8158
|
},
|
|
7152
|
-
[
|
|
7153
|
-
"commitmentType",
|
|
7154
|
-
"occurredAt",
|
|
7155
|
-
"summary",
|
|
7156
|
-
"significance",
|
|
7157
|
-
"participants"
|
|
7158
|
-
],
|
|
8159
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
7159
8160
|
"Commitment payload. Required when action=record."
|
|
7160
8161
|
),
|
|
7161
8162
|
commitmentEventId: integerSchema(
|
|
@@ -7170,6 +8171,19 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7170
8171
|
now: integerSchema("Optional timestamp."),
|
|
7171
8172
|
resolvingEvent: objectSchema(
|
|
7172
8173
|
{
|
|
8174
|
+
occurredOn: stringSchema("Calendar day."),
|
|
8175
|
+
provenance: {},
|
|
8176
|
+
participants: arraySchema(
|
|
8177
|
+
objectSchema(
|
|
8178
|
+
{
|
|
8179
|
+
contactId: integerSchema("Contact id."),
|
|
8180
|
+
role: stringSchema("Role."),
|
|
8181
|
+
directionality: stringSchema("Directionality.")
|
|
8182
|
+
},
|
|
8183
|
+
["contactId", "role"]
|
|
8184
|
+
),
|
|
8185
|
+
"Resolving participants."
|
|
8186
|
+
),
|
|
7173
8187
|
type: stringSchema("Optional resolving event type."),
|
|
7174
8188
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
7175
8189
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -7188,221 +8202,69 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7188
8202
|
"Record or resolve one commitment."
|
|
7189
8203
|
),
|
|
7190
8204
|
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
|
-
}
|
|
8205
|
+
});
|
|
7358
8206
|
|
|
7359
|
-
// src/
|
|
7360
|
-
function
|
|
8207
|
+
// src/contacts/delete_contact.ts
|
|
8208
|
+
function deleteContact(db, locatorId, options) {
|
|
8209
|
+
if (options?.confirm !== true)
|
|
8210
|
+
throw new AffinityValidationError(
|
|
8211
|
+
"delete is permanent; pass confirm: true"
|
|
8212
|
+
);
|
|
7361
8213
|
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
|
|
8214
|
+
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
8215
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
8216
|
+
if (contact.is_owner)
|
|
8217
|
+
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
8218
|
+
const tables = [
|
|
8219
|
+
"contacts",
|
|
8220
|
+
"identities",
|
|
8221
|
+
"attributes",
|
|
8222
|
+
"links",
|
|
8223
|
+
"events",
|
|
8224
|
+
"event_participants",
|
|
8225
|
+
"open_commitments",
|
|
8226
|
+
"contact_merges",
|
|
8227
|
+
"dismissed_duplicates",
|
|
8228
|
+
"link_event_effects",
|
|
8229
|
+
"upcoming_occurrences",
|
|
8230
|
+
"link_rollups",
|
|
8231
|
+
"contact_rollups"
|
|
8232
|
+
];
|
|
8233
|
+
const before = new Map(
|
|
8234
|
+
tables.map((table) => [
|
|
8235
|
+
table,
|
|
8236
|
+
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
8237
|
+
])
|
|
7393
8238
|
);
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
8239
|
+
db.prepare(
|
|
8240
|
+
"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))"
|
|
8241
|
+
).run(locatorId, locatorId);
|
|
8242
|
+
db.prepare(
|
|
8243
|
+
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
8244
|
+
).run(locatorId, locatorId);
|
|
7400
8245
|
db.prepare(
|
|
7401
8246
|
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7402
|
-
).run(
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
8247
|
+
).run(locatorId, locatorId);
|
|
8248
|
+
db.prepare(
|
|
8249
|
+
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
8250
|
+
).run(locatorId);
|
|
8251
|
+
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
8252
|
+
const now = Date.now();
|
|
8253
|
+
const affectedLinks = db.prepare(
|
|
8254
|
+
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
8255
|
+
).all().map((row) => row.id);
|
|
8256
|
+
refreshAllBridgeScores(db, now);
|
|
8257
|
+
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
8258
|
+
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
8259
|
+
refreshContactRollup(db, Number(row.id), now);
|
|
8260
|
+
const removed = tables.map((table) => {
|
|
8261
|
+
const remaining = new Set(
|
|
8262
|
+
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
8263
|
+
);
|
|
8264
|
+
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
8265
|
+
return { entity: table, count: ids.length, ids };
|
|
8266
|
+
}).filter((item) => item.count > 0);
|
|
8267
|
+
return { contactId: locatorId, removed, affectedLinks };
|
|
7406
8268
|
});
|
|
7407
8269
|
}
|
|
7408
8270
|
|
|
@@ -7410,27 +8272,20 @@ function addIdentity(db, contactId, input) {
|
|
|
7410
8272
|
function createContactToolResult(db, input) {
|
|
7411
8273
|
return withTransaction(db, () => {
|
|
7412
8274
|
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
8275
|
const result = mutationToolResult("create", receipt, "contact");
|
|
8276
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
8277
|
+
result.outcome = "no_op";
|
|
8278
|
+
Object.assign(result.data, {
|
|
8279
|
+
matchedIdentities: receipt.matchedIdentities
|
|
8280
|
+
});
|
|
8281
|
+
return result;
|
|
8282
|
+
}
|
|
7428
8283
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
7429
8284
|
const warnings = db.prepare(
|
|
7430
8285
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
7431
8286
|
).all(receipt.primary.id).flatMap((row) => {
|
|
7432
|
-
const score =
|
|
7433
|
-
return score
|
|
8287
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
8288
|
+
return score <= 0 ? [] : [
|
|
7434
8289
|
{
|
|
7435
8290
|
code: "possible_duplicate",
|
|
7436
8291
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -7513,6 +8368,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
7513
8368
|
return contact.result;
|
|
7514
8369
|
}
|
|
7515
8370
|
if (input.action === "delete") {
|
|
8371
|
+
if (contact.value.is_owner)
|
|
8372
|
+
return toolFailure(
|
|
8373
|
+
"domain",
|
|
8374
|
+
"conflict",
|
|
8375
|
+
"Cannot delete owner.",
|
|
8376
|
+
"The owner contact cannot be deleted"
|
|
8377
|
+
);
|
|
7516
8378
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
7517
8379
|
return toolSuccess("Permanently deleted contact.", {
|
|
7518
8380
|
action: "delete",
|
|
@@ -7586,7 +8448,11 @@ var manageContactTool = defineAffinityTool({
|
|
|
7586
8448
|
},
|
|
7587
8449
|
["type", "value"]
|
|
7588
8450
|
),
|
|
7589
|
-
"Optional identities to attach.
|
|
8451
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
8452
|
+
),
|
|
8453
|
+
ifExists: enumSchema(
|
|
8454
|
+
"Identity collision policy; return attaches nothing.",
|
|
8455
|
+
["error", "return"]
|
|
7590
8456
|
),
|
|
7591
8457
|
name: stringSchema("Contact name."),
|
|
7592
8458
|
kind: enumSchema("Contact kind.", [
|
|
@@ -8005,7 +8871,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
8005
8871
|
});
|
|
8006
8872
|
}
|
|
8007
8873
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
8008
|
-
const nextValue =
|
|
8874
|
+
const nextValue = normalizeIdentityValue(
|
|
8875
|
+
nextType,
|
|
8876
|
+
patch.value ?? row.value
|
|
8877
|
+
);
|
|
8009
8878
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
8010
8879
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
8011
8880
|
}
|
|
@@ -8064,6 +8933,10 @@ function contactLocatorSchema6(description) {
|
|
|
8064
8933
|
contactId: integerSchema("Exact contact id."),
|
|
8065
8934
|
identity: objectSchema(
|
|
8066
8935
|
{
|
|
8936
|
+
ifExists: enumSchema(
|
|
8937
|
+
"Return an existing identity on this contact without changes.",
|
|
8938
|
+
["error", "return"]
|
|
8939
|
+
),
|
|
8067
8940
|
type: stringSchema(
|
|
8068
8941
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8069
8942
|
),
|
|
@@ -8160,6 +9033,10 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
8160
9033
|
),
|
|
8161
9034
|
input: objectSchema(
|
|
8162
9035
|
{
|
|
9036
|
+
ifExists: enumSchema(
|
|
9037
|
+
"Return an existing identity on this contact without changes.",
|
|
9038
|
+
["error", "return"]
|
|
9039
|
+
),
|
|
8163
9040
|
type: stringSchema(
|
|
8164
9041
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8165
9042
|
),
|
|
@@ -8494,11 +9371,13 @@ var mergeContactsTool = defineAffinityTool({
|
|
|
8494
9371
|
});
|
|
8495
9372
|
|
|
8496
9373
|
// src/tools/record_event_tool.ts
|
|
8497
|
-
function normalizeParticipant2(p) {
|
|
9374
|
+
function normalizeParticipant2(db, p) {
|
|
8498
9375
|
let contactId = p.contactId;
|
|
8499
9376
|
if (contactId === void 0 && p.contact != null) {
|
|
8500
9377
|
const loc = p.contact;
|
|
8501
|
-
|
|
9378
|
+
const resolved = resolveContactLocator(db, loc);
|
|
9379
|
+
if (!resolved.ok) throw resolved.result;
|
|
9380
|
+
contactId = resolved.value.id;
|
|
8502
9381
|
}
|
|
8503
9382
|
const result = {
|
|
8504
9383
|
contactId,
|
|
@@ -8516,12 +9395,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
8516
9395
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
8517
9396
|
}
|
|
8518
9397
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
8519
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
9398
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
8520
9399
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
8521
9400
|
}
|
|
8522
9401
|
function recordEventToolHandler(db, rawInput) {
|
|
8523
9402
|
const input = structuredClone(rawInput);
|
|
8524
|
-
const inputError = validateToolInput(
|
|
9403
|
+
const inputError = validateToolInput(
|
|
9404
|
+
recordEventTool.inputSchema,
|
|
9405
|
+
input,
|
|
9406
|
+
"input",
|
|
9407
|
+
Date.now(),
|
|
9408
|
+
true
|
|
9409
|
+
);
|
|
8525
9410
|
if (inputError)
|
|
8526
9411
|
return toolFailure(
|
|
8527
9412
|
"protocol",
|
|
@@ -8601,6 +9486,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
8601
9486
|
"correction"
|
|
8602
9487
|
]
|
|
8603
9488
|
),
|
|
9489
|
+
...eventMetadataSchema,
|
|
8604
9490
|
occurredAt: integerSchema(
|
|
8605
9491
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
8606
9492
|
),
|
|
@@ -8609,6 +9495,19 @@ var recordEventTool = defineAffinityTool({
|
|
|
8609
9495
|
participants: arraySchema(
|
|
8610
9496
|
objectSchema(
|
|
8611
9497
|
{
|
|
9498
|
+
contact: objectSchema(
|
|
9499
|
+
{
|
|
9500
|
+
contactId: integerSchema("Contact id."),
|
|
9501
|
+
identity: objectSchema(
|
|
9502
|
+
{
|
|
9503
|
+
type: stringSchema("Identity type."),
|
|
9504
|
+
value: stringSchema("Identity value.")
|
|
9505
|
+
},
|
|
9506
|
+
["type", "value"]
|
|
9507
|
+
)
|
|
9508
|
+
},
|
|
9509
|
+
[]
|
|
9510
|
+
),
|
|
8612
9511
|
contactId: integerSchema(
|
|
8613
9512
|
"Participant contact id. The owner contact MUST be included."
|
|
8614
9513
|
),
|
|
@@ -8632,7 +9531,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
8632
9531
|
),
|
|
8633
9532
|
now: integerSchema("Optional timestamp override.")
|
|
8634
9533
|
},
|
|
8635
|
-
["occurredAt", "
|
|
9534
|
+
["occurredAt", "significance", "participants"],
|
|
8636
9535
|
"Event payload. Always required. Interaction events also require `type`."
|
|
8637
9536
|
)
|
|
8638
9537
|
},
|
|
@@ -8671,13 +9570,40 @@ function enrichReviewItem(db, item) {
|
|
|
8671
9570
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8672
9571
|
}
|
|
8673
9572
|
if (Array.isArray(item.participants)) {
|
|
8674
|
-
result.
|
|
8675
|
-
(p) =>
|
|
9573
|
+
result.participants = item.participants.map(
|
|
9574
|
+
(p) => ({
|
|
9575
|
+
id: p.contactId,
|
|
9576
|
+
name: name(p.contactId),
|
|
9577
|
+
role: p.role
|
|
9578
|
+
})
|
|
8676
9579
|
);
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
9580
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
9581
|
+
result[key] = void 0;
|
|
9582
|
+
}
|
|
9583
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
9584
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
9585
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
9586
|
+
if (result.title === result.summary) result.title = void 0;
|
|
9587
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
9588
|
+
for (const key of [
|
|
9589
|
+
"affinity",
|
|
9590
|
+
"trust",
|
|
9591
|
+
"rank",
|
|
9592
|
+
"recencyScore",
|
|
9593
|
+
"driftPriority",
|
|
9594
|
+
"readinessScore",
|
|
9595
|
+
"normalizedRank",
|
|
9596
|
+
"reciprocityScore",
|
|
9597
|
+
"radarScore",
|
|
9598
|
+
"subtitle",
|
|
9599
|
+
"snippet",
|
|
9600
|
+
"targetRef",
|
|
9601
|
+
"occursOnDate"
|
|
9602
|
+
])
|
|
9603
|
+
result[key] = void 0;
|
|
9604
|
+
} else if (item.kind === "link") {
|
|
9605
|
+
result.recommendedReason = void 0;
|
|
9606
|
+
result.snippet = void 0;
|
|
8681
9607
|
}
|
|
8682
9608
|
if (typeof item.leftContactId === "number")
|
|
8683
9609
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -8706,6 +9632,8 @@ function summarizeCount(count, singular) {
|
|
|
8706
9632
|
// src/tools/review_affinity_tool.ts
|
|
8707
9633
|
function buildReadOptions(input) {
|
|
8708
9634
|
return {
|
|
9635
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
9636
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
8709
9637
|
...input.now === void 0 ? {} : { now: input.now },
|
|
8710
9638
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
8711
9639
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -8842,7 +9770,7 @@ function momentItem(moment) {
|
|
|
8842
9770
|
id: moment.eventId,
|
|
8843
9771
|
title: moment.summary,
|
|
8844
9772
|
subtitle: moment.momentKind,
|
|
8845
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
9773
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8846
9774
|
};
|
|
8847
9775
|
}
|
|
8848
9776
|
function commitmentItem(commitment) {
|
|
@@ -8852,7 +9780,7 @@ function commitmentItem(commitment) {
|
|
|
8852
9780
|
id: commitment.eventId,
|
|
8853
9781
|
title: commitment.summary,
|
|
8854
9782
|
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)}`
|
|
9783
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8856
9784
|
};
|
|
8857
9785
|
}
|
|
8858
9786
|
function radarItem(radar) {
|
|
@@ -8868,6 +9796,7 @@ function radarItem(radar) {
|
|
|
8868
9796
|
function upcomingDateItem(date) {
|
|
8869
9797
|
return {
|
|
8870
9798
|
...date,
|
|
9799
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
8871
9800
|
occursAt: date.occursOn,
|
|
8872
9801
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
8873
9802
|
kind: date.targetRef.kind,
|
|
@@ -8905,6 +9834,9 @@ function compactFilters(input) {
|
|
|
8905
9834
|
);
|
|
8906
9835
|
}
|
|
8907
9836
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
9837
|
+
return reviewPage(db, rawInput, false);
|
|
9838
|
+
}
|
|
9839
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
8908
9840
|
const input = structuredClone(rawInput);
|
|
8909
9841
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8910
9842
|
if (inputError)
|
|
@@ -8919,6 +9851,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8919
9851
|
if (invalid) {
|
|
8920
9852
|
return invalid;
|
|
8921
9853
|
}
|
|
9854
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
9855
|
+
const limit = input.limit ?? 50;
|
|
9856
|
+
const offset = input.offset ?? 0;
|
|
9857
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
|
|
9858
|
+
return toolFailure(
|
|
9859
|
+
"protocol",
|
|
9860
|
+
"invalid_input",
|
|
9861
|
+
"Invalid pagination.",
|
|
9862
|
+
"limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
|
|
9863
|
+
);
|
|
9864
|
+
const all = [];
|
|
9865
|
+
let page;
|
|
9866
|
+
do {
|
|
9867
|
+
page = reviewPage(
|
|
9868
|
+
db,
|
|
9869
|
+
{ ...input, limit: 250, offset: all.length },
|
|
9870
|
+
true
|
|
9871
|
+
);
|
|
9872
|
+
if (!page.ok) return page;
|
|
9873
|
+
all.push(...page.data.items);
|
|
9874
|
+
} while (page.data.items.length === 250);
|
|
9875
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9876
|
+
return toolSuccess(
|
|
9877
|
+
summarizeCount(items2.length, "item"),
|
|
9878
|
+
{
|
|
9879
|
+
view: input.view,
|
|
9880
|
+
items: items2,
|
|
9881
|
+
count: items2.length,
|
|
9882
|
+
total: all.length,
|
|
9883
|
+
hasMore: offset + items2.length < all.length,
|
|
9884
|
+
appliedFilters: compactFilters(input)
|
|
9885
|
+
},
|
|
9886
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
9887
|
+
);
|
|
9888
|
+
}
|
|
8922
9889
|
const options = buildReadOptions(input);
|
|
8923
9890
|
let items = [];
|
|
8924
9891
|
let chart;
|
|
@@ -8944,12 +9911,26 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8944
9911
|
{
|
|
8945
9912
|
...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
|
|
8946
9913
|
...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
|
|
8947
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
9914
|
+
...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
|
|
8948
9915
|
...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
|
|
8949
9916
|
},
|
|
8950
9917
|
options
|
|
8951
9918
|
).map(duplicateCandidateItem);
|
|
8952
9919
|
break;
|
|
9920
|
+
case "events.thread": {
|
|
9921
|
+
if (!input.ref)
|
|
9922
|
+
return toolFailure(
|
|
9923
|
+
"protocol",
|
|
9924
|
+
"invalid_input",
|
|
9925
|
+
"Missing thread reference.",
|
|
9926
|
+
"ref is required for events.thread."
|
|
9927
|
+
);
|
|
9928
|
+
items = getEventThread(db, input.ref.source, input.ref.id, {
|
|
9929
|
+
...options,
|
|
9930
|
+
...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
|
|
9931
|
+
}).map(toEventListToolItem);
|
|
9932
|
+
break;
|
|
9933
|
+
}
|
|
8953
9934
|
case "events.contact_journal": {
|
|
8954
9935
|
const locator = input.contact;
|
|
8955
9936
|
if (locator === void 0) {
|
|
@@ -9091,6 +10072,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9091
10072
|
items = listRadar(
|
|
9092
10073
|
db,
|
|
9093
10074
|
{
|
|
10075
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
9094
10076
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
9095
10077
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
9096
10078
|
},
|
|
@@ -9140,6 +10122,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9140
10122
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
9141
10123
|
break;
|
|
9142
10124
|
}
|
|
10125
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
10126
|
+
items = items.map((item) => {
|
|
10127
|
+
const content = getEventById(
|
|
10128
|
+
db,
|
|
10129
|
+
Number(item.eventId ?? item.id)
|
|
10130
|
+
)?.content;
|
|
10131
|
+
return content === void 0 ? item : { ...item, content };
|
|
10132
|
+
});
|
|
9143
10133
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
9144
10134
|
return toolSuccess(
|
|
9145
10135
|
summarizeCount(items.length, "item"),
|
|
@@ -9151,7 +10141,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9151
10141
|
...chart === void 0 ? {} : { chart }
|
|
9152
10142
|
},
|
|
9153
10143
|
{
|
|
9154
|
-
entities: items,
|
|
10144
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
9155
10145
|
warnings: items.length === 0 ? [
|
|
9156
10146
|
toolWarning(
|
|
9157
10147
|
"empty_result",
|
|
@@ -9197,15 +10187,21 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9197
10187
|
includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
|
|
9198
10188
|
since: "Optional lower timestamp bound for time-based views.",
|
|
9199
10189
|
until: "Optional upper timestamp bound for time-based views.",
|
|
9200
|
-
limit: "Optional result limit.",
|
|
10190
|
+
limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
|
|
9201
10191
|
offset: "Optional result offset."
|
|
9202
10192
|
},
|
|
9203
10193
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
9204
10194
|
inputSchema: objectSchema(
|
|
9205
10195
|
{
|
|
10196
|
+
ref: externalRefSchema,
|
|
10197
|
+
includeContent: booleanSchema(
|
|
10198
|
+
"Include verbatim content in event rows; defaults to false."
|
|
10199
|
+
),
|
|
10200
|
+
channel: stringSchema("Filter events by channel."),
|
|
9206
10201
|
view: enumSchema("Which review surface to return.", [
|
|
9207
10202
|
"contacts.list",
|
|
9208
10203
|
"contacts.duplicates",
|
|
10204
|
+
"events.thread",
|
|
9209
10205
|
"events.contact_journal",
|
|
9210
10206
|
"events.link_timeline",
|
|
9211
10207
|
"events.moments",
|
|
@@ -9260,7 +10256,20 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9260
10256
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
9261
10257
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
9262
10258
|
eventTypes: arraySchema(
|
|
9263
|
-
|
|
10259
|
+
enumSchema("Event type.", [
|
|
10260
|
+
"conversation",
|
|
10261
|
+
"activity",
|
|
10262
|
+
"gift",
|
|
10263
|
+
"support",
|
|
10264
|
+
"milestone",
|
|
10265
|
+
"observation",
|
|
10266
|
+
"conflict",
|
|
10267
|
+
"correction",
|
|
10268
|
+
"transaction",
|
|
10269
|
+
"promise",
|
|
10270
|
+
"agreement",
|
|
10271
|
+
"date_anchor"
|
|
10272
|
+
]),
|
|
9264
10273
|
"Optional event-type filter."
|
|
9265
10274
|
),
|
|
9266
10275
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -9283,13 +10292,17 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9283
10292
|
]),
|
|
9284
10293
|
horizonDays: integerSchema("Optional horizon-days filter."),
|
|
9285
10294
|
exactOnly: booleanSchema("Whether duplicate review is exact-only."),
|
|
9286
|
-
minScore: numberSchema(
|
|
10295
|
+
minScore: numberSchema(
|
|
10296
|
+
"Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
|
|
10297
|
+
),
|
|
9287
10298
|
includeDismissed: booleanSchema(
|
|
9288
10299
|
"Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
|
|
9289
10300
|
),
|
|
9290
10301
|
since: integerSchema("Optional lower timestamp bound."),
|
|
9291
10302
|
until: integerSchema("Optional upper timestamp bound."),
|
|
9292
|
-
limit: integerSchema(
|
|
10303
|
+
limit: integerSchema(
|
|
10304
|
+
"Optional result limit, 1..10000 (default 50); use offset for paging."
|
|
10305
|
+
),
|
|
9293
10306
|
offset: integerSchema("Optional result offset.")
|
|
9294
10307
|
},
|
|
9295
10308
|
["view"],
|
|
@@ -9300,6 +10313,9 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9300
10313
|
|
|
9301
10314
|
// src/tools/search_affinity_tool.ts
|
|
9302
10315
|
function searchAffinityToolHandler(db, rawInput) {
|
|
10316
|
+
return searchPage(db, rawInput, false);
|
|
10317
|
+
}
|
|
10318
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
9303
10319
|
const input = structuredClone(rawInput);
|
|
9304
10320
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9305
10321
|
if (inputError)
|
|
@@ -9310,11 +10326,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9310
10326
|
inputError
|
|
9311
10327
|
);
|
|
9312
10328
|
return withToolHandling(() => {
|
|
10329
|
+
if (!pageOnly) {
|
|
10330
|
+
const limit = input.limit ?? 50;
|
|
10331
|
+
const offset = input.offset ?? 0;
|
|
10332
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
10333
|
+
return toolFailure(
|
|
10334
|
+
"protocol",
|
|
10335
|
+
"invalid_input",
|
|
10336
|
+
"Invalid pagination.",
|
|
10337
|
+
"limit must be 1..250; offset must be non-negative"
|
|
10338
|
+
);
|
|
10339
|
+
const all = [];
|
|
10340
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
10341
|
+
for (const scope of scopes) {
|
|
10342
|
+
let page;
|
|
10343
|
+
let nextOffset = 0;
|
|
10344
|
+
do {
|
|
10345
|
+
page = searchPage(
|
|
10346
|
+
db,
|
|
10347
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
10348
|
+
true
|
|
10349
|
+
);
|
|
10350
|
+
if (!page.ok) return page;
|
|
10351
|
+
all.push(...page.data.items);
|
|
10352
|
+
nextOffset += page.data.count;
|
|
10353
|
+
} while (page.data.count === 250);
|
|
10354
|
+
}
|
|
10355
|
+
const items2 = all.slice(offset, offset + limit);
|
|
10356
|
+
return toolSuccess(
|
|
10357
|
+
summarizeCount(
|
|
10358
|
+
items2.length,
|
|
10359
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10360
|
+
),
|
|
10361
|
+
{
|
|
10362
|
+
query: input.query,
|
|
10363
|
+
items: items2,
|
|
10364
|
+
count: items2.length,
|
|
10365
|
+
total: all.length,
|
|
10366
|
+
hasMore: offset + items2.length < all.length
|
|
10367
|
+
},
|
|
10368
|
+
{ entities: items2 }
|
|
10369
|
+
);
|
|
10370
|
+
}
|
|
9313
10371
|
const options = {
|
|
9314
10372
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
9315
10373
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
9316
10374
|
};
|
|
9317
|
-
const rows = searchContacts(
|
|
10375
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
9318
10376
|
db,
|
|
9319
10377
|
input.query,
|
|
9320
10378
|
{
|
|
@@ -9328,9 +10386,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9328
10386
|
},
|
|
9329
10387
|
options
|
|
9330
10388
|
);
|
|
9331
|
-
const items =
|
|
10389
|
+
const items = [
|
|
10390
|
+
...rows.map(toContactListToolItem),
|
|
10391
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
10392
|
+
];
|
|
9332
10393
|
return toolSuccess(
|
|
9333
|
-
summarizeCount(
|
|
10394
|
+
summarizeCount(
|
|
10395
|
+
items.length,
|
|
10396
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10397
|
+
),
|
|
9334
10398
|
{
|
|
9335
10399
|
query: input.query,
|
|
9336
10400
|
items,
|
|
@@ -9366,6 +10430,11 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
9366
10430
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
9367
10431
|
inputSchema: objectSchema(
|
|
9368
10432
|
{
|
|
10433
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
10434
|
+
"contacts",
|
|
10435
|
+
"events",
|
|
10436
|
+
"all"
|
|
10437
|
+
]),
|
|
9369
10438
|
query: stringSchema("Free-text or natural-key query."),
|
|
9370
10439
|
kind: enumSchema("Optional contact kind filter.", [
|
|
9371
10440
|
"human",
|
|
@@ -9404,6 +10473,268 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
9404
10473
|
handler: searchAffinityToolHandler
|
|
9405
10474
|
});
|
|
9406
10475
|
|
|
10476
|
+
// src/events/event_replay.ts
|
|
10477
|
+
function replayEventLinks(db, linkIds, now) {
|
|
10478
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
10479
|
+
for (const linkId of linkIds) {
|
|
10480
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
10481
|
+
if (!saved) continue;
|
|
10482
|
+
const baseline = JSON.parse(saved.state_json);
|
|
10483
|
+
const effects = db.prepare(
|
|
10484
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
10485
|
+
).all(linkId);
|
|
10486
|
+
const overrides = db.prepare(
|
|
10487
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
10488
|
+
).all(linkId);
|
|
10489
|
+
let overrideIndex = 0;
|
|
10490
|
+
const applyOverrides = (beforeId) => {
|
|
10491
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
10492
|
+
const override = overrides[overrideIndex];
|
|
10493
|
+
if (override)
|
|
10494
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
10495
|
+
override.state,
|
|
10496
|
+
linkId
|
|
10497
|
+
);
|
|
10498
|
+
overrideIndex++;
|
|
10499
|
+
}
|
|
10500
|
+
};
|
|
10501
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
10502
|
+
db.prepare(
|
|
10503
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
10504
|
+
).run(
|
|
10505
|
+
baseline.rank,
|
|
10506
|
+
baseline.affinity,
|
|
10507
|
+
baseline.trust,
|
|
10508
|
+
baseline.state,
|
|
10509
|
+
baseline.cadence_days,
|
|
10510
|
+
now,
|
|
10511
|
+
linkId
|
|
10512
|
+
);
|
|
10513
|
+
for (const effect of effects) {
|
|
10514
|
+
eventIds.add(effect.event_id);
|
|
10515
|
+
applyOverrides(effect.event_id);
|
|
10516
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
10517
|
+
continue;
|
|
10518
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
10519
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
10520
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
10521
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
10522
|
+
applyEventEffectToLink(db, {
|
|
10523
|
+
linkId,
|
|
10524
|
+
eventId: event.id,
|
|
10525
|
+
eventType: event.type,
|
|
10526
|
+
occurredAt: event.occurredAt,
|
|
10527
|
+
significance: event.significance,
|
|
10528
|
+
participant,
|
|
10529
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
10530
|
+
now: effect.created_at
|
|
10531
|
+
});
|
|
10532
|
+
}
|
|
10533
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
10534
|
+
refreshLinkRollup(db, linkId, now);
|
|
10535
|
+
}
|
|
10536
|
+
for (const eventId of eventIds)
|
|
10537
|
+
db.prepare(
|
|
10538
|
+
"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=?"
|
|
10539
|
+
).run(eventId);
|
|
10540
|
+
refreshAllBridgeScores(db, now);
|
|
10541
|
+
}
|
|
10542
|
+
|
|
10543
|
+
// src/events/manage_event.ts
|
|
10544
|
+
function requireEventTarget(db, target) {
|
|
10545
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
10546
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
10547
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
10548
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
10549
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
10550
|
+
return event;
|
|
10551
|
+
}
|
|
10552
|
+
function affected(db, id) {
|
|
10553
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
10554
|
+
}
|
|
10555
|
+
function reviseEvent(db, target, patch) {
|
|
10556
|
+
return withTransaction(db, () => {
|
|
10557
|
+
const allowed = [
|
|
10558
|
+
"summary",
|
|
10559
|
+
"content",
|
|
10560
|
+
"occurredAt",
|
|
10561
|
+
"significance",
|
|
10562
|
+
"channel",
|
|
10563
|
+
"type",
|
|
10564
|
+
"externalRef"
|
|
10565
|
+
];
|
|
10566
|
+
for (const key of Object.keys(patch))
|
|
10567
|
+
if (!allowed.includes(key))
|
|
10568
|
+
throw new AffinityValidationError(
|
|
10569
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
10570
|
+
);
|
|
10571
|
+
const event = requireEventTarget(db, target);
|
|
10572
|
+
const next = { ...event, ...patch };
|
|
10573
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
10574
|
+
validateEventMetadata(metadata);
|
|
10575
|
+
Object.assign(next, metadata);
|
|
10576
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
10577
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
10578
|
+
if (patch.occurredAt !== void 0)
|
|
10579
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
10580
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
10581
|
+
throw new AffinityValidationError(
|
|
10582
|
+
"significance must be an integer from 1 to 10"
|
|
10583
|
+
);
|
|
10584
|
+
const interactions = [
|
|
10585
|
+
"conversation",
|
|
10586
|
+
"activity",
|
|
10587
|
+
"gift",
|
|
10588
|
+
"support",
|
|
10589
|
+
"conflict",
|
|
10590
|
+
"correction"
|
|
10591
|
+
];
|
|
10592
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
10593
|
+
throw new AffinityValidationError(
|
|
10594
|
+
"type must stay within the same event kind"
|
|
10595
|
+
);
|
|
10596
|
+
if (patch.externalRef && event.externalRef)
|
|
10597
|
+
throw new AffinityConflictError(
|
|
10598
|
+
"externalRef can only be set when absent"
|
|
10599
|
+
);
|
|
10600
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
10601
|
+
throw new AffinityConflictError(
|
|
10602
|
+
"externalRef already belongs to an event"
|
|
10603
|
+
);
|
|
10604
|
+
const now = Date.now();
|
|
10605
|
+
const links = affected(db, event.id);
|
|
10606
|
+
db.prepare(
|
|
10607
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
10608
|
+
).run(
|
|
10609
|
+
next.summary.trim(),
|
|
10610
|
+
next.content ?? null,
|
|
10611
|
+
next.occurredAt,
|
|
10612
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
10613
|
+
next.significance,
|
|
10614
|
+
next.channel ?? null,
|
|
10615
|
+
next.type,
|
|
10616
|
+
next.externalRef?.source ?? null,
|
|
10617
|
+
next.externalRef?.id ?? null,
|
|
10618
|
+
now,
|
|
10619
|
+
event.id
|
|
10620
|
+
);
|
|
10621
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
10622
|
+
db.prepare(
|
|
10623
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
10624
|
+
).run(patch.type, event.id);
|
|
10625
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
10626
|
+
replayEventLinks(db, links, now);
|
|
10627
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
10628
|
+
updated: [{ kind: "event", id: event.id }],
|
|
10629
|
+
affectedLinks: links
|
|
10630
|
+
});
|
|
10631
|
+
});
|
|
10632
|
+
}
|
|
10633
|
+
function deleteEvent(db, target, options) {
|
|
10634
|
+
return withTransaction(db, () => {
|
|
10635
|
+
if (options?.confirm !== true)
|
|
10636
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
10637
|
+
const event = requireEventTarget(db, target);
|
|
10638
|
+
const links = affected(db, event.id);
|
|
10639
|
+
for (const table of [
|
|
10640
|
+
"event_participants",
|
|
10641
|
+
"attributes",
|
|
10642
|
+
"open_commitments",
|
|
10643
|
+
"upcoming_occurrences",
|
|
10644
|
+
"link_event_effects"
|
|
10645
|
+
])
|
|
10646
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
10647
|
+
db.prepare(
|
|
10648
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
10649
|
+
).run(event.id);
|
|
10650
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
10651
|
+
replayEventLinks(db, links, Date.now());
|
|
10652
|
+
return buildEventMutationReceipt(event, {
|
|
10653
|
+
removed: [{ kind: "event", id: event.id }],
|
|
10654
|
+
affectedLinks: links
|
|
10655
|
+
});
|
|
10656
|
+
});
|
|
10657
|
+
}
|
|
10658
|
+
|
|
10659
|
+
// src/tools/manage_event_tool.ts
|
|
10660
|
+
function manageEventToolHandler(db, raw) {
|
|
10661
|
+
const input = structuredClone(raw);
|
|
10662
|
+
const error = validateToolInput(
|
|
10663
|
+
manageEventTool.inputSchema,
|
|
10664
|
+
input,
|
|
10665
|
+
"input",
|
|
10666
|
+
Date.now(),
|
|
10667
|
+
true
|
|
10668
|
+
);
|
|
10669
|
+
if (error)
|
|
10670
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
10671
|
+
return withToolHandling(() => {
|
|
10672
|
+
const target = {
|
|
10673
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
10674
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
10675
|
+
};
|
|
10676
|
+
return mutationToolResult(
|
|
10677
|
+
input.action,
|
|
10678
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
10679
|
+
"event"
|
|
10680
|
+
);
|
|
10681
|
+
}, "Event management failed.");
|
|
10682
|
+
}
|
|
10683
|
+
var manageEventTool = defineAffinityTool({
|
|
10684
|
+
name: manageEventToolName,
|
|
10685
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
10686
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
10687
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
10688
|
+
sideEffects: "writes_state",
|
|
10689
|
+
readOnly: false,
|
|
10690
|
+
supportsClarification: false,
|
|
10691
|
+
targetKinds: ["event"],
|
|
10692
|
+
inputDescriptions: {
|
|
10693
|
+
action: "revise or delete",
|
|
10694
|
+
eventId: "Exact event id",
|
|
10695
|
+
ref: "External event reference",
|
|
10696
|
+
patch: "Fields to revise",
|
|
10697
|
+
confirm: "Required true for deletion"
|
|
10698
|
+
},
|
|
10699
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
10700
|
+
inputSchema: objectSchema(
|
|
10701
|
+
{
|
|
10702
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
10703
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
10704
|
+
ref: externalRefSchema,
|
|
10705
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
10706
|
+
patch: objectSchema(
|
|
10707
|
+
{
|
|
10708
|
+
summary: stringSchema("Short non-empty label."),
|
|
10709
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
10710
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
10711
|
+
significance: integerSchema("Significance 1..10."),
|
|
10712
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
10713
|
+
type: enumSchema("Type within the same event kind.", [
|
|
10714
|
+
"conversation",
|
|
10715
|
+
"activity",
|
|
10716
|
+
"gift",
|
|
10717
|
+
"support",
|
|
10718
|
+
"conflict",
|
|
10719
|
+
"correction",
|
|
10720
|
+
"observation",
|
|
10721
|
+
"milestone",
|
|
10722
|
+
"transaction",
|
|
10723
|
+
"promise",
|
|
10724
|
+
"agreement",
|
|
10725
|
+
"date_anchor"
|
|
10726
|
+
]),
|
|
10727
|
+
externalRef: externalRefSchema
|
|
10728
|
+
},
|
|
10729
|
+
[],
|
|
10730
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
10731
|
+
)
|
|
10732
|
+
},
|
|
10733
|
+
["action"]
|
|
10734
|
+
),
|
|
10735
|
+
handler: manageEventToolHandler
|
|
10736
|
+
});
|
|
10737
|
+
|
|
9407
10738
|
// src/tools/tool_registry.ts
|
|
9408
10739
|
var affinityTools = [
|
|
9409
10740
|
searchAffinityTool,
|
|
@@ -9417,7 +10748,8 @@ var affinityTools = [
|
|
|
9417
10748
|
recordEventTool,
|
|
9418
10749
|
manageCommitmentTool,
|
|
9419
10750
|
manageDateAnchorTool,
|
|
9420
|
-
manageAttributeTool
|
|
10751
|
+
manageAttributeTool,
|
|
10752
|
+
manageEventTool
|
|
9421
10753
|
];
|
|
9422
10754
|
function listAffinityToolDefinitions() {
|
|
9423
10755
|
return [...affinityTools];
|
|
@@ -9428,6 +10760,24 @@ function getAffinityToolByName(name) {
|
|
|
9428
10760
|
|
|
9429
10761
|
// src/tools/tool_mapping.ts
|
|
9430
10762
|
var affinityToolMappings = [
|
|
10763
|
+
{
|
|
10764
|
+
source: "getEventById",
|
|
10765
|
+
tool: inspectAffinityItemToolName,
|
|
10766
|
+
action: "event"
|
|
10767
|
+
},
|
|
10768
|
+
{
|
|
10769
|
+
source: "getEventByExternalRef",
|
|
10770
|
+
tool: inspectAffinityItemToolName,
|
|
10771
|
+
action: "event"
|
|
10772
|
+
},
|
|
10773
|
+
{
|
|
10774
|
+
source: "getEventThread",
|
|
10775
|
+
tool: reviewAffinityToolName,
|
|
10776
|
+
view: "events.thread"
|
|
10777
|
+
},
|
|
10778
|
+
{ source: "searchEvents", tool: searchAffinityToolName },
|
|
10779
|
+
{ source: "reviseEvent", tool: "manage_event", action: "revise" },
|
|
10780
|
+
{ source: "deleteEvent", tool: "manage_event", action: "delete" },
|
|
9431
10781
|
{ source: "exportContact", tool: manageContactToolName, action: "export" },
|
|
9432
10782
|
{ source: "exportAffinity", tool: manageContactToolName, action: "export" },
|
|
9433
10783
|
{ source: "deleteContact", tool: manageContactToolName, action: "delete" },
|
|
@@ -9618,6 +10968,7 @@ __export(write_exports, {
|
|
|
9618
10968
|
addIdentity: () => addIdentity,
|
|
9619
10969
|
createContact: () => createContact,
|
|
9620
10970
|
deleteContact: () => deleteContact,
|
|
10971
|
+
deleteEvent: () => deleteEvent,
|
|
9621
10972
|
dismissDuplicate: () => dismissDuplicate,
|
|
9622
10973
|
mergeContacts: () => mergeContacts,
|
|
9623
10974
|
overrideLinkState: () => overrideLinkState,
|
|
@@ -9634,6 +10985,7 @@ __export(write_exports, {
|
|
|
9634
10985
|
reviseBond: () => reviseBond,
|
|
9635
10986
|
reviseContact: () => reviseContact,
|
|
9636
10987
|
reviseDateAnchor: () => reviseDateAnchor,
|
|
10988
|
+
reviseEvent: () => reviseEvent,
|
|
9637
10989
|
reviseIdentity: () => reviseIdentity,
|
|
9638
10990
|
seedSocialLink: () => seedSocialLink,
|
|
9639
10991
|
setAttribute: () => setAttribute,
|