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