@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.cjs
CHANGED
|
@@ -5,7 +5,7 @@ var node_path = require('node:path');
|
|
|
5
5
|
var node_os = require('node:os');
|
|
6
6
|
|
|
7
7
|
// src/version.ts
|
|
8
|
-
var affinityVersion = "0.
|
|
8
|
+
var affinityVersion = "0.6.0";
|
|
9
9
|
|
|
10
10
|
// src/surface/affinity_mcp_instructions.ts
|
|
11
11
|
var affinityMcpInstructions = "Affinity is a local relationship model and social CRM. Search before creating contacts. Inspect before revising or merging. Record direct evidence honestly, keep identities and structural ties distinct from social links, and ask for clarification when a target is ambiguous. Review commitments and recurring dates regularly. Derived scores guide attention and do not prove relationship quality.";
|
|
@@ -78,6 +78,158 @@ var AffinityNotFoundError = class extends AffinityError {
|
|
|
78
78
|
code = "NOT_FOUND";
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// src/lib/normalize_mutation_receipt.ts
|
|
82
|
+
function uniqueEntityRefs(refs) {
|
|
83
|
+
const out = [];
|
|
84
|
+
const seen = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const ref of refs ?? []) {
|
|
86
|
+
const key = `${ref.kind}:${ref.id}`;
|
|
87
|
+
if (seen.has(key)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
seen.add(key);
|
|
91
|
+
out.push(ref);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function uniqueDerivedEffects(effects) {
|
|
96
|
+
const out = [];
|
|
97
|
+
const seen = /* @__PURE__ */ new Set();
|
|
98
|
+
for (const effect of effects ?? []) {
|
|
99
|
+
const key = `${effect.eventId}:${effect.linkId}`;
|
|
100
|
+
if (seen.has(key)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
seen.add(key);
|
|
104
|
+
out.push(effect);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function uniqueAffectedLinks(affectedLinks, derivedEffects) {
|
|
109
|
+
const out = [];
|
|
110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const linkId of affectedLinks ?? []) {
|
|
112
|
+
if (seen.has(linkId)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
seen.add(linkId);
|
|
116
|
+
out.push(linkId);
|
|
117
|
+
}
|
|
118
|
+
for (const effect of derivedEffects) {
|
|
119
|
+
if (seen.has(effect.linkId)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
seen.add(effect.linkId);
|
|
123
|
+
out.push(effect.linkId);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function normalizeMutationReceiptPartial(partial) {
|
|
128
|
+
const derivedEffects = uniqueDerivedEffects(partial?.derivedEffects);
|
|
129
|
+
return {
|
|
130
|
+
created: uniqueEntityRefs(partial?.created),
|
|
131
|
+
updated: uniqueEntityRefs(partial?.updated),
|
|
132
|
+
archived: uniqueEntityRefs(partial?.archived),
|
|
133
|
+
removed: uniqueEntityRefs(partial?.removed),
|
|
134
|
+
affectedLinks: uniqueAffectedLinks(partial?.affectedLinks, derivedEffects),
|
|
135
|
+
derivedEffects
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/events/receipts.ts
|
|
140
|
+
function buildEventMutationReceipt(primary, partial) {
|
|
141
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
142
|
+
return {
|
|
143
|
+
primary,
|
|
144
|
+
...partial?.warnings ? { warnings: partial.warnings } : {},
|
|
145
|
+
created: normalized.created,
|
|
146
|
+
updated: normalized.updated,
|
|
147
|
+
archived: normalized.archived,
|
|
148
|
+
removed: normalized.removed,
|
|
149
|
+
affectedLinks: normalized.affectedLinks,
|
|
150
|
+
derivedEffects: normalized.derivedEffects
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/events/event_metadata.ts
|
|
155
|
+
function normalizeExternalRef(ref) {
|
|
156
|
+
if (!ref || typeof ref.source !== "string" || typeof ref.id !== "string" || !ref.source.trim() || !ref.id.trim())
|
|
157
|
+
throw new AffinityValidationError(
|
|
158
|
+
"external reference source and id must be non-empty strings"
|
|
159
|
+
);
|
|
160
|
+
return { source: ref.source.trim().toLowerCase(), id: ref.id.trim() };
|
|
161
|
+
}
|
|
162
|
+
function deriveEventSummary(content) {
|
|
163
|
+
const line = content.split(/\r?\n/).find((line2) => line2.trim())?.replace(/\s+/g, " ").trim() ?? "(no text)";
|
|
164
|
+
return line.length > 140 ? `${line.slice(0, 139)}\u2026` : line;
|
|
165
|
+
}
|
|
166
|
+
function validateEventMetadata(input) {
|
|
167
|
+
if (input.affectsBond !== void 0 && typeof input.affectsBond !== "boolean")
|
|
168
|
+
throw new AffinityValidationError("affectsBond must be boolean");
|
|
169
|
+
if (input.content !== void 0 && (typeof input.content !== "string" || Buffer.byteLength(input.content, "utf8") > 1048576))
|
|
170
|
+
throw new AffinityValidationError(
|
|
171
|
+
"content must be a string of at most 1 MB (1048576 UTF-8 bytes)"
|
|
172
|
+
);
|
|
173
|
+
for (const key of ["externalRef", "parentRef", "threadRef"])
|
|
174
|
+
if (input[key] !== void 0) input[key] = normalizeExternalRef(input[key]);
|
|
175
|
+
if (input.channel !== void 0) {
|
|
176
|
+
if (typeof input.channel !== "string" || !input.channel.trim() || input.channel.trim().length > 40)
|
|
177
|
+
throw new AffinityValidationError(
|
|
178
|
+
"channel must be non-empty and at most 40 characters"
|
|
179
|
+
);
|
|
180
|
+
input.channel = input.channel.trim().toLowerCase();
|
|
181
|
+
}
|
|
182
|
+
if (input.parentEventId !== void 0 && (!Number.isSafeInteger(input.parentEventId) || input.parentEventId <= 0))
|
|
183
|
+
throw new AffinityValidationError(
|
|
184
|
+
"parentEventId must be a positive integer"
|
|
185
|
+
);
|
|
186
|
+
const moments = [
|
|
187
|
+
"breakthrough",
|
|
188
|
+
"rupture",
|
|
189
|
+
"reconciliation",
|
|
190
|
+
"milestone",
|
|
191
|
+
"turning_point"
|
|
192
|
+
];
|
|
193
|
+
if (input.momentKind !== void 0 && !moments.includes(input.momentKind))
|
|
194
|
+
throw new AffinityValidationError(
|
|
195
|
+
`momentKind must be one of: ${moments.join(", ")}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
function getEventById(db, eventId) {
|
|
199
|
+
return db.prepare("SELECT 1 FROM events WHERE id=? AND deleted_at IS NULL").get(eventId) ? loadEventRecord(db, eventId) : null;
|
|
200
|
+
}
|
|
201
|
+
function getEventByExternalRef(db, source, id) {
|
|
202
|
+
const ref = normalizeExternalRef({ source, id });
|
|
203
|
+
const row = db.prepare(
|
|
204
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
205
|
+
).get(ref.source, ref.id);
|
|
206
|
+
return row ? loadEventRecord(db, row.id) : null;
|
|
207
|
+
}
|
|
208
|
+
function existingEventReceipt(db, ref) {
|
|
209
|
+
const event = ref && getEventByExternalRef(db, ref.source, ref.id);
|
|
210
|
+
return event ? { ...buildEventMutationReceipt(event, {}), outcome: "no_op" } : null;
|
|
211
|
+
}
|
|
212
|
+
function enrichEventMetadata(db, record) {
|
|
213
|
+
const bond = db.prepare("SELECT affects_bond FROM events WHERE id=?").get(record.id);
|
|
214
|
+
record.affectsBond = bond?.affects_bond == null ? !(record.type === "observation" && record.significance <= 2) : Boolean(bond.affects_bond);
|
|
215
|
+
if (record.parentEventId === void 0 && record.parentRef) {
|
|
216
|
+
const row = db.prepare(
|
|
217
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
218
|
+
).get(record.parentRef.source, record.parentRef.id);
|
|
219
|
+
if (row) record.parentEventId = row.id;
|
|
220
|
+
}
|
|
221
|
+
const attrs = db.prepare(
|
|
222
|
+
"SELECT id, name, value FROM attributes WHERE event_id=? AND deleted_at IS NULL ORDER BY id"
|
|
223
|
+
).all(record.id);
|
|
224
|
+
if (attrs.length)
|
|
225
|
+
record.attributes = attrs.map((a) => ({
|
|
226
|
+
...a,
|
|
227
|
+
contactId: null,
|
|
228
|
+
linkId: null,
|
|
229
|
+
eventId: record.id
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
81
233
|
// src/contacts/queries.ts
|
|
82
234
|
function getContactRowById(db, id) {
|
|
83
235
|
const row = db.prepare(
|
|
@@ -156,8 +308,11 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
156
308
|
}
|
|
157
309
|
function validateSocialEventInput(input) {
|
|
158
310
|
assertFiniteTimestamp(input.occurredAt, "occurredAt");
|
|
159
|
-
|
|
160
|
-
if (summary.
|
|
311
|
+
validateEventMetadata(input);
|
|
312
|
+
if (input.summary !== void 0 && typeof input.summary !== "string")
|
|
313
|
+
throw new AffinityValidationError("summary must be a string");
|
|
314
|
+
const summary = input.summary?.trim();
|
|
315
|
+
if (summary === "" || summary === void 0 && input.content === void 0) {
|
|
161
316
|
throw new AffinityValidationError("summary must be non-empty");
|
|
162
317
|
}
|
|
163
318
|
const sig = input.significance;
|
|
@@ -221,11 +376,25 @@ function mapEventRowToEventRecord(row, participants) {
|
|
|
221
376
|
id: row.id,
|
|
222
377
|
type: row.type,
|
|
223
378
|
occurredAt: row.occurred_at,
|
|
224
|
-
|
|
379
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
380
|
+
summary: decodeEventText(row.summary),
|
|
225
381
|
significance: row.significance,
|
|
226
382
|
momentKind: row.moment_kind,
|
|
227
383
|
participants
|
|
228
384
|
};
|
|
385
|
+
if (row.content != null) base.content = decodeEventText(row.content);
|
|
386
|
+
if (row.channel != null) base.channel = row.channel;
|
|
387
|
+
if (row.external_source != null && row.external_id != null)
|
|
388
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
389
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
390
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
391
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
392
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
393
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
394
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
395
|
+
base.dueAt = row.due_at ?? null;
|
|
396
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
397
|
+
}
|
|
229
398
|
if (row.recurrence_kind !== null) {
|
|
230
399
|
base.recurrenceKind = row.recurrence_kind;
|
|
231
400
|
base.anchorMonth = row.anchor_month;
|
|
@@ -255,25 +424,29 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
255
424
|
targetRef,
|
|
256
425
|
recurrenceKind,
|
|
257
426
|
occursOn: row.occurs_on,
|
|
258
|
-
|
|
427
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
428
|
+
summary: decodeEventText(row.summary),
|
|
259
429
|
significance: row.significance
|
|
260
430
|
};
|
|
261
431
|
}
|
|
432
|
+
function decodeEventText(value) {
|
|
433
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
434
|
+
}
|
|
262
435
|
|
|
263
436
|
// src/events/queries.ts
|
|
264
437
|
function getEventRowById(db, id) {
|
|
265
438
|
const row = db.prepare(
|
|
266
|
-
`SELECT
|
|
267
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
268
|
-
anchor_contact_id, anchor_link_id,
|
|
269
|
-
created_at, updated_at, deleted_at
|
|
439
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
270
440
|
FROM events
|
|
271
441
|
WHERE id = ? AND deleted_at IS NULL`
|
|
272
442
|
).get(id);
|
|
273
443
|
if (row === void 0) {
|
|
274
444
|
return null;
|
|
275
445
|
}
|
|
276
|
-
|
|
446
|
+
const event = row;
|
|
447
|
+
event.summary = decodeEventText(event.summary);
|
|
448
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
449
|
+
return event;
|
|
277
450
|
}
|
|
278
451
|
function findDuplicateDateAnchor(db, params) {
|
|
279
452
|
const exclude = params.excludeEventId;
|
|
@@ -341,19 +514,27 @@ function loadEventRecord(db, eventId) {
|
|
|
341
514
|
throw new AffinityInvariantError("event not found");
|
|
342
515
|
}
|
|
343
516
|
const participants = loadEventParticipantViews(db, eventId);
|
|
344
|
-
|
|
517
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
518
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
519
|
+
if (due) {
|
|
520
|
+
record.dueAt = due.due_at;
|
|
521
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
522
|
+
}
|
|
523
|
+
enrichEventMetadata(db, record);
|
|
524
|
+
return record;
|
|
345
525
|
}
|
|
346
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
526
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
347
527
|
if (eventIds.length === 0) {
|
|
348
528
|
return [];
|
|
349
529
|
}
|
|
350
530
|
const ph = eventIds.map(() => "?").join(", ");
|
|
351
531
|
const eventRows = db.prepare(
|
|
352
|
-
`SELECT id, type, occurred_at,
|
|
532
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
533
|
+
e.external_source,e.external_id,e.channel,e.parent_source,e.parent_ref_id,e.parent_event_id,e.thread_source,e.thread_id,
|
|
353
534
|
recurrence_kind, anchor_month, anchor_day,
|
|
354
535
|
anchor_contact_id, anchor_link_id,
|
|
355
|
-
created_at, updated_at, deleted_at
|
|
356
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
536
|
+
e.created_at, e.updated_at, e.deleted_at, ${includeContent ? "CAST(e.content AS BLOB)" : "NULL"} AS content, CAST(e.summary AS BLOB) AS summary
|
|
537
|
+
FROM events e LEFT JOIN open_commitments oc ON oc.event_id = e.id WHERE e.id IN (${ph}) AND e.deleted_at IS NULL`
|
|
357
538
|
).all(...eventIds);
|
|
358
539
|
const partRows = db.prepare(
|
|
359
540
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -377,7 +558,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
377
558
|
for (const id of eventIds) {
|
|
378
559
|
const row = rowById.get(id);
|
|
379
560
|
if (row) {
|
|
380
|
-
|
|
561
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
562
|
+
enrichEventMetadata(db, record);
|
|
563
|
+
out.push(record);
|
|
381
564
|
}
|
|
382
565
|
}
|
|
383
566
|
return out;
|
|
@@ -436,6 +619,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
436
619
|
clauses.push(`e.type IN (${ph})`);
|
|
437
620
|
params.push(...options.eventTypes);
|
|
438
621
|
}
|
|
622
|
+
if (options?.channel !== void 0) {
|
|
623
|
+
clauses.push("e.channel = ?");
|
|
624
|
+
params.push(options.channel.trim().toLowerCase());
|
|
625
|
+
}
|
|
439
626
|
const where = clauses.join(" AND ");
|
|
440
627
|
const rows = db.prepare(
|
|
441
628
|
`SELECT e.id FROM events e
|
|
@@ -446,8 +633,75 @@ function getContactJournal(db, contactId, options) {
|
|
|
446
633
|
).all(...params, limit, offset);
|
|
447
634
|
return batchLoadEventRecords(
|
|
448
635
|
db,
|
|
449
|
-
rows.map((r) => r.id)
|
|
636
|
+
rows.map((r) => r.id),
|
|
637
|
+
options?.includeContent ?? false
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/lib/parse_calendar_time.ts
|
|
642
|
+
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
643
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
644
|
+
if (dueAt === null) return false;
|
|
645
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
646
|
+
return localCalendarDate(now) > dueOn;
|
|
647
|
+
}
|
|
648
|
+
return dueAt < now;
|
|
649
|
+
}
|
|
650
|
+
var OVERDUE_SQL = "(CASE WHEN oc.due_on IS NOT NULL AND oc.due_at = CAST(strftime('%s', oc.due_on) AS INTEGER) * 1000 + 86399999 THEN oc.due_on < ? ELSE oc.due_at < ? END)";
|
|
651
|
+
function localCalendarDate(now) {
|
|
652
|
+
const d = new Date(now);
|
|
653
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
654
|
+
}
|
|
655
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
656
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
657
|
+
throw new AffinityValidationError(
|
|
658
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
659
|
+
);
|
|
660
|
+
const fail = () => {
|
|
661
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
662
|
+
};
|
|
663
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
664
|
+
const timestamp2 = Number(value);
|
|
665
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
666
|
+
return fail();
|
|
667
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
668
|
+
}
|
|
669
|
+
if (typeof value !== "string") return fail();
|
|
670
|
+
let text = value.trim();
|
|
671
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
672
|
+
text = text.replace(" ", "T");
|
|
673
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
674
|
+
}
|
|
675
|
+
let rounding = 0;
|
|
676
|
+
text = text.replace(
|
|
677
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
678
|
+
(_, digits) => {
|
|
679
|
+
const ms = Math.round(Number(`0.${digits}`) * 1e3);
|
|
680
|
+
rounding = ms === 1e3 ? 1 : 0;
|
|
681
|
+
return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
|
|
682
|
+
}
|
|
450
683
|
);
|
|
684
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
685
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
686
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
687
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
688
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
689
|
+
text = date.toISOString().slice(0, 10);
|
|
690
|
+
}
|
|
691
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
692
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
693
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
694
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
695
|
+
text
|
|
696
|
+
))
|
|
697
|
+
return fail();
|
|
698
|
+
const day = text.slice(0, 10);
|
|
699
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
700
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
701
|
+
return fail();
|
|
702
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
|
|
703
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
704
|
+
return { timestamp, day };
|
|
451
705
|
}
|
|
452
706
|
|
|
453
707
|
// src/resolve_now.ts
|
|
@@ -518,9 +772,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
518
772
|
clauses.push("oc.due_at <= ?");
|
|
519
773
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
520
774
|
}
|
|
775
|
+
if (options?.channel !== void 0) {
|
|
776
|
+
clauses.push("e.channel=?");
|
|
777
|
+
params.push(options.channel.trim().toLowerCase());
|
|
778
|
+
}
|
|
521
779
|
const where = clauses.join(" AND ");
|
|
522
780
|
const rows = db.prepare(
|
|
523
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
781
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
524
782
|
e.type, e.summary
|
|
525
783
|
FROM open_commitments oc
|
|
526
784
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -537,7 +795,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
537
795
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
538
796
|
dueAt: r.due_at,
|
|
539
797
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
540
|
-
|
|
798
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
799
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
541
800
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
542
801
|
}));
|
|
543
802
|
}
|
|
@@ -606,8 +865,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
606
865
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
607
866
|
}
|
|
608
867
|
function computeNextAnchorOccursOn(params) {
|
|
609
|
-
const refStart =
|
|
610
|
-
|
|
868
|
+
const refStart = Date.parse(
|
|
869
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
870
|
+
);
|
|
871
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
611
872
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
612
873
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
613
874
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -810,7 +1071,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
810
1071
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
811
1072
|
LIMIT ?`
|
|
812
1073
|
).all(contactId, contactId, topLinksLimit);
|
|
813
|
-
const topLinks = linkRows.map((l) =>
|
|
1074
|
+
const topLinks = linkRows.map((l) => ({
|
|
1075
|
+
...mapLinkRowToLinkListItem(l),
|
|
1076
|
+
counterpartyName: getContactRowById(
|
|
1077
|
+
db,
|
|
1078
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1079
|
+
)?.name ?? ""
|
|
1080
|
+
}));
|
|
814
1081
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
815
1082
|
const contactRollup = loadContactRollup(db, contactId);
|
|
816
1083
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -843,7 +1110,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
843
1110
|
return {
|
|
844
1111
|
...profile,
|
|
845
1112
|
recentEvents: getContactJournal(db, contactId, {
|
|
846
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1113
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1114
|
+
eventTypes: [
|
|
1115
|
+
"conversation",
|
|
1116
|
+
"activity",
|
|
1117
|
+
"gift",
|
|
1118
|
+
"support",
|
|
1119
|
+
"milestone",
|
|
1120
|
+
"observation",
|
|
1121
|
+
"conflict",
|
|
1122
|
+
"correction",
|
|
1123
|
+
"transaction",
|
|
1124
|
+
"promise",
|
|
1125
|
+
"agreement"
|
|
1126
|
+
]
|
|
847
1127
|
}).map((event) => ({
|
|
848
1128
|
...event,
|
|
849
1129
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -898,6 +1178,10 @@ function listMoments(db, filters, options) {
|
|
|
898
1178
|
clauses.push("le.link_id = ?");
|
|
899
1179
|
params.push(filters.linkId);
|
|
900
1180
|
}
|
|
1181
|
+
if (options?.channel !== void 0) {
|
|
1182
|
+
clauses.push("e.channel=?");
|
|
1183
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1184
|
+
}
|
|
901
1185
|
const where = clauses.join(" AND ");
|
|
902
1186
|
return db.prepare(
|
|
903
1187
|
`SELECT
|
|
@@ -905,6 +1189,7 @@ function listMoments(db, filters, options) {
|
|
|
905
1189
|
le.link_id,
|
|
906
1190
|
le.moment_kind,
|
|
907
1191
|
le.occurred_at,
|
|
1192
|
+
e.occurred_on,
|
|
908
1193
|
e.summary,
|
|
909
1194
|
le.impact_score
|
|
910
1195
|
FROM link_event_effects le
|
|
@@ -917,6 +1202,7 @@ function listMoments(db, filters, options) {
|
|
|
917
1202
|
linkId: row.link_id,
|
|
918
1203
|
momentKind: row.moment_kind,
|
|
919
1204
|
occurredAt: row.occurred_at,
|
|
1205
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
920
1206
|
summary: row.summary,
|
|
921
1207
|
impactScore: row.impact_score
|
|
922
1208
|
}));
|
|
@@ -1004,6 +1290,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1004
1290
|
clauses.push(`e.type IN (${ph})`);
|
|
1005
1291
|
params.push(...options.eventTypes);
|
|
1006
1292
|
}
|
|
1293
|
+
if (options?.channel !== void 0) {
|
|
1294
|
+
clauses.push("e.channel = ?");
|
|
1295
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1296
|
+
}
|
|
1007
1297
|
const where = clauses.join(" AND ");
|
|
1008
1298
|
const rows = db.prepare(
|
|
1009
1299
|
`SELECT e.id
|
|
@@ -1015,7 +1305,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1015
1305
|
).all(...params, limit, offset);
|
|
1016
1306
|
return batchLoadEventRecords(
|
|
1017
1307
|
db,
|
|
1018
|
-
rows.map((r) => r.id)
|
|
1308
|
+
rows.map((r) => r.id),
|
|
1309
|
+
options?.includeContent ?? false
|
|
1019
1310
|
);
|
|
1020
1311
|
}
|
|
1021
1312
|
|
|
@@ -1188,26 +1479,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1188
1479
|
const latest = db.prepare(
|
|
1189
1480
|
`SELECT event_id, occurred_at
|
|
1190
1481
|
FROM link_event_effects
|
|
1191
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
1482
|
+
WHERE link_id = ? AND counts_for_recency = 1 AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))
|
|
1192
1483
|
ORDER BY occurred_at DESC, event_id DESC
|
|
1193
1484
|
LIMIT 1`
|
|
1194
|
-
).get(linkId);
|
|
1485
|
+
).get(linkId, now, localCalendarDate(now));
|
|
1195
1486
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
1196
1487
|
const counts = db.prepare(
|
|
1197
1488
|
`SELECT
|
|
1198
1489
|
COUNT(*) AS total_meaningful_events,
|
|
1199
1490
|
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
1200
1491
|
FROM link_event_effects
|
|
1201
|
-
WHERE link_id = ? AND occurred_at >=
|
|
1202
|
-
|
|
1492
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
1493
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
1494
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
1203
1495
|
const directionalityRows = db.prepare(
|
|
1204
1496
|
`SELECT ep.directionality
|
|
1205
1497
|
FROM link_event_effects le
|
|
1206
1498
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
1207
1499
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
1208
1500
|
AND ep.directionality IS NOT NULL
|
|
1209
|
-
AND le.occurred_at >=
|
|
1210
|
-
|
|
1501
|
+
AND le.occurred_at >= ?
|
|
1502
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
1503
|
+
).all(
|
|
1504
|
+
linkId,
|
|
1505
|
+
link.from_contact_id,
|
|
1506
|
+
trailingWindowStart,
|
|
1507
|
+
now,
|
|
1508
|
+
localCalendarDate(now)
|
|
1509
|
+
);
|
|
1211
1510
|
let outboundCount = 0;
|
|
1212
1511
|
let inboundCount = 0;
|
|
1213
1512
|
for (const row of directionalityRows) {
|
|
@@ -1358,37 +1657,8 @@ function getLinkDetail(db, linkId, options) {
|
|
|
1358
1657
|
}
|
|
1359
1658
|
|
|
1360
1659
|
// src/tools/parse_time_input.ts
|
|
1361
|
-
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, today, tomorrow, yesterday, +3d or -2d";
|
|
1362
1660
|
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
1363
|
-
|
|
1364
|
-
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1365
|
-
};
|
|
1366
|
-
if (typeof value === "number")
|
|
1367
|
-
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
1368
|
-
if (typeof value !== "string") return fail();
|
|
1369
|
-
const text = value.trim();
|
|
1370
|
-
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
1371
|
-
let day;
|
|
1372
|
-
if (relative) {
|
|
1373
|
-
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1374
|
-
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
1375
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
1376
|
-
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
1377
|
-
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
1378
|
-
return fail();
|
|
1379
|
-
} else {
|
|
1380
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1381
|
-
text
|
|
1382
|
-
))
|
|
1383
|
-
return fail();
|
|
1384
|
-
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
1385
|
-
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
1386
|
-
return fail();
|
|
1387
|
-
const parsed = Date.parse(text);
|
|
1388
|
-
return Number.isFinite(parsed) ? parsed : fail();
|
|
1389
|
-
}
|
|
1390
|
-
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
1391
|
-
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
1661
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
1392
1662
|
}
|
|
1393
1663
|
|
|
1394
1664
|
// src/tools/tool_metadata.ts
|
|
@@ -1459,6 +1729,39 @@ function oneOfSchema(schemas, description) {
|
|
|
1459
1729
|
return schema;
|
|
1460
1730
|
}
|
|
1461
1731
|
|
|
1732
|
+
// src/tools/event_metadata_schema.ts
|
|
1733
|
+
var externalRefSchema = objectSchema(
|
|
1734
|
+
{
|
|
1735
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
1736
|
+
id: stringSchema("Non-empty external id.")
|
|
1737
|
+
},
|
|
1738
|
+
["source", "id"]
|
|
1739
|
+
);
|
|
1740
|
+
var eventMetadataSchema = {
|
|
1741
|
+
affectsBond: booleanSchema(
|
|
1742
|
+
"Whether evidence affects the relationship; defaults false for observations with significance <=2."
|
|
1743
|
+
),
|
|
1744
|
+
content: stringSchema(
|
|
1745
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
1746
|
+
),
|
|
1747
|
+
externalRef: externalRefSchema,
|
|
1748
|
+
channel: stringSchema(
|
|
1749
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
1750
|
+
),
|
|
1751
|
+
parentRef: externalRefSchema,
|
|
1752
|
+
parentEventId: integerSchema("Parent event id."),
|
|
1753
|
+
threadRef: externalRefSchema,
|
|
1754
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
1755
|
+
"breakthrough",
|
|
1756
|
+
"rupture",
|
|
1757
|
+
"reconciliation",
|
|
1758
|
+
"milestone",
|
|
1759
|
+
"turning_point"
|
|
1760
|
+
]),
|
|
1761
|
+
occurredOn: stringSchema("Calendar day."),
|
|
1762
|
+
provenance: {}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1462
1765
|
// src/tools/tool_names.ts
|
|
1463
1766
|
var searchAffinityToolName = "search_affinity";
|
|
1464
1767
|
var reviewAffinityToolName = "review_affinity";
|
|
@@ -1472,6 +1775,7 @@ var recordEventToolName = "record_event";
|
|
|
1472
1775
|
var manageCommitmentToolName = "manage_commitment";
|
|
1473
1776
|
var manageDateAnchorToolName = "manage_date_anchor";
|
|
1474
1777
|
var manageAttributeToolName = "manage_attribute";
|
|
1778
|
+
var manageEventToolName = "manage_event";
|
|
1475
1779
|
|
|
1476
1780
|
// src/tools/tool_ref.ts
|
|
1477
1781
|
function contactState(value) {
|
|
@@ -1516,7 +1820,7 @@ function toEventEntityRef(value) {
|
|
|
1516
1820
|
return {
|
|
1517
1821
|
kind: "event",
|
|
1518
1822
|
id: value.id,
|
|
1519
|
-
title: value.summary,
|
|
1823
|
+
title: deriveEventSummary(value.summary),
|
|
1520
1824
|
subtitle: value.type,
|
|
1521
1825
|
state: value.type
|
|
1522
1826
|
};
|
|
@@ -1551,14 +1855,39 @@ function toReceiptEntityRefs(receipt) {
|
|
|
1551
1855
|
}
|
|
1552
1856
|
|
|
1553
1857
|
// src/identities/normalize.ts
|
|
1858
|
+
var handleIdentityTypes = /* @__PURE__ */ new Set([
|
|
1859
|
+
"handle",
|
|
1860
|
+
"bluesky",
|
|
1861
|
+
"x",
|
|
1862
|
+
"twitter",
|
|
1863
|
+
"mastodon",
|
|
1864
|
+
"github",
|
|
1865
|
+
"instagram",
|
|
1866
|
+
"telegram",
|
|
1867
|
+
"linkedin",
|
|
1868
|
+
"facebook",
|
|
1869
|
+
"youtube",
|
|
1870
|
+
"tiktok",
|
|
1871
|
+
"discord",
|
|
1872
|
+
"slack"
|
|
1873
|
+
]);
|
|
1874
|
+
function normalizeIdentityValue(type, value) {
|
|
1875
|
+
const v = value.trim();
|
|
1876
|
+
return handleIdentityTypes.has(type.toLowerCase()) ? v.replace(/^@/, "").toLowerCase() : v;
|
|
1877
|
+
}
|
|
1554
1878
|
function normalizeIdentityKey(type, value) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1879
|
+
let t = type.trim().toLowerCase();
|
|
1880
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(t)) t = "did";
|
|
1881
|
+
const v = normalizeIdentityValue(t, value).toLowerCase();
|
|
1557
1882
|
if (t.length === 0 || v.length === 0) {
|
|
1558
1883
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
1559
1884
|
}
|
|
1560
1885
|
return `${t}:${v}`;
|
|
1561
1886
|
}
|
|
1887
|
+
function legacyIdentityKey(key) {
|
|
1888
|
+
const colon = key.indexOf(":");
|
|
1889
|
+
return handleIdentityTypes.has(key.slice(0, colon)) ? `${key.slice(0, colon + 1)}@${key.slice(colon + 1)}` : key;
|
|
1890
|
+
}
|
|
1562
1891
|
|
|
1563
1892
|
// src/lib/errors/is_affinity_error.ts
|
|
1564
1893
|
function isAffinityError(value) {
|
|
@@ -1680,7 +2009,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
1680
2009
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
1681
2010
|
return {
|
|
1682
2011
|
ok: false,
|
|
1683
|
-
result: toolFailure("domain", "
|
|
2012
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
1684
2013
|
};
|
|
1685
2014
|
}
|
|
1686
2015
|
return { ok: true, value: row2 };
|
|
@@ -1705,9 +2034,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
1705
2034
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
1706
2035
|
WHERE i.removed_at IS NULL
|
|
1707
2036
|
AND c.deleted_at IS NULL
|
|
1708
|
-
AND i.normalized_key = ?
|
|
2037
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
1709
2038
|
LIMIT 2`
|
|
1710
|
-
).all(
|
|
2039
|
+
).all(
|
|
2040
|
+
normalizedKey,
|
|
2041
|
+
legacyIdentityKey(normalizedKey),
|
|
2042
|
+
normalizedKey
|
|
2043
|
+
);
|
|
1711
2044
|
if (row.length === 0) {
|
|
1712
2045
|
return {
|
|
1713
2046
|
ok: false,
|
|
@@ -1858,13 +2191,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
1858
2191
|
return { ok: true, value: matchedLink };
|
|
1859
2192
|
}
|
|
1860
2193
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
2194
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
2195
|
+
return error;
|
|
1861
2196
|
if (isAffinityError(error)) {
|
|
1862
|
-
|
|
2197
|
+
const result = toolFailure(
|
|
1863
2198
|
"domain",
|
|
1864
2199
|
affinityErrorCodeToToolCode(error),
|
|
1865
2200
|
summary,
|
|
1866
2201
|
error.message
|
|
1867
2202
|
);
|
|
2203
|
+
const id = error.existingContactId;
|
|
2204
|
+
if (id !== void 0) {
|
|
2205
|
+
result.data = { existingContactId: id };
|
|
2206
|
+
result.error.details = { existingContactId: id };
|
|
2207
|
+
}
|
|
2208
|
+
return result;
|
|
1868
2209
|
}
|
|
1869
2210
|
if (error instanceof Error) {
|
|
1870
2211
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -1903,27 +2244,32 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
1903
2244
|
}
|
|
1904
2245
|
|
|
1905
2246
|
// src/tools/validate_tool_input.ts
|
|
1906
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
2247
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
2248
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
2249
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
2250
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
2251
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
2252
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
1907
2253
|
if (schema.anyOf || schema.oneOf) {
|
|
1908
2254
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
1909
2255
|
return choices.some(
|
|
1910
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
2256
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
1911
2257
|
) ? null : `${path}: invalid value`;
|
|
1912
2258
|
}
|
|
1913
2259
|
if (schema.type === "object") {
|
|
1914
2260
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1915
2261
|
return `${path}: missing input or expected object`;
|
|
1916
2262
|
const object = value;
|
|
1917
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
2263
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
1918
2264
|
for (const key of Object.keys(object)) {
|
|
1919
2265
|
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
1920
|
-
return key === "kind" && "name" in (schema.properties ?? {}) ? "kind cannot be changed after creation" : `${path}.${key}: unknown patch key; allowed keys: ${Object.keys(schema.properties ?? {}).join(", ")}`;
|
|
2266
|
+
return key === "kind" && "name" in (schema.properties ?? {}) ? "kind cannot be changed after creation" : `${path}.${key}: unknown ${path.endsWith(".patch") ? "patch key" : "key"}; allowed keys: ${Object.keys(schema.properties ?? {}).join(", ")}`;
|
|
1921
2267
|
}
|
|
1922
2268
|
}
|
|
1923
2269
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
1924
2270
|
return `${path}.patch: missing required field`;
|
|
1925
2271
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
1926
|
-
return `${path}.patch: unknown
|
|
2272
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
1927
2273
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
1928
2274
|
try {
|
|
1929
2275
|
return parseTimeInput(
|
|
@@ -1938,11 +2284,14 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1938
2284
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
1939
2285
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
1940
2286
|
try {
|
|
1941
|
-
|
|
2287
|
+
const parsed = parseCalendarTime(
|
|
1942
2288
|
object[key],
|
|
1943
2289
|
`${path}.${key}`,
|
|
1944
2290
|
referenceNow
|
|
1945
2291
|
);
|
|
2292
|
+
object[key] = parsed.timestamp;
|
|
2293
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
2294
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
1946
2295
|
} catch (error) {
|
|
1947
2296
|
return error.message;
|
|
1948
2297
|
}
|
|
@@ -1958,7 +2307,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1958
2307
|
child,
|
|
1959
2308
|
object[key],
|
|
1960
2309
|
`${path}.${key}`,
|
|
1961
|
-
referenceNow
|
|
2310
|
+
referenceNow,
|
|
2311
|
+
strict
|
|
1962
2312
|
);
|
|
1963
2313
|
if (error) return error;
|
|
1964
2314
|
}
|
|
@@ -1971,7 +2321,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
1971
2321
|
schema.items ?? {},
|
|
1972
2322
|
item,
|
|
1973
2323
|
`${path}[${index}]`,
|
|
1974
|
-
injectedNow
|
|
2324
|
+
injectedNow,
|
|
2325
|
+
strict
|
|
1975
2326
|
);
|
|
1976
2327
|
if (error) return error;
|
|
1977
2328
|
}
|
|
@@ -2037,6 +2388,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
2037
2388
|
inputError
|
|
2038
2389
|
);
|
|
2039
2390
|
return withToolHandling(() => {
|
|
2391
|
+
if (input.kind === "event") {
|
|
2392
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
2393
|
+
return toolFailure(
|
|
2394
|
+
"protocol",
|
|
2395
|
+
"invalid_input",
|
|
2396
|
+
"Invalid event target.",
|
|
2397
|
+
"Provide exactly one of eventId or ref."
|
|
2398
|
+
);
|
|
2399
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
2400
|
+
return detail2 ? toolSuccess(
|
|
2401
|
+
`Loaded event #${detail2.id}.`,
|
|
2402
|
+
{ kind: "event", detail: detail2 },
|
|
2403
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
2404
|
+
) : toolFailure(
|
|
2405
|
+
"domain",
|
|
2406
|
+
"not_found",
|
|
2407
|
+
"Event not found.",
|
|
2408
|
+
"No event matches the target."
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2040
2411
|
if (input.kind === "owner_profile") {
|
|
2041
2412
|
const detail2 = getOwnerProfile(
|
|
2042
2413
|
db,
|
|
@@ -2134,10 +2505,13 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
2134
2505
|
inputSchema: objectSchema(
|
|
2135
2506
|
{
|
|
2136
2507
|
kind: enumSchema("What to inspect.", [
|
|
2508
|
+
"event",
|
|
2137
2509
|
"owner_profile",
|
|
2138
2510
|
"contact_profile",
|
|
2139
2511
|
"link"
|
|
2140
2512
|
]),
|
|
2513
|
+
eventId: integerSchema("Exact event id."),
|
|
2514
|
+
ref: externalRefSchema,
|
|
2141
2515
|
contact: contactLocatorSchema(
|
|
2142
2516
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
2143
2517
|
),
|
|
@@ -2156,6 +2530,7 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
2156
2530
|
// src/attributes/helpers.ts
|
|
2157
2531
|
function clearedAttributePrimary(target) {
|
|
2158
2532
|
return {
|
|
2533
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
2159
2534
|
id: 0,
|
|
2160
2535
|
contactId: target.kind === "contact" ? target.id : null,
|
|
2161
2536
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -2167,6 +2542,7 @@ function clearedAttributePrimary(target) {
|
|
|
2167
2542
|
// src/attributes/mappers.ts
|
|
2168
2543
|
function mapAttributeRowToAttributeRecord(row) {
|
|
2169
2544
|
return {
|
|
2545
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
2170
2546
|
id: row.id,
|
|
2171
2547
|
contactId: row.contact_id,
|
|
2172
2548
|
linkId: row.link_id,
|
|
@@ -2176,136 +2552,55 @@ function mapAttributeRowToAttributeRecord(row) {
|
|
|
2176
2552
|
}
|
|
2177
2553
|
|
|
2178
2554
|
// src/attributes/queries.ts
|
|
2555
|
+
function attributeTargetSql(target) {
|
|
2556
|
+
return {
|
|
2557
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
2558
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2179
2561
|
function getAttributeRowById(db, id) {
|
|
2180
|
-
|
|
2181
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
2182
|
-
FROM attributes WHERE id = ?`
|
|
2183
|
-
).get(id);
|
|
2184
|
-
if (row === void 0) {
|
|
2185
|
-
return null;
|
|
2186
|
-
}
|
|
2187
|
-
return row;
|
|
2562
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
2188
2563
|
}
|
|
2189
2564
|
function getLiveAttributeRow(db, target, name) {
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
WHERE contact_id = ? AND name = ? AND deleted_at IS NULL`
|
|
2195
|
-
).get(target.id, name);
|
|
2196
|
-
if (row2 === void 0) {
|
|
2197
|
-
return null;
|
|
2198
|
-
}
|
|
2199
|
-
return row2;
|
|
2200
|
-
}
|
|
2201
|
-
const row = db.prepare(
|
|
2202
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
2203
|
-
FROM attributes
|
|
2204
|
-
WHERE link_id = ? AND name = ? AND deleted_at IS NULL`
|
|
2205
|
-
).get(target.id, name);
|
|
2206
|
-
if (row === void 0) {
|
|
2207
|
-
return null;
|
|
2208
|
-
}
|
|
2209
|
-
return row;
|
|
2565
|
+
const t = attributeTargetSql(target);
|
|
2566
|
+
return db.prepare(
|
|
2567
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
2568
|
+
).get(t.id, name) ?? null;
|
|
2210
2569
|
}
|
|
2211
2570
|
function listLiveAttributeIdsForTarget(db, target) {
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
return rows2.map((row) => row.id);
|
|
2217
|
-
}
|
|
2218
|
-
const rows = db.prepare(
|
|
2219
|
-
"SELECT id FROM attributes WHERE link_id = ? AND deleted_at IS NULL ORDER BY id"
|
|
2220
|
-
).all(target.id);
|
|
2221
|
-
return rows.map((row) => row.id);
|
|
2571
|
+
const t = attributeTargetSql(target);
|
|
2572
|
+
return db.prepare(
|
|
2573
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
2574
|
+
).all(t.id).map((r) => r.id);
|
|
2222
2575
|
}
|
|
2223
2576
|
|
|
2224
|
-
// src/
|
|
2225
|
-
function
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
}
|
|
2236
|
-
return out;
|
|
2577
|
+
// src/attributes/receipts.ts
|
|
2578
|
+
function buildAttributeMutationReceipt(primary, partial) {
|
|
2579
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2580
|
+
return {
|
|
2581
|
+
primary,
|
|
2582
|
+
created: normalized.created,
|
|
2583
|
+
updated: normalized.updated,
|
|
2584
|
+
archived: normalized.archived,
|
|
2585
|
+
removed: normalized.removed,
|
|
2586
|
+
affectedLinks: normalized.affectedLinks,
|
|
2587
|
+
derivedEffects: normalized.derivedEffects
|
|
2588
|
+
};
|
|
2237
2589
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
const out = [];
|
|
2253
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2254
|
-
for (const linkId of affectedLinks ?? []) {
|
|
2255
|
-
if (seen.has(linkId)) {
|
|
2256
|
-
continue;
|
|
2257
|
-
}
|
|
2258
|
-
seen.add(linkId);
|
|
2259
|
-
out.push(linkId);
|
|
2260
|
-
}
|
|
2261
|
-
for (const effect of derivedEffects) {
|
|
2262
|
-
if (seen.has(effect.linkId)) {
|
|
2263
|
-
continue;
|
|
2264
|
-
}
|
|
2265
|
-
seen.add(effect.linkId);
|
|
2266
|
-
out.push(effect.linkId);
|
|
2267
|
-
}
|
|
2268
|
-
return out;
|
|
2269
|
-
}
|
|
2270
|
-
function normalizeMutationReceiptPartial(partial) {
|
|
2271
|
-
const derivedEffects = uniqueDerivedEffects(partial?.derivedEffects);
|
|
2272
|
-
return {
|
|
2273
|
-
created: uniqueEntityRefs(partial?.created),
|
|
2274
|
-
updated: uniqueEntityRefs(partial?.updated),
|
|
2275
|
-
archived: uniqueEntityRefs(partial?.archived),
|
|
2276
|
-
removed: uniqueEntityRefs(partial?.removed),
|
|
2277
|
-
affectedLinks: uniqueAffectedLinks(partial?.affectedLinks, derivedEffects),
|
|
2278
|
-
derivedEffects
|
|
2279
|
-
};
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
// src/attributes/receipts.ts
|
|
2283
|
-
function buildAttributeMutationReceipt(primary, partial) {
|
|
2284
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2285
|
-
return {
|
|
2286
|
-
primary,
|
|
2287
|
-
created: normalized.created,
|
|
2288
|
-
updated: normalized.updated,
|
|
2289
|
-
archived: normalized.archived,
|
|
2290
|
-
removed: normalized.removed,
|
|
2291
|
-
affectedLinks: normalized.affectedLinks,
|
|
2292
|
-
derivedEffects: normalized.derivedEffects
|
|
2293
|
-
};
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
// src/links/validators.ts
|
|
2297
|
-
var STATES = /* @__PURE__ */ new Set([
|
|
2298
|
-
"active",
|
|
2299
|
-
"dormant",
|
|
2300
|
-
"strained",
|
|
2301
|
-
"broken",
|
|
2302
|
-
"archived"
|
|
2303
|
-
]);
|
|
2304
|
-
function assertContactEndpointsNotMerged(db, fromContactId, toContactId) {
|
|
2305
|
-
const from = getContactRowById(db, fromContactId);
|
|
2306
|
-
const to = getContactRowById(db, toContactId);
|
|
2307
|
-
if (!from || !to) {
|
|
2308
|
-
throw new AffinityNotFoundError("contact not found");
|
|
2590
|
+
|
|
2591
|
+
// src/links/validators.ts
|
|
2592
|
+
var STATES = /* @__PURE__ */ new Set([
|
|
2593
|
+
"active",
|
|
2594
|
+
"dormant",
|
|
2595
|
+
"strained",
|
|
2596
|
+
"broken",
|
|
2597
|
+
"archived"
|
|
2598
|
+
]);
|
|
2599
|
+
function assertContactEndpointsNotMerged(db, fromContactId, toContactId) {
|
|
2600
|
+
const from = getContactRowById(db, fromContactId);
|
|
2601
|
+
const to = getContactRowById(db, toContactId);
|
|
2602
|
+
if (!from || !to) {
|
|
2603
|
+
throw new AffinityNotFoundError("contact not found");
|
|
2309
2604
|
}
|
|
2310
2605
|
if (from.lifecycle_state === "merged" || to.lifecycle_state === "merged") {
|
|
2311
2606
|
throw new AffinityStateError("merged contact is read-only");
|
|
@@ -2341,6 +2636,8 @@ function normalizeAttributeValue(value) {
|
|
|
2341
2636
|
if (value === null) {
|
|
2342
2637
|
return null;
|
|
2343
2638
|
}
|
|
2639
|
+
if (typeof value !== "string")
|
|
2640
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
2344
2641
|
const trimmed = value.trim();
|
|
2345
2642
|
return trimmed.length === 0 ? null : trimmed;
|
|
2346
2643
|
}
|
|
@@ -2369,6 +2666,11 @@ function validateAttributeEntries(entries) {
|
|
|
2369
2666
|
return out;
|
|
2370
2667
|
}
|
|
2371
2668
|
function assertAttributeTargetWritable(db, target) {
|
|
2669
|
+
if (target.kind === "event") {
|
|
2670
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
2671
|
+
throw new AffinityNotFoundError("event not found");
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2372
2674
|
if (target.kind === "contact") {
|
|
2373
2675
|
const contact = getContactRowById(db, target.id);
|
|
2374
2676
|
if (!contact) {
|
|
@@ -2404,26 +2706,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
2404
2706
|
}
|
|
2405
2707
|
const created = [];
|
|
2406
2708
|
let lastId = 0;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
created.push({ kind: "attribute", id: lastId });
|
|
2416
|
-
}
|
|
2417
|
-
} else {
|
|
2418
|
-
const insert = db.prepare(
|
|
2419
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
2420
|
-
VALUES (NULL, ?, ?, ?, ?, ?)`
|
|
2421
|
-
);
|
|
2422
|
-
for (const e of normalized) {
|
|
2423
|
-
const r = insert.run(target.id, e.name, e.value, now, now);
|
|
2424
|
-
lastId = Number(r.lastInsertRowid);
|
|
2425
|
-
created.push({ kind: "attribute", id: lastId });
|
|
2426
|
-
}
|
|
2709
|
+
const t = attributeTargetSql(target);
|
|
2710
|
+
const insert = db.prepare(
|
|
2711
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
2712
|
+
);
|
|
2713
|
+
for (const e of normalized) {
|
|
2714
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
2715
|
+
lastId = Number(r.lastInsertRowid);
|
|
2716
|
+
created.push({ kind: "attribute", id: lastId });
|
|
2427
2717
|
}
|
|
2428
2718
|
if (normalized.length === 0) {
|
|
2429
2719
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -2468,27 +2758,10 @@ function setAttribute(db, target, name, value) {
|
|
|
2468
2758
|
}
|
|
2469
2759
|
);
|
|
2470
2760
|
}
|
|
2471
|
-
|
|
2472
|
-
const r2 = db.prepare(
|
|
2473
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
2474
|
-
VALUES (?, NULL, ?, ?, ?, ?)`
|
|
2475
|
-
).run(target.id, key, stored, now, now);
|
|
2476
|
-
const id2 = Number(r2.lastInsertRowid);
|
|
2477
|
-
const row2 = getAttributeRowById(db, id2);
|
|
2478
|
-
if (!row2) {
|
|
2479
|
-
throw new AffinityInvariantError("attribute row missing after insert");
|
|
2480
|
-
}
|
|
2481
|
-
return buildAttributeMutationReceipt(
|
|
2482
|
-
mapAttributeRowToAttributeRecord(row2),
|
|
2483
|
-
{
|
|
2484
|
-
created: [{ kind: "attribute", id: id2 }]
|
|
2485
|
-
}
|
|
2486
|
-
);
|
|
2487
|
-
}
|
|
2761
|
+
const t = attributeTargetSql(target);
|
|
2488
2762
|
const r = db.prepare(
|
|
2489
|
-
`INSERT INTO attributes (
|
|
2490
|
-
|
|
2491
|
-
).run(target.id, key, stored, now, now);
|
|
2763
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
2764
|
+
).run(t.id, key, stored, now, now);
|
|
2492
2765
|
const id = Number(r.lastInsertRowid);
|
|
2493
2766
|
const row = getAttributeRowById(db, id);
|
|
2494
2767
|
if (!row) {
|
|
@@ -2544,14 +2817,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
2544
2817
|
affectedLinks: receipt.affectedLinks,
|
|
2545
2818
|
derivedEffects: receipt.derivedEffects
|
|
2546
2819
|
};
|
|
2547
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
2820
|
+
const summary = changedCount === 0 ? receipt.primary && typeof receipt.primary === "object" && "externalRef" in receipt.primary && "id" in receipt.primary ? `Event already recorded (#${receipt.primary.id}, ${receipt.primary.externalRef.source} ${receipt.primary.externalRef.id}).` : `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
2548
2821
|
if (changedCount === 0) {
|
|
2549
2822
|
return toolNoOp(summary, data, {
|
|
2550
2823
|
entities: toReceiptEntityRefs(receipt)
|
|
2551
2824
|
});
|
|
2552
2825
|
}
|
|
2553
2826
|
return toolSuccess(summary, data, {
|
|
2554
|
-
entities: toReceiptEntityRefs(receipt)
|
|
2827
|
+
entities: toReceiptEntityRefs(receipt),
|
|
2828
|
+
warnings: receipt.warnings
|
|
2555
2829
|
});
|
|
2556
2830
|
}
|
|
2557
2831
|
var PLURALS = { identity: "identities" };
|
|
@@ -2608,6 +2882,7 @@ function linkLocatorSchema2(description) {
|
|
|
2608
2882
|
};
|
|
2609
2883
|
}
|
|
2610
2884
|
function resolveAttributeTarget(db, target) {
|
|
2885
|
+
if (target.kind === "event") return { kind: "ok", target };
|
|
2611
2886
|
if (target.kind === "contact") {
|
|
2612
2887
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
2613
2888
|
if (!contact.ok) {
|
|
@@ -2667,7 +2942,7 @@ function manageAttributeToolHandler(db, rawInput) {
|
|
|
2667
2942
|
}
|
|
2668
2943
|
var manageAttributeTool = defineAffinityTool({
|
|
2669
2944
|
name: manageAttributeToolName,
|
|
2670
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
2945
|
+
description: "Set, unset, or replace custom metadata attributes on contacts, links and events. For emails, phones, handles, and URLs use manage_identity instead \u2014 those are routing keys, not attributes.",
|
|
2671
2946
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
2672
2947
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
2673
2948
|
sideEffects: "writes_state",
|
|
@@ -2688,7 +2963,8 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2688
2963
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
2689
2964
|
target: objectSchema(
|
|
2690
2965
|
{
|
|
2691
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
2966
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
2967
|
+
eventId: integerSchema("Target event id."),
|
|
2692
2968
|
contact: contactLocatorSchema2(
|
|
2693
2969
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
2694
2970
|
),
|
|
@@ -2718,7 +2994,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2718
2994
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
2719
2995
|
},
|
|
2720
2996
|
["action"],
|
|
2721
|
-
"Set, unset, or replace attributes on contacts and
|
|
2997
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
2722
2998
|
),
|
|
2723
2999
|
handler: manageAttributeToolHandler
|
|
2724
3000
|
});
|
|
@@ -2726,7 +3002,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
2726
3002
|
// src/events/persistence.ts
|
|
2727
3003
|
function insertJournalEvent(db, params) {
|
|
2728
3004
|
const now = resolveNow(params.now);
|
|
2729
|
-
const summary = params.summary
|
|
3005
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
2730
3006
|
const result = db.prepare(
|
|
2731
3007
|
`INSERT INTO events (
|
|
2732
3008
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -2743,6 +3019,33 @@ function insertJournalEvent(db, params) {
|
|
|
2743
3019
|
now
|
|
2744
3020
|
);
|
|
2745
3021
|
const eventId = Number(result.lastInsertRowid);
|
|
3022
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
3023
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
|
|
3024
|
+
eventId
|
|
3025
|
+
);
|
|
3026
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
3027
|
+
params.momentKind,
|
|
3028
|
+
eventId
|
|
3029
|
+
);
|
|
3030
|
+
db.prepare(
|
|
3031
|
+
"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=?"
|
|
3032
|
+
).run(
|
|
3033
|
+
params.content ?? null,
|
|
3034
|
+
params.externalRef?.source ?? null,
|
|
3035
|
+
params.externalRef?.id ?? null,
|
|
3036
|
+
params.channel ?? null,
|
|
3037
|
+
params.parentRef?.source ?? null,
|
|
3038
|
+
params.parentRef?.id ?? null,
|
|
3039
|
+
params.parentEventId ?? null,
|
|
3040
|
+
params.threadRef?.source ?? null,
|
|
3041
|
+
params.threadRef?.id ?? null,
|
|
3042
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3043
|
+
eventId
|
|
3044
|
+
);
|
|
3045
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3046
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3047
|
+
eventId
|
|
3048
|
+
);
|
|
2746
3049
|
const insertParticipant = db.prepare(
|
|
2747
3050
|
`INSERT INTO event_participants (
|
|
2748
3051
|
event_id, contact_id, role, directionality
|
|
@@ -2785,6 +3088,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
2785
3088
|
now
|
|
2786
3089
|
);
|
|
2787
3090
|
const eventId = Number(result.lastInsertRowid);
|
|
3091
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3092
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3093
|
+
eventId
|
|
3094
|
+
);
|
|
2788
3095
|
const insertParticipant = db.prepare(
|
|
2789
3096
|
`INSERT INTO event_participants (
|
|
2790
3097
|
event_id, contact_id, role, directionality
|
|
@@ -2805,20 +3112,6 @@ function insertDateAnchorEvent(db, params) {
|
|
|
2805
3112
|
return eventId;
|
|
2806
3113
|
}
|
|
2807
3114
|
|
|
2808
|
-
// src/events/receipts.ts
|
|
2809
|
-
function buildEventMutationReceipt(primary, partial) {
|
|
2810
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2811
|
-
return {
|
|
2812
|
-
primary,
|
|
2813
|
-
created: normalized.created,
|
|
2814
|
-
updated: normalized.updated,
|
|
2815
|
-
archived: normalized.archived,
|
|
2816
|
-
removed: normalized.removed,
|
|
2817
|
-
affectedLinks: normalized.affectedLinks,
|
|
2818
|
-
derivedEffects: normalized.derivedEffects
|
|
2819
|
-
};
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
3115
|
// src/graph/betweenness.ts
|
|
2823
3116
|
function computeNodeBetweenness(adjacency) {
|
|
2824
3117
|
const nodes = [...adjacency.keys()];
|
|
@@ -3417,6 +3710,8 @@ function deriveStateAfter(params) {
|
|
|
3417
3710
|
return params.stateBefore;
|
|
3418
3711
|
}
|
|
3419
3712
|
function deriveMomentKind(params) {
|
|
3713
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
3714
|
+
return null;
|
|
3420
3715
|
if (params.rankAfter > params.rankBefore) {
|
|
3421
3716
|
return "breakthrough";
|
|
3422
3717
|
}
|
|
@@ -3446,6 +3741,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
3446
3741
|
if (!link || link.is_structural !== 0) {
|
|
3447
3742
|
throw new AffinityStateError("relational link not found");
|
|
3448
3743
|
}
|
|
3744
|
+
db.prepare(
|
|
3745
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
3746
|
+
).run(
|
|
3747
|
+
params.linkId,
|
|
3748
|
+
JSON.stringify({
|
|
3749
|
+
rank: link.rank ?? 0,
|
|
3750
|
+
affinity: link.affinity ?? 0,
|
|
3751
|
+
trust: link.trust ?? 0,
|
|
3752
|
+
state: link.state ?? "active",
|
|
3753
|
+
cadence_days: link.cadence_days
|
|
3754
|
+
})
|
|
3755
|
+
);
|
|
3449
3756
|
const stateBefore = link.state ?? "active";
|
|
3450
3757
|
const linkKind = link.kind;
|
|
3451
3758
|
const rankBefore = link.rank ?? 0;
|
|
@@ -3490,7 +3797,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3490
3797
|
);
|
|
3491
3798
|
const weightedBase = base * massPenalty(
|
|
3492
3799
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
3493
|
-
) * salienceMultiplier;
|
|
3800
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
3494
3801
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
3495
3802
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
3496
3803
|
const affinityMass = clamp3(
|
|
@@ -3531,15 +3838,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
3531
3838
|
const latest = db.prepare(
|
|
3532
3839
|
`SELECT occurred_at
|
|
3533
3840
|
FROM link_event_effects
|
|
3534
|
-
WHERE link_id = ?
|
|
3841
|
+
WHERE link_id = ? AND counts_for_recency = 1 AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))
|
|
3535
3842
|
ORDER BY occurred_at DESC, event_id DESC
|
|
3536
3843
|
LIMIT 1`
|
|
3537
|
-
).get(params.linkId);
|
|
3844
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
3538
3845
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
3539
3846
|
1,
|
|
3540
3847
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
3541
3848
|
);
|
|
3542
|
-
const
|
|
3849
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
3850
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
3543
3851
|
kind: linkKind,
|
|
3544
3852
|
currentCadenceDays: cadenceBefore,
|
|
3545
3853
|
gapDays
|
|
@@ -3559,7 +3867,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
3559
3867
|
}
|
|
3560
3868
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
3561
3869
|
const trustDelta = trustAfter - trustBefore;
|
|
3562
|
-
const
|
|
3870
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
3871
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
3872
|
+
eventLabel
|
|
3873
|
+
);
|
|
3874
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
3563
3875
|
rankBefore,
|
|
3564
3876
|
rankAfter,
|
|
3565
3877
|
stateBefore,
|
|
@@ -3602,7 +3914,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
3602
3914
|
params.now,
|
|
3603
3915
|
params.now
|
|
3604
3916
|
);
|
|
3605
|
-
|
|
3917
|
+
db.prepare(
|
|
3918
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
3919
|
+
).run(
|
|
3920
|
+
JSON.stringify(params.participant),
|
|
3921
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3922
|
+
params.eventId,
|
|
3923
|
+
params.linkId
|
|
3924
|
+
);
|
|
3925
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
3606
3926
|
db.prepare(
|
|
3607
3927
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
3608
3928
|
).run(params.eventId, params.linkId);
|
|
@@ -3633,6 +3953,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3633
3953
|
|
|
3634
3954
|
// src/lib/errors/affinity_conflict_error.ts
|
|
3635
3955
|
var AffinityConflictError = class extends AffinityError {
|
|
3956
|
+
existingContactId;
|
|
3636
3957
|
code = "CONFLICT";
|
|
3637
3958
|
};
|
|
3638
3959
|
|
|
@@ -3687,6 +4008,8 @@ function buildLinkMutationReceipt(primary, partial) {
|
|
|
3687
4008
|
// src/links/seed_social_link.ts
|
|
3688
4009
|
function seedSocialLink(db, input) {
|
|
3689
4010
|
return withTransaction(db, () => {
|
|
4011
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
4012
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
3690
4013
|
if (!isRelationalLinkKind(input.kind)) {
|
|
3691
4014
|
throw new AffinityValidationError("invalid relational link kind");
|
|
3692
4015
|
}
|
|
@@ -3694,11 +4017,14 @@ function seedSocialLink(db, input) {
|
|
|
3694
4017
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
3695
4018
|
}
|
|
3696
4019
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
3697
|
-
|
|
4020
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
3698
4021
|
db,
|
|
3699
4022
|
input.fromContactId,
|
|
3700
4023
|
input.toContactId
|
|
3701
|
-
)
|
|
4024
|
+
);
|
|
4025
|
+
if (existing) {
|
|
4026
|
+
if (input.ifExists === "return")
|
|
4027
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
3702
4028
|
throw new AffinityConflictError(
|
|
3703
4029
|
"relational link already exists between these contacts"
|
|
3704
4030
|
);
|
|
@@ -3804,15 +4130,36 @@ function ensureRelationalLink(db, params) {
|
|
|
3804
4130
|
};
|
|
3805
4131
|
}
|
|
3806
4132
|
function applyJournalEventMechanics(db, params) {
|
|
4133
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
4134
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
4135
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
4136
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
3807
4137
|
const ownerId = requireOwnerContactId(db);
|
|
3808
|
-
const
|
|
4138
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
4139
|
+
const socialChannel = [
|
|
4140
|
+
"bluesky",
|
|
4141
|
+
"x",
|
|
4142
|
+
"twitter",
|
|
4143
|
+
"mastodon",
|
|
4144
|
+
"github",
|
|
4145
|
+
"instagram"
|
|
4146
|
+
].includes(String(bond?.channel));
|
|
4147
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
4148
|
+
const participants = params.participants.map(
|
|
4149
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
4150
|
+
);
|
|
4151
|
+
if (direction)
|
|
4152
|
+
db.prepare(
|
|
4153
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
4154
|
+
).run(direction, params.eventId);
|
|
4155
|
+
const participantIds = participants.map(
|
|
3809
4156
|
(participant) => participant.contactId
|
|
3810
4157
|
);
|
|
3811
4158
|
const hasOwner = participantIds.includes(ownerId);
|
|
3812
4159
|
const created = [];
|
|
3813
4160
|
const effectInputs = [];
|
|
3814
4161
|
if (hasOwner) {
|
|
3815
|
-
for (const participant of
|
|
4162
|
+
for (const participant of participants) {
|
|
3816
4163
|
if (participant.contactId === ownerId) {
|
|
3817
4164
|
continue;
|
|
3818
4165
|
}
|
|
@@ -3820,13 +4167,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3820
4167
|
fromContactId: ownerId,
|
|
3821
4168
|
toContactId: participant.contactId,
|
|
3822
4169
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
4170
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
3823
4171
|
now: params.now
|
|
3824
4172
|
});
|
|
3825
4173
|
created.push(...ensured.created);
|
|
3826
4174
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
3827
4175
|
}
|
|
3828
|
-
} else if (params.eventType === "observation" &&
|
|
3829
|
-
const [first, second] =
|
|
4176
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
4177
|
+
const [first, second] = participants;
|
|
3830
4178
|
if (first !== void 0 && second !== void 0) {
|
|
3831
4179
|
const a = Math.min(first.contactId, second.contactId);
|
|
3832
4180
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -3861,7 +4209,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3861
4209
|
eventType: params.eventType,
|
|
3862
4210
|
occurredAt: params.occurredAt,
|
|
3863
4211
|
significance: params.significance,
|
|
3864
|
-
participant:
|
|
4212
|
+
participant: {
|
|
4213
|
+
...input.participant,
|
|
4214
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
4215
|
+
},
|
|
3865
4216
|
provenance: params.provenance,
|
|
3866
4217
|
now: params.now
|
|
3867
4218
|
})
|
|
@@ -3885,6 +4236,8 @@ function applyJournalEventMechanics(db, params) {
|
|
|
3885
4236
|
function recordCommitment(db, input) {
|
|
3886
4237
|
return withTransaction(db, () => {
|
|
3887
4238
|
validateSocialEventInput(input);
|
|
4239
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
4240
|
+
if (existing) return existing;
|
|
3888
4241
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
3889
4242
|
throw new AffinityValidationError(
|
|
3890
4243
|
"commitmentType must be promise or agreement"
|
|
@@ -3899,11 +4252,12 @@ function recordCommitment(db, input) {
|
|
|
3899
4252
|
assertOwnerParticipates(ownerId, ids);
|
|
3900
4253
|
const now = resolveNow(input.now);
|
|
3901
4254
|
const eventId = insertJournalEvent(db, {
|
|
4255
|
+
...input,
|
|
3902
4256
|
type: input.commitmentType,
|
|
3903
4257
|
occurredAt: input.occurredAt,
|
|
3904
|
-
|
|
4258
|
+
occurredOn: input.occurredOn,
|
|
3905
4259
|
significance: input.significance,
|
|
3906
|
-
momentKind: null,
|
|
4260
|
+
momentKind: input.momentKind ?? null,
|
|
3907
4261
|
participants: input.participants,
|
|
3908
4262
|
now
|
|
3909
4263
|
});
|
|
@@ -3912,15 +4266,25 @@ function recordCommitment(db, input) {
|
|
|
3912
4266
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
3913
4267
|
) VALUES (?, ?, ?, ?, ?)`
|
|
3914
4268
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
4269
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
4270
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
4271
|
+
eventId
|
|
4272
|
+
);
|
|
3915
4273
|
const mechanics = applyJournalEventMechanics(db, {
|
|
3916
4274
|
eventId,
|
|
3917
4275
|
eventType: input.commitmentType,
|
|
3918
4276
|
occurredAt: input.occurredAt,
|
|
4277
|
+
occurredOn: input.occurredOn,
|
|
3919
4278
|
significance: input.significance,
|
|
3920
4279
|
participants: input.participants,
|
|
3921
4280
|
provenance: input.provenance,
|
|
3922
4281
|
now
|
|
3923
4282
|
});
|
|
4283
|
+
if (input.momentKind !== void 0)
|
|
4284
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
4285
|
+
input.momentKind,
|
|
4286
|
+
eventId
|
|
4287
|
+
);
|
|
3924
4288
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
3925
4289
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
3926
4290
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -3994,6 +4358,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
3994
4358
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
3995
4359
|
validateSocialEventInput({
|
|
3996
4360
|
occurredAt,
|
|
4361
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
3997
4362
|
summary,
|
|
3998
4363
|
significance,
|
|
3999
4364
|
participants,
|
|
@@ -4010,6 +4375,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4010
4375
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4011
4376
|
type: eventType,
|
|
4012
4377
|
occurredAt,
|
|
4378
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4013
4379
|
summary,
|
|
4014
4380
|
significance,
|
|
4015
4381
|
momentKind: null,
|
|
@@ -4070,7 +4436,13 @@ function prepareParticipants(db, raw) {
|
|
|
4070
4436
|
}
|
|
4071
4437
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
4072
4438
|
const input = structuredClone(rawInput);
|
|
4073
|
-
const inputError = validateToolInput(
|
|
4439
|
+
const inputError = validateToolInput(
|
|
4440
|
+
manageCommitmentTool.inputSchema,
|
|
4441
|
+
input,
|
|
4442
|
+
"input",
|
|
4443
|
+
Date.now(),
|
|
4444
|
+
true
|
|
4445
|
+
);
|
|
4074
4446
|
if (inputError)
|
|
4075
4447
|
return toolFailure(
|
|
4076
4448
|
"protocol",
|
|
@@ -4100,6 +4472,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
4100
4472
|
"commitment"
|
|
4101
4473
|
);
|
|
4102
4474
|
}
|
|
4475
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
4476
|
+
return toolFailure(
|
|
4477
|
+
"protocol",
|
|
4478
|
+
"invalid_input",
|
|
4479
|
+
"Missing commitment.",
|
|
4480
|
+
"commitmentEventId is required for resolve."
|
|
4481
|
+
);
|
|
4482
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
4483
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
4484
|
+
return toolFailure(
|
|
4485
|
+
"domain",
|
|
4486
|
+
"invalid_input",
|
|
4487
|
+
"Invalid commitment.",
|
|
4488
|
+
"not a commitment event"
|
|
4489
|
+
);
|
|
4103
4490
|
return mutationToolResult(
|
|
4104
4491
|
"resolve",
|
|
4105
4492
|
resolveCommitment(
|
|
@@ -4138,15 +4525,21 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4138
4525
|
"promise",
|
|
4139
4526
|
"agreement"
|
|
4140
4527
|
]),
|
|
4528
|
+
...eventMetadataSchema,
|
|
4141
4529
|
occurredAt: integerSchema(
|
|
4142
4530
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
4143
4531
|
),
|
|
4144
4532
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
4145
4533
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
4146
4534
|
dueAt: integerSchema("Optional due timestamp."),
|
|
4535
|
+
dueOn: stringSchema("Due calendar day."),
|
|
4147
4536
|
participants: arraySchema(
|
|
4148
4537
|
objectSchema(
|
|
4149
4538
|
{
|
|
4539
|
+
contact: objectSchema(
|
|
4540
|
+
{ contactId: integerSchema("Contact id.") },
|
|
4541
|
+
["contactId"]
|
|
4542
|
+
),
|
|
4150
4543
|
contactId: integerSchema(
|
|
4151
4544
|
"Participant contact id. The owner contact MUST be included."
|
|
4152
4545
|
),
|
|
@@ -4159,13 +4552,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4159
4552
|
),
|
|
4160
4553
|
now: integerSchema("Optional timestamp override.")
|
|
4161
4554
|
},
|
|
4162
|
-
[
|
|
4163
|
-
"commitmentType",
|
|
4164
|
-
"occurredAt",
|
|
4165
|
-
"summary",
|
|
4166
|
-
"significance",
|
|
4167
|
-
"participants"
|
|
4168
|
-
],
|
|
4555
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
4169
4556
|
"Commitment payload. Required when action=record."
|
|
4170
4557
|
),
|
|
4171
4558
|
commitmentEventId: integerSchema(
|
|
@@ -4180,6 +4567,19 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
4180
4567
|
now: integerSchema("Optional timestamp."),
|
|
4181
4568
|
resolvingEvent: objectSchema(
|
|
4182
4569
|
{
|
|
4570
|
+
occurredOn: stringSchema("Calendar day."),
|
|
4571
|
+
provenance: {},
|
|
4572
|
+
participants: arraySchema(
|
|
4573
|
+
objectSchema(
|
|
4574
|
+
{
|
|
4575
|
+
contactId: integerSchema("Contact id."),
|
|
4576
|
+
role: stringSchema("Role."),
|
|
4577
|
+
directionality: stringSchema("Directionality.")
|
|
4578
|
+
},
|
|
4579
|
+
["contactId", "role"]
|
|
4580
|
+
),
|
|
4581
|
+
"Resolving participants."
|
|
4582
|
+
),
|
|
4183
4583
|
type: stringSchema("Optional resolving event type."),
|
|
4184
4584
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
4185
4585
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -4260,30 +4660,46 @@ function deleteContact(db, locatorId, options) {
|
|
|
4260
4660
|
});
|
|
4261
4661
|
}
|
|
4262
4662
|
|
|
4263
|
-
// src/contacts/
|
|
4264
|
-
function
|
|
4265
|
-
|
|
4663
|
+
// src/contacts/export_rows.ts
|
|
4664
|
+
function exportRows(db, sql, ...params) {
|
|
4665
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
4266
4666
|
const result = {};
|
|
4267
4667
|
for (const [key, value] of Object.entries(row)) {
|
|
4268
4668
|
const name = key.replace(
|
|
4269
4669
|
/_([a-z])/g,
|
|
4270
4670
|
(_, letter) => letter.toUpperCase()
|
|
4271
4671
|
);
|
|
4272
|
-
result[name] =
|
|
4273
|
-
|
|
4672
|
+
result[name] = [
|
|
4673
|
+
"affects_bond",
|
|
4674
|
+
"is_owner",
|
|
4675
|
+
"is_structural",
|
|
4676
|
+
"verified",
|
|
4677
|
+
"counts_for_recency",
|
|
4678
|
+
"manual"
|
|
4679
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
4680
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
4274
4681
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4275
4682
|
if (key.endsWith("_json") && typeof value === "string")
|
|
4276
4683
|
result[name] = JSON.parse(value);
|
|
4277
4684
|
}
|
|
4278
4685
|
return result;
|
|
4279
4686
|
});
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
// src/contacts/export_contact.ts
|
|
4690
|
+
function exportContact(db, id) {
|
|
4691
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
4280
4692
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4281
4693
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4282
4694
|
const events = query(
|
|
4283
|
-
"SELECT DISTINCT e
|
|
4695
|
+
"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",
|
|
4284
4696
|
id
|
|
4285
4697
|
).map((event) => ({
|
|
4286
4698
|
...event,
|
|
4699
|
+
attributes: query(
|
|
4700
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
4701
|
+
event.id
|
|
4702
|
+
),
|
|
4287
4703
|
participants: query(
|
|
4288
4704
|
"SELECT p.*, c.name AS contact_name FROM event_participants p JOIN contacts c ON c.id = p.contact_id WHERE event_id = ? ORDER BY p.contact_id",
|
|
4289
4705
|
event.id
|
|
@@ -4328,11 +4744,33 @@ function exportContact(db, id) {
|
|
|
4328
4744
|
// src/contacts/export_affinity.ts
|
|
4329
4745
|
function exportAffinity(db) {
|
|
4330
4746
|
const now = Date.now();
|
|
4747
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
4748
|
+
const events = exportRows(
|
|
4749
|
+
db,
|
|
4750
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
4751
|
+
).map((event) => ({
|
|
4752
|
+
...event,
|
|
4753
|
+
attributes: exportRows(
|
|
4754
|
+
db,
|
|
4755
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
4756
|
+
event.id
|
|
4757
|
+
),
|
|
4758
|
+
participants: exportRows(
|
|
4759
|
+
db,
|
|
4760
|
+
"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",
|
|
4761
|
+
event.id
|
|
4762
|
+
)
|
|
4763
|
+
}));
|
|
4331
4764
|
return {
|
|
4332
4765
|
version: 1,
|
|
4333
4766
|
exportedAt: now,
|
|
4334
4767
|
exportedAtIso: new Date(now).toISOString(),
|
|
4335
|
-
|
|
4768
|
+
events,
|
|
4769
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
4770
|
+
...record,
|
|
4771
|
+
eventIds: events2.map((event) => event.id),
|
|
4772
|
+
dateIds: dates.map((date) => date.id)
|
|
4773
|
+
}))
|
|
4336
4774
|
};
|
|
4337
4775
|
}
|
|
4338
4776
|
|
|
@@ -4405,7 +4843,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
4405
4843
|
}
|
|
4406
4844
|
if (to === "merged") {
|
|
4407
4845
|
throw new AffinityInvariantError(
|
|
4408
|
-
"use
|
|
4846
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
4409
4847
|
);
|
|
4410
4848
|
}
|
|
4411
4849
|
if (from === to) {
|
|
@@ -4457,67 +4895,6 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
4457
4895
|
});
|
|
4458
4896
|
}
|
|
4459
4897
|
|
|
4460
|
-
// src/contacts/create_contact.ts
|
|
4461
|
-
var KINDS = /* @__PURE__ */ new Set([
|
|
4462
|
-
"human",
|
|
4463
|
-
"group",
|
|
4464
|
-
"company",
|
|
4465
|
-
"team",
|
|
4466
|
-
"pet",
|
|
4467
|
-
"service",
|
|
4468
|
-
"other"
|
|
4469
|
-
]);
|
|
4470
|
-
function createContact(db, input) {
|
|
4471
|
-
return withTransaction(db, () => {
|
|
4472
|
-
const name = input.name.trim();
|
|
4473
|
-
if (name.length === 0) {
|
|
4474
|
-
throw new AffinityValidationError("name must be non-empty");
|
|
4475
|
-
}
|
|
4476
|
-
if (!KINDS.has(input.kind)) {
|
|
4477
|
-
throw new AffinityValidationError("invalid contact kind");
|
|
4478
|
-
}
|
|
4479
|
-
if (input.isOwner !== void 0) {
|
|
4480
|
-
throw new AffinityValidationError(
|
|
4481
|
-
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
4482
|
-
);
|
|
4483
|
-
}
|
|
4484
|
-
const now = resolveNow(input.now);
|
|
4485
|
-
if (input.bootstrapOwner) {
|
|
4486
|
-
const existing = db.prepare(
|
|
4487
|
-
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
4488
|
-
).get();
|
|
4489
|
-
if (existing !== void 0) {
|
|
4490
|
-
throw new AffinityConflictError(
|
|
4491
|
-
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
4492
|
-
);
|
|
4493
|
-
}
|
|
4494
|
-
}
|
|
4495
|
-
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
4496
|
-
const result = db.prepare(
|
|
4497
|
-
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
4498
|
-
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
4499
|
-
).run(name, input.kind, isOwner, now, now);
|
|
4500
|
-
const id = Number(result.lastInsertRowid);
|
|
4501
|
-
const row = getContactRowById(db, id);
|
|
4502
|
-
if (!row) {
|
|
4503
|
-
throw new AffinityInvariantError("inserted contact row not found");
|
|
4504
|
-
}
|
|
4505
|
-
refreshContactRollup(db, id, now);
|
|
4506
|
-
const primary = mapContactRowToContactListItem(row);
|
|
4507
|
-
return buildContactMutationReceipt(primary, {
|
|
4508
|
-
created: [{ kind: "contact", id }]
|
|
4509
|
-
});
|
|
4510
|
-
});
|
|
4511
|
-
}
|
|
4512
|
-
|
|
4513
|
-
// src/contacts/fold_search_text.ts
|
|
4514
|
-
function foldSearchText(value) {
|
|
4515
|
-
let folded = value.toLowerCase();
|
|
4516
|
-
if (/[^\u0020-\u007e]/.test(folded))
|
|
4517
|
-
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
4518
|
-
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
4898
|
// src/identities/queries.ts
|
|
4522
4899
|
function getIdentityRowById(db, id) {
|
|
4523
4900
|
const row = db.prepare(
|
|
@@ -4549,14 +4926,19 @@ function buildIdentityMutationReceipt(primary, partial) {
|
|
|
4549
4926
|
// src/identities/validators.ts
|
|
4550
4927
|
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
4551
4928
|
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
4552
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
4553
|
-
).get(normalizedKey) : db.prepare(
|
|
4554
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
4555
|
-
).get(
|
|
4929
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
4930
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey) : db.prepare(
|
|
4931
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL AND id != ?"
|
|
4932
|
+
).get(
|
|
4933
|
+
normalizedKey,
|
|
4934
|
+
legacyIdentityKey(normalizedKey),
|
|
4935
|
+
normalizedKey,
|
|
4936
|
+
excludeIdentityId
|
|
4937
|
+
);
|
|
4556
4938
|
if (row !== void 0) {
|
|
4557
4939
|
const owner = db.prepare(
|
|
4558
|
-
"SELECT c.id, c.name FROM contacts c JOIN identities i ON i.contact_id = c.id WHERE i.normalized_key = ? AND i.removed_at IS NULL"
|
|
4559
|
-
).get(normalizedKey);
|
|
4940
|
+
"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"
|
|
4941
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey);
|
|
4560
4942
|
throw new AffinityConflictError(
|
|
4561
4943
|
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
4562
4944
|
);
|
|
@@ -4565,6 +4947,8 @@ function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
|
4565
4947
|
|
|
4566
4948
|
// src/identities/validate_identity_input.ts
|
|
4567
4949
|
var identityTypes = [
|
|
4950
|
+
"did",
|
|
4951
|
+
"atproto_did",
|
|
4568
4952
|
"email",
|
|
4569
4953
|
"phone",
|
|
4570
4954
|
"url",
|
|
@@ -4591,8 +4975,20 @@ var identityTypes = [
|
|
|
4591
4975
|
"account_id",
|
|
4592
4976
|
"alias"
|
|
4593
4977
|
];
|
|
4594
|
-
function validateIdentityInput(type,
|
|
4978
|
+
function validateIdentityInput(type, rawValue) {
|
|
4595
4979
|
const kind = type.toLowerCase();
|
|
4980
|
+
const value = normalizeIdentityValue(kind, rawValue);
|
|
4981
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(kind) && !/^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/.test(value))
|
|
4982
|
+
throw new AffinityValidationError(
|
|
4983
|
+
"did value must match did:<method>:<identifier> (for example did:plc:abc123)"
|
|
4984
|
+
);
|
|
4985
|
+
if (handleIdentityTypes.has(kind)) {
|
|
4986
|
+
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_.-]+$/;
|
|
4987
|
+
if (!pattern.test(value))
|
|
4988
|
+
throw new AffinityValidationError(
|
|
4989
|
+
`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)"}`
|
|
4990
|
+
);
|
|
4991
|
+
}
|
|
4596
4992
|
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
4597
4993
|
throw new AffinityValidationError(
|
|
4598
4994
|
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
@@ -4631,12 +5027,27 @@ function addIdentity(db, contactId, input) {
|
|
|
4631
5027
|
throw new AffinityStateError("merged contact is read-only");
|
|
4632
5028
|
}
|
|
4633
5029
|
const type = input.type.trim();
|
|
4634
|
-
const value = input.value
|
|
5030
|
+
const value = normalizeIdentityValue(type, input.value);
|
|
4635
5031
|
if (type.length === 0 || value.length === 0) {
|
|
4636
5032
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
4637
5033
|
}
|
|
4638
5034
|
validateIdentityInput(type, value);
|
|
5035
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5036
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
4639
5037
|
const nk = normalizeIdentityKey(type, value);
|
|
5038
|
+
if (input.ifExists === "return") {
|
|
5039
|
+
const existing = db.prepare(
|
|
5040
|
+
"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"
|
|
5041
|
+
).get(contactId, nk, legacyIdentityKey(nk), nk);
|
|
5042
|
+
if (existing) {
|
|
5043
|
+
const row2 = getIdentityRowById(db, Number(existing.id));
|
|
5044
|
+
if (row2)
|
|
5045
|
+
return buildIdentityMutationReceipt(
|
|
5046
|
+
mapIdentityRowToIdentityRecord(row2),
|
|
5047
|
+
{}
|
|
5048
|
+
);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
4640
5051
|
assertNoIdentityCollision(db, nk);
|
|
4641
5052
|
const now = resolveNow(input.now);
|
|
4642
5053
|
const verified = input.verified ? 1 : 0;
|
|
@@ -4670,31 +5081,239 @@ function addIdentity(db, contactId, input) {
|
|
|
4670
5081
|
});
|
|
4671
5082
|
}
|
|
4672
5083
|
|
|
4673
|
-
// src/
|
|
4674
|
-
|
|
5084
|
+
// src/contacts/create_contact.ts
|
|
5085
|
+
var KINDS = /* @__PURE__ */ new Set([
|
|
5086
|
+
"human",
|
|
5087
|
+
"group",
|
|
5088
|
+
"company",
|
|
5089
|
+
"team",
|
|
5090
|
+
"pet",
|
|
5091
|
+
"service",
|
|
5092
|
+
"other"
|
|
5093
|
+
]);
|
|
5094
|
+
function createContact(db, input) {
|
|
4675
5095
|
return withTransaction(db, () => {
|
|
4676
|
-
const
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
5096
|
+
const name = input.name.trim();
|
|
5097
|
+
if (name.length === 0) {
|
|
5098
|
+
throw new AffinityValidationError("name must be non-empty");
|
|
5099
|
+
}
|
|
5100
|
+
if (!KINDS.has(input.kind)) {
|
|
5101
|
+
throw new AffinityValidationError("invalid contact kind");
|
|
5102
|
+
}
|
|
5103
|
+
if (input.isOwner !== void 0) {
|
|
5104
|
+
throw new AffinityValidationError(
|
|
5105
|
+
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
5106
|
+
);
|
|
5107
|
+
}
|
|
5108
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5109
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
5110
|
+
const matches = (input.identities ?? []).flatMap((identity) => {
|
|
5111
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
5112
|
+
const key = normalizeIdentityKey(identity.type, identity.value);
|
|
5113
|
+
const row2 = db.prepare(
|
|
5114
|
+
`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)))`
|
|
5115
|
+
).get(key, legacyIdentityKey(key), key);
|
|
5116
|
+
return row2 ? [{ ...row2, identity }] : [];
|
|
5117
|
+
});
|
|
5118
|
+
const match = matches[0];
|
|
5119
|
+
if (match) {
|
|
5120
|
+
if (input.ifExists === "return") {
|
|
5121
|
+
const existing = getContactRowById(db, match.id);
|
|
5122
|
+
if (!existing)
|
|
5123
|
+
throw new AffinityInvariantError("identity owner not found");
|
|
5124
|
+
const primary2 = mapContactRowToContactListItem(existing);
|
|
5125
|
+
return Object.assign(buildContactMutationReceipt(primary2, {}), {
|
|
5126
|
+
outcome: "no_op",
|
|
5127
|
+
matchedIdentities: matches.map((m) => m.identity)
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
const error = new AffinityConflictError(
|
|
5131
|
+
`identity ${normalizeIdentityKey(match.identity.type, match.identity.value).replace(":", " ")} already belongs to #${match.id} ${match.name}`
|
|
5132
|
+
);
|
|
5133
|
+
error.existingContactId = match.id;
|
|
5134
|
+
throw error;
|
|
5135
|
+
}
|
|
5136
|
+
const now = resolveNow(input.now);
|
|
5137
|
+
if (input.bootstrapOwner) {
|
|
5138
|
+
const existing = db.prepare(
|
|
5139
|
+
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
5140
|
+
).get();
|
|
5141
|
+
if (existing !== void 0) {
|
|
5142
|
+
throw new AffinityConflictError(
|
|
5143
|
+
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
5144
|
+
);
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
5148
|
+
const result = db.prepare(
|
|
5149
|
+
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
5150
|
+
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
5151
|
+
).run(name, input.kind, isOwner, now, now);
|
|
5152
|
+
const id = Number(result.lastInsertRowid);
|
|
5153
|
+
const row = getContactRowById(db, id);
|
|
5154
|
+
if (!row) {
|
|
5155
|
+
throw new AffinityInvariantError("inserted contact row not found");
|
|
5156
|
+
}
|
|
5157
|
+
refreshContactRollup(db, id, now);
|
|
5158
|
+
const primary = mapContactRowToContactListItem(row);
|
|
5159
|
+
const receipt = buildContactMutationReceipt(primary, {
|
|
5160
|
+
created: [{ kind: "contact", id }]
|
|
5161
|
+
});
|
|
5162
|
+
for (const identity of input.identities ?? [])
|
|
5163
|
+
receipt.created.push(...addIdentity(db, id, identity).created);
|
|
5164
|
+
return receipt;
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
|
|
5168
|
+
// src/contacts/fold_search_text.ts
|
|
5169
|
+
function foldSearchText(value) {
|
|
5170
|
+
let folded = value.toLowerCase();
|
|
5171
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
5172
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
5173
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
// src/contacts/list_duplicate_candidates.ts
|
|
5177
|
+
function pairKey(leftContactId, rightContactId) {
|
|
5178
|
+
const left = Math.min(leftContactId, rightContactId);
|
|
5179
|
+
const right = Math.max(leftContactId, rightContactId);
|
|
5180
|
+
return `${left}:${right}`;
|
|
5181
|
+
}
|
|
5182
|
+
function normalizeName(name) {
|
|
5183
|
+
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
5184
|
+
}
|
|
5185
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
5186
|
+
const left = normalizeName(leftInput);
|
|
5187
|
+
const right = normalizeName(rightInput);
|
|
5188
|
+
if (left === right) {
|
|
5189
|
+
return 0.92;
|
|
5190
|
+
}
|
|
5191
|
+
if (left.length >= 3 && right.length >= 3) {
|
|
5192
|
+
if (left.includes(right) || right.includes(left)) {
|
|
5193
|
+
return 0.72;
|
|
5194
|
+
}
|
|
5195
|
+
const leftTokens = new Set(left.split(" "));
|
|
5196
|
+
const rightTokens = new Set(right.split(" "));
|
|
5197
|
+
const overlap = [...leftTokens].filter(
|
|
5198
|
+
(token) => rightTokens.has(token)
|
|
5199
|
+
).length;
|
|
5200
|
+
if (overlap > 0) {
|
|
5201
|
+
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
return 0;
|
|
5205
|
+
}
|
|
5206
|
+
function listDuplicateCandidates(db, filters, options) {
|
|
5207
|
+
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
5208
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5209
|
+
const subset = filters?.contactIds;
|
|
5210
|
+
const contacts = db.prepare(
|
|
5211
|
+
`SELECT id, name
|
|
5212
|
+
FROM contacts
|
|
5213
|
+
WHERE deleted_at IS NULL
|
|
5214
|
+
AND lifecycle_state != 'merged'`
|
|
5215
|
+
).all();
|
|
5216
|
+
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
5217
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
5218
|
+
const exactMatches = db.prepare(
|
|
5219
|
+
`SELECT
|
|
5220
|
+
i1.contact_id AS left_contact_id,
|
|
5221
|
+
i2.contact_id AS right_contact_id,
|
|
5222
|
+
i1.normalized_key
|
|
5223
|
+
FROM identities i1
|
|
5224
|
+
INNER JOIN identities i2
|
|
5225
|
+
ON i1.normalized_key = i2.normalized_key
|
|
5226
|
+
AND i1.contact_id < i2.contact_id
|
|
5227
|
+
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
5228
|
+
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
5229
|
+
WHERE i1.removed_at IS NULL
|
|
5230
|
+
AND i2.removed_at IS NULL
|
|
5231
|
+
AND c1.deleted_at IS NULL
|
|
5232
|
+
AND c2.deleted_at IS NULL
|
|
5233
|
+
AND c1.lifecycle_state != 'merged'
|
|
5234
|
+
AND c2.lifecycle_state != 'merged'`
|
|
5235
|
+
).all();
|
|
5236
|
+
for (const match of exactMatches) {
|
|
5237
|
+
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
5238
|
+
continue;
|
|
5239
|
+
}
|
|
5240
|
+
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
5241
|
+
leftContactId: match.left_contact_id,
|
|
5242
|
+
rightContactId: match.right_contact_id,
|
|
5243
|
+
matchReason: `exact identity ${match.normalized_key}`,
|
|
5244
|
+
matchScore: 1
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
if (filters?.exactOnly !== true) {
|
|
5248
|
+
for (let index = 0; index < contacts.length; index += 1) {
|
|
5249
|
+
const left = contacts[index];
|
|
5250
|
+
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
5251
|
+
continue;
|
|
5252
|
+
}
|
|
5253
|
+
const leftName = normalizeName(left.name);
|
|
5254
|
+
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
5255
|
+
const right = contacts[offsetIndex];
|
|
5256
|
+
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
5257
|
+
continue;
|
|
5258
|
+
}
|
|
5259
|
+
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
5260
|
+
if (score <= 0) {
|
|
5261
|
+
continue;
|
|
5262
|
+
}
|
|
5263
|
+
const key = pairKey(left.id, right.id);
|
|
5264
|
+
if (!pairs.has(key)) {
|
|
5265
|
+
pairs.set(key, {
|
|
5266
|
+
leftContactId: left.id,
|
|
5267
|
+
rightContactId: right.id,
|
|
5268
|
+
matchReason: "name similarity",
|
|
5269
|
+
matchScore: score
|
|
5270
|
+
});
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
5276
|
+
const dismissedSet = new Set(
|
|
5277
|
+
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
5278
|
+
);
|
|
5279
|
+
const includeDismissed = filters?.includeDismissed === true;
|
|
5280
|
+
for (const candidate of pairs.values()) {
|
|
5281
|
+
if (dismissedSet.has(
|
|
5282
|
+
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
5283
|
+
)) {
|
|
5284
|
+
candidate.dismissed = true;
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
const minScore = filters?.minScore ?? 0;
|
|
5288
|
+
return [...pairs.values()].filter((candidate) => {
|
|
5289
|
+
if (candidate.matchScore < minScore) return false;
|
|
5290
|
+
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
5291
|
+
return true;
|
|
5292
|
+
}).sort((left, right) => {
|
|
5293
|
+
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
5294
|
+
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
5295
|
+
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
5296
|
+
}).slice(offset, offset + limit);
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
// src/tools/create_contact_tool_result.ts
|
|
5300
|
+
function createContactToolResult(db, input) {
|
|
5301
|
+
return withTransaction(db, () => {
|
|
5302
|
+
const receipt = createContact(db, input);
|
|
5303
|
+
const result = mutationToolResult("create", receipt, "contact");
|
|
5304
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
5305
|
+
result.outcome = "no_op";
|
|
5306
|
+
Object.assign(result.data, {
|
|
5307
|
+
matchedIdentities: receipt.matchedIdentities
|
|
5308
|
+
});
|
|
5309
|
+
return result;
|
|
5310
|
+
}
|
|
4692
5311
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
4693
5312
|
const warnings = db.prepare(
|
|
4694
5313
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
4695
5314
|
).all(receipt.primary.id).flatMap((row) => {
|
|
4696
|
-
const score =
|
|
4697
|
-
return score
|
|
5315
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
5316
|
+
return score <= 0 ? [] : [
|
|
4698
5317
|
{
|
|
4699
5318
|
code: "possible_duplicate",
|
|
4700
5319
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -4777,6 +5396,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
4777
5396
|
return contact.result;
|
|
4778
5397
|
}
|
|
4779
5398
|
if (input.action === "delete") {
|
|
5399
|
+
if (contact.value.is_owner)
|
|
5400
|
+
return toolFailure(
|
|
5401
|
+
"domain",
|
|
5402
|
+
"conflict",
|
|
5403
|
+
"Cannot delete owner.",
|
|
5404
|
+
"The owner contact cannot be deleted"
|
|
5405
|
+
);
|
|
4780
5406
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
4781
5407
|
return toolSuccess("Permanently deleted contact.", {
|
|
4782
5408
|
action: "delete",
|
|
@@ -4850,7 +5476,11 @@ var manageContactTool = defineAffinityTool({
|
|
|
4850
5476
|
},
|
|
4851
5477
|
["type", "value"]
|
|
4852
5478
|
),
|
|
4853
|
-
"Optional identities to attach.
|
|
5479
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
5480
|
+
),
|
|
5481
|
+
ifExists: enumSchema(
|
|
5482
|
+
"Identity collision policy; return attaches nothing.",
|
|
5483
|
+
["error", "return"]
|
|
4854
5484
|
),
|
|
4855
5485
|
name: stringSchema("Contact name."),
|
|
4856
5486
|
kind: enumSchema("Contact kind.", [
|
|
@@ -5460,6 +6090,268 @@ var manageDuplicateDismissalTool = defineAffinityTool({
|
|
|
5460
6090
|
handler: manageDuplicateDismissalToolHandler
|
|
5461
6091
|
});
|
|
5462
6092
|
|
|
6093
|
+
// src/events/event_replay.ts
|
|
6094
|
+
function replayEventLinks(db, linkIds, now) {
|
|
6095
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
6096
|
+
for (const linkId of linkIds) {
|
|
6097
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
6098
|
+
if (!saved) continue;
|
|
6099
|
+
const baseline = JSON.parse(saved.state_json);
|
|
6100
|
+
const effects = db.prepare(
|
|
6101
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
6102
|
+
).all(linkId);
|
|
6103
|
+
const overrides = db.prepare(
|
|
6104
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
6105
|
+
).all(linkId);
|
|
6106
|
+
let overrideIndex = 0;
|
|
6107
|
+
const applyOverrides = (beforeId) => {
|
|
6108
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
6109
|
+
const override = overrides[overrideIndex];
|
|
6110
|
+
if (override)
|
|
6111
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
6112
|
+
override.state,
|
|
6113
|
+
linkId
|
|
6114
|
+
);
|
|
6115
|
+
overrideIndex++;
|
|
6116
|
+
}
|
|
6117
|
+
};
|
|
6118
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
6119
|
+
db.prepare(
|
|
6120
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
6121
|
+
).run(
|
|
6122
|
+
baseline.rank,
|
|
6123
|
+
baseline.affinity,
|
|
6124
|
+
baseline.trust,
|
|
6125
|
+
baseline.state,
|
|
6126
|
+
baseline.cadence_days,
|
|
6127
|
+
now,
|
|
6128
|
+
linkId
|
|
6129
|
+
);
|
|
6130
|
+
for (const effect of effects) {
|
|
6131
|
+
eventIds.add(effect.event_id);
|
|
6132
|
+
applyOverrides(effect.event_id);
|
|
6133
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
6134
|
+
continue;
|
|
6135
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
6136
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
6137
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
6138
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
6139
|
+
applyEventEffectToLink(db, {
|
|
6140
|
+
linkId,
|
|
6141
|
+
eventId: event.id,
|
|
6142
|
+
eventType: event.type,
|
|
6143
|
+
occurredAt: event.occurredAt,
|
|
6144
|
+
significance: event.significance,
|
|
6145
|
+
participant,
|
|
6146
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
6147
|
+
now: effect.created_at
|
|
6148
|
+
});
|
|
6149
|
+
}
|
|
6150
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
6151
|
+
refreshLinkRollup(db, linkId, now);
|
|
6152
|
+
}
|
|
6153
|
+
for (const eventId of eventIds)
|
|
6154
|
+
db.prepare(
|
|
6155
|
+
"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=?"
|
|
6156
|
+
).run(eventId);
|
|
6157
|
+
refreshAllBridgeScores(db, now);
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
// src/events/manage_event.ts
|
|
6161
|
+
function requireEventTarget(db, target) {
|
|
6162
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
6163
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
6164
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
6165
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
6166
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
6167
|
+
return event;
|
|
6168
|
+
}
|
|
6169
|
+
function affected(db, id) {
|
|
6170
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
6171
|
+
}
|
|
6172
|
+
function reviseEvent(db, target, patch) {
|
|
6173
|
+
return withTransaction(db, () => {
|
|
6174
|
+
const allowed = [
|
|
6175
|
+
"summary",
|
|
6176
|
+
"content",
|
|
6177
|
+
"occurredAt",
|
|
6178
|
+
"significance",
|
|
6179
|
+
"channel",
|
|
6180
|
+
"type",
|
|
6181
|
+
"externalRef"
|
|
6182
|
+
];
|
|
6183
|
+
for (const key of Object.keys(patch))
|
|
6184
|
+
if (!allowed.includes(key))
|
|
6185
|
+
throw new AffinityValidationError(
|
|
6186
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
6187
|
+
);
|
|
6188
|
+
const event = requireEventTarget(db, target);
|
|
6189
|
+
const next = { ...event, ...patch };
|
|
6190
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
6191
|
+
validateEventMetadata(metadata);
|
|
6192
|
+
Object.assign(next, metadata);
|
|
6193
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
6194
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
6195
|
+
if (patch.occurredAt !== void 0)
|
|
6196
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
6197
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
6198
|
+
throw new AffinityValidationError(
|
|
6199
|
+
"significance must be an integer from 1 to 10"
|
|
6200
|
+
);
|
|
6201
|
+
const interactions = [
|
|
6202
|
+
"conversation",
|
|
6203
|
+
"activity",
|
|
6204
|
+
"gift",
|
|
6205
|
+
"support",
|
|
6206
|
+
"conflict",
|
|
6207
|
+
"correction"
|
|
6208
|
+
];
|
|
6209
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
6210
|
+
throw new AffinityValidationError(
|
|
6211
|
+
"type must stay within the same event kind"
|
|
6212
|
+
);
|
|
6213
|
+
if (patch.externalRef && event.externalRef)
|
|
6214
|
+
throw new AffinityConflictError(
|
|
6215
|
+
"externalRef can only be set when absent"
|
|
6216
|
+
);
|
|
6217
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
6218
|
+
throw new AffinityConflictError(
|
|
6219
|
+
"externalRef already belongs to an event"
|
|
6220
|
+
);
|
|
6221
|
+
const now = Date.now();
|
|
6222
|
+
const links = affected(db, event.id);
|
|
6223
|
+
db.prepare(
|
|
6224
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
6225
|
+
).run(
|
|
6226
|
+
next.summary.trim(),
|
|
6227
|
+
next.content ?? null,
|
|
6228
|
+
next.occurredAt,
|
|
6229
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
6230
|
+
next.significance,
|
|
6231
|
+
next.channel ?? null,
|
|
6232
|
+
next.type,
|
|
6233
|
+
next.externalRef?.source ?? null,
|
|
6234
|
+
next.externalRef?.id ?? null,
|
|
6235
|
+
now,
|
|
6236
|
+
event.id
|
|
6237
|
+
);
|
|
6238
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
6239
|
+
db.prepare(
|
|
6240
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
6241
|
+
).run(patch.type, event.id);
|
|
6242
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
6243
|
+
replayEventLinks(db, links, now);
|
|
6244
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
6245
|
+
updated: [{ kind: "event", id: event.id }],
|
|
6246
|
+
affectedLinks: links
|
|
6247
|
+
});
|
|
6248
|
+
});
|
|
6249
|
+
}
|
|
6250
|
+
function deleteEvent(db, target, options) {
|
|
6251
|
+
return withTransaction(db, () => {
|
|
6252
|
+
if (options?.confirm !== true)
|
|
6253
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
6254
|
+
const event = requireEventTarget(db, target);
|
|
6255
|
+
const links = affected(db, event.id);
|
|
6256
|
+
for (const table of [
|
|
6257
|
+
"event_participants",
|
|
6258
|
+
"attributes",
|
|
6259
|
+
"open_commitments",
|
|
6260
|
+
"upcoming_occurrences",
|
|
6261
|
+
"link_event_effects"
|
|
6262
|
+
])
|
|
6263
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
6264
|
+
db.prepare(
|
|
6265
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
6266
|
+
).run(event.id);
|
|
6267
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
6268
|
+
replayEventLinks(db, links, Date.now());
|
|
6269
|
+
return buildEventMutationReceipt(event, {
|
|
6270
|
+
removed: [{ kind: "event", id: event.id }],
|
|
6271
|
+
affectedLinks: links
|
|
6272
|
+
});
|
|
6273
|
+
});
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6276
|
+
// src/tools/manage_event_tool.ts
|
|
6277
|
+
function manageEventToolHandler(db, raw) {
|
|
6278
|
+
const input = structuredClone(raw);
|
|
6279
|
+
const error = validateToolInput(
|
|
6280
|
+
manageEventTool.inputSchema,
|
|
6281
|
+
input,
|
|
6282
|
+
"input",
|
|
6283
|
+
Date.now(),
|
|
6284
|
+
true
|
|
6285
|
+
);
|
|
6286
|
+
if (error)
|
|
6287
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
6288
|
+
return withToolHandling(() => {
|
|
6289
|
+
const target = {
|
|
6290
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
6291
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
6292
|
+
};
|
|
6293
|
+
return mutationToolResult(
|
|
6294
|
+
input.action,
|
|
6295
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
6296
|
+
"event"
|
|
6297
|
+
);
|
|
6298
|
+
}, "Event management failed.");
|
|
6299
|
+
}
|
|
6300
|
+
var manageEventTool = defineAffinityTool({
|
|
6301
|
+
name: manageEventToolName,
|
|
6302
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
6303
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
6304
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
6305
|
+
sideEffects: "writes_state",
|
|
6306
|
+
readOnly: false,
|
|
6307
|
+
supportsClarification: false,
|
|
6308
|
+
targetKinds: ["event"],
|
|
6309
|
+
inputDescriptions: {
|
|
6310
|
+
action: "revise or delete",
|
|
6311
|
+
eventId: "Exact event id",
|
|
6312
|
+
ref: "External event reference",
|
|
6313
|
+
patch: "Fields to revise",
|
|
6314
|
+
confirm: "Required true for deletion"
|
|
6315
|
+
},
|
|
6316
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
6317
|
+
inputSchema: objectSchema(
|
|
6318
|
+
{
|
|
6319
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
6320
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
6321
|
+
ref: externalRefSchema,
|
|
6322
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
6323
|
+
patch: objectSchema(
|
|
6324
|
+
{
|
|
6325
|
+
summary: stringSchema("Short non-empty label."),
|
|
6326
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
6327
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
6328
|
+
significance: integerSchema("Significance 1..10."),
|
|
6329
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
6330
|
+
type: enumSchema("Type within the same event kind.", [
|
|
6331
|
+
"conversation",
|
|
6332
|
+
"activity",
|
|
6333
|
+
"gift",
|
|
6334
|
+
"support",
|
|
6335
|
+
"conflict",
|
|
6336
|
+
"correction",
|
|
6337
|
+
"observation",
|
|
6338
|
+
"milestone",
|
|
6339
|
+
"transaction",
|
|
6340
|
+
"promise",
|
|
6341
|
+
"agreement",
|
|
6342
|
+
"date_anchor"
|
|
6343
|
+
]),
|
|
6344
|
+
externalRef: externalRefSchema
|
|
6345
|
+
},
|
|
6346
|
+
[],
|
|
6347
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
6348
|
+
)
|
|
6349
|
+
},
|
|
6350
|
+
["action"]
|
|
6351
|
+
),
|
|
6352
|
+
handler: manageEventToolHandler
|
|
6353
|
+
});
|
|
6354
|
+
|
|
5463
6355
|
// src/identities/remove_identity.ts
|
|
5464
6356
|
function removeIdentity(db, identityId, removedAt) {
|
|
5465
6357
|
return withTransaction(db, () => {
|
|
@@ -5507,7 +6399,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
5507
6399
|
});
|
|
5508
6400
|
}
|
|
5509
6401
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
5510
|
-
const nextValue =
|
|
6402
|
+
const nextValue = normalizeIdentityValue(
|
|
6403
|
+
nextType,
|
|
6404
|
+
patch.value ?? row.value
|
|
6405
|
+
);
|
|
5511
6406
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
5512
6407
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5513
6408
|
}
|
|
@@ -5566,6 +6461,10 @@ function contactLocatorSchema6(description) {
|
|
|
5566
6461
|
contactId: integerSchema("Exact contact id."),
|
|
5567
6462
|
identity: objectSchema(
|
|
5568
6463
|
{
|
|
6464
|
+
ifExists: enumSchema(
|
|
6465
|
+
"Return an existing identity on this contact without changes.",
|
|
6466
|
+
["error", "return"]
|
|
6467
|
+
),
|
|
5569
6468
|
type: stringSchema(
|
|
5570
6469
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
5571
6470
|
),
|
|
@@ -5662,6 +6561,10 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
5662
6561
|
),
|
|
5663
6562
|
input: objectSchema(
|
|
5664
6563
|
{
|
|
6564
|
+
ifExists: enumSchema(
|
|
6565
|
+
"Return an existing identity on this contact without changes.",
|
|
6566
|
+
["error", "return"]
|
|
6567
|
+
),
|
|
5665
6568
|
type: stringSchema(
|
|
5666
6569
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
5667
6570
|
),
|
|
@@ -5723,6 +6626,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
5723
6626
|
now,
|
|
5724
6627
|
linkId
|
|
5725
6628
|
);
|
|
6629
|
+
db.prepare(
|
|
6630
|
+
"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=?"
|
|
6631
|
+
).run(linkId, state, linkId);
|
|
5726
6632
|
refreshLinkRollup(db, linkId, now);
|
|
5727
6633
|
if (state === "archived" || row.state === "archived") {
|
|
5728
6634
|
refreshAllBridgeScores(db, now);
|
|
@@ -6357,6 +7263,8 @@ var ALLOWED = /* @__PURE__ */ new Set([
|
|
|
6357
7263
|
function recordInteraction(db, input) {
|
|
6358
7264
|
return withTransaction(db, () => {
|
|
6359
7265
|
validateSocialEventInput(input);
|
|
7266
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7267
|
+
if (existing) return existing;
|
|
6360
7268
|
if (!ALLOWED.has(input.type)) {
|
|
6361
7269
|
throw new AffinityValidationError("invalid interaction event type");
|
|
6362
7270
|
}
|
|
@@ -6366,11 +7274,12 @@ function recordInteraction(db, input) {
|
|
|
6366
7274
|
assertOwnerParticipates(ownerId, ids);
|
|
6367
7275
|
const now = resolveNow(input.now);
|
|
6368
7276
|
const eventId = insertJournalEvent(db, {
|
|
7277
|
+
...input,
|
|
6369
7278
|
type: input.type,
|
|
6370
7279
|
occurredAt: input.occurredAt,
|
|
6371
|
-
|
|
7280
|
+
occurredOn: input.occurredOn,
|
|
6372
7281
|
significance: input.significance,
|
|
6373
|
-
momentKind: null,
|
|
7282
|
+
momentKind: input.momentKind ?? null,
|
|
6374
7283
|
participants: input.participants,
|
|
6375
7284
|
now
|
|
6376
7285
|
});
|
|
@@ -6378,12 +7287,24 @@ function recordInteraction(db, input) {
|
|
|
6378
7287
|
eventId,
|
|
6379
7288
|
eventType: input.type,
|
|
6380
7289
|
occurredAt: input.occurredAt,
|
|
7290
|
+
occurredOn: input.occurredOn,
|
|
6381
7291
|
significance: input.significance,
|
|
6382
7292
|
participants: input.participants,
|
|
6383
7293
|
provenance: input.provenance,
|
|
6384
7294
|
now
|
|
6385
7295
|
});
|
|
7296
|
+
if (input.momentKind !== void 0)
|
|
7297
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7298
|
+
input.momentKind,
|
|
7299
|
+
eventId
|
|
7300
|
+
);
|
|
6386
7301
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7302
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7303
|
+
{
|
|
7304
|
+
code: "future_event",
|
|
7305
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7306
|
+
}
|
|
7307
|
+
] : void 0,
|
|
6387
7308
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6388
7309
|
affectedLinks: mechanics.affectedLinks,
|
|
6389
7310
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6395,17 +7316,20 @@ function recordInteraction(db, input) {
|
|
|
6395
7316
|
function recordMilestone(db, input) {
|
|
6396
7317
|
return withTransaction(db, () => {
|
|
6397
7318
|
validateSocialEventInput(input);
|
|
7319
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7320
|
+
if (existing) return existing;
|
|
6398
7321
|
const ownerId = requireOwnerContactId(db);
|
|
6399
7322
|
const ids = input.participants.map((p) => p.contactId);
|
|
6400
7323
|
assertParticipantContactsLive(db, ids);
|
|
6401
7324
|
assertOwnerParticipates(ownerId, ids);
|
|
6402
7325
|
const now = resolveNow(input.now);
|
|
6403
7326
|
const eventId = insertJournalEvent(db, {
|
|
7327
|
+
...input,
|
|
6404
7328
|
type: "milestone",
|
|
6405
7329
|
occurredAt: input.occurredAt,
|
|
6406
|
-
|
|
7330
|
+
occurredOn: input.occurredOn,
|
|
6407
7331
|
significance: input.significance,
|
|
6408
|
-
momentKind: null,
|
|
7332
|
+
momentKind: input.momentKind ?? null,
|
|
6409
7333
|
participants: input.participants,
|
|
6410
7334
|
now
|
|
6411
7335
|
});
|
|
@@ -6413,12 +7337,24 @@ function recordMilestone(db, input) {
|
|
|
6413
7337
|
eventId,
|
|
6414
7338
|
eventType: "milestone",
|
|
6415
7339
|
occurredAt: input.occurredAt,
|
|
7340
|
+
occurredOn: input.occurredOn,
|
|
6416
7341
|
significance: input.significance,
|
|
6417
7342
|
participants: input.participants,
|
|
6418
7343
|
provenance: input.provenance,
|
|
6419
7344
|
now
|
|
6420
7345
|
});
|
|
7346
|
+
if (input.momentKind !== void 0)
|
|
7347
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7348
|
+
input.momentKind,
|
|
7349
|
+
eventId
|
|
7350
|
+
);
|
|
6421
7351
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7352
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7353
|
+
{
|
|
7354
|
+
code: "future_event",
|
|
7355
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7356
|
+
}
|
|
7357
|
+
] : void 0,
|
|
6422
7358
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6423
7359
|
affectedLinks: mechanics.affectedLinks,
|
|
6424
7360
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6430,15 +7366,18 @@ function recordMilestone(db, input) {
|
|
|
6430
7366
|
function recordObservation(db, input) {
|
|
6431
7367
|
return withTransaction(db, () => {
|
|
6432
7368
|
validateSocialEventInput(input);
|
|
7369
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7370
|
+
if (existing) return existing;
|
|
6433
7371
|
const ids = input.participants.map((p) => p.contactId);
|
|
6434
7372
|
assertParticipantContactsLive(db, ids);
|
|
6435
7373
|
const now = resolveNow(input.now);
|
|
6436
7374
|
const eventId = insertJournalEvent(db, {
|
|
7375
|
+
...input,
|
|
6437
7376
|
type: "observation",
|
|
6438
7377
|
occurredAt: input.occurredAt,
|
|
6439
|
-
|
|
7378
|
+
occurredOn: input.occurredOn,
|
|
6440
7379
|
significance: input.significance,
|
|
6441
|
-
momentKind: null,
|
|
7380
|
+
momentKind: input.momentKind ?? null,
|
|
6442
7381
|
participants: input.participants,
|
|
6443
7382
|
now
|
|
6444
7383
|
});
|
|
@@ -6446,12 +7385,24 @@ function recordObservation(db, input) {
|
|
|
6446
7385
|
eventId,
|
|
6447
7386
|
eventType: "observation",
|
|
6448
7387
|
occurredAt: input.occurredAt,
|
|
7388
|
+
occurredOn: input.occurredOn,
|
|
6449
7389
|
significance: input.significance,
|
|
6450
7390
|
participants: input.participants,
|
|
6451
7391
|
provenance: input.provenance,
|
|
6452
7392
|
now
|
|
6453
7393
|
});
|
|
7394
|
+
if (input.momentKind !== void 0)
|
|
7395
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7396
|
+
input.momentKind,
|
|
7397
|
+
eventId
|
|
7398
|
+
);
|
|
6454
7399
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7400
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7401
|
+
{
|
|
7402
|
+
code: "future_event",
|
|
7403
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7404
|
+
}
|
|
7405
|
+
] : void 0,
|
|
6455
7406
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6456
7407
|
affectedLinks: mechanics.affectedLinks,
|
|
6457
7408
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6463,17 +7414,20 @@ function recordObservation(db, input) {
|
|
|
6463
7414
|
function recordTransaction(db, input) {
|
|
6464
7415
|
return withTransaction(db, () => {
|
|
6465
7416
|
validateSocialEventInput(input);
|
|
7417
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
7418
|
+
if (existing) return existing;
|
|
6466
7419
|
const ownerId = requireOwnerContactId(db);
|
|
6467
7420
|
const ids = input.participants.map((p) => p.contactId);
|
|
6468
7421
|
assertParticipantContactsLive(db, ids);
|
|
6469
7422
|
assertOwnerParticipates(ownerId, ids);
|
|
6470
7423
|
const now = resolveNow(input.now);
|
|
6471
7424
|
const eventId = insertJournalEvent(db, {
|
|
7425
|
+
...input,
|
|
6472
7426
|
type: "transaction",
|
|
6473
7427
|
occurredAt: input.occurredAt,
|
|
6474
|
-
|
|
7428
|
+
occurredOn: input.occurredOn,
|
|
6475
7429
|
significance: input.significance,
|
|
6476
|
-
momentKind: null,
|
|
7430
|
+
momentKind: input.momentKind ?? null,
|
|
6477
7431
|
participants: input.participants,
|
|
6478
7432
|
now
|
|
6479
7433
|
});
|
|
@@ -6481,12 +7435,24 @@ function recordTransaction(db, input) {
|
|
|
6481
7435
|
eventId,
|
|
6482
7436
|
eventType: "transaction",
|
|
6483
7437
|
occurredAt: input.occurredAt,
|
|
7438
|
+
occurredOn: input.occurredOn,
|
|
6484
7439
|
significance: input.significance,
|
|
6485
7440
|
participants: input.participants,
|
|
6486
7441
|
provenance: input.provenance,
|
|
6487
7442
|
now
|
|
6488
7443
|
});
|
|
7444
|
+
if (input.momentKind !== void 0)
|
|
7445
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
7446
|
+
input.momentKind,
|
|
7447
|
+
eventId
|
|
7448
|
+
);
|
|
6489
7449
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
7450
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
7451
|
+
{
|
|
7452
|
+
code: "future_event",
|
|
7453
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
7454
|
+
}
|
|
7455
|
+
] : void 0,
|
|
6490
7456
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
6491
7457
|
affectedLinks: mechanics.affectedLinks,
|
|
6492
7458
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -6495,11 +7461,13 @@ function recordTransaction(db, input) {
|
|
|
6495
7461
|
}
|
|
6496
7462
|
|
|
6497
7463
|
// src/tools/record_event_tool.ts
|
|
6498
|
-
function normalizeParticipant2(p) {
|
|
7464
|
+
function normalizeParticipant2(db, p) {
|
|
6499
7465
|
let contactId = p.contactId;
|
|
6500
7466
|
if (contactId === void 0 && p.contact != null) {
|
|
6501
7467
|
const loc = p.contact;
|
|
6502
|
-
|
|
7468
|
+
const resolved = resolveContactLocator(db, loc);
|
|
7469
|
+
if (!resolved.ok) throw resolved.result;
|
|
7470
|
+
contactId = resolved.value.id;
|
|
6503
7471
|
}
|
|
6504
7472
|
const result = {
|
|
6505
7473
|
contactId,
|
|
@@ -6517,12 +7485,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
6517
7485
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
6518
7486
|
}
|
|
6519
7487
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
6520
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
7488
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
6521
7489
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
6522
7490
|
}
|
|
6523
7491
|
function recordEventToolHandler(db, rawInput) {
|
|
6524
7492
|
const input = structuredClone(rawInput);
|
|
6525
|
-
const inputError = validateToolInput(
|
|
7493
|
+
const inputError = validateToolInput(
|
|
7494
|
+
recordEventTool.inputSchema,
|
|
7495
|
+
input,
|
|
7496
|
+
"input",
|
|
7497
|
+
Date.now(),
|
|
7498
|
+
true
|
|
7499
|
+
);
|
|
6526
7500
|
if (inputError)
|
|
6527
7501
|
return toolFailure(
|
|
6528
7502
|
"protocol",
|
|
@@ -6602,6 +7576,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
6602
7576
|
"correction"
|
|
6603
7577
|
]
|
|
6604
7578
|
),
|
|
7579
|
+
...eventMetadataSchema,
|
|
6605
7580
|
occurredAt: integerSchema(
|
|
6606
7581
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
6607
7582
|
),
|
|
@@ -6610,6 +7585,19 @@ var recordEventTool = defineAffinityTool({
|
|
|
6610
7585
|
participants: arraySchema(
|
|
6611
7586
|
objectSchema(
|
|
6612
7587
|
{
|
|
7588
|
+
contact: objectSchema(
|
|
7589
|
+
{
|
|
7590
|
+
contactId: integerSchema("Contact id."),
|
|
7591
|
+
identity: objectSchema(
|
|
7592
|
+
{
|
|
7593
|
+
type: stringSchema("Identity type."),
|
|
7594
|
+
value: stringSchema("Identity value.")
|
|
7595
|
+
},
|
|
7596
|
+
["type", "value"]
|
|
7597
|
+
)
|
|
7598
|
+
},
|
|
7599
|
+
[]
|
|
7600
|
+
),
|
|
6613
7601
|
contactId: integerSchema(
|
|
6614
7602
|
"Participant contact id. The owner contact MUST be included."
|
|
6615
7603
|
),
|
|
@@ -6633,7 +7621,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
6633
7621
|
),
|
|
6634
7622
|
now: integerSchema("Optional timestamp override.")
|
|
6635
7623
|
},
|
|
6636
|
-
["occurredAt", "
|
|
7624
|
+
["occurredAt", "significance", "participants"],
|
|
6637
7625
|
"Event payload. Always required. Interaction events also require `type`."
|
|
6638
7626
|
)
|
|
6639
7627
|
},
|
|
@@ -6649,7 +7637,10 @@ function contactAttributeClauses(filters) {
|
|
|
6649
7637
|
const params = [];
|
|
6650
7638
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
6651
7639
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
6652
|
-
for (const filter of [
|
|
7640
|
+
for (const filter of [
|
|
7641
|
+
...attributes,
|
|
7642
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
7643
|
+
]) {
|
|
6653
7644
|
const separator = filter.indexOf("=");
|
|
6654
7645
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
6655
7646
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -6683,153 +7674,32 @@ function listContacts(db, filters, options) {
|
|
|
6683
7674
|
if (filters?.includeDormant === false || options?.includeDormant === false) {
|
|
6684
7675
|
clauses.push("lifecycle_state != 'dormant'");
|
|
6685
7676
|
}
|
|
6686
|
-
const attributeFilters = contactAttributeClauses(filters);
|
|
6687
|
-
clauses.push(...attributeFilters.clauses);
|
|
6688
|
-
params.push(...attributeFilters.params);
|
|
6689
|
-
const where = clauses.join(" AND ");
|
|
6690
|
-
const rows = db.prepare(
|
|
6691
|
-
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
6692
|
-
created_at, updated_at, deleted_at,
|
|
6693
|
-
(
|
|
6694
|
-
SELECT value
|
|
6695
|
-
FROM identities i
|
|
6696
|
-
WHERE i.contact_id = contacts.id AND i.removed_at IS NULL
|
|
6697
|
-
ORDER BY i.id
|
|
6698
|
-
LIMIT 1
|
|
6699
|
-
) AS primary_identity
|
|
6700
|
-
FROM contacts
|
|
6701
|
-
WHERE ${where}
|
|
6702
|
-
ORDER BY name ASC
|
|
6703
|
-
LIMIT ? OFFSET ?`
|
|
6704
|
-
).all(...params, limit, offset);
|
|
6705
|
-
return rows.map((r) => {
|
|
6706
|
-
const item = mapContactRowToContactListItem(r);
|
|
6707
|
-
return {
|
|
6708
|
-
...item,
|
|
6709
|
-
primaryIdentity: r.primary_identity ?? null
|
|
6710
|
-
};
|
|
6711
|
-
});
|
|
6712
|
-
}
|
|
6713
|
-
|
|
6714
|
-
// src/contacts/list_duplicate_candidates.ts
|
|
6715
|
-
function pairKey(leftContactId, rightContactId) {
|
|
6716
|
-
const left = Math.min(leftContactId, rightContactId);
|
|
6717
|
-
const right = Math.max(leftContactId, rightContactId);
|
|
6718
|
-
return `${left}:${right}`;
|
|
6719
|
-
}
|
|
6720
|
-
function normalizeName(name) {
|
|
6721
|
-
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6722
|
-
}
|
|
6723
|
-
function fuzzyNameScore(left, right) {
|
|
6724
|
-
if (left === right) {
|
|
6725
|
-
return 0.92;
|
|
6726
|
-
}
|
|
6727
|
-
if (left.length >= 3 && right.length >= 3) {
|
|
6728
|
-
if (left.includes(right) || right.includes(left)) {
|
|
6729
|
-
return 0.72;
|
|
6730
|
-
}
|
|
6731
|
-
const leftTokens = new Set(left.split(" "));
|
|
6732
|
-
const rightTokens = new Set(right.split(" "));
|
|
6733
|
-
const overlap = [...leftTokens].filter(
|
|
6734
|
-
(token) => rightTokens.has(token)
|
|
6735
|
-
).length;
|
|
6736
|
-
if (overlap > 0) {
|
|
6737
|
-
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
6738
|
-
}
|
|
6739
|
-
}
|
|
6740
|
-
return 0;
|
|
6741
|
-
}
|
|
6742
|
-
function listDuplicateCandidates(db, filters, options) {
|
|
6743
|
-
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
6744
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
6745
|
-
const subset = filters?.contactIds;
|
|
6746
|
-
const contacts = db.prepare(
|
|
6747
|
-
`SELECT id, name
|
|
6748
|
-
FROM contacts
|
|
6749
|
-
WHERE deleted_at IS NULL
|
|
6750
|
-
AND lifecycle_state != 'merged'`
|
|
6751
|
-
).all();
|
|
6752
|
-
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
6753
|
-
const pairs = /* @__PURE__ */ new Map();
|
|
6754
|
-
const exactMatches = db.prepare(
|
|
6755
|
-
`SELECT
|
|
6756
|
-
i1.contact_id AS left_contact_id,
|
|
6757
|
-
i2.contact_id AS right_contact_id,
|
|
6758
|
-
i1.normalized_key
|
|
6759
|
-
FROM identities i1
|
|
6760
|
-
INNER JOIN identities i2
|
|
6761
|
-
ON i1.normalized_key = i2.normalized_key
|
|
6762
|
-
AND i1.contact_id < i2.contact_id
|
|
6763
|
-
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
6764
|
-
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
6765
|
-
WHERE i1.removed_at IS NULL
|
|
6766
|
-
AND i2.removed_at IS NULL
|
|
6767
|
-
AND c1.deleted_at IS NULL
|
|
6768
|
-
AND c2.deleted_at IS NULL
|
|
6769
|
-
AND c1.lifecycle_state != 'merged'
|
|
6770
|
-
AND c2.lifecycle_state != 'merged'`
|
|
6771
|
-
).all();
|
|
6772
|
-
for (const match of exactMatches) {
|
|
6773
|
-
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
6774
|
-
continue;
|
|
6775
|
-
}
|
|
6776
|
-
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
6777
|
-
leftContactId: match.left_contact_id,
|
|
6778
|
-
rightContactId: match.right_contact_id,
|
|
6779
|
-
matchReason: `exact identity ${match.normalized_key}`,
|
|
6780
|
-
matchScore: 1
|
|
6781
|
-
});
|
|
6782
|
-
}
|
|
6783
|
-
if (filters?.exactOnly !== true) {
|
|
6784
|
-
for (let index = 0; index < contacts.length; index += 1) {
|
|
6785
|
-
const left = contacts[index];
|
|
6786
|
-
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
6787
|
-
continue;
|
|
6788
|
-
}
|
|
6789
|
-
const leftName = normalizeName(left.name);
|
|
6790
|
-
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
6791
|
-
const right = contacts[offsetIndex];
|
|
6792
|
-
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
6793
|
-
continue;
|
|
6794
|
-
}
|
|
6795
|
-
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
6796
|
-
if (score <= 0) {
|
|
6797
|
-
continue;
|
|
6798
|
-
}
|
|
6799
|
-
const key = pairKey(left.id, right.id);
|
|
6800
|
-
if (!pairs.has(key)) {
|
|
6801
|
-
pairs.set(key, {
|
|
6802
|
-
leftContactId: left.id,
|
|
6803
|
-
rightContactId: right.id,
|
|
6804
|
-
matchReason: "name similarity",
|
|
6805
|
-
matchScore: score
|
|
6806
|
-
});
|
|
6807
|
-
}
|
|
6808
|
-
}
|
|
6809
|
-
}
|
|
6810
|
-
}
|
|
6811
|
-
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
6812
|
-
const dismissedSet = new Set(
|
|
6813
|
-
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
6814
|
-
);
|
|
6815
|
-
const includeDismissed = filters?.includeDismissed === true;
|
|
6816
|
-
for (const candidate of pairs.values()) {
|
|
6817
|
-
if (dismissedSet.has(
|
|
6818
|
-
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
6819
|
-
)) {
|
|
6820
|
-
candidate.dismissed = true;
|
|
6821
|
-
}
|
|
6822
|
-
}
|
|
6823
|
-
const minScore = filters?.minScore ?? 0;
|
|
6824
|
-
return [...pairs.values()].filter((candidate) => {
|
|
6825
|
-
if (candidate.matchScore < minScore) return false;
|
|
6826
|
-
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
6827
|
-
return true;
|
|
6828
|
-
}).sort((left, right) => {
|
|
6829
|
-
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
6830
|
-
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
6831
|
-
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
6832
|
-
}).slice(offset, offset + limit);
|
|
7677
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
7678
|
+
clauses.push(...attributeFilters.clauses);
|
|
7679
|
+
params.push(...attributeFilters.params);
|
|
7680
|
+
const where = clauses.join(" AND ");
|
|
7681
|
+
const rows = db.prepare(
|
|
7682
|
+
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
7683
|
+
created_at, updated_at, deleted_at,
|
|
7684
|
+
(
|
|
7685
|
+
SELECT value
|
|
7686
|
+
FROM identities i
|
|
7687
|
+
WHERE i.contact_id = contacts.id AND i.removed_at IS NULL
|
|
7688
|
+
ORDER BY i.id
|
|
7689
|
+
LIMIT 1
|
|
7690
|
+
) AS primary_identity
|
|
7691
|
+
FROM contacts
|
|
7692
|
+
WHERE ${where}
|
|
7693
|
+
ORDER BY name ASC
|
|
7694
|
+
LIMIT ? OFFSET ?`
|
|
7695
|
+
).all(...params, limit, offset);
|
|
7696
|
+
return rows.map((r) => {
|
|
7697
|
+
const item = mapContactRowToContactListItem(r);
|
|
7698
|
+
return {
|
|
7699
|
+
...item,
|
|
7700
|
+
primaryIdentity: r.primary_identity ?? null
|
|
7701
|
+
};
|
|
7702
|
+
});
|
|
6833
7703
|
}
|
|
6834
7704
|
|
|
6835
7705
|
// src/dates/list_upcoming_dates.ts
|
|
@@ -6865,12 +7735,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
6865
7735
|
}
|
|
6866
7736
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
6867
7737
|
const params = [];
|
|
6868
|
-
const since = options?.since ??
|
|
7738
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
6869
7739
|
clauses.push("uo.occurs_on >= ?");
|
|
6870
7740
|
params.push(since);
|
|
6871
7741
|
let maxOccurs = options?.until;
|
|
6872
7742
|
if (filters?.horizonDays !== void 0) {
|
|
6873
|
-
const anchor =
|
|
7743
|
+
const anchor = since;
|
|
6874
7744
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
6875
7745
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
6876
7746
|
}
|
|
@@ -6917,6 +7787,68 @@ function listUpcomingDates(db, filters, options) {
|
|
|
6917
7787
|
return rows.map((r) => mapUpcomingRowToUpcomingDateRecord(r));
|
|
6918
7788
|
}
|
|
6919
7789
|
|
|
7790
|
+
// src/events/event_discovery.ts
|
|
7791
|
+
function searchEvents(db, query, options) {
|
|
7792
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
7793
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
7794
|
+
if (!tokens.length) return [];
|
|
7795
|
+
const rows = db.prepare(
|
|
7796
|
+
"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"
|
|
7797
|
+
).all();
|
|
7798
|
+
return rows.flatMap((row) => {
|
|
7799
|
+
const fields = {
|
|
7800
|
+
summary: decodeEventText(row.summary),
|
|
7801
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
7802
|
+
source: row.external_source,
|
|
7803
|
+
id: row.external_id,
|
|
7804
|
+
channel: row.channel
|
|
7805
|
+
};
|
|
7806
|
+
const folded = Object.entries(fields).map(
|
|
7807
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
7808
|
+
);
|
|
7809
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
7810
|
+
return [];
|
|
7811
|
+
return [
|
|
7812
|
+
{
|
|
7813
|
+
kind: "event",
|
|
7814
|
+
id: row.id,
|
|
7815
|
+
type: row.type,
|
|
7816
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
7817
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
7818
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
7819
|
+
...row.external_source && row.external_id ? {
|
|
7820
|
+
externalRef: {
|
|
7821
|
+
source: row.external_source,
|
|
7822
|
+
id: row.external_id
|
|
7823
|
+
}
|
|
7824
|
+
} : {}
|
|
7825
|
+
}
|
|
7826
|
+
];
|
|
7827
|
+
}).slice(offset, offset + limit);
|
|
7828
|
+
}
|
|
7829
|
+
function getEventThread(db, source, id, options) {
|
|
7830
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
7831
|
+
const root = getEventByExternalRef(db, source, id);
|
|
7832
|
+
if (!root) return [];
|
|
7833
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
7834
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
7835
|
+
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=?))
|
|
7836
|
+
) 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(
|
|
7837
|
+
root.id,
|
|
7838
|
+
root.externalRef?.source ?? null,
|
|
7839
|
+
root.externalRef?.id ?? null,
|
|
7840
|
+
options?.channel ?? null,
|
|
7841
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
7842
|
+
limit,
|
|
7843
|
+
offset
|
|
7844
|
+
);
|
|
7845
|
+
return batchLoadEventRecords(
|
|
7846
|
+
db,
|
|
7847
|
+
rows.map((r) => r.id),
|
|
7848
|
+
options?.includeContent ?? false
|
|
7849
|
+
);
|
|
7850
|
+
}
|
|
7851
|
+
|
|
6920
7852
|
// src/graph/get_affinity_chart.ts
|
|
6921
7853
|
function getAffinityChart(db, options) {
|
|
6922
7854
|
const contactClauses = [
|
|
@@ -7102,21 +8034,6 @@ function listProgressionReadiness(db, filters, options) {
|
|
|
7102
8034
|
}
|
|
7103
8035
|
|
|
7104
8036
|
// src/links/list_radar.ts
|
|
7105
|
-
function deriveRadarReason(drift, recency, normalizedRank3) {
|
|
7106
|
-
if (drift > 0.5) {
|
|
7107
|
-
return "Significantly overdue for contact";
|
|
7108
|
-
}
|
|
7109
|
-
if (drift > 0.2) {
|
|
7110
|
-
return "Drifting \u2014 approaching overdue";
|
|
7111
|
-
}
|
|
7112
|
-
if (recency < 0.3 && normalizedRank3 > 0.3) {
|
|
7113
|
-
return "Important link losing recency";
|
|
7114
|
-
}
|
|
7115
|
-
if (normalizedRank3 > 0.5 && recency < 0.5) {
|
|
7116
|
-
return "High-rank link needs attention";
|
|
7117
|
-
}
|
|
7118
|
-
return "Routine check-in recommended";
|
|
7119
|
-
}
|
|
7120
8037
|
function listRadar(db, filters, options) {
|
|
7121
8038
|
assertDefaultOrdering("listRadar", options);
|
|
7122
8039
|
const ownerId = findOwnerContactId(db);
|
|
@@ -7124,7 +8041,10 @@ function listRadar(db, filters, options) {
|
|
|
7124
8041
|
return [];
|
|
7125
8042
|
}
|
|
7126
8043
|
const now = resolveNow(options?.now);
|
|
7127
|
-
const
|
|
8044
|
+
const today = localCalendarDate(now);
|
|
8045
|
+
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}'))`;
|
|
8046
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
8047
|
+
const overdue = `EXISTS (SELECT 1 FROM open_commitments oc JOIN events e ON e.id = oc.event_id JOIN event_participants ep ON ep.event_id = e.id WHERE ep.contact_id = CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END AND e.deleted_at IS NULL AND oc.resolved_at IS NULL AND (CASE WHEN oc.due_on IS NOT NULL AND oc.due_at = CAST(strftime('%s', oc.due_on) AS INTEGER) * 1000 + 86399999 THEN oc.due_on < '${today}' ELSE oc.due_at < ${now} END))`;
|
|
7128
8048
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
7129
8049
|
const clauses = [
|
|
7130
8050
|
"l.removed_at IS NULL",
|
|
@@ -7149,10 +8069,23 @@ function listRadar(db, filters, options) {
|
|
|
7149
8069
|
if (options?.includeObserved === false) {
|
|
7150
8070
|
clauses.push("l.kind != 'observed'");
|
|
7151
8071
|
}
|
|
8072
|
+
if (options?.includeDormant !== true)
|
|
8073
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
8074
|
+
if (filters?.includeLost !== true)
|
|
8075
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
8076
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
8077
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
8078
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
8079
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
8080
|
+
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'))
|
|
8081
|
+
))`);
|
|
7152
8082
|
const where = clauses.join(" AND ");
|
|
7153
8083
|
const rows = db.prepare(
|
|
7154
8084
|
`SELECT
|
|
7155
8085
|
l.id,
|
|
8086
|
+
l.cadence_days,
|
|
8087
|
+
${lastContact} AS last_contact,
|
|
8088
|
+
${drift} AS drift,
|
|
7156
8089
|
${overdue} AS overdue_commitment,
|
|
7157
8090
|
l.from_contact_id,
|
|
7158
8091
|
l.to_contact_id,
|
|
@@ -7166,23 +8099,44 @@ function listRadar(db, filters, options) {
|
|
|
7166
8099
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
7167
8100
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
7168
8101
|
WHERE ${where}
|
|
7169
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
8102
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
7170
8103
|
LIMIT ? OFFSET ?`
|
|
7171
8104
|
).all(...params, limit, offset);
|
|
7172
|
-
return rows.map((row) =>
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
8105
|
+
return rows.map((row) => {
|
|
8106
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
8107
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
8108
|
+
`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`
|
|
8109
|
+
).get(contactId, today, now) : void 0;
|
|
8110
|
+
const reasons = [
|
|
8111
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
8112
|
+
...row.drift ? ["drift"] : []
|
|
8113
|
+
];
|
|
8114
|
+
const snippets = [
|
|
8115
|
+
...promise ? [
|
|
8116
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
8117
|
+
] : [],
|
|
8118
|
+
...row.drift ? [
|
|
8119
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
8120
|
+
] : []
|
|
8121
|
+
];
|
|
8122
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
8123
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
8124
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
8125
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
8126
|
+
trust: row.trust ?? 0,
|
|
8127
|
+
rank: row.rank ?? 0
|
|
8128
|
+
});
|
|
8129
|
+
return {
|
|
8130
|
+
linkId: row.id,
|
|
8131
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
8132
|
+
reasons,
|
|
8133
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
8134
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
8135
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
8136
|
+
trust: row.trust ?? 0,
|
|
8137
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
8138
|
+
};
|
|
8139
|
+
});
|
|
7186
8140
|
}
|
|
7187
8141
|
|
|
7188
8142
|
// src/merges/get_merge_history.ts
|
|
@@ -7252,13 +8206,40 @@ function enrichReviewItem(db, item) {
|
|
|
7252
8206
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
7253
8207
|
}
|
|
7254
8208
|
if (Array.isArray(item.participants)) {
|
|
7255
|
-
result.
|
|
7256
|
-
(p) =>
|
|
8209
|
+
result.participants = item.participants.map(
|
|
8210
|
+
(p) => ({
|
|
8211
|
+
id: p.contactId,
|
|
8212
|
+
name: name(p.contactId),
|
|
8213
|
+
role: p.role
|
|
8214
|
+
})
|
|
7257
8215
|
);
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
8216
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
8217
|
+
result[key] = void 0;
|
|
8218
|
+
}
|
|
8219
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
8220
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
8221
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
8222
|
+
if (result.title === result.summary) result.title = void 0;
|
|
8223
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
8224
|
+
for (const key of [
|
|
8225
|
+
"affinity",
|
|
8226
|
+
"trust",
|
|
8227
|
+
"rank",
|
|
8228
|
+
"recencyScore",
|
|
8229
|
+
"driftPriority",
|
|
8230
|
+
"readinessScore",
|
|
8231
|
+
"normalizedRank",
|
|
8232
|
+
"reciprocityScore",
|
|
8233
|
+
"radarScore",
|
|
8234
|
+
"subtitle",
|
|
8235
|
+
"snippet",
|
|
8236
|
+
"targetRef",
|
|
8237
|
+
"occursOnDate"
|
|
8238
|
+
])
|
|
8239
|
+
result[key] = void 0;
|
|
8240
|
+
} else if (item.kind === "link") {
|
|
8241
|
+
result.recommendedReason = void 0;
|
|
8242
|
+
result.snippet = void 0;
|
|
7262
8243
|
}
|
|
7263
8244
|
if (typeof item.leftContactId === "number")
|
|
7264
8245
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -7287,6 +8268,8 @@ function summarizeCount(count, singular) {
|
|
|
7287
8268
|
// src/tools/review_affinity_tool.ts
|
|
7288
8269
|
function buildReadOptions(input) {
|
|
7289
8270
|
return {
|
|
8271
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
8272
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
7290
8273
|
...input.now === void 0 ? {} : { now: input.now },
|
|
7291
8274
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7292
8275
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -7423,7 +8406,7 @@ function momentItem(moment) {
|
|
|
7423
8406
|
id: moment.eventId,
|
|
7424
8407
|
title: moment.summary,
|
|
7425
8408
|
subtitle: moment.momentKind,
|
|
7426
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8409
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
7427
8410
|
};
|
|
7428
8411
|
}
|
|
7429
8412
|
function commitmentItem(commitment) {
|
|
@@ -7433,7 +8416,7 @@ function commitmentItem(commitment) {
|
|
|
7433
8416
|
id: commitment.eventId,
|
|
7434
8417
|
title: commitment.summary,
|
|
7435
8418
|
subtitle: commitment.type,
|
|
7436
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8419
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
7437
8420
|
};
|
|
7438
8421
|
}
|
|
7439
8422
|
function radarItem(radar) {
|
|
@@ -7449,6 +8432,7 @@ function radarItem(radar) {
|
|
|
7449
8432
|
function upcomingDateItem(date) {
|
|
7450
8433
|
return {
|
|
7451
8434
|
...date,
|
|
8435
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
7452
8436
|
occursAt: date.occursOn,
|
|
7453
8437
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
7454
8438
|
kind: date.targetRef.kind,
|
|
@@ -7486,6 +8470,9 @@ function compactFilters(input) {
|
|
|
7486
8470
|
);
|
|
7487
8471
|
}
|
|
7488
8472
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
8473
|
+
return reviewPage(db, rawInput, false);
|
|
8474
|
+
}
|
|
8475
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
7489
8476
|
const input = structuredClone(rawInput);
|
|
7490
8477
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
7491
8478
|
if (inputError)
|
|
@@ -7500,6 +8487,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7500
8487
|
if (invalid) {
|
|
7501
8488
|
return invalid;
|
|
7502
8489
|
}
|
|
8490
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
8491
|
+
const limit = input.limit ?? 50;
|
|
8492
|
+
const offset = input.offset ?? 0;
|
|
8493
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
8494
|
+
return toolFailure(
|
|
8495
|
+
"protocol",
|
|
8496
|
+
"invalid_input",
|
|
8497
|
+
"Invalid pagination.",
|
|
8498
|
+
"limit must be 1..250; offset must be non-negative"
|
|
8499
|
+
);
|
|
8500
|
+
const all = [];
|
|
8501
|
+
let page;
|
|
8502
|
+
do {
|
|
8503
|
+
page = reviewPage(
|
|
8504
|
+
db,
|
|
8505
|
+
{ ...input, limit: 250, offset: all.length },
|
|
8506
|
+
true
|
|
8507
|
+
);
|
|
8508
|
+
if (!page.ok) return page;
|
|
8509
|
+
all.push(...page.data.items);
|
|
8510
|
+
} while (page.data.items.length === 250);
|
|
8511
|
+
const items2 = all.slice(offset, offset + limit);
|
|
8512
|
+
return toolSuccess(
|
|
8513
|
+
summarizeCount(items2.length, "item"),
|
|
8514
|
+
{
|
|
8515
|
+
view: input.view,
|
|
8516
|
+
items: items2,
|
|
8517
|
+
count: items2.length,
|
|
8518
|
+
total: all.length,
|
|
8519
|
+
hasMore: offset + items2.length < all.length,
|
|
8520
|
+
appliedFilters: compactFilters(input)
|
|
8521
|
+
},
|
|
8522
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
8523
|
+
);
|
|
8524
|
+
}
|
|
7503
8525
|
const options = buildReadOptions(input);
|
|
7504
8526
|
let items = [];
|
|
7505
8527
|
let chart;
|
|
@@ -7531,6 +8553,19 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7531
8553
|
options
|
|
7532
8554
|
).map(duplicateCandidateItem);
|
|
7533
8555
|
break;
|
|
8556
|
+
case "events.thread": {
|
|
8557
|
+
if (!input.ref)
|
|
8558
|
+
return toolFailure(
|
|
8559
|
+
"protocol",
|
|
8560
|
+
"invalid_input",
|
|
8561
|
+
"Missing thread reference.",
|
|
8562
|
+
"ref is required for events.thread."
|
|
8563
|
+
);
|
|
8564
|
+
items = getEventThread(db, input.ref.source, input.ref.id, options).map(
|
|
8565
|
+
toEventListToolItem
|
|
8566
|
+
);
|
|
8567
|
+
break;
|
|
8568
|
+
}
|
|
7534
8569
|
case "events.contact_journal": {
|
|
7535
8570
|
const locator = input.contact;
|
|
7536
8571
|
if (locator === void 0) {
|
|
@@ -7672,6 +8707,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7672
8707
|
items = listRadar(
|
|
7673
8708
|
db,
|
|
7674
8709
|
{
|
|
8710
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
7675
8711
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
7676
8712
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
7677
8713
|
},
|
|
@@ -7721,6 +8757,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7721
8757
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
7722
8758
|
break;
|
|
7723
8759
|
}
|
|
8760
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
8761
|
+
items = items.map((item) => {
|
|
8762
|
+
const content = getEventById(
|
|
8763
|
+
db,
|
|
8764
|
+
Number(item.eventId ?? item.id)
|
|
8765
|
+
)?.content;
|
|
8766
|
+
return content === void 0 ? item : { ...item, content };
|
|
8767
|
+
});
|
|
7724
8768
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
7725
8769
|
return toolSuccess(
|
|
7726
8770
|
summarizeCount(items.length, "item"),
|
|
@@ -7732,7 +8776,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
7732
8776
|
...chart === void 0 ? {} : { chart }
|
|
7733
8777
|
},
|
|
7734
8778
|
{
|
|
7735
|
-
entities: items,
|
|
8779
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
7736
8780
|
warnings: items.length === 0 ? [
|
|
7737
8781
|
toolWarning(
|
|
7738
8782
|
"empty_result",
|
|
@@ -7784,9 +8828,15 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7784
8828
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
7785
8829
|
inputSchema: objectSchema(
|
|
7786
8830
|
{
|
|
8831
|
+
ref: externalRefSchema,
|
|
8832
|
+
includeContent: booleanSchema(
|
|
8833
|
+
"Include verbatim content in event rows; defaults to false."
|
|
8834
|
+
),
|
|
8835
|
+
channel: stringSchema("Filter events by channel."),
|
|
7787
8836
|
view: enumSchema("Which review surface to return.", [
|
|
7788
8837
|
"contacts.list",
|
|
7789
8838
|
"contacts.duplicates",
|
|
8839
|
+
"events.thread",
|
|
7790
8840
|
"events.contact_journal",
|
|
7791
8841
|
"events.link_timeline",
|
|
7792
8842
|
"events.moments",
|
|
@@ -7841,7 +8891,20 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7841
8891
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
7842
8892
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
7843
8893
|
eventTypes: arraySchema(
|
|
7844
|
-
|
|
8894
|
+
enumSchema("Event type.", [
|
|
8895
|
+
"conversation",
|
|
8896
|
+
"activity",
|
|
8897
|
+
"gift",
|
|
8898
|
+
"support",
|
|
8899
|
+
"milestone",
|
|
8900
|
+
"observation",
|
|
8901
|
+
"conflict",
|
|
8902
|
+
"correction",
|
|
8903
|
+
"transaction",
|
|
8904
|
+
"promise",
|
|
8905
|
+
"agreement",
|
|
8906
|
+
"date_anchor"
|
|
8907
|
+
]),
|
|
7845
8908
|
"Optional event-type filter."
|
|
7846
8909
|
),
|
|
7847
8910
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -7881,7 +8944,7 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
7881
8944
|
|
|
7882
8945
|
// src/contacts/search_contacts.ts
|
|
7883
8946
|
function searchContacts(db, query, filters, options) {
|
|
7884
|
-
const q = foldSearchText(query.trim());
|
|
8947
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
7885
8948
|
if (!q) return [];
|
|
7886
8949
|
assertDefaultOrdering("searchContacts", options);
|
|
7887
8950
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -7901,6 +8964,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
7901
8964
|
const tokens = q.split(/\s+/);
|
|
7902
8965
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
7903
8966
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
8967
|
+
const suffix = digits.replace(/^0/, "");
|
|
7904
8968
|
const attributeFilters = contactAttributeClauses(filters);
|
|
7905
8969
|
const clauses = [
|
|
7906
8970
|
"deleted_at IS NULL",
|
|
@@ -7936,7 +9000,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
7936
9000
|
)
|
|
7937
9001
|
];
|
|
7938
9002
|
const phoneFields = phone ? values.filter(
|
|
7939
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
9003
|
+
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits) || field.source === "identity:phone" && suffix.length >= 6 && field.text.replace(/\D/g, "").endsWith(suffix)
|
|
7940
9004
|
) : [];
|
|
7941
9005
|
if (!phoneFields.length && !tokens.every(
|
|
7942
9006
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -7966,6 +9030,9 @@ function searchContacts(db, query, filters, options) {
|
|
|
7966
9030
|
|
|
7967
9031
|
// src/tools/search_affinity_tool.ts
|
|
7968
9032
|
function searchAffinityToolHandler(db, rawInput) {
|
|
9033
|
+
return searchPage(db, rawInput, false);
|
|
9034
|
+
}
|
|
9035
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
7969
9036
|
const input = structuredClone(rawInput);
|
|
7970
9037
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
7971
9038
|
if (inputError)
|
|
@@ -7976,11 +9043,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
7976
9043
|
inputError
|
|
7977
9044
|
);
|
|
7978
9045
|
return withToolHandling(() => {
|
|
9046
|
+
if (!pageOnly) {
|
|
9047
|
+
const limit = input.limit ?? 50;
|
|
9048
|
+
const offset = input.offset ?? 0;
|
|
9049
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
9050
|
+
return toolFailure(
|
|
9051
|
+
"protocol",
|
|
9052
|
+
"invalid_input",
|
|
9053
|
+
"Invalid pagination.",
|
|
9054
|
+
"limit must be 1..250; offset must be non-negative"
|
|
9055
|
+
);
|
|
9056
|
+
const all = [];
|
|
9057
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
9058
|
+
for (const scope of scopes) {
|
|
9059
|
+
let page;
|
|
9060
|
+
let nextOffset = 0;
|
|
9061
|
+
do {
|
|
9062
|
+
page = searchPage(
|
|
9063
|
+
db,
|
|
9064
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
9065
|
+
true
|
|
9066
|
+
);
|
|
9067
|
+
if (!page.ok) return page;
|
|
9068
|
+
all.push(...page.data.items);
|
|
9069
|
+
nextOffset += page.data.count;
|
|
9070
|
+
} while (page.data.count === 250);
|
|
9071
|
+
}
|
|
9072
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9073
|
+
return toolSuccess(
|
|
9074
|
+
summarizeCount(
|
|
9075
|
+
items2.length,
|
|
9076
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
9077
|
+
),
|
|
9078
|
+
{
|
|
9079
|
+
query: input.query,
|
|
9080
|
+
items: items2,
|
|
9081
|
+
count: items2.length,
|
|
9082
|
+
total: all.length,
|
|
9083
|
+
hasMore: offset + items2.length < all.length
|
|
9084
|
+
},
|
|
9085
|
+
{ entities: items2 }
|
|
9086
|
+
);
|
|
9087
|
+
}
|
|
7979
9088
|
const options = {
|
|
7980
9089
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7981
9090
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
7982
9091
|
};
|
|
7983
|
-
const rows = searchContacts(
|
|
9092
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
7984
9093
|
db,
|
|
7985
9094
|
input.query,
|
|
7986
9095
|
{
|
|
@@ -7994,9 +9103,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
7994
9103
|
},
|
|
7995
9104
|
options
|
|
7996
9105
|
);
|
|
7997
|
-
const items =
|
|
9106
|
+
const items = [
|
|
9107
|
+
...rows.map(toContactListToolItem),
|
|
9108
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
9109
|
+
];
|
|
7998
9110
|
return toolSuccess(
|
|
7999
|
-
summarizeCount(
|
|
9111
|
+
summarizeCount(
|
|
9112
|
+
items.length,
|
|
9113
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
9114
|
+
),
|
|
8000
9115
|
{
|
|
8001
9116
|
query: input.query,
|
|
8002
9117
|
items,
|
|
@@ -8032,6 +9147,11 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
8032
9147
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
8033
9148
|
inputSchema: objectSchema(
|
|
8034
9149
|
{
|
|
9150
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
9151
|
+
"contacts",
|
|
9152
|
+
"events",
|
|
9153
|
+
"all"
|
|
9154
|
+
]),
|
|
8035
9155
|
query: stringSchema("Free-text or natural-key query."),
|
|
8036
9156
|
kind: enumSchema("Optional contact kind filter.", [
|
|
8037
9157
|
"human",
|
|
@@ -8083,72 +9203,36 @@ var affinityTools = [
|
|
|
8083
9203
|
recordEventTool,
|
|
8084
9204
|
manageCommitmentTool,
|
|
8085
9205
|
manageDateAnchorTool,
|
|
8086
|
-
manageAttributeTool
|
|
9206
|
+
manageAttributeTool,
|
|
9207
|
+
manageEventTool
|
|
8087
9208
|
];
|
|
8088
9209
|
|
|
8089
9210
|
// src/surface/plain_dates.ts
|
|
8090
|
-
function
|
|
8091
|
-
const invalid = () => new CliUsageError(
|
|
8092
|
-
`Invalid timestamp "${raw}". Use YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d, -2d or an ISO date-time.`
|
|
8093
|
-
);
|
|
8094
|
-
if (/^-?\d+$/.test(raw)) {
|
|
8095
|
-
const value = Number(raw);
|
|
8096
|
-
if (Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()))
|
|
8097
|
-
return value;
|
|
8098
|
-
throw invalid();
|
|
8099
|
-
}
|
|
8100
|
-
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
8101
|
-
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw);
|
|
8102
|
-
let date;
|
|
8103
|
-
if (iso || european) {
|
|
8104
|
-
const year = Number(iso?.[1] ?? european?.[3]);
|
|
8105
|
-
const month = Number(iso?.[2] ?? european?.[2]);
|
|
8106
|
-
const day = Number(iso?.[3] ?? european?.[1]);
|
|
8107
|
-
date = /* @__PURE__ */ new Date(0);
|
|
8108
|
-
date.setFullYear(year, month - 1, day);
|
|
8109
|
-
date.setHours(
|
|
8110
|
-
endOfDay ? 23 : 0,
|
|
8111
|
-
endOfDay ? 59 : 0,
|
|
8112
|
-
endOfDay ? 59 : 0,
|
|
8113
|
-
endOfDay ? 999 : 0
|
|
8114
|
-
);
|
|
8115
|
-
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day)
|
|
8116
|
-
throw invalid();
|
|
8117
|
-
} else if (raw === "today" || raw === "tomorrow" || raw === "yesterday" || /^[+-]\d+d$/.test(raw)) {
|
|
8118
|
-
date = new Date(now);
|
|
8119
|
-
date.setDate(
|
|
8120
|
-
date.getDate() + (raw === "today" ? 0 : raw === "tomorrow" ? 1 : raw === "yesterday" ? -1 : Number(raw.slice(0, -1)))
|
|
8121
|
-
);
|
|
8122
|
-
date.setHours(
|
|
8123
|
-
endOfDay ? 23 : 0,
|
|
8124
|
-
endOfDay ? 59 : 0,
|
|
8125
|
-
endOfDay ? 59 : 0,
|
|
8126
|
-
endOfDay ? 999 : 0
|
|
8127
|
-
);
|
|
8128
|
-
} else {
|
|
8129
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
8130
|
-
raw
|
|
8131
|
-
))
|
|
8132
|
-
throw invalid();
|
|
8133
|
-
parsePlainDue(raw.slice(0, 10), now);
|
|
8134
|
-
date = new Date(raw);
|
|
8135
|
-
}
|
|
8136
|
-
if (!Number.isFinite(date.getTime())) throw invalid();
|
|
8137
|
-
return date.getTime();
|
|
8138
|
-
}
|
|
8139
|
-
function parsePlainStart(raw, now = Date.now()) {
|
|
8140
|
-
return parsePlainDue(raw, now, false);
|
|
8141
|
-
}
|
|
8142
|
-
function normalizeTimeInputs(value, now = Date.now()) {
|
|
9211
|
+
function normalizeTimeInputs(value, now = Date.now(), inPatch = false) {
|
|
8143
9212
|
if (Array.isArray(value))
|
|
8144
9213
|
return value.map((item) => normalizeTimeInputs(item, now));
|
|
8145
9214
|
if (!value || typeof value !== "object") return value;
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
9215
|
+
const result = {};
|
|
9216
|
+
for (const [key, item] of Object.entries(value)) {
|
|
9217
|
+
if (key === "occurredAt" && item === null)
|
|
9218
|
+
throw new CliUsageError(
|
|
9219
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
9220
|
+
);
|
|
9221
|
+
if (typeof item === "string" && (key.endsWith("At") || ["asOf", "now", "since", "until"].includes(key))) {
|
|
9222
|
+
let parsed;
|
|
9223
|
+
try {
|
|
9224
|
+
parsed = parseCalendarTime(item, key, now);
|
|
9225
|
+
} catch (error) {
|
|
9226
|
+
throw new CliUsageError(
|
|
9227
|
+
`Invalid timestamp "${item}". ${error.message}`
|
|
9228
|
+
);
|
|
9229
|
+
}
|
|
9230
|
+
result[key] = parsed.timestamp;
|
|
9231
|
+
if (!inPatch && ["occurredAt", "dueAt"].includes(key))
|
|
9232
|
+
result[key.replace(/At$/, "On")] = parsed.day;
|
|
9233
|
+
} else result[key] = normalizeTimeInputs(item, now, key === "patch");
|
|
9234
|
+
}
|
|
9235
|
+
return result;
|
|
8152
9236
|
}
|
|
8153
9237
|
|
|
8154
9238
|
// src/surface/timestamp_schema.ts
|
|
@@ -8175,20 +9259,57 @@ function timestampSchema(schema) {
|
|
|
8175
9259
|
}
|
|
8176
9260
|
|
|
8177
9261
|
// src/surface/affinity_surface_tools.ts
|
|
9262
|
+
function surfaceProperties(tool) {
|
|
9263
|
+
return {
|
|
9264
|
+
...tool.inputSchema.properties,
|
|
9265
|
+
...["inspect_affinity_item", "review_affinity", "manage_event"].includes(
|
|
9266
|
+
tool.name
|
|
9267
|
+
) ? {
|
|
9268
|
+
refSource: {
|
|
9269
|
+
type: "string",
|
|
9270
|
+
description: "External reference source."
|
|
9271
|
+
},
|
|
9272
|
+
refId: {
|
|
9273
|
+
type: "string",
|
|
9274
|
+
description: "External reference id."
|
|
9275
|
+
}
|
|
9276
|
+
} : {},
|
|
9277
|
+
...tool.name === "review_affinity" ? {
|
|
9278
|
+
withContent: {
|
|
9279
|
+
type: "boolean",
|
|
9280
|
+
description: "Include verbatim event content."
|
|
9281
|
+
}
|
|
9282
|
+
} : {}
|
|
9283
|
+
};
|
|
9284
|
+
}
|
|
8178
9285
|
var affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
8179
9286
|
name: tool.name,
|
|
8180
9287
|
description: tool.description,
|
|
8181
|
-
whenToUse: tool.whenToUse,
|
|
8182
|
-
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
8183
9288
|
readOnly: tool.readOnly,
|
|
8184
9289
|
sideEffects: tool.sideEffects,
|
|
8185
9290
|
inputSchema: {
|
|
8186
|
-
...timestampSchema({
|
|
9291
|
+
...timestampSchema({
|
|
9292
|
+
...tool.inputSchema,
|
|
9293
|
+
properties: surfaceProperties(tool),
|
|
9294
|
+
type: "object"
|
|
9295
|
+
}),
|
|
8187
9296
|
type: "object"
|
|
8188
9297
|
},
|
|
8189
9298
|
handler: (db, input) => {
|
|
8190
9299
|
try {
|
|
8191
|
-
|
|
9300
|
+
const { withContent, refSource, refId, ...normalized } = input;
|
|
9301
|
+
if (withContent !== void 0) normalized.includeContent = withContent;
|
|
9302
|
+
if (refSource !== void 0 || refId !== void 0) {
|
|
9303
|
+
if (normalized.ref !== void 0)
|
|
9304
|
+
return toolFailure(
|
|
9305
|
+
"protocol",
|
|
9306
|
+
"invalid_input",
|
|
9307
|
+
"Invalid target.",
|
|
9308
|
+
"Provide ref or ref-source/ref-id, not both."
|
|
9309
|
+
);
|
|
9310
|
+
normalized.ref = { source: refSource, id: refId };
|
|
9311
|
+
}
|
|
9312
|
+
return tool.handler(db, normalizeTimeInputs(normalized));
|
|
8192
9313
|
} catch (error) {
|
|
8193
9314
|
if (error instanceof CliUsageError)
|
|
8194
9315
|
return toolFailure(
|
|
@@ -8213,7 +9334,7 @@ function affinityMcpOptions(context) {
|
|
|
8213
9334
|
context,
|
|
8214
9335
|
annotations: (tool) => ({
|
|
8215
9336
|
readOnlyHint: tool.readOnly ?? false,
|
|
8216
|
-
destructiveHint: tool.name === "merge_contacts" || /remove|unset|undismiss/.test(
|
|
9337
|
+
destructiveHint: tool.name === "merge_contacts" || tool.name === "manage_event" || /remove|unset|undismiss/.test(
|
|
8217
9338
|
String(tool.inputSchema.properties?.action?.enum ?? [])
|
|
8218
9339
|
),
|
|
8219
9340
|
idempotentHint: tool.readOnly ?? false,
|
|
@@ -8222,13 +9343,95 @@ function affinityMcpOptions(context) {
|
|
|
8222
9343
|
};
|
|
8223
9344
|
}
|
|
8224
9345
|
|
|
9346
|
+
// src/events/init_events_tables.ts
|
|
9347
|
+
function initEventsTables(db) {
|
|
9348
|
+
db.exec(`
|
|
9349
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9350
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9351
|
+
type TEXT NOT NULL,
|
|
9352
|
+
occurred_at INTEGER NOT NULL,
|
|
9353
|
+
summary TEXT NOT NULL,
|
|
9354
|
+
significance REAL NOT NULL DEFAULT 0,
|
|
9355
|
+
moment_kind TEXT,
|
|
9356
|
+
recurrence_kind TEXT,
|
|
9357
|
+
anchor_month INTEGER,
|
|
9358
|
+
anchor_day INTEGER,
|
|
9359
|
+
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
9360
|
+
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
9361
|
+
created_at INTEGER NOT NULL,
|
|
9362
|
+
updated_at INTEGER NOT NULL,
|
|
9363
|
+
deleted_at INTEGER,
|
|
9364
|
+
CHECK (length(trim(summary)) > 0),
|
|
9365
|
+
CHECK (
|
|
9366
|
+
type IN (
|
|
9367
|
+
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
9368
|
+
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
9369
|
+
)
|
|
9370
|
+
),
|
|
9371
|
+
CHECK (
|
|
9372
|
+
(type = 'date_anchor' AND (
|
|
9373
|
+
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
9374
|
+
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
9375
|
+
))
|
|
9376
|
+
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
9377
|
+
),
|
|
9378
|
+
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
9379
|
+
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
9380
|
+
)),
|
|
9381
|
+
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
9382
|
+
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
9383
|
+
)),
|
|
9384
|
+
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
9385
|
+
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
9386
|
+
)
|
|
9387
|
+
`);
|
|
9388
|
+
db.exec(
|
|
9389
|
+
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
9390
|
+
);
|
|
9391
|
+
db.exec(
|
|
9392
|
+
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
9393
|
+
);
|
|
9394
|
+
const columns = db.prepare("PRAGMA table_info(events)").all();
|
|
9395
|
+
if (!columns.some((c) => c.name === "declared_moment_kind"))
|
|
9396
|
+
db.exec("ALTER TABLE events ADD COLUMN declared_moment_kind TEXT");
|
|
9397
|
+
if (!columns.some((c) => c.name === "affects_bond"))
|
|
9398
|
+
db.exec("ALTER TABLE events ADD COLUMN affects_bond INTEGER");
|
|
9399
|
+
if (!columns.some((c) => c.name === "content"))
|
|
9400
|
+
db.exec("ALTER TABLE events ADD COLUMN content TEXT");
|
|
9401
|
+
if (!columns.some((c) => c.name === "external_source"))
|
|
9402
|
+
db.exec("ALTER TABLE events ADD COLUMN external_source TEXT");
|
|
9403
|
+
if (!columns.some((c) => c.name === "external_id"))
|
|
9404
|
+
db.exec("ALTER TABLE events ADD COLUMN external_id TEXT");
|
|
9405
|
+
if (!columns.some((c) => c.name === "channel"))
|
|
9406
|
+
db.exec("ALTER TABLE events ADD COLUMN channel TEXT");
|
|
9407
|
+
if (!columns.some((c) => c.name === "parent_source"))
|
|
9408
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_source TEXT");
|
|
9409
|
+
if (!columns.some((c) => c.name === "parent_ref_id"))
|
|
9410
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_ref_id TEXT");
|
|
9411
|
+
if (!columns.some((c) => c.name === "parent_event_id"))
|
|
9412
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_event_id INTEGER");
|
|
9413
|
+
if (!columns.some((c) => c.name === "thread_source"))
|
|
9414
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_source TEXT");
|
|
9415
|
+
if (!columns.some((c) => c.name === "thread_id"))
|
|
9416
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_id TEXT");
|
|
9417
|
+
if (!columns.some((c) => c.name === "provenance_json"))
|
|
9418
|
+
db.exec("ALTER TABLE events ADD COLUMN provenance_json TEXT");
|
|
9419
|
+
db.exec(
|
|
9420
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external_ref ON events(external_source, external_id)"
|
|
9421
|
+
);
|
|
9422
|
+
if (!columns.some((column) => column.name === "occurred_on"))
|
|
9423
|
+
db.exec("ALTER TABLE events ADD COLUMN occurred_on TEXT");
|
|
9424
|
+
}
|
|
9425
|
+
|
|
8225
9426
|
// src/attributes/init_attributes_tables.ts
|
|
8226
9427
|
function initAttributesTables(db) {
|
|
9428
|
+
initEventsTables(db);
|
|
8227
9429
|
db.exec(`
|
|
8228
9430
|
CREATE TABLE IF NOT EXISTS attributes (
|
|
8229
9431
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8230
9432
|
contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
8231
9433
|
link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
9434
|
+
event_id INTEGER REFERENCES events (id) ON DELETE CASCADE,
|
|
8232
9435
|
name TEXT NOT NULL,
|
|
8233
9436
|
value TEXT,
|
|
8234
9437
|
created_at INTEGER NOT NULL,
|
|
@@ -8236,15 +9439,35 @@ function initAttributesTables(db) {
|
|
|
8236
9439
|
deleted_at INTEGER,
|
|
8237
9440
|
CHECK (length(trim(name)) > 0),
|
|
8238
9441
|
CHECK (
|
|
8239
|
-
(contact_id IS NOT NULL
|
|
8240
|
-
OR
|
|
8241
|
-
(contact_id IS NULL AND link_id IS NOT NULL)
|
|
9442
|
+
(contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1
|
|
8242
9443
|
)
|
|
8243
9444
|
)
|
|
8244
9445
|
`);
|
|
9446
|
+
const columns = db.prepare("PRAGMA table_info(attributes)").all();
|
|
9447
|
+
if (!columns.some((c) => c.name === "event_id")) {
|
|
9448
|
+
withTransaction(
|
|
9449
|
+
db,
|
|
9450
|
+
() => db.exec(`CREATE TABLE attributes_upgrade (
|
|
9451
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9452
|
+
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
|
|
9453
|
+
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
|
|
9454
|
+
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
|
|
9455
|
+
name TEXT NOT NULL CHECK(length(trim(name)) > 0), value TEXT,
|
|
9456
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
9457
|
+
CHECK ((contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1)
|
|
9458
|
+
);
|
|
9459
|
+
INSERT INTO attributes_upgrade (id,contact_id,link_id,name,value,created_at,updated_at,deleted_at)
|
|
9460
|
+
SELECT id,contact_id,link_id,name,value,created_at,updated_at,deleted_at FROM attributes;
|
|
9461
|
+
DROP TABLE attributes;
|
|
9462
|
+
ALTER TABLE attributes_upgrade RENAME TO attributes;`)
|
|
9463
|
+
);
|
|
9464
|
+
}
|
|
8245
9465
|
db.exec(
|
|
8246
9466
|
"CREATE INDEX IF NOT EXISTS idx_attributes_contact ON attributes(contact_id) WHERE deleted_at IS NULL"
|
|
8247
9467
|
);
|
|
9468
|
+
db.exec(
|
|
9469
|
+
"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"
|
|
9470
|
+
);
|
|
8248
9471
|
db.exec(
|
|
8249
9472
|
"CREATE INDEX IF NOT EXISTS idx_attributes_link ON attributes(link_id) WHERE deleted_at IS NULL"
|
|
8250
9473
|
);
|
|
@@ -8357,56 +9580,6 @@ function initEventParticipantsTables(db) {
|
|
|
8357
9580
|
);
|
|
8358
9581
|
}
|
|
8359
9582
|
|
|
8360
|
-
// src/events/init_events_tables.ts
|
|
8361
|
-
function initEventsTables(db) {
|
|
8362
|
-
db.exec(`
|
|
8363
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
8364
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8365
|
-
type TEXT NOT NULL,
|
|
8366
|
-
occurred_at INTEGER NOT NULL,
|
|
8367
|
-
summary TEXT NOT NULL,
|
|
8368
|
-
significance REAL NOT NULL DEFAULT 0,
|
|
8369
|
-
moment_kind TEXT,
|
|
8370
|
-
recurrence_kind TEXT,
|
|
8371
|
-
anchor_month INTEGER,
|
|
8372
|
-
anchor_day INTEGER,
|
|
8373
|
-
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
8374
|
-
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
8375
|
-
created_at INTEGER NOT NULL,
|
|
8376
|
-
updated_at INTEGER NOT NULL,
|
|
8377
|
-
deleted_at INTEGER,
|
|
8378
|
-
CHECK (length(trim(summary)) > 0),
|
|
8379
|
-
CHECK (
|
|
8380
|
-
type IN (
|
|
8381
|
-
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
8382
|
-
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
8383
|
-
)
|
|
8384
|
-
),
|
|
8385
|
-
CHECK (
|
|
8386
|
-
(type = 'date_anchor' AND (
|
|
8387
|
-
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
8388
|
-
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
8389
|
-
))
|
|
8390
|
-
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
8391
|
-
),
|
|
8392
|
-
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
8393
|
-
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
8394
|
-
)),
|
|
8395
|
-
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
8396
|
-
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
8397
|
-
)),
|
|
8398
|
-
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
8399
|
-
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
8400
|
-
)
|
|
8401
|
-
`);
|
|
8402
|
-
db.exec(
|
|
8403
|
-
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
8404
|
-
);
|
|
8405
|
-
db.exec(
|
|
8406
|
-
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
8407
|
-
);
|
|
8408
|
-
}
|
|
8409
|
-
|
|
8410
9583
|
// src/events/init_open_commitments_tables.ts
|
|
8411
9584
|
function initOpenCommitmentsTables(db) {
|
|
8412
9585
|
db.exec(`
|
|
@@ -8426,6 +9599,9 @@ function initOpenCommitmentsTables(db) {
|
|
|
8426
9599
|
)
|
|
8427
9600
|
)
|
|
8428
9601
|
`);
|
|
9602
|
+
const columns = db.prepare("PRAGMA table_info(open_commitments)").all();
|
|
9603
|
+
if (!columns.some((column) => column.name === "due_on"))
|
|
9604
|
+
db.exec("ALTER TABLE open_commitments ADD COLUMN due_on TEXT");
|
|
8429
9605
|
}
|
|
8430
9606
|
|
|
8431
9607
|
// src/identities/init_identities_tables.ts
|
|
@@ -8490,6 +9666,21 @@ function initLinkEventEffectsTables(db) {
|
|
|
8490
9666
|
db.exec(
|
|
8491
9667
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
8492
9668
|
);
|
|
9669
|
+
for (const name of ["participant_json", "provenance_json"])
|
|
9670
|
+
if (!columns.some((c) => c.name === name))
|
|
9671
|
+
db.exec(`ALTER TABLE link_event_effects ADD COLUMN ${name} TEXT`);
|
|
9672
|
+
db.exec(
|
|
9673
|
+
"CREATE TABLE IF NOT EXISTS event_link_baselines (link_id INTEGER PRIMARY KEY REFERENCES links(id) ON DELETE CASCADE, state_json TEXT NOT NULL)"
|
|
9674
|
+
);
|
|
9675
|
+
db.exec(
|
|
9676
|
+
"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)"
|
|
9677
|
+
);
|
|
9678
|
+
db.exec(`INSERT OR IGNORE INTO event_link_baselines(link_id,state_json)
|
|
9679
|
+
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)
|
|
9680
|
+
FROM links l JOIN link_event_effects e ON e.link_id=l.id GROUP BY l.id`);
|
|
9681
|
+
db.exec(
|
|
9682
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id IN (SELECT id FROM events WHERE type IN ('promise', 'agreement'))"
|
|
9683
|
+
);
|
|
8493
9684
|
db.exec(
|
|
8494
9685
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_moment ON link_event_effects(moment_kind, occurred_at DESC, impact_score DESC) WHERE moment_kind IS NOT NULL"
|
|
8495
9686
|
);
|
|
@@ -8665,7 +9856,10 @@ async function openAffinityDb(path) {
|
|
|
8665
9856
|
} finally {
|
|
8666
9857
|
process.emitWarning = original;
|
|
8667
9858
|
}
|
|
8668
|
-
if (path !== ":memory:"
|
|
9859
|
+
if (path !== ":memory:" && !path.startsWith("file:")) {
|
|
9860
|
+
node_fs.mkdirSync(node_path.dirname(path), { recursive: true, mode: 448 });
|
|
9861
|
+
if (!node_fs.existsSync(path)) node_fs.closeSync(node_fs.openSync(path, "wx", 384));
|
|
9862
|
+
}
|
|
8669
9863
|
const db = new sqlite.DatabaseSync(path);
|
|
8670
9864
|
try {
|
|
8671
9865
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
|
|
@@ -8689,6 +9883,13 @@ function isErrorResult(result) {
|
|
|
8689
9883
|
|
|
8690
9884
|
// src/surfaces/validate_input.ts
|
|
8691
9885
|
function validateInput(schema, input, path = "$") {
|
|
9886
|
+
if (path.endsWith(".occurredAt") && input == null)
|
|
9887
|
+
return ["occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"];
|
|
9888
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
9889
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && input !== null && typeof input !== "string")
|
|
9890
|
+
return [
|
|
9891
|
+
`attribute values are strings or null; pass "${String(input)}" to store it as text`
|
|
9892
|
+
];
|
|
8692
9893
|
const errors = [];
|
|
8693
9894
|
const matchesType = (type) => {
|
|
8694
9895
|
switch (type) {
|
|
@@ -8719,6 +9920,10 @@ function validateInput(schema, input, path = "$") {
|
|
|
8719
9920
|
(branch) => validateInput(branch, input, path).length === 0
|
|
8720
9921
|
))
|
|
8721
9922
|
errors.push(`${path}: must match at least one schema branch`);
|
|
9923
|
+
if (schema.anyOf && !schema.anyOf.some(
|
|
9924
|
+
(branch) => validateInput(branch, input, path).length === 0
|
|
9925
|
+
))
|
|
9926
|
+
errors.push(`${path}: must match at least one schema branch`);
|
|
8722
9927
|
if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
8723
9928
|
const record = input;
|
|
8724
9929
|
for (const name of schema.required ?? [])
|