@ghostpaw/affinity 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -114
- package/SKILL.md +217 -33
- package/dist/cli.js +2433 -913
- package/dist/index.cjs +1397 -504
- package/dist/index.d.cts +556 -55
- package/dist/index.d.ts +556 -55
- package/dist/index.js +1397 -504
- package/dist/mcp-server.js +1811 -694
- package/dist/mcp.cjs +1604 -606
- package/dist/mcp.js +1605 -607
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +217 -33
package/dist/index.cjs
CHANGED
|
@@ -265,6 +265,12 @@ function initLinkEventEffectsTables(db) {
|
|
|
265
265
|
)
|
|
266
266
|
)
|
|
267
267
|
`);
|
|
268
|
+
const columns = db.prepare("PRAGMA table_info(link_event_effects)").all();
|
|
269
|
+
if (!columns.some((column) => column.name === "counts_for_recency")) {
|
|
270
|
+
db.exec(
|
|
271
|
+
"ALTER TABLE link_event_effects ADD COLUMN counts_for_recency INTEGER NOT NULL DEFAULT 1"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
268
274
|
db.exec(
|
|
269
275
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
270
276
|
);
|
|
@@ -515,6 +521,8 @@ function resolveAffinityListLimit(limit) {
|
|
|
515
521
|
// src/read.ts
|
|
516
522
|
var read_exports = {};
|
|
517
523
|
__export(read_exports, {
|
|
524
|
+
exportAffinity: () => exportAffinity,
|
|
525
|
+
exportContact: () => exportContact,
|
|
518
526
|
getAffinityChart: () => getAffinityChart,
|
|
519
527
|
getContactJournal: () => getContactJournal,
|
|
520
528
|
getContactProfile: () => getContactProfile,
|
|
@@ -822,6 +830,25 @@ function createContact(db, input) {
|
|
|
822
830
|
});
|
|
823
831
|
}
|
|
824
832
|
|
|
833
|
+
// src/lib/read/assert_default_ordering.ts
|
|
834
|
+
function assertDefaultOrdering(_readName, options) {
|
|
835
|
+
if (options?.sort !== void 0 || options?.descending !== void 0) {
|
|
836
|
+
throw new AffinityValidationError(
|
|
837
|
+
"This review does not support custom sort ordering"
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/lib/read/resolve_list_limit_offset.ts
|
|
843
|
+
function resolveListLimitOffset(options) {
|
|
844
|
+
const limit = resolveAffinityListLimit(options?.limit);
|
|
845
|
+
const offset = options?.offset ?? 0;
|
|
846
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
847
|
+
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
848
|
+
}
|
|
849
|
+
return { limit, offset };
|
|
850
|
+
}
|
|
851
|
+
|
|
825
852
|
// src/events/validators.ts
|
|
826
853
|
var PARTICIPANT_ROLES = /* @__PURE__ */ new Set([
|
|
827
854
|
"actor",
|
|
@@ -855,7 +882,10 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
855
882
|
throw new AffinityNotFoundError("contact not found");
|
|
856
883
|
}
|
|
857
884
|
if (row.lifecycle_state === "merged") {
|
|
858
|
-
|
|
885
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
886
|
+
throw new AffinityStateError(
|
|
887
|
+
`Contact #${id} was merged into #${row.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row.merged_into_contact_id}`
|
|
888
|
+
);
|
|
859
889
|
}
|
|
860
890
|
}
|
|
861
891
|
}
|
|
@@ -1027,6 +1057,213 @@ function findDuplicateDateAnchor(db, params) {
|
|
|
1027
1057
|
return null;
|
|
1028
1058
|
}
|
|
1029
1059
|
|
|
1060
|
+
// src/events/loaders.ts
|
|
1061
|
+
function loadEventParticipantViews(db, eventId) {
|
|
1062
|
+
const rows = db.prepare(
|
|
1063
|
+
`SELECT contact_id, role, directionality
|
|
1064
|
+
FROM event_participants
|
|
1065
|
+
WHERE event_id = ?`
|
|
1066
|
+
).all(eventId);
|
|
1067
|
+
return rows.map((row) => ({
|
|
1068
|
+
contactId: row.contact_id,
|
|
1069
|
+
role: row.role,
|
|
1070
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1071
|
+
}));
|
|
1072
|
+
}
|
|
1073
|
+
function loadEventRecord(db, eventId) {
|
|
1074
|
+
const row = getEventRowById(db, eventId);
|
|
1075
|
+
if (!row) {
|
|
1076
|
+
throw new AffinityInvariantError("event not found");
|
|
1077
|
+
}
|
|
1078
|
+
const participants = loadEventParticipantViews(db, eventId);
|
|
1079
|
+
return mapEventRowToEventRecord(row, participants);
|
|
1080
|
+
}
|
|
1081
|
+
function batchLoadEventRecords(db, eventIds) {
|
|
1082
|
+
if (eventIds.length === 0) {
|
|
1083
|
+
return [];
|
|
1084
|
+
}
|
|
1085
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
1086
|
+
const eventRows = db.prepare(
|
|
1087
|
+
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
1088
|
+
recurrence_kind, anchor_month, anchor_day,
|
|
1089
|
+
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`
|
|
1092
|
+
).all(...eventIds);
|
|
1093
|
+
const partRows = db.prepare(
|
|
1094
|
+
`SELECT event_id, contact_id, role, directionality
|
|
1095
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
1096
|
+
).all(...eventIds);
|
|
1097
|
+
const partsByEvent = /* @__PURE__ */ new Map();
|
|
1098
|
+
for (const p of partRows) {
|
|
1099
|
+
let arr = partsByEvent.get(p.event_id);
|
|
1100
|
+
if (!arr) {
|
|
1101
|
+
arr = [];
|
|
1102
|
+
partsByEvent.set(p.event_id, arr);
|
|
1103
|
+
}
|
|
1104
|
+
arr.push({
|
|
1105
|
+
contactId: p.contact_id,
|
|
1106
|
+
role: p.role,
|
|
1107
|
+
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
1111
|
+
const out = [];
|
|
1112
|
+
for (const id of eventIds) {
|
|
1113
|
+
const row = rowById.get(id);
|
|
1114
|
+
if (row) {
|
|
1115
|
+
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return out;
|
|
1119
|
+
}
|
|
1120
|
+
function batchLoadEventParticipantViews(db, eventIds) {
|
|
1121
|
+
const result = /* @__PURE__ */ new Map();
|
|
1122
|
+
if (eventIds.length === 0) {
|
|
1123
|
+
return result;
|
|
1124
|
+
}
|
|
1125
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
1126
|
+
const rows = db.prepare(
|
|
1127
|
+
`SELECT event_id, contact_id, role, directionality
|
|
1128
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
1129
|
+
).all(...eventIds);
|
|
1130
|
+
for (const row of rows) {
|
|
1131
|
+
let arr = result.get(row.event_id);
|
|
1132
|
+
if (!arr) {
|
|
1133
|
+
arr = [];
|
|
1134
|
+
result.set(row.event_id, arr);
|
|
1135
|
+
}
|
|
1136
|
+
arr.push({
|
|
1137
|
+
contactId: row.contact_id,
|
|
1138
|
+
role: row.role,
|
|
1139
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1144
|
+
function requireDateAnchorEvent(db, eventId) {
|
|
1145
|
+
const row = getEventRowById(db, eventId);
|
|
1146
|
+
if (!row) {
|
|
1147
|
+
throw new AffinityNotFoundError("event not found");
|
|
1148
|
+
}
|
|
1149
|
+
if (row.type !== "date_anchor") {
|
|
1150
|
+
throw new AffinityInvariantError("not a date anchor");
|
|
1151
|
+
}
|
|
1152
|
+
return row;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/events/get_contact_journal.ts
|
|
1156
|
+
function getContactJournal(db, contactId, options) {
|
|
1157
|
+
assertDefaultOrdering("getContactJournal", options);
|
|
1158
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
1159
|
+
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
1160
|
+
const params = [contactId];
|
|
1161
|
+
if (options?.since !== void 0) {
|
|
1162
|
+
clauses.push("e.occurred_at >= ?");
|
|
1163
|
+
params.push(options.since);
|
|
1164
|
+
}
|
|
1165
|
+
if (options?.until !== void 0) {
|
|
1166
|
+
clauses.push("e.occurred_at <= ?");
|
|
1167
|
+
params.push(options.until);
|
|
1168
|
+
}
|
|
1169
|
+
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
1170
|
+
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
1171
|
+
clauses.push(`e.type IN (${ph})`);
|
|
1172
|
+
params.push(...options.eventTypes);
|
|
1173
|
+
}
|
|
1174
|
+
const where = clauses.join(" AND ");
|
|
1175
|
+
const rows = db.prepare(
|
|
1176
|
+
`SELECT e.id FROM events e
|
|
1177
|
+
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
1178
|
+
WHERE ${where}
|
|
1179
|
+
ORDER BY e.occurred_at DESC, e.id DESC
|
|
1180
|
+
LIMIT ? OFFSET ?`
|
|
1181
|
+
).all(...params, limit, offset);
|
|
1182
|
+
return batchLoadEventRecords(
|
|
1183
|
+
db,
|
|
1184
|
+
rows.map((r) => r.id)
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/events/list_open_commitments.ts
|
|
1189
|
+
var DAY_MS = 864e5;
|
|
1190
|
+
function mapResolution(resolvedAt, resolution) {
|
|
1191
|
+
if (resolvedAt === null) {
|
|
1192
|
+
return "open";
|
|
1193
|
+
}
|
|
1194
|
+
if (resolution === "cancelled") {
|
|
1195
|
+
return "cancelled";
|
|
1196
|
+
}
|
|
1197
|
+
return "resolved";
|
|
1198
|
+
}
|
|
1199
|
+
function listOpenCommitments(db, filters, options) {
|
|
1200
|
+
assertDefaultOrdering("listOpenCommitments", options);
|
|
1201
|
+
const now = resolveNow(options?.now);
|
|
1202
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
1203
|
+
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
1204
|
+
const params = [];
|
|
1205
|
+
if (options?.since !== void 0) {
|
|
1206
|
+
clauses.push("e.occurred_at >= ?");
|
|
1207
|
+
params.push(options.since);
|
|
1208
|
+
}
|
|
1209
|
+
if (options?.until !== void 0) {
|
|
1210
|
+
clauses.push("e.occurred_at <= ?");
|
|
1211
|
+
params.push(options.until);
|
|
1212
|
+
}
|
|
1213
|
+
if (filters?.commitmentType !== void 0) {
|
|
1214
|
+
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
1215
|
+
throw new AffinityValidationError("invalid commitmentType filter");
|
|
1216
|
+
}
|
|
1217
|
+
clauses.push("oc.commitment_type = ?");
|
|
1218
|
+
params.push(filters.commitmentType);
|
|
1219
|
+
}
|
|
1220
|
+
if (filters?.contactId !== void 0) {
|
|
1221
|
+
clauses.push(
|
|
1222
|
+
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
1223
|
+
);
|
|
1224
|
+
params.push(filters.contactId);
|
|
1225
|
+
}
|
|
1226
|
+
if (filters?.linkId !== void 0) {
|
|
1227
|
+
clauses.push(
|
|
1228
|
+
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
1229
|
+
);
|
|
1230
|
+
params.push(filters.linkId);
|
|
1231
|
+
}
|
|
1232
|
+
if (filters?.horizonDays !== void 0) {
|
|
1233
|
+
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
1234
|
+
throw new AffinityValidationError(
|
|
1235
|
+
"horizonDays must be a non-negative integer"
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
const anchor = options?.since ?? now;
|
|
1239
|
+
clauses.push("oc.due_at IS NOT NULL");
|
|
1240
|
+
clauses.push("oc.due_at <= ?");
|
|
1241
|
+
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
1242
|
+
}
|
|
1243
|
+
const where = clauses.join(" AND ");
|
|
1244
|
+
const rows = db.prepare(
|
|
1245
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
1246
|
+
e.type, e.summary
|
|
1247
|
+
FROM open_commitments oc
|
|
1248
|
+
INNER JOIN events e ON e.id = oc.event_id
|
|
1249
|
+
WHERE ${where}
|
|
1250
|
+
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
1251
|
+
LIMIT ? OFFSET ?`
|
|
1252
|
+
).all(...params, limit, offset);
|
|
1253
|
+
const eventIds = rows.map((r) => r.event_id);
|
|
1254
|
+
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
1255
|
+
return rows.map((r) => ({
|
|
1256
|
+
eventId: r.event_id,
|
|
1257
|
+
type: r.type,
|
|
1258
|
+
summary: r.summary,
|
|
1259
|
+
participants: partsByEvent.get(r.event_id) ?? [],
|
|
1260
|
+
dueAt: r.due_at,
|
|
1261
|
+
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
1262
|
+
overdue: r.due_at !== null && r.due_at < now,
|
|
1263
|
+
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
1264
|
+
}));
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1030
1267
|
// src/dates/calendar.ts
|
|
1031
1268
|
function utcStartOfDayMs(ms) {
|
|
1032
1269
|
const date = new Date(ms);
|
|
@@ -1193,13 +1430,20 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1193
1430
|
const topLinks = linkRows.map((l) => mapLinkRowToLinkListItem(l));
|
|
1194
1431
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
1195
1432
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1433
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
1196
1434
|
return {
|
|
1435
|
+
...winner ? { mergedInto: { id: winner.id, name: winner.name } } : {},
|
|
1197
1436
|
contact: mapContactRowToContactCore(row),
|
|
1198
1437
|
identities,
|
|
1199
1438
|
attributes,
|
|
1200
1439
|
topLinks,
|
|
1201
1440
|
activeDates,
|
|
1202
|
-
warnings:
|
|
1441
|
+
warnings: [
|
|
1442
|
+
...contactRollup?.warnings ?? [],
|
|
1443
|
+
...winner ? [
|
|
1444
|
+
`Contact #${row.id} was merged into #${winner.id} ${winner.name}; use contactId ${winner.id}`
|
|
1445
|
+
] : []
|
|
1446
|
+
],
|
|
1203
1447
|
rollups: contactRollup?.rollup ?? null
|
|
1204
1448
|
};
|
|
1205
1449
|
}
|
|
@@ -1207,7 +1451,27 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1207
1451
|
// src/contacts/get_contact_profile.ts
|
|
1208
1452
|
function getContactProfile(db, contactId, options) {
|
|
1209
1453
|
const t = options?.topLinksLimit ?? 8;
|
|
1210
|
-
|
|
1454
|
+
const profile = loadContactProfileRecord(db, contactId, t);
|
|
1455
|
+
if (!profile) return null;
|
|
1456
|
+
const ties = db.prepare(`SELECT l.id, l.kind, l.role, c.id AS contactId, c.name
|
|
1457
|
+
FROM links l JOIN contacts c ON c.id = CASE WHEN l.from_contact_id = ? THEN l.to_contact_id ELSE l.from_contact_id END
|
|
1458
|
+
WHERE l.is_structural = 1 AND l.removed_at IS NULL AND (l.from_contact_id = ? OR l.to_contact_id = ?)
|
|
1459
|
+
AND c.deleted_at IS NULL ORDER BY l.id`).all(contactId, contactId, contactId);
|
|
1460
|
+
return {
|
|
1461
|
+
...profile,
|
|
1462
|
+
recentEvents: getContactJournal(db, contactId, {
|
|
1463
|
+
limit: options?.recentEventsLimit ?? 5
|
|
1464
|
+
}).map((event) => ({
|
|
1465
|
+
...event,
|
|
1466
|
+
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
1467
|
+
})),
|
|
1468
|
+
openCommitments: listOpenCommitments(
|
|
1469
|
+
db,
|
|
1470
|
+
{ contactId },
|
|
1471
|
+
options?.now === void 0 ? void 0 : { now: options.now }
|
|
1472
|
+
),
|
|
1473
|
+
ties
|
|
1474
|
+
};
|
|
1211
1475
|
}
|
|
1212
1476
|
|
|
1213
1477
|
// src/contacts/get_owner_profile.ts
|
|
@@ -1220,23 +1484,21 @@ function getOwnerProfile(db, options) {
|
|
|
1220
1484
|
return loadContactProfileRecord(db, ownerId, t);
|
|
1221
1485
|
}
|
|
1222
1486
|
|
|
1223
|
-
// src/
|
|
1224
|
-
function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
1237
|
-
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
1487
|
+
// src/contacts/contact_attribute_clauses.ts
|
|
1488
|
+
function contactAttributeClauses(filters) {
|
|
1489
|
+
const clauses = [];
|
|
1490
|
+
const params = [];
|
|
1491
|
+
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
1492
|
+
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
1493
|
+
for (const filter of [...attributes, ...tags.map((tag) => `tag:${tag}`)]) {
|
|
1494
|
+
const separator = filter.indexOf("=");
|
|
1495
|
+
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
1496
|
+
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
1497
|
+
AND a.deleted_at IS NULL AND lower(a.name) = lower(?)${separator < 0 ? "" : " AND lower(CAST(CASE WHEN json_valid(a.value) THEN json_extract(a.value, '$') ELSE a.value END AS TEXT)) = lower(?)"})`);
|
|
1498
|
+
params.push(name);
|
|
1499
|
+
if (separator >= 0) params.push(filter.slice(separator + 1));
|
|
1238
1500
|
}
|
|
1239
|
-
return {
|
|
1501
|
+
return { clauses, params };
|
|
1240
1502
|
}
|
|
1241
1503
|
|
|
1242
1504
|
// src/contacts/list_contacts.ts
|
|
@@ -1262,6 +1524,9 @@ function listContacts(db, filters, options) {
|
|
|
1262
1524
|
if (filters?.includeDormant === false || options?.includeDormant === false) {
|
|
1263
1525
|
clauses.push("lifecycle_state != 'dormant'");
|
|
1264
1526
|
}
|
|
1527
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
1528
|
+
clauses.push(...attributeFilters.clauses);
|
|
1529
|
+
params.push(...attributeFilters.params);
|
|
1265
1530
|
const where = clauses.join(" AND ");
|
|
1266
1531
|
const rows = db.prepare(
|
|
1267
1532
|
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
@@ -1444,89 +1709,97 @@ function reviseContact(db, contactId, patch) {
|
|
|
1444
1709
|
});
|
|
1445
1710
|
}
|
|
1446
1711
|
|
|
1447
|
-
// src/contacts/
|
|
1448
|
-
function
|
|
1449
|
-
|
|
1450
|
-
if (
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
return 0.7;
|
|
1712
|
+
// src/contacts/fold_search_text.ts
|
|
1713
|
+
function foldSearchText(value) {
|
|
1714
|
+
let folded = value.toLowerCase();
|
|
1715
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
1716
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
1717
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
1454
1718
|
}
|
|
1719
|
+
|
|
1720
|
+
// src/contacts/search_contacts.ts
|
|
1455
1721
|
function searchContacts(db, query, filters, options) {
|
|
1456
|
-
const q = query.trim();
|
|
1457
|
-
if (q
|
|
1458
|
-
return [];
|
|
1459
|
-
}
|
|
1722
|
+
const q = foldSearchText(query.trim());
|
|
1723
|
+
if (!q) return [];
|
|
1460
1724
|
assertDefaultOrdering("searchContacts", options);
|
|
1461
1725
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
1462
|
-
const
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1726
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1727
|
+
const rows = db.prepare(`
|
|
1728
|
+
SELECT contact_id, value || ' ' || normalized_key AS text, 'identity:' || type AS source FROM identities WHERE removed_at IS NULL
|
|
1729
|
+
UNION ALL SELECT contact_id, name || ' ' || COALESCE(value, ''), 'attribute:' || name FROM attributes WHERE deleted_at IS NULL AND contact_id IS NOT NULL
|
|
1730
|
+
UNION ALL SELECT from_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
1731
|
+
UNION ALL SELECT to_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
1732
|
+
UNION ALL SELECT ep.contact_id, e.summary, 'event' FROM event_participants ep JOIN events e ON e.id = ep.event_id WHERE e.deleted_at IS NULL
|
|
1733
|
+
`).all();
|
|
1734
|
+
for (const row of rows) {
|
|
1735
|
+
const values = fields.get(row.contact_id) ?? [];
|
|
1736
|
+
values.push({ text: foldSearchText(row.text), source: row.source });
|
|
1737
|
+
fields.set(row.contact_id, values);
|
|
1738
|
+
}
|
|
1739
|
+
const tokens = q.split(/\s+/);
|
|
1740
|
+
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
1741
|
+
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
1742
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
1465
1743
|
const clauses = [
|
|
1466
|
-
"
|
|
1467
|
-
"
|
|
1744
|
+
"deleted_at IS NULL",
|
|
1745
|
+
"lifecycle_state != 'merged'",
|
|
1746
|
+
...attributeFilters.clauses
|
|
1468
1747
|
];
|
|
1469
|
-
const params = [];
|
|
1748
|
+
const params = [...attributeFilters.params];
|
|
1470
1749
|
if (filters?.kind !== void 0) {
|
|
1471
|
-
clauses.push("
|
|
1750
|
+
clauses.push("kind = ?");
|
|
1472
1751
|
params.push(filters.kind);
|
|
1473
1752
|
}
|
|
1474
1753
|
if (filters?.lifecycleState !== void 0) {
|
|
1475
|
-
clauses.push("
|
|
1754
|
+
clauses.push("lifecycle_state = ?");
|
|
1476
1755
|
params.push(filters.lifecycleState);
|
|
1477
1756
|
}
|
|
1478
|
-
if (filters?.includeOwner === false)
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
Number(
|
|
1517
|
-
r.matched_normalized_identity ?? 0
|
|
1518
|
-
) > 0
|
|
1757
|
+
if (filters?.includeOwner === false) clauses.push("is_owner = 0");
|
|
1758
|
+
const contacts = db.prepare(
|
|
1759
|
+
`SELECT id, name, kind, lifecycle_state, is_owner, (SELECT value FROM identities i WHERE i.contact_id = contacts.id AND i.removed_at IS NULL ORDER BY i.id LIMIT 1) AS primary_identity FROM contacts WHERE ${clauses.join(" AND ")}`
|
|
1760
|
+
).all(...params).map((row) => ({
|
|
1761
|
+
...mapContactRowToContactListItem(row),
|
|
1762
|
+
primaryIdentity: row.primary_identity
|
|
1763
|
+
}));
|
|
1764
|
+
const scored = [];
|
|
1765
|
+
for (const contact of contacts) {
|
|
1766
|
+
if (contact.lifecycleState === "dormant" && !(filters?.includeDormant ?? options?.includeDormant))
|
|
1767
|
+
continue;
|
|
1768
|
+
if (contact.lifecycleState === "lost" && !filters?.includeLost) continue;
|
|
1769
|
+
const name = foldSearchText(contact.name);
|
|
1770
|
+
const values = [
|
|
1771
|
+
{ text: name, source: "name" },
|
|
1772
|
+
...(fields.get(contact.id) ?? []).filter(
|
|
1773
|
+
(field) => !contact.isOwner || field.source !== "event" && field.source !== "link_role"
|
|
1774
|
+
)
|
|
1775
|
+
];
|
|
1776
|
+
const phoneFields = phone ? values.filter(
|
|
1777
|
+
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
1778
|
+
) : [];
|
|
1779
|
+
if (!phoneFields.length && !tokens.every(
|
|
1780
|
+
(token) => values.some((field) => field.text.includes(token))
|
|
1781
|
+
))
|
|
1782
|
+
continue;
|
|
1783
|
+
const matchedOn = [
|
|
1784
|
+
...new Set(
|
|
1785
|
+
[
|
|
1786
|
+
...values.filter(
|
|
1787
|
+
(field) => tokens.some((token) => field.text.includes(token))
|
|
1788
|
+
),
|
|
1789
|
+
...phoneFields
|
|
1790
|
+
].map((field) => field.source)
|
|
1791
|
+
)
|
|
1792
|
+
];
|
|
1793
|
+
const identities = values.filter(
|
|
1794
|
+
(field) => field.source.startsWith("identity:")
|
|
1519
1795
|
);
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
});
|
|
1528
|
-
scored.sort((a, b) => b.score - a.score);
|
|
1529
|
-
return scored.slice(offset, offset + limit).map((s) => s.item);
|
|
1796
|
+
const score = name === q ? 5 : name.startsWith(q) || tokens.every((token) => name.includes(token)) ? 4 : phoneFields.length || identities.some((field) => field.text.split(" ").includes(q)) ? 3 : identities.some(
|
|
1797
|
+
(field) => tokens.every((token) => field.text.includes(token))
|
|
1798
|
+
) ? 2 : 1;
|
|
1799
|
+
scored.push({ item: { ...contact, matchedOn }, score });
|
|
1800
|
+
}
|
|
1801
|
+
scored.sort((a, b) => b.score - a.score || a.item.id - b.item.id);
|
|
1802
|
+
return scored.slice(offset, offset + limit).map(({ item }) => item);
|
|
1530
1803
|
}
|
|
1531
1804
|
|
|
1532
1805
|
// src/contacts/validators.ts
|
|
@@ -1535,7 +1808,9 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
1535
1808
|
throw new AffinityStateError("merged contact is read-only");
|
|
1536
1809
|
}
|
|
1537
1810
|
if (to === "merged") {
|
|
1538
|
-
throw new AffinityInvariantError(
|
|
1811
|
+
throw new AffinityInvariantError(
|
|
1812
|
+
"use manage_contact merge (CLI: contact merge) to reach merged state"
|
|
1813
|
+
);
|
|
1539
1814
|
}
|
|
1540
1815
|
if (from === to) {
|
|
1541
1816
|
return;
|
|
@@ -1586,101 +1861,6 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
1586
1861
|
});
|
|
1587
1862
|
}
|
|
1588
1863
|
|
|
1589
|
-
// src/events/loaders.ts
|
|
1590
|
-
function loadEventParticipantViews(db, eventId) {
|
|
1591
|
-
const rows = db.prepare(
|
|
1592
|
-
`SELECT contact_id, role, directionality
|
|
1593
|
-
FROM event_participants
|
|
1594
|
-
WHERE event_id = ?`
|
|
1595
|
-
).all(eventId);
|
|
1596
|
-
return rows.map((row) => ({
|
|
1597
|
-
contactId: row.contact_id,
|
|
1598
|
-
role: row.role,
|
|
1599
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1600
|
-
}));
|
|
1601
|
-
}
|
|
1602
|
-
function loadEventRecord(db, eventId) {
|
|
1603
|
-
const row = getEventRowById(db, eventId);
|
|
1604
|
-
if (!row) {
|
|
1605
|
-
throw new AffinityInvariantError("event not found");
|
|
1606
|
-
}
|
|
1607
|
-
const participants = loadEventParticipantViews(db, eventId);
|
|
1608
|
-
return mapEventRowToEventRecord(row, participants);
|
|
1609
|
-
}
|
|
1610
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
1611
|
-
if (eventIds.length === 0) {
|
|
1612
|
-
return [];
|
|
1613
|
-
}
|
|
1614
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
1615
|
-
const eventRows = db.prepare(
|
|
1616
|
-
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
1617
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
1618
|
-
anchor_contact_id, anchor_link_id,
|
|
1619
|
-
created_at, updated_at, deleted_at
|
|
1620
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
1621
|
-
).all(...eventIds);
|
|
1622
|
-
const partRows = db.prepare(
|
|
1623
|
-
`SELECT event_id, contact_id, role, directionality
|
|
1624
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
1625
|
-
).all(...eventIds);
|
|
1626
|
-
const partsByEvent = /* @__PURE__ */ new Map();
|
|
1627
|
-
for (const p of partRows) {
|
|
1628
|
-
let arr = partsByEvent.get(p.event_id);
|
|
1629
|
-
if (!arr) {
|
|
1630
|
-
arr = [];
|
|
1631
|
-
partsByEvent.set(p.event_id, arr);
|
|
1632
|
-
}
|
|
1633
|
-
arr.push({
|
|
1634
|
-
contactId: p.contact_id,
|
|
1635
|
-
role: p.role,
|
|
1636
|
-
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
1637
|
-
});
|
|
1638
|
-
}
|
|
1639
|
-
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
1640
|
-
const out = [];
|
|
1641
|
-
for (const id of eventIds) {
|
|
1642
|
-
const row = rowById.get(id);
|
|
1643
|
-
if (row) {
|
|
1644
|
-
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
return out;
|
|
1648
|
-
}
|
|
1649
|
-
function batchLoadEventParticipantViews(db, eventIds) {
|
|
1650
|
-
const result = /* @__PURE__ */ new Map();
|
|
1651
|
-
if (eventIds.length === 0) {
|
|
1652
|
-
return result;
|
|
1653
|
-
}
|
|
1654
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
1655
|
-
const rows = db.prepare(
|
|
1656
|
-
`SELECT event_id, contact_id, role, directionality
|
|
1657
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
1658
|
-
).all(...eventIds);
|
|
1659
|
-
for (const row of rows) {
|
|
1660
|
-
let arr = result.get(row.event_id);
|
|
1661
|
-
if (!arr) {
|
|
1662
|
-
arr = [];
|
|
1663
|
-
result.set(row.event_id, arr);
|
|
1664
|
-
}
|
|
1665
|
-
arr.push({
|
|
1666
|
-
contactId: row.contact_id,
|
|
1667
|
-
role: row.role,
|
|
1668
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
return result;
|
|
1672
|
-
}
|
|
1673
|
-
function requireDateAnchorEvent(db, eventId) {
|
|
1674
|
-
const row = getEventRowById(db, eventId);
|
|
1675
|
-
if (!row) {
|
|
1676
|
-
throw new AffinityNotFoundError("event not found");
|
|
1677
|
-
}
|
|
1678
|
-
if (row.type !== "date_anchor") {
|
|
1679
|
-
throw new AffinityInvariantError("not a date anchor");
|
|
1680
|
-
}
|
|
1681
|
-
return row;
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
1864
|
// src/events/persistence.ts
|
|
1685
1865
|
function insertJournalEvent(db, params) {
|
|
1686
1866
|
const now = resolveNow(params.now);
|
|
@@ -2544,7 +2724,7 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2544
2724
|
}
|
|
2545
2725
|
|
|
2546
2726
|
// src/links/rollups.ts
|
|
2547
|
-
var
|
|
2727
|
+
var DAY_MS2 = 864e5;
|
|
2548
2728
|
function parseJsonObject2(raw) {
|
|
2549
2729
|
try {
|
|
2550
2730
|
const parsed = JSON.parse(raw);
|
|
@@ -2602,7 +2782,7 @@ function recencyScore(occurredAt, cadenceDays, now) {
|
|
|
2602
2782
|
return 0;
|
|
2603
2783
|
}
|
|
2604
2784
|
const cadence = Math.max(cadenceDays ?? 0, 1);
|
|
2605
|
-
const daysSince = Math.max(0, (now - occurredAt) /
|
|
2785
|
+
const daysSince = Math.max(0, (now - occurredAt) / DAY_MS2);
|
|
2606
2786
|
return Math.max(0, 1 - daysSince / cadence);
|
|
2607
2787
|
}
|
|
2608
2788
|
function refreshLinkRollup(db, linkId, now) {
|
|
@@ -2613,15 +2793,15 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2613
2793
|
const latest = db.prepare(
|
|
2614
2794
|
`SELECT event_id, occurred_at
|
|
2615
2795
|
FROM link_event_effects
|
|
2616
|
-
WHERE link_id = ?
|
|
2796
|
+
WHERE link_id = ? AND counts_for_recency = 1
|
|
2617
2797
|
ORDER BY occurred_at DESC, event_id DESC
|
|
2618
2798
|
LIMIT 1`
|
|
2619
2799
|
).get(linkId);
|
|
2620
|
-
const trailingWindowStart = now - 180 *
|
|
2800
|
+
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
2621
2801
|
const counts = db.prepare(
|
|
2622
2802
|
`SELECT
|
|
2623
2803
|
COUNT(*) AS total_meaningful_events,
|
|
2624
|
-
SUM(CASE WHEN affinity_delta > 0 OR trust_delta > 0 THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
2804
|
+
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
2625
2805
|
FROM link_event_effects
|
|
2626
2806
|
WHERE link_id = ? AND occurred_at >= ?`
|
|
2627
2807
|
).get(linkId, trailingWindowStart);
|
|
@@ -2629,7 +2809,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2629
2809
|
`SELECT ep.directionality
|
|
2630
2810
|
FROM link_event_effects le
|
|
2631
2811
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
2632
|
-
WHERE le.link_id = ? AND ep.contact_id = ?
|
|
2812
|
+
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
2633
2813
|
AND ep.directionality IS NOT NULL
|
|
2634
2814
|
AND le.occurred_at >= ?`
|
|
2635
2815
|
).all(linkId, link.from_contact_id, trailingWindowStart);
|
|
@@ -2655,7 +2835,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2655
2835
|
link.cadence_days,
|
|
2656
2836
|
now
|
|
2657
2837
|
);
|
|
2658
|
-
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) /
|
|
2838
|
+
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) / DAY_MS2);
|
|
2659
2839
|
const driftPriorityValue = driftPriority({
|
|
2660
2840
|
daysSinceLastMeaningfulEvent,
|
|
2661
2841
|
cadenceDays: link.cadence_days ?? 1,
|
|
@@ -2972,13 +3152,15 @@ function listRadar(db, filters, options) {
|
|
|
2972
3152
|
if (ownerId === null) {
|
|
2973
3153
|
return [];
|
|
2974
3154
|
}
|
|
3155
|
+
const now = resolveNow(options?.now);
|
|
3156
|
+
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 oc.due_at < ${now})`;
|
|
2975
3157
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
2976
3158
|
const clauses = [
|
|
2977
3159
|
"l.removed_at IS NULL",
|
|
2978
3160
|
"l.is_structural = 0",
|
|
2979
|
-
"l.from_contact_id = ?"
|
|
3161
|
+
"(l.from_contact_id = ? OR l.to_contact_id = ?)"
|
|
2980
3162
|
];
|
|
2981
|
-
const params = [ownerId];
|
|
3163
|
+
const params = [ownerId, ownerId];
|
|
2982
3164
|
if (filters?.kind !== void 0) {
|
|
2983
3165
|
clauses.push("l.kind = ?");
|
|
2984
3166
|
params.push(filters.kind);
|
|
@@ -3000,6 +3182,7 @@ function listRadar(db, filters, options) {
|
|
|
3000
3182
|
const rows = db.prepare(
|
|
3001
3183
|
`SELECT
|
|
3002
3184
|
l.id,
|
|
3185
|
+
${overdue} AS overdue_commitment,
|
|
3003
3186
|
l.from_contact_id,
|
|
3004
3187
|
l.to_contact_id,
|
|
3005
3188
|
l.trust,
|
|
@@ -3008,17 +3191,18 @@ function listRadar(db, filters, options) {
|
|
|
3008
3191
|
lr.recency_score,
|
|
3009
3192
|
lr.normalized_rank
|
|
3010
3193
|
FROM links l
|
|
3011
|
-
INNER JOIN contacts c_to ON c_to.id = l.to_contact_id
|
|
3194
|
+
INNER JOIN contacts c_to ON c_to.id = CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END
|
|
3012
3195
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
3013
3196
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
3014
3197
|
WHERE ${where}
|
|
3015
|
-
ORDER BY COALESCE(lr.radar_score, 0) DESC
|
|
3198
|
+
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3016
3199
|
LIMIT ? OFFSET ?`
|
|
3017
3200
|
).all(...params, limit, offset);
|
|
3018
3201
|
return rows.map((row) => ({
|
|
3019
3202
|
linkId: row.id,
|
|
3020
3203
|
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
3021
|
-
|
|
3204
|
+
reasons: row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
3205
|
+
driftPriority: row.overdue_commitment ? Math.max(1, row.drift_priority ?? 0) : row.drift_priority ?? 0,
|
|
3022
3206
|
recencyScore: row.recency_score ?? 0,
|
|
3023
3207
|
normalizedRank: row.normalized_rank ?? 0,
|
|
3024
3208
|
trust: row.trust ?? 0,
|
|
@@ -3629,39 +3813,6 @@ function listDismissedDuplicates(db, options) {
|
|
|
3629
3813
|
}));
|
|
3630
3814
|
}
|
|
3631
3815
|
|
|
3632
|
-
// src/events/get_contact_journal.ts
|
|
3633
|
-
function getContactJournal(db, contactId, options) {
|
|
3634
|
-
assertDefaultOrdering("getContactJournal", options);
|
|
3635
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
3636
|
-
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
3637
|
-
const params = [contactId];
|
|
3638
|
-
if (options?.since !== void 0) {
|
|
3639
|
-
clauses.push("e.occurred_at >= ?");
|
|
3640
|
-
params.push(options.since);
|
|
3641
|
-
}
|
|
3642
|
-
if (options?.until !== void 0) {
|
|
3643
|
-
clauses.push("e.occurred_at <= ?");
|
|
3644
|
-
params.push(options.until);
|
|
3645
|
-
}
|
|
3646
|
-
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
3647
|
-
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
3648
|
-
clauses.push(`e.type IN (${ph})`);
|
|
3649
|
-
params.push(...options.eventTypes);
|
|
3650
|
-
}
|
|
3651
|
-
const where = clauses.join(" AND ");
|
|
3652
|
-
const rows = db.prepare(
|
|
3653
|
-
`SELECT e.id FROM events e
|
|
3654
|
-
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
3655
|
-
WHERE ${where}
|
|
3656
|
-
ORDER BY e.occurred_at DESC, e.id DESC
|
|
3657
|
-
LIMIT ? OFFSET ?`
|
|
3658
|
-
).all(...params, limit, offset);
|
|
3659
|
-
return batchLoadEventRecords(
|
|
3660
|
-
db,
|
|
3661
|
-
rows.map((r) => r.id)
|
|
3662
|
-
);
|
|
3663
|
-
}
|
|
3664
|
-
|
|
3665
3816
|
// src/events/mechanics.ts
|
|
3666
3817
|
function clamp2(value, min, max) {
|
|
3667
3818
|
return Math.min(max, Math.max(min, value));
|
|
@@ -3861,111 +4012,35 @@ function warmthMatch(eventType, eventValence) {
|
|
|
3861
4012
|
if (eventType === "transaction") {
|
|
3862
4013
|
return 0.5;
|
|
3863
4014
|
}
|
|
3864
|
-
if (eventType === "conflict") {
|
|
3865
|
-
return 0;
|
|
3866
|
-
}
|
|
3867
|
-
return 0.35;
|
|
3868
|
-
}
|
|
3869
|
-
function reliabilityMatch(params) {
|
|
3870
|
-
if (params?.supportDelivered === true) {
|
|
3871
|
-
return 1;
|
|
3872
|
-
}
|
|
3873
|
-
if (params?.resolution === "kept") {
|
|
3874
|
-
return 1;
|
|
3875
|
-
}
|
|
3876
|
-
if (params?.resolution === "broken") {
|
|
3877
|
-
return 0;
|
|
3878
|
-
}
|
|
3879
|
-
return 0.45;
|
|
3880
|
-
}
|
|
3881
|
-
function baseWeight(params) {
|
|
3882
|
-
return typeWeight(params.eventType) * (0.35 + 0.65 * intensity(params.significance)) * params.directness * params.preferenceMatch * params.novelty;
|
|
3883
|
-
}
|
|
3884
|
-
function massPenalty(weeklyPositiveBaseWeight2) {
|
|
3885
|
-
const excessWeeklyMass = Math.max(0, weeklyPositiveBaseWeight2 - 8);
|
|
3886
|
-
return 1 / (1 + excessWeeklyMass / 8);
|
|
3887
|
-
}
|
|
3888
|
-
function dateSalienceBonus(significance) {
|
|
3889
|
-
return 1 + Math.min(0.25, significance / 40);
|
|
3890
|
-
}
|
|
3891
|
-
function repairBonus(consecutiveRepairEvents) {
|
|
3892
|
-
return Math.min(0.12, consecutiveRepairEvents * 0.02);
|
|
3893
|
-
}
|
|
3894
|
-
|
|
3895
|
-
// src/events/list_open_commitments.ts
|
|
3896
|
-
var DAY_MS2 = 864e5;
|
|
3897
|
-
function mapResolution(resolvedAt, resolution) {
|
|
3898
|
-
if (resolvedAt === null) {
|
|
3899
|
-
return "open";
|
|
3900
|
-
}
|
|
3901
|
-
if (resolution === "cancelled") {
|
|
3902
|
-
return "cancelled";
|
|
3903
|
-
}
|
|
3904
|
-
return "resolved";
|
|
3905
|
-
}
|
|
3906
|
-
function listOpenCommitments(db, filters, options) {
|
|
3907
|
-
assertDefaultOrdering("listOpenCommitments", options);
|
|
3908
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
3909
|
-
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
3910
|
-
const params = [];
|
|
3911
|
-
if (options?.since !== void 0) {
|
|
3912
|
-
clauses.push("e.occurred_at >= ?");
|
|
3913
|
-
params.push(options.since);
|
|
3914
|
-
}
|
|
3915
|
-
if (options?.until !== void 0) {
|
|
3916
|
-
clauses.push("e.occurred_at <= ?");
|
|
3917
|
-
params.push(options.until);
|
|
3918
|
-
}
|
|
3919
|
-
if (filters?.commitmentType !== void 0) {
|
|
3920
|
-
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
3921
|
-
throw new AffinityValidationError("invalid commitmentType filter");
|
|
3922
|
-
}
|
|
3923
|
-
clauses.push("oc.commitment_type = ?");
|
|
3924
|
-
params.push(filters.commitmentType);
|
|
3925
|
-
}
|
|
3926
|
-
if (filters?.contactId !== void 0) {
|
|
3927
|
-
clauses.push(
|
|
3928
|
-
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
3929
|
-
);
|
|
3930
|
-
params.push(filters.contactId);
|
|
3931
|
-
}
|
|
3932
|
-
if (filters?.linkId !== void 0) {
|
|
3933
|
-
clauses.push(
|
|
3934
|
-
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
3935
|
-
);
|
|
3936
|
-
params.push(filters.linkId);
|
|
3937
|
-
}
|
|
3938
|
-
if (filters?.horizonDays !== void 0) {
|
|
3939
|
-
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
3940
|
-
throw new AffinityValidationError(
|
|
3941
|
-
"horizonDays must be a non-negative integer"
|
|
3942
|
-
);
|
|
3943
|
-
}
|
|
3944
|
-
const anchor = options?.since ?? Date.now();
|
|
3945
|
-
clauses.push("oc.due_at IS NOT NULL");
|
|
3946
|
-
clauses.push("oc.due_at <= ?");
|
|
3947
|
-
params.push(anchor + filters.horizonDays * DAY_MS2);
|
|
3948
|
-
}
|
|
3949
|
-
const where = clauses.join(" AND ");
|
|
3950
|
-
const rows = db.prepare(
|
|
3951
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
3952
|
-
e.type, e.summary
|
|
3953
|
-
FROM open_commitments oc
|
|
3954
|
-
INNER JOIN events e ON e.id = oc.event_id
|
|
3955
|
-
WHERE ${where}
|
|
3956
|
-
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
3957
|
-
LIMIT ? OFFSET ?`
|
|
3958
|
-
).all(...params, limit, offset);
|
|
3959
|
-
const eventIds = rows.map((r) => r.event_id);
|
|
3960
|
-
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
3961
|
-
return rows.map((r) => ({
|
|
3962
|
-
eventId: r.event_id,
|
|
3963
|
-
type: r.type,
|
|
3964
|
-
summary: r.summary,
|
|
3965
|
-
participants: partsByEvent.get(r.event_id) ?? [],
|
|
3966
|
-
dueAt: r.due_at,
|
|
3967
|
-
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
3968
|
-
}));
|
|
4015
|
+
if (eventType === "conflict") {
|
|
4016
|
+
return 0;
|
|
4017
|
+
}
|
|
4018
|
+
return 0.35;
|
|
4019
|
+
}
|
|
4020
|
+
function reliabilityMatch(params) {
|
|
4021
|
+
if (params?.supportDelivered === true) {
|
|
4022
|
+
return 1;
|
|
4023
|
+
}
|
|
4024
|
+
if (params?.resolution === "kept") {
|
|
4025
|
+
return 1;
|
|
4026
|
+
}
|
|
4027
|
+
if (params?.resolution === "broken") {
|
|
4028
|
+
return 0;
|
|
4029
|
+
}
|
|
4030
|
+
return 0.45;
|
|
4031
|
+
}
|
|
4032
|
+
function baseWeight(params) {
|
|
4033
|
+
return typeWeight(params.eventType) * (0.35 + 0.65 * intensity(params.significance)) * params.directness * params.preferenceMatch * params.novelty;
|
|
4034
|
+
}
|
|
4035
|
+
function massPenalty(weeklyPositiveBaseWeight2) {
|
|
4036
|
+
const excessWeeklyMass = Math.max(0, weeklyPositiveBaseWeight2 - 8);
|
|
4037
|
+
return 1 / (1 + excessWeeklyMass / 8);
|
|
4038
|
+
}
|
|
4039
|
+
function dateSalienceBonus(significance) {
|
|
4040
|
+
return 1 + Math.min(0.25, significance / 40);
|
|
4041
|
+
}
|
|
4042
|
+
function repairBonus(consecutiveRepairEvents) {
|
|
4043
|
+
return Math.min(0.12, consecutiveRepairEvents * 0.02);
|
|
3969
4044
|
}
|
|
3970
4045
|
|
|
3971
4046
|
// src/links/effects.ts
|
|
@@ -4245,7 +4320,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
4245
4320
|
1,
|
|
4246
4321
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
4247
4322
|
);
|
|
4248
|
-
const cadenceAfter = nextCadenceDays({
|
|
4323
|
+
const cadenceAfter = commitmentResolution === "broken" || commitmentResolution === "cancelled" ? cadenceBefore : nextCadenceDays({
|
|
4249
4324
|
kind: linkKind,
|
|
4250
4325
|
currentCadenceDays: cadenceBefore,
|
|
4251
4326
|
gapDays
|
|
@@ -4308,6 +4383,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
4308
4383
|
params.now,
|
|
4309
4384
|
params.now
|
|
4310
4385
|
);
|
|
4386
|
+
if (commitmentResolution === "broken" || commitmentResolution === "cancelled") {
|
|
4387
|
+
db.prepare(
|
|
4388
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4389
|
+
).run(params.eventId, params.linkId);
|
|
4390
|
+
}
|
|
4311
4391
|
refreshLinkRollup(db, params.linkId, params.now);
|
|
4312
4392
|
return {
|
|
4313
4393
|
linkId: params.linkId,
|
|
@@ -4687,12 +4767,16 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4687
4767
|
if (row.type !== "promise" && row.type !== "agreement") {
|
|
4688
4768
|
throw new AffinityInvariantError("not a commitment event");
|
|
4689
4769
|
}
|
|
4690
|
-
const open = db.prepare(
|
|
4770
|
+
const open = db.prepare(
|
|
4771
|
+
"SELECT resolved_at, resolution FROM open_commitments WHERE event_id = ?"
|
|
4772
|
+
).get(commitmentEventId);
|
|
4691
4773
|
if (!open) {
|
|
4692
4774
|
throw new AffinityNotFoundError("open commitment not found");
|
|
4693
4775
|
}
|
|
4694
4776
|
if (open.resolved_at !== null) {
|
|
4695
|
-
throw new
|
|
4777
|
+
throw new AffinityConflictError(
|
|
4778
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4779
|
+
);
|
|
4696
4780
|
}
|
|
4697
4781
|
const now = resolveNow(options?.now);
|
|
4698
4782
|
const originalParticipants = loadEventParticipantViews(
|
|
@@ -4705,7 +4789,9 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4705
4789
|
WHERE event_id = ? AND resolved_at IS NULL`
|
|
4706
4790
|
).run(resolution, now, now, commitmentEventId);
|
|
4707
4791
|
if (Number(result.changes ?? 0) === 0) {
|
|
4708
|
-
throw new
|
|
4792
|
+
throw new AffinityConflictError(
|
|
4793
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4794
|
+
);
|
|
4709
4795
|
}
|
|
4710
4796
|
const shouldCreateResolvingEvent = options?.resolvingEvent !== void 0 || resolution === "broken";
|
|
4711
4797
|
const created = [];
|
|
@@ -4768,6 +4854,82 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4768
4854
|
});
|
|
4769
4855
|
}
|
|
4770
4856
|
|
|
4857
|
+
// src/contacts/export_contact.ts
|
|
4858
|
+
function exportContact(db, id) {
|
|
4859
|
+
const query = (sql, ...params) => db.prepare(sql).all(...params).map((row) => {
|
|
4860
|
+
const result = {};
|
|
4861
|
+
for (const [key, value] of Object.entries(row)) {
|
|
4862
|
+
const name = key.replace(
|
|
4863
|
+
/_([a-z])/g,
|
|
4864
|
+
(_, letter) => letter.toUpperCase()
|
|
4865
|
+
);
|
|
4866
|
+
result[name] = value;
|
|
4867
|
+
if ((name.endsWith("At") || name === "dueAt") && typeof value === "number")
|
|
4868
|
+
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4869
|
+
if (key.endsWith("_json") && typeof value === "string")
|
|
4870
|
+
result[name] = JSON.parse(value);
|
|
4871
|
+
}
|
|
4872
|
+
return result;
|
|
4873
|
+
});
|
|
4874
|
+
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4875
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4876
|
+
const events = query(
|
|
4877
|
+
"SELECT DISTINCT e.* FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? ORDER BY e.id",
|
|
4878
|
+
id
|
|
4879
|
+
).map((event) => ({
|
|
4880
|
+
...event,
|
|
4881
|
+
participants: query(
|
|
4882
|
+
"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
|
+
event.id
|
|
4884
|
+
)
|
|
4885
|
+
}));
|
|
4886
|
+
return {
|
|
4887
|
+
contact,
|
|
4888
|
+
identities: query(
|
|
4889
|
+
"SELECT * FROM identities WHERE contact_id = ? ORDER BY id",
|
|
4890
|
+
id
|
|
4891
|
+
),
|
|
4892
|
+
attributes: query(
|
|
4893
|
+
"SELECT * FROM attributes WHERE contact_id = ? OR link_id IN (SELECT id FROM links WHERE from_contact_id = ? OR to_contact_id = ?) ORDER BY id",
|
|
4894
|
+
id,
|
|
4895
|
+
id,
|
|
4896
|
+
id
|
|
4897
|
+
),
|
|
4898
|
+
links: query(
|
|
4899
|
+
"SELECT l.*, f.name AS from_contact_name, t.name AS to_contact_name FROM links l JOIN contacts f ON f.id = l.from_contact_id JOIN contacts t ON t.id = l.to_contact_id WHERE from_contact_id = ? OR to_contact_id = ? ORDER BY l.id",
|
|
4900
|
+
id,
|
|
4901
|
+
id
|
|
4902
|
+
),
|
|
4903
|
+
events,
|
|
4904
|
+
commitments: query(
|
|
4905
|
+
"SELECT o.* FROM open_commitments o JOIN event_participants p ON p.event_id = o.event_id WHERE p.contact_id = ? ORDER BY o.event_id",
|
|
4906
|
+
id
|
|
4907
|
+
),
|
|
4908
|
+
dates: query(
|
|
4909
|
+
"SELECT * FROM events WHERE anchor_contact_id = ? OR anchor_link_id IN (SELECT id FROM links WHERE from_contact_id = ? OR to_contact_id = ?) ORDER BY id",
|
|
4910
|
+
id,
|
|
4911
|
+
id,
|
|
4912
|
+
id
|
|
4913
|
+
),
|
|
4914
|
+
mergeHistory: query(
|
|
4915
|
+
"SELECT * FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ? ORDER BY id",
|
|
4916
|
+
id,
|
|
4917
|
+
id
|
|
4918
|
+
)
|
|
4919
|
+
};
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
// src/contacts/export_affinity.ts
|
|
4923
|
+
function exportAffinity(db) {
|
|
4924
|
+
const now = Date.now();
|
|
4925
|
+
return {
|
|
4926
|
+
version: 1,
|
|
4927
|
+
exportedAt: now,
|
|
4928
|
+
exportedAtIso: new Date(now).toISOString(),
|
|
4929
|
+
contacts: db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id))
|
|
4930
|
+
};
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4771
4933
|
// src/soul.ts
|
|
4772
4934
|
var soul_exports = {};
|
|
4773
4935
|
__export(soul_exports, {
|
|
@@ -5535,6 +5697,7 @@ __export(tools_exports, {
|
|
|
5535
5697
|
numberSchema: () => numberSchema,
|
|
5536
5698
|
objectSchema: () => objectSchema,
|
|
5537
5699
|
oneOfSchema: () => oneOfSchema,
|
|
5700
|
+
parseTimeInput: () => parseTimeInput,
|
|
5538
5701
|
recordEventTool: () => recordEventTool,
|
|
5539
5702
|
recordEventToolHandler: () => recordEventToolHandler,
|
|
5540
5703
|
recordEventToolName: () => recordEventToolName,
|
|
@@ -5568,8 +5731,55 @@ __export(tools_exports, {
|
|
|
5568
5731
|
withToolHandling: () => withToolHandling
|
|
5569
5732
|
});
|
|
5570
5733
|
|
|
5734
|
+
// 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
|
+
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
5737
|
+
const fail = () => {
|
|
5738
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
5739
|
+
};
|
|
5740
|
+
if (typeof value === "number")
|
|
5741
|
+
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
5742
|
+
if (typeof value !== "string") return fail();
|
|
5743
|
+
const text = value.trim();
|
|
5744
|
+
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
5745
|
+
let day;
|
|
5746
|
+
if (relative) {
|
|
5747
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
5748
|
+
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
5749
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
5750
|
+
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
5751
|
+
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
5752
|
+
return fail();
|
|
5753
|
+
} else {
|
|
5754
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
5755
|
+
text
|
|
5756
|
+
))
|
|
5757
|
+
return fail();
|
|
5758
|
+
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
5759
|
+
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
5760
|
+
return fail();
|
|
5761
|
+
const parsed = Date.parse(text);
|
|
5762
|
+
return Number.isFinite(parsed) ? parsed : fail();
|
|
5763
|
+
}
|
|
5764
|
+
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
5765
|
+
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
5766
|
+
}
|
|
5767
|
+
|
|
5571
5768
|
// src/tools/tool_metadata.ts
|
|
5572
5769
|
function defineAffinityTool(definition) {
|
|
5770
|
+
const visit = (schema) => {
|
|
5771
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
5772
|
+
if ((key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.type === "integer") {
|
|
5773
|
+
child.type = void 0;
|
|
5774
|
+
child.oneOf = [{ type: "integer" }, { type: "string" }];
|
|
5775
|
+
child.description = `${child.description ?? "Time."} Accepts ${timeInputForms}. UTC midnight for dates; dueAt uses UTC end of day.`;
|
|
5776
|
+
}
|
|
5777
|
+
visit(child);
|
|
5778
|
+
}
|
|
5779
|
+
if (schema.items) visit(schema.items);
|
|
5780
|
+
for (const child of schema.oneOf ?? schema.anyOf ?? []) visit(child);
|
|
5781
|
+
};
|
|
5782
|
+
visit(definition.inputSchema);
|
|
5573
5783
|
return definition;
|
|
5574
5784
|
}
|
|
5575
5785
|
function stringSchema(description, options = {}) {
|
|
@@ -5668,6 +5878,7 @@ function toContactEntityRef(value) {
|
|
|
5668
5878
|
function toContactListToolItem(value) {
|
|
5669
5879
|
return {
|
|
5670
5880
|
...toContactEntityRef(value),
|
|
5881
|
+
..."matchedOn" in value ? { matchedOn: value.matchedOn } : {},
|
|
5671
5882
|
subtitle: "kind" in value ? value.kind : void 0
|
|
5672
5883
|
};
|
|
5673
5884
|
}
|
|
@@ -5714,7 +5925,9 @@ function toEventEntityRef(value) {
|
|
|
5714
5925
|
}
|
|
5715
5926
|
function toEventListToolItem(value) {
|
|
5716
5927
|
return {
|
|
5928
|
+
...value,
|
|
5717
5929
|
...toEventEntityRef(value),
|
|
5930
|
+
occurredAtIso: new Date(value.occurredAt).toISOString(),
|
|
5718
5931
|
snippet: `${value.participants.length} participant(s)`
|
|
5719
5932
|
};
|
|
5720
5933
|
}
|
|
@@ -5771,7 +5984,7 @@ function withOptionalFields(result, options) {
|
|
|
5771
5984
|
if (options.warnings && options.warnings.length > 0) {
|
|
5772
5985
|
result.warnings = options.warnings;
|
|
5773
5986
|
}
|
|
5774
|
-
return result;
|
|
5987
|
+
return roundToolValues(result);
|
|
5775
5988
|
}
|
|
5776
5989
|
function toolWarning(code, message) {
|
|
5777
5990
|
return { code, message };
|
|
@@ -5835,9 +6048,31 @@ function toolFailure(kind, code, summary, message, options = {}) {
|
|
|
5835
6048
|
options
|
|
5836
6049
|
);
|
|
5837
6050
|
}
|
|
6051
|
+
function roundToolValues(value) {
|
|
6052
|
+
if (typeof value === "number")
|
|
6053
|
+
return Number.isInteger(value) ? value : Math.round(value * 1e3) / 1e3;
|
|
6054
|
+
if (Array.isArray(value)) return value.map(roundToolValues);
|
|
6055
|
+
if (value !== null && typeof value === "object")
|
|
6056
|
+
return Object.fromEntries(
|
|
6057
|
+
Object.entries(value).map(([key, child]) => [
|
|
6058
|
+
key,
|
|
6059
|
+
roundToolValues(child)
|
|
6060
|
+
])
|
|
6061
|
+
);
|
|
6062
|
+
return value;
|
|
6063
|
+
}
|
|
5838
6064
|
|
|
5839
6065
|
// src/tools/tool_resolvers.ts
|
|
5840
|
-
function resolveContactLocator(db, locator, field = "contact") {
|
|
6066
|
+
function resolveContactLocator(db, locator, field = "contact", allowMerged = false) {
|
|
6067
|
+
if (!locator || typeof locator !== "object")
|
|
6068
|
+
return {
|
|
6069
|
+
ok: false,
|
|
6070
|
+
result: toolNeedsClarification(
|
|
6071
|
+
"missing_required_choice",
|
|
6072
|
+
`Provide ${field} locator.`,
|
|
6073
|
+
[field]
|
|
6074
|
+
)
|
|
6075
|
+
};
|
|
5841
6076
|
if ("contactId" in locator) {
|
|
5842
6077
|
const row2 = getContactRowById(db, locator.contactId);
|
|
5843
6078
|
if (!row2) {
|
|
@@ -5851,8 +6086,25 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
5851
6086
|
)
|
|
5852
6087
|
};
|
|
5853
6088
|
}
|
|
6089
|
+
if (!allowMerged && row2.merged_into_contact_id !== null) {
|
|
6090
|
+
const winner = getContactRowById(db, row2.merged_into_contact_id);
|
|
6091
|
+
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
6092
|
+
return {
|
|
6093
|
+
ok: false,
|
|
6094
|
+
result: toolFailure("domain", "invalid_state", message, message)
|
|
6095
|
+
};
|
|
6096
|
+
}
|
|
5854
6097
|
return { ok: true, value: row2 };
|
|
5855
6098
|
}
|
|
6099
|
+
if (!("identity" in locator) || !locator.identity)
|
|
6100
|
+
return {
|
|
6101
|
+
ok: false,
|
|
6102
|
+
result: toolNeedsClarification(
|
|
6103
|
+
"missing_required_choice",
|
|
6104
|
+
`Provide ${field}.contactId or identity.`,
|
|
6105
|
+
[field]
|
|
6106
|
+
)
|
|
6107
|
+
};
|
|
5856
6108
|
const normalizedKey = normalizeIdentityKey(
|
|
5857
6109
|
locator.identity.type,
|
|
5858
6110
|
locator.identity.value
|
|
@@ -5910,6 +6162,15 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
5910
6162
|
return { ok: true, value: matchedContact };
|
|
5911
6163
|
}
|
|
5912
6164
|
function resolveLinkLocator(db, locator, field = "link") {
|
|
6165
|
+
if (!locator || typeof locator !== "object")
|
|
6166
|
+
return {
|
|
6167
|
+
ok: false,
|
|
6168
|
+
result: toolNeedsClarification(
|
|
6169
|
+
"missing_required_choice",
|
|
6170
|
+
`Provide ${field} locator.`,
|
|
6171
|
+
[field]
|
|
6172
|
+
)
|
|
6173
|
+
};
|
|
5913
6174
|
if ("linkId" in locator) {
|
|
5914
6175
|
const row = getLinkRowById(db, locator.linkId);
|
|
5915
6176
|
if (!row) {
|
|
@@ -5925,6 +6186,15 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
5925
6186
|
}
|
|
5926
6187
|
return { ok: true, value: row };
|
|
5927
6188
|
}
|
|
6189
|
+
if (!("endpoints" in locator) || !locator.endpoints)
|
|
6190
|
+
return {
|
|
6191
|
+
ok: false,
|
|
6192
|
+
result: toolNeedsClarification(
|
|
6193
|
+
"missing_required_choice",
|
|
6194
|
+
`Provide ${field}.linkId or endpoints.`,
|
|
6195
|
+
[field]
|
|
6196
|
+
)
|
|
6197
|
+
};
|
|
5928
6198
|
const { endpoints } = locator;
|
|
5929
6199
|
const clauses = [
|
|
5930
6200
|
"removed_at IS NULL",
|
|
@@ -6043,6 +6313,92 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
6043
6313
|
}
|
|
6044
6314
|
}
|
|
6045
6315
|
|
|
6316
|
+
// src/tools/validate_tool_input.ts
|
|
6317
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
6318
|
+
if (schema.anyOf || schema.oneOf) {
|
|
6319
|
+
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
6320
|
+
return choices.some(
|
|
6321
|
+
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
6322
|
+
) ? null : `${path}: invalid value`;
|
|
6323
|
+
}
|
|
6324
|
+
if (schema.type === "object") {
|
|
6325
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
6326
|
+
return `${path}: missing input or expected object`;
|
|
6327
|
+
const object = value;
|
|
6328
|
+
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
6329
|
+
for (const key of Object.keys(object)) {
|
|
6330
|
+
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(", ")}`;
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
6335
|
+
return `${path}.patch: missing required field`;
|
|
6336
|
+
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
6337
|
+
return `${path}.patch: unknown patch key; allowed keys: none (use the action's named fields)`;
|
|
6338
|
+
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
6339
|
+
try {
|
|
6340
|
+
return parseTimeInput(
|
|
6341
|
+
object.now,
|
|
6342
|
+
`${path}.now`,
|
|
6343
|
+
injectedNow
|
|
6344
|
+
);
|
|
6345
|
+
} catch {
|
|
6346
|
+
return Date.now();
|
|
6347
|
+
}
|
|
6348
|
+
})();
|
|
6349
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
6350
|
+
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
6351
|
+
try {
|
|
6352
|
+
object[key] = parseTimeInput(
|
|
6353
|
+
object[key],
|
|
6354
|
+
`${path}.${key}`,
|
|
6355
|
+
referenceNow
|
|
6356
|
+
);
|
|
6357
|
+
} catch (error) {
|
|
6358
|
+
return error.message;
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6362
|
+
for (const key of schema.required ?? []) {
|
|
6363
|
+
if (object[key] === void 0)
|
|
6364
|
+
return `${path}.${key}: missing required field`;
|
|
6365
|
+
}
|
|
6366
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
6367
|
+
if (object[key] === void 0) continue;
|
|
6368
|
+
const error = validateToolInput(
|
|
6369
|
+
child,
|
|
6370
|
+
object[key],
|
|
6371
|
+
`${path}.${key}`,
|
|
6372
|
+
referenceNow
|
|
6373
|
+
);
|
|
6374
|
+
if (error) return error;
|
|
6375
|
+
}
|
|
6376
|
+
return null;
|
|
6377
|
+
}
|
|
6378
|
+
if (schema.type === "array") {
|
|
6379
|
+
if (!Array.isArray(value)) return `${path}: expected array`;
|
|
6380
|
+
for (const [index, item] of value.entries()) {
|
|
6381
|
+
const error = validateToolInput(
|
|
6382
|
+
schema.items ?? {},
|
|
6383
|
+
item,
|
|
6384
|
+
`${path}[${index}]`,
|
|
6385
|
+
injectedNow
|
|
6386
|
+
);
|
|
6387
|
+
if (error) return error;
|
|
6388
|
+
}
|
|
6389
|
+
return null;
|
|
6390
|
+
}
|
|
6391
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
6392
|
+
return `${path}: expected ${schema.enum.join(", ")}`;
|
|
6393
|
+
if (schema.type === "null")
|
|
6394
|
+
return value === null ? null : `${path}: expected null`;
|
|
6395
|
+
if (schema.type === "integer")
|
|
6396
|
+
return Number.isInteger(value) ? null : `${path}: expected integer`;
|
|
6397
|
+
const valid = schema.type === void 0 || schema.type === "string" && typeof value === "string" || schema.type === "number" && typeof value === "number" && Number.isFinite(value) || schema.type === "boolean" && typeof value === "boolean";
|
|
6398
|
+
if (!valid) return `${path}: expected ${schema.type}`;
|
|
6399
|
+
return null;
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6046
6402
|
// src/tools/inspect_affinity_item_tool.ts
|
|
6047
6403
|
function contactLocatorSchema(description) {
|
|
6048
6404
|
return {
|
|
@@ -6078,7 +6434,19 @@ function linkLocatorSchema(description) {
|
|
|
6078
6434
|
description
|
|
6079
6435
|
};
|
|
6080
6436
|
}
|
|
6081
|
-
function inspectAffinityItemToolHandler(db,
|
|
6437
|
+
function inspectAffinityItemToolHandler(db, rawInput) {
|
|
6438
|
+
const input = structuredClone(rawInput);
|
|
6439
|
+
const inputError = validateToolInput(
|
|
6440
|
+
inspectAffinityItemTool.inputSchema,
|
|
6441
|
+
input
|
|
6442
|
+
);
|
|
6443
|
+
if (inputError)
|
|
6444
|
+
return toolFailure(
|
|
6445
|
+
"protocol",
|
|
6446
|
+
"invalid_input",
|
|
6447
|
+
"Invalid input.",
|
|
6448
|
+
inputError
|
|
6449
|
+
);
|
|
6082
6450
|
return withToolHandling(() => {
|
|
6083
6451
|
if (input.kind === "owner_profile") {
|
|
6084
6452
|
const detail2 = getOwnerProfile(
|
|
@@ -6096,19 +6464,21 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
6096
6464
|
return toolSuccess(
|
|
6097
6465
|
`Loaded owner profile for ${detail2.contact.name}.`,
|
|
6098
6466
|
{ kind: input.kind, detail: detail2 },
|
|
6099
|
-
{
|
|
6467
|
+
{
|
|
6468
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
6469
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
6470
|
+
}
|
|
6100
6471
|
);
|
|
6101
6472
|
}
|
|
6102
6473
|
if (input.kind === "contact_profile") {
|
|
6103
|
-
const contact = resolveContactLocator(db, input.contact, "contact");
|
|
6474
|
+
const contact = resolveContactLocator(db, input.contact, "contact", true);
|
|
6104
6475
|
if (!contact.ok) {
|
|
6105
6476
|
return contact.result;
|
|
6106
6477
|
}
|
|
6107
|
-
const detail2 = getContactProfile(
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
);
|
|
6478
|
+
const detail2 = getContactProfile(db, contact.value.id, {
|
|
6479
|
+
...input.topLinksLimit === void 0 ? {} : { topLinksLimit: input.topLinksLimit },
|
|
6480
|
+
...input.recentEventsLimit === void 0 ? {} : { recentEventsLimit: input.recentEventsLimit }
|
|
6481
|
+
});
|
|
6112
6482
|
if (!detail2) {
|
|
6113
6483
|
return toolFailure(
|
|
6114
6484
|
"domain",
|
|
@@ -6120,7 +6490,10 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
6120
6490
|
return toolSuccess(
|
|
6121
6491
|
`Loaded contact profile for ${detail2.contact.name}.`,
|
|
6122
6492
|
{ kind: input.kind, detail: detail2 },
|
|
6123
|
-
{
|
|
6493
|
+
{
|
|
6494
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
6495
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
6496
|
+
}
|
|
6124
6497
|
);
|
|
6125
6498
|
}
|
|
6126
6499
|
const link = resolveLinkLocator(db, input.link, "link");
|
|
@@ -6461,9 +6834,17 @@ function unsetAttribute(db, target, name, removedAt) {
|
|
|
6461
6834
|
}
|
|
6462
6835
|
|
|
6463
6836
|
// src/tools/tool_mutation.ts
|
|
6464
|
-
function mutationToolResult(action, receipt, entitySummary) {
|
|
6837
|
+
function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
6465
6838
|
const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
|
|
6466
6839
|
const data = {
|
|
6840
|
+
changes: ["created", "updated", "archived", "removed"].flatMap(
|
|
6841
|
+
(verb) => receipt[verb].map((ref) => ({
|
|
6842
|
+
entity: ref.kind,
|
|
6843
|
+
id: ref.id,
|
|
6844
|
+
action: verb,
|
|
6845
|
+
...verb === "updated" && fields ? { fields } : {}
|
|
6846
|
+
}))
|
|
6847
|
+
),
|
|
6467
6848
|
action,
|
|
6468
6849
|
primary: receipt.primary,
|
|
6469
6850
|
created: receipt.created,
|
|
@@ -6550,7 +6931,23 @@ function resolveAttributeTarget(db, target) {
|
|
|
6550
6931
|
}
|
|
6551
6932
|
return { kind: "ok", target: { kind: "link", id: link.value.id } };
|
|
6552
6933
|
}
|
|
6553
|
-
function manageAttributeToolHandler(db,
|
|
6934
|
+
function manageAttributeToolHandler(db, rawInput) {
|
|
6935
|
+
const input = structuredClone(rawInput);
|
|
6936
|
+
const inputError = validateToolInput(manageAttributeTool.inputSchema, input);
|
|
6937
|
+
if (inputError)
|
|
6938
|
+
return toolFailure(
|
|
6939
|
+
"protocol",
|
|
6940
|
+
"invalid_input",
|
|
6941
|
+
"Invalid input.",
|
|
6942
|
+
inputError
|
|
6943
|
+
);
|
|
6944
|
+
if (!input.target)
|
|
6945
|
+
return toolFailure(
|
|
6946
|
+
"protocol",
|
|
6947
|
+
"invalid_input",
|
|
6948
|
+
"Missing target.",
|
|
6949
|
+
"target is required."
|
|
6950
|
+
);
|
|
6554
6951
|
return withToolHandling(() => {
|
|
6555
6952
|
const resolved = resolveAttributeTarget(db, input.target);
|
|
6556
6953
|
if (resolved.kind === "error") {
|
|
@@ -6661,9 +7058,25 @@ function ensureOwnerParticipates(db, participants) {
|
|
|
6661
7058
|
function prepareParticipants(db, raw) {
|
|
6662
7059
|
return ensureOwnerParticipates(db, raw.map(normalizeParticipant));
|
|
6663
7060
|
}
|
|
6664
|
-
function manageCommitmentToolHandler(db,
|
|
7061
|
+
function manageCommitmentToolHandler(db, rawInput) {
|
|
7062
|
+
const input = structuredClone(rawInput);
|
|
7063
|
+
const inputError = validateToolInput(manageCommitmentTool.inputSchema, input);
|
|
7064
|
+
if (inputError)
|
|
7065
|
+
return toolFailure(
|
|
7066
|
+
"protocol",
|
|
7067
|
+
"invalid_input",
|
|
7068
|
+
"Invalid input.",
|
|
7069
|
+
inputError
|
|
7070
|
+
);
|
|
6665
7071
|
return withToolHandling(() => {
|
|
6666
7072
|
if (input.action === "record") {
|
|
7073
|
+
if (!input.input)
|
|
7074
|
+
return toolFailure(
|
|
7075
|
+
"protocol",
|
|
7076
|
+
"invalid_input",
|
|
7077
|
+
"Missing input.",
|
|
7078
|
+
"input is required for record."
|
|
7079
|
+
);
|
|
6667
7080
|
const patched = {
|
|
6668
7081
|
...input.input,
|
|
6669
7082
|
participants: prepareParticipants(
|
|
@@ -6730,7 +7143,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
6730
7143
|
role: stringSchema("Participant role."),
|
|
6731
7144
|
directionality: stringSchema("Optional directionality.")
|
|
6732
7145
|
},
|
|
6733
|
-
["
|
|
7146
|
+
["role"]
|
|
6734
7147
|
),
|
|
6735
7148
|
"Participant list. Must be non-empty and include the owner contact."
|
|
6736
7149
|
),
|
|
@@ -6754,28 +7167,282 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
6754
7167
|
),
|
|
6755
7168
|
options: objectSchema(
|
|
6756
7169
|
{
|
|
6757
|
-
now: integerSchema("Optional timestamp."),
|
|
6758
|
-
resolvingEvent: objectSchema(
|
|
6759
|
-
{
|
|
6760
|
-
type: stringSchema("Optional resolving event type."),
|
|
6761
|
-
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
6762
|
-
summary: stringSchema("Optional resolving event summary."),
|
|
6763
|
-
significance: integerSchema(
|
|
6764
|
-
"Optional resolving event significance."
|
|
6765
|
-
)
|
|
6766
|
-
},
|
|
6767
|
-
[]
|
|
6768
|
-
)
|
|
6769
|
-
},
|
|
6770
|
-
[],
|
|
6771
|
-
"Optional metadata."
|
|
6772
|
-
)
|
|
6773
|
-
},
|
|
6774
|
-
["action"],
|
|
6775
|
-
"Record or resolve one commitment."
|
|
6776
|
-
),
|
|
6777
|
-
handler: manageCommitmentToolHandler
|
|
6778
|
-
});
|
|
7170
|
+
now: integerSchema("Optional timestamp."),
|
|
7171
|
+
resolvingEvent: objectSchema(
|
|
7172
|
+
{
|
|
7173
|
+
type: stringSchema("Optional resolving event type."),
|
|
7174
|
+
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
7175
|
+
summary: stringSchema("Optional resolving event summary."),
|
|
7176
|
+
significance: integerSchema(
|
|
7177
|
+
"Optional resolving event significance."
|
|
7178
|
+
)
|
|
7179
|
+
},
|
|
7180
|
+
[]
|
|
7181
|
+
)
|
|
7182
|
+
},
|
|
7183
|
+
[],
|
|
7184
|
+
"Optional metadata."
|
|
7185
|
+
)
|
|
7186
|
+
},
|
|
7187
|
+
["action"],
|
|
7188
|
+
"Record or resolve one commitment."
|
|
7189
|
+
),
|
|
7190
|
+
handler: manageCommitmentToolHandler
|
|
7191
|
+
});
|
|
7192
|
+
|
|
7193
|
+
// src/contacts/delete_contact.ts
|
|
7194
|
+
function deleteContact(db, locatorId, options) {
|
|
7195
|
+
if (options?.confirm !== true)
|
|
7196
|
+
throw new AffinityValidationError(
|
|
7197
|
+
"delete is permanent; pass confirm: true"
|
|
7198
|
+
);
|
|
7199
|
+
return withTransaction(db, () => {
|
|
7200
|
+
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
7201
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
7202
|
+
if (contact.is_owner)
|
|
7203
|
+
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
7204
|
+
const tables = [
|
|
7205
|
+
"contacts",
|
|
7206
|
+
"identities",
|
|
7207
|
+
"attributes",
|
|
7208
|
+
"links",
|
|
7209
|
+
"events",
|
|
7210
|
+
"event_participants",
|
|
7211
|
+
"open_commitments",
|
|
7212
|
+
"contact_merges",
|
|
7213
|
+
"dismissed_duplicates",
|
|
7214
|
+
"link_event_effects",
|
|
7215
|
+
"upcoming_occurrences",
|
|
7216
|
+
"link_rollups",
|
|
7217
|
+
"contact_rollups"
|
|
7218
|
+
];
|
|
7219
|
+
const before = new Map(
|
|
7220
|
+
tables.map((table) => [
|
|
7221
|
+
table,
|
|
7222
|
+
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
7223
|
+
])
|
|
7224
|
+
);
|
|
7225
|
+
db.prepare(
|
|
7226
|
+
"DELETE FROM events WHERE id IN (SELECT p.event_id FROM event_participants p WHERE p.contact_id = ? AND NOT EXISTS (SELECT 1 FROM event_participants q JOIN contacts c ON c.id = q.contact_id WHERE q.event_id = p.event_id AND q.contact_id != ? AND c.is_owner = 0))"
|
|
7227
|
+
).run(locatorId, locatorId);
|
|
7228
|
+
db.prepare(
|
|
7229
|
+
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
7230
|
+
).run(locatorId, locatorId);
|
|
7231
|
+
db.prepare(
|
|
7232
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7233
|
+
).run(locatorId, locatorId);
|
|
7234
|
+
db.prepare(
|
|
7235
|
+
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
7236
|
+
).run(locatorId);
|
|
7237
|
+
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
7238
|
+
const now = Date.now();
|
|
7239
|
+
const affectedLinks = db.prepare(
|
|
7240
|
+
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
7241
|
+
).all().map((row) => row.id);
|
|
7242
|
+
refreshAllBridgeScores(db, now);
|
|
7243
|
+
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
7244
|
+
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
7245
|
+
refreshContactRollup(db, Number(row.id), now);
|
|
7246
|
+
const removed = tables.map((table) => {
|
|
7247
|
+
const remaining = new Set(
|
|
7248
|
+
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
7249
|
+
);
|
|
7250
|
+
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
7251
|
+
return { entity: table, count: ids.length, ids };
|
|
7252
|
+
}).filter((item) => item.count > 0);
|
|
7253
|
+
return { contactId: locatorId, removed, affectedLinks };
|
|
7254
|
+
});
|
|
7255
|
+
}
|
|
7256
|
+
|
|
7257
|
+
// src/identities/queries.ts
|
|
7258
|
+
function getIdentityRowById(db, id) {
|
|
7259
|
+
const row = db.prepare(
|
|
7260
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
7261
|
+
created_at, updated_at, removed_at
|
|
7262
|
+
FROM identities
|
|
7263
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
7264
|
+
).get(id);
|
|
7265
|
+
if (row === void 0) {
|
|
7266
|
+
return null;
|
|
7267
|
+
}
|
|
7268
|
+
return row;
|
|
7269
|
+
}
|
|
7270
|
+
|
|
7271
|
+
// src/identities/receipts.ts
|
|
7272
|
+
function buildIdentityMutationReceipt(primary, partial) {
|
|
7273
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
7274
|
+
return {
|
|
7275
|
+
primary,
|
|
7276
|
+
created: normalized.created,
|
|
7277
|
+
updated: normalized.updated,
|
|
7278
|
+
archived: normalized.archived,
|
|
7279
|
+
removed: normalized.removed,
|
|
7280
|
+
affectedLinks: normalized.affectedLinks,
|
|
7281
|
+
derivedEffects: normalized.derivedEffects
|
|
7282
|
+
};
|
|
7283
|
+
}
|
|
7284
|
+
|
|
7285
|
+
// src/identities/validators.ts
|
|
7286
|
+
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
7287
|
+
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
7288
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7289
|
+
).get(normalizedKey) : db.prepare(
|
|
7290
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
7291
|
+
).get(normalizedKey, excludeIdentityId);
|
|
7292
|
+
if (row !== void 0) {
|
|
7293
|
+
const owner = db.prepare(
|
|
7294
|
+
"SELECT c.id, c.name FROM contacts c JOIN identities i ON i.contact_id = c.id WHERE i.normalized_key = ? AND i.removed_at IS NULL"
|
|
7295
|
+
).get(normalizedKey);
|
|
7296
|
+
throw new AffinityConflictError(
|
|
7297
|
+
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
7298
|
+
);
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
|
|
7302
|
+
// src/identities/validate_identity_input.ts
|
|
7303
|
+
var identityTypes = [
|
|
7304
|
+
"email",
|
|
7305
|
+
"phone",
|
|
7306
|
+
"url",
|
|
7307
|
+
"website",
|
|
7308
|
+
"handle",
|
|
7309
|
+
"x",
|
|
7310
|
+
"twitter",
|
|
7311
|
+
"linkedin",
|
|
7312
|
+
"github",
|
|
7313
|
+
"mastodon",
|
|
7314
|
+
"bluesky",
|
|
7315
|
+
"telegram",
|
|
7316
|
+
"whatsapp",
|
|
7317
|
+
"signal",
|
|
7318
|
+
"matrix",
|
|
7319
|
+
"discord",
|
|
7320
|
+
"slack",
|
|
7321
|
+
"instagram",
|
|
7322
|
+
"facebook",
|
|
7323
|
+
"youtube",
|
|
7324
|
+
"tiktok",
|
|
7325
|
+
"address",
|
|
7326
|
+
"other",
|
|
7327
|
+
"account_id",
|
|
7328
|
+
"alias"
|
|
7329
|
+
];
|
|
7330
|
+
function validateIdentityInput(type, value) {
|
|
7331
|
+
const kind = type.toLowerCase();
|
|
7332
|
+
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
7333
|
+
throw new AffinityValidationError(
|
|
7334
|
+
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
7335
|
+
);
|
|
7336
|
+
if (!value.trim())
|
|
7337
|
+
throw new AffinityValidationError("identity value must be non-empty");
|
|
7338
|
+
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
7339
|
+
throw new AffinityValidationError(
|
|
7340
|
+
"email value must match local@domain.tld"
|
|
7341
|
+
);
|
|
7342
|
+
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
7343
|
+
throw new AffinityValidationError(
|
|
7344
|
+
"phone value must contain at least 5 digits"
|
|
7345
|
+
);
|
|
7346
|
+
if (kind === "url" || kind === "website") {
|
|
7347
|
+
try {
|
|
7348
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
7349
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
7350
|
+
throw new Error();
|
|
7351
|
+
} catch {
|
|
7352
|
+
throw new AffinityValidationError(
|
|
7353
|
+
"url/website value must be an http/https URL or bare domain"
|
|
7354
|
+
);
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
// src/identities/add_identity.ts
|
|
7360
|
+
function addIdentity(db, contactId, input) {
|
|
7361
|
+
return withTransaction(db, () => {
|
|
7362
|
+
const contact = getContactRowById(db, contactId);
|
|
7363
|
+
if (!contact) {
|
|
7364
|
+
throw new AffinityNotFoundError("contact not found");
|
|
7365
|
+
}
|
|
7366
|
+
if (contact.lifecycle_state === "merged") {
|
|
7367
|
+
throw new AffinityStateError("merged contact is read-only");
|
|
7368
|
+
}
|
|
7369
|
+
const type = input.type.trim();
|
|
7370
|
+
const value = input.value.trim();
|
|
7371
|
+
if (type.length === 0 || value.length === 0) {
|
|
7372
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
7373
|
+
}
|
|
7374
|
+
validateIdentityInput(type, value);
|
|
7375
|
+
const nk = normalizeIdentityKey(type, value);
|
|
7376
|
+
assertNoIdentityCollision(db, nk);
|
|
7377
|
+
const now = resolveNow(input.now);
|
|
7378
|
+
const verified = input.verified ? 1 : 0;
|
|
7379
|
+
const verifiedAt = verified === 1 ? now : null;
|
|
7380
|
+
const result = db.prepare(
|
|
7381
|
+
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
7382
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7383
|
+
).run(
|
|
7384
|
+
contactId,
|
|
7385
|
+
type,
|
|
7386
|
+
value,
|
|
7387
|
+
input.label ?? null,
|
|
7388
|
+
nk,
|
|
7389
|
+
verified,
|
|
7390
|
+
verifiedAt,
|
|
7391
|
+
now,
|
|
7392
|
+
now
|
|
7393
|
+
);
|
|
7394
|
+
const id = Number(result.lastInsertRowid);
|
|
7395
|
+
const row = getIdentityRowById(db, id);
|
|
7396
|
+
if (!row) {
|
|
7397
|
+
throw new AffinityInvariantError("inserted identity not found");
|
|
7398
|
+
}
|
|
7399
|
+
refreshContactRollup(db, contactId, now);
|
|
7400
|
+
db.prepare(
|
|
7401
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7402
|
+
).run(contactId, contactId);
|
|
7403
|
+
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
7404
|
+
created: [{ kind: "identity", id }]
|
|
7405
|
+
});
|
|
7406
|
+
});
|
|
7407
|
+
}
|
|
7408
|
+
|
|
7409
|
+
// src/tools/create_contact_tool_result.ts
|
|
7410
|
+
function createContactToolResult(db, input) {
|
|
7411
|
+
return withTransaction(db, () => {
|
|
7412
|
+
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
|
+
const result = mutationToolResult("create", receipt, "contact");
|
|
7428
|
+
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
7429
|
+
const warnings = db.prepare(
|
|
7430
|
+
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
7431
|
+
).all(receipt.primary.id).flatMap((row) => {
|
|
7432
|
+
const score = identityOwners.has(row.id) ? 1 : fold(row.name) === fold(input.name) ? 0.92 : void 0;
|
|
7433
|
+
return score === void 0 ? [] : [
|
|
7434
|
+
{
|
|
7435
|
+
code: "possible_duplicate",
|
|
7436
|
+
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
7437
|
+
contactId: row.id,
|
|
7438
|
+
score
|
|
7439
|
+
}
|
|
7440
|
+
];
|
|
7441
|
+
});
|
|
7442
|
+
if (warnings.length) result.warnings = warnings;
|
|
7443
|
+
return result;
|
|
7444
|
+
});
|
|
7445
|
+
}
|
|
6779
7446
|
|
|
6780
7447
|
// src/tools/manage_contact_tool.ts
|
|
6781
7448
|
function contactLocatorSchema3(description) {
|
|
@@ -6795,24 +7462,76 @@ function contactLocatorSchema3(description) {
|
|
|
6795
7462
|
description
|
|
6796
7463
|
};
|
|
6797
7464
|
}
|
|
6798
|
-
function manageContactToolHandler(db,
|
|
7465
|
+
function manageContactToolHandler(db, rawInput) {
|
|
7466
|
+
const input = structuredClone(rawInput);
|
|
7467
|
+
const inputError = validateToolInput(manageContactTool.inputSchema, input);
|
|
7468
|
+
if (inputError)
|
|
7469
|
+
return toolFailure(
|
|
7470
|
+
"protocol",
|
|
7471
|
+
"invalid_input",
|
|
7472
|
+
"Invalid input.",
|
|
7473
|
+
inputError
|
|
7474
|
+
);
|
|
6799
7475
|
return withToolHandling(() => {
|
|
6800
|
-
if (input.action === "
|
|
6801
|
-
|
|
6802
|
-
"
|
|
6803
|
-
|
|
6804
|
-
|
|
7476
|
+
if (input.action === "export") {
|
|
7477
|
+
if (!input.contact)
|
|
7478
|
+
return toolSuccess("Exported Affinity.", {
|
|
7479
|
+
action: "export",
|
|
7480
|
+
export: exportAffinity(db)
|
|
7481
|
+
});
|
|
7482
|
+
const target = resolveContactLocator(db, input.contact, "contact", true);
|
|
7483
|
+
if (!target.ok) return target.result;
|
|
7484
|
+
return toolSuccess("Exported contact.", {
|
|
7485
|
+
action: "export",
|
|
7486
|
+
export: exportContact(db, target.value.id)
|
|
7487
|
+
});
|
|
7488
|
+
}
|
|
7489
|
+
if (input.action === "delete" && input.confirm !== true)
|
|
7490
|
+
return toolFailure(
|
|
7491
|
+
"protocol",
|
|
7492
|
+
"invalid_input",
|
|
7493
|
+
"Delete requires confirmation.",
|
|
7494
|
+
"delete is permanent; pass confirm: true (CLI: --confirm)"
|
|
6805
7495
|
);
|
|
7496
|
+
if (input.action === "create") {
|
|
7497
|
+
if (!input.input)
|
|
7498
|
+
return toolFailure(
|
|
7499
|
+
"protocol",
|
|
7500
|
+
"invalid_input",
|
|
7501
|
+
"Missing input.",
|
|
7502
|
+
"input is required for create."
|
|
7503
|
+
);
|
|
7504
|
+
return createContactToolResult(db, input.input);
|
|
6806
7505
|
}
|
|
6807
|
-
const contact = resolveContactLocator(
|
|
7506
|
+
const contact = resolveContactLocator(
|
|
7507
|
+
db,
|
|
7508
|
+
input.contact,
|
|
7509
|
+
"contact",
|
|
7510
|
+
input.action === "delete"
|
|
7511
|
+
);
|
|
6808
7512
|
if (!contact.ok) {
|
|
6809
7513
|
return contact.result;
|
|
6810
7514
|
}
|
|
7515
|
+
if (input.action === "delete") {
|
|
7516
|
+
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
7517
|
+
return toolSuccess("Permanently deleted contact.", {
|
|
7518
|
+
action: "delete",
|
|
7519
|
+
deletion,
|
|
7520
|
+
changes: deletion.removed.flatMap(
|
|
7521
|
+
(item) => item.ids.map((id) => ({
|
|
7522
|
+
entity: item.entity,
|
|
7523
|
+
id,
|
|
7524
|
+
action: "removed"
|
|
7525
|
+
}))
|
|
7526
|
+
)
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
6811
7529
|
if (input.action === "revise") {
|
|
6812
7530
|
return mutationToolResult(
|
|
6813
7531
|
"revise",
|
|
6814
7532
|
reviseContact(db, contact.value.id, input.patch),
|
|
6815
|
-
"contact"
|
|
7533
|
+
"contact",
|
|
7534
|
+
Object.keys(input.patch)
|
|
6816
7535
|
);
|
|
6817
7536
|
}
|
|
6818
7537
|
return mutationToolResult(
|
|
@@ -6829,7 +7548,7 @@ function manageContactToolHandler(db, input) {
|
|
|
6829
7548
|
}
|
|
6830
7549
|
var manageContactTool = defineAffinityTool({
|
|
6831
7550
|
name: manageContactToolName,
|
|
6832
|
-
description: "Create a contact
|
|
7551
|
+
description: "Create, revise, export or permanently delete a contact (confirm: true required; owner cannot be deleted). Export without contact exports the whole database. Lifecycle lost is soft delete. ALWAYS search_affinity first before creating \u2014 duplicates corrupt the graph.",
|
|
6833
7552
|
whenToUse: "Use this for contact creation and lifecycle-safe contact maintenance.",
|
|
6834
7553
|
whenNotToUse: "Do not use this for identities, links, attributes, or merges.",
|
|
6835
7554
|
sideEffects: "writes_state",
|
|
@@ -6850,10 +7569,25 @@ var manageContactTool = defineAffinityTool({
|
|
|
6850
7569
|
action: enumSchema("Operation to perform.", [
|
|
6851
7570
|
"create",
|
|
6852
7571
|
"revise",
|
|
6853
|
-
"set_lifecycle"
|
|
7572
|
+
"set_lifecycle",
|
|
7573
|
+
"export",
|
|
7574
|
+
"delete"
|
|
6854
7575
|
]),
|
|
6855
7576
|
input: objectSchema(
|
|
6856
7577
|
{
|
|
7578
|
+
identities: arraySchema(
|
|
7579
|
+
objectSchema(
|
|
7580
|
+
{
|
|
7581
|
+
type: stringSchema("Identity type."),
|
|
7582
|
+
value: stringSchema("Identity value."),
|
|
7583
|
+
label: stringSchema("Optional label."),
|
|
7584
|
+
verified: booleanSchema("Verified identity."),
|
|
7585
|
+
now: integerSchema("Optional timestamp.")
|
|
7586
|
+
},
|
|
7587
|
+
["type", "value"]
|
|
7588
|
+
),
|
|
7589
|
+
"Optional identities to attach. Identities already owned by another contact are left with that owner and reported as possible duplicates."
|
|
7590
|
+
),
|
|
6857
7591
|
name: stringSchema("Contact name."),
|
|
6858
7592
|
kind: enumSchema("Contact kind.", [
|
|
6859
7593
|
"human",
|
|
@@ -6872,6 +7606,7 @@ var manageContactTool = defineAffinityTool({
|
|
|
6872
7606
|
["name", "kind"],
|
|
6873
7607
|
"New contact payload. Required when action=create."
|
|
6874
7608
|
),
|
|
7609
|
+
confirm: booleanSchema("Required true for permanent deletion."),
|
|
6875
7610
|
contact: contactLocatorSchema3(
|
|
6876
7611
|
"Target contact. Required when action=revise or set_lifecycle. Provide contactId or identity."
|
|
6877
7612
|
),
|
|
@@ -6918,7 +7653,19 @@ function contactLocatorSchema4(description) {
|
|
|
6918
7653
|
description
|
|
6919
7654
|
};
|
|
6920
7655
|
}
|
|
6921
|
-
function manageDuplicateDismissalToolHandler(db,
|
|
7656
|
+
function manageDuplicateDismissalToolHandler(db, rawInput) {
|
|
7657
|
+
const input = structuredClone(rawInput);
|
|
7658
|
+
const inputError = validateToolInput(
|
|
7659
|
+
manageDuplicateDismissalTool.inputSchema,
|
|
7660
|
+
input
|
|
7661
|
+
);
|
|
7662
|
+
if (inputError)
|
|
7663
|
+
return toolFailure(
|
|
7664
|
+
"protocol",
|
|
7665
|
+
"invalid_input",
|
|
7666
|
+
"Invalid input.",
|
|
7667
|
+
inputError
|
|
7668
|
+
);
|
|
6922
7669
|
return withToolHandling(() => {
|
|
6923
7670
|
const left = resolveContactLocator(db, input.left, "left");
|
|
6924
7671
|
if (!left.ok) {
|
|
@@ -6928,6 +7675,11 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
6928
7675
|
if (!right.ok) {
|
|
6929
7676
|
return right.result;
|
|
6930
7677
|
}
|
|
7678
|
+
const leftContactId = Math.min(left.value.id, right.value.id);
|
|
7679
|
+
const rightContactId = Math.max(left.value.id, right.value.id);
|
|
7680
|
+
const existed = db.prepare(
|
|
7681
|
+
"SELECT 1 FROM dismissed_duplicates WHERE left_id = ? AND right_id = ?"
|
|
7682
|
+
).get(leftContactId, rightContactId) !== void 0;
|
|
6931
7683
|
if (input.action === "dismiss") {
|
|
6932
7684
|
const receipt2 = dismissDuplicate(
|
|
6933
7685
|
db,
|
|
@@ -6937,19 +7689,41 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
6937
7689
|
);
|
|
6938
7690
|
return toolSuccess(
|
|
6939
7691
|
`Dismissed pair #${receipt2.primary.leftContactId} / #${receipt2.primary.rightContactId}.`,
|
|
6940
|
-
{
|
|
7692
|
+
{
|
|
7693
|
+
...receipt2,
|
|
7694
|
+
action: "dismiss",
|
|
7695
|
+
changes: [
|
|
7696
|
+
{
|
|
7697
|
+
entity: "duplicate_dismissal",
|
|
7698
|
+
leftContactId,
|
|
7699
|
+
rightContactId,
|
|
7700
|
+
action: existed ? "updated" : "created"
|
|
7701
|
+
}
|
|
7702
|
+
]
|
|
7703
|
+
}
|
|
6941
7704
|
);
|
|
6942
7705
|
}
|
|
6943
7706
|
const receipt = undismissDuplicate(db, left.value.id, right.value.id);
|
|
6944
7707
|
return toolSuccess(
|
|
6945
7708
|
`Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
|
|
6946
|
-
{
|
|
7709
|
+
{
|
|
7710
|
+
...receipt,
|
|
7711
|
+
action: "undismiss",
|
|
7712
|
+
changes: existed ? [
|
|
7713
|
+
{
|
|
7714
|
+
entity: "duplicate_dismissal",
|
|
7715
|
+
leftContactId,
|
|
7716
|
+
rightContactId,
|
|
7717
|
+
action: "removed"
|
|
7718
|
+
}
|
|
7719
|
+
] : []
|
|
7720
|
+
}
|
|
6947
7721
|
);
|
|
6948
7722
|
}, "Duplicate dismissal failed.");
|
|
6949
7723
|
}
|
|
6950
7724
|
var manageDuplicateDismissalTool = defineAffinityTool({
|
|
6951
7725
|
name: manageDuplicateDismissalToolName,
|
|
6952
|
-
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in
|
|
7726
|
+
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.",
|
|
6953
7727
|
whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
|
|
6954
7728
|
whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
|
|
6955
7729
|
sideEffects: "writes_state",
|
|
@@ -7035,9 +7809,32 @@ function resolveDateAnchorTarget(db, target) {
|
|
|
7035
7809
|
}
|
|
7036
7810
|
return { kind: "ok", target: { kind: "link", linkId: link.value.id } };
|
|
7037
7811
|
}
|
|
7038
|
-
function manageDateAnchorToolHandler(db,
|
|
7812
|
+
function manageDateAnchorToolHandler(db, rawInput) {
|
|
7813
|
+
const input = structuredClone(rawInput);
|
|
7814
|
+
const inputError = validateToolInput(manageDateAnchorTool.inputSchema, input);
|
|
7815
|
+
if (inputError)
|
|
7816
|
+
return toolFailure(
|
|
7817
|
+
"protocol",
|
|
7818
|
+
"invalid_input",
|
|
7819
|
+
"Invalid input.",
|
|
7820
|
+
inputError
|
|
7821
|
+
);
|
|
7822
|
+
if (input.action !== "add" && input.anchorEventId === void 0)
|
|
7823
|
+
return toolFailure(
|
|
7824
|
+
"protocol",
|
|
7825
|
+
"invalid_input",
|
|
7826
|
+
"Missing anchorEventId.",
|
|
7827
|
+
"anchorEventId is required."
|
|
7828
|
+
);
|
|
7039
7829
|
return withToolHandling(() => {
|
|
7040
7830
|
if (input.action === "add") {
|
|
7831
|
+
if (!input.input)
|
|
7832
|
+
return toolFailure(
|
|
7833
|
+
"protocol",
|
|
7834
|
+
"invalid_input",
|
|
7835
|
+
"Missing input.",
|
|
7836
|
+
"input is required for add."
|
|
7837
|
+
);
|
|
7041
7838
|
const resolved = resolveDateAnchorTarget(db, input.input.target);
|
|
7042
7839
|
if (resolved.kind === "error") {
|
|
7043
7840
|
return resolved.result;
|
|
@@ -7052,7 +7849,8 @@ function manageDateAnchorToolHandler(db, input) {
|
|
|
7052
7849
|
return mutationToolResult(
|
|
7053
7850
|
"revise",
|
|
7054
7851
|
reviseDateAnchor(db, input.anchorEventId, input.patch, input.options),
|
|
7055
|
-
"date anchor"
|
|
7852
|
+
"date anchor",
|
|
7853
|
+
Object.keys(input.patch)
|
|
7056
7854
|
);
|
|
7057
7855
|
}
|
|
7058
7856
|
return mutationToolResult(
|
|
@@ -7160,97 +7958,6 @@ var manageDateAnchorTool = defineAffinityTool({
|
|
|
7160
7958
|
handler: manageDateAnchorToolHandler
|
|
7161
7959
|
});
|
|
7162
7960
|
|
|
7163
|
-
// src/identities/queries.ts
|
|
7164
|
-
function getIdentityRowById(db, id) {
|
|
7165
|
-
const row = db.prepare(
|
|
7166
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
7167
|
-
created_at, updated_at, removed_at
|
|
7168
|
-
FROM identities
|
|
7169
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
7170
|
-
).get(id);
|
|
7171
|
-
if (row === void 0) {
|
|
7172
|
-
return null;
|
|
7173
|
-
}
|
|
7174
|
-
return row;
|
|
7175
|
-
}
|
|
7176
|
-
|
|
7177
|
-
// src/identities/receipts.ts
|
|
7178
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
7179
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
7180
|
-
return {
|
|
7181
|
-
primary,
|
|
7182
|
-
created: normalized.created,
|
|
7183
|
-
updated: normalized.updated,
|
|
7184
|
-
archived: normalized.archived,
|
|
7185
|
-
removed: normalized.removed,
|
|
7186
|
-
affectedLinks: normalized.affectedLinks,
|
|
7187
|
-
derivedEffects: normalized.derivedEffects
|
|
7188
|
-
};
|
|
7189
|
-
}
|
|
7190
|
-
|
|
7191
|
-
// src/identities/validators.ts
|
|
7192
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
7193
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
7194
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7195
|
-
).get(normalizedKey) : db.prepare(
|
|
7196
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
7197
|
-
).get(normalizedKey, excludeIdentityId);
|
|
7198
|
-
if (row !== void 0) {
|
|
7199
|
-
throw new AffinityConflictError(
|
|
7200
|
-
"another contact already owns this normalized identity"
|
|
7201
|
-
);
|
|
7202
|
-
}
|
|
7203
|
-
}
|
|
7204
|
-
|
|
7205
|
-
// src/identities/add_identity.ts
|
|
7206
|
-
function addIdentity(db, contactId, input) {
|
|
7207
|
-
return withTransaction(db, () => {
|
|
7208
|
-
const contact = getContactRowById(db, contactId);
|
|
7209
|
-
if (!contact) {
|
|
7210
|
-
throw new AffinityNotFoundError("contact not found");
|
|
7211
|
-
}
|
|
7212
|
-
if (contact.lifecycle_state === "merged") {
|
|
7213
|
-
throw new AffinityStateError("merged contact is read-only");
|
|
7214
|
-
}
|
|
7215
|
-
const type = input.type.trim();
|
|
7216
|
-
const value = input.value.trim();
|
|
7217
|
-
if (type.length === 0 || value.length === 0) {
|
|
7218
|
-
throw new AffinityValidationError("type and value must be non-empty");
|
|
7219
|
-
}
|
|
7220
|
-
const nk = normalizeIdentityKey(type, value);
|
|
7221
|
-
assertNoIdentityCollision(db, nk);
|
|
7222
|
-
const now = resolveNow(input.now);
|
|
7223
|
-
const verified = input.verified ? 1 : 0;
|
|
7224
|
-
const verifiedAt = verified === 1 ? now : null;
|
|
7225
|
-
const result = db.prepare(
|
|
7226
|
-
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
7227
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7228
|
-
).run(
|
|
7229
|
-
contactId,
|
|
7230
|
-
type,
|
|
7231
|
-
value,
|
|
7232
|
-
input.label ?? null,
|
|
7233
|
-
nk,
|
|
7234
|
-
verified,
|
|
7235
|
-
verifiedAt,
|
|
7236
|
-
now,
|
|
7237
|
-
now
|
|
7238
|
-
);
|
|
7239
|
-
const id = Number(result.lastInsertRowid);
|
|
7240
|
-
const row = getIdentityRowById(db, id);
|
|
7241
|
-
if (!row) {
|
|
7242
|
-
throw new AffinityInvariantError("inserted identity not found");
|
|
7243
|
-
}
|
|
7244
|
-
refreshContactRollup(db, contactId, now);
|
|
7245
|
-
db.prepare(
|
|
7246
|
-
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7247
|
-
).run(contactId, contactId);
|
|
7248
|
-
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
7249
|
-
created: [{ kind: "identity", id }]
|
|
7250
|
-
});
|
|
7251
|
-
});
|
|
7252
|
-
}
|
|
7253
|
-
|
|
7254
7961
|
// src/identities/remove_identity.ts
|
|
7255
7962
|
function removeIdentity(db, identityId, removedAt) {
|
|
7256
7963
|
return withTransaction(db, () => {
|
|
@@ -7302,6 +8009,7 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
7302
8009
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
7303
8010
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
7304
8011
|
}
|
|
8012
|
+
validateIdentityInput(nextType, nextValue);
|
|
7305
8013
|
const nextLabel = patch.label !== void 0 ? patch.label : row.label;
|
|
7306
8014
|
const nkNext = normalizeIdentityKey(nextType, nextValue);
|
|
7307
8015
|
if (nkNext !== row.normalized_key) {
|
|
@@ -7356,7 +8064,9 @@ function contactLocatorSchema6(description) {
|
|
|
7356
8064
|
contactId: integerSchema("Exact contact id."),
|
|
7357
8065
|
identity: objectSchema(
|
|
7358
8066
|
{
|
|
7359
|
-
type: stringSchema(
|
|
8067
|
+
type: stringSchema(
|
|
8068
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8069
|
+
),
|
|
7360
8070
|
value: stringSchema("Identity value.")
|
|
7361
8071
|
},
|
|
7362
8072
|
["type", "value"],
|
|
@@ -7366,7 +8076,23 @@ function contactLocatorSchema6(description) {
|
|
|
7366
8076
|
description
|
|
7367
8077
|
};
|
|
7368
8078
|
}
|
|
7369
|
-
function manageIdentityToolHandler(db,
|
|
8079
|
+
function manageIdentityToolHandler(db, rawInput) {
|
|
8080
|
+
const input = structuredClone(rawInput);
|
|
8081
|
+
const inputError = validateToolInput(manageIdentityTool.inputSchema, input);
|
|
8082
|
+
if (inputError)
|
|
8083
|
+
return toolFailure(
|
|
8084
|
+
"protocol",
|
|
8085
|
+
"invalid_input",
|
|
8086
|
+
"Invalid input.",
|
|
8087
|
+
inputError
|
|
8088
|
+
);
|
|
8089
|
+
if (input.action !== "add" && input.identityId === void 0)
|
|
8090
|
+
return toolFailure(
|
|
8091
|
+
"protocol",
|
|
8092
|
+
"invalid_input",
|
|
8093
|
+
"Missing identityId.",
|
|
8094
|
+
"identityId is required."
|
|
8095
|
+
);
|
|
7370
8096
|
return withToolHandling(() => {
|
|
7371
8097
|
switch (input.action) {
|
|
7372
8098
|
case "add": {
|
|
@@ -7384,7 +8110,8 @@ function manageIdentityToolHandler(db, input) {
|
|
|
7384
8110
|
return mutationToolResult(
|
|
7385
8111
|
"revise",
|
|
7386
8112
|
reviseIdentity(db, input.identityId, input.patch),
|
|
7387
|
-
"identity"
|
|
8113
|
+
"identity",
|
|
8114
|
+
Object.keys(input.patch)
|
|
7388
8115
|
);
|
|
7389
8116
|
case "verify":
|
|
7390
8117
|
return mutationToolResult(
|
|
@@ -7433,7 +8160,9 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
7433
8160
|
),
|
|
7434
8161
|
input: objectSchema(
|
|
7435
8162
|
{
|
|
7436
|
-
type: stringSchema(
|
|
8163
|
+
type: stringSchema(
|
|
8164
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8165
|
+
),
|
|
7437
8166
|
value: stringSchema("Identity value."),
|
|
7438
8167
|
label: stringSchema("Optional label."),
|
|
7439
8168
|
verified: booleanSchema("Whether the identity starts verified."),
|
|
@@ -7447,7 +8176,9 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
7447
8176
|
),
|
|
7448
8177
|
patch: objectSchema(
|
|
7449
8178
|
{
|
|
7450
|
-
type: stringSchema(
|
|
8179
|
+
type: stringSchema(
|
|
8180
|
+
`Optional identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8181
|
+
),
|
|
7451
8182
|
value: stringSchema("Optional identity value."),
|
|
7452
8183
|
label: stringSchema("Optional label.")
|
|
7453
8184
|
},
|
|
@@ -7505,7 +8236,19 @@ function linkLocatorSchema4(description) {
|
|
|
7505
8236
|
description
|
|
7506
8237
|
};
|
|
7507
8238
|
}
|
|
7508
|
-
function manageRelationshipToolHandler(db,
|
|
8239
|
+
function manageRelationshipToolHandler(db, rawInput) {
|
|
8240
|
+
const input = structuredClone(rawInput);
|
|
8241
|
+
const inputError = validateToolInput(
|
|
8242
|
+
manageRelationshipTool.inputSchema,
|
|
8243
|
+
input
|
|
8244
|
+
);
|
|
8245
|
+
if (inputError)
|
|
8246
|
+
return toolFailure(
|
|
8247
|
+
"protocol",
|
|
8248
|
+
"invalid_input",
|
|
8249
|
+
"Invalid input.",
|
|
8250
|
+
inputError
|
|
8251
|
+
);
|
|
7509
8252
|
return withToolHandling(() => {
|
|
7510
8253
|
switch (input.action) {
|
|
7511
8254
|
case "seed_social_link": {
|
|
@@ -7686,7 +8429,16 @@ function contactLocatorSchema8(description) {
|
|
|
7686
8429
|
description
|
|
7687
8430
|
};
|
|
7688
8431
|
}
|
|
7689
|
-
function mergeContactsToolHandler(db,
|
|
8432
|
+
function mergeContactsToolHandler(db, rawInput) {
|
|
8433
|
+
const input = structuredClone(rawInput);
|
|
8434
|
+
const inputError = validateToolInput(mergeContactsTool.inputSchema, input);
|
|
8435
|
+
if (inputError)
|
|
8436
|
+
return toolFailure(
|
|
8437
|
+
"protocol",
|
|
8438
|
+
"invalid_input",
|
|
8439
|
+
"Invalid input.",
|
|
8440
|
+
inputError
|
|
8441
|
+
);
|
|
7690
8442
|
return withToolHandling(() => {
|
|
7691
8443
|
const winner = resolveContactLocator(db, input.winner, "winner");
|
|
7692
8444
|
if (!winner.ok) {
|
|
@@ -7767,7 +8519,16 @@ function prepareParticipants2(db, raw, injectOwner) {
|
|
|
7767
8519
|
const normalized = raw.map(normalizeParticipant2);
|
|
7768
8520
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
7769
8521
|
}
|
|
7770
|
-
function recordEventToolHandler(db,
|
|
8522
|
+
function recordEventToolHandler(db, rawInput) {
|
|
8523
|
+
const input = structuredClone(rawInput);
|
|
8524
|
+
const inputError = validateToolInput(recordEventTool.inputSchema, input);
|
|
8525
|
+
if (inputError)
|
|
8526
|
+
return toolFailure(
|
|
8527
|
+
"protocol",
|
|
8528
|
+
"invalid_input",
|
|
8529
|
+
"Invalid input.",
|
|
8530
|
+
inputError
|
|
8531
|
+
);
|
|
7771
8532
|
return withToolHandling(() => {
|
|
7772
8533
|
const patched = {
|
|
7773
8534
|
...input.input,
|
|
@@ -7865,7 +8626,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
7865
8626
|
"observed"
|
|
7866
8627
|
])
|
|
7867
8628
|
},
|
|
7868
|
-
["
|
|
8629
|
+
["role"]
|
|
7869
8630
|
),
|
|
7870
8631
|
"Participant list. Must be non-empty and include the owner contact."
|
|
7871
8632
|
),
|
|
@@ -7881,6 +8642,56 @@ var recordEventTool = defineAffinityTool({
|
|
|
7881
8642
|
handler: recordEventToolHandler
|
|
7882
8643
|
});
|
|
7883
8644
|
|
|
8645
|
+
// src/tools/enrich_review_item.ts
|
|
8646
|
+
function enrichReviewItem(db, item) {
|
|
8647
|
+
const name = (id) => {
|
|
8648
|
+
if (typeof id !== "number") return "";
|
|
8649
|
+
return db.prepare("SELECT name FROM contacts WHERE id = ?").get(id)?.name ?? `Contact #${id}`;
|
|
8650
|
+
};
|
|
8651
|
+
const result = { ...item };
|
|
8652
|
+
if (item.kind === "link") {
|
|
8653
|
+
const link = db.prepare(`SELECT l.*, a.name AS from_name, b.name AS to_name, a.is_owner AS from_owner, b.is_owner AS to_owner,
|
|
8654
|
+
lr.recency_score, lr.drift_priority, lr.readiness_score, lr.normalized_rank, lr.reciprocity_score, lr.radar_score FROM links l JOIN contacts a ON a.id = l.from_contact_id JOIN contacts b ON b.id = l.to_contact_id LEFT JOIN link_rollups lr ON lr.link_id = l.id WHERE l.id = ?`).get(item.id);
|
|
8655
|
+
if (link) {
|
|
8656
|
+
result.title = link.from_owner ? String(link.to_name) : link.to_owner ? String(link.from_name) : `${link.from_name} \xB7 ${link.to_name}`;
|
|
8657
|
+
result.subtitle = [link.kind, link.role].filter(Boolean).join(" \xB7 ");
|
|
8658
|
+
for (const key of ["affinity", "trust", "rank"])
|
|
8659
|
+
if (typeof link[key] === "number") result[key] = link[key];
|
|
8660
|
+
result.recencyScore ??= link.recency_score;
|
|
8661
|
+
result.driftPriority ??= link.drift_priority;
|
|
8662
|
+
result.readinessScore = link.readiness_score;
|
|
8663
|
+
result.normalizedRank ??= link.normalized_rank;
|
|
8664
|
+
result.reciprocityScore = link.reciprocity_score;
|
|
8665
|
+
result.radarScore = link.radar_score;
|
|
8666
|
+
}
|
|
8667
|
+
}
|
|
8668
|
+
if (item.kind === "event" && typeof item.linkId === "number" && !Array.isArray(item.participants)) {
|
|
8669
|
+
const link = db.prepare("SELECT from_contact_id, to_contact_id FROM links WHERE id = ?").get(item.linkId);
|
|
8670
|
+
if (link)
|
|
8671
|
+
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8672
|
+
}
|
|
8673
|
+
if (Array.isArray(item.participants)) {
|
|
8674
|
+
result.counterpartyNames = item.participants.map(
|
|
8675
|
+
(p) => name(p.contactId)
|
|
8676
|
+
);
|
|
8677
|
+
result.subtitle = [
|
|
8678
|
+
item.subtitle,
|
|
8679
|
+
result.counterpartyNames.join(", ")
|
|
8680
|
+
].filter(Boolean).join(" \xB7 ");
|
|
8681
|
+
}
|
|
8682
|
+
if (typeof item.leftContactId === "number")
|
|
8683
|
+
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
8684
|
+
if (typeof item.winnerContactId === "number")
|
|
8685
|
+
result.title = `Merged ${name(item.loserContactId)} into ${name(item.winnerContactId)}`;
|
|
8686
|
+
if (item.occursAt !== void 0 && item.kind === "contact")
|
|
8687
|
+
result.title = `${name(item.id)} \xB7 ${item.title}`;
|
|
8688
|
+
for (const [key, value] of Object.entries(result)) {
|
|
8689
|
+
if (typeof value === "number" && /score|priority|affinity|trust|rank/i.test(key))
|
|
8690
|
+
result[key] = Math.round(value * 1e3) / 1e3;
|
|
8691
|
+
}
|
|
8692
|
+
return result;
|
|
8693
|
+
}
|
|
8694
|
+
|
|
7884
8695
|
// src/tools/tool_summary.ts
|
|
7885
8696
|
function summarizeCount(count, singular) {
|
|
7886
8697
|
if (count === 0) {
|
|
@@ -7895,6 +8706,7 @@ function summarizeCount(count, singular) {
|
|
|
7895
8706
|
// src/tools/review_affinity_tool.ts
|
|
7896
8707
|
function buildReadOptions(input) {
|
|
7897
8708
|
return {
|
|
8709
|
+
...input.now === void 0 ? {} : { now: input.now },
|
|
7898
8710
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7899
8711
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
7900
8712
|
...input.includeArchived === void 0 ? {} : { includeArchived: input.includeArchived },
|
|
@@ -8014,6 +8826,7 @@ function validateReviewInput(input) {
|
|
|
8014
8826
|
}
|
|
8015
8827
|
function duplicateCandidateItem(candidate) {
|
|
8016
8828
|
return {
|
|
8829
|
+
...candidate,
|
|
8017
8830
|
kind: "contact",
|
|
8018
8831
|
id: candidate.leftContactId,
|
|
8019
8832
|
title: `Possible duplicate: #${candidate.leftContactId} and #${candidate.rightContactId}`,
|
|
@@ -8023,24 +8836,28 @@ function duplicateCandidateItem(candidate) {
|
|
|
8023
8836
|
}
|
|
8024
8837
|
function momentItem(moment) {
|
|
8025
8838
|
return {
|
|
8839
|
+
...moment,
|
|
8840
|
+
occurredAtIso: new Date(moment.occurredAt).toISOString(),
|
|
8026
8841
|
kind: "event",
|
|
8027
8842
|
id: moment.eventId,
|
|
8028
8843
|
title: moment.summary,
|
|
8029
8844
|
subtitle: moment.momentKind,
|
|
8030
|
-
snippet:
|
|
8845
|
+
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8031
8846
|
};
|
|
8032
8847
|
}
|
|
8033
8848
|
function commitmentItem(commitment) {
|
|
8034
8849
|
return {
|
|
8850
|
+
...commitment,
|
|
8035
8851
|
kind: "event",
|
|
8036
8852
|
id: commitment.eventId,
|
|
8037
8853
|
title: commitment.summary,
|
|
8038
8854
|
subtitle: commitment.type,
|
|
8039
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt
|
|
8855
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8040
8856
|
};
|
|
8041
8857
|
}
|
|
8042
8858
|
function radarItem(radar) {
|
|
8043
8859
|
return {
|
|
8860
|
+
...radar,
|
|
8044
8861
|
kind: "link",
|
|
8045
8862
|
id: radar.linkId,
|
|
8046
8863
|
title: `Link #${radar.linkId}`,
|
|
@@ -8050,29 +8867,34 @@ function radarItem(radar) {
|
|
|
8050
8867
|
}
|
|
8051
8868
|
function upcomingDateItem(date) {
|
|
8052
8869
|
return {
|
|
8870
|
+
...date,
|
|
8871
|
+
occursAt: date.occursOn,
|
|
8872
|
+
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
8053
8873
|
kind: date.targetRef.kind,
|
|
8054
8874
|
id: date.targetRef.id,
|
|
8055
8875
|
title: date.summary,
|
|
8056
8876
|
subtitle: date.recurrenceKind,
|
|
8057
|
-
snippet: `occurs ${date.occursOn}`
|
|
8877
|
+
snippet: `occurs ${new Date(date.occursOn).toISOString().slice(0, 10)}`
|
|
8058
8878
|
};
|
|
8059
8879
|
}
|
|
8060
8880
|
function mergeHistoryItem(record) {
|
|
8061
8881
|
return {
|
|
8882
|
+
...record,
|
|
8062
8883
|
kind: "contact",
|
|
8063
8884
|
id: record.winnerContactId,
|
|
8064
8885
|
title: `Merged #${record.loserContactId} into #${record.winnerContactId}`,
|
|
8065
|
-
subtitle:
|
|
8886
|
+
subtitle: new Date(record.mergedAt).toISOString().slice(0, 10),
|
|
8066
8887
|
snippet: record.reasonSummary ?? void 0
|
|
8067
8888
|
};
|
|
8068
8889
|
}
|
|
8069
8890
|
function dismissedPairItem(record) {
|
|
8070
8891
|
return {
|
|
8892
|
+
...record,
|
|
8071
8893
|
kind: "contact",
|
|
8072
8894
|
id: record.leftContactId,
|
|
8073
8895
|
title: `Dismissed pair: #${record.leftContactId} and #${record.rightContactId}`,
|
|
8074
8896
|
subtitle: record.reason ?? "no reason given",
|
|
8075
|
-
snippet: `dismissed at ${record.dismissedAt}`
|
|
8897
|
+
snippet: `dismissed at ${new Date(record.dismissedAt).toISOString().slice(0, 10)}`
|
|
8076
8898
|
};
|
|
8077
8899
|
}
|
|
8078
8900
|
function compactFilters(input) {
|
|
@@ -8082,7 +8904,16 @@ function compactFilters(input) {
|
|
|
8082
8904
|
)
|
|
8083
8905
|
);
|
|
8084
8906
|
}
|
|
8085
|
-
function reviewAffinityToolHandler(db,
|
|
8907
|
+
function reviewAffinityToolHandler(db, rawInput) {
|
|
8908
|
+
const input = structuredClone(rawInput);
|
|
8909
|
+
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8910
|
+
if (inputError)
|
|
8911
|
+
return toolFailure(
|
|
8912
|
+
"protocol",
|
|
8913
|
+
"invalid_input",
|
|
8914
|
+
"Invalid input.",
|
|
8915
|
+
inputError
|
|
8916
|
+
);
|
|
8086
8917
|
return withToolHandling(() => {
|
|
8087
8918
|
const invalid = validateReviewInput(input);
|
|
8088
8919
|
if (invalid) {
|
|
@@ -8096,6 +8927,9 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8096
8927
|
items = listContacts(
|
|
8097
8928
|
db,
|
|
8098
8929
|
{
|
|
8930
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
8931
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
8932
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8099
8933
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8100
8934
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
8101
8935
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -8126,7 +8960,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8126
8960
|
"Provide `contact` when `view` is `events.contact_journal`."
|
|
8127
8961
|
);
|
|
8128
8962
|
}
|
|
8129
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
8963
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
8130
8964
|
if (!contact.ok) {
|
|
8131
8965
|
return contact.result;
|
|
8132
8966
|
}
|
|
@@ -8161,7 +8995,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8161
8995
|
let contactId;
|
|
8162
8996
|
let linkId;
|
|
8163
8997
|
if (input.contact !== void 0) {
|
|
8164
|
-
const contact = resolveContactLocator(
|
|
8998
|
+
const contact = resolveContactLocator(
|
|
8999
|
+
db,
|
|
9000
|
+
input.contact,
|
|
9001
|
+
"contact",
|
|
9002
|
+
true
|
|
9003
|
+
);
|
|
8165
9004
|
if (!contact.ok) {
|
|
8166
9005
|
return contact.result;
|
|
8167
9006
|
}
|
|
@@ -8190,7 +9029,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8190
9029
|
let contactId;
|
|
8191
9030
|
let linkId;
|
|
8192
9031
|
if (input.contact !== void 0) {
|
|
8193
|
-
const contact = resolveContactLocator(
|
|
9032
|
+
const contact = resolveContactLocator(
|
|
9033
|
+
db,
|
|
9034
|
+
input.contact,
|
|
9035
|
+
"contact",
|
|
9036
|
+
true
|
|
9037
|
+
);
|
|
8194
9038
|
if (!contact.ok) {
|
|
8195
9039
|
return contact.result;
|
|
8196
9040
|
}
|
|
@@ -8283,7 +9127,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8283
9127
|
"Provide `contact` when `view` is `merges.history`."
|
|
8284
9128
|
);
|
|
8285
9129
|
}
|
|
8286
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
9130
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
8287
9131
|
if (!contact.ok) {
|
|
8288
9132
|
return contact.result;
|
|
8289
9133
|
}
|
|
@@ -8296,6 +9140,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8296
9140
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
8297
9141
|
break;
|
|
8298
9142
|
}
|
|
9143
|
+
items = items.map((item) => enrichReviewItem(db, item));
|
|
8299
9144
|
return toolSuccess(
|
|
8300
9145
|
summarizeCount(items.length, "item"),
|
|
8301
9146
|
{
|
|
@@ -8328,12 +9173,16 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8328
9173
|
targetKinds: ["contact", "link", "event"],
|
|
8329
9174
|
inputDescriptions: {
|
|
8330
9175
|
view: "Which review surface to return.",
|
|
9176
|
+
now: "Optional current timestamp for overdue calculations.",
|
|
8331
9177
|
contact: "Target contact locator for views that require one.",
|
|
8332
9178
|
link: "Target link locator for views that require one.",
|
|
8333
9179
|
contactIds: "Optional contact id subset for duplicate review or graph review.",
|
|
8334
9180
|
kind: "Optional contact kind or link kind filter, depending on the view.",
|
|
8335
9181
|
lifecycleState: "Optional contact lifecycle filter for the contact list view.",
|
|
8336
9182
|
linkState: "Optional link state filter for link review views.",
|
|
9183
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
9184
|
+
tag: "Tag name; arrays require all tags.",
|
|
9185
|
+
includeLost: "Whether lost contacts are included.",
|
|
8337
9186
|
includeOwner: "Whether owner contacts should be included in contact review.",
|
|
8338
9187
|
includeDormant: "Whether dormant links or contacts should be included where supported.",
|
|
8339
9188
|
includeArchived: "Whether archived links should be included where supported.",
|
|
@@ -8370,6 +9219,9 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8370
9219
|
"merges.history",
|
|
8371
9220
|
"merges.dismissed"
|
|
8372
9221
|
]),
|
|
9222
|
+
now: integerSchema(
|
|
9223
|
+
"Optional current timestamp for overdue calculations."
|
|
9224
|
+
),
|
|
8373
9225
|
contact: contactLocatorSchema9("Optional target contact locator."),
|
|
8374
9226
|
link: linkLocatorSchema5("Optional target link locator."),
|
|
8375
9227
|
contactIds: arraySchema(
|
|
@@ -8392,6 +9244,17 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8392
9244
|
"broken",
|
|
8393
9245
|
"archived"
|
|
8394
9246
|
]),
|
|
9247
|
+
attribute: oneOfSchema([
|
|
9248
|
+
stringSchema("Attribute filter."),
|
|
9249
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9250
|
+
]),
|
|
9251
|
+
tag: oneOfSchema([
|
|
9252
|
+
stringSchema("Tag filter."),
|
|
9253
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9254
|
+
]),
|
|
9255
|
+
includeLost: booleanSchema(
|
|
9256
|
+
"Include lost contacts (search excludes them by default)."
|
|
9257
|
+
),
|
|
8395
9258
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8396
9259
|
includeDormant: booleanSchema("Whether dormant rows are included."),
|
|
8397
9260
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
@@ -8436,7 +9299,16 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8436
9299
|
});
|
|
8437
9300
|
|
|
8438
9301
|
// src/tools/search_affinity_tool.ts
|
|
8439
|
-
function searchAffinityToolHandler(db,
|
|
9302
|
+
function searchAffinityToolHandler(db, rawInput) {
|
|
9303
|
+
const input = structuredClone(rawInput);
|
|
9304
|
+
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9305
|
+
if (inputError)
|
|
9306
|
+
return toolFailure(
|
|
9307
|
+
"protocol",
|
|
9308
|
+
"invalid_input",
|
|
9309
|
+
"Invalid input.",
|
|
9310
|
+
inputError
|
|
9311
|
+
);
|
|
8440
9312
|
return withToolHandling(() => {
|
|
8441
9313
|
const options = {
|
|
8442
9314
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
@@ -8446,6 +9318,9 @@ function searchAffinityToolHandler(db, input) {
|
|
|
8446
9318
|
db,
|
|
8447
9319
|
input.query,
|
|
8448
9320
|
{
|
|
9321
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
9322
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
9323
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8449
9324
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8450
9325
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
8451
9326
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -8469,7 +9344,7 @@ function searchAffinityToolHandler(db, input) {
|
|
|
8469
9344
|
}
|
|
8470
9345
|
var searchAffinityTool = defineAffinityTool({
|
|
8471
9346
|
name: searchAffinityToolName,
|
|
8472
|
-
description: "Search contacts by name
|
|
9347
|
+
description: "Search contacts by name, identity, attributes, link roles, bonds and event text. All whitespace tokens must match. Dormant and lost contacts are excluded by default; use includeDormant and includeLost to include them.",
|
|
8473
9348
|
whenToUse: "Use this when you want to find contacts from free text or a known routing identity such as an email or URL-like handle.",
|
|
8474
9349
|
whenNotToUse: "Do not use this when you already know the exact contact you want to inspect in depth.",
|
|
8475
9350
|
sideEffects: "none",
|
|
@@ -8480,6 +9355,9 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
8480
9355
|
query: "Free-text or natural-key search text.",
|
|
8481
9356
|
kind: "Optional contact kind filter.",
|
|
8482
9357
|
lifecycleState: "Optional lifecycle state filter.",
|
|
9358
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
9359
|
+
tag: "Tag name; arrays require all tags.",
|
|
9360
|
+
includeLost: "Whether lost contacts are included in search.",
|
|
8483
9361
|
includeOwner: "Whether owner contacts are eligible for results.",
|
|
8484
9362
|
includeDormant: "Whether dormant contacts should be eligible for results.",
|
|
8485
9363
|
limit: "Optional result limit.",
|
|
@@ -8504,6 +9382,17 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
8504
9382
|
"merged",
|
|
8505
9383
|
"lost"
|
|
8506
9384
|
]),
|
|
9385
|
+
attribute: oneOfSchema([
|
|
9386
|
+
stringSchema("Attribute filter."),
|
|
9387
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9388
|
+
]),
|
|
9389
|
+
tag: oneOfSchema([
|
|
9390
|
+
stringSchema("Tag filter."),
|
|
9391
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9392
|
+
]),
|
|
9393
|
+
includeLost: booleanSchema(
|
|
9394
|
+
"Include lost contacts (search excludes them by default)."
|
|
9395
|
+
),
|
|
8507
9396
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8508
9397
|
includeDormant: booleanSchema("Whether dormant contacts are included."),
|
|
8509
9398
|
limit: integerSchema("Optional result limit."),
|
|
@@ -8539,6 +9428,9 @@ function getAffinityToolByName(name) {
|
|
|
8539
9428
|
|
|
8540
9429
|
// src/tools/tool_mapping.ts
|
|
8541
9430
|
var affinityToolMappings = [
|
|
9431
|
+
{ source: "exportContact", tool: manageContactToolName, action: "export" },
|
|
9432
|
+
{ source: "exportAffinity", tool: manageContactToolName, action: "export" },
|
|
9433
|
+
{ source: "deleteContact", tool: manageContactToolName, action: "delete" },
|
|
8542
9434
|
{ source: "searchContacts", tool: searchAffinityToolName },
|
|
8543
9435
|
{
|
|
8544
9436
|
source: "getOwnerProfile",
|
|
@@ -8725,6 +9617,7 @@ __export(write_exports, {
|
|
|
8725
9617
|
addDateAnchor: () => addDateAnchor,
|
|
8726
9618
|
addIdentity: () => addIdentity,
|
|
8727
9619
|
createContact: () => createContact,
|
|
9620
|
+
deleteContact: () => deleteContact,
|
|
8728
9621
|
dismissDuplicate: () => dismissDuplicate,
|
|
8729
9622
|
mergeContacts: () => mergeContacts,
|
|
8730
9623
|
overrideLinkState: () => overrideLinkState,
|