@ghostpaw/affinity 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -9
- package/SKILL.md +43 -386
- package/dist/cli.js +2881 -914
- package/dist/index.cjs +1957 -605
- package/dist/index.d.cts +340 -55
- package/dist/index.d.ts +340 -55
- package/dist/index.js +1957 -605
- package/dist/mcp-server.js +2162 -791
- package/dist/mcp.cjs +1962 -701
- package/dist/mcp.d.cts +3 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +1963 -702
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +43 -386
- package/skills/affinity/references/identify-and-locate-contacts.md +9 -0
- package/skills/affinity/references/import-history-without-faking-evidence.md +25 -0
- package/skills/affinity/references/manage-promises-and-agreements.md +8 -0
- package/skills/affinity/references/manage-recurring-dates-and-reminders.md +8 -0
- package/skills/affinity/references/record-direct-evidence-well.md +18 -0
- package/skills/affinity/references/record-observations-and-referrals.md +11 -0
- package/skills/affinity/references/review-radar-progression-and-graph.md +13 -0
- package/skills/affinity/references/tools.md +490 -0
package/dist/mcp-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.1";
|
|
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,27 @@ 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.provenance_json != null)
|
|
771
|
+
base.provenance = JSON.parse(row.provenance_json);
|
|
772
|
+
if (row.content != null) base.content = decodeEventText(row.content);
|
|
773
|
+
if (row.channel != null) base.channel = row.channel;
|
|
774
|
+
if (row.external_source != null && row.external_id != null)
|
|
775
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
776
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
777
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
778
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
779
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
780
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
781
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
782
|
+
base.dueAt = row.due_at ?? null;
|
|
783
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
784
|
+
}
|
|
586
785
|
if (row.recurrence_kind !== null) {
|
|
587
786
|
base.recurrenceKind = row.recurrence_kind;
|
|
588
787
|
base.anchorMonth = row.anchor_month;
|
|
@@ -612,10 +811,14 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
612
811
|
targetRef,
|
|
613
812
|
recurrenceKind,
|
|
614
813
|
occursOn: row.occurs_on,
|
|
615
|
-
|
|
814
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
815
|
+
summary: decodeEventText(row.summary),
|
|
616
816
|
significance: row.significance
|
|
617
817
|
};
|
|
618
818
|
}
|
|
819
|
+
function decodeEventText(value) {
|
|
820
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
821
|
+
}
|
|
619
822
|
var init_mappers = __esm({
|
|
620
823
|
"src/events/mappers.ts"() {
|
|
621
824
|
init_affinity_invariant_error();
|
|
@@ -626,17 +829,17 @@ var init_mappers = __esm({
|
|
|
626
829
|
// src/events/queries.ts
|
|
627
830
|
function getEventRowById(db, id) {
|
|
628
831
|
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
|
|
832
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
633
833
|
FROM events
|
|
634
834
|
WHERE id = ? AND deleted_at IS NULL`
|
|
635
835
|
).get(id);
|
|
636
836
|
if (row === void 0) {
|
|
637
837
|
return null;
|
|
638
838
|
}
|
|
639
|
-
|
|
839
|
+
const event = row;
|
|
840
|
+
event.summary = decodeEventText(event.summary);
|
|
841
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
842
|
+
return event;
|
|
640
843
|
}
|
|
641
844
|
function findDuplicateDateAnchor(db, params) {
|
|
642
845
|
const exclude = params.excludeEventId;
|
|
@@ -686,6 +889,7 @@ function findDuplicateDateAnchor(db, params) {
|
|
|
686
889
|
}
|
|
687
890
|
var init_queries2 = __esm({
|
|
688
891
|
"src/events/queries.ts"() {
|
|
892
|
+
init_mappers();
|
|
689
893
|
}
|
|
690
894
|
});
|
|
691
895
|
|
|
@@ -708,19 +912,27 @@ function loadEventRecord(db, eventId) {
|
|
|
708
912
|
throw new AffinityInvariantError("event not found");
|
|
709
913
|
}
|
|
710
914
|
const participants = loadEventParticipantViews(db, eventId);
|
|
711
|
-
|
|
915
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
916
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
917
|
+
if (due) {
|
|
918
|
+
record.dueAt = due.due_at;
|
|
919
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
920
|
+
}
|
|
921
|
+
enrichEventMetadata(db, record);
|
|
922
|
+
return record;
|
|
712
923
|
}
|
|
713
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
924
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
714
925
|
if (eventIds.length === 0) {
|
|
715
926
|
return [];
|
|
716
927
|
}
|
|
717
928
|
const ph = eventIds.map(() => "?").join(", ");
|
|
718
929
|
const eventRows = db.prepare(
|
|
719
|
-
`SELECT id, type, occurred_at,
|
|
930
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
931
|
+
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
932
|
recurrence_kind, anchor_month, anchor_day,
|
|
721
933
|
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`
|
|
934
|
+
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
|
|
935
|
+
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
936
|
).all(...eventIds);
|
|
725
937
|
const partRows = db.prepare(
|
|
726
938
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -744,7 +956,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
744
956
|
for (const id of eventIds) {
|
|
745
957
|
const row = rowById.get(id);
|
|
746
958
|
if (row) {
|
|
747
|
-
|
|
959
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
960
|
+
enrichEventMetadata(db, record);
|
|
961
|
+
out.push(record);
|
|
748
962
|
}
|
|
749
963
|
}
|
|
750
964
|
return out;
|
|
@@ -787,6 +1001,7 @@ var init_loaders = __esm({
|
|
|
787
1001
|
"src/events/loaders.ts"() {
|
|
788
1002
|
init_affinity_invariant_error();
|
|
789
1003
|
init_affinity_not_found_error();
|
|
1004
|
+
init_event_metadata();
|
|
790
1005
|
init_mappers();
|
|
791
1006
|
init_queries2();
|
|
792
1007
|
}
|
|
@@ -811,6 +1026,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
811
1026
|
clauses.push(`e.type IN (${ph})`);
|
|
812
1027
|
params.push(...options.eventTypes);
|
|
813
1028
|
}
|
|
1029
|
+
if (options?.channel !== void 0) {
|
|
1030
|
+
clauses.push("e.channel = ?");
|
|
1031
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1032
|
+
}
|
|
814
1033
|
const where = clauses.join(" AND ");
|
|
815
1034
|
const rows = db.prepare(
|
|
816
1035
|
`SELECT e.id FROM events e
|
|
@@ -821,7 +1040,8 @@ function getContactJournal(db, contactId, options) {
|
|
|
821
1040
|
).all(...params, limit, offset);
|
|
822
1041
|
return batchLoadEventRecords(
|
|
823
1042
|
db,
|
|
824
|
-
rows.map((r) => r.id)
|
|
1043
|
+
rows.map((r) => r.id),
|
|
1044
|
+
options?.includeContent ?? false
|
|
825
1045
|
);
|
|
826
1046
|
}
|
|
827
1047
|
var init_get_contact_journal = __esm({
|
|
@@ -832,6 +1052,73 @@ var init_get_contact_journal = __esm({
|
|
|
832
1052
|
}
|
|
833
1053
|
});
|
|
834
1054
|
|
|
1055
|
+
// src/lib/parse_calendar_time.ts
|
|
1056
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
1057
|
+
if (dueAt === null) return false;
|
|
1058
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
1059
|
+
return localCalendarDate(now) > dueOn;
|
|
1060
|
+
}
|
|
1061
|
+
return dueAt < now;
|
|
1062
|
+
}
|
|
1063
|
+
function localCalendarDate(now) {
|
|
1064
|
+
const d = new Date(now);
|
|
1065
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1066
|
+
}
|
|
1067
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
1068
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
1069
|
+
throw new AffinityValidationError(
|
|
1070
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
1071
|
+
);
|
|
1072
|
+
const fail = () => {
|
|
1073
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1074
|
+
};
|
|
1075
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
1076
|
+
const timestamp2 = Number(value);
|
|
1077
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
1078
|
+
return fail();
|
|
1079
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
1080
|
+
}
|
|
1081
|
+
if (typeof value !== "string") return fail();
|
|
1082
|
+
let text = value.trim();
|
|
1083
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
1084
|
+
text = text.replace(" ", "T");
|
|
1085
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
1086
|
+
}
|
|
1087
|
+
text = text.replace(
|
|
1088
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
1089
|
+
(_, digits) => `.${digits.slice(0, 3)}`
|
|
1090
|
+
);
|
|
1091
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
1092
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1093
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
1094
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
1095
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
1096
|
+
text = date.toISOString().slice(0, 10);
|
|
1097
|
+
}
|
|
1098
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
1099
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
1100
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
1101
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1102
|
+
text
|
|
1103
|
+
))
|
|
1104
|
+
return fail();
|
|
1105
|
+
const day = text.slice(0, 10);
|
|
1106
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
1107
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
1108
|
+
return fail();
|
|
1109
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
|
|
1110
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
1111
|
+
return { timestamp, day };
|
|
1112
|
+
}
|
|
1113
|
+
var timeInputForms, OVERDUE_SQL;
|
|
1114
|
+
var init_parse_calendar_time = __esm({
|
|
1115
|
+
"src/lib/parse_calendar_time.ts"() {
|
|
1116
|
+
init_affinity_validation_error();
|
|
1117
|
+
timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
1118
|
+
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)";
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
|
|
835
1122
|
// src/resolve_now.ts
|
|
836
1123
|
function resolveNow(now) {
|
|
837
1124
|
if (now !== void 0) {
|
|
@@ -904,9 +1191,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
904
1191
|
clauses.push("oc.due_at <= ?");
|
|
905
1192
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
906
1193
|
}
|
|
1194
|
+
if (options?.channel !== void 0) {
|
|
1195
|
+
clauses.push("e.channel=?");
|
|
1196
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1197
|
+
}
|
|
907
1198
|
const where = clauses.join(" AND ");
|
|
908
1199
|
const rows = db.prepare(
|
|
909
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
1200
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
910
1201
|
e.type, e.summary
|
|
911
1202
|
FROM open_commitments oc
|
|
912
1203
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -923,7 +1214,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
923
1214
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
924
1215
|
dueAt: r.due_at,
|
|
925
1216
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
926
|
-
|
|
1217
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
1218
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
927
1219
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
928
1220
|
}));
|
|
929
1221
|
}
|
|
@@ -931,6 +1223,7 @@ var DAY_MS;
|
|
|
931
1223
|
var init_list_open_commitments = __esm({
|
|
932
1224
|
"src/events/list_open_commitments.ts"() {
|
|
933
1225
|
init_affinity_validation_error();
|
|
1226
|
+
init_parse_calendar_time();
|
|
934
1227
|
init_assert_default_ordering();
|
|
935
1228
|
init_resolve_list_limit_offset();
|
|
936
1229
|
init_resolve_now();
|
|
@@ -1008,8 +1301,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
1008
1301
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
1009
1302
|
}
|
|
1010
1303
|
function computeNextAnchorOccursOn(params) {
|
|
1011
|
-
const refStart =
|
|
1012
|
-
|
|
1304
|
+
const refStart = Date.parse(
|
|
1305
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
1306
|
+
);
|
|
1307
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
1013
1308
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
1014
1309
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
1015
1310
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -1023,6 +1318,7 @@ function computeNextAnchorOccursOn(params) {
|
|
|
1023
1318
|
}
|
|
1024
1319
|
var init_calendar = __esm({
|
|
1025
1320
|
"src/dates/calendar.ts"() {
|
|
1321
|
+
init_parse_calendar_time();
|
|
1026
1322
|
}
|
|
1027
1323
|
});
|
|
1028
1324
|
|
|
@@ -1240,7 +1536,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1240
1536
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
1241
1537
|
LIMIT ?`
|
|
1242
1538
|
).all(contactId, contactId, topLinksLimit);
|
|
1243
|
-
const topLinks = linkRows.map((l) =>
|
|
1539
|
+
const topLinks = linkRows.map((l) => ({
|
|
1540
|
+
...mapLinkRowToLinkListItem(l),
|
|
1541
|
+
counterpartyName: getContactRowById(
|
|
1542
|
+
db,
|
|
1543
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1544
|
+
)?.name ?? ""
|
|
1545
|
+
}));
|
|
1244
1546
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
1245
1547
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1246
1548
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -1283,7 +1585,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
1283
1585
|
return {
|
|
1284
1586
|
...profile,
|
|
1285
1587
|
recentEvents: getContactJournal(db, contactId, {
|
|
1286
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1588
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1589
|
+
eventTypes: [
|
|
1590
|
+
"conversation",
|
|
1591
|
+
"activity",
|
|
1592
|
+
"gift",
|
|
1593
|
+
"support",
|
|
1594
|
+
"milestone",
|
|
1595
|
+
"observation",
|
|
1596
|
+
"conflict",
|
|
1597
|
+
"correction",
|
|
1598
|
+
"transaction",
|
|
1599
|
+
"promise",
|
|
1600
|
+
"agreement"
|
|
1601
|
+
]
|
|
1287
1602
|
}).map((event) => ({
|
|
1288
1603
|
...event,
|
|
1289
1604
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -1351,6 +1666,10 @@ function listMoments(db, filters, options) {
|
|
|
1351
1666
|
clauses.push("le.link_id = ?");
|
|
1352
1667
|
params.push(filters.linkId);
|
|
1353
1668
|
}
|
|
1669
|
+
if (options?.channel !== void 0) {
|
|
1670
|
+
clauses.push("e.channel=?");
|
|
1671
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1672
|
+
}
|
|
1354
1673
|
const where = clauses.join(" AND ");
|
|
1355
1674
|
return db.prepare(
|
|
1356
1675
|
`SELECT
|
|
@@ -1358,6 +1677,7 @@ function listMoments(db, filters, options) {
|
|
|
1358
1677
|
le.link_id,
|
|
1359
1678
|
le.moment_kind,
|
|
1360
1679
|
le.occurred_at,
|
|
1680
|
+
e.occurred_on,
|
|
1361
1681
|
e.summary,
|
|
1362
1682
|
le.impact_score
|
|
1363
1683
|
FROM link_event_effects le
|
|
@@ -1370,6 +1690,7 @@ function listMoments(db, filters, options) {
|
|
|
1370
1690
|
linkId: row.link_id,
|
|
1371
1691
|
momentKind: row.moment_kind,
|
|
1372
1692
|
occurredAt: row.occurred_at,
|
|
1693
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
1373
1694
|
summary: row.summary,
|
|
1374
1695
|
impactScore: row.impact_score
|
|
1375
1696
|
}));
|
|
@@ -1469,6 +1790,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1469
1790
|
clauses.push(`e.type IN (${ph})`);
|
|
1470
1791
|
params.push(...options.eventTypes);
|
|
1471
1792
|
}
|
|
1793
|
+
if (options?.channel !== void 0) {
|
|
1794
|
+
clauses.push("e.channel = ?");
|
|
1795
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1796
|
+
}
|
|
1472
1797
|
const where = clauses.join(" AND ");
|
|
1473
1798
|
const rows = db.prepare(
|
|
1474
1799
|
`SELECT e.id
|
|
@@ -1480,7 +1805,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
1480
1805
|
).all(...params, limit, offset);
|
|
1481
1806
|
return batchLoadEventRecords(
|
|
1482
1807
|
db,
|
|
1483
|
-
rows.map((r) => r.id)
|
|
1808
|
+
rows.map((r) => r.id),
|
|
1809
|
+
options?.includeContent ?? false
|
|
1484
1810
|
);
|
|
1485
1811
|
}
|
|
1486
1812
|
var init_get_link_timeline = __esm({
|
|
@@ -1664,26 +1990,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1664
1990
|
const latest = db.prepare(
|
|
1665
1991
|
`SELECT event_id, occurred_at
|
|
1666
1992
|
FROM link_event_effects
|
|
1667
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
1993
|
+
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
1994
|
ORDER BY occurred_at DESC, event_id DESC
|
|
1669
1995
|
LIMIT 1`
|
|
1670
|
-
).get(linkId);
|
|
1996
|
+
).get(linkId, now, localCalendarDate(now));
|
|
1671
1997
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
1672
1998
|
const counts = db.prepare(
|
|
1673
1999
|
`SELECT
|
|
1674
2000
|
COUNT(*) AS total_meaningful_events,
|
|
1675
2001
|
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
2002
|
FROM link_event_effects
|
|
1677
|
-
WHERE link_id = ? AND occurred_at >=
|
|
1678
|
-
|
|
2003
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
2004
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
2005
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
1679
2006
|
const directionalityRows = db.prepare(
|
|
1680
2007
|
`SELECT ep.directionality
|
|
1681
2008
|
FROM link_event_effects le
|
|
1682
2009
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
1683
2010
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
1684
2011
|
AND ep.directionality IS NOT NULL
|
|
1685
|
-
AND le.occurred_at >=
|
|
1686
|
-
|
|
2012
|
+
AND le.occurred_at >= ?
|
|
2013
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
2014
|
+
).all(
|
|
2015
|
+
linkId,
|
|
2016
|
+
link.from_contact_id,
|
|
2017
|
+
trailingWindowStart,
|
|
2018
|
+
now,
|
|
2019
|
+
localCalendarDate(now)
|
|
2020
|
+
);
|
|
1687
2021
|
let outboundCount = 0;
|
|
1688
2022
|
let inboundCount = 0;
|
|
1689
2023
|
for (const row of directionalityRows) {
|
|
@@ -1793,6 +2127,7 @@ var DAY_MS2;
|
|
|
1793
2127
|
var init_rollups2 = __esm({
|
|
1794
2128
|
"src/links/rollups.ts"() {
|
|
1795
2129
|
init_rollups();
|
|
2130
|
+
init_parse_calendar_time();
|
|
1796
2131
|
init_mechanics();
|
|
1797
2132
|
init_queries3();
|
|
1798
2133
|
DAY_MS2 = 864e5;
|
|
@@ -1855,41 +2190,12 @@ var init_get_link_detail = __esm({
|
|
|
1855
2190
|
|
|
1856
2191
|
// src/tools/parse_time_input.ts
|
|
1857
2192
|
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();
|
|
2193
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
1887
2194
|
}
|
|
1888
|
-
var timeInputForms;
|
|
1889
2195
|
var init_parse_time_input = __esm({
|
|
1890
2196
|
"src/tools/parse_time_input.ts"() {
|
|
1891
|
-
|
|
1892
|
-
|
|
2197
|
+
init_parse_calendar_time();
|
|
2198
|
+
init_parse_calendar_time();
|
|
1893
2199
|
}
|
|
1894
2200
|
});
|
|
1895
2201
|
|
|
@@ -1966,8 +2272,47 @@ var init_tool_metadata = __esm({
|
|
|
1966
2272
|
}
|
|
1967
2273
|
});
|
|
1968
2274
|
|
|
2275
|
+
// src/tools/event_metadata_schema.ts
|
|
2276
|
+
var externalRefSchema, eventMetadataSchema;
|
|
2277
|
+
var init_event_metadata_schema = __esm({
|
|
2278
|
+
"src/tools/event_metadata_schema.ts"() {
|
|
2279
|
+
init_tool_metadata();
|
|
2280
|
+
externalRefSchema = objectSchema(
|
|
2281
|
+
{
|
|
2282
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
2283
|
+
id: stringSchema("Non-empty external id.")
|
|
2284
|
+
},
|
|
2285
|
+
["source", "id"]
|
|
2286
|
+
);
|
|
2287
|
+
eventMetadataSchema = {
|
|
2288
|
+
affectsBond: booleanSchema(
|
|
2289
|
+
"Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
|
|
2290
|
+
),
|
|
2291
|
+
content: stringSchema(
|
|
2292
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
2293
|
+
),
|
|
2294
|
+
externalRef: externalRefSchema,
|
|
2295
|
+
channel: stringSchema(
|
|
2296
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
2297
|
+
),
|
|
2298
|
+
parentRef: externalRefSchema,
|
|
2299
|
+
parentEventId: integerSchema("Parent event id."),
|
|
2300
|
+
threadRef: externalRefSchema,
|
|
2301
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
2302
|
+
"breakthrough",
|
|
2303
|
+
"rupture",
|
|
2304
|
+
"reconciliation",
|
|
2305
|
+
"milestone",
|
|
2306
|
+
"turning_point"
|
|
2307
|
+
]),
|
|
2308
|
+
occurredOn: stringSchema("Calendar day."),
|
|
2309
|
+
provenance: {}
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
|
|
1969
2314
|
// src/tools/tool_names.ts
|
|
1970
|
-
var searchAffinityToolName, reviewAffinityToolName, inspectAffinityItemToolName, manageContactToolName, mergeContactsToolName, manageDuplicateDismissalToolName, manageIdentityToolName, manageRelationshipToolName, recordEventToolName, manageCommitmentToolName, manageDateAnchorToolName, manageAttributeToolName;
|
|
2315
|
+
var searchAffinityToolName, reviewAffinityToolName, inspectAffinityItemToolName, manageContactToolName, mergeContactsToolName, manageDuplicateDismissalToolName, manageIdentityToolName, manageRelationshipToolName, recordEventToolName, manageCommitmentToolName, manageDateAnchorToolName, manageAttributeToolName, manageEventToolName;
|
|
1971
2316
|
var init_tool_names = __esm({
|
|
1972
2317
|
"src/tools/tool_names.ts"() {
|
|
1973
2318
|
searchAffinityToolName = "search_affinity";
|
|
@@ -1982,6 +2327,7 @@ var init_tool_names = __esm({
|
|
|
1982
2327
|
manageCommitmentToolName = "manage_commitment";
|
|
1983
2328
|
manageDateAnchorToolName = "manage_date_anchor";
|
|
1984
2329
|
manageAttributeToolName = "manage_attribute";
|
|
2330
|
+
manageEventToolName = "manage_event";
|
|
1985
2331
|
}
|
|
1986
2332
|
});
|
|
1987
2333
|
|
|
@@ -2028,7 +2374,7 @@ function toEventEntityRef(value) {
|
|
|
2028
2374
|
return {
|
|
2029
2375
|
kind: "event",
|
|
2030
2376
|
id: value.id,
|
|
2031
|
-
title: value.summary,
|
|
2377
|
+
title: deriveEventSummary(value.summary),
|
|
2032
2378
|
subtitle: value.type,
|
|
2033
2379
|
state: value.type
|
|
2034
2380
|
};
|
|
@@ -2063,21 +2409,48 @@ function toReceiptEntityRefs(receipt) {
|
|
|
2063
2409
|
}
|
|
2064
2410
|
var init_tool_ref = __esm({
|
|
2065
2411
|
"src/tools/tool_ref.ts"() {
|
|
2412
|
+
init_event_metadata();
|
|
2066
2413
|
}
|
|
2067
2414
|
});
|
|
2068
2415
|
|
|
2069
2416
|
// src/identities/normalize.ts
|
|
2417
|
+
function normalizeIdentityValue(type, value) {
|
|
2418
|
+
const v = value.trim();
|
|
2419
|
+
return handleIdentityTypes.has(type.toLowerCase()) ? v.replace(/^@/, "").toLowerCase() : v;
|
|
2420
|
+
}
|
|
2070
2421
|
function normalizeIdentityKey(type, value) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2422
|
+
let t = type.trim().toLowerCase();
|
|
2423
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(t)) t = "did";
|
|
2424
|
+
const v = normalizeIdentityValue(t, value).toLowerCase();
|
|
2073
2425
|
if (t.length === 0 || v.length === 0) {
|
|
2074
2426
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
2075
2427
|
}
|
|
2076
2428
|
return `${t}:${v}`;
|
|
2077
2429
|
}
|
|
2430
|
+
function legacyIdentityKey(key) {
|
|
2431
|
+
const colon = key.indexOf(":");
|
|
2432
|
+
return handleIdentityTypes.has(key.slice(0, colon)) ? `${key.slice(0, colon + 1)}@${key.slice(colon + 1)}` : key;
|
|
2433
|
+
}
|
|
2434
|
+
var handleIdentityTypes;
|
|
2078
2435
|
var init_normalize = __esm({
|
|
2079
2436
|
"src/identities/normalize.ts"() {
|
|
2080
2437
|
init_affinity_validation_error();
|
|
2438
|
+
handleIdentityTypes = /* @__PURE__ */ new Set([
|
|
2439
|
+
"handle",
|
|
2440
|
+
"bluesky",
|
|
2441
|
+
"x",
|
|
2442
|
+
"twitter",
|
|
2443
|
+
"mastodon",
|
|
2444
|
+
"github",
|
|
2445
|
+
"instagram",
|
|
2446
|
+
"telegram",
|
|
2447
|
+
"linkedin",
|
|
2448
|
+
"facebook",
|
|
2449
|
+
"youtube",
|
|
2450
|
+
"tiktok",
|
|
2451
|
+
"discord",
|
|
2452
|
+
"slack"
|
|
2453
|
+
]);
|
|
2081
2454
|
}
|
|
2082
2455
|
});
|
|
2083
2456
|
|
|
@@ -2210,7 +2583,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
2210
2583
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
2211
2584
|
return {
|
|
2212
2585
|
ok: false,
|
|
2213
|
-
result: toolFailure("domain", "
|
|
2586
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
2214
2587
|
};
|
|
2215
2588
|
}
|
|
2216
2589
|
return { ok: true, value: row2 };
|
|
@@ -2235,9 +2608,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
2235
2608
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
2236
2609
|
WHERE i.removed_at IS NULL
|
|
2237
2610
|
AND c.deleted_at IS NULL
|
|
2238
|
-
AND i.normalized_key = ?
|
|
2611
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
2239
2612
|
LIMIT 2`
|
|
2240
|
-
).all(
|
|
2613
|
+
).all(
|
|
2614
|
+
normalizedKey,
|
|
2615
|
+
legacyIdentityKey(normalizedKey),
|
|
2616
|
+
normalizedKey
|
|
2617
|
+
);
|
|
2241
2618
|
if (row.length === 0) {
|
|
2242
2619
|
return {
|
|
2243
2620
|
ok: false,
|
|
@@ -2388,13 +2765,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
2388
2765
|
return { ok: true, value: matchedLink };
|
|
2389
2766
|
}
|
|
2390
2767
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
2768
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
2769
|
+
return error;
|
|
2391
2770
|
if (isAffinityError(error)) {
|
|
2392
|
-
|
|
2771
|
+
const result = toolFailure(
|
|
2393
2772
|
"domain",
|
|
2394
2773
|
affinityErrorCodeToToolCode(error),
|
|
2395
2774
|
summary,
|
|
2396
2775
|
error.message
|
|
2397
2776
|
);
|
|
2777
|
+
const id = error.existingContactId;
|
|
2778
|
+
if (id !== void 0) {
|
|
2779
|
+
result.data = { existingContactId: id };
|
|
2780
|
+
result.error.details = { existingContactId: id };
|
|
2781
|
+
}
|
|
2782
|
+
return result;
|
|
2398
2783
|
}
|
|
2399
2784
|
if (error instanceof Error) {
|
|
2400
2785
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -2443,27 +2828,32 @@ var init_tool_resolvers = __esm({
|
|
|
2443
2828
|
});
|
|
2444
2829
|
|
|
2445
2830
|
// src/tools/validate_tool_input.ts
|
|
2446
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
2831
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
2832
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
2833
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
2834
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
2835
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
2836
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
2447
2837
|
if (schema.anyOf || schema.oneOf) {
|
|
2448
2838
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
2449
2839
|
return choices.some(
|
|
2450
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
2840
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
2451
2841
|
) ? null : `${path}: invalid value`;
|
|
2452
2842
|
}
|
|
2453
2843
|
if (schema.type === "object") {
|
|
2454
2844
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
2455
2845
|
return `${path}: missing input or expected object`;
|
|
2456
2846
|
const object = value;
|
|
2457
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
2847
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
2458
2848
|
for (const key of Object.keys(object)) {
|
|
2459
2849
|
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(", ")}`;
|
|
2850
|
+
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
2851
|
}
|
|
2462
2852
|
}
|
|
2463
2853
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
2464
2854
|
return `${path}.patch: missing required field`;
|
|
2465
2855
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
2466
|
-
return `${path}.patch: unknown
|
|
2856
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
2467
2857
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
2468
2858
|
try {
|
|
2469
2859
|
return parseTimeInput(
|
|
@@ -2478,11 +2868,21 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2478
2868
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
2479
2869
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
2480
2870
|
try {
|
|
2481
|
-
|
|
2871
|
+
const parsed = parseCalendarTime(
|
|
2482
2872
|
object[key],
|
|
2483
2873
|
`${path}.${key}`,
|
|
2484
2874
|
referenceNow
|
|
2485
2875
|
);
|
|
2876
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
|
|
2877
|
+
const provenance = object.provenance;
|
|
2878
|
+
object.provenance = {
|
|
2879
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
2880
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
object[key] = parsed.timestamp;
|
|
2884
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
2885
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
2486
2886
|
} catch (error) {
|
|
2487
2887
|
return error.message;
|
|
2488
2888
|
}
|
|
@@ -2497,8 +2897,9 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2497
2897
|
const error = validateToolInput(
|
|
2498
2898
|
child,
|
|
2499
2899
|
object[key],
|
|
2500
|
-
`${path}.${key}`,
|
|
2501
|
-
referenceNow
|
|
2900
|
+
key === "input" && path === "input" ? path : `${path}.${key}`,
|
|
2901
|
+
referenceNow,
|
|
2902
|
+
strict
|
|
2502
2903
|
);
|
|
2503
2904
|
if (error) return error;
|
|
2504
2905
|
}
|
|
@@ -2511,7 +2912,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2511
2912
|
schema.items ?? {},
|
|
2512
2913
|
item,
|
|
2513
2914
|
`${path}[${index}]`,
|
|
2514
|
-
injectedNow
|
|
2915
|
+
injectedNow,
|
|
2916
|
+
strict
|
|
2515
2917
|
);
|
|
2516
2918
|
if (error) return error;
|
|
2517
2919
|
}
|
|
@@ -2529,6 +2931,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
2529
2931
|
}
|
|
2530
2932
|
var init_validate_tool_input = __esm({
|
|
2531
2933
|
"src/tools/validate_tool_input.ts"() {
|
|
2934
|
+
init_parse_calendar_time();
|
|
2532
2935
|
init_parse_time_input();
|
|
2533
2936
|
}
|
|
2534
2937
|
});
|
|
@@ -2582,6 +2985,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
2582
2985
|
inputError
|
|
2583
2986
|
);
|
|
2584
2987
|
return withToolHandling(() => {
|
|
2988
|
+
if (input.kind === "event") {
|
|
2989
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
2990
|
+
return toolFailure(
|
|
2991
|
+
"protocol",
|
|
2992
|
+
"invalid_input",
|
|
2993
|
+
"Invalid event target.",
|
|
2994
|
+
"Provide exactly one of eventId or ref."
|
|
2995
|
+
);
|
|
2996
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
2997
|
+
return detail2 ? toolSuccess(
|
|
2998
|
+
`Loaded event #${detail2.id}.`,
|
|
2999
|
+
{ kind: "event", detail: detail2 },
|
|
3000
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
3001
|
+
) : toolFailure(
|
|
3002
|
+
"domain",
|
|
3003
|
+
"not_found",
|
|
3004
|
+
"Event not found.",
|
|
3005
|
+
"No event matches the target."
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
2585
3008
|
if (input.kind === "owner_profile") {
|
|
2586
3009
|
const detail2 = getOwnerProfile(
|
|
2587
3010
|
db,
|
|
@@ -2664,7 +3087,9 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2664
3087
|
"src/tools/inspect_affinity_item_tool.ts"() {
|
|
2665
3088
|
init_get_contact_profile();
|
|
2666
3089
|
init_get_owner_profile();
|
|
3090
|
+
init_event_metadata();
|
|
2667
3091
|
init_get_link_detail();
|
|
3092
|
+
init_event_metadata_schema();
|
|
2668
3093
|
init_tool_metadata();
|
|
2669
3094
|
init_tool_names();
|
|
2670
3095
|
init_tool_ref();
|
|
@@ -2692,10 +3117,13 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2692
3117
|
inputSchema: objectSchema(
|
|
2693
3118
|
{
|
|
2694
3119
|
kind: enumSchema("What to inspect.", [
|
|
3120
|
+
"event",
|
|
2695
3121
|
"owner_profile",
|
|
2696
3122
|
"contact_profile",
|
|
2697
3123
|
"link"
|
|
2698
3124
|
]),
|
|
3125
|
+
eventId: integerSchema("Exact event id."),
|
|
3126
|
+
ref: externalRefSchema,
|
|
2699
3127
|
contact: contactLocatorSchema(
|
|
2700
3128
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
2701
3129
|
),
|
|
@@ -2716,6 +3144,7 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2716
3144
|
// src/attributes/helpers.ts
|
|
2717
3145
|
function clearedAttributePrimary(target) {
|
|
2718
3146
|
return {
|
|
3147
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
2719
3148
|
id: 0,
|
|
2720
3149
|
contactId: target.kind === "contact" ? target.id : null,
|
|
2721
3150
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -2731,6 +3160,7 @@ var init_helpers = __esm({
|
|
|
2731
3160
|
// src/attributes/mappers.ts
|
|
2732
3161
|
function mapAttributeRowToAttributeRecord(row) {
|
|
2733
3162
|
return {
|
|
3163
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
2734
3164
|
id: row.id,
|
|
2735
3165
|
contactId: row.contact_id,
|
|
2736
3166
|
linkId: row.link_id,
|
|
@@ -2744,131 +3174,46 @@ var init_mappers5 = __esm({
|
|
|
2744
3174
|
});
|
|
2745
3175
|
|
|
2746
3176
|
// src/attributes/queries.ts
|
|
3177
|
+
function attributeTargetSql(target) {
|
|
3178
|
+
return {
|
|
3179
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
3180
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
2747
3183
|
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;
|
|
3184
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
2756
3185
|
}
|
|
2757
3186
|
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;
|
|
3187
|
+
const t = attributeTargetSql(target);
|
|
3188
|
+
return db.prepare(
|
|
3189
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
3190
|
+
).get(t.id, name) ?? null;
|
|
2778
3191
|
}
|
|
2779
3192
|
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);
|
|
3193
|
+
const t = attributeTargetSql(target);
|
|
3194
|
+
return db.prepare(
|
|
3195
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
3196
|
+
).all(t.id).map((r) => r.id);
|
|
2790
3197
|
}
|
|
2791
3198
|
var init_queries4 = __esm({
|
|
2792
3199
|
"src/attributes/queries.ts"() {
|
|
2793
3200
|
}
|
|
2794
3201
|
});
|
|
2795
3202
|
|
|
2796
|
-
// src/
|
|
2797
|
-
function
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
}
|
|
2808
|
-
return out;
|
|
3203
|
+
// src/attributes/receipts.ts
|
|
3204
|
+
function buildAttributeMutationReceipt(primary, partial) {
|
|
3205
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
3206
|
+
return {
|
|
3207
|
+
primary,
|
|
3208
|
+
created: normalized.created,
|
|
3209
|
+
updated: normalized.updated,
|
|
3210
|
+
archived: normalized.archived,
|
|
3211
|
+
removed: normalized.removed,
|
|
3212
|
+
affectedLinks: normalized.affectedLinks,
|
|
3213
|
+
derivedEffects: normalized.derivedEffects
|
|
3214
|
+
};
|
|
2809
3215
|
}
|
|
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({
|
|
3216
|
+
var init_receipts2 = __esm({
|
|
2872
3217
|
"src/attributes/receipts.ts"() {
|
|
2873
3218
|
init_normalize_mutation_receipt();
|
|
2874
3219
|
}
|
|
@@ -2931,11 +3276,14 @@ function normalizeAttributeValue(value) {
|
|
|
2931
3276
|
if (value === null) {
|
|
2932
3277
|
return null;
|
|
2933
3278
|
}
|
|
3279
|
+
if (typeof value !== "string")
|
|
3280
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
2934
3281
|
const trimmed = value.trim();
|
|
2935
3282
|
return trimmed.length === 0 ? null : trimmed;
|
|
2936
3283
|
}
|
|
2937
3284
|
var init_normalize2 = __esm({
|
|
2938
3285
|
"src/attributes/normalize.ts"() {
|
|
3286
|
+
init_affinity_validation_error();
|
|
2939
3287
|
}
|
|
2940
3288
|
});
|
|
2941
3289
|
|
|
@@ -2963,6 +3311,11 @@ function validateAttributeEntries(entries) {
|
|
|
2963
3311
|
return out;
|
|
2964
3312
|
}
|
|
2965
3313
|
function assertAttributeTargetWritable(db, target) {
|
|
3314
|
+
if (target.kind === "event") {
|
|
3315
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
3316
|
+
throw new AffinityNotFoundError("event not found");
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
2966
3319
|
if (target.kind === "contact") {
|
|
2967
3320
|
const contact = getContactRowById(db, target.id);
|
|
2968
3321
|
if (!contact) {
|
|
@@ -3009,26 +3362,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
3009
3362
|
}
|
|
3010
3363
|
const created = [];
|
|
3011
3364
|
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
|
-
}
|
|
3365
|
+
const t = attributeTargetSql(target);
|
|
3366
|
+
const insert = db.prepare(
|
|
3367
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
3368
|
+
);
|
|
3369
|
+
for (const e of normalized) {
|
|
3370
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
3371
|
+
lastId = Number(r.lastInsertRowid);
|
|
3372
|
+
created.push({ kind: "attribute", id: lastId });
|
|
3032
3373
|
}
|
|
3033
3374
|
if (normalized.length === 0) {
|
|
3034
3375
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -3054,11 +3395,12 @@ var init_replace_attributes = __esm({
|
|
|
3054
3395
|
init_helpers();
|
|
3055
3396
|
init_mappers5();
|
|
3056
3397
|
init_queries4();
|
|
3057
|
-
|
|
3398
|
+
init_receipts2();
|
|
3058
3399
|
init_validators3();
|
|
3059
3400
|
init_affinity_invariant_error();
|
|
3060
3401
|
init_resolve_now();
|
|
3061
3402
|
init_with_transaction();
|
|
3403
|
+
init_queries4();
|
|
3062
3404
|
}
|
|
3063
3405
|
});
|
|
3064
3406
|
|
|
@@ -3070,6 +3412,11 @@ function setAttribute(db, target, name, value) {
|
|
|
3070
3412
|
const stored = normalizeAttributeValue(value);
|
|
3071
3413
|
const now = resolveNow();
|
|
3072
3414
|
const existing = getLiveAttributeRow(db, target, key);
|
|
3415
|
+
if (existing?.value === stored)
|
|
3416
|
+
return buildAttributeMutationReceipt(
|
|
3417
|
+
mapAttributeRowToAttributeRecord(existing),
|
|
3418
|
+
{}
|
|
3419
|
+
);
|
|
3073
3420
|
if (existing) {
|
|
3074
3421
|
db.prepare(
|
|
3075
3422
|
"UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
|
|
@@ -3085,27 +3432,10 @@ function setAttribute(db, target, name, value) {
|
|
|
3085
3432
|
}
|
|
3086
3433
|
);
|
|
3087
3434
|
}
|
|
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
|
-
}
|
|
3435
|
+
const t = attributeTargetSql(target);
|
|
3105
3436
|
const r = db.prepare(
|
|
3106
|
-
`INSERT INTO attributes (
|
|
3107
|
-
|
|
3108
|
-
).run(target.id, key, stored, now, now);
|
|
3437
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
3438
|
+
).run(t.id, key, stored, now, now);
|
|
3109
3439
|
const id = Number(r.lastInsertRowid);
|
|
3110
3440
|
const row = getAttributeRowById(db, id);
|
|
3111
3441
|
if (!row) {
|
|
@@ -3124,11 +3454,12 @@ var init_set_attribute = __esm({
|
|
|
3124
3454
|
init_mappers5();
|
|
3125
3455
|
init_normalize2();
|
|
3126
3456
|
init_queries4();
|
|
3127
|
-
|
|
3457
|
+
init_receipts2();
|
|
3128
3458
|
init_validators3();
|
|
3129
3459
|
init_affinity_invariant_error();
|
|
3130
3460
|
init_resolve_now();
|
|
3131
3461
|
init_with_transaction();
|
|
3462
|
+
init_queries4();
|
|
3132
3463
|
}
|
|
3133
3464
|
});
|
|
3134
3465
|
|
|
@@ -3155,7 +3486,7 @@ var init_unset_attribute = __esm({
|
|
|
3155
3486
|
"src/attributes/unset_attribute.ts"() {
|
|
3156
3487
|
init_mappers5();
|
|
3157
3488
|
init_queries4();
|
|
3158
|
-
|
|
3489
|
+
init_receipts2();
|
|
3159
3490
|
init_validators3();
|
|
3160
3491
|
init_affinity_not_found_error();
|
|
3161
3492
|
init_resolve_now();
|
|
@@ -3184,14 +3515,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
3184
3515
|
affectedLinks: receipt.affectedLinks,
|
|
3185
3516
|
derivedEffects: receipt.derivedEffects
|
|
3186
3517
|
};
|
|
3187
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
3518
|
+
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
3519
|
if (changedCount === 0) {
|
|
3189
3520
|
return toolNoOp(summary, data, {
|
|
3190
3521
|
entities: toReceiptEntityRefs(receipt)
|
|
3191
3522
|
});
|
|
3192
3523
|
}
|
|
3193
3524
|
return toolSuccess(summary, data, {
|
|
3194
|
-
entities: toReceiptEntityRefs(receipt)
|
|
3525
|
+
entities: toReceiptEntityRefs(receipt),
|
|
3526
|
+
warnings: receipt.warnings
|
|
3195
3527
|
});
|
|
3196
3528
|
}
|
|
3197
3529
|
function describeChanges(receipt) {
|
|
@@ -3255,6 +3587,30 @@ function linkLocatorSchema2(description) {
|
|
|
3255
3587
|
};
|
|
3256
3588
|
}
|
|
3257
3589
|
function resolveAttributeTarget(db, target) {
|
|
3590
|
+
if (target.kind === "event") {
|
|
3591
|
+
if (target.eventId === void 0 === (target.ref === void 0))
|
|
3592
|
+
return {
|
|
3593
|
+
kind: "error",
|
|
3594
|
+
result: toolFailure(
|
|
3595
|
+
"protocol",
|
|
3596
|
+
"invalid_input",
|
|
3597
|
+
"Invalid event target.",
|
|
3598
|
+
"Provide exactly one of target.eventId or target.ref."
|
|
3599
|
+
)
|
|
3600
|
+
};
|
|
3601
|
+
const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
|
|
3602
|
+
if (eventId === void 0)
|
|
3603
|
+
return {
|
|
3604
|
+
kind: "error",
|
|
3605
|
+
result: toolFailure(
|
|
3606
|
+
"domain",
|
|
3607
|
+
"not_found",
|
|
3608
|
+
"Event not found.",
|
|
3609
|
+
"No event matches target.ref."
|
|
3610
|
+
)
|
|
3611
|
+
};
|
|
3612
|
+
return { kind: "ok", target: { kind: "event", eventId } };
|
|
3613
|
+
}
|
|
3258
3614
|
if (target.kind === "contact") {
|
|
3259
3615
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
3260
3616
|
if (!contact.ok) {
|
|
@@ -3318,6 +3674,8 @@ var init_manage_attribute_tool = __esm({
|
|
|
3318
3674
|
init_replace_attributes();
|
|
3319
3675
|
init_set_attribute();
|
|
3320
3676
|
init_unset_attribute();
|
|
3677
|
+
init_event_metadata();
|
|
3678
|
+
init_event_metadata_schema();
|
|
3321
3679
|
init_tool_metadata();
|
|
3322
3680
|
init_tool_mutation();
|
|
3323
3681
|
init_tool_names();
|
|
@@ -3326,7 +3684,7 @@ var init_manage_attribute_tool = __esm({
|
|
|
3326
3684
|
init_validate_tool_input();
|
|
3327
3685
|
manageAttributeTool = defineAffinityTool({
|
|
3328
3686
|
name: manageAttributeToolName,
|
|
3329
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
3687
|
+
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
3688
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
3331
3689
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
3332
3690
|
sideEffects: "writes_state",
|
|
@@ -3347,7 +3705,9 @@ var init_manage_attribute_tool = __esm({
|
|
|
3347
3705
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
3348
3706
|
target: objectSchema(
|
|
3349
3707
|
{
|
|
3350
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
3708
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
3709
|
+
eventId: integerSchema("Target event id; provide eventId or ref."),
|
|
3710
|
+
ref: externalRefSchema,
|
|
3351
3711
|
contact: contactLocatorSchema2(
|
|
3352
3712
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
3353
3713
|
),
|
|
@@ -3377,7 +3737,7 @@ var init_manage_attribute_tool = __esm({
|
|
|
3377
3737
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
3378
3738
|
},
|
|
3379
3739
|
["action"],
|
|
3380
|
-
"Set, unset, or replace attributes on contacts and
|
|
3740
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
3381
3741
|
),
|
|
3382
3742
|
handler: manageAttributeToolHandler
|
|
3383
3743
|
});
|
|
@@ -3387,7 +3747,7 @@ var init_manage_attribute_tool = __esm({
|
|
|
3387
3747
|
// src/events/persistence.ts
|
|
3388
3748
|
function insertJournalEvent(db, params) {
|
|
3389
3749
|
const now = resolveNow(params.now);
|
|
3390
|
-
const summary = params.summary
|
|
3750
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
3391
3751
|
const result = db.prepare(
|
|
3392
3752
|
`INSERT INTO events (
|
|
3393
3753
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -3404,6 +3764,35 @@ function insertJournalEvent(db, params) {
|
|
|
3404
3764
|
now
|
|
3405
3765
|
);
|
|
3406
3766
|
const eventId = Number(result.lastInsertRowid);
|
|
3767
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
3768
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 || ["activity", "milestone", "observation"].includes(params.type) && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
3769
|
+
summary
|
|
3770
|
+
) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
|
|
3771
|
+
eventId
|
|
3772
|
+
);
|
|
3773
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
3774
|
+
params.momentKind,
|
|
3775
|
+
eventId
|
|
3776
|
+
);
|
|
3777
|
+
db.prepare(
|
|
3778
|
+
"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=?"
|
|
3779
|
+
).run(
|
|
3780
|
+
params.content ?? null,
|
|
3781
|
+
params.externalRef?.source ?? null,
|
|
3782
|
+
params.externalRef?.id ?? null,
|
|
3783
|
+
params.channel ?? null,
|
|
3784
|
+
params.parentRef?.source ?? null,
|
|
3785
|
+
params.parentRef?.id ?? null,
|
|
3786
|
+
params.parentEventId ?? null,
|
|
3787
|
+
params.threadRef?.source ?? null,
|
|
3788
|
+
params.threadRef?.id ?? null,
|
|
3789
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
3790
|
+
eventId
|
|
3791
|
+
);
|
|
3792
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3793
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3794
|
+
eventId
|
|
3795
|
+
);
|
|
3407
3796
|
const insertParticipant = db.prepare(
|
|
3408
3797
|
`INSERT INTO event_participants (
|
|
3409
3798
|
event_id, contact_id, role, directionality
|
|
@@ -3446,6 +3835,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
3446
3835
|
now
|
|
3447
3836
|
);
|
|
3448
3837
|
const eventId = Number(result.lastInsertRowid);
|
|
3838
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
3839
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
3840
|
+
eventId
|
|
3841
|
+
);
|
|
3449
3842
|
const insertParticipant = db.prepare(
|
|
3450
3843
|
`INSERT INTO event_participants (
|
|
3451
3844
|
event_id, contact_id, role, directionality
|
|
@@ -3469,29 +3862,11 @@ var init_persistence = __esm({
|
|
|
3469
3862
|
"src/events/persistence.ts"() {
|
|
3470
3863
|
init_affinity_invariant_error();
|
|
3471
3864
|
init_resolve_now();
|
|
3865
|
+
init_event_metadata();
|
|
3472
3866
|
init_queries2();
|
|
3473
3867
|
}
|
|
3474
3868
|
});
|
|
3475
3869
|
|
|
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
3870
|
// src/graph/betweenness.ts
|
|
3496
3871
|
function computeNodeBetweenness(adjacency) {
|
|
3497
3872
|
const nodes = [...adjacency.keys()];
|
|
@@ -4104,6 +4479,8 @@ function deriveStateAfter(params) {
|
|
|
4104
4479
|
return params.stateBefore;
|
|
4105
4480
|
}
|
|
4106
4481
|
function deriveMomentKind(params) {
|
|
4482
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
4483
|
+
return null;
|
|
4107
4484
|
if (params.rankAfter > params.rankBefore) {
|
|
4108
4485
|
return "breakthrough";
|
|
4109
4486
|
}
|
|
@@ -4133,6 +4510,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
4133
4510
|
if (!link || link.is_structural !== 0) {
|
|
4134
4511
|
throw new AffinityStateError("relational link not found");
|
|
4135
4512
|
}
|
|
4513
|
+
db.prepare(
|
|
4514
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
4515
|
+
).run(
|
|
4516
|
+
params.linkId,
|
|
4517
|
+
JSON.stringify({
|
|
4518
|
+
rank: link.rank ?? 0,
|
|
4519
|
+
affinity: link.affinity ?? 0,
|
|
4520
|
+
trust: link.trust ?? 0,
|
|
4521
|
+
state: link.state ?? "active",
|
|
4522
|
+
cadence_days: link.cadence_days
|
|
4523
|
+
})
|
|
4524
|
+
);
|
|
4136
4525
|
const stateBefore = link.state ?? "active";
|
|
4137
4526
|
const linkKind = link.kind;
|
|
4138
4527
|
const rankBefore = link.rank ?? 0;
|
|
@@ -4177,7 +4566,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
4177
4566
|
);
|
|
4178
4567
|
const weightedBase = base * massPenalty(
|
|
4179
4568
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
4180
|
-
) * salienceMultiplier;
|
|
4569
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
4181
4570
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
4182
4571
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
4183
4572
|
const affinityMass = clamp3(
|
|
@@ -4218,15 +4607,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
4218
4607
|
const latest = db.prepare(
|
|
4219
4608
|
`SELECT occurred_at
|
|
4220
4609
|
FROM link_event_effects
|
|
4221
|
-
WHERE link_id = ?
|
|
4610
|
+
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
4611
|
ORDER BY occurred_at DESC, event_id DESC
|
|
4223
4612
|
LIMIT 1`
|
|
4224
|
-
).get(params.linkId);
|
|
4613
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
4225
4614
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
4226
4615
|
1,
|
|
4227
4616
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
4228
4617
|
);
|
|
4229
|
-
const
|
|
4618
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
4619
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
4230
4620
|
kind: linkKind,
|
|
4231
4621
|
currentCadenceDays: cadenceBefore,
|
|
4232
4622
|
gapDays
|
|
@@ -4246,7 +4636,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
4246
4636
|
}
|
|
4247
4637
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
4248
4638
|
const trustDelta = trustAfter - trustBefore;
|
|
4249
|
-
const
|
|
4639
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
4640
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
4641
|
+
eventLabel
|
|
4642
|
+
);
|
|
4643
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
4250
4644
|
rankBefore,
|
|
4251
4645
|
rankAfter,
|
|
4252
4646
|
stateBefore,
|
|
@@ -4289,7 +4683,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
4289
4683
|
params.now,
|
|
4290
4684
|
params.now
|
|
4291
4685
|
);
|
|
4292
|
-
|
|
4686
|
+
db.prepare(
|
|
4687
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
4688
|
+
).run(
|
|
4689
|
+
JSON.stringify(params.participant),
|
|
4690
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
4691
|
+
params.eventId,
|
|
4692
|
+
params.linkId
|
|
4693
|
+
);
|
|
4694
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
4293
4695
|
db.prepare(
|
|
4294
4696
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4295
4697
|
).run(params.eventId, params.linkId);
|
|
@@ -4323,6 +4725,7 @@ var init_effects = __esm({
|
|
|
4323
4725
|
init_calendar();
|
|
4324
4726
|
init_mechanics2();
|
|
4325
4727
|
init_affinity_state_error();
|
|
4728
|
+
init_parse_calendar_time();
|
|
4326
4729
|
init_mechanics();
|
|
4327
4730
|
init_queries3();
|
|
4328
4731
|
init_rollups2();
|
|
@@ -4337,6 +4740,7 @@ var init_affinity_conflict_error = __esm({
|
|
|
4337
4740
|
"src/lib/errors/affinity_conflict_error.ts"() {
|
|
4338
4741
|
init_affinity_error();
|
|
4339
4742
|
AffinityConflictError = class extends AffinityError {
|
|
4743
|
+
existingContactId;
|
|
4340
4744
|
code = "CONFLICT";
|
|
4341
4745
|
};
|
|
4342
4746
|
}
|
|
@@ -4403,6 +4807,8 @@ var init_receipts3 = __esm({
|
|
|
4403
4807
|
// src/links/seed_social_link.ts
|
|
4404
4808
|
function seedSocialLink(db, input) {
|
|
4405
4809
|
return withTransaction(db, () => {
|
|
4810
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
4811
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
4406
4812
|
if (!isRelationalLinkKind(input.kind)) {
|
|
4407
4813
|
throw new AffinityValidationError("invalid relational link kind");
|
|
4408
4814
|
}
|
|
@@ -4410,11 +4816,14 @@ function seedSocialLink(db, input) {
|
|
|
4410
4816
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
4411
4817
|
}
|
|
4412
4818
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
4413
|
-
|
|
4819
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
4414
4820
|
db,
|
|
4415
4821
|
input.fromContactId,
|
|
4416
4822
|
input.toContactId
|
|
4417
|
-
)
|
|
4823
|
+
);
|
|
4824
|
+
if (existing) {
|
|
4825
|
+
if (input.ifExists === "return")
|
|
4826
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
4418
4827
|
throw new AffinityConflictError(
|
|
4419
4828
|
"relational link already exists between these contacts"
|
|
4420
4829
|
);
|
|
@@ -4534,15 +4943,36 @@ function ensureRelationalLink(db, params) {
|
|
|
4534
4943
|
};
|
|
4535
4944
|
}
|
|
4536
4945
|
function applyJournalEventMechanics(db, params) {
|
|
4946
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
4947
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
4948
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
4949
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
4537
4950
|
const ownerId = requireOwnerContactId(db);
|
|
4538
|
-
const
|
|
4951
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
4952
|
+
const socialChannel = [
|
|
4953
|
+
"bluesky",
|
|
4954
|
+
"x",
|
|
4955
|
+
"twitter",
|
|
4956
|
+
"mastodon",
|
|
4957
|
+
"github",
|
|
4958
|
+
"instagram"
|
|
4959
|
+
].includes(String(bond?.channel));
|
|
4960
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
4961
|
+
const participants = params.participants.map(
|
|
4962
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
4963
|
+
);
|
|
4964
|
+
if (direction)
|
|
4965
|
+
db.prepare(
|
|
4966
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
4967
|
+
).run(direction, params.eventId);
|
|
4968
|
+
const participantIds = participants.map(
|
|
4539
4969
|
(participant) => participant.contactId
|
|
4540
4970
|
);
|
|
4541
4971
|
const hasOwner = participantIds.includes(ownerId);
|
|
4542
4972
|
const created = [];
|
|
4543
4973
|
const effectInputs = [];
|
|
4544
4974
|
if (hasOwner) {
|
|
4545
|
-
for (const participant of
|
|
4975
|
+
for (const participant of participants) {
|
|
4546
4976
|
if (participant.contactId === ownerId) {
|
|
4547
4977
|
continue;
|
|
4548
4978
|
}
|
|
@@ -4550,13 +4980,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4550
4980
|
fromContactId: ownerId,
|
|
4551
4981
|
toContactId: participant.contactId,
|
|
4552
4982
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
4983
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
4553
4984
|
now: params.now
|
|
4554
4985
|
});
|
|
4555
4986
|
created.push(...ensured.created);
|
|
4556
4987
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
4557
4988
|
}
|
|
4558
|
-
} else if (params.eventType === "observation" &&
|
|
4559
|
-
const [first, second] =
|
|
4989
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
4990
|
+
const [first, second] = participants;
|
|
4560
4991
|
if (first !== void 0 && second !== void 0) {
|
|
4561
4992
|
const a = Math.min(first.contactId, second.contactId);
|
|
4562
4993
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -4591,7 +5022,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4591
5022
|
eventType: params.eventType,
|
|
4592
5023
|
occurredAt: params.occurredAt,
|
|
4593
5024
|
significance: params.significance,
|
|
4594
|
-
participant:
|
|
5025
|
+
participant: {
|
|
5026
|
+
...input.participant,
|
|
5027
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
5028
|
+
},
|
|
4595
5029
|
provenance: params.provenance,
|
|
4596
5030
|
now: params.now
|
|
4597
5031
|
})
|
|
@@ -4624,6 +5058,8 @@ var init_apply_journal_event_mechanics = __esm({
|
|
|
4624
5058
|
function recordCommitment(db, input) {
|
|
4625
5059
|
return withTransaction(db, () => {
|
|
4626
5060
|
validateSocialEventInput(input);
|
|
5061
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5062
|
+
if (existing) return existing;
|
|
4627
5063
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
4628
5064
|
throw new AffinityValidationError(
|
|
4629
5065
|
"commitmentType must be promise or agreement"
|
|
@@ -4638,11 +5074,12 @@ function recordCommitment(db, input) {
|
|
|
4638
5074
|
assertOwnerParticipates(ownerId, ids);
|
|
4639
5075
|
const now = resolveNow(input.now);
|
|
4640
5076
|
const eventId = insertJournalEvent(db, {
|
|
5077
|
+
...input,
|
|
4641
5078
|
type: input.commitmentType,
|
|
4642
5079
|
occurredAt: input.occurredAt,
|
|
4643
|
-
|
|
5080
|
+
occurredOn: input.occurredOn,
|
|
4644
5081
|
significance: input.significance,
|
|
4645
|
-
momentKind: null,
|
|
5082
|
+
momentKind: input.momentKind ?? null,
|
|
4646
5083
|
participants: input.participants,
|
|
4647
5084
|
now
|
|
4648
5085
|
});
|
|
@@ -4651,15 +5088,25 @@ function recordCommitment(db, input) {
|
|
|
4651
5088
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
4652
5089
|
) VALUES (?, ?, ?, ?, ?)`
|
|
4653
5090
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
5091
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
5092
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
5093
|
+
eventId
|
|
5094
|
+
);
|
|
4654
5095
|
const mechanics = applyJournalEventMechanics(db, {
|
|
4655
5096
|
eventId,
|
|
4656
5097
|
eventType: input.commitmentType,
|
|
4657
5098
|
occurredAt: input.occurredAt,
|
|
5099
|
+
occurredOn: input.occurredOn,
|
|
4658
5100
|
significance: input.significance,
|
|
4659
5101
|
participants: input.participants,
|
|
4660
5102
|
provenance: input.provenance,
|
|
4661
5103
|
now
|
|
4662
5104
|
});
|
|
5105
|
+
if (input.momentKind !== void 0)
|
|
5106
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5107
|
+
input.momentKind,
|
|
5108
|
+
eventId
|
|
5109
|
+
);
|
|
4663
5110
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
4664
5111
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4665
5112
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -4672,12 +5119,13 @@ var init_record_commitment = __esm({
|
|
|
4672
5119
|
init_queries();
|
|
4673
5120
|
init_loaders();
|
|
4674
5121
|
init_persistence();
|
|
4675
|
-
|
|
5122
|
+
init_receipts();
|
|
4676
5123
|
init_validators();
|
|
4677
5124
|
init_affinity_validation_error();
|
|
4678
5125
|
init_resolve_now();
|
|
4679
5126
|
init_with_transaction();
|
|
4680
5127
|
init_apply_journal_event_mechanics();
|
|
5128
|
+
init_event_metadata();
|
|
4681
5129
|
}
|
|
4682
5130
|
});
|
|
4683
5131
|
|
|
@@ -4746,6 +5194,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4746
5194
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
4747
5195
|
validateSocialEventInput({
|
|
4748
5196
|
occurredAt,
|
|
5197
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4749
5198
|
summary,
|
|
4750
5199
|
significance,
|
|
4751
5200
|
participants,
|
|
@@ -4762,6 +5211,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4762
5211
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4763
5212
|
type: eventType,
|
|
4764
5213
|
occurredAt,
|
|
5214
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4765
5215
|
summary,
|
|
4766
5216
|
significance,
|
|
4767
5217
|
momentKind: null,
|
|
@@ -4800,7 +5250,7 @@ var init_resolve_commitment = __esm({
|
|
|
4800
5250
|
init_loaders();
|
|
4801
5251
|
init_persistence();
|
|
4802
5252
|
init_queries2();
|
|
4803
|
-
|
|
5253
|
+
init_receipts();
|
|
4804
5254
|
init_validators();
|
|
4805
5255
|
init_affinity_conflict_error();
|
|
4806
5256
|
init_affinity_invariant_error();
|
|
@@ -4839,7 +5289,13 @@ function prepareParticipants(db, raw) {
|
|
|
4839
5289
|
}
|
|
4840
5290
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
4841
5291
|
const input = structuredClone(rawInput);
|
|
4842
|
-
const inputError = validateToolInput(
|
|
5292
|
+
const inputError = validateToolInput(
|
|
5293
|
+
manageCommitmentTool.inputSchema,
|
|
5294
|
+
input,
|
|
5295
|
+
"input",
|
|
5296
|
+
Date.now(),
|
|
5297
|
+
true
|
|
5298
|
+
);
|
|
4843
5299
|
if (inputError)
|
|
4844
5300
|
return toolFailure(
|
|
4845
5301
|
"protocol",
|
|
@@ -4869,6 +5325,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
4869
5325
|
"commitment"
|
|
4870
5326
|
);
|
|
4871
5327
|
}
|
|
5328
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
5329
|
+
return toolFailure(
|
|
5330
|
+
"protocol",
|
|
5331
|
+
"invalid_input",
|
|
5332
|
+
"Missing commitment.",
|
|
5333
|
+
"commitmentEventId is required for resolve."
|
|
5334
|
+
);
|
|
5335
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
5336
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
5337
|
+
return toolFailure(
|
|
5338
|
+
"domain",
|
|
5339
|
+
"invalid_input",
|
|
5340
|
+
"Invalid commitment.",
|
|
5341
|
+
"not a commitment event"
|
|
5342
|
+
);
|
|
4872
5343
|
return mutationToolResult(
|
|
4873
5344
|
"resolve",
|
|
4874
5345
|
resolveCommitment(
|
|
@@ -4887,6 +5358,7 @@ var init_manage_commitment_tool = __esm({
|
|
|
4887
5358
|
init_queries();
|
|
4888
5359
|
init_record_commitment();
|
|
4889
5360
|
init_resolve_commitment();
|
|
5361
|
+
init_event_metadata_schema();
|
|
4890
5362
|
init_tool_metadata();
|
|
4891
5363
|
init_tool_mutation();
|
|
4892
5364
|
init_tool_names();
|
|
@@ -4919,15 +5391,21 @@ var init_manage_commitment_tool = __esm({
|
|
|
4919
5391
|
"promise",
|
|
4920
5392
|
"agreement"
|
|
4921
5393
|
]),
|
|
5394
|
+
...eventMetadataSchema,
|
|
4922
5395
|
occurredAt: integerSchema(
|
|
4923
5396
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
4924
5397
|
),
|
|
4925
5398
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
4926
5399
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
4927
5400
|
dueAt: integerSchema("Optional due timestamp."),
|
|
5401
|
+
dueOn: stringSchema("Due calendar day."),
|
|
4928
5402
|
participants: arraySchema(
|
|
4929
5403
|
objectSchema(
|
|
4930
5404
|
{
|
|
5405
|
+
contact: objectSchema(
|
|
5406
|
+
{ contactId: integerSchema("Contact id.") },
|
|
5407
|
+
["contactId"]
|
|
5408
|
+
),
|
|
4931
5409
|
contactId: integerSchema(
|
|
4932
5410
|
"Participant contact id. The owner contact MUST be included."
|
|
4933
5411
|
),
|
|
@@ -4940,13 +5418,7 @@ var init_manage_commitment_tool = __esm({
|
|
|
4940
5418
|
),
|
|
4941
5419
|
now: integerSchema("Optional timestamp override.")
|
|
4942
5420
|
},
|
|
4943
|
-
[
|
|
4944
|
-
"commitmentType",
|
|
4945
|
-
"occurredAt",
|
|
4946
|
-
"summary",
|
|
4947
|
-
"significance",
|
|
4948
|
-
"participants"
|
|
4949
|
-
],
|
|
5421
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
4950
5422
|
"Commitment payload. Required when action=record."
|
|
4951
5423
|
),
|
|
4952
5424
|
commitmentEventId: integerSchema(
|
|
@@ -4961,6 +5433,19 @@ var init_manage_commitment_tool = __esm({
|
|
|
4961
5433
|
now: integerSchema("Optional timestamp."),
|
|
4962
5434
|
resolvingEvent: objectSchema(
|
|
4963
5435
|
{
|
|
5436
|
+
occurredOn: stringSchema("Calendar day."),
|
|
5437
|
+
provenance: {},
|
|
5438
|
+
participants: arraySchema(
|
|
5439
|
+
objectSchema(
|
|
5440
|
+
{
|
|
5441
|
+
contactId: integerSchema("Contact id."),
|
|
5442
|
+
role: stringSchema("Role."),
|
|
5443
|
+
directionality: stringSchema("Directionality.")
|
|
5444
|
+
},
|
|
5445
|
+
["contactId", "role"]
|
|
5446
|
+
),
|
|
5447
|
+
"Resolving participants."
|
|
5448
|
+
),
|
|
4964
5449
|
type: stringSchema("Optional resolving event type."),
|
|
4965
5450
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
4966
5451
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -5054,30 +5539,50 @@ var init_delete_contact = __esm({
|
|
|
5054
5539
|
}
|
|
5055
5540
|
});
|
|
5056
5541
|
|
|
5057
|
-
// src/contacts/
|
|
5058
|
-
function
|
|
5059
|
-
|
|
5542
|
+
// src/contacts/export_rows.ts
|
|
5543
|
+
function exportRows(db, sql, ...params) {
|
|
5544
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
5060
5545
|
const result = {};
|
|
5061
5546
|
for (const [key, value] of Object.entries(row)) {
|
|
5062
5547
|
const name = key.replace(
|
|
5063
5548
|
/_([a-z])/g,
|
|
5064
5549
|
(_, letter) => letter.toUpperCase()
|
|
5065
5550
|
);
|
|
5066
|
-
result[name] =
|
|
5067
|
-
|
|
5551
|
+
result[name] = [
|
|
5552
|
+
"affects_bond",
|
|
5553
|
+
"is_owner",
|
|
5554
|
+
"is_structural",
|
|
5555
|
+
"verified",
|
|
5556
|
+
"counts_for_recency",
|
|
5557
|
+
"manual"
|
|
5558
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
5559
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
5068
5560
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
5069
5561
|
if (key.endsWith("_json") && typeof value === "string")
|
|
5070
5562
|
result[name] = JSON.parse(value);
|
|
5071
5563
|
}
|
|
5072
5564
|
return result;
|
|
5073
5565
|
});
|
|
5566
|
+
}
|
|
5567
|
+
var init_export_rows = __esm({
|
|
5568
|
+
"src/contacts/export_rows.ts"() {
|
|
5569
|
+
}
|
|
5570
|
+
});
|
|
5571
|
+
|
|
5572
|
+
// src/contacts/export_contact.ts
|
|
5573
|
+
function exportContact(db, id) {
|
|
5574
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
5074
5575
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
5075
5576
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
5076
5577
|
const events = query(
|
|
5077
|
-
"SELECT DISTINCT e
|
|
5578
|
+
"SELECT DISTINCT e.*, COALESCE(e.parent_event_id, (SELECT parent.id FROM events parent WHERE parent.external_source=e.parent_source AND parent.external_id=e.parent_ref_id AND parent.deleted_at IS NULL)) AS parent_event_id, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
5078
5579
|
id
|
|
5079
5580
|
).map((event) => ({
|
|
5080
5581
|
...event,
|
|
5582
|
+
attributes: query(
|
|
5583
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5584
|
+
event.id
|
|
5585
|
+
),
|
|
5081
5586
|
participants: query(
|
|
5082
5587
|
"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
5588
|
event.id
|
|
@@ -5121,22 +5626,46 @@ function exportContact(db, id) {
|
|
|
5121
5626
|
var init_export_contact = __esm({
|
|
5122
5627
|
"src/contacts/export_contact.ts"() {
|
|
5123
5628
|
init_affinity_not_found_error();
|
|
5629
|
+
init_export_rows();
|
|
5124
5630
|
}
|
|
5125
5631
|
});
|
|
5126
5632
|
|
|
5127
5633
|
// src/contacts/export_affinity.ts
|
|
5128
5634
|
function exportAffinity(db) {
|
|
5129
5635
|
const now = Date.now();
|
|
5636
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
5637
|
+
const events = exportRows(
|
|
5638
|
+
db,
|
|
5639
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
5640
|
+
).map((event) => ({
|
|
5641
|
+
...event,
|
|
5642
|
+
attributes: exportRows(
|
|
5643
|
+
db,
|
|
5644
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5645
|
+
event.id
|
|
5646
|
+
),
|
|
5647
|
+
participants: exportRows(
|
|
5648
|
+
db,
|
|
5649
|
+
"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",
|
|
5650
|
+
event.id
|
|
5651
|
+
)
|
|
5652
|
+
}));
|
|
5130
5653
|
return {
|
|
5131
5654
|
version: 1,
|
|
5132
5655
|
exportedAt: now,
|
|
5133
5656
|
exportedAtIso: new Date(now).toISOString(),
|
|
5134
|
-
|
|
5657
|
+
events,
|
|
5658
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
5659
|
+
...record,
|
|
5660
|
+
eventIds: events2.map((event) => event.id),
|
|
5661
|
+
dateIds: dates.map((date) => date.id)
|
|
5662
|
+
}))
|
|
5135
5663
|
};
|
|
5136
5664
|
}
|
|
5137
5665
|
var init_export_affinity = __esm({
|
|
5138
5666
|
"src/contacts/export_affinity.ts"() {
|
|
5139
5667
|
init_export_contact();
|
|
5668
|
+
init_export_rows();
|
|
5140
5669
|
}
|
|
5141
5670
|
});
|
|
5142
5671
|
|
|
@@ -5227,7 +5756,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
5227
5756
|
}
|
|
5228
5757
|
if (to === "merged") {
|
|
5229
5758
|
throw new AffinityInvariantError(
|
|
5230
|
-
"use
|
|
5759
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
5231
5760
|
);
|
|
5232
5761
|
}
|
|
5233
5762
|
if (from === to) {
|
|
@@ -5300,97 +5829,18 @@ var init_set_contact_lifecycle = __esm({
|
|
|
5300
5829
|
}
|
|
5301
5830
|
});
|
|
5302
5831
|
|
|
5303
|
-
// src/
|
|
5304
|
-
function
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
5316
|
-
);
|
|
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;
|
|
5832
|
+
// src/identities/queries.ts
|
|
5833
|
+
function getIdentityRowById(db, id) {
|
|
5834
|
+
const row = db.prepare(
|
|
5835
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5836
|
+
created_at, updated_at, removed_at
|
|
5837
|
+
FROM identities
|
|
5838
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
5839
|
+
).get(id);
|
|
5840
|
+
if (row === void 0) {
|
|
5841
|
+
return null;
|
|
5842
|
+
}
|
|
5843
|
+
return row;
|
|
5394
5844
|
}
|
|
5395
5845
|
var init_queries5 = __esm({
|
|
5396
5846
|
"src/identities/queries.ts"() {
|
|
@@ -5419,14 +5869,19 @@ var init_receipts5 = __esm({
|
|
|
5419
5869
|
// src/identities/validators.ts
|
|
5420
5870
|
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5421
5871
|
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(
|
|
5872
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
5873
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey) : db.prepare(
|
|
5874
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL AND id != ?"
|
|
5875
|
+
).get(
|
|
5876
|
+
normalizedKey,
|
|
5877
|
+
legacyIdentityKey(normalizedKey),
|
|
5878
|
+
normalizedKey,
|
|
5879
|
+
excludeIdentityId
|
|
5880
|
+
);
|
|
5426
5881
|
if (row !== void 0) {
|
|
5427
5882
|
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);
|
|
5883
|
+
"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"
|
|
5884
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey);
|
|
5430
5885
|
throw new AffinityConflictError(
|
|
5431
5886
|
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
5432
5887
|
);
|
|
@@ -5435,12 +5890,25 @@ function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
|
5435
5890
|
var init_validators5 = __esm({
|
|
5436
5891
|
"src/identities/validators.ts"() {
|
|
5437
5892
|
init_affinity_conflict_error();
|
|
5893
|
+
init_normalize();
|
|
5438
5894
|
}
|
|
5439
5895
|
});
|
|
5440
5896
|
|
|
5441
5897
|
// src/identities/validate_identity_input.ts
|
|
5442
|
-
function validateIdentityInput(type,
|
|
5898
|
+
function validateIdentityInput(type, rawValue) {
|
|
5443
5899
|
const kind = type.toLowerCase();
|
|
5900
|
+
const value = normalizeIdentityValue(kind, rawValue);
|
|
5901
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(kind) && !/^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/.test(value))
|
|
5902
|
+
throw new AffinityValidationError(
|
|
5903
|
+
"did value must match did:<method>:<identifier> (for example did:plc:abc123)"
|
|
5904
|
+
);
|
|
5905
|
+
if (handleIdentityTypes.has(kind)) {
|
|
5906
|
+
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_.-]+$/;
|
|
5907
|
+
if (!pattern.test(value))
|
|
5908
|
+
throw new AffinityValidationError(
|
|
5909
|
+
`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)"}`
|
|
5910
|
+
);
|
|
5911
|
+
}
|
|
5444
5912
|
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
5445
5913
|
throw new AffinityValidationError(
|
|
5446
5914
|
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
@@ -5471,7 +5939,10 @@ var identityTypes;
|
|
|
5471
5939
|
var init_validate_identity_input = __esm({
|
|
5472
5940
|
"src/identities/validate_identity_input.ts"() {
|
|
5473
5941
|
init_affinity_validation_error();
|
|
5942
|
+
init_normalize();
|
|
5474
5943
|
identityTypes = [
|
|
5944
|
+
"did",
|
|
5945
|
+
"atproto_did",
|
|
5475
5946
|
"email",
|
|
5476
5947
|
"phone",
|
|
5477
5948
|
"url",
|
|
@@ -5512,12 +5983,27 @@ function addIdentity(db, contactId, input) {
|
|
|
5512
5983
|
throw new AffinityStateError("merged contact is read-only");
|
|
5513
5984
|
}
|
|
5514
5985
|
const type = input.type.trim();
|
|
5515
|
-
const value = input.value
|
|
5986
|
+
const value = normalizeIdentityValue(type, input.value);
|
|
5516
5987
|
if (type.length === 0 || value.length === 0) {
|
|
5517
5988
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5518
5989
|
}
|
|
5519
5990
|
validateIdentityInput(type, value);
|
|
5991
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
5992
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
5520
5993
|
const nk = normalizeIdentityKey(type, value);
|
|
5994
|
+
if (input.ifExists === "return") {
|
|
5995
|
+
const existing = db.prepare(
|
|
5996
|
+
"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"
|
|
5997
|
+
).get(contactId, nk, legacyIdentityKey(nk), nk);
|
|
5998
|
+
if (existing) {
|
|
5999
|
+
const row2 = getIdentityRowById(db, Number(existing.id));
|
|
6000
|
+
if (row2)
|
|
6001
|
+
return buildIdentityMutationReceipt(
|
|
6002
|
+
mapIdentityRowToIdentityRecord(row2),
|
|
6003
|
+
{}
|
|
6004
|
+
);
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
5521
6007
|
assertNoIdentityCollision(db, nk);
|
|
5522
6008
|
const now = resolveNow(input.now);
|
|
5523
6009
|
const verified = input.verified ? 1 : 0;
|
|
@@ -5569,31 +6055,266 @@ var init_add_identity = __esm({
|
|
|
5569
6055
|
}
|
|
5570
6056
|
});
|
|
5571
6057
|
|
|
5572
|
-
// src/
|
|
5573
|
-
function
|
|
6058
|
+
// src/contacts/create_contact.ts
|
|
6059
|
+
function createContact(db, input) {
|
|
5574
6060
|
return withTransaction(db, () => {
|
|
5575
|
-
const
|
|
5576
|
-
|
|
5577
|
-
|
|
6061
|
+
const name = input.name.trim();
|
|
6062
|
+
if (name.length === 0) {
|
|
6063
|
+
throw new AffinityValidationError("name must be non-empty");
|
|
6064
|
+
}
|
|
6065
|
+
if (!KINDS.has(input.kind)) {
|
|
6066
|
+
throw new AffinityValidationError("invalid contact kind");
|
|
6067
|
+
}
|
|
6068
|
+
if (input.isOwner !== void 0) {
|
|
6069
|
+
throw new AffinityValidationError(
|
|
6070
|
+
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
6071
|
+
);
|
|
6072
|
+
}
|
|
6073
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
6074
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
6075
|
+
const matches = (input.identities ?? []).flatMap((identity) => {
|
|
5578
6076
|
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
6077
|
+
const key = normalizeIdentityKey(identity.type, identity.value);
|
|
6078
|
+
const row2 = db.prepare(
|
|
6079
|
+
`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)))`
|
|
6080
|
+
).get(key, legacyIdentityKey(key), key);
|
|
6081
|
+
return row2 ? [{ ...row2, identity }] : [];
|
|
6082
|
+
});
|
|
6083
|
+
const match = matches[0];
|
|
6084
|
+
if (match) {
|
|
6085
|
+
if (input.ifExists === "return") {
|
|
6086
|
+
const existing = getContactRowById(db, match.id);
|
|
6087
|
+
if (!existing)
|
|
6088
|
+
throw new AffinityInvariantError("identity owner not found");
|
|
6089
|
+
const primary2 = mapContactRowToContactListItem(existing);
|
|
6090
|
+
return Object.assign(buildContactMutationReceipt(primary2, {}), {
|
|
6091
|
+
outcome: "no_op",
|
|
6092
|
+
matchedIdentities: matches.map((m) => m.identity)
|
|
6093
|
+
});
|
|
6094
|
+
}
|
|
6095
|
+
const error = new AffinityConflictError(
|
|
6096
|
+
`identity ${normalizeIdentityKey(match.identity.type, match.identity.value).replace(":", " ")} already belongs to #${match.id} ${match.name}`
|
|
6097
|
+
);
|
|
6098
|
+
error.existingContactId = match.id;
|
|
6099
|
+
throw error;
|
|
6100
|
+
}
|
|
6101
|
+
const now = resolveNow(input.now);
|
|
6102
|
+
if (input.bootstrapOwner) {
|
|
5579
6103
|
const existing = db.prepare(
|
|
5580
|
-
"SELECT
|
|
5581
|
-
).get(
|
|
5582
|
-
if (existing) {
|
|
5583
|
-
|
|
5584
|
-
|
|
6104
|
+
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
6105
|
+
).get();
|
|
6106
|
+
if (existing !== void 0) {
|
|
6107
|
+
throw new AffinityConflictError(
|
|
6108
|
+
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
6109
|
+
);
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
6113
|
+
const result = db.prepare(
|
|
6114
|
+
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
6115
|
+
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
6116
|
+
).run(name, input.kind, isOwner, now, now);
|
|
6117
|
+
const id = Number(result.lastInsertRowid);
|
|
6118
|
+
const row = getContactRowById(db, id);
|
|
6119
|
+
if (!row) {
|
|
6120
|
+
throw new AffinityInvariantError("inserted contact row not found");
|
|
6121
|
+
}
|
|
6122
|
+
refreshContactRollup(db, id, now);
|
|
6123
|
+
const primary = mapContactRowToContactListItem(row);
|
|
6124
|
+
const receipt = buildContactMutationReceipt(primary, {
|
|
6125
|
+
created: [{ kind: "contact", id }]
|
|
6126
|
+
});
|
|
6127
|
+
for (const identity of input.identities ?? [])
|
|
6128
|
+
receipt.created.push(...addIdentity(db, id, identity).created);
|
|
6129
|
+
return receipt;
|
|
6130
|
+
});
|
|
6131
|
+
}
|
|
6132
|
+
var KINDS;
|
|
6133
|
+
var init_create_contact = __esm({
|
|
6134
|
+
"src/contacts/create_contact.ts"() {
|
|
6135
|
+
init_mappers4();
|
|
6136
|
+
init_queries();
|
|
6137
|
+
init_receipts4();
|
|
6138
|
+
init_rollups();
|
|
6139
|
+
init_add_identity();
|
|
6140
|
+
init_normalize();
|
|
6141
|
+
init_validate_identity_input();
|
|
6142
|
+
init_affinity_conflict_error();
|
|
6143
|
+
init_affinity_invariant_error();
|
|
6144
|
+
init_affinity_validation_error();
|
|
6145
|
+
init_resolve_now();
|
|
6146
|
+
init_with_transaction();
|
|
6147
|
+
KINDS = /* @__PURE__ */ new Set([
|
|
6148
|
+
"human",
|
|
6149
|
+
"group",
|
|
6150
|
+
"company",
|
|
6151
|
+
"team",
|
|
6152
|
+
"pet",
|
|
6153
|
+
"service",
|
|
6154
|
+
"other"
|
|
6155
|
+
]);
|
|
6156
|
+
}
|
|
6157
|
+
});
|
|
6158
|
+
|
|
6159
|
+
// src/contacts/fold_search_text.ts
|
|
6160
|
+
function foldSearchText(value) {
|
|
6161
|
+
let folded = value.toLowerCase();
|
|
6162
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
6163
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
6164
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
6165
|
+
}
|
|
6166
|
+
var init_fold_search_text = __esm({
|
|
6167
|
+
"src/contacts/fold_search_text.ts"() {
|
|
6168
|
+
}
|
|
6169
|
+
});
|
|
6170
|
+
|
|
6171
|
+
// src/contacts/list_duplicate_candidates.ts
|
|
6172
|
+
function pairKey(leftContactId, rightContactId) {
|
|
6173
|
+
const left = Math.min(leftContactId, rightContactId);
|
|
6174
|
+
const right = Math.max(leftContactId, rightContactId);
|
|
6175
|
+
return `${left}:${right}`;
|
|
6176
|
+
}
|
|
6177
|
+
function normalizeName(name) {
|
|
6178
|
+
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6179
|
+
}
|
|
6180
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
6181
|
+
const left = normalizeName(leftInput);
|
|
6182
|
+
const right = normalizeName(rightInput);
|
|
6183
|
+
if (left === right) {
|
|
6184
|
+
return 0.92;
|
|
6185
|
+
}
|
|
6186
|
+
if (left.length >= 3 && right.length >= 3) {
|
|
6187
|
+
if (left.includes(right) || right.includes(left)) {
|
|
6188
|
+
return 0.72;
|
|
6189
|
+
}
|
|
6190
|
+
const leftTokens = new Set(left.split(" "));
|
|
6191
|
+
const rightTokens = new Set(right.split(" "));
|
|
6192
|
+
const overlap = [...leftTokens].filter(
|
|
6193
|
+
(token) => rightTokens.has(token)
|
|
6194
|
+
).length;
|
|
6195
|
+
if (overlap > 0) {
|
|
6196
|
+
return overlap / Math.max(leftTokens.size, rightTokens.size) * 0.7;
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
return 0;
|
|
6200
|
+
}
|
|
6201
|
+
function listDuplicateCandidates(db, filters, options) {
|
|
6202
|
+
assertDefaultOrdering("listDuplicateCandidates", options);
|
|
6203
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
6204
|
+
const subset = filters?.contactIds;
|
|
6205
|
+
const contacts = db.prepare(
|
|
6206
|
+
`SELECT id, name
|
|
6207
|
+
FROM contacts
|
|
6208
|
+
WHERE deleted_at IS NULL
|
|
6209
|
+
AND lifecycle_state != 'merged'`
|
|
6210
|
+
).all();
|
|
6211
|
+
const allowedIds = subset === void 0 ? null : new Set(subset.map((contactId) => Number(contactId)));
|
|
6212
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
6213
|
+
const exactMatches = db.prepare(
|
|
6214
|
+
`SELECT
|
|
6215
|
+
i1.contact_id AS left_contact_id,
|
|
6216
|
+
i2.contact_id AS right_contact_id,
|
|
6217
|
+
i1.normalized_key
|
|
6218
|
+
FROM identities i1
|
|
6219
|
+
INNER JOIN identities i2
|
|
6220
|
+
ON i1.normalized_key = i2.normalized_key
|
|
6221
|
+
AND i1.contact_id < i2.contact_id
|
|
6222
|
+
INNER JOIN contacts c1 ON c1.id = i1.contact_id
|
|
6223
|
+
INNER JOIN contacts c2 ON c2.id = i2.contact_id
|
|
6224
|
+
WHERE i1.removed_at IS NULL
|
|
6225
|
+
AND i2.removed_at IS NULL
|
|
6226
|
+
AND c1.deleted_at IS NULL
|
|
6227
|
+
AND c2.deleted_at IS NULL
|
|
6228
|
+
AND c1.lifecycle_state != 'merged'
|
|
6229
|
+
AND c2.lifecycle_state != 'merged'`
|
|
6230
|
+
).all();
|
|
6231
|
+
for (const match of exactMatches) {
|
|
6232
|
+
if (allowedIds !== null && (!allowedIds.has(match.left_contact_id) || !allowedIds.has(match.right_contact_id))) {
|
|
6233
|
+
continue;
|
|
6234
|
+
}
|
|
6235
|
+
pairs.set(pairKey(match.left_contact_id, match.right_contact_id), {
|
|
6236
|
+
leftContactId: match.left_contact_id,
|
|
6237
|
+
rightContactId: match.right_contact_id,
|
|
6238
|
+
matchReason: `exact identity ${match.normalized_key}`,
|
|
6239
|
+
matchScore: 1
|
|
6240
|
+
});
|
|
6241
|
+
}
|
|
6242
|
+
if (filters?.exactOnly !== true) {
|
|
6243
|
+
for (let index = 0; index < contacts.length; index += 1) {
|
|
6244
|
+
const left = contacts[index];
|
|
6245
|
+
if (left === void 0 || allowedIds !== null && !allowedIds.has(left.id)) {
|
|
5585
6246
|
continue;
|
|
5586
6247
|
}
|
|
5587
|
-
const
|
|
5588
|
-
|
|
6248
|
+
const leftName = normalizeName(left.name);
|
|
6249
|
+
for (let offsetIndex = index + 1; offsetIndex < contacts.length; offsetIndex += 1) {
|
|
6250
|
+
const right = contacts[offsetIndex];
|
|
6251
|
+
if (right === void 0 || allowedIds !== null && !allowedIds.has(right.id)) {
|
|
6252
|
+
continue;
|
|
6253
|
+
}
|
|
6254
|
+
const score = fuzzyNameScore(leftName, normalizeName(right.name));
|
|
6255
|
+
if (score <= 0) {
|
|
6256
|
+
continue;
|
|
6257
|
+
}
|
|
6258
|
+
const key = pairKey(left.id, right.id);
|
|
6259
|
+
if (!pairs.has(key)) {
|
|
6260
|
+
pairs.set(key, {
|
|
6261
|
+
leftContactId: left.id,
|
|
6262
|
+
rightContactId: right.id,
|
|
6263
|
+
matchReason: "name similarity",
|
|
6264
|
+
matchScore: score
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
|
|
6271
|
+
const dismissedSet = new Set(
|
|
6272
|
+
dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
|
|
6273
|
+
);
|
|
6274
|
+
const includeDismissed = filters?.includeDismissed === true;
|
|
6275
|
+
for (const candidate of pairs.values()) {
|
|
6276
|
+
if (dismissedSet.has(
|
|
6277
|
+
pairKey(candidate.leftContactId, candidate.rightContactId)
|
|
6278
|
+
)) {
|
|
6279
|
+
candidate.dismissed = true;
|
|
5589
6280
|
}
|
|
6281
|
+
}
|
|
6282
|
+
const minScore = filters?.minScore ?? 0;
|
|
6283
|
+
return [...pairs.values()].filter((candidate) => {
|
|
6284
|
+
if (candidate.matchScore < minScore) return false;
|
|
6285
|
+
if (candidate.dismissed === true && !includeDismissed) return false;
|
|
6286
|
+
return true;
|
|
6287
|
+
}).sort((left, right) => {
|
|
6288
|
+
const leftExact = left.matchScore === 1 ? 1 : 0;
|
|
6289
|
+
const rightExact = right.matchScore === 1 ? 1 : 0;
|
|
6290
|
+
return rightExact - leftExact || right.matchScore - left.matchScore;
|
|
6291
|
+
}).slice(offset, offset + limit);
|
|
6292
|
+
}
|
|
6293
|
+
var init_list_duplicate_candidates = __esm({
|
|
6294
|
+
"src/contacts/list_duplicate_candidates.ts"() {
|
|
6295
|
+
init_assert_default_ordering();
|
|
6296
|
+
init_resolve_list_limit_offset();
|
|
6297
|
+
}
|
|
6298
|
+
});
|
|
6299
|
+
|
|
6300
|
+
// src/tools/create_contact_tool_result.ts
|
|
6301
|
+
function createContactToolResult(db, input) {
|
|
6302
|
+
return withTransaction(db, () => {
|
|
6303
|
+
const receipt = createContact(db, input);
|
|
5590
6304
|
const result = mutationToolResult("create", receipt, "contact");
|
|
6305
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
6306
|
+
result.outcome = "no_op";
|
|
6307
|
+
Object.assign(result.data, {
|
|
6308
|
+
matchedIdentities: receipt.matchedIdentities
|
|
6309
|
+
});
|
|
6310
|
+
return result;
|
|
6311
|
+
}
|
|
5591
6312
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
5592
6313
|
const warnings = db.prepare(
|
|
5593
6314
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
5594
6315
|
).all(receipt.primary.id).flatMap((row) => {
|
|
5595
|
-
const score =
|
|
5596
|
-
return score
|
|
6316
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
6317
|
+
return score <= 0 ? [] : [
|
|
5597
6318
|
{
|
|
5598
6319
|
code: "possible_duplicate",
|
|
5599
6320
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -5610,9 +6331,7 @@ var init_create_contact_tool_result = __esm({
|
|
|
5610
6331
|
"src/tools/create_contact_tool_result.ts"() {
|
|
5611
6332
|
init_create_contact();
|
|
5612
6333
|
init_fold_search_text();
|
|
5613
|
-
|
|
5614
|
-
init_normalize();
|
|
5615
|
-
init_validate_identity_input();
|
|
6334
|
+
init_list_duplicate_candidates();
|
|
5616
6335
|
init_with_transaction();
|
|
5617
6336
|
init_tool_mutation();
|
|
5618
6337
|
}
|
|
@@ -5687,6 +6406,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
5687
6406
|
return contact.result;
|
|
5688
6407
|
}
|
|
5689
6408
|
if (input.action === "delete") {
|
|
6409
|
+
if (contact.value.is_owner)
|
|
6410
|
+
return toolFailure(
|
|
6411
|
+
"domain",
|
|
6412
|
+
"conflict",
|
|
6413
|
+
"Cannot delete owner.",
|
|
6414
|
+
"The owner contact cannot be deleted"
|
|
6415
|
+
);
|
|
5690
6416
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
5691
6417
|
return toolSuccess("Permanently deleted contact.", {
|
|
5692
6418
|
action: "delete",
|
|
@@ -5776,7 +6502,11 @@ var init_manage_contact_tool = __esm({
|
|
|
5776
6502
|
},
|
|
5777
6503
|
["type", "value"]
|
|
5778
6504
|
),
|
|
5779
|
-
"Optional identities to attach.
|
|
6505
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
6506
|
+
),
|
|
6507
|
+
ifExists: enumSchema(
|
|
6508
|
+
"Identity collision policy; return attaches nothing.",
|
|
6509
|
+
["error", "return"]
|
|
5780
6510
|
),
|
|
5781
6511
|
name: stringSchema("Contact name."),
|
|
5782
6512
|
kind: enumSchema("Contact kind.", [
|
|
@@ -5900,7 +6630,7 @@ var init_add_date_anchor = __esm({
|
|
|
5900
6630
|
init_loaders();
|
|
5901
6631
|
init_persistence();
|
|
5902
6632
|
init_queries2();
|
|
5903
|
-
|
|
6633
|
+
init_receipts();
|
|
5904
6634
|
init_validators();
|
|
5905
6635
|
init_affinity_conflict_error();
|
|
5906
6636
|
init_affinity_not_found_error();
|
|
@@ -5932,7 +6662,7 @@ var init_remove_date_anchor = __esm({
|
|
|
5932
6662
|
"src/dates/remove_date_anchor.ts"() {
|
|
5933
6663
|
init_loaders();
|
|
5934
6664
|
init_mappers();
|
|
5935
|
-
|
|
6665
|
+
init_receipts();
|
|
5936
6666
|
init_resolve_now();
|
|
5937
6667
|
init_with_transaction();
|
|
5938
6668
|
init_upcoming_occurrences();
|
|
@@ -6001,7 +6731,7 @@ var init_revise_date_anchor = __esm({
|
|
|
6001
6731
|
"src/dates/revise_date_anchor.ts"() {
|
|
6002
6732
|
init_loaders();
|
|
6003
6733
|
init_queries2();
|
|
6004
|
-
|
|
6734
|
+
init_receipts();
|
|
6005
6735
|
init_validators();
|
|
6006
6736
|
init_affinity_conflict_error();
|
|
6007
6737
|
init_affinity_validation_error();
|
|
@@ -6414,67 +7144,362 @@ function manageDuplicateDismissalToolHandler(db, rawInput) {
|
|
|
6414
7144
|
]
|
|
6415
7145
|
}
|
|
6416
7146
|
);
|
|
6417
|
-
}
|
|
6418
|
-
const receipt = undismissDuplicate(db, left.value.id, right.value.id);
|
|
6419
|
-
return toolSuccess(
|
|
6420
|
-
`Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
|
|
6421
|
-
{
|
|
6422
|
-
...receipt,
|
|
6423
|
-
action: "undismiss",
|
|
6424
|
-
changes: existed ? [
|
|
6425
|
-
{
|
|
6426
|
-
entity: "duplicate_dismissal",
|
|
6427
|
-
leftContactId,
|
|
6428
|
-
rightContactId,
|
|
6429
|
-
action: "removed"
|
|
6430
|
-
}
|
|
6431
|
-
] : []
|
|
6432
|
-
}
|
|
7147
|
+
}
|
|
7148
|
+
const receipt = undismissDuplicate(db, left.value.id, right.value.id);
|
|
7149
|
+
return toolSuccess(
|
|
7150
|
+
`Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
|
|
7151
|
+
{
|
|
7152
|
+
...receipt,
|
|
7153
|
+
action: "undismiss",
|
|
7154
|
+
changes: existed ? [
|
|
7155
|
+
{
|
|
7156
|
+
entity: "duplicate_dismissal",
|
|
7157
|
+
leftContactId,
|
|
7158
|
+
rightContactId,
|
|
7159
|
+
action: "removed"
|
|
7160
|
+
}
|
|
7161
|
+
] : []
|
|
7162
|
+
}
|
|
7163
|
+
);
|
|
7164
|
+
}, "Duplicate dismissal failed.");
|
|
7165
|
+
}
|
|
7166
|
+
var manageDuplicateDismissalTool;
|
|
7167
|
+
var init_manage_duplicate_dismissal_tool = __esm({
|
|
7168
|
+
"src/tools/manage_duplicate_dismissal_tool.ts"() {
|
|
7169
|
+
init_dismiss_duplicate();
|
|
7170
|
+
init_undismiss_duplicate();
|
|
7171
|
+
init_tool_metadata();
|
|
7172
|
+
init_tool_names();
|
|
7173
|
+
init_tool_resolvers();
|
|
7174
|
+
init_tool_types();
|
|
7175
|
+
init_tool_types();
|
|
7176
|
+
init_validate_tool_input();
|
|
7177
|
+
manageDuplicateDismissalTool = defineAffinityTool({
|
|
7178
|
+
name: manageDuplicateDismissalToolName,
|
|
7179
|
+
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.",
|
|
7180
|
+
whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
|
|
7181
|
+
whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
|
|
7182
|
+
sideEffects: "writes_state",
|
|
7183
|
+
readOnly: false,
|
|
7184
|
+
supportsClarification: true,
|
|
7185
|
+
targetKinds: ["contact"],
|
|
7186
|
+
inputDescriptions: {
|
|
7187
|
+
action: "dismiss to suppress the pair; undismiss to re-surface it.",
|
|
7188
|
+
left: "One contact of the pair.",
|
|
7189
|
+
right: "The other contact of the pair.",
|
|
7190
|
+
reason: "Optional reason for the dismissal (audit trail)."
|
|
7191
|
+
},
|
|
7192
|
+
outputDescription: "Returns the dismissal receipt with the canonical pair identifiers.",
|
|
7193
|
+
inputSchema: objectSchema(
|
|
7194
|
+
{
|
|
7195
|
+
action: enumSchema("Action to perform.", ["dismiss", "undismiss"]),
|
|
7196
|
+
left: contactLocatorSchema5(
|
|
7197
|
+
"Left contact. Provide contactId or identity."
|
|
7198
|
+
),
|
|
7199
|
+
right: contactLocatorSchema5(
|
|
7200
|
+
"Right contact. Provide contactId or identity."
|
|
7201
|
+
),
|
|
7202
|
+
reason: nullableStringSchema("Optional reason for the dismissal.")
|
|
7203
|
+
},
|
|
7204
|
+
["action", "left", "right"],
|
|
7205
|
+
"Dismiss or un-dismiss a duplicate candidate pair."
|
|
7206
|
+
),
|
|
7207
|
+
handler: manageDuplicateDismissalToolHandler
|
|
7208
|
+
});
|
|
7209
|
+
}
|
|
7210
|
+
});
|
|
7211
|
+
|
|
7212
|
+
// src/events/event_replay.ts
|
|
7213
|
+
function replayEventLinks(db, linkIds, now) {
|
|
7214
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
7215
|
+
for (const linkId of linkIds) {
|
|
7216
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
7217
|
+
if (!saved) continue;
|
|
7218
|
+
const baseline = JSON.parse(saved.state_json);
|
|
7219
|
+
const effects = db.prepare(
|
|
7220
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
7221
|
+
).all(linkId);
|
|
7222
|
+
const overrides = db.prepare(
|
|
7223
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
7224
|
+
).all(linkId);
|
|
7225
|
+
let overrideIndex = 0;
|
|
7226
|
+
const applyOverrides = (beforeId) => {
|
|
7227
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
7228
|
+
const override = overrides[overrideIndex];
|
|
7229
|
+
if (override)
|
|
7230
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
7231
|
+
override.state,
|
|
7232
|
+
linkId
|
|
7233
|
+
);
|
|
7234
|
+
overrideIndex++;
|
|
7235
|
+
}
|
|
7236
|
+
};
|
|
7237
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
7238
|
+
db.prepare(
|
|
7239
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
7240
|
+
).run(
|
|
7241
|
+
baseline.rank,
|
|
7242
|
+
baseline.affinity,
|
|
7243
|
+
baseline.trust,
|
|
7244
|
+
baseline.state,
|
|
7245
|
+
baseline.cadence_days,
|
|
7246
|
+
now,
|
|
7247
|
+
linkId
|
|
7248
|
+
);
|
|
7249
|
+
for (const effect of effects) {
|
|
7250
|
+
eventIds.add(effect.event_id);
|
|
7251
|
+
applyOverrides(effect.event_id);
|
|
7252
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
7253
|
+
continue;
|
|
7254
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
7255
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
7256
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
7257
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
7258
|
+
applyEventEffectToLink(db, {
|
|
7259
|
+
linkId,
|
|
7260
|
+
eventId: event.id,
|
|
7261
|
+
eventType: event.type,
|
|
7262
|
+
occurredAt: event.occurredAt,
|
|
7263
|
+
significance: event.significance,
|
|
7264
|
+
participant,
|
|
7265
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
7266
|
+
now: effect.created_at
|
|
7267
|
+
});
|
|
7268
|
+
}
|
|
7269
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
7270
|
+
refreshLinkRollup(db, linkId, now);
|
|
7271
|
+
}
|
|
7272
|
+
for (const eventId of eventIds)
|
|
7273
|
+
db.prepare(
|
|
7274
|
+
"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=?"
|
|
7275
|
+
).run(eventId);
|
|
7276
|
+
refreshAllBridgeScores(db, now);
|
|
7277
|
+
}
|
|
7278
|
+
var init_event_replay = __esm({
|
|
7279
|
+
"src/events/event_replay.ts"() {
|
|
7280
|
+
init_bridge_scores();
|
|
7281
|
+
init_effects();
|
|
7282
|
+
init_rollups2();
|
|
7283
|
+
init_loaders();
|
|
7284
|
+
}
|
|
7285
|
+
});
|
|
7286
|
+
|
|
7287
|
+
// src/events/manage_event.ts
|
|
7288
|
+
function requireEventTarget(db, target) {
|
|
7289
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
7290
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
7291
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
7292
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
7293
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
7294
|
+
return event;
|
|
7295
|
+
}
|
|
7296
|
+
function affected(db, id) {
|
|
7297
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
7298
|
+
}
|
|
7299
|
+
function reviseEvent(db, target, patch) {
|
|
7300
|
+
return withTransaction(db, () => {
|
|
7301
|
+
const allowed = [
|
|
7302
|
+
"summary",
|
|
7303
|
+
"content",
|
|
7304
|
+
"occurredAt",
|
|
7305
|
+
"significance",
|
|
7306
|
+
"channel",
|
|
7307
|
+
"type",
|
|
7308
|
+
"externalRef"
|
|
7309
|
+
];
|
|
7310
|
+
for (const key of Object.keys(patch))
|
|
7311
|
+
if (!allowed.includes(key))
|
|
7312
|
+
throw new AffinityValidationError(
|
|
7313
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
7314
|
+
);
|
|
7315
|
+
const event = requireEventTarget(db, target);
|
|
7316
|
+
const next = { ...event, ...patch };
|
|
7317
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
7318
|
+
validateEventMetadata(metadata);
|
|
7319
|
+
Object.assign(next, metadata);
|
|
7320
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
7321
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
7322
|
+
if (patch.occurredAt !== void 0)
|
|
7323
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
7324
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
7325
|
+
throw new AffinityValidationError(
|
|
7326
|
+
"significance must be an integer from 1 to 10"
|
|
7327
|
+
);
|
|
7328
|
+
const interactions = [
|
|
7329
|
+
"conversation",
|
|
7330
|
+
"activity",
|
|
7331
|
+
"gift",
|
|
7332
|
+
"support",
|
|
7333
|
+
"conflict",
|
|
7334
|
+
"correction"
|
|
7335
|
+
];
|
|
7336
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
7337
|
+
throw new AffinityValidationError(
|
|
7338
|
+
"type must stay within the same event kind"
|
|
7339
|
+
);
|
|
7340
|
+
if (patch.externalRef && event.externalRef)
|
|
7341
|
+
throw new AffinityConflictError(
|
|
7342
|
+
"externalRef can only be set when absent"
|
|
7343
|
+
);
|
|
7344
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
7345
|
+
throw new AffinityConflictError(
|
|
7346
|
+
"externalRef already belongs to an event"
|
|
7347
|
+
);
|
|
7348
|
+
const now = Date.now();
|
|
7349
|
+
const links = affected(db, event.id);
|
|
7350
|
+
db.prepare(
|
|
7351
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
7352
|
+
).run(
|
|
7353
|
+
next.summary.trim(),
|
|
7354
|
+
next.content ?? null,
|
|
7355
|
+
next.occurredAt,
|
|
7356
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
7357
|
+
next.significance,
|
|
7358
|
+
next.channel ?? null,
|
|
7359
|
+
next.type,
|
|
7360
|
+
next.externalRef?.source ?? null,
|
|
7361
|
+
next.externalRef?.id ?? null,
|
|
7362
|
+
now,
|
|
7363
|
+
event.id
|
|
6433
7364
|
);
|
|
6434
|
-
|
|
7365
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
7366
|
+
db.prepare(
|
|
7367
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
7368
|
+
).run(patch.type, event.id);
|
|
7369
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
7370
|
+
replayEventLinks(db, links, now);
|
|
7371
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
7372
|
+
updated: [{ kind: "event", id: event.id }],
|
|
7373
|
+
affectedLinks: links
|
|
7374
|
+
});
|
|
7375
|
+
});
|
|
6435
7376
|
}
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
7377
|
+
function deleteEvent(db, target, options) {
|
|
7378
|
+
return withTransaction(db, () => {
|
|
7379
|
+
if (options?.confirm !== true)
|
|
7380
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
7381
|
+
const event = requireEventTarget(db, target);
|
|
7382
|
+
const links = affected(db, event.id);
|
|
7383
|
+
for (const table of [
|
|
7384
|
+
"event_participants",
|
|
7385
|
+
"attributes",
|
|
7386
|
+
"open_commitments",
|
|
7387
|
+
"upcoming_occurrences",
|
|
7388
|
+
"link_event_effects"
|
|
7389
|
+
])
|
|
7390
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
7391
|
+
db.prepare(
|
|
7392
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
7393
|
+
).run(event.id);
|
|
7394
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
7395
|
+
replayEventLinks(db, links, Date.now());
|
|
7396
|
+
return buildEventMutationReceipt(event, {
|
|
7397
|
+
removed: [{ kind: "event", id: event.id }],
|
|
7398
|
+
affectedLinks: links
|
|
7399
|
+
});
|
|
7400
|
+
});
|
|
7401
|
+
}
|
|
7402
|
+
var init_manage_event = __esm({
|
|
7403
|
+
"src/events/manage_event.ts"() {
|
|
7404
|
+
init_affinity_conflict_error();
|
|
7405
|
+
init_affinity_not_found_error();
|
|
7406
|
+
init_affinity_validation_error();
|
|
7407
|
+
init_with_transaction();
|
|
7408
|
+
init_event_metadata();
|
|
7409
|
+
init_event_replay();
|
|
7410
|
+
init_receipts();
|
|
7411
|
+
init_validators();
|
|
7412
|
+
}
|
|
7413
|
+
});
|
|
7414
|
+
|
|
7415
|
+
// src/tools/manage_event_tool.ts
|
|
7416
|
+
function manageEventToolHandler(db, raw) {
|
|
7417
|
+
const input = structuredClone(raw);
|
|
7418
|
+
const error = validateToolInput(
|
|
7419
|
+
manageEventTool.inputSchema,
|
|
7420
|
+
input,
|
|
7421
|
+
"input",
|
|
7422
|
+
Date.now(),
|
|
7423
|
+
true
|
|
7424
|
+
);
|
|
7425
|
+
if (error)
|
|
7426
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
7427
|
+
return withToolHandling(() => {
|
|
7428
|
+
const target = {
|
|
7429
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
7430
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
7431
|
+
};
|
|
7432
|
+
return mutationToolResult(
|
|
7433
|
+
input.action,
|
|
7434
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
7435
|
+
"event"
|
|
7436
|
+
);
|
|
7437
|
+
}, "Event management failed.");
|
|
7438
|
+
}
|
|
7439
|
+
var manageEventTool;
|
|
7440
|
+
var init_manage_event_tool = __esm({
|
|
7441
|
+
"src/tools/manage_event_tool.ts"() {
|
|
7442
|
+
init_manage_event();
|
|
7443
|
+
init_event_metadata_schema();
|
|
6441
7444
|
init_tool_metadata();
|
|
7445
|
+
init_tool_mutation();
|
|
6442
7446
|
init_tool_names();
|
|
6443
7447
|
init_tool_resolvers();
|
|
6444
7448
|
init_tool_types();
|
|
6445
|
-
init_tool_types();
|
|
6446
7449
|
init_validate_tool_input();
|
|
6447
|
-
|
|
6448
|
-
name:
|
|
6449
|
-
description: "
|
|
6450
|
-
whenToUse: "
|
|
6451
|
-
whenNotToUse: "
|
|
7450
|
+
manageEventTool = defineAffinityTool({
|
|
7451
|
+
name: manageEventToolName,
|
|
7452
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
7453
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
7454
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
6452
7455
|
sideEffects: "writes_state",
|
|
6453
7456
|
readOnly: false,
|
|
6454
|
-
supportsClarification:
|
|
6455
|
-
targetKinds: ["
|
|
7457
|
+
supportsClarification: false,
|
|
7458
|
+
targetKinds: ["event"],
|
|
6456
7459
|
inputDescriptions: {
|
|
6457
|
-
action: "
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
7460
|
+
action: "revise or delete",
|
|
7461
|
+
eventId: "Exact event id",
|
|
7462
|
+
ref: "External event reference",
|
|
7463
|
+
patch: "Fields to revise",
|
|
7464
|
+
confirm: "Required true for deletion"
|
|
6461
7465
|
},
|
|
6462
|
-
outputDescription: "
|
|
7466
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
6463
7467
|
inputSchema: objectSchema(
|
|
6464
7468
|
{
|
|
6465
|
-
action: enumSchema("Action
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
),
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
7469
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
7470
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
7471
|
+
ref: externalRefSchema,
|
|
7472
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
7473
|
+
patch: objectSchema(
|
|
7474
|
+
{
|
|
7475
|
+
summary: stringSchema("Short non-empty label."),
|
|
7476
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
7477
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
7478
|
+
significance: integerSchema("Significance 1..10."),
|
|
7479
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
7480
|
+
type: enumSchema("Type within the same event kind.", [
|
|
7481
|
+
"conversation",
|
|
7482
|
+
"activity",
|
|
7483
|
+
"gift",
|
|
7484
|
+
"support",
|
|
7485
|
+
"conflict",
|
|
7486
|
+
"correction",
|
|
7487
|
+
"observation",
|
|
7488
|
+
"milestone",
|
|
7489
|
+
"transaction",
|
|
7490
|
+
"promise",
|
|
7491
|
+
"agreement",
|
|
7492
|
+
"date_anchor"
|
|
7493
|
+
]),
|
|
7494
|
+
externalRef: externalRefSchema
|
|
7495
|
+
},
|
|
7496
|
+
[],
|
|
7497
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
7498
|
+
)
|
|
6473
7499
|
},
|
|
6474
|
-
["action"
|
|
6475
|
-
"Dismiss or un-dismiss a duplicate candidate pair."
|
|
7500
|
+
["action"]
|
|
6476
7501
|
),
|
|
6477
|
-
handler:
|
|
7502
|
+
handler: manageEventToolHandler
|
|
6478
7503
|
});
|
|
6479
7504
|
}
|
|
6480
7505
|
});
|
|
@@ -6539,7 +7564,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
6539
7564
|
});
|
|
6540
7565
|
}
|
|
6541
7566
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
6542
|
-
const nextValue =
|
|
7567
|
+
const nextValue = normalizeIdentityValue(
|
|
7568
|
+
nextType,
|
|
7569
|
+
patch.value ?? row.value
|
|
7570
|
+
);
|
|
6543
7571
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
6544
7572
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
6545
7573
|
}
|
|
@@ -6626,6 +7654,10 @@ function contactLocatorSchema6(description) {
|
|
|
6626
7654
|
contactId: integerSchema("Exact contact id."),
|
|
6627
7655
|
identity: objectSchema(
|
|
6628
7656
|
{
|
|
7657
|
+
ifExists: enumSchema(
|
|
7658
|
+
"Return an existing identity on this contact without changes.",
|
|
7659
|
+
["error", "return"]
|
|
7660
|
+
),
|
|
6629
7661
|
type: stringSchema(
|
|
6630
7662
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6631
7663
|
),
|
|
@@ -6736,6 +7768,10 @@ var init_manage_identity_tool = __esm({
|
|
|
6736
7768
|
),
|
|
6737
7769
|
input: objectSchema(
|
|
6738
7770
|
{
|
|
7771
|
+
ifExists: enumSchema(
|
|
7772
|
+
"Return an existing identity on this contact without changes.",
|
|
7773
|
+
["error", "return"]
|
|
7774
|
+
),
|
|
6739
7775
|
type: stringSchema(
|
|
6740
7776
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6741
7777
|
),
|
|
@@ -6799,6 +7835,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
6799
7835
|
now,
|
|
6800
7836
|
linkId
|
|
6801
7837
|
);
|
|
7838
|
+
db.prepare(
|
|
7839
|
+
"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=?"
|
|
7840
|
+
).run(linkId, state, linkId);
|
|
6802
7841
|
refreshLinkRollup(db, linkId, now);
|
|
6803
7842
|
if (state === "archived" || row.state === "archived") {
|
|
6804
7843
|
refreshAllBridgeScores(db, now);
|
|
@@ -7523,6 +8562,8 @@ var init_merge_contacts_tool = __esm({
|
|
|
7523
8562
|
function recordInteraction(db, input) {
|
|
7524
8563
|
return withTransaction(db, () => {
|
|
7525
8564
|
validateSocialEventInput(input);
|
|
8565
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8566
|
+
if (existing) return existing;
|
|
7526
8567
|
if (!ALLOWED.has(input.type)) {
|
|
7527
8568
|
throw new AffinityValidationError("invalid interaction event type");
|
|
7528
8569
|
}
|
|
@@ -7532,11 +8573,12 @@ function recordInteraction(db, input) {
|
|
|
7532
8573
|
assertOwnerParticipates(ownerId, ids);
|
|
7533
8574
|
const now = resolveNow(input.now);
|
|
7534
8575
|
const eventId = insertJournalEvent(db, {
|
|
8576
|
+
...input,
|
|
7535
8577
|
type: input.type,
|
|
7536
8578
|
occurredAt: input.occurredAt,
|
|
7537
|
-
|
|
8579
|
+
occurredOn: input.occurredOn,
|
|
7538
8580
|
significance: input.significance,
|
|
7539
|
-
momentKind: null,
|
|
8581
|
+
momentKind: input.momentKind ?? null,
|
|
7540
8582
|
participants: input.participants,
|
|
7541
8583
|
now
|
|
7542
8584
|
});
|
|
@@ -7544,12 +8586,24 @@ function recordInteraction(db, input) {
|
|
|
7544
8586
|
eventId,
|
|
7545
8587
|
eventType: input.type,
|
|
7546
8588
|
occurredAt: input.occurredAt,
|
|
8589
|
+
occurredOn: input.occurredOn,
|
|
7547
8590
|
significance: input.significance,
|
|
7548
8591
|
participants: input.participants,
|
|
7549
8592
|
provenance: input.provenance,
|
|
7550
8593
|
now
|
|
7551
8594
|
});
|
|
8595
|
+
if (input.momentKind !== void 0)
|
|
8596
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8597
|
+
input.momentKind,
|
|
8598
|
+
eventId
|
|
8599
|
+
);
|
|
7552
8600
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8601
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8602
|
+
{
|
|
8603
|
+
code: "future_event",
|
|
8604
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8605
|
+
}
|
|
8606
|
+
] : void 0,
|
|
7553
8607
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7554
8608
|
affectedLinks: mechanics.affectedLinks,
|
|
7555
8609
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7562,12 +8616,14 @@ var init_record_interaction = __esm({
|
|
|
7562
8616
|
init_queries();
|
|
7563
8617
|
init_loaders();
|
|
7564
8618
|
init_persistence();
|
|
7565
|
-
|
|
8619
|
+
init_receipts();
|
|
7566
8620
|
init_validators();
|
|
7567
8621
|
init_affinity_validation_error();
|
|
8622
|
+
init_parse_calendar_time();
|
|
7568
8623
|
init_resolve_now();
|
|
7569
8624
|
init_with_transaction();
|
|
7570
8625
|
init_apply_journal_event_mechanics();
|
|
8626
|
+
init_event_metadata();
|
|
7571
8627
|
ALLOWED = /* @__PURE__ */ new Set([
|
|
7572
8628
|
"conversation",
|
|
7573
8629
|
"activity",
|
|
@@ -7583,17 +8639,20 @@ var init_record_interaction = __esm({
|
|
|
7583
8639
|
function recordMilestone(db, input) {
|
|
7584
8640
|
return withTransaction(db, () => {
|
|
7585
8641
|
validateSocialEventInput(input);
|
|
8642
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8643
|
+
if (existing) return existing;
|
|
7586
8644
|
const ownerId = requireOwnerContactId(db);
|
|
7587
8645
|
const ids = input.participants.map((p) => p.contactId);
|
|
7588
8646
|
assertParticipantContactsLive(db, ids);
|
|
7589
8647
|
assertOwnerParticipates(ownerId, ids);
|
|
7590
8648
|
const now = resolveNow(input.now);
|
|
7591
8649
|
const eventId = insertJournalEvent(db, {
|
|
8650
|
+
...input,
|
|
7592
8651
|
type: "milestone",
|
|
7593
8652
|
occurredAt: input.occurredAt,
|
|
7594
|
-
|
|
8653
|
+
occurredOn: input.occurredOn,
|
|
7595
8654
|
significance: input.significance,
|
|
7596
|
-
momentKind: null,
|
|
8655
|
+
momentKind: input.momentKind ?? null,
|
|
7597
8656
|
participants: input.participants,
|
|
7598
8657
|
now
|
|
7599
8658
|
});
|
|
@@ -7601,12 +8660,24 @@ function recordMilestone(db, input) {
|
|
|
7601
8660
|
eventId,
|
|
7602
8661
|
eventType: "milestone",
|
|
7603
8662
|
occurredAt: input.occurredAt,
|
|
8663
|
+
occurredOn: input.occurredOn,
|
|
7604
8664
|
significance: input.significance,
|
|
7605
8665
|
participants: input.participants,
|
|
7606
8666
|
provenance: input.provenance,
|
|
7607
8667
|
now
|
|
7608
8668
|
});
|
|
8669
|
+
if (input.momentKind !== void 0)
|
|
8670
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8671
|
+
input.momentKind,
|
|
8672
|
+
eventId
|
|
8673
|
+
);
|
|
7609
8674
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8675
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8676
|
+
{
|
|
8677
|
+
code: "future_event",
|
|
8678
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8679
|
+
}
|
|
8680
|
+
] : void 0,
|
|
7610
8681
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7611
8682
|
affectedLinks: mechanics.affectedLinks,
|
|
7612
8683
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7618,11 +8689,13 @@ var init_record_milestone = __esm({
|
|
|
7618
8689
|
init_queries();
|
|
7619
8690
|
init_loaders();
|
|
7620
8691
|
init_persistence();
|
|
7621
|
-
|
|
8692
|
+
init_receipts();
|
|
7622
8693
|
init_validators();
|
|
8694
|
+
init_parse_calendar_time();
|
|
7623
8695
|
init_resolve_now();
|
|
7624
8696
|
init_with_transaction();
|
|
7625
8697
|
init_apply_journal_event_mechanics();
|
|
8698
|
+
init_event_metadata();
|
|
7626
8699
|
}
|
|
7627
8700
|
});
|
|
7628
8701
|
|
|
@@ -7630,15 +8703,18 @@ var init_record_milestone = __esm({
|
|
|
7630
8703
|
function recordObservation(db, input) {
|
|
7631
8704
|
return withTransaction(db, () => {
|
|
7632
8705
|
validateSocialEventInput(input);
|
|
8706
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8707
|
+
if (existing) return existing;
|
|
7633
8708
|
const ids = input.participants.map((p) => p.contactId);
|
|
7634
8709
|
assertParticipantContactsLive(db, ids);
|
|
7635
8710
|
const now = resolveNow(input.now);
|
|
7636
8711
|
const eventId = insertJournalEvent(db, {
|
|
8712
|
+
...input,
|
|
7637
8713
|
type: "observation",
|
|
7638
8714
|
occurredAt: input.occurredAt,
|
|
7639
|
-
|
|
8715
|
+
occurredOn: input.occurredOn,
|
|
7640
8716
|
significance: input.significance,
|
|
7641
|
-
momentKind: null,
|
|
8717
|
+
momentKind: input.momentKind ?? null,
|
|
7642
8718
|
participants: input.participants,
|
|
7643
8719
|
now
|
|
7644
8720
|
});
|
|
@@ -7646,12 +8722,24 @@ function recordObservation(db, input) {
|
|
|
7646
8722
|
eventId,
|
|
7647
8723
|
eventType: "observation",
|
|
7648
8724
|
occurredAt: input.occurredAt,
|
|
8725
|
+
occurredOn: input.occurredOn,
|
|
7649
8726
|
significance: input.significance,
|
|
7650
8727
|
participants: input.participants,
|
|
7651
8728
|
provenance: input.provenance,
|
|
7652
8729
|
now
|
|
7653
8730
|
});
|
|
8731
|
+
if (input.momentKind !== void 0)
|
|
8732
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8733
|
+
input.momentKind,
|
|
8734
|
+
eventId
|
|
8735
|
+
);
|
|
7654
8736
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8737
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8738
|
+
{
|
|
8739
|
+
code: "future_event",
|
|
8740
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8741
|
+
}
|
|
8742
|
+
] : void 0,
|
|
7655
8743
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7656
8744
|
affectedLinks: mechanics.affectedLinks,
|
|
7657
8745
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7662,11 +8750,13 @@ var init_record_observation = __esm({
|
|
|
7662
8750
|
"src/events/record_observation.ts"() {
|
|
7663
8751
|
init_loaders();
|
|
7664
8752
|
init_persistence();
|
|
7665
|
-
|
|
8753
|
+
init_receipts();
|
|
7666
8754
|
init_validators();
|
|
8755
|
+
init_parse_calendar_time();
|
|
7667
8756
|
init_resolve_now();
|
|
7668
8757
|
init_with_transaction();
|
|
7669
8758
|
init_apply_journal_event_mechanics();
|
|
8759
|
+
init_event_metadata();
|
|
7670
8760
|
}
|
|
7671
8761
|
});
|
|
7672
8762
|
|
|
@@ -7674,17 +8764,20 @@ var init_record_observation = __esm({
|
|
|
7674
8764
|
function recordTransaction(db, input) {
|
|
7675
8765
|
return withTransaction(db, () => {
|
|
7676
8766
|
validateSocialEventInput(input);
|
|
8767
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
8768
|
+
if (existing) return existing;
|
|
7677
8769
|
const ownerId = requireOwnerContactId(db);
|
|
7678
8770
|
const ids = input.participants.map((p) => p.contactId);
|
|
7679
8771
|
assertParticipantContactsLive(db, ids);
|
|
7680
8772
|
assertOwnerParticipates(ownerId, ids);
|
|
7681
8773
|
const now = resolveNow(input.now);
|
|
7682
8774
|
const eventId = insertJournalEvent(db, {
|
|
8775
|
+
...input,
|
|
7683
8776
|
type: "transaction",
|
|
7684
8777
|
occurredAt: input.occurredAt,
|
|
7685
|
-
|
|
8778
|
+
occurredOn: input.occurredOn,
|
|
7686
8779
|
significance: input.significance,
|
|
7687
|
-
momentKind: null,
|
|
8780
|
+
momentKind: input.momentKind ?? null,
|
|
7688
8781
|
participants: input.participants,
|
|
7689
8782
|
now
|
|
7690
8783
|
});
|
|
@@ -7692,12 +8785,24 @@ function recordTransaction(db, input) {
|
|
|
7692
8785
|
eventId,
|
|
7693
8786
|
eventType: "transaction",
|
|
7694
8787
|
occurredAt: input.occurredAt,
|
|
8788
|
+
occurredOn: input.occurredOn,
|
|
7695
8789
|
significance: input.significance,
|
|
7696
8790
|
participants: input.participants,
|
|
7697
8791
|
provenance: input.provenance,
|
|
7698
8792
|
now
|
|
7699
8793
|
});
|
|
8794
|
+
if (input.momentKind !== void 0)
|
|
8795
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
8796
|
+
input.momentKind,
|
|
8797
|
+
eventId
|
|
8798
|
+
);
|
|
7700
8799
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
8800
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
8801
|
+
{
|
|
8802
|
+
code: "future_event",
|
|
8803
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
8804
|
+
}
|
|
8805
|
+
] : void 0,
|
|
7701
8806
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
7702
8807
|
affectedLinks: mechanics.affectedLinks,
|
|
7703
8808
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -7709,20 +8814,24 @@ var init_record_transaction = __esm({
|
|
|
7709
8814
|
init_queries();
|
|
7710
8815
|
init_loaders();
|
|
7711
8816
|
init_persistence();
|
|
7712
|
-
|
|
8817
|
+
init_receipts();
|
|
7713
8818
|
init_validators();
|
|
8819
|
+
init_parse_calendar_time();
|
|
7714
8820
|
init_resolve_now();
|
|
7715
8821
|
init_with_transaction();
|
|
7716
8822
|
init_apply_journal_event_mechanics();
|
|
8823
|
+
init_event_metadata();
|
|
7717
8824
|
}
|
|
7718
8825
|
});
|
|
7719
8826
|
|
|
7720
8827
|
// src/tools/record_event_tool.ts
|
|
7721
|
-
function normalizeParticipant2(p) {
|
|
8828
|
+
function normalizeParticipant2(db, p) {
|
|
7722
8829
|
let contactId = p.contactId;
|
|
7723
8830
|
if (contactId === void 0 && p.contact != null) {
|
|
7724
8831
|
const loc = p.contact;
|
|
7725
|
-
|
|
8832
|
+
const resolved = resolveContactLocator(db, loc);
|
|
8833
|
+
if (!resolved.ok) throw resolved.result;
|
|
8834
|
+
contactId = resolved.value.id;
|
|
7726
8835
|
}
|
|
7727
8836
|
const result = {
|
|
7728
8837
|
contactId,
|
|
@@ -7740,12 +8849,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
7740
8849
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
7741
8850
|
}
|
|
7742
8851
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
7743
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
8852
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
7744
8853
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
7745
8854
|
}
|
|
7746
8855
|
function recordEventToolHandler(db, rawInput) {
|
|
7747
8856
|
const input = structuredClone(rawInput);
|
|
7748
|
-
const inputError = validateToolInput(
|
|
8857
|
+
const inputError = validateToolInput(
|
|
8858
|
+
recordEventTool.inputSchema,
|
|
8859
|
+
input,
|
|
8860
|
+
"input",
|
|
8861
|
+
Date.now(),
|
|
8862
|
+
true
|
|
8863
|
+
);
|
|
7749
8864
|
if (inputError)
|
|
7750
8865
|
return toolFailure(
|
|
7751
8866
|
"protocol",
|
|
@@ -7798,6 +8913,7 @@ var init_record_event_tool = __esm({
|
|
|
7798
8913
|
init_record_milestone();
|
|
7799
8914
|
init_record_observation();
|
|
7800
8915
|
init_record_transaction();
|
|
8916
|
+
init_event_metadata_schema();
|
|
7801
8917
|
init_tool_metadata();
|
|
7802
8918
|
init_tool_mutation();
|
|
7803
8919
|
init_tool_names();
|
|
@@ -7839,6 +8955,7 @@ var init_record_event_tool = __esm({
|
|
|
7839
8955
|
"correction"
|
|
7840
8956
|
]
|
|
7841
8957
|
),
|
|
8958
|
+
...eventMetadataSchema,
|
|
7842
8959
|
occurredAt: integerSchema(
|
|
7843
8960
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
7844
8961
|
),
|
|
@@ -7847,6 +8964,19 @@ var init_record_event_tool = __esm({
|
|
|
7847
8964
|
participants: arraySchema(
|
|
7848
8965
|
objectSchema(
|
|
7849
8966
|
{
|
|
8967
|
+
contact: objectSchema(
|
|
8968
|
+
{
|
|
8969
|
+
contactId: integerSchema("Contact id."),
|
|
8970
|
+
identity: objectSchema(
|
|
8971
|
+
{
|
|
8972
|
+
type: stringSchema("Identity type."),
|
|
8973
|
+
value: stringSchema("Identity value.")
|
|
8974
|
+
},
|
|
8975
|
+
["type", "value"]
|
|
8976
|
+
)
|
|
8977
|
+
},
|
|
8978
|
+
[]
|
|
8979
|
+
),
|
|
7850
8980
|
contactId: integerSchema(
|
|
7851
8981
|
"Participant contact id. The owner contact MUST be included."
|
|
7852
8982
|
),
|
|
@@ -7870,7 +9000,7 @@ var init_record_event_tool = __esm({
|
|
|
7870
9000
|
),
|
|
7871
9001
|
now: integerSchema("Optional timestamp override.")
|
|
7872
9002
|
},
|
|
7873
|
-
["occurredAt", "
|
|
9003
|
+
["occurredAt", "significance", "participants"],
|
|
7874
9004
|
"Event payload. Always required. Interaction events also require `type`."
|
|
7875
9005
|
)
|
|
7876
9006
|
},
|
|
@@ -7888,7 +9018,10 @@ function contactAttributeClauses(filters) {
|
|
|
7888
9018
|
const params = [];
|
|
7889
9019
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
7890
9020
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
7891
|
-
for (const filter of [
|
|
9021
|
+
for (const filter of [
|
|
9022
|
+
...attributes,
|
|
9023
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
9024
|
+
]) {
|
|
7892
9025
|
const separator = filter.indexOf("=");
|
|
7893
9026
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
7894
9027
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -7949,143 +9082,16 @@ function listContacts(db, filters, options) {
|
|
|
7949
9082
|
const item = mapContactRowToContactListItem(r);
|
|
7950
9083
|
return {
|
|
7951
9084
|
...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);
|
|
9085
|
+
primaryIdentity: r.primary_identity ?? null
|
|
9086
|
+
};
|
|
9087
|
+
});
|
|
8084
9088
|
}
|
|
8085
|
-
var
|
|
8086
|
-
"src/contacts/
|
|
9089
|
+
var init_list_contacts = __esm({
|
|
9090
|
+
"src/contacts/list_contacts.ts"() {
|
|
8087
9091
|
init_assert_default_ordering();
|
|
8088
9092
|
init_resolve_list_limit_offset();
|
|
9093
|
+
init_contact_attribute_clauses();
|
|
9094
|
+
init_mappers4();
|
|
8089
9095
|
}
|
|
8090
9096
|
});
|
|
8091
9097
|
|
|
@@ -8113,12 +9119,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
8113
9119
|
}
|
|
8114
9120
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
8115
9121
|
const params = [];
|
|
8116
|
-
const since = options?.since ??
|
|
9122
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
8117
9123
|
clauses.push("uo.occurs_on >= ?");
|
|
8118
9124
|
params.push(since);
|
|
8119
9125
|
let maxOccurs = options?.until;
|
|
8120
9126
|
if (filters?.horizonDays !== void 0) {
|
|
8121
|
-
const anchor =
|
|
9127
|
+
const anchor = since;
|
|
8122
9128
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
8123
9129
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
8124
9130
|
}
|
|
@@ -8170,9 +9176,9 @@ var init_list_upcoming_dates = __esm({
|
|
|
8170
9176
|
init_mappers();
|
|
8171
9177
|
init_validators();
|
|
8172
9178
|
init_affinity_validation_error();
|
|
9179
|
+
init_parse_calendar_time();
|
|
8173
9180
|
init_assert_default_ordering();
|
|
8174
9181
|
init_resolve_list_limit_offset();
|
|
8175
|
-
init_calendar();
|
|
8176
9182
|
CONTACT_KINDS = /* @__PURE__ */ new Set([
|
|
8177
9183
|
"human",
|
|
8178
9184
|
"group",
|
|
@@ -8185,6 +9191,79 @@ var init_list_upcoming_dates = __esm({
|
|
|
8185
9191
|
}
|
|
8186
9192
|
});
|
|
8187
9193
|
|
|
9194
|
+
// src/events/event_discovery.ts
|
|
9195
|
+
function searchEvents(db, query, options) {
|
|
9196
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
9197
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
9198
|
+
if (!tokens.length) return [];
|
|
9199
|
+
const rows = db.prepare(
|
|
9200
|
+
"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"
|
|
9201
|
+
).all();
|
|
9202
|
+
return rows.flatMap((row) => {
|
|
9203
|
+
const fields = {
|
|
9204
|
+
summary: decodeEventText(row.summary),
|
|
9205
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
9206
|
+
source: row.external_source,
|
|
9207
|
+
id: row.external_id,
|
|
9208
|
+
channel: row.channel
|
|
9209
|
+
};
|
|
9210
|
+
const folded = Object.entries(fields).map(
|
|
9211
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
9212
|
+
);
|
|
9213
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
9214
|
+
return [];
|
|
9215
|
+
return [
|
|
9216
|
+
{
|
|
9217
|
+
kind: "event",
|
|
9218
|
+
id: row.id,
|
|
9219
|
+
type: row.type,
|
|
9220
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
9221
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
9222
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
9223
|
+
...row.external_source && row.external_id ? {
|
|
9224
|
+
externalRef: {
|
|
9225
|
+
source: row.external_source,
|
|
9226
|
+
id: row.external_id
|
|
9227
|
+
}
|
|
9228
|
+
} : {}
|
|
9229
|
+
}
|
|
9230
|
+
];
|
|
9231
|
+
}).slice(offset, offset + limit);
|
|
9232
|
+
}
|
|
9233
|
+
function getEventThread(db, source, id, options) {
|
|
9234
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
9235
|
+
const root = getEventByExternalRef(db, source, id);
|
|
9236
|
+
if (!root) return [];
|
|
9237
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
9238
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
9239
|
+
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=?))
|
|
9240
|
+
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) AND (? IS NULL OR e.type IN (SELECT value FROM json_each(?))) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
9241
|
+
root.id,
|
|
9242
|
+
root.externalRef?.source ?? null,
|
|
9243
|
+
root.externalRef?.id ?? null,
|
|
9244
|
+
options?.channel ?? null,
|
|
9245
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
9246
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
9247
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
9248
|
+
limit,
|
|
9249
|
+
offset
|
|
9250
|
+
);
|
|
9251
|
+
return batchLoadEventRecords(
|
|
9252
|
+
db,
|
|
9253
|
+
rows.map((r) => r.id),
|
|
9254
|
+
options?.includeContent ?? false
|
|
9255
|
+
);
|
|
9256
|
+
}
|
|
9257
|
+
var init_event_discovery = __esm({
|
|
9258
|
+
"src/events/event_discovery.ts"() {
|
|
9259
|
+
init_fold_search_text();
|
|
9260
|
+
init_resolve_list_limit_offset();
|
|
9261
|
+
init_event_metadata();
|
|
9262
|
+
init_loaders();
|
|
9263
|
+
init_mappers();
|
|
9264
|
+
}
|
|
9265
|
+
});
|
|
9266
|
+
|
|
8188
9267
|
// src/graph/get_affinity_chart.ts
|
|
8189
9268
|
function getAffinityChart(db, options) {
|
|
8190
9269
|
const contactClauses = [
|
|
@@ -8397,21 +9476,6 @@ var init_list_progression_readiness = __esm({
|
|
|
8397
9476
|
});
|
|
8398
9477
|
|
|
8399
9478
|
// 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
9479
|
function listRadar(db, filters, options) {
|
|
8416
9480
|
assertDefaultOrdering("listRadar", options);
|
|
8417
9481
|
const ownerId = findOwnerContactId(db);
|
|
@@ -8419,7 +9483,10 @@ function listRadar(db, filters, options) {
|
|
|
8419
9483
|
return [];
|
|
8420
9484
|
}
|
|
8421
9485
|
const now = resolveNow(options?.now);
|
|
8422
|
-
const
|
|
9486
|
+
const today = localCalendarDate(now);
|
|
9487
|
+
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}'))`;
|
|
9488
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
9489
|
+
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
9490
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
8424
9491
|
const clauses = [
|
|
8425
9492
|
"l.removed_at IS NULL",
|
|
@@ -8444,10 +9511,23 @@ function listRadar(db, filters, options) {
|
|
|
8444
9511
|
if (options?.includeObserved === false) {
|
|
8445
9512
|
clauses.push("l.kind != 'observed'");
|
|
8446
9513
|
}
|
|
9514
|
+
if (options?.includeDormant !== true)
|
|
9515
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
9516
|
+
if (filters?.includeLost !== true)
|
|
9517
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
9518
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
9519
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
9520
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
9521
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
9522
|
+
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'))
|
|
9523
|
+
))`);
|
|
8447
9524
|
const where = clauses.join(" AND ");
|
|
8448
9525
|
const rows = db.prepare(
|
|
8449
9526
|
`SELECT
|
|
8450
9527
|
l.id,
|
|
9528
|
+
l.cadence_days,
|
|
9529
|
+
${lastContact} AS last_contact,
|
|
9530
|
+
${drift} AS drift,
|
|
8451
9531
|
${overdue} AS overdue_commitment,
|
|
8452
9532
|
l.from_contact_id,
|
|
8453
9533
|
l.to_contact_id,
|
|
@@ -8461,30 +9541,53 @@ function listRadar(db, filters, options) {
|
|
|
8461
9541
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
8462
9542
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
8463
9543
|
WHERE ${where}
|
|
8464
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
9544
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
8465
9545
|
LIMIT ? OFFSET ?`
|
|
8466
9546
|
).all(...params, limit, offset);
|
|
8467
|
-
return rows.map((row) =>
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
9547
|
+
return rows.map((row) => {
|
|
9548
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
9549
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
9550
|
+
`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`
|
|
9551
|
+
).get(contactId, today, now) : void 0;
|
|
9552
|
+
const reasons = [
|
|
9553
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
9554
|
+
...row.drift ? ["drift"] : []
|
|
9555
|
+
];
|
|
9556
|
+
const snippets = [
|
|
9557
|
+
...promise ? [
|
|
9558
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
9559
|
+
] : [],
|
|
9560
|
+
...row.drift ? [
|
|
9561
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
9562
|
+
] : []
|
|
9563
|
+
];
|
|
9564
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
9565
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
9566
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
9567
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
9568
|
+
trust: row.trust ?? 0,
|
|
9569
|
+
rank: row.rank ?? 0
|
|
9570
|
+
});
|
|
9571
|
+
return {
|
|
9572
|
+
linkId: row.id,
|
|
9573
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
9574
|
+
reasons,
|
|
9575
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
9576
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
9577
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
9578
|
+
trust: row.trust ?? 0,
|
|
9579
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
9580
|
+
};
|
|
9581
|
+
});
|
|
8481
9582
|
}
|
|
8482
9583
|
var init_list_radar = __esm({
|
|
8483
9584
|
"src/links/list_radar.ts"() {
|
|
8484
9585
|
init_queries();
|
|
9586
|
+
init_parse_calendar_time();
|
|
8485
9587
|
init_assert_default_ordering();
|
|
8486
9588
|
init_resolve_list_limit_offset();
|
|
8487
9589
|
init_resolve_now();
|
|
9590
|
+
init_mechanics();
|
|
8488
9591
|
}
|
|
8489
9592
|
});
|
|
8490
9593
|
|
|
@@ -8567,13 +9670,40 @@ function enrichReviewItem(db, item) {
|
|
|
8567
9670
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8568
9671
|
}
|
|
8569
9672
|
if (Array.isArray(item.participants)) {
|
|
8570
|
-
result.
|
|
8571
|
-
(p) =>
|
|
9673
|
+
result.participants = item.participants.map(
|
|
9674
|
+
(p) => ({
|
|
9675
|
+
id: p.contactId,
|
|
9676
|
+
name: name(p.contactId),
|
|
9677
|
+
role: p.role
|
|
9678
|
+
})
|
|
8572
9679
|
);
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
9680
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
9681
|
+
result[key] = void 0;
|
|
9682
|
+
}
|
|
9683
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
9684
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
9685
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
9686
|
+
if (result.title === result.summary) result.title = void 0;
|
|
9687
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
9688
|
+
for (const key of [
|
|
9689
|
+
"affinity",
|
|
9690
|
+
"trust",
|
|
9691
|
+
"rank",
|
|
9692
|
+
"recencyScore",
|
|
9693
|
+
"driftPriority",
|
|
9694
|
+
"readinessScore",
|
|
9695
|
+
"normalizedRank",
|
|
9696
|
+
"reciprocityScore",
|
|
9697
|
+
"radarScore",
|
|
9698
|
+
"subtitle",
|
|
9699
|
+
"snippet",
|
|
9700
|
+
"targetRef",
|
|
9701
|
+
"occursOnDate"
|
|
9702
|
+
])
|
|
9703
|
+
result[key] = void 0;
|
|
9704
|
+
} else if (item.kind === "link") {
|
|
9705
|
+
result.recommendedReason = void 0;
|
|
9706
|
+
result.snippet = void 0;
|
|
8577
9707
|
}
|
|
8578
9708
|
if (typeof item.leftContactId === "number")
|
|
8579
9709
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -8610,6 +9740,8 @@ var init_tool_summary = __esm({
|
|
|
8610
9740
|
// src/tools/review_affinity_tool.ts
|
|
8611
9741
|
function buildReadOptions(input) {
|
|
8612
9742
|
return {
|
|
9743
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
9744
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
8613
9745
|
...input.now === void 0 ? {} : { now: input.now },
|
|
8614
9746
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
8615
9747
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -8746,7 +9878,7 @@ function momentItem(moment) {
|
|
|
8746
9878
|
id: moment.eventId,
|
|
8747
9879
|
title: moment.summary,
|
|
8748
9880
|
subtitle: moment.momentKind,
|
|
8749
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
9881
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8750
9882
|
};
|
|
8751
9883
|
}
|
|
8752
9884
|
function commitmentItem(commitment) {
|
|
@@ -8756,7 +9888,7 @@ function commitmentItem(commitment) {
|
|
|
8756
9888
|
id: commitment.eventId,
|
|
8757
9889
|
title: commitment.summary,
|
|
8758
9890
|
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)}`
|
|
9891
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8760
9892
|
};
|
|
8761
9893
|
}
|
|
8762
9894
|
function radarItem(radar) {
|
|
@@ -8772,6 +9904,7 @@ function radarItem(radar) {
|
|
|
8772
9904
|
function upcomingDateItem(date) {
|
|
8773
9905
|
return {
|
|
8774
9906
|
...date,
|
|
9907
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
8775
9908
|
occursAt: date.occursOn,
|
|
8776
9909
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
8777
9910
|
kind: date.targetRef.kind,
|
|
@@ -8809,6 +9942,9 @@ function compactFilters(input) {
|
|
|
8809
9942
|
);
|
|
8810
9943
|
}
|
|
8811
9944
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
9945
|
+
return reviewPage(db, rawInput, false);
|
|
9946
|
+
}
|
|
9947
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
8812
9948
|
const input = structuredClone(rawInput);
|
|
8813
9949
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8814
9950
|
if (inputError)
|
|
@@ -8823,6 +9959,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8823
9959
|
if (invalid) {
|
|
8824
9960
|
return invalid;
|
|
8825
9961
|
}
|
|
9962
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
9963
|
+
const limit = input.limit ?? 50;
|
|
9964
|
+
const offset = input.offset ?? 0;
|
|
9965
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
|
|
9966
|
+
return toolFailure(
|
|
9967
|
+
"protocol",
|
|
9968
|
+
"invalid_input",
|
|
9969
|
+
"Invalid pagination.",
|
|
9970
|
+
"limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
|
|
9971
|
+
);
|
|
9972
|
+
const all = [];
|
|
9973
|
+
let page;
|
|
9974
|
+
do {
|
|
9975
|
+
page = reviewPage(
|
|
9976
|
+
db,
|
|
9977
|
+
{ ...input, limit: 250, offset: all.length },
|
|
9978
|
+
true
|
|
9979
|
+
);
|
|
9980
|
+
if (!page.ok) return page;
|
|
9981
|
+
all.push(...page.data.items);
|
|
9982
|
+
} while (page.data.items.length === 250);
|
|
9983
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9984
|
+
return toolSuccess(
|
|
9985
|
+
summarizeCount(items2.length, "item"),
|
|
9986
|
+
{
|
|
9987
|
+
view: input.view,
|
|
9988
|
+
items: items2,
|
|
9989
|
+
count: items2.length,
|
|
9990
|
+
total: all.length,
|
|
9991
|
+
hasMore: offset + items2.length < all.length,
|
|
9992
|
+
appliedFilters: compactFilters(input)
|
|
9993
|
+
},
|
|
9994
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
9995
|
+
);
|
|
9996
|
+
}
|
|
8826
9997
|
const options = buildReadOptions(input);
|
|
8827
9998
|
let items = [];
|
|
8828
9999
|
let chart;
|
|
@@ -8848,12 +10019,26 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8848
10019
|
{
|
|
8849
10020
|
...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
|
|
8850
10021
|
...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
|
|
8851
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
10022
|
+
...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
|
|
8852
10023
|
...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
|
|
8853
10024
|
},
|
|
8854
10025
|
options
|
|
8855
10026
|
).map(duplicateCandidateItem);
|
|
8856
10027
|
break;
|
|
10028
|
+
case "events.thread": {
|
|
10029
|
+
if (!input.ref)
|
|
10030
|
+
return toolFailure(
|
|
10031
|
+
"protocol",
|
|
10032
|
+
"invalid_input",
|
|
10033
|
+
"Missing thread reference.",
|
|
10034
|
+
"ref is required for events.thread."
|
|
10035
|
+
);
|
|
10036
|
+
items = getEventThread(db, input.ref.source, input.ref.id, {
|
|
10037
|
+
...options,
|
|
10038
|
+
...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
|
|
10039
|
+
}).map(toEventListToolItem);
|
|
10040
|
+
break;
|
|
10041
|
+
}
|
|
8857
10042
|
case "events.contact_journal": {
|
|
8858
10043
|
const locator = input.contact;
|
|
8859
10044
|
if (locator === void 0) {
|
|
@@ -8995,6 +10180,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8995
10180
|
items = listRadar(
|
|
8996
10181
|
db,
|
|
8997
10182
|
{
|
|
10183
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8998
10184
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8999
10185
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
9000
10186
|
},
|
|
@@ -9044,6 +10230,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9044
10230
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
9045
10231
|
break;
|
|
9046
10232
|
}
|
|
10233
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
10234
|
+
items = items.map((item) => {
|
|
10235
|
+
const content = getEventById(
|
|
10236
|
+
db,
|
|
10237
|
+
Number(item.eventId ?? item.id)
|
|
10238
|
+
)?.content;
|
|
10239
|
+
return content === void 0 ? item : { ...item, content };
|
|
10240
|
+
});
|
|
9047
10241
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
9048
10242
|
return toolSuccess(
|
|
9049
10243
|
summarizeCount(items.length, "item"),
|
|
@@ -9055,7 +10249,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9055
10249
|
...chart === void 0 ? {} : { chart }
|
|
9056
10250
|
},
|
|
9057
10251
|
{
|
|
9058
|
-
entities: items,
|
|
10252
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
9059
10253
|
warnings: items.length === 0 ? [
|
|
9060
10254
|
toolWarning(
|
|
9061
10255
|
"empty_result",
|
|
@@ -9072,6 +10266,8 @@ var init_review_affinity_tool = __esm({
|
|
|
9072
10266
|
init_list_contacts();
|
|
9073
10267
|
init_list_duplicate_candidates();
|
|
9074
10268
|
init_list_upcoming_dates();
|
|
10269
|
+
init_event_discovery();
|
|
10270
|
+
init_event_metadata();
|
|
9075
10271
|
init_get_contact_journal();
|
|
9076
10272
|
init_list_moments();
|
|
9077
10273
|
init_list_open_commitments();
|
|
@@ -9084,6 +10280,7 @@ var init_review_affinity_tool = __esm({
|
|
|
9084
10280
|
init_get_merge_history();
|
|
9085
10281
|
init_list_dismissed_duplicates();
|
|
9086
10282
|
init_enrich_review_item();
|
|
10283
|
+
init_event_metadata_schema();
|
|
9087
10284
|
init_tool_metadata();
|
|
9088
10285
|
init_tool_names();
|
|
9089
10286
|
init_tool_ref();
|
|
@@ -9127,15 +10324,21 @@ var init_review_affinity_tool = __esm({
|
|
|
9127
10324
|
includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
|
|
9128
10325
|
since: "Optional lower timestamp bound for time-based views.",
|
|
9129
10326
|
until: "Optional upper timestamp bound for time-based views.",
|
|
9130
|
-
limit: "Optional result limit.",
|
|
10327
|
+
limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
|
|
9131
10328
|
offset: "Optional result offset."
|
|
9132
10329
|
},
|
|
9133
10330
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
9134
10331
|
inputSchema: objectSchema(
|
|
9135
10332
|
{
|
|
10333
|
+
ref: externalRefSchema,
|
|
10334
|
+
includeContent: booleanSchema(
|
|
10335
|
+
"Include verbatim content in event rows; defaults to false."
|
|
10336
|
+
),
|
|
10337
|
+
channel: stringSchema("Filter events by channel."),
|
|
9136
10338
|
view: enumSchema("Which review surface to return.", [
|
|
9137
10339
|
"contacts.list",
|
|
9138
10340
|
"contacts.duplicates",
|
|
10341
|
+
"events.thread",
|
|
9139
10342
|
"events.contact_journal",
|
|
9140
10343
|
"events.link_timeline",
|
|
9141
10344
|
"events.moments",
|
|
@@ -9190,7 +10393,20 @@ var init_review_affinity_tool = __esm({
|
|
|
9190
10393
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
9191
10394
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
9192
10395
|
eventTypes: arraySchema(
|
|
9193
|
-
|
|
10396
|
+
enumSchema("Event type.", [
|
|
10397
|
+
"conversation",
|
|
10398
|
+
"activity",
|
|
10399
|
+
"gift",
|
|
10400
|
+
"support",
|
|
10401
|
+
"milestone",
|
|
10402
|
+
"observation",
|
|
10403
|
+
"conflict",
|
|
10404
|
+
"correction",
|
|
10405
|
+
"transaction",
|
|
10406
|
+
"promise",
|
|
10407
|
+
"agreement",
|
|
10408
|
+
"date_anchor"
|
|
10409
|
+
]),
|
|
9194
10410
|
"Optional event-type filter."
|
|
9195
10411
|
),
|
|
9196
10412
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -9213,13 +10429,17 @@ var init_review_affinity_tool = __esm({
|
|
|
9213
10429
|
]),
|
|
9214
10430
|
horizonDays: integerSchema("Optional horizon-days filter."),
|
|
9215
10431
|
exactOnly: booleanSchema("Whether duplicate review is exact-only."),
|
|
9216
|
-
minScore: numberSchema(
|
|
10432
|
+
minScore: numberSchema(
|
|
10433
|
+
"Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
|
|
10434
|
+
),
|
|
9217
10435
|
includeDismissed: booleanSchema(
|
|
9218
10436
|
"Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
|
|
9219
10437
|
),
|
|
9220
10438
|
since: integerSchema("Optional lower timestamp bound."),
|
|
9221
10439
|
until: integerSchema("Optional upper timestamp bound."),
|
|
9222
|
-
limit: integerSchema(
|
|
10440
|
+
limit: integerSchema(
|
|
10441
|
+
"Optional result limit, 1..10000 (default 50); use offset for paging."
|
|
10442
|
+
),
|
|
9223
10443
|
offset: integerSchema("Optional result offset.")
|
|
9224
10444
|
},
|
|
9225
10445
|
["view"],
|
|
@@ -9232,7 +10452,7 @@ var init_review_affinity_tool = __esm({
|
|
|
9232
10452
|
|
|
9233
10453
|
// src/contacts/search_contacts.ts
|
|
9234
10454
|
function searchContacts(db, query, filters, options) {
|
|
9235
|
-
const q = foldSearchText(query.trim());
|
|
10455
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
9236
10456
|
if (!q) return [];
|
|
9237
10457
|
assertDefaultOrdering("searchContacts", options);
|
|
9238
10458
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -9252,6 +10472,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
9252
10472
|
const tokens = q.split(/\s+/);
|
|
9253
10473
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
9254
10474
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
10475
|
+
const suffix = digits.replace(/^0/, "");
|
|
9255
10476
|
const attributeFilters = contactAttributeClauses(filters);
|
|
9256
10477
|
const clauses = [
|
|
9257
10478
|
"deleted_at IS NULL",
|
|
@@ -9287,7 +10508,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
9287
10508
|
)
|
|
9288
10509
|
];
|
|
9289
10510
|
const phoneFields = phone ? values.filter(
|
|
9290
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
10511
|
+
(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
10512
|
) : [];
|
|
9292
10513
|
if (!phoneFields.length && !tokens.every(
|
|
9293
10514
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -9326,6 +10547,9 @@ var init_search_contacts = __esm({
|
|
|
9326
10547
|
|
|
9327
10548
|
// src/tools/search_affinity_tool.ts
|
|
9328
10549
|
function searchAffinityToolHandler(db, rawInput) {
|
|
10550
|
+
return searchPage(db, rawInput, false);
|
|
10551
|
+
}
|
|
10552
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
9329
10553
|
const input = structuredClone(rawInput);
|
|
9330
10554
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9331
10555
|
if (inputError)
|
|
@@ -9336,11 +10560,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9336
10560
|
inputError
|
|
9337
10561
|
);
|
|
9338
10562
|
return withToolHandling(() => {
|
|
10563
|
+
if (!pageOnly) {
|
|
10564
|
+
const limit = input.limit ?? 50;
|
|
10565
|
+
const offset = input.offset ?? 0;
|
|
10566
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
10567
|
+
return toolFailure(
|
|
10568
|
+
"protocol",
|
|
10569
|
+
"invalid_input",
|
|
10570
|
+
"Invalid pagination.",
|
|
10571
|
+
"limit must be 1..250; offset must be non-negative"
|
|
10572
|
+
);
|
|
10573
|
+
const all = [];
|
|
10574
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
10575
|
+
for (const scope of scopes) {
|
|
10576
|
+
let page;
|
|
10577
|
+
let nextOffset = 0;
|
|
10578
|
+
do {
|
|
10579
|
+
page = searchPage(
|
|
10580
|
+
db,
|
|
10581
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
10582
|
+
true
|
|
10583
|
+
);
|
|
10584
|
+
if (!page.ok) return page;
|
|
10585
|
+
all.push(...page.data.items);
|
|
10586
|
+
nextOffset += page.data.count;
|
|
10587
|
+
} while (page.data.count === 250);
|
|
10588
|
+
}
|
|
10589
|
+
const items2 = all.slice(offset, offset + limit);
|
|
10590
|
+
return toolSuccess(
|
|
10591
|
+
summarizeCount(
|
|
10592
|
+
items2.length,
|
|
10593
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10594
|
+
),
|
|
10595
|
+
{
|
|
10596
|
+
query: input.query,
|
|
10597
|
+
items: items2,
|
|
10598
|
+
count: items2.length,
|
|
10599
|
+
total: all.length,
|
|
10600
|
+
hasMore: offset + items2.length < all.length
|
|
10601
|
+
},
|
|
10602
|
+
{ entities: items2 }
|
|
10603
|
+
);
|
|
10604
|
+
}
|
|
9339
10605
|
const options = {
|
|
9340
10606
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
9341
10607
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
9342
10608
|
};
|
|
9343
|
-
const rows = searchContacts(
|
|
10609
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
9344
10610
|
db,
|
|
9345
10611
|
input.query,
|
|
9346
10612
|
{
|
|
@@ -9354,9 +10620,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9354
10620
|
},
|
|
9355
10621
|
options
|
|
9356
10622
|
);
|
|
9357
|
-
const items =
|
|
10623
|
+
const items = [
|
|
10624
|
+
...rows.map(toContactListToolItem),
|
|
10625
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
10626
|
+
];
|
|
9358
10627
|
return toolSuccess(
|
|
9359
|
-
summarizeCount(
|
|
10628
|
+
summarizeCount(
|
|
10629
|
+
items.length,
|
|
10630
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10631
|
+
),
|
|
9360
10632
|
{
|
|
9361
10633
|
query: input.query,
|
|
9362
10634
|
items,
|
|
@@ -9372,6 +10644,7 @@ var searchAffinityTool;
|
|
|
9372
10644
|
var init_search_affinity_tool = __esm({
|
|
9373
10645
|
"src/tools/search_affinity_tool.ts"() {
|
|
9374
10646
|
init_search_contacts();
|
|
10647
|
+
init_event_discovery();
|
|
9375
10648
|
init_tool_metadata();
|
|
9376
10649
|
init_tool_names();
|
|
9377
10650
|
init_tool_ref();
|
|
@@ -9404,6 +10677,11 @@ var init_search_affinity_tool = __esm({
|
|
|
9404
10677
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
9405
10678
|
inputSchema: objectSchema(
|
|
9406
10679
|
{
|
|
10680
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
10681
|
+
"contacts",
|
|
10682
|
+
"events",
|
|
10683
|
+
"all"
|
|
10684
|
+
]),
|
|
9407
10685
|
query: stringSchema("Free-text or natural-key query."),
|
|
9408
10686
|
kind: enumSchema("Optional contact kind filter.", [
|
|
9409
10687
|
"human",
|
|
@@ -9454,6 +10732,7 @@ var init_tool_registry = __esm({
|
|
|
9454
10732
|
init_manage_contact_tool();
|
|
9455
10733
|
init_manage_date_anchor_tool();
|
|
9456
10734
|
init_manage_duplicate_dismissal_tool();
|
|
10735
|
+
init_manage_event_tool();
|
|
9457
10736
|
init_manage_identity_tool();
|
|
9458
10737
|
init_manage_relationship_tool();
|
|
9459
10738
|
init_merge_contacts_tool();
|
|
@@ -9472,77 +10751,50 @@ var init_tool_registry = __esm({
|
|
|
9472
10751
|
recordEventTool,
|
|
9473
10752
|
manageCommitmentTool,
|
|
9474
10753
|
manageDateAnchorTool,
|
|
9475
|
-
manageAttributeTool
|
|
10754
|
+
manageAttributeTool,
|
|
10755
|
+
manageEventTool
|
|
9476
10756
|
];
|
|
9477
10757
|
}
|
|
9478
10758
|
});
|
|
9479
10759
|
|
|
9480
10760
|
// 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()) {
|
|
10761
|
+
function normalizeTimeInputs(value, now = Date.now(), inPatch = false) {
|
|
9534
10762
|
if (Array.isArray(value))
|
|
9535
10763
|
return value.map((item) => normalizeTimeInputs(item, now));
|
|
9536
10764
|
if (!value || typeof value !== "object") return value;
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
10765
|
+
const result = {};
|
|
10766
|
+
for (const [key, item] of Object.entries(value)) {
|
|
10767
|
+
if (key === "occurredAt" && item === null)
|
|
10768
|
+
throw new CliUsageError(
|
|
10769
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
10770
|
+
);
|
|
10771
|
+
if (typeof item === "string" && (key.endsWith("At") || ["asOf", "now", "since", "until"].includes(key))) {
|
|
10772
|
+
let parsed;
|
|
10773
|
+
try {
|
|
10774
|
+
parsed = parseCalendarTime(item, key, now);
|
|
10775
|
+
} catch (error) {
|
|
10776
|
+
throw new CliUsageError(
|
|
10777
|
+
`Invalid timestamp "${item}". ${error.message}`
|
|
10778
|
+
);
|
|
10779
|
+
}
|
|
10780
|
+
if (!inPatch && ["occurredAt", "dueAt"].includes(key) && typeof item === "string" && /\.\d{4,}/.test(item)) {
|
|
10781
|
+
const provenance = result.provenance ?? value.provenance;
|
|
10782
|
+
result.provenance = {
|
|
10783
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
10784
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: item
|
|
10785
|
+
};
|
|
10786
|
+
}
|
|
10787
|
+
result[key] = parsed.timestamp;
|
|
10788
|
+
if (!inPatch && ["occurredAt", "dueAt"].includes(key))
|
|
10789
|
+
result[key.replace(/At$/, "On")] = parsed.day;
|
|
10790
|
+
} else if (key !== "provenance" || result.provenance === void 0)
|
|
10791
|
+
result[key] = key === "provenance" ? item : normalizeTimeInputs(item, now, key === "patch");
|
|
10792
|
+
}
|
|
10793
|
+
return result;
|
|
9543
10794
|
}
|
|
9544
10795
|
var init_plain_dates = __esm({
|
|
9545
10796
|
"src/surface/plain_dates.ts"() {
|
|
10797
|
+
init_parse_calendar_time();
|
|
9546
10798
|
init_cli_args();
|
|
9547
10799
|
}
|
|
9548
10800
|
});
|
|
@@ -9575,6 +10827,35 @@ var init_timestamp_schema = __esm({
|
|
|
9575
10827
|
});
|
|
9576
10828
|
|
|
9577
10829
|
// src/surface/affinity_surface_tools.ts
|
|
10830
|
+
function surfaceProperties(tool) {
|
|
10831
|
+
return {
|
|
10832
|
+
...tool.inputSchema.properties,
|
|
10833
|
+
...tool.name === "review_affinity" ? {
|
|
10834
|
+
includeContent: {
|
|
10835
|
+
type: "boolean",
|
|
10836
|
+
description: "Deprecated alias for --with-content; includes verbatim event content."
|
|
10837
|
+
}
|
|
10838
|
+
} : {},
|
|
10839
|
+
...["inspect_affinity_item", "review_affinity", "manage_event"].includes(
|
|
10840
|
+
tool.name
|
|
10841
|
+
) ? {
|
|
10842
|
+
refSource: {
|
|
10843
|
+
type: "string",
|
|
10844
|
+
description: "External reference source."
|
|
10845
|
+
},
|
|
10846
|
+
refId: {
|
|
10847
|
+
type: "string",
|
|
10848
|
+
description: "External reference id."
|
|
10849
|
+
}
|
|
10850
|
+
} : {},
|
|
10851
|
+
...tool.name === "review_affinity" ? {
|
|
10852
|
+
withContent: {
|
|
10853
|
+
type: "boolean",
|
|
10854
|
+
description: "Include verbatim event content."
|
|
10855
|
+
}
|
|
10856
|
+
} : {}
|
|
10857
|
+
};
|
|
10858
|
+
}
|
|
9578
10859
|
var affinitySurfaceTools;
|
|
9579
10860
|
var init_affinity_surface_tools = __esm({
|
|
9580
10861
|
"src/surface/affinity_surface_tools.ts"() {
|
|
@@ -9586,17 +10867,31 @@ var init_affinity_surface_tools = __esm({
|
|
|
9586
10867
|
affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
9587
10868
|
name: tool.name,
|
|
9588
10869
|
description: tool.description,
|
|
9589
|
-
whenToUse: tool.whenToUse,
|
|
9590
|
-
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
9591
10870
|
readOnly: tool.readOnly,
|
|
9592
10871
|
sideEffects: tool.sideEffects,
|
|
9593
10872
|
inputSchema: {
|
|
9594
|
-
...timestampSchema({
|
|
10873
|
+
...timestampSchema({
|
|
10874
|
+
...tool.inputSchema,
|
|
10875
|
+
properties: surfaceProperties(tool),
|
|
10876
|
+
type: "object"
|
|
10877
|
+
}),
|
|
9595
10878
|
type: "object"
|
|
9596
10879
|
},
|
|
9597
10880
|
handler: (db, input) => {
|
|
9598
10881
|
try {
|
|
9599
|
-
|
|
10882
|
+
const { withContent, refSource, refId, ...normalized } = input;
|
|
10883
|
+
if (withContent !== void 0) normalized.includeContent = withContent;
|
|
10884
|
+
if (refSource !== void 0 || refId !== void 0) {
|
|
10885
|
+
if (normalized.ref !== void 0)
|
|
10886
|
+
return toolFailure(
|
|
10887
|
+
"protocol",
|
|
10888
|
+
"invalid_input",
|
|
10889
|
+
"Invalid target.",
|
|
10890
|
+
"Provide ref or ref-source/ref-id, not both."
|
|
10891
|
+
);
|
|
10892
|
+
normalized.ref = { source: refSource, id: refId };
|
|
10893
|
+
}
|
|
10894
|
+
return tool.handler(db, normalizeTimeInputs(normalized));
|
|
9600
10895
|
} catch (error) {
|
|
9601
10896
|
if (error instanceof CliUsageError)
|
|
9602
10897
|
return toolFailure(
|
|
@@ -9623,7 +10918,7 @@ function affinityMcpOptions(context) {
|
|
|
9623
10918
|
context,
|
|
9624
10919
|
annotations: (tool) => ({
|
|
9625
10920
|
readOnlyHint: tool.readOnly ?? false,
|
|
9626
|
-
destructiveHint: tool.name === "merge_contacts" || /remove|unset|undismiss/.test(
|
|
10921
|
+
destructiveHint: tool.name === "merge_contacts" || tool.name === "manage_event" || /remove|unset|undismiss/.test(
|
|
9627
10922
|
String(tool.inputSchema.properties?.action?.enum ?? [])
|
|
9628
10923
|
),
|
|
9629
10924
|
idempotentHint: tool.readOnly ?? false,
|
|
@@ -9639,13 +10934,99 @@ var init_affinity_mcp_options = __esm({
|
|
|
9639
10934
|
}
|
|
9640
10935
|
});
|
|
9641
10936
|
|
|
10937
|
+
// src/events/init_events_tables.ts
|
|
10938
|
+
function initEventsTables(db) {
|
|
10939
|
+
db.exec(`
|
|
10940
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
10941
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10942
|
+
type TEXT NOT NULL,
|
|
10943
|
+
occurred_at INTEGER NOT NULL,
|
|
10944
|
+
summary TEXT NOT NULL,
|
|
10945
|
+
significance REAL NOT NULL DEFAULT 0,
|
|
10946
|
+
moment_kind TEXT,
|
|
10947
|
+
recurrence_kind TEXT,
|
|
10948
|
+
anchor_month INTEGER,
|
|
10949
|
+
anchor_day INTEGER,
|
|
10950
|
+
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
10951
|
+
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
10952
|
+
created_at INTEGER NOT NULL,
|
|
10953
|
+
updated_at INTEGER NOT NULL,
|
|
10954
|
+
deleted_at INTEGER,
|
|
10955
|
+
CHECK (length(trim(summary)) > 0),
|
|
10956
|
+
CHECK (
|
|
10957
|
+
type IN (
|
|
10958
|
+
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
10959
|
+
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
10960
|
+
)
|
|
10961
|
+
),
|
|
10962
|
+
CHECK (
|
|
10963
|
+
(type = 'date_anchor' AND (
|
|
10964
|
+
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
10965
|
+
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
10966
|
+
))
|
|
10967
|
+
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
10968
|
+
),
|
|
10969
|
+
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
10970
|
+
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
10971
|
+
)),
|
|
10972
|
+
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
10973
|
+
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
10974
|
+
)),
|
|
10975
|
+
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
10976
|
+
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
10977
|
+
)
|
|
10978
|
+
`);
|
|
10979
|
+
db.exec(
|
|
10980
|
+
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
10981
|
+
);
|
|
10982
|
+
db.exec(
|
|
10983
|
+
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
10984
|
+
);
|
|
10985
|
+
const columns = db.prepare("PRAGMA table_info(events)").all();
|
|
10986
|
+
if (!columns.some((c) => c.name === "declared_moment_kind"))
|
|
10987
|
+
db.exec("ALTER TABLE events ADD COLUMN declared_moment_kind TEXT");
|
|
10988
|
+
if (!columns.some((c) => c.name === "affects_bond"))
|
|
10989
|
+
db.exec("ALTER TABLE events ADD COLUMN affects_bond INTEGER");
|
|
10990
|
+
if (!columns.some((c) => c.name === "content"))
|
|
10991
|
+
db.exec("ALTER TABLE events ADD COLUMN content TEXT");
|
|
10992
|
+
if (!columns.some((c) => c.name === "external_source"))
|
|
10993
|
+
db.exec("ALTER TABLE events ADD COLUMN external_source TEXT");
|
|
10994
|
+
if (!columns.some((c) => c.name === "external_id"))
|
|
10995
|
+
db.exec("ALTER TABLE events ADD COLUMN external_id TEXT");
|
|
10996
|
+
if (!columns.some((c) => c.name === "channel"))
|
|
10997
|
+
db.exec("ALTER TABLE events ADD COLUMN channel TEXT");
|
|
10998
|
+
if (!columns.some((c) => c.name === "parent_source"))
|
|
10999
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_source TEXT");
|
|
11000
|
+
if (!columns.some((c) => c.name === "parent_ref_id"))
|
|
11001
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_ref_id TEXT");
|
|
11002
|
+
if (!columns.some((c) => c.name === "parent_event_id"))
|
|
11003
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_event_id INTEGER");
|
|
11004
|
+
if (!columns.some((c) => c.name === "thread_source"))
|
|
11005
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_source TEXT");
|
|
11006
|
+
if (!columns.some((c) => c.name === "thread_id"))
|
|
11007
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_id TEXT");
|
|
11008
|
+
if (!columns.some((c) => c.name === "provenance_json"))
|
|
11009
|
+
db.exec("ALTER TABLE events ADD COLUMN provenance_json TEXT");
|
|
11010
|
+
db.exec(
|
|
11011
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external_ref ON events(external_source, external_id)"
|
|
11012
|
+
);
|
|
11013
|
+
if (!columns.some((column) => column.name === "occurred_on"))
|
|
11014
|
+
db.exec("ALTER TABLE events ADD COLUMN occurred_on TEXT");
|
|
11015
|
+
}
|
|
11016
|
+
var init_init_events_tables = __esm({
|
|
11017
|
+
"src/events/init_events_tables.ts"() {
|
|
11018
|
+
}
|
|
11019
|
+
});
|
|
11020
|
+
|
|
9642
11021
|
// src/attributes/init_attributes_tables.ts
|
|
9643
11022
|
function initAttributesTables(db) {
|
|
11023
|
+
initEventsTables(db);
|
|
9644
11024
|
db.exec(`
|
|
9645
11025
|
CREATE TABLE IF NOT EXISTS attributes (
|
|
9646
11026
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9647
11027
|
contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
9648
11028
|
link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
11029
|
+
event_id INTEGER REFERENCES events (id) ON DELETE CASCADE,
|
|
9649
11030
|
name TEXT NOT NULL,
|
|
9650
11031
|
value TEXT,
|
|
9651
11032
|
created_at INTEGER NOT NULL,
|
|
@@ -9653,15 +11034,35 @@ function initAttributesTables(db) {
|
|
|
9653
11034
|
deleted_at INTEGER,
|
|
9654
11035
|
CHECK (length(trim(name)) > 0),
|
|
9655
11036
|
CHECK (
|
|
9656
|
-
(contact_id IS NOT NULL
|
|
9657
|
-
OR
|
|
9658
|
-
(contact_id IS NULL AND link_id IS NOT NULL)
|
|
11037
|
+
(contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1
|
|
9659
11038
|
)
|
|
9660
11039
|
)
|
|
9661
11040
|
`);
|
|
11041
|
+
const columns = db.prepare("PRAGMA table_info(attributes)").all();
|
|
11042
|
+
if (!columns.some((c) => c.name === "event_id")) {
|
|
11043
|
+
withTransaction(
|
|
11044
|
+
db,
|
|
11045
|
+
() => db.exec(`CREATE TABLE attributes_upgrade (
|
|
11046
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11047
|
+
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
|
|
11048
|
+
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
|
|
11049
|
+
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
|
|
11050
|
+
name TEXT NOT NULL CHECK(length(trim(name)) > 0), value TEXT,
|
|
11051
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
11052
|
+
CHECK ((contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1)
|
|
11053
|
+
);
|
|
11054
|
+
INSERT INTO attributes_upgrade (id,contact_id,link_id,name,value,created_at,updated_at,deleted_at)
|
|
11055
|
+
SELECT id,contact_id,link_id,name,value,created_at,updated_at,deleted_at FROM attributes;
|
|
11056
|
+
DROP TABLE attributes;
|
|
11057
|
+
ALTER TABLE attributes_upgrade RENAME TO attributes;`)
|
|
11058
|
+
);
|
|
11059
|
+
}
|
|
9662
11060
|
db.exec(
|
|
9663
11061
|
"CREATE INDEX IF NOT EXISTS idx_attributes_contact ON attributes(contact_id) WHERE deleted_at IS NULL"
|
|
9664
11062
|
);
|
|
11063
|
+
db.exec(
|
|
11064
|
+
"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"
|
|
11065
|
+
);
|
|
9665
11066
|
db.exec(
|
|
9666
11067
|
"CREATE INDEX IF NOT EXISTS idx_attributes_link ON attributes(link_id) WHERE deleted_at IS NULL"
|
|
9667
11068
|
);
|
|
@@ -9678,6 +11079,8 @@ function initAttributesTables(db) {
|
|
|
9678
11079
|
}
|
|
9679
11080
|
var init_init_attributes_tables = __esm({
|
|
9680
11081
|
"src/attributes/init_attributes_tables.ts"() {
|
|
11082
|
+
init_init_events_tables();
|
|
11083
|
+
init_with_transaction();
|
|
9681
11084
|
}
|
|
9682
11085
|
});
|
|
9683
11086
|
|
|
@@ -9794,60 +11197,6 @@ var init_init_event_participants_tables = __esm({
|
|
|
9794
11197
|
}
|
|
9795
11198
|
});
|
|
9796
11199
|
|
|
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
11200
|
// src/events/init_open_commitments_tables.ts
|
|
9852
11201
|
function initOpenCommitmentsTables(db) {
|
|
9853
11202
|
db.exec(`
|
|
@@ -9867,6 +11216,9 @@ function initOpenCommitmentsTables(db) {
|
|
|
9867
11216
|
)
|
|
9868
11217
|
)
|
|
9869
11218
|
`);
|
|
11219
|
+
const columns = db.prepare("PRAGMA table_info(open_commitments)").all();
|
|
11220
|
+
if (!columns.some((column) => column.name === "due_on"))
|
|
11221
|
+
db.exec("ALTER TABLE open_commitments ADD COLUMN due_on TEXT");
|
|
9870
11222
|
}
|
|
9871
11223
|
var init_init_open_commitments_tables = __esm({
|
|
9872
11224
|
"src/events/init_open_commitments_tables.ts"() {
|
|
@@ -9939,6 +11291,21 @@ function initLinkEventEffectsTables(db) {
|
|
|
9939
11291
|
db.exec(
|
|
9940
11292
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
9941
11293
|
);
|
|
11294
|
+
for (const name of ["participant_json", "provenance_json"])
|
|
11295
|
+
if (!columns.some((c) => c.name === name))
|
|
11296
|
+
db.exec(`ALTER TABLE link_event_effects ADD COLUMN ${name} TEXT`);
|
|
11297
|
+
db.exec(
|
|
11298
|
+
"CREATE TABLE IF NOT EXISTS event_link_baselines (link_id INTEGER PRIMARY KEY REFERENCES links(id) ON DELETE CASCADE, state_json TEXT NOT NULL)"
|
|
11299
|
+
);
|
|
11300
|
+
db.exec(
|
|
11301
|
+
"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)"
|
|
11302
|
+
);
|
|
11303
|
+
db.exec(`INSERT OR IGNORE INTO event_link_baselines(link_id,state_json)
|
|
11304
|
+
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)
|
|
11305
|
+
FROM links l JOIN link_event_effects e ON e.link_id=l.id GROUP BY l.id`);
|
|
11306
|
+
db.exec(
|
|
11307
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id IN (SELECT id FROM events WHERE type IN ('promise', 'agreement'))"
|
|
11308
|
+
);
|
|
9942
11309
|
db.exec(
|
|
9943
11310
|
"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
11311
|
);
|
|
@@ -10149,12 +11516,15 @@ async function openAffinityDb(path) {
|
|
|
10149
11516
|
} finally {
|
|
10150
11517
|
process.emitWarning = original;
|
|
10151
11518
|
}
|
|
10152
|
-
if (path !== ":memory:"
|
|
11519
|
+
if (path !== ":memory:" && !path.startsWith("file:")) {
|
|
11520
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
11521
|
+
if (!existsSync(path)) closeSync(openSync(path, "wx", 384));
|
|
11522
|
+
}
|
|
10153
11523
|
const db = new sqlite.DatabaseSync(path);
|
|
10154
11524
|
try {
|
|
10155
|
-
db.exec("PRAGMA
|
|
11525
|
+
db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;");
|
|
10156
11526
|
const context = db;
|
|
10157
|
-
initAffinityTables(context);
|
|
11527
|
+
withTransaction(context, () => initAffinityTables(context));
|
|
10158
11528
|
return context;
|
|
10159
11529
|
} catch (error) {
|
|
10160
11530
|
db.close();
|
|
@@ -10164,6 +11534,7 @@ async function openAffinityDb(path) {
|
|
|
10164
11534
|
var init_open_affinity_db = __esm({
|
|
10165
11535
|
"src/surface/open_affinity_db.ts"() {
|
|
10166
11536
|
init_init_affinity_tables();
|
|
11537
|
+
init_with_transaction();
|
|
10167
11538
|
}
|
|
10168
11539
|
});
|
|
10169
11540
|
function resolveAffinityDbPath(options = {}) {
|