fullstackgtm 0.46.0 → 0.48.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +44 -6
  2. package/INSTALL_FOR_AGENTS.md +3 -3
  3. package/README.md +16 -8
  4. package/dist/audit.js +7 -0
  5. package/dist/backfill.js +2 -16
  6. package/dist/cli/fix.d.ts +3 -0
  7. package/dist/cli/fix.js +70 -1
  8. package/dist/cli/help.d.ts +6 -0
  9. package/dist/cli/help.js +76 -2
  10. package/dist/cli/suggest.d.ts +2 -1
  11. package/dist/cli/suggest.js +49 -3
  12. package/dist/cli.js +40 -15
  13. package/dist/connector.js +96 -25
  14. package/dist/connectors/hubspot.d.ts +2 -1
  15. package/dist/connectors/hubspot.js +170 -4
  16. package/dist/connectors/salesforce.d.ts +2 -1
  17. package/dist/connectors/salesforce.js +135 -0
  18. package/dist/connectors/signalSources.js +2 -11
  19. package/dist/format.js +31 -5
  20. package/dist/freeEmailDomains.d.ts +1 -0
  21. package/dist/freeEmailDomains.js +14 -0
  22. package/dist/hierarchy.d.ts +29 -0
  23. package/dist/hierarchy.js +164 -0
  24. package/dist/index.d.ts +6 -2
  25. package/dist/index.js +5 -1
  26. package/dist/mcp.js +19 -15
  27. package/dist/relationships.d.ts +39 -0
  28. package/dist/relationships.js +101 -0
  29. package/dist/route.d.ts +46 -0
  30. package/dist/route.js +228 -0
  31. package/dist/rules.d.ts +1 -0
  32. package/dist/rules.js +172 -12
  33. package/dist/types.d.ts +30 -0
  34. package/docs/api.md +22 -4
  35. package/docs/architecture.md +5 -2
  36. package/package.json +1 -1
  37. package/skills/fullstackgtm/SKILL.md +2 -2
  38. package/src/audit.ts +7 -0
  39. package/src/backfill.ts +2 -17
  40. package/src/cli/fix.ts +68 -1
  41. package/src/cli/help.ts +83 -2
  42. package/src/cli/suggest.ts +58 -4
  43. package/src/cli.ts +38 -13
  44. package/src/connector.ts +94 -20
  45. package/src/connectors/hubspot.ts +164 -5
  46. package/src/connectors/salesforce.ts +136 -1
  47. package/src/connectors/signalSources.ts +2 -12
  48. package/src/format.ts +33 -5
  49. package/src/freeEmailDomains.ts +14 -0
  50. package/src/hierarchy.ts +185 -0
  51. package/src/index.ts +25 -0
  52. package/src/mcp.ts +22 -16
  53. package/src/relationships.ts +115 -0
  54. package/src/route.ts +278 -0
  55. package/src/rules.ts +192 -12
  56. package/src/types.ts +32 -0
package/src/rules.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  CanonicalGtmSnapshot,
8
8
  GtmAuditRule,
9
9
  GtmSnapshotIndex,
10
+ PatchPlanAssumption,
10
11
  RiskLevel,
11
12
  } from "./types.ts";
12
13
 
@@ -50,6 +51,109 @@ export function patchOperationId(ruleId: string, objectId: string) {
50
51
  return `op_${stableHash(`${ruleId}:${objectId}`)}`;
51
52
  }
52
53
 
