fullstackgtm 0.14.0 → 0.15.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/src/rules.ts CHANGED
@@ -21,6 +21,27 @@ export function requiresHumanInput(value: unknown): boolean {
21
21
  return typeof value === "string" && value.startsWith(REQUIRES_HUMAN_PREFIX);
22
22
  }
23
23
 
24
+ /**
25
+ * Attribution for duplicate groups: when the provider exposes record-source
26
+ * provenance (RecordProvenance), name the writer(s) that created the group —
27
+ * the fix for recurring dupes is upstream in the writer, not in the records.
28
+ */
29
+ export function provenanceSummary(records: Array<{ provenance?: { source?: string; sourceLabel?: string; sourceId?: string } }>): string {
30
+ const counts = new Map<string, number>();
31
+ for (const record of records) {
32
+ const p = record.provenance;
33
+ if (!p) continue;
34
+ const label = p.sourceLabel ?? p.source ?? "unknown source";
35
+ const key = p.sourceId ? `${label} (${p.sourceId})` : label;
36
+ counts.set(key, (counts.get(key) ?? 0) + 1);
37
+ }
38
+ if (counts.size === 0) return "";
39
+ const parts = [...counts.entries()]
40
+ .sort((a, b) => b[1] - a[1])
41
+ .map(([key, count]) => (count > 1 ? `${key} ×${count}` : key));
42
+ return ` Created by: ${parts.join(", ")}.`;
43
+ }
44
+
24
45
  export function auditFindingId(ruleId: string, objectId: string) {
25
46
  return `finding_${stableHash(`${ruleId}:${objectId}`)}`;
26
47
  }
@@ -343,7 +364,7 @@ export const duplicateAccountDomainRule: GtmAuditRule = {
343
364
  ruleId: "duplicate-account-domain",
344
365
  title: "Accounts share the same domain",
345
366
  severity: "warning",
346
- summary: `${accounts.length} accounts share ${domain}: ${accounts.map((account) => account.name).join(", ")}.`,
367
+ summary: `${accounts.length} accounts share ${domain}: ${accounts.map((account) => account.name).join(", ")}.${provenanceSummary(accounts)}`,
347
368
  recommendation: "Review the group and merge duplicates so activity and deals roll up once.",
348
369
  });
349
370
  operations.push({
@@ -383,7 +404,7 @@ export const duplicateContactEmailRule: GtmAuditRule = {
383
404
  ruleId: "duplicate-contact-email",
384
405
  title: "Contacts share the same email",
385
406
  severity: "warning",
386
- summary: `${contacts.length} contacts share ${email}.`,
407
+ summary: `${contacts.length} contacts share ${email}.${provenanceSummary(contacts)}`,
387
408
  recommendation: "Merge the duplicates so engagement history and routing stay coherent.",
388
409
  });
389
410
  operations.push({
@@ -433,7 +454,7 @@ export const duplicateOpenDealRule: GtmAuditRule = {
433
454
  severity: "warning",
434
455
  summary: `${deals.length} open deals named "${anchor.name}"${
435
456
  anchor.accountId ? " on the same account" : ""
436
- }: ${deals.map((deal) => deal.id).join(", ")}.`,
457
+ }: ${deals.map((deal) => deal.id).join(", ")}.${provenanceSummary(deals)}`,
437
458
  recommendation:
438
459
  "Keep one deal, archive the copies, and fix the integration that is re-creating them.",
439
460
  });
package/src/types.ts CHANGED
@@ -141,11 +141,26 @@ export type CanonicalUser = {
141
141
  active?: boolean;
142
142
  };
143
143
 
144
+ /**
145
+ * Who created a record, per the provider's read-only record-source fields
146
+ * (HubSpot: hs_object_source / _label / _id). Populated on read; used to
147
+ * attribute duplicate findings to the writer that produced them.
148
+ */
149
+ export type RecordProvenance = {
150
+ /** Provider source code, e.g. INTEGRATION, API, CRM_UI, IMPORT, FORM. */
151
+ source?: string;
152
+ /** Human label, e.g. an integration's name. */
153
+ sourceLabel?: string;
154
+ /** Provider-side id of the source (e.g. app id, import id). */
155
+ sourceId?: string;
156
+ };
157
+
144
158
  export type CanonicalAccount = {
145
159
  id: string;
146
160
  provider?: CrmProvider;
147
161
  crmId?: string;
148
162
  identities?: ProviderIdentity[];
163
+ provenance?: RecordProvenance;
149
164
  name: string;
150
165
  domain?: string;
151
166
  industry?: string;
@@ -163,6 +178,7 @@ export type CanonicalContact = {
163
178
  provider?: CrmProvider;
164
179
  crmId?: string;
165
180
  identities?: ProviderIdentity[];
181
+ provenance?: RecordProvenance;
166
182
  accountId?: string;
167
183
  firstName?: string;
168
184
  lastName?: string;
@@ -181,6 +197,7 @@ export type CanonicalDeal = {
181
197
  provider?: CrmProvider;
182
198
  crmId?: string;
183
199
  identities?: ProviderIdentity[];
200
+ provenance?: RecordProvenance;
184
201
  accountId?: string;
185
202
  ownerId?: string;
186
203
  name: string;