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.
- package/CHANGELOG.md +381 -0
- package/INSTALL_FOR_AGENTS.md +87 -0
- package/LICENSE +202 -0
- package/README.md +230 -0
- package/dist/audit.d.ts +7 -0
- package/dist/audit.js +202 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +6 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +915 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +85 -0
- package/dist/connector.d.ts +30 -0
- package/dist/connector.js +94 -0
- package/dist/connectors/hubspot.d.ts +20 -0
- package/dist/connectors/hubspot.js +409 -0
- package/dist/connectors/hubspotAuth.d.ts +42 -0
- package/dist/connectors/hubspotAuth.js +189 -0
- package/dist/connectors/salesforce.d.ts +26 -0
- package/dist/connectors/salesforce.js +318 -0
- package/dist/connectors/salesforceAuth.d.ts +44 -0
- package/dist/connectors/salesforceAuth.js +120 -0
- package/dist/connectors/stripe.d.ts +27 -0
- package/dist/connectors/stripe.js +176 -0
- package/dist/credentials.d.ts +75 -0
- package/dist/credentials.js +197 -0
- package/dist/demo.d.ts +20 -0
- package/dist/demo.js +169 -0
- package/dist/diff.d.ts +46 -0
- package/dist/diff.js +107 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.js +109 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/mappings.d.ts +8 -0
- package/dist/mappings.js +123 -0
- package/dist/mcp-bin.d.ts +2 -0
- package/dist/mcp-bin.js +33 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +140 -0
- package/dist/merge.d.ts +48 -0
- package/dist/merge.js +145 -0
- package/dist/planStore.d.ts +31 -0
- package/dist/planStore.js +116 -0
- package/dist/rules.d.ts +24 -0
- package/dist/rules.js +512 -0
- package/dist/sampleData.d.ts +2 -0
- package/dist/sampleData.js +115 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +8 -0
- package/docs/api.md +72 -0
- package/docs/roadmap-to-1.0.md +121 -0
- package/llms.txt +25 -0
- package/package.json +76 -0
- package/src/audit.ts +242 -0
- package/src/bin.ts +7 -0
- package/src/cli.ts +1042 -0
- package/src/config.ts +113 -0
- package/src/connector.ts +140 -0
- package/src/connectors/hubspot.ts +528 -0
- package/src/connectors/hubspotAuth.ts +246 -0
- package/src/connectors/salesforce.ts +420 -0
- package/src/connectors/salesforceAuth.ts +167 -0
- package/src/connectors/stripe.ts +215 -0
- package/src/credentials.ts +282 -0
- package/src/demo.ts +200 -0
- package/src/diff.ts +158 -0
- package/src/format.ts +162 -0
- package/src/index.ts +129 -0
- package/src/mappings.ts +157 -0
- package/src/mcp-bin.ts +32 -0
- package/src/mcp.ts +185 -0
- package/src/merge.ts +235 -0
- package/src/planStore.ts +155 -0
- package/src/rules.ts +539 -0
- package/src/sampleData.ts +117 -0
- package/src/types.ts +372 -0
package/src/demo.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CanonicalAccount,
|
|
3
|
+
CanonicalActivity,
|
|
4
|
+
CanonicalContact,
|
|
5
|
+
CanonicalDeal,
|
|
6
|
+
CanonicalGtmSnapshot,
|
|
7
|
+
CanonicalUser,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export type DemoSnapshotOptions = {
|
|
11
|
+
/** PRNG seed; the same seed always produces the same snapshot. */
|
|
12
|
+
seed?: number;
|
|
13
|
+
/** Anchor date (YYYY-MM-DD) all relative dates derive from. */
|
|
14
|
+
today?: string;
|
|
15
|
+
accounts?: number;
|
|
16
|
+
deals?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FIRST_NAMES = [
|
|
20
|
+
"Ava", "Marcus", "Priya", "Diego", "Sofia", "Jordan",
|
|
21
|
+
"Mei", "Tomás", "Nia", "Ethan", "Lena", "Omar",
|
|
22
|
+
];
|
|
23
|
+
const LAST_NAMES = [
|
|
24
|
+
"Calloway", "Reyes", "Iyer", "Novak", "Bennett", "Okafor",
|
|
25
|
+
"Lindqvist", "Moreau", "Tanaka", "Whitfield", "Drummond", "Vargas",
|
|
26
|
+
];
|
|
27
|
+
const COMPANY_HEADS = [
|
|
28
|
+
"Halcyon", "Northwind", "Cobalt", "Ridgeline", "Lumen", "Vantage",
|
|
29
|
+
"Harbor", "Atlas", "Crestline", "Meridian", "Bluff", "Juniper",
|
|
30
|
+
"Granite", "Summit", "Beacon",
|
|
31
|
+
];
|
|
32
|
+
const COMPANY_TAILS = [
|
|
33
|
+
"Analytics", "Logistics", "Biotech", "Robotics", "Software",
|
|
34
|
+
"Manufacturing", "Health", "Financial", "Media", "Systems",
|
|
35
|
+
"Foods", "Energy", "Labs", "Dynamics",
|
|
36
|
+
];
|
|
37
|
+
const INDUSTRIES = [
|
|
38
|
+
"SaaS", "Logistics", "Healthcare", "Manufacturing", "Financial Services",
|
|
39
|
+
"Media", "Energy", "Retail",
|
|
40
|
+
];
|
|
41
|
+
const OPEN_STAGES = ["discovery", "qualification", "proposal", "negotiation"];
|
|
42
|
+
const DEAL_LABELS = [
|
|
43
|
+
"Platform Subscription", "Enterprise Rollout", "Pilot Expansion",
|
|
44
|
+
"Renewal", "Multi-Year Agreement", "Team Upgrade", "Add-On Seats",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Deterministic PRNG (mulberry32) so demo data is stable across runs. */
|
|
48
|
+
function mulberry32(seed: number) {
|
|
49
|
+
let state = seed >>> 0;
|
|
50
|
+
return () => {
|
|
51
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
52
|
+
let t = state;
|
|
53
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
54
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
55
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shiftDate(today: string, days: number): string {
|
|
60
|
+
const base = Date.parse(`${today}T00:00:00Z`);
|
|
61
|
+
return new Date(base + days * 86_400_000).toISOString().slice(0, 10);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a realistic, deliberately messy mid-market CRM snapshot.
|
|
66
|
+
*
|
|
67
|
+
* The mess is injected at fixed indices so audits over a given seed produce
|
|
68
|
+
* stable, testable findings, while the PRNG varies names, amounts, and dates:
|
|
69
|
+
* - every 8th account is an orphan (no contacts, no deals)
|
|
70
|
+
* - every 9th deal references a departed owner that no longer exists
|
|
71
|
+
* - every 11th deal lost its account association
|
|
72
|
+
* - open deals carry a realistic spread of past close dates and stale activity
|
|
73
|
+
*/
|
|
74
|
+
export function generateDemoSnapshot(options: DemoSnapshotOptions = {}): CanonicalGtmSnapshot {
|
|
75
|
+
const seed = options.seed ?? 7;
|
|
76
|
+
const today = options.today ?? "2026-06-09";
|
|
77
|
+
const accountCount = options.accounts ?? 56;
|
|
78
|
+
const dealCount = options.deals ?? 80;
|
|
79
|
+
const random = mulberry32(seed);
|
|
80
|
+
const pick = <T,>(items: T[]) => items[Math.floor(random() * items.length)];
|
|
81
|
+
const between = (min: number, max: number) => min + Math.floor(random() * (max - min + 1));
|
|
82
|
+
|
|
83
|
+
const users: CanonicalUser[] = FIRST_NAMES.map((firstName, index) => ({
|
|
84
|
+
id: `user_${String(index + 1).padStart(2, "0")}`,
|
|
85
|
+
provider: "mock",
|
|
86
|
+
crmId: `${9000 + index}`,
|
|
87
|
+
name: `${firstName} ${LAST_NAMES[index]}`,
|
|
88
|
+
email: `${firstName.toLowerCase()}.${LAST_NAMES[index].toLowerCase()}@example.com`,
|
|
89
|
+
title: index === 0 ? "VP Sales" : index === 1 ? "Sales Manager" : "Account Executive",
|
|
90
|
+
active: index < 10,
|
|
91
|
+
}));
|
|
92
|
+
const activeReps = users.slice(2, 10);
|
|
93
|
+
|
|
94
|
+
const accounts: CanonicalAccount[] = [];
|
|
95
|
+
for (let index = 0; index < accountCount; index += 1) {
|
|
96
|
+
const name = `${COMPANY_HEADS[index % COMPANY_HEADS.length]} ${pick(COMPANY_TAILS)}`;
|
|
97
|
+
accounts.push({
|
|
98
|
+
id: `acct_${String(index + 1).padStart(3, "0")}`,
|
|
99
|
+
provider: "mock",
|
|
100
|
+
crmId: `${10_000 + index}`,
|
|
101
|
+
name,
|
|
102
|
+
domain: `${name.toLowerCase().replace(/[^a-z]/g, "")}.com`,
|
|
103
|
+
industry: pick(INDUSTRIES),
|
|
104
|
+
ownerId: random() < 0.85 ? pick(activeReps).id : undefined,
|
|
105
|
+
employeeCount: between(40, 4000),
|
|
106
|
+
annualRevenue: between(2, 400) * 1_000_000,
|
|
107
|
+
lastSyncAt: shiftDate(today, -between(0, 3)),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const orphanAccountIds = new Set(
|
|
111
|
+
accounts.filter((_, index) => index % 8 === 0).map((account) => account.id),
|
|
112
|
+
);
|
|
113
|
+
const linkableAccounts = accounts.filter((account) => !orphanAccountIds.has(account.id));
|
|
114
|
+
|
|
115
|
+
const contacts: CanonicalContact[] = [];
|
|
116
|
+
for (const account of linkableAccounts) {
|
|
117
|
+
const count = between(1, 3);
|
|
118
|
+
for (let index = 0; index < count; index += 1) {
|
|
119
|
+
const firstName = pick(FIRST_NAMES);
|
|
120
|
+
const lastName = pick(LAST_NAMES);
|
|
121
|
+
contacts.push({
|
|
122
|
+
id: `contact_${String(contacts.length + 1).padStart(3, "0")}`,
|
|
123
|
+
provider: "mock",
|
|
124
|
+
crmId: `${20_000 + contacts.length}`,
|
|
125
|
+
accountId: account.id,
|
|
126
|
+
firstName,
|
|
127
|
+
lastName,
|
|
128
|
+
email: `${firstName.toLowerCase()}@${account.domain}`,
|
|
129
|
+
title: pick(["CTO", "VP Operations", "Head of RevOps", "Director of IT", "CFO"]),
|
|
130
|
+
ownerId: account.ownerId,
|
|
131
|
+
lastSyncAt: account.lastSyncAt,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const deals: CanonicalDeal[] = [];
|
|
137
|
+
const activities: CanonicalActivity[] = [];
|
|
138
|
+
for (let index = 0; index < dealCount; index += 1) {
|
|
139
|
+
const account = linkableAccounts[index % linkableAccounts.length];
|
|
140
|
+
const departedOwner = index % 9 === 0;
|
|
141
|
+
const unlinked = index % 11 === 0;
|
|
142
|
+
const stageRoll = random();
|
|
143
|
+
const stage =
|
|
144
|
+
stageRoll < 0.7 ? pick(OPEN_STAGES) : stageRoll < 0.88 ? "closedwon" : "closedlost";
|
|
145
|
+
const isWon = stage === "closedwon";
|
|
146
|
+
const isClosed = isWon || stage === "closedlost";
|
|
147
|
+
const closeDate = isClosed
|
|
148
|
+
? shiftDate(today, -between(5, 120))
|
|
149
|
+
: shiftDate(today, between(-60, 150));
|
|
150
|
+
const daysSinceActivity = isClosed ? between(5, 60) : between(0, 75);
|
|
151
|
+
const lastActivityAt = shiftDate(today, -daysSinceActivity);
|
|
152
|
+
|
|
153
|
+
const deal: CanonicalDeal = {
|
|
154
|
+
id: `deal_${String(index + 1).padStart(3, "0")}`,
|
|
155
|
+
provider: "mock",
|
|
156
|
+
crmId: `${30_000 + index}`,
|
|
157
|
+
accountId: unlinked ? undefined : account.id,
|
|
158
|
+
ownerId: departedOwner ? `user_departed_${index % 2}` : (account.ownerId ?? pick(activeReps).id),
|
|
159
|
+
name: `${account.name} — ${pick(DEAL_LABELS)}`,
|
|
160
|
+
amount: between(8, 480) * 500,
|
|
161
|
+
currency: "USD",
|
|
162
|
+
stage,
|
|
163
|
+
closeDate,
|
|
164
|
+
forecastCategory: isWon ? "closed_won" : isClosed ? "closed_lost" : "pipeline",
|
|
165
|
+
probability: isClosed ? (isWon ? 1 : 0) : between(10, 80) / 100,
|
|
166
|
+
isClosed,
|
|
167
|
+
isWon,
|
|
168
|
+
lastActivityAt,
|
|
169
|
+
lastSyncAt: shiftDate(today, -between(0, 2)),
|
|
170
|
+
};
|
|
171
|
+
deals.push(deal);
|
|
172
|
+
|
|
173
|
+
const activityCount = Math.max(1, 3 - Math.floor(daysSinceActivity / 25));
|
|
174
|
+
for (let activityIndex = 0; activityIndex < activityCount; activityIndex += 1) {
|
|
175
|
+
activities.push({
|
|
176
|
+
id: `activity_${String(activities.length + 1).padStart(4, "0")}`,
|
|
177
|
+
provider: "mock",
|
|
178
|
+
dealId: deal.id,
|
|
179
|
+
accountId: deal.accountId,
|
|
180
|
+
ownerId: deal.ownerId,
|
|
181
|
+
type: pick(["call", "email", "meeting"] as const),
|
|
182
|
+
occurredAt: shiftDate(today, -(daysSinceActivity + activityIndex * between(3, 12))),
|
|
183
|
+
subject: pick([
|
|
184
|
+
"Discovery call", "Pricing discussion", "Security review",
|
|
185
|
+
"Champion sync", "Procurement follow-up", "Executive alignment",
|
|
186
|
+
]),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
generatedAt: `${today}T00:00:00.000Z`,
|
|
193
|
+
provider: "mock",
|
|
194
|
+
users,
|
|
195
|
+
accounts,
|
|
196
|
+
contacts,
|
|
197
|
+
deals,
|
|
198
|
+
activities,
|
|
199
|
+
};
|
|
200
|
+
}
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { AuditFinding, CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Snapshot and audit drift. Record ids and finding ids are stable across
|
|
5
|
+
* runs, so two snapshots (or two plans) can be compared directly: what
|
|
6
|
+
* appeared, what disappeared, what changed — and whether hygiene regressed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Fields that change on every sync without semantic meaning.
|
|
10
|
+
const IGNORED_FIELDS = new Set(["raw", "lastSyncAt", "identities"]);
|
|
11
|
+
|
|
12
|
+
export type FieldChange = { field: string; before: unknown; after: unknown };
|
|
13
|
+
|
|
14
|
+
export type RecordChange = { id: string; label: string; changes: FieldChange[] };
|
|
15
|
+
|
|
16
|
+
export type CollectionDiff = {
|
|
17
|
+
added: Array<{ id: string; label: string }>;
|
|
18
|
+
removed: Array<{ id: string; label: string }>;
|
|
19
|
+
changed: RecordChange[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SnapshotDiff = {
|
|
23
|
+
before: { provider: string; generatedAt: string };
|
|
24
|
+
after: { provider: string; generatedAt: string };
|
|
25
|
+
users: CollectionDiff;
|
|
26
|
+
accounts: CollectionDiff;
|
|
27
|
+
contacts: CollectionDiff;
|
|
28
|
+
deals: CollectionDiff;
|
|
29
|
+
activities: CollectionDiff;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type AnyRecord = { id: string; name?: string; email?: string };
|
|
33
|
+
|
|
34
|
+
function labelOf(record: AnyRecord): string {
|
|
35
|
+
return record.name ?? record.email ?? record.id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function diffCollection(before: AnyRecord[], after: AnyRecord[]): CollectionDiff {
|
|
39
|
+
const beforeById = new Map(before.map((record) => [record.id, record]));
|
|
40
|
+
const afterById = new Map(after.map((record) => [record.id, record]));
|
|
41
|
+
|
|
42
|
+
const added = after
|
|
43
|
+
.filter((record) => !beforeById.has(record.id))
|
|
44
|
+
.map((record) => ({ id: record.id, label: labelOf(record) }));
|
|
45
|
+
const removed = before
|
|
46
|
+
.filter((record) => !afterById.has(record.id))
|
|
47
|
+
.map((record) => ({ id: record.id, label: labelOf(record) }));
|
|
48
|
+
|
|
49
|
+
const changed: RecordChange[] = [];
|
|
50
|
+
for (const record of after) {
|
|
51
|
+
const previous = beforeById.get(record.id);
|
|
52
|
+
if (!previous) continue;
|
|
53
|
+
const fields = new Set([...Object.keys(previous), ...Object.keys(record)]);
|
|
54
|
+
const changes: FieldChange[] = [];
|
|
55
|
+
for (const field of fields) {
|
|
56
|
+
if (IGNORED_FIELDS.has(field)) continue;
|
|
57
|
+
const beforeValue = (previous as Record<string, unknown>)[field];
|
|
58
|
+
const afterValue = (record as Record<string, unknown>)[field];
|
|
59
|
+
if (JSON.stringify(beforeValue) !== JSON.stringify(afterValue)) {
|
|
60
|
+
changes.push({ field, before: beforeValue ?? null, after: afterValue ?? null });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (changes.length > 0) {
|
|
64
|
+
changed.push({ id: record.id, label: labelOf(record), changes });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { added, removed, changed };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function diffSnapshots(
|
|
72
|
+
before: CanonicalGtmSnapshot,
|
|
73
|
+
after: CanonicalGtmSnapshot,
|
|
74
|
+
): SnapshotDiff {
|
|
75
|
+
return {
|
|
76
|
+
before: { provider: before.provider, generatedAt: before.generatedAt },
|
|
77
|
+
after: { provider: after.provider, generatedAt: after.generatedAt },
|
|
78
|
+
users: diffCollection(before.users, after.users),
|
|
79
|
+
accounts: diffCollection(before.accounts, after.accounts),
|
|
80
|
+
contacts: diffCollection(before.contacts, after.contacts),
|
|
81
|
+
deals: diffCollection(before.deals, after.deals),
|
|
82
|
+
activities: diffCollection(before.activities, after.activities),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type FindingsDrift = {
|
|
87
|
+
newFindings: AuditFinding[];
|
|
88
|
+
resolvedFindings: AuditFinding[];
|
|
89
|
+
persistingCount: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Hygiene drift between two plans, keyed on stable finding ids. */
|
|
93
|
+
export function diffFindings(before: PatchPlan, after: PatchPlan): FindingsDrift {
|
|
94
|
+
const beforeIds = new Set(before.findings.map((finding) => finding.id));
|
|
95
|
+
const afterIds = new Set(after.findings.map((finding) => finding.id));
|
|
96
|
+
return {
|
|
97
|
+
newFindings: after.findings.filter((finding) => !beforeIds.has(finding.id)),
|
|
98
|
+
resolvedFindings: before.findings.filter((finding) => !afterIds.has(finding.id)),
|
|
99
|
+
persistingCount: after.findings.filter((finding) => beforeIds.has(finding.id)).length,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function diffToMarkdown(diff: SnapshotDiff, drift?: FindingsDrift): string {
|
|
104
|
+
const lines = [
|
|
105
|
+
"# Snapshot diff",
|
|
106
|
+
"",
|
|
107
|
+
`Before: ${diff.before.provider} @ ${diff.before.generatedAt}`,
|
|
108
|
+
`After: ${diff.after.provider} @ ${diff.after.generatedAt}`,
|
|
109
|
+
"",
|
|
110
|
+
"| Collection | Added | Removed | Changed |",
|
|
111
|
+
"| --- | --- | --- | --- |",
|
|
112
|
+
];
|
|
113
|
+
for (const collection of ["users", "accounts", "contacts", "deals", "activities"] as const) {
|
|
114
|
+
const entry = diff[collection];
|
|
115
|
+
lines.push(
|
|
116
|
+
`| ${collection} | ${entry.added.length} | ${entry.removed.length} | ${entry.changed.length} |`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const collection of ["users", "accounts", "contacts", "deals", "activities"] as const) {
|
|
121
|
+
const entry = diff[collection];
|
|
122
|
+
if (entry.added.length + entry.removed.length + entry.changed.length === 0) continue;
|
|
123
|
+
lines.push("", `## ${collection}`, "");
|
|
124
|
+
for (const record of entry.added) lines.push(`- added ${record.label} (${record.id})`);
|
|
125
|
+
for (const record of entry.removed) lines.push(`- removed ${record.label} (${record.id})`);
|
|
126
|
+
for (const record of entry.changed) {
|
|
127
|
+
lines.push(
|
|
128
|
+
`- changed ${record.label} (${record.id}): ${record.changes
|
|
129
|
+
.map((change) => `${change.field} ${formatValue(change.before)} → ${formatValue(change.after)}`)
|
|
130
|
+
.join("; ")}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (drift) {
|
|
136
|
+
lines.push(
|
|
137
|
+
"",
|
|
138
|
+
"## Hygiene drift",
|
|
139
|
+
"",
|
|
140
|
+
`New findings: ${drift.newFindings.length}`,
|
|
141
|
+
`Resolved findings: ${drift.resolvedFindings.length}`,
|
|
142
|
+
`Persisting findings: ${drift.persistingCount}`,
|
|
143
|
+
);
|
|
144
|
+
for (const finding of drift.newFindings) {
|
|
145
|
+
lines.push(`- NEW [${finding.ruleId}] ${finding.summary}`);
|
|
146
|
+
}
|
|
147
|
+
for (const finding of drift.resolvedFindings) {
|
|
148
|
+
lines.push(`- resolved [${finding.ruleId}] ${finding.summary}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return `${lines.join("\n")}\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatValue(value: unknown) {
|
|
156
|
+
if (value === undefined || value === null || value === "") return "∅";
|
|
157
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
158
|
+
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { PatchPlan, PatchPlanRun } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function patchPlanToMarkdown(plan: PatchPlan) {
|
|
4
|
+
const lines = [
|
|
5
|
+
`# ${plan.title}`,
|
|
6
|
+
"",
|
|
7
|
+
`Status: ${plan.status}`,
|
|
8
|
+
`Dry run: ${plan.dryRun ? "yes" : "no"}`,
|
|
9
|
+
`Summary: ${plan.summary}`,
|
|
10
|
+
"",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
if (plan.findings.length > 0) {
|
|
14
|
+
const byRule = new Map<string, { count: number; severity: string }>();
|
|
15
|
+
for (const finding of plan.findings) {
|
|
16
|
+
const entry = byRule.get(finding.ruleId) ?? { count: 0, severity: finding.severity };
|
|
17
|
+
entry.count += 1;
|
|
18
|
+
byRule.set(finding.ruleId, entry);
|
|
19
|
+
}
|
|
20
|
+
lines.push("## Findings by Rule", "", "| Rule | Findings | Severity |", "| --- | --- | --- |");
|
|
21
|
+
byRule.forEach((entry, ruleId) => {
|
|
22
|
+
lines.push(`| ${ruleId} | ${entry.count} | ${entry.severity} |`);
|
|
23
|
+
});
|
|
24
|
+
lines.push("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lines.push("## Findings", "");
|
|
28
|
+
|
|
29
|
+
const findings = plan.pipelineFindings ?? [];
|
|
30
|
+
if (findings.length > 0) {
|
|
31
|
+
for (const finding of findings) {
|
|
32
|
+
lines.push(
|
|
33
|
+
`- **${finding.title}** (${finding.severity}, ${finding.status})`,
|
|
34
|
+
` - Finding: ${finding.type}`,
|
|
35
|
+
` - Object: ${finding.objectType}/${finding.objectId}`,
|
|
36
|
+
` - Summary: ${finding.summary}`,
|
|
37
|
+
` - Recommendation: ${finding.recommendation}`,
|
|
38
|
+
` - Evidence refs: ${formatRefs(finding.evidenceIds)}`,
|
|
39
|
+
` - Current CRM value: ${formatValue(finding.currentCrmValue)}`,
|
|
40
|
+
` - Proposed value: ${formatValue(finding.proposedValue)}`,
|
|
41
|
+
` - Source freshness: ${formatFreshness(finding.freshness)}`,
|
|
42
|
+
` - Patch eligible: ${finding.patchEligibility.eligible ? "yes" : "no"} (${finding.patchEligibility.reason})`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const pipelineFindingIds = new Set(findings.map((finding) => finding.id));
|
|
46
|
+
const otherFindings = plan.findings.filter((finding) => !pipelineFindingIds.has(finding.id));
|
|
47
|
+
if (otherFindings.length > 0) {
|
|
48
|
+
lines.push("", "### Other Findings", "");
|
|
49
|
+
for (const finding of otherFindings) {
|
|
50
|
+
lines.push(
|
|
51
|
+
`- **${finding.title}** (${finding.severity})`,
|
|
52
|
+
` - Object: ${finding.objectType}/${finding.objectId}`,
|
|
53
|
+
` - Rule: ${finding.ruleId}`,
|
|
54
|
+
` - Summary: ${finding.summary}`,
|
|
55
|
+
` - Recommendation: ${finding.recommendation}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else if (plan.findings.length === 0) {
|
|
60
|
+
lines.push("No findings.");
|
|
61
|
+
} else {
|
|
62
|
+
for (const finding of plan.findings) {
|
|
63
|
+
lines.push(
|
|
64
|
+
`- **${finding.title}** (${finding.severity})`,
|
|
65
|
+
` - Object: ${finding.objectType}/${finding.objectId}`,
|
|
66
|
+
` - Rule: ${finding.ruleId}`,
|
|
67
|
+
` - Summary: ${finding.summary}`,
|
|
68
|
+
` - Recommendation: ${finding.recommendation}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (plan.evidence && plan.evidence.length > 0) {
|
|
74
|
+
lines.push("", "## Evidence", "");
|
|
75
|
+
for (const evidence of plan.evidence) {
|
|
76
|
+
lines.push(
|
|
77
|
+
`- **${evidence.id}** ${evidence.sourceSystem}:${evidence.sourceObjectType}/${evidence.sourceObjectId}`,
|
|
78
|
+
` - Object: ${evidence.objectType ?? "unknown"}/${evidence.objectId ?? "unknown"}`,
|
|
79
|
+
` - Text: ${evidence.text}`,
|
|
80
|
+
` - Freshness: ${evidence.freshness ? formatFreshness(evidence.freshness) : "unknown"}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lines.push("", "## Proposed Patch Operations", "");
|
|
86
|
+
if (plan.operations.length === 0) {
|
|
87
|
+
lines.push("No patch operations proposed.");
|
|
88
|
+
} else {
|
|
89
|
+
for (const operation of plan.operations) {
|
|
90
|
+
lines.push(
|
|
91
|
+
`- **${operation.operation}** ${operation.objectType}/${operation.objectId}`,
|
|
92
|
+
` - ID: ${operation.id}`,
|
|
93
|
+
` - Field/action: ${operation.field ?? operation.operation}`,
|
|
94
|
+
` - Findings: ${formatRefs(operation.findingIds)}`,
|
|
95
|
+
` - Evidence refs: ${formatRefs(operation.evidenceIds)}`,
|
|
96
|
+
` - Before: ${formatValue(operation.beforeValue)}`,
|
|
97
|
+
` - After: ${formatValue(operation.afterValue)}`,
|
|
98
|
+
` - Reason: ${operation.reason}`,
|
|
99
|
+
` - Source rule/policy: ${operation.sourceRuleOrPolicy ?? "unspecified"}`,
|
|
100
|
+
` - Risk: ${operation.riskLevel}`,
|
|
101
|
+
` - Approval required: ${operation.approvalRequired ? "yes" : "no"}`,
|
|
102
|
+
` - Rollback: ${operation.rollback ?? "restore prior value if apply is rejected or verification fails"}`,
|
|
103
|
+
` - Verification: ${formatVerification(operation.verification)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push(
|
|
109
|
+
"",
|
|
110
|
+
"> This prototype is dry-run only. Real CRM writes must require explicit human approval before connector adapters apply any operation.",
|
|
111
|
+
);
|
|
112
|
+
return `${lines.join("\n")}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatPatchPlanRun(run: PatchPlanRun) {
|
|
116
|
+
const lines = [
|
|
117
|
+
`# Patch plan run`,
|
|
118
|
+
"",
|
|
119
|
+
`Plan: ${run.planId}`,
|
|
120
|
+
`Provider: ${run.provider}`,
|
|
121
|
+
`Status: ${run.status}`,
|
|
122
|
+
`Started: ${run.startedAt}`,
|
|
123
|
+
`Finished: ${run.finishedAt}`,
|
|
124
|
+
"",
|
|
125
|
+
"## Operation Results",
|
|
126
|
+
"",
|
|
127
|
+
];
|
|
128
|
+
if (run.results.length === 0) {
|
|
129
|
+
lines.push("No operations were attempted.");
|
|
130
|
+
} else {
|
|
131
|
+
for (const result of run.results) {
|
|
132
|
+
lines.push(
|
|
133
|
+
`- **${result.status}** ${result.operationId}${result.detail ? ` — ${result.detail}` : ""}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return `${lines.join("\n")}\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatValue(value: unknown) {
|
|
141
|
+
if (value === undefined || value === null || value === "") return "unset";
|
|
142
|
+
if (typeof value === "string") return value;
|
|
143
|
+
return JSON.stringify(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatRefs(refs: string[] | undefined) {
|
|
147
|
+
return refs && refs.length > 0 ? refs.join(", ") : "none";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatFreshness(freshness: NonNullable<PatchPlan["pipelineFindings"]>[number]["freshness"]) {
|
|
151
|
+
const age = freshness.ageDays === undefined ? "age unknown" : `${freshness.ageDays}d old`;
|
|
152
|
+
const source = freshness.sourceUpdatedAt ? `source ${freshness.sourceUpdatedAt}` : "source date unknown";
|
|
153
|
+
return `${freshness.state}, ${age}, ${source}, checked ${freshness.checkedAt}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatVerification(verification: NonNullable<PatchPlan["operations"]>[number]["verification"]) {
|
|
157
|
+
if (!verification) return "not started";
|
|
158
|
+
const observed = verification.observedValue === undefined
|
|
159
|
+
? ""
|
|
160
|
+
: `, observed ${formatValue(verification.observedValue)}`;
|
|
161
|
+
return `${verification.status}${observed}${verification.auditText ? ` (${verification.auditText})` : ""}`;
|
|
162
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export { auditSnapshot, defaultPolicy } from "./audit.ts";
|
|
2
|
+
export {
|
|
3
|
+
CONFIG_FILE_NAME,
|
|
4
|
+
loadConfig,
|
|
5
|
+
mergePolicy,
|
|
6
|
+
resolveConfiguredRules,
|
|
7
|
+
type FullstackgtmConfig,
|
|
8
|
+
type LoadedConfig,
|
|
9
|
+
} from "./config.ts";
|
|
10
|
+
export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
|
|
11
|
+
export { createHubspotConnector, type HubspotConnectorOptions } from "./connectors/hubspot.ts";
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_LOOPBACK_PORT,
|
|
14
|
+
DEFAULT_OAUTH_SCOPES,
|
|
15
|
+
exchangeHubspotCode,
|
|
16
|
+
refreshHubspotToken,
|
|
17
|
+
runHubspotLoopbackLogin,
|
|
18
|
+
validateHubspotToken,
|
|
19
|
+
type HubspotTokenSet,
|
|
20
|
+
type LoopbackLoginOptions,
|
|
21
|
+
} from "./connectors/hubspotAuth.ts";
|
|
22
|
+
export {
|
|
23
|
+
createSalesforceConnector,
|
|
24
|
+
type SalesforceConnection,
|
|
25
|
+
type SalesforceConnectorOptions,
|
|
26
|
+
} from "./connectors/salesforce.ts";
|
|
27
|
+
export {
|
|
28
|
+
pollSalesforceDeviceLogin,
|
|
29
|
+
refreshSalesforceToken,
|
|
30
|
+
startSalesforceDeviceLogin,
|
|
31
|
+
validateSalesforceToken,
|
|
32
|
+
type SalesforceDeviceAuthorization,
|
|
33
|
+
type SalesforceTokenSet,
|
|
34
|
+
} from "./connectors/salesforceAuth.ts";
|
|
35
|
+
export { createStripeConnector, type StripeConnectorOptions } from "./connectors/stripe.ts";
|
|
36
|
+
export {
|
|
37
|
+
credentialsDir,
|
|
38
|
+
credentialsPath,
|
|
39
|
+
deleteCredential,
|
|
40
|
+
getCredential,
|
|
41
|
+
resolveHubspotAccessToken,
|
|
42
|
+
resolveHubspotConnection,
|
|
43
|
+
storeCredential,
|
|
44
|
+
type HubspotConnection,
|
|
45
|
+
type StoredCredential,
|
|
46
|
+
} from "./credentials.ts";
|
|
47
|
+
export { generateDemoSnapshot, type DemoSnapshotOptions } from "./demo.ts";
|
|
48
|
+
export {
|
|
49
|
+
diffFindings,
|
|
50
|
+
diffSnapshots,
|
|
51
|
+
diffToMarkdown,
|
|
52
|
+
type CollectionDiff,
|
|
53
|
+
type FieldChange,
|
|
54
|
+
type FindingsDrift,
|
|
55
|
+
type RecordChange,
|
|
56
|
+
type SnapshotDiff,
|
|
57
|
+
} from "./diff.ts";
|
|
58
|
+
export {
|
|
59
|
+
mergeSnapshots,
|
|
60
|
+
type MergeConflict,
|
|
61
|
+
type MergeMatch,
|
|
62
|
+
type MergeReport,
|
|
63
|
+
type MergeSuggestion,
|
|
64
|
+
} from "./merge.ts";
|
|
65
|
+
export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
|
|
66
|
+
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
67
|
+
export {
|
|
68
|
+
HUBSPOT_DEFAULT_FIELD_MAPPINGS,
|
|
69
|
+
SALESFORCE_DEFAULT_FIELD_MAPPINGS,
|
|
70
|
+
mappedField,
|
|
71
|
+
mappedFields,
|
|
72
|
+
normalizeFieldMappings,
|
|
73
|
+
readMappedValue,
|
|
74
|
+
type CrmObjectType,
|
|
75
|
+
type FieldMappings,
|
|
76
|
+
} from "./mappings.ts";
|
|
77
|
+
export {
|
|
78
|
+
accountSingleSourceRule,
|
|
79
|
+
activeDealAccountWithoutContactsRule,
|
|
80
|
+
auditFindingId,
|
|
81
|
+
buildSnapshotIndex,
|
|
82
|
+
builtinAuditRules,
|
|
83
|
+
closingSoonInactiveRule,
|
|
84
|
+
duplicateAccountDomainRule,
|
|
85
|
+
duplicateContactEmailRule,
|
|
86
|
+
missingDealAccountRule,
|
|
87
|
+
missingDealAmountRule,
|
|
88
|
+
missingDealOwnerRule,
|
|
89
|
+
orphanAccountRule,
|
|
90
|
+
pastCloseDateRule,
|
|
91
|
+
patchOperationId,
|
|
92
|
+
requiresHumanInput,
|
|
93
|
+
staleDealRule,
|
|
94
|
+
} from "./rules.ts";
|
|
95
|
+
export { sampleSnapshot } from "./sampleData.ts";
|
|
96
|
+
export type {
|
|
97
|
+
ApprovalStatus,
|
|
98
|
+
AuditFinding,
|
|
99
|
+
AuditFindingSeverity,
|
|
100
|
+
CanonicalAccount,
|
|
101
|
+
CanonicalActivity,
|
|
102
|
+
CanonicalContact,
|
|
103
|
+
CanonicalDeal,
|
|
104
|
+
CanonicalGtmSnapshot,
|
|
105
|
+
CanonicalUser,
|
|
106
|
+
CrmProvider,
|
|
107
|
+
GtmAuditRule,
|
|
108
|
+
GtmConnector,
|
|
109
|
+
GtmEvidence,
|
|
110
|
+
GtmEvidenceSourceSystem,
|
|
111
|
+
GtmObjectType,
|
|
112
|
+
GtmPolicy,
|
|
113
|
+
GtmRuleContext,
|
|
114
|
+
GtmRuleResult,
|
|
115
|
+
GtmSnapshotIndex,
|
|
116
|
+
PatchOperation,
|
|
117
|
+
PatchOperationResult,
|
|
118
|
+
PatchOperationType,
|
|
119
|
+
PatchPlan,
|
|
120
|
+
PatchPlanRun,
|
|
121
|
+
PatchPlanRunStatus,
|
|
122
|
+
PatchVerification,
|
|
123
|
+
PipelineFinding,
|
|
124
|
+
PipelineFindingStatus,
|
|
125
|
+
PipelineFindingType,
|
|
126
|
+
ProviderIdentity,
|
|
127
|
+
RiskLevel,
|
|
128
|
+
SourceFreshness,
|
|
129
|
+
} from "./types.ts";
|