54
+
55
+ const OPEN_DEAL_FLAGS_ASSUMPTION: PatchPlanAssumption = {
56
+ id: "open-deal-derived-from-closed-won-flags",
57
+ text: "Derived open-deal status from canonical isClosed/isWon flags; a deal is open when neither flag is true.",
58
+ source: "CanonicalDeal.isClosed/isWon",
59
+ confidence: "derived",
60
+ };
61
+
62
+ const MISSING_CLOSED_WON_FLAGS_ASSUMPTION: PatchPlanAssumption = {
63
+ id: "missing-closed-won-flags-treated-open",
64
+ text: "Treated deals with missing isClosed/isWon flags as open so hygiene rules do not silently ignore active pipeline.",
65
+ source: "CanonicalDeal.isClosed/isWon",
66
+ confidence: "heuristic",
67
+ };
68
+
69
+ const ORPHAN_ACCOUNT_RELATIONSHIP_ASSUMPTION: PatchPlanAssumption = {
70
+ id: "account-without-contacts-or-deals-treated-orphan",
71
+ text: "Treated an account as orphaned when the snapshot has no contacts and no deals linked to that account.",
72
+ source: "snapshot account/contact/deal associations",
73
+ confidence: "derived",
74
+ };
75
+
76
+ const DEAL_OWNER_POLICY_ASSUMPTION: PatchPlanAssumption = {
77
+ id: "deal-owner-required-by-policy",
78
+ text: "Treated a missing or unknown deal owner as invalid when requireDealOwner policy is enabled.",
79
+ source: "GtmPolicy.requireDealOwner",
80
+ confidence: "heuristic",
81
+ };
82
+
83
+ const DEAL_ACCOUNT_POLICY_ASSUMPTION: PatchPlanAssumption = {
84
+ id: "deal-account-required-by-policy",
85
+ text: "Treated a missing or unknown deal account link as invalid when requireAccountForDeal policy is enabled.",
86
+ source: "GtmPolicy.requireAccountForDeal",
87
+ confidence: "heuristic",
88
+ };
89
+
90
+ const DEAL_AMOUNT_POLICY_ASSUMPTION: PatchPlanAssumption = {
91
+ id: "open-deal-amount-required-by-policy",
92
+ text: "Treated open deals with missing or zero amount as forecast-risk findings unless requireDealAmount is disabled.",
93
+ source: "GtmPolicy.requireDealAmount",
94
+ confidence: "heuristic",
95
+ };
96
+
97
+ const DUPLICATE_ACCOUNT_DOMAIN_ASSUMPTION: PatchPlanAssumption = {
98
+ id: "same-normalized-domain-duplicate-account",
99
+ text: "Treated accounts with the same normalized domain as duplicate-account candidates.",
100
+ source: "CanonicalAccount.domain",
101
+ confidence: "heuristic",
102
+ };
103
+
104
+ const DUPLICATE_CONTACT_EMAIL_ASSUMPTION: PatchPlanAssumption = {
105
+ id: "same-email-duplicate-contact",
106
+ text: "Treated contacts with the same lowercased email address as duplicate-contact candidates.",
107
+ source: "CanonicalContact.email",
108
+ confidence: "heuristic",
109
+ };
110
+
111
+ const DUPLICATE_OPEN_DEAL_ASSUMPTION: PatchPlanAssumption = {
112
+ id: "same-account-and-name-duplicate-open-deal",
113
+ text: "Treated open deals with the same normalized name on the same account scope as duplicate-opportunity candidates.",
114
+ source: "CanonicalDeal.accountId/name",
115
+ confidence: "heuristic",
116
+ };
117
+
118
+ const ACCOUNT_WITH_OPEN_DEAL_NEEDS_CONTACT_ASSUMPTION: PatchPlanAssumption = {
119
+ id: "open-pipeline-account-needs-contact",
120
+ text: "Treated accounts with open pipeline but no linked contacts as coverage gaps requiring a buying-committee contact.",
121
+ source: "snapshot account/contact/deal associations",
122
+ confidence: "heuristic",
123
+ };
124
+
125
+ const SINGLE_SOURCE_ACCOUNT_ASSUMPTION: PatchPlanAssumption = {
126
+ id: "single-source-account-is-cross-system-gap",
127
+ text: "Treated an account present in only one connected system as a cross-system reconciliation gap.",
128
+ source: "ProviderIdentity.provider",
129
+ confidence: "heuristic",
130
+ };
131
+
132
+ function staleDealPolicyAssumption(days: number): PatchPlanAssumption {
133
+ return {
134
+ id: "stale-deal-days-policy",
135
+ text: `Treated open deals with no recorded activity for more than ${days} days as stale.`,
136
+ source: "GtmPolicy.staleDealDays",
137
+ confidence: "heuristic",
138
+ };
139
+ }
140
+
141
+ function closingSoonPolicyAssumption(windowDays: number, idleDays: number): PatchPlanAssumption {
142
+ return {
143
+ id: "closing-soon-idle-policy",
144
+ text: `Treated open deals closing within ${windowDays} days and idle for more than ${idleDays} days as silent slip risk.`,
145
+ source: "GtmPolicy.closingSoonDays/closingSoonIdleDays",
146
+ confidence: "heuristic",
147
+ };
148
+ }
149
+
150
+ function assumptionsIfFindings(
151
+ findings: AuditFinding[],
152
+ assumptions: PatchPlanAssumption[],
153
+ ): PatchPlanAssumption[] | undefined {
154
+ return findings.length > 0 ? assumptions : undefined;
155
+ }
156
+
53
157
  export function stableHash(value: string) {
54
158
  let hash = 0;
55
159
  for (let index = 0; index < value.length; index += 1) {
@@ -168,7 +272,11 @@ export const orphanAccountRule: GtmAuditRule = {
168
272
  approvalRequired: true,
169
273
  });
170
274
  }
