fullstackgtm 0.45.0 → 0.47.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 (95) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/INSTALL_FOR_AGENTS.md +13 -11
  3. package/README.md +27 -18
  4. package/dist/backfill.d.ts +86 -0
  5. package/dist/backfill.js +251 -0
  6. package/dist/bin.js +4 -1
  7. package/dist/cli/auth.d.ts +2 -0
  8. package/dist/cli/auth.js +119 -12
  9. package/dist/cli/backfill.d.ts +1 -0
  10. package/dist/cli/backfill.js +125 -0
  11. package/dist/cli/backfillRuns.d.ts +1 -0
  12. package/dist/cli/backfillRuns.js +187 -0
  13. package/dist/cli/call.js +23 -9
  14. package/dist/cli/enrich.js +28 -10
  15. package/dist/cli/fix.js +9 -7
  16. package/dist/cli/help.js +60 -23
  17. package/dist/cli/plans.js +13 -11
  18. package/dist/cli/shared.d.ts +4 -3
  19. package/dist/cli/shared.js +31 -20
  20. package/dist/cli/ui.d.ts +21 -0
  21. package/dist/cli/ui.js +53 -1
  22. package/dist/cli.js +37 -41
  23. package/dist/connector.d.ts +8 -0
  24. package/dist/connector.js +104 -21
  25. package/dist/connectors/hubspot.d.ts +8 -1
  26. package/dist/connectors/hubspot.js +406 -13
  27. package/dist/connectors/hubspotAuth.js +5 -1
  28. package/dist/connectors/linkedin.js +14 -14
  29. package/dist/connectors/salesforce.d.ts +8 -1
  30. package/dist/connectors/salesforce.js +163 -2
  31. package/dist/connectors/stripe.d.ts +37 -0
  32. package/dist/connectors/stripe.js +103 -31
  33. package/dist/enrich.d.ts +7 -0
  34. package/dist/enrich.js +7 -0
  35. package/dist/health.d.ts +11 -69
  36. package/dist/health.js +4 -134
  37. package/dist/healthScore.d.ts +71 -0
  38. package/dist/healthScore.js +143 -0
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.js +3 -1
  41. package/dist/llm.d.ts +29 -0
  42. package/dist/llm.js +206 -0
  43. package/dist/market.d.ts +39 -1
  44. package/dist/market.js +116 -44
  45. package/dist/marketClassify.d.ts +9 -1
  46. package/dist/marketClassify.js +10 -1
  47. package/dist/marketTaxonomy.d.ts +6 -1
  48. package/dist/marketTaxonomy.js +4 -2
  49. package/dist/mcp-bin.js +29 -0
  50. package/dist/mcp.js +117 -4
  51. package/dist/progress.d.ts +96 -0
  52. package/dist/progress.js +142 -0
  53. package/dist/runReport.d.ts +24 -0
  54. package/dist/runReport.js +139 -4
  55. package/dist/types.d.ts +33 -1
  56. package/docs/api.md +4 -2
  57. package/docs/architecture.md +2 -0
  58. package/docs/linkedin-connector-spec.md +1 -1
  59. package/llms.txt +3 -3
  60. package/package.json +10 -3
  61. package/skills/fullstackgtm/SKILL.md +1 -0
  62. package/src/backfill.ts +340 -0
  63. package/src/bin.ts +5 -1
  64. package/src/cli/auth.ts +135 -15
  65. package/src/cli/backfill.ts +156 -0
  66. package/src/cli/backfillRuns.ts +198 -0
  67. package/src/cli/call.ts +26 -10
  68. package/src/cli/enrich.ts +33 -10
  69. package/src/cli/fix.ts +11 -10
  70. package/src/cli/help.ts +61 -23
  71. package/src/cli/plans.ts +15 -14
  72. package/src/cli/shared.ts +44 -27
  73. package/src/cli/ui.ts +72 -1
  74. package/src/cli.ts +38 -41
  75. package/src/connector.ts +110 -16
  76. package/src/connectors/hubspot.ts +423 -14
  77. package/src/connectors/hubspotAuth.ts +5 -1
  78. package/src/connectors/linkedin.ts +14 -14
  79. package/src/connectors/salesforce.ts +168 -3
  80. package/src/connectors/stripe.ts +154 -34
  81. package/src/enrich.ts +13 -0
  82. package/src/health.ts +14 -213
  83. package/src/healthScore.ts +223 -0
  84. package/src/index.ts +28 -1
  85. package/src/llm.ts +239 -0
  86. package/src/market.ts +157 -44
  87. package/src/marketClassify.ts +18 -1
  88. package/src/marketTaxonomy.ts +10 -2
  89. package/src/mcp-bin.ts +32 -0
  90. package/src/mcp.ts +140 -6
  91. package/src/progress.ts +197 -0
  92. package/src/runReport.ts +159 -4
  93. package/src/types.ts +35 -2
  94. package/docs/dx-punch-list.md +0 -87
  95. package/docs/roadmap-to-1.0.md +0 -211
