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.
@@ -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";
@@ -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>${"${vendorHeads}"}<th></th></tr></thead><tbody>${claimIds.map(matrixRow).join("")}</tbody></table>
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, front by front</h2>
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` / `report` / `refresh`), `rules`, `profiles`, `doctor`.
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.18); shapes are settling toward the 1.0 contract. A live
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 page captures (`captureMarket`,
89
- `loadCaptureTexts`), append-only observations (`ObservationSet`,
90
- `MarketObservation`, `ObservationStore` / `createFileObservationStore` —
91
- profile-scoped under `<home>/market/<category>`), and deterministic
92
- derivations: `computeFrontStates` / `diffFrontStates` (front rule v1),
93
- `assessAxes` / `pcaTop2` / `axisPosition` (axis discovery), and
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.22.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",