fullstackgtm 0.10.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 (77) hide show
  1. package/CHANGELOG.md +381 -0
  2. package/INSTALL_FOR_AGENTS.md +87 -0
  3. package/LICENSE +202 -0
  4. package/README.md +230 -0
  5. package/dist/audit.d.ts +7 -0
  6. package/dist/audit.js +202 -0
  7. package/dist/bin.d.ts +2 -0
  8. package/dist/bin.js +6 -0
  9. package/dist/cli.d.ts +38 -0
  10. package/dist/cli.js +915 -0
  11. package/dist/config.d.ts +36 -0
  12. package/dist/config.js +85 -0
  13. package/dist/connector.d.ts +30 -0
  14. package/dist/connector.js +94 -0
  15. package/dist/connectors/hubspot.d.ts +20 -0
  16. package/dist/connectors/hubspot.js +409 -0
  17. package/dist/connectors/hubspotAuth.d.ts +42 -0
  18. package/dist/connectors/hubspotAuth.js +189 -0
  19. package/dist/connectors/salesforce.d.ts +26 -0
  20. package/dist/connectors/salesforce.js +318 -0
  21. package/dist/connectors/salesforceAuth.d.ts +44 -0
  22. package/dist/connectors/salesforceAuth.js +120 -0
  23. package/dist/connectors/stripe.d.ts +27 -0
  24. package/dist/connectors/stripe.js +176 -0
  25. package/dist/credentials.d.ts +75 -0
  26. package/dist/credentials.js +197 -0
  27. package/dist/demo.d.ts +20 -0
  28. package/dist/demo.js +169 -0
  29. package/dist/diff.d.ts +46 -0
  30. package/dist/diff.js +107 -0
  31. package/dist/format.d.ts +3 -0
  32. package/dist/format.js +109 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.js +17 -0
  35. package/dist/mappings.d.ts +8 -0
  36. package/dist/mappings.js +123 -0
  37. package/dist/mcp-bin.d.ts +2 -0
  38. package/dist/mcp-bin.js +33 -0
  39. package/dist/mcp.d.ts +1 -0
  40. package/dist/mcp.js +140 -0
  41. package/dist/merge.d.ts +48 -0
  42. package/dist/merge.js +145 -0
  43. package/dist/planStore.d.ts +31 -0
  44. package/dist/planStore.js +116 -0
  45. package/dist/rules.d.ts +24 -0
  46. package/dist/rules.js +512 -0
  47. package/dist/sampleData.d.ts +2 -0
  48. package/dist/sampleData.js +115 -0
  49. package/dist/types.d.ts +294 -0
  50. package/dist/types.js +8 -0
  51. package/docs/api.md +72 -0
  52. package/docs/roadmap-to-1.0.md +121 -0
  53. package/llms.txt +25 -0
  54. package/package.json +76 -0
  55. package/src/audit.ts +242 -0
  56. package/src/bin.ts +7 -0
  57. package/src/cli.ts +1042 -0
  58. package/src/config.ts +113 -0
  59. package/src/connector.ts +140 -0
  60. package/src/connectors/hubspot.ts +528 -0
  61. package/src/connectors/hubspotAuth.ts +246 -0
  62. package/src/connectors/salesforce.ts +420 -0
  63. package/src/connectors/salesforceAuth.ts +167 -0
  64. package/src/connectors/stripe.ts +215 -0
  65. package/src/credentials.ts +282 -0
  66. package/src/demo.ts +200 -0
  67. package/src/diff.ts +158 -0
  68. package/src/format.ts +162 -0
  69. package/src/index.ts +129 -0
  70. package/src/mappings.ts +157 -0
  71. package/src/mcp-bin.ts +32 -0
  72. package/src/mcp.ts +185 -0
  73. package/src/merge.ts +235 -0
  74. package/src/planStore.ts +155 -0
  75. package/src/rules.ts +539 -0
  76. package/src/sampleData.ts +117 -0
  77. package/src/types.ts +372 -0
