fullstackgtm 0.34.0 → 0.38.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 +141 -0
- package/INSTALL_FOR_AGENTS.md +8 -0
- package/README.md +39 -0
- package/dist/acquireLinkedIn.d.ts +41 -0
- package/dist/acquireLinkedIn.js +57 -0
- package/dist/acquireMeter.d.ts +67 -0
- package/dist/acquireMeter.js +145 -0
- package/dist/acquireSeen.d.ts +5 -0
- package/dist/acquireSeen.js +54 -0
- package/dist/assign.d.ts +83 -0
- package/dist/assign.js +146 -0
- package/dist/bin.js +14 -2
- package/dist/cli.js +817 -26
- package/dist/connectors/hubspot.js +140 -0
- package/dist/connectors/linkedin.d.ts +78 -0
- package/dist/connectors/linkedin.js +199 -0
- package/dist/connectors/prospectSources.d.ts +91 -0
- package/dist/connectors/prospectSources.js +227 -0
- package/dist/enrich.d.ts +107 -0
- package/dist/enrich.js +315 -5
- package/dist/format.d.ts +3 -1
- package/dist/format.js +14 -2
- package/dist/health.d.ts +71 -0
- package/dist/health.js +172 -0
- package/dist/icp.d.ts +96 -0
- package/dist/icp.js +256 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mappings.js +3 -0
- package/dist/reassign.d.ts +11 -2
- package/dist/reassign.js +13 -6
- package/dist/runReport.d.ts +9 -0
- package/dist/runReport.js +66 -0
- package/dist/types.d.ts +25 -1
- package/docs/api.md +53 -3
- package/docs/architecture.md +11 -1
- package/docs/dx-punch-list.md +87 -0
- package/docs/linkedin-connector-spec.md +87 -0
- package/llms.txt +38 -1
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +5 -3
- package/src/acquireLinkedIn.ts +83 -0
- package/src/acquireMeter.ts +186 -0
- package/src/acquireSeen.ts +57 -0
- package/src/assign.ts +193 -0
- package/src/bin.ts +17 -4
- package/src/cli.ts +965 -25
- package/src/connectors/hubspot.ts +145 -0
- package/src/connectors/linkedin.ts +272 -0
- package/src/connectors/prospectSources.ts +324 -0
- package/src/enrich.ts +411 -5
- package/src/format.ts +20 -2
- package/src/health.ts +238 -0
- package/src/icp.ts +313 -0
- package/src/index.ts +18 -0
- package/src/mappings.ts +3 -0
- package/src/reassign.ts +24 -8
- package/src/runReport.ts +76 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
function splitName(full) {
|
|
2
|
+
if (!full)
|
|
3
|
+
return {};
|
|
4
|
+
const parts = full.trim().split(/\s+/);
|
|
5
|
+
if (parts.length === 1)
|
|
6
|
+
return { firstName: parts[0] };
|
|
7
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
8
|
+
}
|
|
9
|
+
function bareDomain(value) {
|
|
10
|
+
if (!value)
|
|
11
|
+
return undefined;
|
|
12
|
+
return value
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/^https?:\/\//i, "")
|
|
15
|
+
.replace(/^www\./i, "")
|
|
16
|
+
.replace(/\/.*$/, "")
|
|
17
|
+
.toLowerCase() || undefined;
|
|
18
|
+
}
|
|
19
|
+
export async function fetchExploriumProspects(opts) {
|
|
20
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
21
|
+
const base = (opts.apiBaseUrl ?? "https://api.explorium.ai").replace(/\/$/, "");
|
|
22
|
+
const size = Math.min(opts.size ?? 25, 100);
|
|
23
|
+
const response = await fetchImpl(`${base}/v1/prospects`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "api_key": opts.apiKey, "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ mode: "full", size, page_size: size, page: 1, filters: opts.filters }),
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Explorium /v1/prospects failed: HTTP ${response.status} ${await safeText(response)}`);
|
|
30
|
+
}
|
|
31
|
+
const data = (await response.json());
|
|
32
|
+
return (data.data ?? []).map((row) => ({
|
|
33
|
+
firstName: row.first_name,
|
|
34
|
+
lastName: row.last_name,
|
|
35
|
+
fullName: row.full_name ?? ([row.first_name, row.last_name].filter(Boolean).join(" ") || undefined),
|
|
36
|
+
jobTitle: row.job_title,
|
|
37
|
+
jobLevel: row.job_level_main,
|
|
38
|
+
jobDepartment: row.job_department_main,
|
|
39
|
+
companyName: row.company_name,
|
|
40
|
+
companyDomain: bareDomain(row.company_website),
|
|
41
|
+
linkedin: normalizeLinkedin(row.linkedin),
|
|
42
|
+
sourceId: row.prospect_id,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// pipe0 / Crustdata — net-new discovery (search). Crustdata is built into pipe0
|
|
47
|
+
// (no separate connection needed). Returns profiles; pair with
|
|
48
|
+
// pipe0ResolveWorkEmails to get real emails.
|
|
49
|
+
export async function fetchPipe0CrustdataProspects(opts) {
|
|
50
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
51
|
+
const base = (opts.apiBaseUrl ?? "https://api.pipe0.com").replace(/\/$/, "");
|
|
52
|
+
const limit = Math.min(opts.limit ?? 25, 100);
|
|
53
|
+
const response = await fetchImpl(`${base}/v1/searches/run/sync`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { Authorization: `Bearer ${opts.apiKey}`, "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
searches: [{ search_id: "people:profiles:crustdata@1", config: { limit, filters: opts.filters } }],
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`pipe0 /v1/searches/run/sync failed: HTTP ${response.status} ${await safeText(response)}`);
|
|
62
|
+
}
|
|
63
|
+
const body = (await response.json());
|
|
64
|
+
// Surface upstream provider errors (e.g. CreditBalanceInsufficient) instead of
|
|
65
|
+
// silently returning [] — a throttle/credit failure must not look like "no ICP matches".
|
|
66
|
+
const upstreamErrors = (body.search_statuses ?? []).flatMap((s) => s.errors ?? []);
|
|
67
|
+
if (upstreamErrors.length > 0 && !(body.results ?? []).length) {
|
|
68
|
+
throw new Error(`pipe0/Crustdata search error: ${upstreamErrors.map((e) => `${e.code ?? "error"}: ${e.message ?? ""}`).join("; ")}`);
|
|
69
|
+
}
|
|
70
|
+
return (body.results ?? []).map((row) => {
|
|
71
|
+
const fullName = strField(row.name);
|
|
72
|
+
const { firstName, lastName } = splitName(fullName);
|
|
73
|
+
return {
|
|
74
|
+
firstName,
|
|
75
|
+
lastName,
|
|
76
|
+
fullName,
|
|
77
|
+
jobTitle: strField(row.job_title),
|
|
78
|
+
companyDomain: bareDomain(strField(row.company_website_url)),
|
|
79
|
+
linkedin: normalizeLinkedin(strField(row.profile_url)),
|
|
80
|
+
sourceId: strField(row.profile_url) ?? fullName,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function strField(field) {
|
|
85
|
+
const v = field?.value;
|
|
86
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
87
|
+
}
|
|
88
|
+
function normalizeLinkedin(value) {
|
|
89
|
+
if (!value)
|
|
90
|
+
return undefined;
|
|
91
|
+
const v = value.trim().replace(/^https?:\/\//i, "").replace(/\/$/, "");
|
|
92
|
+
return v ? `https://${v}` : undefined;
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// pipe0 — work-email resolution (waterfall)
|
|
96
|
+
/**
|
|
97
|
+
* Resolve real work emails for people via pipe0's waterfall. Input rows need a
|
|
98
|
+
* full name + a company domain (or name). Returns the prospects with `email`
|
|
99
|
+
* filled where the waterfall found one; rows with no hit are returned unchanged.
|
|
100
|
+
*/
|
|
101
|
+
export async function pipe0ResolveWorkEmails(opts) {
|
|
102
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
103
|
+
const base = (opts.apiBaseUrl ?? "https://api.pipe0.com").replace(/\/$/, "");
|
|
104
|
+
const chunkSize = Math.max(1, opts.chunkSize ?? 3);
|
|
105
|
+
// Only rows with the waterfall's required inputs (name + company_domain|name).
|
|
106
|
+
const resolvable = opts.prospects.filter((p) => p.fullName && (p.companyDomain || p.companyName));
|
|
107
|
+
if (resolvable.length === 0)
|
|
108
|
+
return opts.prospects;
|
|
109
|
+
const emailByKey = new Map();
|
|
110
|
+
for (let i = 0; i < resolvable.length; i += chunkSize) {
|
|
111
|
+
const chunk = resolvable.slice(i, i + chunkSize);
|
|
112
|
+
const input = chunk.map((p) => ({
|
|
113
|
+
name: p.fullName,
|
|
114
|
+
...(p.companyDomain ? { company_domain: p.companyDomain } : { company_name: p.companyName }),
|
|
115
|
+
}));
|
|
116
|
+
let body;
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetchImpl(`${base}/v1/pipes/run/sync`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { Authorization: `Bearer ${opts.apiKey}`, "Content-Type": "application/json" },
|
|
121
|
+
body: JSON.stringify({ pipes: [{ pipe_id: "person:workemail:waterfall@1" }], input }),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
continue; // throttled/5xx chunk — skip, keep the rest
|
|
125
|
+
body = (await response.json());
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue; // network hiccup on one chunk must not sink the whole run
|
|
129
|
+
}
|
|
130
|
+
for (const recordId of body.order ?? []) {
|
|
131
|
+
const fields = body.records?.[recordId]?.fields ?? {};
|
|
132
|
+
const email = fields.work_email?.status === "completed" ? fieldValue(fields.work_email) : undefined;
|
|
133
|
+
if (email) {
|
|
134
|
+
const domain = fieldValue(fields.company_domain) ?? fieldValue(fields.company_name);
|
|
135
|
+
emailByKey.set(personKey(fieldValue(fields.name), domain), email);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return opts.prospects.map((p) => {
|
|
140
|
+
const email = emailByKey.get(personKey(p.fullName, p.companyDomain ?? p.companyName));
|
|
141
|
+
return email ? { ...p, email } : p;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function personKey(name, company) {
|
|
145
|
+
return `${(name ?? "").trim().toLowerCase()}|${(company ?? "").trim().toLowerCase()}`;
|
|
146
|
+
}
|
|
147
|
+
function fieldValue(field) {
|
|
148
|
+
const v = field?.value;
|
|
149
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
150
|
+
}
|
|
151
|
+
async function safeText(response) {
|
|
152
|
+
try {
|
|
153
|
+
return (await response.text()).slice(0, 300);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Pre-email dedup: identity keys shared between prospects and CRM contacts, so
|
|
161
|
+
// we can drop already-in-CRM / already-seen people BEFORE paying for emails.
|
|
162
|
+
function normName(value) {
|
|
163
|
+
return (value ?? "").trim().toLowerCase().replace(/\s+/g, " ");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Stable identity keys for a prospect (prefixed by kind). A match on ANY key
|
|
167
|
+
* means "same person". LinkedIn is strongest; name+domain is the only key
|
|
168
|
+
* shareable with the canonical CRM snapshot (which has no LinkedIn); email is
|
|
169
|
+
* available only after resolution (so it can't pre-filter, but it strengthens
|
|
170
|
+
* the seen cache).
|
|
171
|
+
*/
|
|
172
|
+
export function prospectIdentityKeys(p) {
|
|
173
|
+
const keys = [];
|
|
174
|
+
if (p.linkedin)
|
|
175
|
+
keys.push(`li:${p.linkedin.trim().toLowerCase().replace(/\/+$/, "")}`);
|
|
176
|
+
const name = normName(p.fullName ?? [p.firstName, p.lastName].filter(Boolean).join(" "));
|
|
177
|
+
const domain = bareDomain(p.companyDomain);
|
|
178
|
+
if (name && domain)
|
|
179
|
+
keys.push(`nd:${name}|${domain}`);
|
|
180
|
+
if (p.email)
|
|
181
|
+
keys.push(`em:${p.email.trim().toLowerCase()}`);
|
|
182
|
+
return keys;
|
|
183
|
+
}
|
|
184
|
+
/** Identity keys for every contact already in the CRM snapshot (email + name|domain). */
|
|
185
|
+
export function crmContactKeys(snapshot) {
|
|
186
|
+
const domainByAccount = new Map();
|
|
187
|
+
for (const account of snapshot.accounts ?? []) {
|
|
188
|
+
const d = bareDomain(account.domain);
|
|
189
|
+
if (d)
|
|
190
|
+
domainByAccount.set(account.id, d);
|
|
191
|
+
}
|
|
192
|
+
const set = new Set();
|
|
193
|
+
for (const contact of snapshot.contacts ?? []) {
|
|
194
|
+
if (contact.linkedin)
|
|
195
|
+
set.add(`li:${contact.linkedin.trim().toLowerCase().replace(/\/+$/, "")}`);
|
|
196
|
+
if (contact.email)
|
|
197
|
+
set.add(`em:${contact.email.trim().toLowerCase()}`);
|
|
198
|
+
const name = normName([contact.firstName, contact.lastName].filter(Boolean).join(" "));
|
|
199
|
+
const domain = contact.accountId ? domainByAccount.get(contact.accountId) : undefined;
|
|
200
|
+
if (name && domain)
|
|
201
|
+
set.add(`nd:${name}|${domain}`);
|
|
202
|
+
}
|
|
203
|
+
return set;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Split prospects into those worth paying to enrich vs. drop. A prospect is
|
|
207
|
+
* dropped if any identity key is already in the CRM (`crmKeys`) or in the
|
|
208
|
+
* cross-run `seen` cache. Pure + deterministic.
|
|
209
|
+
*/
|
|
210
|
+
export function partitionFreshProspects(prospects, crmKeys, seen) {
|
|
211
|
+
const fresh = [];
|
|
212
|
+
let skippedCrm = 0;
|
|
213
|
+
let skippedSeen = 0;
|
|
214
|
+
for (const p of prospects) {
|
|
215
|
+
const keys = prospectIdentityKeys(p);
|
|
216
|
+
if (keys.some((k) => crmKeys.has(k))) {
|
|
217
|
+
skippedCrm += 1;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (keys.some((k) => seen.has(k))) {
|
|
221
|
+
skippedSeen += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
fresh.push(p);
|
|
225
|
+
}
|
|
226
|
+
return { fresh, skippedCrm, skippedSeen };
|
|
227
|
+
}
|
package/dist/enrich.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AcquireBudget } from "./acquireMeter.ts";
|
|
2
|
+
import type { AssignmentContext, AssignmentPolicy } from "./assign.ts";
|
|
1
3
|
import type { CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
|
|
2
4
|
/**
|
|
3
5
|
* The enrich layer: governed append/refresh of third-party data into the CRM.
|
|
@@ -55,6 +57,47 @@ export type EnrichConfig = {
|
|
|
55
57
|
match: Partial<Record<EnrichObjectType, EnrichMatchConfig>>;
|
|
56
58
|
fields: Partial<Record<EnrichObjectType, EnrichFieldConfig[]>>;
|
|
57
59
|
policy: EnrichPolicyConfig;
|
|
60
|
+
/** Net-new lead creation (`enrich acquire`). Optional; absent = acquire off. */
|
|
61
|
+
acquire?: AcquireConfig;
|
|
62
|
+
};
|
|
63
|
+
/** Maps a sourced record to the CRM properties a net-new record is created with. */
|
|
64
|
+
export type AcquireCreateMap = {
|
|
65
|
+
/** CRM property name → source payload path (read via sourceValueAt). */
|
|
66
|
+
properties: Record<string, string>;
|
|
67
|
+
/** Source path whose value becomes the dedupe key (e.g. "email"). */
|
|
68
|
+
matchKey: string;
|
|
69
|
+
/** Optional source path to a company name; the connector resolves-or-creates it. */
|
|
70
|
+
associateCompanyFrom?: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Net-new discovery for an API acquire source. `provider` selects the adapter
|
|
74
|
+
* (explorium = prospect search; pipe0 = work-email waterfall). `filters` is the
|
|
75
|
+
* provider-native query; `resolveEmailsWith` chains a second provider to fill
|
|
76
|
+
* real emails (e.g. explorium discovers, pipe0 resolves the work email).
|
|
77
|
+
*/
|
|
78
|
+
export type AcquireDiscoveryConfig = {
|
|
79
|
+
provider: "explorium" | "pipe0" | "linkedin" | "heyreach";
|
|
80
|
+
filters?: Record<string, unknown>;
|
|
81
|
+
size?: number;
|
|
82
|
+
resolveEmailsWith?: "pipe0";
|
|
83
|
+
/** LinkedIn sources only: the provider-native list id to read (HeyReach lead-list id). */
|
|
84
|
+
listId?: string;
|
|
85
|
+
};
|
|
86
|
+
export type AcquireConfig = {
|
|
87
|
+
/** Windowed budget enforced by the acquire meter; absent = unmetered. */
|
|
88
|
+
budget?: AcquireBudget;
|
|
89
|
+
/** Estimated provider cost (USD) per created record, by source id. */
|
|
90
|
+
costPerRecord?: Record<string, number>;
|
|
91
|
+
/** How to build a net-new record from a sourced row, per object type. */
|
|
92
|
+
create: Partial<Record<EnrichObjectType, AcquireCreateMap>>;
|
|
93
|
+
/** Net-new discovery params for API sources, by source id. */
|
|
94
|
+
discovery?: Record<string, AcquireDiscoveryConfig>;
|
|
95
|
+
/**
|
|
96
|
+
* Ownership rule stamped onto every created lead so it is never born
|
|
97
|
+
* ownerless. Absent = no auto-assignment (the CLI may still default to the
|
|
98
|
+
* portal's sole active owner). Shared with `reassign --assign-unowned`.
|
|
99
|
+
*/
|
|
100
|
+
assign?: AssignmentPolicy;
|
|
58
101
|
};
|
|
59
102
|
export declare const ENRICH_CONFIG_FILE_NAME = "enrich.config.json";
|
|
60
103
|
export declare const DEFAULT_STALE_DAYS = 90;
|
|
@@ -68,6 +111,25 @@ export declare function resolveCrmField(objectType: EnrichObjectType, name: stri
|
|
|
68
111
|
* the accepted values.
|
|
69
112
|
*/
|
|
70
113
|
export declare function parseEnrichConfig(raw: string): EnrichConfig;
|
|
114
|
+
/**
|
|
115
|
+
* Built-in zero-config presets for well-known sources, so the common Mode-A
|
|
116
|
+
* loop — "feed a Clay export, get the dedup/routing verdict" — needs no
|
|
117
|
+
* hand-authored config. These are **match-only** (empty `fields`): the value is
|
|
118
|
+
* the matched / unmatched / ambiguous routing and collision detection, not field
|
|
119
|
+
* writes. Drop an `enrich.config.json` next to your run to map fields for actual
|
|
120
|
+
* field enrichment; an explicit config always wins over a preset. Constructed
|
|
121
|
+
* directly (not via `parseEnrichConfig`) so the match-only shape is allowed.
|
|
122
|
+
*/
|
|
123
|
+
export declare const BUILTIN_ENRICH_PRESETS: Record<string, EnrichConfig>;
|
|
124
|
+
export declare function builtinEnrichPreset(source: string | undefined): EnrichConfig | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* Zero-config `enrich acquire` preset so every client gets targeted, deduped,
|
|
127
|
+
* metered lead-fill out of the box — no hand-authored enrich.config.json. The
|
|
128
|
+
* ICP (icp.json) supplies the targeting; this supplies sensible plumbing. The
|
|
129
|
+
* create mapping writes the prospect's LinkedIn URL into hs_linkedin_url, so
|
|
130
|
+
* each created contact strengthens future dedup. An explicit config always wins.
|
|
131
|
+
*/
|
|
132
|
+
export declare function builtinAcquirePreset(source: string | undefined): EnrichConfig | undefined;
|
|
71
133
|
export declare function loadEnrichConfig(path: string): EnrichConfig;
|
|
72
134
|
export declare function parseCsv(text: string): Array<Record<string, string>>;
|
|
73
135
|
export type EnrichSourceRecord = {
|
|
@@ -154,6 +216,51 @@ export type EnrichPlanResult = {
|
|
|
154
216
|
* current CRM value → apply-time compare-and-set rejects drifted records).
|
|
155
217
|
*/
|
|
156
218
|
export declare function buildEnrichPlan(options: BuildEnrichPlanOptions): EnrichPlanResult;
|
|
219
|
+
export type AcquireCounts = {
|
|
220
|
+
fetched: number;
|
|
221
|
+
matched: number;
|
|
222
|
+
ambiguous: number;
|
|
223
|
+
unmatched: number;
|
|
224
|
+
/** create_record ops emitted. */
|
|
225
|
+
created: number;
|
|
226
|
+
/** unmatched rows that would have been created but for the meter ceiling. */
|
|
227
|
+
withheldByMeter: number;
|
|
228
|
+
/** created ops that got an owner stamped (a subset of `created`). */
|
|
229
|
+
assigned: number;
|
|
230
|
+
/** created ops left ownerless (no policy, or policy could not place them). */
|
|
231
|
+
unassigned: number;
|
|
232
|
+
};
|
|
233
|
+
export type AcquirePlanResult = {
|
|
234
|
+
plan: PatchPlan;
|
|
235
|
+
counts: AcquireCounts;
|
|
236
|
+
estCostUsd: number;
|
|
237
|
+
};
|
|
238
|
+
export type BuildAcquirePlanOptions = {
|
|
239
|
+
config: EnrichConfig;
|
|
240
|
+
source: string;
|
|
241
|
+
snapshot: CanonicalGtmSnapshot;
|
|
242
|
+
records: EnrichSourceRecord[];
|
|
243
|
+
runLabel: string;
|
|
244
|
+
/** Meter ceiling: max create ops to emit. null/undefined = unlimited. */
|
|
245
|
+
maxRecords?: number | null;
|
|
246
|
+
now?: () => Date;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Lift a prospect/source payload into the routing attributes a territory rule
|
|
250
|
+
* reads. Tolerant of provider spellings (explorium vs pipe0); absent attributes
|
|
251
|
+
* stay undefined, so a rule that routes on them simply won't match (falling to
|
|
252
|
+
* the policy fallback) instead of mis-routing.
|
|
253
|
+
*/
|
|
254
|
+
export declare function acquireAssignmentContext(payload: Record<string, unknown>, companyName: string | undefined, accountOwnerByName: ReadonlyMap<string, string>): AssignmentContext;
|
|
255
|
+
/**
|
|
256
|
+
* Match each sourced record against the snapshot and route it: matched =
|
|
257
|
+
* already in the CRM (skip), ambiguous = a possible duplicate exists (skip —
|
|
258
|
+
* resolve-first never creates over ambiguity), unmatched = a net-new lead.
|
|
259
|
+
* Unmatched rows with a dedupe key and at least one mapped property become
|
|
260
|
+
* `create_record` operations, capped at `maxRecords` (the meter's headroom).
|
|
261
|
+
* Always a dry-run plan; nothing is written until apply.
|
|
262
|
+
*/
|
|
263
|
+
export declare function buildAcquirePlan(options: BuildAcquirePlanOptions): AcquirePlanResult;
|
|
157
264
|
/** Latest stamp per (objectType, objectId, field) across a source's runs. */
|
|
158
265
|
export declare function latestStamps(runs: EnrichRun[], source: string): Map<string, EnrichStamp>;
|
|
159
266
|
export declare function staleDaysFor(config: EnrichConfig, objectType: EnrichObjectType, field: string): number;
|