@ghostpaw/affinity 0.1.6 → 0.2.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/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;
@@ -3292,11 +3330,10 @@ function dedupeLiveLinks(db, now) {
3292
3330
  }
3293
3331
  return [...removed];
3294
3332
  }
3295
- function softDeleteSelfLoops(db, contactId, now) {
3333
+ function softDeleteSelfLoops(db, now) {
3296
3334
  const rows = db.prepare(
3297
- `SELECT id FROM links
3298
- WHERE removed_at IS NULL AND from_contact_id = to_contact_id AND from_contact_id = ?`
3299
- ).all(contactId);
3335
+ "SELECT id FROM links WHERE removed_at IS NULL AND from_contact_id = to_contact_id"
3336
+ ).all();
3300
3337
  const ids = [];
3301
3338
  for (const r of rows) {
3302
3339
  db.prepare(
@@ -3399,7 +3436,7 @@ function mergeContacts(db, input) {
3399
3436
  WHERE to_contact_id = ? AND removed_at IS NULL`
3400
3437
  ).run(winnerId, now, loserId);
3401
3438
  const affectedLinks = /* @__PURE__ */ new Set();
3402
- for (const id of softDeleteSelfLoops(db, winnerId, now)) {
3439
+ for (const id of softDeleteSelfLoops(db, now)) {
3403
3440
  affectedLinks.add(id);
3404
3441
  }
3405
3442
  for (const id of dedupeLiveLinks(db, now)) {
@@ -3416,6 +3453,12 @@ function mergeContacts(db, input) {
3416
3453
  `INSERT INTO contact_merges (winner_contact_id, loser_contact_id, merged_at, reason_summary, manual)
3417
3454
  VALUES (?, ?, ?, ?, 1)`
3418
3455
  ).run(winnerId, loserId, now, reasonSummary);
3456
+ db.prepare(
3457
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
3458
+ ).run(loserId, loserId);
3459
+ db.prepare(
3460
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
3461
+ ).run(winnerId, winnerId);
3419
3462
  db.prepare(
3420
3463
  "DELETE FROM link_rollups WHERE link_id IN (SELECT id FROM links WHERE removed_at IS NOT NULL)"
3421
3464
  ).run();
@@ -3443,6 +3486,117 @@ function mergeContacts(db, input) {
3443
3486
  });
3444
3487
  }
3445
3488
 
3489
+ // src/merges/receipts.ts
3490
+ function buildDismissalMutationReceipt(primary, partial) {
3491
+ const normalized = normalizeMutationReceiptPartial(partial);
3492
+ return {
3493
+ primary,
3494
+ created: normalized.created,
3495
+ updated: normalized.updated,
3496
+ archived: normalized.archived,
3497
+ removed: normalized.removed,
3498
+ affectedLinks: normalized.affectedLinks,
3499
+ derivedEffects: normalized.derivedEffects
3500
+ };
3501
+ }
3502
+
3503
+ // src/merges/dismiss_duplicate.ts
3504
+ function canonicalize(a, b) {
3505
+ return {
3506
+ leftContactId: Math.min(a, b),
3507
+ rightContactId: Math.max(a, b)
3508
+ };
3509
+ }
3510
+ function dismissDuplicate(db, leftContactId, rightContactId, reason, now) {
3511
+ return withTransaction(db, () => {
3512
+ if (leftContactId === rightContactId) {
3513
+ throw new AffinityValidationError(
3514
+ "cannot dismiss a contact as a duplicate of itself"
3515
+ );
3516
+ }
3517
+ const left = getContactRowById(db, leftContactId);
3518
+ if (!left) {
3519
+ throw new AffinityNotFoundError(`contact ${leftContactId} not found`);
3520
+ }
3521
+ if (left.lifecycle_state === "merged") {
3522
+ throw new AffinityStateError(
3523
+ `contact ${leftContactId} is merged and cannot be dismissed`
3524
+ );
3525
+ }
3526
+ const right = getContactRowById(db, rightContactId);
3527
+ if (!right) {
3528
+ throw new AffinityNotFoundError(`contact ${rightContactId} not found`);
3529
+ }
3530
+ if (right.lifecycle_state === "merged") {
3531
+ throw new AffinityStateError(
3532
+ `contact ${rightContactId} is merged and cannot be dismissed`
3533
+ );
3534
+ }
3535
+ const pair = canonicalize(leftContactId, rightContactId);
3536
+ const resolvedNow = resolveNow(now);
3537
+ const resolvedReason = reason === void 0 || reason === null ? null : (() => {
3538
+ const t = reason.trim();
3539
+ return t.length === 0 ? null : t;
3540
+ })();
3541
+ db.prepare(
3542
+ `INSERT INTO dismissed_duplicates (left_id, right_id, reason, dismissed_at)
3543
+ VALUES (?, ?, ?, ?)
3544
+ ON CONFLICT(left_id, right_id) DO UPDATE SET
3545
+ reason = excluded.reason,
3546
+ dismissed_at = excluded.dismissed_at`
3547
+ ).run(pair.leftContactId, pair.rightContactId, resolvedReason, resolvedNow);
3548
+ return buildDismissalMutationReceipt(pair);
3549
+ });
3550
+ }
3551
+
3552
+ // src/merges/undismiss_duplicate.ts
3553
+ function canonicalize2(a, b) {
3554
+ return {
3555
+ leftContactId: Math.min(a, b),
3556
+ rightContactId: Math.max(a, b)
3557
+ };
3558
+ }
3559
+ function undismissDuplicate(db, leftContactId, rightContactId) {
3560
+ return withTransaction(db, () => {
3561
+ if (leftContactId === rightContactId) {
3562
+ throw new AffinityValidationError(
3563
+ "cannot undismiss a contact from itself"
3564
+ );
3565
+ }
3566
+ const left = getContactRowById(db, leftContactId);
3567
+ if (!left) {
3568
+ throw new AffinityNotFoundError(`contact ${leftContactId} not found`);
3569
+ }
3570
+ const right = getContactRowById(db, rightContactId);
3571
+ if (!right) {
3572
+ throw new AffinityNotFoundError(`contact ${rightContactId} not found`);
3573
+ }
3574
+ const pair = canonicalize2(leftContactId, rightContactId);
3575
+ db.prepare(
3576
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? AND right_id = ?"
3577
+ ).run(pair.leftContactId, pair.rightContactId);
3578
+ return buildDismissalMutationReceipt(pair);
3579
+ });
3580
+ }
3581
+
3582
+ // src/merges/list_dismissed_duplicates.ts
3583
+ function listDismissedDuplicates(db, options) {
3584
+ assertDefaultOrdering("listDismissedDuplicates", options);
3585
+ const { limit, offset } = resolveListLimitOffset(options);
3586
+ const rows = db.prepare(
3587
+ `SELECT left_id, right_id, reason, dismissed_at
3588
+ FROM dismissed_duplicates
3589
+ ORDER BY dismissed_at DESC
3590
+ LIMIT ? OFFSET ?`
3591
+ ).all(limit, offset);
3592
+ return rows.map((row) => ({
3593
+ leftContactId: row.left_id,
3594
+ rightContactId: row.right_id,
3595
+ reason: row.reason,
3596
+ dismissedAt: row.dismissed_at
3597
+ }));
3598
+ }
3599
+
3446
3600
  // src/events/get_contact_journal.ts
3447
3601
  function getContactJournal(db, contactId, options) {
3448
3602
  assertDefaultOrdering("getContactJournal", options);
@@ -5331,6 +5485,9 @@ __export(tools_exports, {
5331
5485
  manageDateAnchorTool: () => manageDateAnchorTool,
5332
5486
  manageDateAnchorToolHandler: () => manageDateAnchorToolHandler,
5333
5487
  manageDateAnchorToolName: () => manageDateAnchorToolName,
5488
+ manageDuplicateDismissalTool: () => manageDuplicateDismissalTool,
5489
+ manageDuplicateDismissalToolHandler: () => manageDuplicateDismissalToolHandler,
5490
+ manageDuplicateDismissalToolName: () => manageDuplicateDismissalToolName,
5334
5491
  manageIdentityTool: () => manageIdentityTool,
5335
5492
  manageIdentityToolHandler: () => manageIdentityToolHandler,
5336
5493
  manageIdentityToolName: () => manageIdentityToolName,
@@ -5453,6 +5610,7 @@ var reviewAffinityToolName = "review_affinity";
5453
5610
  var inspectAffinityItemToolName = "inspect_affinity_item";
5454
5611
  var manageContactToolName = "manage_contact";
5455
5612
  var mergeContactsToolName = "merge_contacts";
5613
+ var manageDuplicateDismissalToolName = "manage_duplicate_dismissal";
5456
5614
  var manageIdentityToolName = "manage_identity";
5457
5615
  var manageRelationshipToolName = "manage_relationship";
5458
5616
  var recordEventToolName = "record_event";
@@ -6704,7 +6862,7 @@ var manageContactTool = defineAffinityTool({
6704
6862
  handler: manageContactToolHandler
6705
6863
  });
6706
6864
 
6707
- // src/tools/manage_date_anchor_tool.ts
6865
+ // src/tools/manage_duplicate_dismissal_tool.ts
6708
6866
  function contactLocatorSchema4(description) {
6709
6867
  return {
6710
6868
  type: "object",
@@ -6722,6 +6880,86 @@ function contactLocatorSchema4(description) {
6722
6880
  description
6723
6881
  };
6724
6882
  }
6883
+ function manageDuplicateDismissalToolHandler(db, input) {
6884
+ return withToolHandling(() => {
6885
+ const left = resolveContactLocator(db, input.left, "left");
6886
+ if (!left.ok) {
6887
+ return left.result;
6888
+ }
6889
+ const right = resolveContactLocator(db, input.right, "right");
6890
+ if (!right.ok) {
6891
+ return right.result;
6892
+ }
6893
+ if (input.action === "dismiss") {
6894
+ const receipt2 = dismissDuplicate(
6895
+ db,
6896
+ left.value.id,
6897
+ right.value.id,
6898
+ input.reason ?? null
6899
+ );
6900
+ return toolSuccess(
6901
+ `Dismissed pair #${receipt2.primary.leftContactId} / #${receipt2.primary.rightContactId}.`,
6902
+ { action: "dismiss", primary: receipt2.primary }
6903
+ );
6904
+ }
6905
+ const receipt = undismissDuplicate(db, left.value.id, right.value.id);
6906
+ return toolSuccess(
6907
+ `Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
6908
+ { action: "undismiss", primary: receipt.primary }
6909
+ );
6910
+ }, "Duplicate dismissal failed.");
6911
+ }
6912
+ var manageDuplicateDismissalTool = defineAffinityTool({
6913
+ name: manageDuplicateDismissalToolName,
6914
+ description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in listDuplicateCandidates.",
6915
+ whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
6916
+ whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
6917
+ sideEffects: "writes_state",
6918
+ readOnly: false,
6919
+ supportsClarification: true,
6920
+ targetKinds: ["contact"],
6921
+ inputDescriptions: {
6922
+ action: "dismiss to suppress the pair; undismiss to re-surface it.",
6923
+ left: "One contact of the pair.",
6924
+ right: "The other contact of the pair.",
6925
+ reason: "Optional reason for the dismissal (audit trail)."
6926
+ },
6927
+ outputDescription: "Returns the dismissal receipt with the canonical pair identifiers.",
6928
+ inputSchema: objectSchema(
6929
+ {
6930
+ action: enumSchema("Action to perform.", ["dismiss", "undismiss"]),
6931
+ left: contactLocatorSchema4(
6932
+ "Left contact. Provide contactId or identity."
6933
+ ),
6934
+ right: contactLocatorSchema4(
6935
+ "Right contact. Provide contactId or identity."
6936
+ ),
6937
+ reason: nullableStringSchema("Optional reason for the dismissal.")
6938
+ },
6939
+ ["action", "left", "right"],
6940
+ "Dismiss or un-dismiss a duplicate candidate pair."
6941
+ ),
6942
+ handler: manageDuplicateDismissalToolHandler
6943
+ });
6944
+
6945
+ // src/tools/manage_date_anchor_tool.ts
6946
+ function contactLocatorSchema5(description) {
6947
+ return {
6948
+ type: "object",
6949
+ properties: {
6950
+ contactId: integerSchema("Exact contact id."),
6951
+ identity: objectSchema(
6952
+ {
6953
+ type: stringSchema("Identity type."),
6954
+ value: stringSchema("Identity value.")
6955
+ },
6956
+ ["type", "value"],
6957
+ "Contact identity locator."
6958
+ )
6959
+ },
6960
+ description
6961
+ };
6962
+ }
6725
6963
  function linkLocatorSchema3(description) {
6726
6964
  return {
6727
6965
  type: "object",
@@ -6812,7 +7050,7 @@ var manageDateAnchorTool = defineAffinityTool({
6812
7050
  target: objectSchema(
6813
7051
  {
6814
7052
  kind: enumSchema("Target kind.", ["contact", "link"]),
6815
- contact: contactLocatorSchema4(
7053
+ contact: contactLocatorSchema5(
6816
7054
  "Contact locator. Required when kind=contact. Provide contactId or identity."
6817
7055
  ),
6818
7056
  link: linkLocatorSchema3(
@@ -6966,6 +7204,9 @@ function addIdentity(db, contactId, input) {
6966
7204
  throw new AffinityInvariantError("inserted identity not found");
6967
7205
  }
6968
7206
  refreshContactRollup(db, contactId, now);
7207
+ db.prepare(
7208
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
7209
+ ).run(contactId, contactId);
6969
7210
  return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
6970
7211
  created: [{ kind: "identity", id }]
6971
7212
  });
@@ -6989,6 +7230,9 @@ function removeIdentity(db, identityId, removedAt) {
6989
7230
  "UPDATE identities SET removed_at = ?, updated_at = ? WHERE id = ?"
6990
7231
  ).run(now, now, identityId);
6991
7232
  refreshContactRollup(db, row.contact_id, now);
7233
+ db.prepare(
7234
+ "DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
7235
+ ).run(row.contact_id, row.contact_id);
6992
7236
  return buildIdentityMutationReceipt(primary, {
6993
7237
  removed: [{ kind: "identity", id: identityId }]
6994
7238
  });
@@ -7067,7 +7311,7 @@ function verifyIdentity(db, identityId, verifiedAt) {
7067
7311
  }
7068
7312
 
7069
7313
  // src/tools/manage_identity_tool.ts
7070
- function contactLocatorSchema5(description) {
7314
+ function contactLocatorSchema6(description) {
7071
7315
  return {
7072
7316
  type: "object",
7073
7317
  properties: {
@@ -7146,7 +7390,7 @@ var manageIdentityTool = defineAffinityTool({
7146
7390
  "verify",
7147
7391
  "remove"
7148
7392
  ]),
7149
- contact: contactLocatorSchema5(
7393
+ contact: contactLocatorSchema6(
7150
7394
  "Target contact. Required when action=add. Provide contactId or identity."
7151
7395
  ),
7152
7396
  input: objectSchema(
@@ -7186,7 +7430,7 @@ var manageIdentityTool = defineAffinityTool({
7186
7430
  });
7187
7431
 
7188
7432
  // src/tools/manage_relationship_tool.ts
7189
- function contactLocatorSchema6(description) {
7433
+ function contactLocatorSchema7(description) {
7190
7434
  return {
7191
7435
  type: "object",
7192
7436
  properties: {
@@ -7342,10 +7586,10 @@ var manageRelationshipTool = defineAffinityTool({
7342
7586
  "set_structural_tie",
7343
7587
  "remove_structural_tie"
7344
7588
  ]),
7345
- from: contactLocatorSchema6(
7589
+ from: contactLocatorSchema7(
7346
7590
  "Source contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity."
7347
7591
  ),
7348
- to: contactLocatorSchema6(
7592
+ to: contactLocatorSchema7(
7349
7593
  "Target contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity."
7350
7594
  ),
7351
7595
  kind: stringSchema(
@@ -7387,7 +7631,7 @@ var manageRelationshipTool = defineAffinityTool({
7387
7631
  });
7388
7632
 
7389
7633
  // src/tools/merge_contacts_tool.ts
7390
- function contactLocatorSchema7(description) {
7634
+ function contactLocatorSchema8(description) {
7391
7635
  return {
7392
7636
  type: "object",
7393
7637
  properties: {
@@ -7444,10 +7688,10 @@ var mergeContactsTool = defineAffinityTool({
7444
7688
  outputDescription: "Returns the merge receipt with rewired entities and affected-link information.",
7445
7689
  inputSchema: objectSchema(
7446
7690
  {
7447
- winner: contactLocatorSchema7(
7691
+ winner: contactLocatorSchema8(
7448
7692
  "Winner contact. Provide contactId or identity."
7449
7693
  ),
7450
- loser: contactLocatorSchema7(
7694
+ loser: contactLocatorSchema8(
7451
7695
  "Loser contact. Provide contactId or identity."
7452
7696
  ),
7453
7697
  reasonSummary: stringSchema("Optional merge reason."),
@@ -7618,7 +7862,7 @@ function buildChartOptions(input) {
7618
7862
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds }
7619
7863
  };
7620
7864
  }
7621
- function contactLocatorSchema8(description) {
7865
+ function contactLocatorSchema9(description) {
7622
7866
  return {
7623
7867
  type: "object",
7624
7868
  properties: {
@@ -7709,6 +7953,14 @@ function validateReviewInput(input) {
7709
7953
  "Use `contactIds` only with duplicate-candidate or chart review views."
7710
7954
  );
7711
7955
  }
7956
+ if (input.includeDismissed !== void 0 && input.view !== "contacts.duplicates") {
7957
+ return toolFailure(
7958
+ "protocol",
7959
+ "invalid_input",
7960
+ "The `includeDismissed` filter is not valid for this review view.",
7961
+ "Use `includeDismissed` only with the contacts.duplicates review view."
7962
+ );
7963
+ }
7712
7964
  return null;
7713
7965
  }
7714
7966
  function duplicateCandidateItem(candidate) {
@@ -7765,6 +8017,15 @@ function mergeHistoryItem(record) {
7765
8017
  snippet: record.reasonSummary ?? void 0
7766
8018
  };
7767
8019
  }
8020
+ function dismissedPairItem(record) {
8021
+ return {
8022
+ kind: "contact",
8023
+ id: record.leftContactId,
8024
+ title: `Dismissed pair: #${record.leftContactId} and #${record.rightContactId}`,
8025
+ subtitle: record.reason ?? "no reason given",
8026
+ snippet: `dismissed at ${record.dismissedAt}`
8027
+ };
8028
+ }
7768
8029
  function compactFilters(input) {
7769
8030
  return Object.fromEntries(
7770
8031
  Object.entries(input).filter(
@@ -7800,7 +8061,8 @@ function reviewAffinityToolHandler(db, input) {
7800
8061
  {
7801
8062
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
7802
8063
  ...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
7803
- ...input.minScore === void 0 ? {} : { minScore: input.minScore }
8064
+ ...input.minScore === void 0 ? {} : { minScore: input.minScore },
8065
+ ...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
7804
8066
  },
7805
8067
  options
7806
8068
  ).map(duplicateCandidateItem);
@@ -7981,6 +8243,9 @@ function reviewAffinityToolHandler(db, input) {
7981
8243
  );
7982
8244
  break;
7983
8245
  }
8246
+ case "merges.dismissed":
8247
+ items = listDismissedDuplicates(db, options).map(dismissedPairItem);
8248
+ break;
7984
8249
  }
7985
8250
  return toolSuccess(
7986
8251
  summarizeCount(items.length, "item"),
@@ -8031,6 +8296,7 @@ var reviewAffinityTool = defineAffinityTool({
8031
8296
  horizonDays: "Optional horizon filter for commitments or upcoming dates.",
8032
8297
  exactOnly: "Whether duplicate review should only include exact identity matches.",
8033
8298
  minScore: "Optional minimum duplicate score.",
8299
+ includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
8034
8300
  since: "Optional lower timestamp bound for time-based views.",
8035
8301
  until: "Optional upper timestamp bound for time-based views.",
8036
8302
  limit: "Optional result limit.",
@@ -8052,9 +8318,10 @@ var reviewAffinityTool = defineAffinityTool({
8052
8318
  "links.radar",
8053
8319
  "dates.upcoming",
8054
8320
  "graph.chart",
8055
- "merges.history"
8321
+ "merges.history",
8322
+ "merges.dismissed"
8056
8323
  ]),
8057
- contact: contactLocatorSchema8("Optional target contact locator."),
8324
+ contact: contactLocatorSchema9("Optional target contact locator."),
8058
8325
  link: linkLocatorSchema5("Optional target link locator."),
8059
8326
  contactIds: arraySchema(
8060
8327
  integerSchema("Contact id."),
@@ -8105,6 +8372,9 @@ var reviewAffinityTool = defineAffinityTool({
8105
8372
  horizonDays: integerSchema("Optional horizon-days filter."),
8106
8373
  exactOnly: booleanSchema("Whether duplicate review is exact-only."),
8107
8374
  minScore: numberSchema("Optional duplicate minimum score."),
8375
+ includeDismissed: booleanSchema(
8376
+ "Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
8377
+ ),
8108
8378
  since: integerSchema("Optional lower timestamp bound."),
8109
8379
  until: integerSchema("Optional upper timestamp bound."),
8110
8380
  limit: integerSchema("Optional result limit."),
@@ -8203,6 +8473,7 @@ var affinityTools = [
8203
8473
  inspectAffinityItemTool,
8204
8474
  manageContactTool,
8205
8475
  mergeContactsTool,
8476
+ manageDuplicateDismissalTool,
8206
8477
  manageIdentityTool,
8207
8478
  manageRelationshipTool,
8208
8479
  recordEventTool,
@@ -8304,6 +8575,21 @@ var affinityToolMappings = [
8304
8575
  action: "set_lifecycle"
8305
8576
  },
8306
8577
  { source: "mergeContacts", tool: mergeContactsToolName },
8578
+ {
8579
+ source: "dismissDuplicate",
8580
+ tool: manageDuplicateDismissalToolName,
8581
+ action: "dismiss"
8582
+ },
8583
+ {
8584
+ source: "undismissDuplicate",
8585
+ tool: manageDuplicateDismissalToolName,
8586
+ action: "undismiss"
8587
+ },
8588
+ {
8589
+ source: "listDismissedDuplicates",
8590
+ tool: reviewAffinityToolName,
8591
+ view: "merges.dismissed"
8592
+ },
8307
8593
  { source: "addIdentity", tool: manageIdentityToolName, action: "add" },
8308
8594
  { source: "reviseIdentity", tool: manageIdentityToolName, action: "revise" },
8309
8595
  { source: "verifyIdentity", tool: manageIdentityToolName, action: "verify" },
@@ -8385,6 +8671,7 @@ __export(write_exports, {
8385
8671
  addDateAnchor: () => addDateAnchor,
8386
8672
  addIdentity: () => addIdentity,
8387
8673
  createContact: () => createContact,
8674
+ dismissDuplicate: () => dismissDuplicate,
8388
8675
  mergeContacts: () => mergeContacts,
8389
8676
  overrideLinkState: () => overrideLinkState,
8390
8677
  recordCommitment: () => recordCommitment,
@@ -8405,6 +8692,7 @@ __export(write_exports, {
8405
8692
  setAttribute: () => setAttribute,
8406
8693
  setContactLifecycle: () => setContactLifecycle,
8407
8694
  setStructuralTie: () => setStructuralTie,
8695
+ undismissDuplicate: () => undismissDuplicate,
8408
8696
  unsetAttribute: () => unsetAttribute,
8409
8697
  verifyIdentity: () => verifyIdentity
8410
8698
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostpaw/affinity",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "Relationship intelligence and social CRM model for Node.js — SQLite, zero dependencies, with a built-in LLM tool facade",
5
5
  "license": "MIT",
6
6
  "author": {