package/src/audit.ts ADDED
@@ -0,0 +1,242 @@
1
+ import { buildSnapshotIndex, builtinAuditRules, stableHash } from "./rules.ts";
2
+ import type {
3
+ AuditFinding,
4
+ CanonicalGtmSnapshot,
5
+ GtmAuditRule,
6
+ GtmEvidence,
7
+ GtmPolicy,
8
+ PipelineFinding,
9
+ PipelineFindingType,
10
+ PatchOperation,
11
+ PatchPlan,
12
+ SourceFreshness,
13
+ } from "./types.ts";
14
+
15
+ const DEFAULT_POLICY: Omit<GtmPolicy, "today"> = {
16
+ staleDealDays: 30,
17
+ requireDealOwner: true,
18
+ requireAccountForDeal: true,
19
+ };
20
+
21
+ export function defaultPolicy(today = new Date().toISOString().slice(0, 10)): GtmPolicy {
22
+ return {
23
+ ...DEFAULT_POLICY,
24
+ today,
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Run every rule over the snapshot and collect the results into a single
30
+ * dry-run patch plan. Pass custom rules to extend or replace the built-ins.
31
+ */
32
+ export function auditSnapshot(
33
+ snapshot: CanonicalGtmSnapshot,
34
+ policy: GtmPolicy = defaultPolicy(),
35
+ rules: GtmAuditRule[] = builtinAuditRules,
36
+ ): PatchPlan {
37
+ const context = { snapshot, policy, index: buildSnapshotIndex(snapshot) };
38
+ const findings: AuditFinding[] = [];
39
+ const operations: PatchOperation[] = [];
40
+
41
+ for (const rule of rules) {
42
+ const result = rule.evaluate(context);
43
+ findings.push(...result.findings);
44
+ operations.push(...result.operations);
45
+ }
46
+
47
+
48
+ const evidence = buildEvidence(snapshot, findings, policy.today);
49
+ const pipelineFindings = buildPipelineFindings(findings, operations, evidence, policy.today);
50
+ linkOperationContext(operations, findings, evidence);
51
+
52
+ return {
53
+ id: `patch_plan_${stableHash(`${snapshot.provider}:${snapshot.generatedAt}:${findings.length}:${operations.length}`)}`,
54
+ title: "GTM hygiene audit patch plan",
55
+ createdAt: new Date().toISOString(),
56
+ status: operations.length > 0 ? "needs_approval" : "draft",
57
+ dryRun: true,
58
+ summary: `${findings.length} findings and ${operations.length} proposed dry-run operations.`,
59
+ findings,
60
+ pipelineFindings,
61
+ evidence,
62
+ operations,
63
+ };
64
+ }
65
+
66
+ function daysBetween(start: string, end: string) {
67
+ const startMs = Date.parse(start);
68
+ const endMs = Date.parse(end);
69
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return 0;
70
+ return Math.floor((endMs - startMs) / 86_400_000);
71
+ }
72
+
73
+ function buildEvidence(
74
+ snapshot: CanonicalGtmSnapshot,
75
+ findings: AuditFinding[],
76
+ today: string,
77
+ ): GtmEvidence[] {
78
+ const recordsByKey = buildRecordIndex(snapshot);
79
+ return findings.map((finding) => {
80
+ const source = recordsByKey.get(`${finding.objectType}:${finding.objectId}`);
81
+ const provider = source && "provider" in source && typeof source.provider === "string"
82
+ ? source.provider
83
+ : snapshot.provider;
84
+ const lastSyncAt = source && "lastSyncAt" in source && typeof source.lastSyncAt === "string"
85
+ ? source.lastSyncAt
86
+ : undefined;
87
+ const lastActivityAt = source && "lastActivityAt" in source && typeof source.lastActivityAt === "string"
88
+ ? source.lastActivityAt
89
+ : undefined;
90
+ const sourceObjectId = source && "crmId" in source && typeof source.crmId === "string"
91
+ ? source.crmId
92
+ : finding.objectId;
93
+
94
+ return {
95
+ id: evidenceId(finding.ruleId, finding.objectId),
96
+ sourceSystem: sourceSystem(provider),
97
+ sourceObjectType: finding.objectType,
98
+ sourceObjectId,
99
+ objectType: finding.objectType,
100
+ objectId: finding.objectId,
101
+ title: finding.title,
102
+ text: evidenceText(finding, source),
103
+ observedAt: lastActivityAt ?? lastSyncAt,
104
+ capturedAt: snapshot.generatedAt,
105
+ freshness: freshness(lastSyncAt ?? lastActivityAt, today),
106
+ metadata: {
107
+ ruleId: finding.ruleId,
108
+ generatedAt: snapshot.generatedAt,
109
+ },
110
+ };
111
+ });
112
+ }
113
+
114
+ function buildPipelineFindings(
115
+ findings: AuditFinding[],
116
+ operations: PatchOperation[],
117
+ evidence: GtmEvidence[],
118
+ today: string,
119
+ ): PipelineFinding[] {
120
+ const operationsById = new Map(operations.map((operation) => [operation.id, operation]));
121
+ const evidenceById = new Map(evidence.map((item) => [item.id, item]));
122
+ return findings
123
+ .map((finding): PipelineFinding | null => {
124
+ const type = finding.type ?? findingType(finding.ruleId);
125
+ if (!type) return null;
126
+ const operation = operationsById.get(operationIdFromFindingId(finding.id));
127
+ const evidenceItem = evidenceById.get(evidenceIdFromFindingId(finding.id));
128
+ const evidenceIds = evidenceItem ? [evidenceItem.id] : [];
129
+ return {
130
+ id: finding.id,
131
+ type,
132
+ objectType: finding.objectType,
133
+ objectId: finding.objectId,
134
+ severity: finding.severity,
135
+ status: operation ? "planned" : "open",
136
+ title: finding.title,
137
+ summary: finding.summary,
138
+ recommendation: finding.recommendation,
139
+ evidenceIds,
140
+ currentCrmValue: finding.currentCrmValue ?? operation?.beforeValue,
141
+ proposedValue: finding.proposedValue ?? operation?.afterValue,
142
+ freshness: finding.freshness ?? evidenceItem?.freshness ?? freshness(undefined, today),
143
+ patchEligibility: {
144
+ eligible: Boolean(operation),
145
+ operation: operation?.operation ?? "none",
146
+ field: operation?.field,
147
+ reason: operation
148
+ ? "A typed patch operation can be reviewed before writeback."
149
+ : "No supported patch operation exists yet.",
150
+ approvalRequired: operation?.approvalRequired ?? true,
151
+ },
152
+ };
153
+ })
154
+ .filter((finding): finding is PipelineFinding => finding !== null);
155
+ }
156
+
157
+ function linkOperationContext(
158
+ operations: PatchOperation[],
159
+ findings: AuditFinding[],
160
+ evidence: GtmEvidence[],
161
+ ) {
162
+ const findingIdsByOperationId = new Map<string, string[]>();
163
+ for (const finding of findings) {
164
+ const operationIdForFinding = operationIdFromFindingId(finding.id);
165
+ const ids = findingIdsByOperationId.get(operationIdForFinding) ?? [];
166
+ ids.push(finding.id);
167
+ findingIdsByOperationId.set(operationIdForFinding, ids);
168
+ }
169
+ const evidenceIdSet = new Set(evidence.map((item) => item.id));
170
+ for (const operation of operations) {
171
+ operation.findingIds = findingIdsByOperationId.get(operation.id) ?? [];
172
+ operation.evidenceIds = operation.findingIds
173
+ .map((findingId) => evidenceIdFromFindingId(findingId))
174
+ .filter((id) => evidenceIdSet.has(id));
175
+ operation.verification = {
176
+ status: "not_started",
177
+ expectedValue: operation.afterValue,
178
+ auditText: "Dry-run audit only; verification starts after an approved provider/local write.",
179
+ };
180
+ }
181
+ }
182
+
183
+ function operationIdFromFindingId(findingId: string) {
184
+ return findingId.replace(/^finding_/, "op_");
185
+ }
186
+
187
+ function evidenceIdFromFindingId(findingId: string) {
188
+ return findingId.replace(/^finding_/, "ev_");
189
+ }
190
+
191
+ function buildRecordIndex(snapshot: CanonicalGtmSnapshot) {
192
+ const recordsByKey = new Map<string, Record<string, unknown>>();
193
+ for (const row of snapshot.accounts) recordsByKey.set(`account:${row.id}`, row);
194
+ for (const row of snapshot.contacts) recordsByKey.set(`contact:${row.id}`, row);
195
+ for (const row of snapshot.deals) recordsByKey.set(`deal:${row.id}`, row);
196
+ for (const row of snapshot.users) recordsByKey.set(`user:${row.id}`, row);
197
+ for (const row of snapshot.activities) recordsByKey.set(`activity:${row.id}`, row);
198
+ return recordsByKey;
199
+ }
200
+
201
+ function evidenceText(finding: AuditFinding, source: unknown) {
202
+ const name = source && typeof source === "object" && "name" in source ? String(source.name) : finding.objectId;
203
+ return `${finding.ruleId}: ${name}. ${finding.summary}`;
204
+ }
205
+
206
+ function sourceSystem(value: string) {
207
+ if (
208
+ value === "salesforce" ||
209
+ value === "hubspot" ||
210
+ value === "gong" ||
211
+ value === "chorus" ||
212
+ value === "fathom" ||
213
+ value === "manual" ||
214
+ value === "csv" ||
215
+ value === "mock"
216
+ ) {
217
+ return value;
218
+ }
219
+ return "unknown";
220
+ }
221
+
222
+ function freshness(sourceUpdatedAt: string | undefined, today: string): SourceFreshness {
223
+ const ageDays = sourceUpdatedAt ? daysBetween(sourceUpdatedAt, today) : undefined;
224
+ return {
225
+ state: ageDays === undefined ? "unknown" : ageDays > 30 ? "stale" : "fresh",
226
+ checkedAt: today,
227
+ sourceUpdatedAt,
228
+ ageDays,
229
+ };
230
+ }
231
+
232
+ function findingType(ruleId: string): PipelineFindingType | undefined {
233
+ if (ruleId === "missing-deal-owner") return "deal_missing_owner";
234
+ if (ruleId === "missing-deal-account") return "deal_missing_account";
235
+ if (ruleId === "past-close-date") return "deal_past_close_date";
236
+ if (ruleId === "stale-deal") return "deal_stale_activity";
237
+ return undefined;
238
+ }
239
+
240
+ function evidenceId(ruleId: string, objectId: string) {
241
+ return `ev_${stableHash(`${ruleId}:${objectId}`)}`;
242
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./cli.ts";
3
+
4
+ runCli(process.argv.slice(2)).catch((error) => {
5
+ console.error(error instanceof Error ? error.message : String(error));
6
+ process.exitCode = 1;
7
+ });