@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-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, existsSync, closeSync, openSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
|
|
@@ -29,6 +29,13 @@ var init_surface_types = __esm({
|
|
|
29
29
|
|
|
30
30
|
// src/surfaces/validate_input.ts
|
|
31
31
|
function validateInput(schema, input, path = "$") {
|
|
32
|
+
if (path.endsWith(".occurredAt") && input == null)
|
|
33
|
+
return ["occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"];
|
|
34
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
35
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && input !== null && typeof input !== "string")
|
|
36
|
+
return [
|
|
37
|
+
`attribute values are strings or null; pass "${String(input)}" to store it as text`
|
|
38
|
+
];
|
|
32
39
|
const errors = [];
|
|
33
40
|
const matchesType = (type) => {
|
|
34
41
|
switch (type) {
|
|
@@ -59,6 +66,10 @@ function validateInput(schema, input, path = "$") {
|
|
|
59
66
|
(branch) => validateInput(branch, input, path).length === 0
|
|
60
67
|
))
|
|
61
68
|
errors.push(`${path}: must match at least one schema branch`);
|
|
69
|
+
if (schema.anyOf && !schema.anyOf.some(
|
|
70
|
+
(branch) => validateInput(branch, input, path).length === 0
|
|
71
|
+
))
|
|
72
|
+
errors.push(`${path}: must match at least one schema branch`);
|
|
62
73
|
if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
63
74
|
const record = input;
|
|
64
75
|
for (const name of schema.required ?? [])
|
|
@@ -285,7 +296,7 @@ var init_mcp_stdio = __esm({
|
|
|
285
296
|
var affinityVersion;
|
|
286
297
|
var init_version = __esm({
|
|
287
298
|
"src/version.ts"() {
|
|
288
|
-
affinityVersion = "0.
|
|
299
|
+
affinityVersion = "0.6.0";
|
|
289
300
|
}
|
|
290
301
|
});
|
|
291
302
|
|
|
@@ -415,6 +426,174 @@ var init_affinity_not_found_error = __esm({
|
|
|
415
426
|
}
|
|
416
427
|
});
|
|
417
428
|
|
|
429
|
+
// src/lib/normalize_mutation_receipt.ts
|
|
430
|
+
function uniqueEntityRefs(refs) {
|
|
431
|
+
const out = [];
|
|
432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
433
|
+
for (const ref of refs ?? []) {
|
|
434
|
+
const key = `${ref.kind}:${ref.id}`;
|
|
435
|
+
if (seen.has(key)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
seen.add(key);
|
|
439
|
+
out.push(ref);
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
function uniqueDerivedEffects(effects) {
|
|
444
|
+
const out = [];
|
|
445
|
+
const seen = /* @__PURE__ */ new Set();
|
|
446
|
+
for (const effect of effects ?? []) {
|
|
447
|
+
const key = `${effect.eventId}:${effect.linkId}`;
|
|
448
|
+
if (seen.has(key)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
seen.add(key);
|
|
452
|
+
out.push(effect);
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
function uniqueAffectedLinks(affectedLinks, derivedEffects) {
|
|
457
|
+
const out = [];
|
|
458
|
+
const seen = /* @__PURE__ */ new Set();
|
|
459
|
+
for (const linkId of affectedLinks ?? []) {
|
|
460
|
+
if (seen.has(linkId)) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
seen.add(linkId);
|
|
464
|
+
out.push(linkId);
|
|
465
|
+
}
|
|
466
|
+
for (const effect of derivedEffects) {
|
|
467
|
+
if (seen.has(effect.linkId)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
seen.add(effect.linkId);
|
|
471
|
+
out.push(effect.linkId);
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
function normalizeMutationReceiptPartial(partial) {
|
|
476
|
+
const derivedEffects = uniqueDerivedEffects(partial?.derivedEffects);
|
|
477
|
+
return {
|
|
478
|
+
created: uniqueEntityRefs(partial?.created),
|
|
479
|
+
updated: uniqueEntityRefs(partial?.updated),
|
|
480
|
+
archived: uniqueEntityRefs(partial?.archived),
|
|
481
|
+
removed: uniqueEntityRefs(partial?.removed),
|
|
482
|
+
affectedLinks: uniqueAffectedLinks(partial?.affectedLinks, derivedEffects),
|
|
483
|
+
derivedEffects
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
var init_normalize_mutation_receipt = __esm({
|
|
487
|
+
"src/lib/normalize_mutation_receipt.ts"() {
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// src/events/receipts.ts
|
|
492
|
+
function buildEventMutationReceipt(primary, partial) {
|
|
493
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
494
|
+
return {
|
|
495
|
+
primary,
|
|
496
|
+
...partial?.warnings ? { warnings: partial.warnings } : {},
|
|
497
|
+
created: normalized.created,
|
|
498
|
+
updated: normalized.updated,
|
|
499
|
+
archived: normalized.archived,
|
|
500
|
+
removed: normalized.removed,
|
|
501
|
+
affectedLinks: normalized.affectedLinks,
|
|
502
|
+
derivedEffects: normalized.derivedEffects
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
var init_receipts = __esm({
|
|
506
|
+
"src/events/receipts.ts"() {
|
|
507
|
+
init_normalize_mutation_receipt();
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// src/events/event_metadata.ts
|
|
512
|
+
function normalizeExternalRef(ref) {
|
|
513
|
+
if (!ref || typeof ref.source !== "string" || typeof ref.id !== "string" || !ref.source.trim() || !ref.id.trim())
|
|
514
|
+
throw new AffinityValidationError(
|
|
515
|
+
"external reference source and id must be non-empty strings"
|
|
516
|
+
);
|
|
517
|
+
return { source: ref.source.trim().toLowerCase(), id: ref.id.trim() };
|
|
518
|
+
}
|
|
519
|
+
function deriveEventSummary(content) {
|
|
520
|
+
const line = content.split(/\r?\n/).find((line2) => line2.trim())?.replace(/\s+/g, " ").trim() ?? "(no text)";
|
|
521
|
+
return line.length > 140 ? `${line.slice(0, 139)}\u2026` : line;
|
|
522
|
+
}
|
|
523
|
+
function validateEventMetadata(input) {
|
|
524
|
+
if (input.affectsBond !== void 0 && typeof input.affectsBond !== "boolean")
|
|
525
|
+
throw new AffinityValidationError("affectsBond must be boolean");
|
|
526
|
+
if (input.content !== void 0 && (typeof input.content !== "string" || Buffer.byteLength(input.content, "utf8") > 1048576))
|
|
527
|
+
throw new AffinityValidationError(
|
|
528
|
+
"content must be a string of at most 1 MB (1048576 UTF-8 bytes)"
|
|
529
|
+
);
|
|
530
|
+
for (const key of ["externalRef", "parentRef", "threadRef"])
|
|
531
|
+
if (input[key] !== void 0) input[key] = normalizeExternalRef(input[key]);
|
|
532
|
+
if (input.channel !== void 0) {
|
|
533
|
+
if (typeof input.channel !== "string" || !input.channel.trim() || input.channel.trim().length > 40)
|
|
534
|
+
throw new AffinityValidationError(
|
|
535
|
+
"channel must be non-empty and at most 40 characters"
|
|
536
|
+
);
|
|
537
|
+
input.channel = input.channel.trim().toLowerCase();
|
|
538
|
+
}
|
|
539
|
+
if (input.parentEventId !== void 0 && (!Number.isSafeInteger(input.parentEventId) || input.parentEventId <= 0))
|
|
540
|
+
throw new AffinityValidationError(
|
|
541
|
+
"parentEventId must be a positive integer"
|
|
542
|
+
);
|
|
543
|
+
const moments = [
|
|
544
|
+
"breakthrough",
|
|
545
|
+
"rupture",
|
|
546
|
+
"reconciliation",
|
|
547
|
+
"milestone",
|
|
548
|
+
"turning_point"
|
|
549
|
+
];
|
|
550
|
+
if (input.momentKind !== void 0 && !moments.includes(input.momentKind))
|
|
551
|
+
throw new AffinityValidationError(
|
|
552
|
+
`momentKind must be one of: ${moments.join(", ")}`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
function getEventById(db, eventId) {
|
|
556
|
+
return db.prepare("SELECT 1 FROM events WHERE id=? AND deleted_at IS NULL").get(eventId) ? loadEventRecord(db, eventId) : null;
|
|
557
|
+
}
|
|
558
|
+
function getEventByExternalRef(db, source, id) {
|
|
559
|
+
const ref = normalizeExternalRef({ source, id });
|
|
560
|
+
const row = db.prepare(
|
|
561
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
562
|
+
).get(ref.source, ref.id);
|
|
563
|
+
return row ? loadEventRecord(db, row.id) : null;
|
|
564
|
+
}
|
|
565
|
+
function existingEventReceipt(db, ref) {
|
|
566
|
+
const event = ref && getEventByExternalRef(db, ref.source, ref.id);
|
|
567
|
+
return event ? { ...buildEventMutationReceipt(event, {}), outcome: "no_op" } : null;
|
|
568
|
+
}
|
|
569
|
+
function enrichEventMetadata(db, record) {
|
|
570
|
+
const bond = db.prepare("SELECT affects_bond FROM events WHERE id=?").get(record.id);
|
|
571
|
+
record.affectsBond = bond?.affects_bond == null ? !(record.type === "observation" && record.significance <= 2) : Boolean(bond.affects_bond);
|
|
572
|
+
if (record.parentEventId === void 0 && record.parentRef) {
|
|
573
|
+
const row = db.prepare(
|
|
574
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
575
|
+
).get(record.parentRef.source, record.parentRef.id);
|
|
576
|
+
if (row) record.parentEventId = row.id;
|
|
577
|
+
}
|
|
578
|
+
const attrs = db.prepare(
|
|
579
|
+
"SELECT id, name, value FROM attributes WHERE event_id=? AND deleted_at IS NULL ORDER BY id"
|
|
580
|
+
).all(record.id);
|
|
581
|
+
if (attrs.length)
|
|
582
|
+
record.attributes = attrs.map((a) => ({
|
|
583
|
+
...a,
|
|
584
|
+
contactId: null,
|
|
585
|
+
linkId: null,
|
|
586
|
+
eventId: record.id
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
var init_event_metadata = __esm({
|
|
590
|
+
"src/events/event_metadata.ts"() {
|
|
591
|
+
init_affinity_validation_error();
|
|
592
|
+
init_loaders();
|
|
593
|
+
init_receipts();
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
418
597
|
// src/contacts/queries.ts
|
|
419
598
|
function getContactRowById(db, id) {
|
|
420
599
|
const row = db.prepare(
|
|
@@ -484,8 +663,11 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
484
663
|
}
|
|
485
664
|
function validateSocialEventInput(input) {
|
|
486
665
|
assertFiniteTimestamp(input.occurredAt, "occurredAt");
|
|
487
|
-
|
|
488
|
-
if (summary.
|
|
666
|
+
validateEventMetadata(input);
|
|
667
|
+
if (input.summary !== void 0 && typeof input.summary !== "string")
|
|
668
|
+
throw new AffinityValidationError("summary must be a string");
|
|
669
|
+
const summary = input.summary?.trim();
|
|
670
|
+
if (summary === "" || summary === void 0 && input.content === void 0) {
|
|
489
671
|
throw new AffinityValidationError("summary must be non-empty");
|
|
490
672
|
}
|
|
491
673
|
const sig = input.significance;
|
|
@@ -548,6 +730,7 @@ var init_validators = __esm({
|
|
|
548
730
|
init_affinity_not_found_error();
|
|
549
731
|
init_affinity_state_error();
|
|
550
732
|
init_affinity_validation_error();
|
|
733
|
+
init_event_metadata();
|
|
551
734
|
PARTICIPANT_ROLES = /* @__PURE__ */ new Set([
|
|
552
735
|
"actor",
|
|
553
736
|
"recipient",
|
|
@@ -578,11 +761,25 @@ function mapEventRowToEventRecord(row, participants) {
|
|
|
578
761
|
id: row.id,
|
|
579
762
|
type: row.type,
|
|
580
763
|
occurredAt: row.occurred_at,
|
|
581
|
-
|
|
764
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
765
|
+
summary: decodeEventText(row.summary),
|
|
582
766
|
significance: row.significance,
|
|
583
767
|
momentKind: row.moment_kind,
|
|
584
768
|
participants
|
|
585
769
|
};
|
|
770
|
+
if (row.content != null) base.content = decodeEventText(row.content);
|
|
771
|
+
if (row.channel != null) base.channel = row.channel;
|
|
772
|
+
if (row.external_source != null && row.external_id != null)
|
|
773
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
774
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
775
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
776
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
777
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
778
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
779
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
780
|
+
base.dueAt = row.due_at ?? null;
|
|
781
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
782
|
+
}
|
|
586
783
|
if (row.recurrence_kind !== null) {
|
|
587
784
|
base.recurrenceKind = row.recurrence_kind;
|
|
588
785
|
base.anchorMonth = row.anchor_month;
|
|
@@ -612,10 +809,14 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
612
809
|
targetRef,
|
|
613
810
|
recurrenceKind,
|
|
614
811
|
occursOn: row.occurs_on,
|
|
615
|
-
|
|
812
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
813
|
+
summary: decodeEventText(row.summary),
|
|
616
814
|
significance: row.significance
|
|
617
815
|
};
|
|
618
816
|
}
|
|
817
|
+
function decodeEventText(value) {
|
|
818
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
819
|
+
}
|
|
619
820
|
var init_mappers = __esm({
|
|
620
821
|
"src/events/mappers.ts"() {
|
|
621
822
|
init_affinity_invariant_error();
|
|
@@ -626,17 +827,17 @@ var init_mappers = __esm({
|
|
|
626
827
|
// src/events/queries.ts
|
|
627
828
|
function getEventRowById(db, id) {
|
|
628
829
|
const row = db.prepare(
|
|
629
|
-
`SELECT
|
|
630
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
631
|
-
anchor_contact_id, anchor_link_id,
|
|
632
|
-
created_at, updated_at, deleted_at
|
|
830
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
633
831
|
FROM events
|
|
634
832
|
WHERE id = ? AND deleted_at IS NULL`
|
|
635
833
|
).get(id);
|
|
636
834
|
if (row === void 0) {
|
|
637
835
|
return null;
|
|
638
836
|
}
|
|
639
|
-
|
|
837
|
+
const event = row;
|
|
838
|
+
event.summary = decodeEventText(event.summary);
|
|
839
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
840
|
+
return event;
|
|
640
841
|
}
|
|
641
842
|
function findDuplicateDateAnchor(db, params) {
|
|
642
843
|
const exclude = params.excludeEventId;
|
|
@@ -686,6 +887,7 @@ function findDuplicateDateAnchor(db, params) {
|
|
|
686
887
|
}
|
|
687
888
|
var init_queries2 = __esm({
|
|
688
889
|
"src/events/queries.ts"() {
|
|
890
|
+
init_mappers();
|
|
689
891
|
}
|
|
690
892
|
});
|
|
691
893
|
|
|
@@ -708,19 +910,27 @@ function loadEventRecord(db, eventId) {
|
|
|
708
910
|
throw new AffinityInvariantError("event not found");
|
|
709
911
|
}
|
|
710
912
|
const participants = loadEventParticipantViews(db, eventId);
|
|
711
|
-
|
|
913
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
914
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
915
|
+
if (due) {
|
|
916
|
+
record.dueAt = due.due_at;
|
|
917
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
918
|
+
}
|
|
919
|
+
enrichEventMetadata(db, record);
|
|
920
|
+
return record;
|
|
712
921
|
}
|
|
713
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
922
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
714
923
|
if (eventIds.length === 0) {
|
|
715
924
|
return [];
|
|
716
925
|
}
|
|
717
926
|
const ph = eventIds.map(() => "?").join(", ");
|
|
718
927
|
const eventRows = db.prepare(
|
|
719
|
-
`SELECT id, type, occurred_at,
|
|
928
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
929
|
+
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,
|
|
720
930
|
recurrence_kind, anchor_month, anchor_day,
|
|
721
931
|
anchor_contact_id, anchor_link_id,
|
|
722
|
-
created_at, updated_at, deleted_at
|
|
723
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
932
|
+
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
|
|
933
|
+
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`
|
|
724
934
|
).all(...eventIds);
|
|
725
935
|
const partRows = db.prepare(
|
|
726
936
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -744,7 +954,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
744
954
|
for (const id of eventIds) {
|
|
745
955
|
const row = rowById.get(id);
|
|
746
956
|
if (row) {
|
|
747
|
-
|
|
957
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
958
|
+
enrichEventMetadata(db, record);
|
|
959
|
+
out.push(record);
|
|
748
960
|
}
|
|
749
961
|
}
|
|
750
962
|
return out;
|
|
@@ -787,6 +999,7 @@ var init_loaders = __esm({
|
|
|
787
999
|
"src/events/loaders.ts"() {
|
|
788
1000
|
init_affinity_invariant_error();
|
|
789
1001
|
init_affinity_not_found_error();
|
|
1002
|
+
init_event_metadata();
|
|
790
1003
|
init_mappers();
|
|
791
1004
|
init_queries2();
|
|
792
1005
|
}
|
|
@@ -811,6 +1024,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
811
1024
|
clauses.push(`e.type IN (${ph})`);
|
|
812
1025
|
params.push(...options.eventTypes);
|
|
813
1026
|
}
|
|
1027
|
+
if (options?.channel !== void 0) {
|
|
1028
|
+
clauses.push("e.channel = ?");
|
|
1029
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1030
|
+
}
|
|
814
1031
|
const where = clauses.join(" AND ");
|
|
815
1032
|
const rows = db.prepare(
|
|
816
1033
|
`SELECT e.id FROM events e
|
|
@@ -821,7 +1038,8 @@ function getContactJournal(db, contactId, options) {
|
|
|
821
1038
|
).all(...params, limit, offset);
|
|
822
1039
|
return batchLoadEventRecords(
|
|
823
1040
|
db,
|
|
824
|
-
rows.map((r) => r.id)
|
|
1041
|
+
rows.map((r) => r.id),
|
|
1042
|
+
options?.includeContent ?? false
|
|
825
1043
|
);
|
|
826
1044
|
}
|
|
827
1045
|
var init_get_contact_journal = __esm({
|
|
@@ -832,6 +1050,78 @@ var init_get_contact_journal = __esm({
|
|
|
832
1050
|
}
|
|
833
1051
|
});
|
|
834
1052
|
|
|
1053
|
+
// src/lib/parse_calendar_time.ts
|
|
1054
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
1055
|
+
if (dueAt === null) return false;
|
|
1056
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
1057
|
+
return localCalendarDate(now) > dueOn;
|
|
1058
|
+
}
|
|
1059
|
+
return dueAt < now;
|
|
1060
|
+
}
|
|
1061
|
+
function localCalendarDate(now) {
|
|
1062
|
+
const d = new Date(now);
|
|
1063
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1064
|
+
}
|
|
1065
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
1066
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
1067
|
+
throw new AffinityValidationError(
|
|
1068
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
1069
|
+
);
|
|
1070
|
+
const fail = () => {
|
|
1071
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1072
|
+
};
|
|
1073
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
1074
|
+
const timestamp2 = Number(value);
|
|
1075
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
1076
|
+
return fail();
|
|
1077
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
1078
|
+
}
|
|
1079
|
+
if (typeof value !== "string") return fail();
|
|
1080
|
+
let text = value.trim();
|
|
1081
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
1082
|
+
text = text.replace(" ", "T");
|
|
1083
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
1084
|
+
}
|
|
1085
|
+
let rounding = 0;
|
|
1086
|
+
text = text.replace(
|
|
1087
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
1088
|
+
(_, digits) => {
|
|
1089
|
+
const ms = Math.round(Number(`0.${digits}`) * 1e3);
|
|
1090
|
+
rounding = ms === 1e3 ? 1 : 0;
|
|
1091
|
+
return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
1095
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1096
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
1097
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
1098
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
1099
|
+
text = date.toISOString().slice(0, 10);
|
|
1100
|
+
}
|
|
1101
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
1102
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
1103
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
1104
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1105
|
+
text
|
|
1106
|
+
))
|
|
1107
|
+
return fail();
|
|
1108
|
+
const day = text.slice(0, 10);
|
|
1109
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
1110
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
1111
|
+
return fail();
|
|
1112
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
|
|
1113
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
1114
|
+
return { timestamp, day };
|
|
1115
|
+
}
|
|
1116
|
+
var timeInputForms, OVERDUE_SQL;
|
|
1117
|
+
var init_parse_calendar_time = __esm({
|
|
1118
|
+
"src/lib/parse_calendar_time.ts"() {
|
|
1119
|
+
init_affinity_validation_error();
|
|
1120
|
+
timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
1121
|
+
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)";
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
|
|
835
1125
|
// src/resolve_now.ts
|
|
836
1126
|
function resolveNow(now) {
|
|
837
1127
|
if (now !== void 0) {
|
|
@@ -904,9 +1194,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
904
1194
|
clauses.push("oc.due_at <= ?");
|
|
905
1195
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
906
1196
|
}
|
|
1197
|
+
if (options?.channel !== void 0) {
|
|
1198
|
+
clauses.push("e.channel=?");
|
|
1199
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1200
|
+
}
|
|
907
1201
|
const where = clauses.join(" AND ");
|
|
908
1202
|
const rows = db.prepare(
|
|
909
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
1203
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
910
1204
|
e.type, e.summary
|
|
911
1205
|
FROM open_commitments oc
|
|
912
1206
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -923,7 +1217,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
923
1217
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
924
1218
|
dueAt: r.due_at,
|
|
925
1219
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
926
|
-
|
|
1220
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
1221
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
927
1222
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
928
1223
|
}));
|
|
929
1224
|
}
|
|
@@ -931,6 +1226,7 @@ var DAY_MS;
|
|
|
931
1226
|
var init_list_open_commitments = __esm({
|
|
932
1227
|
"src/events/list_open_commitments.ts"() {
|
|
933
1228
|
init_affinity_validation_error();
|
|
1229
|
+
init_parse_calendar_time();
|
|
934
1230
|
init_assert_default_ordering();
|
|
935
1231
|
init_resolve_list_limit_offset();
|
|
936
1232
|
init_resolve_now();
|
|
@@ -1008,8 +1304,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
1008
1304
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
1009
1305
|
}
|
|
1010
1306
|
function computeNextAnchorOccursOn(params) {
|
|
1011
|
-
const refStart =
|
|
1012
|
-
|
|
1307
|
+
const refStart = Date.parse(
|
|
1308
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
1309
|
+
);
|
|
1310
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
1013
1311
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
1014
1312
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
1015
1313
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -1023,6 +1321,7 @@ function computeNextAnchorOccursOn(params) {
|
|
|
1023
1321
|
}
|
|
1024
1322
|
var init_calendar = __esm({
|
|
1025
1323
|
"src/dates/calendar.ts"() {
|
|
1324
|
+
init_parse_calendar_time();
|
|
1026
1325
|
}
|
|
1027
1326
|
});
|
|
1028
1327
|
|
|
@@ -1240,7 +1539,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1240
1539
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
1241
1540
|
LIMIT ?`
|
|
1242
1541
|
).all(contactId, contactId, topLinksLimit);
|
|
1243
|
-
const topLinks = linkRows.map((l) =>
|
|
1542
|
+
const topLinks = linkRows.map((l) => ({
|
|
1543
|
+
...mapLinkRowToLinkListItem(l),
|
|
1544
|
+
counterpartyName: getContactRowById(
|
|
1545
|
+
db,
|
|
1546
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1547
|
+
)?.name ?? ""
|
|
1548
|
+
}));
|
|
1244
1549
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
1245
1550
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1246
1551
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -1283,7 +1588,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
1283
1588
|
return {
|
|
1284
1589
|
...profile,
|
|
1285
1590
|
recentEvents: getContactJournal(db, contactId, {
|
|
1286
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1591
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1592
|
+
eventTypes: [
|
|
1593
|
+
"conversation",
|
|
1594
|
+
"activity",
|
|
1595
|
+
"gift",
|
|
1596
|
+
"support",
|
|
1597
|
+
"milestone",
|
|
1598
|
+
"observation",
|
|
1599
|
+
"conflict",
|
|
1600
|
+
"correction",
|
|
1601
|
+
"transaction",
|
|
1602
|
+
"promise",
|
|
1603
|
+
"agreement"
|
|
1604
|
+
]
|
|
1287
1605
|
}).map((event) => ({
|
|
1288
1606
|
...event,
|
|
1289
1607
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -1351,6 +1669,10 @@ function listMoments(db, filters, options) {
|
|
|
1351
1669
|
clauses.push("le.link_id = ?");
|
|
1352
1670
|
params.push(filters.linkId);
|
|
1353
1671
|
}
|
|
1672
|
+
if (options?.channel !== void 0) {
|
|
1673
|
+
clauses.push("e.channel=?");
|
|
1674
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1675
|
+
}
|
|
1354
1676
|
const where = clauses.join(" AND ");
|
|
1355
1677
|
return db.prepare(
|
|
1356
1678
|
`SELECT
|
|
@@ -1358,6 +1680,7 @@ function listMoments(db, filters, options) {
|
|
|
1358
1680
|
le.link_id,
|
|
1359
1681
|
le.moment_kind,
|
|
1360
1682
|
le.occurred_at,
|
|
1683
|
+
e.occurred_on,
|
|
1361
1684
|
e.summary,
|
|
1362
1685
|
le.impact_score
|
|
1363
1686
|
FROM link_event_effects le
|
|
@@ -1370,6 +1693,7 @@ function listMoments(db, filters, options) {
|
|
|
1370
1693
|
linkId: row.link_id,
|
|
1371
1694
|
momentKind: row.moment_kind,
|
|
1372
1695
|
occurredAt: row.occurred_at,
|
|
1696
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
1373
1697
|
summary: row.summary,
|
|
1374
1698
|
impactScore: row.impact_score
|
|
1375
1699
|
}));
|
|
@@ -1469,6 +1793,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1469
1793
|
clauses.push(`e.type IN (${ph})`);
|
|
1470
1794
|
params.push(...options.eventTypes);
|
|
1471
1795
|
}
|
|
1796
|
+
if (options?.channel !== void 0) {
|
|
1797
|
+
clauses.push("e.channel = ?");
|
|
1798
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1799
|
+
}
|
|
1472
1800
|
const where = clauses.join(" AND ");
|
|
1473
1801
|
const rows = db.prepare(
|
|
1474
1802
|
`SELECT e.id
|
|
@@ -1480,7 +1808,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1480
1808
|
).all(...params, limit, offset);
|
|
1481
1809
|
return batchLoadEventRecords(
|
|
1482
1810
|
db,
|
|
1483
|
-
rows.map((r) => r.id)
|
|
1811
|
+
rows.map((r) => r.id),
|
|
1812
|
+
options?.includeContent ?? false
|
|
1484
1813
|
);
|
|
1485
1814
|
}
|
|
1486
1815
|
var init_get_link_timeline = __esm({
|
|
@@ -1664,26 +1993,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1664
1993
|
const latest = db.prepare(
|
|
1665
1994
|
`SELECT event_id, occurred_at
|
|
1666
1995
|
FROM link_event_effects
|
|
1667
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
1996
|
+
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 <= ?))
|
|
1668
1997
|
ORDER BY occurred_at DESC, event_id DESC
|
|
1669
1998
|
LIMIT 1`
|
|
1670
|
-
).get(linkId);
|
|
1999
|
+
).get(linkId, now, localCalendarDate(now));
|
|
1671
2000
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
1672
2001
|
const counts = db.prepare(
|
|
1673
2002
|
`SELECT
|
|
1674
2003
|
COUNT(*) AS total_meaningful_events,
|
|
1675
2004
|
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
1676
2005
|
FROM link_event_effects
|
|
1677
|
-
WHERE link_id = ? AND occurred_at >=
|
|
1678
|
-
|
|
2006
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
2007
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
2008
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
1679
2009
|
const directionalityRows = db.prepare(
|
|
1680
2010
|
`SELECT ep.directionality
|
|
1681
2011
|
FROM link_event_effects le
|
|
1682
2012
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
1683
2013
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
1684
2014
|
AND ep.directionality IS NOT NULL
|
|
1685
|
-
AND le.occurred_at >=
|
|
1686
|
-
|
|
2015
|
+
AND le.occurred_at >= ?
|
|
2016
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
2017
|
+
).all(
|
|
2018
|
+
linkId,
|
|
2019
|
+
link.from_contact_id,
|
|
2020
|
+
trailingWindowStart,
|
|
2021
|
+
now,
|
|
2022
|
+
localCalendarDate(now)
|
|
2023
|
+
);
|
|
1687
2024
|
let outboundCount = 0;
|
|
1688
2025
|
let inboundCount = 0;
|
|
1689
2026
|
for (const row of directionalityRows) {
|
|
@@ -1793,6 +2130,7 @@ var DAY_MS2;
|
|
|
1793
2130
|
var init_rollups2 = __esm({
|
|
1794
2131
|
"src/links/rollups.ts"() {
|
|
1795
2132
|
init_rollups();
|
|
2133
|
+
init_parse_calendar_time();
|
|
1796
2134
|
init_mechanics();
|
|
1797
2135
|
init_queries3();
|
|
1798
2136
|
DAY_MS2 = 864e5;
|
|
@@ -1855,41 +2193,12 @@ var init_get_link_detail = __esm({
|
|
|
1855
2193
|
|
|
1856
2194
|
// src/tools/parse_time_input.ts
|
|
1857
2195
|
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
1858
|
-
|
|
1859
|
-
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1860
|
-
};
|
|
1861
|
-
if (typeof value === "number")
|
|
1862
|
-
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
1863
|
-
if (typeof value !== "string") return fail();
|
|
1864
|
-
const text = value.trim();
|
|
1865
|
-
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
1866
|
-
let day;
|
|
1867
|
-
if (relative) {
|
|
1868
|
-
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1869
|
-
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
1870
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
1871
|
-
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
1872
|
-
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
1873
|
-
return fail();
|
|
1874
|
-
} else {
|
|
1875
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1876
|
-
text
|
|
1877
|
-
))
|
|
1878
|
-
return fail();
|
|
1879
|
-
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
1880
|
-
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
1881
|
-
return fail();
|
|
1882
|
-
const parsed = Date.parse(text);
|
|
1883
|
-
return Number.isFinite(parsed) ? parsed : fail();
|
|
1884
|
-
}
|
|
1885
|
-
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
1886
|
-
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
2196
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
1887
2197
|
}
|
|
1888
|
-
var timeInputForms;
|
|
1889
2198
|
var init_parse_time_input = __esm({
|
|
1890
2199
|
"src/tools/parse_time_input.ts"() {
|
|
1891
|
-
|
|
1892
|
-
|
|
2200
|
+
init_parse_calendar_time();
|
|
2201
|
+
init_parse_calendar_time();
|
|
1893
2202
|
}
|
|
1894
2203
|
});
|
|
1895
2204
|
|
|
@@ -1966,8 +2275,47 @@ var init_tool_metadata = __esm({
|
|
|
1966
2275
|
}
|
|
1967
2276
|
});
|
|
1968
2277
|
|
|
2278
|
+
// src/tools/event_metadata_schema.ts
|
|
2279
|
+
var externalRefSchema, eventMetadataSchema;
|
|
2280
|
+
var init_event_metadata_schema = __esm({
|
|
2281
|
+
"src/tools/event_metadata_schema.ts"() {
|
|
2282
|
+
init_tool_metadata();
|
|
2283
|
+
externalRefSchema = objectSchema(
|
|
2284
|
+
{
|
|
2285
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
2286
|
+
id: stringSchema("Non-empty external id.")
|
|
2287
|
+
},
|
|
2288
|
+
["source", "id"]
|
|
2289
|
+
);
|
|
2290
|
+
eventMetadataSchema = {
|
|
2291
|
+
affectsBond: booleanSchema(
|
|
2292
|
+
"Whether evidence affects the relationship; defaults false for observations with significance <=2."
|
|
2293
|
+
),
|
|
2294
|
+
content: stringSchema(
|
|
2295
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
2296
|
+
),
|
|
2297
|
+
externalRef: externalRefSchema,
|
|
2298
|
+
channel: stringSchema(
|
|
2299
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
2300
|
+
),
|
|
2301
|
+
parentRef: externalRefSchema,
|
|
2302
|
+
parentEventId: integerSchema("Parent event id."),
|
|
2303
|
+
threadRef: externalRefSchema,
|
|
2304
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
2305
|
+
"breakthrough",
|
|
2306
|
+
"rupture",
|
|
2307
|
+
"reconciliation",
|
|
2308
|
+
"milestone",
|
|
2309
|
+
"turning_point"
|
|
2310
|
+
]),
|
|
2311
|
+
occurredOn: stringSchema("Calendar day."),
|
|
2312
|
+
provenance: {}
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
|
|
1969
2317
|
// src/tools/tool_names.ts
|
|
1970
|
-
var searchAffinityToolName, reviewAffinityToolName, inspectAffinityItemToolName, manageContactToolName, mergeContactsToolName, manageDuplicateDismissalToolName, manageIdentityToolName, manageRelationshipToolName, recordEventToolName, manageCommitmentToolName, manageDateAnchorToolName, manageAttributeToolName;
|
|
2318
|
+
var searchAffinityToolName, reviewAffinityToolName, inspectAffinityItemToolName, manageContactToolName, mergeContactsToolName, manageDuplicateDismissalToolName, manageIdentityToolName, manageRelationshipToolName, recordEventToolName, manageCommitmentToolName, manageDateAnchorToolName, manageAttributeToolName, manageEventToolName;
|
|
1971
2319
|
var init_tool_names = __esm({
|
|
1972
2320
|
"src/tools/tool_names.ts"() {
|
|
1973
2321
|
searchAffinityToolName = "search_affinity";
|
|
@@ -1982,6 +2330,7 @@ var init_tool_names = __esm({
|
|
|
1982
2330
|
manageCommitmentToolName = "manage_commitment";
|
|
1983
2331
|
manageDateAnchorToolName = "manage_date_anchor";
|
|
1984
2332
|
manageAttributeToolName = "manage_attribute";
|
|
2333
|
+
manageEventToolName = "manage_event";
|
|
1985
2334
|
}
|
|
1986
2335
|
});
|
|
1987
2336
|
|
|
@@ -2028,7 +2377,7 @@ function toEventEntityRef(value) {
|
|
|
2028
2377
|
return {
|
|
2029
2378
|
kind: "event",
|
|
2030
2379
|
id: value.id,
|
|
2031
|
-
title: value.summary,
|
|
2380
|
+
title: deriveEventSummary(value.summary),
|
|
2032
2381
|
subtitle: value.type,
|
|
2033
2382
|
state: value.type
|
|
2034
2383
|
};
|
|
@@ -2063,21 +2412,48 @@ function toReceiptEntityRefs(receipt) {
|
|
|
2063
2412
|
}
|
|
2064
2413
|
var init_tool_ref = __esm({
|
|
2065
2414
|
"src/tools/tool_ref.ts"() {
|
|
2415
|
+
init_event_metadata();
|
|
2066
2416
|
}
|
|
2067
2417
|
});
|
|
2068
2418
|
|
|
2069
2419
|
// src/identities/normalize.ts
|
|
2420
|
+
function normalizeIdentityValue(type, value) {
|
|
2421
|
+
const v = value.trim();
|
|
2422
|
+
return handleIdentityTypes.has(type.toLowerCase()) ? v.replace(/^@/, "").toLowerCase() : v;
|
|
2423
|
+
}
|
|
2070
2424
|
function normalizeIdentityKey(type, value) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2425
|
+
let t = type.trim().toLowerCase();
|
|
2426
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(t)) t = "did";
|
|
2427
|
+
const v = normalizeIdentityValue(t, value).toLowerCase();
|
|
2073
2428
|
if (t.length === 0 || v.length === 0) {
|
|
2074
2429
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
2075
2430
|
}
|
|
2076
2431
|
return `${t}:${v}`;
|
|
2077
2432
|
}
|
|
2433
|
+
function legacyIdentityKey(key) {
|
|
2434
|
+
const colon = key.indexOf(":");
|
|
2435
|
+
return handleIdentityTypes.has(key.slice(0, colon)) ? `${key.slice(0, colon + 1)}@${key.slice(colon + 1)}` : key;
|
|
2436
|
+
}
|
|
2437
|
+
var handleIdentityTypes;
|
|
2078
2438
|
var init_normalize = __esm({
|
|
2079
2439
|
"src/identities/normalize.ts"() {
|
|
2080
2440
|
init_affinity_validation_error();
|
|
2441
|
+
handleIdentityTypes = /* @__PURE__ */ new Set([
|
|
2442
|
+
"handle",
|
|
2443
|
+
"bluesky",
|
|
2444
|
+
"x",
|
|
2445
|
+
"twitter",
|
|
2446
|
+
"mastodon",
|
|
2447
|
+
"github",
|
|
2448
|
+
"instagram",
|
|
2449
|
+
"telegram",
|
|
2450
|
+
"linkedin",
|
|
2451
|
+
"facebook",
|
|
2452
|
+
"youtube",
|
|
2453
|
+
"tiktok",
|
|
2454
|
+
"discord",
|
|
2455
|
+
"slack"
|
|
2456
|
+
]);
|
|
2081
2457
|
}
|
|
2082
2458
|
});
|
|
2083
2459
|
|
|
@@ -2210,7 +2586,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
2210
2586
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
2211
2587
|
return {
|
|
2212
2588
|
ok: false,
|
|
2213
|
-
result: toolFailure("domain", "
|
|
2589
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
2214
2590
|
};
|
|
2215
2591
|
}
|
|
2216
2592
|
return { ok: true, value: row2 };
|
|
@@ -2235,9 +2611,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
2235
2611
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
2236
2612
|
WHERE i.removed_at IS NULL
|
|
2237
2613
|
AND c.deleted_at IS NULL
|
|
2238
|
-
AND i.normalized_key = ?
|
|
2614
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
2239
2615
|
LIMIT 2`
|
|
2240
|
-
).all(
|
|
2616
|
+
).all(
|
|
2617
|
+
normalizedKey,
|
|
2618
|
+
legacyIdentityKey(normalizedKey),
|
|
2619
|
+
normalizedKey
|
|
2620
|
+
);
|
|
2241
2621
|
if (row.length === 0) {
|
|
2242
2622
|
return {
|
|
2243
2623
|
ok: false,
|
|
@@ -2388,13 +2768,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
2388
2768
|
return { ok: true, value: matchedLink };
|
|
2389
2769
|
}
|
|
2390
2770
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
2771
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
2772
|
+
return error;
|
|
2391
2773
|
if (isAffinityError(error)) {
|
|
2392
|
-
|
|
2774
|
+
const result = toolFailure(
|
|
2393
2775
|
"domain",
|
|
2394
2776
|
affinityErrorCodeToToolCode(error),
|
|
2395
2777
|
summary,
|
|
2396
2778
|
error.message
|
|
2397
2779
|
);
|
|
2780
|
+
const id = error.existingContactId;
|
|
2781
|
+
if (id !== void 0) {
|
|
2782
|
+
result.data = { existingContactId: id };
|
|
2783
|
+
result.error.details = { existingContactId: id };
|
|
2784
|
+
}
|
|
2785
|
+
return result;
|
|
2398
2786
|
}
|
|
2399
2787
|
if (error instanceof Error) {
|
|
2400
2788
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -2443,27 +2831,32 @@ var init_tool_resolvers = __esm({
|
|
|
2443
2831
|
});
|
|
2444
2832
|
|
|
2445
2833
|
// src/tools/validate_tool_input.ts
|
|
2446
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
2834
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
2835
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
2836
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
2837
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
2838
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
2839
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
2447
2840
|
if (schema.anyOf || schema.oneOf) {
|
|
2448
2841
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
2449
2842
|
return choices.some(
|
|
2450
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
2843
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
2451
2844
|
) ? null : `${path}: invalid value`;
|
|
2452
2845
|
}
|
|
2453
2846
|
if (schema.type === "object") {
|
|
2454
2847
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
2455
2848
|
return `${path}: missing input or expected object`;
|
|
2456
2849
|
const object = value;
|
|
2457
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
2850
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
2458
2851
|
for (const key of Object.keys(object)) {
|
|
2459
2852
|
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
2460
|
-
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(", ")}`;
|
|
2853
|
+
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(", ")}`;
|
|
2461
2854
|
}
|
|
2462
2855
|
}
|
|
2463
2856
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
2464
2857
|
return `${path}.patch: missing required field`;
|
|
2465
2858
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
2466
|
-
return `${path}.patch: unknown
|
|
2859
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
2467
2860
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
2468
2861
|
try {
|
|
2469
2862
|
return parseTimeInput(
|
|
@@ -2478,11 +2871,14 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2478
2871
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
2479
2872
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
2480
2873
|
try {
|
|
2481
|
-
|
|
2874
|
+
const parsed = parseCalendarTime(
|
|
2482
2875
|
object[key],
|
|
2483
2876
|
`${path}.${key}`,
|
|
2484
2877
|
referenceNow
|
|
2485
2878
|
);
|
|
2879
|
+
object[key] = parsed.timestamp;
|
|
2880
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
2881
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
2486
2882
|
} catch (error) {
|
|
2487
2883
|
return error.message;
|
|
2488
2884
|
}
|
|
@@ -2498,7 +2894,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2498
2894
|
child,
|
|
2499
2895
|
object[key],
|
|
2500
2896
|
`${path}.${key}`,
|
|
2501
|
-
referenceNow
|
|
2897
|
+
referenceNow,
|
|
2898
|
+
strict
|
|
2502
2899
|
);
|
|
2503
2900
|
if (error) return error;
|
|
2504
2901
|
}
|
|
@@ -2511,7 +2908,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2511
2908
|
schema.items ?? {},
|
|
2512
2909
|
item,
|
|
2513
2910
|
`${path}[${index}]`,
|
|
2514
|
-
injectedNow
|
|
2911
|
+
injectedNow,
|
|
2912
|
+
strict
|
|
2515
2913
|
);
|
|
2516
2914
|
if (error) return error;
|
|
2517
2915
|
}
|
|
@@ -2529,6 +2927,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2529
2927
|
}
|
|
2530
2928
|
var init_validate_tool_input = __esm({
|
|
2531
2929
|
"src/tools/validate_tool_input.ts"() {
|
|
2930
|
+
init_parse_calendar_time();
|
|
2532
2931
|
init_parse_time_input();
|
|
2533
2932
|
}
|
|
2534
2933
|
});
|
|
@@ -2582,6 +2981,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
2582
2981
|
inputError
|
|
2583
2982
|
);
|
|
2584
2983
|
return withToolHandling(() => {
|
|
2984
|
+
if (input.kind === "event") {
|
|
2985
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
2986
|
+
return toolFailure(
|
|
2987
|
+
"protocol",
|
|
2988
|
+
"invalid_input",
|
|
2989
|
+
"Invalid event target.",
|
|
2990
|
+
"Provide exactly one of eventId or ref."
|
|
2991
|
+
);
|
|
2992
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
2993
|
+
return detail2 ? toolSuccess(
|
|
2994
|
+
`Loaded event #${detail2.id}.`,
|
|
2995
|
+
{ kind: "event", detail: detail2 },
|
|
2996
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
2997
|
+
) : toolFailure(
|
|
2998
|
+
"domain",
|
|
2999
|
+
"not_found",
|
|
3000
|
+
"Event not found.",
|
|
3001
|
+
"No event matches the target."
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
2585
3004
|
if (input.kind === "owner_profile") {
|
|
2586
3005
|
const detail2 = getOwnerProfile(
|
|
2587
3006
|
db,
|
|
@@ -2664,7 +3083,9 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2664
3083
|
"src/tools/inspect_affinity_item_tool.ts"() {
|
|
2665
3084
|
init_get_contact_profile();
|
|
2666
3085
|
init_get_owner_profile();
|
|
3086
|
+
init_event_metadata();
|
|
2667
3087
|
init_get_link_detail();
|
|
3088
|
+
init_event_metadata_schema();
|
|
2668
3089
|
init_tool_metadata();
|
|
2669
3090
|
init_tool_names();
|
|
2670
3091
|
init_tool_ref();
|
|
@@ -2692,10 +3113,13 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2692
3113
|
inputSchema: objectSchema(
|
|
2693
3114
|
{
|
|
2694
3115
|
kind: enumSchema("What to inspect.", [
|
|
3116
|
+
"event",
|
|
2695
3117
|
"owner_profile",
|
|
2696
3118
|
"contact_profile",
|
|
2697
3119
|
"link"
|
|
2698
3120
|
]),
|
|
3121
|
+
eventId: integerSchema("Exact event id."),
|
|
3122
|
+
ref: externalRefSchema,
|
|
2699
3123
|
contact: contactLocatorSchema(
|
|
2700
3124
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
2701
3125
|
),
|
|
@@ -2716,6 +3140,7 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2716
3140
|
// src/attributes/helpers.ts
|
|
2717
3141
|
function clearedAttributePrimary(target) {
|
|
2718
3142
|
return {
|
|
3143
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
2719
3144
|
id: 0,
|
|
2720
3145
|
contactId: target.kind === "contact" ? target.id : null,
|
|
2721
3146
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -2731,6 +3156,7 @@ var init_helpers = __esm({
|
|
|
2731
3156
|
// src/attributes/mappers.ts
|
|
2732
3157
|
function mapAttributeRowToAttributeRecord(row) {
|
|
2733
3158
|
return {
|
|
3159
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
2734
3160
|
id: row.id,
|
|
2735
3161
|
contactId: row.contact_id,
|
|
2736
3162
|
linkId: row.link_id,
|
|
@@ -2744,131 +3170,46 @@ var init_mappers5 = __esm({
|
|
|
2744
3170
|
});
|
|
2745
3171
|
|
|
2746
3172
|
// src/attributes/queries.ts
|
|
3173
|
+
function attributeTargetSql(target) {
|
|
3174
|
+
return {
|
|
3175
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
3176
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
2747
3179
|
function getAttributeRowById(db, id) {
|
|
2748
|
-
|
|
2749
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
2750
|
-
FROM attributes WHERE id = ?`
|
|
2751
|
-
).get(id);
|
|
2752
|
-
if (row === void 0) {
|
|
2753
|
-
return null;
|
|
2754
|
-
}
|
|
2755
|
-
return row;
|
|
3180
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
2756
3181
|
}
|
|
2757
3182
|
function getLiveAttributeRow(db, target, name) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
WHERE contact_id = ? AND name = ? AND deleted_at IS NULL`
|
|
2763
|
-
).get(target.id, name);
|
|
2764
|
-
if (row2 === void 0) {
|
|
2765
|
-
return null;
|
|
2766
|
-
}
|
|
2767
|
-
return row2;
|
|
2768
|
-
}
|
|
2769
|
-
const row = db.prepare(
|
|
2770
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
2771
|
-
FROM attributes
|
|
2772
|
-
WHERE link_id = ? AND name = ? AND deleted_at IS NULL`
|
|
2773
|
-
).get(target.id, name);
|
|
2774
|
-
if (row === void 0) {
|
|
2775
|
-
return null;
|
|
2776
|
-
}
|
|
2777
|
-
return row;
|
|
3183
|
+
const t = attributeTargetSql(target);
|
|
3184
|
+
return db.prepare(
|
|
3185
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
3186
|
+
).get(t.id, name) ?? null;
|
|
2778
3187
|
}
|
|
2779
3188
|
function listLiveAttributeIdsForTarget(db, target) {
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
return rows2.map((row) => row.id);
|
|
2785
|
-
}
|
|
2786
|
-
const rows = db.prepare(
|
|
2787
|
-
"SELECT id FROM attributes WHERE link_id = ? AND deleted_at IS NULL ORDER BY id"
|
|
2788
|
-
).all(target.id);
|
|
2789
|
-
return rows.map((row) => row.id);
|
|
3189
|
+
const t = attributeTargetSql(target);
|
|
3190
|
+
return db.prepare(
|
|
3191
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
3192
|
+
).all(t.id).map((r) => r.id);
|
|
2790
3193
|
}
|
|
2791
3194
|
var init_queries4 = __esm({
|
|
2792
3195
|
"src/attributes/queries.ts"() {
|
|
2793
3196
|
}
|
|
2794
3197
|
});
|
|
2795
3198
|
|
|
2796
|
-
// src/
|
|
2797
|
-
function
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
}
|
|
2808
|
-
return out;
|
|
3199
|
+
// src/attributes/receipts.ts
|
|
3200
|
+
function buildAttributeMutationReceipt(primary, partial) {
|
|
3201
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
3202
|
+
return {
|
|
3203
|
+
primary,
|
|
3204
|
+
created: normalized.created,
|
|
3205
|
+
updated: normalized.updated,
|
|
3206
|
+
archived: normalized.archived,
|
|
3207
|
+
removed: normalized.removed,
|
|
3208
|
+
affectedLinks: normalized.affectedLinks,
|
|
3209
|
+
derivedEffects: normalized.derivedEffects
|
|
3210
|
+
};
|
|
2809
3211
|
}
|
|
2810
|
-
|
|
2811
|
-
const out = [];
|
|
2812
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2813
|
-
for (const effect of effects ?? []) {
|
|
2814
|
-
const key = `${effect.eventId}:${effect.linkId}`;
|
|
2815
|
-
if (seen.has(key)) {
|
|
2816
|
-
continue;
|
|
2817
|
-
}
|
|
2818
|
-
seen.add(key);
|
|
2819
|
-
out.push(effect);
|
|
2820
|
-
}
|
|
2821
|
-
return out;
|
|
2822
|
-
}
|
|
2823
|
-
function uniqueAffectedLinks(affectedLinks, derivedEffects) {
|
|
2824
|
-
const out = [];
|
|
2825
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2826
|
-
for (const linkId of affectedLinks ?? []) {
|
|
2827
|
-
if (seen.has(linkId)) {
|
|
2828
|
-
continue;
|
|
2829
|
-
}
|
|
2830
|
-
seen.add(linkId);
|
|
2831
|
-
out.push(linkId);
|
|
2832
|
-
}
|
|
2833
|
-
for (const effect of derivedEffects) {
|
|
2834
|
-
if (seen.has(effect.linkId)) {
|
|
2835
|
-
continue;
|
|
2836
|
-
}
|
|
2837
|
-
seen.add(effect.linkId);
|
|
2838
|
-
out.push(effect.linkId);
|
|
2839
|
-
}
|
|
2840
|
-
return out;
|
|
2841
|
-
}
|
|
2842
|
-
function normalizeMutationReceiptPartial(partial) {
|
|
2843
|
-
const derivedEffects = uniqueDerivedEffects(partial?.derivedEffects);
|
|
2844
|
-
return {
|
|
2845
|
-
created: uniqueEntityRefs(partial?.created),
|
|
2846
|
-
updated: uniqueEntityRefs(partial?.updated),
|
|
2847
|
-
archived: uniqueEntityRefs(partial?.archived),
|
|
2848
|
-
removed: uniqueEntityRefs(partial?.removed),
|
|
2849
|
-
affectedLinks: uniqueAffectedLinks(partial?.affectedLinks, derivedEffects),
|
|
2850
|
-
derivedEffects
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
|
-
var init_normalize_mutation_receipt = __esm({
|
|
2854
|
-
"src/lib/normalize_mutation_receipt.ts"() {
|
|
2855
|
-
}
|
|
2856
|
-
});
|
|
2857
|
-
|
|
2858
|
-
// src/attributes/receipts.ts
|
|
2859
|
-
function buildAttributeMutationReceipt(primary, partial) {
|
|
2860
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
2861
|
-
return {
|
|
2862
|
-
primary,
|
|
2863
|
-
created: normalized.created,
|
|
2864
|
-
updated: normalized.updated,
|
|
2865
|
-
archived: normalized.archived,
|
|
2866
|
-
removed: normalized.removed,
|
|
2867
|
-
affectedLinks: normalized.affectedLinks,
|
|
2868
|
-
derivedEffects: normalized.derivedEffects
|
|
2869
|
-
};
|
|
2870
|
-
}
|
|
2871
|
-
var init_receipts = __esm({
|
|
3212
|
+
var init_receipts2 = __esm({
|
|
2872
3213
|
"src/attributes/receipts.ts"() {
|
|
2873
3214
|
init_normalize_mutation_receipt();
|
|
2874
3215
|
}
|
|
@@ -2931,11 +3272,14 @@ function normalizeAttributeValue(value) {
|
|
|
2931
3272
|
if (value === null) {
|
|
2932
3273
|
return null;
|
|
2933
3274
|
}
|
|
3275
|
+
if (typeof value !== "string")
|
|
3276
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
2934
3277
|
const trimmed = value.trim();
|
|
2935
3278
|
return trimmed.length === 0 ? null : trimmed;
|
|
2936
3279
|
}
|
|
2937
3280
|
var init_normalize2 = __esm({
|
|
2938
3281
|
"src/attributes/normalize.ts"() {
|
|
3282
|
+
init_affinity_validation_error();
|
|
2939
3283
|
}
|
|
2940
3284
|
});
|
|
2941
3285
|
|
|
@@ -2963,6 +3307,11 @@ function validateAttributeEntries(entries) {
|
|
|
2963
3307
|
return out;
|
|
2964
3308
|
}
|
|
2965
3309
|
function assertAttributeTargetWritable(db, target) {
|
|
3310
|
+
if (target.kind === "event") {
|
|
3311
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
3312
|
+
throw new AffinityNotFoundError("event not found");
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
2966
3315
|
if (target.kind === "contact") {
|
|
2967
3316
|
const contact = getContactRowById(db, target.id);
|
|
2968
3317
|
if (!contact) {
|
|
@@ -3009,26 +3358,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
3009
3358
|
}
|
|
3010
3359
|
const created = [];
|
|
3011
3360
|
let lastId = 0;
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
created.push({ kind: "attribute", id: lastId });
|
|
3021
|
-
}
|
|
3022
|
-
} else {
|
|
3023
|
-
const insert = db.prepare(
|
|
3024
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
3025
|
-
VALUES (NULL, ?, ?, ?, ?, ?)`
|
|
3026
|
-
);
|
|
3027
|
-
for (const e of normalized) {
|
|
3028
|
-
const r = insert.run(target.id, e.name, e.value, now, now);
|
|
3029
|
-
lastId = Number(r.lastInsertRowid);
|
|
3030
|
-
created.push({ kind: "attribute", id: lastId });
|
|
3031
|
-
}
|
|
3361
|
+
const t = attributeTargetSql(target);
|
|
3362
|
+
const insert = db.prepare(
|
|
3363
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
3364
|
+
);
|
|
3365
|
+
for (const e of normalized) {
|
|
3366
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
3367
|
+
lastId = Number(r.lastInsertRowid);
|
|
3368
|
+
created.push({ kind: "attribute", id: lastId });
|
|
3032
3369
|
}
|
|
3033
3370
|
if (normalized.length === 0) {
|
|
3034
3371
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -3054,11 +3391,12 @@ var init_replace_attributes = __esm({
|
|
|
3054
3391
|
init_helpers();
|
|
3055
3392
|
init_mappers5();
|
|
3056
3393
|
init_queries4();
|
|
3057
|
-
|
|
3394
|
+
init_receipts2();
|
|
3058
3395
|
init_validators3();
|
|
3059
3396
|
init_affinity_invariant_error();
|
|
3060
3397
|
init_resolve_now();
|
|
3061
3398
|
init_with_transaction();
|
|
3399
|
+
init_queries4();
|
|
3062
3400
|
}
|
|
3063
3401
|
});
|
|
3064
3402
|
|
|
@@ -3085,27 +3423,10 @@ function setAttribute(db, target, name, value) {
|
|
|
3085
3423
|
}
|
|
3086
3424
|
);
|
|
3087
3425
|
}
|
|
3088
|
-
|
|
3089
|
-
const r2 = db.prepare(
|
|
3090
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
3091
|
-
VALUES (?, NULL, ?, ?, ?, ?)`
|
|
3092
|
-
).run(target.id, key, stored, now, now);
|
|
3093
|
-
const id2 = Number(r2.lastInsertRowid);
|
|
3094
|
-
const row2 = getAttributeRowById(db, id2);
|
|
3095
|
-
if (!row2) {
|
|
3096
|
-
throw new AffinityInvariantError("attribute row missing after insert");
|
|
3097
|
-
}
|
|
3098
|
-
return buildAttributeMutationReceipt(
|
|
3099
|
-
mapAttributeRowToAttributeRecord(row2),
|
|
3100
|
-
{
|
|
3101
|
-
created: [{ kind: "attribute", id: id2 }]
|
|
3102
|
-
}
|
|
3103
|
-
);
|
|
3104
|
-
}
|
|
3426
|
+
const t = attributeTargetSql(target);
|
|
3105
3427
|
const r = db.prepare(
|
|
3106
|
-
`INSERT INTO attributes (
|
|
3107
|
-
|
|
3108
|
-
).run(target.id, key, stored, now, now);
|
|
3428
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
3429
|
+
).run(t.id, key, stored, now, now);
|
|
3109
3430
|
const id = Number(r.lastInsertRowid);
|
|
3110
3431
|
const row = getAttributeRowById(db, id);
|
|
3111
3432
|
if (!row) {
|
|
@@ -3124,11 +3445,12 @@ var init_set_attribute = __esm({
|
|
|
3124
3445
|
init_mappers5();
|
|
3125
3446
|
init_normalize2();
|
|
3126
3447
|
init_queries4();
|
|
3127
|
-
|
|
3448
|
+
init_receipts2();
|
|
3128
3449
|
init_validators3();
|
|
3129
3450
|
init_affinity_invariant_error();
|
|
3130
3451
|
init_resolve_now();
|
|
3131
3452
|
init_with_transaction();
|
|
3453
|
+
init_queries4();
|
|
3132
3454
|
}
|
|
3133
3455
|
});
|
|
3134
3456
|
|
|
@@ -3155,7 +3477,7 @@ var init_unset_attribute = __esm({
|
|
|
3155
3477
|
"src/attributes/unset_attribute.ts"() {
|
|
3156
3478
|
init_mappers5();
|
|
3157
3479
|
init_queries4();
|
|
3158
|
-
|
|
3480
|
+
init_receipts2();
|
|
3159
3481
|
init_validators3();
|
|
3160
3482
|
init_affinity_not_found_error();
|
|
3161
3483
|
init_resolve_now();
|
|
@@ -3184,14 +3506,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
3184
3506
|
affectedLinks: receipt.affectedLinks,
|
|
3185
3507
|
derivedEffects: receipt.derivedEffects
|
|
3186
3508
|
};
|
|
3187
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
3509
|
+
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);
|
|
3188
3510
|
if (changedCount === 0) {
|
|
3189
3511
|
return toolNoOp(summary, data, {
|
|
3190
3512
|
entities: toReceiptEntityRefs(receipt)
|
|
3191
3513
|
});
|
|
3192
3514
|
}
|
|
3193
3515
|
return toolSuccess(summary, data, {
|
|
3194
|
-
entities: toReceiptEntityRefs(receipt)
|
|
3516
|
+
entities: toReceiptEntityRefs(receipt),
|
|
3517
|
+
warnings: receipt.warnings
|
|
3195
3518
|
});
|
|
3196
3519
|
}
|
|
3197
3520
|
function describeChanges(receipt) {
|
|
@@ -3255,6 +3578,7 @@ function linkLocatorSchema2(description) {
|
|
|
3255
3578
|
};
|
|
3256
3579
|
}
|
|
3257
3580
|
function resolveAttributeTarget(db, target) {
|
|
3581
|
+
if (target.kind === "event") return { kind: "ok", target };
|
|
3258
3582
|
if (target.kind === "contact") {
|
|
3259
3583
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
3260
3584
|
if (!contact.ok) {
|
|
@@ -3326,7 +3650,7 @@ var init_manage_attribute_tool = __esm({
|
|
|
3326
3650
|
init_validate_tool_input();
|
|
3327
3651
|
manageAttributeTool = defineAffinityTool({
|
|
3328
3652
|
name: manageAttributeToolName,
|
|
3329
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
3653
|
+
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.",
|
|
3330
3654
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
3331
3655
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
3332
3656
|
sideEffects: "writes_state",
|
|
@@ -3347,7 +3671,8 @@ var init_manage_attribute_tool = __esm({
|
|
|
3347
3671
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
3348
3672
|
target: objectSchema(
|
|
3349
3673
|
{
|
|
3350
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
3674
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
3675
|
+
eventId: integerSchema("Target event id."),
|
|
3351
3676
|
contact: contactLocatorSchema2(
|
|
3352
3677
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
3353
3678
|
),
|
|
@@ -3377,7 +3702,7 @@ var init_manage_attribute_tool = __esm({
|
|
|
3377
3702
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
3378
3703
|
},
|
|
3379
3704
|
["action"],
|
|
3380
|
-
"Set, unset, or replace attributes on contacts and
|
|
3705
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
3381
3706
|
),
|
|
3382
3707
|
handler: manageAttributeToolHandler
|
|
3383
3708
|
});
|
|
@@ -3387,7 +3712,7 @@ var init_manage_attribute_tool = __esm({
|
|
|
3387
3712
|
// src/events/persistence.ts
|
|
3388
3713
|
function insertJournalEvent(db, params) {
|
|
3389
3714
|
const now = resolveNow(params.now);
|
|
3390
|
-
const summary = params.summary
|
|
3715
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
3391
3716
|
const result = db.prepare(
|
|
3392
3717
|
`INSERT INTO events (
|
|
3393
3718
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -3404,6 +3729,33 @@ function insertJournalEvent(db, params) {
|
|
|
3404
3729
|
now
|
|
3405
3730
|
);
|
|
3406
3731
|
const eventId = Number(result.lastInsertRowid);
|
|
3732
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
3733
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
|
|
3734
|
+
eventId
|
|
3735
|
+
);
|
|
3736
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
3737
|
+
params.momentKind,
|
|
3738
|
+
eventId
|
|
3739
|
+
);
|
|
3740
|
+
db.prepare(
|
|
3741
|
+
"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=?"
|
|
3742
|
+
).run(
|
|
3743
|
+
params.content ?? null,
|
|
3744
|
+
params.externalRef?.source ?? null,
|
|
3745
|
+
params.externalRef?.id ?? null,
|
|
3746
|
+
params.channel ?? null,
|
|
3747
|
+
params.parentRef?.source ?? null,
|
|
3748
|
+
params.parentRef?.id ?? null,
|
|
3749
|
+
params.parentEventId ?? null,
|
|
3750
|
+
params.threadRef?.source ?? null,
|
|
3751
|
+
params.threadRef?.id ?? null,
|
|
3752
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3753
|
+
eventId
|
|
3754
|
+
);
|
|
3755
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3756
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3757
|
+
eventId
|
|
3758
|
+
);
|
|
3407
3759
|
const insertParticipant = db.prepare(
|
|
3408
3760
|
`INSERT INTO event_participants (
|
|
3409
3761
|
event_id, contact_id, role, directionality
|
|
@@ -3446,6 +3798,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
3446
3798
|
now
|
|
3447
3799
|
);
|
|
3448
3800
|
const eventId = Number(result.lastInsertRowid);
|
|
3801
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3802
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3803
|
+
eventId
|
|
3804
|
+
);
|
|
3449
3805
|
const insertParticipant = db.prepare(
|
|
3450
3806
|
`INSERT INTO event_participants (
|
|
3451
3807
|
event_id, contact_id, role, directionality
|
|
@@ -3469,29 +3825,11 @@ var init_persistence = __esm({
|
|
|
3469
3825
|
"src/events/persistence.ts"() {
|
|
3470
3826
|
init_affinity_invariant_error();
|
|
3471
3827
|
init_resolve_now();
|
|
3828
|
+
init_event_metadata();
|
|
3472
3829
|
init_queries2();
|
|
3473
3830
|
}
|
|
3474
3831
|
});
|
|
3475
3832
|
|
|
3476
|
-
// src/events/receipts.ts
|
|
3477
|
-
function buildEventMutationReceipt(primary, partial) {
|
|
3478
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
3479
|
-
return {
|
|
3480
|
-
primary,
|
|
3481
|
-
created: normalized.created,
|
|
3482
|
-
updated: normalized.updated,
|
|
3483
|
-
archived: normalized.archived,
|
|
3484
|
-
removed: normalized.removed,
|
|
3485
|
-
affectedLinks: normalized.affectedLinks,
|
|
3486
|
-
derivedEffects: normalized.derivedEffects
|
|
3487
|
-
};
|
|
3488
|
-
}
|
|
3489
|
-
var init_receipts2 = __esm({
|
|
3490
|
-
"src/events/receipts.ts"() {
|
|
3491
|
-
init_normalize_mutation_receipt();
|
|
3492
|
-
}
|
|
3493
|
-
});
|
|
3494
|
-
|
|
3495
3833
|
// src/graph/betweenness.ts
|
|
3496
3834
|
function computeNodeBetweenness(adjacency) {
|
|
3497
3835
|
const nodes = [...adjacency.keys()];
|
|
@@ -4104,6 +4442,8 @@ function deriveStateAfter(params) {
|
|
|
4104
4442
|
return params.stateBefore;
|
|
4105
4443
|
}
|
|
4106
4444
|
function deriveMomentKind(params) {
|
|
4445
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
4446
|
+
return null;
|
|
4107
4447
|
if (params.rankAfter > params.rankBefore) {
|
|
4108
4448
|
return "breakthrough";
|
|
4109
4449
|
}
|
|
@@ -4133,6 +4473,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
4133
4473
|
if (!link || link.is_structural !== 0) {
|
|
4134
4474
|
throw new AffinityStateError("relational link not found");
|
|
4135
4475
|
}
|
|
4476
|
+
db.prepare(
|
|
4477
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
4478
|
+
).run(
|
|
4479
|
+
params.linkId,
|
|
4480
|
+
JSON.stringify({
|
|
4481
|
+
rank: link.rank ?? 0,
|
|
4482
|
+
affinity: link.affinity ?? 0,
|
|
4483
|
+
trust: link.trust ?? 0,
|
|
4484
|
+
state: link.state ?? "active",
|
|
4485
|
+
cadence_days: link.cadence_days
|
|
4486
|
+
})
|
|
4487
|
+
);
|
|
4136
4488
|
const stateBefore = link.state ?? "active";
|
|
4137
4489
|
const linkKind = link.kind;
|
|
4138
4490
|
const rankBefore = link.rank ?? 0;
|
|
@@ -4177,7 +4529,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
4177
4529
|
);
|
|
4178
4530
|
const weightedBase = base * massPenalty(
|
|
4179
4531
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
4180
|
-
) * salienceMultiplier;
|
|
4532
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
4181
4533
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
4182
4534
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
4183
4535
|
const affinityMass = clamp3(
|
|
@@ -4218,15 +4570,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
4218
4570
|
const latest = db.prepare(
|
|
4219
4571
|
`SELECT occurred_at
|
|
4220
4572
|
FROM link_event_effects
|
|
4221
|
-
WHERE link_id = ?
|
|
4573
|
+
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 <= ?))
|
|
4222
4574
|
ORDER BY occurred_at DESC, event_id DESC
|
|
4223
4575
|
LIMIT 1`
|
|
4224
|
-
).get(params.linkId);
|
|
4576
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
4225
4577
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
4226
4578
|
1,
|
|
4227
4579
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
4228
4580
|
);
|
|
4229
|
-
const
|
|
4581
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
4582
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
4230
4583
|
kind: linkKind,
|
|
4231
4584
|
currentCadenceDays: cadenceBefore,
|
|
4232
4585
|
gapDays
|
|
@@ -4246,7 +4599,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
4246
4599
|
}
|
|
4247
4600
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
4248
4601
|
const trustDelta = trustAfter - trustBefore;
|
|
4249
|
-
const
|
|
4602
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
4603
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
4604
|
+
eventLabel
|
|
4605
|
+
);
|
|
4606
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
4250
4607
|
rankBefore,
|
|
4251
4608
|
rankAfter,
|
|
4252
4609
|
stateBefore,
|
|
@@ -4289,7 +4646,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
4289
4646
|
params.now,
|
|
4290
4647
|
params.now
|
|
4291
4648
|
);
|
|
4292
|
-
|
|
4649
|
+
db.prepare(
|
|
4650
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
4651
|
+
).run(
|
|
4652
|
+
JSON.stringify(params.participant),
|
|
4653
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
4654
|
+
params.eventId,
|
|
4655
|
+
params.linkId
|
|
4656
|
+
);
|
|
4657
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
4293
4658
|
db.prepare(
|
|
4294
4659
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4295
4660
|
).run(params.eventId, params.linkId);
|
|
@@ -4323,6 +4688,7 @@ var init_effects = __esm({
|
|
|
4323
4688
|
init_calendar();
|
|
4324
4689
|
init_mechanics2();
|
|
4325
4690
|
init_affinity_state_error();
|
|
4691
|
+
init_parse_calendar_time();
|
|
4326
4692
|
init_mechanics();
|
|
4327
4693
|
init_queries3();
|
|
4328
4694
|
init_rollups2();
|
|
@@ -4337,6 +4703,7 @@ var init_affinity_conflict_error = __esm({
|
|
|
4337
4703
|
"src/lib/errors/affinity_conflict_error.ts"() {
|
|
4338
4704
|
init_affinity_error();
|
|
4339
4705
|
AffinityConflictError = class extends AffinityError {
|
|
4706
|
+
existingContactId;
|
|
4340
4707
|
code = "CONFLICT";
|
|
4341
4708
|
};
|
|
4342
4709
|
}
|
|
@@ -4403,6 +4770,8 @@ var init_receipts3 = __esm({
|
|
|
4403
4770
|
// src/links/seed_social_link.ts
|
|
4404
4771
|
function seedSocialLink(db, input) {
|
|
4405
4772
|
return withTransaction(db, () => {
|
|
4773
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
4774
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
4406
4775
|
if (!isRelationalLinkKind(input.kind)) {
|
|
4407
4776
|
throw new AffinityValidationError("invalid relational link kind");
|
|
4408
4777
|
}
|
|
@@ -4410,11 +4779,14 @@ function seedSocialLink(db, input) {
|
|
|
4410
4779
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
4411
4780
|
}
|
|
4412
4781
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
4413
|
-
|
|
4782
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
4414
4783
|
db,
|
|
4415
4784
|
input.fromContactId,
|
|
4416
4785
|
input.toContactId
|
|
4417
|
-
)
|
|
4786
|
+
);
|
|
4787
|
+
if (existing) {
|
|
4788
|
+
if (input.ifExists === "return")
|
|
4789
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
4418
4790
|
throw new AffinityConflictError(
|
|
4419
4791
|
"relational link already exists between these contacts"
|
|
4420
4792
|
);
|
|
@@ -4534,15 +4906,36 @@ function ensureRelationalLink(db, params) {
|
|
|
4534
4906
|
};
|
|
4535
4907
|
}
|
|
4536
4908
|
function applyJournalEventMechanics(db, params) {
|
|
4909
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
4910
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
4911
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
4912
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
4537
4913
|
const ownerId = requireOwnerContactId(db);
|
|
4538
|
-
const
|
|
4914
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
4915
|
+
const socialChannel = [
|
|
4916
|
+
"bluesky",
|
|
4917
|
+
"x",
|
|
4918
|
+
"twitter",
|
|
4919
|
+
"mastodon",
|
|
4920
|
+
"github",
|
|
4921
|
+
"instagram"
|
|
4922
|
+
].includes(String(bond?.channel));
|
|
4923
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
4924
|
+
const participants = params.participants.map(
|
|
4925
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
4926
|
+
);
|
|
4927
|
+
if (direction)
|
|
4928
|
+
db.prepare(
|
|
4929
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
4930
|
+
).run(direction, params.eventId);
|
|
4931
|
+
const participantIds = participants.map(
|
|
4539
4932
|
(participant) => participant.contactId
|
|
4540
4933
|
);
|
|
4541
4934
|
const hasOwner = participantIds.includes(ownerId);
|
|
4542
4935
|
const created = [];
|
|
4543
4936
|
const effectInputs = [];
|
|
4544
4937
|
if (hasOwner) {
|
|
4545
|
-
for (const participant of
|
|
4938
|
+
for (const participant of participants) {
|
|
4546
4939
|
if (participant.contactId === ownerId) {
|
|
4547
4940
|
continue;
|
|
4548
4941
|
}
|
|
@@ -4550,13 +4943,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4550
4943
|
fromContactId: ownerId,
|
|
4551
4944
|
toContactId: participant.contactId,
|
|
4552
4945
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
4946
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
4553
4947
|
now: params.now
|
|
4554
4948
|
});
|
|
4555
4949
|
created.push(...ensured.created);
|
|
4556
4950
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
4557
4951
|
}
|
|
4558
|
-
} else if (params.eventType === "observation" &&
|
|
4559
|
-
const [first, second] =
|
|
4952
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
4953
|
+
const [first, second] = participants;
|
|
4560
4954
|
if (first !== void 0 && second !== void 0) {
|
|
4561
4955
|
const a = Math.min(first.contactId, second.contactId);
|
|
4562
4956
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -4591,7 +4985,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4591
4985
|
eventType: params.eventType,
|
|
4592
4986
|
occurredAt: params.occurredAt,
|
|
4593
4987
|
significance: params.significance,
|
|
4594
|
-
participant:
|
|
4988
|
+
participant: {
|
|
4989
|
+
...input.participant,
|
|
4990
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
4991
|
+
},
|
|
4595
4992
|
provenance: params.provenance,
|
|
4596
4993
|
now: params.now
|
|
4597
4994
|
})
|
|
@@ -4624,6 +5021,8 @@ var init_apply_journal_event_mechanics = __esm({
|
|
|
4624
5021
|
function recordCommitment(db, input) {
|
|
4625
5022
|
return withTransaction(db, () => {
|
|
4626
5023
|
validateSocialEventInput(input);
|
|
5024
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5025
|
+
if (existing) return existing;
|
|
4627
5026
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
4628
5027
|
throw new AffinityValidationError(
|
|
4629
5028
|
"commitmentType must be promise or agreement"
|
|
@@ -4638,11 +5037,12 @@ function recordCommitment(db, input) {
|
|
|
4638
5037
|
assertOwnerParticipates(ownerId, ids);
|
|
4639
5038
|
const now = resolveNow(input.now);
|
|
4640
5039
|
const eventId = insertJournalEvent(db, {
|
|
5040
|
+
...input,
|
|
4641
5041
|
type: input.commitmentType,
|
|
4642
5042
|
occurredAt: input.occurredAt,
|
|
4643
|
-
|
|
5043
|
+
occurredOn: input.occurredOn,
|
|
4644
5044
|
significance: input.significance,
|
|
4645
|
-
momentKind: null,
|
|
5045
|
+
momentKind: input.momentKind ?? null,
|
|
4646
5046
|
participants: input.participants,
|
|
4647
5047
|
now
|
|
4648
5048
|
});
|
|
@@ -4651,15 +5051,25 @@ function recordCommitment(db, input) {
|
|
|
4651
5051
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
4652
5052
|
) VALUES (?, ?, ?, ?, ?)`
|
|
4653
5053
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
5054
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
5055
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
5056
|
+
eventId
|
|
5057
|
+
);
|
|
4654
5058
|
const mechanics = applyJournalEventMechanics(db, {
|
|
4655
5059
|
eventId,
|
|
4656
5060
|
eventType: input.commitmentType,
|
|
4657
5061
|
occurredAt: input.occurredAt,
|
|
5062
|
+
occurredOn: input.occurredOn,
|
|
4658
5063
|
significance: input.significance,
|
|
4659
5064
|
participants: input.participants,
|
|
4660
5065
|
provenance: input.provenance,
|
|
4661
5066
|
now
|
|
4662
5067
|
});
|
|
5068
|
+
if (input.momentKind !== void 0)
|
|
5069
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5070
|
+
input.momentKind,
|
|
5071
|
+
eventId
|
|
5072
|
+
);
|
|
4663
5073
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
4664
5074
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4665
5075
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -4672,12 +5082,13 @@ var init_record_commitment = __esm({
|
|
|
4672
5082
|
init_queries();
|
|
4673
5083
|
init_loaders();
|
|
4674
5084
|
init_persistence();
|
|
4675
|
-
|
|
5085
|
+
init_receipts();
|
|
4676
5086
|
init_validators();
|
|
4677
5087
|
init_affinity_validation_error();
|
|
4678
5088
|
init_resolve_now();
|
|
4679
5089
|
init_with_transaction();
|
|
4680
5090
|
init_apply_journal_event_mechanics();
|
|
5091
|
+
init_event_metadata();
|
|
4681
5092
|
}
|
|
4682
5093
|
});
|
|
4683
5094
|
|
|
@@ -4746,6 +5157,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4746
5157
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
4747
5158
|
validateSocialEventInput({
|
|
4748
5159
|
occurredAt,
|
|
5160
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4749
5161
|
summary,
|
|
4750
5162
|
significance,
|
|
4751
5163
|
participants,
|
|
@@ -4762,6 +5174,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4762
5174
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4763
5175
|
type: eventType,
|
|
4764
5176
|
occurredAt,
|
|
5177
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4765
5178
|
summary,
|
|
4766
5179
|
significance,
|
|
4767
5180
|
momentKind: null,
|
|
@@ -4800,7 +5213,7 @@ var init_resolve_commitment = __esm({
|
|
|
4800
5213
|
init_loaders();
|
|
4801
5214
|
init_persistence();
|
|
4802
5215
|
init_queries2();
|
|
4803
|
-
|
|
5216
|
+
init_receipts();
|
|
4804
5217
|
init_validators();
|
|
4805
5218
|
init_affinity_conflict_error();
|
|
4806
5219
|
init_affinity_invariant_error();
|
|
@@ -4839,7 +5252,13 @@ function prepareParticipants(db, raw) {
|
|
|
4839
5252
|
}
|
|
4840
5253
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
4841
5254
|
const input = structuredClone(rawInput);
|
|
4842
|
-
const inputError = validateToolInput(
|
|
5255
|
+
const inputError = validateToolInput(
|
|
5256
|
+
manageCommitmentTool.inputSchema,
|
|
5257
|
+
input,
|
|
5258
|
+
"input",
|
|
5259
|
+
Date.now(),
|
|
5260
|
+
true
|
|
5261
|
+
);
|
|
4843
5262
|
if (inputError)
|
|
4844
5263
|
return toolFailure(
|
|
4845
5264
|
"protocol",
|
|
@@ -4869,6 +5288,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
4869
5288
|
"commitment"
|
|
4870
5289
|
);
|
|
4871
5290
|
}
|
|
5291
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
5292
|
+
return toolFailure(
|
|
5293
|
+
"protocol",
|
|
5294
|
+
"invalid_input",
|
|
5295
|
+
"Missing commitment.",
|
|
5296
|
+
"commitmentEventId is required for resolve."
|
|
5297
|
+
);
|
|
5298
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
5299
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
5300
|
+
return toolFailure(
|
|
5301
|
+
"domain",
|
|
5302
|
+
"invalid_input",
|
|
5303
|
+
"Invalid commitment.",
|
|
5304
|
+
"not a commitment event"
|
|
5305
|
+
);
|
|
4872
5306
|
return mutationToolResult(
|
|
4873
5307
|
"resolve",
|
|
4874
5308
|
resolveCommitment(
|
|
@@ -4887,6 +5321,7 @@ var init_manage_commitment_tool = __esm({
|
|
|
4887
5321
|
init_queries();
|
|
4888
5322
|
init_record_commitment();
|
|
4889
5323
|
init_resolve_commitment();
|
|
5324
|
+
init_event_metadata_schema();
|
|
4890
5325
|
init_tool_metadata();
|
|
4891
5326
|
init_tool_mutation();
|
|
4892
5327
|
init_tool_names();
|
|
@@ -4919,15 +5354,21 @@ var init_manage_commitment_tool = __esm({
|
|
|
4919
5354
|
"promise",
|
|
4920
5355
|
"agreement"
|
|
4921
5356
|
]),
|
|
5357
|
+
...eventMetadataSchema,
|
|
4922
5358
|
occurredAt: integerSchema(
|
|
4923
5359
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
4924
5360
|
),
|
|
4925
5361
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
4926
5362
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
4927
5363
|
dueAt: integerSchema("Optional due timestamp."),
|
|
5364
|
+
dueOn: stringSchema("Due calendar day."),
|
|
4928
5365
|
participants: arraySchema(
|
|
4929
5366
|
objectSchema(
|
|
4930
5367
|
{
|
|
5368
|
+
contact: objectSchema(
|
|
5369
|
+
{ contactId: integerSchema("Contact id.") },
|
|
5370
|
+
["contactId"]
|
|
5371
|
+
),
|
|
4931
5372
|
contactId: integerSchema(
|
|
4932
5373
|
"Participant contact id. The owner contact MUST be included."
|
|
4933
5374
|
),
|
|
@@ -4940,13 +5381,7 @@ var init_manage_commitment_tool = __esm({
|
|
|
4940
5381
|
),
|
|
4941
5382
|
now: integerSchema("Optional timestamp override.")
|
|
4942
5383
|
},
|
|
4943
|
-
[
|
|
4944
|
-
"commitmentType",
|
|
4945
|
-
"occurredAt",
|
|
4946
|
-
"summary",
|
|
4947
|
-
"significance",
|
|
4948
|
-
"participants"
|
|
4949
|
-
],
|
|
5384
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
4950
5385
|
"Commitment payload. Required when action=record."
|
|
4951
5386
|
),
|
|
4952
5387
|
commitmentEventId: integerSchema(
|
|
@@ -4961,6 +5396,19 @@ var init_manage_commitment_tool = __esm({
|
|
|
4961
5396
|
now: integerSchema("Optional timestamp."),
|
|
4962
5397
|
resolvingEvent: objectSchema(
|
|
4963
5398
|
{
|
|
5399
|
+
occurredOn: stringSchema("Calendar day."),
|
|
5400
|
+
provenance: {},
|
|
5401
|
+
participants: arraySchema(
|
|
5402
|
+
objectSchema(
|
|
5403
|
+
{
|
|
5404
|
+
contactId: integerSchema("Contact id."),
|
|
5405
|
+
role: stringSchema("Role."),
|
|
5406
|
+
directionality: stringSchema("Directionality.")
|
|
5407
|
+
},
|
|
5408
|
+
["contactId", "role"]
|
|
5409
|
+
),
|
|
5410
|
+
"Resolving participants."
|
|
5411
|
+
),
|
|
4964
5412
|
type: stringSchema("Optional resolving event type."),
|
|
4965
5413
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
4966
5414
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -5054,30 +5502,50 @@ var init_delete_contact = __esm({
|
|
|
5054
5502
|
}
|
|
5055
5503
|
});
|
|
5056
5504
|
|
|
5057
|
-
// src/contacts/
|
|
5058
|
-
function
|
|
5059
|
-
|
|
5505
|
+
// src/contacts/export_rows.ts
|
|
5506
|
+
function exportRows(db, sql, ...params) {
|
|
5507
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
5060
5508
|
const result = {};
|
|
5061
5509
|
for (const [key, value] of Object.entries(row)) {
|
|
5062
5510
|
const name = key.replace(
|
|
5063
5511
|
/_([a-z])/g,
|
|
5064
5512
|
(_, letter) => letter.toUpperCase()
|
|
5065
5513
|
);
|
|
5066
|
-
result[name] =
|
|
5067
|
-
|
|
5514
|
+
result[name] = [
|
|
5515
|
+
"affects_bond",
|
|
5516
|
+
"is_owner",
|
|
5517
|
+
"is_structural",
|
|
5518
|
+
"verified",
|
|
5519
|
+
"counts_for_recency",
|
|
5520
|
+
"manual"
|
|
5521
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
5522
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
5068
5523
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
5069
5524
|
if (key.endsWith("_json") && typeof value === "string")
|
|
5070
5525
|
result[name] = JSON.parse(value);
|
|
5071
5526
|
}
|
|
5072
5527
|
return result;
|
|
5073
5528
|
});
|
|
5529
|
+
}
|
|
5530
|
+
var init_export_rows = __esm({
|
|
5531
|
+
"src/contacts/export_rows.ts"() {
|
|
5532
|
+
}
|
|
5533
|
+
});
|
|
5534
|
+
|
|
5535
|
+
// src/contacts/export_contact.ts
|
|
5536
|
+
function exportContact(db, id) {
|
|
5537
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
5074
5538
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
5075
5539
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
5076
5540
|
const events = query(
|
|
5077
|
-
"SELECT DISTINCT e
|
|
5541
|
+
"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",
|
|
5078
5542
|
id
|
|
5079
5543
|
).map((event) => ({
|
|
5080
5544
|
...event,
|
|
5545
|
+
attributes: query(
|
|
5546
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5547
|
+
event.id
|
|
5548
|
+
),
|
|
5081
5549
|
participants: query(
|
|
5082
5550
|
"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",
|
|
5083
5551
|
event.id
|
|
@@ -5121,22 +5589,46 @@ function exportContact(db, id) {
|
|
|
5121
5589
|
var init_export_contact = __esm({
|
|
5122
5590
|
"src/contacts/export_contact.ts"() {
|
|
5123
5591
|
init_affinity_not_found_error();
|
|
5592
|
+
init_export_rows();
|
|
5124
5593
|
}
|
|
5125
5594
|
});
|
|
5126
5595
|
|
|
5127
5596
|
// src/contacts/export_affinity.ts
|
|
5128
5597
|
function exportAffinity(db) {
|
|
5129
5598
|
const now = Date.now();
|
|
5599
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
5600
|
+
const events = exportRows(
|
|
5601
|
+
db,
|
|
5602
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
5603
|
+
).map((event) => ({
|
|
5604
|
+
...event,
|
|
5605
|
+
attributes: exportRows(
|
|
5606
|
+
db,
|
|
5607
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5608
|
+
event.id
|
|
5609
|
+
),
|
|
5610
|
+
participants: exportRows(
|
|
5611
|
+
db,
|
|
5612
|
+
"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",
|
|
5613
|
+
event.id
|
|
5614
|
+
)
|
|
5615
|
+
}));
|
|
5130
5616
|
return {
|
|
5131
5617
|
version: 1,
|
|
5132
5618
|
exportedAt: now,
|
|
5133
5619
|
exportedAtIso: new Date(now).toISOString(),
|
|
5134
|
-
|
|
5620
|
+
events,
|
|
5621
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
5622
|
+
...record,
|
|
5623
|
+
eventIds: events2.map((event) => event.id),
|
|
5624
|
+
dateIds: dates.map((date) => date.id)
|
|
5625
|
+
}))
|
|
5135
5626
|
};
|
|
5136
5627
|
}
|
|
5137
5628
|
var init_export_affinity = __esm({
|
|
5138
5629
|
"src/contacts/export_affinity.ts"() {
|
|
5139
5630
|
init_export_contact();
|
|
5631
|
+
init_export_rows();
|
|
5140
5632
|
}
|
|
5141
5633
|
});
|
|
5142
5634
|
|
|
@@ -5227,7 +5719,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
5227
5719
|
}
|
|
5228
5720
|
if (to === "merged") {
|
|
5229
5721
|
throw new AffinityInvariantError(
|
|
5230
|
-
"use
|
|
5722
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
5231
5723
|
);
|
|
5232
5724
|
}
|
|
5233
5725
|
if (from === to) {
|
|
@@ -5300,147 +5792,86 @@ var init_set_contact_lifecycle = __esm({
|
|
|
5300
5792
|
}
|
|
5301
5793
|
});
|
|
5302
5794
|
|
|
5303
|
-
// src/
|
|
5304
|
-
function
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5795
|
+
// src/identities/queries.ts
|
|
5796
|
+
function getIdentityRowById(db, id) {
|
|
5797
|
+
const row = db.prepare(
|
|
5798
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5799
|
+
created_at, updated_at, removed_at
|
|
5800
|
+
FROM identities
|
|
5801
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
5802
|
+
).get(id);
|
|
5803
|
+
if (row === void 0) {
|
|
5804
|
+
return null;
|
|
5805
|
+
}
|
|
5806
|
+
return row;
|
|
5807
|
+
}
|
|
5808
|
+
var init_queries5 = __esm({
|
|
5809
|
+
"src/identities/queries.ts"() {
|
|
5810
|
+
}
|
|
5811
|
+
});
|
|
5812
|
+
|
|
5813
|
+
// src/identities/receipts.ts
|
|
5814
|
+
function buildIdentityMutationReceipt(primary, partial) {
|
|
5815
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
5816
|
+
return {
|
|
5817
|
+
primary,
|
|
5818
|
+
created: normalized.created,
|
|
5819
|
+
updated: normalized.updated,
|
|
5820
|
+
archived: normalized.archived,
|
|
5821
|
+
removed: normalized.removed,
|
|
5822
|
+
affectedLinks: normalized.affectedLinks,
|
|
5823
|
+
derivedEffects: normalized.derivedEffects
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
var init_receipts5 = __esm({
|
|
5827
|
+
"src/identities/receipts.ts"() {
|
|
5828
|
+
init_normalize_mutation_receipt();
|
|
5829
|
+
}
|
|
5830
|
+
});
|
|
5831
|
+
|
|
5832
|
+
// src/identities/validators.ts
|
|
5833
|
+
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5834
|
+
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
5835
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
5836
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey) : db.prepare(
|
|
5837
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL AND id != ?"
|
|
5838
|
+
).get(
|
|
5839
|
+
normalizedKey,
|
|
5840
|
+
legacyIdentityKey(normalizedKey),
|
|
5841
|
+
normalizedKey,
|
|
5842
|
+
excludeIdentityId
|
|
5843
|
+
);
|
|
5844
|
+
if (row !== void 0) {
|
|
5845
|
+
const owner = db.prepare(
|
|
5846
|
+
"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"
|
|
5847
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey);
|
|
5848
|
+
throw new AffinityConflictError(
|
|
5849
|
+
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
5850
|
+
);
|
|
5851
|
+
}
|
|
5852
|
+
}
|
|
5853
|
+
var init_validators5 = __esm({
|
|
5854
|
+
"src/identities/validators.ts"() {
|
|
5855
|
+
init_affinity_conflict_error();
|
|
5856
|
+
init_normalize();
|
|
5857
|
+
}
|
|
5858
|
+
});
|
|
5859
|
+
|
|
5860
|
+
// src/identities/validate_identity_input.ts
|
|
5861
|
+
function validateIdentityInput(type, rawValue) {
|
|
5862
|
+
const kind = type.toLowerCase();
|
|
5863
|
+
const value = normalizeIdentityValue(kind, rawValue);
|
|
5864
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(kind) && !/^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/.test(value))
|
|
5865
|
+
throw new AffinityValidationError(
|
|
5866
|
+
"did value must match did:<method>:<identifier> (for example did:plc:abc123)"
|
|
5867
|
+
);
|
|
5868
|
+
if (handleIdentityTypes.has(kind)) {
|
|
5869
|
+
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_.-]+$/;
|
|
5870
|
+
if (!pattern.test(value))
|
|
5314
5871
|
throw new AffinityValidationError(
|
|
5315
|
-
"
|
|
5872
|
+
`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)"}`
|
|
5316
5873
|
);
|
|
5317
|
-
}
|
|
5318
|
-
const now = resolveNow(input.now);
|
|
5319
|
-
if (input.bootstrapOwner) {
|
|
5320
|
-
const existing = db.prepare(
|
|
5321
|
-
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
5322
|
-
).get();
|
|
5323
|
-
if (existing !== void 0) {
|
|
5324
|
-
throw new AffinityConflictError(
|
|
5325
|
-
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
5326
|
-
);
|
|
5327
|
-
}
|
|
5328
|
-
}
|
|
5329
|
-
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
5330
|
-
const result = db.prepare(
|
|
5331
|
-
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
5332
|
-
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
5333
|
-
).run(name, input.kind, isOwner, now, now);
|
|
5334
|
-
const id = Number(result.lastInsertRowid);
|
|
5335
|
-
const row = getContactRowById(db, id);
|
|
5336
|
-
if (!row) {
|
|
5337
|
-
throw new AffinityInvariantError("inserted contact row not found");
|
|
5338
|
-
}
|
|
5339
|
-
refreshContactRollup(db, id, now);
|
|
5340
|
-
const primary = mapContactRowToContactListItem(row);
|
|
5341
|
-
return buildContactMutationReceipt(primary, {
|
|
5342
|
-
created: [{ kind: "contact", id }]
|
|
5343
|
-
});
|
|
5344
|
-
});
|
|
5345
|
-
}
|
|
5346
|
-
var KINDS;
|
|
5347
|
-
var init_create_contact = __esm({
|
|
5348
|
-
"src/contacts/create_contact.ts"() {
|
|
5349
|
-
init_mappers4();
|
|
5350
|
-
init_queries();
|
|
5351
|
-
init_receipts4();
|
|
5352
|
-
init_rollups();
|
|
5353
|
-
init_affinity_conflict_error();
|
|
5354
|
-
init_affinity_invariant_error();
|
|
5355
|
-
init_affinity_validation_error();
|
|
5356
|
-
init_resolve_now();
|
|
5357
|
-
init_with_transaction();
|
|
5358
|
-
KINDS = /* @__PURE__ */ new Set([
|
|
5359
|
-
"human",
|
|
5360
|
-
"group",
|
|
5361
|
-
"company",
|
|
5362
|
-
"team",
|
|
5363
|
-
"pet",
|
|
5364
|
-
"service",
|
|
5365
|
-
"other"
|
|
5366
|
-
]);
|
|
5367
|
-
}
|
|
5368
|
-
});
|
|
5369
|
-
|
|
5370
|
-
// src/contacts/fold_search_text.ts
|
|
5371
|
-
function foldSearchText(value) {
|
|
5372
|
-
let folded = value.toLowerCase();
|
|
5373
|
-
if (/[^\u0020-\u007e]/.test(folded))
|
|
5374
|
-
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
5375
|
-
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
5376
|
-
}
|
|
5377
|
-
var init_fold_search_text = __esm({
|
|
5378
|
-
"src/contacts/fold_search_text.ts"() {
|
|
5379
|
-
}
|
|
5380
|
-
});
|
|
5381
|
-
|
|
5382
|
-
// src/identities/queries.ts
|
|
5383
|
-
function getIdentityRowById(db, id) {
|
|
5384
|
-
const row = db.prepare(
|
|
5385
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5386
|
-
created_at, updated_at, removed_at
|
|
5387
|
-
FROM identities
|
|
5388
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
5389
|
-
).get(id);
|
|
5390
|
-
if (row === void 0) {
|
|
5391
|
-
return null;
|
|
5392
|
-
}
|
|
5393
|
-
return row;
|
|
5394
|
-
}
|
|
5395
|
-
var init_queries5 = __esm({
|
|
5396
|
-
"src/identities/queries.ts"() {
|
|
5397
|
-
}
|
|
5398
|
-
});
|
|
5399
|
-
|
|
5400
|
-
// src/identities/receipts.ts
|
|
5401
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
5402
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
5403
|
-
return {
|
|
5404
|
-
primary,
|
|
5405
|
-
created: normalized.created,
|
|
5406
|
-
updated: normalized.updated,
|
|
5407
|
-
archived: normalized.archived,
|
|
5408
|
-
removed: normalized.removed,
|
|
5409
|
-
affectedLinks: normalized.affectedLinks,
|
|
5410
|
-
derivedEffects: normalized.derivedEffects
|
|
5411
|
-
};
|
|
5412
|
-
}
|
|
5413
|
-
var init_receipts5 = __esm({
|
|
5414
|
-
"src/identities/receipts.ts"() {
|
|
5415
|
-
init_normalize_mutation_receipt();
|
|
5416
|
-
}
|
|
5417
|
-
});
|
|
5418
|
-
|
|
5419
|
-
// src/identities/validators.ts
|
|
5420
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5421
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
5422
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5423
|
-
).get(normalizedKey) : db.prepare(
|
|
5424
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
5425
|
-
).get(normalizedKey, excludeIdentityId);
|
|
5426
|
-
if (row !== void 0) {
|
|
5427
|
-
const owner = db.prepare(
|
|
5428
|
-
"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"
|
|
5429
|
-
).get(normalizedKey);
|
|
5430
|
-
throw new AffinityConflictError(
|
|
5431
|
-
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
5432
|
-
);
|
|
5433
5874
|
}
|
|
5434
|
-
}
|
|
5435
|
-
var init_validators5 = __esm({
|
|
5436
|
-
"src/identities/validators.ts"() {
|
|
5437
|
-
init_affinity_conflict_error();
|
|
5438
|
-
}
|
|
5439
|
-
});
|
|
5440
|
-
|
|
5441
|
-
// src/identities/validate_identity_input.ts
|
|
5442
|
-
function validateIdentityInput(type, value) {
|
|
5443
|
-
const kind = type.toLowerCase();
|
|
5444
5875
|
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
5445
5876
|
throw new AffinityValidationError(
|
|
5446
5877
|
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
@@ -5471,7 +5902,10 @@ var identityTypes;
|
|
|
5471
5902
|
var init_validate_identity_input = __esm({
|
|
5472
5903
|
"src/identities/validate_identity_input.ts"() {
|
|
5473
5904
|
init_affinity_validation_error();
|
|
5905
|
+
init_normalize();
|
|
5474
5906
|
identityTypes = [
|
|
5907
|
+
"did",
|
|
5908
|
+
"atproto_did",
|
|
5475
5909
|
"email",
|
|
5476
5910
|
"phone",
|
|
5477
5911
|
"url",
|
|
@@ -5512,12 +5946,27 @@ function addIdentity(db, contactId, input) {
|
|
|
5512
5946
|
throw new AffinityStateError("merged contact is read-only");
|
|
5513
5947
|
}
|
|
5514
5948
|
const type = input.type.trim();
|
|
5515
|
-
const value = input.value
|
|
5949
|
+
const value = normalizeIdentityValue(type, input.value);
|
|
5516
5950
|
if (type.length === 0 || value.length === 0) {
|
|
5517
5951
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5518
5952
|
}
|
|
5519
5953
|
validateIdentityInput(type, value);
|
|
5954
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5955
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
5520
5956
|
const nk = normalizeIdentityKey(type, value);
|
|
5957
|
+
if (input.ifExists === "return") {
|
|
5958
|
+
const existing = db.prepare(
|
|
5959
|
+
"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"
|
|
5960
|
+
).get(contactId, nk, legacyIdentityKey(nk), nk);
|
|
5961
|
+
if (existing) {
|
|
5962
|
+
const row2 = getIdentityRowById(db, Number(existing.id));
|
|
5963
|
+
if (row2)
|
|
5964
|
+
return buildIdentityMutationReceipt(
|
|
5965
|
+
mapIdentityRowToIdentityRecord(row2),
|
|
5966
|
+
{}
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5521
5970
|
assertNoIdentityCollision(db, nk);
|
|
5522
5971
|
const now = resolveNow(input.now);
|
|
5523
5972
|
const verified = input.verified ? 1 : 0;
|
|
@@ -5569,31 +6018,266 @@ var init_add_identity = __esm({
|
|
|
5569
6018
|
}
|
|
5570
6019
|
});
|
|
5571
6020
|
|
|
5572
|
-
// src/
|
|
5573
|
-
function
|
|
6021
|
+
// src/contacts/create_contact.ts
|
|
6022
|
+
function createContact(db, input) {
|
|
5574
6023
|
return withTransaction(db, () => {
|
|
5575
|
-
const
|
|
5576
|
-
|
|
5577
|
-
|
|
6024
|
+
const name = input.name.trim();
|
|
6025
|
+
if (name.length === 0) {
|
|
6026
|
+
throw new AffinityValidationError("name must be non-empty");
|
|
6027
|
+
}
|
|
6028
|
+
if (!KINDS.has(input.kind)) {
|
|
6029
|
+
throw new AffinityValidationError("invalid contact kind");
|
|
6030
|
+
}
|
|
6031
|
+
if (input.isOwner !== void 0) {
|
|
6032
|
+
throw new AffinityValidationError(
|
|
6033
|
+
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
6034
|
+
);
|
|
6035
|
+
}
|
|
6036
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
6037
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
6038
|
+
const matches = (input.identities ?? []).flatMap((identity) => {
|
|
5578
6039
|
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
6040
|
+
const key = normalizeIdentityKey(identity.type, identity.value);
|
|
6041
|
+
const row2 = db.prepare(
|
|
6042
|
+
`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)))`
|
|
6043
|
+
).get(key, legacyIdentityKey(key), key);
|
|
6044
|
+
return row2 ? [{ ...row2, identity }] : [];
|
|
6045
|
+
});
|
|
6046
|
+
const match = matches[0];
|
|
6047
|
+
if (match) {
|
|
6048
|
+
if (input.ifExists === "return") {
|
|
6049
|
+
const existing = getContactRowById(db, match.id);
|
|
6050
|
+
if (!existing)
|
|
6051
|
+
throw new AffinityInvariantError("identity owner not found");
|
|
6052
|
+
const primary2 = mapContactRowToContactListItem(existing);
|
|
6053
|
+
return Object.assign(buildContactMutationReceipt(primary2, {}), {
|
|
6054
|
+
outcome: "no_op",
|
|
6055
|
+
matchedIdentities: matches.map((m) => m.identity)
|
|
6056
|
+
});
|
|
6057
|
+
}
|
|
6058
|
+
const error = new AffinityConflictError(
|
|
6059
|
+
`identity ${normalizeIdentityKey(match.identity.type, match.identity.value).replace(":", " ")} already belongs to #${match.id} ${match.name}`
|
|
6060
|
+
);
|
|
6061
|
+
error.existingContactId = match.id;
|
|
6062
|
+
throw error;
|
|
6063
|
+
}
|
|
6064
|
+
const now = resolveNow(input.now);
|
|
6065
|
+
if (input.bootstrapOwner) {
|
|
5579
6066
|
const existing = db.prepare(
|
|
5580
|
-
"SELECT
|
|
5581
|
-
).get(
|
|
5582
|
-
if (existing) {
|
|
5583
|
-
|
|
5584
|
-
|
|
6067
|
+
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
6068
|
+
).get();
|
|
6069
|
+
if (existing !== void 0) {
|
|
6070
|
+
throw new AffinityConflictError(
|
|
6071
|
+
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
6072
|
+
);
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
6076
|
+
const result = db.prepare(
|
|
6077
|
+
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
6078
|
+
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
6079
|
+
).run(name, input.kind, isOwner, now, now);
|
|
6080
|
+
const id = Number(result.lastInsertRowid);
|
|
6081
|
+
const row = getContactRowById(db, id);
|
|
6082
|
+
if (!row) {
|
|
6083
|
+
throw new AffinityInvariantError("inserted contact row not found");
|
|
6084
|
+
}
|
|
6085
|
+
refreshContactRollup(db, id, now);
|
|
6086
|
+
const primary = mapContactRowToContactListItem(row);
|
|
6087
|
+
const receipt = buildContactMutationReceipt(primary, {
|
|
6088
|
+
created: [{ kind: "contact", id }]
|
|
6089
|
+
});
|
|
6090
|
+
for (const identity of input.identities ?? [])
|
|
6091
|
+
receipt.created.push(...addIdentity(db, id, identity).created);
|
|
6092
|
+
return receipt;
|
|
6093
|
+
});
|
|
6094
|
+
}
|
|
6095
|
+
var KINDS;
|
|
6096
|
+
var init_create_contact = __esm({
|
|
6097
|
+
"src/contacts/create_contact.ts"() {
|
|
6098
|
+
init_mappers4();
|
|
6099
|
+
init_queries();
|
|
6100
|
+
init_receipts4();
|
|
6101
|
+
init_rollups();
|
|
6102
|
+
init_add_identity();
|
|
6103
|
+
init_normalize();
|
|
6104
|
+
init_validate_identity_input();
|
|
6105
|
+
init_affinity_conflict_error();
|
|
6106
|
+
init_affinity_invariant_error();
|
|
6107
|
+
init_affinity_validation_error();
|
|
6108
|
+
init_resolve_now();
|
|
6109
|
+
init_with_transaction();
|
|
6110
|
+
KINDS = /* @__PURE__ */ new Set([
|
|
6111
|
+
"human",
|
|
6112
|
+
"group",
|
|
6113
|
+
"company",
|
|
6114
|
+
"team",
|
|
6115
|
+
"pet",
|
|
6116
|
+
"service",
|
|
6117
|
+
"other"
|
|
6118
|
+
]);
|
|
6119
|
+
}
|
|
6120
|
+
});
|
|
6121
|
+
|
|
6122
|
+
// src/contacts/fold_search_text.ts
|
|
6123
|
+
function foldSearchText(value) {
|
|
6124
|
+
let folded = value.toLowerCase();
|
|
6125
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
6126
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
6127
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
6128
|
+
}
|
|
6129
|
+
var init_fold_search_text = __esm({
|
|
6130
|
+
"src/contacts/fold_search_text.ts"() {
|
|
6131
|
+
}
|
|
6132
|
+
});
|
|
6133
|
+
|
|
6134
|
+
// src/contacts/list_duplicate_candidates.ts
|
|
6135
|
+
function pairKey(leftContactId, rightContactId) {
|
|
6136
|
+
const left = Math.min(leftContactId, rightContactId);
|
|
6137
|
+
const right = Math.max(leftContactId, rightContactId);
|
|
6138
|
+
return `${left}:${right}`;
|
|
6139
|
+
}
|
|
6140
|
+
function normalizeName(name) {
|
|
6141
|
+
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6142
|
+
}
|
|
6143
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
6144
|
+
const left = normalizeName(leftInput);
|
|
6145
|
+
const right = normalizeName(rightInput);
|
|
6146
|
+
if (left === right) {
|
|
6147
|
+
return 0.92;
|
|
6148
|
+
}
|
|
6149
|
+
if (left.length >= 3 && right.length >= 3) {
|
|
6150
|
+
if (left.includes(right) || right.includes(left)) {
|
|
6151
|
+
return 0.72;
|
|
6152
|
+
}
|
|
6153
|
+
const leftTokens = new Set(left.split(" "));
|
|
6154
|
+
const rightTokens = new Set(right.split(" "));
|
|
6155
|
+
const overlap = [...leftTokens].filter(
|
|
6156
|
+
(token) => rightTokens.has(token)
|
|
6157
|
+
).length;
|
|
6158
|
+
if (overlap > 0) {
|
|
6159
|
+
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
return 0;
|
|
6163
|
+
}
|
|
6164
|
+
function listDuplicateCandidates(db, filters, options) {
|
|
6165
|
+
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
6166
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
6167
|
+
const subset = filters?.contactIds;
|
|
6168
|
+
const contacts = db.prepare(
|
|
6169
|
+
`SELECT id, name
|
|
6170
|
+
FROM contacts
|
|
6171
|
+
WHERE deleted_at IS NULL
|
|
6172
|
+
AND lifecycle_state != 'merged'`
|
|
6173
|
+
).all();
|
|
6174
|
+
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
6175
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
6176
|
+
const exactMatches = db.prepare(
|
|
6177
|
+
`SELECT
|
|
6178
|
+
i1.contact_id AS left_contact_id,
|
|
6179
|
+
i2.contact_id AS right_contact_id,
|
|
6180
|
+
i1.normalized_key
|
|
6181
|
+
FROM identities i1
|
|
6182
|
+
INNER JOIN identities i2
|
|
6183
|
+
ON i1.normalized_key = i2.normalized_key
|
|
6184
|
+
AND i1.contact_id < i2.contact_id
|
|
6185
|
+
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
6186
|
+
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
6187
|
+
WHERE i1.removed_at IS NULL
|
|
6188
|
+
AND i2.removed_at IS NULL
|
|
6189
|
+
AND c1.deleted_at IS NULL
|
|
6190
|
+
AND c2.deleted_at IS NULL
|
|
6191
|
+
AND c1.lifecycle_state != 'merged'
|
|
6192
|
+
AND c2.lifecycle_state != 'merged'`
|
|
6193
|
+
).all();
|
|
6194
|
+
for (const match of exactMatches) {
|
|
6195
|
+
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
6196
|
+
continue;
|
|
6197
|
+
}
|
|
6198
|
+
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
6199
|
+
leftContactId: match.left_contact_id,
|
|
6200
|
+
rightContactId: match.right_contact_id,
|
|
6201
|
+
matchReason: `exact identity ${match.normalized_key}`,
|
|
6202
|
+
matchScore: 1
|
|
6203
|
+
});
|
|
6204
|
+
}
|
|
6205
|
+
if (filters?.exactOnly !== true) {
|
|
6206
|
+
for (let index = 0; index < contacts.length; index += 1) {
|
|
6207
|
+
const left = contacts[index];
|
|
6208
|
+
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
5585
6209
|
continue;
|
|
5586
6210
|
}
|
|
5587
|
-
const
|
|
5588
|
-
|
|
6211
|
+
const leftName = normalizeName(left.name);
|
|
6212
|
+
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
6213
|
+
const right = contacts[offsetIndex];
|
|
6214
|
+
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
6215
|
+
continue;
|
|
6216
|
+
}
|
|
6217
|
+
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
6218
|
+
if (score <= 0) {
|
|
6219
|
+
continue;
|
|
6220
|
+
}
|
|
6221
|
+
const key = pairKey(left.id, right.id);
|
|
6222
|
+
if (!pairs.has(key)) {
|
|
6223
|
+
pairs.set(key, {
|
|
6224
|
+
leftContactId: left.id,
|
|
6225
|
+
rightContactId: right.id,
|
|
6226
|
+
matchReason: "name similarity",
|
|
6227
|
+
matchScore: score
|
|
6228
|
+
});
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
5589
6231
|
}
|
|
6232
|
+
}
|
|
6233
|
+
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
6234
|
+
const dismissedSet = new Set(
|
|
6235
|
+
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
6236
|
+
);
|
|
6237
|
+
const includeDismissed = filters?.includeDismissed === true;
|
|
6238
|
+
for (const candidate of pairs.values()) {
|
|
6239
|
+
if (dismissedSet.has(
|
|
6240
|
+
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
6241
|
+
)) {
|
|
6242
|
+
candidate.dismissed = true;
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
const minScore = filters?.minScore ?? 0;
|
|
6246
|
+
return [...pairs.values()].filter((candidate) => {
|
|
6247
|
+
if (candidate.matchScore < minScore) return false;
|
|
6248
|
+
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
6249
|
+
return true;
|
|
6250
|
+
}).sort((left, right) => {
|
|
6251
|
+
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
6252
|
+
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
6253
|
+
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
6254
|
+
}).slice(offset, offset + limit);
|
|
6255
|
+
}
|
|
6256
|
+
var init_list_duplicate_candidates = __esm({
|
|
6257
|
+
"src/contacts/list_duplicate_candidates.ts"() {
|
|
6258
|
+
init_assert_default_ordering();
|
|
6259
|
+
init_resolve_list_limit_offset();
|
|
6260
|
+
}
|
|
6261
|
+
});
|
|
6262
|
+
|
|
6263
|
+
// src/tools/create_contact_tool_result.ts
|
|
6264
|
+
function createContactToolResult(db, input) {
|
|
6265
|
+
return withTransaction(db, () => {
|
|
6266
|
+
const receipt = createContact(db, input);
|
|
5590
6267
|
const result = mutationToolResult("create", receipt, "contact");
|
|
6268
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
6269
|
+
result.outcome = "no_op";
|
|
6270
|
+
Object.assign(result.data, {
|
|
6271
|
+
matchedIdentities: receipt.matchedIdentities
|
|
6272
|
+
});
|
|
6273
|
+
return result;
|
|
6274
|
+
}
|
|
5591
6275
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
5592
6276
|
const warnings = db.prepare(
|
|
5593
6277
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
5594
6278
|
).all(receipt.primary.id).flatMap((row) => {
|
|
5595
|
-
const score =
|
|
5596
|
-
return score
|
|
6279
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
6280
|
+
return score <= 0 ? [] : [
|
|
5597
6281
|
{
|
|
5598
6282
|
code: "possible_duplicate",
|
|
5599
6283
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -5610,9 +6294,7 @@ var init_create_contact_tool_result = __esm({
|
|
|
5610
6294
|
"src/tools/create_contact_tool_result.ts"() {
|
|
5611
6295
|
init_create_contact();
|
|
5612
6296
|
init_fold_search_text();
|
|
5613
|
-
|
|
5614
|
-
init_normalize();
|
|
5615
|
-
init_validate_identity_input();
|
|
6297
|
+
init_list_duplicate_candidates();
|
|
5616
6298
|
init_with_transaction();
|
|
5617
6299
|
init_tool_mutation();
|
|
5618
6300
|
}
|
|
@@ -5687,6 +6369,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
5687
6369
|
return contact.result;
|
|
5688
6370
|
}
|
|
5689
6371
|
if (input.action === "delete") {
|
|
6372
|
+
if (contact.value.is_owner)
|
|
6373
|
+
return toolFailure(
|
|
6374
|
+
"domain",
|
|
6375
|
+
"conflict",
|
|
6376
|
+
"Cannot delete owner.",
|
|
6377
|
+
"The owner contact cannot be deleted"
|
|
6378
|
+
);
|
|
5690
6379
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
5691
6380
|
return toolSuccess("Permanently deleted contact.", {
|
|
5692
6381
|
action: "delete",
|
|
@@ -5776,7 +6465,11 @@ var init_manage_contact_tool = __esm({
|
|
|
5776
6465
|
},
|
|
5777
6466
|
["type", "value"]
|
|
5778
6467
|
),
|
|
5779
|
-
"Optional identities to attach.
|
|
6468
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
6469
|
+
),
|
|
6470
|
+
ifExists: enumSchema(
|
|
6471
|
+
"Identity collision policy; return attaches nothing.",
|
|
6472
|
+
["error", "return"]
|
|
5780
6473
|
),
|
|
5781
6474
|
name: stringSchema("Contact name."),
|
|
5782
6475
|
kind: enumSchema("Contact kind.", [
|
|
@@ -5900,7 +6593,7 @@ var init_add_date_anchor = __esm({
|
|
|
5900
6593
|
init_loaders();
|
|
5901
6594
|
init_persistence();
|
|
5902
6595
|
init_queries2();
|
|
5903
|
-
|
|
6596
|
+
init_receipts();
|
|
5904
6597
|
init_validators();
|
|
5905
6598
|
init_affinity_conflict_error();
|
|
5906
6599
|
init_affinity_not_found_error();
|
|
@@ -5932,7 +6625,7 @@ var init_remove_date_anchor = __esm({
|
|
|
5932
6625
|
"src/dates/remove_date_anchor.ts"() {
|
|
5933
6626
|
init_loaders();
|
|
5934
6627
|
init_mappers();
|
|
5935
|
-
|
|
6628
|
+
init_receipts();
|
|
5936
6629
|
init_resolve_now();
|
|
5937
6630
|
init_with_transaction();
|
|
5938
6631
|
init_upcoming_occurrences();
|
|
@@ -6001,7 +6694,7 @@ var init_revise_date_anchor = __esm({
|
|
|
6001
6694
|
"src/dates/revise_date_anchor.ts"() {
|
|
6002
6695
|
init_loaders();
|
|
6003
6696
|
init_queries2();
|
|
6004
|
-
|
|
6697
|
+
init_receipts();
|
|
6005
6698
|
init_validators();
|
|
6006
6699
|
init_affinity_conflict_error();
|
|
6007
6700
|
init_affinity_validation_error();
|
|
@@ -6431,50 +7124,345 @@ function manageDuplicateDismissalToolHandler(db, rawInput) {
|
|
|
6431
7124
|
] : []
|
|
6432
7125
|
}
|
|
6433
7126
|
);
|
|
6434
|
-
}, "Duplicate dismissal failed.");
|
|
7127
|
+
}, "Duplicate dismissal failed.");
|
|
7128
|
+
}
|
|
7129
|
+
var manageDuplicateDismissalTool;
|
|
7130
|
+
var init_manage_duplicate_dismissal_tool = __esm({
|
|
7131
|
+
"src/tools/manage_duplicate_dismissal_tool.ts"() {
|
|
7132
|
+
init_dismiss_duplicate();
|
|
7133
|
+
init_undismiss_duplicate();
|
|
7134
|
+
init_tool_metadata();
|
|
7135
|
+
init_tool_names();
|
|
7136
|
+
init_tool_resolvers();
|
|
7137
|
+
init_tool_types();
|
|
7138
|
+
init_tool_types();
|
|
7139
|
+
init_validate_tool_input();
|
|
7140
|
+
manageDuplicateDismissalTool = defineAffinityTool({
|
|
7141
|
+
name: manageDuplicateDismissalToolName,
|
|
7142
|
+
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.",
|
|
7143
|
+
whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
|
|
7144
|
+
whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
|
|
7145
|
+
sideEffects: "writes_state",
|
|
7146
|
+
readOnly: false,
|
|
7147
|
+
supportsClarification: true,
|
|
7148
|
+
targetKinds: ["contact"],
|
|
7149
|
+
inputDescriptions: {
|
|
7150
|
+
action: "dismiss to suppress the pair; undismiss to re-surface it.",
|
|
7151
|
+
left: "One contact of the pair.",
|
|
7152
|
+
right: "The other contact of the pair.",
|
|
7153
|
+
reason: "Optional reason for the dismissal (audit trail)."
|
|
7154
|
+
},
|
|
7155
|
+
outputDescription: "Returns the dismissal receipt with the canonical pair identifiers.",
|
|
7156
|
+
inputSchema: objectSchema(
|
|
7157
|
+
{
|
|
7158
|
+
action: enumSchema("Action to perform.", ["dismiss", "undismiss"]),
|
|
7159
|
+
left: contactLocatorSchema5(
|
|
7160
|
+
"Left contact. Provide contactId or identity."
|
|
7161
|
+
),
|
|
7162
|
+
right: contactLocatorSchema5(
|
|
7163
|
+
"Right contact. Provide contactId or identity."
|
|
7164
|
+
),
|
|
7165
|
+
reason: nullableStringSchema("Optional reason for the dismissal.")
|
|
7166
|
+
},
|
|
7167
|
+
["action", "left", "right"],
|
|
7168
|
+
"Dismiss or un-dismiss a duplicate candidate pair."
|
|
7169
|
+
),
|
|
7170
|
+
handler: manageDuplicateDismissalToolHandler
|
|
7171
|
+
});
|
|
7172
|
+
}
|
|
7173
|
+
});
|
|
7174
|
+
|
|
7175
|
+
// src/events/event_replay.ts
|
|
7176
|
+
function replayEventLinks(db, linkIds, now) {
|
|
7177
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
7178
|
+
for (const linkId of linkIds) {
|
|
7179
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
7180
|
+
if (!saved) continue;
|
|
7181
|
+
const baseline = JSON.parse(saved.state_json);
|
|
7182
|
+
const effects = db.prepare(
|
|
7183
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
7184
|
+
).all(linkId);
|
|
7185
|
+
const overrides = db.prepare(
|
|
7186
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
7187
|
+
).all(linkId);
|
|
7188
|
+
let overrideIndex = 0;
|
|
7189
|
+
const applyOverrides = (beforeId) => {
|
|
7190
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
7191
|
+
const override = overrides[overrideIndex];
|
|
7192
|
+
if (override)
|
|
7193
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
7194
|
+
override.state,
|
|
7195
|
+
linkId
|
|
7196
|
+
);
|
|
7197
|
+
overrideIndex++;
|
|
7198
|
+
}
|
|
7199
|
+
};
|
|
7200
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
7201
|
+
db.prepare(
|
|
7202
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
7203
|
+
).run(
|
|
7204
|
+
baseline.rank,
|
|
7205
|
+
baseline.affinity,
|
|
7206
|
+
baseline.trust,
|
|
7207
|
+
baseline.state,
|
|
7208
|
+
baseline.cadence_days,
|
|
7209
|
+
now,
|
|
7210
|
+
linkId
|
|
7211
|
+
);
|
|
7212
|
+
for (const effect of effects) {
|
|
7213
|
+
eventIds.add(effect.event_id);
|
|
7214
|
+
applyOverrides(effect.event_id);
|
|
7215
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
7216
|
+
continue;
|
|
7217
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
7218
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
7219
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
7220
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
7221
|
+
applyEventEffectToLink(db, {
|
|
7222
|
+
linkId,
|
|
7223
|
+
eventId: event.id,
|
|
7224
|
+
eventType: event.type,
|
|
7225
|
+
occurredAt: event.occurredAt,
|
|
7226
|
+
significance: event.significance,
|
|
7227
|
+
participant,
|
|
7228
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
7229
|
+
now: effect.created_at
|
|
7230
|
+
});
|
|
7231
|
+
}
|
|
7232
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
7233
|
+
refreshLinkRollup(db, linkId, now);
|
|
7234
|
+
}
|
|
7235
|
+
for (const eventId of eventIds)
|
|
7236
|
+
db.prepare(
|
|
7237
|
+
"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=?"
|
|
7238
|
+
).run(eventId);
|
|
7239
|
+
refreshAllBridgeScores(db, now);
|
|
7240
|
+
}
|
|
7241
|
+
var init_event_replay = __esm({
|
|
7242
|
+
"src/events/event_replay.ts"() {
|
|
7243
|
+
init_bridge_scores();
|
|
7244
|
+
init_effects();
|
|
7245
|
+
init_rollups2();
|
|
7246
|
+
init_loaders();
|
|
7247
|
+
}
|
|
7248
|
+
});
|
|
7249
|
+
|
|
7250
|
+
// src/events/manage_event.ts
|
|
7251
|
+
function requireEventTarget(db, target) {
|
|
7252
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
7253
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
7254
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
7255
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
7256
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
7257
|
+
return event;
|
|
7258
|
+
}
|
|
7259
|
+
function affected(db, id) {
|
|
7260
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
7261
|
+
}
|
|
7262
|
+
function reviseEvent(db, target, patch) {
|
|
7263
|
+
return withTransaction(db, () => {
|
|
7264
|
+
const allowed = [
|
|
7265
|
+
"summary",
|
|
7266
|
+
"content",
|
|
7267
|
+
"occurredAt",
|
|
7268
|
+
"significance",
|
|
7269
|
+
"channel",
|
|
7270
|
+
"type",
|
|
7271
|
+
"externalRef"
|
|
7272
|
+
];
|
|
7273
|
+
for (const key of Object.keys(patch))
|
|
7274
|
+
if (!allowed.includes(key))
|
|
7275
|
+
throw new AffinityValidationError(
|
|
7276
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
7277
|
+
);
|
|
7278
|
+
const event = requireEventTarget(db, target);
|
|
7279
|
+
const next = { ...event, ...patch };
|
|
7280
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
7281
|
+
validateEventMetadata(metadata);
|
|
7282
|
+
Object.assign(next, metadata);
|
|
7283
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
7284
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
7285
|
+
if (patch.occurredAt !== void 0)
|
|
7286
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
7287
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
7288
|
+
throw new AffinityValidationError(
|
|
7289
|
+
"significance must be an integer from 1 to 10"
|
|
7290
|
+
);
|
|
7291
|
+
const interactions = [
|
|
7292
|
+
"conversation",
|
|
7293
|
+
"activity",
|
|
7294
|
+
"gift",
|
|
7295
|
+
"support",
|
|
7296
|
+
"conflict",
|
|
7297
|
+
"correction"
|
|
7298
|
+
];
|
|
7299
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
7300
|
+
throw new AffinityValidationError(
|
|
7301
|
+
"type must stay within the same event kind"
|
|
7302
|
+
);
|
|
7303
|
+
if (patch.externalRef && event.externalRef)
|
|
7304
|
+
throw new AffinityConflictError(
|
|
7305
|
+
"externalRef can only be set when absent"
|
|
7306
|
+
);
|
|
7307
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
7308
|
+
throw new AffinityConflictError(
|
|
7309
|
+
"externalRef already belongs to an event"
|
|
7310
|
+
);
|
|
7311
|
+
const now = Date.now();
|
|
7312
|
+
const links = affected(db, event.id);
|
|
7313
|
+
db.prepare(
|
|
7314
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
7315
|
+
).run(
|
|
7316
|
+
next.summary.trim(),
|
|
7317
|
+
next.content ?? null,
|
|
7318
|
+
next.occurredAt,
|
|
7319
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
7320
|
+
next.significance,
|
|
7321
|
+
next.channel ?? null,
|
|
7322
|
+
next.type,
|
|
7323
|
+
next.externalRef?.source ?? null,
|
|
7324
|
+
next.externalRef?.id ?? null,
|
|
7325
|
+
now,
|
|
7326
|
+
event.id
|
|
7327
|
+
);
|
|
7328
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
7329
|
+
db.prepare(
|
|
7330
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
7331
|
+
).run(patch.type, event.id);
|
|
7332
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
7333
|
+
replayEventLinks(db, links, now);
|
|
7334
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
7335
|
+
updated: [{ kind: "event", id: event.id }],
|
|
7336
|
+
affectedLinks: links
|
|
7337
|
+
});
|
|
7338
|
+
});
|
|
6435
7339
|
}
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
7340
|
+
function deleteEvent(db, target, options) {
|
|
7341
|
+
return withTransaction(db, () => {
|
|
7342
|
+
if (options?.confirm !== true)
|
|
7343
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
7344
|
+
const event = requireEventTarget(db, target);
|
|
7345
|
+
const links = affected(db, event.id);
|
|
7346
|
+
for (const table of [
|
|
7347
|
+
"event_participants",
|
|
7348
|
+
"attributes",
|
|
7349
|
+
"open_commitments",
|
|
7350
|
+
"upcoming_occurrences",
|
|
7351
|
+
"link_event_effects"
|
|
7352
|
+
])
|
|
7353
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
7354
|
+
db.prepare(
|
|
7355
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
7356
|
+
).run(event.id);
|
|
7357
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
7358
|
+
replayEventLinks(db, links, Date.now());
|
|
7359
|
+
return buildEventMutationReceipt(event, {
|
|
7360
|
+
removed: [{ kind: "event", id: event.id }],
|
|
7361
|
+
affectedLinks: links
|
|
7362
|
+
});
|
|
7363
|
+
});
|
|
7364
|
+
}
|
|
7365
|
+
var init_manage_event = __esm({
|
|
7366
|
+
"src/events/manage_event.ts"() {
|
|
7367
|
+
init_affinity_conflict_error();
|
|
7368
|
+
init_affinity_not_found_error();
|
|
7369
|
+
init_affinity_validation_error();
|
|
7370
|
+
init_with_transaction();
|
|
7371
|
+
init_event_metadata();
|
|
7372
|
+
init_event_replay();
|
|
7373
|
+
init_receipts();
|
|
7374
|
+
init_validators();
|
|
7375
|
+
}
|
|
7376
|
+
});
|
|
7377
|
+
|
|
7378
|
+
// src/tools/manage_event_tool.ts
|
|
7379
|
+
function manageEventToolHandler(db, raw) {
|
|
7380
|
+
const input = structuredClone(raw);
|
|
7381
|
+
const error = validateToolInput(
|
|
7382
|
+
manageEventTool.inputSchema,
|
|
7383
|
+
input,
|
|
7384
|
+
"input",
|
|
7385
|
+
Date.now(),
|
|
7386
|
+
true
|
|
7387
|
+
);
|
|
7388
|
+
if (error)
|
|
7389
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
7390
|
+
return withToolHandling(() => {
|
|
7391
|
+
const target = {
|
|
7392
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
7393
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
7394
|
+
};
|
|
7395
|
+
return mutationToolResult(
|
|
7396
|
+
input.action,
|
|
7397
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
7398
|
+
"event"
|
|
7399
|
+
);
|
|
7400
|
+
}, "Event management failed.");
|
|
7401
|
+
}
|
|
7402
|
+
var manageEventTool;
|
|
7403
|
+
var init_manage_event_tool = __esm({
|
|
7404
|
+
"src/tools/manage_event_tool.ts"() {
|
|
7405
|
+
init_manage_event();
|
|
7406
|
+
init_event_metadata_schema();
|
|
6441
7407
|
init_tool_metadata();
|
|
7408
|
+
init_tool_mutation();
|
|
6442
7409
|
init_tool_names();
|
|
6443
7410
|
init_tool_resolvers();
|
|
6444
7411
|
init_tool_types();
|
|
6445
|
-
init_tool_types();
|
|
6446
7412
|
init_validate_tool_input();
|
|
6447
|
-
|
|
6448
|
-
name:
|
|
6449
|
-
description: "
|
|
6450
|
-
whenToUse: "
|
|
6451
|
-
whenNotToUse: "
|
|
7413
|
+
manageEventTool = defineAffinityTool({
|
|
7414
|
+
name: manageEventToolName,
|
|
7415
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
7416
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
7417
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
6452
7418
|
sideEffects: "writes_state",
|
|
6453
7419
|
readOnly: false,
|
|
6454
|
-
supportsClarification:
|
|
6455
|
-
targetKinds: ["
|
|
7420
|
+
supportsClarification: false,
|
|
7421
|
+
targetKinds: ["event"],
|
|
6456
7422
|
inputDescriptions: {
|
|
6457
|
-
action: "
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
7423
|
+
action: "revise or delete",
|
|
7424
|
+
eventId: "Exact event id",
|
|
7425
|
+
ref: "External event reference",
|
|
7426
|
+
patch: "Fields to revise",
|
|
7427
|
+
confirm: "Required true for deletion"
|
|
6461
7428
|
},
|
|
6462
|
-
outputDescription: "
|
|
7429
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
6463
7430
|
inputSchema: objectSchema(
|
|
6464
7431
|
{
|
|
6465
|
-
action: enumSchema("Action
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
),
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
7432
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
7433
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
7434
|
+
ref: externalRefSchema,
|
|
7435
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
7436
|
+
patch: objectSchema(
|
|
7437
|
+
{
|
|
7438
|
+
summary: stringSchema("Short non-empty label."),
|
|
7439
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
7440
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
7441
|
+
significance: integerSchema("Significance 1..10."),
|
|
7442
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
7443
|
+
type: enumSchema("Type within the same event kind.", [
|
|
7444
|
+
"conversation",
|
|
7445
|
+
"activity",
|
|
7446
|
+
"gift",
|
|
7447
|
+
"support",
|
|
7448
|
+
"conflict",
|
|
7449
|
+
"correction",
|
|
7450
|
+
"observation",
|
|
7451
|
+
"milestone",
|
|
7452
|
+
"transaction",
|
|
7453
|
+
"promise",
|
|
7454
|
+
"agreement",
|
|
7455
|
+
"date_anchor"
|
|
7456
|
+
]),
|
|
7457
|
+
externalRef: externalRefSchema
|
|
7458
|
+
},
|
|
7459
|
+
[],
|
|
7460
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
7461
|
+
)
|
|
6473
7462
|
},
|
|
6474
|
-
["action"
|
|
6475
|
-
"Dismiss or un-dismiss a duplicate candidate pair."
|
|
7463
|
+
["action"]
|
|
6476
7464
|
),
|
|
6477
|
-
handler:
|
|
7465
|
+
handler: manageEventToolHandler
|
|
6478
7466
|
});
|
|
6479
7467
|
}
|
|
6480
7468
|
});
|
|
@@ -6539,7 +7527,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
6539
7527
|
});
|
|
6540
7528
|
}
|
|
6541
7529
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
6542
|
-
const nextValue =
|
|
7530
|
+
const nextValue = normalizeIdentityValue(
|
|
7531
|
+
nextType,
|
|
7532
|
+
patch.value ?? row.value
|
|
7533
|
+
);
|
|
6543
7534
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
6544
7535
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
6545
7536
|
}
|
|
@@ -6626,6 +7617,10 @@ function contactLocatorSchema6(description) {
|
|
|
6626
7617
|
contactId: integerSchema("Exact contact id."),
|
|
6627
7618
|
identity: objectSchema(
|
|
6628
7619
|
{
|
|
7620
|
+
ifExists: enumSchema(
|
|
7621
|
+
"Return an existing identity on this contact without changes.",
|
|
7622
|
+
["error", "return"]
|
|
7623
|
+
),
|
|
6629
7624
|
type: stringSchema(
|
|
6630
7625
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6631
7626
|
),
|
|
@@ -6736,6 +7731,10 @@ var init_manage_identity_tool = __esm({
|
|
|
6736
7731
|
),
|
|
6737
7732
|
input: objectSchema(
|
|
6738
7733
|
{
|
|
7734
|
+
ifExists: enumSchema(
|
|
7735
|
+
"Return an existing identity on this contact without changes.",
|
|
7736
|
+
["error", "return"]
|
|
7737
|
+
),
|
|
6739
7738
|
type: stringSchema(
|
|
6740
7739
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6741
7740
|
),
|
|
@@ -6799,6 +7798,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
6799
7798
|
now,
|
|
6800
7799
|
linkId
|
|
6801
7800
|
);
|
|
7801
|
+
db.prepare(
|
|
7802
|
+
"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=?"
|
|
7803
|
+
).run(linkId, state, linkId);
|
|
6802
7804
|
refreshLinkRollup(db, linkId, now);
|
|
6803
7805
|
if (state === "archived" || row.state === "archived") {
|
|
6804
7806
|
refreshAllBridgeScores(db, now);
|
|
@@ -7523,6 +8525,8 @@ var init_merge_contacts_tool = __esm({
|
|
|
7523
8525
|
function recordInteraction(db, input) {
|
|
7524
8526
|
return withTransaction(db, () => {
|
|
7525
8527
|
validateSocialEventInput(input);
|
|
8528
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8529
|
+
if (existing) return existing;
|
|
7526
8530
|
if (!ALLOWED.has(input.type)) {
|
|
7527
8531
|
throw new AffinityValidationError("invalid interaction event type");
|
|
7528
8532
|
}
|
|
@@ -7532,11 +8536,12 @@ function recordInteraction(db, input) {
|
|
|
7532
8536
|
assertOwnerParticipates(ownerId, ids);
|
|
7533
8537
|
const now = resolveNow(input.now);
|
|
7534
8538
|
const eventId = insertJournalEvent(db, {
|
|
8539
|
+
...input,
|
|
7535
8540
|
type: input.type,
|
|
7536
8541
|
occurredAt: input.occurredAt,
|
|
7537
|
-
|
|
8542
|
+
occurredOn: input.occurredOn,
|
|
7538
8543
|
significance: input.significance,
|
|
7539
|
-
momentKind: null,
|
|
8544
|
+
momentKind: input.momentKind ?? null,
|
|
7540
8545
|
participants: input.participants,
|
|
7541
8546
|
now
|
|
7542
8547
|
});
|
|
@@ -7544,12 +8549,24 @@ function recordInteraction(db, input) {
|
|
|
7544
8549
|
eventId,
|
|
7545
8550
|
eventType: input.type,
|
|
7546
8551
|
occurredAt: input.occurredAt,
|
|
8552
|
+
occurredOn: input.occurredOn,
|
|
7547
8553
|
significance: input.significance,
|
|
7548
8554
|
participants: input.participants,
|
|
7549
8555
|
provenance: input.provenance,
|
|
7550
8556
|
now
|
|
7551
8557
|
});
|
|
8558
|
+
if (input.momentKind !== void 0)
|
|
8559
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8560
|
+
input.momentKind,
|
|
8561
|
+
eventId
|
|
8562
|
+
);
|
|
7552
8563
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8564
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8565
|
+
{
|
|
8566
|
+
code: "future_event",
|
|
8567
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8568
|
+
}
|
|
8569
|
+
] : void 0,
|
|
7553
8570
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7554
8571
|
affectedLinks: mechanics.affectedLinks,
|
|
7555
8572
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7562,12 +8579,14 @@ var init_record_interaction = __esm({
|
|
|
7562
8579
|
init_queries();
|
|
7563
8580
|
init_loaders();
|
|
7564
8581
|
init_persistence();
|
|
7565
|
-
|
|
8582
|
+
init_receipts();
|
|
7566
8583
|
init_validators();
|
|
7567
8584
|
init_affinity_validation_error();
|
|
8585
|
+
init_parse_calendar_time();
|
|
7568
8586
|
init_resolve_now();
|
|
7569
8587
|
init_with_transaction();
|
|
7570
8588
|
init_apply_journal_event_mechanics();
|
|
8589
|
+
init_event_metadata();
|
|
7571
8590
|
ALLOWED = /* @__PURE__ */ new Set([
|
|
7572
8591
|
"conversation",
|
|
7573
8592
|
"activity",
|
|
@@ -7583,17 +8602,20 @@ var init_record_interaction = __esm({
|
|
|
7583
8602
|
function recordMilestone(db, input) {
|
|
7584
8603
|
return withTransaction(db, () => {
|
|
7585
8604
|
validateSocialEventInput(input);
|
|
8605
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8606
|
+
if (existing) return existing;
|
|
7586
8607
|
const ownerId = requireOwnerContactId(db);
|
|
7587
8608
|
const ids = input.participants.map((p) => p.contactId);
|
|
7588
8609
|
assertParticipantContactsLive(db, ids);
|
|
7589
8610
|
assertOwnerParticipates(ownerId, ids);
|
|
7590
8611
|
const now = resolveNow(input.now);
|
|
7591
8612
|
const eventId = insertJournalEvent(db, {
|
|
8613
|
+
...input,
|
|
7592
8614
|
type: "milestone",
|
|
7593
8615
|
occurredAt: input.occurredAt,
|
|
7594
|
-
|
|
8616
|
+
occurredOn: input.occurredOn,
|
|
7595
8617
|
significance: input.significance,
|
|
7596
|
-
momentKind: null,
|
|
8618
|
+
momentKind: input.momentKind ?? null,
|
|
7597
8619
|
participants: input.participants,
|
|
7598
8620
|
now
|
|
7599
8621
|
});
|
|
@@ -7601,12 +8623,24 @@ function recordMilestone(db, input) {
|
|
|
7601
8623
|
eventId,
|
|
7602
8624
|
eventType: "milestone",
|
|
7603
8625
|
occurredAt: input.occurredAt,
|
|
8626
|
+
occurredOn: input.occurredOn,
|
|
7604
8627
|
significance: input.significance,
|
|
7605
8628
|
participants: input.participants,
|
|
7606
8629
|
provenance: input.provenance,
|
|
7607
8630
|
now
|
|
7608
8631
|
});
|
|
8632
|
+
if (input.momentKind !== void 0)
|
|
8633
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8634
|
+
input.momentKind,
|
|
8635
|
+
eventId
|
|
8636
|
+
);
|
|
7609
8637
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8638
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8639
|
+
{
|
|
8640
|
+
code: "future_event",
|
|
8641
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8642
|
+
}
|
|
8643
|
+
] : void 0,
|
|
7610
8644
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7611
8645
|
affectedLinks: mechanics.affectedLinks,
|
|
7612
8646
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7618,11 +8652,13 @@ var init_record_milestone = __esm({
|
|
|
7618
8652
|
init_queries();
|
|
7619
8653
|
init_loaders();
|
|
7620
8654
|
init_persistence();
|
|
7621
|
-
|
|
8655
|
+
init_receipts();
|
|
7622
8656
|
init_validators();
|
|
8657
|
+
init_parse_calendar_time();
|
|
7623
8658
|
init_resolve_now();
|
|
7624
8659
|
init_with_transaction();
|
|
7625
8660
|
init_apply_journal_event_mechanics();
|
|
8661
|
+
init_event_metadata();
|
|
7626
8662
|
}
|
|
7627
8663
|
});
|
|
7628
8664
|
|
|
@@ -7630,15 +8666,18 @@ var init_record_milestone = __esm({
|
|
|
7630
8666
|
function recordObservation(db, input) {
|
|
7631
8667
|
return withTransaction(db, () => {
|
|
7632
8668
|
validateSocialEventInput(input);
|
|
8669
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8670
|
+
if (existing) return existing;
|
|
7633
8671
|
const ids = input.participants.map((p) => p.contactId);
|
|
7634
8672
|
assertParticipantContactsLive(db, ids);
|
|
7635
8673
|
const now = resolveNow(input.now);
|
|
7636
8674
|
const eventId = insertJournalEvent(db, {
|
|
8675
|
+
...input,
|
|
7637
8676
|
type: "observation",
|
|
7638
8677
|
occurredAt: input.occurredAt,
|
|
7639
|
-
|
|
8678
|
+
occurredOn: input.occurredOn,
|
|
7640
8679
|
significance: input.significance,
|
|
7641
|
-
momentKind: null,
|
|
8680
|
+
momentKind: input.momentKind ?? null,
|
|
7642
8681
|
participants: input.participants,
|
|
7643
8682
|
now
|
|
7644
8683
|
});
|
|
@@ -7646,12 +8685,24 @@ function recordObservation(db, input) {
|
|
|
7646
8685
|
eventId,
|
|
7647
8686
|
eventType: "observation",
|
|
7648
8687
|
occurredAt: input.occurredAt,
|
|
8688
|
+
occurredOn: input.occurredOn,
|
|
7649
8689
|
significance: input.significance,
|
|
7650
8690
|
participants: input.participants,
|
|
7651
8691
|
provenance: input.provenance,
|
|
7652
8692
|
now
|
|
7653
8693
|
});
|
|
8694
|
+
if (input.momentKind !== void 0)
|
|
8695
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8696
|
+
input.momentKind,
|
|
8697
|
+
eventId
|
|
8698
|
+
);
|
|
7654
8699
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8700
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8701
|
+
{
|
|
8702
|
+
code: "future_event",
|
|
8703
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8704
|
+
}
|
|
8705
|
+
] : void 0,
|
|
7655
8706
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7656
8707
|
affectedLinks: mechanics.affectedLinks,
|
|
7657
8708
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7662,11 +8713,13 @@ var init_record_observation = __esm({
|
|
|
7662
8713
|
"src/events/record_observation.ts"() {
|
|
7663
8714
|
init_loaders();
|
|
7664
8715
|
init_persistence();
|
|
7665
|
-
|
|
8716
|
+
init_receipts();
|
|
7666
8717
|
init_validators();
|
|
8718
|
+
init_parse_calendar_time();
|
|
7667
8719
|
init_resolve_now();
|
|
7668
8720
|
init_with_transaction();
|
|
7669
8721
|
init_apply_journal_event_mechanics();
|
|
8722
|
+
init_event_metadata();
|
|
7670
8723
|
}
|
|
7671
8724
|
});
|
|
7672
8725
|
|
|
@@ -7674,17 +8727,20 @@ var init_record_observation = __esm({
|
|
|
7674
8727
|
function recordTransaction(db, input) {
|
|
7675
8728
|
return withTransaction(db, () => {
|
|
7676
8729
|
validateSocialEventInput(input);
|
|
8730
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8731
|
+
if (existing) return existing;
|
|
7677
8732
|
const ownerId = requireOwnerContactId(db);
|
|
7678
8733
|
const ids = input.participants.map((p) => p.contactId);
|
|
7679
8734
|
assertParticipantContactsLive(db, ids);
|
|
7680
8735
|
assertOwnerParticipates(ownerId, ids);
|
|
7681
8736
|
const now = resolveNow(input.now);
|
|
7682
8737
|
const eventId = insertJournalEvent(db, {
|
|
8738
|
+
...input,
|
|
7683
8739
|
type: "transaction",
|
|
7684
8740
|
occurredAt: input.occurredAt,
|
|
7685
|
-
|
|
8741
|
+
occurredOn: input.occurredOn,
|
|
7686
8742
|
significance: input.significance,
|
|
7687
|
-
momentKind: null,
|
|
8743
|
+
momentKind: input.momentKind ?? null,
|
|
7688
8744
|
participants: input.participants,
|
|
7689
8745
|
now
|
|
7690
8746
|
});
|
|
@@ -7692,12 +8748,24 @@ function recordTransaction(db, input) {
|
|
|
7692
8748
|
eventId,
|
|
7693
8749
|
eventType: "transaction",
|
|
7694
8750
|
occurredAt: input.occurredAt,
|
|
8751
|
+
occurredOn: input.occurredOn,
|
|
7695
8752
|
significance: input.significance,
|
|
7696
8753
|
participants: input.participants,
|
|
7697
8754
|
provenance: input.provenance,
|
|
7698
8755
|
now
|
|
7699
8756
|
});
|
|
8757
|
+
if (input.momentKind !== void 0)
|
|
8758
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8759
|
+
input.momentKind,
|
|
8760
|
+
eventId
|
|
8761
|
+
);
|
|
7700
8762
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8763
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8764
|
+
{
|
|
8765
|
+
code: "future_event",
|
|
8766
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8767
|
+
}
|
|
8768
|
+
] : void 0,
|
|
7701
8769
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7702
8770
|
affectedLinks: mechanics.affectedLinks,
|
|
7703
8771
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7709,20 +8777,24 @@ var init_record_transaction = __esm({
|
|
|
7709
8777
|
init_queries();
|
|
7710
8778
|
init_loaders();
|
|
7711
8779
|
init_persistence();
|
|
7712
|
-
|
|
8780
|
+
init_receipts();
|
|
7713
8781
|
init_validators();
|
|
8782
|
+
init_parse_calendar_time();
|
|
7714
8783
|
init_resolve_now();
|
|
7715
8784
|
init_with_transaction();
|
|
7716
8785
|
init_apply_journal_event_mechanics();
|
|
8786
|
+
init_event_metadata();
|
|
7717
8787
|
}
|
|
7718
8788
|
});
|
|
7719
8789
|
|
|
7720
8790
|
// src/tools/record_event_tool.ts
|
|
7721
|
-
function normalizeParticipant2(p) {
|
|
8791
|
+
function normalizeParticipant2(db, p) {
|
|
7722
8792
|
let contactId = p.contactId;
|
|
7723
8793
|
if (contactId === void 0 && p.contact != null) {
|
|
7724
8794
|
const loc = p.contact;
|
|
7725
|
-
|
|
8795
|
+
const resolved = resolveContactLocator(db, loc);
|
|
8796
|
+
if (!resolved.ok) throw resolved.result;
|
|
8797
|
+
contactId = resolved.value.id;
|
|
7726
8798
|
}
|
|
7727
8799
|
const result = {
|
|
7728
8800
|
contactId,
|
|
@@ -7740,12 +8812,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
7740
8812
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
7741
8813
|
}
|
|
7742
8814
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
7743
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
8815
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
7744
8816
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
7745
8817
|
}
|
|
7746
8818
|
function recordEventToolHandler(db, rawInput) {
|
|
7747
8819
|
const input = structuredClone(rawInput);
|
|
7748
|
-
const inputError = validateToolInput(
|
|
8820
|
+
const inputError = validateToolInput(
|
|
8821
|
+
recordEventTool.inputSchema,
|
|
8822
|
+
input,
|
|
8823
|
+
"input",
|
|
8824
|
+
Date.now(),
|
|
8825
|
+
true
|
|
8826
|
+
);
|
|
7749
8827
|
if (inputError)
|
|
7750
8828
|
return toolFailure(
|
|
7751
8829
|
"protocol",
|
|
@@ -7798,6 +8876,7 @@ var init_record_event_tool = __esm({
|
|
|
7798
8876
|
init_record_milestone();
|
|
7799
8877
|
init_record_observation();
|
|
7800
8878
|
init_record_transaction();
|
|
8879
|
+
init_event_metadata_schema();
|
|
7801
8880
|
init_tool_metadata();
|
|
7802
8881
|
init_tool_mutation();
|
|
7803
8882
|
init_tool_names();
|
|
@@ -7839,6 +8918,7 @@ var init_record_event_tool = __esm({
|
|
|
7839
8918
|
"correction"
|
|
7840
8919
|
]
|
|
7841
8920
|
),
|
|
8921
|
+
...eventMetadataSchema,
|
|
7842
8922
|
occurredAt: integerSchema(
|
|
7843
8923
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
7844
8924
|
),
|
|
@@ -7847,6 +8927,19 @@ var init_record_event_tool = __esm({
|
|
|
7847
8927
|
participants: arraySchema(
|
|
7848
8928
|
objectSchema(
|
|
7849
8929
|
{
|
|
8930
|
+
contact: objectSchema(
|
|
8931
|
+
{
|
|
8932
|
+
contactId: integerSchema("Contact id."),
|
|
8933
|
+
identity: objectSchema(
|
|
8934
|
+
{
|
|
8935
|
+
type: stringSchema("Identity type."),
|
|
8936
|
+
value: stringSchema("Identity value.")
|
|
8937
|
+
},
|
|
8938
|
+
["type", "value"]
|
|
8939
|
+
)
|
|
8940
|
+
},
|
|
8941
|
+
[]
|
|
8942
|
+
),
|
|
7850
8943
|
contactId: integerSchema(
|
|
7851
8944
|
"Participant contact id. The owner contact MUST be included."
|
|
7852
8945
|
),
|
|
@@ -7870,7 +8963,7 @@ var init_record_event_tool = __esm({
|
|
|
7870
8963
|
),
|
|
7871
8964
|
now: integerSchema("Optional timestamp override.")
|
|
7872
8965
|
},
|
|
7873
|
-
["occurredAt", "
|
|
8966
|
+
["occurredAt", "significance", "participants"],
|
|
7874
8967
|
"Event payload. Always required. Interaction events also require `type`."
|
|
7875
8968
|
)
|
|
7876
8969
|
},
|
|
@@ -7888,7 +8981,10 @@ function contactAttributeClauses(filters) {
|
|
|
7888
8981
|
const params = [];
|
|
7889
8982
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
7890
8983
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
7891
|
-
for (const filter of [
|
|
8984
|
+
for (const filter of [
|
|
8985
|
+
...attributes,
|
|
8986
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
8987
|
+
]) {
|
|
7892
8988
|
const separator = filter.indexOf("=");
|
|
7893
8989
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
7894
8990
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -7938,154 +9034,27 @@ function listContacts(db, filters, options) {
|
|
|
7938
9034
|
FROM identities i
|
|
7939
9035
|
WHERE i.contact_id = contacts.id AND i.removed_at IS NULL
|
|
7940
9036
|
ORDER BY i.id
|
|
7941
|
-
LIMIT 1
|
|
7942
|
-
) AS primary_identity
|
|
7943
|
-
FROM contacts
|
|
7944
|
-
WHERE ${where}
|
|
7945
|
-
ORDER BY name ASC
|
|
7946
|
-
LIMIT ? OFFSET ?`
|
|
7947
|
-
).all(...params, limit, offset);
|
|
7948
|
-
return rows.map((r) => {
|
|
7949
|
-
const item = mapContactRowToContactListItem(r);
|
|
7950
|
-
return {
|
|
7951
|
-
...item,
|
|
7952
|
-
primaryIdentity: r.primary_identity ?? null
|
|
7953
|
-
};
|
|
7954
|
-
});
|
|
7955
|
-
}
|
|
7956
|
-
var init_list_contacts = __esm({
|
|
7957
|
-
"src/contacts/list_contacts.ts"() {
|
|
7958
|
-
init_assert_default_ordering();
|
|
7959
|
-
init_resolve_list_limit_offset();
|
|
7960
|
-
init_contact_attribute_clauses();
|
|
7961
|
-
init_mappers4();
|
|
7962
|
-
}
|
|
7963
|
-
});
|
|
7964
|
-
|
|
7965
|
-
// src/contacts/list_duplicate_candidates.ts
|
|
7966
|
-
function pairKey(leftContactId, rightContactId) {
|
|
7967
|
-
const left = Math.min(leftContactId, rightContactId);
|
|
7968
|
-
const right = Math.max(leftContactId, rightContactId);
|
|
7969
|
-
return `${left}:${right}`;
|
|
7970
|
-
}
|
|
7971
|
-
function normalizeName(name) {
|
|
7972
|
-
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
7973
|
-
}
|
|
7974
|
-
function fuzzyNameScore(left, right) {
|
|
7975
|
-
if (left === right) {
|
|
7976
|
-
return 0.92;
|
|
7977
|
-
}
|
|
7978
|
-
if (left.length >= 3 && right.length >= 3) {
|
|
7979
|
-
if (left.includes(right) || right.includes(left)) {
|
|
7980
|
-
return 0.72;
|
|
7981
|
-
}
|
|
7982
|
-
const leftTokens = new Set(left.split(" "));
|
|
7983
|
-
const rightTokens = new Set(right.split(" "));
|
|
7984
|
-
const overlap = [...leftTokens].filter(
|
|
7985
|
-
(token) => rightTokens.has(token)
|
|
7986
|
-
).length;
|
|
7987
|
-
if (overlap > 0) {
|
|
7988
|
-
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
7989
|
-
}
|
|
7990
|
-
}
|
|
7991
|
-
return 0;
|
|
7992
|
-
}
|
|
7993
|
-
function listDuplicateCandidates(db, filters, options) {
|
|
7994
|
-
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
7995
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
7996
|
-
const subset = filters?.contactIds;
|
|
7997
|
-
const contacts = db.prepare(
|
|
7998
|
-
`SELECT id, name
|
|
7999
|
-
FROM contacts
|
|
8000
|
-
WHERE deleted_at IS NULL
|
|
8001
|
-
AND lifecycle_state != 'merged'`
|
|
8002
|
-
).all();
|
|
8003
|
-
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
8004
|
-
const pairs = /* @__PURE__ */ new Map();
|
|
8005
|
-
const exactMatches = db.prepare(
|
|
8006
|
-
`SELECT
|
|
8007
|
-
i1.contact_id AS left_contact_id,
|
|
8008
|
-
i2.contact_id AS right_contact_id,
|
|
8009
|
-
i1.normalized_key
|
|
8010
|
-
FROM identities i1
|
|
8011
|
-
INNER JOIN identities i2
|
|
8012
|
-
ON i1.normalized_key = i2.normalized_key
|
|
8013
|
-
AND i1.contact_id < i2.contact_id
|
|
8014
|
-
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
8015
|
-
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
8016
|
-
WHERE i1.removed_at IS NULL
|
|
8017
|
-
AND i2.removed_at IS NULL
|
|
8018
|
-
AND c1.deleted_at IS NULL
|
|
8019
|
-
AND c2.deleted_at IS NULL
|
|
8020
|
-
AND c1.lifecycle_state != 'merged'
|
|
8021
|
-
AND c2.lifecycle_state != 'merged'`
|
|
8022
|
-
).all();
|
|
8023
|
-
for (const match of exactMatches) {
|
|
8024
|
-
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
8025
|
-
continue;
|
|
8026
|
-
}
|
|
8027
|
-
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
8028
|
-
leftContactId: match.left_contact_id,
|
|
8029
|
-
rightContactId: match.right_contact_id,
|
|
8030
|
-
matchReason: `exact identity ${match.normalized_key}`,
|
|
8031
|
-
matchScore: 1
|
|
8032
|
-
});
|
|
8033
|
-
}
|
|
8034
|
-
if (filters?.exactOnly !== true) {
|
|
8035
|
-
for (let index = 0; index < contacts.length; index += 1) {
|
|
8036
|
-
const left = contacts[index];
|
|
8037
|
-
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
8038
|
-
continue;
|
|
8039
|
-
}
|
|
8040
|
-
const leftName = normalizeName(left.name);
|
|
8041
|
-
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
8042
|
-
const right = contacts[offsetIndex];
|
|
8043
|
-
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
8044
|
-
continue;
|
|
8045
|
-
}
|
|
8046
|
-
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
8047
|
-
if (score <= 0) {
|
|
8048
|
-
continue;
|
|
8049
|
-
}
|
|
8050
|
-
const key = pairKey(left.id, right.id);
|
|
8051
|
-
if (!pairs.has(key)) {
|
|
8052
|
-
pairs.set(key, {
|
|
8053
|
-
leftContactId: left.id,
|
|
8054
|
-
rightContactId: right.id,
|
|
8055
|
-
matchReason: "name similarity",
|
|
8056
|
-
matchScore: score
|
|
8057
|
-
});
|
|
8058
|
-
}
|
|
8059
|
-
}
|
|
8060
|
-
}
|
|
8061
|
-
}
|
|
8062
|
-
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
8063
|
-
const dismissedSet = new Set(
|
|
8064
|
-
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
8065
|
-
);
|
|
8066
|
-
const includeDismissed = filters?.includeDismissed === true;
|
|
8067
|
-
for (const candidate of pairs.values()) {
|
|
8068
|
-
if (dismissedSet.has(
|
|
8069
|
-
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
8070
|
-
)) {
|
|
8071
|
-
candidate.dismissed = true;
|
|
8072
|
-
}
|
|
8073
|
-
}
|
|
8074
|
-
const minScore = filters?.minScore ?? 0;
|
|
8075
|
-
return [...pairs.values()].filter((candidate) => {
|
|
8076
|
-
if (candidate.matchScore < minScore) return false;
|
|
8077
|
-
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
8078
|
-
return true;
|
|
8079
|
-
}).sort((left, right) => {
|
|
8080
|
-
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
8081
|
-
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
8082
|
-
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
8083
|
-
}).slice(offset, offset + limit);
|
|
9037
|
+
LIMIT 1
|
|
9038
|
+
) AS primary_identity
|
|
9039
|
+
FROM contacts
|
|
9040
|
+
WHERE ${where}
|
|
9041
|
+
ORDER BY name ASC
|
|
9042
|
+
LIMIT ? OFFSET ?`
|
|
9043
|
+
).all(...params, limit, offset);
|
|
9044
|
+
return rows.map((r) => {
|
|
9045
|
+
const item = mapContactRowToContactListItem(r);
|
|
9046
|
+
return {
|
|
9047
|
+
...item,
|
|
9048
|
+
primaryIdentity: r.primary_identity ?? null
|
|
9049
|
+
};
|
|
9050
|
+
});
|
|
8084
9051
|
}
|
|
8085
|
-
var
|
|
8086
|
-
"src/contacts/
|
|
9052
|
+
var init_list_contacts = __esm({
|
|
9053
|
+
"src/contacts/list_contacts.ts"() {
|
|
8087
9054
|
init_assert_default_ordering();
|
|
8088
9055
|
init_resolve_list_limit_offset();
|
|
9056
|
+
init_contact_attribute_clauses();
|
|
9057
|
+
init_mappers4();
|
|
8089
9058
|
}
|
|
8090
9059
|
});
|
|
8091
9060
|
|
|
@@ -8113,12 +9082,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
8113
9082
|
}
|
|
8114
9083
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
8115
9084
|
const params = [];
|
|
8116
|
-
const since = options?.since ??
|
|
9085
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
8117
9086
|
clauses.push("uo.occurs_on >= ?");
|
|
8118
9087
|
params.push(since);
|
|
8119
9088
|
let maxOccurs = options?.until;
|
|
8120
9089
|
if (filters?.horizonDays !== void 0) {
|
|
8121
|
-
const anchor =
|
|
9090
|
+
const anchor = since;
|
|
8122
9091
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
8123
9092
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
8124
9093
|
}
|
|
@@ -8170,9 +9139,9 @@ var init_list_upcoming_dates = __esm({
|
|
|
8170
9139
|
init_mappers();
|
|
8171
9140
|
init_validators();
|
|
8172
9141
|
init_affinity_validation_error();
|
|
9142
|
+
init_parse_calendar_time();
|
|
8173
9143
|
init_assert_default_ordering();
|
|
8174
9144
|
init_resolve_list_limit_offset();
|
|
8175
|
-
init_calendar();
|
|
8176
9145
|
CONTACT_KINDS = /* @__PURE__ */ new Set([
|
|
8177
9146
|
"human",
|
|
8178
9147
|
"group",
|
|
@@ -8185,6 +9154,77 @@ var init_list_upcoming_dates = __esm({
|
|
|
8185
9154
|
}
|
|
8186
9155
|
});
|
|
8187
9156
|
|
|
9157
|
+
// src/events/event_discovery.ts
|
|
9158
|
+
function searchEvents(db, query, options) {
|
|
9159
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
9160
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
9161
|
+
if (!tokens.length) return [];
|
|
9162
|
+
const rows = db.prepare(
|
|
9163
|
+
"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"
|
|
9164
|
+
).all();
|
|
9165
|
+
return rows.flatMap((row) => {
|
|
9166
|
+
const fields = {
|
|
9167
|
+
summary: decodeEventText(row.summary),
|
|
9168
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
9169
|
+
source: row.external_source,
|
|
9170
|
+
id: row.external_id,
|
|
9171
|
+
channel: row.channel
|
|
9172
|
+
};
|
|
9173
|
+
const folded = Object.entries(fields).map(
|
|
9174
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
9175
|
+
);
|
|
9176
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
9177
|
+
return [];
|
|
9178
|
+
return [
|
|
9179
|
+
{
|
|
9180
|
+
kind: "event",
|
|
9181
|
+
id: row.id,
|
|
9182
|
+
type: row.type,
|
|
9183
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
9184
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
9185
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
9186
|
+
...row.external_source && row.external_id ? {
|
|
9187
|
+
externalRef: {
|
|
9188
|
+
source: row.external_source,
|
|
9189
|
+
id: row.external_id
|
|
9190
|
+
}
|
|
9191
|
+
} : {}
|
|
9192
|
+
}
|
|
9193
|
+
];
|
|
9194
|
+
}).slice(offset, offset + limit);
|
|
9195
|
+
}
|
|
9196
|
+
function getEventThread(db, source, id, options) {
|
|
9197
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
9198
|
+
const root = getEventByExternalRef(db, source, id);
|
|
9199
|
+
if (!root) return [];
|
|
9200
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
9201
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
9202
|
+
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=?))
|
|
9203
|
+
) 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(
|
|
9204
|
+
root.id,
|
|
9205
|
+
root.externalRef?.source ?? null,
|
|
9206
|
+
root.externalRef?.id ?? null,
|
|
9207
|
+
options?.channel ?? null,
|
|
9208
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
9209
|
+
limit,
|
|
9210
|
+
offset
|
|
9211
|
+
);
|
|
9212
|
+
return batchLoadEventRecords(
|
|
9213
|
+
db,
|
|
9214
|
+
rows.map((r) => r.id),
|
|
9215
|
+
options?.includeContent ?? false
|
|
9216
|
+
);
|
|
9217
|
+
}
|
|
9218
|
+
var init_event_discovery = __esm({
|
|
9219
|
+
"src/events/event_discovery.ts"() {
|
|
9220
|
+
init_fold_search_text();
|
|
9221
|
+
init_resolve_list_limit_offset();
|
|
9222
|
+
init_event_metadata();
|
|
9223
|
+
init_loaders();
|
|
9224
|
+
init_mappers();
|
|
9225
|
+
}
|
|
9226
|
+
});
|
|
9227
|
+
|
|
8188
9228
|
// src/graph/get_affinity_chart.ts
|
|
8189
9229
|
function getAffinityChart(db, options) {
|
|
8190
9230
|
const contactClauses = [
|
|
@@ -8397,21 +9437,6 @@ var init_list_progression_readiness = __esm({
|
|
|
8397
9437
|
});
|
|
8398
9438
|
|
|
8399
9439
|
// src/links/list_radar.ts
|
|
8400
|
-
function deriveRadarReason(drift, recency, normalizedRank3) {
|
|
8401
|
-
if (drift > 0.5) {
|
|
8402
|
-
return "Significantly overdue for contact";
|
|
8403
|
-
}
|
|
8404
|
-
if (drift > 0.2) {
|
|
8405
|
-
return "Drifting \u2014 approaching overdue";
|
|
8406
|
-
}
|
|
8407
|
-
if (recency < 0.3 && normalizedRank3 > 0.3) {
|
|
8408
|
-
return "Important link losing recency";
|
|
8409
|
-
}
|
|
8410
|
-
if (normalizedRank3 > 0.5 && recency < 0.5) {
|
|
8411
|
-
return "High-rank link needs attention";
|
|
8412
|
-
}
|
|
8413
|
-
return "Routine check-in recommended";
|
|
8414
|
-
}
|
|
8415
9440
|
function listRadar(db, filters, options) {
|
|
8416
9441
|
assertDefaultOrdering("listRadar", options);
|
|
8417
9442
|
const ownerId = findOwnerContactId(db);
|
|
@@ -8419,7 +9444,10 @@ function listRadar(db, filters, options) {
|
|
|
8419
9444
|
return [];
|
|
8420
9445
|
}
|
|
8421
9446
|
const now = resolveNow(options?.now);
|
|
8422
|
-
const
|
|
9447
|
+
const today = localCalendarDate(now);
|
|
9448
|
+
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}'))`;
|
|
9449
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
9450
|
+
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))`;
|
|
8423
9451
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
8424
9452
|
const clauses = [
|
|
8425
9453
|
"l.removed_at IS NULL",
|
|
@@ -8444,10 +9472,23 @@ function listRadar(db, filters, options) {
|
|
|
8444
9472
|
if (options?.includeObserved === false) {
|
|
8445
9473
|
clauses.push("l.kind != 'observed'");
|
|
8446
9474
|
}
|
|
9475
|
+
if (options?.includeDormant !== true)
|
|
9476
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
9477
|
+
if (filters?.includeLost !== true)
|
|
9478
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
9479
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
9480
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
9481
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
9482
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
9483
|
+
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'))
|
|
9484
|
+
))`);
|
|
8447
9485
|
const where = clauses.join(" AND ");
|
|
8448
9486
|
const rows = db.prepare(
|
|
8449
9487
|
`SELECT
|
|
8450
9488
|
l.id,
|
|
9489
|
+
l.cadence_days,
|
|
9490
|
+
${lastContact} AS last_contact,
|
|
9491
|
+
${drift} AS drift,
|
|
8451
9492
|
${overdue} AS overdue_commitment,
|
|
8452
9493
|
l.from_contact_id,
|
|
8453
9494
|
l.to_contact_id,
|
|
@@ -8461,30 +9502,53 @@ function listRadar(db, filters, options) {
|
|
|
8461
9502
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
8462
9503
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
8463
9504
|
WHERE ${where}
|
|
8464
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
9505
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
8465
9506
|
LIMIT ? OFFSET ?`
|
|
8466
9507
|
).all(...params, limit, offset);
|
|
8467
|
-
return rows.map((row) =>
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
9508
|
+
return rows.map((row) => {
|
|
9509
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
9510
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
9511
|
+
`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`
|
|
9512
|
+
).get(contactId, today, now) : void 0;
|
|
9513
|
+
const reasons = [
|
|
9514
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
9515
|
+
...row.drift ? ["drift"] : []
|
|
9516
|
+
];
|
|
9517
|
+
const snippets = [
|
|
9518
|
+
...promise ? [
|
|
9519
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
9520
|
+
] : [],
|
|
9521
|
+
...row.drift ? [
|
|
9522
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
9523
|
+
] : []
|
|
9524
|
+
];
|
|
9525
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
9526
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
9527
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
9528
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
9529
|
+
trust: row.trust ?? 0,
|
|
9530
|
+
rank: row.rank ?? 0
|
|
9531
|
+
});
|
|
9532
|
+
return {
|
|
9533
|
+
linkId: row.id,
|
|
9534
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
9535
|
+
reasons,
|
|
9536
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
9537
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
9538
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
9539
|
+
trust: row.trust ?? 0,
|
|
9540
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
9541
|
+
};
|
|
9542
|
+
});
|
|
8481
9543
|
}
|
|
8482
9544
|
var init_list_radar = __esm({
|
|
8483
9545
|
"src/links/list_radar.ts"() {
|
|
8484
9546
|
init_queries();
|
|
9547
|
+
init_parse_calendar_time();
|
|
8485
9548
|
init_assert_default_ordering();
|
|
8486
9549
|
init_resolve_list_limit_offset();
|
|
8487
9550
|
init_resolve_now();
|
|
9551
|
+
init_mechanics();
|
|
8488
9552
|
}
|
|
8489
9553
|
});
|
|
8490
9554
|
|
|
@@ -8567,13 +9631,40 @@ function enrichReviewItem(db, item) {
|
|
|
8567
9631
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8568
9632
|
}
|
|
8569
9633
|
if (Array.isArray(item.participants)) {
|
|
8570
|
-
result.
|
|
8571
|
-
(p) =>
|
|
9634
|
+
result.participants = item.participants.map(
|
|
9635
|
+
(p) => ({
|
|
9636
|
+
id: p.contactId,
|
|
9637
|
+
name: name(p.contactId),
|
|
9638
|
+
role: p.role
|
|
9639
|
+
})
|
|
8572
9640
|
);
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
9641
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
9642
|
+
result[key] = void 0;
|
|
9643
|
+
}
|
|
9644
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
9645
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
9646
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
9647
|
+
if (result.title === result.summary) result.title = void 0;
|
|
9648
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
9649
|
+
for (const key of [
|
|
9650
|
+
"affinity",
|
|
9651
|
+
"trust",
|
|
9652
|
+
"rank",
|
|
9653
|
+
"recencyScore",
|
|
9654
|
+
"driftPriority",
|
|
9655
|
+
"readinessScore",
|
|
9656
|
+
"normalizedRank",
|
|
9657
|
+
"reciprocityScore",
|
|
9658
|
+
"radarScore",
|
|
9659
|
+
"subtitle",
|
|
9660
|
+
"snippet",
|
|
9661
|
+
"targetRef",
|
|
9662
|
+
"occursOnDate"
|
|
9663
|
+
])
|
|
9664
|
+
result[key] = void 0;
|
|
9665
|
+
} else if (item.kind === "link") {
|
|
9666
|
+
result.recommendedReason = void 0;
|
|
9667
|
+
result.snippet = void 0;
|
|
8577
9668
|
}
|
|
8578
9669
|
if (typeof item.leftContactId === "number")
|
|
8579
9670
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -8610,6 +9701,8 @@ var init_tool_summary = __esm({
|
|
|
8610
9701
|
// src/tools/review_affinity_tool.ts
|
|
8611
9702
|
function buildReadOptions(input) {
|
|
8612
9703
|
return {
|
|
9704
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
9705
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
8613
9706
|
...input.now === void 0 ? {} : { now: input.now },
|
|
8614
9707
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
8615
9708
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -8746,7 +9839,7 @@ function momentItem(moment) {
|
|
|
8746
9839
|
id: moment.eventId,
|
|
8747
9840
|
title: moment.summary,
|
|
8748
9841
|
subtitle: moment.momentKind,
|
|
8749
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
9842
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8750
9843
|
};
|
|
8751
9844
|
}
|
|
8752
9845
|
function commitmentItem(commitment) {
|
|
@@ -8756,7 +9849,7 @@ function commitmentItem(commitment) {
|
|
|
8756
9849
|
id: commitment.eventId,
|
|
8757
9850
|
title: commitment.summary,
|
|
8758
9851
|
subtitle: commitment.type,
|
|
8759
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
9852
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8760
9853
|
};
|
|
8761
9854
|
}
|
|
8762
9855
|
function radarItem(radar) {
|
|
@@ -8772,6 +9865,7 @@ function radarItem(radar) {
|
|
|
8772
9865
|
function upcomingDateItem(date) {
|
|
8773
9866
|
return {
|
|
8774
9867
|
...date,
|
|
9868
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
8775
9869
|
occursAt: date.occursOn,
|
|
8776
9870
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
8777
9871
|
kind: date.targetRef.kind,
|
|
@@ -8809,6 +9903,9 @@ function compactFilters(input) {
|
|
|
8809
9903
|
);
|
|
8810
9904
|
}
|
|
8811
9905
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
9906
|
+
return reviewPage(db, rawInput, false);
|
|
9907
|
+
}
|
|
9908
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
8812
9909
|
const input = structuredClone(rawInput);
|
|
8813
9910
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8814
9911
|
if (inputError)
|
|
@@ -8823,6 +9920,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8823
9920
|
if (invalid) {
|
|
8824
9921
|
return invalid;
|
|
8825
9922
|
}
|
|
9923
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
9924
|
+
const limit = input.limit ?? 50;
|
|
9925
|
+
const offset = input.offset ?? 0;
|
|
9926
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
9927
|
+
return toolFailure(
|
|
9928
|
+
"protocol",
|
|
9929
|
+
"invalid_input",
|
|
9930
|
+
"Invalid pagination.",
|
|
9931
|
+
"limit must be 1..250; offset must be non-negative"
|
|
9932
|
+
);
|
|
9933
|
+
const all = [];
|
|
9934
|
+
let page;
|
|
9935
|
+
do {
|
|
9936
|
+
page = reviewPage(
|
|
9937
|
+
db,
|
|
9938
|
+
{ ...input, limit: 250, offset: all.length },
|
|
9939
|
+
true
|
|
9940
|
+
);
|
|
9941
|
+
if (!page.ok) return page;
|
|
9942
|
+
all.push(...page.data.items);
|
|
9943
|
+
} while (page.data.items.length === 250);
|
|
9944
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9945
|
+
return toolSuccess(
|
|
9946
|
+
summarizeCount(items2.length, "item"),
|
|
9947
|
+
{
|
|
9948
|
+
view: input.view,
|
|
9949
|
+
items: items2,
|
|
9950
|
+
count: items2.length,
|
|
9951
|
+
total: all.length,
|
|
9952
|
+
hasMore: offset + items2.length < all.length,
|
|
9953
|
+
appliedFilters: compactFilters(input)
|
|
9954
|
+
},
|
|
9955
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
9956
|
+
);
|
|
9957
|
+
}
|
|
8826
9958
|
const options = buildReadOptions(input);
|
|
8827
9959
|
let items = [];
|
|
8828
9960
|
let chart;
|
|
@@ -8854,6 +9986,19 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8854
9986
|
options
|
|
8855
9987
|
).map(duplicateCandidateItem);
|
|
8856
9988
|
break;
|
|
9989
|
+
case "events.thread": {
|
|
9990
|
+
if (!input.ref)
|
|
9991
|
+
return toolFailure(
|
|
9992
|
+
"protocol",
|
|
9993
|
+
"invalid_input",
|
|
9994
|
+
"Missing thread reference.",
|
|
9995
|
+
"ref is required for events.thread."
|
|
9996
|
+
);
|
|
9997
|
+
items = getEventThread(db, input.ref.source, input.ref.id, options).map(
|
|
9998
|
+
toEventListToolItem
|
|
9999
|
+
);
|
|
10000
|
+
break;
|
|
10001
|
+
}
|
|
8857
10002
|
case "events.contact_journal": {
|
|
8858
10003
|
const locator = input.contact;
|
|
8859
10004
|
if (locator === void 0) {
|
|
@@ -8995,6 +10140,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8995
10140
|
items = listRadar(
|
|
8996
10141
|
db,
|
|
8997
10142
|
{
|
|
10143
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8998
10144
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8999
10145
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
9000
10146
|
},
|
|
@@ -9044,6 +10190,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9044
10190
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
9045
10191
|
break;
|
|
9046
10192
|
}
|
|
10193
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
10194
|
+
items = items.map((item) => {
|
|
10195
|
+
const content = getEventById(
|
|
10196
|
+
db,
|
|
10197
|
+
Number(item.eventId ?? item.id)
|
|
10198
|
+
)?.content;
|
|
10199
|
+
return content === void 0 ? item : { ...item, content };
|
|
10200
|
+
});
|
|
9047
10201
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
9048
10202
|
return toolSuccess(
|
|
9049
10203
|
summarizeCount(items.length, "item"),
|
|
@@ -9055,7 +10209,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9055
10209
|
...chart === void 0 ? {} : { chart }
|
|
9056
10210
|
},
|
|
9057
10211
|
{
|
|
9058
|
-
entities: items,
|
|
10212
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
9059
10213
|
warnings: items.length === 0 ? [
|
|
9060
10214
|
toolWarning(
|
|
9061
10215
|
"empty_result",
|
|
@@ -9072,6 +10226,8 @@ var init_review_affinity_tool = __esm({
|
|
|
9072
10226
|
init_list_contacts();
|
|
9073
10227
|
init_list_duplicate_candidates();
|
|
9074
10228
|
init_list_upcoming_dates();
|
|
10229
|
+
init_event_discovery();
|
|
10230
|
+
init_event_metadata();
|
|
9075
10231
|
init_get_contact_journal();
|
|
9076
10232
|
init_list_moments();
|
|
9077
10233
|
init_list_open_commitments();
|
|
@@ -9084,6 +10240,7 @@ var init_review_affinity_tool = __esm({
|
|
|
9084
10240
|
init_get_merge_history();
|
|
9085
10241
|
init_list_dismissed_duplicates();
|
|
9086
10242
|
init_enrich_review_item();
|
|
10243
|
+
init_event_metadata_schema();
|
|
9087
10244
|
init_tool_metadata();
|
|
9088
10245
|
init_tool_names();
|
|
9089
10246
|
init_tool_ref();
|
|
@@ -9133,9 +10290,15 @@ var init_review_affinity_tool = __esm({
|
|
|
9133
10290
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
9134
10291
|
inputSchema: objectSchema(
|
|
9135
10292
|
{
|
|
10293
|
+
ref: externalRefSchema,
|
|
10294
|
+
includeContent: booleanSchema(
|
|
10295
|
+
"Include verbatim content in event rows; defaults to false."
|
|
10296
|
+
),
|
|
10297
|
+
channel: stringSchema("Filter events by channel."),
|
|
9136
10298
|
view: enumSchema("Which review surface to return.", [
|
|
9137
10299
|
"contacts.list",
|
|
9138
10300
|
"contacts.duplicates",
|
|
10301
|
+
"events.thread",
|
|
9139
10302
|
"events.contact_journal",
|
|
9140
10303
|
"events.link_timeline",
|
|
9141
10304
|
"events.moments",
|
|
@@ -9190,7 +10353,20 @@ var init_review_affinity_tool = __esm({
|
|
|
9190
10353
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
9191
10354
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
9192
10355
|
eventTypes: arraySchema(
|
|
9193
|
-
|
|
10356
|
+
enumSchema("Event type.", [
|
|
10357
|
+
"conversation",
|
|
10358
|
+
"activity",
|
|
10359
|
+
"gift",
|
|
10360
|
+
"support",
|
|
10361
|
+
"milestone",
|
|
10362
|
+
"observation",
|
|
10363
|
+
"conflict",
|
|
10364
|
+
"correction",
|
|
10365
|
+
"transaction",
|
|
10366
|
+
"promise",
|
|
10367
|
+
"agreement",
|
|
10368
|
+
"date_anchor"
|
|
10369
|
+
]),
|
|
9194
10370
|
"Optional event-type filter."
|
|
9195
10371
|
),
|
|
9196
10372
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -9232,7 +10408,7 @@ var init_review_affinity_tool = __esm({
|
|
|
9232
10408
|
|
|
9233
10409
|
// src/contacts/search_contacts.ts
|
|
9234
10410
|
function searchContacts(db, query, filters, options) {
|
|
9235
|
-
const q = foldSearchText(query.trim());
|
|
10411
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
9236
10412
|
if (!q) return [];
|
|
9237
10413
|
assertDefaultOrdering("searchContacts", options);
|
|
9238
10414
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -9252,6 +10428,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
9252
10428
|
const tokens = q.split(/\s+/);
|
|
9253
10429
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
9254
10430
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
10431
|
+
const suffix = digits.replace(/^0/, "");
|
|
9255
10432
|
const attributeFilters = contactAttributeClauses(filters);
|
|
9256
10433
|
const clauses = [
|
|
9257
10434
|
"deleted_at IS NULL",
|
|
@@ -9287,7 +10464,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
9287
10464
|
)
|
|
9288
10465
|
];
|
|
9289
10466
|
const phoneFields = phone ? values.filter(
|
|
9290
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
10467
|
+
(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)
|
|
9291
10468
|
) : [];
|
|
9292
10469
|
if (!phoneFields.length && !tokens.every(
|
|
9293
10470
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -9326,6 +10503,9 @@ var init_search_contacts = __esm({
|
|
|
9326
10503
|
|
|
9327
10504
|
// src/tools/search_affinity_tool.ts
|
|
9328
10505
|
function searchAffinityToolHandler(db, rawInput) {
|
|
10506
|
+
return searchPage(db, rawInput, false);
|
|
10507
|
+
}
|
|
10508
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
9329
10509
|
const input = structuredClone(rawInput);
|
|
9330
10510
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9331
10511
|
if (inputError)
|
|
@@ -9336,11 +10516,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9336
10516
|
inputError
|
|
9337
10517
|
);
|
|
9338
10518
|
return withToolHandling(() => {
|
|
10519
|
+
if (!pageOnly) {
|
|
10520
|
+
const limit = input.limit ?? 50;
|
|
10521
|
+
const offset = input.offset ?? 0;
|
|
10522
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
10523
|
+
return toolFailure(
|
|
10524
|
+
"protocol",
|
|
10525
|
+
"invalid_input",
|
|
10526
|
+
"Invalid pagination.",
|
|
10527
|
+
"limit must be 1..250; offset must be non-negative"
|
|
10528
|
+
);
|
|
10529
|
+
const all = [];
|
|
10530
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
10531
|
+
for (const scope of scopes) {
|
|
10532
|
+
let page;
|
|
10533
|
+
let nextOffset = 0;
|
|
10534
|
+
do {
|
|
10535
|
+
page = searchPage(
|
|
10536
|
+
db,
|
|
10537
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
10538
|
+
true
|
|
10539
|
+
);
|
|
10540
|
+
if (!page.ok) return page;
|
|
10541
|
+
all.push(...page.data.items);
|
|
10542
|
+
nextOffset += page.data.count;
|
|
10543
|
+
} while (page.data.count === 250);
|
|
10544
|
+
}
|
|
10545
|
+
const items2 = all.slice(offset, offset + limit);
|
|
10546
|
+
return toolSuccess(
|
|
10547
|
+
summarizeCount(
|
|
10548
|
+
items2.length,
|
|
10549
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10550
|
+
),
|
|
10551
|
+
{
|
|
10552
|
+
query: input.query,
|
|
10553
|
+
items: items2,
|
|
10554
|
+
count: items2.length,
|
|
10555
|
+
total: all.length,
|
|
10556
|
+
hasMore: offset + items2.length < all.length
|
|
10557
|
+
},
|
|
10558
|
+
{ entities: items2 }
|
|
10559
|
+
);
|
|
10560
|
+
}
|
|
9339
10561
|
const options = {
|
|
9340
10562
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
9341
10563
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
9342
10564
|
};
|
|
9343
|
-
const rows = searchContacts(
|
|
10565
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
9344
10566
|
db,
|
|
9345
10567
|
input.query,
|
|
9346
10568
|
{
|
|
@@ -9354,9 +10576,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9354
10576
|
},
|
|
9355
10577
|
options
|
|
9356
10578
|
);
|
|
9357
|
-
const items =
|
|
10579
|
+
const items = [
|
|
10580
|
+
...rows.map(toContactListToolItem),
|
|
10581
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
10582
|
+
];
|
|
9358
10583
|
return toolSuccess(
|
|
9359
|
-
summarizeCount(
|
|
10584
|
+
summarizeCount(
|
|
10585
|
+
items.length,
|
|
10586
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10587
|
+
),
|
|
9360
10588
|
{
|
|
9361
10589
|
query: input.query,
|
|
9362
10590
|
items,
|
|
@@ -9372,6 +10600,7 @@ var searchAffinityTool;
|
|
|
9372
10600
|
var init_search_affinity_tool = __esm({
|
|
9373
10601
|
"src/tools/search_affinity_tool.ts"() {
|
|
9374
10602
|
init_search_contacts();
|
|
10603
|
+
init_event_discovery();
|
|
9375
10604
|
init_tool_metadata();
|
|
9376
10605
|
init_tool_names();
|
|
9377
10606
|
init_tool_ref();
|
|
@@ -9404,6 +10633,11 @@ var init_search_affinity_tool = __esm({
|
|
|
9404
10633
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
9405
10634
|
inputSchema: objectSchema(
|
|
9406
10635
|
{
|
|
10636
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
10637
|
+
"contacts",
|
|
10638
|
+
"events",
|
|
10639
|
+
"all"
|
|
10640
|
+
]),
|
|
9407
10641
|
query: stringSchema("Free-text or natural-key query."),
|
|
9408
10642
|
kind: enumSchema("Optional contact kind filter.", [
|
|
9409
10643
|
"human",
|
|
@@ -9454,6 +10688,7 @@ var init_tool_registry = __esm({
|
|
|
9454
10688
|
init_manage_contact_tool();
|
|
9455
10689
|
init_manage_date_anchor_tool();
|
|
9456
10690
|
init_manage_duplicate_dismissal_tool();
|
|
10691
|
+
init_manage_event_tool();
|
|
9457
10692
|
init_manage_identity_tool();
|
|
9458
10693
|
init_manage_relationship_tool();
|
|
9459
10694
|
init_merge_contacts_tool();
|
|
@@ -9472,77 +10707,42 @@ var init_tool_registry = __esm({
|
|
|
9472
10707
|
recordEventTool,
|
|
9473
10708
|
manageCommitmentTool,
|
|
9474
10709
|
manageDateAnchorTool,
|
|
9475
|
-
manageAttributeTool
|
|
10710
|
+
manageAttributeTool,
|
|
10711
|
+
manageEventTool
|
|
9476
10712
|
];
|
|
9477
10713
|
}
|
|
9478
10714
|
});
|
|
9479
10715
|
|
|
9480
10716
|
// src/surface/plain_dates.ts
|
|
9481
|
-
function
|
|
9482
|
-
const invalid = () => new CliUsageError(
|
|
9483
|
-
`Invalid timestamp "${raw}". Use YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d, -2d or an ISO date-time.`
|
|
9484
|
-
);
|
|
9485
|
-
if (/^-?\d+$/.test(raw)) {
|
|
9486
|
-
const value = Number(raw);
|
|
9487
|
-
if (Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()))
|
|
9488
|
-
return value;
|
|
9489
|
-
throw invalid();
|
|
9490
|
-
}
|
|
9491
|
-
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
9492
|
-
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw);
|
|
9493
|
-
let date;
|
|
9494
|
-
if (iso || european) {
|
|
9495
|
-
const year = Number(iso?.[1] ?? european?.[3]);
|
|
9496
|
-
const month = Number(iso?.[2] ?? european?.[2]);
|
|
9497
|
-
const day = Number(iso?.[3] ?? european?.[1]);
|
|
9498
|
-
date = /* @__PURE__ */ new Date(0);
|
|
9499
|
-
date.setFullYear(year, month - 1, day);
|
|
9500
|
-
date.setHours(
|
|
9501
|
-
endOfDay ? 23 : 0,
|
|
9502
|
-
endOfDay ? 59 : 0,
|
|
9503
|
-
endOfDay ? 59 : 0,
|
|
9504
|
-
endOfDay ? 999 : 0
|
|
9505
|
-
);
|
|
9506
|
-
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day)
|
|
9507
|
-
throw invalid();
|
|
9508
|
-
} else if (raw === "today" || raw === "tomorrow" || raw === "yesterday" || /^[+-]\d+d$/.test(raw)) {
|
|
9509
|
-
date = new Date(now);
|
|
9510
|
-
date.setDate(
|
|
9511
|
-
date.getDate() + (raw === "today" ? 0 : raw === "tomorrow" ? 1 : raw === "yesterday" ? -1 : Number(raw.slice(0, -1)))
|
|
9512
|
-
);
|
|
9513
|
-
date.setHours(
|
|
9514
|
-
endOfDay ? 23 : 0,
|
|
9515
|
-
endOfDay ? 59 : 0,
|
|
9516
|
-
endOfDay ? 59 : 0,
|
|
9517
|
-
endOfDay ? 999 : 0
|
|
9518
|
-
);
|
|
9519
|
-
} else {
|
|
9520
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
9521
|
-
raw
|
|
9522
|
-
))
|
|
9523
|
-
throw invalid();
|
|
9524
|
-
parsePlainDue(raw.slice(0, 10), now);
|
|
9525
|
-
date = new Date(raw);
|
|
9526
|
-
}
|
|
9527
|
-
if (!Number.isFinite(date.getTime())) throw invalid();
|
|
9528
|
-
return date.getTime();
|
|
9529
|
-
}
|
|
9530
|
-
function parsePlainStart(raw, now = Date.now()) {
|
|
9531
|
-
return parsePlainDue(raw, now, false);
|
|
9532
|
-
}
|
|
9533
|
-
function normalizeTimeInputs(value, now = Date.now()) {
|
|
10717
|
+
function normalizeTimeInputs(value, now = Date.now(), inPatch = false) {
|
|
9534
10718
|
if (Array.isArray(value))
|
|
9535
10719
|
return value.map((item) => normalizeTimeInputs(item, now));
|
|
9536
10720
|
if (!value || typeof value !== "object") return value;
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
10721
|
+
const result = {};
|
|
10722
|
+
for (const [key, item] of Object.entries(value)) {
|
|
10723
|
+
if (key === "occurredAt" && item === null)
|
|
10724
|
+
throw new CliUsageError(
|
|
10725
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
10726
|
+
);
|
|
10727
|
+
if (typeof item === "string" && (key.endsWith("At") || ["asOf", "now", "since", "until"].includes(key))) {
|
|
10728
|
+
let parsed;
|
|
10729
|
+
try {
|
|
10730
|
+
parsed = parseCalendarTime(item, key, now);
|
|
10731
|
+
} catch (error) {
|
|
10732
|
+
throw new CliUsageError(
|
|
10733
|
+
`Invalid timestamp "${item}". ${error.message}`
|
|
10734
|
+
);
|
|
10735
|
+
}
|
|
10736
|
+
result[key] = parsed.timestamp;
|
|
10737
|
+
if (!inPatch && ["occurredAt", "dueAt"].includes(key))
|
|
10738
|
+
result[key.replace(/At$/, "On")] = parsed.day;
|
|
10739
|
+
} else result[key] = normalizeTimeInputs(item, now, key === "patch");
|
|
10740
|
+
}
|
|
10741
|
+
return result;
|
|
9543
10742
|
}
|
|
9544
10743
|
var init_plain_dates = __esm({
|
|
9545
10744
|
"src/surface/plain_dates.ts"() {
|
|
10745
|
+
init_parse_calendar_time();
|
|
9546
10746
|
init_cli_args();
|
|
9547
10747
|
}
|
|
9548
10748
|
});
|
|
@@ -9575,6 +10775,29 @@ var init_timestamp_schema = __esm({
|
|
|
9575
10775
|
});
|
|
9576
10776
|
|
|
9577
10777
|
// src/surface/affinity_surface_tools.ts
|
|
10778
|
+
function surfaceProperties(tool) {
|
|
10779
|
+
return {
|
|
10780
|
+
...tool.inputSchema.properties,
|
|
10781
|
+
...["inspect_affinity_item", "review_affinity", "manage_event"].includes(
|
|
10782
|
+
tool.name
|
|
10783
|
+
) ? {
|
|
10784
|
+
refSource: {
|
|
10785
|
+
type: "string",
|
|
10786
|
+
description: "External reference source."
|
|
10787
|
+
},
|
|
10788
|
+
refId: {
|
|
10789
|
+
type: "string",
|
|
10790
|
+
description: "External reference id."
|
|
10791
|
+
}
|
|
10792
|
+
} : {},
|
|
10793
|
+
...tool.name === "review_affinity" ? {
|
|
10794
|
+
withContent: {
|
|
10795
|
+
type: "boolean",
|
|
10796
|
+
description: "Include verbatim event content."
|
|
10797
|
+
}
|
|
10798
|
+
} : {}
|
|
10799
|
+
};
|
|
10800
|
+
}
|
|
9578
10801
|
var affinitySurfaceTools;
|
|
9579
10802
|
var init_affinity_surface_tools = __esm({
|
|
9580
10803
|
"src/surface/affinity_surface_tools.ts"() {
|
|
@@ -9586,17 +10809,31 @@ var init_affinity_surface_tools = __esm({
|
|
|
9586
10809
|
affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
9587
10810
|
name: tool.name,
|
|
9588
10811
|
description: tool.description,
|
|
9589
|
-
whenToUse: tool.whenToUse,
|
|
9590
|
-
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
9591
10812
|
readOnly: tool.readOnly,
|
|
9592
10813
|
sideEffects: tool.sideEffects,
|
|
9593
10814
|
inputSchema: {
|
|
9594
|
-
...timestampSchema({
|
|
10815
|
+
...timestampSchema({
|
|
10816
|
+
...tool.inputSchema,
|
|
10817
|
+
properties: surfaceProperties(tool),
|
|
10818
|
+
type: "object"
|
|
10819
|
+
}),
|
|
9595
10820
|
type: "object"
|
|
9596
10821
|
},
|
|
9597
10822
|
handler: (db, input) => {
|
|
9598
10823
|
try {
|
|
9599
|
-
|
|
10824
|
+
const { withContent, refSource, refId, ...normalized } = input;
|
|
10825
|
+
if (withContent !== void 0) normalized.includeContent = withContent;
|
|
10826
|
+
if (refSource !== void 0 || refId !== void 0) {
|
|
10827
|
+
if (normalized.ref !== void 0)
|
|
10828
|
+
return toolFailure(
|
|
10829
|
+
"protocol",
|
|
10830
|
+
"invalid_input",
|
|
10831
|
+
"Invalid target.",
|
|
10832
|
+
"Provide ref or ref-source/ref-id, not both."
|
|
10833
|
+
);
|
|
10834
|
+
normalized.ref = { source: refSource, id: refId };
|
|
10835
|
+
}
|
|
10836
|
+
return tool.handler(db, normalizeTimeInputs(normalized));
|
|
9600
10837
|
} catch (error) {
|
|
9601
10838
|
if (error instanceof CliUsageError)
|
|
9602
10839
|
return toolFailure(
|
|
@@ -9623,7 +10860,7 @@ function affinityMcpOptions(context) {
|
|
|
9623
10860
|
context,
|
|
9624
10861
|
annotations: (tool) => ({
|
|
9625
10862
|
readOnlyHint: tool.readOnly ?? false,
|
|
9626
|
-
destructiveHint: tool.name === "merge_contacts" || /remove|unset|undismiss/.test(
|
|
10863
|
+
destructiveHint: tool.name === "merge_contacts" || tool.name === "manage_event" || /remove|unset|undismiss/.test(
|
|
9627
10864
|
String(tool.inputSchema.properties?.action?.enum ?? [])
|
|
9628
10865
|
),
|
|
9629
10866
|
idempotentHint: tool.readOnly ?? false,
|
|
@@ -9639,13 +10876,99 @@ var init_affinity_mcp_options = __esm({
|
|
|
9639
10876
|
}
|
|
9640
10877
|
});
|
|
9641
10878
|
|
|
10879
|
+
// src/events/init_events_tables.ts
|
|
10880
|
+
function initEventsTables(db) {
|
|
10881
|
+
db.exec(`
|
|
10882
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
10883
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10884
|
+
type TEXT NOT NULL,
|
|
10885
|
+
occurred_at INTEGER NOT NULL,
|
|
10886
|
+
summary TEXT NOT NULL,
|
|
10887
|
+
significance REAL NOT NULL DEFAULT 0,
|
|
10888
|
+
moment_kind TEXT,
|
|
10889
|
+
recurrence_kind TEXT,
|
|
10890
|
+
anchor_month INTEGER,
|
|
10891
|
+
anchor_day INTEGER,
|
|
10892
|
+
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
10893
|
+
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
10894
|
+
created_at INTEGER NOT NULL,
|
|
10895
|
+
updated_at INTEGER NOT NULL,
|
|
10896
|
+
deleted_at INTEGER,
|
|
10897
|
+
CHECK (length(trim(summary)) > 0),
|
|
10898
|
+
CHECK (
|
|
10899
|
+
type IN (
|
|
10900
|
+
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
10901
|
+
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
10902
|
+
)
|
|
10903
|
+
),
|
|
10904
|
+
CHECK (
|
|
10905
|
+
(type = 'date_anchor' AND (
|
|
10906
|
+
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
10907
|
+
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
10908
|
+
))
|
|
10909
|
+
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
10910
|
+
),
|
|
10911
|
+
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
10912
|
+
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
10913
|
+
)),
|
|
10914
|
+
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
10915
|
+
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
10916
|
+
)),
|
|
10917
|
+
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
10918
|
+
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
10919
|
+
)
|
|
10920
|
+
`);
|
|
10921
|
+
db.exec(
|
|
10922
|
+
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
10923
|
+
);
|
|
10924
|
+
db.exec(
|
|
10925
|
+
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
10926
|
+
);
|
|
10927
|
+
const columns = db.prepare("PRAGMA table_info(events)").all();
|
|
10928
|
+
if (!columns.some((c) => c.name === "declared_moment_kind"))
|
|
10929
|
+
db.exec("ALTER TABLE events ADD COLUMN declared_moment_kind TEXT");
|
|
10930
|
+
if (!columns.some((c) => c.name === "affects_bond"))
|
|
10931
|
+
db.exec("ALTER TABLE events ADD COLUMN affects_bond INTEGER");
|
|
10932
|
+
if (!columns.some((c) => c.name === "content"))
|
|
10933
|
+
db.exec("ALTER TABLE events ADD COLUMN content TEXT");
|
|
10934
|
+
if (!columns.some((c) => c.name === "external_source"))
|
|
10935
|
+
db.exec("ALTER TABLE events ADD COLUMN external_source TEXT");
|
|
10936
|
+
if (!columns.some((c) => c.name === "external_id"))
|
|
10937
|
+
db.exec("ALTER TABLE events ADD COLUMN external_id TEXT");
|
|
10938
|
+
if (!columns.some((c) => c.name === "channel"))
|
|
10939
|
+
db.exec("ALTER TABLE events ADD COLUMN channel TEXT");
|
|
10940
|
+
if (!columns.some((c) => c.name === "parent_source"))
|
|
10941
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_source TEXT");
|
|
10942
|
+
if (!columns.some((c) => c.name === "parent_ref_id"))
|
|
10943
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_ref_id TEXT");
|
|
10944
|
+
if (!columns.some((c) => c.name === "parent_event_id"))
|
|
10945
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_event_id INTEGER");
|
|
10946
|
+
if (!columns.some((c) => c.name === "thread_source"))
|
|
10947
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_source TEXT");
|
|
10948
|
+
if (!columns.some((c) => c.name === "thread_id"))
|
|
10949
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_id TEXT");
|
|
10950
|
+
if (!columns.some((c) => c.name === "provenance_json"))
|
|
10951
|
+
db.exec("ALTER TABLE events ADD COLUMN provenance_json TEXT");
|
|
10952
|
+
db.exec(
|
|
10953
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external_ref ON events(external_source, external_id)"
|
|
10954
|
+
);
|
|
10955
|
+
if (!columns.some((column) => column.name === "occurred_on"))
|
|
10956
|
+
db.exec("ALTER TABLE events ADD COLUMN occurred_on TEXT");
|
|
10957
|
+
}
|
|
10958
|
+
var init_init_events_tables = __esm({
|
|
10959
|
+
"src/events/init_events_tables.ts"() {
|
|
10960
|
+
}
|
|
10961
|
+
});
|
|
10962
|
+
|
|
9642
10963
|
// src/attributes/init_attributes_tables.ts
|
|
9643
10964
|
function initAttributesTables(db) {
|
|
10965
|
+
initEventsTables(db);
|
|
9644
10966
|
db.exec(`
|
|
9645
10967
|
CREATE TABLE IF NOT EXISTS attributes (
|
|
9646
10968
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9647
10969
|
contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
9648
10970
|
link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
10971
|
+
event_id INTEGER REFERENCES events (id) ON DELETE CASCADE,
|
|
9649
10972
|
name TEXT NOT NULL,
|
|
9650
10973
|
value TEXT,
|
|
9651
10974
|
created_at INTEGER NOT NULL,
|
|
@@ -9653,15 +10976,35 @@ function initAttributesTables(db) {
|
|
|
9653
10976
|
deleted_at INTEGER,
|
|
9654
10977
|
CHECK (length(trim(name)) > 0),
|
|
9655
10978
|
CHECK (
|
|
9656
|
-
(contact_id IS NOT NULL
|
|
9657
|
-
OR
|
|
9658
|
-
(contact_id IS NULL AND link_id IS NOT NULL)
|
|
10979
|
+
(contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1
|
|
9659
10980
|
)
|
|
9660
10981
|
)
|
|
9661
10982
|
`);
|
|
10983
|
+
const columns = db.prepare("PRAGMA table_info(attributes)").all();
|
|
10984
|
+
if (!columns.some((c) => c.name === "event_id")) {
|
|
10985
|
+
withTransaction(
|
|
10986
|
+
db,
|
|
10987
|
+
() => db.exec(`CREATE TABLE attributes_upgrade (
|
|
10988
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10989
|
+
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
|
|
10990
|
+
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
|
|
10991
|
+
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
|
|
10992
|
+
name TEXT NOT NULL CHECK(length(trim(name)) > 0), value TEXT,
|
|
10993
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
10994
|
+
CHECK ((contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1)
|
|
10995
|
+
);
|
|
10996
|
+
INSERT INTO attributes_upgrade (id,contact_id,link_id,name,value,created_at,updated_at,deleted_at)
|
|
10997
|
+
SELECT id,contact_id,link_id,name,value,created_at,updated_at,deleted_at FROM attributes;
|
|
10998
|
+
DROP TABLE attributes;
|
|
10999
|
+
ALTER TABLE attributes_upgrade RENAME TO attributes;`)
|
|
11000
|
+
);
|
|
11001
|
+
}
|
|
9662
11002
|
db.exec(
|
|
9663
11003
|
"CREATE INDEX IF NOT EXISTS idx_attributes_contact ON attributes(contact_id) WHERE deleted_at IS NULL"
|
|
9664
11004
|
);
|
|
11005
|
+
db.exec(
|
|
11006
|
+
"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"
|
|
11007
|
+
);
|
|
9665
11008
|
db.exec(
|
|
9666
11009
|
"CREATE INDEX IF NOT EXISTS idx_attributes_link ON attributes(link_id) WHERE deleted_at IS NULL"
|
|
9667
11010
|
);
|
|
@@ -9678,6 +11021,8 @@ function initAttributesTables(db) {
|
|
|
9678
11021
|
}
|
|
9679
11022
|
var init_init_attributes_tables = __esm({
|
|
9680
11023
|
"src/attributes/init_attributes_tables.ts"() {
|
|
11024
|
+
init_init_events_tables();
|
|
11025
|
+
init_with_transaction();
|
|
9681
11026
|
}
|
|
9682
11027
|
});
|
|
9683
11028
|
|
|
@@ -9794,60 +11139,6 @@ var init_init_event_participants_tables = __esm({
|
|
|
9794
11139
|
}
|
|
9795
11140
|
});
|
|
9796
11141
|
|
|
9797
|
-
// src/events/init_events_tables.ts
|
|
9798
|
-
function initEventsTables(db) {
|
|
9799
|
-
db.exec(`
|
|
9800
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
9801
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9802
|
-
type TEXT NOT NULL,
|
|
9803
|
-
occurred_at INTEGER NOT NULL,
|
|
9804
|
-
summary TEXT NOT NULL,
|
|
9805
|
-
significance REAL NOT NULL DEFAULT 0,
|
|
9806
|
-
moment_kind TEXT,
|
|
9807
|
-
recurrence_kind TEXT,
|
|
9808
|
-
anchor_month INTEGER,
|
|
9809
|
-
anchor_day INTEGER,
|
|
9810
|
-
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
9811
|
-
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
9812
|
-
created_at INTEGER NOT NULL,
|
|
9813
|
-
updated_at INTEGER NOT NULL,
|
|
9814
|
-
deleted_at INTEGER,
|
|
9815
|
-
CHECK (length(trim(summary)) > 0),
|
|
9816
|
-
CHECK (
|
|
9817
|
-
type IN (
|
|
9818
|
-
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
9819
|
-
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
9820
|
-
)
|
|
9821
|
-
),
|
|
9822
|
-
CHECK (
|
|
9823
|
-
(type = 'date_anchor' AND (
|
|
9824
|
-
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
9825
|
-
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
9826
|
-
))
|
|
9827
|
-
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
9828
|
-
),
|
|
9829
|
-
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
9830
|
-
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
9831
|
-
)),
|
|
9832
|
-
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
9833
|
-
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
9834
|
-
)),
|
|
9835
|
-
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
9836
|
-
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
9837
|
-
)
|
|
9838
|
-
`);
|
|
9839
|
-
db.exec(
|
|
9840
|
-
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
9841
|
-
);
|
|
9842
|
-
db.exec(
|
|
9843
|
-
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
9844
|
-
);
|
|
9845
|
-
}
|
|
9846
|
-
var init_init_events_tables = __esm({
|
|
9847
|
-
"src/events/init_events_tables.ts"() {
|
|
9848
|
-
}
|
|
9849
|
-
});
|
|
9850
|
-
|
|
9851
11142
|
// src/events/init_open_commitments_tables.ts
|
|
9852
11143
|
function initOpenCommitmentsTables(db) {
|
|
9853
11144
|
db.exec(`
|
|
@@ -9867,6 +11158,9 @@ function initOpenCommitmentsTables(db) {
|
|
|
9867
11158
|
)
|
|
9868
11159
|
)
|
|
9869
11160
|
`);
|
|
11161
|
+
const columns = db.prepare("PRAGMA table_info(open_commitments)").all();
|
|
11162
|
+
if (!columns.some((column) => column.name === "due_on"))
|
|
11163
|
+
db.exec("ALTER TABLE open_commitments ADD COLUMN due_on TEXT");
|
|
9870
11164
|
}
|
|
9871
11165
|
var init_init_open_commitments_tables = __esm({
|
|
9872
11166
|
"src/events/init_open_commitments_tables.ts"() {
|
|
@@ -9939,6 +11233,21 @@ function initLinkEventEffectsTables(db) {
|
|
|
9939
11233
|
db.exec(
|
|
9940
11234
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
9941
11235
|
);
|
|
11236
|
+
for (const name of ["participant_json", "provenance_json"])
|
|
11237
|
+
if (!columns.some((c) => c.name === name))
|
|
11238
|
+
db.exec(`ALTER TABLE link_event_effects ADD COLUMN ${name} TEXT`);
|
|
11239
|
+
db.exec(
|
|
11240
|
+
"CREATE TABLE IF NOT EXISTS event_link_baselines (link_id INTEGER PRIMARY KEY REFERENCES links(id) ON DELETE CASCADE, state_json TEXT NOT NULL)"
|
|
11241
|
+
);
|
|
11242
|
+
db.exec(
|
|
11243
|
+
"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)"
|
|
11244
|
+
);
|
|
11245
|
+
db.exec(`INSERT OR IGNORE INTO event_link_baselines(link_id,state_json)
|
|
11246
|
+
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)
|
|
11247
|
+
FROM links l JOIN link_event_effects e ON e.link_id=l.id GROUP BY l.id`);
|
|
11248
|
+
db.exec(
|
|
11249
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id IN (SELECT id FROM events WHERE type IN ('promise', 'agreement'))"
|
|
11250
|
+
);
|
|
9942
11251
|
db.exec(
|
|
9943
11252
|
"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"
|
|
9944
11253
|
);
|
|
@@ -10149,7 +11458,10 @@ async function openAffinityDb(path) {
|
|
|
10149
11458
|
} finally {
|
|
10150
11459
|
process.emitWarning = original;
|
|
10151
11460
|
}
|
|
10152
|
-
if (path !== ":memory:"
|
|
11461
|
+
if (path !== ":memory:" && !path.startsWith("file:")) {
|
|
11462
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
11463
|
+
if (!existsSync(path)) closeSync(openSync(path, "wx", 384));
|
|
11464
|
+
}
|
|
10153
11465
|
const db = new sqlite.DatabaseSync(path);
|
|
10154
11466
|
try {
|
|
10155
11467
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
|