@ghostpaw/affinity 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -388,11 +388,32 @@ function initContactMergesTables(db) {
388
388
  );
389
389
  }
390
390
 
391
+ // src/merges/init_dismissed_duplicates_tables.ts
392
+ function initDismissedDuplicatesTables(db) {
393
+ db.exec(`
394
+ CREATE TABLE IF NOT EXISTS dismissed_duplicates (
395
+ left_id INTEGER NOT NULL,
396
+ right_id INTEGER NOT NULL,
397
+ reason TEXT,
398
+ dismissed_at INTEGER NOT NULL,
399
+ PRIMARY KEY (left_id, right_id),
400
+ CHECK (left_id < right_id)
401
+ )
402
+ `);
403
+ db.exec(
404
+ "CREATE INDEX IF NOT EXISTS idx_dismissed_duplicates_left ON dismissed_duplicates(left_id)"
405
+ );
406
+ db.exec(
407
+ "CREATE INDEX IF NOT EXISTS idx_dismissed_duplicates_right ON dismissed_duplicates(right_id)"
408
+ );
409
+ }
410
+
391
411
  // src/init_affinity_tables.ts
392
412
  function initAffinityTables(db) {
393
413
  initContactsTables(db);
394
414
  initContactRollupsTables(db);
395
415
  initContactMergesTables(db);
416
+ initDismissedDuplicatesTables(db);
396
417
  initIdentitiesTables(db);
397
418
  initLinksTables(db);
398
419
  initLinkRollupsTables(db);
@@ -500,6 +521,7 @@ __export(read_exports, {
500
521
  getMergeHistory: () => getMergeHistory,
501
522
  getOwnerProfile: () => getOwnerProfile,
502
523
  listContacts: () => listContacts,
524
+ listDismissedDuplicates: () => listDismissedDuplicates,
503
525
  listDuplicateCandidates: () => listDuplicateCandidates,
504
526
  listMoments: () => listMoments,
505
527
  listObservedLinks: () => listObservedLinks,
@@ -1352,8 +1374,24 @@ function listDuplicateCandidates(db, filters, options) {
1352
1374
  }
1353
1375
  }
1354
1376
  }
1377
+ const dismissedRows = db.prepare("SELECT left_id, right_id FROM dismissed_duplicates").all();
1378
+ const dismissedSet = new Set(
1379
+ dismissedRows.map((r) => pairKey(r.left_id, r.right_id))
1380
+ );
1381
+ const includeDismissed = filters?.includeDismissed === true;
1382
+ for (const candidate of pairs.values()) {
1383
+ if (dismissedSet.has(
1384
+ pairKey(candidate.leftContactId, candidate.rightContactId)
1385
+ )) {
1386
+ candidate.dismissed = true;
1387
+ }
1388
+ }
1355
1389
  const minScore = filters?.minScore ?? 0;
1356
- return [...pairs.values()].filter((candidate) => candidate.matchScore >= minScore).sort((left, right) => {
1390
+ return [...pairs.values()].filter((candidate) => {
1391
+ if (candidate.matchScore < minScore) return false;
1392
+ if (candidate.dismissed === true && !includeDismissed) return false;
1393
+ return true;
1394
+ }).sort((left, right) => {
1357
1395
  const leftExact = left.matchScore === 1 ? 1 : 0;
1358
1396
  const rightExact = right.matchScore === 1 ? 1 : 0;
1359
1397
  return rightExact - leftExact || right.matchScore - left.matchScore;
@@ -3416,6 +3454,12 @@ function mergeContacts(db, input) {
3416
3454
  `INSERT INTO contact_merges (winner_contact_id, loser_contact_id, merged_at, reason_summary, manual)
3417
3455
  VALUES (?, ?, ?, ?, 1)`
3418
3456
  ).run(winnerId, loserId, now, reasonSummary);
3457
+ db.prepare(
3458
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
3459
+ ).run(loserId, loserId);
3460
+ db.prepare(
3461
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
3462
+ ).run(winnerId, winnerId);
3419
3463
  db.prepare(
3420
3464
  "DELETE FROM link_rollups WHERE link_id IN (SELECT id FROM links WHERE removed_at IS NOT NULL)"
3421
3465
  ).run();
@@ -3443,6 +3487,117 @@ function mergeContacts(db, input) {
3443
3487
  });
3444
3488
  }
3445
3489
 
3490
+ // src/merges/receipts.ts
3491
+ function buildDismissalMutationReceipt(primary, partial) {
3492
+ const normalized = normalizeMutationReceiptPartial(partial);
3493
+ return {
3494
+ primary,
3495
+ created: normalized.created,
3496
+ updated: normalized.updated,
3497
+ archived: normalized.archived,
3498
+ removed: normalized.removed,
3499
+ affectedLinks: normalized.affectedLinks,
3500
+ derivedEffects: normalized.derivedEffects
3501
+ };
3502
+ }
3503
+
3504
+ // src/merges/dismiss_duplicate.ts
3505
+ function canonicalize(a, b) {
3506
+ return {
3507
+ leftContactId: Math.min(a, b),
3508
+ rightContactId: Math.max(a, b)
3509
+ };
3510
+ }
3511
+ function dismissDuplicate(db, leftContactId, rightContactId, reason, now) {
3512
+ return withTransaction(db, () => {
3513
+ if (leftContactId === rightContactId) {
3514
+ throw new AffinityValidationError(
3515
+ "cannot dismiss a contact as a duplicate of itself"
3516
+ );
3517
+ }
3518
+ const left = getContactRowById(db, leftContactId);
3519
+ if (!left) {
3520
+ throw new AffinityNotFoundError(`contact ${leftContactId} not found`);
3521
+ }
3522
+ if (left.lifecycle_state === "merged") {
3523
+ throw new AffinityStateError(
3524
+ `contact ${leftContactId} is merged and cannot be dismissed`
3525
+ );
3526
+ }
3527
+ const right = getContactRowById(db, rightContactId);
3528
+ if (!right) {
3529
+ throw new AffinityNotFoundError(`contact ${rightContactId} not found`);
3530
+ }
3531
+ if (right.lifecycle_state === "merged") {
3532
+ throw new AffinityStateError(
3533
+ `contact ${rightContactId} is merged and cannot be dismissed`
3534
+ );
3535
+ }
3536
+ const pair = canonicalize(leftContactId, rightContactId);
3537
+ const resolvedNow = resolveNow(now);
3538
+ const resolvedReason = reason === void 0 || reason === null ? null : (() => {
3539
+ const t = reason.trim();
3540
+ return t.length === 0 ? null : t;
3541
+ })();
3542
+ db.prepare(
3543
+ `INSERT INTO dismissed_duplicates (left_id, right_id, reason, dismissed_at)
3544
+ VALUES (?, ?, ?, ?)
3545
+ ON CONFLICT(left_id, right_id) DO UPDATE SET
3546
+ reason = excluded.reason,
3547
+ dismissed_at = excluded.dismissed_at`
3548
+ ).run(pair.leftContactId, pair.rightContactId, resolvedReason, resolvedNow);
3549
+ return buildDismissalMutationReceipt(pair);
3550
+ });
3551
+ }
3552
+
3553
+ // src/merges/undismiss_duplicate.ts
3554
+ function canonicalize2(a, b) {
3555
+ return {
3556
+ leftContactId: Math.min(a, b),
3557
+ rightContactId: Math.max(a, b)
3558
+ };
3559
+ }
3560
+ function undismissDuplicate(db, leftContactId, rightContactId) {
3561
+ return withTransaction(db, () => {
3562
+ if (leftContactId === rightContactId) {
3563
+ throw new AffinityValidationError(
3564
+ "cannot undismiss a contact from itself"
3565
+ );
3566
+ }
3567
+ const left = getContactRowById(db, leftContactId);
3568
+ if (!left) {
3569
+ throw new AffinityNotFoundError(`contact ${leftContactId} not found`);
3570
+ }
3571
+ const right = getContactRowById(db, rightContactId);
3572
+ if (!right) {
3573
+ throw new AffinityNotFoundError(`contact ${rightContactId} not found`);
3574
+ }
3575
+ const pair = canonicalize2(leftContactId, rightContactId);
3576
+ db.prepare(
3577
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? AND right_id = ?"
3578
+ ).run(pair.leftContactId, pair.rightContactId);
3579
+ return buildDismissalMutationReceipt(pair);
3580
+ });
3581
+ }
3582
+
3583
+ // src/merges/list_dismissed_duplicates.ts
3584
+ function listDismissedDuplicates(db, options) {
3585
+ assertDefaultOrdering("listDismissedDuplicates", options);
3586
+ const { limit, offset } = resolveListLimitOffset(options);
3587
+ const rows = db.prepare(
3588
+ `SELECT left_id, right_id, reason, dismissed_at
3589
+ FROM dismissed_duplicates
3590
+ ORDER BY dismissed_at DESC
3591
+ LIMIT ? OFFSET ?`
3592
+ ).all(limit, offset);
3593
+ return rows.map((row) => ({
3594
+ leftContactId: row.left_id,
3595
+ rightContactId: row.right_id,
3596
+ reason: row.reason,
3597
+ dismissedAt: row.dismissed_at
3598
+ }));
3599
+ }
3600
+
3446
3601
  // src/events/get_contact_journal.ts
3447
3602
  function getContactJournal(db, contactId, options) {
3448
3603
  assertDefaultOrdering("getContactJournal", options);
@@ -5331,6 +5486,9 @@ __export(tools_exports, {
5331
5486
  manageDateAnchorTool: () => manageDateAnchorTool,
5332
5487
  manageDateAnchorToolHandler: () => manageDateAnchorToolHandler,
5333
5488
  manageDateAnchorToolName: () => manageDateAnchorToolName,
5489
+ manageDuplicateDismissalTool: () => manageDuplicateDismissalTool,
5490
+ manageDuplicateDismissalToolHandler: () => manageDuplicateDismissalToolHandler,
5491
+ manageDuplicateDismissalToolName: () => manageDuplicateDismissalToolName,
5334
5492
  manageIdentityTool: () => manageIdentityTool,
5335
5493
  manageIdentityToolHandler: () => manageIdentityToolHandler,
5336
5494
  manageIdentityToolName: () => manageIdentityToolName,
@@ -5453,6 +5611,7 @@ var reviewAffinityToolName = "review_affinity";
5453
5611
  var inspectAffinityItemToolName = "inspect_affinity_item";
5454
5612
  var manageContactToolName = "manage_contact";
5455
5613
  var mergeContactsToolName = "merge_contacts";
5614
+ var manageDuplicateDismissalToolName = "manage_duplicate_dismissal";
5456
5615
  var manageIdentityToolName = "manage_identity";
5457
5616
  var manageRelationshipToolName = "manage_relationship";
5458
5617
  var recordEventToolName = "record_event";
@@ -6441,12 +6600,43 @@ var manageAttributeTool = defineAffinityTool({
6441
6600
  });
6442
6601
 
6443
6602
  // src/tools/manage_commitment_tool.ts
6603
+ function normalizeParticipant(p) {
6604
+ let contactId = p.contactId;
6605
+ if (contactId === void 0 && p.contact != null) {
6606
+ const loc = p.contact;
6607
+ contactId = loc.contactId;
6608
+ }
6609
+ const result = {
6610
+ contactId,
6611
+ role: p.role
6612
+ };
6613
+ if (p.directionality !== void 0) {
6614
+ result.directionality = p.directionality;
6615
+ }
6616
+ return result;
6617
+ }
6618
+ function ensureOwnerParticipates(db, participants) {
6619
+ const ownerId = findOwnerContactId(db);
6620
+ if (ownerId === null) return participants;
6621
+ if (participants.some((p) => p.contactId === ownerId)) return participants;
6622
+ return [{ contactId: ownerId, role: "actor" }, ...participants];
6623
+ }
6624
+ function prepareParticipants(db, raw) {
6625
+ return ensureOwnerParticipates(db, raw.map(normalizeParticipant));
6626
+ }
6444
6627
  function manageCommitmentToolHandler(db, input) {
6445
6628
  return withToolHandling(() => {
6446
6629
  if (input.action === "record") {
6630
+ const patched = {
6631
+ ...input.input,
6632
+ participants: prepareParticipants(
6633
+ db,
6634
+ input.input.participants
6635
+ )
6636
+ };
6447
6637
  return mutationToolResult(
6448
6638
  "record",
6449
- recordCommitment(db, input.input),
6639
+ recordCommitment(db, patched),
6450
6640
  "commitment"
6451
6641
  );
6452
6642
  }
@@ -6464,7 +6654,7 @@ function manageCommitmentToolHandler(db, input) {
6464
6654
  }
6465
6655
  var manageCommitmentTool = defineAffinityTool({
6466
6656
  name: manageCommitmentToolName,
6467
- description: "Record or resolve one commitment such as a promise or agreement.",
6657
+ description: "Record or resolve one commitment such as a promise or agreement. The owner contact is auto-injected into participants if missing.",
6468
6658
  whenToUse: "Use this when the work is specifically about a commitment lifecycle rather than a general event.",
6469
6659
  whenNotToUse: "Do not use this for ordinary interactions or direct relationship state changes.",
6470
6660
  sideEffects: "writes_state",
@@ -6482,10 +6672,42 @@ var manageCommitmentTool = defineAffinityTool({
6482
6672
  inputSchema: objectSchema(
6483
6673
  {
6484
6674
  action: enumSchema("Operation to perform.", ["record", "resolve"]),
6485
- input: {
6486
- type: "object",
6487
- description: "Commitment payload. Required when action=record."
6488
- },
6675
+ input: objectSchema(
6676
+ {
6677
+ commitmentType: enumSchema("Promise or agreement.", [
6678
+ "promise",
6679
+ "agreement"
6680
+ ]),
6681
+ occurredAt: integerSchema(
6682
+ "When the commitment was made, Unix timestamp in milliseconds."
6683
+ ),
6684
+ summary: stringSchema("Commitment summary. Must be non-empty."),
6685
+ significance: integerSchema("Commitment significance from 1 to 10."),
6686
+ dueAt: integerSchema("Optional due timestamp."),
6687
+ participants: arraySchema(
6688
+ objectSchema(
6689
+ {
6690
+ contactId: integerSchema(
6691
+ "Participant contact id. The owner contact MUST be included."
6692
+ ),
6693
+ role: stringSchema("Participant role."),
6694
+ directionality: stringSchema("Optional directionality.")
6695
+ },
6696
+ ["contactId", "role"]
6697
+ ),
6698
+ "Participant list. Must be non-empty and include the owner contact."
6699
+ ),
6700
+ now: integerSchema("Optional timestamp override.")
6701
+ },
6702
+ [
6703
+ "commitmentType",
6704
+ "occurredAt",
6705
+ "summary",
6706
+ "significance",
6707
+ "participants"
6708
+ ],
6709
+ "Commitment payload. Required when action=record."
6710
+ ),
6489
6711
  commitmentEventId: integerSchema(
6490
6712
  "Commitment event id. Required when action=resolve."
6491
6713
  ),
@@ -6641,7 +6863,124 @@ var manageContactTool = defineAffinityTool({
6641
6863
  handler: manageContactToolHandler
6642
6864
  });
6643
6865
 
6866
+ // src/tools/manage_duplicate_dismissal_tool.ts
6867
+ function contactLocatorSchema4(description) {
6868
+ return {
6869
+ type: "object",
6870
+ properties: {
6871
+ contactId: integerSchema("Exact contact id."),
6872
+ identity: objectSchema(
6873
+ {
6874
+ type: stringSchema("Identity type."),
6875
+ value: stringSchema("Identity value.")
6876
+ },
6877
+ ["type", "value"],
6878
+ "Contact identity locator."
6879
+ )
6880
+ },
6881
+ description
6882
+ };
6883
+ }
6884
+ function manageDuplicateDismissalToolHandler(db, input) {
6885
+ return withToolHandling(() => {
6886
+ const left = resolveContactLocator(db, input.left, "left");
6887
+ if (!left.ok) {
6888
+ return left.result;
6889
+ }
6890
+ const right = resolveContactLocator(db, input.right, "right");
6891
+ if (!right.ok) {
6892
+ return right.result;
6893
+ }
6894
+ if (input.action === "dismiss") {
6895
+ const receipt2 = dismissDuplicate(
6896
+ db,
6897
+ left.value.id,
6898
+ right.value.id,
6899
+ input.reason ?? null
6900
+ );
6901
+ return toolSuccess(
6902
+ `Dismissed pair #${receipt2.primary.leftContactId} / #${receipt2.primary.rightContactId}.`,
6903
+ { action: "dismiss", primary: receipt2.primary }
6904
+ );
6905
+ }
6906
+ const receipt = undismissDuplicate(db, left.value.id, right.value.id);
6907
+ return toolSuccess(
6908
+ `Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
6909
+ { action: "undismiss", primary: receipt.primary }
6910
+ );
6911
+ }, "Duplicate dismissal failed.");
6912
+ }
6913
+ var manageDuplicateDismissalTool = defineAffinityTool({
6914
+ name: manageDuplicateDismissalToolName,
6915
+ description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in listDuplicateCandidates.",
6916
+ whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
6917
+ whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
6918
+ sideEffects: "writes_state",
6919
+ readOnly: false,
6920
+ supportsClarification: true,
6921
+ targetKinds: ["contact"],
6922
+ inputDescriptions: {
6923
+ action: "dismiss to suppress the pair; undismiss to re-surface it.",
6924
+ left: "One contact of the pair.",
6925
+ right: "The other contact of the pair.",
6926
+ reason: "Optional reason for the dismissal (audit trail)."
6927
+ },
6928
+ outputDescription: "Returns the dismissal receipt with the canonical pair identifiers.",
6929
+ inputSchema: objectSchema(
6930
+ {
6931
+ action: enumSchema("Action to perform.", ["dismiss", "undismiss"]),
6932
+ left: contactLocatorSchema4(
6933
+ "Left contact. Provide contactId or identity."
6934
+ ),
6935
+ right: contactLocatorSchema4(
6936
+ "Right contact. Provide contactId or identity."
6937
+ ),
6938
+ reason: nullableStringSchema("Optional reason for the dismissal.")
6939
+ },
6940
+ ["action", "left", "right"],
6941
+ "Dismiss or un-dismiss a duplicate candidate pair."
6942
+ ),
6943
+ handler: manageDuplicateDismissalToolHandler
6944
+ });
6945
+
6644
6946
  // src/tools/manage_date_anchor_tool.ts
6947
+ function contactLocatorSchema5(description) {
6948
+ return {
6949
+ type: "object",
6950
+ properties: {
6951
+ contactId: integerSchema("Exact contact id."),
6952
+ identity: objectSchema(
6953
+ {
6954
+ type: stringSchema("Identity type."),
6955
+ value: stringSchema("Identity value.")
6956
+ },
6957
+ ["type", "value"],
6958
+ "Contact identity locator."
6959
+ )
6960
+ },
6961
+ description
6962
+ };
6963
+ }
6964
+ function linkLocatorSchema3(description) {
6965
+ return {
6966
+ type: "object",
6967
+ properties: {
6968
+ linkId: integerSchema("Exact link id."),
6969
+ endpoints: objectSchema(
6970
+ {
6971
+ fromContactId: integerSchema("From contact id."),
6972
+ toContactId: integerSchema("To contact id."),
6973
+ kind: stringSchema("Optional link kind."),
6974
+ role: stringSchema("Optional link role."),
6975
+ isStructural: booleanSchema("Optional structural discriminator.")
6976
+ },
6977
+ ["fromContactId", "toContactId"],
6978
+ "Endpoint-based link locator."
6979
+ )
6980
+ },
6981
+ description
6982
+ };
6983
+ }
6645
6984
  function resolveDateAnchorTarget(db, target) {
6646
6985
  if (target.kind === "contact") {
6647
6986
  const contact = resolveContactLocator(db, target.contact, "target.contact");
@@ -6707,17 +7046,65 @@ var manageDateAnchorTool = defineAffinityTool({
6707
7046
  inputSchema: objectSchema(
6708
7047
  {
6709
7048
  action: enumSchema("Operation to perform.", ["add", "revise", "remove"]),
6710
- input: {
6711
- type: "object",
6712
- description: "Date anchor payload. Required when action=add."
6713
- },
7049
+ input: objectSchema(
7050
+ {
7051
+ target: objectSchema(
7052
+ {
7053
+ kind: enumSchema("Target kind.", ["contact", "link"]),
7054
+ contact: contactLocatorSchema5(
7055
+ "Contact locator. Required when kind=contact. Provide contactId or identity."
7056
+ ),
7057
+ link: linkLocatorSchema3(
7058
+ "Link locator. Required when kind=link. Provide linkId or endpoints."
7059
+ )
7060
+ },
7061
+ ["kind"],
7062
+ "Date-anchor target."
7063
+ ),
7064
+ recurrenceKind: enumSchema("Recurrence kind.", [
7065
+ "birthday",
7066
+ "anniversary",
7067
+ "renewal",
7068
+ "memorial",
7069
+ "custom_yearly"
7070
+ ]),
7071
+ anchorMonth: integerSchema("Anchor month, 1\u201312."),
7072
+ anchorDay: integerSchema("Anchor day, 1\u201331 (validated per month)."),
7073
+ summary: stringSchema("Summary. Must be non-empty."),
7074
+ significance: integerSchema("Significance from 1 to 10."),
7075
+ now: integerSchema("Optional timestamp override."),
7076
+ force: booleanSchema("Whether to bypass duplicate checks.")
7077
+ },
7078
+ [
7079
+ "target",
7080
+ "recurrenceKind",
7081
+ "anchorMonth",
7082
+ "anchorDay",
7083
+ "summary",
7084
+ "significance"
7085
+ ],
7086
+ "Date anchor payload. Required when action=add."
7087
+ ),
6714
7088
  anchorEventId: integerSchema(
6715
7089
  "Anchor event id. Required when action=revise or remove."
6716
7090
  ),
6717
- patch: {
6718
- type: "object",
6719
- description: "Anchor patch. Required when action=revise."
6720
- },
7091
+ patch: objectSchema(
7092
+ {
7093
+ recurrenceKind: enumSchema("Optional recurrence kind.", [
7094
+ "birthday",
7095
+ "anniversary",
7096
+ "renewal",
7097
+ "memorial",
7098
+ "custom_yearly"
7099
+ ]),
7100
+ anchorMonth: integerSchema("Optional month, 1\u201312."),
7101
+ anchorDay: integerSchema("Optional day, 1\u201331."),
7102
+ summary: stringSchema("Optional summary."),
7103
+ significance: integerSchema("Optional significance, 1\u201310.")
7104
+ },
7105
+ [],
7106
+ "Anchor patch. Required when action=revise."
7107
+ ),
6721
7108
  options: objectSchema(
6722
7109
  {
6723
7110
  now: integerSchema("Optional timestamp."),
@@ -6818,6 +7205,9 @@ function addIdentity(db, contactId, input) {
6818
7205
  throw new AffinityInvariantError("inserted identity not found");
6819
7206
  }
6820
7207
  refreshContactRollup(db, contactId, now);
7208
+ db.prepare(
7209
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
7210
+ ).run(contactId, contactId);
6821
7211
  return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
6822
7212
  created: [{ kind: "identity", id }]
6823
7213
  });
@@ -6841,6 +7231,9 @@ function removeIdentity(db, identityId, removedAt) {
6841
7231
  "UPDATE identities SET removed_at = ?, updated_at = ? WHERE id = ?"
6842
7232
  ).run(now, now, identityId);
6843
7233
  refreshContactRollup(db, row.contact_id, now);
7234
+ db.prepare(
7235
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
7236
+ ).run(row.contact_id, row.contact_id);
6844
7237
  return buildIdentityMutationReceipt(primary, {
6845
7238
  removed: [{ kind: "identity", id: identityId }]
6846
7239
  });
@@ -6919,7 +7312,7 @@ function verifyIdentity(db, identityId, verifiedAt) {
6919
7312
  }
6920
7313
 
6921
7314
  // src/tools/manage_identity_tool.ts
6922
- function contactLocatorSchema4(description) {
7315
+ function contactLocatorSchema6(description) {
6923
7316
  return {
6924
7317
  type: "object",
6925
7318
  properties: {
@@ -6998,7 +7391,7 @@ var manageIdentityTool = defineAffinityTool({
6998
7391
  "verify",
6999
7392
  "remove"
7000
7393
  ]),
7001
- contact: contactLocatorSchema4(
7394
+ contact: contactLocatorSchema6(
7002
7395
  "Target contact. Required when action=add. Provide contactId or identity."
7003
7396
  ),
7004
7397
  input: objectSchema(
@@ -7038,7 +7431,7 @@ var manageIdentityTool = defineAffinityTool({
7038
7431
  });
7039
7432
 
7040
7433
  // src/tools/manage_relationship_tool.ts
7041
- function contactLocatorSchema5(description) {
7434
+ function contactLocatorSchema7(description) {
7042
7435
  return {
7043
7436
  type: "object",
7044
7437
  properties: {
@@ -7055,7 +7448,7 @@ function contactLocatorSchema5(description) {
7055
7448
  description
7056
7449
  };
7057
7450
  }
7058
- function linkLocatorSchema3(description) {
7451
+ function linkLocatorSchema4(description) {
7059
7452
  return {
7060
7453
  type: "object",
7061
7454
  properties: {
@@ -7194,14 +7587,14 @@ var manageRelationshipTool = defineAffinityTool({
7194
7587
  "set_structural_tie",
7195
7588
  "remove_structural_tie"
7196
7589
  ]),
7197
- from: contactLocatorSchema5(
7590
+ from: contactLocatorSchema7(
7198
7591
  "Source contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity."
7199
7592
  ),
7200
- to: contactLocatorSchema5(
7593
+ to: contactLocatorSchema7(
7201
7594
  "Target contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity."
7202
7595
  ),
7203
7596
  kind: stringSchema(
7204
- "Link kind. Required for seed_social_link and set_structural_tie. Can be passed here or inside input."
7597
+ "Link kind. Required for seed_social_link and set_structural_tie. Can be passed here or inside input. For seed_social_link: personal, family, professional, romantic, care, service, observed, other_relational. For set_structural_tie: works_at, manages, member_of, married_to, partner_of, parent_of, child_of, sibling_of, friend_of, client_of, vendor_of, reports_to, belongs_to, other_structural."
7205
7598
  ),
7206
7599
  role: stringSchema(
7207
7600
  "Optional role for seed_social_link or set_structural_tie. Can be passed here or inside input."
@@ -7210,7 +7603,7 @@ var manageRelationshipTool = defineAffinityTool({
7210
7603
  type: "object",
7211
7604
  description: "Full payload for seed_social_link or set_structural_tie. Alternative to top-level kind/role."
7212
7605
  },
7213
- link: linkLocatorSchema3(
7606
+ link: linkLocatorSchema4(
7214
7607
  "Target link. Required when action=revise_bond, override_state, or remove_structural_tie. Provide linkId or endpoints."
7215
7608
  ),
7216
7609
  bond: nullableStringSchema(
@@ -7239,7 +7632,7 @@ var manageRelationshipTool = defineAffinityTool({
7239
7632
  });
7240
7633
 
7241
7634
  // src/tools/merge_contacts_tool.ts
7242
- function contactLocatorSchema6(description) {
7635
+ function contactLocatorSchema8(description) {
7243
7636
  return {
7244
7637
  type: "object",
7245
7638
  properties: {
@@ -7296,10 +7689,10 @@ var mergeContactsTool = defineAffinityTool({
7296
7689
  outputDescription: "Returns the merge receipt with rewired entities and affected-link information.",
7297
7690
  inputSchema: objectSchema(
7298
7691
  {
7299
- winner: contactLocatorSchema6(
7692
+ winner: contactLocatorSchema8(
7300
7693
  "Winner contact. Provide contactId or identity."
7301
7694
  ),
7302
- loser: contactLocatorSchema6(
7695
+ loser: contactLocatorSchema8(
7303
7696
  "Loser contact. Provide contactId or identity."
7304
7697
  ),
7305
7698
  reasonSummary: stringSchema("Optional merge reason."),
@@ -7312,31 +7705,64 @@ var mergeContactsTool = defineAffinityTool({
7312
7705
  });
7313
7706
 
7314
7707
  // src/tools/record_event_tool.ts
7708
+ function normalizeParticipant2(p) {
7709
+ let contactId = p.contactId;
7710
+ if (contactId === void 0 && p.contact != null) {
7711
+ const loc = p.contact;
7712
+ contactId = loc.contactId;
7713
+ }
7714
+ const result = {
7715
+ contactId,
7716
+ role: p.role
7717
+ };
7718
+ if (p.directionality !== void 0) {
7719
+ result.directionality = p.directionality;
7720
+ }
7721
+ return result;
7722
+ }
7723
+ function ensureOwnerParticipates2(db, participants) {
7724
+ const ownerId = findOwnerContactId(db);
7725
+ if (ownerId === null) return participants;
7726
+ if (participants.some((p) => p.contactId === ownerId)) return participants;
7727
+ return [{ contactId: ownerId, role: "actor" }, ...participants];
7728
+ }
7729
+ function prepareParticipants2(db, raw, injectOwner) {
7730
+ const normalized = raw.map(normalizeParticipant2);
7731
+ return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
7732
+ }
7315
7733
  function recordEventToolHandler(db, input) {
7316
7734
  return withToolHandling(() => {
7735
+ const patched = {
7736
+ ...input.input,
7737
+ participants: prepareParticipants2(
7738
+ db,
7739
+ input.input.participants,
7740
+ input.kind !== "observation"
7741
+ )
7742
+ };
7317
7743
  switch (input.kind) {
7318
7744
  case "interaction":
7319
7745
  return mutationToolResult(
7320
7746
  "interaction",
7321
- recordInteraction(db, input.input),
7747
+ recordInteraction(db, patched),
7322
7748
  "event"
7323
7749
  );
7324
7750
  case "observation":
7325
7751
  return mutationToolResult(
7326
7752
  "observation",
7327
- recordObservation(db, input.input),
7753
+ recordObservation(db, patched),
7328
7754
  "event"
7329
7755
  );
7330
7756
  case "milestone":
7331
7757
  return mutationToolResult(
7332
7758
  "milestone",
7333
- recordMilestone(db, input.input),
7759
+ recordMilestone(db, patched),
7334
7760
  "event"
7335
7761
  );
7336
7762
  case "transaction":
7337
7763
  return mutationToolResult(
7338
7764
  "transaction",
7339
- recordTransaction(db, input.input),
7765
+ recordTransaction(db, patched),
7340
7766
  "event"
7341
7767
  );
7342
7768
  }
@@ -7344,7 +7770,7 @@ function recordEventToolHandler(db, input) {
7344
7770
  }
7345
7771
  var recordEventTool = defineAffinityTool({
7346
7772
  name: recordEventToolName,
7347
- description: "Record one social event such as an interaction, observation, milestone, or transaction.",
7773
+ description: "Record one social event such as an interaction, observation, milestone, or transaction. For interactions, milestones, and transactions the owner contact is auto-injected into participants if missing. Observations may omit the owner when recording third-party evidence.",
7348
7774
  whenToUse: "Use this when you are capturing something that happened in the relationship graph.",
7349
7775
  whenNotToUse: "Do not use this for commitments, date anchors, or direct relationship state overrides.",
7350
7776
  sideEffects: "writes_state",
@@ -7364,10 +7790,53 @@ var recordEventTool = defineAffinityTool({
7364
7790
  "milestone",
7365
7791
  "transaction"
7366
7792
  ]),
7367
- input: {
7368
- type: "object",
7369
- description: "Event payload. Shape depends on kind. Interaction events require a `type` subtype; others do not. Always required."
7370
- }
7793
+ input: objectSchema(
7794
+ {
7795
+ type: enumSchema(
7796
+ "Interaction subtype. Required when kind=interaction, omit otherwise.",
7797
+ [
7798
+ "conversation",
7799
+ "activity",
7800
+ "gift",
7801
+ "support",
7802
+ "conflict",
7803
+ "correction"
7804
+ ]
7805
+ ),
7806
+ occurredAt: integerSchema(
7807
+ "When the event occurred, Unix timestamp in milliseconds."
7808
+ ),
7809
+ summary: stringSchema("Event summary. Must be non-empty."),
7810
+ significance: integerSchema("Event significance from 1 to 10."),
7811
+ participants: arraySchema(
7812
+ objectSchema(
7813
+ {
7814
+ contactId: integerSchema(
7815
+ "Participant contact id. The owner contact MUST be included."
7816
+ ),
7817
+ role: enumSchema("Participant role.", [
7818
+ "actor",
7819
+ "recipient",
7820
+ "subject",
7821
+ "observer",
7822
+ "mentioned"
7823
+ ]),
7824
+ directionality: enumSchema("Optional directionality.", [
7825
+ "owner_initiated",
7826
+ "other_initiated",
7827
+ "mutual",
7828
+ "observed"
7829
+ ])
7830
+ },
7831
+ ["contactId", "role"]
7832
+ ),
7833
+ "Participant list. Must be non-empty and include the owner contact."
7834
+ ),
7835
+ now: integerSchema("Optional timestamp override.")
7836
+ },
7837
+ ["occurredAt", "summary", "significance", "participants"],
7838
+ "Event payload. Always required. Interaction events also require `type`."
7839
+ )
7371
7840
  },
7372
7841
  ["kind", "input"],
7373
7842
  "Record one social event."
@@ -7394,7 +7863,7 @@ function buildChartOptions(input) {
7394
7863
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds }
7395
7864
  };
7396
7865
  }
7397
- function contactLocatorSchema7(description) {
7866
+ function contactLocatorSchema9(description) {
7398
7867
  return {
7399
7868
  type: "object",
7400
7869
  properties: {
@@ -7411,7 +7880,7 @@ function contactLocatorSchema7(description) {
7411
7880
  description
7412
7881
  };
7413
7882
  }
7414
- function linkLocatorSchema4(description) {
7883
+ function linkLocatorSchema5(description) {
7415
7884
  return {
7416
7885
  type: "object",
7417
7886
  properties: {
@@ -7485,6 +7954,14 @@ function validateReviewInput(input) {
7485
7954
  "Use `contactIds` only with duplicate-candidate or chart review views."
7486
7955
  );
7487
7956
  }
7957
+ if (input.includeDismissed !== void 0 && input.view !== "contacts.duplicates") {
7958
+ return toolFailure(
7959
+ "protocol",
7960
+ "invalid_input",
7961
+ "The `includeDismissed` filter is not valid for this review view.",
7962
+ "Use `includeDismissed` only with the contacts.duplicates review view."
7963
+ );
7964
+ }
7488
7965
  return null;
7489
7966
  }
7490
7967
  function duplicateCandidateItem(candidate) {
@@ -7541,6 +8018,15 @@ function mergeHistoryItem(record) {
7541
8018
  snippet: record.reasonSummary ?? void 0
7542
8019
  };
7543
8020
  }
8021
+ function dismissedPairItem(record) {
8022
+ return {
8023
+ kind: "contact",
8024
+ id: record.leftContactId,
8025
+ title: `Dismissed pair: #${record.leftContactId} and #${record.rightContactId}`,
8026
+ subtitle: record.reason ?? "no reason given",
8027
+ snippet: `dismissed at ${record.dismissedAt}`
8028
+ };
8029
+ }
7544
8030
  function compactFilters(input) {
7545
8031
  return Object.fromEntries(
7546
8032
  Object.entries(input).filter(
@@ -7576,7 +8062,8 @@ function reviewAffinityToolHandler(db, input) {
7576
8062
  {
7577
8063
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
7578
8064
  ...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
7579
- ...input.minScore === void 0 ? {} : { minScore: input.minScore }
8065
+ ...input.minScore === void 0 ? {} : { minScore: input.minScore },
8066
+ ...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
7580
8067
  },
7581
8068
  options
7582
8069
  ).map(duplicateCandidateItem);
@@ -7757,6 +8244,9 @@ function reviewAffinityToolHandler(db, input) {
7757
8244
  );
7758
8245
  break;
7759
8246
  }
8247
+ case "merges.dismissed":
8248
+ items = listDismissedDuplicates(db, options).map(dismissedPairItem);
8249
+ break;
7760
8250
  }
7761
8251
  return toolSuccess(
7762
8252
  summarizeCount(items.length, "item"),
@@ -7807,6 +8297,7 @@ var reviewAffinityTool = defineAffinityTool({
7807
8297
  horizonDays: "Optional horizon filter for commitments or upcoming dates.",
7808
8298
  exactOnly: "Whether duplicate review should only include exact identity matches.",
7809
8299
  minScore: "Optional minimum duplicate score.",
8300
+ includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
7810
8301
  since: "Optional lower timestamp bound for time-based views.",
7811
8302
  until: "Optional upper timestamp bound for time-based views.",
7812
8303
  limit: "Optional result limit.",
@@ -7828,10 +8319,11 @@ var reviewAffinityTool = defineAffinityTool({
7828
8319
  "links.radar",
7829
8320
  "dates.upcoming",
7830
8321
  "graph.chart",
7831
- "merges.history"
8322
+ "merges.history",
8323
+ "merges.dismissed"
7832
8324
  ]),
7833
- contact: contactLocatorSchema7("Optional target contact locator."),
7834
- link: linkLocatorSchema4("Optional target link locator."),
8325
+ contact: contactLocatorSchema9("Optional target contact locator."),
8326
+ link: linkLocatorSchema5("Optional target link locator."),
7835
8327
  contactIds: arraySchema(
7836
8328
  integerSchema("Contact id."),
7837
8329
  "Optional contact-id subset."
@@ -7881,6 +8373,9 @@ var reviewAffinityTool = defineAffinityTool({
7881
8373
  horizonDays: integerSchema("Optional horizon-days filter."),
7882
8374
  exactOnly: booleanSchema("Whether duplicate review is exact-only."),
7883
8375
  minScore: numberSchema("Optional duplicate minimum score."),
8376
+ includeDismissed: booleanSchema(
8377
+ "Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
8378
+ ),
7884
8379
  since: integerSchema("Optional lower timestamp bound."),
7885
8380
  until: integerSchema("Optional upper timestamp bound."),
7886
8381
  limit: integerSchema("Optional result limit."),
@@ -7979,6 +8474,7 @@ var affinityTools = [
7979
8474
  inspectAffinityItemTool,
7980
8475
  manageContactTool,
7981
8476
  mergeContactsTool,
8477
+ manageDuplicateDismissalTool,
7982
8478
  manageIdentityTool,
7983
8479
  manageRelationshipTool,
7984
8480
  recordEventTool,
@@ -8080,6 +8576,21 @@ var affinityToolMappings = [
8080
8576
  action: "set_lifecycle"
8081
8577
  },
8082
8578
  { source: "mergeContacts", tool: mergeContactsToolName },
8579
+ {
8580
+ source: "dismissDuplicate",
8581
+ tool: manageDuplicateDismissalToolName,
8582
+ action: "dismiss"
8583
+ },
8584
+ {
8585
+ source: "undismissDuplicate",
8586
+ tool: manageDuplicateDismissalToolName,
8587
+ action: "undismiss"
8588
+ },
8589
+ {
8590
+ source: "listDismissedDuplicates",
8591
+ tool: reviewAffinityToolName,
8592
+ view: "merges.dismissed"
8593
+ },
8083
8594
  { source: "addIdentity", tool: manageIdentityToolName, action: "add" },
8084
8595
  { source: "reviseIdentity", tool: manageIdentityToolName, action: "revise" },
8085
8596
  { source: "verifyIdentity", tool: manageIdentityToolName, action: "verify" },
@@ -8161,6 +8672,7 @@ __export(write_exports, {
8161
8672
  addDateAnchor: () => addDateAnchor,
8162
8673
  addIdentity: () => addIdentity,
8163
8674
  createContact: () => createContact,
8675
+ dismissDuplicate: () => dismissDuplicate,
8164
8676
  mergeContacts: () => mergeContacts,
8165
8677
  overrideLinkState: () => overrideLinkState,
8166
8678
  recordCommitment: () => recordCommitment,
@@ -8181,6 +8693,7 @@ __export(write_exports, {
8181
8693
  setAttribute: () => setAttribute,
8182
8694
  setContactLifecycle: () => setContactLifecycle,
8183
8695
  setStructuralTie: () => setStructuralTie,
8696
+ undismissDuplicate: () => undismissDuplicate,
8184
8697
  unsetAttribute: () => unsetAttribute,
8185
8698
  verifyIdentity: () => verifyIdentity
8186
8699
  });