@ghostpaw/affinity 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -9
- package/SKILL.md +43 -386
- package/dist/cli.js +2797 -894
- package/dist/index.cjs +1922 -613
- package/dist/index.d.cts +333 -55
- package/dist/index.d.ts +333 -55
- package/dist/index.js +1922 -613
- package/dist/mcp-server.js +2131 -819
- package/dist/mcp.cjs +1912 -707
- package/dist/mcp.d.cts +3 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +1913 -708
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +43 -386
- package/skills/affinity/references/identify-and-locate-contacts.md +9 -0
- package/skills/affinity/references/import-history-without-faking-evidence.md +25 -0
- package/skills/affinity/references/manage-promises-and-agreements.md +8 -0
- package/skills/affinity/references/manage-recurring-dates-and-reminders.md +8 -0
- package/skills/affinity/references/record-direct-evidence-well.md +18 -0
- package/skills/affinity/references/record-observations-and-referrals.md +10 -0
- package/skills/affinity/references/review-radar-progression-and-graph.md +13 -0
- package/skills/affinity/references/tools.md +489 -0
package/dist/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.0";
|
|
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,25 @@ 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.content != null) base.content = decodeEventText(row.content);
|
|
384
|
+
if (row.channel != null) base.channel = row.channel;
|
|
385
|
+
if (row.external_source != null && row.external_id != null)
|
|
386
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
387
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
388
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
389
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
390
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
391
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
392
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
393
|
+
base.dueAt = row.due_at ?? null;
|
|
394
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
395
|
+
}
|
|
227
396
|
if (row.recurrence_kind !== null) {
|
|
228
397
|
base.recurrenceKind = row.recurrence_kind;
|
|
229
398
|
base.anchorMonth = row.anchor_month;
|
|
@@ -253,25 +422,29 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
253
422
|
targetRef,
|
|
254
423
|
recurrenceKind,
|
|
255
424
|
occursOn: row.occurs_on,
|
|
256
|
-
|
|
425
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
426
|
+
summary: decodeEventText(row.summary),
|
|
257
427
|
significance: row.significance
|
|
258
428
|
};
|
|
259
429
|
}
|
|
430
|
+
function decodeEventText(value) {
|
|
431
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
432
|
+
}
|
|
260
433
|
|
|
261
434
|
// src/events/queries.ts
|
|
262
435
|
function getEventRowById(db, id) {
|
|
263
436
|
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
|
|
437
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
268
438
|
FROM events
|
|
269
439
|
WHERE id = ? AND deleted_at IS NULL`
|
|
270
440
|
).get(id);
|
|
271
441
|
if (row === void 0) {
|
|
272
442
|
return null;
|
|
273
443
|
}
|
|
274
|
-
|
|
444
|
+
const event = row;
|
|
445
|
+
event.summary = decodeEventText(event.summary);
|
|
446
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
447
|
+
return event;
|
|
275
448
|
}
|
|
276
449
|
function findDuplicateDateAnchor(db, params) {
|
|
277
450
|
const exclude = params.excludeEventId;
|
|
@@ -339,19 +512,27 @@ function loadEventRecord(db, eventId) {
|
|
|
339
512
|
throw new AffinityInvariantError("event not found");
|
|
340
513
|
}
|
|
341
514
|
const participants = loadEventParticipantViews(db, eventId);
|
|
342
|
-
|
|
515
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
516
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
517
|
+
if (due) {
|
|
518
|
+
record.dueAt = due.due_at;
|
|
519
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
520
|
+
}
|
|
521
|
+
enrichEventMetadata(db, record);
|
|
522
|
+
return record;
|
|
343
523
|
}
|
|
344
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
524
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
345
525
|
if (eventIds.length === 0) {
|
|
346
526
|
return [];
|
|
347
527
|
}
|
|
348
528
|
const ph = eventIds.map(() => "?").join(", ");
|
|
349
529
|
const eventRows = db.prepare(
|
|
350
|
-
`SELECT id, type, occurred_at,
|
|
530
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
531
|
+
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
532
|
recurrence_kind, anchor_month, anchor_day,
|
|
352
533
|
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`
|
|
534
|
+
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
|
|
535
|
+
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
536
|
).all(...eventIds);
|
|
356
537
|
const partRows = db.prepare(
|
|
357
538
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -375,7 +556,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
375
556
|
for (const id of eventIds) {
|
|
376
557
|
const row = rowById.get(id);
|
|
377
558
|
if (row) {
|
|
378
|
-
|
|
559
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
560
|
+
enrichEventMetadata(db, record);
|
|
561
|
+
out.push(record);
|
|
379
562
|
}
|
|
380
563
|
}
|
|
381
564
|
return out;
|
|
@@ -434,6 +617,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
434
617
|
clauses.push(`e.type IN (${ph})`);
|
|
435
618
|
params.push(...options.eventTypes);
|
|
436
619
|
}
|
|
620
|
+
if (options?.channel !== void 0) {
|
|
621
|
+
clauses.push("e.channel = ?");
|
|
622
|
+
params.push(options.channel.trim().toLowerCase());
|
|
623
|
+
}
|
|
437
624
|
const where = clauses.join(" AND ");
|
|
438
625
|
const rows = db.prepare(
|
|
439
626
|
`SELECT e.id FROM events e
|
|
@@ -444,8 +631,75 @@ function getContactJournal(db, contactId, options) {
|
|
|
444
631
|
).all(...params, limit, offset);
|
|
445
632
|
return batchLoadEventRecords(
|
|
446
633
|
db,
|
|
447
|
-
rows.map((r) => r.id)
|
|
634
|
+
rows.map((r) => r.id),
|
|
635
|
+
options?.includeContent ?? false
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/lib/parse_calendar_time.ts
|
|
640
|
+
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
641
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
642
|
+
if (dueAt === null) return false;
|
|
643
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
644
|
+
return localCalendarDate(now) > dueOn;
|
|
645
|
+
}
|
|
646
|
+
return dueAt < now;
|
|
647
|
+
}
|
|
648
|
+
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)";
|
|
649
|
+
function localCalendarDate(now) {
|
|
650
|
+
const d = new Date(now);
|
|
651
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
652
|
+
}
|
|
653
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
654
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
655
|
+
throw new AffinityValidationError(
|
|
656
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
657
|
+
);
|
|
658
|
+
const fail = () => {
|
|
659
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
660
|
+
};
|
|
661
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
662
|
+
const timestamp2 = Number(value);
|
|
663
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
664
|
+
return fail();
|
|
665
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
666
|
+
}
|
|
667
|
+
if (typeof value !== "string") return fail();
|
|
668
|
+
let text = value.trim();
|
|
669
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
670
|
+
text = text.replace(" ", "T");
|
|
671
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
672
|
+
}
|
|
673
|
+
let rounding = 0;
|
|
674
|
+
text = text.replace(
|
|
675
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
676
|
+
(_, digits) => {
|
|
677
|
+
const ms = Math.round(Number(`0.${digits}`) * 1e3);
|
|
678
|
+
rounding = ms === 1e3 ? 1 : 0;
|
|
679
|
+
return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
|
|
680
|
+
}
|
|
448
681
|
);
|
|
682
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
683
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
684
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
685
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
686
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
687
|
+
text = date.toISOString().slice(0, 10);
|
|
688
|
+
}
|
|
689
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
690
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
691
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
692
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
693
|
+
text
|
|
694
|
+
))
|
|
695
|
+
return fail();
|
|
696
|
+
const day = text.slice(0, 10);
|
|
697
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
698
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
699
|
+
return fail();
|
|
700
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
|
|
701
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
702
|
+
return { timestamp, day };
|
|
449
703
|
}
|
|
450
704
|
|
|
451
705
|
// src/resolve_now.ts
|
|
@@ -516,9 +770,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
516
770
|
clauses.push("oc.due_at <= ?");
|
|
517
771
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
518
772
|
}
|
|
773
|
+
if (options?.channel !== void 0) {
|
|
774
|
+
clauses.push("e.channel=?");
|
|
775
|
+
params.push(options.channel.trim().toLowerCase());
|
|
776
|
+
}
|
|
519
777
|
const where = clauses.join(" AND ");
|
|
520
778
|
const rows = db.prepare(
|
|
521
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
779
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
522
780
|
e.type, e.summary
|
|
523
781
|
FROM open_commitments oc
|
|
524
782
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -535,7 +793,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
535
793
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
536
794
|
dueAt: r.due_at,
|
|
537
795
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
538
|
-
|
|
796
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
797
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
539
798
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
540
799
|
}));
|
|
541
800
|
}
|
|
@@ -604,8 +863,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
604
863
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
605
864
|
}
|
|
606
865
|
function computeNextAnchorOccursOn(params) {
|
|
607
|
-
const refStart =
|
|
608
|
-
|
|
866
|
+
const refStart = Date.parse(
|
|
867
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
868
|
+
);
|
|
869
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
609
870
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
610
871
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
611
872
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -808,7 +1069,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
808
1069
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
809
1070
|
LIMIT ?`
|
|
810
1071
|
).all(contactId, contactId, topLinksLimit);
|
|
811
|
-
const topLinks = linkRows.map((l) =>
|
|
1072
|
+
const topLinks = linkRows.map((l) => ({
|
|
1073
|
+
...mapLinkRowToLinkListItem(l),
|
|
1074
|
+
counterpartyName: getContactRowById(
|
|
1075
|
+
db,
|
|
1076
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1077
|
+
)?.name ?? ""
|
|
1078
|
+
}));
|
|
812
1079
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
813
1080
|
const contactRollup = loadContactRollup(db, contactId);
|
|
814
1081
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -841,7 +1108,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
841
1108
|
return {
|
|
842
1109
|
...profile,
|
|
843
1110
|
recentEvents: getContactJournal(db, contactId, {
|
|
844
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1111
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1112
|
+
eventTypes: [
|
|
1113
|
+
"conversation",
|
|
1114
|
+
"activity",
|
|
1115
|
+
"gift",
|
|
1116
|
+
"support",
|
|
1117
|
+
"milestone",
|
|
1118
|
+
"observation",
|
|
1119
|
+
"conflict",
|
|
1120
|
+
"correction",
|
|
1121
|
+
"transaction",
|
|
1122
|
+
"promise",
|
|
1123
|
+
"agreement"
|
|
1124
|
+
]
|
|
845
1125
|
}).map((event) => ({
|
|
846
1126
|
...event,
|
|
847
1127
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -896,6 +1176,10 @@ function listMoments(db, filters, options) {
|
|
|
896
1176
|
clauses.push("le.link_id = ?");
|
|
897
1177
|
params.push(filters.linkId);
|
|
898
1178
|
}
|
|
1179
|
+
if (options?.channel !== void 0) {
|
|
1180
|
+
clauses.push("e.channel=?");
|
|
1181
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1182
|
+
}
|
|
899
1183
|
const where = clauses.join(" AND ");
|
|
900
1184
|
return db.prepare(
|
|
901
1185
|
`SELECT
|
|
@@ -903,6 +1187,7 @@ function listMoments(db, filters, options) {
|
|
|
903
1187
|
le.link_id,
|
|
904
1188
|
le.moment_kind,
|
|
905
1189
|
le.occurred_at,
|
|
1190
|
+
e.occurred_on,
|
|
906
1191
|
e.summary,
|
|
907
1192
|
le.impact_score
|
|
908
1193
|
FROM link_event_effects le
|
|
@@ -915,6 +1200,7 @@ function listMoments(db, filters, options) {
|
|
|
915
1200
|
linkId: row.link_id,
|
|
916
1201
|
momentKind: row.moment_kind,
|
|
917
1202
|
occurredAt: row.occurred_at,
|
|
1203
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
918
1204
|
summary: row.summary,
|
|
919
1205
|
impactScore: row.impact_score
|
|
920
1206
|
}));
|
|
@@ -1002,6 +1288,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1002
1288
|
clauses.push(`e.type IN (${ph})`);
|
|
1003
1289
|
params.push(...options.eventTypes);
|
|
1004
1290
|
}
|
|
1291
|
+
if (options?.channel !== void 0) {
|
|
1292
|
+
clauses.push("e.channel = ?");
|
|
1293
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1294
|
+
}
|
|
1005
1295
|
const where = clauses.join(" AND ");
|
|
1006
1296
|
const rows = db.prepare(
|
|
1007
1297
|
`SELECT e.id
|
|
@@ -1013,7 +1303,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1013
1303
|
).all(...params, limit, offset);
|
|
1014
1304
|
return batchLoadEventRecords(
|
|
1015
1305
|
db,
|
|
1016
|
-
rows.map((r) => r.id)
|
|
1306
|
+
rows.map((r) => r.id),
|
|
1307
|
+
options?.includeContent ?? false
|
|
1017
1308
|
);
|
|
1018
1309
|
}
|
|
1019
1310
|
|
|
@@ -1186,26 +1477,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1186
1477
|
const latest = db.prepare(
|
|
1187
1478
|
`SELECT event_id, occurred_at
|
|
1188
1479
|
FROM link_event_effects
|
|
1189
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
1480
|
+
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
1481
|
ORDER BY occurred_at DESC, event_id DESC
|
|
1191
1482
|
LIMIT 1`
|
|
1192
|
-
).get(linkId);
|
|
1483
|
+
).get(linkId, now, localCalendarDate(now));
|
|
1193
1484
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
1194
1485
|
const counts = db.prepare(
|
|
1195
1486
|
`SELECT
|
|
1196
1487
|
COUNT(*) AS total_meaningful_events,
|
|
1197
1488
|
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
1489
|
FROM link_event_effects
|
|
1199
|
-
WHERE link_id = ? AND occurred_at >=
|
|
1200
|
-
|
|
1490
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
1491
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
1492
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
1201
1493
|
const directionalityRows = db.prepare(
|
|
1202
1494
|
`SELECT ep.directionality
|
|
1203
1495
|
FROM link_event_effects le
|
|
1204
1496
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
1205
1497
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
1206
1498
|
AND ep.directionality IS NOT NULL
|
|
1207
|
-
AND le.occurred_at >=
|
|
1208
|
-
|
|
1499
|
+
AND le.occurred_at >= ?
|
|
1500
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
1501
|
+
).all(
|
|
1502
|
+
linkId,
|
|
1503
|
+
link.from_contact_id,
|
|
1504
|
+
trailingWindowStart,
|
|
1505
|
+
now,
|
|
1506
|
+
localCalendarDate(now)
|
|
1507
|
+
);
|
|
1209
1508
|
let outboundCount = 0;
|
|
1210
1509
|
let inboundCount = 0;
|
|
1211
1510
|
for (const row of directionalityRows) {
|
|
@@ -1356,37 +1655,8 @@ function getLinkDetail(db, linkId, options) {
|
|
|
1356
1655
|
}
|
|
1357
1656
|
|
|
1358
1657
|
// 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
1658
|
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();
|
|
1659
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
1390
1660
|
}
|
|
1391
1661
|
|
|
1392
1662
|
// src/tools/tool_metadata.ts
|
|
@@ -1457,6 +1727,39 @@ function oneOfSchema(schemas, description) {
|
|
|
1457
1727
|
return schema;
|
|
1458
1728
|
}
|
|
1459
1729
|
|
|
1730
|
+
// src/tools/event_metadata_schema.ts
|
|
1731
|
+
var externalRefSchema = objectSchema(
|
|
1732
|
+
{
|
|
1733
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
1734
|
+
id: stringSchema("Non-empty external id.")
|
|
1735
|
+
},
|
|
1736
|
+
["source", "id"]
|
|
1737
|
+
);
|
|
1738
|
+
var eventMetadataSchema = {
|
|
1739
|
+
affectsBond: booleanSchema(
|
|
1740
|
+
"Whether evidence affects the relationship; defaults false for observations with significance <=2."
|
|
1741
|
+
),
|
|
1742
|
+
content: stringSchema(
|
|
1743
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
1744
|
+
),
|
|
1745
|
+
externalRef: externalRefSchema,
|
|
1746
|
+
channel: stringSchema(
|
|
1747
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
1748
|
+
),
|
|
1749
|
+
parentRef: externalRefSchema,
|
|
1750
|
+
parentEventId: integerSchema("Parent event id."),
|
|
1751
|
+
threadRef: externalRefSchema,
|
|
1752
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
1753
|
+
"breakthrough",
|
|
1754
|
+
"rupture",
|
|
1755
|
+
"reconciliation",
|
|
1756
|
+
"milestone",
|
|
1757
|
+
"turning_point"
|
|
1758
|
+
]),
|
|
1759
|
+
occurredOn: stringSchema("Calendar day."),
|
|
1760
|
+
provenance: {}
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1460
1763
|
// src/tools/tool_names.ts
|
|
1461
1764
|
var searchAffinityToolName = "search_affinity";
|
|
1462
1765
|
var reviewAffinityToolName = "review_affinity";
|
|
@@ -1470,6 +1773,7 @@ var recordEventToolName = "record_event";
|
|
|
1470
1773
|
var manageCommitmentToolName = "manage_commitment";
|
|
1471
1774
|
var manageDateAnchorToolName = "manage_date_anchor";
|
|
1472
1775
|
var manageAttributeToolName = "manage_attribute";
|
|
1776
|
+
var manageEventToolName = "manage_event";
|
|
1473
1777
|
|
|
1474
1778
|
// src/tools/tool_ref.ts
|
|
1475
1779
|
function contactState(value) {
|
|
@@ -1514,7 +1818,7 @@ function toEventEntityRef(value) {
|
|
|
1514
1818
|
return {
|
|
1515
1819
|
kind: "event",
|
|
1516
1820
|
id: value.id,
|
|
1517
|
-
title: value.summary,
|
|
1821
|
+
title: deriveEventSummary(value.summary),
|
|
1518
1822
|
subtitle: value.type,
|
|
1519
1823
|
state: value.type
|
|
1520
1824
|
};
|
|
@@ -1549,14 +1853,39 @@ function toReceiptEntityRefs(receipt) {
|
|
|
1549
1853
|
}
|
|
1550
1854
|
|
|
1551
1855
|
// src/identities/normalize.ts
|
|
1856
|
+
var handleIdentityTypes = /* @__PURE__ */ new Set([
|
|
1857
|
+
"handle",
|
|
1858
|
+
"bluesky",
|
|
1859
|
+
"x",
|
|
1860
|
+
"twitter",
|
|
1861
|
+
"mastodon",
|
|
1862
|
+
"github",
|
|
1863
|
+
"instagram",
|
|
1864
|
+
"telegram",
|
|
1865
|
+
"linkedin",
|
|
1866
|
+
"facebook",
|
|
1867
|
+
"youtube",
|
|
1868
|
+
"tiktok",
|
|
1869
|
+
"discord",
|
|
1870
|
+
"slack"
|
|
1871
|
+
]);
|
|
1872
|
+
function normalizeIdentityValue(type, value) {
|
|
1873
|
+
const v = value.trim();
|
|
1874
|
+
return handleIdentityTypes.has(type.toLowerCase()) ? v.replace(/^@/, "").toLowerCase() : v;
|
|
1875
|
+
}
|
|
1552
1876
|
function normalizeIdentityKey(type, value) {
|
|
1553
|
-
|
|
1554
|
-
|
|
1877
|
+
let t = type.trim().toLowerCase();
|
|
1878
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(t)) t = "did";
|
|
1879
|
+
const v = normalizeIdentityValue(t, value).toLowerCase();
|
|
1555
1880
|
if (t.length === 0 || v.length === 0) {
|
|
1556
1881
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
1557
1882
|
}
|
|
1558
1883
|
return `${t}:${v}`;
|
|
1559
1884
|
}
|
|
1885
|
+
function legacyIdentityKey(key) {
|
|
1886
|
+
const colon = key.indexOf(":");
|
|
1887
|
+
return handleIdentityTypes.has(key.slice(0, colon)) ? `${key.slice(0, colon + 1)}@${key.slice(colon + 1)}` : key;
|
|
1888
|
+
}
|
|
1560
1889
|
|
|
1561
1890
|
// src/lib/errors/is_affinity_error.ts
|
|
1562
1891
|
function isAffinityError(value) {
|
|
@@ -1678,7 +2007,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
1678
2007
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
1679
2008
|
return {
|
|
1680
2009
|
ok: false,
|
|
1681
|
-
result: toolFailure("domain", "
|
|
2010
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
1682
2011
|
};
|
|
1683
2012
|
}
|
|
1684
2013
|
return { ok: true, value: row2 };
|
|
@@ -1703,9 +2032,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
1703
2032
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
1704
2033
|
WHERE i.removed_at IS NULL
|
|
1705
2034
|
AND c.deleted_at IS NULL
|
|
1706
|
-
AND i.normalized_key = ?
|
|
2035
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
1707
2036
|
LIMIT 2`
|
|
1708
|
-
).all(
|
|
2037
|
+
).all(
|
|
2038
|
+
normalizedKey,
|
|
2039
|
+
legacyIdentityKey(normalizedKey),
|
|
2040
|
+
normalizedKey
|
|
2041
|
+
);
|
|
1709
2042
|
if (row.length === 0) {
|
|
1710
2043
|
return {
|
|
1711
2044
|
ok: false,
|
|
@@ -1856,13 +2189,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
1856
2189
|
return { ok: true, value: matchedLink };
|
|
1857
2190
|
}
|
|
1858
2191
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
2192
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
2193
|
+
return error;
|
|
1859
2194
|
if (isAffinityError(error)) {
|
|
1860
|
-
|
|
2195
|
+
const result = toolFailure(
|
|
1861
2196
|
"domain",
|
|
1862
2197
|
affinityErrorCodeToToolCode(error),
|
|
1863
2198
|
summary,
|
|
1864
2199
|
error.message
|
|
1865
2200
|
);
|
|
2201
|
+
const id = error.existingContactId;
|
|
2202
|
+
if (id !== void 0) {
|
|
2203
|
+
result.data = { existingContactId: id };
|
|
2204
|
+
result.error.details = { existingContactId: id };
|
|
2205
|
+
}
|
|
2206
|
+
return result;
|
|
1866
2207
|
}
|
|
1867
2208
|
if (error instanceof Error) {
|
|
1868
2209
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -1901,27 +2242,32 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
1901
2242
|
}
|
|
1902
2243
|
|
|
1903
2244
|
// src/tools/validate_tool_input.ts
|
|
1904
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
2245
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
2246
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
2247
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
2248
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
2249
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
2250
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
1905
2251
|
if (schema.anyOf || schema.oneOf) {
|
|
1906
2252
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
1907
2253
|
return choices.some(
|
|
1908
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
2254
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
1909
2255
|
) ? null : `${path}: invalid value`;
|
|
1910
2256
|
}
|
|
1911
2257
|
if (schema.type === "object") {
|
|
1912
2258
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1913
2259
|
return `${path}: missing input or expected object`;
|
|
1914
2260
|
const object = value;
|
|
1915
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
2261
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
1916
2262
|
for (const key of Object.keys(object)) {
|
|
1917
2263
|
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(", ")}`;
|
|
2264
|
+
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
2265
|
}
|
|
1920
2266
|
}
|
|
1921
2267
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
1922
2268
|
return `${path}.patch: missing required field`;
|
|
1923
2269
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
1924
|
-
return `${path}.patch: unknown
|
|
2270
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
1925
2271
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
1926
2272
|
try {
|
|
1927
2273
|
return parseTimeInput(
|
|
@@ -1936,11 +2282,14 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1936
2282
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
1937
2283
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
1938
2284
|
try {
|
|
1939
|
-
|
|
2285
|
+
const parsed = parseCalendarTime(
|
|
1940
2286
|
object[key],
|
|
1941
2287
|
`${path}.${key}`,
|
|
1942
2288
|
referenceNow
|
|
1943
2289
|
);
|
|
2290
|
+
object[key] = parsed.timestamp;
|
|
2291
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
2292
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
1944
2293
|
} catch (error) {
|
|
1945
2294
|
return error.message;
|
|
1946
2295
|
}
|
|
@@ -1956,7 +2305,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1956
2305
|
child,
|
|
1957
2306
|
object[key],
|
|
1958
2307
|
`${path}.${key}`,
|
|
1959
|
-
referenceNow
|
|
2308
|
+
referenceNow,
|
|
2309
|
+
strict
|
|
1960
2310
|
);
|
|
1961
2311
|
if (error) return error;
|
|
1962
2312
|
}
|
|
@@ -1969,7 +2319,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1969
2319
|
schema.items ?? {},
|
|
1970
2320
|
item,
|
|
1971
2321
|
`${path}[${index}]`,
|
|
1972
|
-
injectedNow
|
|
2322
|
+
injectedNow,
|
|
2323
|
+
strict
|
|
1973
2324
|
);
|
|
1974
2325
|
if (error) return error;
|
|
1975
2326
|
}
|
|
@@ -2035,6 +2386,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
2035
2386
|
inputError
|
|
2036
2387
|
);
|
|
2037
2388
|
return withToolHandling(() => {
|
|
2389
|
+
if (input.kind === "event") {
|
|
2390
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
2391
|
+
return toolFailure(
|
|
2392
|
+
"protocol",
|
|
2393
|
+
"invalid_input",
|
|
2394
|
+
"Invalid event target.",
|
|
2395
|
+
"Provide exactly one of eventId or ref."
|
|
2396
|
+
);
|
|
2397
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
2398
|
+
return detail2 ? toolSuccess(
|
|
2399
|
+
`Loaded event #${detail2.id}.`,
|
|
2400
|
+
{ kind: "event", detail: detail2 },
|
|
2401
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
2402
|
+
) : toolFailure(
|
|
2403
|
+
"domain",
|
|
2404
|
+
"not_found",
|
|
2405
|
+
"Event not found.",
|
|
2406
|
+
"No event matches the target."
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2038
2409
|
if (input.kind === "owner_profile") {
|
|
2039
2410
|
const detail2 = getOwnerProfile(
|
|
2040
2411
|
db,
|
|
@@ -2132,10 +2503,13 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
2132
2503
|
inputSchema: objectSchema(
|
|
2133
2504
|
{
|
|
2134
2505
|
kind: enumSchema("What to inspect.", [
|
|
2506
|
+
"event",
|
|
2135
2507
|
"owner_profile",
|
|
2136
2508
|
"contact_profile",
|
|
2137
2509
|
"link"
|
|
2138
2510
|
]),
|
|
2511
|
+
eventId: integerSchema("Exact event id."),
|
|
2512
|
+
ref: externalRefSchema,
|
|
2139
2513
|
contact: contactLocatorSchema(
|
|
2140
2514
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
2141
2515
|
),
|
|
@@ -2154,6 +2528,7 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
2154
2528
|
// src/attributes/helpers.ts
|
|
2155
2529
|
function clearedAttributePrimary(target) {
|
|
2156
2530
|
return {
|
|
2531
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
2157
2532
|
id: 0,
|
|
2158
2533
|
contactId: target.kind === "contact" ? target.id : null,
|
|
2159
2534
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -2165,6 +2540,7 @@ function clearedAttributePrimary(target) {
|
|
|
2165
2540
|
// src/attributes/mappers.ts
|
|
2166
2541
|
function mapAttributeRowToAttributeRecord(row) {
|
|
2167
2542
|
return {
|
|
2543
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
2168
2544
|
id: row.id,
|
|
2169
2545
|
contactId: row.contact_id,
|
|
2170
2546
|
linkId: row.link_id,
|
|
@@ -2174,136 +2550,55 @@ function mapAttributeRowToAttributeRecord(row) {
|
|
|
2174
2550
|
}
|
|
2175
2551
|
|
|
2176
2552
|
// src/attributes/queries.ts
|
|
2553
|
+
function attributeTargetSql(target) {
|
|
2554
|
+
return {
|
|
2555
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
2556
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2177
2559
|
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;
|
|
2560
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
2186
2561
|
}
|
|
2187
2562
|
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;
|
|
2563
|
+
const t = attributeTargetSql(target);
|
|
2564
|
+
return db.prepare(
|
|
2565
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
2566
|
+
).get(t.id, name) ?? null;
|
|
2208
2567
|
}
|
|
2209
2568
|
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);
|
|
2569
|
+
const t = attributeTargetSql(target);
|
|
2570
|
+
return db.prepare(
|
|
2571
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
2572
|
+
).all(t.id).map((r) => r.id);
|
|
2220
2573
|
}
|
|
2221
2574
|
|
|
2222
|
-
// src/
|
|
2223
|
-
function
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
}
|
|
2234
|
-
return out;
|
|
2575
|
+
// src/attributes/receipts.ts
|
|
2576
|
+
function buildAttributeMutationReceipt(primary, partial) {
|
|
2577
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2578
|
+
return {
|
|
2579
|
+
primary,
|
|
2580
|
+
created: normalized.created,
|
|
2581
|
+
updated: normalized.updated,
|
|
2582
|
+
archived: normalized.archived,
|
|
2583
|
+
removed: normalized.removed,
|
|
2584
|
+
affectedLinks: normalized.affectedLinks,
|
|
2585
|
+
derivedEffects: normalized.derivedEffects
|
|
2586
|
+
};
|
|
2235
2587
|
}
|
|
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");
|
|
2588
|
+
|
|
2589
|
+
// src/links/validators.ts
|
|
2590
|
+
var STATES = /* @__PURE__ */ new Set([
|
|
2591
|
+
"active",
|
|
2592
|
+
"dormant",
|
|
2593
|
+
"strained",
|
|
2594
|
+
"broken",
|
|
2595
|
+
"archived"
|
|
2596
|
+
]);
|
|
2597
|
+
function assertContactEndpointsNotMerged(db, fromContactId, toContactId) {
|
|
2598
|
+
const from = getContactRowById(db, fromContactId);
|
|
2599
|
+
const to = getContactRowById(db, toContactId);
|
|
2600
|
+
if (!from || !to) {
|
|
2601
|
+
throw new AffinityNotFoundError("contact not found");
|
|
2307
2602
|
}
|
|
2308
2603
|
if (from.lifecycle_state === "merged" || to.lifecycle_state === "merged") {
|
|
2309
2604
|
throw new AffinityStateError("merged contact is read-only");
|
|
@@ -2339,6 +2634,8 @@ function normalizeAttributeValue(value) {
|
|
|
2339
2634
|
if (value === null) {
|
|
2340
2635
|
return null;
|
|
2341
2636
|
}
|
|
2637
|
+
if (typeof value !== "string")
|
|
2638
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
2342
2639
|
const trimmed = value.trim();
|
|
2343
2640
|
return trimmed.length === 0 ? null : trimmed;
|
|
2344
2641
|
}
|
|
@@ -2367,6 +2664,11 @@ function validateAttributeEntries(entries) {
|
|
|
2367
2664
|
return out;
|
|
2368
2665
|
}
|
|
2369
2666
|
function assertAttributeTargetWritable(db, target) {
|
|
2667
|
+
if (target.kind === "event") {
|
|
2668
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
2669
|
+
throw new AffinityNotFoundError("event not found");
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2370
2672
|
if (target.kind === "contact") {
|
|
2371
2673
|
const contact = getContactRowById(db, target.id);
|
|
2372
2674
|
if (!contact) {
|
|
@@ -2402,26 +2704,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
2402
2704
|
}
|
|
2403
2705
|
const created = [];
|
|
2404
2706
|
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
|
-
}
|
|
2707
|
+
const t = attributeTargetSql(target);
|
|
2708
|
+
const insert = db.prepare(
|
|
2709
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
2710
|
+
);
|
|
2711
|
+
for (const e of normalized) {
|
|
2712
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
2713
|
+
lastId = Number(r.lastInsertRowid);
|
|
2714
|
+
created.push({ kind: "attribute", id: lastId });
|
|
2425
2715
|
}
|
|
2426
2716
|
if (normalized.length === 0) {
|
|
2427
2717
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -2466,27 +2756,10 @@ function setAttribute(db, target, name, value) {
|
|
|
2466
2756
|
}
|
|
2467
2757
|
);
|
|
2468
2758
|
}
|
|
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
|
-
}
|
|
2759
|
+
const t = attributeTargetSql(target);
|
|
2486
2760
|
const r = db.prepare(
|
|
2487
|
-
`INSERT INTO attributes (
|
|
2488
|
-
|
|
2489
|
-
).run(target.id, key, stored, now, now);
|
|
2761
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
2762
|
+
).run(t.id, key, stored, now, now);
|
|
2490
2763
|
const id = Number(r.lastInsertRowid);
|
|
2491
2764
|
const row = getAttributeRowById(db, id);
|
|
2492
2765
|
if (!row) {
|
|
@@ -2542,14 +2815,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
2542
2815
|
affectedLinks: receipt.affectedLinks,
|
|
2543
2816
|
derivedEffects: receipt.derivedEffects
|
|
2544
2817
|
};
|
|
2545
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
2818
|
+
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
2819
|
if (changedCount === 0) {
|
|
2547
2820
|
return toolNoOp(summary, data, {
|
|
2548
2821
|
entities: toReceiptEntityRefs(receipt)
|
|
2549
2822
|
});
|
|
2550
2823
|
}
|
|
2551
2824
|
return toolSuccess(summary, data, {
|
|
2552
|
-
entities: toReceiptEntityRefs(receipt)
|
|
2825
|
+
entities: toReceiptEntityRefs(receipt),
|
|
2826
|
+
warnings: receipt.warnings
|
|
2553
2827
|
});
|
|
2554
2828
|
}
|
|
2555
2829
|
var PLURALS = { identity: "identities" };
|
|
@@ -2606,6 +2880,7 @@ function linkLocatorSchema2(description) {
|
|
|
2606
2880
|
};
|
|
2607
2881
|
}
|
|
2608
2882
|
function resolveAttributeTarget(db, target) {
|
|
2883
|
+
if (target.kind === "event") return { kind: "ok", target };
|
|
2609
2884
|
if (target.kind === "contact") {
|
|
2610
2885
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
2611
2886
|
if (!contact.ok) {
|
|
@@ -2665,7 +2940,7 @@ function manageAttributeToolHandler(db, rawInput) {
|
|
|
2665
2940
|
}
|
|
2666
2941
|
var manageAttributeTool = defineAffinityTool({
|
|
2667
2942
|
name: manageAttributeToolName,
|
|
2668
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
2943
|
+
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
2944
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
2670
2945
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
2671
2946
|
sideEffects: "writes_state",
|
|
@@ -2686,7 +2961,8 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2686
2961
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
2687
2962
|
target: objectSchema(
|
|
2688
2963
|
{
|
|
2689
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
2964
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
2965
|
+
eventId: integerSchema("Target event id."),
|
|
2690
2966
|
contact: contactLocatorSchema2(
|
|
2691
2967
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
2692
2968
|
),
|
|
@@ -2716,7 +2992,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2716
2992
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
2717
2993
|
},
|
|
2718
2994
|
["action"],
|
|
2719
|
-
"Set, unset, or replace attributes on contacts and
|
|
2995
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
2720
2996
|
),
|
|
2721
2997
|
handler: manageAttributeToolHandler
|
|
2722
2998
|
});
|
|
@@ -2724,7 +3000,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2724
3000
|
// src/events/persistence.ts
|
|
2725
3001
|
function insertJournalEvent(db, params) {
|
|
2726
3002
|
const now = resolveNow(params.now);
|
|
2727
|
-
const summary = params.summary
|
|
3003
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
2728
3004
|
const result = db.prepare(
|
|
2729
3005
|
`INSERT INTO events (
|
|
2730
3006
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -2741,6 +3017,33 @@ function insertJournalEvent(db, params) {
|
|
|
2741
3017
|
now
|
|
2742
3018
|
);
|
|
2743
3019
|
const eventId = Number(result.lastInsertRowid);
|
|
3020
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
3021
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
|
|
3022
|
+
eventId
|
|
3023
|
+
);
|
|
3024
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
3025
|
+
params.momentKind,
|
|
3026
|
+
eventId
|
|
3027
|
+
);
|
|
3028
|
+
db.prepare(
|
|
3029
|
+
"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=?"
|
|
3030
|
+
).run(
|
|
3031
|
+
params.content ?? null,
|
|
3032
|
+
params.externalRef?.source ?? null,
|
|
3033
|
+
params.externalRef?.id ?? null,
|
|
3034
|
+
params.channel ?? null,
|
|
3035
|
+
params.parentRef?.source ?? null,
|
|
3036
|
+
params.parentRef?.id ?? null,
|
|
3037
|
+
params.parentEventId ?? null,
|
|
3038
|
+
params.threadRef?.source ?? null,
|
|
3039
|
+
params.threadRef?.id ?? null,
|
|
3040
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3041
|
+
eventId
|
|
3042
|
+
);
|
|
3043
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3044
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3045
|
+
eventId
|
|
3046
|
+
);
|
|
2744
3047
|
const insertParticipant = db.prepare(
|
|
2745
3048
|
`INSERT INTO event_participants (
|
|
2746
3049
|
event_id, contact_id, role, directionality
|
|
@@ -2783,6 +3086,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
2783
3086
|
now
|
|
2784
3087
|
);
|
|
2785
3088
|
const eventId = Number(result.lastInsertRowid);
|
|
3089
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3090
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3091
|
+
eventId
|
|
3092
|
+
);
|
|
2786
3093
|
const insertParticipant = db.prepare(
|
|
2787
3094
|
`INSERT INTO event_participants (
|
|
2788
3095
|
event_id, contact_id, role, directionality
|
|
@@ -2803,20 +3110,6 @@ function insertDateAnchorEvent(db, params) {
|
|
|
2803
3110
|
return eventId;
|
|
2804
3111
|
}
|
|
2805
3112
|
|
|
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
3113
|
// src/graph/betweenness.ts
|
|
2821
3114
|
function computeNodeBetweenness(adjacency) {
|
|
2822
3115
|
const nodes = [...adjacency.keys()];
|
|
@@ -3415,6 +3708,8 @@ function deriveStateAfter(params) {
|
|
|
3415
3708
|
return params.stateBefore;
|
|
3416
3709
|
}
|
|
3417
3710
|
function deriveMomentKind(params) {
|
|
3711
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
3712
|
+
return null;
|
|
3418
3713
|
if (params.rankAfter > params.rankBefore) {
|
|
3419
3714
|
return "breakthrough";
|
|
3420
3715
|
}
|
|
@@ -3444,6 +3739,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
3444
3739
|
if (!link || link.is_structural !== 0) {
|
|
3445
3740
|
throw new AffinityStateError("relational link not found");
|
|
3446
3741
|
}
|
|
3742
|
+
db.prepare(
|
|
3743
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
3744
|
+
).run(
|
|
3745
|
+
params.linkId,
|
|
3746
|
+
JSON.stringify({
|
|
3747
|
+
rank: link.rank ?? 0,
|
|
3748
|
+
affinity: link.affinity ?? 0,
|
|
3749
|
+
trust: link.trust ?? 0,
|
|
3750
|
+
state: link.state ?? "active",
|
|
3751
|
+
cadence_days: link.cadence_days
|
|
3752
|
+
})
|
|
3753
|
+
);
|
|
3447
3754
|
const stateBefore = link.state ?? "active";
|
|
3448
3755
|
const linkKind = link.kind;
|
|
3449
3756
|
const rankBefore = link.rank ?? 0;
|
|
@@ -3488,7 +3795,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3488
3795
|
);
|
|
3489
3796
|
const weightedBase = base * massPenalty(
|
|
3490
3797
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
3491
|
-
) * salienceMultiplier;
|
|
3798
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
3492
3799
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
3493
3800
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
3494
3801
|
const affinityMass = clamp3(
|
|
@@ -3529,15 +3836,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
3529
3836
|
const latest = db.prepare(
|
|
3530
3837
|
`SELECT occurred_at
|
|
3531
3838
|
FROM link_event_effects
|
|
3532
|
-
WHERE link_id = ?
|
|
3839
|
+
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
3840
|
ORDER BY occurred_at DESC, event_id DESC
|
|
3534
3841
|
LIMIT 1`
|
|
3535
|
-
).get(params.linkId);
|
|
3842
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
3536
3843
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
3537
3844
|
1,
|
|
3538
3845
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
3539
3846
|
);
|
|
3540
|
-
const
|
|
3847
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
3848
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
3541
3849
|
kind: linkKind,
|
|
3542
3850
|
currentCadenceDays: cadenceBefore,
|
|
3543
3851
|
gapDays
|
|
@@ -3557,7 +3865,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
3557
3865
|
}
|
|
3558
3866
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
3559
3867
|
const trustDelta = trustAfter - trustBefore;
|
|
3560
|
-
const
|
|
3868
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
3869
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
3870
|
+
eventLabel
|
|
3871
|
+
);
|
|
3872
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
3561
3873
|
rankBefore,
|
|
3562
3874
|
rankAfter,
|
|
3563
3875
|
stateBefore,
|
|
@@ -3600,7 +3912,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
3600
3912
|
params.now,
|
|
3601
3913
|
params.now
|
|
3602
3914
|
);
|
|
3603
|
-
|
|
3915
|
+
db.prepare(
|
|
3916
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
3917
|
+
).run(
|
|
3918
|
+
JSON.stringify(params.participant),
|
|
3919
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3920
|
+
params.eventId,
|
|
3921
|
+
params.linkId
|
|
3922
|
+
);
|
|
3923
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
3604
3924
|
db.prepare(
|
|
3605
3925
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
3606
3926
|
).run(params.eventId, params.linkId);
|
|
@@ -3631,6 +3951,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3631
3951
|
|
|
3632
3952
|
// src/lib/errors/affinity_conflict_error.ts
|
|
3633
3953
|
var AffinityConflictError = class extends AffinityError {
|
|
3954
|
+
existingContactId;
|
|
3634
3955
|
code = "CONFLICT";
|
|
3635
3956
|
};
|
|
3636
3957
|
|
|
@@ -3685,6 +4006,8 @@ function buildLinkMutationReceipt(primary, partial) {
|
|
|
3685
4006
|
// src/links/seed_social_link.ts
|
|
3686
4007
|
function seedSocialLink(db, input) {
|
|
3687
4008
|
return withTransaction(db, () => {
|
|
4009
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
4010
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
3688
4011
|
if (!isRelationalLinkKind(input.kind)) {
|
|
3689
4012
|
throw new AffinityValidationError("invalid relational link kind");
|
|
3690
4013
|
}
|
|
@@ -3692,11 +4015,14 @@ function seedSocialLink(db, input) {
|
|
|
3692
4015
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
3693
4016
|
}
|
|
3694
4017
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
3695
|
-
|
|
4018
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
3696
4019
|
db,
|
|
3697
4020
|
input.fromContactId,
|
|
3698
4021
|
input.toContactId
|
|
3699
|
-
)
|
|
4022
|
+
);
|
|
4023
|
+
if (existing) {
|
|
4024
|
+
if (input.ifExists === "return")
|
|
4025
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
3700
4026
|
throw new AffinityConflictError(
|
|
3701
4027
|
"relational link already exists between these contacts"
|
|
3702
4028
|
);
|
|
@@ -3802,15 +4128,36 @@ function ensureRelationalLink(db, params) {
|
|
|
3802
4128
|
};
|
|
3803
4129
|
}
|
|
3804
4130
|
function applyJournalEventMechanics(db, params) {
|
|
4131
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
4132
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
4133
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
4134
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
3805
4135
|
const ownerId = requireOwnerContactId(db);
|
|
3806
|
-
const
|
|
4136
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
4137
|
+
const socialChannel = [
|
|
4138
|
+
"bluesky",
|
|
4139
|
+
"x",
|
|
4140
|
+
"twitter",
|
|
4141
|
+
"mastodon",
|
|
4142
|
+
"github",
|
|
4143
|
+
"instagram"
|
|
4144
|
+
].includes(String(bond?.channel));
|
|
4145
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
4146
|
+
const participants = params.participants.map(
|
|
4147
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
4148
|
+
);
|
|
4149
|
+
if (direction)
|
|
4150
|
+
db.prepare(
|
|
4151
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
4152
|
+
).run(direction, params.eventId);
|
|
4153
|
+
const participantIds = participants.map(
|
|
3807
4154
|
(participant) => participant.contactId
|
|
3808
4155
|
);
|
|
3809
4156
|
const hasOwner = participantIds.includes(ownerId);
|
|
3810
4157
|
const created = [];
|
|
3811
4158
|
const effectInputs = [];
|
|
3812
4159
|
if (hasOwner) {
|
|
3813
|
-
for (const participant of
|
|
4160
|
+
for (const participant of participants) {
|
|
3814
4161
|
if (participant.contactId === ownerId) {
|
|
3815
4162
|
continue;
|
|
3816
4163
|
}
|
|
@@ -3818,13 +4165,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3818
4165
|
fromContactId: ownerId,
|
|
3819
4166
|
toContactId: participant.contactId,
|
|
3820
4167
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
4168
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
3821
4169
|
now: params.now
|
|
3822
4170
|
});
|
|
3823
4171
|
created.push(...ensured.created);
|
|
3824
4172
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
3825
4173
|
}
|
|
3826
|
-
} else if (params.eventType === "observation" &&
|
|
3827
|
-
const [first, second] =
|
|
4174
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
4175
|
+
const [first, second] = participants;
|
|
3828
4176
|
if (first !== void 0 && second !== void 0) {
|
|
3829
4177
|
const a = Math.min(first.contactId, second.contactId);
|
|
3830
4178
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -3859,7 +4207,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3859
4207
|
eventType: params.eventType,
|
|
3860
4208
|
occurredAt: params.occurredAt,
|
|
3861
4209
|
significance: params.significance,
|
|
3862
|
-
participant:
|
|
4210
|
+
participant: {
|
|
4211
|
+
...input.participant,
|
|
4212
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
4213
|
+
},
|
|
3863
4214
|
provenance: params.provenance,
|
|
3864
4215
|
now: params.now
|
|
3865
4216
|
})
|
|
@@ -3883,6 +4234,8 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3883
4234
|
function recordCommitment(db, input) {
|
|
3884
4235
|
return withTransaction(db, () => {
|
|
3885
4236
|
validateSocialEventInput(input);
|
|
4237
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
4238
|
+
if (existing) return existing;
|
|
3886
4239
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
3887
4240
|
throw new AffinityValidationError(
|
|
3888
4241
|
"commitmentType must be promise or agreement"
|
|
@@ -3897,11 +4250,12 @@ function recordCommitment(db, input) {
|
|
|
3897
4250
|
assertOwnerParticipates(ownerId, ids);
|
|
3898
4251
|
const now = resolveNow(input.now);
|
|
3899
4252
|
const eventId = insertJournalEvent(db, {
|
|
4253
|
+
...input,
|
|
3900
4254
|
type: input.commitmentType,
|
|
3901
4255
|
occurredAt: input.occurredAt,
|
|
3902
|
-
|
|
4256
|
+
occurredOn: input.occurredOn,
|
|
3903
4257
|
significance: input.significance,
|
|
3904
|
-
momentKind: null,
|
|
4258
|
+
momentKind: input.momentKind ?? null,
|
|
3905
4259
|
participants: input.participants,
|
|
3906
4260
|
now
|
|
3907
4261
|
});
|
|
@@ -3910,15 +4264,25 @@ function recordCommitment(db, input) {
|
|
|
3910
4264
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
3911
4265
|
) VALUES (?, ?, ?, ?, ?)`
|
|
3912
4266
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
4267
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
4268
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
4269
|
+
eventId
|
|
4270
|
+
);
|
|
3913
4271
|
const mechanics = applyJournalEventMechanics(db, {
|
|
3914
4272
|
eventId,
|
|
3915
4273
|
eventType: input.commitmentType,
|
|
3916
4274
|
occurredAt: input.occurredAt,
|
|
4275
|
+
occurredOn: input.occurredOn,
|
|
3917
4276
|
significance: input.significance,
|
|
3918
4277
|
participants: input.participants,
|
|
3919
4278
|
provenance: input.provenance,
|
|
3920
4279
|
now
|
|
3921
4280
|
});
|
|
4281
|
+
if (input.momentKind !== void 0)
|
|
4282
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
4283
|
+
input.momentKind,
|
|
4284
|
+
eventId
|
|
4285
|
+
);
|
|
3922
4286
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
3923
4287
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
3924
4288
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -3992,6 +4356,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
3992
4356
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
3993
4357
|
validateSocialEventInput({
|
|
3994
4358
|
occurredAt,
|
|
4359
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
3995
4360
|
summary,
|
|
3996
4361
|
significance,
|
|
3997
4362
|
participants,
|
|
@@ -4008,6 +4373,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4008
4373
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4009
4374
|
type: eventType,
|
|
4010
4375
|
occurredAt,
|
|
4376
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4011
4377
|
summary,
|
|
4012
4378
|
significance,
|
|
4013
4379
|
momentKind: null,
|
|
@@ -4068,7 +4434,13 @@ function prepareParticipants(db, raw) {
|
|
|
4068
4434
|
}
|
|
4069
4435
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
4070
4436
|
const input = structuredClone(rawInput);
|
|
4071
|
-
const inputError = validateToolInput(
|
|
4437
|
+
const inputError = validateToolInput(
|
|
4438
|
+
manageCommitmentTool.inputSchema,
|
|
4439
|
+
input,
|
|
4440
|
+
"input",
|
|
4441
|
+
Date.now(),
|
|
4442
|
+
true
|
|
4443
|
+
);
|
|
4072
4444
|
if (inputError)
|
|
4073
4445
|
return toolFailure(
|
|
4074
4446
|
"protocol",
|
|
@@ -4098,6 +4470,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
4098
4470
|
"commitment"
|
|
4099
4471
|
);
|
|
4100
4472
|
}
|
|
4473
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
4474
|
+
return toolFailure(
|
|
4475
|
+
"protocol",
|
|
4476
|
+
"invalid_input",
|
|
4477
|
+
"Missing commitment.",
|
|
4478
|
+
"commitmentEventId is required for resolve."
|
|
4479
|
+
);
|
|
4480
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
4481
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
4482
|
+
return toolFailure(
|
|
4483
|
+
"domain",
|
|
4484
|
+
"invalid_input",
|
|
4485
|
+
"Invalid commitment.",
|
|
4486
|
+
"not a commitment event"
|
|
4487
|
+
);
|
|
4101
4488
|
return mutationToolResult(
|
|
4102
4489
|
"resolve",
|
|
4103
4490
|
resolveCommitment(
|
|
@@ -4136,15 +4523,21 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4136
4523
|
"promise",
|
|
4137
4524
|
"agreement"
|
|
4138
4525
|
]),
|
|
4526
|
+
...eventMetadataSchema,
|
|
4139
4527
|
occurredAt: integerSchema(
|
|
4140
4528
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
4141
4529
|
),
|
|
4142
4530
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
4143
4531
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
4144
4532
|
dueAt: integerSchema("Optional due timestamp."),
|
|
4533
|
+
dueOn: stringSchema("Due calendar day."),
|
|
4145
4534
|
participants: arraySchema(
|
|
4146
4535
|
objectSchema(
|
|
4147
4536
|
{
|
|
4537
|
+
contact: objectSchema(
|
|
4538
|
+
{ contactId: integerSchema("Contact id.") },
|
|
4539
|
+
["contactId"]
|
|
4540
|
+
),
|
|
4148
4541
|
contactId: integerSchema(
|
|
4149
4542
|
"Participant contact id. The owner contact MUST be included."
|
|
4150
4543
|
),
|
|
@@ -4157,13 +4550,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4157
4550
|
),
|
|
4158
4551
|
now: integerSchema("Optional timestamp override.")
|
|
4159
4552
|
},
|
|
4160
|
-
[
|
|
4161
|
-
"commitmentType",
|
|
4162
|
-
"occurredAt",
|
|
4163
|
-
"summary",
|
|
4164
|
-
"significance",
|
|
4165
|
-
"participants"
|
|
4166
|
-
],
|
|
4553
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
4167
4554
|
"Commitment payload. Required when action=record."
|
|
4168
4555
|
),
|
|
4169
4556
|
commitmentEventId: integerSchema(
|
|
@@ -4178,6 +4565,19 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4178
4565
|
now: integerSchema("Optional timestamp."),
|
|
4179
4566
|
resolvingEvent: objectSchema(
|
|
4180
4567
|
{
|
|
4568
|
+
occurredOn: stringSchema("Calendar day."),
|
|
4569
|
+
provenance: {},
|
|
4570
|
+
participants: arraySchema(
|
|
4571
|
+
objectSchema(
|
|
4572
|
+
{
|
|
4573
|
+
contactId: integerSchema("Contact id."),
|
|
4574
|
+
role: stringSchema("Role."),
|
|
4575
|
+
directionality: stringSchema("Directionality.")
|
|
4576
|
+
},
|
|
4577
|
+
["contactId", "role"]
|
|
4578
|
+
),
|
|
4579
|
+
"Resolving participants."
|
|
4580
|
+
),
|
|
4181
4581
|
type: stringSchema("Optional resolving event type."),
|
|
4182
4582
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
4183
4583
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -4258,30 +4658,46 @@ function deleteContact(db, locatorId, options) {
|
|
|
4258
4658
|
});
|
|
4259
4659
|
}
|
|
4260
4660
|
|
|
4261
|
-
// src/contacts/
|
|
4262
|
-
function
|
|
4263
|
-
|
|
4661
|
+
// src/contacts/export_rows.ts
|
|
4662
|
+
function exportRows(db, sql, ...params) {
|
|
4663
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
4264
4664
|
const result = {};
|
|
4265
4665
|
for (const [key, value] of Object.entries(row)) {
|
|
4266
4666
|
const name = key.replace(
|
|
4267
4667
|
/_([a-z])/g,
|
|
4268
4668
|
(_, letter) => letter.toUpperCase()
|
|
4269
4669
|
);
|
|
4270
|
-
result[name] =
|
|
4271
|
-
|
|
4670
|
+
result[name] = [
|
|
4671
|
+
"affects_bond",
|
|
4672
|
+
"is_owner",
|
|
4673
|
+
"is_structural",
|
|
4674
|
+
"verified",
|
|
4675
|
+
"counts_for_recency",
|
|
4676
|
+
"manual"
|
|
4677
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
4678
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
4272
4679
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4273
4680
|
if (key.endsWith("_json") && typeof value === "string")
|
|
4274
4681
|
result[name] = JSON.parse(value);
|
|
4275
4682
|
}
|
|
4276
4683
|
return result;
|
|
4277
4684
|
});
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
// src/contacts/export_contact.ts
|
|
4688
|
+
function exportContact(db, id) {
|
|
4689
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
4278
4690
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4279
4691
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4280
4692
|
const events = query(
|
|
4281
|
-
"SELECT DISTINCT e
|
|
4693
|
+
"SELECT DISTINCT e.*, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
4282
4694
|
id
|
|
4283
4695
|
).map((event) => ({
|
|
4284
4696
|
...event,
|
|
4697
|
+
attributes: query(
|
|
4698
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
4699
|
+
event.id
|
|
4700
|
+
),
|
|
4285
4701
|
participants: query(
|
|
4286
4702
|
"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
4703
|
event.id
|
|
@@ -4326,11 +4742,33 @@ function exportContact(db, id) {
|
|
|
4326
4742
|
// src/contacts/export_affinity.ts
|
|
4327
4743
|
function exportAffinity(db) {
|
|
4328
4744
|
const now = Date.now();
|
|
4745
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
4746
|
+
const events = exportRows(
|
|
4747
|
+
db,
|
|
4748
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
4749
|
+
).map((event) => ({
|
|
4750
|
+
...event,
|
|
4751
|
+
attributes: exportRows(
|
|
4752
|
+
db,
|
|
4753
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
4754
|
+
event.id
|
|
4755
|
+
),
|
|
4756
|
+
participants: exportRows(
|
|
4757
|
+
db,
|
|
4758
|
+
"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",
|
|
4759
|
+
event.id
|
|
4760
|
+
)
|
|
4761
|
+
}));
|
|
4329
4762
|
return {
|
|
4330
4763
|
version: 1,
|
|
4331
4764
|
exportedAt: now,
|
|
4332
4765
|
exportedAtIso: new Date(now).toISOString(),
|
|
4333
|
-
|
|
4766
|
+
events,
|
|
4767
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
4768
|
+
...record,
|
|
4769
|
+
eventIds: events2.map((event) => event.id),
|
|
4770
|
+
dateIds: dates.map((date) => date.id)
|
|
4771
|
+
}))
|
|
4334
4772
|
};
|
|
4335
4773
|
}
|
|
4336
4774
|
|
|
@@ -4403,7 +4841,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
4403
4841
|
}
|
|
4404
4842
|
if (to === "merged") {
|
|
4405
4843
|
throw new AffinityInvariantError(
|
|
4406
|
-
"use
|
|
4844
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
4407
4845
|
);
|
|
4408
4846
|
}
|
|
4409
4847
|
if (from === to) {
|
|
@@ -4455,67 +4893,6 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
4455
4893
|
});
|
|
4456
4894
|
}
|
|
4457
4895
|
|
|
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
4896
|
// src/identities/queries.ts
|
|
4520
4897
|
function getIdentityRowById(db, id) {
|
|
4521
4898
|
const row = db.prepare(
|
|
@@ -4547,14 +4924,19 @@ function buildIdentityMutationReceipt(primary, partial) {
|
|
|
4547
4924
|
// src/identities/validators.ts
|
|
4548
4925
|
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
4549
4926
|
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(
|
|
4927
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
4928
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey) : db.prepare(
|
|
4929
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL AND id != ?"
|
|
4930
|
+
).get(
|
|
4931
|
+
normalizedKey,
|
|
4932
|
+
legacyIdentityKey(normalizedKey),
|
|
4933
|
+
normalizedKey,
|
|
4934
|
+
excludeIdentityId
|
|
4935
|
+
);
|
|
4554
4936
|
if (row !== void 0) {
|
|
4555
4937
|
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);
|
|
4938
|
+
"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"
|
|
4939
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey);
|
|
4558
4940
|
throw new AffinityConflictError(
|
|
4559
4941
|
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
4560
4942
|
);
|
|
@@ -4563,6 +4945,8 @@ function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
|
4563
4945
|
|
|
4564
4946
|
// src/identities/validate_identity_input.ts
|
|
4565
4947
|
var identityTypes = [
|
|
4948
|
+
"did",
|
|
4949
|
+
"atproto_did",
|
|
4566
4950
|
"email",
|
|
4567
4951
|
"phone",
|
|
4568
4952
|
"url",
|
|
@@ -4589,8 +4973,20 @@ var identityTypes = [
|
|
|
4589
4973
|
"account_id",
|
|
4590
4974
|
"alias"
|
|
4591
4975
|
];
|
|
4592
|
-
function validateIdentityInput(type,
|
|
4976
|
+
function validateIdentityInput(type, rawValue) {
|
|
4593
4977
|
const kind = type.toLowerCase();
|
|
4978
|
+
const value = normalizeIdentityValue(kind, rawValue);
|
|
4979
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(kind) && !/^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/.test(value))
|
|
4980
|
+
throw new AffinityValidationError(
|
|
4981
|
+
"did value must match did:<method>:<identifier> (for example did:plc:abc123)"
|
|
4982
|
+
);
|
|
4983
|
+
if (handleIdentityTypes.has(kind)) {
|
|
4984
|
+
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_.-]+$/;
|
|
4985
|
+
if (!pattern.test(value))
|
|
4986
|
+
throw new AffinityValidationError(
|
|
4987
|
+
`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)"}`
|
|
4988
|
+
);
|
|
4989
|
+
}
|
|
4594
4990
|
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
4595
4991
|
throw new AffinityValidationError(
|
|
4596
4992
|
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
@@ -4629,12 +5025,27 @@ function addIdentity(db, contactId, input) {
|
|
|
4629
5025
|
throw new AffinityStateError("merged contact is read-only");
|
|
4630
5026
|
}
|
|
4631
5027
|
const type = input.type.trim();
|
|
4632
|
-
const value = input.value
|
|
5028
|
+
const value = normalizeIdentityValue(type, input.value);
|
|
4633
5029
|
if (type.length === 0 || value.length === 0) {
|
|
4634
5030
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
4635
5031
|
}
|
|
4636
5032
|
validateIdentityInput(type, value);
|
|
5033
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5034
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
4637
5035
|
const nk = normalizeIdentityKey(type, value);
|
|
5036
|
+
if (input.ifExists === "return") {
|
|
5037
|
+
const existing = db.prepare(
|
|
5038
|
+
"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"
|
|
5039
|
+
).get(contactId, nk, legacyIdentityKey(nk), nk);
|
|
5040
|
+
if (existing) {
|
|
5041
|
+
const row2 = getIdentityRowById(db, Number(existing.id));
|
|
5042
|
+
if (row2)
|
|
5043
|
+
return buildIdentityMutationReceipt(
|
|
5044
|
+
mapIdentityRowToIdentityRecord(row2),
|
|
5045
|
+
{}
|
|
5046
|
+
);
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
4638
5049
|
assertNoIdentityCollision(db, nk);
|
|
4639
5050
|
const now = resolveNow(input.now);
|
|
4640
5051
|
const verified = input.verified ? 1 : 0;
|
|
@@ -4668,31 +5079,239 @@ function addIdentity(db, contactId, input) {
|
|
|
4668
5079
|
});
|
|
4669
5080
|
}
|
|
4670
5081
|
|
|
4671
|
-
// src/
|
|
4672
|
-
|
|
5082
|
+
// src/contacts/create_contact.ts
|
|
5083
|
+
var KINDS = /* @__PURE__ */ new Set([
|
|
5084
|
+
"human",
|
|
5085
|
+
"group",
|
|
5086
|
+
"company",
|
|
5087
|
+
"team",
|
|
5088
|
+
"pet",
|
|
5089
|
+
"service",
|
|
5090
|
+
"other"
|
|
5091
|
+
]);
|
|
5092
|
+
function createContact(db, input) {
|
|
4673
5093
|
return withTransaction(db, () => {
|
|
4674
|
-
const
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
5094
|
+
const name = input.name.trim();
|
|
5095
|
+
if (name.length === 0) {
|
|
5096
|
+
throw new AffinityValidationError("name must be non-empty");
|
|
5097
|
+
}
|
|
5098
|
+
if (!KINDS.has(input.kind)) {
|
|
5099
|
+
throw new AffinityValidationError("invalid contact kind");
|
|
5100
|
+
}
|
|
5101
|
+
if (input.isOwner !== void 0) {
|
|
5102
|
+
throw new AffinityValidationError(
|
|
5103
|
+
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
5104
|
+
);
|
|
5105
|
+
}
|
|
5106
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5107
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
5108
|
+
const matches = (input.identities ?? []).flatMap((identity) => {
|
|
5109
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
5110
|
+
const key = normalizeIdentityKey(identity.type, identity.value);
|
|
5111
|
+
const row2 = db.prepare(
|
|
5112
|
+
`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)))`
|
|
5113
|
+
).get(key, legacyIdentityKey(key), key);
|
|
5114
|
+
return row2 ? [{ ...row2, identity }] : [];
|
|
5115
|
+
});
|
|
5116
|
+
const match = matches[0];
|
|
5117
|
+
if (match) {
|
|
5118
|
+
if (input.ifExists === "return") {
|
|
5119
|
+
const existing = getContactRowById(db, match.id);
|
|
5120
|
+
if (!existing)
|
|
5121
|
+
throw new AffinityInvariantError("identity owner not found");
|
|
5122
|
+
const primary2 = mapContactRowToContactListItem(existing);
|
|
5123
|
+
return Object.assign(buildContactMutationReceipt(primary2, {}), {
|
|
5124
|
+
outcome: "no_op",
|
|
5125
|
+
matchedIdentities: matches.map((m) => m.identity)
|
|
5126
|
+
});
|
|
5127
|
+
}
|
|
5128
|
+
const error = new AffinityConflictError(
|
|
5129
|
+
`identity ${normalizeIdentityKey(match.identity.type, match.identity.value).replace(":", " ")} already belongs to #${match.id} ${match.name}`
|
|
5130
|
+
);
|
|
5131
|
+
error.existingContactId = match.id;
|
|
5132
|
+
throw error;
|
|
5133
|
+
}
|
|
5134
|
+
const now = resolveNow(input.now);
|
|
5135
|
+
if (input.bootstrapOwner) {
|
|
5136
|
+
const existing = db.prepare(
|
|
5137
|
+
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
5138
|
+
).get();
|
|
5139
|
+
if (existing !== void 0) {
|
|
5140
|
+
throw new AffinityConflictError(
|
|
5141
|
+
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
5146
|
+
const result = db.prepare(
|
|
5147
|
+
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
5148
|
+
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
5149
|
+
).run(name, input.kind, isOwner, now, now);
|
|
5150
|
+
const id = Number(result.lastInsertRowid);
|
|
5151
|
+
const row = getContactRowById(db, id);
|
|
5152
|
+
if (!row) {
|
|
5153
|
+
throw new AffinityInvariantError("inserted contact row not found");
|
|
5154
|
+
}
|
|
5155
|
+
refreshContactRollup(db, id, now);
|
|
5156
|
+
const primary = mapContactRowToContactListItem(row);
|
|
5157
|
+
const receipt = buildContactMutationReceipt(primary, {
|
|
5158
|
+
created: [{ kind: "contact", id }]
|
|
5159
|
+
});
|
|
5160
|
+
for (const identity of input.identities ?? [])
|
|
5161
|
+
receipt.created.push(...addIdentity(db, id, identity).created);
|
|
5162
|
+
return receipt;
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
// src/contacts/fold_search_text.ts
|
|
5167
|
+
function foldSearchText(value) {
|
|
5168
|
+
let folded = value.toLowerCase();
|
|
5169
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
5170
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
5171
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5174
|
+
// src/contacts/list_duplicate_candidates.ts
|
|
5175
|
+
function pairKey(leftContactId, rightContactId) {
|
|
5176
|
+
const left = Math.min(leftContactId, rightContactId);
|
|
5177
|
+
const right = Math.max(leftContactId, rightContactId);
|
|
5178
|
+
return `${left}:${right}`;
|
|
5179
|
+
}
|
|
5180
|
+
function normalizeName(name) {
|
|
5181
|
+
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
5182
|
+
}
|
|
5183
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
5184
|
+
const left = normalizeName(leftInput);
|
|
5185
|
+
const right = normalizeName(rightInput);
|
|
5186
|
+
if (left === right) {
|
|
5187
|
+
return 0.92;
|
|
5188
|
+
}
|
|
5189
|
+
if (left.length >= 3 && right.length >= 3) {
|
|
5190
|
+
if (left.includes(right) || right.includes(left)) {
|
|
5191
|
+
return 0.72;
|
|
5192
|
+
}
|
|
5193
|
+
const leftTokens = new Set(left.split(" "));
|
|
5194
|
+
const rightTokens = new Set(right.split(" "));
|
|
5195
|
+
const overlap = [...leftTokens].filter(
|
|
5196
|
+
(token) => rightTokens.has(token)
|
|
5197
|
+
).length;
|
|
5198
|
+
if (overlap > 0) {
|
|
5199
|
+
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
return 0;
|
|
5203
|
+
}
|
|
5204
|
+
function listDuplicateCandidates(db, filters, options) {
|
|
5205
|
+
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
5206
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5207
|
+
const subset = filters?.contactIds;
|
|
5208
|
+
const contacts = db.prepare(
|
|
5209
|
+
`SELECT id, name
|
|
5210
|
+
FROM contacts
|
|
5211
|
+
WHERE deleted_at IS NULL
|
|
5212
|
+
AND lifecycle_state != 'merged'`
|
|
5213
|
+
).all();
|
|
5214
|
+
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
5215
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
5216
|
+
const exactMatches = db.prepare(
|
|
5217
|
+
`SELECT
|
|
5218
|
+
i1.contact_id AS left_contact_id,
|
|
5219
|
+
i2.contact_id AS right_contact_id,
|
|
5220
|
+
i1.normalized_key
|
|
5221
|
+
FROM identities i1
|
|
5222
|
+
INNER JOIN identities i2
|
|
5223
|
+
ON i1.normalized_key = i2.normalized_key
|
|
5224
|
+
AND i1.contact_id < i2.contact_id
|
|
5225
|
+
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
5226
|
+
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
5227
|
+
WHERE i1.removed_at IS NULL
|
|
5228
|
+
AND i2.removed_at IS NULL
|
|
5229
|
+
AND c1.deleted_at IS NULL
|
|
5230
|
+
AND c2.deleted_at IS NULL
|
|
5231
|
+
AND c1.lifecycle_state != 'merged'
|
|
5232
|
+
AND c2.lifecycle_state != 'merged'`
|
|
5233
|
+
).all();
|
|
5234
|
+
for (const match of exactMatches) {
|
|
5235
|
+
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
5236
|
+
continue;
|
|
5237
|
+
}
|
|
5238
|
+
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
5239
|
+
leftContactId: match.left_contact_id,
|
|
5240
|
+
rightContactId: match.right_contact_id,
|
|
5241
|
+
matchReason: `exact identity ${match.normalized_key}`,
|
|
5242
|
+
matchScore: 1
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
if (filters?.exactOnly !== true) {
|
|
5246
|
+
for (let index = 0; index < contacts.length; index += 1) {
|
|
5247
|
+
const left = contacts[index];
|
|
5248
|
+
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
5249
|
+
continue;
|
|
5250
|
+
}
|
|
5251
|
+
const leftName = normalizeName(left.name);
|
|
5252
|
+
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
5253
|
+
const right = contacts[offsetIndex];
|
|
5254
|
+
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
5255
|
+
continue;
|
|
5256
|
+
}
|
|
5257
|
+
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
5258
|
+
if (score <= 0) {
|
|
5259
|
+
continue;
|
|
5260
|
+
}
|
|
5261
|
+
const key = pairKey(left.id, right.id);
|
|
5262
|
+
if (!pairs.has(key)) {
|
|
5263
|
+
pairs.set(key, {
|
|
5264
|
+
leftContactId: left.id,
|
|
5265
|
+
rightContactId: right.id,
|
|
5266
|
+
matchReason: "name similarity",
|
|
5267
|
+
matchScore: score
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
5274
|
+
const dismissedSet = new Set(
|
|
5275
|
+
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
5276
|
+
);
|
|
5277
|
+
const includeDismissed = filters?.includeDismissed === true;
|
|
5278
|
+
for (const candidate of pairs.values()) {
|
|
5279
|
+
if (dismissedSet.has(
|
|
5280
|
+
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
5281
|
+
)) {
|
|
5282
|
+
candidate.dismissed = true;
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
const minScore = filters?.minScore ?? 0;
|
|
5286
|
+
return [...pairs.values()].filter((candidate) => {
|
|
5287
|
+
if (candidate.matchScore < minScore) return false;
|
|
5288
|
+
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
5289
|
+
return true;
|
|
5290
|
+
}).sort((left, right) => {
|
|
5291
|
+
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
5292
|
+
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
5293
|
+
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
5294
|
+
}).slice(offset, offset + limit);
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5297
|
+
// src/tools/create_contact_tool_result.ts
|
|
5298
|
+
function createContactToolResult(db, input) {
|
|
5299
|
+
return withTransaction(db, () => {
|
|
5300
|
+
const receipt = createContact(db, input);
|
|
5301
|
+
const result = mutationToolResult("create", receipt, "contact");
|
|
5302
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
5303
|
+
result.outcome = "no_op";
|
|
5304
|
+
Object.assign(result.data, {
|
|
5305
|
+
matchedIdentities: receipt.matchedIdentities
|
|
5306
|
+
});
|
|
5307
|
+
return result;
|
|
5308
|
+
}
|
|
4690
5309
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
4691
5310
|
const warnings = db.prepare(
|
|
4692
5311
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
4693
5312
|
).all(receipt.primary.id).flatMap((row) => {
|
|
4694
|
-
const score =
|
|
4695
|
-
return score
|
|
5313
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
5314
|
+
return score <= 0 ? [] : [
|
|
4696
5315
|
{
|
|
4697
5316
|
code: "possible_duplicate",
|
|
4698
5317
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -4775,6 +5394,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
4775
5394
|
return contact.result;
|
|
4776
5395
|
}
|
|
4777
5396
|
if (input.action === "delete") {
|
|
5397
|
+
if (contact.value.is_owner)
|
|
5398
|
+
return toolFailure(
|
|
5399
|
+
"domain",
|
|
5400
|
+
"conflict",
|
|
5401
|
+
"Cannot delete owner.",
|
|
5402
|
+
"The owner contact cannot be deleted"
|
|
5403
|
+
);
|
|
4778
5404
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
4779
5405
|
return toolSuccess("Permanently deleted contact.", {
|
|
4780
5406
|
action: "delete",
|
|
@@ -4848,7 +5474,11 @@ var manageContactTool = defineAffinityTool({
|
|
|
4848
5474
|
},
|
|
4849
5475
|
["type", "value"]
|
|
4850
5476
|
),
|
|
4851
|
-
"Optional identities to attach.
|
|
5477
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
5478
|
+
),
|
|
5479
|
+
ifExists: enumSchema(
|
|
5480
|
+
"Identity collision policy; return attaches nothing.",
|
|
5481
|
+
["error", "return"]
|
|
4852
5482
|
),
|
|
4853
5483
|
name: stringSchema("Contact name."),
|
|
4854
5484
|
kind: enumSchema("Contact kind.", [
|
|
@@ -5458,6 +6088,268 @@ var manageDuplicateDismissalTool = defineAffinityTool({
|
|
|
5458
6088
|
handler: manageDuplicateDismissalToolHandler
|
|
5459
6089
|
});
|
|
5460
6090
|
|
|
6091
|
+
// src/events/event_replay.ts
|
|
6092
|
+
function replayEventLinks(db, linkIds, now) {
|
|
6093
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
6094
|
+
for (const linkId of linkIds) {
|
|
6095
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
6096
|
+
if (!saved) continue;
|
|
6097
|
+
const baseline = JSON.parse(saved.state_json);
|
|
6098
|
+
const effects = db.prepare(
|
|
6099
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
6100
|
+
).all(linkId);
|
|
6101
|
+
const overrides = db.prepare(
|
|
6102
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
6103
|
+
).all(linkId);
|
|
6104
|
+
let overrideIndex = 0;
|
|
6105
|
+
const applyOverrides = (beforeId) => {
|
|
6106
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
6107
|
+
const override = overrides[overrideIndex];
|
|
6108
|
+
if (override)
|
|
6109
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
6110
|
+
override.state,
|
|
6111
|
+
linkId
|
|
6112
|
+
);
|
|
6113
|
+
overrideIndex++;
|
|
6114
|
+
}
|
|
6115
|
+
};
|
|
6116
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
6117
|
+
db.prepare(
|
|
6118
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
6119
|
+
).run(
|
|
6120
|
+
baseline.rank,
|
|
6121
|
+
baseline.affinity,
|
|
6122
|
+
baseline.trust,
|
|
6123
|
+
baseline.state,
|
|
6124
|
+
baseline.cadence_days,
|
|
6125
|
+
now,
|
|
6126
|
+
linkId
|
|
6127
|
+
);
|
|
6128
|
+
for (const effect of effects) {
|
|
6129
|
+
eventIds.add(effect.event_id);
|
|
6130
|
+
applyOverrides(effect.event_id);
|
|
6131
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
6132
|
+
continue;
|
|
6133
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
6134
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
6135
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
6136
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
6137
|
+
applyEventEffectToLink(db, {
|
|
6138
|
+
linkId,
|
|
6139
|
+
eventId: event.id,
|
|
6140
|
+
eventType: event.type,
|
|
6141
|
+
occurredAt: event.occurredAt,
|
|
6142
|
+
significance: event.significance,
|
|
6143
|
+
participant,
|
|
6144
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
6145
|
+
now: effect.created_at
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6148
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
6149
|
+
refreshLinkRollup(db, linkId, now);
|
|
6150
|
+
}
|
|
6151
|
+
for (const eventId of eventIds)
|
|
6152
|
+
db.prepare(
|
|
6153
|
+
"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=?"
|
|
6154
|
+
).run(eventId);
|
|
6155
|
+
refreshAllBridgeScores(db, now);
|
|
6156
|
+
}
|
|
6157
|
+
|
|
6158
|
+
// src/events/manage_event.ts
|
|
6159
|
+
function requireEventTarget(db, target) {
|
|
6160
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
6161
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
6162
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
6163
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
6164
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
6165
|
+
return event;
|
|
6166
|
+
}
|
|
6167
|
+
function affected(db, id) {
|
|
6168
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
6169
|
+
}
|
|
6170
|
+
function reviseEvent(db, target, patch) {
|
|
6171
|
+
return withTransaction(db, () => {
|
|
6172
|
+
const allowed = [
|
|
6173
|
+
"summary",
|
|
6174
|
+
"content",
|
|
6175
|
+
"occurredAt",
|
|
6176
|
+
"significance",
|
|
6177
|
+
"channel",
|
|
6178
|
+
"type",
|
|
6179
|
+
"externalRef"
|
|
6180
|
+
];
|
|
6181
|
+
for (const key of Object.keys(patch))
|
|
6182
|
+
if (!allowed.includes(key))
|
|
6183
|
+
throw new AffinityValidationError(
|
|
6184
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
6185
|
+
);
|
|
6186
|
+
const event = requireEventTarget(db, target);
|
|
6187
|
+
const next = { ...event, ...patch };
|
|
6188
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
6189
|
+
validateEventMetadata(metadata);
|
|
6190
|
+
Object.assign(next, metadata);
|
|
6191
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
6192
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
6193
|
+
if (patch.occurredAt !== void 0)
|
|
6194
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
6195
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
6196
|
+
throw new AffinityValidationError(
|
|
6197
|
+
"significance must be an integer from 1 to 10"
|
|
6198
|
+
);
|
|
6199
|
+
const interactions = [
|
|
6200
|
+
"conversation",
|
|
6201
|
+
"activity",
|
|
6202
|
+
"gift",
|
|
6203
|
+
"support",
|
|
6204
|
+
"conflict",
|
|
6205
|
+
"correction"
|
|
6206
|
+
];
|
|
6207
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
6208
|
+
throw new AffinityValidationError(
|
|
6209
|
+
"type must stay within the same event kind"
|
|
6210
|
+
);
|
|
6211
|
+
if (patch.externalRef && event.externalRef)
|
|
6212
|
+
throw new AffinityConflictError(
|
|
6213
|
+
"externalRef can only be set when absent"
|
|
6214
|
+
);
|
|
6215
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
6216
|
+
throw new AffinityConflictError(
|
|
6217
|
+
"externalRef already belongs to an event"
|
|
6218
|
+
);
|
|
6219
|
+
const now = Date.now();
|
|
6220
|
+
const links = affected(db, event.id);
|
|
6221
|
+
db.prepare(
|
|
6222
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
6223
|
+
).run(
|
|
6224
|
+
next.summary.trim(),
|
|
6225
|
+
next.content ?? null,
|
|
6226
|
+
next.occurredAt,
|
|
6227
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
6228
|
+
next.significance,
|
|
6229
|
+
next.channel ?? null,
|
|
6230
|
+
next.type,
|
|
6231
|
+
next.externalRef?.source ?? null,
|
|
6232
|
+
next.externalRef?.id ?? null,
|
|
6233
|
+
now,
|
|
6234
|
+
event.id
|
|
6235
|
+
);
|
|
6236
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
6237
|
+
db.prepare(
|
|
6238
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
6239
|
+
).run(patch.type, event.id);
|
|
6240
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
6241
|
+
replayEventLinks(db, links, now);
|
|
6242
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
6243
|
+
updated: [{ kind: "event", id: event.id }],
|
|
6244
|
+
affectedLinks: links
|
|
6245
|
+
});
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
function deleteEvent(db, target, options) {
|
|
6249
|
+
return withTransaction(db, () => {
|
|
6250
|
+
if (options?.confirm !== true)
|
|
6251
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
6252
|
+
const event = requireEventTarget(db, target);
|
|
6253
|
+
const links = affected(db, event.id);
|
|
6254
|
+
for (const table of [
|
|
6255
|
+
"event_participants",
|
|
6256
|
+
"attributes",
|
|
6257
|
+
"open_commitments",
|
|
6258
|
+
"upcoming_occurrences",
|
|
6259
|
+
"link_event_effects"
|
|
6260
|
+
])
|
|
6261
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
6262
|
+
db.prepare(
|
|
6263
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
6264
|
+
).run(event.id);
|
|
6265
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
6266
|
+
replayEventLinks(db, links, Date.now());
|
|
6267
|
+
return buildEventMutationReceipt(event, {
|
|
6268
|
+
removed: [{ kind: "event", id: event.id }],
|
|
6269
|
+
affectedLinks: links
|
|
6270
|
+
});
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
|
|
6274
|
+
// src/tools/manage_event_tool.ts
|
|
6275
|
+
function manageEventToolHandler(db, raw) {
|
|
6276
|
+
const input = structuredClone(raw);
|
|
6277
|
+
const error = validateToolInput(
|
|
6278
|
+
manageEventTool.inputSchema,
|
|
6279
|
+
input,
|
|
6280
|
+
"input",
|
|
6281
|
+
Date.now(),
|
|
6282
|
+
true
|
|
6283
|
+
);
|
|
6284
|
+
if (error)
|
|
6285
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
6286
|
+
return withToolHandling(() => {
|
|
6287
|
+
const target = {
|
|
6288
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
6289
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
6290
|
+
};
|
|
6291
|
+
return mutationToolResult(
|
|
6292
|
+
input.action,
|
|
6293
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
6294
|
+
"event"
|
|
6295
|
+
);
|
|
6296
|
+
}, "Event management failed.");
|
|
6297
|
+
}
|
|
6298
|
+
var manageEventTool = defineAffinityTool({
|
|
6299
|
+
name: manageEventToolName,
|
|
6300
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
6301
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
6302
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
6303
|
+
sideEffects: "writes_state",
|
|
6304
|
+
readOnly: false,
|
|
6305
|
+
supportsClarification: false,
|
|
6306
|
+
targetKinds: ["event"],
|
|
6307
|
+
inputDescriptions: {
|
|
6308
|
+
action: "revise or delete",
|
|
6309
|
+
eventId: "Exact event id",
|
|
6310
|
+
ref: "External event reference",
|
|
6311
|
+
patch: "Fields to revise",
|
|
6312
|
+
confirm: "Required true for deletion"
|
|
6313
|
+
},
|
|
6314
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
6315
|
+
inputSchema: objectSchema(
|
|
6316
|
+
{
|
|
6317
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
6318
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
6319
|
+
ref: externalRefSchema,
|
|
6320
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
6321
|
+
patch: objectSchema(
|
|
6322
|
+
{
|
|
6323
|
+
summary: stringSchema("Short non-empty label."),
|
|
6324
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
6325
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
6326
|
+
significance: integerSchema("Significance 1..10."),
|
|
6327
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
6328
|
+
type: enumSchema("Type within the same event kind.", [
|
|
6329
|
+
"conversation",
|
|
6330
|
+
"activity",
|
|
6331
|
+
"gift",
|
|
6332
|
+
"support",
|
|
6333
|
+
"conflict",
|
|
6334
|
+
"correction",
|
|
6335
|
+
"observation",
|
|
6336
|
+
"milestone",
|
|
6337
|
+
"transaction",
|
|
6338
|
+
"promise",
|
|
6339
|
+
"agreement",
|
|
6340
|
+
"date_anchor"
|
|
6341
|
+
]),
|
|
6342
|
+
externalRef: externalRefSchema
|
|
6343
|
+
},
|
|
6344
|
+
[],
|
|
6345
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
6346
|
+
)
|
|
6347
|
+
},
|
|
6348
|
+
["action"]
|
|
6349
|
+
),
|
|
6350
|
+
handler: manageEventToolHandler
|
|
6351
|
+
});
|
|
6352
|
+
|
|
5461
6353
|
// src/identities/remove_identity.ts
|
|
5462
6354
|
function removeIdentity(db, identityId, removedAt) {
|
|
5463
6355
|
return withTransaction(db, () => {
|
|
@@ -5505,7 +6397,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
5505
6397
|
});
|
|
5506
6398
|
}
|
|
5507
6399
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
5508
|
-
const nextValue =
|
|
6400
|
+
const nextValue = normalizeIdentityValue(
|
|
6401
|
+
nextType,
|
|
6402
|
+
patch.value ?? row.value
|
|
6403
|
+
);
|
|
5509
6404
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
5510
6405
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5511
6406
|
}
|
|
@@ -5564,6 +6459,10 @@ function contactLocatorSchema6(description) {
|
|
|
5564
6459
|
contactId: integerSchema("Exact contact id."),
|
|
5565
6460
|
identity: objectSchema(
|
|
5566
6461
|
{
|
|
6462
|
+
ifExists: enumSchema(
|
|
6463
|
+
"Return an existing identity on this contact without changes.",
|
|
6464
|
+
["error", "return"]
|
|
6465
|
+
),
|
|
5567
6466
|
type: stringSchema(
|
|
5568
6467
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
5569
6468
|
),
|
|
@@ -5660,6 +6559,10 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
5660
6559
|
),
|
|
5661
6560
|
input: objectSchema(
|
|
5662
6561
|
{
|
|
6562
|
+
ifExists: enumSchema(
|
|
6563
|
+
"Return an existing identity on this contact without changes.",
|
|
6564
|
+
["error", "return"]
|
|
6565
|
+
),
|
|
5663
6566
|
type: stringSchema(
|
|
5664
6567
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
5665
6568
|
),
|
|
@@ -5721,6 +6624,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
5721
6624
|
now,
|
|
5722
6625
|
linkId
|
|
5723
6626
|
);
|
|
6627
|
+
db.prepare(
|
|
6628
|
+
"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=?"
|
|
6629
|
+
).run(linkId, state, linkId);
|
|
5724
6630
|
refreshLinkRollup(db, linkId, now);
|
|
5725
6631
|
if (state === "archived" || row.state === "archived") {
|
|
5726
6632
|
refreshAllBridgeScores(db, now);
|
|
@@ -6355,6 +7261,8 @@ var ALLOWED = /* @__PURE__ */ new Set([
|
|
|
6355
7261
|
function recordInteraction(db, input) {
|
|
6356
7262
|
return withTransaction(db, () => {
|
|
6357
7263
|
validateSocialEventInput(input);
|
|
7264
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7265
|
+
if (existing) return existing;
|
|
6358
7266
|
if (!ALLOWED.has(input.type)) {
|
|
6359
7267
|
throw new AffinityValidationError("invalid interaction event type");
|
|
6360
7268
|
}
|
|
@@ -6364,11 +7272,12 @@ function recordInteraction(db, input) {
|
|
|
6364
7272
|
assertOwnerParticipates(ownerId, ids);
|
|
6365
7273
|
const now = resolveNow(input.now);
|
|
6366
7274
|
const eventId = insertJournalEvent(db, {
|
|
7275
|
+
...input,
|
|
6367
7276
|
type: input.type,
|
|
6368
7277
|
occurredAt: input.occurredAt,
|
|
6369
|
-
|
|
7278
|
+
occurredOn: input.occurredOn,
|
|
6370
7279
|
significance: input.significance,
|
|
6371
|
-
momentKind: null,
|
|
7280
|
+
momentKind: input.momentKind ?? null,
|
|
6372
7281
|
participants: input.participants,
|
|
6373
7282
|
now
|
|
6374
7283
|
});
|
|
@@ -6376,12 +7285,24 @@ function recordInteraction(db, input) {
|
|
|
6376
7285
|
eventId,
|
|
6377
7286
|
eventType: input.type,
|
|
6378
7287
|
occurredAt: input.occurredAt,
|
|
7288
|
+
occurredOn: input.occurredOn,
|
|
6379
7289
|
significance: input.significance,
|
|
6380
7290
|
participants: input.participants,
|
|
6381
7291
|
provenance: input.provenance,
|
|
6382
7292
|
now
|
|
6383
7293
|
});
|
|
7294
|
+
if (input.momentKind !== void 0)
|
|
7295
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7296
|
+
input.momentKind,
|
|
7297
|
+
eventId
|
|
7298
|
+
);
|
|
6384
7299
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7300
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7301
|
+
{
|
|
7302
|
+
code: "future_event",
|
|
7303
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7304
|
+
}
|
|
7305
|
+
] : void 0,
|
|
6385
7306
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6386
7307
|
affectedLinks: mechanics.affectedLinks,
|
|
6387
7308
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6393,17 +7314,20 @@ function recordInteraction(db, input) {
|
|
|
6393
7314
|
function recordMilestone(db, input) {
|
|
6394
7315
|
return withTransaction(db, () => {
|
|
6395
7316
|
validateSocialEventInput(input);
|
|
7317
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7318
|
+
if (existing) return existing;
|
|
6396
7319
|
const ownerId = requireOwnerContactId(db);
|
|
6397
7320
|
const ids = input.participants.map((p) => p.contactId);
|
|
6398
7321
|
assertParticipantContactsLive(db, ids);
|
|
6399
7322
|
assertOwnerParticipates(ownerId, ids);
|
|
6400
7323
|
const now = resolveNow(input.now);
|
|
6401
7324
|
const eventId = insertJournalEvent(db, {
|
|
7325
|
+
...input,
|
|
6402
7326
|
type: "milestone",
|
|
6403
7327
|
occurredAt: input.occurredAt,
|
|
6404
|
-
|
|
7328
|
+
occurredOn: input.occurredOn,
|
|
6405
7329
|
significance: input.significance,
|
|
6406
|
-
momentKind: null,
|
|
7330
|
+
momentKind: input.momentKind ?? null,
|
|
6407
7331
|
participants: input.participants,
|
|
6408
7332
|
now
|
|
6409
7333
|
});
|
|
@@ -6411,12 +7335,24 @@ function recordMilestone(db, input) {
|
|
|
6411
7335
|
eventId,
|
|
6412
7336
|
eventType: "milestone",
|
|
6413
7337
|
occurredAt: input.occurredAt,
|
|
7338
|
+
occurredOn: input.occurredOn,
|
|
6414
7339
|
significance: input.significance,
|
|
6415
7340
|
participants: input.participants,
|
|
6416
7341
|
provenance: input.provenance,
|
|
6417
7342
|
now
|
|
6418
7343
|
});
|
|
7344
|
+
if (input.momentKind !== void 0)
|
|
7345
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7346
|
+
input.momentKind,
|
|
7347
|
+
eventId
|
|
7348
|
+
);
|
|
6419
7349
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7350
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7351
|
+
{
|
|
7352
|
+
code: "future_event",
|
|
7353
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7354
|
+
}
|
|
7355
|
+
] : void 0,
|
|
6420
7356
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6421
7357
|
affectedLinks: mechanics.affectedLinks,
|
|
6422
7358
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6428,15 +7364,18 @@ function recordMilestone(db, input) {
|
|
|
6428
7364
|
function recordObservation(db, input) {
|
|
6429
7365
|
return withTransaction(db, () => {
|
|
6430
7366
|
validateSocialEventInput(input);
|
|
7367
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7368
|
+
if (existing) return existing;
|
|
6431
7369
|
const ids = input.participants.map((p) => p.contactId);
|
|
6432
7370
|
assertParticipantContactsLive(db, ids);
|
|
6433
7371
|
const now = resolveNow(input.now);
|
|
6434
7372
|
const eventId = insertJournalEvent(db, {
|
|
7373
|
+
...input,
|
|
6435
7374
|
type: "observation",
|
|
6436
7375
|
occurredAt: input.occurredAt,
|
|
6437
|
-
|
|
7376
|
+
occurredOn: input.occurredOn,
|
|
6438
7377
|
significance: input.significance,
|
|
6439
|
-
momentKind: null,
|
|
7378
|
+
momentKind: input.momentKind ?? null,
|
|
6440
7379
|
participants: input.participants,
|
|
6441
7380
|
now
|
|
6442
7381
|
});
|
|
@@ -6444,12 +7383,24 @@ function recordObservation(db, input) {
|
|
|
6444
7383
|
eventId,
|
|
6445
7384
|
eventType: "observation",
|
|
6446
7385
|
occurredAt: input.occurredAt,
|
|
7386
|
+
occurredOn: input.occurredOn,
|
|
6447
7387
|
significance: input.significance,
|
|
6448
7388
|
participants: input.participants,
|
|
6449
7389
|
provenance: input.provenance,
|
|
6450
7390
|
now
|
|
6451
7391
|
});
|
|
7392
|
+
if (input.momentKind !== void 0)
|
|
7393
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7394
|
+
input.momentKind,
|
|
7395
|
+
eventId
|
|
7396
|
+
);
|
|
6452
7397
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7398
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7399
|
+
{
|
|
7400
|
+
code: "future_event",
|
|
7401
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7402
|
+
}
|
|
7403
|
+
] : void 0,
|
|
6453
7404
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6454
7405
|
affectedLinks: mechanics.affectedLinks,
|
|
6455
7406
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6461,17 +7412,20 @@ function recordObservation(db, input) {
|
|
|
6461
7412
|
function recordTransaction(db, input) {
|
|
6462
7413
|
return withTransaction(db, () => {
|
|
6463
7414
|
validateSocialEventInput(input);
|
|
7415
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7416
|
+
if (existing) return existing;
|
|
6464
7417
|
const ownerId = requireOwnerContactId(db);
|
|
6465
7418
|
const ids = input.participants.map((p) => p.contactId);
|
|
6466
7419
|
assertParticipantContactsLive(db, ids);
|
|
6467
7420
|
assertOwnerParticipates(ownerId, ids);
|
|
6468
7421
|
const now = resolveNow(input.now);
|
|
6469
7422
|
const eventId = insertJournalEvent(db, {
|
|
7423
|
+
...input,
|
|
6470
7424
|
type: "transaction",
|
|
6471
7425
|
occurredAt: input.occurredAt,
|
|
6472
|
-
|
|
7426
|
+
occurredOn: input.occurredOn,
|
|
6473
7427
|
significance: input.significance,
|
|
6474
|
-
momentKind: null,
|
|
7428
|
+
momentKind: input.momentKind ?? null,
|
|
6475
7429
|
participants: input.participants,
|
|
6476
7430
|
now
|
|
6477
7431
|
});
|
|
@@ -6479,12 +7433,24 @@ function recordTransaction(db, input) {
|
|
|
6479
7433
|
eventId,
|
|
6480
7434
|
eventType: "transaction",
|
|
6481
7435
|
occurredAt: input.occurredAt,
|
|
7436
|
+
occurredOn: input.occurredOn,
|
|
6482
7437
|
significance: input.significance,
|
|
6483
7438
|
participants: input.participants,
|
|
6484
7439
|
provenance: input.provenance,
|
|
6485
7440
|
now
|
|
6486
7441
|
});
|
|
7442
|
+
if (input.momentKind !== void 0)
|
|
7443
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7444
|
+
input.momentKind,
|
|
7445
|
+
eventId
|
|
7446
|
+
);
|
|
6487
7447
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7448
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7449
|
+
{
|
|
7450
|
+
code: "future_event",
|
|
7451
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7452
|
+
}
|
|
7453
|
+
] : void 0,
|
|
6488
7454
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6489
7455
|
affectedLinks: mechanics.affectedLinks,
|
|
6490
7456
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6493,11 +7459,13 @@ function recordTransaction(db, input) {
|
|
|
6493
7459
|
}
|
|
6494
7460
|
|
|
6495
7461
|
// src/tools/record_event_tool.ts
|
|
6496
|
-
function normalizeParticipant2(p) {
|
|
7462
|
+
function normalizeParticipant2(db, p) {
|
|
6497
7463
|
let contactId = p.contactId;
|
|
6498
7464
|
if (contactId === void 0 && p.contact != null) {
|
|
6499
7465
|
const loc = p.contact;
|
|
6500
|
-
|
|
7466
|
+
const resolved = resolveContactLocator(db, loc);
|
|
7467
|
+
if (!resolved.ok) throw resolved.result;
|
|
7468
|
+
contactId = resolved.value.id;
|
|
6501
7469
|
}
|
|
6502
7470
|
const result = {
|
|
6503
7471
|
contactId,
|
|
@@ -6515,12 +7483,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
6515
7483
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
6516
7484
|
}
|
|
6517
7485
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
6518
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
7486
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
6519
7487
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
6520
7488
|
}
|
|
6521
7489
|
function recordEventToolHandler(db, rawInput) {
|
|
6522
7490
|
const input = structuredClone(rawInput);
|
|
6523
|
-
const inputError = validateToolInput(
|
|
7491
|
+
const inputError = validateToolInput(
|
|
7492
|
+
recordEventTool.inputSchema,
|
|
7493
|
+
input,
|
|
7494
|
+
"input",
|
|
7495
|
+
Date.now(),
|
|
7496
|
+
true
|
|
7497
|
+
);
|
|
6524
7498
|
if (inputError)
|
|
6525
7499
|
return toolFailure(
|
|
6526
7500
|
"protocol",
|
|
@@ -6600,6 +7574,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
6600
7574
|
"correction"
|
|
6601
7575
|
]
|
|
6602
7576
|
),
|
|
7577
|
+
...eventMetadataSchema,
|
|
6603
7578
|
occurredAt: integerSchema(
|
|
6604
7579
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
6605
7580
|
),
|
|
@@ -6608,6 +7583,19 @@ var recordEventTool = defineAffinityTool({
|
|
|
6608
7583
|
participants: arraySchema(
|
|
6609
7584
|
objectSchema(
|
|
6610
7585
|
{
|
|
7586
|
+
contact: objectSchema(
|
|
7587
|
+
{
|
|
7588
|
+
contactId: integerSchema("Contact id."),
|
|
7589
|
+
identity: objectSchema(
|
|
7590
|
+
{
|
|
7591
|
+
type: stringSchema("Identity type."),
|
|
7592
|
+
value: stringSchema("Identity value.")
|
|
7593
|
+
},
|
|
7594
|
+
["type", "value"]
|
|
7595
|
+
)
|
|
7596
|
+
},
|
|
7597
|
+
[]
|
|
7598
|
+
),
|
|
6611
7599
|
contactId: integerSchema(
|
|
6612
7600
|
"Participant contact id. The owner contact MUST be included."
|
|
6613
7601
|
),
|
|
@@ -6631,7 +7619,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
6631
7619
|
),
|
|
6632
7620
|
now: integerSchema("Optional timestamp override.")
|
|
6633
7621
|
},
|
|
6634
|
-
["occurredAt", "
|
|
7622
|
+
["occurredAt", "significance", "participants"],
|
|
6635
7623
|
"Event payload. Always required. Interaction events also require `type`."
|
|
6636
7624
|
)
|
|
6637
7625
|
},
|
|
@@ -6647,7 +7635,10 @@ function contactAttributeClauses(filters) {
|
|
|
6647
7635
|
const params = [];
|
|
6648
7636
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
6649
7637
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
6650
|
-
for (const filter of [
|
|
7638
|
+
for (const filter of [
|
|
7639
|
+
...attributes,
|
|
7640
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
7641
|
+
]) {
|
|
6651
7642
|
const separator = filter.indexOf("=");
|
|
6652
7643
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
6653
7644
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -6681,153 +7672,32 @@ function listContacts(db, filters, options) {
|
|
|
6681
7672
|
if (filters?.includeDormant === false || options?.includeDormant === false) {
|
|
6682
7673
|
clauses.push("lifecycle_state != 'dormant'");
|
|
6683
7674
|
}
|
|
6684
|
-
const attributeFilters = contactAttributeClauses(filters);
|
|
6685
|
-
clauses.push(...attributeFilters.clauses);
|
|
6686
|
-
params.push(...attributeFilters.params);
|
|
6687
|
-
const where = clauses.join(" AND ");
|
|
6688
|
-
const rows = db.prepare(
|
|
6689
|
-
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
6690
|
-
created_at, updated_at, deleted_at,
|
|
6691
|
-
(
|
|
6692
|
-
SELECT value
|
|
6693
|
-
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);
|
|
7675
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
7676
|
+
clauses.push(...attributeFilters.clauses);
|
|
7677
|
+
params.push(...attributeFilters.params);
|
|
7678
|
+
const where = clauses.join(" AND ");
|
|
7679
|
+
const rows = db.prepare(
|
|
7680
|
+
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
7681
|
+
created_at, updated_at, deleted_at,
|
|
7682
|
+
(
|
|
7683
|
+
SELECT value
|
|
7684
|
+
FROM identities i
|
|
7685
|
+
WHERE i.contact_id = contacts.id AND i.removed_at IS NULL
|
|
7686
|
+
ORDER BY i.id
|
|
7687
|
+
LIMIT 1
|
|
7688
|
+
) AS primary_identity
|
|
7689
|
+
FROM contacts
|
|
7690
|
+
WHERE ${where}
|
|
7691
|
+
ORDER BY name ASC
|
|
7692
|
+
LIMIT ? OFFSET ?`
|
|
7693
|
+
).all(...params, limit, offset);
|
|
7694
|
+
return rows.map((r) => {
|
|
7695
|
+
const item = mapContactRowToContactListItem(r);
|
|
7696
|
+
return {
|
|
7697
|
+
...item,
|
|
7698
|
+
primaryIdentity: r.primary_identity ?? null
|
|
7699
|
+
};
|
|
7700
|
+
});
|
|
6831
7701
|
}
|
|
6832
7702
|
|
|
6833
7703
|
// src/dates/list_upcoming_dates.ts
|
|
@@ -6863,12 +7733,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
6863
7733
|
}
|
|
6864
7734
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
6865
7735
|
const params = [];
|
|
6866
|
-
const since = options?.since ??
|
|
7736
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
6867
7737
|
clauses.push("uo.occurs_on >= ?");
|
|
6868
7738
|
params.push(since);
|
|
6869
7739
|
let maxOccurs = options?.until;
|
|
6870
7740
|
if (filters?.horizonDays !== void 0) {
|
|
6871
|
-
const anchor =
|
|
7741
|
+
const anchor = since;
|
|
6872
7742
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
6873
7743
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
6874
7744
|
}
|
|
@@ -6915,6 +7785,68 @@ function listUpcomingDates(db, filters, options) {
|
|
|
6915
7785
|
return rows.map((r) => mapUpcomingRowToUpcomingDateRecord(r));
|
|
6916
7786
|
}
|
|
6917
7787
|
|
|
7788
|
+
// src/events/event_discovery.ts
|
|
7789
|
+
function searchEvents(db, query, options) {
|
|
7790
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
7791
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
7792
|
+
if (!tokens.length) return [];
|
|
7793
|
+
const rows = db.prepare(
|
|
7794
|
+
"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"
|
|
7795
|
+
).all();
|
|
7796
|
+
return rows.flatMap((row) => {
|
|
7797
|
+
const fields = {
|
|
7798
|
+
summary: decodeEventText(row.summary),
|
|
7799
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
7800
|
+
source: row.external_source,
|
|
7801
|
+
id: row.external_id,
|
|
7802
|
+
channel: row.channel
|
|
7803
|
+
};
|
|
7804
|
+
const folded = Object.entries(fields).map(
|
|
7805
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
7806
|
+
);
|
|
7807
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
7808
|
+
return [];
|
|
7809
|
+
return [
|
|
7810
|
+
{
|
|
7811
|
+
kind: "event",
|
|
7812
|
+
id: row.id,
|
|
7813
|
+
type: row.type,
|
|
7814
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
7815
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
7816
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
7817
|
+
...row.external_source && row.external_id ? {
|
|
7818
|
+
externalRef: {
|
|
7819
|
+
source: row.external_source,
|
|
7820
|
+
id: row.external_id
|
|
7821
|
+
}
|
|
7822
|
+
} : {}
|
|
7823
|
+
}
|
|
7824
|
+
];
|
|
7825
|
+
}).slice(offset, offset + limit);
|
|
7826
|
+
}
|
|
7827
|
+
function getEventThread(db, source, id, options) {
|
|
7828
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
7829
|
+
const root = getEventByExternalRef(db, source, id);
|
|
7830
|
+
if (!root) return [];
|
|
7831
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
7832
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
7833
|
+
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=?))
|
|
7834
|
+
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
7835
|
+
root.id,
|
|
7836
|
+
root.externalRef?.source ?? null,
|
|
7837
|
+
root.externalRef?.id ?? null,
|
|
7838
|
+
options?.channel ?? null,
|
|
7839
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
7840
|
+
limit,
|
|
7841
|
+
offset
|
|
7842
|
+
);
|
|
7843
|
+
return batchLoadEventRecords(
|
|
7844
|
+
db,
|
|
7845
|
+
rows.map((r) => r.id),
|
|
7846
|
+
options?.includeContent ?? false
|
|
7847
|
+
);
|
|
7848
|
+
}
|
|
7849
|
+
|
|
6918
7850
|
// src/graph/get_affinity_chart.ts
|
|
6919
7851
|
function getAffinityChart(db, options) {
|
|
6920
7852
|
const contactClauses = [
|
|
@@ -7100,21 +8032,6 @@ function listProgressionReadiness(db, filters, options) {
|
|
|
7100
8032
|
}
|
|
7101
8033
|
|
|
7102
8034
|
// 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
8035
|
function listRadar(db, filters, options) {
|
|
7119
8036
|
assertDefaultOrdering("listRadar", options);
|
|
7120
8037
|
const ownerId = findOwnerContactId(db);
|
|
@@ -7122,7 +8039,10 @@ function listRadar(db, filters, options) {
|
|
|
7122
8039
|
return [];
|
|
7123
8040
|
}
|
|
7124
8041
|
const now = resolveNow(options?.now);
|
|
7125
|
-
const
|
|
8042
|
+
const today = localCalendarDate(now);
|
|
8043
|
+
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}'))`;
|
|
8044
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
8045
|
+
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
8046
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
7127
8047
|
const clauses = [
|
|
7128
8048
|
"l.removed_at IS NULL",
|
|
@@ -7147,10 +8067,23 @@ function listRadar(db, filters, options) {
|
|
|
7147
8067
|
if (options?.includeObserved === false) {
|
|
7148
8068
|
clauses.push("l.kind != 'observed'");
|
|
7149
8069
|
}
|
|
8070
|
+
if (options?.includeDormant !== true)
|
|
8071
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
8072
|
+
if (filters?.includeLost !== true)
|
|
8073
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
8074
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
8075
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
8076
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
8077
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
8078
|
+
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'))
|
|
8079
|
+
))`);
|
|
7150
8080
|
const where = clauses.join(" AND ");
|
|
7151
8081
|
const rows = db.prepare(
|
|
7152
8082
|
`SELECT
|
|
7153
8083
|
l.id,
|
|
8084
|
+
l.cadence_days,
|
|
8085
|
+
${lastContact} AS last_contact,
|
|
8086
|
+
${drift} AS drift,
|
|
7154
8087
|
${overdue} AS overdue_commitment,
|
|
7155
8088
|
l.from_contact_id,
|
|
7156
8089
|
l.to_contact_id,
|
|
@@ -7164,23 +8097,44 @@ function listRadar(db, filters, options) {
|
|
|
7164
8097
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
7165
8098
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
7166
8099
|
WHERE ${where}
|
|
7167
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
8100
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
7168
8101
|
LIMIT ? OFFSET ?`
|
|
7169
8102
|
).all(...params, limit, offset);
|
|
7170
|
-
return rows.map((row) =>
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
8103
|
+
return rows.map((row) => {
|
|
8104
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
8105
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
8106
|
+
`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`
|
|
8107
|
+
).get(contactId, today, now) : void 0;
|
|
8108
|
+
const reasons = [
|
|
8109
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
8110
|
+
...row.drift ? ["drift"] : []
|
|
8111
|
+
];
|
|
8112
|
+
const snippets = [
|
|
8113
|
+
...promise ? [
|
|
8114
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
8115
|
+
] : [],
|
|
8116
|
+
...row.drift ? [
|
|
8117
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
8118
|
+
] : []
|
|
8119
|
+
];
|
|
8120
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
8121
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
8122
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
8123
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
8124
|
+
trust: row.trust ?? 0,
|
|
8125
|
+
rank: row.rank ?? 0
|
|
8126
|
+
});
|
|
8127
|
+
return {
|
|
8128
|
+
linkId: row.id,
|
|
8129
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
8130
|
+
reasons,
|
|
8131
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
8132
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
8133
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
8134
|
+
trust: row.trust ?? 0,
|
|
8135
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
8136
|
+
};
|
|
8137
|
+
});
|
|
7184
8138
|
}
|
|
7185
8139
|
|
|
7186
8140
|
// src/merges/get_merge_history.ts
|
|
@@ -7250,13 +8204,40 @@ function enrichReviewItem(db, item) {
|
|
|
7250
8204
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
7251
8205
|
}
|
|
7252
8206
|
if (Array.isArray(item.participants)) {
|
|
7253
|
-
result.
|
|
7254
|
-
(p) =>
|
|
8207
|
+
result.participants = item.participants.map(
|
|
8208
|
+
(p) => ({
|
|
8209
|
+
id: p.contactId,
|
|
8210
|
+
name: name(p.contactId),
|
|
8211
|
+
role: p.role
|
|
8212
|
+
})
|
|
7255
8213
|
);
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
8214
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
8215
|
+
result[key] = void 0;
|
|
8216
|
+
}
|
|
8217
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
8218
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
8219
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
8220
|
+
if (result.title === result.summary) result.title = void 0;
|
|
8221
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
8222
|
+
for (const key of [
|
|
8223
|
+
"affinity",
|
|
8224
|
+
"trust",
|
|
8225
|
+
"rank",
|
|
8226
|
+
"recencyScore",
|
|
8227
|
+
"driftPriority",
|
|
8228
|
+
"readinessScore",
|
|
8229
|
+
"normalizedRank",
|
|
8230
|
+
"reciprocityScore",
|
|
8231
|
+
"radarScore",
|
|
8232
|
+
"subtitle",
|
|
8233
|
+
"snippet",
|
|
8234
|
+
"targetRef",
|
|
8235
|
+
"occursOnDate"
|
|
8236
|
+
])
|
|
8237
|
+
result[key] = void 0;
|
|
8238
|
+
} else if (item.kind === "link") {
|
|
8239
|
+
result.recommendedReason = void 0;
|
|
8240
|
+
result.snippet = void 0;
|
|
7260
8241
|
}
|
|
7261
8242
|
if (typeof item.leftContactId === "number")
|
|
7262
8243
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -7285,6 +8266,8 @@ function summarizeCount(count, singular) {
|
|
|
7285
8266
|
// src/tools/review_affinity_tool.ts
|
|
7286
8267
|
function buildReadOptions(input) {
|
|
7287
8268
|
return {
|
|
8269
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
8270
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
7288
8271
|
...input.now === void 0 ? {} : { now: input.now },
|
|
7289
8272
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7290
8273
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -7421,7 +8404,7 @@ function momentItem(moment) {
|
|
|
7421
8404
|
id: moment.eventId,
|
|
7422
8405
|
title: moment.summary,
|
|
7423
8406
|
subtitle: moment.momentKind,
|
|
7424
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8407
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
7425
8408
|
};
|
|
7426
8409
|
}
|
|
7427
8410
|
function commitmentItem(commitment) {
|
|
@@ -7431,7 +8414,7 @@ function commitmentItem(commitment) {
|
|
|
7431
8414
|
id: commitment.eventId,
|
|
7432
8415
|
title: commitment.summary,
|
|
7433
8416
|
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)}`
|
|
8417
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
7435
8418
|
};
|
|
7436
8419
|
}
|
|
7437
8420
|
function radarItem(radar) {
|
|
@@ -7447,6 +8430,7 @@ function radarItem(radar) {
|
|
|
7447
8430
|
function upcomingDateItem(date) {
|
|
7448
8431
|
return {
|
|
7449
8432
|
...date,
|
|
8433
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
7450
8434
|
occursAt: date.occursOn,
|
|
7451
8435
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
7452
8436
|
kind: date.targetRef.kind,
|
|
@@ -7484,6 +8468,9 @@ function compactFilters(input) {
|
|
|
7484
8468
|
);
|
|
7485
8469
|
}
|
|
7486
8470
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
8471
|
+
return reviewPage(db, rawInput, false);
|
|
8472
|
+
}
|
|
8473
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
7487
8474
|
const input = structuredClone(rawInput);
|
|
7488
8475
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
7489
8476
|
if (inputError)
|
|
@@ -7498,6 +8485,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7498
8485
|
if (invalid) {
|
|
7499
8486
|
return invalid;
|
|
7500
8487
|
}
|
|
8488
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
8489
|
+
const limit = input.limit ?? 50;
|
|
8490
|
+
const offset = input.offset ?? 0;
|
|
8491
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
8492
|
+
return toolFailure(
|
|
8493
|
+
"protocol",
|
|
8494
|
+
"invalid_input",
|
|
8495
|
+
"Invalid pagination.",
|
|
8496
|
+
"limit must be 1..250; offset must be non-negative"
|
|
8497
|
+
);
|
|
8498
|
+
const all = [];
|
|
8499
|
+
let page;
|
|
8500
|
+
do {
|
|
8501
|
+
page = reviewPage(
|
|
8502
|
+
db,
|
|
8503
|
+
{ ...input, limit: 250, offset: all.length },
|
|
8504
|
+
true
|
|
8505
|
+
);
|
|
8506
|
+
if (!page.ok) return page;
|
|
8507
|
+
all.push(...page.data.items);
|
|
8508
|
+
} while (page.data.items.length === 250);
|
|
8509
|
+
const items2 = all.slice(offset, offset + limit);
|
|
8510
|
+
return toolSuccess(
|
|
8511
|
+
summarizeCount(items2.length, "item"),
|
|
8512
|
+
{
|
|
8513
|
+
view: input.view,
|
|
8514
|
+
items: items2,
|
|
8515
|
+
count: items2.length,
|
|
8516
|
+
total: all.length,
|
|
8517
|
+
hasMore: offset + items2.length < all.length,
|
|
8518
|
+
appliedFilters: compactFilters(input)
|
|
8519
|
+
},
|
|
8520
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
8521
|
+
);
|
|
8522
|
+
}
|
|
7501
8523
|
const options = buildReadOptions(input);
|
|
7502
8524
|
let items = [];
|
|
7503
8525
|
let chart;
|
|
@@ -7529,6 +8551,19 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7529
8551
|
options
|
|
7530
8552
|
).map(duplicateCandidateItem);
|
|
7531
8553
|
break;
|
|
8554
|
+
case "events.thread": {
|
|
8555
|
+
if (!input.ref)
|
|
8556
|
+
return toolFailure(
|
|
8557
|
+
"protocol",
|
|
8558
|
+
"invalid_input",
|
|
8559
|
+
"Missing thread reference.",
|
|
8560
|
+
"ref is required for events.thread."
|
|
8561
|
+
);
|
|
8562
|
+
items = getEventThread(db, input.ref.source, input.ref.id, options).map(
|
|
8563
|
+
toEventListToolItem
|
|
8564
|
+
);
|
|
8565
|
+
break;
|
|
8566
|
+
}
|
|
7532
8567
|
case "events.contact_journal": {
|
|
7533
8568
|
const locator = input.contact;
|
|
7534
8569
|
if (locator === void 0) {
|
|
@@ -7670,6 +8705,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7670
8705
|
items = listRadar(
|
|
7671
8706
|
db,
|
|
7672
8707
|
{
|
|
8708
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
7673
8709
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
7674
8710
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
7675
8711
|
},
|
|
@@ -7719,6 +8755,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7719
8755
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
7720
8756
|
break;
|
|
7721
8757
|
}
|
|
8758
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
8759
|
+
items = items.map((item) => {
|
|
8760
|
+
const content = getEventById(
|
|
8761
|
+
db,
|
|
8762
|
+
Number(item.eventId ?? item.id)
|
|
8763
|
+
)?.content;
|
|
8764
|
+
return content === void 0 ? item : { ...item, content };
|
|
8765
|
+
});
|
|
7722
8766
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
7723
8767
|
return toolSuccess(
|
|
7724
8768
|
summarizeCount(items.length, "item"),
|
|
@@ -7730,7 +8774,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7730
8774
|
...chart === void 0 ? {} : { chart }
|
|
7731
8775
|
},
|
|
7732
8776
|
{
|
|
7733
|
-
entities: items,
|
|
8777
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
7734
8778
|
warnings: items.length === 0 ? [
|
|
7735
8779
|
toolWarning(
|
|
7736
8780
|
"empty_result",
|
|
@@ -7782,9 +8826,15 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7782
8826
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
7783
8827
|
inputSchema: objectSchema(
|
|
7784
8828
|
{
|
|
8829
|
+
ref: externalRefSchema,
|
|
8830
|
+
includeContent: booleanSchema(
|
|
8831
|
+
"Include verbatim content in event rows; defaults to false."
|
|
8832
|
+
),
|
|
8833
|
+
channel: stringSchema("Filter events by channel."),
|
|
7785
8834
|
view: enumSchema("Which review surface to return.", [
|
|
7786
8835
|
"contacts.list",
|
|
7787
8836
|
"contacts.duplicates",
|
|
8837
|
+
"events.thread",
|
|
7788
8838
|
"events.contact_journal",
|
|
7789
8839
|
"events.link_timeline",
|
|
7790
8840
|
"events.moments",
|
|
@@ -7839,7 +8889,20 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7839
8889
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
7840
8890
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
7841
8891
|
eventTypes: arraySchema(
|
|
7842
|
-
|
|
8892
|
+
enumSchema("Event type.", [
|
|
8893
|
+
"conversation",
|
|
8894
|
+
"activity",
|
|
8895
|
+
"gift",
|
|
8896
|
+
"support",
|
|
8897
|
+
"milestone",
|
|
8898
|
+
"observation",
|
|
8899
|
+
"conflict",
|
|
8900
|
+
"correction",
|
|
8901
|
+
"transaction",
|
|
8902
|
+
"promise",
|
|
8903
|
+
"agreement",
|
|
8904
|
+
"date_anchor"
|
|
8905
|
+
]),
|
|
7843
8906
|
"Optional event-type filter."
|
|
7844
8907
|
),
|
|
7845
8908
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -7879,7 +8942,7 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7879
8942
|
|
|
7880
8943
|
// src/contacts/search_contacts.ts
|
|
7881
8944
|
function searchContacts(db, query, filters, options) {
|
|
7882
|
-
const q = foldSearchText(query.trim());
|
|
8945
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
7883
8946
|
if (!q) return [];
|
|
7884
8947
|
assertDefaultOrdering("searchContacts", options);
|
|
7885
8948
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -7899,6 +8962,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
7899
8962
|
const tokens = q.split(/\s+/);
|
|
7900
8963
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
7901
8964
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
8965
|
+
const suffix = digits.replace(/^0/, "");
|
|
7902
8966
|
const attributeFilters = contactAttributeClauses(filters);
|
|
7903
8967
|
const clauses = [
|
|
7904
8968
|
"deleted_at IS NULL",
|
|
@@ -7934,7 +8998,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
7934
8998
|
)
|
|
7935
8999
|
];
|
|
7936
9000
|
const phoneFields = phone ? values.filter(
|
|
7937
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
9001
|
+
(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
9002
|
) : [];
|
|
7939
9003
|
if (!phoneFields.length && !tokens.every(
|
|
7940
9004
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -7964,6 +9028,9 @@ function searchContacts(db, query, filters, options) {
|
|
|
7964
9028
|
|
|
7965
9029
|
// src/tools/search_affinity_tool.ts
|
|
7966
9030
|
function searchAffinityToolHandler(db, rawInput) {
|
|
9031
|
+
return searchPage(db, rawInput, false);
|
|
9032
|
+
}
|
|
9033
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
7967
9034
|
const input = structuredClone(rawInput);
|
|
7968
9035
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
7969
9036
|
if (inputError)
|
|
@@ -7974,11 +9041,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
7974
9041
|
inputError
|
|
7975
9042
|
);
|
|
7976
9043
|
return withToolHandling(() => {
|
|
9044
|
+
if (!pageOnly) {
|
|
9045
|
+
const limit = input.limit ?? 50;
|
|
9046
|
+
const offset = input.offset ?? 0;
|
|
9047
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
9048
|
+
return toolFailure(
|
|
9049
|
+
"protocol",
|
|
9050
|
+
"invalid_input",
|
|
9051
|
+
"Invalid pagination.",
|
|
9052
|
+
"limit must be 1..250; offset must be non-negative"
|
|
9053
|
+
);
|
|
9054
|
+
const all = [];
|
|
9055
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
9056
|
+
for (const scope of scopes) {
|
|
9057
|
+
let page;
|
|
9058
|
+
let nextOffset = 0;
|
|
9059
|
+
do {
|
|
9060
|
+
page = searchPage(
|
|
9061
|
+
db,
|
|
9062
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
9063
|
+
true
|
|
9064
|
+
);
|
|
9065
|
+
if (!page.ok) return page;
|
|
9066
|
+
all.push(...page.data.items);
|
|
9067
|
+
nextOffset += page.data.count;
|
|
9068
|
+
} while (page.data.count === 250);
|
|
9069
|
+
}
|
|
9070
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9071
|
+
return toolSuccess(
|
|
9072
|
+
summarizeCount(
|
|
9073
|
+
items2.length,
|
|
9074
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
9075
|
+
),
|
|
9076
|
+
{
|
|
9077
|
+
query: input.query,
|
|
9078
|
+
items: items2,
|
|
9079
|
+
count: items2.length,
|
|
9080
|
+
total: all.length,
|
|
9081
|
+
hasMore: offset + items2.length < all.length
|
|
9082
|
+
},
|
|
9083
|
+
{ entities: items2 }
|
|
9084
|
+
);
|
|
9085
|
+
}
|
|
7977
9086
|
const options = {
|
|
7978
9087
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7979
9088
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
7980
9089
|
};
|
|
7981
|
-
const rows = searchContacts(
|
|
9090
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
7982
9091
|
db,
|
|
7983
9092
|
input.query,
|
|
7984
9093
|
{
|
|
@@ -7992,9 +9101,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
7992
9101
|
},
|
|
7993
9102
|
options
|
|
7994
9103
|
);
|
|
7995
|
-
const items =
|
|
9104
|
+
const items = [
|
|
9105
|
+
...rows.map(toContactListToolItem),
|
|
9106
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
9107
|
+
];
|
|
7996
9108
|
return toolSuccess(
|
|
7997
|
-
summarizeCount(
|
|
9109
|
+
summarizeCount(
|
|
9110
|
+
items.length,
|
|
9111
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
9112
|
+
),
|
|
7998
9113
|
{
|
|
7999
9114
|
query: input.query,
|
|
8000
9115
|
items,
|
|
@@ -8030,6 +9145,11 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
8030
9145
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
8031
9146
|
inputSchema: objectSchema(
|
|
8032
9147
|
{
|
|
9148
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
9149
|
+
"contacts",
|
|
9150
|
+
"events",
|
|
9151
|
+
"all"
|
|
9152
|
+
]),
|
|
8033
9153
|
query: stringSchema("Free-text or natural-key query."),
|
|
8034
9154
|
kind: enumSchema("Optional contact kind filter.", [
|
|
8035
9155
|
"human",
|
|
@@ -8081,72 +9201,36 @@ var affinityTools = [
|
|
|
8081
9201
|
recordEventTool,
|
|
8082
9202
|
manageCommitmentTool,
|
|
8083
9203
|
manageDateAnchorTool,
|
|
8084
|
-
manageAttributeTool
|
|
9204
|
+
manageAttributeTool,
|
|
9205
|
+
manageEventTool
|
|
8085
9206
|
];
|
|
8086
9207
|
|
|
8087
9208
|
// 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()) {
|
|
9209
|
+
function normalizeTimeInputs(value, now = Date.now(), inPatch = false) {
|
|
8141
9210
|
if (Array.isArray(value))
|
|
8142
9211
|
return value.map((item) => normalizeTimeInputs(item, now));
|
|
8143
9212
|
if (!value || typeof value !== "object") return value;
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
9213
|
+
const result = {};
|
|
9214
|
+
for (const [key, item] of Object.entries(value)) {
|
|
9215
|
+
if (key === "occurredAt" && item === null)
|
|
9216
|
+
throw new CliUsageError(
|
|
9217
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
9218
|
+
);
|
|
9219
|
+
if (typeof item === "string" && (key.endsWith("At") || ["asOf", "now", "since", "until"].includes(key))) {
|
|
9220
|
+
let parsed;
|
|
9221
|
+
try {
|
|
9222
|
+
parsed = parseCalendarTime(item, key, now);
|
|
9223
|
+
} catch (error) {
|
|
9224
|
+
throw new CliUsageError(
|
|
9225
|
+
`Invalid timestamp "${item}". ${error.message}`
|
|
9226
|
+
);
|
|
9227
|
+
}
|
|
9228
|
+
result[key] = parsed.timestamp;
|
|
9229
|
+
if (!inPatch && ["occurredAt", "dueAt"].includes(key))
|
|
9230
|
+
result[key.replace(/At$/, "On")] = parsed.day;
|
|
9231
|
+
} else result[key] = normalizeTimeInputs(item, now, key === "patch");
|
|
9232
|
+
}
|
|
9233
|
+
return result;
|
|
8150
9234
|
}
|
|
8151
9235
|
|
|
8152
9236
|
// src/surface/timestamp_schema.ts
|
|
@@ -8173,20 +9257,57 @@ function timestampSchema(schema) {
|
|
|
8173
9257
|
}
|
|
8174
9258
|
|
|
8175
9259
|
// src/surface/affinity_surface_tools.ts
|
|
9260
|
+
function surfaceProperties(tool) {
|
|
9261
|
+
return {
|
|
9262
|
+
...tool.inputSchema.properties,
|
|
9263
|
+
...["inspect_affinity_item", "review_affinity", "manage_event"].includes(
|
|
9264
|
+
tool.name
|
|
9265
|
+
) ? {
|
|
9266
|
+
refSource: {
|
|
9267
|
+
type: "string",
|
|
9268
|
+
description: "External reference source."
|
|
9269
|
+
},
|
|
9270
|
+
refId: {
|
|
9271
|
+
type: "string",
|
|
9272
|
+
description: "External reference id."
|
|
9273
|
+
}
|
|
9274
|
+
} : {},
|
|
9275
|
+
...tool.name === "review_affinity" ? {
|
|
9276
|
+
withContent: {
|
|
9277
|
+
type: "boolean",
|
|
9278
|
+
description: "Include verbatim event content."
|
|
9279
|
+
}
|
|
9280
|
+
} : {}
|
|
9281
|
+
};
|
|
9282
|
+
}
|
|
8176
9283
|
var affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
8177
9284
|
name: tool.name,
|
|
8178
9285
|
description: tool.description,
|
|
8179
|
-
whenToUse: tool.whenToUse,
|
|
8180
|
-
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
8181
9286
|
readOnly: tool.readOnly,
|
|
8182
9287
|
sideEffects: tool.sideEffects,
|
|
8183
9288
|
inputSchema: {
|
|
8184
|
-
...timestampSchema({
|
|
9289
|
+
...timestampSchema({
|
|
9290
|
+
...tool.inputSchema,
|
|
9291
|
+
properties: surfaceProperties(tool),
|
|
9292
|
+
type: "object"
|
|
9293
|
+
}),
|
|
8185
9294
|
type: "object"
|
|
8186
9295
|
},
|
|
8187
9296
|
handler: (db, input) => {
|
|
8188
9297
|
try {
|
|
8189
|
-
|
|
9298
|
+
const { withContent, refSource, refId, ...normalized } = input;
|
|
9299
|
+
if (withContent !== void 0) normalized.includeContent = withContent;
|
|
9300
|
+
if (refSource !== void 0 || refId !== void 0) {
|
|
9301
|
+
if (normalized.ref !== void 0)
|
|
9302
|
+
return toolFailure(
|
|
9303
|
+
"protocol",
|
|
9304
|
+
"invalid_input",
|
|
9305
|
+
"Invalid target.",
|
|
9306
|
+
"Provide ref or ref-source/ref-id, not both."
|
|
9307
|
+
);
|
|
9308
|
+
normalized.ref = { source: refSource, id: refId };
|
|
9309
|
+
}
|
|
9310
|
+
return tool.handler(db, normalizeTimeInputs(normalized));
|
|
8190
9311
|
} catch (error) {
|
|
8191
9312
|
if (error instanceof CliUsageError)
|
|
8192
9313
|
return toolFailure(
|
|
@@ -8211,7 +9332,7 @@ function affinityMcpOptions(context) {
|
|
|
8211
9332
|
context,
|
|
8212
9333
|
annotations: (tool) => ({
|
|
8213
9334
|
readOnlyHint: tool.readOnly ?? false,
|
|
8214
|
-
destructiveHint: tool.name === "merge_contacts" || /remove|unset|undismiss/.test(
|
|
9335
|
+
destructiveHint: tool.name === "merge_contacts" || tool.name === "manage_event" || /remove|unset|undismiss/.test(
|
|
8215
9336
|
String(tool.inputSchema.properties?.action?.enum ?? [])
|
|
8216
9337
|
),
|
|
8217
9338
|
idempotentHint: tool.readOnly ?? false,
|
|
@@ -8220,13 +9341,95 @@ function affinityMcpOptions(context) {
|
|
|
8220
9341
|
};
|
|
8221
9342
|
}
|
|
8222
9343
|
|
|
9344
|
+
// src/events/init_events_tables.ts
|
|
9345
|
+
function initEventsTables(db) {
|
|
9346
|
+
db.exec(`
|
|
9347
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9348
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9349
|
+
type TEXT NOT NULL,
|
|
9350
|
+
occurred_at INTEGER NOT NULL,
|
|
9351
|
+
summary TEXT NOT NULL,
|
|
9352
|
+
significance REAL NOT NULL DEFAULT 0,
|
|
9353
|
+
moment_kind TEXT,
|
|
9354
|
+
recurrence_kind TEXT,
|
|
9355
|
+
anchor_month INTEGER,
|
|
9356
|
+
anchor_day INTEGER,
|
|
9357
|
+
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
9358
|
+
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
9359
|
+
created_at INTEGER NOT NULL,
|
|
9360
|
+
updated_at INTEGER NOT NULL,
|
|
9361
|
+
deleted_at INTEGER,
|
|
9362
|
+
CHECK (length(trim(summary)) > 0),
|
|
9363
|
+
CHECK (
|
|
9364
|
+
type IN (
|
|
9365
|
+
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
9366
|
+
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
9367
|
+
)
|
|
9368
|
+
),
|
|
9369
|
+
CHECK (
|
|
9370
|
+
(type = 'date_anchor' AND (
|
|
9371
|
+
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
9372
|
+
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
9373
|
+
))
|
|
9374
|
+
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
9375
|
+
),
|
|
9376
|
+
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
9377
|
+
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
9378
|
+
)),
|
|
9379
|
+
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
9380
|
+
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
9381
|
+
)),
|
|
9382
|
+
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
9383
|
+
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
9384
|
+
)
|
|
9385
|
+
`);
|
|
9386
|
+
db.exec(
|
|
9387
|
+
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
9388
|
+
);
|
|
9389
|
+
db.exec(
|
|
9390
|
+
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
9391
|
+
);
|
|
9392
|
+
const columns = db.prepare("PRAGMA table_info(events)").all();
|
|
9393
|
+
if (!columns.some((c) => c.name === "declared_moment_kind"))
|
|
9394
|
+
db.exec("ALTER TABLE events ADD COLUMN declared_moment_kind TEXT");
|
|
9395
|
+
if (!columns.some((c) => c.name === "affects_bond"))
|
|
9396
|
+
db.exec("ALTER TABLE events ADD COLUMN affects_bond INTEGER");
|
|
9397
|
+
if (!columns.some((c) => c.name === "content"))
|
|
9398
|
+
db.exec("ALTER TABLE events ADD COLUMN content TEXT");
|
|
9399
|
+
if (!columns.some((c) => c.name === "external_source"))
|
|
9400
|
+
db.exec("ALTER TABLE events ADD COLUMN external_source TEXT");
|
|
9401
|
+
if (!columns.some((c) => c.name === "external_id"))
|
|
9402
|
+
db.exec("ALTER TABLE events ADD COLUMN external_id TEXT");
|
|
9403
|
+
if (!columns.some((c) => c.name === "channel"))
|
|
9404
|
+
db.exec("ALTER TABLE events ADD COLUMN channel TEXT");
|
|
9405
|
+
if (!columns.some((c) => c.name === "parent_source"))
|
|
9406
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_source TEXT");
|
|
9407
|
+
if (!columns.some((c) => c.name === "parent_ref_id"))
|
|
9408
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_ref_id TEXT");
|
|
9409
|
+
if (!columns.some((c) => c.name === "parent_event_id"))
|
|
9410
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_event_id INTEGER");
|
|
9411
|
+
if (!columns.some((c) => c.name === "thread_source"))
|
|
9412
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_source TEXT");
|
|
9413
|
+
if (!columns.some((c) => c.name === "thread_id"))
|
|
9414
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_id TEXT");
|
|
9415
|
+
if (!columns.some((c) => c.name === "provenance_json"))
|
|
9416
|
+
db.exec("ALTER TABLE events ADD COLUMN provenance_json TEXT");
|
|
9417
|
+
db.exec(
|
|
9418
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external_ref ON events(external_source, external_id)"
|
|
9419
|
+
);
|
|
9420
|
+
if (!columns.some((column) => column.name === "occurred_on"))
|
|
9421
|
+
db.exec("ALTER TABLE events ADD COLUMN occurred_on TEXT");
|
|
9422
|
+
}
|
|
9423
|
+
|
|
8223
9424
|
// src/attributes/init_attributes_tables.ts
|
|
8224
9425
|
function initAttributesTables(db) {
|
|
9426
|
+
initEventsTables(db);
|
|
8225
9427
|
db.exec(`
|
|
8226
9428
|
CREATE TABLE IF NOT EXISTS attributes (
|
|
8227
9429
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8228
9430
|
contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
8229
9431
|
link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
9432
|
+
event_id INTEGER REFERENCES events (id) ON DELETE CASCADE,
|
|
8230
9433
|
name TEXT NOT NULL,
|
|
8231
9434
|
value TEXT,
|
|
8232
9435
|
created_at INTEGER NOT NULL,
|
|
@@ -8234,15 +9437,35 @@ function initAttributesTables(db) {
|
|
|
8234
9437
|
deleted_at INTEGER,
|
|
8235
9438
|
CHECK (length(trim(name)) > 0),
|
|
8236
9439
|
CHECK (
|
|
8237
|
-
(contact_id IS NOT NULL
|
|
8238
|
-
OR
|
|
8239
|
-
(contact_id IS NULL AND link_id IS NOT NULL)
|
|
9440
|
+
(contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1
|
|
8240
9441
|
)
|
|
8241
9442
|
)
|
|
8242
9443
|
`);
|
|
9444
|
+
const columns = db.prepare("PRAGMA table_info(attributes)").all();
|
|
9445
|
+
if (!columns.some((c) => c.name === "event_id")) {
|
|
9446
|
+
withTransaction(
|
|
9447
|
+
db,
|
|
9448
|
+
() => db.exec(`CREATE TABLE attributes_upgrade (
|
|
9449
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9450
|
+
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
|
|
9451
|
+
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
|
|
9452
|
+
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
|
|
9453
|
+
name TEXT NOT NULL CHECK(length(trim(name)) > 0), value TEXT,
|
|
9454
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
9455
|
+
CHECK ((contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1)
|
|
9456
|
+
);
|
|
9457
|
+
INSERT INTO attributes_upgrade (id,contact_id,link_id,name,value,created_at,updated_at,deleted_at)
|
|
9458
|
+
SELECT id,contact_id,link_id,name,value,created_at,updated_at,deleted_at FROM attributes;
|
|
9459
|
+
DROP TABLE attributes;
|
|
9460
|
+
ALTER TABLE attributes_upgrade RENAME TO attributes;`)
|
|
9461
|
+
);
|
|
9462
|
+
}
|
|
8243
9463
|
db.exec(
|
|
8244
9464
|
"CREATE INDEX IF NOT EXISTS idx_attributes_contact ON attributes(contact_id) WHERE deleted_at IS NULL"
|
|
8245
9465
|
);
|
|
9466
|
+
db.exec(
|
|
9467
|
+
"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"
|
|
9468
|
+
);
|
|
8246
9469
|
db.exec(
|
|
8247
9470
|
"CREATE INDEX IF NOT EXISTS idx_attributes_link ON attributes(link_id) WHERE deleted_at IS NULL"
|
|
8248
9471
|
);
|
|
@@ -8355,56 +9578,6 @@ function initEventParticipantsTables(db) {
|
|
|
8355
9578
|
);
|
|
8356
9579
|
}
|
|
8357
9580
|
|
|
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
9581
|
// src/events/init_open_commitments_tables.ts
|
|
8409
9582
|
function initOpenCommitmentsTables(db) {
|
|
8410
9583
|
db.exec(`
|
|
@@ -8424,6 +9597,9 @@ function initOpenCommitmentsTables(db) {
|
|
|
8424
9597
|
)
|
|
8425
9598
|
)
|
|
8426
9599
|
`);
|
|
9600
|
+
const columns = db.prepare("PRAGMA table_info(open_commitments)").all();
|
|
9601
|
+
if (!columns.some((column) => column.name === "due_on"))
|
|
9602
|
+
db.exec("ALTER TABLE open_commitments ADD COLUMN due_on TEXT");
|
|
8427
9603
|
}
|
|
8428
9604
|
|
|
8429
9605
|
// src/identities/init_identities_tables.ts
|
|
@@ -8488,6 +9664,21 @@ function initLinkEventEffectsTables(db) {
|
|
|
8488
9664
|
db.exec(
|
|
8489
9665
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
8490
9666
|
);
|
|
9667
|
+
for (const name of ["participant_json", "provenance_json"])
|
|
9668
|
+
if (!columns.some((c) => c.name === name))
|
|
9669
|
+
db.exec(`ALTER TABLE link_event_effects ADD COLUMN ${name} TEXT`);
|
|
9670
|
+
db.exec(
|
|
9671
|
+
"CREATE TABLE IF NOT EXISTS event_link_baselines (link_id INTEGER PRIMARY KEY REFERENCES links(id) ON DELETE CASCADE, state_json TEXT NOT NULL)"
|
|
9672
|
+
);
|
|
9673
|
+
db.exec(
|
|
9674
|
+
"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)"
|
|
9675
|
+
);
|
|
9676
|
+
db.exec(`INSERT OR IGNORE INTO event_link_baselines(link_id,state_json)
|
|
9677
|
+
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)
|
|
9678
|
+
FROM links l JOIN link_event_effects e ON e.link_id=l.id GROUP BY l.id`);
|
|
9679
|
+
db.exec(
|
|
9680
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id IN (SELECT id FROM events WHERE type IN ('promise', 'agreement'))"
|
|
9681
|
+
);
|
|
8491
9682
|
db.exec(
|
|
8492
9683
|
"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
9684
|
);
|
|
@@ -8663,7 +9854,10 @@ async function openAffinityDb(path) {
|
|
|
8663
9854
|
} finally {
|
|
8664
9855
|
process.emitWarning = original;
|
|
8665
9856
|
}
|
|
8666
|
-
if (path !== ":memory:"
|
|
9857
|
+
if (path !== ":memory:" && !path.startsWith("file:")) {
|
|
9858
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
9859
|
+
if (!existsSync(path)) closeSync(openSync(path, "wx", 384));
|
|
9860
|
+
}
|
|
8667
9861
|
const db = new sqlite.DatabaseSync(path);
|
|
8668
9862
|
try {
|
|
8669
9863
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
|
|
@@ -8687,6 +9881,13 @@ function isErrorResult(result) {
|
|
|
8687
9881
|
|
|
8688
9882
|
// src/surfaces/validate_input.ts
|
|
8689
9883
|
function validateInput(schema, input, path = "$") {
|
|
9884
|
+
if (path.endsWith(".occurredAt") && input == null)
|
|
9885
|
+
return ["occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"];
|
|
9886
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
9887
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && input !== null && typeof input !== "string")
|
|
9888
|
+
return [
|
|
9889
|
+
`attribute values are strings or null; pass "${String(input)}" to store it as text`
|
|
9890
|
+
];
|
|
8690
9891
|
const errors = [];
|
|
8691
9892
|
const matchesType = (type) => {
|
|
8692
9893
|
switch (type) {
|
|
@@ -8717,6 +9918,10 @@ function validateInput(schema, input, path = "$") {
|
|
|
8717
9918
|
(branch) => validateInput(branch, input, path).length === 0
|
|
8718
9919
|
))
|
|
8719
9920
|
errors.push(`${path}: must match at least one schema branch`);
|
|
9921
|
+
if (schema.anyOf && !schema.anyOf.some(
|
|
9922
|
+
(branch) => validateInput(branch, input, path).length === 0
|
|
9923
|
+
))
|
|
9924
|
+
errors.push(`${path}: must match at least one schema branch`);
|
|
8720
9925
|
if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
8721
9926
|
const record = input;
|
|
8722
9927
|
for (const name of schema.required ?? [])
|