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