fullstackgtm 0.47.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.
- package/CHANGELOG.md +38 -6
- package/INSTALL_FOR_AGENTS.md +3 -3
- package/README.md +16 -8
- package/dist/audit.js +7 -0
- package/dist/backfill.js +2 -16
- package/dist/cli/fix.d.ts +3 -0
- package/dist/cli/fix.js +70 -1
- package/dist/cli/help.d.ts +6 -0
- package/dist/cli/help.js +76 -2
- package/dist/cli/suggest.d.ts +2 -1
- package/dist/cli/suggest.js +49 -3
- package/dist/cli.js +40 -15
- package/dist/connectors/hubspot.d.ts +2 -1
- package/dist/connectors/salesforce.d.ts +2 -1
- package/dist/connectors/signalSources.js +2 -11
- package/dist/format.js +31 -5
- package/dist/freeEmailDomains.d.ts +1 -0
- package/dist/freeEmailDomains.js +14 -0
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/mcp.js +19 -15
- package/dist/relationships.d.ts +39 -0
- package/dist/relationships.js +101 -0
- package/dist/route.d.ts +46 -0
- package/dist/route.js +228 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +172 -12
- package/dist/types.d.ts +15 -0
- package/docs/api.md +20 -3
- package/docs/architecture.md +5 -2
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +2 -2
- package/src/audit.ts +7 -0
- package/src/backfill.ts +2 -17
- package/src/cli/fix.ts +68 -1
- package/src/cli/help.ts +83 -2
- package/src/cli/suggest.ts +58 -4
- package/src/cli.ts +38 -13
- package/src/connectors/hubspot.ts +3 -1
- package/src/connectors/salesforce.ts +3 -1
- package/src/connectors/signalSources.ts +2 -12
- package/src/format.ts +33 -5
- package/src/freeEmailDomains.ts +14 -0
- package/src/hierarchy.ts +185 -0
- package/src/index.ts +25 -0
- package/src/mcp.ts +22 -16
- package/src/relationships.ts +115 -0
- package/src/route.ts +278 -0
- package/src/rules.ts +192 -12
- package/src/types.ts +17 -0
package/dist/rules.js
CHANGED
|
@@ -36,6 +36,91 @@ export function auditFindingId(ruleId, objectId) {
|
|
|
36
36
|
export function patchOperationId(ruleId, objectId) {
|
|
37
37
|
return `op_${stableHash(`${ruleId}:${objectId}`)}`;
|
|
38
38
|
}
|
|
39
|
+
const OPEN_DEAL_FLAGS_ASSUMPTION = {
|
|
40
|
+
id: "open-deal-derived-from-closed-won-flags",
|
|
41
|
+
text: "Derived open-deal status from canonical isClosed/isWon flags; a deal is open when neither flag is true.",
|
|
42
|
+
source: "CanonicalDeal.isClosed/isWon",
|
|
43
|
+
confidence: "derived",
|
|
44
|
+
};
|
|
45
|
+
const MISSING_CLOSED_WON_FLAGS_ASSUMPTION = {
|
|
46
|
+
id: "missing-closed-won-flags-treated-open",
|
|
47
|
+
text: "Treated deals with missing isClosed/isWon flags as open so hygiene rules do not silently ignore active pipeline.",
|
|
48
|
+
source: "CanonicalDeal.isClosed/isWon",
|
|
49
|
+
confidence: "heuristic",
|
|
50
|
+
};
|
|
51
|
+
const ORPHAN_ACCOUNT_RELATIONSHIP_ASSUMPTION = {
|
|
52
|
+
id: "account-without-contacts-or-deals-treated-orphan",
|
|
53
|
+
text: "Treated an account as orphaned when the snapshot has no contacts and no deals linked to that account.",
|
|
54
|
+
source: "snapshot account/contact/deal associations",
|
|
55
|
+
confidence: "derived",
|
|
56
|
+
};
|
|
57
|
+
const DEAL_OWNER_POLICY_ASSUMPTION = {
|
|
58
|
+
id: "deal-owner-required-by-policy",
|
|
59
|
+
text: "Treated a missing or unknown deal owner as invalid when requireDealOwner policy is enabled.",
|
|
60
|
+
source: "GtmPolicy.requireDealOwner",
|
|
61
|
+
confidence: "heuristic",
|
|
62
|
+
};
|
|
63
|
+
const DEAL_ACCOUNT_POLICY_ASSUMPTION = {
|
|
64
|
+
id: "deal-account-required-by-policy",
|
|
65
|
+
text: "Treated a missing or unknown deal account link as invalid when requireAccountForDeal policy is enabled.",
|
|
66
|
+
source: "GtmPolicy.requireAccountForDeal",
|
|
67
|
+
confidence: "heuristic",
|
|
68
|
+
};
|
|
69
|
+
const DEAL_AMOUNT_POLICY_ASSUMPTION = {
|
|
70
|
+
id: "open-deal-amount-required-by-policy",
|
|
71
|
+
text: "Treated open deals with missing or zero amount as forecast-risk findings unless requireDealAmount is disabled.",
|
|
72
|
+
source: "GtmPolicy.requireDealAmount",
|
|
73
|
+
confidence: "heuristic",
|
|
74
|
+
};
|
|
75
|
+
const DUPLICATE_ACCOUNT_DOMAIN_ASSUMPTION = {
|
|
76
|
+
id: "same-normalized-domain-duplicate-account",
|
|
77
|
+
text: "Treated accounts with the same normalized domain as duplicate-account candidates.",
|
|
78
|
+
source: "CanonicalAccount.domain",
|
|
79
|
+
confidence: "heuristic",
|
|
80
|
+
};
|
|
81
|
+
const DUPLICATE_CONTACT_EMAIL_ASSUMPTION = {
|
|
82
|
+
id: "same-email-duplicate-contact",
|
|
83
|
+
text: "Treated contacts with the same lowercased email address as duplicate-contact candidates.",
|
|
84
|
+
source: "CanonicalContact.email",
|
|
85
|
+
confidence: "heuristic",
|
|
86
|
+
};
|
|
87
|
+
const DUPLICATE_OPEN_DEAL_ASSUMPTION = {
|
|
88
|
+
id: "same-account-and-name-duplicate-open-deal",
|
|
89
|
+
text: "Treated open deals with the same normalized name on the same account scope as duplicate-opportunity candidates.",
|
|
90
|
+
source: "CanonicalDeal.accountId/name",
|
|
91
|
+
confidence: "heuristic",
|
|
92
|
+
};
|
|
93
|
+
const ACCOUNT_WITH_OPEN_DEAL_NEEDS_CONTACT_ASSUMPTION = {
|
|
94
|
+
id: "open-pipeline-account-needs-contact",
|
|
95
|
+
text: "Treated accounts with open pipeline but no linked contacts as coverage gaps requiring a buying-committee contact.",
|
|
96
|
+
source: "snapshot account/contact/deal associations",
|
|
97
|
+
confidence: "heuristic",
|
|
98
|
+
};
|
|
99
|
+
const SINGLE_SOURCE_ACCOUNT_ASSUMPTION = {
|
|
100
|
+
id: "single-source-account-is-cross-system-gap",
|
|
101
|
+
text: "Treated an account present in only one connected system as a cross-system reconciliation gap.",
|
|
102
|
+
source: "ProviderIdentity.provider",
|
|
103
|
+
confidence: "heuristic",
|
|
104
|
+
};
|
|
105
|
+
function staleDealPolicyAssumption(days) {
|
|
106
|
+
return {
|
|
107
|
+
id: "stale-deal-days-policy",
|
|
108
|
+
text: `Treated open deals with no recorded activity for more than ${days} days as stale.`,
|
|
109
|
+
source: "GtmPolicy.staleDealDays",
|
|
110
|
+
confidence: "heuristic",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function closingSoonPolicyAssumption(windowDays, idleDays) {
|
|
114
|
+
return {
|
|
115
|
+
id: "closing-soon-idle-policy",
|
|
116
|
+
text: `Treated open deals closing within ${windowDays} days and idle for more than ${idleDays} days as silent slip risk.`,
|
|
117
|
+
source: "GtmPolicy.closingSoonDays/closingSoonIdleDays",
|
|
118
|
+
confidence: "heuristic",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function assumptionsIfFindings(findings, assumptions) {
|
|
122
|
+
return findings.length > 0 ? assumptions : undefined;
|
|
123
|
+
}
|
|
39
124
|
export function stableHash(value) {
|
|
40
125
|
let hash = 0;
|
|
41
126
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -150,7 +235,11 @@ export const orphanAccountRule = {
|
|
|
150
235
|
approvalRequired: true,
|
|
151
236
|
});
|
|
152
237
|
}
|
|
153
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
findings,
|
|
240
|
+
operations,
|
|
241
|
+
assumptions: assumptionsIfFindings(findings, [ORPHAN_ACCOUNT_RELATIONSHIP_ASSUMPTION]),
|
|
242
|
+
};
|
|
154
243
|
},
|
|
155
244
|
};
|
|
156
245
|
export const missingDealOwnerRule = {
|
|
@@ -180,7 +269,11 @@ export const missingDealOwnerRule = {
|
|
|
180
269
|
approvalRequired: true,
|
|
181
270
|
});
|
|
182
271
|
}
|
|
183
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
findings,
|
|
274
|
+
operations,
|
|
275
|
+
assumptions: assumptionsIfFindings(findings, [DEAL_OWNER_POLICY_ASSUMPTION]),
|
|
276
|
+
};
|
|
184
277
|
},
|
|
185
278
|
};
|
|
186
279
|
export const missingDealAccountRule = {
|
|
@@ -210,7 +303,11 @@ export const missingDealAccountRule = {
|
|
|
210
303
|
approvalRequired: true,
|
|
211
304
|
});
|
|
212
305
|
}
|
|
213
|
-
return {
|
|
306
|
+
return {
|
|
307
|
+
findings,
|
|
308
|
+
operations,
|
|
309
|
+
assumptions: assumptionsIfFindings(findings, [DEAL_ACCOUNT_POLICY_ASSUMPTION]),
|
|
310
|
+
};
|
|
214
311
|
},
|
|
215
312
|
};
|
|
216
313
|
export const pastCloseDateRule = {
|
|
@@ -240,7 +337,14 @@ export const pastCloseDateRule = {
|
|
|
240
337
|
approvalRequired: true,
|
|
241
338
|
});
|
|
242
339
|
}
|
|
243
|
-
return {
|
|
340
|
+
return {
|
|
341
|
+
findings,
|
|
342
|
+
operations,
|
|
343
|
+
assumptions: assumptionsIfFindings(findings, [
|
|
344
|
+
OPEN_DEAL_FLAGS_ASSUMPTION,
|
|
345
|
+
MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
|
|
346
|
+
]),
|
|
347
|
+
};
|
|
244
348
|
},
|
|
245
349
|
};
|
|
246
350
|
export const staleDealRule = {
|
|
@@ -280,7 +384,15 @@ export const staleDealRule = {
|
|
|
280
384
|
approvalRequired: true,
|
|
281
385
|
});
|
|
282
386
|
}
|
|
283
|
-
return {
|
|
387
|
+
return {
|
|
388
|
+
findings,
|
|
389
|
+
operations,
|
|
390
|
+
assumptions: assumptionsIfFindings(findings, [
|
|
391
|
+
OPEN_DEAL_FLAGS_ASSUMPTION,
|
|
392
|
+
MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
|
|
393
|
+
staleDealPolicyAssumption(policy.staleDealDays),
|
|
394
|
+
]),
|
|
395
|
+
};
|
|
284
396
|
},
|
|
285
397
|
};
|
|
286
398
|
export const missingDealAmountRule = {
|
|
@@ -312,7 +424,15 @@ export const missingDealAmountRule = {
|
|
|
312
424
|
approvalRequired: true,
|
|
313
425
|
});
|
|
314
426
|
}
|
|
315
|
-
return {
|
|
427
|
+
return {
|
|
428
|
+
findings,
|
|
429
|
+
operations,
|
|
430
|
+
assumptions: assumptionsIfFindings(findings, [
|
|
431
|
+
OPEN_DEAL_FLAGS_ASSUMPTION,
|
|
432
|
+
MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
|
|
433
|
+
DEAL_AMOUNT_POLICY_ASSUMPTION,
|
|
434
|
+
]),
|
|
435
|
+
};
|
|
316
436
|
},
|
|
317
437
|
};
|
|
318
438
|
function duplicateGroups(items, keyOf) {
|
|
@@ -365,7 +485,11 @@ export const duplicateAccountDomainRule = {
|
|
|
365
485
|
rollback: "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.",
|
|
366
486
|
});
|
|
367
487
|
}
|
|
368
|
-
return {
|
|
488
|
+
return {
|
|
489
|
+
findings,
|
|
490
|
+
operations,
|
|
491
|
+
assumptions: assumptionsIfFindings(findings, [DUPLICATE_ACCOUNT_DOMAIN_ASSUMPTION]),
|
|
492
|
+
};
|
|
369
493
|
},
|
|
370
494
|
};
|
|
371
495
|
export const duplicateContactEmailRule = {
|
|
@@ -402,7 +526,11 @@ export const duplicateContactEmailRule = {
|
|
|
402
526
|
rollback: "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.",
|
|
403
527
|
});
|
|
404
528
|
}
|
|
405
|
-
return {
|
|
529
|
+
return {
|
|
530
|
+
findings,
|
|
531
|
+
operations,
|
|
532
|
+
assumptions: assumptionsIfFindings(findings, [DUPLICATE_CONTACT_EMAIL_ASSUMPTION]),
|
|
533
|
+
};
|
|
406
534
|
},
|
|
407
535
|
};
|
|
408
536
|
export const duplicateOpenDealRule = {
|
|
@@ -449,7 +577,15 @@ export const duplicateOpenDealRule = {
|
|
|
449
577
|
rollback: "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.",
|
|
450
578
|
});
|
|
451
579
|
}
|
|
452
|
-
return {
|
|
580
|
+
return {
|
|
581
|
+
findings,
|
|
582
|
+
operations,
|
|
583
|
+
assumptions: assumptionsIfFindings(findings, [
|
|
584
|
+
OPEN_DEAL_FLAGS_ASSUMPTION,
|
|
585
|
+
MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
|
|
586
|
+
DUPLICATE_OPEN_DEAL_ASSUMPTION,
|
|
587
|
+
]),
|
|
588
|
+
};
|
|
453
589
|
},
|
|
454
590
|
};
|
|
455
591
|
export const activeDealAccountWithoutContactsRule = {
|
|
@@ -491,7 +627,15 @@ export const activeDealAccountWithoutContactsRule = {
|
|
|
491
627
|
approvalRequired: true,
|
|
492
628
|
});
|
|
493
629
|
}
|
|
494
|
-
return {
|
|
630
|
+
return {
|
|
631
|
+
findings,
|
|
632
|
+
operations,
|
|
633
|
+
assumptions: assumptionsIfFindings(findings, [
|
|
634
|
+
OPEN_DEAL_FLAGS_ASSUMPTION,
|
|
635
|
+
MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
|
|
636
|
+
ACCOUNT_WITH_OPEN_DEAL_NEEDS_CONTACT_ASSUMPTION,
|
|
637
|
+
]),
|
|
638
|
+
};
|
|
495
639
|
},
|
|
496
640
|
};
|
|
497
641
|
export const closingSoonInactiveRule = {
|
|
@@ -536,12 +680,21 @@ export const closingSoonInactiveRule = {
|
|
|
536
680
|
approvalRequired: true,
|
|
537
681
|
});
|
|
538
682
|
}
|
|
539
|
-
return {
|
|
683
|
+
return {
|
|
684
|
+
findings,
|
|
685
|
+
operations,
|
|
686
|
+
assumptions: assumptionsIfFindings(findings, [
|
|
687
|
+
OPEN_DEAL_FLAGS_ASSUMPTION,
|
|
688
|
+
MISSING_CLOSED_WON_FLAGS_ASSUMPTION,
|
|
689
|
+
closingSoonPolicyAssumption(windowDays, idleDays),
|
|
690
|
+
]),
|
|
691
|
+
};
|
|
540
692
|
},
|
|
541
693
|
};
|
|
542
694
|
export const accountSingleSourceRule = {
|
|
543
695
|
id: "account-single-source",
|
|
544
696
|
title: "Account exists in only one connected system",
|
|
697
|
+
emitsOperations: false,
|
|
545
698
|
description: "On merged multi-system snapshots, flags accounts known to just one source — the seams where GTM systems disagree.",
|
|
546
699
|
category: "cross-system",
|
|
547
700
|
evaluate: ({ snapshot }) => {
|
|
@@ -566,9 +719,16 @@ export const accountSingleSourceRule = {
|
|
|
566
719
|
recommendation: "Check whether the account is missing from the other systems or matched under a different domain/name.",
|
|
567
720
|
});
|
|
568
721
|
}
|
|
569
|
-
return {
|
|
722
|
+
return {
|
|
723
|
+
findings,
|
|
724
|
+
operations: [],
|
|
725
|
+
assumptions: assumptionsIfFindings(findings, [SINGLE_SOURCE_ACCOUNT_ASSUMPTION]),
|
|
726
|
+
};
|
|
570
727
|
},
|
|
571
728
|
};
|
|
729
|
+
export function isFindingsOnlyRule(rule) {
|
|
730
|
+
return rule.emitsOperations === false;
|
|
731
|
+
}
|
|
572
732
|
export const builtinAuditRules = [
|
|
573
733
|
orphanAccountRule,
|
|
574
734
|
missingDealOwnerRule,
|
package/dist/types.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export type CreateRecordPayload = {
|
|
|
58
58
|
dealPipeline?: string;
|
|
59
59
|
};
|
|
60
60
|
export type AuditFindingSeverity = "info" | "warning" | "critical";
|
|
61
|
+
export type PatchPlanAssumptionConfidence = "derived" | "heuristic" | "guess";
|
|
62
|
+
export type PatchPlanAssumption = {
|
|
63
|
+
id: string;
|
|
64
|
+
text: string;
|
|
65
|
+
source?: string;
|
|
66
|
+
confidence?: PatchPlanAssumptionConfidence;
|
|
67
|
+
};
|
|
61
68
|
/**
|
|
62
69
|
* One claim that a canonical record exists in an external system. A record
|
|
63
70
|
* merged from several providers carries one identity per provider.
|
|
@@ -323,6 +330,10 @@ export type PatchPlan = {
|
|
|
323
330
|
* Account/contact hygiene findings are NOT here; read `findings` for those. */
|
|
324
331
|
pipelineFindings?: PipelineFinding[];
|
|
325
332
|
evidence?: GtmEvidence[];
|
|
333
|
+
/** Assumptions used to turn audit observations into findings and operations. */
|
|
334
|
+
assumptions?: PatchPlanAssumption[];
|
|
335
|
+
/** Human decisions or unresolved questions that affect how the plan should be applied. */
|
|
336
|
+
openQuestions?: string[];
|
|
326
337
|
operations: PatchOperation[];
|
|
327
338
|
/**
|
|
328
339
|
* The filter this plan's operations were selected by. Re-evaluated per
|
|
@@ -374,6 +385,8 @@ export type GtmRuleContext = {
|
|
|
374
385
|
export type GtmRuleResult = {
|
|
375
386
|
findings: AuditFinding[];
|
|
376
387
|
operations: PatchOperation[];
|
|
388
|
+
/** Rule-level assumptions that explain the policy or heuristic behind returned findings. */
|
|
389
|
+
assumptions?: PatchPlanAssumption[];
|
|
377
390
|
};
|
|
378
391
|
/**
|
|
379
392
|
* A deterministic audit rule. Rules read the snapshot through the context and
|
|
@@ -385,6 +398,8 @@ export type GtmAuditRule = {
|
|
|
385
398
|
description: string;
|
|
386
399
|
/** Grouping for docs and discovery, e.g. "hygiene", "forecast", "coverage". */
|
|
387
400
|
category?: string;
|
|
401
|
+
/** False for findings-only rules whose evaluator intentionally emits no patch operations. */
|
|
402
|
+
emitsOperations?: boolean;
|
|
388
403
|
evaluate: (context: GtmRuleContext) => GtmRuleResult;
|
|
389
404
|
};
|
|
390
405
|
export type PatchOperationResult = {
|
package/docs/api.md
CHANGED
|
@@ -69,7 +69,8 @@ release.
|
|
|
69
69
|
Commands: `init`, `login` / `logout`, `snapshot`, `audit`, `report`, `diff`, `merge`, `plans`,
|
|
70
70
|
`apply`, `suggest`, `audit-log` (`export` / `verify`),
|
|
71
71
|
`call` (`parse` / `classify` / `score` / `link` / `plan`), `resolve`,
|
|
72
|
-
`
|
|
72
|
+
`hierarchy` (`report`), `relationships` (`account`),
|
|
73
|
+
`route` (`leads`), `bulk-update`, `dedupe`, `reassign`, `fix`, `health`,
|
|
73
74
|
`market` (`init` / `capture` / `classify` / `worksheet` / `observe` / `fronts` /
|
|
74
75
|
`axes` / `overlay` / `scale` / `report` / `refresh`),
|
|
75
76
|
`tam` (`estimate` / `accounts` / `status` / `report` / `populate`),
|
|
@@ -160,8 +161,8 @@ per-rule detail with capped examples, and next steps. `auditReportToMarkdown` /
|
|
|
160
161
|
|
|
161
162
|
## Governed write verbs
|
|
162
163
|
|
|
163
|
-
Plan builders behind `bulk-update`, `dedupe`, and `reassign` — every
|
|
164
|
-
emits a standard dry-run `PatchPlan` for the normal approve → apply chain:
|
|
164
|
+
Plan builders behind `route`, `bulk-update`, `dedupe`, and `reassign` — every
|
|
165
|
+
one emits a standard dry-run `PatchPlan` for the normal approve → apply chain:
|
|
165
166
|
|
|
166
167
|
- `buildBulkUpdatePlan(snapshot, options: BulkUpdateOptions)` with
|
|
167
168
|
`parseWhere` (filter expressions: `=`, `!=`, `~`, `!~`, `:empty`,
|
|
@@ -179,6 +180,22 @@ emits a standard dry-run `PatchPlan` for the normal approve → apply chain:
|
|
|
179
180
|
`assignUnowned` (CLI `--assign-unowned`) it targets ownerless records
|
|
180
181
|
(`ownerId:empty`) and claims them for `--to` — the backfill twin of
|
|
181
182
|
`enrich acquire`'s create-time assignment, for clearing existing ownerless debt.
|
|
183
|
+
- `buildLeadRoutePlan(snapshot, options: LeadRouteOptions)` — matches contacts
|
|
184
|
+
to accounts by email domain and optional company-name fallback, skips free
|
|
185
|
+
email domains and ambiguous matches, then emits approval-gated account-link
|
|
186
|
+
and owner-assignment operations. Owner routing can inherit active account
|
|
187
|
+
owners for ownerless contacts or use the shared deterministic
|
|
188
|
+
`AssignmentPolicy`; existing owners are preserved unless
|
|
189
|
+
`reassignExistingOwner` (CLI `--reassign-owned`) is set.
|
|
190
|
+
|
|
191
|
+
Read-only prevention reports:
|
|
192
|
+
|
|
193
|
+
- `buildAccountHierarchy(snapshot)` / `accountHierarchyToMarkdown(report)` —
|
|
194
|
+
provider parent hints plus subdomain inference, with duplicate-domain,
|
|
195
|
+
ambiguous-parent, and parent-cycle conflicts surfaced rather than written.
|
|
196
|
+
- `buildRelationshipMap(snapshot, { accountId | domain })` /
|
|
197
|
+
`relationshipMapToMarkdown(map)` — account stakeholders, inferred roles,
|
|
198
|
+
activity sentiment evidence, open deals, and missing-role gaps.
|
|
182
199
|
|
|
183
200
|
`fix` is CLI-only composition of existing surfaces (audit → suggest →
|
|
184
201
|
approve → apply for one rule).
|
package/docs/architecture.md
CHANGED
|
@@ -57,9 +57,12 @@ provider ──fetchSnapshot()──► CanonicalGtmSnapshot
|
|
|
57
57
|
- `auditLog.ts` — hash-chained, signed export of every apply run.
|
|
58
58
|
|
|
59
59
|
**Governed write verbs (each builds a plan; never writes directly)**
|
|
60
|
-
- `bulkUpdate.ts`, `dedupe.ts`, `reassign.ts` — filtered
|
|
61
|
-
|
|
60
|
+
- `bulkUpdate.ts`, `dedupe.ts`, `reassign.ts`, `route.ts` — filtered,
|
|
61
|
+
duplicate, ownership handoff, and lead-to-account/owner routing plan builders.
|
|
62
|
+
`merge.ts` — snapshot diff/merge + entity resolution.
|
|
62
63
|
- `resolve.ts` — the create-gate (exists / ambiguous / safe_to_create).
|
|
64
|
+
- `hierarchy.ts`, `relationships.ts` — read-only account hierarchy and
|
|
65
|
+
stakeholder relationship-map reports for planning/prevention workflows.
|
|
63
66
|
|
|
64
67
|
**Connectors** (`connectors/`)
|
|
65
68
|
- `hubspot.ts`, `salesforce.ts`, `stripe.ts` + their `*Auth.ts` OAuth/device
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
|
|
@@ -94,7 +94,7 @@ Tools over stdio: `fullstackgtm_audit` (read-only), `fullstackgtm_rules`,
|
|
|
94
94
|
This CLI ships governed **primitives** — there is no `outbound` mega-command.
|
|
95
95
|
**You (the agent) are the orchestrator:** chain these verbs into the play the
|
|
96
96
|
operator wants, surface the one approve gate, and bridge the last mile to the
|
|
97
|
-
sender (**the package never sends**). [docs/recipes.md](https://github.com/fullstackgtm/core/blob/main/
|
|
97
|
+
sender (**the package never sends**). [docs/recipes.md](https://github.com/fullstackgtm/core/blob/main/docs/recipes.md)
|
|
98
98
|
has five worked plays — cold-start lead-fill, the trigger→judge→draft outbound
|
|
99
99
|
loop, scheduled-continuous, ABM-from-companies, and hygiene-gated outbound.
|
|
100
100
|
`fullstackgtm init` scaffolds a workspace (icp.json + enrich config + a PLAYBOOK
|
|
@@ -102,7 +102,7 @@ pointing at those recipes) to start from.
|
|
|
102
102
|
|
|
103
103
|
## Going deeper
|
|
104
104
|
|
|
105
|
-
- [docs/recipes.md](https://github.com/fullstackgtm/core/blob/main/
|
|
105
|
+
- [docs/recipes.md](https://github.com/fullstackgtm/core/blob/main/docs/recipes.md) — five composable GTM plays over the primitives (cold-start, outbound loop, scheduled, ABM, hygiene-gated)
|
|
106
106
|
- [llms.txt](https://github.com/fullstackgtm/core/blob/main/llms.txt) — the full invariant map per layer (calls, market, write verbs, enrich, schedule, engagement/health)
|
|
107
107
|
- [INSTALL_FOR_AGENTS.md](https://github.com/fullstackgtm/core/blob/main/INSTALL_FOR_AGENTS.md) — deterministic install-and-verify with expected outputs
|
|
108
108
|
- [docs/api.md](https://github.com/fullstackgtm/core/blob/main/docs/api.md) — semver-covered surfaces: canonical model, rule interface, plan/apply contract, connectors, config, CLI, MCP
|
package/src/audit.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
PipelineFindingType,
|
|
10
10
|
PatchOperation,
|
|
11
11
|
PatchPlan,
|
|
12
|
+
PatchPlanAssumption,
|
|
12
13
|
SourceFreshness,
|
|
13
14
|
} from "./types.ts";
|
|
14
15
|
|
|
@@ -53,6 +54,7 @@ export function auditSnapshot(
|
|
|
53
54
|
const context = { snapshot, policy, index: buildSnapshotIndex(snapshot) };
|
|
54
55
|
const findings: AuditFinding[] = [];
|
|
55
56
|
const operations: PatchOperation[] = [];
|
|
57
|
+
const assumptionsById = new Map<string, PatchPlanAssumption>();
|
|
56
58
|
|
|
57
59
|
for (const rule of rules) {
|
|
58
60
|
try {
|
|
@@ -63,6 +65,9 @@ export function auditSnapshot(
|
|
|
63
65
|
const result = rule.evaluate(context);
|
|
64
66
|
findings.push(...result.findings);
|
|
65
67
|
operations.push(...result.operations);
|
|
68
|
+
for (const assumption of result.assumptions ?? []) {
|
|
69
|
+
if (!assumptionsById.has(assumption.id)) assumptionsById.set(assumption.id, assumption);
|
|
70
|
+
}
|
|
66
71
|
try {
|
|
67
72
|
onRule?.(rule.id, "done", result.findings.length);
|
|
68
73
|
} catch {
|
|
@@ -70,6 +75,7 @@ export function auditSnapshot(
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
const assumptions = Array.from(assumptionsById.values());
|
|
73
79
|
|
|
74
80
|
const evidence = buildEvidence(snapshot, findings, policy.today);
|
|
75
81
|
const pipelineFindings = buildPipelineFindings(findings, operations, evidence, policy.today);
|
|
@@ -85,6 +91,7 @@ export function auditSnapshot(
|
|
|
85
91
|
findings,
|
|
86
92
|
pipelineFindings,
|
|
87
93
|
evidence,
|
|
94
|
+
assumptions: assumptions.length > 0 ? assumptions : undefined,
|
|
88
95
|
operations,
|
|
89
96
|
};
|
|
90
97
|
}
|
package/src/backfill.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
PatchPlan,
|
|
22
22
|
} from "./types.ts";
|
|
23
23
|
import type { StripePaidInvoice } from "./connectors/stripe.ts";
|
|
24
|
+
import { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.ts";
|
|
24
25
|
import { normalizeDomain } from "./merge.ts";
|
|
25
26
|
|
|
26
27
|
// Mirrors stableHash in rules.ts (FNV-1a); duplicated to keep backfill.ts
|
|
@@ -36,22 +37,6 @@ function fnv1a(value: string): string {
|
|
|
36
37
|
|
|
37
38
|
export const DEFAULT_BACKFILL_MATCH_PROPERTY = "stripe_invoice_id";
|
|
38
39
|
|
|
39
|
-
// Freemail domains never become a company domain: stamping "gmail.com" on an
|
|
40
|
-
// account (or resolving a company BY gmail.com) would collapse unrelated
|
|
41
|
-
// customers onto one record. Mirrors FREE_MAIL in connectors/signalSources.ts
|
|
42
|
-
// (duplicated to keep backfill.ts importable without connector code, the
|
|
43
|
-
// fnv1a precedent above).
|
|
44
|
-
const FREE_MAIL = new Set([
|
|
45
|
-
"gmail.com",
|
|
46
|
-
"yahoo.com",
|
|
47
|
-
"hotmail.com",
|
|
48
|
-
"outlook.com",
|
|
49
|
-
"icloud.com",
|
|
50
|
-
"aol.com",
|
|
51
|
-
"proton.me",
|
|
52
|
-
"protonmail.com",
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
40
|
export type StripeBackfillOptions = {
|
|
56
41
|
/** Pipeline id or case-insensitive label; absent = the portal's default pipeline. */
|
|
57
42
|
pipeline?: string;
|
|
@@ -183,7 +168,7 @@ export function buildStripeBackfillPlan(
|
|
|
183
168
|
let companyDomain = normalizeDomain(account?.domain);
|
|
184
169
|
let accountIsNew = false;
|
|
185
170
|
if (!account && createMissingAccounts) {
|
|
186
|
-
const usableDomain = domain && !
|
|
171
|
+
const usableDomain = domain && !FREE_EMAIL_DOMAINS.has(domain) ? domain : undefined;
|
|
187
172
|
const name = invoice.customerName?.trim() || usableDomain;
|
|
188
173
|
if (name) {
|
|
189
174
|
companyName = name;
|
package/src/cli/fix.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { auditSnapshot, defaultPolicy } from "../audit.ts";
|
|
6
6
|
import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.ts";
|
|
@@ -8,6 +8,10 @@ import { applyPatchPlan } from "../connector.ts";
|
|
|
8
8
|
import { patchPlanToMarkdown } from "../format.ts";
|
|
9
9
|
import { createFilePlanStore } from "../planStore.ts";
|
|
10
10
|
import { resolveRecord, type ResolveCandidate } from "../resolve.ts";
|
|
11
|
+
import { parseAssignmentPolicy } from "../assign.ts";
|
|
12
|
+
import { buildLeadRoutePlan } from "../route.ts";
|
|
13
|
+
import { accountHierarchyToMarkdown, buildAccountHierarchy } from "../hierarchy.ts";
|
|
14
|
+
import { buildRelationshipMap, relationshipMapToMarkdown } from "../relationships.ts";
|
|
11
15
|
import { buildBulkUpdatePlan } from "../bulkUpdate.ts";
|
|
12
16
|
import { buildDedupePlan, type DedupeOptions } from "../dedupe.ts";
|
|
13
17
|
import { buildReassignPlans, type ReassignObjectType } from "../reassign.ts";
|
|
@@ -371,3 +375,66 @@ function snapshotSourceHint(args: string[]) {
|
|
|
371
375
|
if (input) return `--input ${input} `;
|
|
372
376
|
return "";
|
|
373
377
|
}
|
|
378
|
+
|
|
379
|
+
function parseJsonOrFile(value: string): unknown {
|
|
380
|
+
const candidate = resolve(process.cwd(), value);
|
|
381
|
+
const text = existsSync(candidate) ? readFileSync(candidate, "utf8") : value;
|
|
382
|
+
return JSON.parse(text);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export async function routeCommand(args: string[]) {
|
|
386
|
+
const [subcommand, ...rest] = args;
|
|
387
|
+
if (subcommand !== "leads") {
|
|
388
|
+
throw new Error("Usage: fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save] [--json] [--out <path>]");
|
|
389
|
+
}
|
|
390
|
+
const match = option(rest, "--match") ?? "domain";
|
|
391
|
+
if (!["domain", "company", "both"].includes(match)) throw new Error("--match must be domain, company, or both");
|
|
392
|
+
const policyRaw = option(rest, "--policy");
|
|
393
|
+
const snapshot = await readSnapshot(rest);
|
|
394
|
+
const result = buildLeadRoutePlan(snapshot, {
|
|
395
|
+
matchByDomain: match === "domain" || match === "both",
|
|
396
|
+
matchByCompanyName: match === "company" || match === "both",
|
|
397
|
+
inheritAccountOwner: !rest.includes("--no-inherit-owner"),
|
|
398
|
+
reassignExistingOwner: rest.includes("--reassign-owned"),
|
|
399
|
+
assignmentPolicy: policyRaw ? parseAssignmentPolicy(parseJsonOrFile(policyRaw)) : undefined,
|
|
400
|
+
maxOperations: numericOption(rest, "--max-operations"),
|
|
401
|
+
reason: option(rest, "--reason") ?? undefined,
|
|
402
|
+
});
|
|
403
|
+
if (rest.includes("--json")) {
|
|
404
|
+
const out = option(rest, "--out");
|
|
405
|
+
if (out) writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(result.plan, null, 2)}\n`);
|
|
406
|
+
if (saveRequested(rest)) await createFilePlanStore().save(result.plan);
|
|
407
|
+
console.error(`Route dry-run: ${JSON.stringify(result.counts)}`);
|
|
408
|
+
console.log(JSON.stringify({ counts: result.counts, plan: result.plan }, null, 2));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
console.error(
|
|
412
|
+
`Route dry-run: ${result.counts.linkOperations} link(s), ${result.counts.ownerOperations} owner assignment(s), ${result.counts.ambiguousAccountMatches} ambiguous match(es).`,
|
|
413
|
+
);
|
|
414
|
+
await emitPlan(result.plan, rest);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function hierarchyCommand(args: string[]) {
|
|
418
|
+
const [subcommand, ...rest] = args;
|
|
419
|
+
if (subcommand !== "report") throw new Error("Usage: fullstackgtm hierarchy report [source options] [--json|--out <path>]");
|
|
420
|
+
const snapshot = await readSnapshot(rest);
|
|
421
|
+
const report = buildAccountHierarchy(snapshot);
|
|
422
|
+
const out = option(rest, "--out");
|
|
423
|
+
const rendered = rest.includes("--json") ? `${JSON.stringify(report, null, 2)}\n` : `${accountHierarchyToMarkdown(report)}\n`;
|
|
424
|
+
if (out) writeFileSync(resolve(process.cwd(), out), rendered);
|
|
425
|
+
console.log(rendered.trimEnd());
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export async function relationshipsCommand(args: string[]) {
|
|
429
|
+
const [subcommand, ...rest] = args;
|
|
430
|
+
if (subcommand !== "account") throw new Error("Usage: fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]");
|
|
431
|
+
const accountId = option(rest, "--account-id") ?? undefined;
|
|
432
|
+
const domain = option(rest, "--domain") ?? undefined;
|
|
433
|
+
if (!accountId && !domain) throw new Error("relationships account needs --account-id <id> or --domain <domain>");
|
|
434
|
+
const snapshot = await readSnapshot(rest);
|
|
435
|
+
const map = buildRelationshipMap(snapshot, { accountId, domain });
|
|
436
|
+
const out = option(rest, "--out");
|
|
437
|
+
const rendered = rest.includes("--json") ? `${JSON.stringify(map, null, 2)}\n` : `${relationshipMapToMarkdown(map)}\n`;
|
|
438
|
+
if (out) writeFileSync(resolve(process.cwd(), out), rendered);
|
|
439
|
+
console.log(rendered.trimEnd());
|
|
440
|
+
}
|