171
- return { findings, operations };
275
+ return {
276
+ findings,
277
+ operations,
278
+ assumptions: assumptionsIfFindings(findings, [ORPHAN_ACCOUNT_RELATIONSHIP_ASSUMPTION]),
279
+ };
172
280
  },
173
281
  };
174
282
 
@@ -198,7 +306,11 @@ export const missingDealOwnerRule: GtmAuditRule = {
198
306
  approvalRequired: true,
199
307
  });
200
308
  }
201
- return { findings, operations };
309
+ return {
310
+ findings,
311
+ operations,
312
+ assumptions: assumptionsIfFindings(findings, [DEAL_OWNER_POLICY_ASSUMPTION]),
313
+ };
202
314
  },
203
315
  };
204
316
 
@@ -227,7 +339,11 @@ export const missingDealAccountRule: GtmAuditRule = {
227
339
  approvalRequired: true,
228
340
  });
229
341
  }
230
- return { findings, operations };
342
+ return {
343
+ findings,
344
+ operations,
345
+ assumptions: assumptionsIfFindings(findings, [DEAL_ACCOUNT_POLICY_ASSUMPTION]),
346
+ };
231
347
  },
232
348
  };
233
349
 
@@ -256,7 +372,14 @@ export const pastCloseDateRule: GtmAuditRule = {
256
372
  approvalRequired: true,
257
373
  });
258
374
  }
259
- return { findings, operations };
375
+ return {
376
+ findings,
377
+ operations,
378
+ assumptions: assumptionsIfFindings(findings, [
379
+ OPEN_DEAL_FLAGS_ASSUMPTION,
380
+ MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
381
+ ]),
382
+ };
260
383
  },
261
384
  };
262
385
 
@@ -297,7 +420,15 @@ export const staleDealRule: GtmAuditRule = {
297
420
  approvalRequired: true,
298
421
  });
299
422
  }
300
- return { findings, operations };
423
+ return {
424
+ findings,
425
+ operations,
426
+ assumptions: assumptionsIfFindings(findings, [
427
+ OPEN_DEAL_FLAGS_ASSUMPTION,
428
+ MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
429
+ staleDealPolicyAssumption(policy.staleDealDays),
430
+ ]),
431
+ };
301
432
  },
302
433
  };
303
434
 
@@ -327,7 +458,15 @@ export const missingDealAmountRule: GtmAuditRule = {
327
458
  approvalRequired: true,
328
459
  });
329
460
  }
330
- return { findings, operations };
461
+ return {
462
+ findings,
463
+ operations,
464
+ assumptions: assumptionsIfFindings(findings, [
465
+ OPEN_DEAL_FLAGS_ASSUMPTION,
466
+ MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
467
+ DEAL_AMOUNT_POLICY_ASSUMPTION,
468
+ ]),
469
+ };
331
470
  },
