@ghostpaw/affinity 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -9
- package/SKILL.md +43 -386
- package/dist/cli.js +2881 -914
- package/dist/index.cjs +1957 -605
- package/dist/index.d.cts +340 -55
- package/dist/index.d.ts +340 -55
- package/dist/index.js +1957 -605
- package/dist/mcp-server.js +2162 -791
- package/dist/mcp.cjs +1962 -701
- package/dist/mcp.d.cts +3 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +1963 -702
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +43 -386
- package/skills/affinity/references/identify-and-locate-contacts.md +9 -0
- package/skills/affinity/references/import-history-without-faking-evidence.md +25 -0
- package/skills/affinity/references/manage-promises-and-agreements.md +8 -0
- package/skills/affinity/references/manage-recurring-dates-and-reminders.md +8 -0
- package/skills/affinity/references/record-direct-evidence-well.md +18 -0
- package/skills/affinity/references/record-observations-and-referrals.md +11 -0
- package/skills/affinity/references/review-radar-progression-and-graph.md +13 -0
- package/skills/affinity/references/tools.md +490 -0
package/dist/mcp.cjs
CHANGED
|
@@ -5,7 +5,7 @@ var node_path = require('node:path');
|
|
|
5
5
|
var node_os = require('node:os');
|
|
6
6
|
|
|
7
7
|
// src/version.ts
|
|
8
|
-
var affinityVersion = "0.
|
|
8
|
+
var affinityVersion = "0.6.1";
|
|
9
9
|
|
|
10
10
|
// src/surface/affinity_mcp_instructions.ts
|
|
11
11
|
var affinityMcpInstructions = "Affinity is a local relationship model and social CRM. Search before creating contacts. Inspect before revising or merging. Record direct evidence honestly, keep identities and structural ties distinct from social links, and ask for clarification when a target is ambiguous. Review commitments and recurring dates regularly. Derived scores guide attention and do not prove relationship quality.";
|
|
@@ -78,6 +78,158 @@ var AffinityNotFoundError = class extends AffinityError {
|
|
|
78
78
|
code = "NOT_FOUND";
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// src/lib/normalize_mutation_receipt.ts
|
|
82
|
+
function uniqueEntityRefs(refs) {
|
|
83
|
+
const out = [];
|
|
84
|
+
const seen = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const ref of refs ?? []) {
|
|
86
|
+
const key = `${ref.kind}:${ref.id}`;
|
|
87
|
+
if (seen.has(key)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
seen.add(key);
|
|
91
|
+
out.push(ref);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function uniqueDerivedEffects(effects) {
|
|
96
|
+
const out = [];
|
|
97
|
+
const seen = /* @__PURE__ */ new Set();
|
|
98
|
+
for (const effect of effects ?? []) {
|
|
99
|
+
const key = `${effect.eventId}:${effect.linkId}`;
|
|
100
|
+
if (seen.has(key)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
seen.add(key);
|
|
104
|
+
out.push(effect);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function uniqueAffectedLinks(affectedLinks, derivedEffects) {
|
|
109
|
+
const out = [];
|
|
110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const linkId of affectedLinks ?? []) {
|
|
112
|
+
if (seen.has(linkId)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
seen.add(linkId);
|
|
116
|
+
out.push(linkId);
|
|
117
|
+
}
|
|
118
|
+
for (const effect of derivedEffects) {
|
|
119
|
+
if (seen.has(effect.linkId)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
seen.add(effect.linkId);
|
|
123
|
+
out.push(effect.linkId);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function normalizeMutationReceiptPartial(partial) {
|
|
128
|
+
const derivedEffects = uniqueDerivedEffects(partial?.derivedEffects);
|
|
129
|
+
return {
|
|
130
|
+
created: uniqueEntityRefs(partial?.created),
|
|
131
|
+
updated: uniqueEntityRefs(partial?.updated),
|
|
132
|
+
archived: uniqueEntityRefs(partial?.archived),
|
|
133
|
+
removed: uniqueEntityRefs(partial?.removed),
|
|
134
|
+
affectedLinks: uniqueAffectedLinks(partial?.affectedLinks, derivedEffects),
|
|
135
|
+
derivedEffects
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/events/receipts.ts
|
|
140
|
+
function buildEventMutationReceipt(primary, partial) {
|
|
141
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
142
|
+
return {
|
|
143
|
+
primary,
|
|
144
|
+
...partial?.warnings ? { warnings: partial.warnings } : {},
|
|
145
|
+
created: normalized.created,
|
|
146
|
+
updated: normalized.updated,
|
|
147
|
+
archived: normalized.archived,
|
|
148
|
+
removed: normalized.removed,
|
|
149
|
+
affectedLinks: normalized.affectedLinks,
|
|
150
|
+
derivedEffects: normalized.derivedEffects
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/events/event_metadata.ts
|
|
155
|
+
function normalizeExternalRef(ref) {
|
|
156
|
+
if (!ref || typeof ref.source !== "string" || typeof ref.id !== "string" || !ref.source.trim() || !ref.id.trim())
|
|
157
|
+
throw new AffinityValidationError(
|
|
158
|
+
"external reference source and id must be non-empty strings"
|
|
159
|
+
);
|
|
160
|
+
return { source: ref.source.trim().toLowerCase(), id: ref.id.trim() };
|
|
161
|
+
}
|
|
162
|
+
function deriveEventSummary(content) {
|
|
163
|
+
const line = content.split(/\r?\n/).find((line2) => line2.trim())?.replace(/\s+/g, " ").trim() ?? "(no text)";
|
|
164
|
+
return line.length > 140 ? `${line.slice(0, 139)}\u2026` : line;
|
|
165
|
+
}
|
|
166
|
+
function validateEventMetadata(input) {
|
|
167
|
+
if (input.affectsBond !== void 0 && typeof input.affectsBond !== "boolean")
|
|
168
|
+
throw new AffinityValidationError("affectsBond must be boolean");
|
|
169
|
+
if (input.content !== void 0 && (typeof input.content !== "string" || Buffer.byteLength(input.content, "utf8") > 1048576))
|
|
170
|
+
throw new AffinityValidationError(
|
|
171
|
+
"content must be a string of at most 1 MB (1048576 UTF-8 bytes)"
|
|
172
|
+
);
|
|
173
|
+
for (const key of ["externalRef", "parentRef", "threadRef"])
|
|
174
|
+
if (input[key] !== void 0) input[key] = normalizeExternalRef(input[key]);
|
|
175
|
+
if (input.channel !== void 0) {
|
|
176
|
+
if (typeof input.channel !== "string" || !input.channel.trim() || input.channel.trim().length > 40)
|
|
177
|
+
throw new AffinityValidationError(
|
|
178
|
+
"channel must be non-empty and at most 40 characters"
|
|
179
|
+
);
|
|
180
|
+
input.channel = input.channel.trim().toLowerCase();
|
|
181
|
+
}
|
|
182
|
+
if (input.parentEventId !== void 0 && (!Number.isSafeInteger(input.parentEventId) || input.parentEventId <= 0))
|
|
183
|
+
throw new AffinityValidationError(
|
|
184
|
+
"parentEventId must be a positive integer"
|
|
185
|
+
);
|
|
186
|
+
const moments = [
|
|
187
|
+
"breakthrough",
|
|
188
|
+
"rupture",
|
|
189
|
+
"reconciliation",
|
|
190
|
+
"milestone",
|
|
191
|
+
"turning_point"
|
|
192
|
+
];
|
|
193
|
+
if (input.momentKind !== void 0 && !moments.includes(input.momentKind))
|
|
194
|
+
throw new AffinityValidationError(
|
|
195
|
+
`momentKind must be one of: ${moments.join(", ")}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
function getEventById(db, eventId) {
|
|
199
|
+
return db.prepare("SELECT 1 FROM events WHERE id=? AND deleted_at IS NULL").get(eventId) ? loadEventRecord(db, eventId) : null;
|
|
200
|
+
}
|
|
201
|
+
function getEventByExternalRef(db, source, id) {
|
|
202
|
+
const ref = normalizeExternalRef({ source, id });
|
|
203
|
+
const row = db.prepare(
|
|
204
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
205
|
+
).get(ref.source, ref.id);
|
|
206
|
+
return row ? loadEventRecord(db, row.id) : null;
|
|
207
|
+
}
|
|
208
|
+
function existingEventReceipt(db, ref) {
|
|
209
|
+
const event = ref && getEventByExternalRef(db, ref.source, ref.id);
|
|
210
|
+
return event ? { ...buildEventMutationReceipt(event, {}), outcome: "no_op" } : null;
|
|
211
|
+
}
|
|
212
|
+
function enrichEventMetadata(db, record) {
|
|
213
|
+
const bond = db.prepare("SELECT affects_bond FROM events WHERE id=?").get(record.id);
|
|
214
|
+
record.affectsBond = bond?.affects_bond == null ? !(record.type === "observation" && record.significance <= 2) : Boolean(bond.affects_bond);
|
|
215
|
+
if (record.parentEventId === void 0 && record.parentRef) {
|
|
216
|
+
const row = db.prepare(
|
|
217
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
218
|
+
).get(record.parentRef.source, record.parentRef.id);
|
|
219
|
+
if (row) record.parentEventId = row.id;
|
|
220
|
+
}
|
|
221
|
+
const attrs = db.prepare(
|
|
222
|
+
"SELECT id, name, value FROM attributes WHERE event_id=? AND deleted_at IS NULL ORDER BY id"
|
|
223
|
+
).all(record.id);
|
|
224
|
+
if (attrs.length)
|
|
225
|
+
record.attributes = attrs.map((a) => ({
|
|
226
|
+
...a,
|
|
227
|
+
contactId: null,
|
|
228
|
+
linkId: null,
|
|
229
|
+
eventId: record.id
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
81
233
|
// src/contacts/queries.ts
|
|
82
234
|
function getContactRowById(db, id) {
|
|
83
235
|
const row = db.prepare(
|
|
@@ -156,8 +308,11 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
156
308
|
}
|
|
157
309
|
function validateSocialEventInput(input) {
|
|
158
310
|
assertFiniteTimestamp(input.occurredAt, "occurredAt");
|
|
159
|
-
|
|
160
|
-
if (summary.
|
|
311
|
+
validateEventMetadata(input);
|
|
312
|
+
if (input.summary !== void 0 && typeof input.summary !== "string")
|
|
313
|
+
throw new AffinityValidationError("summary must be a string");
|
|
314
|
+
const summary = input.summary?.trim();
|
|
315
|
+
if (summary === "" || summary === void 0 && input.content === void 0) {
|
|
161
316
|
throw new AffinityValidationError("summary must be non-empty");
|
|
162
317
|
}
|
|
163
318
|
const sig = input.significance;
|
|
@@ -221,11 +376,27 @@ function mapEventRowToEventRecord(row, participants) {
|
|
|
221
376
|
id: row.id,
|
|
222
377
|
type: row.type,
|
|
223
378
|
occurredAt: row.occurred_at,
|
|
224
|
-
|
|
379
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
380
|
+
summary: decodeEventText(row.summary),
|
|
225
381
|
significance: row.significance,
|
|
226
382
|
momentKind: row.moment_kind,
|
|
227
383
|
participants
|
|
228
384
|
};
|
|
385
|
+
if (row.provenance_json != null)
|
|
386
|
+
base.provenance = JSON.parse(row.provenance_json);
|
|
387
|
+
if (row.content != null) base.content = decodeEventText(row.content);
|
|
388
|
+
if (row.channel != null) base.channel = row.channel;
|
|
389
|
+
if (row.external_source != null && row.external_id != null)
|
|
390
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
391
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
392
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
393
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
394
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
395
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
396
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
397
|
+
base.dueAt = row.due_at ?? null;
|
|
398
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
399
|
+
}
|
|
229
400
|
if (row.recurrence_kind !== null) {
|
|
230
401
|
base.recurrenceKind = row.recurrence_kind;
|
|
231
402
|
base.anchorMonth = row.anchor_month;
|
|
@@ -255,25 +426,29 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
255
426
|
targetRef,
|
|
256
427
|
recurrenceKind,
|
|
257
428
|
occursOn: row.occurs_on,
|
|
258
|
-
|
|
429
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
430
|
+
summary: decodeEventText(row.summary),
|
|
259
431
|
significance: row.significance
|
|
260
432
|
};
|
|
261
433
|
}
|
|
434
|
+
function decodeEventText(value) {
|
|
435
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
436
|
+
}
|
|
262
437
|
|
|
263
438
|
// src/events/queries.ts
|
|
264
439
|
function getEventRowById(db, id) {
|
|
265
440
|
const row = db.prepare(
|
|
266
|
-
`SELECT
|
|
267
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
268
|
-
anchor_contact_id, anchor_link_id,
|
|
269
|
-
created_at, updated_at, deleted_at
|
|
441
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
270
442
|
FROM events
|
|
271
443
|
WHERE id = ? AND deleted_at IS NULL`
|
|
272
444
|
).get(id);
|
|
273
445
|
if (row === void 0) {
|
|
274
446
|
return null;
|
|
275
447
|
}
|
|
276
|
-
|
|
448
|
+
const event = row;
|
|
449
|
+
event.summary = decodeEventText(event.summary);
|
|
450
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
451
|
+
return event;
|
|
277
452
|
}
|
|
278
453
|
function findDuplicateDateAnchor(db, params) {
|
|
279
454
|
const exclude = params.excludeEventId;
|
|
@@ -341,19 +516,27 @@ function loadEventRecord(db, eventId) {
|
|
|
341
516
|
throw new AffinityInvariantError("event not found");
|
|
342
517
|
}
|
|
343
518
|
const participants = loadEventParticipantViews(db, eventId);
|
|
344
|
-
|
|
519
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
520
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
521
|
+
if (due) {
|
|
522
|
+
record.dueAt = due.due_at;
|
|
523
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
524
|
+
}
|
|
525
|
+
enrichEventMetadata(db, record);
|
|
526
|
+
return record;
|
|
345
527
|
}
|
|
346
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
528
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
347
529
|
if (eventIds.length === 0) {
|
|
348
530
|
return [];
|
|
349
531
|
}
|
|
350
532
|
const ph = eventIds.map(() => "?").join(", ");
|
|
351
533
|
const eventRows = db.prepare(
|
|
352
|
-
`SELECT id, type, occurred_at,
|
|
534
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
535
|
+
e.external_source,e.external_id,e.channel,e.parent_source,e.parent_ref_id,e.parent_event_id,e.thread_source,e.thread_id,
|
|
353
536
|
recurrence_kind, anchor_month, anchor_day,
|
|
354
537
|
anchor_contact_id, anchor_link_id,
|
|
355
|
-
created_at, updated_at, deleted_at
|
|
356
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
538
|
+
e.created_at, e.updated_at, e.deleted_at, ${includeContent ? "CAST(e.content AS BLOB)" : "NULL"} AS content, CAST(e.summary AS BLOB) AS summary
|
|
539
|
+
FROM events e LEFT JOIN open_commitments oc ON oc.event_id = e.id WHERE e.id IN (${ph}) AND e.deleted_at IS NULL`
|
|
357
540
|
).all(...eventIds);
|
|
358
541
|
const partRows = db.prepare(
|
|
359
542
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -377,7 +560,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
377
560
|
for (const id of eventIds) {
|
|
378
561
|
const row = rowById.get(id);
|
|
379
562
|
if (row) {
|
|
380
|
-
|
|
563
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
564
|
+
enrichEventMetadata(db, record);
|
|
565
|
+
out.push(record);
|
|
381
566
|
}
|
|
382
567
|
}
|
|
383
568
|
return out;
|
|
@@ -436,6 +621,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
436
621
|
clauses.push(`e.type IN (${ph})`);
|
|
437
622
|
params.push(...options.eventTypes);
|
|
438
623
|
}
|
|
624
|
+
if (options?.channel !== void 0) {
|
|
625
|
+
clauses.push("e.channel = ?");
|
|
626
|
+
params.push(options.channel.trim().toLowerCase());
|
|
627
|
+
}
|
|
439
628
|
const where = clauses.join(" AND ");
|
|
440
629
|
const rows = db.prepare(
|
|
441
630
|
`SELECT e.id FROM events e
|
|
@@ -446,8 +635,70 @@ function getContactJournal(db, contactId, options) {
|
|
|
446
635
|
).all(...params, limit, offset);
|
|
447
636
|
return batchLoadEventRecords(
|
|
448
637
|
db,
|
|
449
|
-
rows.map((r) => r.id)
|
|
638
|
+
rows.map((r) => r.id),
|
|
639
|
+
options?.includeContent ?? false
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/lib/parse_calendar_time.ts
|
|
644
|
+
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
645
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
646
|
+
if (dueAt === null) return false;
|
|
647
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
648
|
+
return localCalendarDate(now) > dueOn;
|
|
649
|
+
}
|
|
650
|
+
return dueAt < now;
|
|
651
|
+
}
|
|
652
|
+
var OVERDUE_SQL = "(CASE WHEN oc.due_on IS NOT NULL AND oc.due_at = CAST(strftime('%s', oc.due_on) AS INTEGER) * 1000 + 86399999 THEN oc.due_on < ? ELSE oc.due_at < ? END)";
|
|
653
|
+
function localCalendarDate(now) {
|
|
654
|
+
const d = new Date(now);
|
|
655
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
656
|
+
}
|
|
657
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
658
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
659
|
+
throw new AffinityValidationError(
|
|
660
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
661
|
+
);
|
|
662
|
+
const fail = () => {
|
|
663
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
664
|
+
};
|
|
665
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
666
|
+
const timestamp2 = Number(value);
|
|
667
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
668
|
+
return fail();
|
|
669
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
670
|
+
}
|
|
671
|
+
if (typeof value !== "string") return fail();
|
|
672
|
+
let text = value.trim();
|
|
673
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
674
|
+
text = text.replace(" ", "T");
|
|
675
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
676
|
+
}
|
|
677
|
+
text = text.replace(
|
|
678
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
679
|
+
(_, digits) => `.${digits.slice(0, 3)}`
|
|
450
680
|
);
|
|
681
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
682
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
683
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
684
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
685
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
686
|
+
text = date.toISOString().slice(0, 10);
|
|
687
|
+
}
|
|
688
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
689
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
690
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
691
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
692
|
+
text
|
|
693
|
+
))
|
|
694
|
+
return fail();
|
|
695
|
+
const day = text.slice(0, 10);
|
|
696
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
697
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
698
|
+
return fail();
|
|
699
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
|
|
700
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
701
|
+
return { timestamp, day };
|
|
451
702
|
}
|
|
452
703
|
|
|
453
704
|
// src/resolve_now.ts
|
|
@@ -518,9 +769,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
518
769
|
clauses.push("oc.due_at <= ?");
|
|
519
770
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
520
771
|
}
|
|
772
|
+
if (options?.channel !== void 0) {
|
|
773
|
+
clauses.push("e.channel=?");
|
|
774
|
+
params.push(options.channel.trim().toLowerCase());
|
|
775
|
+
}
|
|
521
776
|
const where = clauses.join(" AND ");
|
|
522
777
|
const rows = db.prepare(
|
|
523
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
778
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
524
779
|
e.type, e.summary
|
|
525
780
|
FROM open_commitments oc
|
|
526
781
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -537,7 +792,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
537
792
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
538
793
|
dueAt: r.due_at,
|
|
539
794
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
540
|
-
|
|
795
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
796
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
541
797
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
542
798
|
}));
|
|
543
799
|
}
|
|
@@ -606,8 +862,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
606
862
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
607
863
|
}
|
|
608
864
|
function computeNextAnchorOccursOn(params) {
|
|
609
|
-
const refStart =
|
|
610
|
-
|
|
865
|
+
const refStart = Date.parse(
|
|
866
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
867
|
+
);
|
|
868
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
611
869
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
612
870
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
613
871
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -810,7 +1068,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
810
1068
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
811
1069
|
LIMIT ?`
|
|
812
1070
|
).all(contactId, contactId, topLinksLimit);
|
|
813
|
-
const topLinks = linkRows.map((l) =>
|
|
1071
|
+
const topLinks = linkRows.map((l) => ({
|
|
1072
|
+
...mapLinkRowToLinkListItem(l),
|
|
1073
|
+
counterpartyName: getContactRowById(
|
|
1074
|
+
db,
|
|
1075
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1076
|
+
)?.name ?? ""
|
|
1077
|
+
}));
|
|
814
1078
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
815
1079
|
const contactRollup = loadContactRollup(db, contactId);
|
|
816
1080
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -843,7 +1107,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
843
1107
|
return {
|
|
844
1108
|
...profile,
|
|
845
1109
|
recentEvents: getContactJournal(db, contactId, {
|
|
846
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1110
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1111
|
+
eventTypes: [
|
|
1112
|
+
"conversation",
|
|
1113
|
+
"activity",
|
|
1114
|
+
"gift",
|
|
1115
|
+
"support",
|
|
1116
|
+
"milestone",
|
|
1117
|
+
"observation",
|
|
1118
|
+
"conflict",
|
|
1119
|
+
"correction",
|
|
1120
|
+
"transaction",
|
|
1121
|
+
"promise",
|
|
1122
|
+
"agreement"
|
|
1123
|
+
]
|
|
847
1124
|
}).map((event) => ({
|
|
848
1125
|
...event,
|
|
849
1126
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -898,6 +1175,10 @@ function listMoments(db, filters, options) {
|
|
|
898
1175
|
clauses.push("le.link_id = ?");
|
|
899
1176
|
params.push(filters.linkId);
|
|
900
1177
|
}
|
|
1178
|
+
if (options?.channel !== void 0) {
|
|
1179
|
+
clauses.push("e.channel=?");
|
|
1180
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1181
|
+
}
|
|
901
1182
|
const where = clauses.join(" AND ");
|
|
902
1183
|
return db.prepare(
|
|
903
1184
|
`SELECT
|
|
@@ -905,6 +1186,7 @@ function listMoments(db, filters, options) {
|
|
|
905
1186
|
le.link_id,
|
|
906
1187
|
le.moment_kind,
|
|
907
1188
|
le.occurred_at,
|
|
1189
|
+
e.occurred_on,
|
|
908
1190
|
e.summary,
|
|
909
1191
|
le.impact_score
|
|
910
1192
|
FROM link_event_effects le
|
|
@@ -917,6 +1199,7 @@ function listMoments(db, filters, options) {
|
|
|
917
1199
|
linkId: row.link_id,
|
|
918
1200
|
momentKind: row.moment_kind,
|
|
919
1201
|
occurredAt: row.occurred_at,
|
|
1202
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
920
1203
|
summary: row.summary,
|
|
921
1204
|
impactScore: row.impact_score
|
|
922
1205
|
}));
|
|
@@ -1004,6 +1287,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1004
1287
|
clauses.push(`e.type IN (${ph})`);
|
|
1005
1288
|
params.push(...options.eventTypes);
|
|
1006
1289
|
}
|
|
1290
|
+
if (options?.channel !== void 0) {
|
|
1291
|
+
clauses.push("e.channel = ?");
|
|
1292
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1293
|
+
}
|
|
1007
1294
|
const where = clauses.join(" AND ");
|
|
1008
1295
|
const rows = db.prepare(
|
|
1009
1296
|
`SELECT e.id
|
|
@@ -1015,7 +1302,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1015
1302
|
).all(...params, limit, offset);
|
|
1016
1303
|
return batchLoadEventRecords(
|
|
1017
1304
|
db,
|
|
1018
|
-
rows.map((r) => r.id)
|
|
1305
|
+
rows.map((r) => r.id),
|
|
1306
|
+
options?.includeContent ?? false
|
|
1019
1307
|
);
|
|
1020
1308
|
}
|
|
1021
1309
|
|
|
@@ -1188,26 +1476,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1188
1476
|
const latest = db.prepare(
|
|
1189
1477
|
`SELECT event_id, occurred_at
|
|
1190
1478
|
FROM link_event_effects
|
|
1191
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
1479
|
+
WHERE link_id = ? AND counts_for_recency = 1 AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))
|
|
1192
1480
|
ORDER BY occurred_at DESC, event_id DESC
|
|
1193
1481
|
LIMIT 1`
|
|
1194
|
-
).get(linkId);
|
|
1482
|
+
).get(linkId, now, localCalendarDate(now));
|
|
1195
1483
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
1196
1484
|
const counts = db.prepare(
|
|
1197
1485
|
`SELECT
|
|
1198
1486
|
COUNT(*) AS total_meaningful_events,
|
|
1199
1487
|
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
1200
1488
|
FROM link_event_effects
|
|
1201
|
-
WHERE link_id = ? AND occurred_at >=
|
|
1202
|
-
|
|
1489
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
1490
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
1491
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
1203
1492
|
const directionalityRows = db.prepare(
|
|
1204
1493
|
`SELECT ep.directionality
|
|
1205
1494
|
FROM link_event_effects le
|
|
1206
1495
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
1207
1496
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
1208
1497
|
AND ep.directionality IS NOT NULL
|
|
1209
|
-
AND le.occurred_at >=
|
|
1210
|
-
|
|
1498
|
+
AND le.occurred_at >= ?
|
|
1499
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
1500
|
+
).all(
|
|
1501
|
+
linkId,
|
|
1502
|
+
link.from_contact_id,
|
|
1503
|
+
trailingWindowStart,
|
|
1504
|
+
now,
|
|
1505
|
+
localCalendarDate(now)
|
|
1506
|
+
);
|
|
1211
1507
|
let outboundCount = 0;
|
|
1212
1508
|
let inboundCount = 0;
|
|
1213
1509
|
for (const row of directionalityRows) {
|
|
@@ -1358,37 +1654,8 @@ function getLinkDetail(db, linkId, options) {
|
|
|
1358
1654
|
}
|
|
1359
1655
|
|
|
1360
1656
|
// src/tools/parse_time_input.ts
|
|
1361
|
-
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, today, tomorrow, yesterday, +3d or -2d";
|
|
1362
1657
|
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
1363
|
-
|
|
1364
|
-
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1365
|
-
};
|
|
1366
|
-
if (typeof value === "number")
|
|
1367
|
-
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
1368
|
-
if (typeof value !== "string") return fail();
|
|
1369
|
-
const text = value.trim();
|
|
1370
|
-
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
1371
|
-
let day;
|
|
1372
|
-
if (relative) {
|
|
1373
|
-
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1374
|
-
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
1375
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
1376
|
-
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
1377
|
-
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
1378
|
-
return fail();
|
|
1379
|
-
} else {
|
|
1380
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1381
|
-
text
|
|
1382
|
-
))
|
|
1383
|
-
return fail();
|
|
1384
|
-
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
1385
|
-
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
1386
|
-
return fail();
|
|
1387
|
-
const parsed = Date.parse(text);
|
|
1388
|
-
return Number.isFinite(parsed) ? parsed : fail();
|
|
1389
|
-
}
|
|
1390
|
-
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
1391
|
-
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
1658
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
1392
1659
|
}
|
|
1393
1660
|
|
|
1394
1661
|
// src/tools/tool_metadata.ts
|
|
@@ -1459,6 +1726,39 @@ function oneOfSchema(schemas, description) {
|
|
|
1459
1726
|
return schema;
|
|
1460
1727
|
}
|
|
1461
1728
|
|
|
1729
|
+
// src/tools/event_metadata_schema.ts
|
|
1730
|
+
var externalRefSchema = objectSchema(
|
|
1731
|
+
{
|
|
1732
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
1733
|
+
id: stringSchema("Non-empty external id.")
|
|
1734
|
+
},
|
|
1735
|
+
["source", "id"]
|
|
1736
|
+
);
|
|
1737
|
+
var eventMetadataSchema = {
|
|
1738
|
+
affectsBond: booleanSchema(
|
|
1739
|
+
"Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
|
|
1740
|
+
),
|
|
1741
|
+
content: stringSchema(
|
|
1742
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
1743
|
+
),
|
|
1744
|
+
externalRef: externalRefSchema,
|
|
1745
|
+
channel: stringSchema(
|
|
1746
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
1747
|
+
),
|
|
1748
|
+
parentRef: externalRefSchema,
|
|
1749
|
+
parentEventId: integerSchema("Parent event id."),
|
|
1750
|
+
threadRef: externalRefSchema,
|
|
1751
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
1752
|
+
"breakthrough",
|
|
1753
|
+
"rupture",
|
|
1754
|
+
"reconciliation",
|
|
1755
|
+
"milestone",
|
|
1756
|
+
"turning_point"
|
|
1757
|
+
]),
|
|
1758
|
+
occurredOn: stringSchema("Calendar day."),
|
|
1759
|
+
provenance: {}
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1462
1762
|
// src/tools/tool_names.ts
|
|
1463
1763
|
var searchAffinityToolName = "search_affinity";
|
|
1464
1764
|
var reviewAffinityToolName = "review_affinity";
|
|
@@ -1472,6 +1772,7 @@ var recordEventToolName = "record_event";
|
|
|
1472
1772
|
var manageCommitmentToolName = "manage_commitment";
|
|
1473
1773
|
var manageDateAnchorToolName = "manage_date_anchor";
|
|
1474
1774
|
var manageAttributeToolName = "manage_attribute";
|
|
1775
|
+
var manageEventToolName = "manage_event";
|
|
1475
1776
|
|
|
1476
1777
|
// src/tools/tool_ref.ts
|
|
1477
1778
|
function contactState(value) {
|
|
@@ -1516,7 +1817,7 @@ function toEventEntityRef(value) {
|
|
|
1516
1817
|
return {
|
|
1517
1818
|
kind: "event",
|
|
1518
1819
|
id: value.id,
|
|
1519
|
-
title: value.summary,
|
|
1820
|
+
title: deriveEventSummary(value.summary),
|
|
1520
1821
|
subtitle: value.type,
|
|
1521
1822
|
state: value.type
|
|
1522
1823
|
};
|
|
@@ -1551,14 +1852,39 @@ function toReceiptEntityRefs(receipt) {
|
|
|
1551
1852
|
}
|
|
1552
1853
|
|
|
1553
1854
|
// src/identities/normalize.ts
|
|
1855
|
+
var handleIdentityTypes = /* @__PURE__ */ new Set([
|
|
1856
|
+
"handle",
|
|
1857
|
+
"bluesky",
|
|
1858
|
+
"x",
|
|
1859
|
+
"twitter",
|
|
1860
|
+
"mastodon",
|
|
1861
|
+
"github",
|
|
1862
|
+
"instagram",
|
|
1863
|
+
"telegram",
|
|
1864
|
+
"linkedin",
|
|
1865
|
+
"facebook",
|
|
1866
|
+
"youtube",
|
|
1867
|
+
"tiktok",
|
|
1868
|
+
"discord",
|
|
1869
|
+
"slack"
|
|
1870
|
+
]);
|
|
1871
|
+
function normalizeIdentityValue(type, value) {
|
|
1872
|
+
const v = value.trim();
|
|
1873
|
+
return handleIdentityTypes.has(type.toLowerCase()) ? v.replace(/^@/, "").toLowerCase() : v;
|
|
1874
|
+
}
|
|
1554
1875
|
function normalizeIdentityKey(type, value) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1876
|
+
let t = type.trim().toLowerCase();
|
|
1877
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(t)) t = "did";
|
|
1878
|
+
const v = normalizeIdentityValue(t, value).toLowerCase();
|
|
1557
1879
|
if (t.length === 0 || v.length === 0) {
|
|
1558
1880
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
1559
1881
|
}
|
|
1560
1882
|
return `${t}:${v}`;
|
|
1561
1883
|
}
|
|
1884
|
+
function legacyIdentityKey(key) {
|
|
1885
|
+
const colon = key.indexOf(":");
|
|
1886
|
+
return handleIdentityTypes.has(key.slice(0, colon)) ? `${key.slice(0, colon + 1)}@${key.slice(colon + 1)}` : key;
|
|
1887
|
+
}
|
|
1562
1888
|
|
|
1563
1889
|
// src/lib/errors/is_affinity_error.ts
|
|
1564
1890
|
function isAffinityError(value) {
|
|
@@ -1680,7 +2006,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
1680
2006
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
1681
2007
|
return {
|
|
1682
2008
|
ok: false,
|
|
1683
|
-
result: toolFailure("domain", "
|
|
2009
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
1684
2010
|
};
|
|
1685
2011
|
}
|
|
1686
2012
|
return { ok: true, value: row2 };
|
|
@@ -1705,9 +2031,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
1705
2031
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
1706
2032
|
WHERE i.removed_at IS NULL
|
|
1707
2033
|
AND c.deleted_at IS NULL
|
|
1708
|
-
AND i.normalized_key = ?
|
|
2034
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
1709
2035
|
LIMIT 2`
|
|
1710
|
-
).all(
|
|
2036
|
+
).all(
|
|
2037
|
+
normalizedKey,
|
|
2038
|
+
legacyIdentityKey(normalizedKey),
|
|
2039
|
+
normalizedKey
|
|
2040
|
+
);
|
|
1711
2041
|
if (row.length === 0) {
|
|
1712
2042
|
return {
|
|
1713
2043
|
ok: false,
|
|
@@ -1858,13 +2188,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
1858
2188
|
return { ok: true, value: matchedLink };
|
|
1859
2189
|
}
|
|
1860
2190
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
2191
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
2192
|
+
return error;
|
|
1861
2193
|
if (isAffinityError(error)) {
|
|
1862
|
-
|
|
2194
|
+
const result = toolFailure(
|
|
1863
2195
|
"domain",
|
|
1864
2196
|
affinityErrorCodeToToolCode(error),
|
|
1865
2197
|
summary,
|
|
1866
2198
|
error.message
|
|
1867
2199
|
);
|
|
2200
|
+
const id = error.existingContactId;
|
|
2201
|
+
if (id !== void 0) {
|
|
2202
|
+
result.data = { existingContactId: id };
|
|
2203
|
+
result.error.details = { existingContactId: id };
|
|
2204
|
+
}
|
|
2205
|
+
return result;
|
|
1868
2206
|
}
|
|
1869
2207
|
if (error instanceof Error) {
|
|
1870
2208
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -1903,27 +2241,32 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
1903
2241
|
}
|
|
1904
2242
|
|
|
1905
2243
|
// src/tools/validate_tool_input.ts
|
|
1906
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
2244
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
2245
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
2246
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
2247
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
2248
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
2249
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
1907
2250
|
if (schema.anyOf || schema.oneOf) {
|
|
1908
2251
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
1909
2252
|
return choices.some(
|
|
1910
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
2253
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
1911
2254
|
) ? null : `${path}: invalid value`;
|
|
1912
2255
|
}
|
|
1913
2256
|
if (schema.type === "object") {
|
|
1914
2257
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1915
2258
|
return `${path}: missing input or expected object`;
|
|
1916
2259
|
const object = value;
|
|
1917
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
2260
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
1918
2261
|
for (const key of Object.keys(object)) {
|
|
1919
2262
|
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
1920
|
-
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(", ")}`;
|
|
2263
|
+
return key === "kind" && "name" in (schema.properties ?? {}) ? "kind cannot be changed after creation" : `${path}.${key}: unknown ${path.endsWith(".patch") ? "patch key" : "key"}; allowed keys: ${Object.keys(schema.properties ?? {}).join(", ")}`;
|
|
1921
2264
|
}
|
|
1922
2265
|
}
|
|
1923
2266
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
1924
2267
|
return `${path}.patch: missing required field`;
|
|
1925
2268
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
1926
|
-
return `${path}.patch: unknown
|
|
2269
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
1927
2270
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
1928
2271
|
try {
|
|
1929
2272
|
return parseTimeInput(
|
|
@@ -1938,11 +2281,21 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1938
2281
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
1939
2282
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
1940
2283
|
try {
|
|
1941
|
-
|
|
2284
|
+
const parsed = parseCalendarTime(
|
|
1942
2285
|
object[key],
|
|
1943
2286
|
`${path}.${key}`,
|
|
1944
2287
|
referenceNow
|
|
1945
2288
|
);
|
|
2289
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
|
|
2290
|
+
const provenance = object.provenance;
|
|
2291
|
+
object.provenance = {
|
|
2292
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
2293
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
object[key] = parsed.timestamp;
|
|
2297
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
2298
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
1946
2299
|
} catch (error) {
|
|
1947
2300
|
return error.message;
|
|
1948
2301
|
}
|
|
@@ -1957,8 +2310,9 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1957
2310
|
const error = validateToolInput(
|
|
1958
2311
|
child,
|
|
1959
2312
|
object[key],
|
|
1960
|
-
`${path}.${key}`,
|
|
1961
|
-
referenceNow
|
|
2313
|
+
key === "input" && path === "input" ? path : `${path}.${key}`,
|
|
2314
|
+
referenceNow,
|
|
2315
|
+
strict
|
|
1962
2316
|
);
|
|
1963
2317
|
if (error) return error;
|
|
1964
2318
|
}
|
|
@@ -1971,7 +2325,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1971
2325
|
schema.items ?? {},
|
|
1972
2326
|
item,
|
|
1973
2327
|
`${path}[${index}]`,
|
|
1974
|
-
injectedNow
|
|
2328
|
+
injectedNow,
|
|
2329
|
+
strict
|
|
1975
2330
|
);
|
|
1976
2331
|
if (error) return error;
|
|
1977
2332
|
}
|
|
@@ -2037,6 +2392,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
2037
2392
|
inputError
|
|
2038
2393
|
);
|
|
2039
2394
|
return withToolHandling(() => {
|
|
2395
|
+
if (input.kind === "event") {
|
|
2396
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
2397
|
+
return toolFailure(
|
|
2398
|
+
"protocol",
|
|
2399
|
+
"invalid_input",
|
|
2400
|
+
"Invalid event target.",
|
|
2401
|
+
"Provide exactly one of eventId or ref."
|
|
2402
|
+
);
|
|
2403
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
2404
|
+
return detail2 ? toolSuccess(
|
|
2405
|
+
`Loaded event #${detail2.id}.`,
|
|
2406
|
+
{ kind: "event", detail: detail2 },
|
|
2407
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
2408
|
+
) : toolFailure(
|
|
2409
|
+
"domain",
|
|
2410
|
+
"not_found",
|
|
2411
|
+
"Event not found.",
|
|
2412
|
+
"No event matches the target."
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2040
2415
|
if (input.kind === "owner_profile") {
|
|
2041
2416
|
const detail2 = getOwnerProfile(
|
|
2042
2417
|
db,
|
|
@@ -2134,10 +2509,13 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
2134
2509
|
inputSchema: objectSchema(
|
|
2135
2510
|
{
|
|
2136
2511
|
kind: enumSchema("What to inspect.", [
|
|
2512
|
+
"event",
|
|
2137
2513
|
"owner_profile",
|
|
2138
2514
|
"contact_profile",
|
|
2139
2515
|
"link"
|
|
2140
2516
|
]),
|
|
2517
|
+
eventId: integerSchema("Exact event id."),
|
|
2518
|
+
ref: externalRefSchema,
|
|
2141
2519
|
contact: contactLocatorSchema(
|
|
2142
2520
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
2143
2521
|
),
|
|
@@ -2156,6 +2534,7 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
2156
2534
|
// src/attributes/helpers.ts
|
|
2157
2535
|
function clearedAttributePrimary(target) {
|
|
2158
2536
|
return {
|
|
2537
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
2159
2538
|
id: 0,
|
|
2160
2539
|
contactId: target.kind === "contact" ? target.id : null,
|
|
2161
2540
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -2167,6 +2546,7 @@ function clearedAttributePrimary(target) {
|
|
|
2167
2546
|
// src/attributes/mappers.ts
|
|
2168
2547
|
function mapAttributeRowToAttributeRecord(row) {
|
|
2169
2548
|
return {
|
|
2549
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
2170
2550
|
id: row.id,
|
|
2171
2551
|
contactId: row.contact_id,
|
|
2172
2552
|
linkId: row.link_id,
|
|
@@ -2176,136 +2556,55 @@ function mapAttributeRowToAttributeRecord(row) {
|
|
|
2176
2556
|
}
|
|
2177
2557
|
|
|
2178
2558
|
// src/attributes/queries.ts
|
|
2559
|
+
function attributeTargetSql(target) {
|
|
2560
|
+
return {
|
|
2561
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
2562
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2179
2565
|
function getAttributeRowById(db, id) {
|
|
2180
|
-
|
|
2181
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
2182
|
-
FROM attributes WHERE id = ?`
|
|
2183
|
-
).get(id);
|
|
2184
|
-
if (row === void 0) {
|
|
2185
|
-
return null;
|
|
2186
|
-
}
|
|
2187
|
-
return row;
|
|
2566
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
2188
2567
|
}
|
|
2189
2568
|
function getLiveAttributeRow(db, target, name) {
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
WHERE contact_id = ? AND name = ? AND deleted_at IS NULL`
|
|
2195
|
-
).get(target.id, name);
|
|
2196
|
-
if (row2 === void 0) {
|
|
2197
|
-
return null;
|
|
2198
|
-
}
|
|
2199
|
-
return row2;
|
|
2200
|
-
}
|
|
2201
|
-
const row = db.prepare(
|
|
2202
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
2203
|
-
FROM attributes
|
|
2204
|
-
WHERE link_id = ? AND name = ? AND deleted_at IS NULL`
|
|
2205
|
-
).get(target.id, name);
|
|
2206
|
-
if (row === void 0) {
|
|
2207
|
-
return null;
|
|
2208
|
-
}
|
|
2209
|
-
return row;
|
|
2569
|
+
const t = attributeTargetSql(target);
|
|
2570
|
+
return db.prepare(
|
|
2571
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
2572
|
+
).get(t.id, name) ?? null;
|
|
2210
2573
|
}
|
|
2211
2574
|
function listLiveAttributeIdsForTarget(db, target) {
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
return rows2.map((row) => row.id);
|
|
2217
|
-
}
|
|
2218
|
-
const rows = db.prepare(
|
|
2219
|
-
"SELECT id FROM attributes WHERE link_id = ? AND deleted_at IS NULL ORDER BY id"
|
|
2220
|
-
).all(target.id);
|
|
2221
|
-
return rows.map((row) => row.id);
|
|
2575
|
+
const t = attributeTargetSql(target);
|
|
2576
|
+
return db.prepare(
|
|
2577
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
2578
|
+
).all(t.id).map((r) => r.id);
|
|
2222
2579
|
}
|
|
2223
2580
|
|
|
2224
|
-
// src/
|
|
2225
|
-
function
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
}
|
|
2236
|
-
return out;
|
|
2581
|
+
// src/attributes/receipts.ts
|
|
2582
|
+
function buildAttributeMutationReceipt(primary, partial) {
|
|
2583
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2584
|
+
return {
|
|
2585
|
+
primary,
|
|
2586
|
+
created: normalized.created,
|
|
2587
|
+
updated: normalized.updated,
|
|
2588
|
+
archived: normalized.archived,
|
|
2589
|
+
removed: normalized.removed,
|
|
2590
|
+
affectedLinks: normalized.affectedLinks,
|
|
2591
|
+
derivedEffects: normalized.derivedEffects
|
|
2592
|
+
};
|
|
2237
2593
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
const out = [];
|
|
2253
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2254
|
-
for (const linkId of affectedLinks ?? []) {
|
|
2255
|
-
if (seen.has(linkId)) {
|
|
2256
|
-
continue;
|
|
2257
|
-
}
|
|
2258
|
-
seen.add(linkId);
|
|
2259
|
-
out.push(linkId);
|
|
2260
|
-
}
|
|
2261
|
-
for (const effect of derivedEffects) {
|
|
2262
|
-
if (seen.has(effect.linkId)) {
|
|
2263
|
-
continue;
|
|
2264
|
-
}
|
|
2265
|
-
seen.add(effect.linkId);
|
|
2266
|
-
out.push(effect.linkId);
|
|
2267
|
-
}
|
|
2268
|
-
return out;
|
|
2269
|
-
}
|
|
2270
|
-
function normalizeMutationReceiptPartial(partial) {
|
|
2271
|
-
const derivedEffects = uniqueDerivedEffects(partial?.derivedEffects);
|
|
2272
|
-
return {
|
|
2273
|
-
created: uniqueEntityRefs(partial?.created),
|
|
2274
|
-
updated: uniqueEntityRefs(partial?.updated),
|
|
2275
|
-
archived: uniqueEntityRefs(partial?.archived),
|
|
2276
|
-
removed: uniqueEntityRefs(partial?.removed),
|
|
2277
|
-
affectedLinks: uniqueAffectedLinks(partial?.affectedLinks, derivedEffects),
|
|
2278
|
-
derivedEffects
|
|
2279
|
-
};
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
// src/attributes/receipts.ts
|
|
2283
|
-
function buildAttributeMutationReceipt(primary, partial) {
|
|
2284
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2285
|
-
return {
|
|
2286
|
-
primary,
|
|
2287
|
-
created: normalized.created,
|
|
2288
|
-
updated: normalized.updated,
|
|
2289
|
-
archived: normalized.archived,
|
|
2290
|
-
removed: normalized.removed,
|
|
2291
|
-
affectedLinks: normalized.affectedLinks,
|
|
2292
|
-
derivedEffects: normalized.derivedEffects
|
|
2293
|
-
};
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
// src/links/validators.ts
|
|
2297
|
-
var STATES = /* @__PURE__ */ new Set([
|
|
2298
|
-
"active",
|
|
2299
|
-
"dormant",
|
|
2300
|
-
"strained",
|
|
2301
|
-
"broken",
|
|
2302
|
-
"archived"
|
|
2303
|
-
]);
|
|
2304
|
-
function assertContactEndpointsNotMerged(db, fromContactId, toContactId) {
|
|
2305
|
-
const from = getContactRowById(db, fromContactId);
|
|
2306
|
-
const to = getContactRowById(db, toContactId);
|
|
2307
|
-
if (!from || !to) {
|
|
2308
|
-
throw new AffinityNotFoundError("contact not found");
|
|
2594
|
+
|
|
2595
|
+
// src/links/validators.ts
|
|
2596
|
+
var STATES = /* @__PURE__ */ new Set([
|
|
2597
|
+
"active",
|
|
2598
|
+
"dormant",
|
|
2599
|
+
"strained",
|
|
2600
|
+
"broken",
|
|
2601
|
+
"archived"
|
|
2602
|
+
]);
|
|
2603
|
+
function assertContactEndpointsNotMerged(db, fromContactId, toContactId) {
|
|
2604
|
+
const from = getContactRowById(db, fromContactId);
|
|
2605
|
+
const to = getContactRowById(db, toContactId);
|
|
2606
|
+
if (!from || !to) {
|
|
2607
|
+
throw new AffinityNotFoundError("contact not found");
|
|
2309
2608
|
}
|
|
2310
2609
|
if (from.lifecycle_state === "merged" || to.lifecycle_state === "merged") {
|
|
2311
2610
|
throw new AffinityStateError("merged contact is read-only");
|
|
@@ -2341,6 +2640,8 @@ function normalizeAttributeValue(value) {
|
|
|
2341
2640
|
if (value === null) {
|
|
2342
2641
|
return null;
|
|
2343
2642
|
}
|
|
2643
|
+
if (typeof value !== "string")
|
|
2644
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
2344
2645
|
const trimmed = value.trim();
|
|
2345
2646
|
return trimmed.length === 0 ? null : trimmed;
|
|
2346
2647
|
}
|
|
@@ -2369,6 +2670,11 @@ function validateAttributeEntries(entries) {
|
|
|
2369
2670
|
return out;
|
|
2370
2671
|
}
|
|
2371
2672
|
function assertAttributeTargetWritable(db, target) {
|
|
2673
|
+
if (target.kind === "event") {
|
|
2674
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
2675
|
+
throw new AffinityNotFoundError("event not found");
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2372
2678
|
if (target.kind === "contact") {
|
|
2373
2679
|
const contact = getContactRowById(db, target.id);
|
|
2374
2680
|
if (!contact) {
|
|
@@ -2404,26 +2710,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
2404
2710
|
}
|
|
2405
2711
|
const created = [];
|
|
2406
2712
|
let lastId = 0;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
created.push({ kind: "attribute", id: lastId });
|
|
2416
|
-
}
|
|
2417
|
-
} else {
|
|
2418
|
-
const insert = db.prepare(
|
|
2419
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
2420
|
-
VALUES (NULL, ?, ?, ?, ?, ?)`
|
|
2421
|
-
);
|
|
2422
|
-
for (const e of normalized) {
|
|
2423
|
-
const r = insert.run(target.id, e.name, e.value, now, now);
|
|
2424
|
-
lastId = Number(r.lastInsertRowid);
|
|
2425
|
-
created.push({ kind: "attribute", id: lastId });
|
|
2426
|
-
}
|
|
2713
|
+
const t = attributeTargetSql(target);
|
|
2714
|
+
const insert = db.prepare(
|
|
2715
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
2716
|
+
);
|
|
2717
|
+
for (const e of normalized) {
|
|
2718
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
2719
|
+
lastId = Number(r.lastInsertRowid);
|
|
2720
|
+
created.push({ kind: "attribute", id: lastId });
|
|
2427
2721
|
}
|
|
2428
2722
|
if (normalized.length === 0) {
|
|
2429
2723
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -2453,6 +2747,11 @@ function setAttribute(db, target, name, value) {
|
|
|
2453
2747
|
const stored = normalizeAttributeValue(value);
|
|
2454
2748
|
const now = resolveNow();
|
|
2455
2749
|
const existing = getLiveAttributeRow(db, target, key);
|
|
2750
|
+
if (existing?.value === stored)
|
|
2751
|
+
return buildAttributeMutationReceipt(
|
|
2752
|
+
mapAttributeRowToAttributeRecord(existing),
|
|
2753
|
+
{}
|
|
2754
|
+
);
|
|
2456
2755
|
if (existing) {
|
|
2457
2756
|
db.prepare(
|
|
2458
2757
|
"UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
|
|
@@ -2468,27 +2767,10 @@ function setAttribute(db, target, name, value) {
|
|
|
2468
2767
|
}
|
|
2469
2768
|
);
|
|
2470
2769
|
}
|
|
2471
|
-
|
|
2472
|
-
const r2 = db.prepare(
|
|
2473
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
2474
|
-
VALUES (?, NULL, ?, ?, ?, ?)`
|
|
2475
|
-
).run(target.id, key, stored, now, now);
|
|
2476
|
-
const id2 = Number(r2.lastInsertRowid);
|
|
2477
|
-
const row2 = getAttributeRowById(db, id2);
|
|
2478
|
-
if (!row2) {
|
|
2479
|
-
throw new AffinityInvariantError("attribute row missing after insert");
|
|
2480
|
-
}
|
|
2481
|
-
return buildAttributeMutationReceipt(
|
|
2482
|
-
mapAttributeRowToAttributeRecord(row2),
|
|
2483
|
-
{
|
|
2484
|
-
created: [{ kind: "attribute", id: id2 }]
|
|
2485
|
-
}
|
|
2486
|
-
);
|
|
2487
|
-
}
|
|
2770
|
+
const t = attributeTargetSql(target);
|
|
2488
2771
|
const r = db.prepare(
|
|
2489
|
-
`INSERT INTO attributes (
|
|
2490
|
-
|
|
2491
|
-
).run(target.id, key, stored, now, now);
|
|
2772
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
2773
|
+
).run(t.id, key, stored, now, now);
|
|
2492
2774
|
const id = Number(r.lastInsertRowid);
|
|
2493
2775
|
const row = getAttributeRowById(db, id);
|
|
2494
2776
|
if (!row) {
|
|
@@ -2544,14 +2826,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
2544
2826
|
affectedLinks: receipt.affectedLinks,
|
|
2545
2827
|
derivedEffects: receipt.derivedEffects
|
|
2546
2828
|
};
|
|
2547
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
2829
|
+
const summary = changedCount === 0 ? receipt.primary && typeof receipt.primary === "object" && "externalRef" in receipt.primary && "id" in receipt.primary ? `Event already recorded (#${receipt.primary.id}, ${receipt.primary.externalRef.source} ${receipt.primary.externalRef.id}).` : `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
2548
2830
|
if (changedCount === 0) {
|
|
2549
2831
|
return toolNoOp(summary, data, {
|
|
2550
2832
|
entities: toReceiptEntityRefs(receipt)
|
|
2551
2833
|
});
|
|
2552
2834
|
}
|
|
2553
2835
|
return toolSuccess(summary, data, {
|
|
2554
|
-
entities: toReceiptEntityRefs(receipt)
|
|
2836
|
+
entities: toReceiptEntityRefs(receipt),
|
|
2837
|
+
warnings: receipt.warnings
|
|
2555
2838
|
});
|
|
2556
2839
|
}
|
|
2557
2840
|
var PLURALS = { identity: "identities" };
|
|
@@ -2608,6 +2891,30 @@ function linkLocatorSchema2(description) {
|
|
|
2608
2891
|
};
|
|
2609
2892
|
}
|
|
2610
2893
|
function resolveAttributeTarget(db, target) {
|
|
2894
|
+
if (target.kind === "event") {
|
|
2895
|
+
if (target.eventId === void 0 === (target.ref === void 0))
|
|
2896
|
+
return {
|
|
2897
|
+
kind: "error",
|
|
2898
|
+
result: toolFailure(
|
|
2899
|
+
"protocol",
|
|
2900
|
+
"invalid_input",
|
|
2901
|
+
"Invalid event target.",
|
|
2902
|
+
"Provide exactly one of target.eventId or target.ref."
|
|
2903
|
+
)
|
|
2904
|
+
};
|
|
2905
|
+
const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
|
|
2906
|
+
if (eventId === void 0)
|
|
2907
|
+
return {
|
|
2908
|
+
kind: "error",
|
|
2909
|
+
result: toolFailure(
|
|
2910
|
+
"domain",
|
|
2911
|
+
"not_found",
|
|
2912
|
+
"Event not found.",
|
|
2913
|
+
"No event matches target.ref."
|
|
2914
|
+
)
|
|
2915
|
+
};
|
|
2916
|
+
return { kind: "ok", target: { kind: "event", eventId } };
|
|
2917
|
+
}
|
|
2611
2918
|
if (target.kind === "contact") {
|
|
2612
2919
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
2613
2920
|
if (!contact.ok) {
|
|
@@ -2667,7 +2974,7 @@ function manageAttributeToolHandler(db, rawInput) {
|
|
|
2667
2974
|
}
|
|
2668
2975
|
var manageAttributeTool = defineAffinityTool({
|
|
2669
2976
|
name: manageAttributeToolName,
|
|
2670
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
2977
|
+
description: "Set, unset, or replace custom metadata attributes on contacts, links and events. For emails, phones, handles, and URLs use manage_identity instead \u2014 those are routing keys, not attributes.",
|
|
2671
2978
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
2672
2979
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
2673
2980
|
sideEffects: "writes_state",
|
|
@@ -2688,7 +2995,9 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2688
2995
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
2689
2996
|
target: objectSchema(
|
|
2690
2997
|
{
|
|
2691
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
2998
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
2999
|
+
eventId: integerSchema("Target event id; provide eventId or ref."),
|
|
3000
|
+
ref: externalRefSchema,
|
|
2692
3001
|
contact: contactLocatorSchema2(
|
|
2693
3002
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
2694
3003
|
),
|
|
@@ -2718,7 +3027,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2718
3027
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
2719
3028
|
},
|
|
2720
3029
|
["action"],
|
|
2721
|
-
"Set, unset, or replace attributes on contacts and
|
|
3030
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
2722
3031
|
),
|
|
2723
3032
|
handler: manageAttributeToolHandler
|
|
2724
3033
|
});
|
|
@@ -2726,7 +3035,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2726
3035
|
// src/events/persistence.ts
|
|
2727
3036
|
function insertJournalEvent(db, params) {
|
|
2728
3037
|
const now = resolveNow(params.now);
|
|
2729
|
-
const summary = params.summary
|
|
3038
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
2730
3039
|
const result = db.prepare(
|
|
2731
3040
|
`INSERT INTO events (
|
|
2732
3041
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -2743,6 +3052,35 @@ function insertJournalEvent(db, params) {
|
|
|
2743
3052
|
now
|
|
2744
3053
|
);
|
|
2745
3054
|
const eventId = Number(result.lastInsertRowid);
|
|
3055
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
3056
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 || ["activity", "milestone", "observation"].includes(params.type) && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
3057
|
+
summary
|
|
3058
|
+
) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
|
|
3059
|
+
eventId
|
|
3060
|
+
);
|
|
3061
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
3062
|
+
params.momentKind,
|
|
3063
|
+
eventId
|
|
3064
|
+
);
|
|
3065
|
+
db.prepare(
|
|
3066
|
+
"UPDATE events SET content=?, external_source=?, external_id=?, channel=?, parent_source=?, parent_ref_id=?, parent_event_id=?, thread_source=?, thread_id=?, provenance_json=? WHERE id=?"
|
|
3067
|
+
).run(
|
|
3068
|
+
params.content ?? null,
|
|
3069
|
+
params.externalRef?.source ?? null,
|
|
3070
|
+
params.externalRef?.id ?? null,
|
|
3071
|
+
params.channel ?? null,
|
|
3072
|
+
params.parentRef?.source ?? null,
|
|
3073
|
+
params.parentRef?.id ?? null,
|
|
3074
|
+
params.parentEventId ?? null,
|
|
3075
|
+
params.threadRef?.source ?? null,
|
|
3076
|
+
params.threadRef?.id ?? null,
|
|
3077
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3078
|
+
eventId
|
|
3079
|
+
);
|
|
3080
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3081
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3082
|
+
eventId
|
|
3083
|
+
);
|
|
2746
3084
|
const insertParticipant = db.prepare(
|
|
2747
3085
|
`INSERT INTO event_participants (
|
|
2748
3086
|
event_id, contact_id, role, directionality
|
|
@@ -2785,6 +3123,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
2785
3123
|
now
|
|
2786
3124
|
);
|
|
2787
3125
|
const eventId = Number(result.lastInsertRowid);
|
|
3126
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3127
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3128
|
+
eventId
|
|
3129
|
+
);
|
|
2788
3130
|
const insertParticipant = db.prepare(
|
|
2789
3131
|
`INSERT INTO event_participants (
|
|
2790
3132
|
event_id, contact_id, role, directionality
|
|
@@ -2805,20 +3147,6 @@ function insertDateAnchorEvent(db, params) {
|
|
|
2805
3147
|
return eventId;
|
|
2806
3148
|
}
|
|
2807
3149
|
|
|
2808
|
-
// src/events/receipts.ts
|
|
2809
|
-
function buildEventMutationReceipt(primary, partial) {
|
|
2810
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2811
|
-
return {
|
|
2812
|
-
primary,
|
|
2813
|
-
created: normalized.created,
|
|
2814
|
-
updated: normalized.updated,
|
|
2815
|
-
archived: normalized.archived,
|
|
2816
|
-
removed: normalized.removed,
|
|
2817
|
-
affectedLinks: normalized.affectedLinks,
|
|
2818
|
-
derivedEffects: normalized.derivedEffects
|
|
2819
|
-
};
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
3150
|
// src/graph/betweenness.ts
|
|
2823
3151
|
function computeNodeBetweenness(adjacency) {
|
|
2824
3152
|
const nodes = [...adjacency.keys()];
|
|
@@ -3417,6 +3745,8 @@ function deriveStateAfter(params) {
|
|
|
3417
3745
|
return params.stateBefore;
|
|
3418
3746
|
}
|
|
3419
3747
|
function deriveMomentKind(params) {
|
|
3748
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
3749
|
+
return null;
|
|
3420
3750
|
if (params.rankAfter > params.rankBefore) {
|
|
3421
3751
|
return "breakthrough";
|
|
3422
3752
|
}
|
|
@@ -3446,6 +3776,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
3446
3776
|
if (!link || link.is_structural !== 0) {
|
|
3447
3777
|
throw new AffinityStateError("relational link not found");
|
|
3448
3778
|
}
|
|
3779
|
+
db.prepare(
|
|
3780
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
3781
|
+
).run(
|
|
3782
|
+
params.linkId,
|
|
3783
|
+
JSON.stringify({
|
|
3784
|
+
rank: link.rank ?? 0,
|
|
3785
|
+
affinity: link.affinity ?? 0,
|
|
3786
|
+
trust: link.trust ?? 0,
|
|
3787
|
+
state: link.state ?? "active",
|
|
3788
|
+
cadence_days: link.cadence_days
|
|
3789
|
+
})
|
|
3790
|
+
);
|
|
3449
3791
|
const stateBefore = link.state ?? "active";
|
|
3450
3792
|
const linkKind = link.kind;
|
|
3451
3793
|
const rankBefore = link.rank ?? 0;
|
|
@@ -3490,7 +3832,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3490
3832
|
);
|
|
3491
3833
|
const weightedBase = base * massPenalty(
|
|
3492
3834
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
3493
|
-
) * salienceMultiplier;
|
|
3835
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
3494
3836
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
3495
3837
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
3496
3838
|
const affinityMass = clamp3(
|
|
@@ -3531,15 +3873,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
3531
3873
|
const latest = db.prepare(
|
|
3532
3874
|
`SELECT occurred_at
|
|
3533
3875
|
FROM link_event_effects
|
|
3534
|
-
WHERE link_id = ?
|
|
3876
|
+
WHERE link_id = ? AND counts_for_recency = 1 AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))
|
|
3535
3877
|
ORDER BY occurred_at DESC, event_id DESC
|
|
3536
3878
|
LIMIT 1`
|
|
3537
|
-
).get(params.linkId);
|
|
3879
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
3538
3880
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
3539
3881
|
1,
|
|
3540
3882
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
3541
3883
|
);
|
|
3542
|
-
const
|
|
3884
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
3885
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
3543
3886
|
kind: linkKind,
|
|
3544
3887
|
currentCadenceDays: cadenceBefore,
|
|
3545
3888
|
gapDays
|
|
@@ -3559,7 +3902,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
3559
3902
|
}
|
|
3560
3903
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
3561
3904
|
const trustDelta = trustAfter - trustBefore;
|
|
3562
|
-
const
|
|
3905
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
3906
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
3907
|
+
eventLabel
|
|
3908
|
+
);
|
|
3909
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
3563
3910
|
rankBefore,
|
|
3564
3911
|
rankAfter,
|
|
3565
3912
|
stateBefore,
|
|
@@ -3602,7 +3949,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
3602
3949
|
params.now,
|
|
3603
3950
|
params.now
|
|
3604
3951
|
);
|
|
3605
|
-
|
|
3952
|
+
db.prepare(
|
|
3953
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
3954
|
+
).run(
|
|
3955
|
+
JSON.stringify(params.participant),
|
|
3956
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3957
|
+
params.eventId,
|
|
3958
|
+
params.linkId
|
|
3959
|
+
);
|
|
3960
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
3606
3961
|
db.prepare(
|
|
3607
3962
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
3608
3963
|
).run(params.eventId, params.linkId);
|
|
@@ -3633,6 +3988,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3633
3988
|
|
|
3634
3989
|
// src/lib/errors/affinity_conflict_error.ts
|
|
3635
3990
|
var AffinityConflictError = class extends AffinityError {
|
|
3991
|
+
existingContactId;
|
|
3636
3992
|
code = "CONFLICT";
|
|
3637
3993
|
};
|
|
3638
3994
|
|
|
@@ -3687,6 +4043,8 @@ function buildLinkMutationReceipt(primary, partial) {
|
|
|
3687
4043
|
// src/links/seed_social_link.ts
|
|
3688
4044
|
function seedSocialLink(db, input) {
|
|
3689
4045
|
return withTransaction(db, () => {
|
|
4046
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
4047
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
3690
4048
|
if (!isRelationalLinkKind(input.kind)) {
|
|
3691
4049
|
throw new AffinityValidationError("invalid relational link kind");
|
|
3692
4050
|
}
|
|
@@ -3694,11 +4052,14 @@ function seedSocialLink(db, input) {
|
|
|
3694
4052
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
3695
4053
|
}
|
|
3696
4054
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
3697
|
-
|
|
4055
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
3698
4056
|
db,
|
|
3699
4057
|
input.fromContactId,
|
|
3700
4058
|
input.toContactId
|
|
3701
|
-
)
|
|
4059
|
+
);
|
|
4060
|
+
if (existing) {
|
|
4061
|
+
if (input.ifExists === "return")
|
|
4062
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
3702
4063
|
throw new AffinityConflictError(
|
|
3703
4064
|
"relational link already exists between these contacts"
|
|
3704
4065
|
);
|
|
@@ -3804,15 +4165,36 @@ function ensureRelationalLink(db, params) {
|
|
|
3804
4165
|
};
|
|
3805
4166
|
}
|
|
3806
4167
|
function applyJournalEventMechanics(db, params) {
|
|
4168
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
4169
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
4170
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
4171
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
3807
4172
|
const ownerId = requireOwnerContactId(db);
|
|
3808
|
-
const
|
|
4173
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
4174
|
+
const socialChannel = [
|
|
4175
|
+
"bluesky",
|
|
4176
|
+
"x",
|
|
4177
|
+
"twitter",
|
|
4178
|
+
"mastodon",
|
|
4179
|
+
"github",
|
|
4180
|
+
"instagram"
|
|
4181
|
+
].includes(String(bond?.channel));
|
|
4182
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
4183
|
+
const participants = params.participants.map(
|
|
4184
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
4185
|
+
);
|
|
4186
|
+
if (direction)
|
|
4187
|
+
db.prepare(
|
|
4188
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
4189
|
+
).run(direction, params.eventId);
|
|
4190
|
+
const participantIds = participants.map(
|
|
3809
4191
|
(participant) => participant.contactId
|
|
3810
4192
|
);
|
|
3811
4193
|
const hasOwner = participantIds.includes(ownerId);
|
|
3812
4194
|
const created = [];
|
|
3813
4195
|
const effectInputs = [];
|
|
3814
4196
|
if (hasOwner) {
|
|
3815
|
-
for (const participant of
|
|
4197
|
+
for (const participant of participants) {
|
|
3816
4198
|
if (participant.contactId === ownerId) {
|
|
3817
4199
|
continue;
|
|
3818
4200
|
}
|
|
@@ -3820,13 +4202,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3820
4202
|
fromContactId: ownerId,
|
|
3821
4203
|
toContactId: participant.contactId,
|
|
3822
4204
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
4205
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
3823
4206
|
now: params.now
|
|
3824
4207
|
});
|
|
3825
4208
|
created.push(...ensured.created);
|
|
3826
4209
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
3827
4210
|
}
|
|
3828
|
-
} else if (params.eventType === "observation" &&
|
|
3829
|
-
const [first, second] =
|
|
4211
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
4212
|
+
const [first, second] = participants;
|
|
3830
4213
|
if (first !== void 0 && second !== void 0) {
|
|
3831
4214
|
const a = Math.min(first.contactId, second.contactId);
|
|
3832
4215
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -3861,7 +4244,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3861
4244
|
eventType: params.eventType,
|
|
3862
4245
|
occurredAt: params.occurredAt,
|
|
3863
4246
|
significance: params.significance,
|
|
3864
|
-
participant:
|
|
4247
|
+
participant: {
|
|
4248
|
+
...input.participant,
|
|
4249
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
4250
|
+
},
|
|
3865
4251
|
provenance: params.provenance,
|
|
3866
4252
|
now: params.now
|
|
3867
4253
|
})
|
|
@@ -3885,6 +4271,8 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3885
4271
|
function recordCommitment(db, input) {
|
|
3886
4272
|
return withTransaction(db, () => {
|
|
3887
4273
|
validateSocialEventInput(input);
|
|
4274
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
4275
|
+
if (existing) return existing;
|
|
3888
4276
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
3889
4277
|
throw new AffinityValidationError(
|
|
3890
4278
|
"commitmentType must be promise or agreement"
|
|
@@ -3899,11 +4287,12 @@ function recordCommitment(db, input) {
|
|
|
3899
4287
|
assertOwnerParticipates(ownerId, ids);
|
|
3900
4288
|
const now = resolveNow(input.now);
|
|
3901
4289
|
const eventId = insertJournalEvent(db, {
|
|
4290
|
+
...input,
|
|
3902
4291
|
type: input.commitmentType,
|
|
3903
4292
|
occurredAt: input.occurredAt,
|
|
3904
|
-
|
|
4293
|
+
occurredOn: input.occurredOn,
|
|
3905
4294
|
significance: input.significance,
|
|
3906
|
-
momentKind: null,
|
|
4295
|
+
momentKind: input.momentKind ?? null,
|
|
3907
4296
|
participants: input.participants,
|
|
3908
4297
|
now
|
|
3909
4298
|
});
|
|
@@ -3912,15 +4301,25 @@ function recordCommitment(db, input) {
|
|
|
3912
4301
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
3913
4302
|
) VALUES (?, ?, ?, ?, ?)`
|
|
3914
4303
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
4304
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
4305
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
4306
|
+
eventId
|
|
4307
|
+
);
|
|
3915
4308
|
const mechanics = applyJournalEventMechanics(db, {
|
|
3916
4309
|
eventId,
|
|
3917
4310
|
eventType: input.commitmentType,
|
|
3918
4311
|
occurredAt: input.occurredAt,
|
|
4312
|
+
occurredOn: input.occurredOn,
|
|
3919
4313
|
significance: input.significance,
|
|
3920
4314
|
participants: input.participants,
|
|
3921
4315
|
provenance: input.provenance,
|
|
3922
4316
|
now
|
|
3923
4317
|
});
|
|
4318
|
+
if (input.momentKind !== void 0)
|
|
4319
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
4320
|
+
input.momentKind,
|
|
4321
|
+
eventId
|
|
4322
|
+
);
|
|
3924
4323
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
3925
4324
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
3926
4325
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -3994,6 +4393,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
3994
4393
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
3995
4394
|
validateSocialEventInput({
|
|
3996
4395
|
occurredAt,
|
|
4396
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
3997
4397
|
summary,
|
|
3998
4398
|
significance,
|
|
3999
4399
|
participants,
|
|
@@ -4010,6 +4410,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4010
4410
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4011
4411
|
type: eventType,
|
|
4012
4412
|
occurredAt,
|
|
4413
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4013
4414
|
summary,
|
|
4014
4415
|
significance,
|
|
4015
4416
|
momentKind: null,
|
|
@@ -4070,7 +4471,13 @@ function prepareParticipants(db, raw) {
|
|
|
4070
4471
|
}
|
|
4071
4472
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
4072
4473
|
const input = structuredClone(rawInput);
|
|
4073
|
-
const inputError = validateToolInput(
|
|
4474
|
+
const inputError = validateToolInput(
|
|
4475
|
+
manageCommitmentTool.inputSchema,
|
|
4476
|
+
input,
|
|
4477
|
+
"input",
|
|
4478
|
+
Date.now(),
|
|
4479
|
+
true
|
|
4480
|
+
);
|
|
4074
4481
|
if (inputError)
|
|
4075
4482
|
return toolFailure(
|
|
4076
4483
|
"protocol",
|
|
@@ -4100,6 +4507,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
4100
4507
|
"commitment"
|
|
4101
4508
|
);
|
|
4102
4509
|
}
|
|
4510
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
4511
|
+
return toolFailure(
|
|
4512
|
+
"protocol",
|
|
4513
|
+
"invalid_input",
|
|
4514
|
+
"Missing commitment.",
|
|
4515
|
+
"commitmentEventId is required for resolve."
|
|
4516
|
+
);
|
|
4517
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
4518
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
4519
|
+
return toolFailure(
|
|
4520
|
+
"domain",
|
|
4521
|
+
"invalid_input",
|
|
4522
|
+
"Invalid commitment.",
|
|
4523
|
+
"not a commitment event"
|
|
4524
|
+
);
|
|
4103
4525
|
return mutationToolResult(
|
|
4104
4526
|
"resolve",
|
|
4105
4527
|
resolveCommitment(
|
|
@@ -4138,15 +4560,21 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4138
4560
|
"promise",
|
|
4139
4561
|
"agreement"
|
|
4140
4562
|
]),
|
|
4563
|
+
...eventMetadataSchema,
|
|
4141
4564
|
occurredAt: integerSchema(
|
|
4142
4565
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
4143
4566
|
),
|
|
4144
4567
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
4145
4568
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
4146
4569
|
dueAt: integerSchema("Optional due timestamp."),
|
|
4570
|
+
dueOn: stringSchema("Due calendar day."),
|
|
4147
4571
|
participants: arraySchema(
|
|
4148
4572
|
objectSchema(
|
|
4149
4573
|
{
|
|
4574
|
+
contact: objectSchema(
|
|
4575
|
+
{ contactId: integerSchema("Contact id.") },
|
|
4576
|
+
["contactId"]
|
|
4577
|
+
),
|
|
4150
4578
|
contactId: integerSchema(
|
|
4151
4579
|
"Participant contact id. The owner contact MUST be included."
|
|
4152
4580
|
),
|
|
@@ -4159,13 +4587,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4159
4587
|
),
|
|
4160
4588
|
now: integerSchema("Optional timestamp override.")
|
|
4161
4589
|
},
|
|
4162
|
-
[
|
|
4163
|
-
"commitmentType",
|
|
4164
|
-
"occurredAt",
|
|
4165
|
-
"summary",
|
|
4166
|
-
"significance",
|
|
4167
|
-
"participants"
|
|
4168
|
-
],
|
|
4590
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
4169
4591
|
"Commitment payload. Required when action=record."
|
|
4170
4592
|
),
|
|
4171
4593
|
commitmentEventId: integerSchema(
|
|
@@ -4180,6 +4602,19 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4180
4602
|
now: integerSchema("Optional timestamp."),
|
|
4181
4603
|
resolvingEvent: objectSchema(
|
|
4182
4604
|
{
|
|
4605
|
+
occurredOn: stringSchema("Calendar day."),
|
|
4606
|
+
provenance: {},
|
|
4607
|
+
participants: arraySchema(
|
|
4608
|
+
objectSchema(
|
|
4609
|
+
{
|
|
4610
|
+
contactId: integerSchema("Contact id."),
|
|
4611
|
+
role: stringSchema("Role."),
|
|
4612
|
+
directionality: stringSchema("Directionality.")
|
|
4613
|
+
},
|
|
4614
|
+
["contactId", "role"]
|
|
4615
|
+
),
|
|
4616
|
+
"Resolving participants."
|
|
4617
|
+
),
|
|
4183
4618
|
type: stringSchema("Optional resolving event type."),
|
|
4184
4619
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
4185
4620
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -4260,30 +4695,46 @@ function deleteContact(db, locatorId, options) {
|
|
|
4260
4695
|
});
|
|
4261
4696
|
}
|
|
4262
4697
|
|
|
4263
|
-
// src/contacts/
|
|
4264
|
-
function
|
|
4265
|
-
|
|
4698
|
+
// src/contacts/export_rows.ts
|
|
4699
|
+
function exportRows(db, sql, ...params) {
|
|
4700
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
4266
4701
|
const result = {};
|
|
4267
4702
|
for (const [key, value] of Object.entries(row)) {
|
|
4268
4703
|
const name = key.replace(
|
|
4269
4704
|
/_([a-z])/g,
|
|
4270
4705
|
(_, letter) => letter.toUpperCase()
|
|
4271
4706
|
);
|
|
4272
|
-
result[name] =
|
|
4273
|
-
|
|
4707
|
+
result[name] = [
|
|
4708
|
+
"affects_bond",
|
|
4709
|
+
"is_owner",
|
|
4710
|
+
"is_structural",
|
|
4711
|
+
"verified",
|
|
4712
|
+
"counts_for_recency",
|
|
4713
|
+
"manual"
|
|
4714
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
4715
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
4274
4716
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4275
4717
|
if (key.endsWith("_json") && typeof value === "string")
|
|
4276
4718
|
result[name] = JSON.parse(value);
|
|
4277
4719
|
}
|
|
4278
4720
|
return result;
|
|
4279
4721
|
});
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
// src/contacts/export_contact.ts
|
|
4725
|
+
function exportContact(db, id) {
|
|
4726
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
4280
4727
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4281
4728
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4282
4729
|
const events = query(
|
|
4283
|
-
"SELECT DISTINCT e
|
|
4730
|
+
"SELECT DISTINCT e.*, COALESCE(e.parent_event_id, (SELECT parent.id FROM events parent WHERE parent.external_source=e.parent_source AND parent.external_id=e.parent_ref_id AND parent.deleted_at IS NULL)) AS parent_event_id, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
4284
4731
|
id
|
|
4285
4732
|
).map((event) => ({
|
|
4286
4733
|
...event,
|
|
4734
|
+
attributes: query(
|
|
4735
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
4736
|
+
event.id
|
|
4737
|
+
),
|
|
4287
4738
|
participants: query(
|
|
4288
4739
|
"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",
|
|
4289
4740
|
event.id
|
|
@@ -4328,11 +4779,33 @@ function exportContact(db, id) {
|
|
|
4328
4779
|
// src/contacts/export_affinity.ts
|
|
4329
4780
|
function exportAffinity(db) {
|
|
4330
4781
|
const now = Date.now();
|
|
4782
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
4783
|
+
const events = exportRows(
|
|
4784
|
+
db,
|
|
4785
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
4786
|
+
).map((event) => ({
|
|
4787
|
+
...event,
|
|
4788
|
+
attributes: exportRows(
|
|
4789
|
+
db,
|
|
4790
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
4791
|
+
event.id
|
|
4792
|
+
),
|
|
4793
|
+
participants: exportRows(
|
|
4794
|
+
db,
|
|
4795
|
+
"SELECT p.contact_id, p.role, c.name AS contact_name FROM event_participants p JOIN contacts c ON c.id = p.contact_id WHERE p.event_id = ? ORDER BY p.contact_id",
|
|
4796
|
+
event.id
|
|
4797
|
+
)
|
|
4798
|
+
}));
|
|
4331
4799
|
return {
|
|
4332
4800
|
version: 1,
|
|
4333
4801
|
exportedAt: now,
|
|
4334
4802
|
exportedAtIso: new Date(now).toISOString(),
|
|
4335
|
-
|
|
4803
|
+
events,
|
|
4804
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
4805
|
+
...record,
|
|
4806
|
+
eventIds: events2.map((event) => event.id),
|
|
4807
|
+
dateIds: dates.map((date) => date.id)
|
|
4808
|
+
}))
|
|
4336
4809
|
};
|
|
4337
4810
|
}
|
|
4338
4811
|
|
|
@@ -4405,7 +4878,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
4405
4878
|
}
|
|
4406
4879
|
if (to === "merged") {
|
|
4407
4880
|
throw new AffinityInvariantError(
|
|
4408
|
-
"use
|
|
4881
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
4409
4882
|
);
|
|
4410
4883
|
}
|
|
4411
4884
|
if (from === to) {
|
|
@@ -4457,67 +4930,6 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
4457
4930
|
});
|
|
4458
4931
|
}
|
|
4459
4932
|
|
|
4460
|
-
// src/contacts/create_contact.ts
|
|
4461
|
-
var KINDS = /* @__PURE__ */ new Set([
|
|
4462
|
-
"human",
|
|
4463
|
-
"group",
|
|
4464
|
-
"company",
|
|
4465
|
-
"team",
|
|
4466
|
-
"pet",
|
|
4467
|
-
"service",
|
|
4468
|
-
"other"
|
|
4469
|
-
]);
|
|
4470
|
-
function createContact(db, input) {
|
|
4471
|
-
return withTransaction(db, () => {
|
|
4472
|
-
const name = input.name.trim();
|
|
4473
|
-
if (name.length === 0) {
|
|
4474
|
-
throw new AffinityValidationError("name must be non-empty");
|
|
4475
|
-
}
|
|
4476
|
-
if (!KINDS.has(input.kind)) {
|
|
4477
|
-
throw new AffinityValidationError("invalid contact kind");
|
|
4478
|
-
}
|
|
4479
|
-
if (input.isOwner !== void 0) {
|
|
4480
|
-
throw new AffinityValidationError(
|
|
4481
|
-
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
4482
|
-
);
|
|
4483
|
-
}
|
|
4484
|
-
const now = resolveNow(input.now);
|
|
4485
|
-
if (input.bootstrapOwner) {
|
|
4486
|
-
const existing = db.prepare(
|
|
4487
|
-
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
4488
|
-
).get();
|
|
4489
|
-
if (existing !== void 0) {
|
|
4490
|
-
throw new AffinityConflictError(
|
|
4491
|
-
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
4492
|
-
);
|
|
4493
|
-
}
|
|
4494
|
-
}
|
|
4495
|
-
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
4496
|
-
const result = db.prepare(
|
|
4497
|
-
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
4498
|
-
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
4499
|
-
).run(name, input.kind, isOwner, now, now);
|
|
4500
|
-
const id = Number(result.lastInsertRowid);
|
|
4501
|
-
const row = getContactRowById(db, id);
|
|
4502
|
-
if (!row) {
|
|
4503
|
-
throw new AffinityInvariantError("inserted contact row not found");
|
|
4504
|
-
}
|
|
4505
|
-
refreshContactRollup(db, id, now);
|
|
4506
|
-
const primary = mapContactRowToContactListItem(row);
|
|
4507
|
-
return buildContactMutationReceipt(primary, {
|
|
4508
|
-
created: [{ kind: "contact", id }]
|
|
4509
|
-
});
|
|
4510
|
-
});
|
|
4511
|
-
}
|
|
4512
|
-
|
|
4513
|
-
// src/contacts/fold_search_text.ts
|
|
4514
|
-
function foldSearchText(value) {
|
|
4515
|
-
let folded = value.toLowerCase();
|
|
4516
|
-
if (/[^\u0020-\u007e]/.test(folded))
|
|
4517
|
-
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
4518
|
-
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
4933
|
// src/identities/queries.ts
|
|
4522
4934
|
function getIdentityRowById(db, id) {
|
|
4523
4935
|
const row = db.prepare(
|
|
@@ -4549,14 +4961,19 @@ function buildIdentityMutationReceipt(primary, partial) {
|
|
|
4549
4961
|
// src/identities/validators.ts
|
|
4550
4962
|
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
4551
4963
|
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
4552
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
4553
|
-
).get(normalizedKey) : db.prepare(
|
|
4554
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
4555
|
-
).get(
|
|
4964
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
4965
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey) : db.prepare(
|
|
4966
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL AND id != ?"
|
|
4967
|
+
).get(
|
|
4968
|
+
normalizedKey,
|
|
4969
|
+
legacyIdentityKey(normalizedKey),
|
|
4970
|
+
normalizedKey,
|
|
4971
|
+
excludeIdentityId
|
|
4972
|
+
);
|
|
4556
4973
|
if (row !== void 0) {
|
|
4557
4974
|
const owner = db.prepare(
|
|
4558
|
-
"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"
|
|
4559
|
-
).get(normalizedKey);
|
|
4975
|
+
"SELECT c.id, c.name FROM contacts c JOIN identities i ON i.contact_id = c.id WHERE (i.normalized_key IN (?, ?) OR (i.type='custom:atproto-did' AND ? = 'did:' || lower(i.value))) AND i.removed_at IS NULL"
|
|
4976
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey);
|
|
4560
4977
|
throw new AffinityConflictError(
|
|
4561
4978
|
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
4562
4979
|
);
|
|
@@ -4565,6 +4982,8 @@ function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
|
4565
4982
|
|
|
4566
4983
|
// src/identities/validate_identity_input.ts
|
|
4567
4984
|
var identityTypes = [
|
|
4985
|
+
"did",
|
|
4986
|
+
"atproto_did",
|
|
4568
4987
|
"email",
|
|
4569
4988
|
"phone",
|
|
4570
4989
|
"url",
|
|
@@ -4591,8 +5010,20 @@ var identityTypes = [
|
|
|
4591
5010
|
"account_id",
|
|
4592
5011
|
"alias"
|
|
4593
5012
|
];
|
|
4594
|
-
function validateIdentityInput(type,
|
|
5013
|
+
function validateIdentityInput(type, rawValue) {
|
|
4595
5014
|
const kind = type.toLowerCase();
|
|
5015
|
+
const value = normalizeIdentityValue(kind, rawValue);
|
|
5016
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(kind) && !/^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/.test(value))
|
|
5017
|
+
throw new AffinityValidationError(
|
|
5018
|
+
"did value must match did:<method>:<identifier> (for example did:plc:abc123)"
|
|
5019
|
+
);
|
|
5020
|
+
if (handleIdentityTypes.has(kind)) {
|
|
5021
|
+
const pattern = kind === "bluesky" ? /^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/ : ["x", "twitter"].includes(kind) ? /^[a-z0-9_]{1,15}$/ : kind === "mastodon" ? /^[a-z0-9_.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+$/ : /^[a-z0-9_.-]+$/;
|
|
5022
|
+
if (!pattern.test(value))
|
|
5023
|
+
throw new AffinityValidationError(
|
|
5024
|
+
`invalid ${kind === "handle" ? "" : `${kind} `}handle; use a handle without spaces or a URI${kind === "mastodon" ? ": user@host.tld" : kind === "bluesky" ? ": name.bsky.social" : " (leading @ is optional)"}`
|
|
5025
|
+
);
|
|
5026
|
+
}
|
|
4596
5027
|
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
4597
5028
|
throw new AffinityValidationError(
|
|
4598
5029
|
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
@@ -4631,12 +5062,27 @@ function addIdentity(db, contactId, input) {
|
|
|
4631
5062
|
throw new AffinityStateError("merged contact is read-only");
|
|
4632
5063
|
}
|
|
4633
5064
|
const type = input.type.trim();
|
|
4634
|
-
const value = input.value
|
|
5065
|
+
const value = normalizeIdentityValue(type, input.value);
|
|
4635
5066
|
if (type.length === 0 || value.length === 0) {
|
|
4636
5067
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
4637
5068
|
}
|
|
4638
5069
|
validateIdentityInput(type, value);
|
|
5070
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5071
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
4639
5072
|
const nk = normalizeIdentityKey(type, value);
|
|
5073
|
+
if (input.ifExists === "return") {
|
|
5074
|
+
const existing = db.prepare(
|
|
5075
|
+
"SELECT id FROM identities WHERE contact_id=? AND (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
5076
|
+
).get(contactId, nk, legacyIdentityKey(nk), nk);
|
|
5077
|
+
if (existing) {
|
|
5078
|
+
const row2 = getIdentityRowById(db, Number(existing.id));
|
|
5079
|
+
if (row2)
|
|
5080
|
+
return buildIdentityMutationReceipt(
|
|
5081
|
+
mapIdentityRowToIdentityRecord(row2),
|
|
5082
|
+
{}
|
|
5083
|
+
);
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
4640
5086
|
assertNoIdentityCollision(db, nk);
|
|
4641
5087
|
const now = resolveNow(input.now);
|
|
4642
5088
|
const verified = input.verified ? 1 : 0;
|
|
@@ -4670,31 +5116,239 @@ function addIdentity(db, contactId, input) {
|
|
|
4670
5116
|
});
|
|
4671
5117
|
}
|
|
4672
5118
|
|
|
4673
|
-
// src/
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
5119
|
+
// src/contacts/create_contact.ts
|
|
5120
|
+
var KINDS = /* @__PURE__ */ new Set([
|
|
5121
|
+
"human",
|
|
5122
|
+
"group",
|
|
5123
|
+
"company",
|
|
5124
|
+
"team",
|
|
5125
|
+
"pet",
|
|
5126
|
+
"service",
|
|
5127
|
+
"other"
|
|
5128
|
+
]);
|
|
5129
|
+
function createContact(db, input) {
|
|
5130
|
+
return withTransaction(db, () => {
|
|
5131
|
+
const name = input.name.trim();
|
|
5132
|
+
if (name.length === 0) {
|
|
5133
|
+
throw new AffinityValidationError("name must be non-empty");
|
|
5134
|
+
}
|
|
5135
|
+
if (!KINDS.has(input.kind)) {
|
|
5136
|
+
throw new AffinityValidationError("invalid contact kind");
|
|
5137
|
+
}
|
|
5138
|
+
if (input.isOwner !== void 0) {
|
|
5139
|
+
throw new AffinityValidationError(
|
|
5140
|
+
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
5141
|
+
);
|
|
5142
|
+
}
|
|
5143
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5144
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
5145
|
+
const matches = (input.identities ?? []).flatMap((identity) => {
|
|
5146
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
5147
|
+
const key = normalizeIdentityKey(identity.type, identity.value);
|
|
5148
|
+
const row2 = db.prepare(
|
|
5149
|
+
`SELECT c.id, c.name FROM identities i JOIN contacts c ON c.id=i.contact_id WHERE i.removed_at IS NULL AND c.deleted_at IS NULL AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))`
|
|
5150
|
+
).get(key, legacyIdentityKey(key), key);
|
|
5151
|
+
return row2 ? [{ ...row2, identity }] : [];
|
|
5152
|
+
});
|
|
5153
|
+
const match = matches[0];
|
|
5154
|
+
if (match) {
|
|
5155
|
+
if (input.ifExists === "return") {
|
|
5156
|
+
const existing = getContactRowById(db, match.id);
|
|
5157
|
+
if (!existing)
|
|
5158
|
+
throw new AffinityInvariantError("identity owner not found");
|
|
5159
|
+
const primary2 = mapContactRowToContactListItem(existing);
|
|
5160
|
+
return Object.assign(buildContactMutationReceipt(primary2, {}), {
|
|
5161
|
+
outcome: "no_op",
|
|
5162
|
+
matchedIdentities: matches.map((m) => m.identity)
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
const error = new AffinityConflictError(
|
|
5166
|
+
`identity ${normalizeIdentityKey(match.identity.type, match.identity.value).replace(":", " ")} already belongs to #${match.id} ${match.name}`
|
|
5167
|
+
);
|
|
5168
|
+
error.existingContactId = match.id;
|
|
5169
|
+
throw error;
|
|
5170
|
+
}
|
|
5171
|
+
const now = resolveNow(input.now);
|
|
5172
|
+
if (input.bootstrapOwner) {
|
|
5173
|
+
const existing = db.prepare(
|
|
5174
|
+
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
5175
|
+
).get();
|
|
5176
|
+
if (existing !== void 0) {
|
|
5177
|
+
throw new AffinityConflictError(
|
|
5178
|
+
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
5179
|
+
);
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
5183
|
+
const result = db.prepare(
|
|
5184
|
+
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
5185
|
+
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
5186
|
+
).run(name, input.kind, isOwner, now, now);
|
|
5187
|
+
const id = Number(result.lastInsertRowid);
|
|
5188
|
+
const row = getContactRowById(db, id);
|
|
5189
|
+
if (!row) {
|
|
5190
|
+
throw new AffinityInvariantError("inserted contact row not found");
|
|
5191
|
+
}
|
|
5192
|
+
refreshContactRollup(db, id, now);
|
|
5193
|
+
const primary = mapContactRowToContactListItem(row);
|
|
5194
|
+
const receipt = buildContactMutationReceipt(primary, {
|
|
5195
|
+
created: [{ kind: "contact", id }]
|
|
5196
|
+
});
|
|
5197
|
+
for (const identity of input.identities ?? [])
|
|
5198
|
+
receipt.created.push(...addIdentity(db, id, identity).created);
|
|
5199
|
+
return receipt;
|
|
5200
|
+
});
|
|
5201
|
+
}
|
|
5202
|
+
|
|
5203
|
+
// src/contacts/fold_search_text.ts
|
|
5204
|
+
function foldSearchText(value) {
|
|
5205
|
+
let folded = value.toLowerCase();
|
|
5206
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
5207
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
5208
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
// src/contacts/list_duplicate_candidates.ts
|
|
5212
|
+
function pairKey(leftContactId, rightContactId) {
|
|
5213
|
+
const left = Math.min(leftContactId, rightContactId);
|
|
5214
|
+
const right = Math.max(leftContactId, rightContactId);
|
|
5215
|
+
return `${left}:${right}`;
|
|
5216
|
+
}
|
|
5217
|
+
function normalizeName(name) {
|
|
5218
|
+
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
5219
|
+
}
|
|
5220
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
5221
|
+
const left = normalizeName(leftInput);
|
|
5222
|
+
const right = normalizeName(rightInput);
|
|
5223
|
+
if (left === right) {
|
|
5224
|
+
return 0.92;
|
|
5225
|
+
}
|
|
5226
|
+
if (left.length >= 3 && right.length >= 3) {
|
|
5227
|
+
if (left.includes(right) || right.includes(left)) {
|
|
5228
|
+
return 0.72;
|
|
5229
|
+
}
|
|
5230
|
+
const leftTokens = new Set(left.split(" "));
|
|
5231
|
+
const rightTokens = new Set(right.split(" "));
|
|
5232
|
+
const overlap = [...leftTokens].filter(
|
|
5233
|
+
(token) => rightTokens.has(token)
|
|
5234
|
+
).length;
|
|
5235
|
+
if (overlap > 0) {
|
|
5236
|
+
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
return 0;
|
|
5240
|
+
}
|
|
5241
|
+
function listDuplicateCandidates(db, filters, options) {
|
|
5242
|
+
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
5243
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5244
|
+
const subset = filters?.contactIds;
|
|
5245
|
+
const contacts = db.prepare(
|
|
5246
|
+
`SELECT id, name
|
|
5247
|
+
FROM contacts
|
|
5248
|
+
WHERE deleted_at IS NULL
|
|
5249
|
+
AND lifecycle_state != 'merged'`
|
|
5250
|
+
).all();
|
|
5251
|
+
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
5252
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
5253
|
+
const exactMatches = db.prepare(
|
|
5254
|
+
`SELECT
|
|
5255
|
+
i1.contact_id AS left_contact_id,
|
|
5256
|
+
i2.contact_id AS right_contact_id,
|
|
5257
|
+
i1.normalized_key
|
|
5258
|
+
FROM identities i1
|
|
5259
|
+
INNER JOIN identities i2
|
|
5260
|
+
ON i1.normalized_key = i2.normalized_key
|
|
5261
|
+
AND i1.contact_id < i2.contact_id
|
|
5262
|
+
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
5263
|
+
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
5264
|
+
WHERE i1.removed_at IS NULL
|
|
5265
|
+
AND i2.removed_at IS NULL
|
|
5266
|
+
AND c1.deleted_at IS NULL
|
|
5267
|
+
AND c2.deleted_at IS NULL
|
|
5268
|
+
AND c1.lifecycle_state != 'merged'
|
|
5269
|
+
AND c2.lifecycle_state != 'merged'`
|
|
5270
|
+
).all();
|
|
5271
|
+
for (const match of exactMatches) {
|
|
5272
|
+
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
5273
|
+
continue;
|
|
5274
|
+
}
|
|
5275
|
+
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
5276
|
+
leftContactId: match.left_contact_id,
|
|
5277
|
+
rightContactId: match.right_contact_id,
|
|
5278
|
+
matchReason: `exact identity ${match.normalized_key}`,
|
|
5279
|
+
matchScore: 1
|
|
5280
|
+
});
|
|
5281
|
+
}
|
|
5282
|
+
if (filters?.exactOnly !== true) {
|
|
5283
|
+
for (let index = 0; index < contacts.length; index += 1) {
|
|
5284
|
+
const left = contacts[index];
|
|
5285
|
+
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
4686
5286
|
continue;
|
|
4687
5287
|
}
|
|
4688
|
-
const
|
|
4689
|
-
|
|
5288
|
+
const leftName = normalizeName(left.name);
|
|
5289
|
+
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
5290
|
+
const right = contacts[offsetIndex];
|
|
5291
|
+
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
5292
|
+
continue;
|
|
5293
|
+
}
|
|
5294
|
+
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
5295
|
+
if (score <= 0) {
|
|
5296
|
+
continue;
|
|
5297
|
+
}
|
|
5298
|
+
const key = pairKey(left.id, right.id);
|
|
5299
|
+
if (!pairs.has(key)) {
|
|
5300
|
+
pairs.set(key, {
|
|
5301
|
+
leftContactId: left.id,
|
|
5302
|
+
rightContactId: right.id,
|
|
5303
|
+
matchReason: "name similarity",
|
|
5304
|
+
matchScore: score
|
|
5305
|
+
});
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
4690
5308
|
}
|
|
5309
|
+
}
|
|
5310
|
+
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
5311
|
+
const dismissedSet = new Set(
|
|
5312
|
+
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
5313
|
+
);
|
|
5314
|
+
const includeDismissed = filters?.includeDismissed === true;
|
|
5315
|
+
for (const candidate of pairs.values()) {
|
|
5316
|
+
if (dismissedSet.has(
|
|
5317
|
+
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
5318
|
+
)) {
|
|
5319
|
+
candidate.dismissed = true;
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
const minScore = filters?.minScore ?? 0;
|
|
5323
|
+
return [...pairs.values()].filter((candidate) => {
|
|
5324
|
+
if (candidate.matchScore < minScore) return false;
|
|
5325
|
+
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
5326
|
+
return true;
|
|
5327
|
+
}).sort((left, right) => {
|
|
5328
|
+
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
5329
|
+
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
5330
|
+
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
5331
|
+
}).slice(offset, offset + limit);
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
// src/tools/create_contact_tool_result.ts
|
|
5335
|
+
function createContactToolResult(db, input) {
|
|
5336
|
+
return withTransaction(db, () => {
|
|
5337
|
+
const receipt = createContact(db, input);
|
|
4691
5338
|
const result = mutationToolResult("create", receipt, "contact");
|
|
5339
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
5340
|
+
result.outcome = "no_op";
|
|
5341
|
+
Object.assign(result.data, {
|
|
5342
|
+
matchedIdentities: receipt.matchedIdentities
|
|
5343
|
+
});
|
|
5344
|
+
return result;
|
|
5345
|
+
}
|
|
4692
5346
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
4693
5347
|
const warnings = db.prepare(
|
|
4694
5348
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
4695
5349
|
).all(receipt.primary.id).flatMap((row) => {
|
|
4696
|
-
const score =
|
|
4697
|
-
return score
|
|
5350
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
5351
|
+
return score <= 0 ? [] : [
|
|
4698
5352
|
{
|
|
4699
5353
|
code: "possible_duplicate",
|
|
4700
5354
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -4777,6 +5431,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
4777
5431
|
return contact.result;
|
|
4778
5432
|
}
|
|
4779
5433
|
if (input.action === "delete") {
|
|
5434
|
+
if (contact.value.is_owner)
|
|
5435
|
+
return toolFailure(
|
|
5436
|
+
"domain",
|
|
5437
|
+
"conflict",
|
|
5438
|
+
"Cannot delete owner.",
|
|
5439
|
+
"The owner contact cannot be deleted"
|
|
5440
|
+
);
|
|
4780
5441
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
4781
5442
|
return toolSuccess("Permanently deleted contact.", {
|
|
4782
5443
|
action: "delete",
|
|
@@ -4850,7 +5511,11 @@ var manageContactTool = defineAffinityTool({
|
|
|
4850
5511
|
},
|
|
4851
5512
|
["type", "value"]
|
|
4852
5513
|
),
|
|
4853
|
-
"Optional identities to attach.
|
|
5514
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
5515
|
+
),
|
|
5516
|
+
ifExists: enumSchema(
|
|
5517
|
+
"Identity collision policy; return attaches nothing.",
|
|
5518
|
+
["error", "return"]
|
|
4854
5519
|
),
|
|
4855
5520
|
name: stringSchema("Contact name."),
|
|
4856
5521
|
kind: enumSchema("Contact kind.", [
|
|
@@ -5460,6 +6125,268 @@ var manageDuplicateDismissalTool = defineAffinityTool({
|
|
|
5460
6125
|
handler: manageDuplicateDismissalToolHandler
|
|
5461
6126
|
});
|
|
5462
6127
|
|
|
6128
|
+
// src/events/event_replay.ts
|
|
6129
|
+
function replayEventLinks(db, linkIds, now) {
|
|
6130
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
6131
|
+
for (const linkId of linkIds) {
|
|
6132
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
6133
|
+
if (!saved) continue;
|
|
6134
|
+
const baseline = JSON.parse(saved.state_json);
|
|
6135
|
+
const effects = db.prepare(
|
|
6136
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
6137
|
+
).all(linkId);
|
|
6138
|
+
const overrides = db.prepare(
|
|
6139
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
6140
|
+
).all(linkId);
|
|
6141
|
+
let overrideIndex = 0;
|
|
6142
|
+
const applyOverrides = (beforeId) => {
|
|
6143
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
6144
|
+
const override = overrides[overrideIndex];
|
|
6145
|
+
if (override)
|
|
6146
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
6147
|
+
override.state,
|
|
6148
|
+
linkId
|
|
6149
|
+
);
|
|
6150
|
+
overrideIndex++;
|
|
6151
|
+
}
|
|
6152
|
+
};
|
|
6153
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
6154
|
+
db.prepare(
|
|
6155
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
6156
|
+
).run(
|
|
6157
|
+
baseline.rank,
|
|
6158
|
+
baseline.affinity,
|
|
6159
|
+
baseline.trust,
|
|
6160
|
+
baseline.state,
|
|
6161
|
+
baseline.cadence_days,
|
|
6162
|
+
now,
|
|
6163
|
+
linkId
|
|
6164
|
+
);
|
|
6165
|
+
for (const effect of effects) {
|
|
6166
|
+
eventIds.add(effect.event_id);
|
|
6167
|
+
applyOverrides(effect.event_id);
|
|
6168
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
6169
|
+
continue;
|
|
6170
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
6171
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
6172
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
6173
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
6174
|
+
applyEventEffectToLink(db, {
|
|
6175
|
+
linkId,
|
|
6176
|
+
eventId: event.id,
|
|
6177
|
+
eventType: event.type,
|
|
6178
|
+
occurredAt: event.occurredAt,
|
|
6179
|
+
significance: event.significance,
|
|
6180
|
+
participant,
|
|
6181
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
6182
|
+
now: effect.created_at
|
|
6183
|
+
});
|
|
6184
|
+
}
|
|
6185
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
6186
|
+
refreshLinkRollup(db, linkId, now);
|
|
6187
|
+
}
|
|
6188
|
+
for (const eventId of eventIds)
|
|
6189
|
+
db.prepare(
|
|
6190
|
+
"UPDATE events SET moment_kind=COALESCE(declared_moment_kind,(SELECT moment_kind FROM link_event_effects WHERE event_id=events.id AND moment_kind IS NOT NULL ORDER BY link_id LIMIT 1)) WHERE id=?"
|
|
6191
|
+
).run(eventId);
|
|
6192
|
+
refreshAllBridgeScores(db, now);
|
|
6193
|
+
}
|
|
6194
|
+
|
|
6195
|
+
// src/events/manage_event.ts
|
|
6196
|
+
function requireEventTarget(db, target) {
|
|
6197
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
6198
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
6199
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
6200
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
6201
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
6202
|
+
return event;
|
|
6203
|
+
}
|
|
6204
|
+
function affected(db, id) {
|
|
6205
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
6206
|
+
}
|
|
6207
|
+
function reviseEvent(db, target, patch) {
|
|
6208
|
+
return withTransaction(db, () => {
|
|
6209
|
+
const allowed = [
|
|
6210
|
+
"summary",
|
|
6211
|
+
"content",
|
|
6212
|
+
"occurredAt",
|
|
6213
|
+
"significance",
|
|
6214
|
+
"channel",
|
|
6215
|
+
"type",
|
|
6216
|
+
"externalRef"
|
|
6217
|
+
];
|
|
6218
|
+
for (const key of Object.keys(patch))
|
|
6219
|
+
if (!allowed.includes(key))
|
|
6220
|
+
throw new AffinityValidationError(
|
|
6221
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
6222
|
+
);
|
|
6223
|
+
const event = requireEventTarget(db, target);
|
|
6224
|
+
const next = { ...event, ...patch };
|
|
6225
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
6226
|
+
validateEventMetadata(metadata);
|
|
6227
|
+
Object.assign(next, metadata);
|
|
6228
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
6229
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
6230
|
+
if (patch.occurredAt !== void 0)
|
|
6231
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
6232
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
6233
|
+
throw new AffinityValidationError(
|
|
6234
|
+
"significance must be an integer from 1 to 10"
|
|
6235
|
+
);
|
|
6236
|
+
const interactions = [
|
|
6237
|
+
"conversation",
|
|
6238
|
+
"activity",
|
|
6239
|
+
"gift",
|
|
6240
|
+
"support",
|
|
6241
|
+
"conflict",
|
|
6242
|
+
"correction"
|
|
6243
|
+
];
|
|
6244
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
6245
|
+
throw new AffinityValidationError(
|
|
6246
|
+
"type must stay within the same event kind"
|
|
6247
|
+
);
|
|
6248
|
+
if (patch.externalRef && event.externalRef)
|
|
6249
|
+
throw new AffinityConflictError(
|
|
6250
|
+
"externalRef can only be set when absent"
|
|
6251
|
+
);
|
|
6252
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
6253
|
+
throw new AffinityConflictError(
|
|
6254
|
+
"externalRef already belongs to an event"
|
|
6255
|
+
);
|
|
6256
|
+
const now = Date.now();
|
|
6257
|
+
const links = affected(db, event.id);
|
|
6258
|
+
db.prepare(
|
|
6259
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
6260
|
+
).run(
|
|
6261
|
+
next.summary.trim(),
|
|
6262
|
+
next.content ?? null,
|
|
6263
|
+
next.occurredAt,
|
|
6264
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
6265
|
+
next.significance,
|
|
6266
|
+
next.channel ?? null,
|
|
6267
|
+
next.type,
|
|
6268
|
+
next.externalRef?.source ?? null,
|
|
6269
|
+
next.externalRef?.id ?? null,
|
|
6270
|
+
now,
|
|
6271
|
+
event.id
|
|
6272
|
+
);
|
|
6273
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
6274
|
+
db.prepare(
|
|
6275
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
6276
|
+
).run(patch.type, event.id);
|
|
6277
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
6278
|
+
replayEventLinks(db, links, now);
|
|
6279
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
6280
|
+
updated: [{ kind: "event", id: event.id }],
|
|
6281
|
+
affectedLinks: links
|
|
6282
|
+
});
|
|
6283
|
+
});
|
|
6284
|
+
}
|
|
6285
|
+
function deleteEvent(db, target, options) {
|
|
6286
|
+
return withTransaction(db, () => {
|
|
6287
|
+
if (options?.confirm !== true)
|
|
6288
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
6289
|
+
const event = requireEventTarget(db, target);
|
|
6290
|
+
const links = affected(db, event.id);
|
|
6291
|
+
for (const table of [
|
|
6292
|
+
"event_participants",
|
|
6293
|
+
"attributes",
|
|
6294
|
+
"open_commitments",
|
|
6295
|
+
"upcoming_occurrences",
|
|
6296
|
+
"link_event_effects"
|
|
6297
|
+
])
|
|
6298
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
6299
|
+
db.prepare(
|
|
6300
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
6301
|
+
).run(event.id);
|
|
6302
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
6303
|
+
replayEventLinks(db, links, Date.now());
|
|
6304
|
+
return buildEventMutationReceipt(event, {
|
|
6305
|
+
removed: [{ kind: "event", id: event.id }],
|
|
6306
|
+
affectedLinks: links
|
|
6307
|
+
});
|
|
6308
|
+
});
|
|
6309
|
+
}
|
|
6310
|
+
|
|
6311
|
+
// src/tools/manage_event_tool.ts
|
|
6312
|
+
function manageEventToolHandler(db, raw) {
|
|
6313
|
+
const input = structuredClone(raw);
|
|
6314
|
+
const error = validateToolInput(
|
|
6315
|
+
manageEventTool.inputSchema,
|
|
6316
|
+
input,
|
|
6317
|
+
"input",
|
|
6318
|
+
Date.now(),
|
|
6319
|
+
true
|
|
6320
|
+
);
|
|
6321
|
+
if (error)
|
|
6322
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
6323
|
+
return withToolHandling(() => {
|
|
6324
|
+
const target = {
|
|
6325
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
6326
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
6327
|
+
};
|
|
6328
|
+
return mutationToolResult(
|
|
6329
|
+
input.action,
|
|
6330
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
6331
|
+
"event"
|
|
6332
|
+
);
|
|
6333
|
+
}, "Event management failed.");
|
|
6334
|
+
}
|
|
6335
|
+
var manageEventTool = defineAffinityTool({
|
|
6336
|
+
name: manageEventToolName,
|
|
6337
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
6338
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
6339
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
6340
|
+
sideEffects: "writes_state",
|
|
6341
|
+
readOnly: false,
|
|
6342
|
+
supportsClarification: false,
|
|
6343
|
+
targetKinds: ["event"],
|
|
6344
|
+
inputDescriptions: {
|
|
6345
|
+
action: "revise or delete",
|
|
6346
|
+
eventId: "Exact event id",
|
|
6347
|
+
ref: "External event reference",
|
|
6348
|
+
patch: "Fields to revise",
|
|
6349
|
+
confirm: "Required true for deletion"
|
|
6350
|
+
},
|
|
6351
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
6352
|
+
inputSchema: objectSchema(
|
|
6353
|
+
{
|
|
6354
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
6355
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
6356
|
+
ref: externalRefSchema,
|
|
6357
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
6358
|
+
patch: objectSchema(
|
|
6359
|
+
{
|
|
6360
|
+
summary: stringSchema("Short non-empty label."),
|
|
6361
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
6362
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
6363
|
+
significance: integerSchema("Significance 1..10."),
|
|
6364
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
6365
|
+
type: enumSchema("Type within the same event kind.", [
|
|
6366
|
+
"conversation",
|
|
6367
|
+
"activity",
|
|
6368
|
+
"gift",
|
|
6369
|
+
"support",
|
|
6370
|
+
"conflict",
|
|
6371
|
+
"correction",
|
|
6372
|
+
"observation",
|
|
6373
|
+
"milestone",
|
|
6374
|
+
"transaction",
|
|
6375
|
+
"promise",
|
|
6376
|
+
"agreement",
|
|
6377
|
+
"date_anchor"
|
|
6378
|
+
]),
|
|
6379
|
+
externalRef: externalRefSchema
|
|
6380
|
+
},
|
|
6381
|
+
[],
|
|
6382
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
6383
|
+
)
|
|
6384
|
+
},
|
|
6385
|
+
["action"]
|
|
6386
|
+
),
|
|
6387
|
+
handler: manageEventToolHandler
|
|
6388
|
+
});
|
|
6389
|
+
|
|
5463
6390
|
// src/identities/remove_identity.ts
|
|
5464
6391
|
function removeIdentity(db, identityId, removedAt) {
|
|
5465
6392
|
return withTransaction(db, () => {
|
|
@@ -5507,7 +6434,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
5507
6434
|
});
|
|
5508
6435
|
}
|
|
5509
6436
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
5510
|
-
const nextValue =
|
|
6437
|
+
const nextValue = normalizeIdentityValue(
|
|
6438
|
+
nextType,
|
|
6439
|
+
patch.value ?? row.value
|
|
6440
|
+
);
|
|
5511
6441
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
5512
6442
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5513
6443
|
}
|
|
@@ -5566,6 +6496,10 @@ function contactLocatorSchema6(description) {
|
|
|
5566
6496
|
contactId: integerSchema("Exact contact id."),
|
|
5567
6497
|
identity: objectSchema(
|
|
5568
6498
|
{
|
|
6499
|
+
ifExists: enumSchema(
|
|
6500
|
+
"Return an existing identity on this contact without changes.",
|
|
6501
|
+
["error", "return"]
|
|
6502
|
+
),
|
|
5569
6503
|
type: stringSchema(
|
|
5570
6504
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
5571
6505
|
),
|
|
@@ -5662,6 +6596,10 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
5662
6596
|
),
|
|
5663
6597
|
input: objectSchema(
|
|
5664
6598
|
{
|
|
6599
|
+
ifExists: enumSchema(
|
|
6600
|
+
"Return an existing identity on this contact without changes.",
|
|
6601
|
+
["error", "return"]
|
|
6602
|
+
),
|
|
5665
6603
|
type: stringSchema(
|
|
5666
6604
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
5667
6605
|
),
|
|
@@ -5723,6 +6661,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
5723
6661
|
now,
|
|
5724
6662
|
linkId
|
|
5725
6663
|
);
|
|
6664
|
+
db.prepare(
|
|
6665
|
+
"INSERT INTO event_link_state_changes(link_id,after_event_id,state) SELECT ?, COALESCE(MAX(event_id),0), ? FROM link_event_effects WHERE link_id=?"
|
|
6666
|
+
).run(linkId, state, linkId);
|
|
5726
6667
|
refreshLinkRollup(db, linkId, now);
|
|
5727
6668
|
if (state === "archived" || row.state === "archived") {
|
|
5728
6669
|
refreshAllBridgeScores(db, now);
|
|
@@ -6357,6 +7298,8 @@ var ALLOWED = /* @__PURE__ */ new Set([
|
|
|
6357
7298
|
function recordInteraction(db, input) {
|
|
6358
7299
|
return withTransaction(db, () => {
|
|
6359
7300
|
validateSocialEventInput(input);
|
|
7301
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7302
|
+
if (existing) return existing;
|
|
6360
7303
|
if (!ALLOWED.has(input.type)) {
|
|
6361
7304
|
throw new AffinityValidationError("invalid interaction event type");
|
|
6362
7305
|
}
|
|
@@ -6366,11 +7309,12 @@ function recordInteraction(db, input) {
|
|
|
6366
7309
|
assertOwnerParticipates(ownerId, ids);
|
|
6367
7310
|
const now = resolveNow(input.now);
|
|
6368
7311
|
const eventId = insertJournalEvent(db, {
|
|
7312
|
+
...input,
|
|
6369
7313
|
type: input.type,
|
|
6370
7314
|
occurredAt: input.occurredAt,
|
|
6371
|
-
|
|
7315
|
+
occurredOn: input.occurredOn,
|
|
6372
7316
|
significance: input.significance,
|
|
6373
|
-
momentKind: null,
|
|
7317
|
+
momentKind: input.momentKind ?? null,
|
|
6374
7318
|
participants: input.participants,
|
|
6375
7319
|
now
|
|
6376
7320
|
});
|
|
@@ -6378,12 +7322,24 @@ function recordInteraction(db, input) {
|
|
|
6378
7322
|
eventId,
|
|
6379
7323
|
eventType: input.type,
|
|
6380
7324
|
occurredAt: input.occurredAt,
|
|
7325
|
+
occurredOn: input.occurredOn,
|
|
6381
7326
|
significance: input.significance,
|
|
6382
7327
|
participants: input.participants,
|
|
6383
7328
|
provenance: input.provenance,
|
|
6384
7329
|
now
|
|
6385
7330
|
});
|
|
7331
|
+
if (input.momentKind !== void 0)
|
|
7332
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7333
|
+
input.momentKind,
|
|
7334
|
+
eventId
|
|
7335
|
+
);
|
|
6386
7336
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7337
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7338
|
+
{
|
|
7339
|
+
code: "future_event",
|
|
7340
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7341
|
+
}
|
|
7342
|
+
] : void 0,
|
|
6387
7343
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6388
7344
|
affectedLinks: mechanics.affectedLinks,
|
|
6389
7345
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6395,17 +7351,20 @@ function recordInteraction(db, input) {
|
|
|
6395
7351
|
function recordMilestone(db, input) {
|
|
6396
7352
|
return withTransaction(db, () => {
|
|
6397
7353
|
validateSocialEventInput(input);
|
|
7354
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7355
|
+
if (existing) return existing;
|
|
6398
7356
|
const ownerId = requireOwnerContactId(db);
|
|
6399
7357
|
const ids = input.participants.map((p) => p.contactId);
|
|
6400
7358
|
assertParticipantContactsLive(db, ids);
|
|
6401
7359
|
assertOwnerParticipates(ownerId, ids);
|
|
6402
7360
|
const now = resolveNow(input.now);
|
|
6403
7361
|
const eventId = insertJournalEvent(db, {
|
|
7362
|
+
...input,
|
|
6404
7363
|
type: "milestone",
|
|
6405
7364
|
occurredAt: input.occurredAt,
|
|
6406
|
-
|
|
7365
|
+
occurredOn: input.occurredOn,
|
|
6407
7366
|
significance: input.significance,
|
|
6408
|
-
momentKind: null,
|
|
7367
|
+
momentKind: input.momentKind ?? null,
|
|
6409
7368
|
participants: input.participants,
|
|
6410
7369
|
now
|
|
6411
7370
|
});
|
|
@@ -6413,12 +7372,24 @@ function recordMilestone(db, input) {
|
|
|
6413
7372
|
eventId,
|
|
6414
7373
|
eventType: "milestone",
|
|
6415
7374
|
occurredAt: input.occurredAt,
|
|
7375
|
+
occurredOn: input.occurredOn,
|
|
6416
7376
|
significance: input.significance,
|
|
6417
7377
|
participants: input.participants,
|
|
6418
7378
|
provenance: input.provenance,
|
|
6419
7379
|
now
|
|
6420
7380
|
});
|
|
7381
|
+
if (input.momentKind !== void 0)
|
|
7382
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7383
|
+
input.momentKind,
|
|
7384
|
+
eventId
|
|
7385
|
+
);
|
|
6421
7386
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7387
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7388
|
+
{
|
|
7389
|
+
code: "future_event",
|
|
7390
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7391
|
+
}
|
|
7392
|
+
] : void 0,
|
|
6422
7393
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6423
7394
|
affectedLinks: mechanics.affectedLinks,
|
|
6424
7395
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6430,15 +7401,18 @@ function recordMilestone(db, input) {
|
|
|
6430
7401
|
function recordObservation(db, input) {
|
|
6431
7402
|
return withTransaction(db, () => {
|
|
6432
7403
|
validateSocialEventInput(input);
|
|
7404
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7405
|
+
if (existing) return existing;
|
|
6433
7406
|
const ids = input.participants.map((p) => p.contactId);
|
|
6434
7407
|
assertParticipantContactsLive(db, ids);
|
|
6435
7408
|
const now = resolveNow(input.now);
|
|
6436
7409
|
const eventId = insertJournalEvent(db, {
|
|
7410
|
+
...input,
|
|
6437
7411
|
type: "observation",
|
|
6438
7412
|
occurredAt: input.occurredAt,
|
|
6439
|
-
|
|
7413
|
+
occurredOn: input.occurredOn,
|
|
6440
7414
|
significance: input.significance,
|
|
6441
|
-
momentKind: null,
|
|
7415
|
+
momentKind: input.momentKind ?? null,
|
|
6442
7416
|
participants: input.participants,
|
|
6443
7417
|
now
|
|
6444
7418
|
});
|
|
@@ -6446,12 +7420,24 @@ function recordObservation(db, input) {
|
|
|
6446
7420
|
eventId,
|
|
6447
7421
|
eventType: "observation",
|
|
6448
7422
|
occurredAt: input.occurredAt,
|
|
7423
|
+
occurredOn: input.occurredOn,
|
|
6449
7424
|
significance: input.significance,
|
|
6450
7425
|
participants: input.participants,
|
|
6451
7426
|
provenance: input.provenance,
|
|
6452
7427
|
now
|
|
6453
7428
|
});
|
|
7429
|
+
if (input.momentKind !== void 0)
|
|
7430
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7431
|
+
input.momentKind,
|
|
7432
|
+
eventId
|
|
7433
|
+
);
|
|
6454
7434
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7435
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7436
|
+
{
|
|
7437
|
+
code: "future_event",
|
|
7438
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7439
|
+
}
|
|
7440
|
+
] : void 0,
|
|
6455
7441
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6456
7442
|
affectedLinks: mechanics.affectedLinks,
|
|
6457
7443
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6463,17 +7449,20 @@ function recordObservation(db, input) {
|
|
|
6463
7449
|
function recordTransaction(db, input) {
|
|
6464
7450
|
return withTransaction(db, () => {
|
|
6465
7451
|
validateSocialEventInput(input);
|
|
7452
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7453
|
+
if (existing) return existing;
|
|
6466
7454
|
const ownerId = requireOwnerContactId(db);
|
|
6467
7455
|
const ids = input.participants.map((p) => p.contactId);
|
|
6468
7456
|
assertParticipantContactsLive(db, ids);
|
|
6469
7457
|
assertOwnerParticipates(ownerId, ids);
|
|
6470
7458
|
const now = resolveNow(input.now);
|
|
6471
7459
|
const eventId = insertJournalEvent(db, {
|
|
7460
|
+
...input,
|
|
6472
7461
|
type: "transaction",
|
|
6473
7462
|
occurredAt: input.occurredAt,
|
|
6474
|
-
|
|
7463
|
+
occurredOn: input.occurredOn,
|
|
6475
7464
|
significance: input.significance,
|
|
6476
|
-
momentKind: null,
|
|
7465
|
+
momentKind: input.momentKind ?? null,
|
|
6477
7466
|
participants: input.participants,
|
|
6478
7467
|
now
|
|
6479
7468
|
});
|
|
@@ -6481,12 +7470,24 @@ function recordTransaction(db, input) {
|
|
|
6481
7470
|
eventId,
|
|
6482
7471
|
eventType: "transaction",
|
|
6483
7472
|
occurredAt: input.occurredAt,
|
|
7473
|
+
occurredOn: input.occurredOn,
|
|
6484
7474
|
significance: input.significance,
|
|
6485
7475
|
participants: input.participants,
|
|
6486
7476
|
provenance: input.provenance,
|
|
6487
7477
|
now
|
|
6488
7478
|
});
|
|
7479
|
+
if (input.momentKind !== void 0)
|
|
7480
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7481
|
+
input.momentKind,
|
|
7482
|
+
eventId
|
|
7483
|
+
);
|
|
6489
7484
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7485
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7486
|
+
{
|
|
7487
|
+
code: "future_event",
|
|
7488
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7489
|
+
}
|
|
7490
|
+
] : void 0,
|
|
6490
7491
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6491
7492
|
affectedLinks: mechanics.affectedLinks,
|
|
6492
7493
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6495,11 +7496,13 @@ function recordTransaction(db, input) {
|
|
|
6495
7496
|
}
|
|
6496
7497
|
|
|
6497
7498
|
// src/tools/record_event_tool.ts
|
|
6498
|
-
function normalizeParticipant2(p) {
|
|
7499
|
+
function normalizeParticipant2(db, p) {
|
|
6499
7500
|
let contactId = p.contactId;
|
|
6500
7501
|
if (contactId === void 0 && p.contact != null) {
|
|
6501
7502
|
const loc = p.contact;
|
|
6502
|
-
|
|
7503
|
+
const resolved = resolveContactLocator(db, loc);
|
|
7504
|
+
if (!resolved.ok) throw resolved.result;
|
|
7505
|
+
contactId = resolved.value.id;
|
|
6503
7506
|
}
|
|
6504
7507
|
const result = {
|
|
6505
7508
|
contactId,
|
|
@@ -6517,12 +7520,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
6517
7520
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
6518
7521
|
}
|
|
6519
7522
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
6520
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
7523
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
6521
7524
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
6522
7525
|
}
|
|
6523
7526
|
function recordEventToolHandler(db, rawInput) {
|
|
6524
7527
|
const input = structuredClone(rawInput);
|
|
6525
|
-
const inputError = validateToolInput(
|
|
7528
|
+
const inputError = validateToolInput(
|
|
7529
|
+
recordEventTool.inputSchema,
|
|
7530
|
+
input,
|
|
7531
|
+
"input",
|
|
7532
|
+
Date.now(),
|
|
7533
|
+
true
|
|
7534
|
+
);
|
|
6526
7535
|
if (inputError)
|
|
6527
7536
|
return toolFailure(
|
|
6528
7537
|
"protocol",
|
|
@@ -6602,6 +7611,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
6602
7611
|
"correction"
|
|
6603
7612
|
]
|
|
6604
7613
|
),
|
|
7614
|
+
...eventMetadataSchema,
|
|
6605
7615
|
occurredAt: integerSchema(
|
|
6606
7616
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
6607
7617
|
),
|
|
@@ -6610,6 +7620,19 @@ var recordEventTool = defineAffinityTool({
|
|
|
6610
7620
|
participants: arraySchema(
|
|
6611
7621
|
objectSchema(
|
|
6612
7622
|
{
|
|
7623
|
+
contact: objectSchema(
|
|
7624
|
+
{
|
|
7625
|
+
contactId: integerSchema("Contact id."),
|
|
7626
|
+
identity: objectSchema(
|
|
7627
|
+
{
|
|
7628
|
+
type: stringSchema("Identity type."),
|
|
7629
|
+
value: stringSchema("Identity value.")
|
|
7630
|
+
},
|
|
7631
|
+
["type", "value"]
|
|
7632
|
+
)
|
|
7633
|
+
},
|
|
7634
|
+
[]
|
|
7635
|
+
),
|
|
6613
7636
|
contactId: integerSchema(
|
|
6614
7637
|
"Participant contact id. The owner contact MUST be included."
|
|
6615
7638
|
),
|
|
@@ -6633,7 +7656,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
6633
7656
|
),
|
|
6634
7657
|
now: integerSchema("Optional timestamp override.")
|
|
6635
7658
|
},
|
|
6636
|
-
["occurredAt", "
|
|
7659
|
+
["occurredAt", "significance", "participants"],
|
|
6637
7660
|
"Event payload. Always required. Interaction events also require `type`."
|
|
6638
7661
|
)
|
|
6639
7662
|
},
|
|
@@ -6649,7 +7672,10 @@ function contactAttributeClauses(filters) {
|
|
|
6649
7672
|
const params = [];
|
|
6650
7673
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
6651
7674
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
6652
|
-
for (const filter of [
|
|
7675
|
+
for (const filter of [
|
|
7676
|
+
...attributes,
|
|
7677
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
7678
|
+
]) {
|
|
6653
7679
|
const separator = filter.indexOf("=");
|
|
6654
7680
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
6655
7681
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -6693,143 +7719,22 @@ function listContacts(db, filters, options) {
|
|
|
6693
7719
|
(
|
|
6694
7720
|
SELECT value
|
|
6695
7721
|
FROM identities i
|
|
6696
|
-
WHERE i.contact_id = contacts.id AND i.removed_at IS NULL
|
|
6697
|
-
ORDER BY i.id
|
|
6698
|
-
LIMIT 1
|
|
6699
|
-
) AS primary_identity
|
|
6700
|
-
FROM contacts
|
|
6701
|
-
WHERE ${where}
|
|
6702
|
-
ORDER BY name ASC
|
|
6703
|
-
LIMIT ? OFFSET ?`
|
|
6704
|
-
).all(...params, limit, offset);
|
|
6705
|
-
return rows.map((r) => {
|
|
6706
|
-
const item = mapContactRowToContactListItem(r);
|
|
6707
|
-
return {
|
|
6708
|
-
...item,
|
|
6709
|
-
primaryIdentity: r.primary_identity ?? null
|
|
6710
|
-
};
|
|
6711
|
-
});
|
|
6712
|
-
}
|
|
6713
|
-
|
|
6714
|
-
// src/contacts/list_duplicate_candidates.ts
|
|
6715
|
-
function pairKey(leftContactId, rightContactId) {
|
|
6716
|
-
const left = Math.min(leftContactId, rightContactId);
|
|
6717
|
-
const right = Math.max(leftContactId, rightContactId);
|
|
6718
|
-
return `${left}:${right}`;
|
|
6719
|
-
}
|
|
6720
|
-
function normalizeName(name) {
|
|
6721
|
-
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6722
|
-
}
|
|
6723
|
-
function fuzzyNameScore(left, right) {
|
|
6724
|
-
if (left === right) {
|
|
6725
|
-
return 0.92;
|
|
6726
|
-
}
|
|
6727
|
-
if (left.length >= 3 && right.length >= 3) {
|
|
6728
|
-
if (left.includes(right) || right.includes(left)) {
|
|
6729
|
-
return 0.72;
|
|
6730
|
-
}
|
|
6731
|
-
const leftTokens = new Set(left.split(" "));
|
|
6732
|
-
const rightTokens = new Set(right.split(" "));
|
|
6733
|
-
const overlap = [...leftTokens].filter(
|
|
6734
|
-
(token) => rightTokens.has(token)
|
|
6735
|
-
).length;
|
|
6736
|
-
if (overlap > 0) {
|
|
6737
|
-
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
6738
|
-
}
|
|
6739
|
-
}
|
|
6740
|
-
return 0;
|
|
6741
|
-
}
|
|
6742
|
-
function listDuplicateCandidates(db, filters, options) {
|
|
6743
|
-
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
6744
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
6745
|
-
const subset = filters?.contactIds;
|
|
6746
|
-
const contacts = db.prepare(
|
|
6747
|
-
`SELECT id, name
|
|
6748
|
-
FROM contacts
|
|
6749
|
-
WHERE deleted_at IS NULL
|
|
6750
|
-
AND lifecycle_state != 'merged'`
|
|
6751
|
-
).all();
|
|
6752
|
-
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
6753
|
-
const pairs = /* @__PURE__ */ new Map();
|
|
6754
|
-
const exactMatches = db.prepare(
|
|
6755
|
-
`SELECT
|
|
6756
|
-
i1.contact_id AS left_contact_id,
|
|
6757
|
-
i2.contact_id AS right_contact_id,
|
|
6758
|
-
i1.normalized_key
|
|
6759
|
-
FROM identities i1
|
|
6760
|
-
INNER JOIN identities i2
|
|
6761
|
-
ON i1.normalized_key = i2.normalized_key
|
|
6762
|
-
AND i1.contact_id < i2.contact_id
|
|
6763
|
-
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
6764
|
-
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
6765
|
-
WHERE i1.removed_at IS NULL
|
|
6766
|
-
AND i2.removed_at IS NULL
|
|
6767
|
-
AND c1.deleted_at IS NULL
|
|
6768
|
-
AND c2.deleted_at IS NULL
|
|
6769
|
-
AND c1.lifecycle_state != 'merged'
|
|
6770
|
-
AND c2.lifecycle_state != 'merged'`
|
|
6771
|
-
).all();
|
|
6772
|
-
for (const match of exactMatches) {
|
|
6773
|
-
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
6774
|
-
continue;
|
|
6775
|
-
}
|
|
6776
|
-
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
6777
|
-
leftContactId: match.left_contact_id,
|
|
6778
|
-
rightContactId: match.right_contact_id,
|
|
6779
|
-
matchReason: `exact identity ${match.normalized_key}`,
|
|
6780
|
-
matchScore: 1
|
|
6781
|
-
});
|
|
6782
|
-
}
|
|
6783
|
-
if (filters?.exactOnly !== true) {
|
|
6784
|
-
for (let index = 0; index < contacts.length; index += 1) {
|
|
6785
|
-
const left = contacts[index];
|
|
6786
|
-
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
6787
|
-
continue;
|
|
6788
|
-
}
|
|
6789
|
-
const leftName = normalizeName(left.name);
|
|
6790
|
-
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
6791
|
-
const right = contacts[offsetIndex];
|
|
6792
|
-
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
6793
|
-
continue;
|
|
6794
|
-
}
|
|
6795
|
-
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
6796
|
-
if (score <= 0) {
|
|
6797
|
-
continue;
|
|
6798
|
-
}
|
|
6799
|
-
const key = pairKey(left.id, right.id);
|
|
6800
|
-
if (!pairs.has(key)) {
|
|
6801
|
-
pairs.set(key, {
|
|
6802
|
-
leftContactId: left.id,
|
|
6803
|
-
rightContactId: right.id,
|
|
6804
|
-
matchReason: "name similarity",
|
|
6805
|
-
matchScore: score
|
|
6806
|
-
});
|
|
6807
|
-
}
|
|
6808
|
-
}
|
|
6809
|
-
}
|
|
6810
|
-
}
|
|
6811
|
-
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
6812
|
-
const dismissedSet = new Set(
|
|
6813
|
-
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
6814
|
-
);
|
|
6815
|
-
const includeDismissed = filters?.includeDismissed === true;
|
|
6816
|
-
for (const candidate of pairs.values()) {
|
|
6817
|
-
if (dismissedSet.has(
|
|
6818
|
-
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
6819
|
-
)) {
|
|
6820
|
-
candidate.dismissed = true;
|
|
6821
|
-
}
|
|
6822
|
-
}
|
|
6823
|
-
const minScore = filters?.minScore ?? 0;
|
|
6824
|
-
return [...pairs.values()].filter((candidate) => {
|
|
6825
|
-
if (candidate.matchScore < minScore) return false;
|
|
6826
|
-
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
6827
|
-
return true;
|
|
6828
|
-
}).sort((left, right) => {
|
|
6829
|
-
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
6830
|
-
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
6831
|
-
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
6832
|
-
}).slice(offset, offset + limit);
|
|
7722
|
+
WHERE i.contact_id = contacts.id AND i.removed_at IS NULL
|
|
7723
|
+
ORDER BY i.id
|
|
7724
|
+
LIMIT 1
|
|
7725
|
+
) AS primary_identity
|
|
7726
|
+
FROM contacts
|
|
7727
|
+
WHERE ${where}
|
|
7728
|
+
ORDER BY name ASC
|
|
7729
|
+
LIMIT ? OFFSET ?`
|
|
7730
|
+
).all(...params, limit, offset);
|
|
7731
|
+
return rows.map((r) => {
|
|
7732
|
+
const item = mapContactRowToContactListItem(r);
|
|
7733
|
+
return {
|
|
7734
|
+
...item,
|
|
7735
|
+
primaryIdentity: r.primary_identity ?? null
|
|
7736
|
+
};
|
|
7737
|
+
});
|
|
6833
7738
|
}
|
|
6834
7739
|
|
|
6835
7740
|
// src/dates/list_upcoming_dates.ts
|
|
@@ -6865,12 +7770,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
6865
7770
|
}
|
|
6866
7771
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
6867
7772
|
const params = [];
|
|
6868
|
-
const since = options?.since ??
|
|
7773
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
6869
7774
|
clauses.push("uo.occurs_on >= ?");
|
|
6870
7775
|
params.push(since);
|
|
6871
7776
|
let maxOccurs = options?.until;
|
|
6872
7777
|
if (filters?.horizonDays !== void 0) {
|
|
6873
|
-
const anchor =
|
|
7778
|
+
const anchor = since;
|
|
6874
7779
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
6875
7780
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
6876
7781
|
}
|
|
@@ -6917,6 +7822,70 @@ function listUpcomingDates(db, filters, options) {
|
|
|
6917
7822
|
return rows.map((r) => mapUpcomingRowToUpcomingDateRecord(r));
|
|
6918
7823
|
}
|
|
6919
7824
|
|
|
7825
|
+
// src/events/event_discovery.ts
|
|
7826
|
+
function searchEvents(db, query, options) {
|
|
7827
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
7828
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
7829
|
+
if (!tokens.length) return [];
|
|
7830
|
+
const rows = db.prepare(
|
|
7831
|
+
"SELECT id,type,occurred_at,occurred_on,CAST(summary AS BLOB) AS summary,CAST(content AS BLOB) AS content,external_source,external_id,channel FROM events WHERE deleted_at IS NULL ORDER BY occurred_at DESC,id DESC"
|
|
7832
|
+
).all();
|
|
7833
|
+
return rows.flatMap((row) => {
|
|
7834
|
+
const fields = {
|
|
7835
|
+
summary: decodeEventText(row.summary),
|
|
7836
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
7837
|
+
source: row.external_source,
|
|
7838
|
+
id: row.external_id,
|
|
7839
|
+
channel: row.channel
|
|
7840
|
+
};
|
|
7841
|
+
const folded = Object.entries(fields).map(
|
|
7842
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
7843
|
+
);
|
|
7844
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
7845
|
+
return [];
|
|
7846
|
+
return [
|
|
7847
|
+
{
|
|
7848
|
+
kind: "event",
|
|
7849
|
+
id: row.id,
|
|
7850
|
+
type: row.type,
|
|
7851
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
7852
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
7853
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
7854
|
+
...row.external_source && row.external_id ? {
|
|
7855
|
+
externalRef: {
|
|
7856
|
+
source: row.external_source,
|
|
7857
|
+
id: row.external_id
|
|
7858
|
+
}
|
|
7859
|
+
} : {}
|
|
7860
|
+
}
|
|
7861
|
+
];
|
|
7862
|
+
}).slice(offset, offset + limit);
|
|
7863
|
+
}
|
|
7864
|
+
function getEventThread(db, source, id, options) {
|
|
7865
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
7866
|
+
const root = getEventByExternalRef(db, source, id);
|
|
7867
|
+
if (!root) return [];
|
|
7868
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
7869
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
7870
|
+
WHERE e.deleted_at IS NULL AND (e.parent_event_id=p.id OR (e.parent_source=p.external_source AND e.parent_ref_id=p.external_id) OR (e.thread_source=? AND e.thread_id=?))
|
|
7871
|
+
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) AND (? IS NULL OR e.type IN (SELECT value FROM json_each(?))) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
7872
|
+
root.id,
|
|
7873
|
+
root.externalRef?.source ?? null,
|
|
7874
|
+
root.externalRef?.id ?? null,
|
|
7875
|
+
options?.channel ?? null,
|
|
7876
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
7877
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
7878
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
7879
|
+
limit,
|
|
7880
|
+
offset
|
|
7881
|
+
);
|
|
7882
|
+
return batchLoadEventRecords(
|
|
7883
|
+
db,
|
|
7884
|
+
rows.map((r) => r.id),
|
|
7885
|
+
options?.includeContent ?? false
|
|
7886
|
+
);
|
|
7887
|
+
}
|
|
7888
|
+
|
|
6920
7889
|
// src/graph/get_affinity_chart.ts
|
|
6921
7890
|
function getAffinityChart(db, options) {
|
|
6922
7891
|
const contactClauses = [
|
|
@@ -7102,21 +8071,6 @@ function listProgressionReadiness(db, filters, options) {
|
|
|
7102
8071
|
}
|
|
7103
8072
|
|
|
7104
8073
|
// src/links/list_radar.ts
|
|
7105
|
-
function deriveRadarReason(drift, recency, normalizedRank3) {
|
|
7106
|
-
if (drift > 0.5) {
|
|
7107
|
-
return "Significantly overdue for contact";
|
|
7108
|
-
}
|
|
7109
|
-
if (drift > 0.2) {
|
|
7110
|
-
return "Drifting \u2014 approaching overdue";
|
|
7111
|
-
}
|
|
7112
|
-
if (recency < 0.3 && normalizedRank3 > 0.3) {
|
|
7113
|
-
return "Important link losing recency";
|
|
7114
|
-
}
|
|
7115
|
-
if (normalizedRank3 > 0.5 && recency < 0.5) {
|
|
7116
|
-
return "High-rank link needs attention";
|
|
7117
|
-
}
|
|
7118
|
-
return "Routine check-in recommended";
|
|
7119
|
-
}
|
|
7120
8074
|
function listRadar(db, filters, options) {
|
|
7121
8075
|
assertDefaultOrdering("listRadar", options);
|
|
7122
8076
|
const ownerId = findOwnerContactId(db);
|
|
@@ -7124,7 +8078,10 @@ function listRadar(db, filters, options) {
|
|
|
7124
8078
|
return [];
|
|
7125
8079
|
}
|
|
7126
8080
|
const now = resolveNow(options?.now);
|
|
7127
|
-
const
|
|
8081
|
+
const today = localCalendarDate(now);
|
|
8082
|
+
const lastContact = `(SELECT MAX(le.occurred_at) FROM link_event_effects le JOIN events ev ON ev.id = le.event_id WHERE le.link_id = l.id AND le.counts_for_recency = 1 AND ev.deleted_at IS NULL AND (le.occurred_at <= ${now} OR ev.occurred_on <= '${today}'))`;
|
|
8083
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
8084
|
+
const overdue = `EXISTS (SELECT 1 FROM open_commitments oc JOIN events e ON e.id = oc.event_id JOIN event_participants ep ON ep.event_id = e.id WHERE ep.contact_id = CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END AND e.deleted_at IS NULL AND oc.resolved_at IS NULL AND (CASE WHEN oc.due_on IS NOT NULL AND oc.due_at = CAST(strftime('%s', oc.due_on) AS INTEGER) * 1000 + 86399999 THEN oc.due_on < '${today}' ELSE oc.due_at < ${now} END))`;
|
|
7128
8085
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
7129
8086
|
const clauses = [
|
|
7130
8087
|
"l.removed_at IS NULL",
|
|
@@ -7149,10 +8106,23 @@ function listRadar(db, filters, options) {
|
|
|
7149
8106
|
if (options?.includeObserved === false) {
|
|
7150
8107
|
clauses.push("l.kind != 'observed'");
|
|
7151
8108
|
}
|
|
8109
|
+
if (options?.includeDormant !== true)
|
|
8110
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
8111
|
+
if (filters?.includeLost !== true)
|
|
8112
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
8113
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
8114
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
8115
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
8116
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
8117
|
+
AND (ev.affects_bond=0 OR (ev.type='observation' AND ev.significance<=2) OR json_extract(ev.provenance_json, '$.ambient')=1 OR ep.directionality='owner_initiated' OR EXISTS (SELECT 1 FROM event_participants op WHERE op.event_id=ev.id AND op.contact_id=${ownerId} AND op.directionality='owner_initiated'))
|
|
8118
|
+
))`);
|
|
7152
8119
|
const where = clauses.join(" AND ");
|
|
7153
8120
|
const rows = db.prepare(
|
|
7154
8121
|
`SELECT
|
|
7155
8122
|
l.id,
|
|
8123
|
+
l.cadence_days,
|
|
8124
|
+
${lastContact} AS last_contact,
|
|
8125
|
+
${drift} AS drift,
|
|
7156
8126
|
${overdue} AS overdue_commitment,
|
|
7157
8127
|
l.from_contact_id,
|
|
7158
8128
|
l.to_contact_id,
|
|
@@ -7166,23 +8136,44 @@ function listRadar(db, filters, options) {
|
|
|
7166
8136
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
7167
8137
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
7168
8138
|
WHERE ${where}
|
|
7169
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
8139
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
7170
8140
|
LIMIT ? OFFSET ?`
|
|
7171
8141
|
).all(...params, limit, offset);
|
|
7172
|
-
return rows.map((row) =>
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
8142
|
+
return rows.map((row) => {
|
|
8143
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
8144
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
8145
|
+
`SELECT e.summary, oc.due_at, oc.due_on FROM open_commitments oc JOIN events e ON e.id = oc.event_id JOIN event_participants ep ON ep.event_id = e.id WHERE ep.contact_id = ? AND e.deleted_at IS NULL AND oc.resolved_at IS NULL AND ${OVERDUE_SQL} ORDER BY oc.due_at LIMIT 1`
|
|
8146
|
+
).get(contactId, today, now) : void 0;
|
|
8147
|
+
const reasons = [
|
|
8148
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
8149
|
+
...row.drift ? ["drift"] : []
|
|
8150
|
+
];
|
|
8151
|
+
const snippets = [
|
|
8152
|
+
...promise ? [
|
|
8153
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
8154
|
+
] : [],
|
|
8155
|
+
...row.drift ? [
|
|
8156
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
8157
|
+
] : []
|
|
8158
|
+
];
|
|
8159
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
8160
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
8161
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
8162
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
8163
|
+
trust: row.trust ?? 0,
|
|
8164
|
+
rank: row.rank ?? 0
|
|
8165
|
+
});
|
|
8166
|
+
return {
|
|
8167
|
+
linkId: row.id,
|
|
8168
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
8169
|
+
reasons,
|
|
8170
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
8171
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
8172
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
8173
|
+
trust: row.trust ?? 0,
|
|
8174
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
8175
|
+
};
|
|
8176
|
+
});
|
|
7186
8177
|
}
|
|
7187
8178
|
|
|
7188
8179
|
// src/merges/get_merge_history.ts
|
|
@@ -7252,13 +8243,40 @@ function enrichReviewItem(db, item) {
|
|
|
7252
8243
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
7253
8244
|
}
|
|
7254
8245
|
if (Array.isArray(item.participants)) {
|
|
7255
|
-
result.
|
|
7256
|
-
(p) =>
|
|
8246
|
+
result.participants = item.participants.map(
|
|
8247
|
+
(p) => ({
|
|
8248
|
+
id: p.contactId,
|
|
8249
|
+
name: name(p.contactId),
|
|
8250
|
+
role: p.role
|
|
8251
|
+
})
|
|
7257
8252
|
);
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
8253
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
8254
|
+
result[key] = void 0;
|
|
8255
|
+
}
|
|
8256
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
8257
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
8258
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
8259
|
+
if (result.title === result.summary) result.title = void 0;
|
|
8260
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
8261
|
+
for (const key of [
|
|
8262
|
+
"affinity",
|
|
8263
|
+
"trust",
|
|
8264
|
+
"rank",
|
|
8265
|
+
"recencyScore",
|
|
8266
|
+
"driftPriority",
|
|
8267
|
+
"readinessScore",
|
|
8268
|
+
"normalizedRank",
|
|
8269
|
+
"reciprocityScore",
|
|
8270
|
+
"radarScore",
|
|
8271
|
+
"subtitle",
|
|
8272
|
+
"snippet",
|
|
8273
|
+
"targetRef",
|
|
8274
|
+
"occursOnDate"
|
|
8275
|
+
])
|
|
8276
|
+
result[key] = void 0;
|
|
8277
|
+
} else if (item.kind === "link") {
|
|
8278
|
+
result.recommendedReason = void 0;
|
|
8279
|
+
result.snippet = void 0;
|
|
7262
8280
|
}
|
|
7263
8281
|
if (typeof item.leftContactId === "number")
|
|
7264
8282
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -7287,6 +8305,8 @@ function summarizeCount(count, singular) {
|
|
|
7287
8305
|
// src/tools/review_affinity_tool.ts
|
|
7288
8306
|
function buildReadOptions(input) {
|
|
7289
8307
|
return {
|
|
8308
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
8309
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
7290
8310
|
...input.now === void 0 ? {} : { now: input.now },
|
|
7291
8311
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7292
8312
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -7423,7 +8443,7 @@ function momentItem(moment) {
|
|
|
7423
8443
|
id: moment.eventId,
|
|
7424
8444
|
title: moment.summary,
|
|
7425
8445
|
subtitle: moment.momentKind,
|
|
7426
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8446
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
7427
8447
|
};
|
|
7428
8448
|
}
|
|
7429
8449
|
function commitmentItem(commitment) {
|
|
@@ -7433,7 +8453,7 @@ function commitmentItem(commitment) {
|
|
|
7433
8453
|
id: commitment.eventId,
|
|
7434
8454
|
title: commitment.summary,
|
|
7435
8455
|
subtitle: commitment.type,
|
|
7436
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8456
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
7437
8457
|
};
|
|
7438
8458
|
}
|
|
7439
8459
|
function radarItem(radar) {
|
|
@@ -7449,6 +8469,7 @@ function radarItem(radar) {
|
|
|
7449
8469
|
function upcomingDateItem(date) {
|
|
7450
8470
|
return {
|
|
7451
8471
|
...date,
|
|
8472
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
7452
8473
|
occursAt: date.occursOn,
|
|
7453
8474
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
7454
8475
|
kind: date.targetRef.kind,
|
|
@@ -7486,6 +8507,9 @@ function compactFilters(input) {
|
|
|
7486
8507
|
);
|
|
7487
8508
|
}
|
|
7488
8509
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
8510
|
+
return reviewPage(db, rawInput, false);
|
|
8511
|
+
}
|
|
8512
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
7489
8513
|
const input = structuredClone(rawInput);
|
|
7490
8514
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
7491
8515
|
if (inputError)
|
|
@@ -7500,6 +8524,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7500
8524
|
if (invalid) {
|
|
7501
8525
|
return invalid;
|
|
7502
8526
|
}
|
|
8527
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
8528
|
+
const limit = input.limit ?? 50;
|
|
8529
|
+
const offset = input.offset ?? 0;
|
|
8530
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
|
|
8531
|
+
return toolFailure(
|
|
8532
|
+
"protocol",
|
|
8533
|
+
"invalid_input",
|
|
8534
|
+
"Invalid pagination.",
|
|
8535
|
+
"limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
|
|
8536
|
+
);
|
|
8537
|
+
const all = [];
|
|
8538
|
+
let page;
|
|
8539
|
+
do {
|
|
8540
|
+
page = reviewPage(
|
|
8541
|
+
db,
|
|
8542
|
+
{ ...input, limit: 250, offset: all.length },
|
|
8543
|
+
true
|
|
8544
|
+
);
|
|
8545
|
+
if (!page.ok) return page;
|
|
8546
|
+
all.push(...page.data.items);
|
|
8547
|
+
} while (page.data.items.length === 250);
|
|
8548
|
+
const items2 = all.slice(offset, offset + limit);
|
|
8549
|
+
return toolSuccess(
|
|
8550
|
+
summarizeCount(items2.length, "item"),
|
|
8551
|
+
{
|
|
8552
|
+
view: input.view,
|
|
8553
|
+
items: items2,
|
|
8554
|
+
count: items2.length,
|
|
8555
|
+
total: all.length,
|
|
8556
|
+
hasMore: offset + items2.length < all.length,
|
|
8557
|
+
appliedFilters: compactFilters(input)
|
|
8558
|
+
},
|
|
8559
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
8560
|
+
);
|
|
8561
|
+
}
|
|
7503
8562
|
const options = buildReadOptions(input);
|
|
7504
8563
|
let items = [];
|
|
7505
8564
|
let chart;
|
|
@@ -7525,12 +8584,26 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7525
8584
|
{
|
|
7526
8585
|
...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
|
|
7527
8586
|
...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
|
|
7528
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
8587
|
+
...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
|
|
7529
8588
|
...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
|
|
7530
8589
|
},
|
|
7531
8590
|
options
|
|
7532
8591
|
).map(duplicateCandidateItem);
|
|
7533
8592
|
break;
|
|
8593
|
+
case "events.thread": {
|
|
8594
|
+
if (!input.ref)
|
|
8595
|
+
return toolFailure(
|
|
8596
|
+
"protocol",
|
|
8597
|
+
"invalid_input",
|
|
8598
|
+
"Missing thread reference.",
|
|
8599
|
+
"ref is required for events.thread."
|
|
8600
|
+
);
|
|
8601
|
+
items = getEventThread(db, input.ref.source, input.ref.id, {
|
|
8602
|
+
...options,
|
|
8603
|
+
...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
|
|
8604
|
+
}).map(toEventListToolItem);
|
|
8605
|
+
break;
|
|
8606
|
+
}
|
|
7534
8607
|
case "events.contact_journal": {
|
|
7535
8608
|
const locator = input.contact;
|
|
7536
8609
|
if (locator === void 0) {
|
|
@@ -7672,6 +8745,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7672
8745
|
items = listRadar(
|
|
7673
8746
|
db,
|
|
7674
8747
|
{
|
|
8748
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
7675
8749
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
7676
8750
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
7677
8751
|
},
|
|
@@ -7721,6 +8795,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7721
8795
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
7722
8796
|
break;
|
|
7723
8797
|
}
|
|
8798
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
8799
|
+
items = items.map((item) => {
|
|
8800
|
+
const content = getEventById(
|
|
8801
|
+
db,
|
|
8802
|
+
Number(item.eventId ?? item.id)
|
|
8803
|
+
)?.content;
|
|
8804
|
+
return content === void 0 ? item : { ...item, content };
|
|
8805
|
+
});
|
|
7724
8806
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
7725
8807
|
return toolSuccess(
|
|
7726
8808
|
summarizeCount(items.length, "item"),
|
|
@@ -7732,7 +8814,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7732
8814
|
...chart === void 0 ? {} : { chart }
|
|
7733
8815
|
},
|
|
7734
8816
|
{
|
|
7735
|
-
entities: items,
|
|
8817
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
7736
8818
|
warnings: items.length === 0 ? [
|
|
7737
8819
|
toolWarning(
|
|
7738
8820
|
"empty_result",
|
|
@@ -7778,15 +8860,21 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7778
8860
|
includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
|
|
7779
8861
|
since: "Optional lower timestamp bound for time-based views.",
|
|
7780
8862
|
until: "Optional upper timestamp bound for time-based views.",
|
|
7781
|
-
limit: "Optional result limit.",
|
|
8863
|
+
limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
|
|
7782
8864
|
offset: "Optional result offset."
|
|
7783
8865
|
},
|
|
7784
8866
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
7785
8867
|
inputSchema: objectSchema(
|
|
7786
8868
|
{
|
|
8869
|
+
ref: externalRefSchema,
|
|
8870
|
+
includeContent: booleanSchema(
|
|
8871
|
+
"Include verbatim content in event rows; defaults to false."
|
|
8872
|
+
),
|
|
8873
|
+
channel: stringSchema("Filter events by channel."),
|
|
7787
8874
|
view: enumSchema("Which review surface to return.", [
|
|
7788
8875
|
"contacts.list",
|
|
7789
8876
|
"contacts.duplicates",
|
|
8877
|
+
"events.thread",
|
|
7790
8878
|
"events.contact_journal",
|
|
7791
8879
|
"events.link_timeline",
|
|
7792
8880
|
"events.moments",
|
|
@@ -7841,7 +8929,20 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7841
8929
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
7842
8930
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
7843
8931
|
eventTypes: arraySchema(
|
|
7844
|
-
|
|
8932
|
+
enumSchema("Event type.", [
|
|
8933
|
+
"conversation",
|
|
8934
|
+
"activity",
|
|
8935
|
+
"gift",
|
|
8936
|
+
"support",
|
|
8937
|
+
"milestone",
|
|
8938
|
+
"observation",
|
|
8939
|
+
"conflict",
|
|
8940
|
+
"correction",
|
|
8941
|
+
"transaction",
|
|
8942
|
+
"promise",
|
|
8943
|
+
"agreement",
|
|
8944
|
+
"date_anchor"
|
|
8945
|
+
]),
|
|
7845
8946
|
"Optional event-type filter."
|
|
7846
8947
|
),
|
|
7847
8948
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -7864,13 +8965,17 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7864
8965
|
]),
|
|
7865
8966
|
horizonDays: integerSchema("Optional horizon-days filter."),
|
|
7866
8967
|
exactOnly: booleanSchema("Whether duplicate review is exact-only."),
|
|
7867
|
-
minScore: numberSchema(
|
|
8968
|
+
minScore: numberSchema(
|
|
8969
|
+
"Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
|
|
8970
|
+
),
|
|
7868
8971
|
includeDismissed: booleanSchema(
|
|
7869
8972
|
"Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
|
|
7870
8973
|
),
|
|
7871
8974
|
since: integerSchema("Optional lower timestamp bound."),
|
|
7872
8975
|
until: integerSchema("Optional upper timestamp bound."),
|
|
7873
|
-
limit: integerSchema(
|
|
8976
|
+
limit: integerSchema(
|
|
8977
|
+
"Optional result limit, 1..10000 (default 50); use offset for paging."
|
|
8978
|
+
),
|
|
7874
8979
|
offset: integerSchema("Optional result offset.")
|
|
7875
8980
|
},
|
|
7876
8981
|
["view"],
|
|
@@ -7881,7 +8986,7 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7881
8986
|
|
|
7882
8987
|
// src/contacts/search_contacts.ts
|
|
7883
8988
|
function searchContacts(db, query, filters, options) {
|
|
7884
|
-
const q = foldSearchText(query.trim());
|
|
8989
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
7885
8990
|
if (!q) return [];
|
|
7886
8991
|
assertDefaultOrdering("searchContacts", options);
|
|
7887
8992
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -7901,6 +9006,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
7901
9006
|
const tokens = q.split(/\s+/);
|
|
7902
9007
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
7903
9008
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
9009
|
+
const suffix = digits.replace(/^0/, "");
|
|
7904
9010
|
const attributeFilters = contactAttributeClauses(filters);
|
|
7905
9011
|
const clauses = [
|
|
7906
9012
|
"deleted_at IS NULL",
|
|
@@ -7936,7 +9042,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
7936
9042
|
)
|
|
7937
9043
|
];
|
|
7938
9044
|
const phoneFields = phone ? values.filter(
|
|
7939
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
9045
|
+
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits) || field.source === "identity:phone" && suffix.length >= 6 && field.text.replace(/\D/g, "").endsWith(suffix)
|
|
7940
9046
|
) : [];
|
|
7941
9047
|
if (!phoneFields.length && !tokens.every(
|
|
7942
9048
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -7966,6 +9072,9 @@ function searchContacts(db, query, filters, options) {
|
|
|
7966
9072
|
|
|
7967
9073
|
// src/tools/search_affinity_tool.ts
|
|
7968
9074
|
function searchAffinityToolHandler(db, rawInput) {
|
|
9075
|
+
return searchPage(db, rawInput, false);
|
|
9076
|
+
}
|
|
9077
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
7969
9078
|
const input = structuredClone(rawInput);
|
|
7970
9079
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
7971
9080
|
if (inputError)
|
|
@@ -7976,11 +9085,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
7976
9085
|
inputError
|
|
7977
9086
|
);
|
|
7978
9087
|
return withToolHandling(() => {
|
|
9088
|
+
if (!pageOnly) {
|
|
9089
|
+
const limit = input.limit ?? 50;
|
|
9090
|
+
const offset = input.offset ?? 0;
|
|
9091
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
9092
|
+
return toolFailure(
|
|
9093
|
+
"protocol",
|
|
9094
|
+
"invalid_input",
|
|
9095
|
+
"Invalid pagination.",
|
|
9096
|
+
"limit must be 1..250; offset must be non-negative"
|
|
9097
|
+
);
|
|
9098
|
+
const all = [];
|
|
9099
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
9100
|
+
for (const scope of scopes) {
|
|
9101
|
+
let page;
|
|
9102
|
+
let nextOffset = 0;
|
|
9103
|
+
do {
|
|
9104
|
+
page = searchPage(
|
|
9105
|
+
db,
|
|
9106
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
9107
|
+
true
|
|
9108
|
+
);
|
|
9109
|
+
if (!page.ok) return page;
|
|
9110
|
+
all.push(...page.data.items);
|
|
9111
|
+
nextOffset += page.data.count;
|
|
9112
|
+
} while (page.data.count === 250);
|
|
9113
|
+
}
|
|
9114
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9115
|
+
return toolSuccess(
|
|
9116
|
+
summarizeCount(
|
|
9117
|
+
items2.length,
|
|
9118
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
9119
|
+
),
|
|
9120
|
+
{
|
|
9121
|
+
query: input.query,
|
|
9122
|
+
items: items2,
|
|
9123
|
+
count: items2.length,
|
|
9124
|
+
total: all.length,
|
|
9125
|
+
hasMore: offset + items2.length < all.length
|
|
9126
|
+
},
|
|
9127
|
+
{ entities: items2 }
|
|
9128
|
+
);
|
|
9129
|
+
}
|
|
7979
9130
|
const options = {
|
|
7980
9131
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7981
9132
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
7982
9133
|
};
|
|
7983
|
-
const rows = searchContacts(
|
|
9134
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
7984
9135
|
db,
|
|
7985
9136
|
input.query,
|
|
7986
9137
|
{
|
|
@@ -7994,9 +9145,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
7994
9145
|
},
|
|
7995
9146
|
options
|
|
7996
9147
|
);
|
|
7997
|
-
const items =
|
|
9148
|
+
const items = [
|
|
9149
|
+
...rows.map(toContactListToolItem),
|
|
9150
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
9151
|
+
];
|
|
7998
9152
|
return toolSuccess(
|
|
7999
|
-
summarizeCount(
|
|
9153
|
+
summarizeCount(
|
|
9154
|
+
items.length,
|
|
9155
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
9156
|
+
),
|
|
8000
9157
|
{
|
|
8001
9158
|
query: input.query,
|
|
8002
9159
|
items,
|
|
@@ -8032,6 +9189,11 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
8032
9189
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
8033
9190
|
inputSchema: objectSchema(
|
|
8034
9191
|
{
|
|
9192
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
9193
|
+
"contacts",
|
|
9194
|
+
"events",
|
|
9195
|
+
"all"
|
|
9196
|
+
]),
|
|
8035
9197
|
query: stringSchema("Free-text or natural-key query."),
|
|
8036
9198
|
kind: enumSchema("Optional contact kind filter.", [
|
|
8037
9199
|
"human",
|
|
@@ -8083,72 +9245,44 @@ var affinityTools = [
|
|
|
8083
9245
|
recordEventTool,
|
|
8084
9246
|
manageCommitmentTool,
|
|
8085
9247
|
manageDateAnchorTool,
|
|
8086
|
-
manageAttributeTool
|
|
9248
|
+
manageAttributeTool,
|
|
9249
|
+
manageEventTool
|
|
8087
9250
|
];
|
|
8088
9251
|
|
|
8089
9252
|
// src/surface/plain_dates.ts
|
|
8090
|
-
function
|
|
8091
|
-
const invalid = () => new CliUsageError(
|
|
8092
|
-
`Invalid timestamp "${raw}". Use YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d, -2d or an ISO date-time.`
|
|
8093
|
-
);
|
|
8094
|
-
if (/^-?\d+$/.test(raw)) {
|
|
8095
|
-
const value = Number(raw);
|
|
8096
|
-
if (Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()))
|
|
8097
|
-
return value;
|
|
8098
|
-
throw invalid();
|
|
8099
|
-
}
|
|
8100
|
-
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
8101
|
-
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw);
|
|
8102
|
-
let date;
|
|
8103
|
-
if (iso || european) {
|
|
8104
|
-
const year = Number(iso?.[1] ?? european?.[3]);
|
|
8105
|
-
const month = Number(iso?.[2] ?? european?.[2]);
|
|
8106
|
-
const day = Number(iso?.[3] ?? european?.[1]);
|
|
8107
|
-
date = /* @__PURE__ */ new Date(0);
|
|
8108
|
-
date.setFullYear(year, month - 1, day);
|
|
8109
|
-
date.setHours(
|
|
8110
|
-
endOfDay ? 23 : 0,
|
|
8111
|
-
endOfDay ? 59 : 0,
|
|
8112
|
-
endOfDay ? 59 : 0,
|
|
8113
|
-
endOfDay ? 999 : 0
|
|
8114
|
-
);
|
|
8115
|
-
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day)
|
|
8116
|
-
throw invalid();
|
|
8117
|
-
} else if (raw === "today" || raw === "tomorrow" || raw === "yesterday" || /^[+-]\d+d$/.test(raw)) {
|
|
8118
|
-
date = new Date(now);
|
|
8119
|
-
date.setDate(
|
|
8120
|
-
date.getDate() + (raw === "today" ? 0 : raw === "tomorrow" ? 1 : raw === "yesterday" ? -1 : Number(raw.slice(0, -1)))
|
|
8121
|
-
);
|
|
8122
|
-
date.setHours(
|
|
8123
|
-
endOfDay ? 23 : 0,
|
|
8124
|
-
endOfDay ? 59 : 0,
|
|
8125
|
-
endOfDay ? 59 : 0,
|
|
8126
|
-
endOfDay ? 999 : 0
|
|
8127
|
-
);
|
|
8128
|
-
} else {
|
|
8129
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
8130
|
-
raw
|
|
8131
|
-
))
|
|
8132
|
-
throw invalid();
|
|
8133
|
-
parsePlainDue(raw.slice(0, 10), now);
|
|
8134
|
-
date = new Date(raw);
|
|
8135
|
-
}
|
|
8136
|
-
if (!Number.isFinite(date.getTime())) throw invalid();
|
|
8137
|
-
return date.getTime();
|
|
8138
|
-
}
|
|
8139
|
-
function parsePlainStart(raw, now = Date.now()) {
|
|
8140
|
-
return parsePlainDue(raw, now, false);
|
|
8141
|
-
}
|
|
8142
|
-
function normalizeTimeInputs(value, now = Date.now()) {
|
|
9253
|
+
function normalizeTimeInputs(value, now = Date.now(), inPatch = false) {
|
|
8143
9254
|
if (Array.isArray(value))
|
|
8144
9255
|
return value.map((item) => normalizeTimeInputs(item, now));
|
|
8145
9256
|
if (!value || typeof value !== "object") return value;
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
9257
|
+
const result = {};
|
|
9258
|
+
for (const [key, item] of Object.entries(value)) {
|
|
9259
|
+
if (key === "occurredAt" && item === null)
|
|
9260
|
+
throw new CliUsageError(
|
|
9261
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
9262
|
+
);
|
|
9263
|
+
if (typeof item === "string" && (key.endsWith("At") || ["asOf", "now", "since", "until"].includes(key))) {
|
|
9264
|
+
let parsed;
|
|
9265
|
+
try {
|
|
9266
|
+
parsed = parseCalendarTime(item, key, now);
|
|
9267
|
+
} catch (error) {
|
|
9268
|
+
throw new CliUsageError(
|
|
9269
|
+
`Invalid timestamp "${item}". ${error.message}`
|
|
9270
|
+
);
|
|
9271
|
+
}
|
|
9272
|
+
if (!inPatch && ["occurredAt", "dueAt"].includes(key) && typeof item === "string" && /\.\d{4,}/.test(item)) {
|
|
9273
|
+
const provenance = result.provenance ?? value.provenance;
|
|
9274
|
+
result.provenance = {
|
|
9275
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
9276
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: item
|
|
9277
|
+
};
|
|
9278
|
+
}
|
|
9279
|
+
result[key] = parsed.timestamp;
|
|
9280
|
+
if (!inPatch && ["occurredAt", "dueAt"].includes(key))
|
|
9281
|
+
result[key.replace(/At$/, "On")] = parsed.day;
|
|
9282
|
+
} else if (key !== "provenance" || result.provenance === void 0)
|
|
9283
|
+
result[key] = key === "provenance" ? item : normalizeTimeInputs(item, now, key === "patch");
|
|
9284
|
+
}
|
|
9285
|
+
return result;
|
|
8152
9286
|
}
|
|
8153
9287
|
|
|
8154
9288
|
// src/surface/timestamp_schema.ts
|
|
@@ -8175,20 +9309,63 @@ function timestampSchema(schema) {
|
|
|
8175
9309
|
}
|
|
8176
9310
|
|
|
8177
9311
|
// src/surface/affinity_surface_tools.ts
|
|
9312
|
+
function surfaceProperties(tool) {
|
|
9313
|
+
return {
|
|
9314
|
+
...tool.inputSchema.properties,
|
|
9315
|
+
...tool.name === "review_affinity" ? {
|
|
9316
|
+
includeContent: {
|
|
9317
|
+
type: "boolean",
|
|
9318
|
+
description: "Deprecated alias for --with-content; includes verbatim event content."
|
|
9319
|
+
}
|
|
9320
|
+
} : {},
|
|
9321
|
+
...["inspect_affinity_item", "review_affinity", "manage_event"].includes(
|
|
9322
|
+
tool.name
|
|
9323
|
+
) ? {
|
|
9324
|
+
refSource: {
|
|
9325
|
+
type: "string",
|
|
9326
|
+
description: "External reference source."
|
|
9327
|
+
},
|
|
9328
|
+
refId: {
|
|
9329
|
+
type: "string",
|
|
9330
|
+
description: "External reference id."
|
|
9331
|
+
}
|
|
9332
|
+
} : {},
|
|
9333
|
+
...tool.name === "review_affinity" ? {
|
|
9334
|
+
withContent: {
|
|
9335
|
+
type: "boolean",
|
|
9336
|
+
description: "Include verbatim event content."
|
|
9337
|
+
}
|
|
9338
|
+
} : {}
|
|
9339
|
+
};
|
|
9340
|
+
}
|
|
8178
9341
|
var affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
8179
9342
|
name: tool.name,
|
|
8180
9343
|
description: tool.description,
|
|
8181
|
-
whenToUse: tool.whenToUse,
|
|
8182
|
-
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
8183
9344
|
readOnly: tool.readOnly,
|
|
8184
9345
|
sideEffects: tool.sideEffects,
|
|
8185
9346
|
inputSchema: {
|
|
8186
|
-
...timestampSchema({
|
|
9347
|
+
...timestampSchema({
|
|
9348
|
+
...tool.inputSchema,
|
|
9349
|
+
properties: surfaceProperties(tool),
|
|
9350
|
+
type: "object"
|
|
9351
|
+
}),
|
|
8187
9352
|
type: "object"
|
|
8188
9353
|
},
|
|
8189
9354
|
handler: (db, input) => {
|
|
8190
9355
|
try {
|
|
8191
|
-
|
|
9356
|
+
const { withContent, refSource, refId, ...normalized } = input;
|
|
9357
|
+
if (withContent !== void 0) normalized.includeContent = withContent;
|
|
9358
|
+
if (refSource !== void 0 || refId !== void 0) {
|
|
9359
|
+
if (normalized.ref !== void 0)
|
|
9360
|
+
return toolFailure(
|
|
9361
|
+
"protocol",
|
|
9362
|
+
"invalid_input",
|
|
9363
|
+
"Invalid target.",
|
|
9364
|
+
"Provide ref or ref-source/ref-id, not both."
|
|
9365
|
+
);
|
|
9366
|
+
normalized.ref = { source: refSource, id: refId };
|
|
9367
|
+
}
|
|
9368
|
+
return tool.handler(db, normalizeTimeInputs(normalized));
|
|
8192
9369
|
} catch (error) {
|
|
8193
9370
|
if (error instanceof CliUsageError)
|
|
8194
9371
|
return toolFailure(
|
|
@@ -8213,7 +9390,7 @@ function affinityMcpOptions(context) {
|
|
|
8213
9390
|
context,
|
|
8214
9391
|
annotations: (tool) => ({
|
|
8215
9392
|
readOnlyHint: tool.readOnly ?? false,
|
|
8216
|
-
destructiveHint: tool.name === "merge_contacts" || /remove|unset|undismiss/.test(
|
|
9393
|
+
destructiveHint: tool.name === "merge_contacts" || tool.name === "manage_event" || /remove|unset|undismiss/.test(
|
|
8217
9394
|
String(tool.inputSchema.properties?.action?.enum ?? [])
|
|
8218
9395
|
),
|
|
8219
9396
|
idempotentHint: tool.readOnly ?? false,
|
|
@@ -8222,13 +9399,95 @@ function affinityMcpOptions(context) {
|
|
|
8222
9399
|
};
|
|
8223
9400
|
}
|
|
8224
9401
|
|
|
9402
|
+
// src/events/init_events_tables.ts
|
|
9403
|
+
function initEventsTables(db) {
|
|
9404
|
+
db.exec(`
|
|
9405
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9406
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9407
|
+
type TEXT NOT NULL,
|
|
9408
|
+
occurred_at INTEGER NOT NULL,
|
|
9409
|
+
summary TEXT NOT NULL,
|
|
9410
|
+
significance REAL NOT NULL DEFAULT 0,
|
|
9411
|
+
moment_kind TEXT,
|
|
9412
|
+
recurrence_kind TEXT,
|
|
9413
|
+
anchor_month INTEGER,
|
|
9414
|
+
anchor_day INTEGER,
|
|
9415
|
+
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
9416
|
+
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
9417
|
+
created_at INTEGER NOT NULL,
|
|
9418
|
+
updated_at INTEGER NOT NULL,
|
|
9419
|
+
deleted_at INTEGER,
|
|
9420
|
+
CHECK (length(trim(summary)) > 0),
|
|
9421
|
+
CHECK (
|
|
9422
|
+
type IN (
|
|
9423
|
+
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
9424
|
+
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
9425
|
+
)
|
|
9426
|
+
),
|
|
9427
|
+
CHECK (
|
|
9428
|
+
(type = 'date_anchor' AND (
|
|
9429
|
+
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
9430
|
+
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
9431
|
+
))
|
|
9432
|
+
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
9433
|
+
),
|
|
9434
|
+
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
9435
|
+
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
9436
|
+
)),
|
|
9437
|
+
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
9438
|
+
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
9439
|
+
)),
|
|
9440
|
+
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
9441
|
+
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
9442
|
+
)
|
|
9443
|
+
`);
|
|
9444
|
+
db.exec(
|
|
9445
|
+
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
9446
|
+
);
|
|
9447
|
+
db.exec(
|
|
9448
|
+
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
9449
|
+
);
|
|
9450
|
+
const columns = db.prepare("PRAGMA table_info(events)").all();
|
|
9451
|
+
if (!columns.some((c) => c.name === "declared_moment_kind"))
|
|
9452
|
+
db.exec("ALTER TABLE events ADD COLUMN declared_moment_kind TEXT");
|
|
9453
|
+
if (!columns.some((c) => c.name === "affects_bond"))
|
|
9454
|
+
db.exec("ALTER TABLE events ADD COLUMN affects_bond INTEGER");
|
|
9455
|
+
if (!columns.some((c) => c.name === "content"))
|
|
9456
|
+
db.exec("ALTER TABLE events ADD COLUMN content TEXT");
|
|
9457
|
+
if (!columns.some((c) => c.name === "external_source"))
|
|
9458
|
+
db.exec("ALTER TABLE events ADD COLUMN external_source TEXT");
|
|
9459
|
+
if (!columns.some((c) => c.name === "external_id"))
|
|
9460
|
+
db.exec("ALTER TABLE events ADD COLUMN external_id TEXT");
|
|
9461
|
+
if (!columns.some((c) => c.name === "channel"))
|
|
9462
|
+
db.exec("ALTER TABLE events ADD COLUMN channel TEXT");
|
|
9463
|
+
if (!columns.some((c) => c.name === "parent_source"))
|
|
9464
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_source TEXT");
|
|
9465
|
+
if (!columns.some((c) => c.name === "parent_ref_id"))
|
|
9466
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_ref_id TEXT");
|
|
9467
|
+
if (!columns.some((c) => c.name === "parent_event_id"))
|
|
9468
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_event_id INTEGER");
|
|
9469
|
+
if (!columns.some((c) => c.name === "thread_source"))
|
|
9470
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_source TEXT");
|
|
9471
|
+
if (!columns.some((c) => c.name === "thread_id"))
|
|
9472
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_id TEXT");
|
|
9473
|
+
if (!columns.some((c) => c.name === "provenance_json"))
|
|
9474
|
+
db.exec("ALTER TABLE events ADD COLUMN provenance_json TEXT");
|
|
9475
|
+
db.exec(
|
|
9476
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external_ref ON events(external_source, external_id)"
|
|
9477
|
+
);
|
|
9478
|
+
if (!columns.some((column) => column.name === "occurred_on"))
|
|
9479
|
+
db.exec("ALTER TABLE events ADD COLUMN occurred_on TEXT");
|
|
9480
|
+
}
|
|
9481
|
+
|
|
8225
9482
|
// src/attributes/init_attributes_tables.ts
|
|
8226
9483
|
function initAttributesTables(db) {
|
|
9484
|
+
initEventsTables(db);
|
|
8227
9485
|
db.exec(`
|
|
8228
9486
|
CREATE TABLE IF NOT EXISTS attributes (
|
|
8229
9487
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8230
9488
|
contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
8231
9489
|
link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
9490
|
+
event_id INTEGER REFERENCES events (id) ON DELETE CASCADE,
|
|
8232
9491
|
name TEXT NOT NULL,
|
|
8233
9492
|
value TEXT,
|
|
8234
9493
|
created_at INTEGER NOT NULL,
|
|
@@ -8236,15 +9495,35 @@ function initAttributesTables(db) {
|
|
|
8236
9495
|
deleted_at INTEGER,
|
|
8237
9496
|
CHECK (length(trim(name)) > 0),
|
|
8238
9497
|
CHECK (
|
|
8239
|
-
(contact_id IS NOT NULL
|
|
8240
|
-
OR
|
|
8241
|
-
(contact_id IS NULL AND link_id IS NOT NULL)
|
|
9498
|
+
(contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1
|
|
8242
9499
|
)
|
|
8243
9500
|
)
|
|
8244
9501
|
`);
|
|
9502
|
+
const columns = db.prepare("PRAGMA table_info(attributes)").all();
|
|
9503
|
+
if (!columns.some((c) => c.name === "event_id")) {
|
|
9504
|
+
withTransaction(
|
|
9505
|
+
db,
|
|
9506
|
+
() => db.exec(`CREATE TABLE attributes_upgrade (
|
|
9507
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9508
|
+
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
|
|
9509
|
+
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
|
|
9510
|
+
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
|
|
9511
|
+
name TEXT NOT NULL CHECK(length(trim(name)) > 0), value TEXT,
|
|
9512
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
9513
|
+
CHECK ((contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1)
|
|
9514
|
+
);
|
|
9515
|
+
INSERT INTO attributes_upgrade (id,contact_id,link_id,name,value,created_at,updated_at,deleted_at)
|
|
9516
|
+
SELECT id,contact_id,link_id,name,value,created_at,updated_at,deleted_at FROM attributes;
|
|
9517
|
+
DROP TABLE attributes;
|
|
9518
|
+
ALTER TABLE attributes_upgrade RENAME TO attributes;`)
|
|
9519
|
+
);
|
|
9520
|
+
}
|
|
8245
9521
|
db.exec(
|
|
8246
9522
|
"CREATE INDEX IF NOT EXISTS idx_attributes_contact ON attributes(contact_id) WHERE deleted_at IS NULL"
|
|
8247
9523
|
);
|
|
9524
|
+
db.exec(
|
|
9525
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_attributes_event_name_live ON attributes(event_id,name) WHERE deleted_at IS NULL AND event_id IS NOT NULL"
|
|
9526
|
+
);
|
|
8248
9527
|
db.exec(
|
|
8249
9528
|
"CREATE INDEX IF NOT EXISTS idx_attributes_link ON attributes(link_id) WHERE deleted_at IS NULL"
|
|
8250
9529
|
);
|
|
@@ -8357,56 +9636,6 @@ function initEventParticipantsTables(db) {
|
|
|
8357
9636
|
);
|
|
8358
9637
|
}
|
|
8359
9638
|
|
|
8360
|
-
// src/events/init_events_tables.ts
|
|
8361
|
-
function initEventsTables(db) {
|
|
8362
|
-
db.exec(`
|
|
8363
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
8364
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8365
|
-
type TEXT NOT NULL,
|
|
8366
|
-
occurred_at INTEGER NOT NULL,
|
|
8367
|
-
summary TEXT NOT NULL,
|
|
8368
|
-
significance REAL NOT NULL DEFAULT 0,
|
|
8369
|
-
moment_kind TEXT,
|
|
8370
|
-
recurrence_kind TEXT,
|
|
8371
|
-
anchor_month INTEGER,
|
|
8372
|
-
anchor_day INTEGER,
|
|
8373
|
-
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
8374
|
-
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
8375
|
-
created_at INTEGER NOT NULL,
|
|
8376
|
-
updated_at INTEGER NOT NULL,
|
|
8377
|
-
deleted_at INTEGER,
|
|
8378
|
-
CHECK (length(trim(summary)) > 0),
|
|
8379
|
-
CHECK (
|
|
8380
|
-
type IN (
|
|
8381
|
-
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
8382
|
-
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
8383
|
-
)
|
|
8384
|
-
),
|
|
8385
|
-
CHECK (
|
|
8386
|
-
(type = 'date_anchor' AND (
|
|
8387
|
-
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
8388
|
-
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
8389
|
-
))
|
|
8390
|
-
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
8391
|
-
),
|
|
8392
|
-
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
8393
|
-
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
8394
|
-
)),
|
|
8395
|
-
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
8396
|
-
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
8397
|
-
)),
|
|
8398
|
-
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
8399
|
-
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
8400
|
-
)
|
|
8401
|
-
`);
|
|
8402
|
-
db.exec(
|
|
8403
|
-
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
8404
|
-
);
|
|
8405
|
-
db.exec(
|
|
8406
|
-
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
8407
|
-
);
|
|
8408
|
-
}
|
|
8409
|
-
|
|
8410
9639
|
// src/events/init_open_commitments_tables.ts
|
|
8411
9640
|
function initOpenCommitmentsTables(db) {
|
|
8412
9641
|
db.exec(`
|
|
@@ -8426,6 +9655,9 @@ function initOpenCommitmentsTables(db) {
|
|
|
8426
9655
|
)
|
|
8427
9656
|
)
|
|
8428
9657
|
`);
|
|
9658
|
+
const columns = db.prepare("PRAGMA table_info(open_commitments)").all();
|
|
9659
|
+
if (!columns.some((column) => column.name === "due_on"))
|
|
9660
|
+
db.exec("ALTER TABLE open_commitments ADD COLUMN due_on TEXT");
|
|
8429
9661
|
}
|
|
8430
9662
|
|
|
8431
9663
|
// src/identities/init_identities_tables.ts
|
|
@@ -8490,6 +9722,21 @@ function initLinkEventEffectsTables(db) {
|
|
|
8490
9722
|
db.exec(
|
|
8491
9723
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
8492
9724
|
);
|
|
9725
|
+
for (const name of ["participant_json", "provenance_json"])
|
|
9726
|
+
if (!columns.some((c) => c.name === name))
|
|
9727
|
+
db.exec(`ALTER TABLE link_event_effects ADD COLUMN ${name} TEXT`);
|
|
9728
|
+
db.exec(
|
|
9729
|
+
"CREATE TABLE IF NOT EXISTS event_link_baselines (link_id INTEGER PRIMARY KEY REFERENCES links(id) ON DELETE CASCADE, state_json TEXT NOT NULL)"
|
|
9730
|
+
);
|
|
9731
|
+
db.exec(
|
|
9732
|
+
"CREATE TABLE IF NOT EXISTS event_link_state_changes (id INTEGER PRIMARY KEY AUTOINCREMENT, link_id INTEGER NOT NULL REFERENCES links(id) ON DELETE CASCADE, after_event_id INTEGER NOT NULL, state TEXT NOT NULL)"
|
|
9733
|
+
);
|
|
9734
|
+
db.exec(`INSERT OR IGNORE INTO event_link_baselines(link_id,state_json)
|
|
9735
|
+
SELECT l.id,json_object('rank',l.rank-COALESCE(SUM(e.rank_delta),0),'affinity',l.affinity-COALESCE(SUM(e.affinity_delta),0),'trust',l.trust-COALESCE(SUM(e.trust_delta),0),'state','active','cadence_days',NULL)
|
|
9736
|
+
FROM links l JOIN link_event_effects e ON e.link_id=l.id GROUP BY l.id`);
|
|
9737
|
+
db.exec(
|
|
9738
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id IN (SELECT id FROM events WHERE type IN ('promise', 'agreement'))"
|
|
9739
|
+
);
|
|
8493
9740
|
db.exec(
|
|
8494
9741
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_moment ON link_event_effects(moment_kind, occurred_at DESC, impact_score DESC) WHERE moment_kind IS NOT NULL"
|
|
8495
9742
|
);
|
|
@@ -8665,12 +9912,15 @@ async function openAffinityDb(path) {
|
|
|
8665
9912
|
} finally {
|
|
8666
9913
|
process.emitWarning = original;
|
|
8667
9914
|
}
|
|
8668
|
-
if (path !== ":memory:"
|
|
9915
|
+
if (path !== ":memory:" && !path.startsWith("file:")) {
|
|
9916
|
+
node_fs.mkdirSync(node_path.dirname(path), { recursive: true, mode: 448 });
|
|
9917
|
+
if (!node_fs.existsSync(path)) node_fs.closeSync(node_fs.openSync(path, "wx", 384));
|
|
9918
|
+
}
|
|
8669
9919
|
const db = new sqlite.DatabaseSync(path);
|
|
8670
9920
|
try {
|
|
8671
|
-
db.exec("PRAGMA
|
|
9921
|
+
db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;");
|
|
8672
9922
|
const context = db;
|
|
8673
|
-
initAffinityTables(context);
|
|
9923
|
+
withTransaction(context, () => initAffinityTables(context));
|
|
8674
9924
|
return context;
|
|
8675
9925
|
} catch (error) {
|
|
8676
9926
|
db.close();
|
|
@@ -8689,6 +9939,13 @@ function isErrorResult(result) {
|
|
|
8689
9939
|
|
|
8690
9940
|
// src/surfaces/validate_input.ts
|
|
8691
9941
|
function validateInput(schema, input, path = "$") {
|
|
9942
|
+
if (path.endsWith(".occurredAt") && input == null)
|
|
9943
|
+
return ["occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"];
|
|
9944
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
9945
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && input !== null && typeof input !== "string")
|
|
9946
|
+
return [
|
|
9947
|
+
`attribute values are strings or null; pass "${String(input)}" to store it as text`
|
|
9948
|
+
];
|
|
8692
9949
|
const errors = [];
|
|
8693
9950
|
const matchesType = (type) => {
|
|
8694
9951
|
switch (type) {
|
|
@@ -8719,6 +9976,10 @@ function validateInput(schema, input, path = "$") {
|
|
|
8719
9976
|
(branch) => validateInput(branch, input, path).length === 0
|
|
8720
9977
|
))
|
|
8721
9978
|
errors.push(`${path}: must match at least one schema branch`);
|
|
9979
|
+
if (schema.anyOf && !schema.anyOf.some(
|
|
9980
|
+
(branch) => validateInput(branch, input, path).length === 0
|
|
9981
|
+
))
|
|
9982
|
+
errors.push(`${path}: must match at least one schema branch`);
|
|
8722
9983
|
if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
8723
9984
|
const record = input;
|
|
8724
9985
|
for (const name of schema.required ?? [])
|