@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/index.js
CHANGED
|
@@ -4,13 +4,130 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
+
// src/events/init_events_tables.ts
|
|
8
|
+
function initEventsTables(db) {
|
|
9
|
+
db.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
type TEXT NOT NULL,
|
|
13
|
+
occurred_at INTEGER NOT NULL,
|
|
14
|
+
summary TEXT NOT NULL,
|
|
15
|
+
significance REAL NOT NULL DEFAULT 0,
|
|
16
|
+
moment_kind TEXT,
|
|
17
|
+
recurrence_kind TEXT,
|
|
18
|
+
anchor_month INTEGER,
|
|
19
|
+
anchor_day INTEGER,
|
|
20
|
+
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
21
|
+
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
22
|
+
created_at INTEGER NOT NULL,
|
|
23
|
+
updated_at INTEGER NOT NULL,
|
|
24
|
+
deleted_at INTEGER,
|
|
25
|
+
CHECK (length(trim(summary)) > 0),
|
|
26
|
+
CHECK (
|
|
27
|
+
type IN (
|
|
28
|
+
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
29
|
+
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
30
|
+
)
|
|
31
|
+
),
|
|
32
|
+
CHECK (
|
|
33
|
+
(type = 'date_anchor' AND (
|
|
34
|
+
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
35
|
+
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
36
|
+
))
|
|
37
|
+
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
38
|
+
),
|
|
39
|
+
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
40
|
+
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
41
|
+
)),
|
|
42
|
+
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
43
|
+
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
44
|
+
)),
|
|
45
|
+
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
46
|
+
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
db.exec(
|
|
50
|
+
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
51
|
+
);
|
|
52
|
+
db.exec(
|
|
53
|
+
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
54
|
+
);
|
|
55
|
+
const columns = db.prepare("PRAGMA table_info(events)").all();
|
|
56
|
+
if (!columns.some((c) => c.name === "declared_moment_kind"))
|
|
57
|
+
db.exec("ALTER TABLE events ADD COLUMN declared_moment_kind TEXT");
|
|
58
|
+
if (!columns.some((c) => c.name === "affects_bond"))
|
|
59
|
+
db.exec("ALTER TABLE events ADD COLUMN affects_bond INTEGER");
|
|
60
|
+
if (!columns.some((c) => c.name === "content"))
|
|
61
|
+
db.exec("ALTER TABLE events ADD COLUMN content TEXT");
|
|
62
|
+
if (!columns.some((c) => c.name === "external_source"))
|
|
63
|
+
db.exec("ALTER TABLE events ADD COLUMN external_source TEXT");
|
|
64
|
+
if (!columns.some((c) => c.name === "external_id"))
|
|
65
|
+
db.exec("ALTER TABLE events ADD COLUMN external_id TEXT");
|
|
66
|
+
if (!columns.some((c) => c.name === "channel"))
|
|
67
|
+
db.exec("ALTER TABLE events ADD COLUMN channel TEXT");
|
|
68
|
+
if (!columns.some((c) => c.name === "parent_source"))
|
|
69
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_source TEXT");
|
|
70
|
+
if (!columns.some((c) => c.name === "parent_ref_id"))
|
|
71
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_ref_id TEXT");
|
|
72
|
+
if (!columns.some((c) => c.name === "parent_event_id"))
|
|
73
|
+
db.exec("ALTER TABLE events ADD COLUMN parent_event_id INTEGER");
|
|
74
|
+
if (!columns.some((c) => c.name === "thread_source"))
|
|
75
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_source TEXT");
|
|
76
|
+
if (!columns.some((c) => c.name === "thread_id"))
|
|
77
|
+
db.exec("ALTER TABLE events ADD COLUMN thread_id TEXT");
|
|
78
|
+
if (!columns.some((c) => c.name === "provenance_json"))
|
|
79
|
+
db.exec("ALTER TABLE events ADD COLUMN provenance_json TEXT");
|
|
80
|
+
db.exec(
|
|
81
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external_ref ON events(external_source, external_id)"
|
|
82
|
+
);
|
|
83
|
+
if (!columns.some((column) => column.name === "occurred_on"))
|
|
84
|
+
db.exec("ALTER TABLE events ADD COLUMN occurred_on TEXT");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/with_transaction.ts
|
|
88
|
+
var transactionDepth = /* @__PURE__ */ new WeakMap();
|
|
89
|
+
function withTransaction(db, fn) {
|
|
90
|
+
const depth = transactionDepth.get(db) ?? 0;
|
|
91
|
+
const savepoint = `affinity_${String(depth)}`;
|
|
92
|
+
if (depth === 0) {
|
|
93
|
+
db.exec("BEGIN IMMEDIATE");
|
|
94
|
+
} else {
|
|
95
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
96
|
+
}
|
|
97
|
+
transactionDepth.set(db, depth + 1);
|
|
98
|
+
try {
|
|
99
|
+
const result = fn();
|
|
100
|
+
if (depth === 0) {
|
|
101
|
+
db.exec("COMMIT");
|
|
102
|
+
} else {
|
|
103
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
104
|
+
}
|
|
105
|
+
transactionDepth.set(db, depth);
|
|
106
|
+
return result;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
try {
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
db.exec("ROLLBACK");
|
|
111
|
+
} else {
|
|
112
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
113
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
transactionDepth.set(db, depth);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
7
122
|
// src/attributes/init_attributes_tables.ts
|
|
8
123
|
function initAttributesTables(db) {
|
|
124
|
+
initEventsTables(db);
|
|
9
125
|
db.exec(`
|
|
10
126
|
CREATE TABLE IF NOT EXISTS attributes (
|
|
11
127
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
128
|
contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
13
129
|
link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
130
|
+
event_id INTEGER REFERENCES events (id) ON DELETE CASCADE,
|
|
14
131
|
name TEXT NOT NULL,
|
|
15
132
|
value TEXT,
|
|
16
133
|
created_at INTEGER NOT NULL,
|
|
@@ -18,15 +135,35 @@ function initAttributesTables(db) {
|
|
|
18
135
|
deleted_at INTEGER,
|
|
19
136
|
CHECK (length(trim(name)) > 0),
|
|
20
137
|
CHECK (
|
|
21
|
-
(contact_id IS NOT NULL
|
|
22
|
-
OR
|
|
23
|
-
(contact_id IS NULL AND link_id IS NOT NULL)
|
|
138
|
+
(contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1
|
|
24
139
|
)
|
|
25
140
|
)
|
|
26
141
|
`);
|
|
142
|
+
const columns = db.prepare("PRAGMA table_info(attributes)").all();
|
|
143
|
+
if (!columns.some((c) => c.name === "event_id")) {
|
|
144
|
+
withTransaction(
|
|
145
|
+
db,
|
|
146
|
+
() => db.exec(`CREATE TABLE attributes_upgrade (
|
|
147
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
148
|
+
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
|
|
149
|
+
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
|
|
150
|
+
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
|
|
151
|
+
name TEXT NOT NULL CHECK(length(trim(name)) > 0), value TEXT,
|
|
152
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
153
|
+
CHECK ((contact_id IS NOT NULL) + (link_id IS NOT NULL) + (event_id IS NOT NULL) = 1)
|
|
154
|
+
);
|
|
155
|
+
INSERT INTO attributes_upgrade (id,contact_id,link_id,name,value,created_at,updated_at,deleted_at)
|
|
156
|
+
SELECT id,contact_id,link_id,name,value,created_at,updated_at,deleted_at FROM attributes;
|
|
157
|
+
DROP TABLE attributes;
|
|
158
|
+
ALTER TABLE attributes_upgrade RENAME TO attributes;`)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
27
161
|
db.exec(
|
|
28
162
|
"CREATE INDEX IF NOT EXISTS idx_attributes_contact ON attributes(contact_id) WHERE deleted_at IS NULL"
|
|
29
163
|
);
|
|
164
|
+
db.exec(
|
|
165
|
+
"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"
|
|
166
|
+
);
|
|
30
167
|
db.exec(
|
|
31
168
|
"CREATE INDEX IF NOT EXISTS idx_attributes_link ON attributes(link_id) WHERE deleted_at IS NULL"
|
|
32
169
|
);
|
|
@@ -139,56 +276,6 @@ function initEventParticipantsTables(db) {
|
|
|
139
276
|
);
|
|
140
277
|
}
|
|
141
278
|
|
|
142
|
-
// src/events/init_events_tables.ts
|
|
143
|
-
function initEventsTables(db) {
|
|
144
|
-
db.exec(`
|
|
145
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
146
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
147
|
-
type TEXT NOT NULL,
|
|
148
|
-
occurred_at INTEGER NOT NULL,
|
|
149
|
-
summary TEXT NOT NULL,
|
|
150
|
-
significance REAL NOT NULL DEFAULT 0,
|
|
151
|
-
moment_kind TEXT,
|
|
152
|
-
recurrence_kind TEXT,
|
|
153
|
-
anchor_month INTEGER,
|
|
154
|
-
anchor_day INTEGER,
|
|
155
|
-
anchor_contact_id INTEGER REFERENCES contacts (id) ON DELETE CASCADE,
|
|
156
|
-
anchor_link_id INTEGER REFERENCES links (id) ON DELETE CASCADE,
|
|
157
|
-
created_at INTEGER NOT NULL,
|
|
158
|
-
updated_at INTEGER NOT NULL,
|
|
159
|
-
deleted_at INTEGER,
|
|
160
|
-
CHECK (length(trim(summary)) > 0),
|
|
161
|
-
CHECK (
|
|
162
|
-
type IN (
|
|
163
|
-
'conversation', 'activity', 'gift', 'support', 'milestone', 'observation',
|
|
164
|
-
'conflict', 'correction', 'transaction', 'promise', 'agreement', 'date_anchor'
|
|
165
|
-
)
|
|
166
|
-
),
|
|
167
|
-
CHECK (
|
|
168
|
-
(type = 'date_anchor' AND (
|
|
169
|
-
(anchor_contact_id IS NOT NULL AND anchor_link_id IS NULL)
|
|
170
|
-
OR (anchor_contact_id IS NULL AND anchor_link_id IS NOT NULL)
|
|
171
|
-
))
|
|
172
|
-
OR (type != 'date_anchor' AND anchor_contact_id IS NULL AND anchor_link_id IS NULL)
|
|
173
|
-
),
|
|
174
|
-
CHECK (moment_kind IS NULL OR moment_kind IN (
|
|
175
|
-
'breakthrough', 'rupture', 'reconciliation', 'milestone', 'turning_point'
|
|
176
|
-
)),
|
|
177
|
-
CHECK (recurrence_kind IS NULL OR recurrence_kind IN (
|
|
178
|
-
'birthday', 'anniversary', 'renewal', 'memorial', 'custom_yearly'
|
|
179
|
-
)),
|
|
180
|
-
CHECK (anchor_month IS NULL OR (anchor_month >= 1 AND anchor_month <= 12)),
|
|
181
|
-
CHECK (anchor_day IS NULL OR (anchor_day >= 1 AND anchor_day <= 31))
|
|
182
|
-
)
|
|
183
|
-
`);
|
|
184
|
-
db.exec(
|
|
185
|
-
"CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at DESC) WHERE deleted_at IS NULL"
|
|
186
|
-
);
|
|
187
|
-
db.exec(
|
|
188
|
-
"CREATE INDEX IF NOT EXISTS idx_events_type ON events(type) WHERE deleted_at IS NULL"
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
279
|
// src/events/init_open_commitments_tables.ts
|
|
193
280
|
function initOpenCommitmentsTables(db) {
|
|
194
281
|
db.exec(`
|
|
@@ -208,6 +295,9 @@ function initOpenCommitmentsTables(db) {
|
|
|
208
295
|
)
|
|
209
296
|
)
|
|
210
297
|
`);
|
|
298
|
+
const columns = db.prepare("PRAGMA table_info(open_commitments)").all();
|
|
299
|
+
if (!columns.some((column) => column.name === "due_on"))
|
|
300
|
+
db.exec("ALTER TABLE open_commitments ADD COLUMN due_on TEXT");
|
|
211
301
|
}
|
|
212
302
|
|
|
213
303
|
// src/identities/init_identities_tables.ts
|
|
@@ -272,6 +362,21 @@ function initLinkEventEffectsTables(db) {
|
|
|
272
362
|
db.exec(
|
|
273
363
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
274
364
|
);
|
|
365
|
+
for (const name of ["participant_json", "provenance_json"])
|
|
366
|
+
if (!columns.some((c) => c.name === name))
|
|
367
|
+
db.exec(`ALTER TABLE link_event_effects ADD COLUMN ${name} TEXT`);
|
|
368
|
+
db.exec(
|
|
369
|
+
"CREATE TABLE IF NOT EXISTS event_link_baselines (link_id INTEGER PRIMARY KEY REFERENCES links(id) ON DELETE CASCADE, state_json TEXT NOT NULL)"
|
|
370
|
+
);
|
|
371
|
+
db.exec(
|
|
372
|
+
"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)"
|
|
373
|
+
);
|
|
374
|
+
db.exec(`INSERT OR IGNORE INTO event_link_baselines(link_id,state_json)
|
|
375
|
+
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)
|
|
376
|
+
FROM links l JOIN link_event_effects e ON e.link_id=l.id GROUP BY l.id`);
|
|
377
|
+
db.exec(
|
|
378
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id IN (SELECT id FROM events WHERE type IN ('promise', 'agreement'))"
|
|
379
|
+
);
|
|
275
380
|
db.exec(
|
|
276
381
|
"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"
|
|
277
382
|
);
|
|
@@ -455,6 +560,7 @@ var AffinityError = class extends Error {
|
|
|
455
560
|
|
|
456
561
|
// src/lib/errors/affinity_conflict_error.ts
|
|
457
562
|
var AffinityConflictError = class extends AffinityError {
|
|
563
|
+
existingContactId;
|
|
458
564
|
code = "CONFLICT";
|
|
459
565
|
};
|
|
460
566
|
|
|
@@ -524,6 +630,9 @@ __export(read_exports, {
|
|
|
524
630
|
getAffinityChart: () => getAffinityChart,
|
|
525
631
|
getContactJournal: () => getContactJournal,
|
|
526
632
|
getContactProfile: () => getContactProfile,
|
|
633
|
+
getEventByExternalRef: () => getEventByExternalRef,
|
|
634
|
+
getEventById: () => getEventById,
|
|
635
|
+
getEventThread: () => getEventThread,
|
|
527
636
|
getLinkDetail: () => getLinkDetail,
|
|
528
637
|
getLinkTimeline: () => getLinkTimeline,
|
|
529
638
|
getLinksBetween: () => getLinksBetween,
|
|
@@ -539,7 +648,8 @@ __export(read_exports, {
|
|
|
539
648
|
listProgressionReadiness: () => listProgressionReadiness,
|
|
540
649
|
listRadar: () => listRadar,
|
|
541
650
|
listUpcomingDates: () => listUpcomingDates,
|
|
542
|
-
searchContacts: () => searchContacts
|
|
651
|
+
searchContacts: () => searchContacts,
|
|
652
|
+
searchEvents: () => searchEvents
|
|
543
653
|
});
|
|
544
654
|
|
|
545
655
|
// src/contacts/mappers.ts
|
|
@@ -727,6 +837,104 @@ function refreshContactRollup(db, contactId, now) {
|
|
|
727
837
|
).run(contactId, warnings.length, JSON.stringify(warnings), "{}", now);
|
|
728
838
|
}
|
|
729
839
|
|
|
840
|
+
// src/identities/mappers.ts
|
|
841
|
+
function mapIdentityRowToIdentityRecord(row) {
|
|
842
|
+
return {
|
|
843
|
+
id: row.id,
|
|
844
|
+
contactId: row.contact_id,
|
|
845
|
+
type: row.type,
|
|
846
|
+
value: row.value,
|
|
847
|
+
label: row.label,
|
|
848
|
+
verified: row.verified === 1,
|
|
849
|
+
verifiedAt: row.verified_at
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/identities/normalize.ts
|
|
854
|
+
var handleIdentityTypes = /* @__PURE__ */ new Set([
|
|
855
|
+
"handle",
|
|
856
|
+
"bluesky",
|
|
857
|
+
"x",
|
|
858
|
+
"twitter",
|
|
859
|
+
"mastodon",
|
|
860
|
+
"github",
|
|
861
|
+
"instagram",
|
|
862
|
+
"telegram",
|
|
863
|
+
"linkedin",
|
|
864
|
+
"facebook",
|
|
865
|
+
"youtube",
|
|
866
|
+
"tiktok",
|
|
867
|
+
"discord",
|
|
868
|
+
"slack"
|
|
869
|
+
]);
|
|
870
|
+
function normalizeIdentityValue(type, value) {
|
|
871
|
+
const v = value.trim();
|
|
872
|
+
return handleIdentityTypes.has(type.toLowerCase()) ? v.replace(/^@/, "").toLowerCase() : v;
|
|
873
|
+
}
|
|
874
|
+
function normalizeIdentityKey(type, value) {
|
|
875
|
+
let t = type.trim().toLowerCase();
|
|
876
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(t)) t = "did";
|
|
877
|
+
const v = normalizeIdentityValue(t, value).toLowerCase();
|
|
878
|
+
if (t.length === 0 || v.length === 0) {
|
|
879
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
880
|
+
}
|
|
881
|
+
return `${t}:${v}`;
|
|
882
|
+
}
|
|
883
|
+
function legacyIdentityKey(key) {
|
|
884
|
+
const colon = key.indexOf(":");
|
|
885
|
+
return handleIdentityTypes.has(key.slice(0, colon)) ? `${key.slice(0, colon + 1)}@${key.slice(colon + 1)}` : key;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/identities/queries.ts
|
|
889
|
+
function getIdentityRowById(db, id) {
|
|
890
|
+
const row = db.prepare(
|
|
891
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
892
|
+
created_at, updated_at, removed_at
|
|
893
|
+
FROM identities
|
|
894
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
895
|
+
).get(id);
|
|
896
|
+
if (row === void 0) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return row;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/identities/receipts.ts
|
|
903
|
+
function buildIdentityMutationReceipt(primary, partial) {
|
|
904
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
905
|
+
return {
|
|
906
|
+
primary,
|
|
907
|
+
created: normalized.created,
|
|
908
|
+
updated: normalized.updated,
|
|
909
|
+
archived: normalized.archived,
|
|
910
|
+
removed: normalized.removed,
|
|
911
|
+
affectedLinks: normalized.affectedLinks,
|
|
912
|
+
derivedEffects: normalized.derivedEffects
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/identities/validators.ts
|
|
917
|
+
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
918
|
+
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
919
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL"
|
|
920
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey) : db.prepare(
|
|
921
|
+
"SELECT id FROM identities WHERE (normalized_key IN (?, ?) OR (type='custom:atproto-did' AND ? = 'did:' || lower(value))) AND removed_at IS NULL AND id != ?"
|
|
922
|
+
).get(
|
|
923
|
+
normalizedKey,
|
|
924
|
+
legacyIdentityKey(normalizedKey),
|
|
925
|
+
normalizedKey,
|
|
926
|
+
excludeIdentityId
|
|
927
|
+
);
|
|
928
|
+
if (row !== void 0) {
|
|
929
|
+
const owner = db.prepare(
|
|
930
|
+
"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"
|
|
931
|
+
).get(normalizedKey, legacyIdentityKey(normalizedKey), normalizedKey);
|
|
932
|
+
throw new AffinityConflictError(
|
|
933
|
+
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
730
938
|
// src/resolve_now.ts
|
|
731
939
|
function resolveNow(now) {
|
|
732
940
|
if (now !== void 0) {
|
|
@@ -740,39 +948,140 @@ function resolveNow(now) {
|
|
|
740
948
|
return Date.now();
|
|
741
949
|
}
|
|
742
950
|
|
|
743
|
-
// src/
|
|
744
|
-
var
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
951
|
+
// src/identities/validate_identity_input.ts
|
|
952
|
+
var identityTypes = [
|
|
953
|
+
"did",
|
|
954
|
+
"atproto_did",
|
|
955
|
+
"email",
|
|
956
|
+
"phone",
|
|
957
|
+
"url",
|
|
958
|
+
"website",
|
|
959
|
+
"handle",
|
|
960
|
+
"x",
|
|
961
|
+
"twitter",
|
|
962
|
+
"linkedin",
|
|
963
|
+
"github",
|
|
964
|
+
"mastodon",
|
|
965
|
+
"bluesky",
|
|
966
|
+
"telegram",
|
|
967
|
+
"whatsapp",
|
|
968
|
+
"signal",
|
|
969
|
+
"matrix",
|
|
970
|
+
"discord",
|
|
971
|
+
"slack",
|
|
972
|
+
"instagram",
|
|
973
|
+
"facebook",
|
|
974
|
+
"youtube",
|
|
975
|
+
"tiktok",
|
|
976
|
+
"address",
|
|
977
|
+
"other",
|
|
978
|
+
"account_id",
|
|
979
|
+
"alias"
|
|
980
|
+
];
|
|
981
|
+
function validateIdentityInput(type, rawValue) {
|
|
982
|
+
const kind = type.toLowerCase();
|
|
983
|
+
const value = normalizeIdentityValue(kind, rawValue);
|
|
984
|
+
if (["did", "atproto_did", "custom:atproto-did"].includes(kind) && !/^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/.test(value))
|
|
985
|
+
throw new AffinityValidationError(
|
|
986
|
+
"did value must match did:<method>:<identifier> (for example did:plc:abc123)"
|
|
987
|
+
);
|
|
988
|
+
if (handleIdentityTypes.has(kind)) {
|
|
989
|
+
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_.-]+$/;
|
|
990
|
+
if (!pattern.test(value))
|
|
991
|
+
throw new AffinityValidationError(
|
|
992
|
+
`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)"}`
|
|
993
|
+
);
|
|
752
994
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
995
|
+
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
996
|
+
throw new AffinityValidationError(
|
|
997
|
+
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
998
|
+
);
|
|
999
|
+
if (!value.trim())
|
|
1000
|
+
throw new AffinityValidationError("identity value must be non-empty");
|
|
1001
|
+
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
1002
|
+
throw new AffinityValidationError(
|
|
1003
|
+
"email value must match local@domain.tld"
|
|
1004
|
+
);
|
|
1005
|
+
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
1006
|
+
throw new AffinityValidationError(
|
|
1007
|
+
"phone value must contain at least 5 digits"
|
|
1008
|
+
);
|
|
1009
|
+
if (kind === "url" || kind === "website") {
|
|
764
1010
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
769
|
-
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
770
|
-
}
|
|
1011
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
1012
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
1013
|
+
throw new Error();
|
|
771
1014
|
} catch {
|
|
1015
|
+
throw new AffinityValidationError(
|
|
1016
|
+
"url/website value must be an http/https URL or bare domain"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/identities/add_identity.ts
|
|
1023
|
+
function addIdentity(db, contactId, input) {
|
|
1024
|
+
return withTransaction(db, () => {
|
|
1025
|
+
const contact = getContactRowById(db, contactId);
|
|
1026
|
+
if (!contact) {
|
|
1027
|
+
throw new AffinityNotFoundError("contact not found");
|
|
1028
|
+
}
|
|
1029
|
+
if (contact.lifecycle_state === "merged") {
|
|
1030
|
+
throw new AffinityStateError("merged contact is read-only");
|
|
1031
|
+
}
|
|
1032
|
+
const type = input.type.trim();
|
|
1033
|
+
const value = normalizeIdentityValue(type, input.value);
|
|
1034
|
+
if (type.length === 0 || value.length === 0) {
|
|
1035
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
1036
|
+
}
|
|
1037
|
+
validateIdentityInput(type, value);
|
|
1038
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
1039
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
1040
|
+
const nk = normalizeIdentityKey(type, value);
|
|
1041
|
+
if (input.ifExists === "return") {
|
|
1042
|
+
const existing = db.prepare(
|
|
1043
|
+
"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"
|
|
1044
|
+
).get(contactId, nk, legacyIdentityKey(nk), nk);
|
|
1045
|
+
if (existing) {
|
|
1046
|
+
const row2 = getIdentityRowById(db, Number(existing.id));
|
|
1047
|
+
if (row2)
|
|
1048
|
+
return buildIdentityMutationReceipt(
|
|
1049
|
+
mapIdentityRowToIdentityRecord(row2),
|
|
1050
|
+
{}
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
assertNoIdentityCollision(db, nk);
|
|
1055
|
+
const now = resolveNow(input.now);
|
|
1056
|
+
const verified = input.verified ? 1 : 0;
|
|
1057
|
+
const verifiedAt = verified === 1 ? now : null;
|
|
1058
|
+
const result = db.prepare(
|
|
1059
|
+
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
1060
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1061
|
+
).run(
|
|
1062
|
+
contactId,
|
|
1063
|
+
type,
|
|
1064
|
+
value,
|
|
1065
|
+
input.label ?? null,
|
|
1066
|
+
nk,
|
|
1067
|
+
verified,
|
|
1068
|
+
verifiedAt,
|
|
1069
|
+
now,
|
|
1070
|
+
now
|
|
1071
|
+
);
|
|
1072
|
+
const id = Number(result.lastInsertRowid);
|
|
1073
|
+
const row = getIdentityRowById(db, id);
|
|
1074
|
+
if (!row) {
|
|
1075
|
+
throw new AffinityInvariantError("inserted identity not found");
|
|
772
1076
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1077
|
+
refreshContactRollup(db, contactId, now);
|
|
1078
|
+
db.prepare(
|
|
1079
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
1080
|
+
).run(contactId, contactId);
|
|
1081
|
+
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
1082
|
+
created: [{ kind: "identity", id }]
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
776
1085
|
}
|
|
777
1086
|
|
|
778
1087
|
// src/contacts/create_contact.ts
|
|
@@ -799,6 +1108,34 @@ function createContact(db, input) {
|
|
|
799
1108
|
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
800
1109
|
);
|
|
801
1110
|
}
|
|
1111
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
1112
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
1113
|
+
const matches = (input.identities ?? []).flatMap((identity) => {
|
|
1114
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
1115
|
+
const key = normalizeIdentityKey(identity.type, identity.value);
|
|
1116
|
+
const row2 = db.prepare(
|
|
1117
|
+
`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)))`
|
|
1118
|
+
).get(key, legacyIdentityKey(key), key);
|
|
1119
|
+
return row2 ? [{ ...row2, identity }] : [];
|
|
1120
|
+
});
|
|
1121
|
+
const match = matches[0];
|
|
1122
|
+
if (match) {
|
|
1123
|
+
if (input.ifExists === "return") {
|
|
1124
|
+
const existing = getContactRowById(db, match.id);
|
|
1125
|
+
if (!existing)
|
|
1126
|
+
throw new AffinityInvariantError("identity owner not found");
|
|
1127
|
+
const primary2 = mapContactRowToContactListItem(existing);
|
|
1128
|
+
return Object.assign(buildContactMutationReceipt(primary2, {}), {
|
|
1129
|
+
outcome: "no_op",
|
|
1130
|
+
matchedIdentities: matches.map((m) => m.identity)
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const error = new AffinityConflictError(
|
|
1134
|
+
`identity ${normalizeIdentityKey(match.identity.type, match.identity.value).replace(":", " ")} already belongs to #${match.id} ${match.name}`
|
|
1135
|
+
);
|
|
1136
|
+
error.existingContactId = match.id;
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
802
1139
|
const now = resolveNow(input.now);
|
|
803
1140
|
if (input.bootstrapOwner) {
|
|
804
1141
|
const existing = db.prepare(
|
|
@@ -822,9 +1159,12 @@ function createContact(db, input) {
|
|
|
822
1159
|
}
|
|
823
1160
|
refreshContactRollup(db, id, now);
|
|
824
1161
|
const primary = mapContactRowToContactListItem(row);
|
|
825
|
-
|
|
1162
|
+
const receipt = buildContactMutationReceipt(primary, {
|
|
826
1163
|
created: [{ kind: "contact", id }]
|
|
827
1164
|
});
|
|
1165
|
+
for (const identity of input.identities ?? [])
|
|
1166
|
+
receipt.created.push(...addIdentity(db, id, identity).created);
|
|
1167
|
+
return receipt;
|
|
828
1168
|
});
|
|
829
1169
|
}
|
|
830
1170
|
|
|
@@ -847,6 +1187,100 @@ function resolveListLimitOffset(options) {
|
|
|
847
1187
|
return { limit, offset };
|
|
848
1188
|
}
|
|
849
1189
|
|
|
1190
|
+
// src/events/receipts.ts
|
|
1191
|
+
function buildEventMutationReceipt(primary, partial) {
|
|
1192
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
1193
|
+
return {
|
|
1194
|
+
primary,
|
|
1195
|
+
...partial?.warnings ? { warnings: partial.warnings } : {},
|
|
1196
|
+
created: normalized.created,
|
|
1197
|
+
updated: normalized.updated,
|
|
1198
|
+
archived: normalized.archived,
|
|
1199
|
+
removed: normalized.removed,
|
|
1200
|
+
affectedLinks: normalized.affectedLinks,
|
|
1201
|
+
derivedEffects: normalized.derivedEffects
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/events/event_metadata.ts
|
|
1206
|
+
function normalizeExternalRef(ref) {
|
|
1207
|
+
if (!ref || typeof ref.source !== "string" || typeof ref.id !== "string" || !ref.source.trim() || !ref.id.trim())
|
|
1208
|
+
throw new AffinityValidationError(
|
|
1209
|
+
"external reference source and id must be non-empty strings"
|
|
1210
|
+
);
|
|
1211
|
+
return { source: ref.source.trim().toLowerCase(), id: ref.id.trim() };
|
|
1212
|
+
}
|
|
1213
|
+
function deriveEventSummary(content) {
|
|
1214
|
+
const line = content.split(/\r?\n/).find((line2) => line2.trim())?.replace(/\s+/g, " ").trim() ?? "(no text)";
|
|
1215
|
+
return line.length > 140 ? `${line.slice(0, 139)}\u2026` : line;
|
|
1216
|
+
}
|
|
1217
|
+
function validateEventMetadata(input) {
|
|
1218
|
+
if (input.affectsBond !== void 0 && typeof input.affectsBond !== "boolean")
|
|
1219
|
+
throw new AffinityValidationError("affectsBond must be boolean");
|
|
1220
|
+
if (input.content !== void 0 && (typeof input.content !== "string" || Buffer.byteLength(input.content, "utf8") > 1048576))
|
|
1221
|
+
throw new AffinityValidationError(
|
|
1222
|
+
"content must be a string of at most 1 MB (1048576 UTF-8 bytes)"
|
|
1223
|
+
);
|
|
1224
|
+
for (const key of ["externalRef", "parentRef", "threadRef"])
|
|
1225
|
+
if (input[key] !== void 0) input[key] = normalizeExternalRef(input[key]);
|
|
1226
|
+
if (input.channel !== void 0) {
|
|
1227
|
+
if (typeof input.channel !== "string" || !input.channel.trim() || input.channel.trim().length > 40)
|
|
1228
|
+
throw new AffinityValidationError(
|
|
1229
|
+
"channel must be non-empty and at most 40 characters"
|
|
1230
|
+
);
|
|
1231
|
+
input.channel = input.channel.trim().toLowerCase();
|
|
1232
|
+
}
|
|
1233
|
+
if (input.parentEventId !== void 0 && (!Number.isSafeInteger(input.parentEventId) || input.parentEventId <= 0))
|
|
1234
|
+
throw new AffinityValidationError(
|
|
1235
|
+
"parentEventId must be a positive integer"
|
|
1236
|
+
);
|
|
1237
|
+
const moments = [
|
|
1238
|
+
"breakthrough",
|
|
1239
|
+
"rupture",
|
|
1240
|
+
"reconciliation",
|
|
1241
|
+
"milestone",
|
|
1242
|
+
"turning_point"
|
|
1243
|
+
];
|
|
1244
|
+
if (input.momentKind !== void 0 && !moments.includes(input.momentKind))
|
|
1245
|
+
throw new AffinityValidationError(
|
|
1246
|
+
`momentKind must be one of: ${moments.join(", ")}`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
function getEventById(db, eventId) {
|
|
1250
|
+
return db.prepare("SELECT 1 FROM events WHERE id=? AND deleted_at IS NULL").get(eventId) ? loadEventRecord(db, eventId) : null;
|
|
1251
|
+
}
|
|
1252
|
+
function getEventByExternalRef(db, source, id) {
|
|
1253
|
+
const ref = normalizeExternalRef({ source, id });
|
|
1254
|
+
const row = db.prepare(
|
|
1255
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
1256
|
+
).get(ref.source, ref.id);
|
|
1257
|
+
return row ? loadEventRecord(db, row.id) : null;
|
|
1258
|
+
}
|
|
1259
|
+
function existingEventReceipt(db, ref) {
|
|
1260
|
+
const event = ref && getEventByExternalRef(db, ref.source, ref.id);
|
|
1261
|
+
return event ? { ...buildEventMutationReceipt(event, {}), outcome: "no_op" } : null;
|
|
1262
|
+
}
|
|
1263
|
+
function enrichEventMetadata(db, record) {
|
|
1264
|
+
const bond = db.prepare("SELECT affects_bond FROM events WHERE id=?").get(record.id);
|
|
1265
|
+
record.affectsBond = bond?.affects_bond == null ? !(record.type === "observation" && record.significance <= 2) : Boolean(bond.affects_bond);
|
|
1266
|
+
if (record.parentEventId === void 0 && record.parentRef) {
|
|
1267
|
+
const row = db.prepare(
|
|
1268
|
+
"SELECT id FROM events WHERE external_source=? AND external_id=? AND deleted_at IS NULL"
|
|
1269
|
+
).get(record.parentRef.source, record.parentRef.id);
|
|
1270
|
+
if (row) record.parentEventId = row.id;
|
|
1271
|
+
}
|
|
1272
|
+
const attrs = db.prepare(
|
|
1273
|
+
"SELECT id, name, value FROM attributes WHERE event_id=? AND deleted_at IS NULL ORDER BY id"
|
|
1274
|
+
).all(record.id);
|
|
1275
|
+
if (attrs.length)
|
|
1276
|
+
record.attributes = attrs.map((a) => ({
|
|
1277
|
+
...a,
|
|
1278
|
+
contactId: null,
|
|
1279
|
+
linkId: null,
|
|
1280
|
+
eventId: record.id
|
|
1281
|
+
}));
|
|
1282
|
+
}
|
|
1283
|
+
|
|
850
1284
|
// src/events/validators.ts
|
|
851
1285
|
var PARTICIPANT_ROLES = /* @__PURE__ */ new Set([
|
|
852
1286
|
"actor",
|
|
@@ -889,8 +1323,11 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
889
1323
|
}
|
|
890
1324
|
function validateSocialEventInput(input) {
|
|
891
1325
|
assertFiniteTimestamp(input.occurredAt, "occurredAt");
|
|
892
|
-
|
|
893
|
-
if (summary.
|
|
1326
|
+
validateEventMetadata(input);
|
|
1327
|
+
if (input.summary !== void 0 && typeof input.summary !== "string")
|
|
1328
|
+
throw new AffinityValidationError("summary must be a string");
|
|
1329
|
+
const summary = input.summary?.trim();
|
|
1330
|
+
if (summary === "" || summary === void 0 && input.content === void 0) {
|
|
894
1331
|
throw new AffinityValidationError("summary must be non-empty");
|
|
895
1332
|
}
|
|
896
1333
|
const sig = input.significance;
|
|
@@ -954,11 +1391,27 @@ function mapEventRowToEventRecord(row, participants) {
|
|
|
954
1391
|
id: row.id,
|
|
955
1392
|
type: row.type,
|
|
956
1393
|
occurredAt: row.occurred_at,
|
|
957
|
-
|
|
1394
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
1395
|
+
summary: decodeEventText(row.summary),
|
|
958
1396
|
significance: row.significance,
|
|
959
1397
|
momentKind: row.moment_kind,
|
|
960
1398
|
participants
|
|
961
1399
|
};
|
|
1400
|
+
if (row.provenance_json != null)
|
|
1401
|
+
base.provenance = JSON.parse(row.provenance_json);
|
|
1402
|
+
if (row.content != null) base.content = decodeEventText(row.content);
|
|
1403
|
+
if (row.channel != null) base.channel = row.channel;
|
|
1404
|
+
if (row.external_source != null && row.external_id != null)
|
|
1405
|
+
base.externalRef = { source: row.external_source, id: row.external_id };
|
|
1406
|
+
if (row.parent_source != null && row.parent_ref_id != null)
|
|
1407
|
+
base.parentRef = { source: row.parent_source, id: row.parent_ref_id };
|
|
1408
|
+
if (row.thread_source != null && row.thread_id != null)
|
|
1409
|
+
base.threadRef = { source: row.thread_source, id: row.thread_id };
|
|
1410
|
+
if (row.parent_event_id != null) base.parentEventId = row.parent_event_id;
|
|
1411
|
+
if (row.type === "promise" || row.type === "agreement") {
|
|
1412
|
+
base.dueAt = row.due_at ?? null;
|
|
1413
|
+
base.dueOn = row.due_on ?? (row.due_at == null ? null : new Date(row.due_at).toISOString().slice(0, 10));
|
|
1414
|
+
}
|
|
962
1415
|
if (row.recurrence_kind !== null) {
|
|
963
1416
|
base.recurrenceKind = row.recurrence_kind;
|
|
964
1417
|
base.anchorMonth = row.anchor_month;
|
|
@@ -988,25 +1441,29 @@ function mapUpcomingRowToUpcomingDateRecord(row) {
|
|
|
988
1441
|
targetRef,
|
|
989
1442
|
recurrenceKind,
|
|
990
1443
|
occursOn: row.occurs_on,
|
|
991
|
-
|
|
1444
|
+
occursOnDate: new Date(row.occurs_on).toISOString().slice(0, 10),
|
|
1445
|
+
summary: decodeEventText(row.summary),
|
|
992
1446
|
significance: row.significance
|
|
993
1447
|
};
|
|
994
1448
|
}
|
|
1449
|
+
function decodeEventText(value) {
|
|
1450
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
1451
|
+
}
|
|
995
1452
|
|
|
996
1453
|
// src/events/queries.ts
|
|
997
1454
|
function getEventRowById(db, id) {
|
|
998
1455
|
const row = db.prepare(
|
|
999
|
-
`SELECT
|
|
1000
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
1001
|
-
anchor_contact_id, anchor_link_id,
|
|
1002
|
-
created_at, updated_at, deleted_at
|
|
1456
|
+
`SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary
|
|
1003
1457
|
FROM events
|
|
1004
1458
|
WHERE id = ? AND deleted_at IS NULL`
|
|
1005
1459
|
).get(id);
|
|
1006
1460
|
if (row === void 0) {
|
|
1007
1461
|
return null;
|
|
1008
1462
|
}
|
|
1009
|
-
|
|
1463
|
+
const event = row;
|
|
1464
|
+
event.summary = decodeEventText(event.summary);
|
|
1465
|
+
if (event.content != null) event.content = decodeEventText(event.content);
|
|
1466
|
+
return event;
|
|
1010
1467
|
}
|
|
1011
1468
|
function findDuplicateDateAnchor(db, params) {
|
|
1012
1469
|
const exclude = params.excludeEventId;
|
|
@@ -1074,19 +1531,27 @@ function loadEventRecord(db, eventId) {
|
|
|
1074
1531
|
throw new AffinityInvariantError("event not found");
|
|
1075
1532
|
}
|
|
1076
1533
|
const participants = loadEventParticipantViews(db, eventId);
|
|
1077
|
-
|
|
1534
|
+
const record = mapEventRowToEventRecord(row, participants);
|
|
1535
|
+
const due = db.prepare("SELECT due_at, due_on FROM open_commitments WHERE event_id = ?").get(eventId);
|
|
1536
|
+
if (due) {
|
|
1537
|
+
record.dueAt = due.due_at;
|
|
1538
|
+
record.dueOn = due.due_on ?? (due.due_at === null ? null : new Date(due.due_at).toISOString().slice(0, 10));
|
|
1539
|
+
}
|
|
1540
|
+
enrichEventMetadata(db, record);
|
|
1541
|
+
return record;
|
|
1078
1542
|
}
|
|
1079
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
1543
|
+
function batchLoadEventRecords(db, eventIds, includeContent = false) {
|
|
1080
1544
|
if (eventIds.length === 0) {
|
|
1081
1545
|
return [];
|
|
1082
1546
|
}
|
|
1083
1547
|
const ph = eventIds.map(() => "?").join(", ");
|
|
1084
1548
|
const eventRows = db.prepare(
|
|
1085
|
-
`SELECT id, type, occurred_at,
|
|
1549
|
+
`SELECT e.id, e.type, e.occurred_at, e.occurred_on, oc.due_at, oc.due_on, significance, moment_kind,
|
|
1550
|
+
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,
|
|
1086
1551
|
recurrence_kind, anchor_month, anchor_day,
|
|
1087
1552
|
anchor_contact_id, anchor_link_id,
|
|
1088
|
-
created_at, updated_at, deleted_at
|
|
1089
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
1553
|
+
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
|
|
1554
|
+
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`
|
|
1090
1555
|
).all(...eventIds);
|
|
1091
1556
|
const partRows = db.prepare(
|
|
1092
1557
|
`SELECT event_id, contact_id, role, directionality
|
|
@@ -1110,7 +1575,9 @@ function batchLoadEventRecords(db, eventIds) {
|
|
|
1110
1575
|
for (const id of eventIds) {
|
|
1111
1576
|
const row = rowById.get(id);
|
|
1112
1577
|
if (row) {
|
|
1113
|
-
|
|
1578
|
+
const record = mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []);
|
|
1579
|
+
enrichEventMetadata(db, record);
|
|
1580
|
+
out.push(record);
|
|
1114
1581
|
}
|
|
1115
1582
|
}
|
|
1116
1583
|
return out;
|
|
@@ -1169,6 +1636,10 @@ function getContactJournal(db, contactId, options) {
|
|
|
1169
1636
|
clauses.push(`e.type IN (${ph})`);
|
|
1170
1637
|
params.push(...options.eventTypes);
|
|
1171
1638
|
}
|
|
1639
|
+
if (options?.channel !== void 0) {
|
|
1640
|
+
clauses.push("e.channel = ?");
|
|
1641
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1642
|
+
}
|
|
1172
1643
|
const where = clauses.join(" AND ");
|
|
1173
1644
|
const rows = db.prepare(
|
|
1174
1645
|
`SELECT e.id FROM events e
|
|
@@ -1179,8 +1650,70 @@ function getContactJournal(db, contactId, options) {
|
|
|
1179
1650
|
).all(...params, limit, offset);
|
|
1180
1651
|
return batchLoadEventRecords(
|
|
1181
1652
|
db,
|
|
1182
|
-
rows.map((r) => r.id)
|
|
1653
|
+
rows.map((r) => r.id),
|
|
1654
|
+
options?.includeContent ?? false
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// src/lib/parse_calendar_time.ts
|
|
1659
|
+
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d";
|
|
1660
|
+
function isCommitmentOverdue(dueAt, dueOn, now) {
|
|
1661
|
+
if (dueAt === null) return false;
|
|
1662
|
+
if (dueOn && dueAt === Date.parse(`${dueOn}T23:59:59.999Z`)) {
|
|
1663
|
+
return localCalendarDate(now) > dueOn;
|
|
1664
|
+
}
|
|
1665
|
+
return dueAt < now;
|
|
1666
|
+
}
|
|
1667
|
+
var OVERDUE_SQL = "(CASE WHEN oc.due_on IS NOT NULL AND oc.due_at = CAST(strftime('%s', oc.due_on) AS INTEGER) * 1000 + 86399999 THEN oc.due_on < ? ELSE oc.due_at < ? END)";
|
|
1668
|
+
function localCalendarDate(now) {
|
|
1669
|
+
const d = new Date(now);
|
|
1670
|
+
return `${String(d.getFullYear()).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1671
|
+
}
|
|
1672
|
+
function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
1673
|
+
if (value == null && field.endsWith("occurredAt"))
|
|
1674
|
+
throw new AffinityValidationError(
|
|
1675
|
+
"occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)"
|
|
1676
|
+
);
|
|
1677
|
+
const fail = () => {
|
|
1678
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1679
|
+
};
|
|
1680
|
+
if (typeof value === "number" || typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
1681
|
+
const timestamp2 = Number(value);
|
|
1682
|
+
if (!Number.isSafeInteger(timestamp2) || !Number.isFinite(new Date(timestamp2).getTime()))
|
|
1683
|
+
return fail();
|
|
1684
|
+
return { timestamp: timestamp2, day: new Date(timestamp2).toISOString().slice(0, 10) };
|
|
1685
|
+
}
|
|
1686
|
+
if (typeof value !== "string") return fail();
|
|
1687
|
+
let text = value.trim();
|
|
1688
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)) {
|
|
1689
|
+
text = text.replace(" ", "T");
|
|
1690
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
1691
|
+
}
|
|
1692
|
+
text = text.replace(
|
|
1693
|
+
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
1694
|
+
(_, digits) => `.${digits.slice(0, 3)}`
|
|
1183
1695
|
);
|
|
1696
|
+
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
1697
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1698
|
+
const date = /* @__PURE__ */ new Date(`${localCalendarDate(now)}T00:00:00Z`);
|
|
1699
|
+
date.setUTCDate(date.getUTCDate() + offset);
|
|
1700
|
+
if (!Number.isFinite(date.getTime())) return fail();
|
|
1701
|
+
text = date.toISOString().slice(0, 10);
|
|
1702
|
+
}
|
|
1703
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text);
|
|
1704
|
+
if (european) text = `${european[3]}-${european[2]}-${european[1]}`;
|
|
1705
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(text);
|
|
1706
|
+
if (!dateOnly && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1707
|
+
text
|
|
1708
|
+
))
|
|
1709
|
+
return fail();
|
|
1710
|
+
const day = text.slice(0, 10);
|
|
1711
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
1712
|
+
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
1713
|
+
return fail();
|
|
1714
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
|
|
1715
|
+
if (!Number.isFinite(timestamp)) return fail();
|
|
1716
|
+
return { timestamp, day };
|
|
1184
1717
|
}
|
|
1185
1718
|
|
|
1186
1719
|
// src/events/list_open_commitments.ts
|
|
@@ -1238,9 +1771,13 @@ function listOpenCommitments(db, filters, options) {
|
|
|
1238
1771
|
clauses.push("oc.due_at <= ?");
|
|
1239
1772
|
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
1240
1773
|
}
|
|
1774
|
+
if (options?.channel !== void 0) {
|
|
1775
|
+
clauses.push("e.channel=?");
|
|
1776
|
+
params.push(options.channel.trim().toLowerCase());
|
|
1777
|
+
}
|
|
1241
1778
|
const where = clauses.join(" AND ");
|
|
1242
1779
|
const rows = db.prepare(
|
|
1243
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
1780
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.due_on, oc.resolved_at, oc.resolution,
|
|
1244
1781
|
e.type, e.summary
|
|
1245
1782
|
FROM open_commitments oc
|
|
1246
1783
|
INNER JOIN events e ON e.id = oc.event_id
|
|
@@ -1257,7 +1794,8 @@ function listOpenCommitments(db, filters, options) {
|
|
|
1257
1794
|
participants: partsByEvent.get(r.event_id) ?? [],
|
|
1258
1795
|
dueAt: r.due_at,
|
|
1259
1796
|
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
1260
|
-
|
|
1797
|
+
dueOn: r.due_on ?? (r.due_at === null ? null : new Date(r.due_at).toISOString().slice(0, 10)),
|
|
1798
|
+
overdue: isCommitmentOverdue(r.due_at, r.due_on, now),
|
|
1261
1799
|
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
1262
1800
|
}));
|
|
1263
1801
|
}
|
|
@@ -1291,8 +1829,10 @@ function utcMidnight(year, month1Based, day) {
|
|
|
1291
1829
|
return Date.UTC(year, month1Based - 1, day, 0, 0, 0, 0);
|
|
1292
1830
|
}
|
|
1293
1831
|
function computeNextAnchorOccursOn(params) {
|
|
1294
|
-
const refStart =
|
|
1295
|
-
|
|
1832
|
+
const refStart = Date.parse(
|
|
1833
|
+
`${localCalendarDate(params.referenceMs)}T00:00:00Z`
|
|
1834
|
+
);
|
|
1835
|
+
const refYear = new Date(refStart).getUTCFullYear();
|
|
1296
1836
|
for (let year2 = refYear; year2 <= refYear + 2; year2 += 1) {
|
|
1297
1837
|
const day2 = effectiveAnchorDay(year2, params.anchorMonth, params.anchorDay);
|
|
1298
1838
|
const candidate = utcMidnight(year2, params.anchorMonth, day2);
|
|
@@ -1352,19 +1892,6 @@ function loadActiveDatesForContact(db, contactId) {
|
|
|
1352
1892
|
return rows.map((row) => mapUpcomingRowToUpcomingDateRecord(row));
|
|
1353
1893
|
}
|
|
1354
1894
|
|
|
1355
|
-
// src/identities/mappers.ts
|
|
1356
|
-
function mapIdentityRowToIdentityRecord(row) {
|
|
1357
|
-
return {
|
|
1358
|
-
id: row.id,
|
|
1359
|
-
contactId: row.contact_id,
|
|
1360
|
-
type: row.type,
|
|
1361
|
-
value: row.value,
|
|
1362
|
-
label: row.label,
|
|
1363
|
-
verified: row.verified === 1,
|
|
1364
|
-
verifiedAt: row.verified_at
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
1895
|
// src/links/mappers.ts
|
|
1369
1896
|
function mapLinkRowToLinkListItem(row) {
|
|
1370
1897
|
const kind = row.kind;
|
|
@@ -1425,7 +1952,13 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1425
1952
|
ORDER BY COALESCE(lr.normalized_rank, 0) DESC, l.trust DESC
|
|
1426
1953
|
LIMIT ?`
|
|
1427
1954
|
).all(contactId, contactId, topLinksLimit);
|
|
1428
|
-
const topLinks = linkRows.map((l) =>
|
|
1955
|
+
const topLinks = linkRows.map((l) => ({
|
|
1956
|
+
...mapLinkRowToLinkListItem(l),
|
|
1957
|
+
counterpartyName: getContactRowById(
|
|
1958
|
+
db,
|
|
1959
|
+
l.from_contact_id === contactId ? l.to_contact_id : l.from_contact_id
|
|
1960
|
+
)?.name ?? ""
|
|
1961
|
+
}));
|
|
1429
1962
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
1430
1963
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1431
1964
|
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
@@ -1458,7 +1991,20 @@ function getContactProfile(db, contactId, options) {
|
|
|
1458
1991
|
return {
|
|
1459
1992
|
...profile,
|
|
1460
1993
|
recentEvents: getContactJournal(db, contactId, {
|
|
1461
|
-
limit: options?.recentEventsLimit ?? 5
|
|
1994
|
+
limit: options?.recentEventsLimit ?? 5,
|
|
1995
|
+
eventTypes: [
|
|
1996
|
+
"conversation",
|
|
1997
|
+
"activity",
|
|
1998
|
+
"gift",
|
|
1999
|
+
"support",
|
|
2000
|
+
"milestone",
|
|
2001
|
+
"observation",
|
|
2002
|
+
"conflict",
|
|
2003
|
+
"correction",
|
|
2004
|
+
"transaction",
|
|
2005
|
+
"promise",
|
|
2006
|
+
"agreement"
|
|
2007
|
+
]
|
|
1462
2008
|
}).map((event) => ({
|
|
1463
2009
|
...event,
|
|
1464
2010
|
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
@@ -1488,7 +2034,10 @@ function contactAttributeClauses(filters) {
|
|
|
1488
2034
|
const params = [];
|
|
1489
2035
|
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
1490
2036
|
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
1491
|
-
for (const filter of [
|
|
2037
|
+
for (const filter of [
|
|
2038
|
+
...attributes,
|
|
2039
|
+
...tags.map((tag) => `tag:${tag.replace(/^tag:/i, "")}`)
|
|
2040
|
+
]) {
|
|
1492
2041
|
const separator = filter.indexOf("=");
|
|
1493
2042
|
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
1494
2043
|
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
@@ -1559,7 +2108,9 @@ function pairKey(leftContactId, rightContactId) {
|
|
|
1559
2108
|
function normalizeName(name) {
|
|
1560
2109
|
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
1561
2110
|
}
|
|
1562
|
-
function fuzzyNameScore(
|
|
2111
|
+
function fuzzyNameScore(leftInput, rightInput) {
|
|
2112
|
+
const left = normalizeName(leftInput);
|
|
2113
|
+
const right = normalizeName(rightInput);
|
|
1563
2114
|
if (left === right) {
|
|
1564
2115
|
return 0.92;
|
|
1565
2116
|
}
|
|
@@ -1717,7 +2268,7 @@ function foldSearchText(value) {
|
|
|
1717
2268
|
|
|
1718
2269
|
// src/contacts/search_contacts.ts
|
|
1719
2270
|
function searchContacts(db, query, filters, options) {
|
|
1720
|
-
const q = foldSearchText(query.trim());
|
|
2271
|
+
const q = foldSearchText(query.trim().replace(/^@/, ""));
|
|
1721
2272
|
if (!q) return [];
|
|
1722
2273
|
assertDefaultOrdering("searchContacts", options);
|
|
1723
2274
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
@@ -1737,6 +2288,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
1737
2288
|
const tokens = q.split(/\s+/);
|
|
1738
2289
|
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
1739
2290
|
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
2291
|
+
const suffix = digits.replace(/^0/, "");
|
|
1740
2292
|
const attributeFilters = contactAttributeClauses(filters);
|
|
1741
2293
|
const clauses = [
|
|
1742
2294
|
"deleted_at IS NULL",
|
|
@@ -1772,7 +2324,7 @@ function searchContacts(db, query, filters, options) {
|
|
|
1772
2324
|
)
|
|
1773
2325
|
];
|
|
1774
2326
|
const phoneFields = phone ? values.filter(
|
|
1775
|
-
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
2327
|
+
(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)
|
|
1776
2328
|
) : [];
|
|
1777
2329
|
if (!phoneFields.length && !tokens.every(
|
|
1778
2330
|
(token) => values.some((field) => field.text.includes(token))
|
|
@@ -1807,7 +2359,7 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
1807
2359
|
}
|
|
1808
2360
|
if (to === "merged") {
|
|
1809
2361
|
throw new AffinityInvariantError(
|
|
1810
|
-
"use
|
|
2362
|
+
"use merge_contacts (CLI: contact merge) to reach merged state"
|
|
1811
2363
|
);
|
|
1812
2364
|
}
|
|
1813
2365
|
if (from === to) {
|
|
@@ -1862,7 +2414,7 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
1862
2414
|
// src/events/persistence.ts
|
|
1863
2415
|
function insertJournalEvent(db, params) {
|
|
1864
2416
|
const now = resolveNow(params.now);
|
|
1865
|
-
const summary = params.summary
|
|
2417
|
+
const summary = params.summary?.trim() ?? deriveEventSummary(params.content ?? "");
|
|
1866
2418
|
const result = db.prepare(
|
|
1867
2419
|
`INSERT INTO events (
|
|
1868
2420
|
type, occurred_at, summary, significance, moment_kind,
|
|
@@ -1879,6 +2431,35 @@ function insertJournalEvent(db, params) {
|
|
|
1879
2431
|
now
|
|
1880
2432
|
);
|
|
1881
2433
|
const eventId = Number(result.lastInsertRowid);
|
|
2434
|
+
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
2435
|
+
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(
|
|
2436
|
+
summary
|
|
2437
|
+
) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
|
|
2438
|
+
eventId
|
|
2439
|
+
);
|
|
2440
|
+
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
2441
|
+
params.momentKind,
|
|
2442
|
+
eventId
|
|
2443
|
+
);
|
|
2444
|
+
db.prepare(
|
|
2445
|
+
"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=?"
|
|
2446
|
+
).run(
|
|
2447
|
+
params.content ?? null,
|
|
2448
|
+
params.externalRef?.source ?? null,
|
|
2449
|
+
params.externalRef?.id ?? null,
|
|
2450
|
+
params.channel ?? null,
|
|
2451
|
+
params.parentRef?.source ?? null,
|
|
2452
|
+
params.parentRef?.id ?? null,
|
|
2453
|
+
params.parentEventId ?? null,
|
|
2454
|
+
params.threadRef?.source ?? null,
|
|
2455
|
+
params.threadRef?.id ?? null,
|
|
2456
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
2457
|
+
eventId
|
|
2458
|
+
);
|
|
2459
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
2460
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
2461
|
+
eventId
|
|
2462
|
+
);
|
|
1882
2463
|
const insertParticipant = db.prepare(
|
|
1883
2464
|
`INSERT INTO event_participants (
|
|
1884
2465
|
event_id, contact_id, role, directionality
|
|
@@ -1921,6 +2502,10 @@ function insertDateAnchorEvent(db, params) {
|
|
|
1921
2502
|
now
|
|
1922
2503
|
);
|
|
1923
2504
|
const eventId = Number(result.lastInsertRowid);
|
|
2505
|
+
db.prepare("UPDATE events SET occurred_on = ? WHERE id = ?").run(
|
|
2506
|
+
params.occurredOn ?? new Date(params.occurredAt).toISOString().slice(0, 10),
|
|
2507
|
+
eventId
|
|
2508
|
+
);
|
|
1924
2509
|
const insertParticipant = db.prepare(
|
|
1925
2510
|
`INSERT INTO event_participants (
|
|
1926
2511
|
event_id, contact_id, role, directionality
|
|
@@ -1941,20 +2526,6 @@ function insertDateAnchorEvent(db, params) {
|
|
|
1941
2526
|
return eventId;
|
|
1942
2527
|
}
|
|
1943
2528
|
|
|
1944
|
-
// src/events/receipts.ts
|
|
1945
|
-
function buildEventMutationReceipt(primary, partial) {
|
|
1946
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
1947
|
-
return {
|
|
1948
|
-
primary,
|
|
1949
|
-
created: normalized.created,
|
|
1950
|
-
updated: normalized.updated,
|
|
1951
|
-
archived: normalized.archived,
|
|
1952
|
-
removed: normalized.removed,
|
|
1953
|
-
affectedLinks: normalized.affectedLinks,
|
|
1954
|
-
derivedEffects: normalized.derivedEffects
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
2529
|
// src/links/queries.ts
|
|
1959
2530
|
function getLinkRowById(db, id) {
|
|
1960
2531
|
const row = db.prepare(
|
|
@@ -2116,12 +2687,12 @@ function listUpcomingDates(db, filters, options) {
|
|
|
2116
2687
|
}
|
|
2117
2688
|
const clauses = ["e.deleted_at IS NULL", "e.type = 'date_anchor'"];
|
|
2118
2689
|
const params = [];
|
|
2119
|
-
const since = options?.since ??
|
|
2690
|
+
const since = options?.since ?? Date.parse(`${localCalendarDate(options?.now ?? Date.now())}T00:00:00Z`);
|
|
2120
2691
|
clauses.push("uo.occurs_on >= ?");
|
|
2121
2692
|
params.push(since);
|
|
2122
2693
|
let maxOccurs = options?.until;
|
|
2123
2694
|
if (filters?.horizonDays !== void 0) {
|
|
2124
|
-
const anchor =
|
|
2695
|
+
const anchor = since;
|
|
2125
2696
|
const horizonEnd = anchor + filters.horizonDays * 864e5;
|
|
2126
2697
|
maxOccurs = maxOccurs === void 0 ? horizonEnd : Math.min(maxOccurs, horizonEnd);
|
|
2127
2698
|
}
|
|
@@ -2659,6 +3230,10 @@ function listMoments(db, filters, options) {
|
|
|
2659
3230
|
clauses.push("le.link_id = ?");
|
|
2660
3231
|
params.push(filters.linkId);
|
|
2661
3232
|
}
|
|
3233
|
+
if (options?.channel !== void 0) {
|
|
3234
|
+
clauses.push("e.channel=?");
|
|
3235
|
+
params.push(options.channel.trim().toLowerCase());
|
|
3236
|
+
}
|
|
2662
3237
|
const where = clauses.join(" AND ");
|
|
2663
3238
|
return db.prepare(
|
|
2664
3239
|
`SELECT
|
|
@@ -2666,6 +3241,7 @@ function listMoments(db, filters, options) {
|
|
|
2666
3241
|
le.link_id,
|
|
2667
3242
|
le.moment_kind,
|
|
2668
3243
|
le.occurred_at,
|
|
3244
|
+
e.occurred_on,
|
|
2669
3245
|
e.summary,
|
|
2670
3246
|
le.impact_score
|
|
2671
3247
|
FROM link_event_effects le
|
|
@@ -2678,6 +3254,7 @@ function listMoments(db, filters, options) {
|
|
|
2678
3254
|
linkId: row.link_id,
|
|
2679
3255
|
momentKind: row.moment_kind,
|
|
2680
3256
|
occurredAt: row.occurred_at,
|
|
3257
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
2681
3258
|
summary: row.summary,
|
|
2682
3259
|
impactScore: row.impact_score
|
|
2683
3260
|
}));
|
|
@@ -2706,6 +3283,10 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2706
3283
|
clauses.push(`e.type IN (${ph})`);
|
|
2707
3284
|
params.push(...options.eventTypes);
|
|
2708
3285
|
}
|
|
3286
|
+
if (options?.channel !== void 0) {
|
|
3287
|
+
clauses.push("e.channel = ?");
|
|
3288
|
+
params.push(options.channel.trim().toLowerCase());
|
|
3289
|
+
}
|
|
2709
3290
|
const where = clauses.join(" AND ");
|
|
2710
3291
|
const rows = db.prepare(
|
|
2711
3292
|
`SELECT e.id
|
|
@@ -2717,7 +3298,8 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2717
3298
|
).all(...params, limit, offset);
|
|
2718
3299
|
return batchLoadEventRecords(
|
|
2719
3300
|
db,
|
|
2720
|
-
rows.map((r) => r.id)
|
|
3301
|
+
rows.map((r) => r.id),
|
|
3302
|
+
options?.includeContent ?? false
|
|
2721
3303
|
);
|
|
2722
3304
|
}
|
|
2723
3305
|
|
|
@@ -2791,26 +3373,34 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2791
3373
|
const latest = db.prepare(
|
|
2792
3374
|
`SELECT event_id, occurred_at
|
|
2793
3375
|
FROM link_event_effects
|
|
2794
|
-
WHERE link_id = ? AND counts_for_recency = 1
|
|
3376
|
+
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 <= ?))
|
|
2795
3377
|
ORDER BY occurred_at DESC, event_id DESC
|
|
2796
3378
|
LIMIT 1`
|
|
2797
|
-
).get(linkId);
|
|
3379
|
+
).get(linkId, now, localCalendarDate(now));
|
|
2798
3380
|
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
2799
3381
|
const counts = db.prepare(
|
|
2800
3382
|
`SELECT
|
|
2801
3383
|
COUNT(*) AS total_meaningful_events,
|
|
2802
3384
|
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
2803
3385
|
FROM link_event_effects
|
|
2804
|
-
WHERE link_id = ? AND occurred_at >=
|
|
2805
|
-
|
|
3386
|
+
WHERE link_id = ? AND occurred_at >= ? AND counts_for_recency = 1
|
|
3387
|
+
AND event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
3388
|
+
).get(linkId, trailingWindowStart, now, localCalendarDate(now));
|
|
2806
3389
|
const directionalityRows = db.prepare(
|
|
2807
3390
|
`SELECT ep.directionality
|
|
2808
3391
|
FROM link_event_effects le
|
|
2809
3392
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
2810
3393
|
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
2811
3394
|
AND ep.directionality IS NOT NULL
|
|
2812
|
-
AND le.occurred_at >=
|
|
2813
|
-
|
|
3395
|
+
AND le.occurred_at >= ?
|
|
3396
|
+
AND le.event_id IN (SELECT id FROM events WHERE deleted_at IS NULL AND (occurred_at <= ? OR occurred_on <= ?))`
|
|
3397
|
+
).all(
|
|
3398
|
+
linkId,
|
|
3399
|
+
link.from_contact_id,
|
|
3400
|
+
trailingWindowStart,
|
|
3401
|
+
now,
|
|
3402
|
+
localCalendarDate(now)
|
|
3403
|
+
);
|
|
2814
3404
|
let outboundCount = 0;
|
|
2815
3405
|
let inboundCount = 0;
|
|
2816
3406
|
for (const row of directionalityRows) {
|
|
@@ -3129,21 +3719,6 @@ function listProgressionReadiness(db, filters, options) {
|
|
|
3129
3719
|
}
|
|
3130
3720
|
|
|
3131
3721
|
// src/links/list_radar.ts
|
|
3132
|
-
function deriveRadarReason(drift, recency, normalizedRank3) {
|
|
3133
|
-
if (drift > 0.5) {
|
|
3134
|
-
return "Significantly overdue for contact";
|
|
3135
|
-
}
|
|
3136
|
-
if (drift > 0.2) {
|
|
3137
|
-
return "Drifting \u2014 approaching overdue";
|
|
3138
|
-
}
|
|
3139
|
-
if (recency < 0.3 && normalizedRank3 > 0.3) {
|
|
3140
|
-
return "Important link losing recency";
|
|
3141
|
-
}
|
|
3142
|
-
if (normalizedRank3 > 0.5 && recency < 0.5) {
|
|
3143
|
-
return "High-rank link needs attention";
|
|
3144
|
-
}
|
|
3145
|
-
return "Routine check-in recommended";
|
|
3146
|
-
}
|
|
3147
3722
|
function listRadar(db, filters, options) {
|
|
3148
3723
|
assertDefaultOrdering("listRadar", options);
|
|
3149
3724
|
const ownerId = findOwnerContactId(db);
|
|
@@ -3151,7 +3726,10 @@ function listRadar(db, filters, options) {
|
|
|
3151
3726
|
return [];
|
|
3152
3727
|
}
|
|
3153
3728
|
const now = resolveNow(options?.now);
|
|
3154
|
-
const
|
|
3729
|
+
const today = localCalendarDate(now);
|
|
3730
|
+
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}'))`;
|
|
3731
|
+
const drift = `(${lastContact} IS NOT NULL AND (${now} - ${lastContact}) > 1.5 * COALESCE(l.cadence_days, 1) * 86400000)`;
|
|
3732
|
+
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))`;
|
|
3155
3733
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
3156
3734
|
const clauses = [
|
|
3157
3735
|
"l.removed_at IS NULL",
|
|
@@ -3176,10 +3754,23 @@ function listRadar(db, filters, options) {
|
|
|
3176
3754
|
if (options?.includeObserved === false) {
|
|
3177
3755
|
clauses.push("l.kind != 'observed'");
|
|
3178
3756
|
}
|
|
3757
|
+
if (options?.includeDormant !== true)
|
|
3758
|
+
clauses.push("c_to.lifecycle_state != 'dormant'");
|
|
3759
|
+
if (filters?.includeLost !== true)
|
|
3760
|
+
clauses.push("c_to.lifecycle_state != 'lost'");
|
|
3761
|
+
const other = `CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END`;
|
|
3762
|
+
clauses.push(`(${lastContact} IS NOT NULL OR ${overdue} OR NOT EXISTS (
|
|
3763
|
+
SELECT 1 FROM event_participants ep JOIN events ev ON ev.id=ep.event_id
|
|
3764
|
+
WHERE ep.contact_id=${other} AND ev.deleted_at IS NULL
|
|
3765
|
+
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'))
|
|
3766
|
+
))`);
|
|
3179
3767
|
const where = clauses.join(" AND ");
|
|
3180
3768
|
const rows = db.prepare(
|
|
3181
3769
|
`SELECT
|
|
3182
3770
|
l.id,
|
|
3771
|
+
l.cadence_days,
|
|
3772
|
+
${lastContact} AS last_contact,
|
|
3773
|
+
${drift} AS drift,
|
|
3183
3774
|
${overdue} AS overdue_commitment,
|
|
3184
3775
|
l.from_contact_id,
|
|
3185
3776
|
l.to_contact_id,
|
|
@@ -3193,23 +3784,44 @@ function listRadar(db, filters, options) {
|
|
|
3193
3784
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
3194
3785
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
3195
3786
|
WHERE ${where}
|
|
3196
|
-
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3787
|
+
ORDER BY (${overdue} OR ${drift}) DESC, ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3197
3788
|
LIMIT ? OFFSET ?`
|
|
3198
3789
|
).all(...params, limit, offset);
|
|
3199
|
-
return rows.map((row) =>
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3790
|
+
return rows.map((row) => {
|
|
3791
|
+
const contactId = row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id;
|
|
3792
|
+
const promise = row.overdue_commitment ? db.prepare(
|
|
3793
|
+
`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`
|
|
3794
|
+
).get(contactId, today, now) : void 0;
|
|
3795
|
+
const reasons = [
|
|
3796
|
+
...row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
3797
|
+
...row.drift ? ["drift"] : []
|
|
3798
|
+
];
|
|
3799
|
+
const snippets = [
|
|
3800
|
+
...promise ? [
|
|
3801
|
+
`Overdue promise: ${promise.summary} (due ${promise.due_on ?? new Date(promise.due_at).toISOString().slice(0, 10)})`
|
|
3802
|
+
] : [],
|
|
3803
|
+
...row.drift ? [
|
|
3804
|
+
`No contact for ${Math.floor((now - (row.last_contact ?? now)) / 864e5)} days (cadence ${row.cadence_days ?? 1} days)`
|
|
3805
|
+
] : []
|
|
3806
|
+
];
|
|
3807
|
+
const elapsed = row.last_contact === null ? null : Math.max(0, (now - row.last_contact) / 864e5);
|
|
3808
|
+
const currentDrift = elapsed === null ? 0 : driftPriority({
|
|
3809
|
+
daysSinceLastMeaningfulEvent: elapsed,
|
|
3810
|
+
cadenceDays: row.cadence_days ?? 1,
|
|
3811
|
+
trust: row.trust ?? 0,
|
|
3812
|
+
rank: row.rank ?? 0
|
|
3813
|
+
});
|
|
3814
|
+
return {
|
|
3815
|
+
linkId: row.id,
|
|
3816
|
+
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
3817
|
+
reasons,
|
|
3818
|
+
driftPriority: row.overdue_commitment ? Math.max(1, currentDrift) : currentDrift,
|
|
3819
|
+
recencyScore: elapsed === null ? 0 : Math.max(0, 1 - elapsed / Math.max(row.cadence_days ?? 1, 1)),
|
|
3820
|
+
normalizedRank: row.normalized_rank ?? 0,
|
|
3821
|
+
trust: row.trust ?? 0,
|
|
3822
|
+
recommendedReason: snippets.join("; ") || "Routine check-in recommended"
|
|
3823
|
+
};
|
|
3824
|
+
});
|
|
3213
3825
|
}
|
|
3214
3826
|
|
|
3215
3827
|
// src/links/receipts.ts
|
|
@@ -3292,6 +3904,9 @@ function overrideLinkState(db, linkId, state, options) {
|
|
|
3292
3904
|
now,
|
|
3293
3905
|
linkId
|
|
3294
3906
|
);
|
|
3907
|
+
db.prepare(
|
|
3908
|
+
"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=?"
|
|
3909
|
+
).run(linkId, state, linkId);
|
|
3295
3910
|
refreshLinkRollup(db, linkId, now);
|
|
3296
3911
|
if (state === "archived" || row.state === "archived") {
|
|
3297
3912
|
refreshAllBridgeScores(db, now);
|
|
@@ -3366,6 +3981,8 @@ function reviseBond(db, linkId, bond, options) {
|
|
|
3366
3981
|
// src/links/seed_social_link.ts
|
|
3367
3982
|
function seedSocialLink(db, input) {
|
|
3368
3983
|
return withTransaction(db, () => {
|
|
3984
|
+
if (input.ifExists !== void 0 && !["error", "return"].includes(input.ifExists))
|
|
3985
|
+
throw new AffinityValidationError("ifExists must be error or return");
|
|
3369
3986
|
if (!isRelationalLinkKind(input.kind)) {
|
|
3370
3987
|
throw new AffinityValidationError("invalid relational link kind");
|
|
3371
3988
|
}
|
|
@@ -3373,11 +3990,14 @@ function seedSocialLink(db, input) {
|
|
|
3373
3990
|
throw new AffinityValidationError("from and to contacts must differ");
|
|
3374
3991
|
}
|
|
3375
3992
|
assertContactEndpointsNotMerged(db, input.fromContactId, input.toContactId);
|
|
3376
|
-
|
|
3993
|
+
const existing = findLiveRelationalLinkAnyDirection(
|
|
3377
3994
|
db,
|
|
3378
3995
|
input.fromContactId,
|
|
3379
3996
|
input.toContactId
|
|
3380
|
-
)
|
|
3997
|
+
);
|
|
3998
|
+
if (existing) {
|
|
3999
|
+
if (input.ifExists === "return")
|
|
4000
|
+
return buildLinkMutationReceipt(mapLinkRowToLinkListItem(existing), {});
|
|
3381
4001
|
throw new AffinityConflictError(
|
|
3382
4002
|
"relational link already exists between these contacts"
|
|
3383
4003
|
);
|
|
@@ -4196,6 +4816,8 @@ function deriveStateAfter(params) {
|
|
|
4196
4816
|
return params.stateBefore;
|
|
4197
4817
|
}
|
|
4198
4818
|
function deriveMomentKind(params) {
|
|
4819
|
+
if (params.eventType === "observation" || params.eventType === "milestone" && params.significance < 7)
|
|
4820
|
+
return null;
|
|
4199
4821
|
if (params.rankAfter > params.rankBefore) {
|
|
4200
4822
|
return "breakthrough";
|
|
4201
4823
|
}
|
|
@@ -4225,6 +4847,18 @@ function applyEventEffectToLink(db, params) {
|
|
|
4225
4847
|
if (!link || link.is_structural !== 0) {
|
|
4226
4848
|
throw new AffinityStateError("relational link not found");
|
|
4227
4849
|
}
|
|
4850
|
+
db.prepare(
|
|
4851
|
+
"INSERT OR IGNORE INTO event_link_baselines(link_id,state_json) VALUES (?,?)"
|
|
4852
|
+
).run(
|
|
4853
|
+
params.linkId,
|
|
4854
|
+
JSON.stringify({
|
|
4855
|
+
rank: link.rank ?? 0,
|
|
4856
|
+
affinity: link.affinity ?? 0,
|
|
4857
|
+
trust: link.trust ?? 0,
|
|
4858
|
+
state: link.state ?? "active",
|
|
4859
|
+
cadence_days: link.cadence_days
|
|
4860
|
+
})
|
|
4861
|
+
);
|
|
4228
4862
|
const stateBefore = link.state ?? "active";
|
|
4229
4863
|
const linkKind = link.kind;
|
|
4230
4864
|
const rankBefore = link.rank ?? 0;
|
|
@@ -4269,7 +4903,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
4269
4903
|
);
|
|
4270
4904
|
const weightedBase = base * massPenalty(
|
|
4271
4905
|
weeklyPositiveBaseWeight(db, params.linkId, params.occurredAt)
|
|
4272
|
-
) * salienceMultiplier;
|
|
4906
|
+
) * salienceMultiplier * (params.participant.directionality === "owner_initiated" ? 0.05 : 1);
|
|
4273
4907
|
const affinityGain = weightedBase * Math.max(eventValence, 0) * (0.55 + 0.45 * eventIntimacyDepth) * (0.7 + 0.3 * participantReciprocity) * (1 / (1 + 0.22 * rankBefore));
|
|
4274
4908
|
const affinityLoss = weightedBase * Math.max(-eventValence, 0) * 0.35;
|
|
4275
4909
|
const affinityMass = clamp3(
|
|
@@ -4310,15 +4944,16 @@ function applyEventEffectToLink(db, params) {
|
|
|
4310
4944
|
const latest = db.prepare(
|
|
4311
4945
|
`SELECT occurred_at
|
|
4312
4946
|
FROM link_event_effects
|
|
4313
|
-
WHERE link_id = ?
|
|
4947
|
+
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 <= ?))
|
|
4314
4948
|
ORDER BY occurred_at DESC, event_id DESC
|
|
4315
4949
|
LIMIT 1`
|
|
4316
|
-
).get(params.linkId);
|
|
4950
|
+
).get(params.linkId, params.now, localCalendarDate(params.now));
|
|
4317
4951
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
4318
4952
|
1,
|
|
4319
4953
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
4320
4954
|
);
|
|
4321
|
-
const
|
|
4955
|
+
const eventDay = db.prepare("SELECT occurred_on FROM events WHERE id = ?").get(params.eventId)?.occurred_on;
|
|
4956
|
+
const cadenceAfter = commitmentResolution != null || params.eventType === "promise" || params.eventType === "agreement" || params.occurredAt > params.now && (eventDay == null || eventDay > localCalendarDate(params.now)) ? cadenceBefore : nextCadenceDays({
|
|
4322
4957
|
kind: linkKind,
|
|
4323
4958
|
currentCadenceDays: cadenceBefore,
|
|
4324
4959
|
gapDays
|
|
@@ -4338,7 +4973,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
4338
4973
|
}
|
|
4339
4974
|
const affinityDelta = affinityAfter - affinityBefore;
|
|
4340
4975
|
const trustDelta = trustAfter - trustBefore;
|
|
4341
|
-
const
|
|
4976
|
+
const eventLabel = db.prepare("SELECT summary FROM events WHERE id=?").get(params.eventId)?.summary;
|
|
4977
|
+
const followLike = params.eventType === "milestone" && typeof eventLabel === "string" && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
4978
|
+
eventLabel
|
|
4979
|
+
);
|
|
4980
|
+
const momentKind = followLike ? null : deriveMomentKind({
|
|
4342
4981
|
rankBefore,
|
|
4343
4982
|
rankAfter,
|
|
4344
4983
|
stateBefore,
|
|
@@ -4381,7 +5020,15 @@ function applyEventEffectToLink(db, params) {
|
|
|
4381
5020
|
params.now,
|
|
4382
5021
|
params.now
|
|
4383
5022
|
);
|
|
4384
|
-
|
|
5023
|
+
db.prepare(
|
|
5024
|
+
"UPDATE link_event_effects SET participant_json=?,provenance_json=? WHERE event_id=? AND link_id=?"
|
|
5025
|
+
).run(
|
|
5026
|
+
JSON.stringify(params.participant),
|
|
5027
|
+
params.provenance === void 0 ? null : JSON.stringify(params.provenance),
|
|
5028
|
+
params.eventId,
|
|
5029
|
+
params.linkId
|
|
5030
|
+
);
|
|
5031
|
+
if (commitmentResolution !== void 0 && commitmentResolution !== null || params.eventType === "promise" || params.eventType === "agreement" || params.participant.directionality === "owner_initiated") {
|
|
4385
5032
|
db.prepare(
|
|
4386
5033
|
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4387
5034
|
).run(params.eventId, params.linkId);
|
|
@@ -4464,15 +5111,36 @@ function ensureRelationalLink(db, params) {
|
|
|
4464
5111
|
};
|
|
4465
5112
|
}
|
|
4466
5113
|
function applyJournalEventMechanics(db, params) {
|
|
5114
|
+
const bond = db.prepare("SELECT affects_bond, channel FROM events WHERE id=?").get(params.eventId);
|
|
5115
|
+
const ambient = params.provenance && typeof params.provenance === "object" && (params.provenance.ambient === true || params.provenance.tags?.includes("ambient"));
|
|
5116
|
+
if (bond?.affects_bond === 0 || bond?.affects_bond == null && params.eventType === "observation" && params.significance <= 2 || ambient)
|
|
5117
|
+
return { created: [], affectedLinks: [], derivedEffects: [] };
|
|
4467
5118
|
const ownerId = requireOwnerContactId(db);
|
|
4468
|
-
const
|
|
5119
|
+
const owner = params.participants.find((p) => p.contactId === ownerId);
|
|
5120
|
+
const socialChannel = [
|
|
5121
|
+
"bluesky",
|
|
5122
|
+
"x",
|
|
5123
|
+
"twitter",
|
|
5124
|
+
"mastodon",
|
|
5125
|
+
"github",
|
|
5126
|
+
"instagram"
|
|
5127
|
+
].includes(String(bond?.channel));
|
|
5128
|
+
const direction = owner?.directionality ?? (socialChannel ? owner?.role === "actor" ? "owner_initiated" : owner?.role === "recipient" ? "other_initiated" : void 0 : void 0);
|
|
5129
|
+
const participants = params.participants.map(
|
|
5130
|
+
(p) => direction && p.directionality === void 0 ? { ...p, directionality: direction } : p
|
|
5131
|
+
);
|
|
5132
|
+
if (direction)
|
|
5133
|
+
db.prepare(
|
|
5134
|
+
"UPDATE event_participants SET directionality=? WHERE event_id=? AND directionality IS NULL"
|
|
5135
|
+
).run(direction, params.eventId);
|
|
5136
|
+
const participantIds = participants.map(
|
|
4469
5137
|
(participant) => participant.contactId
|
|
4470
5138
|
);
|
|
4471
5139
|
const hasOwner = participantIds.includes(ownerId);
|
|
4472
5140
|
const created = [];
|
|
4473
5141
|
const effectInputs = [];
|
|
4474
5142
|
if (hasOwner) {
|
|
4475
|
-
for (const participant of
|
|
5143
|
+
for (const participant of participants) {
|
|
4476
5144
|
if (participant.contactId === ownerId) {
|
|
4477
5145
|
continue;
|
|
4478
5146
|
}
|
|
@@ -4480,13 +5148,14 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4480
5148
|
fromContactId: ownerId,
|
|
4481
5149
|
toContactId: participant.contactId,
|
|
4482
5150
|
kind: inferAutoLinkKind(db, ownerId, participant.contactId),
|
|
5151
|
+
...(participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality) === "owner_initiated" ? { affinity: 0.15 } : {},
|
|
4483
5152
|
now: params.now
|
|
4484
5153
|
});
|
|
4485
5154
|
created.push(...ensured.created);
|
|
4486
5155
|
effectInputs.push({ linkId: ensured.linkId, participant });
|
|
4487
5156
|
}
|
|
4488
|
-
} else if (params.eventType === "observation" &&
|
|
4489
|
-
const [first, second] =
|
|
5157
|
+
} else if (params.eventType === "observation" && participants.length === 2) {
|
|
5158
|
+
const [first, second] = participants;
|
|
4490
5159
|
if (first !== void 0 && second !== void 0) {
|
|
4491
5160
|
const a = Math.min(first.contactId, second.contactId);
|
|
4492
5161
|
const b = Math.max(first.contactId, second.contactId);
|
|
@@ -4521,7 +5190,10 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4521
5190
|
eventType: params.eventType,
|
|
4522
5191
|
occurredAt: params.occurredAt,
|
|
4523
5192
|
significance: params.significance,
|
|
4524
|
-
participant:
|
|
5193
|
+
participant: {
|
|
5194
|
+
...input.participant,
|
|
5195
|
+
directionality: input.participant.directionality ?? participants.find((p) => p.contactId === ownerId)?.directionality
|
|
5196
|
+
},
|
|
4525
5197
|
provenance: params.provenance,
|
|
4526
5198
|
now: params.now
|
|
4527
5199
|
})
|
|
@@ -4545,6 +5217,8 @@ function applyJournalEventMechanics(db, params) {
|
|
|
4545
5217
|
function recordCommitment(db, input) {
|
|
4546
5218
|
return withTransaction(db, () => {
|
|
4547
5219
|
validateSocialEventInput(input);
|
|
5220
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5221
|
+
if (existing) return existing;
|
|
4548
5222
|
if (input.commitmentType !== "promise" && input.commitmentType !== "agreement") {
|
|
4549
5223
|
throw new AffinityValidationError(
|
|
4550
5224
|
"commitmentType must be promise or agreement"
|
|
@@ -4559,11 +5233,12 @@ function recordCommitment(db, input) {
|
|
|
4559
5233
|
assertOwnerParticipates(ownerId, ids);
|
|
4560
5234
|
const now = resolveNow(input.now);
|
|
4561
5235
|
const eventId = insertJournalEvent(db, {
|
|
5236
|
+
...input,
|
|
4562
5237
|
type: input.commitmentType,
|
|
4563
5238
|
occurredAt: input.occurredAt,
|
|
4564
|
-
|
|
5239
|
+
occurredOn: input.occurredOn,
|
|
4565
5240
|
significance: input.significance,
|
|
4566
|
-
momentKind: null,
|
|
5241
|
+
momentKind: input.momentKind ?? null,
|
|
4567
5242
|
participants: input.participants,
|
|
4568
5243
|
now
|
|
4569
5244
|
});
|
|
@@ -4572,15 +5247,25 @@ function recordCommitment(db, input) {
|
|
|
4572
5247
|
event_id, commitment_type, due_at, created_at, updated_at
|
|
4573
5248
|
) VALUES (?, ?, ?, ?, ?)`
|
|
4574
5249
|
).run(eventId, input.commitmentType, input.dueAt ?? null, now, now);
|
|
5250
|
+
db.prepare("UPDATE open_commitments SET due_on = ? WHERE event_id = ?").run(
|
|
5251
|
+
input.dueAt == null ? null : input.dueOn ?? new Date(input.dueAt).toISOString().slice(0, 10),
|
|
5252
|
+
eventId
|
|
5253
|
+
);
|
|
4575
5254
|
const mechanics = applyJournalEventMechanics(db, {
|
|
4576
5255
|
eventId,
|
|
4577
5256
|
eventType: input.commitmentType,
|
|
4578
5257
|
occurredAt: input.occurredAt,
|
|
5258
|
+
occurredOn: input.occurredOn,
|
|
4579
5259
|
significance: input.significance,
|
|
4580
5260
|
participants: input.participants,
|
|
4581
5261
|
provenance: input.provenance,
|
|
4582
5262
|
now
|
|
4583
5263
|
});
|
|
5264
|
+
if (input.momentKind !== void 0)
|
|
5265
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5266
|
+
input.momentKind,
|
|
5267
|
+
eventId
|
|
5268
|
+
);
|
|
4584
5269
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
4585
5270
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4586
5271
|
affectedLinks: mechanics.affectedLinks,
|
|
@@ -4601,6 +5286,8 @@ var ALLOWED = /* @__PURE__ */ new Set([
|
|
|
4601
5286
|
function recordInteraction(db, input) {
|
|
4602
5287
|
return withTransaction(db, () => {
|
|
4603
5288
|
validateSocialEventInput(input);
|
|
5289
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5290
|
+
if (existing) return existing;
|
|
4604
5291
|
if (!ALLOWED.has(input.type)) {
|
|
4605
5292
|
throw new AffinityValidationError("invalid interaction event type");
|
|
4606
5293
|
}
|
|
@@ -4610,11 +5297,12 @@ function recordInteraction(db, input) {
|
|
|
4610
5297
|
assertOwnerParticipates(ownerId, ids);
|
|
4611
5298
|
const now = resolveNow(input.now);
|
|
4612
5299
|
const eventId = insertJournalEvent(db, {
|
|
5300
|
+
...input,
|
|
4613
5301
|
type: input.type,
|
|
4614
5302
|
occurredAt: input.occurredAt,
|
|
4615
|
-
|
|
5303
|
+
occurredOn: input.occurredOn,
|
|
4616
5304
|
significance: input.significance,
|
|
4617
|
-
momentKind: null,
|
|
5305
|
+
momentKind: input.momentKind ?? null,
|
|
4618
5306
|
participants: input.participants,
|
|
4619
5307
|
now
|
|
4620
5308
|
});
|
|
@@ -4622,12 +5310,24 @@ function recordInteraction(db, input) {
|
|
|
4622
5310
|
eventId,
|
|
4623
5311
|
eventType: input.type,
|
|
4624
5312
|
occurredAt: input.occurredAt,
|
|
5313
|
+
occurredOn: input.occurredOn,
|
|
4625
5314
|
significance: input.significance,
|
|
4626
5315
|
participants: input.participants,
|
|
4627
5316
|
provenance: input.provenance,
|
|
4628
5317
|
now
|
|
4629
5318
|
});
|
|
5319
|
+
if (input.momentKind !== void 0)
|
|
5320
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5321
|
+
input.momentKind,
|
|
5322
|
+
eventId
|
|
5323
|
+
);
|
|
4630
5324
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5325
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5326
|
+
{
|
|
5327
|
+
code: "future_event",
|
|
5328
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5329
|
+
}
|
|
5330
|
+
] : void 0,
|
|
4631
5331
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4632
5332
|
affectedLinks: mechanics.affectedLinks,
|
|
4633
5333
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4639,17 +5339,20 @@ function recordInteraction(db, input) {
|
|
|
4639
5339
|
function recordMilestone(db, input) {
|
|
4640
5340
|
return withTransaction(db, () => {
|
|
4641
5341
|
validateSocialEventInput(input);
|
|
5342
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5343
|
+
if (existing) return existing;
|
|
4642
5344
|
const ownerId = requireOwnerContactId(db);
|
|
4643
5345
|
const ids = input.participants.map((p) => p.contactId);
|
|
4644
5346
|
assertParticipantContactsLive(db, ids);
|
|
4645
5347
|
assertOwnerParticipates(ownerId, ids);
|
|
4646
5348
|
const now = resolveNow(input.now);
|
|
4647
5349
|
const eventId = insertJournalEvent(db, {
|
|
5350
|
+
...input,
|
|
4648
5351
|
type: "milestone",
|
|
4649
5352
|
occurredAt: input.occurredAt,
|
|
4650
|
-
|
|
5353
|
+
occurredOn: input.occurredOn,
|
|
4651
5354
|
significance: input.significance,
|
|
4652
|
-
momentKind: null,
|
|
5355
|
+
momentKind: input.momentKind ?? null,
|
|
4653
5356
|
participants: input.participants,
|
|
4654
5357
|
now
|
|
4655
5358
|
});
|
|
@@ -4657,12 +5360,24 @@ function recordMilestone(db, input) {
|
|
|
4657
5360
|
eventId,
|
|
4658
5361
|
eventType: "milestone",
|
|
4659
5362
|
occurredAt: input.occurredAt,
|
|
5363
|
+
occurredOn: input.occurredOn,
|
|
4660
5364
|
significance: input.significance,
|
|
4661
5365
|
participants: input.participants,
|
|
4662
5366
|
provenance: input.provenance,
|
|
4663
5367
|
now
|
|
4664
5368
|
});
|
|
5369
|
+
if (input.momentKind !== void 0)
|
|
5370
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5371
|
+
input.momentKind,
|
|
5372
|
+
eventId
|
|
5373
|
+
);
|
|
4665
5374
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5375
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5376
|
+
{
|
|
5377
|
+
code: "future_event",
|
|
5378
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5379
|
+
}
|
|
5380
|
+
] : void 0,
|
|
4666
5381
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4667
5382
|
affectedLinks: mechanics.affectedLinks,
|
|
4668
5383
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4674,15 +5389,18 @@ function recordMilestone(db, input) {
|
|
|
4674
5389
|
function recordObservation(db, input) {
|
|
4675
5390
|
return withTransaction(db, () => {
|
|
4676
5391
|
validateSocialEventInput(input);
|
|
5392
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5393
|
+
if (existing) return existing;
|
|
4677
5394
|
const ids = input.participants.map((p) => p.contactId);
|
|
4678
5395
|
assertParticipantContactsLive(db, ids);
|
|
4679
5396
|
const now = resolveNow(input.now);
|
|
4680
5397
|
const eventId = insertJournalEvent(db, {
|
|
5398
|
+
...input,
|
|
4681
5399
|
type: "observation",
|
|
4682
5400
|
occurredAt: input.occurredAt,
|
|
4683
|
-
|
|
5401
|
+
occurredOn: input.occurredOn,
|
|
4684
5402
|
significance: input.significance,
|
|
4685
|
-
momentKind: null,
|
|
5403
|
+
momentKind: input.momentKind ?? null,
|
|
4686
5404
|
participants: input.participants,
|
|
4687
5405
|
now
|
|
4688
5406
|
});
|
|
@@ -4690,12 +5408,24 @@ function recordObservation(db, input) {
|
|
|
4690
5408
|
eventId,
|
|
4691
5409
|
eventType: "observation",
|
|
4692
5410
|
occurredAt: input.occurredAt,
|
|
5411
|
+
occurredOn: input.occurredOn,
|
|
4693
5412
|
significance: input.significance,
|
|
4694
5413
|
participants: input.participants,
|
|
4695
5414
|
provenance: input.provenance,
|
|
4696
5415
|
now
|
|
4697
5416
|
});
|
|
5417
|
+
if (input.momentKind !== void 0)
|
|
5418
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5419
|
+
input.momentKind,
|
|
5420
|
+
eventId
|
|
5421
|
+
);
|
|
4698
5422
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5423
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5424
|
+
{
|
|
5425
|
+
code: "future_event",
|
|
5426
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5427
|
+
}
|
|
5428
|
+
] : void 0,
|
|
4699
5429
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4700
5430
|
affectedLinks: mechanics.affectedLinks,
|
|
4701
5431
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4707,17 +5437,20 @@ function recordObservation(db, input) {
|
|
|
4707
5437
|
function recordTransaction(db, input) {
|
|
4708
5438
|
return withTransaction(db, () => {
|
|
4709
5439
|
validateSocialEventInput(input);
|
|
5440
|
+
const existing = existingEventReceipt(db, input.externalRef);
|
|
5441
|
+
if (existing) return existing;
|
|
4710
5442
|
const ownerId = requireOwnerContactId(db);
|
|
4711
5443
|
const ids = input.participants.map((p) => p.contactId);
|
|
4712
5444
|
assertParticipantContactsLive(db, ids);
|
|
4713
5445
|
assertOwnerParticipates(ownerId, ids);
|
|
4714
5446
|
const now = resolveNow(input.now);
|
|
4715
5447
|
const eventId = insertJournalEvent(db, {
|
|
5448
|
+
...input,
|
|
4716
5449
|
type: "transaction",
|
|
4717
5450
|
occurredAt: input.occurredAt,
|
|
4718
|
-
|
|
5451
|
+
occurredOn: input.occurredOn,
|
|
4719
5452
|
significance: input.significance,
|
|
4720
|
-
momentKind: null,
|
|
5453
|
+
momentKind: input.momentKind ?? null,
|
|
4721
5454
|
participants: input.participants,
|
|
4722
5455
|
now
|
|
4723
5456
|
});
|
|
@@ -4725,12 +5458,24 @@ function recordTransaction(db, input) {
|
|
|
4725
5458
|
eventId,
|
|
4726
5459
|
eventType: "transaction",
|
|
4727
5460
|
occurredAt: input.occurredAt,
|
|
5461
|
+
occurredOn: input.occurredOn,
|
|
4728
5462
|
significance: input.significance,
|
|
4729
5463
|
participants: input.participants,
|
|
4730
5464
|
provenance: input.provenance,
|
|
4731
5465
|
now
|
|
4732
5466
|
});
|
|
5467
|
+
if (input.momentKind !== void 0)
|
|
5468
|
+
db.prepare("UPDATE events SET moment_kind=? WHERE id=?").run(
|
|
5469
|
+
input.momentKind,
|
|
5470
|
+
eventId
|
|
5471
|
+
);
|
|
4733
5472
|
return buildEventMutationReceipt(loadEventRecord(db, eventId), {
|
|
5473
|
+
warnings: input.occurredAt > now && (input.occurredOn ?? new Date(input.occurredAt).toISOString().slice(0, 10)) > localCalendarDate(now) ? [
|
|
5474
|
+
{
|
|
5475
|
+
code: "future_event",
|
|
5476
|
+
message: "occurredAt is in the future; it will not count as contact until then"
|
|
5477
|
+
}
|
|
5478
|
+
] : void 0,
|
|
4734
5479
|
created: [{ kind: "event", id: eventId }, ...mechanics.created],
|
|
4735
5480
|
affectedLinks: mechanics.affectedLinks,
|
|
4736
5481
|
derivedEffects: mechanics.derivedEffects
|
|
@@ -4803,6 +5548,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4803
5548
|
const occurredAt = options?.resolvingEvent?.occurredAt ?? now;
|
|
4804
5549
|
validateSocialEventInput({
|
|
4805
5550
|
occurredAt,
|
|
5551
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4806
5552
|
summary,
|
|
4807
5553
|
significance,
|
|
4808
5554
|
participants,
|
|
@@ -4819,6 +5565,7 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4819
5565
|
const resolvingEventId = insertJournalEvent(db, {
|
|
4820
5566
|
type: eventType,
|
|
4821
5567
|
occurredAt,
|
|
5568
|
+
...options?.resolvingEvent?.occurredOn ? { occurredOn: options.resolvingEvent.occurredOn } : {},
|
|
4822
5569
|
summary,
|
|
4823
5570
|
significance,
|
|
4824
5571
|
momentKind: null,
|
|
@@ -4852,30 +5599,46 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4852
5599
|
});
|
|
4853
5600
|
}
|
|
4854
5601
|
|
|
4855
|
-
// src/contacts/
|
|
4856
|
-
function
|
|
4857
|
-
|
|
5602
|
+
// src/contacts/export_rows.ts
|
|
5603
|
+
function exportRows(db, sql, ...params) {
|
|
5604
|
+
return db.prepare(sql).all(...params).map((row) => {
|
|
4858
5605
|
const result = {};
|
|
4859
5606
|
for (const [key, value] of Object.entries(row)) {
|
|
4860
5607
|
const name = key.replace(
|
|
4861
5608
|
/_([a-z])/g,
|
|
4862
5609
|
(_, letter) => letter.toUpperCase()
|
|
4863
5610
|
);
|
|
4864
|
-
result[name] =
|
|
4865
|
-
|
|
5611
|
+
result[name] = [
|
|
5612
|
+
"affects_bond",
|
|
5613
|
+
"is_owner",
|
|
5614
|
+
"is_structural",
|
|
5615
|
+
"verified",
|
|
5616
|
+
"counts_for_recency",
|
|
5617
|
+
"manual"
|
|
5618
|
+
].includes(key) ? Boolean(value) : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : value;
|
|
5619
|
+
if (name.endsWith("At") && typeof value === "number")
|
|
4866
5620
|
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4867
5621
|
if (key.endsWith("_json") && typeof value === "string")
|
|
4868
5622
|
result[name] = JSON.parse(value);
|
|
4869
5623
|
}
|
|
4870
5624
|
return result;
|
|
4871
5625
|
});
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
// src/contacts/export_contact.ts
|
|
5629
|
+
function exportContact(db, id) {
|
|
5630
|
+
const query = (sql, ...params) => exportRows(db, sql, ...params);
|
|
4872
5631
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4873
5632
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4874
5633
|
const events = query(
|
|
4875
|
-
"SELECT DISTINCT e
|
|
5634
|
+
"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",
|
|
4876
5635
|
id
|
|
4877
5636
|
).map((event) => ({
|
|
4878
5637
|
...event,
|
|
5638
|
+
attributes: query(
|
|
5639
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5640
|
+
event.id
|
|
5641
|
+
),
|
|
4879
5642
|
participants: query(
|
|
4880
5643
|
"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",
|
|
4881
5644
|
event.id
|
|
@@ -4920,14 +5683,100 @@ function exportContact(db, id) {
|
|
|
4920
5683
|
// src/contacts/export_affinity.ts
|
|
4921
5684
|
function exportAffinity(db) {
|
|
4922
5685
|
const now = Date.now();
|
|
5686
|
+
const records = db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id));
|
|
5687
|
+
const events = exportRows(
|
|
5688
|
+
db,
|
|
5689
|
+
"SELECT *, CAST(content AS BLOB) AS content, CAST(summary AS BLOB) AS summary FROM events ORDER BY id"
|
|
5690
|
+
).map((event) => ({
|
|
5691
|
+
...event,
|
|
5692
|
+
attributes: exportRows(
|
|
5693
|
+
db,
|
|
5694
|
+
"SELECT * FROM attributes WHERE event_id=? ORDER BY id",
|
|
5695
|
+
event.id
|
|
5696
|
+
),
|
|
5697
|
+
participants: exportRows(
|
|
5698
|
+
db,
|
|
5699
|
+
"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",
|
|
5700
|
+
event.id
|
|
5701
|
+
)
|
|
5702
|
+
}));
|
|
4923
5703
|
return {
|
|
4924
5704
|
version: 1,
|
|
4925
5705
|
exportedAt: now,
|
|
4926
5706
|
exportedAtIso: new Date(now).toISOString(),
|
|
4927
|
-
|
|
5707
|
+
events,
|
|
5708
|
+
contacts: records.map(({ events: events2, dates, ...record }) => ({
|
|
5709
|
+
...record,
|
|
5710
|
+
eventIds: events2.map((event) => event.id),
|
|
5711
|
+
dateIds: dates.map((date) => date.id)
|
|
5712
|
+
}))
|
|
4928
5713
|
};
|
|
4929
5714
|
}
|
|
4930
5715
|
|
|
5716
|
+
// src/events/event_discovery.ts
|
|
5717
|
+
function searchEvents(db, query, options) {
|
|
5718
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5719
|
+
const tokens = foldSearchText(query).split(/\s+/).filter(Boolean);
|
|
5720
|
+
if (!tokens.length) return [];
|
|
5721
|
+
const rows = db.prepare(
|
|
5722
|
+
"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"
|
|
5723
|
+
).all();
|
|
5724
|
+
return rows.flatMap((row) => {
|
|
5725
|
+
const fields = {
|
|
5726
|
+
summary: decodeEventText(row.summary),
|
|
5727
|
+
content: row.content === null ? null : decodeEventText(row.content),
|
|
5728
|
+
source: row.external_source,
|
|
5729
|
+
id: row.external_id,
|
|
5730
|
+
channel: row.channel
|
|
5731
|
+
};
|
|
5732
|
+
const folded = Object.entries(fields).map(
|
|
5733
|
+
([key, value]) => [key, foldSearchText(value ?? "")]
|
|
5734
|
+
);
|
|
5735
|
+
if (!tokens.every((t) => folded.some(([, v]) => v.includes(t))))
|
|
5736
|
+
return [];
|
|
5737
|
+
return [
|
|
5738
|
+
{
|
|
5739
|
+
kind: "event",
|
|
5740
|
+
id: row.id,
|
|
5741
|
+
type: row.type,
|
|
5742
|
+
title: deriveEventSummary(decodeEventText(row.summary)),
|
|
5743
|
+
occurredOn: row.occurred_on ?? new Date(row.occurred_at).toISOString().slice(0, 10),
|
|
5744
|
+
matchedOn: folded.filter(([, v]) => tokens.some((t) => v.includes(t))).map(([k]) => k),
|
|
5745
|
+
...row.external_source && row.external_id ? {
|
|
5746
|
+
externalRef: {
|
|
5747
|
+
source: row.external_source,
|
|
5748
|
+
id: row.external_id
|
|
5749
|
+
}
|
|
5750
|
+
} : {}
|
|
5751
|
+
}
|
|
5752
|
+
];
|
|
5753
|
+
}).slice(offset, offset + limit);
|
|
5754
|
+
}
|
|
5755
|
+
function getEventThread(db, source, id, options) {
|
|
5756
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
5757
|
+
const root = getEventByExternalRef(db, source, id);
|
|
5758
|
+
if (!root) return [];
|
|
5759
|
+
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
5760
|
+
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
5761
|
+
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=?))
|
|
5762
|
+
) 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(
|
|
5763
|
+
root.id,
|
|
5764
|
+
root.externalRef?.source ?? null,
|
|
5765
|
+
root.externalRef?.id ?? null,
|
|
5766
|
+
options?.channel ?? null,
|
|
5767
|
+
options?.channel?.trim().toLowerCase() ?? null,
|
|
5768
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5769
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5770
|
+
limit,
|
|
5771
|
+
offset
|
|
5772
|
+
);
|
|
5773
|
+
return batchLoadEventRecords(
|
|
5774
|
+
db,
|
|
5775
|
+
rows.map((r) => r.id),
|
|
5776
|
+
options?.includeContent ?? false
|
|
5777
|
+
);
|
|
5778
|
+
}
|
|
5779
|
+
|
|
4931
5780
|
// src/soul.ts
|
|
4932
5781
|
var soul_exports = {};
|
|
4933
5782
|
__export(soul_exports, {
|
|
@@ -5134,6 +5983,15 @@ var identifyAndLocateContactsSkill = defineAffinitySkill({
|
|
|
5134
5983
|
description: "Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.",
|
|
5135
5984
|
content: `# Identify And Locate Contacts
|
|
5136
5985
|
|
|
5986
|
+
Handle identities normalize leading @ and case. Bluesky uses a domain handle,
|
|
5987
|
+
X/Twitter uses 1\u201315 letters/digits/underscores, Mastodon uses user@host.tld.
|
|
5988
|
+
Native did (alias atproto_did) validates did:<method>:<identifier>; legacy
|
|
5989
|
+
custom:atproto-did locators find the same identity. Keep old handles as identities.
|
|
5990
|
+
An owned identity makes contact creation fail atomically with conflict and
|
|
5991
|
+
existingContactId. input.ifExists="return" returns the matching contact and
|
|
5992
|
+
matchedIdentities without attaching any free identities. CLI: --if-exists return.
|
|
5993
|
+
Name-only possible duplicates still warn. Search @handle is always literal.
|
|
5994
|
+
|
|
5137
5995
|
Primary tools:
|
|
5138
5996
|
- \`search_affinity\`
|
|
5139
5997
|
- \`inspect_affinity_item\`
|
|
@@ -5191,6 +6049,31 @@ var importHistoryWithoutFakingEvidenceSkill = defineAffinitySkill({
|
|
|
5191
6049
|
description: "Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.",
|
|
5192
6050
|
content: `# Import History Without Faking Evidence
|
|
5193
6051
|
|
|
6052
|
+
For social imports, use native did identities (atproto_did is an alias; legacy
|
|
6053
|
+
custom:atproto-did locators remain equivalent). Create with input.ifExists="return"
|
|
6054
|
+
for safe retries: this attaches nothing to an existing contact. For handle changes,
|
|
6055
|
+
call manage_identity add separately with input.ifExists="return"; an identity already
|
|
6056
|
+
on that contact is a no-op, while another owner still conflicts. Seed links with
|
|
6057
|
+
input.ifExists="return" to reuse them without changing their state.
|
|
6058
|
+
Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
|
|
6059
|
+
{op:"event",kind:"interaction",input:{...}}. Per-line savepoints isolate failures;
|
|
6060
|
+
--stop-on-error stops early. Exit 2 identifies malformed JSON; other failures exit 1.
|
|
6061
|
+
Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
|
|
6062
|
+
Participant contact locators may use identities, including did, instead of ids.
|
|
6063
|
+
Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
|
|
6064
|
+
SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
|
|
6065
|
+
Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
|
|
6066
|
+
The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
|
|
6067
|
+
show the mapping and verify an idempotent retry and byte-identical text.
|
|
6068
|
+
|
|
6069
|
+
For archived messages with known timestamps, record actual evidence with externalRef:
|
|
6070
|
+
{source, id} deduplicates retries across event families and commitments. Keep original
|
|
6071
|
+
message text in content, use channel and parentRef/threadRef for context, and keep
|
|
6072
|
+
summary short or let it derive from content. Content is limited to 1 MB UTF-8;
|
|
6073
|
+
keep larger raw archives in the source system. Inspect events by external reference,
|
|
6074
|
+
search scope "events", and use manage_event revise/delete to correct imports.
|
|
6075
|
+
See the tools guide for field details. Existing 0.5.x databases upgrade in place.
|
|
6076
|
+
|
|
5194
6077
|
Primary tools:
|
|
5195
6078
|
- \`manage_contact\`
|
|
5196
6079
|
- \`manage_identity\`
|
|
@@ -5247,6 +6130,14 @@ var managePromisesAndAgreementsSkill = defineAffinitySkill({
|
|
|
5247
6130
|
description: "Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.",
|
|
5248
6131
|
content: `# Manage Promises And Agreements
|
|
5249
6132
|
|
|
6133
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6134
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6135
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6136
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6137
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6138
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6139
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6140
|
+
|
|
5250
6141
|
Primary tools:
|
|
5251
6142
|
- \`manage_commitment\`
|
|
5252
6143
|
- \`review_affinity\`
|
|
@@ -5302,6 +6193,14 @@ var manageRecurringDatesAndRemindersSkill = defineAffinitySkill({
|
|
|
5302
6193
|
description: "Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.",
|
|
5303
6194
|
content: `# Manage Recurring Dates And Reminders
|
|
5304
6195
|
|
|
6196
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6197
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6198
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6199
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6200
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6201
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6202
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6203
|
+
|
|
5305
6204
|
Primary tools:
|
|
5306
6205
|
- \`manage_date_anchor\`
|
|
5307
6206
|
- \`review_affinity\`
|
|
@@ -5413,6 +6312,24 @@ var recordDirectEvidenceWellSkill = defineAffinitySkill({
|
|
|
5413
6312
|
description: "Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.",
|
|
5414
6313
|
content: `# Record Direct Evidence Well
|
|
5415
6314
|
|
|
6315
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6316
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d). Date-only inputs
|
|
6317
|
+
preserve the calendar day at UTC midnight (UTC end of day for dueAt).
|
|
6318
|
+
Keywords use the process local date. Read occurredOn/dueOn and review occursOn
|
|
6319
|
+
for calendar dates; *Iso fields remain UTC. Future events carry future_event
|
|
6320
|
+
warnings and do not count for recency, cadence or drift until their time and day
|
|
6321
|
+
arrive. Commitments and resolutions never reset contact recency or cadence.
|
|
6322
|
+
|
|
6323
|
+
Imported text and event maintenance:
|
|
6324
|
+
- Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
|
|
6325
|
+
- Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.
|
|
6326
|
+
- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId} or {kind: "event", ref: {source, id}}. Unchanged values return no_op.
|
|
6327
|
+
- Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
|
|
6328
|
+
- Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
|
|
6329
|
+
- Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.
|
|
6330
|
+
- Use manage_event delete with confirm: true to remove incorrect evidence and recompute affected relationship scores.
|
|
6331
|
+
- Send large payloads through whole-payload stdin or @file rather than argv. For example, affinity event record < payload.json or affinity call record_event - < payload.json.
|
|
6332
|
+
|
|
5416
6333
|
Primary tools:
|
|
5417
6334
|
- \`record_event\`
|
|
5418
6335
|
- \`inspect_affinity_item\`
|
|
@@ -5471,6 +6388,17 @@ var recordObservationsAndReferralsSkill = defineAffinitySkill({
|
|
|
5471
6388
|
description: "Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.",
|
|
5472
6389
|
content: `# Record Observations And Referrals
|
|
5473
6390
|
|
|
6391
|
+
Likes, reposts and follows are ambient observations, not milestones. Record them
|
|
6392
|
+
with affectsBond:false. Like/follow/repost labels default to false across families;
|
|
6393
|
+
explicit true opts in. Observations with significance <=2 also default to false.
|
|
6394
|
+
They stay in journals but do not change affinity or meaningful-contact recency.
|
|
6395
|
+
provenance.ambient:true also suppresses effects. Use explicit owner and other
|
|
6396
|
+
participants, actor/recipient roles, and the correct directionality.
|
|
6397
|
+
Unanswered owner_initiated evidence contributes much less and does not reset
|
|
6398
|
+
radar recency. A relationship with only outbound activity gets no routine check-in
|
|
6399
|
+
or drift prompt. other_initiated evidence counts fully. Observations and follow-like
|
|
6400
|
+
milestones never derive breakthroughs; low-significance milestones derive no moment.
|
|
6401
|
+
|
|
5474
6402
|
Primary tools:
|
|
5475
6403
|
- \`record_event\`
|
|
5476
6404
|
- \`review_affinity\`
|
|
@@ -5580,6 +6508,19 @@ var reviewRadarProgressionAndGraphSkill = defineAffinitySkill({
|
|
|
5580
6508
|
description: "Use the review surfaces to prioritize maintenance, understand progression readiness, and read the shape of the relationship graph without guessing from raw rows.",
|
|
5581
6509
|
content: `# Review Radar, Progression, And Graph
|
|
5582
6510
|
|
|
6511
|
+
List reviews accept limit (default 50, maximum 250) and offset. Results include
|
|
6512
|
+
total and hasMore; continue paging while hasMore is true. Event type array flags
|
|
6513
|
+
accept JSON arrays, comma lists, or repeated --event-types flags; invalid types
|
|
6514
|
+
report allowed values with exit 2. Use skill --short for a compact CLI reference.
|
|
6515
|
+
|
|
6516
|
+
CLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,
|
|
6517
|
+
today/yesterday/tomorrow and signed day offsets (+3d/-2d).
|
|
6518
|
+
Date-only values keep their day with UTC boundaries; keywords use the process
|
|
6519
|
+
local date. Read occurredOn/dueOn and review occursOn for calendar dates;
|
|
6520
|
+
*Iso fields stay UTC. Future events warn and count as contact only when their
|
|
6521
|
+
time and day arrive. Commitments and resolutions never reset recency/cadence.
|
|
6522
|
+
Radar explains overdue promises and drift (gap > 1.5 times cadence).
|
|
6523
|
+
|
|
5583
6524
|
Primary tools:
|
|
5584
6525
|
- \`review_affinity\`
|
|
5585
6526
|
- \`inspect_affinity_item\`
|
|
@@ -5680,6 +6621,9 @@ __export(tools_exports, {
|
|
|
5680
6621
|
manageDuplicateDismissalTool: () => manageDuplicateDismissalTool,
|
|
5681
6622
|
manageDuplicateDismissalToolHandler: () => manageDuplicateDismissalToolHandler,
|
|
5682
6623
|
manageDuplicateDismissalToolName: () => manageDuplicateDismissalToolName,
|
|
6624
|
+
manageEventTool: () => manageEventTool,
|
|
6625
|
+
manageEventToolHandler: () => manageEventToolHandler,
|
|
6626
|
+
manageEventToolName: () => manageEventToolName,
|
|
5683
6627
|
manageIdentityTool: () => manageIdentityTool,
|
|
5684
6628
|
manageIdentityToolHandler: () => manageIdentityToolHandler,
|
|
5685
6629
|
manageIdentityToolName: () => manageIdentityToolName,
|
|
@@ -5730,37 +6674,8 @@ __export(tools_exports, {
|
|
|
5730
6674
|
});
|
|
5731
6675
|
|
|
5732
6676
|
// src/tools/parse_time_input.ts
|
|
5733
|
-
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, today, tomorrow, yesterday, +3d or -2d";
|
|
5734
6677
|
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
5735
|
-
|
|
5736
|
-
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
5737
|
-
};
|
|
5738
|
-
if (typeof value === "number")
|
|
5739
|
-
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
5740
|
-
if (typeof value !== "string") return fail();
|
|
5741
|
-
const text = value.trim();
|
|
5742
|
-
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
5743
|
-
let day;
|
|
5744
|
-
if (relative) {
|
|
5745
|
-
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
5746
|
-
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
5747
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
5748
|
-
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
5749
|
-
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
5750
|
-
return fail();
|
|
5751
|
-
} else {
|
|
5752
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
5753
|
-
text
|
|
5754
|
-
))
|
|
5755
|
-
return fail();
|
|
5756
|
-
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
5757
|
-
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
5758
|
-
return fail();
|
|
5759
|
-
const parsed = Date.parse(text);
|
|
5760
|
-
return Number.isFinite(parsed) ? parsed : fail();
|
|
5761
|
-
}
|
|
5762
|
-
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
5763
|
-
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
6678
|
+
return parseCalendarTime(value, field, now).timestamp;
|
|
5764
6679
|
}
|
|
5765
6680
|
|
|
5766
6681
|
// src/tools/tool_metadata.ts
|
|
@@ -5844,6 +6759,39 @@ function oneOfSchema(schemas, description) {
|
|
|
5844
6759
|
return schema;
|
|
5845
6760
|
}
|
|
5846
6761
|
|
|
6762
|
+
// src/tools/event_metadata_schema.ts
|
|
6763
|
+
var externalRefSchema = objectSchema(
|
|
6764
|
+
{
|
|
6765
|
+
source: stringSchema("External source; trimmed and lowercased."),
|
|
6766
|
+
id: stringSchema("Non-empty external id.")
|
|
6767
|
+
},
|
|
6768
|
+
["source", "id"]
|
|
6769
|
+
);
|
|
6770
|
+
var eventMetadataSchema = {
|
|
6771
|
+
affectsBond: booleanSchema(
|
|
6772
|
+
"Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
|
|
6773
|
+
),
|
|
6774
|
+
content: stringSchema(
|
|
6775
|
+
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
6776
|
+
),
|
|
6777
|
+
externalRef: externalRefSchema,
|
|
6778
|
+
channel: stringSchema(
|
|
6779
|
+
"Free-text channel, trimmed and lowercased, at most 40 characters."
|
|
6780
|
+
),
|
|
6781
|
+
parentRef: externalRefSchema,
|
|
6782
|
+
parentEventId: integerSchema("Parent event id."),
|
|
6783
|
+
threadRef: externalRefSchema,
|
|
6784
|
+
momentKind: enumSchema("Optional moment kind.", [
|
|
6785
|
+
"breakthrough",
|
|
6786
|
+
"rupture",
|
|
6787
|
+
"reconciliation",
|
|
6788
|
+
"milestone",
|
|
6789
|
+
"turning_point"
|
|
6790
|
+
]),
|
|
6791
|
+
occurredOn: stringSchema("Calendar day."),
|
|
6792
|
+
provenance: {}
|
|
6793
|
+
};
|
|
6794
|
+
|
|
5847
6795
|
// src/tools/tool_names.ts
|
|
5848
6796
|
var searchAffinityToolName = "search_affinity";
|
|
5849
6797
|
var reviewAffinityToolName = "review_affinity";
|
|
@@ -5857,6 +6805,7 @@ var recordEventToolName = "record_event";
|
|
|
5857
6805
|
var manageCommitmentToolName = "manage_commitment";
|
|
5858
6806
|
var manageDateAnchorToolName = "manage_date_anchor";
|
|
5859
6807
|
var manageAttributeToolName = "manage_attribute";
|
|
6808
|
+
var manageEventToolName = "manage_event";
|
|
5860
6809
|
|
|
5861
6810
|
// src/tools/tool_ref.ts
|
|
5862
6811
|
function contactState(value) {
|
|
@@ -5916,7 +6865,7 @@ function toEventEntityRef(value) {
|
|
|
5916
6865
|
return {
|
|
5917
6866
|
kind: "event",
|
|
5918
6867
|
id: value.id,
|
|
5919
|
-
title: value.summary,
|
|
6868
|
+
title: deriveEventSummary(value.summary),
|
|
5920
6869
|
subtitle: value.type,
|
|
5921
6870
|
state: value.type
|
|
5922
6871
|
};
|
|
@@ -5964,16 +6913,6 @@ function toReceiptEntityRefs(receipt) {
|
|
|
5964
6913
|
});
|
|
5965
6914
|
}
|
|
5966
6915
|
|
|
5967
|
-
// src/identities/normalize.ts
|
|
5968
|
-
function normalizeIdentityKey(type, value) {
|
|
5969
|
-
const t = type.trim().toLowerCase();
|
|
5970
|
-
const v = value.trim().toLowerCase();
|
|
5971
|
-
if (t.length === 0 || v.length === 0) {
|
|
5972
|
-
throw new AffinityValidationError("type and value must be non-empty");
|
|
5973
|
-
}
|
|
5974
|
-
return `${t}:${v}`;
|
|
5975
|
-
}
|
|
5976
|
-
|
|
5977
6916
|
// src/tools/tool_types.ts
|
|
5978
6917
|
function withOptionalFields(result, options) {
|
|
5979
6918
|
if (options.next && options.next.length > 0) {
|
|
@@ -6089,7 +7028,7 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
6089
7028
|
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
6090
7029
|
return {
|
|
6091
7030
|
ok: false,
|
|
6092
|
-
result: toolFailure("domain", "
|
|
7031
|
+
result: toolFailure("domain", "conflict", message, message)
|
|
6093
7032
|
};
|
|
6094
7033
|
}
|
|
6095
7034
|
return { ok: true, value: row2 };
|
|
@@ -6114,9 +7053,13 @@ function resolveContactLocator(db, locator, field = "contact", allowMerged = fal
|
|
|
6114
7053
|
INNER JOIN contacts c ON c.id = i.contact_id
|
|
6115
7054
|
WHERE i.removed_at IS NULL
|
|
6116
7055
|
AND c.deleted_at IS NULL
|
|
6117
|
-
AND i.normalized_key = ?
|
|
7056
|
+
AND (i.normalized_key IN (?, ?) OR (i.type = 'custom:atproto-did' AND ? = 'did:' || lower(i.value)))
|
|
6118
7057
|
LIMIT 2`
|
|
6119
|
-
).all(
|
|
7058
|
+
).all(
|
|
7059
|
+
normalizedKey,
|
|
7060
|
+
legacyIdentityKey(normalizedKey),
|
|
7061
|
+
normalizedKey
|
|
7062
|
+
);
|
|
6120
7063
|
if (row.length === 0) {
|
|
6121
7064
|
return {
|
|
6122
7065
|
ok: false,
|
|
@@ -6267,13 +7210,21 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
6267
7210
|
return { ok: true, value: matchedLink };
|
|
6268
7211
|
}
|
|
6269
7212
|
function mapErrorToToolFailure(error, summary = "The tool failed.") {
|
|
7213
|
+
if (error && typeof error === "object" && "ok" in error && error.ok === false && "outcome" in error && error.outcome === "error")
|
|
7214
|
+
return error;
|
|
6270
7215
|
if (isAffinityError(error)) {
|
|
6271
|
-
|
|
7216
|
+
const result = toolFailure(
|
|
6272
7217
|
"domain",
|
|
6273
7218
|
affinityErrorCodeToToolCode(error),
|
|
6274
7219
|
summary,
|
|
6275
7220
|
error.message
|
|
6276
7221
|
);
|
|
7222
|
+
const id = error.existingContactId;
|
|
7223
|
+
if (id !== void 0) {
|
|
7224
|
+
result.data = { existingContactId: id };
|
|
7225
|
+
result.error.details = { existingContactId: id };
|
|
7226
|
+
}
|
|
7227
|
+
return result;
|
|
6277
7228
|
}
|
|
6278
7229
|
if (error instanceof Error) {
|
|
6279
7230
|
return toolFailure("system", "system_error", summary, error.message);
|
|
@@ -6312,27 +7263,32 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
6312
7263
|
}
|
|
6313
7264
|
|
|
6314
7265
|
// src/tools/validate_tool_input.ts
|
|
6315
|
-
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
7266
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now(), strict = false) {
|
|
7267
|
+
if (path.endsWith(".occurredAt") && value == null)
|
|
7268
|
+
return "occurredAt is required (ISO, YYYY-MM-DD, epoch ms, today, +3d \u2026)";
|
|
7269
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
7270
|
+
if (path.endsWith(".value") && branches?.some((b) => b.type === "string") && branches.some((b) => b.type === "null") && value !== null && typeof value !== "string")
|
|
7271
|
+
return `attribute values are strings or null; pass "${String(value)}" to store it as text`;
|
|
6316
7272
|
if (schema.anyOf || schema.oneOf) {
|
|
6317
7273
|
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
6318
7274
|
return choices.some(
|
|
6319
|
-
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
7275
|
+
(choice) => validateToolInput(choice, value, path, injectedNow, strict) === null
|
|
6320
7276
|
) ? null : `${path}: invalid value`;
|
|
6321
7277
|
}
|
|
6322
7278
|
if (schema.type === "object") {
|
|
6323
7279
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
6324
7280
|
return `${path}: missing input or expected object`;
|
|
6325
7281
|
const object = value;
|
|
6326
|
-
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
7282
|
+
if ((strict || path.endsWith(".patch")) && schema.additionalProperties === false) {
|
|
6327
7283
|
for (const key of Object.keys(object)) {
|
|
6328
7284
|
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
6329
|
-
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(", ")}`;
|
|
7285
|
+
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(", ")}`;
|
|
6330
7286
|
}
|
|
6331
7287
|
}
|
|
6332
7288
|
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
6333
7289
|
return `${path}.patch: missing required field`;
|
|
6334
7290
|
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
6335
|
-
return `${path}.patch: unknown
|
|
7291
|
+
return `${path}.patch: unknown key; allowed keys: none (use the action's named fields)`;
|
|
6336
7292
|
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
6337
7293
|
try {
|
|
6338
7294
|
return parseTimeInput(
|
|
@@ -6347,11 +7303,21 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6347
7303
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
6348
7304
|
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
6349
7305
|
try {
|
|
6350
|
-
|
|
7306
|
+
const parsed = parseCalendarTime(
|
|
6351
7307
|
object[key],
|
|
6352
7308
|
`${path}.${key}`,
|
|
6353
7309
|
referenceNow
|
|
6354
7310
|
);
|
|
7311
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
|
|
7312
|
+
const provenance = object.provenance;
|
|
7313
|
+
object.provenance = {
|
|
7314
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
7315
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
|
|
7316
|
+
};
|
|
7317
|
+
}
|
|
7318
|
+
object[key] = parsed.timestamp;
|
|
7319
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
7320
|
+
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
6355
7321
|
} catch (error) {
|
|
6356
7322
|
return error.message;
|
|
6357
7323
|
}
|
|
@@ -6366,8 +7332,9 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6366
7332
|
const error = validateToolInput(
|
|
6367
7333
|
child,
|
|
6368
7334
|
object[key],
|
|
6369
|
-
`${path}.${key}`,
|
|
6370
|
-
referenceNow
|
|
7335
|
+
key === "input" && path === "input" ? path : `${path}.${key}`,
|
|
7336
|
+
referenceNow,
|
|
7337
|
+
strict
|
|
6371
7338
|
);
|
|
6372
7339
|
if (error) return error;
|
|
6373
7340
|
}
|
|
@@ -6380,7 +7347,8 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
6380
7347
|
schema.items ?? {},
|
|
6381
7348
|
item,
|
|
6382
7349
|
`${path}[${index}]`,
|
|
6383
|
-
injectedNow
|
|
7350
|
+
injectedNow,
|
|
7351
|
+
strict
|
|
6384
7352
|
);
|
|
6385
7353
|
if (error) return error;
|
|
6386
7354
|
}
|
|
@@ -6446,6 +7414,26 @@ function inspectAffinityItemToolHandler(db, rawInput) {
|
|
|
6446
7414
|
inputError
|
|
6447
7415
|
);
|
|
6448
7416
|
return withToolHandling(() => {
|
|
7417
|
+
if (input.kind === "event") {
|
|
7418
|
+
if (input.eventId === void 0 === (input.ref === void 0))
|
|
7419
|
+
return toolFailure(
|
|
7420
|
+
"protocol",
|
|
7421
|
+
"invalid_input",
|
|
7422
|
+
"Invalid event target.",
|
|
7423
|
+
"Provide exactly one of eventId or ref."
|
|
7424
|
+
);
|
|
7425
|
+
const detail2 = input.ref ? getEventByExternalRef(db, input.ref.source, input.ref.id) : getEventById(db, input.eventId);
|
|
7426
|
+
return detail2 ? toolSuccess(
|
|
7427
|
+
`Loaded event #${detail2.id}.`,
|
|
7428
|
+
{ kind: "event", detail: detail2 },
|
|
7429
|
+
{ entities: [{ kind: "event", id: detail2.id }] }
|
|
7430
|
+
) : toolFailure(
|
|
7431
|
+
"domain",
|
|
7432
|
+
"not_found",
|
|
7433
|
+
"Event not found.",
|
|
7434
|
+
"No event matches the target."
|
|
7435
|
+
);
|
|
7436
|
+
}
|
|
6449
7437
|
if (input.kind === "owner_profile") {
|
|
6450
7438
|
const detail2 = getOwnerProfile(
|
|
6451
7439
|
db,
|
|
@@ -6543,10 +7531,13 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
6543
7531
|
inputSchema: objectSchema(
|
|
6544
7532
|
{
|
|
6545
7533
|
kind: enumSchema("What to inspect.", [
|
|
7534
|
+
"event",
|
|
6546
7535
|
"owner_profile",
|
|
6547
7536
|
"contact_profile",
|
|
6548
7537
|
"link"
|
|
6549
7538
|
]),
|
|
7539
|
+
eventId: integerSchema("Exact event id."),
|
|
7540
|
+
ref: externalRefSchema,
|
|
6550
7541
|
contact: contactLocatorSchema(
|
|
6551
7542
|
"Target contact. Required when kind=contact_profile. Provide contactId or identity."
|
|
6552
7543
|
),
|
|
@@ -6565,6 +7556,7 @@ var inspectAffinityItemTool = defineAffinityTool({
|
|
|
6565
7556
|
// src/attributes/helpers.ts
|
|
6566
7557
|
function clearedAttributePrimary(target) {
|
|
6567
7558
|
return {
|
|
7559
|
+
...target.kind === "event" ? { eventId: target.eventId } : {},
|
|
6568
7560
|
id: 0,
|
|
6569
7561
|
contactId: target.kind === "contact" ? target.id : null,
|
|
6570
7562
|
linkId: target.kind === "link" ? target.id : null,
|
|
@@ -6576,6 +7568,7 @@ function clearedAttributePrimary(target) {
|
|
|
6576
7568
|
// src/attributes/mappers.ts
|
|
6577
7569
|
function mapAttributeRowToAttributeRecord(row) {
|
|
6578
7570
|
return {
|
|
7571
|
+
...row.event_id == null ? {} : { eventId: row.event_id },
|
|
6579
7572
|
id: row.id,
|
|
6580
7573
|
contactId: row.contact_id,
|
|
6581
7574
|
linkId: row.link_id,
|
|
@@ -6585,49 +7578,26 @@ function mapAttributeRowToAttributeRecord(row) {
|
|
|
6585
7578
|
}
|
|
6586
7579
|
|
|
6587
7580
|
// src/attributes/queries.ts
|
|
7581
|
+
function attributeTargetSql(target) {
|
|
7582
|
+
return {
|
|
7583
|
+
column: target.kind === "event" ? "event_id" : target.kind === "contact" ? "contact_id" : "link_id",
|
|
7584
|
+
id: target.kind === "event" ? target.eventId : target.id
|
|
7585
|
+
};
|
|
7586
|
+
}
|
|
6588
7587
|
function getAttributeRowById(db, id) {
|
|
6589
|
-
|
|
6590
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
6591
|
-
FROM attributes WHERE id = ?`
|
|
6592
|
-
).get(id);
|
|
6593
|
-
if (row === void 0) {
|
|
6594
|
-
return null;
|
|
6595
|
-
}
|
|
6596
|
-
return row;
|
|
7588
|
+
return db.prepare("SELECT * FROM attributes WHERE id=?").get(id) ?? null;
|
|
6597
7589
|
}
|
|
6598
7590
|
function getLiveAttributeRow(db, target, name) {
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
WHERE contact_id = ? AND name = ? AND deleted_at IS NULL`
|
|
6604
|
-
).get(target.id, name);
|
|
6605
|
-
if (row2 === void 0) {
|
|
6606
|
-
return null;
|
|
6607
|
-
}
|
|
6608
|
-
return row2;
|
|
6609
|
-
}
|
|
6610
|
-
const row = db.prepare(
|
|
6611
|
-
`SELECT id, contact_id, link_id, name, value, created_at, updated_at, deleted_at
|
|
6612
|
-
FROM attributes
|
|
6613
|
-
WHERE link_id = ? AND name = ? AND deleted_at IS NULL`
|
|
6614
|
-
).get(target.id, name);
|
|
6615
|
-
if (row === void 0) {
|
|
6616
|
-
return null;
|
|
6617
|
-
}
|
|
6618
|
-
return row;
|
|
7591
|
+
const t = attributeTargetSql(target);
|
|
7592
|
+
return db.prepare(
|
|
7593
|
+
`SELECT * FROM attributes WHERE ${t.column}=? AND name=? AND deleted_at IS NULL`
|
|
7594
|
+
).get(t.id, name) ?? null;
|
|
6619
7595
|
}
|
|
6620
7596
|
function listLiveAttributeIdsForTarget(db, target) {
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
return rows2.map((row) => row.id);
|
|
6626
|
-
}
|
|
6627
|
-
const rows = db.prepare(
|
|
6628
|
-
"SELECT id FROM attributes WHERE link_id = ? AND deleted_at IS NULL ORDER BY id"
|
|
6629
|
-
).all(target.id);
|
|
6630
|
-
return rows.map((row) => row.id);
|
|
7597
|
+
const t = attributeTargetSql(target);
|
|
7598
|
+
return db.prepare(
|
|
7599
|
+
`SELECT id FROM attributes WHERE ${t.column}=? AND deleted_at IS NULL ORDER BY id`
|
|
7600
|
+
).all(t.id).map((r) => r.id);
|
|
6631
7601
|
}
|
|
6632
7602
|
|
|
6633
7603
|
// src/attributes/receipts.ts
|
|
@@ -6649,6 +7619,8 @@ function normalizeAttributeValue(value) {
|
|
|
6649
7619
|
if (value === null) {
|
|
6650
7620
|
return null;
|
|
6651
7621
|
}
|
|
7622
|
+
if (typeof value !== "string")
|
|
7623
|
+
throw new AffinityValidationError("attribute values are strings or null");
|
|
6652
7624
|
const trimmed = value.trim();
|
|
6653
7625
|
return trimmed.length === 0 ? null : trimmed;
|
|
6654
7626
|
}
|
|
@@ -6677,6 +7649,11 @@ function validateAttributeEntries(entries) {
|
|
|
6677
7649
|
return out;
|
|
6678
7650
|
}
|
|
6679
7651
|
function assertAttributeTargetWritable(db, target) {
|
|
7652
|
+
if (target.kind === "event") {
|
|
7653
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(target.eventId))
|
|
7654
|
+
throw new AffinityNotFoundError("event not found");
|
|
7655
|
+
return;
|
|
7656
|
+
}
|
|
6680
7657
|
if (target.kind === "contact") {
|
|
6681
7658
|
const contact = getContactRowById(db, target.id);
|
|
6682
7659
|
if (!contact) {
|
|
@@ -6712,26 +7689,14 @@ function replaceAttributes(db, target, entries) {
|
|
|
6712
7689
|
}
|
|
6713
7690
|
const created = [];
|
|
6714
7691
|
let lastId = 0;
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
created.push({ kind: "attribute", id: lastId });
|
|
6724
|
-
}
|
|
6725
|
-
} else {
|
|
6726
|
-
const insert = db.prepare(
|
|
6727
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
6728
|
-
VALUES (NULL, ?, ?, ?, ?, ?)`
|
|
6729
|
-
);
|
|
6730
|
-
for (const e of normalized) {
|
|
6731
|
-
const r = insert.run(target.id, e.name, e.value, now, now);
|
|
6732
|
-
lastId = Number(r.lastInsertRowid);
|
|
6733
|
-
created.push({ kind: "attribute", id: lastId });
|
|
6734
|
-
}
|
|
7692
|
+
const t = attributeTargetSql(target);
|
|
7693
|
+
const insert = db.prepare(
|
|
7694
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
7695
|
+
);
|
|
7696
|
+
for (const e of normalized) {
|
|
7697
|
+
const r = insert.run(t.id, e.name, e.value, now, now);
|
|
7698
|
+
lastId = Number(r.lastInsertRowid);
|
|
7699
|
+
created.push({ kind: "attribute", id: lastId });
|
|
6735
7700
|
}
|
|
6736
7701
|
if (normalized.length === 0) {
|
|
6737
7702
|
return buildAttributeMutationReceipt(clearedAttributePrimary(target), {
|
|
@@ -6761,6 +7726,11 @@ function setAttribute(db, target, name, value) {
|
|
|
6761
7726
|
const stored = normalizeAttributeValue(value);
|
|
6762
7727
|
const now = resolveNow();
|
|
6763
7728
|
const existing = getLiveAttributeRow(db, target, key);
|
|
7729
|
+
if (existing?.value === stored)
|
|
7730
|
+
return buildAttributeMutationReceipt(
|
|
7731
|
+
mapAttributeRowToAttributeRecord(existing),
|
|
7732
|
+
{}
|
|
7733
|
+
);
|
|
6764
7734
|
if (existing) {
|
|
6765
7735
|
db.prepare(
|
|
6766
7736
|
"UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
|
|
@@ -6776,27 +7746,10 @@ function setAttribute(db, target, name, value) {
|
|
|
6776
7746
|
}
|
|
6777
7747
|
);
|
|
6778
7748
|
}
|
|
6779
|
-
|
|
6780
|
-
const r2 = db.prepare(
|
|
6781
|
-
`INSERT INTO attributes (contact_id, link_id, name, value, created_at, updated_at)
|
|
6782
|
-
VALUES (?, NULL, ?, ?, ?, ?)`
|
|
6783
|
-
).run(target.id, key, stored, now, now);
|
|
6784
|
-
const id2 = Number(r2.lastInsertRowid);
|
|
6785
|
-
const row2 = getAttributeRowById(db, id2);
|
|
6786
|
-
if (!row2) {
|
|
6787
|
-
throw new AffinityInvariantError("attribute row missing after insert");
|
|
6788
|
-
}
|
|
6789
|
-
return buildAttributeMutationReceipt(
|
|
6790
|
-
mapAttributeRowToAttributeRecord(row2),
|
|
6791
|
-
{
|
|
6792
|
-
created: [{ kind: "attribute", id: id2 }]
|
|
6793
|
-
}
|
|
6794
|
-
);
|
|
6795
|
-
}
|
|
7749
|
+
const t = attributeTargetSql(target);
|
|
6796
7750
|
const r = db.prepare(
|
|
6797
|
-
`INSERT INTO attributes (
|
|
6798
|
-
|
|
6799
|
-
).run(target.id, key, stored, now, now);
|
|
7751
|
+
`INSERT INTO attributes (${t.column},name,value,created_at,updated_at) VALUES (?,?,?,?,?)`
|
|
7752
|
+
).run(t.id, key, stored, now, now);
|
|
6800
7753
|
const id = Number(r.lastInsertRowid);
|
|
6801
7754
|
const row = getAttributeRowById(db, id);
|
|
6802
7755
|
if (!row) {
|
|
@@ -6852,14 +7805,15 @@ function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
|
6852
7805
|
affectedLinks: receipt.affectedLinks,
|
|
6853
7806
|
derivedEffects: receipt.derivedEffects
|
|
6854
7807
|
};
|
|
6855
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
7808
|
+
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);
|
|
6856
7809
|
if (changedCount === 0) {
|
|
6857
7810
|
return toolNoOp(summary, data, {
|
|
6858
7811
|
entities: toReceiptEntityRefs(receipt)
|
|
6859
7812
|
});
|
|
6860
7813
|
}
|
|
6861
7814
|
return toolSuccess(summary, data, {
|
|
6862
|
-
entities: toReceiptEntityRefs(receipt)
|
|
7815
|
+
entities: toReceiptEntityRefs(receipt),
|
|
7816
|
+
warnings: receipt.warnings
|
|
6863
7817
|
});
|
|
6864
7818
|
}
|
|
6865
7819
|
var PLURALS = { identity: "identities" };
|
|
@@ -6916,6 +7870,30 @@ function linkLocatorSchema2(description) {
|
|
|
6916
7870
|
};
|
|
6917
7871
|
}
|
|
6918
7872
|
function resolveAttributeTarget(db, target) {
|
|
7873
|
+
if (target.kind === "event") {
|
|
7874
|
+
if (target.eventId === void 0 === (target.ref === void 0))
|
|
7875
|
+
return {
|
|
7876
|
+
kind: "error",
|
|
7877
|
+
result: toolFailure(
|
|
7878
|
+
"protocol",
|
|
7879
|
+
"invalid_input",
|
|
7880
|
+
"Invalid event target.",
|
|
7881
|
+
"Provide exactly one of target.eventId or target.ref."
|
|
7882
|
+
)
|
|
7883
|
+
};
|
|
7884
|
+
const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
|
|
7885
|
+
if (eventId === void 0)
|
|
7886
|
+
return {
|
|
7887
|
+
kind: "error",
|
|
7888
|
+
result: toolFailure(
|
|
7889
|
+
"domain",
|
|
7890
|
+
"not_found",
|
|
7891
|
+
"Event not found.",
|
|
7892
|
+
"No event matches target.ref."
|
|
7893
|
+
)
|
|
7894
|
+
};
|
|
7895
|
+
return { kind: "ok", target: { kind: "event", eventId } };
|
|
7896
|
+
}
|
|
6919
7897
|
if (target.kind === "contact") {
|
|
6920
7898
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
6921
7899
|
if (!contact.ok) {
|
|
@@ -6975,7 +7953,7 @@ function manageAttributeToolHandler(db, rawInput) {
|
|
|
6975
7953
|
}
|
|
6976
7954
|
var manageAttributeTool = defineAffinityTool({
|
|
6977
7955
|
name: manageAttributeToolName,
|
|
6978
|
-
description: "Set, unset, or replace custom metadata attributes on contacts and
|
|
7956
|
+
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.",
|
|
6979
7957
|
whenToUse: "Use this for metadata such as tags, notes, or small labeled facts attached to a contact or link.",
|
|
6980
7958
|
whenNotToUse: "Do not use this for core contact fields, identities, or direct relationship mechanics.",
|
|
6981
7959
|
sideEffects: "writes_state",
|
|
@@ -6996,7 +7974,9 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
6996
7974
|
action: enumSchema("Operation to perform.", ["set", "unset", "replace"]),
|
|
6997
7975
|
target: objectSchema(
|
|
6998
7976
|
{
|
|
6999
|
-
kind: enumSchema("Target kind.", ["contact", "link"]),
|
|
7977
|
+
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
7978
|
+
eventId: integerSchema("Target event id; provide eventId or ref."),
|
|
7979
|
+
ref: externalRefSchema,
|
|
7000
7980
|
contact: contactLocatorSchema2(
|
|
7001
7981
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
7002
7982
|
),
|
|
@@ -7026,7 +8006,7 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
7026
8006
|
removedAt: integerSchema("Removal timestamp. Optional for action=unset.")
|
|
7027
8007
|
},
|
|
7028
8008
|
["action"],
|
|
7029
|
-
"Set, unset, or replace attributes on contacts and
|
|
8009
|
+
"Set, unset, or replace attributes on contacts, links and events."
|
|
7030
8010
|
),
|
|
7031
8011
|
handler: manageAttributeToolHandler
|
|
7032
8012
|
});
|
|
@@ -7058,7 +8038,13 @@ function prepareParticipants(db, raw) {
|
|
|
7058
8038
|
}
|
|
7059
8039
|
function manageCommitmentToolHandler(db, rawInput) {
|
|
7060
8040
|
const input = structuredClone(rawInput);
|
|
7061
|
-
const inputError = validateToolInput(
|
|
8041
|
+
const inputError = validateToolInput(
|
|
8042
|
+
manageCommitmentTool.inputSchema,
|
|
8043
|
+
input,
|
|
8044
|
+
"input",
|
|
8045
|
+
Date.now(),
|
|
8046
|
+
true
|
|
8047
|
+
);
|
|
7062
8048
|
if (inputError)
|
|
7063
8049
|
return toolFailure(
|
|
7064
8050
|
"protocol",
|
|
@@ -7088,6 +8074,21 @@ function manageCommitmentToolHandler(db, rawInput) {
|
|
|
7088
8074
|
"commitment"
|
|
7089
8075
|
);
|
|
7090
8076
|
}
|
|
8077
|
+
if (!Number.isInteger(input.commitmentEventId))
|
|
8078
|
+
return toolFailure(
|
|
8079
|
+
"protocol",
|
|
8080
|
+
"invalid_input",
|
|
8081
|
+
"Missing commitment.",
|
|
8082
|
+
"commitmentEventId is required for resolve."
|
|
8083
|
+
);
|
|
8084
|
+
const event = db.prepare("SELECT type FROM events WHERE id = ? AND deleted_at IS NULL").get(input.commitmentEventId);
|
|
8085
|
+
if (event && !["promise", "agreement"].includes(event.type))
|
|
8086
|
+
return toolFailure(
|
|
8087
|
+
"domain",
|
|
8088
|
+
"invalid_input",
|
|
8089
|
+
"Invalid commitment.",
|
|
8090
|
+
"not a commitment event"
|
|
8091
|
+
);
|
|
7091
8092
|
return mutationToolResult(
|
|
7092
8093
|
"resolve",
|
|
7093
8094
|
resolveCommitment(
|
|
@@ -7126,15 +8127,21 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7126
8127
|
"promise",
|
|
7127
8128
|
"agreement"
|
|
7128
8129
|
]),
|
|
8130
|
+
...eventMetadataSchema,
|
|
7129
8131
|
occurredAt: integerSchema(
|
|
7130
8132
|
"When the commitment was made, Unix timestamp in milliseconds."
|
|
7131
8133
|
),
|
|
7132
8134
|
summary: stringSchema("Commitment summary. Must be non-empty."),
|
|
7133
8135
|
significance: integerSchema("Commitment significance from 1 to 10."),
|
|
7134
8136
|
dueAt: integerSchema("Optional due timestamp."),
|
|
8137
|
+
dueOn: stringSchema("Due calendar day."),
|
|
7135
8138
|
participants: arraySchema(
|
|
7136
8139
|
objectSchema(
|
|
7137
8140
|
{
|
|
8141
|
+
contact: objectSchema(
|
|
8142
|
+
{ contactId: integerSchema("Contact id.") },
|
|
8143
|
+
["contactId"]
|
|
8144
|
+
),
|
|
7138
8145
|
contactId: integerSchema(
|
|
7139
8146
|
"Participant contact id. The owner contact MUST be included."
|
|
7140
8147
|
),
|
|
@@ -7147,13 +8154,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7147
8154
|
),
|
|
7148
8155
|
now: integerSchema("Optional timestamp override.")
|
|
7149
8156
|
},
|
|
7150
|
-
[
|
|
7151
|
-
"commitmentType",
|
|
7152
|
-
"occurredAt",
|
|
7153
|
-
"summary",
|
|
7154
|
-
"significance",
|
|
7155
|
-
"participants"
|
|
7156
|
-
],
|
|
8157
|
+
["commitmentType", "occurredAt", "significance", "participants"],
|
|
7157
8158
|
"Commitment payload. Required when action=record."
|
|
7158
8159
|
),
|
|
7159
8160
|
commitmentEventId: integerSchema(
|
|
@@ -7168,6 +8169,19 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7168
8169
|
now: integerSchema("Optional timestamp."),
|
|
7169
8170
|
resolvingEvent: objectSchema(
|
|
7170
8171
|
{
|
|
8172
|
+
occurredOn: stringSchema("Calendar day."),
|
|
8173
|
+
provenance: {},
|
|
8174
|
+
participants: arraySchema(
|
|
8175
|
+
objectSchema(
|
|
8176
|
+
{
|
|
8177
|
+
contactId: integerSchema("Contact id."),
|
|
8178
|
+
role: stringSchema("Role."),
|
|
8179
|
+
directionality: stringSchema("Directionality.")
|
|
8180
|
+
},
|
|
8181
|
+
["contactId", "role"]
|
|
8182
|
+
),
|
|
8183
|
+
"Resolving participants."
|
|
8184
|
+
),
|
|
7171
8185
|
type: stringSchema("Optional resolving event type."),
|
|
7172
8186
|
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
7173
8187
|
summary: stringSchema("Optional resolving event summary."),
|
|
@@ -7186,221 +8200,69 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
7186
8200
|
"Record or resolve one commitment."
|
|
7187
8201
|
),
|
|
7188
8202
|
handler: manageCommitmentToolHandler
|
|
7189
|
-
});
|
|
7190
|
-
|
|
7191
|
-
// src/contacts/delete_contact.ts
|
|
7192
|
-
function deleteContact(db, locatorId, options) {
|
|
7193
|
-
if (options?.confirm !== true)
|
|
7194
|
-
throw new AffinityValidationError(
|
|
7195
|
-
"delete is permanent; pass confirm: true"
|
|
7196
|
-
);
|
|
7197
|
-
return withTransaction(db, () => {
|
|
7198
|
-
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
7199
|
-
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
7200
|
-
if (contact.is_owner)
|
|
7201
|
-
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
7202
|
-
const tables = [
|
|
7203
|
-
"contacts",
|
|
7204
|
-
"identities",
|
|
7205
|
-
"attributes",
|
|
7206
|
-
"links",
|
|
7207
|
-
"events",
|
|
7208
|
-
"event_participants",
|
|
7209
|
-
"open_commitments",
|
|
7210
|
-
"contact_merges",
|
|
7211
|
-
"dismissed_duplicates",
|
|
7212
|
-
"link_event_effects",
|
|
7213
|
-
"upcoming_occurrences",
|
|
7214
|
-
"link_rollups",
|
|
7215
|
-
"contact_rollups"
|
|
7216
|
-
];
|
|
7217
|
-
const before = new Map(
|
|
7218
|
-
tables.map((table) => [
|
|
7219
|
-
table,
|
|
7220
|
-
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
7221
|
-
])
|
|
7222
|
-
);
|
|
7223
|
-
db.prepare(
|
|
7224
|
-
"DELETE FROM events WHERE id IN (SELECT p.event_id FROM event_participants p WHERE p.contact_id = ? AND NOT EXISTS (SELECT 1 FROM event_participants q JOIN contacts c ON c.id = q.contact_id WHERE q.event_id = p.event_id AND q.contact_id != ? AND c.is_owner = 0))"
|
|
7225
|
-
).run(locatorId, locatorId);
|
|
7226
|
-
db.prepare(
|
|
7227
|
-
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
7228
|
-
).run(locatorId, locatorId);
|
|
7229
|
-
db.prepare(
|
|
7230
|
-
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7231
|
-
).run(locatorId, locatorId);
|
|
7232
|
-
db.prepare(
|
|
7233
|
-
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
7234
|
-
).run(locatorId);
|
|
7235
|
-
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
7236
|
-
const now = Date.now();
|
|
7237
|
-
const affectedLinks = db.prepare(
|
|
7238
|
-
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
7239
|
-
).all().map((row) => row.id);
|
|
7240
|
-
refreshAllBridgeScores(db, now);
|
|
7241
|
-
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
7242
|
-
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
7243
|
-
refreshContactRollup(db, Number(row.id), now);
|
|
7244
|
-
const removed = tables.map((table) => {
|
|
7245
|
-
const remaining = new Set(
|
|
7246
|
-
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
7247
|
-
);
|
|
7248
|
-
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
7249
|
-
return { entity: table, count: ids.length, ids };
|
|
7250
|
-
}).filter((item) => item.count > 0);
|
|
7251
|
-
return { contactId: locatorId, removed, affectedLinks };
|
|
7252
|
-
});
|
|
7253
|
-
}
|
|
7254
|
-
|
|
7255
|
-
// src/identities/queries.ts
|
|
7256
|
-
function getIdentityRowById(db, id) {
|
|
7257
|
-
const row = db.prepare(
|
|
7258
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
7259
|
-
created_at, updated_at, removed_at
|
|
7260
|
-
FROM identities
|
|
7261
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
7262
|
-
).get(id);
|
|
7263
|
-
if (row === void 0) {
|
|
7264
|
-
return null;
|
|
7265
|
-
}
|
|
7266
|
-
return row;
|
|
7267
|
-
}
|
|
7268
|
-
|
|
7269
|
-
// src/identities/receipts.ts
|
|
7270
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
7271
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
7272
|
-
return {
|
|
7273
|
-
primary,
|
|
7274
|
-
created: normalized.created,
|
|
7275
|
-
updated: normalized.updated,
|
|
7276
|
-
archived: normalized.archived,
|
|
7277
|
-
removed: normalized.removed,
|
|
7278
|
-
affectedLinks: normalized.affectedLinks,
|
|
7279
|
-
derivedEffects: normalized.derivedEffects
|
|
7280
|
-
};
|
|
7281
|
-
}
|
|
7282
|
-
|
|
7283
|
-
// src/identities/validators.ts
|
|
7284
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
7285
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
7286
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7287
|
-
).get(normalizedKey) : db.prepare(
|
|
7288
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
7289
|
-
).get(normalizedKey, excludeIdentityId);
|
|
7290
|
-
if (row !== void 0) {
|
|
7291
|
-
const owner = db.prepare(
|
|
7292
|
-
"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"
|
|
7293
|
-
).get(normalizedKey);
|
|
7294
|
-
throw new AffinityConflictError(
|
|
7295
|
-
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
7296
|
-
);
|
|
7297
|
-
}
|
|
7298
|
-
}
|
|
7299
|
-
|
|
7300
|
-
// src/identities/validate_identity_input.ts
|
|
7301
|
-
var identityTypes = [
|
|
7302
|
-
"email",
|
|
7303
|
-
"phone",
|
|
7304
|
-
"url",
|
|
7305
|
-
"website",
|
|
7306
|
-
"handle",
|
|
7307
|
-
"x",
|
|
7308
|
-
"twitter",
|
|
7309
|
-
"linkedin",
|
|
7310
|
-
"github",
|
|
7311
|
-
"mastodon",
|
|
7312
|
-
"bluesky",
|
|
7313
|
-
"telegram",
|
|
7314
|
-
"whatsapp",
|
|
7315
|
-
"signal",
|
|
7316
|
-
"matrix",
|
|
7317
|
-
"discord",
|
|
7318
|
-
"slack",
|
|
7319
|
-
"instagram",
|
|
7320
|
-
"facebook",
|
|
7321
|
-
"youtube",
|
|
7322
|
-
"tiktok",
|
|
7323
|
-
"address",
|
|
7324
|
-
"other",
|
|
7325
|
-
"account_id",
|
|
7326
|
-
"alias"
|
|
7327
|
-
];
|
|
7328
|
-
function validateIdentityInput(type, value) {
|
|
7329
|
-
const kind = type.toLowerCase();
|
|
7330
|
-
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
7331
|
-
throw new AffinityValidationError(
|
|
7332
|
-
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
7333
|
-
);
|
|
7334
|
-
if (!value.trim())
|
|
7335
|
-
throw new AffinityValidationError("identity value must be non-empty");
|
|
7336
|
-
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
7337
|
-
throw new AffinityValidationError(
|
|
7338
|
-
"email value must match local@domain.tld"
|
|
7339
|
-
);
|
|
7340
|
-
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
7341
|
-
throw new AffinityValidationError(
|
|
7342
|
-
"phone value must contain at least 5 digits"
|
|
7343
|
-
);
|
|
7344
|
-
if (kind === "url" || kind === "website") {
|
|
7345
|
-
try {
|
|
7346
|
-
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
7347
|
-
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
7348
|
-
throw new Error();
|
|
7349
|
-
} catch {
|
|
7350
|
-
throw new AffinityValidationError(
|
|
7351
|
-
"url/website value must be an http/https URL or bare domain"
|
|
7352
|
-
);
|
|
7353
|
-
}
|
|
7354
|
-
}
|
|
7355
|
-
}
|
|
8203
|
+
});
|
|
7356
8204
|
|
|
7357
|
-
// src/
|
|
7358
|
-
function
|
|
8205
|
+
// src/contacts/delete_contact.ts
|
|
8206
|
+
function deleteContact(db, locatorId, options) {
|
|
8207
|
+
if (options?.confirm !== true)
|
|
8208
|
+
throw new AffinityValidationError(
|
|
8209
|
+
"delete is permanent; pass confirm: true"
|
|
8210
|
+
);
|
|
7359
8211
|
return withTransaction(db, () => {
|
|
7360
|
-
const contact =
|
|
7361
|
-
if (!contact)
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
value,
|
|
7385
|
-
input.label ?? null,
|
|
7386
|
-
nk,
|
|
7387
|
-
verified,
|
|
7388
|
-
verifiedAt,
|
|
7389
|
-
now,
|
|
7390
|
-
now
|
|
8212
|
+
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
8213
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
8214
|
+
if (contact.is_owner)
|
|
8215
|
+
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
8216
|
+
const tables = [
|
|
8217
|
+
"contacts",
|
|
8218
|
+
"identities",
|
|
8219
|
+
"attributes",
|
|
8220
|
+
"links",
|
|
8221
|
+
"events",
|
|
8222
|
+
"event_participants",
|
|
8223
|
+
"open_commitments",
|
|
8224
|
+
"contact_merges",
|
|
8225
|
+
"dismissed_duplicates",
|
|
8226
|
+
"link_event_effects",
|
|
8227
|
+
"upcoming_occurrences",
|
|
8228
|
+
"link_rollups",
|
|
8229
|
+
"contact_rollups"
|
|
8230
|
+
];
|
|
8231
|
+
const before = new Map(
|
|
8232
|
+
tables.map((table) => [
|
|
8233
|
+
table,
|
|
8234
|
+
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
8235
|
+
])
|
|
7391
8236
|
);
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
8237
|
+
db.prepare(
|
|
8238
|
+
"DELETE FROM events WHERE id IN (SELECT p.event_id FROM event_participants p WHERE p.contact_id = ? AND NOT EXISTS (SELECT 1 FROM event_participants q JOIN contacts c ON c.id = q.contact_id WHERE q.event_id = p.event_id AND q.contact_id != ? AND c.is_owner = 0))"
|
|
8239
|
+
).run(locatorId, locatorId);
|
|
8240
|
+
db.prepare(
|
|
8241
|
+
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
8242
|
+
).run(locatorId, locatorId);
|
|
7398
8243
|
db.prepare(
|
|
7399
8244
|
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7400
|
-
).run(
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
8245
|
+
).run(locatorId, locatorId);
|
|
8246
|
+
db.prepare(
|
|
8247
|
+
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
8248
|
+
).run(locatorId);
|
|
8249
|
+
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
8250
|
+
const now = Date.now();
|
|
8251
|
+
const affectedLinks = db.prepare(
|
|
8252
|
+
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
8253
|
+
).all().map((row) => row.id);
|
|
8254
|
+
refreshAllBridgeScores(db, now);
|
|
8255
|
+
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
8256
|
+
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
8257
|
+
refreshContactRollup(db, Number(row.id), now);
|
|
8258
|
+
const removed = tables.map((table) => {
|
|
8259
|
+
const remaining = new Set(
|
|
8260
|
+
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
8261
|
+
);
|
|
8262
|
+
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
8263
|
+
return { entity: table, count: ids.length, ids };
|
|
8264
|
+
}).filter((item) => item.count > 0);
|
|
8265
|
+
return { contactId: locatorId, removed, affectedLinks };
|
|
7404
8266
|
});
|
|
7405
8267
|
}
|
|
7406
8268
|
|
|
@@ -7408,27 +8270,20 @@ function addIdentity(db, contactId, input) {
|
|
|
7408
8270
|
function createContactToolResult(db, input) {
|
|
7409
8271
|
return withTransaction(db, () => {
|
|
7410
8272
|
const receipt = createContact(db, input);
|
|
7411
|
-
const identityOwners = /* @__PURE__ */ new Set();
|
|
7412
|
-
for (const identity of input.identities ?? []) {
|
|
7413
|
-
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
7414
|
-
const existing = db.prepare(
|
|
7415
|
-
"SELECT contact_id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7416
|
-
).get(normalizeIdentityKey(identity.type, identity.value));
|
|
7417
|
-
if (existing) {
|
|
7418
|
-
if (Number(existing.contact_id) !== receipt.primary.id)
|
|
7419
|
-
identityOwners.add(Number(existing.contact_id));
|
|
7420
|
-
continue;
|
|
7421
|
-
}
|
|
7422
|
-
const added = addIdentity(db, receipt.primary.id, identity);
|
|
7423
|
-
receipt.created.push(...added.created);
|
|
7424
|
-
}
|
|
7425
8273
|
const result = mutationToolResult("create", receipt, "contact");
|
|
8274
|
+
if (result.ok && receipt.outcome === "no_op") {
|
|
8275
|
+
result.outcome = "no_op";
|
|
8276
|
+
Object.assign(result.data, {
|
|
8277
|
+
matchedIdentities: receipt.matchedIdentities
|
|
8278
|
+
});
|
|
8279
|
+
return result;
|
|
8280
|
+
}
|
|
7426
8281
|
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
7427
8282
|
const warnings = db.prepare(
|
|
7428
8283
|
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
7429
8284
|
).all(receipt.primary.id).flatMap((row) => {
|
|
7430
|
-
const score =
|
|
7431
|
-
return score
|
|
8285
|
+
const score = fold(row.name) === fold(input.name) ? 0.92 : fuzzyNameScore(row.name, input.name);
|
|
8286
|
+
return score <= 0 ? [] : [
|
|
7432
8287
|
{
|
|
7433
8288
|
code: "possible_duplicate",
|
|
7434
8289
|
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
@@ -7511,6 +8366,13 @@ function manageContactToolHandler(db, rawInput) {
|
|
|
7511
8366
|
return contact.result;
|
|
7512
8367
|
}
|
|
7513
8368
|
if (input.action === "delete") {
|
|
8369
|
+
if (contact.value.is_owner)
|
|
8370
|
+
return toolFailure(
|
|
8371
|
+
"domain",
|
|
8372
|
+
"conflict",
|
|
8373
|
+
"Cannot delete owner.",
|
|
8374
|
+
"The owner contact cannot be deleted"
|
|
8375
|
+
);
|
|
7514
8376
|
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
7515
8377
|
return toolSuccess("Permanently deleted contact.", {
|
|
7516
8378
|
action: "delete",
|
|
@@ -7584,7 +8446,11 @@ var manageContactTool = defineAffinityTool({
|
|
|
7584
8446
|
},
|
|
7585
8447
|
["type", "value"]
|
|
7586
8448
|
),
|
|
7587
|
-
"Optional identities to attach.
|
|
8449
|
+
"Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return."
|
|
8450
|
+
),
|
|
8451
|
+
ifExists: enumSchema(
|
|
8452
|
+
"Identity collision policy; return attaches nothing.",
|
|
8453
|
+
["error", "return"]
|
|
7588
8454
|
),
|
|
7589
8455
|
name: stringSchema("Contact name."),
|
|
7590
8456
|
kind: enumSchema("Contact kind.", [
|
|
@@ -8003,7 +8869,10 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
8003
8869
|
});
|
|
8004
8870
|
}
|
|
8005
8871
|
const nextType = patch.type !== void 0 ? patch.type.trim() : row.type;
|
|
8006
|
-
const nextValue =
|
|
8872
|
+
const nextValue = normalizeIdentityValue(
|
|
8873
|
+
nextType,
|
|
8874
|
+
patch.value ?? row.value
|
|
8875
|
+
);
|
|
8007
8876
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
8008
8877
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
8009
8878
|
}
|
|
@@ -8062,6 +8931,10 @@ function contactLocatorSchema6(description) {
|
|
|
8062
8931
|
contactId: integerSchema("Exact contact id."),
|
|
8063
8932
|
identity: objectSchema(
|
|
8064
8933
|
{
|
|
8934
|
+
ifExists: enumSchema(
|
|
8935
|
+
"Return an existing identity on this contact without changes.",
|
|
8936
|
+
["error", "return"]
|
|
8937
|
+
),
|
|
8065
8938
|
type: stringSchema(
|
|
8066
8939
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8067
8940
|
),
|
|
@@ -8158,6 +9031,10 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
8158
9031
|
),
|
|
8159
9032
|
input: objectSchema(
|
|
8160
9033
|
{
|
|
9034
|
+
ifExists: enumSchema(
|
|
9035
|
+
"Return an existing identity on this contact without changes.",
|
|
9036
|
+
["error", "return"]
|
|
9037
|
+
),
|
|
8161
9038
|
type: stringSchema(
|
|
8162
9039
|
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8163
9040
|
),
|
|
@@ -8492,11 +9369,13 @@ var mergeContactsTool = defineAffinityTool({
|
|
|
8492
9369
|
});
|
|
8493
9370
|
|
|
8494
9371
|
// src/tools/record_event_tool.ts
|
|
8495
|
-
function normalizeParticipant2(p) {
|
|
9372
|
+
function normalizeParticipant2(db, p) {
|
|
8496
9373
|
let contactId = p.contactId;
|
|
8497
9374
|
if (contactId === void 0 && p.contact != null) {
|
|
8498
9375
|
const loc = p.contact;
|
|
8499
|
-
|
|
9376
|
+
const resolved = resolveContactLocator(db, loc);
|
|
9377
|
+
if (!resolved.ok) throw resolved.result;
|
|
9378
|
+
contactId = resolved.value.id;
|
|
8500
9379
|
}
|
|
8501
9380
|
const result = {
|
|
8502
9381
|
contactId,
|
|
@@ -8514,12 +9393,18 @@ function ensureOwnerParticipates2(db, participants) {
|
|
|
8514
9393
|
return [{ contactId: ownerId, role: "actor" }, ...participants];
|
|
8515
9394
|
}
|
|
8516
9395
|
function prepareParticipants2(db, raw, injectOwner) {
|
|
8517
|
-
const normalized = raw.map(normalizeParticipant2);
|
|
9396
|
+
const normalized = raw.map((p) => normalizeParticipant2(db, p));
|
|
8518
9397
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
8519
9398
|
}
|
|
8520
9399
|
function recordEventToolHandler(db, rawInput) {
|
|
8521
9400
|
const input = structuredClone(rawInput);
|
|
8522
|
-
const inputError = validateToolInput(
|
|
9401
|
+
const inputError = validateToolInput(
|
|
9402
|
+
recordEventTool.inputSchema,
|
|
9403
|
+
input,
|
|
9404
|
+
"input",
|
|
9405
|
+
Date.now(),
|
|
9406
|
+
true
|
|
9407
|
+
);
|
|
8523
9408
|
if (inputError)
|
|
8524
9409
|
return toolFailure(
|
|
8525
9410
|
"protocol",
|
|
@@ -8599,6 +9484,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
8599
9484
|
"correction"
|
|
8600
9485
|
]
|
|
8601
9486
|
),
|
|
9487
|
+
...eventMetadataSchema,
|
|
8602
9488
|
occurredAt: integerSchema(
|
|
8603
9489
|
"When the event occurred, Unix timestamp in milliseconds."
|
|
8604
9490
|
),
|
|
@@ -8607,6 +9493,19 @@ var recordEventTool = defineAffinityTool({
|
|
|
8607
9493
|
participants: arraySchema(
|
|
8608
9494
|
objectSchema(
|
|
8609
9495
|
{
|
|
9496
|
+
contact: objectSchema(
|
|
9497
|
+
{
|
|
9498
|
+
contactId: integerSchema("Contact id."),
|
|
9499
|
+
identity: objectSchema(
|
|
9500
|
+
{
|
|
9501
|
+
type: stringSchema("Identity type."),
|
|
9502
|
+
value: stringSchema("Identity value.")
|
|
9503
|
+
},
|
|
9504
|
+
["type", "value"]
|
|
9505
|
+
)
|
|
9506
|
+
},
|
|
9507
|
+
[]
|
|
9508
|
+
),
|
|
8610
9509
|
contactId: integerSchema(
|
|
8611
9510
|
"Participant contact id. The owner contact MUST be included."
|
|
8612
9511
|
),
|
|
@@ -8630,7 +9529,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
8630
9529
|
),
|
|
8631
9530
|
now: integerSchema("Optional timestamp override.")
|
|
8632
9531
|
},
|
|
8633
|
-
["occurredAt", "
|
|
9532
|
+
["occurredAt", "significance", "participants"],
|
|
8634
9533
|
"Event payload. Always required. Interaction events also require `type`."
|
|
8635
9534
|
)
|
|
8636
9535
|
},
|
|
@@ -8669,13 +9568,40 @@ function enrichReviewItem(db, item) {
|
|
|
8669
9568
|
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8670
9569
|
}
|
|
8671
9570
|
if (Array.isArray(item.participants)) {
|
|
8672
|
-
result.
|
|
8673
|
-
(p) =>
|
|
9571
|
+
result.participants = item.participants.map(
|
|
9572
|
+
(p) => ({
|
|
9573
|
+
id: p.contactId,
|
|
9574
|
+
name: name(p.contactId),
|
|
9575
|
+
role: p.role
|
|
9576
|
+
})
|
|
8674
9577
|
);
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
9578
|
+
for (const key of ["title", "subtitle", "snippet", "state", "eventId"])
|
|
9579
|
+
result[key] = void 0;
|
|
9580
|
+
}
|
|
9581
|
+
if (result.momentKind === null) result.momentKind = void 0;
|
|
9582
|
+
if (result.eventId === result.id) result.eventId = void 0;
|
|
9583
|
+
if (result.linkId === result.id) result.linkId = void 0;
|
|
9584
|
+
if (result.title === result.summary) result.title = void 0;
|
|
9585
|
+
if (item.kind === "link" && item.occursAt !== void 0) {
|
|
9586
|
+
for (const key of [
|
|
9587
|
+
"affinity",
|
|
9588
|
+
"trust",
|
|
9589
|
+
"rank",
|
|
9590
|
+
"recencyScore",
|
|
9591
|
+
"driftPriority",
|
|
9592
|
+
"readinessScore",
|
|
9593
|
+
"normalizedRank",
|
|
9594
|
+
"reciprocityScore",
|
|
9595
|
+
"radarScore",
|
|
9596
|
+
"subtitle",
|
|
9597
|
+
"snippet",
|
|
9598
|
+
"targetRef",
|
|
9599
|
+
"occursOnDate"
|
|
9600
|
+
])
|
|
9601
|
+
result[key] = void 0;
|
|
9602
|
+
} else if (item.kind === "link") {
|
|
9603
|
+
result.recommendedReason = void 0;
|
|
9604
|
+
result.snippet = void 0;
|
|
8679
9605
|
}
|
|
8680
9606
|
if (typeof item.leftContactId === "number")
|
|
8681
9607
|
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
@@ -8704,6 +9630,8 @@ function summarizeCount(count, singular) {
|
|
|
8704
9630
|
// src/tools/review_affinity_tool.ts
|
|
8705
9631
|
function buildReadOptions(input) {
|
|
8706
9632
|
return {
|
|
9633
|
+
...input.includeContent === void 0 ? {} : { includeContent: input.includeContent },
|
|
9634
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
8707
9635
|
...input.now === void 0 ? {} : { now: input.now },
|
|
8708
9636
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
8709
9637
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
@@ -8840,7 +9768,7 @@ function momentItem(moment) {
|
|
|
8840
9768
|
id: moment.eventId,
|
|
8841
9769
|
title: moment.summary,
|
|
8842
9770
|
subtitle: moment.momentKind,
|
|
8843
|
-
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
9771
|
+
snippet: moment.occurredOn ?? new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8844
9772
|
};
|
|
8845
9773
|
}
|
|
8846
9774
|
function commitmentItem(commitment) {
|
|
@@ -8850,7 +9778,7 @@ function commitmentItem(commitment) {
|
|
|
8850
9778
|
id: commitment.eventId,
|
|
8851
9779
|
title: commitment.summary,
|
|
8852
9780
|
subtitle: commitment.type,
|
|
8853
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
9781
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : commitment.dueOn ?? new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8854
9782
|
};
|
|
8855
9783
|
}
|
|
8856
9784
|
function radarItem(radar) {
|
|
@@ -8866,6 +9794,7 @@ function radarItem(radar) {
|
|
|
8866
9794
|
function upcomingDateItem(date) {
|
|
8867
9795
|
return {
|
|
8868
9796
|
...date,
|
|
9797
|
+
occursOn: new Date(date.occursOn).toISOString().slice(0, 10),
|
|
8869
9798
|
occursAt: date.occursOn,
|
|
8870
9799
|
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
8871
9800
|
kind: date.targetRef.kind,
|
|
@@ -8903,6 +9832,9 @@ function compactFilters(input) {
|
|
|
8903
9832
|
);
|
|
8904
9833
|
}
|
|
8905
9834
|
function reviewAffinityToolHandler(db, rawInput) {
|
|
9835
|
+
return reviewPage(db, rawInput, false);
|
|
9836
|
+
}
|
|
9837
|
+
function reviewPage(db, rawInput, pageOnly) {
|
|
8906
9838
|
const input = structuredClone(rawInput);
|
|
8907
9839
|
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8908
9840
|
if (inputError)
|
|
@@ -8917,6 +9849,41 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8917
9849
|
if (invalid) {
|
|
8918
9850
|
return invalid;
|
|
8919
9851
|
}
|
|
9852
|
+
if (!pageOnly && input.view !== "graph.chart") {
|
|
9853
|
+
const limit = input.limit ?? 50;
|
|
9854
|
+
const offset = input.offset ?? 0;
|
|
9855
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
|
|
9856
|
+
return toolFailure(
|
|
9857
|
+
"protocol",
|
|
9858
|
+
"invalid_input",
|
|
9859
|
+
"Invalid pagination.",
|
|
9860
|
+
"limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
|
|
9861
|
+
);
|
|
9862
|
+
const all = [];
|
|
9863
|
+
let page;
|
|
9864
|
+
do {
|
|
9865
|
+
page = reviewPage(
|
|
9866
|
+
db,
|
|
9867
|
+
{ ...input, limit: 250, offset: all.length },
|
|
9868
|
+
true
|
|
9869
|
+
);
|
|
9870
|
+
if (!page.ok) return page;
|
|
9871
|
+
all.push(...page.data.items);
|
|
9872
|
+
} while (page.data.items.length === 250);
|
|
9873
|
+
const items2 = all.slice(offset, offset + limit);
|
|
9874
|
+
return toolSuccess(
|
|
9875
|
+
summarizeCount(items2.length, "item"),
|
|
9876
|
+
{
|
|
9877
|
+
view: input.view,
|
|
9878
|
+
items: items2,
|
|
9879
|
+
count: items2.length,
|
|
9880
|
+
total: all.length,
|
|
9881
|
+
hasMore: offset + items2.length < all.length,
|
|
9882
|
+
appliedFilters: compactFilters(input)
|
|
9883
|
+
},
|
|
9884
|
+
{ entities: items2.map(({ content: _content, ...item }) => item) }
|
|
9885
|
+
);
|
|
9886
|
+
}
|
|
8920
9887
|
const options = buildReadOptions(input);
|
|
8921
9888
|
let items = [];
|
|
8922
9889
|
let chart;
|
|
@@ -8942,12 +9909,26 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
8942
9909
|
{
|
|
8943
9910
|
...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
|
|
8944
9911
|
...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
|
|
8945
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
9912
|
+
...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
|
|
8946
9913
|
...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
|
|
8947
9914
|
},
|
|
8948
9915
|
options
|
|
8949
9916
|
).map(duplicateCandidateItem);
|
|
8950
9917
|
break;
|
|
9918
|
+
case "events.thread": {
|
|
9919
|
+
if (!input.ref)
|
|
9920
|
+
return toolFailure(
|
|
9921
|
+
"protocol",
|
|
9922
|
+
"invalid_input",
|
|
9923
|
+
"Missing thread reference.",
|
|
9924
|
+
"ref is required for events.thread."
|
|
9925
|
+
);
|
|
9926
|
+
items = getEventThread(db, input.ref.source, input.ref.id, {
|
|
9927
|
+
...options,
|
|
9928
|
+
...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
|
|
9929
|
+
}).map(toEventListToolItem);
|
|
9930
|
+
break;
|
|
9931
|
+
}
|
|
8951
9932
|
case "events.contact_journal": {
|
|
8952
9933
|
const locator = input.contact;
|
|
8953
9934
|
if (locator === void 0) {
|
|
@@ -9089,6 +10070,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9089
10070
|
items = listRadar(
|
|
9090
10071
|
db,
|
|
9091
10072
|
{
|
|
10073
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
9092
10074
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
9093
10075
|
...input.linkState === void 0 ? {} : { state: input.linkState }
|
|
9094
10076
|
},
|
|
@@ -9138,6 +10120,14 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9138
10120
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
9139
10121
|
break;
|
|
9140
10122
|
}
|
|
10123
|
+
if (input.includeContent && ["events.moments", "commitments.open"].includes(input.view))
|
|
10124
|
+
items = items.map((item) => {
|
|
10125
|
+
const content = getEventById(
|
|
10126
|
+
db,
|
|
10127
|
+
Number(item.eventId ?? item.id)
|
|
10128
|
+
)?.content;
|
|
10129
|
+
return content === void 0 ? item : { ...item, content };
|
|
10130
|
+
});
|
|
9141
10131
|
items = items.map((item) => enrichReviewItem(db, item));
|
|
9142
10132
|
return toolSuccess(
|
|
9143
10133
|
summarizeCount(items.length, "item"),
|
|
@@ -9149,7 +10139,7 @@ function reviewAffinityToolHandler(db, rawInput) {
|
|
|
9149
10139
|
...chart === void 0 ? {} : { chart }
|
|
9150
10140
|
},
|
|
9151
10141
|
{
|
|
9152
|
-
entities: items,
|
|
10142
|
+
entities: items.map(({ content: _content, ...item }) => item),
|
|
9153
10143
|
warnings: items.length === 0 ? [
|
|
9154
10144
|
toolWarning(
|
|
9155
10145
|
"empty_result",
|
|
@@ -9195,15 +10185,21 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9195
10185
|
includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
|
|
9196
10186
|
since: "Optional lower timestamp bound for time-based views.",
|
|
9197
10187
|
until: "Optional upper timestamp bound for time-based views.",
|
|
9198
|
-
limit: "Optional result limit.",
|
|
10188
|
+
limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
|
|
9199
10189
|
offset: "Optional result offset."
|
|
9200
10190
|
},
|
|
9201
10191
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
9202
10192
|
inputSchema: objectSchema(
|
|
9203
10193
|
{
|
|
10194
|
+
ref: externalRefSchema,
|
|
10195
|
+
includeContent: booleanSchema(
|
|
10196
|
+
"Include verbatim content in event rows; defaults to false."
|
|
10197
|
+
),
|
|
10198
|
+
channel: stringSchema("Filter events by channel."),
|
|
9204
10199
|
view: enumSchema("Which review surface to return.", [
|
|
9205
10200
|
"contacts.list",
|
|
9206
10201
|
"contacts.duplicates",
|
|
10202
|
+
"events.thread",
|
|
9207
10203
|
"events.contact_journal",
|
|
9208
10204
|
"events.link_timeline",
|
|
9209
10205
|
"events.moments",
|
|
@@ -9258,7 +10254,20 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9258
10254
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
9259
10255
|
includeObserved: booleanSchema("Whether observed links are included."),
|
|
9260
10256
|
eventTypes: arraySchema(
|
|
9261
|
-
|
|
10257
|
+
enumSchema("Event type.", [
|
|
10258
|
+
"conversation",
|
|
10259
|
+
"activity",
|
|
10260
|
+
"gift",
|
|
10261
|
+
"support",
|
|
10262
|
+
"milestone",
|
|
10263
|
+
"observation",
|
|
10264
|
+
"conflict",
|
|
10265
|
+
"correction",
|
|
10266
|
+
"transaction",
|
|
10267
|
+
"promise",
|
|
10268
|
+
"agreement",
|
|
10269
|
+
"date_anchor"
|
|
10270
|
+
]),
|
|
9262
10271
|
"Optional event-type filter."
|
|
9263
10272
|
),
|
|
9264
10273
|
momentKind: enumSchema("Optional moment kind filter.", [
|
|
@@ -9281,13 +10290,17 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9281
10290
|
]),
|
|
9282
10291
|
horizonDays: integerSchema("Optional horizon-days filter."),
|
|
9283
10292
|
exactOnly: booleanSchema("Whether duplicate review is exact-only."),
|
|
9284
|
-
minScore: numberSchema(
|
|
10293
|
+
minScore: numberSchema(
|
|
10294
|
+
"Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
|
|
10295
|
+
),
|
|
9285
10296
|
includeDismissed: booleanSchema(
|
|
9286
10297
|
"Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
|
|
9287
10298
|
),
|
|
9288
10299
|
since: integerSchema("Optional lower timestamp bound."),
|
|
9289
10300
|
until: integerSchema("Optional upper timestamp bound."),
|
|
9290
|
-
limit: integerSchema(
|
|
10301
|
+
limit: integerSchema(
|
|
10302
|
+
"Optional result limit, 1..10000 (default 50); use offset for paging."
|
|
10303
|
+
),
|
|
9291
10304
|
offset: integerSchema("Optional result offset.")
|
|
9292
10305
|
},
|
|
9293
10306
|
["view"],
|
|
@@ -9298,6 +10311,9 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
9298
10311
|
|
|
9299
10312
|
// src/tools/search_affinity_tool.ts
|
|
9300
10313
|
function searchAffinityToolHandler(db, rawInput) {
|
|
10314
|
+
return searchPage(db, rawInput, false);
|
|
10315
|
+
}
|
|
10316
|
+
function searchPage(db, rawInput, pageOnly) {
|
|
9301
10317
|
const input = structuredClone(rawInput);
|
|
9302
10318
|
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9303
10319
|
if (inputError)
|
|
@@ -9308,11 +10324,53 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9308
10324
|
inputError
|
|
9309
10325
|
);
|
|
9310
10326
|
return withToolHandling(() => {
|
|
10327
|
+
if (!pageOnly) {
|
|
10328
|
+
const limit = input.limit ?? 50;
|
|
10329
|
+
const offset = input.offset ?? 0;
|
|
10330
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
|
|
10331
|
+
return toolFailure(
|
|
10332
|
+
"protocol",
|
|
10333
|
+
"invalid_input",
|
|
10334
|
+
"Invalid pagination.",
|
|
10335
|
+
"limit must be 1..250; offset must be non-negative"
|
|
10336
|
+
);
|
|
10337
|
+
const all = [];
|
|
10338
|
+
const scopes = input.scope === "all" ? ["contacts", "events"] : [input.scope ?? "contacts"];
|
|
10339
|
+
for (const scope of scopes) {
|
|
10340
|
+
let page;
|
|
10341
|
+
let nextOffset = 0;
|
|
10342
|
+
do {
|
|
10343
|
+
page = searchPage(
|
|
10344
|
+
db,
|
|
10345
|
+
{ ...input, scope, limit: 250, offset: nextOffset },
|
|
10346
|
+
true
|
|
10347
|
+
);
|
|
10348
|
+
if (!page.ok) return page;
|
|
10349
|
+
all.push(...page.data.items);
|
|
10350
|
+
nextOffset += page.data.count;
|
|
10351
|
+
} while (page.data.count === 250);
|
|
10352
|
+
}
|
|
10353
|
+
const items2 = all.slice(offset, offset + limit);
|
|
10354
|
+
return toolSuccess(
|
|
10355
|
+
summarizeCount(
|
|
10356
|
+
items2.length,
|
|
10357
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10358
|
+
),
|
|
10359
|
+
{
|
|
10360
|
+
query: input.query,
|
|
10361
|
+
items: items2,
|
|
10362
|
+
count: items2.length,
|
|
10363
|
+
total: all.length,
|
|
10364
|
+
hasMore: offset + items2.length < all.length
|
|
10365
|
+
},
|
|
10366
|
+
{ entities: items2 }
|
|
10367
|
+
);
|
|
10368
|
+
}
|
|
9311
10369
|
const options = {
|
|
9312
10370
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
9313
10371
|
...input.offset === void 0 ? {} : { offset: input.offset }
|
|
9314
10372
|
};
|
|
9315
|
-
const rows = searchContacts(
|
|
10373
|
+
const rows = input.scope === "events" ? [] : searchContacts(
|
|
9316
10374
|
db,
|
|
9317
10375
|
input.query,
|
|
9318
10376
|
{
|
|
@@ -9326,9 +10384,15 @@ function searchAffinityToolHandler(db, rawInput) {
|
|
|
9326
10384
|
},
|
|
9327
10385
|
options
|
|
9328
10386
|
);
|
|
9329
|
-
const items =
|
|
10387
|
+
const items = [
|
|
10388
|
+
...rows.map(toContactListToolItem),
|
|
10389
|
+
...input.scope === "events" || input.scope === "all" ? searchEvents(db, input.query, options) : []
|
|
10390
|
+
];
|
|
9330
10391
|
return toolSuccess(
|
|
9331
|
-
summarizeCount(
|
|
10392
|
+
summarizeCount(
|
|
10393
|
+
items.length,
|
|
10394
|
+
input.scope === "events" ? "event" : input.scope === "all" ? "item" : "contact"
|
|
10395
|
+
),
|
|
9332
10396
|
{
|
|
9333
10397
|
query: input.query,
|
|
9334
10398
|
items,
|
|
@@ -9364,6 +10428,11 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
9364
10428
|
outputDescription: "Returns compact contact hits, the original query, and the hit count.",
|
|
9365
10429
|
inputSchema: objectSchema(
|
|
9366
10430
|
{
|
|
10431
|
+
scope: enumSchema("Search scope; defaults to contacts.", [
|
|
10432
|
+
"contacts",
|
|
10433
|
+
"events",
|
|
10434
|
+
"all"
|
|
10435
|
+
]),
|
|
9367
10436
|
query: stringSchema("Free-text or natural-key query."),
|
|
9368
10437
|
kind: enumSchema("Optional contact kind filter.", [
|
|
9369
10438
|
"human",
|
|
@@ -9402,6 +10471,268 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
9402
10471
|
handler: searchAffinityToolHandler
|
|
9403
10472
|
});
|
|
9404
10473
|
|
|
10474
|
+
// src/events/event_replay.ts
|
|
10475
|
+
function replayEventLinks(db, linkIds, now) {
|
|
10476
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
10477
|
+
for (const linkId of linkIds) {
|
|
10478
|
+
const saved = db.prepare("SELECT state_json FROM event_link_baselines WHERE link_id=?").get(linkId);
|
|
10479
|
+
if (!saved) continue;
|
|
10480
|
+
const baseline = JSON.parse(saved.state_json);
|
|
10481
|
+
const effects = db.prepare(
|
|
10482
|
+
"SELECT event_id, participant_json, provenance_json, created_at FROM link_event_effects WHERE link_id=? ORDER BY event_id"
|
|
10483
|
+
).all(linkId);
|
|
10484
|
+
const overrides = db.prepare(
|
|
10485
|
+
"SELECT after_event_id,state FROM event_link_state_changes WHERE link_id=? ORDER BY id"
|
|
10486
|
+
).all(linkId);
|
|
10487
|
+
let overrideIndex = 0;
|
|
10488
|
+
const applyOverrides = (beforeId) => {
|
|
10489
|
+
while (overrideIndex < overrides.length && (overrides[overrideIndex]?.after_event_id ?? Number.POSITIVE_INFINITY) < beforeId) {
|
|
10490
|
+
const override = overrides[overrideIndex];
|
|
10491
|
+
if (override)
|
|
10492
|
+
db.prepare("UPDATE links SET state=? WHERE id=?").run(
|
|
10493
|
+
override.state,
|
|
10494
|
+
linkId
|
|
10495
|
+
);
|
|
10496
|
+
overrideIndex++;
|
|
10497
|
+
}
|
|
10498
|
+
};
|
|
10499
|
+
db.prepare("DELETE FROM link_event_effects WHERE link_id=?").run(linkId);
|
|
10500
|
+
db.prepare(
|
|
10501
|
+
"UPDATE links SET rank=?,affinity=?,trust=?,state=?,cadence_days=?,updated_at=? WHERE id=?"
|
|
10502
|
+
).run(
|
|
10503
|
+
baseline.rank,
|
|
10504
|
+
baseline.affinity,
|
|
10505
|
+
baseline.trust,
|
|
10506
|
+
baseline.state,
|
|
10507
|
+
baseline.cadence_days,
|
|
10508
|
+
now,
|
|
10509
|
+
linkId
|
|
10510
|
+
);
|
|
10511
|
+
for (const effect of effects) {
|
|
10512
|
+
eventIds.add(effect.event_id);
|
|
10513
|
+
applyOverrides(effect.event_id);
|
|
10514
|
+
if (!db.prepare("SELECT id FROM events WHERE id=? AND deleted_at IS NULL").get(effect.event_id))
|
|
10515
|
+
continue;
|
|
10516
|
+
const event = loadEventRecord(db, effect.event_id);
|
|
10517
|
+
const link = db.prepare("SELECT from_contact_id,to_contact_id FROM links WHERE id=?").get(linkId);
|
|
10518
|
+
const participant = effect.participant_json ? JSON.parse(effect.participant_json) : event.participants.find((p) => p.contactId === link.to_contact_id) ?? event.participants[0];
|
|
10519
|
+
const provenance = db.prepare("SELECT provenance_json FROM events WHERE id=?").get(event.id);
|
|
10520
|
+
applyEventEffectToLink(db, {
|
|
10521
|
+
linkId,
|
|
10522
|
+
eventId: event.id,
|
|
10523
|
+
eventType: event.type,
|
|
10524
|
+
occurredAt: event.occurredAt,
|
|
10525
|
+
significance: event.significance,
|
|
10526
|
+
participant,
|
|
10527
|
+
provenance: provenance.provenance_json ? JSON.parse(provenance.provenance_json) : effect.provenance_json ? JSON.parse(effect.provenance_json) : void 0,
|
|
10528
|
+
now: effect.created_at
|
|
10529
|
+
});
|
|
10530
|
+
}
|
|
10531
|
+
applyOverrides(Number.POSITIVE_INFINITY);
|
|
10532
|
+
refreshLinkRollup(db, linkId, now);
|
|
10533
|
+
}
|
|
10534
|
+
for (const eventId of eventIds)
|
|
10535
|
+
db.prepare(
|
|
10536
|
+
"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=?"
|
|
10537
|
+
).run(eventId);
|
|
10538
|
+
refreshAllBridgeScores(db, now);
|
|
10539
|
+
}
|
|
10540
|
+
|
|
10541
|
+
// src/events/manage_event.ts
|
|
10542
|
+
function requireEventTarget(db, target) {
|
|
10543
|
+
const selector = typeof target === "number" ? { eventId: target } : target;
|
|
10544
|
+
if (!selector || selector.eventId === void 0 === (selector.ref === void 0))
|
|
10545
|
+
throw new AffinityValidationError("Provide exactly one of eventId or ref");
|
|
10546
|
+
const event = selector.ref ? getEventByExternalRef(db, selector.ref.source, selector.ref.id) : getEventById(db, selector.eventId);
|
|
10547
|
+
if (!event) throw new AffinityNotFoundError("event not found");
|
|
10548
|
+
return event;
|
|
10549
|
+
}
|
|
10550
|
+
function affected(db, id) {
|
|
10551
|
+
return db.prepare("SELECT link_id FROM link_event_effects WHERE event_id=?").all(id).map((r) => r.link_id);
|
|
10552
|
+
}
|
|
10553
|
+
function reviseEvent(db, target, patch) {
|
|
10554
|
+
return withTransaction(db, () => {
|
|
10555
|
+
const allowed = [
|
|
10556
|
+
"summary",
|
|
10557
|
+
"content",
|
|
10558
|
+
"occurredAt",
|
|
10559
|
+
"significance",
|
|
10560
|
+
"channel",
|
|
10561
|
+
"type",
|
|
10562
|
+
"externalRef"
|
|
10563
|
+
];
|
|
10564
|
+
for (const key of Object.keys(patch))
|
|
10565
|
+
if (!allowed.includes(key))
|
|
10566
|
+
throw new AffinityValidationError(
|
|
10567
|
+
`unknown patch key ${key}; allowed keys: ${allowed.join(", ")}`
|
|
10568
|
+
);
|
|
10569
|
+
const event = requireEventTarget(db, target);
|
|
10570
|
+
const next = { ...event, ...patch };
|
|
10571
|
+
const { momentKind: _momentKind, ...metadata } = next;
|
|
10572
|
+
validateEventMetadata(metadata);
|
|
10573
|
+
Object.assign(next, metadata);
|
|
10574
|
+
if (patch.summary !== void 0 && (typeof patch.summary !== "string" || !patch.summary.trim()))
|
|
10575
|
+
throw new AffinityValidationError("summary must be non-empty");
|
|
10576
|
+
if (patch.occurredAt !== void 0)
|
|
10577
|
+
assertFiniteTimestamp(patch.occurredAt, "occurredAt");
|
|
10578
|
+
if (!Number.isInteger(next.significance) || next.significance < 1 || next.significance > 10)
|
|
10579
|
+
throw new AffinityValidationError(
|
|
10580
|
+
"significance must be an integer from 1 to 10"
|
|
10581
|
+
);
|
|
10582
|
+
const interactions = [
|
|
10583
|
+
"conversation",
|
|
10584
|
+
"activity",
|
|
10585
|
+
"gift",
|
|
10586
|
+
"support",
|
|
10587
|
+
"conflict",
|
|
10588
|
+
"correction"
|
|
10589
|
+
];
|
|
10590
|
+
if (next.type !== event.type && !(interactions.includes(event.type) && interactions.includes(next.type)) && !(["promise", "agreement"].includes(event.type) && ["promise", "agreement"].includes(next.type)))
|
|
10591
|
+
throw new AffinityValidationError(
|
|
10592
|
+
"type must stay within the same event kind"
|
|
10593
|
+
);
|
|
10594
|
+
if (patch.externalRef && event.externalRef)
|
|
10595
|
+
throw new AffinityConflictError(
|
|
10596
|
+
"externalRef can only be set when absent"
|
|
10597
|
+
);
|
|
10598
|
+
if (patch.externalRef && next.externalRef && getEventByExternalRef(db, next.externalRef.source, next.externalRef.id))
|
|
10599
|
+
throw new AffinityConflictError(
|
|
10600
|
+
"externalRef already belongs to an event"
|
|
10601
|
+
);
|
|
10602
|
+
const now = Date.now();
|
|
10603
|
+
const links = affected(db, event.id);
|
|
10604
|
+
db.prepare(
|
|
10605
|
+
"UPDATE events SET summary=?,content=?,occurred_at=?,occurred_on=?,significance=?,channel=?,type=?,external_source=?,external_id=?,updated_at=? WHERE id=?"
|
|
10606
|
+
).run(
|
|
10607
|
+
next.summary.trim(),
|
|
10608
|
+
next.content ?? null,
|
|
10609
|
+
next.occurredAt,
|
|
10610
|
+
patch.occurredAt === void 0 ? event.occurredOn ?? null : new Date(next.occurredAt).toISOString().slice(0, 10),
|
|
10611
|
+
next.significance,
|
|
10612
|
+
next.channel ?? null,
|
|
10613
|
+
next.type,
|
|
10614
|
+
next.externalRef?.source ?? null,
|
|
10615
|
+
next.externalRef?.id ?? null,
|
|
10616
|
+
now,
|
|
10617
|
+
event.id
|
|
10618
|
+
);
|
|
10619
|
+
if (patch.type && ["promise", "agreement"].includes(patch.type))
|
|
10620
|
+
db.prepare(
|
|
10621
|
+
"UPDATE open_commitments SET commitment_type=? WHERE event_id=?"
|
|
10622
|
+
).run(patch.type, event.id);
|
|
10623
|
+
if (patch.significance !== void 0 || patch.type !== void 0 || patch.occurredAt !== void 0)
|
|
10624
|
+
replayEventLinks(db, links, now);
|
|
10625
|
+
return buildEventMutationReceipt(requireEventTarget(db, event.id), {
|
|
10626
|
+
updated: [{ kind: "event", id: event.id }],
|
|
10627
|
+
affectedLinks: links
|
|
10628
|
+
});
|
|
10629
|
+
});
|
|
10630
|
+
}
|
|
10631
|
+
function deleteEvent(db, target, options) {
|
|
10632
|
+
return withTransaction(db, () => {
|
|
10633
|
+
if (options?.confirm !== true)
|
|
10634
|
+
throw new AffinityValidationError("deleteEvent requires confirm: true");
|
|
10635
|
+
const event = requireEventTarget(db, target);
|
|
10636
|
+
const links = affected(db, event.id);
|
|
10637
|
+
for (const table of [
|
|
10638
|
+
"event_participants",
|
|
10639
|
+
"attributes",
|
|
10640
|
+
"open_commitments",
|
|
10641
|
+
"upcoming_occurrences",
|
|
10642
|
+
"link_event_effects"
|
|
10643
|
+
])
|
|
10644
|
+
db.prepare(`DELETE FROM ${table} WHERE event_id=?`).run(event.id);
|
|
10645
|
+
db.prepare(
|
|
10646
|
+
"UPDATE events SET parent_event_id=NULL WHERE parent_event_id=?"
|
|
10647
|
+
).run(event.id);
|
|
10648
|
+
db.prepare("DELETE FROM events WHERE id=?").run(event.id);
|
|
10649
|
+
replayEventLinks(db, links, Date.now());
|
|
10650
|
+
return buildEventMutationReceipt(event, {
|
|
10651
|
+
removed: [{ kind: "event", id: event.id }],
|
|
10652
|
+
affectedLinks: links
|
|
10653
|
+
});
|
|
10654
|
+
});
|
|
10655
|
+
}
|
|
10656
|
+
|
|
10657
|
+
// src/tools/manage_event_tool.ts
|
|
10658
|
+
function manageEventToolHandler(db, raw) {
|
|
10659
|
+
const input = structuredClone(raw);
|
|
10660
|
+
const error = validateToolInput(
|
|
10661
|
+
manageEventTool.inputSchema,
|
|
10662
|
+
input,
|
|
10663
|
+
"input",
|
|
10664
|
+
Date.now(),
|
|
10665
|
+
true
|
|
10666
|
+
);
|
|
10667
|
+
if (error)
|
|
10668
|
+
return toolFailure("protocol", "invalid_input", "Invalid input.", error);
|
|
10669
|
+
return withToolHandling(() => {
|
|
10670
|
+
const target = {
|
|
10671
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId },
|
|
10672
|
+
...input.ref === void 0 ? {} : { ref: input.ref }
|
|
10673
|
+
};
|
|
10674
|
+
return mutationToolResult(
|
|
10675
|
+
input.action,
|
|
10676
|
+
input.action === "revise" ? reviseEvent(db, target, input.patch ?? {}) : deleteEvent(db, target, { confirm: input.confirm === true }),
|
|
10677
|
+
"event"
|
|
10678
|
+
);
|
|
10679
|
+
}, "Event management failed.");
|
|
10680
|
+
}
|
|
10681
|
+
var manageEventTool = defineAffinityTool({
|
|
10682
|
+
name: manageEventToolName,
|
|
10683
|
+
description: "Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.",
|
|
10684
|
+
whenToUse: "Correct imported event text or metadata, or remove an event.",
|
|
10685
|
+
whenNotToUse: "Use record_event to capture new evidence.",
|
|
10686
|
+
sideEffects: "writes_state",
|
|
10687
|
+
readOnly: false,
|
|
10688
|
+
supportsClarification: false,
|
|
10689
|
+
targetKinds: ["event"],
|
|
10690
|
+
inputDescriptions: {
|
|
10691
|
+
action: "revise or delete",
|
|
10692
|
+
eventId: "Exact event id",
|
|
10693
|
+
ref: "External event reference",
|
|
10694
|
+
patch: "Fields to revise",
|
|
10695
|
+
confirm: "Required true for deletion"
|
|
10696
|
+
},
|
|
10697
|
+
outputDescription: "Event mutation receipt and affected links.",
|
|
10698
|
+
inputSchema: objectSchema(
|
|
10699
|
+
{
|
|
10700
|
+
action: enumSchema("Action.", ["revise", "delete"]),
|
|
10701
|
+
eventId: integerSchema("Event id; provide eventId or ref."),
|
|
10702
|
+
ref: externalRefSchema,
|
|
10703
|
+
confirm: booleanSchema("Must be true to delete."),
|
|
10704
|
+
patch: objectSchema(
|
|
10705
|
+
{
|
|
10706
|
+
summary: stringSchema("Short non-empty label."),
|
|
10707
|
+
content: stringSchema("Verbatim content, at most 1 MB UTF-8."),
|
|
10708
|
+
occurredAt: integerSchema("Event timestamp."),
|
|
10709
|
+
significance: integerSchema("Significance 1..10."),
|
|
10710
|
+
channel: stringSchema("Channel, at most 40 characters."),
|
|
10711
|
+
type: enumSchema("Type within the same event kind.", [
|
|
10712
|
+
"conversation",
|
|
10713
|
+
"activity",
|
|
10714
|
+
"gift",
|
|
10715
|
+
"support",
|
|
10716
|
+
"conflict",
|
|
10717
|
+
"correction",
|
|
10718
|
+
"observation",
|
|
10719
|
+
"milestone",
|
|
10720
|
+
"transaction",
|
|
10721
|
+
"promise",
|
|
10722
|
+
"agreement",
|
|
10723
|
+
"date_anchor"
|
|
10724
|
+
]),
|
|
10725
|
+
externalRef: externalRefSchema
|
|
10726
|
+
},
|
|
10727
|
+
[],
|
|
10728
|
+
"Fields to revise; externalRef may only be added when absent."
|
|
10729
|
+
)
|
|
10730
|
+
},
|
|
10731
|
+
["action"]
|
|
10732
|
+
),
|
|
10733
|
+
handler: manageEventToolHandler
|
|
10734
|
+
});
|
|
10735
|
+
|
|
9405
10736
|
// src/tools/tool_registry.ts
|
|
9406
10737
|
var affinityTools = [
|
|
9407
10738
|
searchAffinityTool,
|
|
@@ -9415,7 +10746,8 @@ var affinityTools = [
|
|
|
9415
10746
|
recordEventTool,
|
|
9416
10747
|
manageCommitmentTool,
|
|
9417
10748
|
manageDateAnchorTool,
|
|
9418
|
-
manageAttributeTool
|
|
10749
|
+
manageAttributeTool,
|
|
10750
|
+
manageEventTool
|
|
9419
10751
|
];
|
|
9420
10752
|
function listAffinityToolDefinitions() {
|
|
9421
10753
|
return [...affinityTools];
|
|
@@ -9426,6 +10758,24 @@ function getAffinityToolByName(name) {
|
|
|
9426
10758
|
|
|
9427
10759
|
// src/tools/tool_mapping.ts
|
|
9428
10760
|
var affinityToolMappings = [
|
|
10761
|
+
{
|
|
10762
|
+
source: "getEventById",
|
|
10763
|
+
tool: inspectAffinityItemToolName,
|
|
10764
|
+
action: "event"
|
|
10765
|
+
},
|
|
10766
|
+
{
|
|
10767
|
+
source: "getEventByExternalRef",
|
|
10768
|
+
tool: inspectAffinityItemToolName,
|
|
10769
|
+
action: "event"
|
|
10770
|
+
},
|
|
10771
|
+
{
|
|
10772
|
+
source: "getEventThread",
|
|
10773
|
+
tool: reviewAffinityToolName,
|
|
10774
|
+
view: "events.thread"
|
|
10775
|
+
},
|
|
10776
|
+
{ source: "searchEvents", tool: searchAffinityToolName },
|
|
10777
|
+
{ source: "reviseEvent", tool: "manage_event", action: "revise" },
|
|
10778
|
+
{ source: "deleteEvent", tool: "manage_event", action: "delete" },
|
|
9429
10779
|
{ source: "exportContact", tool: manageContactToolName, action: "export" },
|
|
9430
10780
|
{ source: "exportAffinity", tool: manageContactToolName, action: "export" },
|
|
9431
10781
|
{ source: "deleteContact", tool: manageContactToolName, action: "delete" },
|
|
@@ -9616,6 +10966,7 @@ __export(write_exports, {
|
|
|
9616
10966
|
addIdentity: () => addIdentity,
|
|
9617
10967
|
createContact: () => createContact,
|
|
9618
10968
|
deleteContact: () => deleteContact,
|
|
10969
|
+
deleteEvent: () => deleteEvent,
|
|
9619
10970
|
dismissDuplicate: () => dismissDuplicate,
|
|
9620
10971
|
mergeContacts: () => mergeContacts,
|
|
9621
10972
|
overrideLinkState: () => overrideLinkState,
|
|
@@ -9632,6 +10983,7 @@ __export(write_exports, {
|
|
|
9632
10983
|
reviseBond: () => reviseBond,
|
|
9633
10984
|
reviseContact: () => reviseContact,
|
|
9634
10985
|
reviseDateAnchor: () => reviseDateAnchor,
|
|
10986
|
+
reviseEvent: () => reviseEvent,
|
|
9635
10987
|
reviseIdentity: () => reviseIdentity,
|
|
9636
10988
|
seedSocialLink: () => seedSocialLink,
|
|
9637
10989
|
setAttribute: () => setAttribute,
|