332
471
  };
333
472
 
@@ -382,7 +521,11 @@ export const duplicateAccountDomainRule: GtmAuditRule = {
382
521
  "IRREVERSIBLE: provider merges cannot be unmerged. The pre-apply snapshot retains every record's field values; recreate a record manually from it if a merge was wrong.",
383
522
  });
384
523
  }
385
- return { findings, operations };
524
+ return {
525
+ findings,
526
+ operations,
527
+ assumptions: assumptionsIfFindings(findings, [DUPLICATE_ACCOUNT_DOMAIN_ASSUMPTION]),
528
+ };
386
529
  },
387
530
  };
388
531
 
@@ -422,7 +565,11 @@ export const duplicateContactEmailRule: GtmAuditRule = {
422
565
  "IRREVERSIBLE: provider merges cannot be unmerged. The pre-apply snapshot retains every record's field values; recreate a record manually from it if a merge was wrong.",
423
566
  });
424
567
  }
425
- return { findings, operations };
568
+ return {
569
+ findings,
570
+ operations,
571
+ assumptions: assumptionsIfFindings(findings, [DUPLICATE_CONTACT_EMAIL_ASSUMPTION]),
572
+ };
426
573
  },
427
574
  };
428
575
 
@@ -473,7 +620,15 @@ export const duplicateOpenDealRule: GtmAuditRule = {
473
620
  "IRREVERSIBLE: provider merges cannot be unmerged. The pre-apply snapshot retains every record's field values; recreate a record manually from it if a merge was wrong.",
474
621
  });
475
622
  }
476
- return { findings, operations };
623
+ return {
624
+ findings,
625
+ operations,
626
+ assumptions: assumptionsIfFindings(findings, [
627
+ OPEN_DEAL_FLAGS_ASSUMPTION,
628
+ MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
629
+ DUPLICATE_OPEN_DEAL_ASSUMPTION,
630
+ ]),
631
+ };
477
632
  },
478
633
  };
479
634
 
@@ -514,7 +669,15 @@ export const activeDealAccountWithoutContactsRule: GtmAuditRule = {
514
669
  approvalRequired: true,
515
670
  });
516
671
  }
517
- return { findings, operations };
672
+ return {
673
+ findings,
674
+ operations,
675
+ assumptions: assumptionsIfFindings(findings, [
676
+ OPEN_DEAL_FLAGS_ASSUMPTION,
677
+ MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
678
+ ACCOUNT_WITH_OPEN_DEAL_NEEDS_CONTACT_ASSUMPTION,
679
+ ]),
680
+ };
518
681
  },
519
682
  };
520
683
 
@@ -559,13 +722,22 @@ export const closingSoonInactiveRule: GtmAuditRule = {
559
722
  approvalRequired: true,
560
723
  });
561
724
  }
562
- return { findings, operations };
725
+ return {
726
+ findings,
727
+ operations,
728
+ assumptions: assumptionsIfFindings(findings, [
729
+ OPEN_DEAL_FLAGS_ASSUMPTION,
730
+ MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
731
+ closingSoonPolicyAssumption(windowDays, idleDays),
732
+ ]),
733
+ };
563
734
  },
564
735
  };
565
736
 
566
737
  export const accountSingleSourceRule: GtmAuditRule = {
567
738
  id: "account-single-source",
568
739
  title: "Account exists in only one connected system",
740
+ emitsOperations: false,
569
741
  description:
570
742
  "On merged multi-system snapshots, flags accounts known to just one source — the seams where GTM systems disagree.",
571
743
  category: "cross-system",
@@ -597,10 +769,18 @@ export const accountSingleSourceRule: GtmAuditRule = {
597
769
  "Check whether the account is missing from the other systems or matched under a different domain/name.",
598
770
  });
599
771
  }