package/src/health.ts CHANGED
@@ -2,7 +2,20 @@ import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { activeProfile, credentialsDir, ensureSecureHomeDir, writeSecureFile } from "./credentials.ts";
5
- import type { AuditFindingSeverity, CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
5
+ import type { CanonicalGtmSnapshot } from "./types.ts";
6
+ import type { HealthEntry } from "./healthScore.ts";
7
+
8
+ // Pure scoring lives in healthScore.ts (node-free, importable from V8 runtimes
9
+ // like the hosted app's Convex queries); this module owns the fs-backed
10
+ // profile timeline. Re-exported so existing imports keep working.
11
+ export {
12
+ computeHealth,
13
+ summarizeHealth,
14
+ healthToMarkdown,
15
+ type HealthEntry,
16
+ type HealthRuleDelta,
17
+ type HealthRollup,
18
+ } from "./healthScore.ts";
6
19
 
7
20
  /**
8
21
  * The engagement workspace: a per-client health timeline that accrues from the
@@ -15,218 +28,6 @@ import type { AuditFindingSeverity, CanonicalGtmSnapshot, PatchPlan } from "./ty
15
28
  * so it is stable in CI and comparable run-over-run.
16
29
  */
17
30
 
18
- // Severity weights: a critical finding costs ~3× a warning, ~10× an info.
19
- const SEVERITY_WEIGHT: Record<AuditFindingSeverity, number> = { info: 1, warning: 3, critical: 10 };
20
-
21
- export type HealthEntry = {
22
- /** ISO timestamp of the audit that produced this entry. */
23
- at: string;
24
- /** The saved plan this entry summarizes (correlates to plans/ and snapshots/). */
25
- planId: string;
26
- /** Deterministic 0–100 hygiene score (higher is healthier). */
27
- score: number;
28
- /** Total finding count across all rules. */
29
- findings: number;
30
- /** Severity-weighted finding total (drives the score). */
31
- weightedFindings: number;
32
- /** Record counts at audit time — the denominator that normalizes the score. */
33
- records: { accounts: number; contacts: number; deals: number; total: number };
34
- /** Finding count per rule id. */
35
- byRule: Record<string, number>;
36
- /** Finding count per severity. */
37
- severityCounts: Record<AuditFindingSeverity, number>;
38
- /**
39
- * Per-object-type breakdown — so "is my contact data clean but my pipeline
40
- * messy?" is answerable without re-auditing. Each type carries its own
41
- * record-normalized score (same curve as the overall score, scoped to that
42
- * type's records + findings).
43
- */
44
- byObjectType: Record<"account" | "contact" | "deal", {
45
- records: number;
46
- findings: number;
47
- weightedFindings: number;
48
- score: number;
49
- }>;
50
- };
51
-
52
- export type HealthRuleDelta = {
53
- ruleId: string;
54
- current: number;
55
- previous: number;
56
- /** current − previous: positive = more findings (worse), negative = fewer (better). */
57
- delta: number;
58
- };
59
-
60
- export type HealthRollup = {
61
- profile: string;
62
- auditCount: number;
63
- first: string;
64
- latest: string;
65
- current: HealthEntry;
66
- previous: HealthEntry | null;
67
- /** current.score − previous.score: positive = improving. null on the first audit. */
68
- scoreDelta: number | null;
69
- ruleDeltas: HealthRuleDelta[];
70
- history: Array<{ at: string; score: number; findings: number }>;
71
- };
72
-
73
- /**
74
- * Deterministically score a saved audit. score = 100 / (1 + weighted findings
75
- * per record): 0 findings → 100; one weighted finding per record → 50; the
76
- * curve is bounded (0, 100], monotonic, and needs no clamping or magic
77
- * constants. A messy 50-record CRM scores worse than the same finding count
78
- * across 5,000 records, which is the point.
79
- */
80
- export function computeHealth(plan: PatchPlan, snapshot: CanonicalGtmSnapshot, at: string): HealthEntry {
81
- const byRule: Record<string, number> = {};
82
- const severityCounts: Record<AuditFindingSeverity, number> = { info: 0, warning: 0, critical: 0 };
83
- const typeTally: Record<"account" | "contact" | "deal", { findings: number; weightedFindings: number }> = {
84
- account: { findings: 0, weightedFindings: 0 },
85
- contact: { findings: 0, weightedFindings: 0 },
86
- deal: { findings: 0, weightedFindings: 0 },
87
- };
88
- let weightedFindings = 0;
89
- for (const finding of plan.findings) {
90
- byRule[finding.ruleId] = (byRule[finding.ruleId] ?? 0) + 1;
91
- severityCounts[finding.severity] += 1;
92
- weightedFindings += SEVERITY_WEIGHT[finding.severity];
93
- const t = finding.objectType;
94
- if (t === "account" || t === "contact" || t === "deal") {
95
- typeTally[t].findings += 1;
96
- typeTally[t].weightedFindings += SEVERITY_WEIGHT[finding.severity];
97
- }
98
- }
99
-
100
- const records = {
101
- accounts: snapshot.accounts.length,
102
- contacts: snapshot.contacts.length,
103
- deals: snapshot.deals.length,
104
- total: snapshot.accounts.length + snapshot.contacts.length + snapshot.deals.length,
105
- };
106
-
107
- const scoreFor = (weighted: number, recordCount: number) =>
108
- Math.round(100 / (1 + weighted / Math.max(recordCount, 1)));
109
- const recordsByType = { account: records.accounts, contact: records.contacts, deal: records.deals };
110
- const byObjectType = (["account", "contact", "deal"] as const).reduce(
111
- (acc, t) => {
112
- acc[t] = {
113
- records: recordsByType[t],
114
- findings: typeTally[t].findings,
115
- weightedFindings: typeTally[t].weightedFindings,
116
- score: scoreFor(typeTally[t].weightedFindings, recordsByType[t]),
117
- };
118
- return acc;
119
- },
120
- {} as HealthEntry["byObjectType"],
121
- );
122
-
123
- const score = scoreFor(weightedFindings, records.total);
124
-
125
- return {
126
- at,
127
- planId: plan.id,
128
- score,
129
- findings: plan.findings.length,
130
- weightedFindings,
131
- records,
132
- byRule,
133
- severityCounts,
134
- byObjectType,
135
- };
136
- }
137
-
138
- /** Roll a timeline up into the current state, the change since last time, and per-rule deltas. */
139
- export function summarizeHealth(entries: HealthEntry[], profile: string): HealthRollup | null {
140
- if (entries.length === 0) return null;
141
- // Oldest → newest, so `current` is the last entry.
142
- const sorted = [...entries].sort((a, b) => a.at.localeCompare(b.at));
143
- const current = sorted[sorted.length - 1];
144
- const previous = sorted.length > 1 ? sorted[sorted.length - 2] : null;
145
-
146
- const ruleIds = new Set<string>([
147
- ...Object.keys(current.byRule),
148
- ...(previous ? Object.keys(previous.byRule) : []),
149
- ]);
150
- const ruleDeltas: HealthRuleDelta[] = [...ruleIds]
151
- .map((ruleId) => {
152
- const cur = current.byRule[ruleId] ?? 0;
153
- const prev = previous?.byRule[ruleId] ?? 0;
154
- return { ruleId, current: cur, previous: prev, delta: cur - prev };
155
- })
156
- .sort((a, b) => b.current - a.current || a.ruleId.localeCompare(b.ruleId));
157
-
158
- return {
159
- profile,
160
- auditCount: sorted.length,
161
- first: sorted[0].at,
162
- latest: current.at,
163
- current,
164
- previous,
165
- scoreDelta: previous ? current.score - previous.score : null,
166
- ruleDeltas,
167
- history: sorted.map((entry) => ({ at: entry.at, score: entry.score, findings: entry.findings })),
168
- };
169
- }
170
-
171
- // Sign-based and uniform: ▲ = the number went up, ▼ = down. The reader supplies
172
- // the meaning (score up = good; findings up = bad) — mixing arrow direction with
173
- // good/bad reads as contradictory ("▲ -3").
174
- function arrow(delta: number): string {
175
- if (delta === 0) return "·";
176
- return delta > 0 ? "▲" : "▼";
177
- }
178
-
179
- /** Human-readable rollup: headline score + trend, then per-rule change since last audit. */
180
- export function healthToMarkdown(rollup: HealthRollup): string {
181
- const { current, scoreDelta } = rollup;
182
- const lines = [`# GTM health — profile \`${rollup.profile}\``, ""];
183
-
184
- const deltaText =
185
- scoreDelta === null
186
- ? "(first audit — no prior reading)"
187
- : `${arrow(scoreDelta)} ${scoreDelta >= 0 ? "+" : ""}${scoreDelta} since the previous audit`;
188
- lines.push(
189
- `Score: **${current.score}/100** ${deltaText}`,
190
- `Audits: ${rollup.auditCount} over ${shortDate(rollup.first)} → ${shortDate(rollup.latest)}`,
191
- `Findings: ${current.findings} (${current.severityCounts.critical} critical, ${current.severityCounts.warning} warning, ${current.severityCounts.info} info)`,
192
- `Records: ${current.records.accounts} accounts, ${current.records.contacts} contacts, ${current.records.deals} deals`,
193
- "",
194
- );
195
-
196
- lines.push("## By object type", "", "| Type | Score | Findings | Records |", "| --- | --- | --- | --- |");
197
- for (const t of ["account", "contact", "deal"] as const) {
198
- const b = current.byObjectType[t];
199
- lines.push(`| ${t} | ${b.score}/100 | ${b.findings} | ${b.records} |`);
200
- }
201
- lines.push("");
202
-
203
- if (rollup.history.length > 1) {
204
- lines.push("## Trend", "");
205
- for (const point of rollup.history) {
206
- const marker = point.at === rollup.latest ? " ← latest" : "";
207
- lines.push(`- ${shortDate(point.at)} score ${point.score} (${point.findings} findings)${marker}`);
208
- }
209
- lines.push("");
210
- }
211
-
212
- if (rollup.ruleDeltas.length > 0) {
213
- lines.push("## By rule (Δ since previous audit)", "", "| Rule | Findings | Δ |", "| --- | --- | --- |");
214
- for (const rule of rollup.ruleDeltas) {
215
- const sign = rule.delta > 0 ? `+${rule.delta}` : `${rule.delta}`;
216
- const cell = rule.delta === 0 ? "·" : `${arrow(rule.delta)} ${sign}`;
217
- lines.push(`| ${rule.ruleId} | ${rule.current} | ${cell} |`);
218
- }
219
- lines.push("");
220
- }
221
-
222
- lines.push("> Score is deterministic: 100 / (1 + severity-weighted findings per record). Lower findings or more clean records ⇒ higher score.");
223
- return `${lines.join("\n")}\n`;
224
- }
225
-
226
- function shortDate(iso: string): string {
227
- return iso.slice(0, 10);
228
- }
229
-
230
31
  // ── Profile-scoped storage ──────────────────────────────────────────────────
231
32
 
232
33
  /** `$FSGTM_HOME[/profiles/<name>]/health.jsonl` — append-only, one entry per audit-save. */
@@ -0,0 +1,223 @@
1
+ import type { AuditFindingSeverity, CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
2
+
3
+ /**
4
+ * Pure, node-free health scoring. Split from health.ts (which owns the
5
+ * fs-backed profile timeline) so V8-runtime consumers — the hosted app's
6
+ * Convex queries — can import the scoring math without pulling node built-ins,
7
+ * the same pattern as audit.ts/format.ts.
8
+ *
9
+ * The score is DETERMINISTIC (no LLM): same plan + same snapshot → same score,
10
+ * so it is stable in CI and comparable run-over-run.
11
+ */
12
+
13
+ // Severity weights: a critical finding costs ~3× a warning, ~10× an info.
14
+ const SEVERITY_WEIGHT: Record<AuditFindingSeverity, number> = { info: 1, warning: 3, critical: 10 };
15
+
16
+ export type HealthEntry = {
17
+ /** ISO timestamp of the audit that produced this entry. */
18
+ at: string;
19
+ /** The saved plan this entry summarizes (correlates to plans/ and snapshots/). */
20
+ planId: string;
21
+ /** Deterministic 0–100 hygiene score (higher is healthier). */
22
+ score: number;
23
+ /** Total finding count across all rules. */
24
+ findings: number;
25
+ /** Severity-weighted finding total (drives the score). */
26
+ weightedFindings: number;
27
+ /** Record counts at audit time — the denominator that normalizes the score. */
28
+ records: { accounts: number; contacts: number; deals: number; total: number };
29
+ /** Finding count per rule id. */
30
+ byRule: Record<string, number>;
31
+ /** Finding count per severity. */
32
+ severityCounts: Record<AuditFindingSeverity, number>;
33
+ /**
34
+ * Per-object-type breakdown — so "is my contact data clean but my pipeline
35
+ * messy?" is answerable without re-auditing. Each type carries its own
36
+ * record-normalized score (same curve as the overall score, scoped to that
37
+ * type's records + findings).
38
+ */
39
+ byObjectType: Record<"account" | "contact" | "deal", {
40
+ records: number;
41
+ findings: number;
42
+ weightedFindings: number;
43
+ score: number;
44
+ }>;
45
+ };
46
+
47
+ export type HealthRuleDelta = {
48
+ ruleId: string;
49
+ current: number;
50
+ previous: number;
51
+ /** current − previous: positive = more findings (worse), negative = fewer (better). */
52
+ delta: number;
53
+ };
54
+
55
+ export type HealthRollup = {
56
+ profile: string;
57
+ auditCount: number;
58
+ first: string;
59
+ latest: string;
60
+ current: HealthEntry;
61
+ previous: HealthEntry | null;
62
+ /** current.score − previous.score: positive = improving. null on the first audit. */
63
+ scoreDelta: number | null;
64
+ ruleDeltas: HealthRuleDelta[];
65
+ history: Array<{ at: string; score: number; findings: number }>;
66
+ };
67
+
68
+ /**
69
+ * Deterministically score a saved audit. score = 100 / (1 + weighted findings
70
+ * per record): 0 findings → 100; one weighted finding per record → 50; the
71
+ * curve is bounded (0, 100], monotonic, and needs no clamping or magic
72
+ * constants. A messy 50-record CRM scores worse than the same finding count
73
+ * across 5,000 records, which is the point.
74
+ */
75
+ export function computeHealth(plan: PatchPlan, snapshot: CanonicalGtmSnapshot, at: string): HealthEntry {
76
+ const byRule: Record<string, number> = {};
77
+ const severityCounts: Record<AuditFindingSeverity, number> = { info: 0, warning: 0, critical: 0 };
78
+ const typeTally: Record<"account" | "contact" | "deal", { findings: number; weightedFindings: number }> = {
79
+ account: { findings: 0, weightedFindings: 0 },
80
+ contact: { findings: 0, weightedFindings: 0 },
81
+ deal: { findings: 0, weightedFindings: 0 },
82
+ };
83
+ let weightedFindings = 0;
84
+ for (const finding of plan.findings) {
85
+ byRule[finding.ruleId] = (byRule[finding.ruleId] ?? 0) + 1;
86
+ severityCounts[finding.severity] += 1;
87
+ weightedFindings += SEVERITY_WEIGHT[finding.severity];
88
+ const t = finding.objectType;
89
+ if (t === "account" || t === "contact" || t === "deal") {
90
+ typeTally[t].findings += 1;
91
+ typeTally[t].weightedFindings += SEVERITY_WEIGHT[finding.severity];
92
+ }
93
+ }
94
+
95
+ const records = {
96
+ accounts: snapshot.accounts.length,
97
+ contacts: snapshot.contacts.length,
98
+ deals: snapshot.deals.length,
99
+ total: snapshot.accounts.length + snapshot.contacts.length + snapshot.deals.length,
100
+ };
101
+
102
+ const scoreFor = (weighted: number, recordCount: number) =>
103
+ Math.round(100 / (1 + weighted / Math.max(recordCount, 1)));
104
+ const recordsByType = { account: records.accounts, contact: records.contacts, deal: records.deals };
105
+ const byObjectType = (["account", "contact", "deal"] as const).reduce(
106
+ (acc, t) => {
107
+ acc[t] = {
108
+ records: recordsByType[t],
109
+ findings: typeTally[t].findings,
110
+ weightedFindings: typeTally[t].weightedFindings,
111
+ score: scoreFor(typeTally[t].weightedFindings, recordsByType[t]),
112
+ };
113
+ return acc;
114
+ },
115
+ {} as HealthEntry["byObjectType"],
116
+ );
117
+
118
+ const score = scoreFor(weightedFindings, records.total);
119
+
120
+ return {
121
+ at,
122
+ planId: plan.id,
123
+ score,
124
+ findings: plan.findings.length,
125
+ weightedFindings,
126
+ records,
127
+ byRule,
128
+ severityCounts,
129
+ byObjectType,
130
+ };
131
+ }
132
+
133
+ /** Roll a timeline up into the current state, the change since last time, and per-rule deltas. */
134
+ export function summarizeHealth(entries: HealthEntry[], profile: string): HealthRollup | null {
135
+ if (entries.length === 0) return null;
136
+ // Oldest → newest, so `current` is the last entry.
137
+ const sorted = [...entries].sort((a, b) => a.at.localeCompare(b.at));
138
+ const current = sorted[sorted.length - 1];
139
+ const previous = sorted.length > 1 ? sorted[sorted.length - 2] : null;
140
+
141
+ const ruleIds = new Set<string>([
142
+ ...Object.keys(current.byRule),
143
+ ...(previous ? Object.keys(previous.byRule) : []),
144
+ ]);
145
+ const ruleDeltas: HealthRuleDelta[] = [...ruleIds]
146
+ .map((ruleId) => {
147
+ const cur = current.byRule[ruleId] ?? 0;
148
+ const prev = previous?.byRule[ruleId] ?? 0;
149
+ return { ruleId, current: cur, previous: prev, delta: cur - prev };
150
+ })
151
+ .sort((a, b) => b.current - a.current || a.ruleId.localeCompare(b.ruleId));
152
+
153
+ return {
154
+ profile,
155
+ auditCount: sorted.length,
156
+ first: sorted[0].at,
157
+ latest: current.at,
158
+ current,
159
+ previous,
160
+ scoreDelta: previous ? current.score - previous.score : null,
161
+ ruleDeltas,
162
+ history: sorted.map((entry) => ({ at: entry.at, score: entry.score, findings: entry.findings })),
163
+ };
164
+ }
165
+
166
+ // Sign-based and uniform: ▲ = the number went up, ▼ = down. The reader supplies
167
+ // the meaning (score up = good; findings up = bad) — mixing arrow direction with
168
+ // good/bad reads as contradictory ("▲ -3").
169
+ function arrow(delta: number): string {
170
+ if (delta === 0) return "·";
171
+ return delta > 0 ? "▲" : "▼";
172
+ }
173
+
174
+ /** Human-readable rollup: headline score + trend, then per-rule change since last audit. */
175
+ export function healthToMarkdown(rollup: HealthRollup): string {
176
+ const { current, scoreDelta } = rollup;
177
+ const lines = [`# GTM health — profile \`${rollup.profile}\``, ""];
178
+
179
+ const deltaText =
180
+ scoreDelta === null
181
+ ? "(first audit — no prior reading)"
182
+ : `${arrow(scoreDelta)} ${scoreDelta >= 0 ? "+" : ""}${scoreDelta} since the previous audit`;
183
+ lines.push(
184
+ `Score: **${current.score}/100** ${deltaText}`,
185
+ `Audits: ${rollup.auditCount} over ${shortDate(rollup.first)} → ${shortDate(rollup.latest)}`,
186
+ `Findings: ${current.findings} (${current.severityCounts.critical} critical, ${current.severityCounts.warning} warning, ${current.severityCounts.info} info)`,
187
+ `Records: ${current.records.accounts} accounts, ${current.records.contacts} contacts, ${current.records.deals} deals`,
188
+ "",
189
+ );
190
+
191
+ lines.push("## By object type", "", "| Type | Score | Findings | Records |", "| --- | --- | --- | --- |");
192
+ for (const t of ["account", "contact", "deal"] as const) {
193
+ const b = current.byObjectType[t];
194
+ lines.push(`| ${t} | ${b.score}/100 | ${b.findings} | ${b.records} |`);
195
+ }
196
+ lines.push("");
197
+
198
+ if (rollup.history.length > 1) {
199
+ lines.push("## Trend", "");
200
+ for (const point of rollup.history) {
201
+ const marker = point.at === rollup.latest ? " ← latest" : "";
202
+ lines.push(`- ${shortDate(point.at)} score ${point.score} (${point.findings} findings)${marker}`);
203
+ }
204
+ lines.push("");
205
+ }
206
+
207
+ if (rollup.ruleDeltas.length > 0) {
208
+ lines.push("## By rule (Δ since previous audit)", "", "| Rule | Findings | Δ |", "| --- | --- | --- |");
209
+ for (const rule of rollup.ruleDeltas) {
210
+ const sign = rule.delta > 0 ? `+${rule.delta}` : `${rule.delta}`;
211
+ const cell = rule.delta === 0 ? "·" : `${arrow(rule.delta)} ${sign}`;
212
+ lines.push(`| ${rule.ruleId} | ${rule.current} | ${cell} |`);
213
+ }
214
+ lines.push("");
215
+ }
216
+
217
+ lines.push("> Score is deterministic: 100 / (1 + severity-weighted findings per record). Lower findings or more clean records ⇒ higher score.");
218
+ return `${lines.join("\n")}\n`;
219
+ }
220
+
221
+ function shortDate(iso: string): string {
222
+ return iso.slice(0, 10);
223
+ }
package/src/index.ts CHANGED
@@ -21,6 +21,20 @@ export {
21
21
  type LoadedConfig,
22
22
  } from "./config.ts";
23
23
  export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
24
+ export {
25
+ APPLY_STAGES,
26
+ BACKFILL_STRIPE_STAGES,
27
+ composeListeners,
28
+ createProgressEmitter,
29
+ CRM_SYNC_STAGES,
30
+ nullProgressEmitter,
31
+ SNAPSHOT_PULL_STAGES,
32
+ STRIPE_SNAPSHOT_STAGES,
33
+ type ProgressEmitter,
34
+ type ProgressEvent,
35
+ type ProgressListener,
36
+ type ProgressSnapshot,
37
+ } from "./progress.ts";
24
38
  export { createHubspotConnector, type HubspotConnectorOptions } from "./connectors/hubspot.ts";
25
39
  export {
26
40
  DEFAULT_LOOPBACK_PORT,
@@ -45,7 +59,20 @@ export {
45
59
  type SalesforceDeviceAuthorization,
46
60
  type SalesforceTokenSet,
47
61
  } from "./connectors/salesforceAuth.ts";
48
- export { createStripeConnector, type StripeConnectorOptions } from "./connectors/stripe.ts";
62
+ export {
63
+ createStripeConnector,
64
+ fetchStripePaidInvoices,
65
+ type StripeConnectorOptions,
66
+ type StripePaidInvoice,
67
+ } from "./connectors/stripe.ts";
68
+ export {
69
+ buildStripeBackfillPlan,
70
+ DEFAULT_BACKFILL_MATCH_PROPERTY,
71
+ type StripeBackfillCounts,
72
+ type StripeBackfillOptions,
73
+ type StripeBackfillResult,
74
+ type StripeBackfillUnmatched,
75
+ } from "./backfill.ts";
49
76
  export {
50
77
  activeProfile,
51
78
  credentialsDir,