fullstackgtm 0.22.0 → 0.23.2
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 +75 -0
- package/README.md +33 -0
- package/dist/cli.js +456 -4
- package/dist/enrich.d.ts +220 -0
- package/dist/enrich.js +724 -0
- package/dist/enrichApollo.d.ts +59 -0
- package/dist/enrichApollo.js +192 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/marketReport.js +5 -5
- package/docs/api.md +51 -9
- package/llms.txt +40 -0
- package/package.json +1 -1
- package/src/cli.ts +525 -4
- package/src/enrich.ts +1016 -0
- package/src/enrichApollo.ts +250 -0
- package/src/index.ts +45 -0
- package/src/marketReport.ts +9 -9
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { CanonicalGtmSnapshot } from "./types.ts";
|
|
2
|
+
import type { EnrichObjectType, EnrichSourceRecord, EnrichWorkItem } from "./enrich.ts";
|
|
3
|
+
export type ApolloClientOptions = {
|
|
4
|
+
getApiKey: () => string | Promise<string>;
|
|
5
|
+
apiBaseUrl?: string;
|
|
6
|
+
/** Injectable fetch for testing — tests must never hit the real Apollo API. */
|
|
7
|
+
fetchImpl?: typeof fetch;
|
|
8
|
+
/** Max retries after a 429 (default 3). */
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
/** Injectable sleep for testing backoff without waiting. */
|
|
11
|
+
sleep?: (ms: number) => Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
export type ApolloClient = {
|
|
14
|
+
/** GET /api/v1/organizations/enrich?domain=… → response body, or null on no data. */
|
|
15
|
+
enrichOrganization(domain: string): Promise<Record<string, unknown> | null>;
|
|
16
|
+
/** POST /api/v1/people/match { email } → response body, or null on no data. */
|
|
17
|
+
matchPerson(email: string): Promise<Record<string, unknown> | null>;
|
|
18
|
+
};
|
|
19
|
+
export declare function createApolloClient(options: ApolloClientOptions): ApolloClient;
|
|
20
|
+
export type ApolloPullKey = {
|
|
21
|
+
objectType: EnrichObjectType;
|
|
22
|
+
/** "domain" for companies, "email" for contacts. */
|
|
23
|
+
key: "domain" | "email";
|
|
24
|
+
value: string;
|
|
25
|
+
};
|
|
26
|
+
export type ApolloPullResult = {
|
|
27
|
+
records: EnrichSourceRecord[];
|
|
28
|
+
/** Pull keys Apollo returned no data for. */
|
|
29
|
+
misses: ApolloPullKey[];
|
|
30
|
+
};
|
|
31
|
+
export type ApolloPullProgress = {
|
|
32
|
+
/** The pull key just processed — the run-store cursor value. */
|
|
33
|
+
lastKeyValue: string;
|
|
34
|
+
record?: EnrichSourceRecord;
|
|
35
|
+
miss?: ApolloPullKey;
|
|
36
|
+
};
|
|
37
|
+
export type ApolloPullOptions = {
|
|
38
|
+
/** Resume checkpoint: pull keys at or before this value are skipped. */
|
|
39
|
+
resumeAfter?: string | null;
|
|
40
|
+
/** Checkpoint callback after each pull key is processed. */
|
|
41
|
+
onProgress?: (progress: ApolloPullProgress) => void | Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Pull keys for an append run: records of the requested types where at least
|
|
45
|
+
* one configured field is blank and the pull key (companies: domain,
|
|
46
|
+
* contacts: email) is present. Deduplicated — two CRM companies sharing a
|
|
47
|
+
* domain produce ONE Apollo call, and the matcher then reports the collision.
|
|
48
|
+
*/
|
|
49
|
+
export declare function apolloPullKeysForAppend(snapshot: CanonicalGtmSnapshot, objectTypes: EnrichObjectType[], needsEnrichment: (objectType: EnrichObjectType, record: Record<string, unknown>) => boolean): ApolloPullKey[];
|
|
50
|
+
/** Pull keys for a refresh run: the stale work set mapped back to pull keys. */
|
|
51
|
+
export declare function apolloPullKeysForRefresh(snapshot: CanonicalGtmSnapshot, workSet: EnrichWorkItem[]): ApolloPullKey[];
|
|
52
|
+
/**
|
|
53
|
+
* Execute the pull: one Apollo call per key, normalized into source records.
|
|
54
|
+
* Sequential on purpose (enrichment endpoints rate-limit aggressively); the
|
|
55
|
+
* 429 backoff lives in the client. `onProgress` checkpoints the run-store
|
|
56
|
+
* cursor after every key so an interrupted pull resumes instead of re-paying
|
|
57
|
+
* for completed lookups.
|
|
58
|
+
*/
|
|
59
|
+
export declare function pullApolloRecords(client: ApolloClient, keys: ApolloPullKey[], options?: ApolloPullOptions): Promise<ApolloPullResult>;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apollo source client for the enrich layer: raw fetch against Apollo's REST
|
|
3
|
+
* enrichment endpoints (people match, organization enrich), bring-your-own
|
|
4
|
+
* key (`login apollo`), no SDK.
|
|
5
|
+
*
|
|
6
|
+
* The one genuinely new HTTP behavior lives here and ONLY here: enrichment
|
|
7
|
+
* APIs rate-limit aggressively, so requests retry on 429 with capped
|
|
8
|
+
* exponential backoff (honoring Retry-After when present). The shared
|
|
9
|
+
* connector contract is deliberately untouched.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_API_BASE_URL = "https://api.apollo.io";
|
|
12
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
13
|
+
const BASE_BACKOFF_MS = 500;
|
|
14
|
+
const MAX_BACKOFF_MS = 8_000;
|
|
15
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
function retryDelayMs(response, attempt) {
|
|
17
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
18
|
+
if (retryAfter) {
|
|
19
|
+
const seconds = Number(retryAfter);
|
|
20
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
21
|
+
return Math.min(seconds * 1000, MAX_BACKOFF_MS);
|
|
22
|
+
const at = Date.parse(retryAfter);
|
|
23
|
+
if (Number.isFinite(at))
|
|
24
|
+
return Math.min(Math.max(at - Date.now(), 0), MAX_BACKOFF_MS);
|
|
25
|
+
}
|
|
26
|
+
return Math.min(BASE_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS);
|
|
27
|
+
}
|
|
28
|
+
export function createApolloClient(options) {
|
|
29
|
+
const baseUrl = (options.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
30
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
31
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
32
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
33
|
+
async function request(path, init = {}) {
|
|
34
|
+
const apiKey = await options.getApiKey();
|
|
35
|
+
for (let attempt = 0;; attempt += 1) {
|
|
36
|
+
let response;
|
|
37
|
+
try {
|
|
38
|
+
response = await fetchImpl(`${baseUrl}${path}`, {
|
|
39
|
+
...init,
|
|
40
|
+
headers: {
|
|
41
|
+
"X-Api-Key": apiKey,
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
Accept: "application/json",
|
|
44
|
+
...(init.headers ?? {}),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const cause = error instanceof Error && error.cause instanceof Error ? `: ${error.cause.message}` : "";
|
|
50
|
+
throw new Error(`Cannot reach Apollo at ${baseUrl}${cause}. Check network access.`);
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 429 && attempt < maxRetries) {
|
|
53
|
+
await sleep(retryDelayMs(response, attempt));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (response.status === 404)
|
|
57
|
+
return null;
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const body = await response.text();
|
|
60
|
+
const exhausted = response.status === 429 ? ` (rate limited; ${maxRetries} retries exhausted)` : "";
|
|
61
|
+
throw new Error(`Apollo API error ${response.status}${exhausted}: ${body}`);
|
|
62
|
+
}
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
return text ? JSON.parse(text) : null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
async enrichOrganization(domain) {
|
|
69
|
+
return request(`/api/v1/organizations/enrich?domain=${encodeURIComponent(domain)}`, {
|
|
70
|
+
method: "GET",
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
async matchPerson(email) {
|
|
74
|
+
return request("/api/v1/people/match", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
body: JSON.stringify({ email }),
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function stringAt(payload, path) {
|
|
82
|
+
let current = payload;
|
|
83
|
+
for (const segment of path) {
|
|
84
|
+
if (!current || typeof current !== "object")
|
|
85
|
+
return undefined;
|
|
86
|
+
current = current[segment];
|
|
87
|
+
}
|
|
88
|
+
return typeof current === "string" && current ? current : undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Pull keys for an append run: records of the requested types where at least
|
|
92
|
+
* one configured field is blank and the pull key (companies: domain,
|
|
93
|
+
* contacts: email) is present. Deduplicated — two CRM companies sharing a
|
|
94
|
+
* domain produce ONE Apollo call, and the matcher then reports the collision.
|
|
95
|
+
*/
|
|
96
|
+
export function apolloPullKeysForAppend(snapshot, objectTypes, needsEnrichment) {
|
|
97
|
+
const keys = [];
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
for (const objectType of objectTypes) {
|
|
100
|
+
const records = objectType === "company" ? snapshot.accounts : snapshot.contacts;
|
|
101
|
+
const keyName = objectType === "company" ? "domain" : "email";
|
|
102
|
+
for (const record of records) {
|
|
103
|
+
const value = record[keyName]?.trim();
|
|
104
|
+
if (!value)
|
|
105
|
+
continue;
|
|
106
|
+
if (!needsEnrichment(objectType, record))
|
|
107
|
+
continue;
|
|
108
|
+
const dedupe = `${objectType}|${value.toLowerCase()}`;
|
|
109
|
+
if (seen.has(dedupe))
|
|
110
|
+
continue;
|
|
111
|
+
seen.add(dedupe);
|
|
112
|
+
keys.push({ objectType, key: keyName, value });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return keys;
|
|
116
|
+
}
|
|
117
|
+
/** Pull keys for a refresh run: the stale work set mapped back to pull keys. */
|
|
118
|
+
export function apolloPullKeysForRefresh(snapshot, workSet) {
|
|
119
|
+
const keys = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (const item of workSet) {
|
|
122
|
+
const records = item.objectType === "company" ? snapshot.accounts : snapshot.contacts;
|
|
123
|
+
const record = records.find((entry) => entry.id === item.objectId);
|
|
124
|
+
if (!record)
|
|
125
|
+
continue;
|
|
126
|
+
const keyName = item.objectType === "company" ? "domain" : "email";
|
|
127
|
+
const value = record[keyName]?.trim();
|
|
128
|
+
if (!value)
|
|
129
|
+
continue;
|
|
130
|
+
const dedupe = `${item.objectType}|${value.toLowerCase()}`;
|
|
131
|
+
if (seen.has(dedupe))
|
|
132
|
+
continue;
|
|
133
|
+
seen.add(dedupe);
|
|
134
|
+
keys.push({ objectType: item.objectType, key: keyName, value });
|
|
135
|
+
}
|
|
136
|
+
return keys;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Execute the pull: one Apollo call per key, normalized into source records.
|
|
140
|
+
* Sequential on purpose (enrichment endpoints rate-limit aggressively); the
|
|
141
|
+
* 429 backoff lives in the client. `onProgress` checkpoints the run-store
|
|
142
|
+
* cursor after every key so an interrupted pull resumes instead of re-paying
|
|
143
|
+
* for completed lookups.
|
|
144
|
+
*/
|
|
145
|
+
export async function pullApolloRecords(client, keys, options = {}) {
|
|
146
|
+
const records = [];
|
|
147
|
+
const misses = [];
|
|
148
|
+
// Resume: skip keys up to and including the checkpoint. An unknown
|
|
149
|
+
// checkpoint (snapshot changed since the interrupted run) restarts cleanly.
|
|
150
|
+
const resumeIndex = options.resumeAfter
|
|
151
|
+
? keys.findIndex((key) => key.value === options.resumeAfter)
|
|
152
|
+
: -1;
|
|
153
|
+
for (const key of keys.slice(resumeIndex + 1)) {
|
|
154
|
+
let record;
|
|
155
|
+
if (key.objectType === "company") {
|
|
156
|
+
const payload = await client.enrichOrganization(key.value);
|
|
157
|
+
const organization = payload?.organization;
|
|
158
|
+
if (organization) {
|
|
159
|
+
record = {
|
|
160
|
+
id: `apollo:org_${stringAt(payload, ["organization", "id"]) ?? key.value}`,
|
|
161
|
+
objectType: "company",
|
|
162
|
+
keys: {
|
|
163
|
+
domain: stringAt(payload, ["organization", "primary_domain"]) ?? key.value,
|
|
164
|
+
name: stringAt(payload, ["organization", "name"]),
|
|
165
|
+
},
|
|
166
|
+
payload: payload,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const payload = await client.matchPerson(key.value);
|
|
172
|
+
const person = payload?.person;
|
|
173
|
+
if (person) {
|
|
174
|
+
record = {
|
|
175
|
+
id: `apollo:person_${stringAt(payload, ["person", "id"]) ?? key.value}`,
|
|
176
|
+
objectType: "contact",
|
|
177
|
+
keys: {
|
|
178
|
+
email: stringAt(payload, ["person", "email"]) ?? key.value,
|
|
179
|
+
name: stringAt(payload, ["person", "name"]),
|
|
180
|
+
},
|
|
181
|
+
payload: payload,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (record)
|
|
186
|
+
records.push(record);
|
|
187
|
+
else
|
|
188
|
+
misses.push(key);
|
|
189
|
+
await options.onProgress?.({ lastKeyValue: key.value, record, miss: record ? undefined : key });
|
|
190
|
+
}
|
|
191
|
+
return { records, misses };
|
|
192
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDevic
|
|
|
11
11
|
export { createStripeConnector, type StripeConnectorOptions } from "./connectors/stripe.ts";
|
|
12
12
|
export { activeProfile, credentialsDir, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, listProfiles, resolveHubspotAccessToken, resolveHubspotConnection, setActiveProfile, storeCredential, type HubspotConnection, type StoredCredential, } from "./credentials.ts";
|
|
13
13
|
export { generateDemoSnapshot, type DemoSnapshotOptions } from "./demo.ts";
|
|
14
|
+
export { buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, enrichRunId, inferIngestObjectType, ingestKeyValue, latestStamps, loadEnrichConfig, matchSourceRecord, parseCsv, parseEnrichConfig, resolveCrmField, selectStaleWork, sourceValueAt, stagedSourceRecords, staleDaysFor, type BuildEnrichPlanOptions, type EnrichAmbiguity, type EnrichConfig, type EnrichCounts, type EnrichFieldConfig, type EnrichMatchConfig, type EnrichMode, type EnrichObjectType, type EnrichPlanResult, type EnrichRun, type EnrichRunStore, type EnrichSourceConfig, type EnrichSourceRecord, type EnrichStamp, type EnrichWorkItem, type MatchOutcome, } from "./enrich.ts";
|
|
15
|
+
export { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords, type ApolloClient, type ApolloClientOptions, type ApolloPullKey, type ApolloPullResult, } from "./enrichApollo.ts";
|
|
14
16
|
export { diffFindings, diffSnapshots, diffToMarkdown, type CollectionDiff, type FieldChange, type FindingsDrift, type RecordChange, type SnapshotDiff, } from "./diff.ts";
|
|
15
17
|
export { mergeSnapshots, type MergeConflict, type MergeMatch, type MergeReport, type MergeSuggestion, } from "./merge.ts";
|
|
16
18
|
export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDevic
|
|
|
11
11
|
export { createStripeConnector } from "./connectors/stripe.js";
|
|
12
12
|
export { activeProfile, credentialsDir, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, listProfiles, resolveHubspotAccessToken, resolveHubspotConnection, setActiveProfile, storeCredential, } from "./credentials.js";
|
|
13
13
|
export { generateDemoSnapshot } from "./demo.js";
|
|
14
|
+
export { buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, enrichRunId, inferIngestObjectType, ingestKeyValue, latestStamps, loadEnrichConfig, matchSourceRecord, parseCsv, parseEnrichConfig, resolveCrmField, selectStaleWork, sourceValueAt, stagedSourceRecords, staleDaysFor, } from "./enrich.js";
|
|
15
|
+
export { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords, } from "./enrichApollo.js";
|
|
14
16
|
export { diffFindings, diffSnapshots, diffToMarkdown, } from "./diff.js";
|
|
15
17
|
export { mergeSnapshots, } from "./merge.js";
|
|
16
18
|
export { createFilePlanStore } from "./planStore.js";
|
package/dist/marketReport.js
CHANGED
|
@@ -400,6 +400,9 @@ export function marketMapToHtml(config, set) {
|
|
|
400
400
|
`<span class="claim-meta">${e(claim.icp)} · ${e(claim.pricingStructure)}</span></th>${cells}` +
|
|
401
401
|
`<td class="front"><span class="chip chip-${state}">${state.toUpperCase()}</span></td></tr>`);
|
|
402
402
|
};
|
|
403
|
+
const vendorHeads = config.vendors
|
|
404
|
+
.map((vendor) => `<th class="vh${vendor.id === anchor ? " anchor-col" : ""}"><span>${e(vendor.name)}</span></th>`)
|
|
405
|
+
.join("");
|
|
403
406
|
// Claims grouped by front state, each group a collapsed <details> whose
|
|
404
407
|
// summary carries the stats a skimmer needs; the full matrix is one click
|
|
405
408
|
// away, not a wall the reader must climb to reach the takeaway.
|
|
@@ -418,7 +421,7 @@ export function marketMapToHtml(config, set) {
|
|
|
418
421
|
: 0;
|
|
419
422
|
const anchorNote = anchor ? ` · ${vendorNamesById.get(anchor) ?? anchor} loud on ${anchorLoud}` : "";
|
|
420
423
|
return `<details class="claim-group"><summary><b>${e(group.title)}</b> — ${claimIds.length} claim${claimIds.length === 1 ? "" : "s"} <span class="sum-soft">(${e(group.blurb)}${anchorNote})</span></summary>
|
|
421
|
-
<table><thead><tr><th></th>${
|
|
424
|
+
<table><thead><tr><th></th>${vendorHeads}<th></th></tr></thead><tbody>${claimIds.map(matrixRow).join("")}</tbody></table>
|
|
422
425
|
</details>`;
|
|
423
426
|
}).join("");
|
|
424
427
|
// The closing argument: walk from open ground to a reasoned target list.
|
|
@@ -481,9 +484,6 @@ export function marketMapToHtml(config, set) {
|
|
|
481
484
|
return `<details class="ev-group"><summary><b>${e(vendor.name)}</b> <span class="sum-soft">— ${items.length} quoted span${items.length === 1 ? "" : "s"}</span></summary>${items.join("")}</details>`;
|
|
482
485
|
})
|
|
483
486
|
.join("");
|
|
484
|
-
const vendorHeads = config.vendors
|
|
485
|
-
.map((vendor) => `<th class="vh${vendor.id === anchor ? " anchor-col" : ""}"><span>${e(vendor.name)}</span></th>`)
|
|
486
|
-
.join("");
|
|
487
487
|
return `<!doctype html>
|
|
488
488
|
<html lang="en"><head><meta charset="utf-8">
|
|
489
489
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -571,7 +571,7 @@ footer { margin-top:60px; border-top:1px solid var(--ink); padding-top:12px; fon
|
|
|
571
571
|
</header>
|
|
572
572
|
${axisHtml.strategicMap}
|
|
573
573
|
<section>
|
|
574
|
-
<h2>Claims
|
|
574
|
+
<h2>Market Claims</h2>
|
|
575
575
|
<div class="key">
|
|
576
576
|
<span class="lg"><i class="g g-loud"></i>LOUD — hero-level claim</span>
|
|
577
577
|
<span class="lg"><i class="g g-quiet"></i>QUIET — shipped, buried</span>
|
package/docs/api.md
CHANGED
|
@@ -59,8 +59,10 @@ release.
|
|
|
59
59
|
|
|
60
60
|
Commands: `login` / `logout`, `snapshot`, `audit`, `report`, `diff`, `merge`, `plans`,
|
|
61
61
|
`apply`, `suggest`, `call` (`parse` / `score` / `link` / `plan`), `resolve`,
|
|
62
|
+
`bulk-update`, `dedupe`, `reassign`, `fix`,
|
|
62
63
|
`market` (`init` / `capture` / `classify` / `worksheet` / `observe` / `fronts` /
|
|
63
|
-
`axes` / `
|
|
64
|
+
`axes` / `overlay` / `scale` / `report` / `refresh`),
|
|
65
|
+
`enrich` (`append` / `refresh` / `ingest` / `status`), `rules`, `profiles`, `doctor`.
|
|
64
66
|
Exit codes: `0` success · `1` error · `2` findings/regressions at the requested gate
|
|
65
67
|
(`--fail-on`, `--fail-on-new-findings`). `--json` everywhere; JSON output shapes are stable.
|
|
66
68
|
|
|
@@ -80,19 +82,59 @@ deliverable in markdown or self-contained HTML: severity counts, prose summary,
|
|
|
80
82
|
per-rule detail with capped examples, and next steps. `auditReportToMarkdown` /
|
|
81
83
|
`auditReportToHtml` expose the same rendering programmatically.
|
|
82
84
|
|
|
85
|
+
## Governed write verbs
|
|
86
|
+
|
|
87
|
+
Plan builders behind `bulk-update`, `dedupe`, and `reassign` — every one
|
|
88
|
+
emits a standard dry-run `PatchPlan` for the normal approve → apply chain:
|
|
89
|
+
|
|
90
|
+
- `buildBulkUpdatePlan(snapshot, options: BulkUpdateOptions)` with
|
|
91
|
+
`parseWhere` (filter expressions: `=`, `!=`, `~`, `!~`, `:empty`,
|
|
92
|
+
`:notempty`, `|` any-of, relational pseudo-fields) and
|
|
93
|
+
`isFilterableField`. Filters are re-verified per record at apply time;
|
|
94
|
+
`from:<sourceField>` values derive per record from the snapshot.
|
|
95
|
+
- `buildDedupePlan(snapshot, options: DedupeOptions)` with `dedupeKey` —
|
|
96
|
+
duplicate groups by normalized identity key, one `merge_records` per group,
|
|
97
|
+
deterministic survivor selection (`richest` / `oldest`).
|
|
98
|
+
- `buildReassignPlans(snapshot, options: ReassignOptions)` — one plan per
|
|
99
|
+
`ReassignObjectType`, account-lifted scoping, stage exclusions.
|
|
100
|
+
|
|
101
|
+
`fix` is CLI-only composition of existing surfaces (audit → suggest →
|
|
102
|
+
approve → apply for one rule).
|
|
103
|
+
|
|
104
|
+
## Enrich
|
|
105
|
+
|
|
106
|
+
Governed third-party enrichment (spec-first; `enrich.config.json` validated
|
|
107
|
+
by `parseEnrichConfig` / `loadEnrichConfig`, types `EnrichConfig`,
|
|
108
|
+
`EnrichFieldConfig`, `EnrichSourceConfig`): `buildEnrichPlan` matches staged
|
|
109
|
+
or pulled source records to CRM records (`matchSourceRecord` — ordered keys,
|
|
110
|
+
`MatchOutcome` of matched / unmatched / ambiguous) and emits fill-blanks-only
|
|
111
|
+
operations. `createFileEnrichRunStore` / `EnrichRunStore` is the profile-scoped
|
|
112
|
+
append-only run store (resume cursor, per-record/per-field `enrichedAt`
|
|
113
|
+
stamps read by `latestStamps` / `selectStaleWork`). `parseCsv` is the
|
|
114
|
+
dependency-free CSV intake; the Apollo client (`createApolloClient`,
|
|
115
|
+
`pullApolloRecords`, 429-aware with `Retry-After`) is the first `api`-kind
|
|
116
|
+
source.
|
|
117
|
+
|
|
83
118
|
## Market map
|
|
84
119
|
|
|
85
|
-
Newer surface (0.16–0.
|
|
120
|
+
Newer surface (0.16–0.23); shapes are settling toward the 1.0 contract. A live
|
|
86
121
|
model of the competitive category: claim taxonomy + vendor registry as a
|
|
87
122
|
reviewable `market.config.json` (`MarketConfig`, `MarketClaim`, `MarketVendor`,
|
|
88
|
-
`MarketAxis`), content-addressed
|
|
89
|
-
`loadCaptureTexts`), append-only observations
|
|
90
|
-
`MarketObservation`, `ObservationStore` /
|
|
91
|
-
profile-scoped under `<home>/market/<category>`),
|
|
92
|
-
derivations: `computeFrontStates` / `diffFrontStates`
|
|
93
|
-
`assessAxes` / `pcaTop2` / `axisPosition` (axis discovery),
|
|
123
|
+
`MarketAxis` — axes require `negativePole`/`positivePole` labels), content-addressed
|
|
124
|
+
page captures (`captureMarket`, `loadCaptureTexts`), append-only observations
|
|
125
|
+
(`ObservationSet`, `MarketObservation`, `ObservationStore` /
|
|
126
|
+
`createFileObservationStore` — profile-scoped under `<home>/market/<category>`),
|
|
127
|
+
and deterministic derivations: `computeFrontStates` / `diffFrontStates`
|
|
128
|
+
(front rule v1), `assessAxes` / `pcaTop2` / `axisPosition` (axis discovery),
|
|
129
|
+
`computeDirectives` / `computeOverlayStats` / `directivesToPlan`
|
|
130
|
+
(`market overlay` — observations × CRM snapshot × call corpus →
|
|
131
|
+
OCCUPY/PROMOTE/URGENT/RETREAT `MarketDirective`s, convertible to an
|
|
132
|
+
approval-gated plan of `create_task` operations), `computeScaleIndex` /
|
|
133
|
+
`scaleReportToText` (`market scale` — citable `ScaleSignal`s → within-set,
|
|
134
|
+
ACV-band-stratified revenue estimates with disclosed uncertainty), and
|
|
94
135
|
`marketMapToMarkdown` / `marketMapToHtml` (the field report; renders the
|
|
95
|
-
primary strategic 2×2 when `axes` / `primaryAxes` are configured
|
|
136
|
+
primary strategic 2×2 when `axes` / `primaryAxes` are configured, bubbles
|
|
137
|
+
area-proportional to estimated revenue share when scale signals exist).
|
|
96
138
|
|
|
97
139
|
Intensity readings are proposals: `classifyMarket` (LLM, bring-your-own-key,
|
|
98
140
|
provenance-marked) or `buildWorksheet` + `market observe` (agent/human). Every
|
package/llms.txt
CHANGED
|
@@ -44,9 +44,49 @@ elsewhere). Failed captures read UNOBSERVABLE, never ABSENT. `fronts --diff`
|
|
|
44
44
|
= deterministic front states + drift between runs; `axes` = PCA axis
|
|
45
45
|
discovery + orthogonality screen; `report` = self-contained HTML field
|
|
46
46
|
report; `refresh` = capture → classify → drift → report in one command.
|
|
47
|
+
`overlay --snapshot <crm.json> [--calls <files>]` joins observations to YOUR
|
|
48
|
+
CRM/calls (deterministic mention matching, no LLM) and emits OCCUPY/PROMOTE/
|
|
49
|
+
URGENT/RETREAT directives — each needs ≥1 observation + ≥1 CRM stat with
|
|
50
|
+
sample size; below thresholds = no directive; `--save` = approval-gated
|
|
51
|
+
create_task ops. `scale` = citable scaleSignals (sourceUrl + verbatim quote
|
|
52
|
+
each) → revenue-space estimates calibrated within-set by acvBand, disclosed
|
|
53
|
+
uncertainty; report bubbles ∝ estimated share only when signals exist.
|
|
47
54
|
Storage is profile-scoped under `<home>/market/<category>`. MCP:
|
|
48
55
|
`fullstackgtm_market_worksheet`, `fullstackgtm_market_observe`.
|
|
49
56
|
|
|
57
|
+
## Key invariants (governed write verbs)
|
|
58
|
+
|
|
59
|
+
`bulk-update <object> --where … (--set|--archive|--create-task)` filters the
|
|
60
|
+
snapshot into a dry-run plan; the FULL filter is re-verified per record at
|
|
61
|
+
apply time (plus mid-apply rechecks); equality filters double as
|
|
62
|
+
preconditions, `--require`/`--guard` add explicit ones, `--max-operations`
|
|
63
|
+
caps blast radius. `--set f=from:<source>` derives per-record values (empty
|
|
64
|
+
source = skip + count, never guess). `--archive` refuses records sharing an
|
|
65
|
+
identity key — merge with `dedupe` instead. `dedupe <object> --key
|
|
66
|
+
<domain|email|name>` = one merge_records op per duplicate group,
|
|
67
|
+
deterministic survivor (`--keep richest|oldest`); merges are irreversible and
|
|
68
|
+
stay low-confidence-capped at approval. `reassign --from <owner> --to
|
|
69
|
+
<owner>` = ownership handoff plans per object type; `--except-deal-stage`
|
|
70
|
+
also excludes records whose account has an open deal in that stage. `fix
|
|
71
|
+
--rule <id>` = audit one rule → suggest → approve at the confidence bar →
|
|
72
|
+
apply only with `--yes`. All four produce plans; none writes outside
|
|
73
|
+
approve → apply.
|
|
74
|
+
|
|
75
|
+
## Key invariants (enrich)
|
|
76
|
+
|
|
77
|
+
`fullstackgtm enrich` is governed enrichment: `append` pulls (Apollo, BYO key
|
|
78
|
+
via `login apollo`/APOLLO_API_KEY) or reads data staged by `ingest` (Clay CSV
|
|
79
|
+
exports / webhook payload JSON), matches source records to CRM records via
|
|
80
|
+
ordered keys in `enrich.config.json` (unique hit wins; ambiguity skips or
|
|
81
|
+
becomes `requires_human_record_selection` placeholders — never a coin flip),
|
|
82
|
+
and emits a fill-blanks-only patch plan through the normal dry-run → approve →
|
|
83
|
+
apply gate. No `--save` = dry-run diff, nothing written. `refresh` re-checks
|
|
84
|
+
stale fields the run-store ledger proves enrich stamped, proposing ops only
|
|
85
|
+
where the source value changed (beforeValue = current CRM value → apply-time
|
|
86
|
+
CAS). Conflict policy MVP is `never`; `system-only`/`always` are phase 2 and
|
|
87
|
+
refused explicitly. Run store (checkpoint + staleness ledger + `status`) is
|
|
88
|
+
profile-scoped under `<home>/enrich/runs`. No cron — scheduling is horizontal.
|
|
89
|
+
|
|
50
90
|
## Key invariants
|
|
51
91
|
|
|
52
92
|
- Reads are safe by default; nothing is written without explicit `--approve`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.2",
|
|
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",
|