600
- return { findings, operations: [] };
772
+ return {
773
+ findings,
774
+ operations: [],
775
+ assumptions: assumptionsIfFindings(findings, [SINGLE_SOURCE_ACCOUNT_ASSUMPTION]),
776
+ };
601
777
  },
602
778
  };
603
779
 
780
+ export function isFindingsOnlyRule(rule: GtmAuditRule): boolean {
781
+ return rule.emitsOperations === false;
782
+ }
783
+
604
784
  export const builtinAuditRules: GtmAuditRule[] = [
605
785
  orphanAccountRule,
606
786
  missingDealOwnerRule,
package/src/types.ts CHANGED
@@ -105,6 +105,15 @@ export type CreateRecordPayload = {
105
105
 
106
106
  export type AuditFindingSeverity = "info" | "warning" | "critical";
107
107
 
108
+ export type PatchPlanAssumptionConfidence = "derived" | "heuristic" | "guess";
109
+
110
+ export type PatchPlanAssumption = {
111
+ id: string;
112
+ text: string;
113
+ source?: string;
114
+ confidence?: PatchPlanAssumptionConfidence;
115
+ };
116
+
108
117
  /**
109
118
  * One claim that a canonical record exists in an external system. A record
110
119
  * merged from several providers carries one identity per provider.
@@ -397,6 +406,10 @@ export type PatchPlan = {
397
406
  * Account/contact hygiene findings are NOT here; read `findings` for those. */
398
407
  pipelineFindings?: PipelineFinding[];
399
408
  evidence?: GtmEvidence[];
409
+ /** Assumptions used to turn audit observations into findings and operations. */
410
+ assumptions?: PatchPlanAssumption[];
411
+ /** Human decisions or unresolved questions that affect how the plan should be applied. */
412
+ openQuestions?: string[];
400
413
  operations: PatchOperation[];
401
414
  /**
402
415
  * The filter this plan's operations were selected by. Re-evaluated per
@@ -454,6 +467,8 @@ export type GtmRuleContext = {
454
467
  export type GtmRuleResult = {
455
468
  findings: AuditFinding[];
456
469
  operations: PatchOperation[];
470
+ /** Rule-level assumptions that explain the policy or heuristic behind returned findings. */
471
+ assumptions?: PatchPlanAssumption[];
457
472
  };
458
473
 
459
474
  /**
@@ -466,6 +481,8 @@ export type GtmAuditRule = {
466
481
  description: string;
467
482
  /** Grouping for docs and discovery, e.g. "hygiene", "forecast", "coverage". */
468
483
  category?: string;
484
+ /** False for findings-only rules whose evaluator intentionally emits no patch operations. */
485
+ emitsOperations?: boolean;
469
486
  evaluate: (context: GtmRuleContext) => GtmRuleResult;
470
487
  };
471
488
 
@@ -523,6 +540,21 @@ export type GtmConnector = {
523
540
  * result per input op. Optional: connectors without it use `applyOperation`.
524
541
  */
525
542
  applyCreateContactsBatch?: (operations: PatchOperation[]) => Promise<PatchOperationResult[]>;
543
+ /**
544
+ * Optional bulk apply path for homogeneous operation runs selected by
545
+ * `applyPatchPlan` (currently consecutive `set_field` ops on one object type
546
+ * and consecutive `archive_record` ops on one object type). Implementations
547
+ * MUST return exactly one result per input operation, preserve per-operation
548
+ * CAS/conflict semantics for field writes, and must not write operations that
549
+ * would have conflicted under the serial `readField` + `applyOperation` path.
550
+ * Connectors without it use `applyOperation` per operation.
551
+ */
552
+ applyBatch?: (operations: PatchOperation[]) => Promise<PatchOperationResult[]>;
553
+ /**
554
+ * Maximum number of operations `applyPatchPlan` should pass to one
555
+ * `applyBatch` call. Defaults to 200 when unspecified.
556
+ */
557
+ applyBatchLimit?: number;
526
558
  /**
527
559
  * Read the live value of one canonical field, used for compare-and-set:
528
560
  * apply orchestration refuses to write over values that drifted since the