fullstackgtm 0.18.0 → 0.20.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/dist/cli.js CHANGED
@@ -20,10 +20,13 @@ import { sampleSnapshot } from "./sampleData.js";
20
20
  import { normalizeTranscript, parseCall, suggestCallDeal } from "./calls.js";
21
21
  import { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, loadCaptureTexts, loadMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, } from "./market.js";
22
22
  import { assessAxes, axesReportToText } from "./marketAxes.js";
23
+ import { computeDirectives, computeOverlayStats, directivesToPlan, overlayToMarkdown, } from "./marketOverlay.js";
24
+ import { computeScaleIndex, scaleReportToText } from "./marketScale.js";
23
25
  import { buildWorksheet, classifyMarket } from "./marketClassify.js";
24
26
  import { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
25
27
  import { DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
26
28
  import { resolveRecord } from "./resolve.js";
29
+ import { buildBulkUpdatePlan } from "./bulkUpdate.js";
27
30
  import { suggestValues } from "./suggest.js";
28
31
  function usage() {
29
32
  return `FullStackGTM — audit GTM data across providers, propose reviewable patch plans,
@@ -67,6 +70,8 @@ Usage:
67
70
  fullstackgtm market fronts [--run <label>] [--diff <prior-run>] [--json]
68
71
  fullstackgtm market axes [--run <label>] [--json]
69
72
  fullstackgtm market report [--run <label>] [--format md|html] [--out <path>]
73
+ fullstackgtm market overlay --snapshot <crm.json> [--calls <files>] [--save]
74
+ fullstackgtm market scale [--json]
70
75
  fullstackgtm market refresh [--run <label>] [--model m]
71
76
  the live competitive map: capture vendor pages (content-addressed),
72
77
  classify intensity per claim (LLM bring-your-own-key, or fill the
@@ -74,6 +79,18 @@ Usage:
74
79
  against the stored capture it cites before it's accepted — then
75
80
  compute deterministic front states and drift, render the field
76
81
  report. refresh = capture → classify → drift → report in one step
82
+ fullstackgtm bulk-update <account|contact|deal> --where <expr> [--where …] (--set <field>=<value> [--set …] | --archive | --create-task <text>) [--require <field>=<value> …] [--guard <object>:<where>[;<where>]:<none|some> …] [source options] [--save] [--json] [--out <path>]
83
+ governed generic writes: filter the snapshot
84
+ (field=value, field!=value, field~substr, field!~substr,
85
+ field:empty, field:notempty, '|' = any-of; canonical fields
86
+ like ownerId, stage, closeDate, amount; relational
87
+ pseudo-fields account.name/domain/ownerId/contactCount/
88
+ openDealStages on deals and contacts, contactCount/
89
+ openDealCount/openDealStages on accounts) into a dry-run
90
+ patch plan. The full filter is re-verified per record at
91
+ apply time (incl. mid-apply rechecks); equality filters
92
+ double as preconditions; per-record ops apply
93
+ all-or-nothing; guards assert cross-record conditions.
77
94
  fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
78
95
  derive values for requires_human_* placeholders
79
96
  from snapshot evidence, with confidence + reasons
@@ -751,7 +768,9 @@ function buildCallPlan(parsed, deal, proposed, current, extraNextSteps) {
751
768
  async function marketCommand(args) {
752
769
  const [subcommand, ...rest] = args;
753
770
  const configPath = () => resolve(process.cwd(), option(rest, "--config") ?? "market.config.json");
754
- if (!subcommand || subcommand === "--help") {
771
+ // Catch --help anywhere before loadMarketConfig/credential checks run —
772
+ // several subcommands (capture, refresh) have side effects on bare invocation.
773
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || rest.includes("--help") || rest.includes("-h")) {
755
774
  console.log(`Usage:
756
775
  market init --category <name> [--out <path>] write a starter market.config.json
757
776
  market capture [--config <path>] [--run <label>]
@@ -760,9 +779,24 @@ market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
760
779
  market observe --from <observations.json> [--unverified]
761
780
  market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
762
781
  market axes [--config <path>] [--run <label>] [--json]
782
+ market overlay --snapshot <crm.json> [--calls <parsed.json|manifest.json>]... [--prior-run <label>]
783
+ [--min-mentions N] [--promote-lift X] [--json] [--save --task-account <id>|--task-deal <id>]
784
+ market scale [--config <path>] [--json]
763
785
  market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
764
786
  market refresh [--run <label>] [--model m] capture → classify → fronts drift → HTML report
765
787
 
788
+ overlay is the directive layer: joins the map to YOUR CRM ground truth and
789
+ emits OCCUPY / PROMOTE / URGENT / RETREAT directives, each carrying ≥1
790
+ observation and ≥1 CRM statistic with its sample size. Claim mentions are
791
+ deterministic word-boundary matches of each claim's "terms" against call
792
+ documents (call parse output); small samples refuse to become strategy
793
+ (--min-mentions, default 3). --save turns directives into approval-gated
794
+ create_task operations through the normal plans → approve → apply gate.
795
+
796
+ scale prints the relative scale index that sizes the report's bubbles when
797
+ vendors carry scaleSignals (citable review counts / headcount / revenue —
798
+ a within-set index, never "market share" unqualified).
799
+
766
800
  axes runs the axis-discovery math: PCA over the vendor × claim intensity
767
801
  matrix (PC1 = the category's primary axis, PC2 = the max-differentiation
768
802
  direction orthogonal to it), triangulation of configured axes against the
@@ -967,7 +1001,76 @@ recomputed deterministically on every invocation — never stored.`);
967
1001
  console.log(axesReportToText(report));
968
1002
  return;
969
1003
  }
970
- throw new Error(`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, axes, report, refresh)`);
1004
+ if (subcommand === "scale") {
1005
+ const report = computeScaleIndex(config);
1006
+ if (rest.includes("--json")) {
1007
+ console.log(JSON.stringify(report, null, 2));
1008
+ return;
1009
+ }
1010
+ console.log(scaleReportToText(config, report));
1011
+ return;
1012
+ }
1013
+ if (subcommand === "overlay") {
1014
+ const set = await loadSet();
1015
+ const snapshotPath = option(rest, "--snapshot");
1016
+ if (!snapshotPath) {
1017
+ throw new Error("market overlay requires --snapshot <canonical-snapshot.json> (fullstackgtm snapshot --out it first) — directives need CRM ground truth");
1018
+ }
1019
+ const snapshot = JSON.parse(readFileSync(resolve(process.cwd(), snapshotPath), "utf8"));
1020
+ // --calls accepts ParsedCall JSON files (from `call parse --out`) and/or
1021
+ // manifest arrays [{path, dealId?}] linking calls to deals. Repeatable.
1022
+ const documents = [];
1023
+ const addParsedCall = (parsedPath, dealId) => {
1024
+ const parsed = JSON.parse(readFileSync(resolve(process.cwd(), parsedPath), "utf8"));
1025
+ const text = [
1026
+ ...(parsed.segments ?? []).map((segment) => segment.text ?? ""),
1027
+ ...(parsed.insights ?? []).map((insight) => `${insight.text ?? ""} ${insight.evidence ?? ""}`),
1028
+ ].join("\n");
1029
+ documents.push({ id: parsed.id ?? parsedPath, text, dealId, occurredAt: parsed.evidence?.[0]?.capturedAt });
1030
+ };
1031
+ for (let i = 0; i < rest.length; i += 1) {
1032
+ if (rest[i] !== "--calls")
1033
+ continue;
1034
+ const callsPath = rest[i + 1];
1035
+ if (!callsPath)
1036
+ throw new Error("--calls needs a path");
1037
+ const raw = JSON.parse(readFileSync(resolve(process.cwd(), callsPath), "utf8"));
1038
+ if (Array.isArray(raw)) {
1039
+ for (const entry of raw)
1040
+ addParsedCall(entry.path, entry.dealId);
1041
+ }
1042
+ else {
1043
+ addParsedCall(callsPath);
1044
+ }
1045
+ }
1046
+ const priorLabel = option(rest, "--prior-run");
1047
+ const priorSet = priorLabel ? await store.get(priorLabel) : null;
1048
+ if (priorLabel && !priorSet)
1049
+ throw new Error(`No observation run "${priorLabel}" for URGENT drift`);
1050
+ const stats = computeOverlayStats(config, snapshot, documents);
1051
+ const directives = computeDirectives(config, set, stats, {
1052
+ minMentions: numericOption(rest, "--min-mentions") ?? undefined,
1053
+ promoteLift: numericOption(rest, "--promote-lift") ?? undefined,
1054
+ priorSet: priorSet ?? undefined,
1055
+ });
1056
+ if (rest.includes("--json")) {
1057
+ console.log(JSON.stringify({ stats, directives }, null, 2));
1058
+ return;
1059
+ }
1060
+ console.log(overlayToMarkdown(stats, directives));
1061
+ if (rest.includes("--save")) {
1062
+ const taskAccount = option(rest, "--task-account");
1063
+ const taskDeal = option(rest, "--task-deal");
1064
+ if (!taskAccount && !taskDeal) {
1065
+ throw new Error("--save needs --task-account <id> or --task-deal <id>: directives become approval-gated create_task operations, and the CRM needs a record to hang them on (your own company's account record works well)");
1066
+ }
1067
+ const plan = directivesToPlan(config, set, directives, taskDeal ? { objectType: "deal", objectId: taskDeal } : { objectType: "account", objectId: taskAccount });
1068
+ const stored = await createFilePlanStore().save(plan);
1069
+ console.log(`Saved plan ${stored.plan.id} (${directives.length} directive task(s); approve via \`plans approve\`)`);
1070
+ }
1071
+ return;
1072
+ }
1073
+ throw new Error(`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, axes, overlay, scale, report, refresh)`);
971
1074
  }
972
1075
  /**
973
1076
  * The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
@@ -1001,6 +1104,51 @@ async function resolveCommand(args) {
1001
1104
  if (result.verdict !== "safe_to_create")
1002
1105
  process.exitCode = 2;
1003
1106
  }
1107
+ /**
1108
+ * Governed generic writes: build a dry-run patch plan from a snapshot filter
1109
+ * plus field assignments (or --archive). Never writes — approve and apply the
1110
+ * plan like any audit plan; compare-and-set protects every operation.
1111
+ */
1112
+ async function bulkUpdateCommand(args) {
1113
+ const [objectType, ...rest] = args;
1114
+ if (!objectType || !["account", "contact", "deal"].includes(objectType)) {
1115
+ throw new Error("Usage: fullstackgtm bulk-update <account|contact|deal> --where <field=value|field!=value|field~substr|field:empty|field:notempty> [--where …] (--set <field>=<value> [--set …] | --archive) [source options] [--reason <text>] [--max-operations <n>] [--save] [--out <path>] [--json]");
1116
+ }
1117
+ const where = repeatedOption(rest, "--where");
1118
+ const set = {};
1119
+ for (const pair of repeatedOption(rest, "--set")) {
1120
+ const separator = pair.indexOf("=");
1121
+ if (separator === -1)
1122
+ throw new Error(`--set must look like <field>=<value>, got "${pair}"`);
1123
+ set[pair.slice(0, separator)] = pair.slice(separator + 1);
1124
+ }
1125
+ const snapshot = await readSnapshot(rest);
1126
+ const plan = buildBulkUpdatePlan(snapshot, {
1127
+ objectType: objectType,
1128
+ where,
1129
+ set: Object.keys(set).length > 0 ? set : undefined,
1130
+ archive: rest.includes("--archive"),
1131
+ createTask: option(rest, "--create-task") ?? undefined,
1132
+ require: repeatedOption(rest, "--require"),
1133
+ guard: repeatedOption(rest, "--guard"),
1134
+ reason: option(rest, "--reason") ?? undefined,
1135
+ maxOperations: numericOption(rest, "--max-operations"),
1136
+ });
1137
+ const out = option(rest, "--out");
1138
+ if (out) {
1139
+ writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(plan, null, 2)}\n`);
1140
+ }
1141
+ if (rest.includes("--save")) {
1142
+ await createFilePlanStore().save(plan);
1143
+ console.error(`Saved plan ${plan.id} (${plan.operations.length} operations). Review with \`fullstackgtm plans show ${plan.id}\`, approve with \`fullstackgtm plans approve ${plan.id} --operations <ids|all>\`, then \`fullstackgtm apply --plan-id ${plan.id} --provider <name>\`.`);
1144
+ }
1145
+ if (rest.includes("--json")) {
1146
+ console.log(JSON.stringify(plan, null, 2));
1147
+ }
1148
+ else {
1149
+ console.log(patchPlanToMarkdown(plan));
1150
+ }
1151
+ }
1004
1152
  async function suggest(args) {
1005
1153
  const planId = option(args, "--plan-id");
1006
1154
  const planPath = option(args, "--plan");
@@ -1710,6 +1858,13 @@ export async function runCli(argv) {
1710
1858
  console.log(readPackageInfo().version);
1711
1859
  return;
1712
1860
  }
1861
+ // Commands without bespoke help fall back to the top-level usage on --help
1862
+ // instead of executing (audit used to silently run the sample audit).
1863
+ // call/market/bulk-update print their own richer help.
1864
+ if (!["call", "market", "bulk-update"].includes(command) && (args.includes("--help") || args.includes("-h"))) {
1865
+ console.log(usage());
1866
+ return;
1867
+ }
1713
1868
  if (command === "login") {
1714
1869
  await login(args);
1715
1870
  return;
@@ -1750,6 +1905,10 @@ export async function runCli(argv) {
1750
1905
  await resolveCommand(args);
1751
1906
  return;
1752
1907
  }
1908
+ if (command === "bulk-update") {
1909
+ await bulkUpdateCommand(args);
1910
+ return;
1911
+ }
1753
1912
  if (command === "market") {
1754
1913
  await marketCommand(args);
1755
1914
  return;
@@ -18,6 +18,12 @@ export type ApplyPatchPlanOptions = {
18
18
  * `readField`.
19
19
  */
20
20
  checkConflicts?: boolean;
21
+ /**
22
+ * For plans carrying a filter or guards: re-run the snapshot checks after
23
+ * the first applied write and then every N applied writes, so a record
24
+ * edited mid-apply is conflicted out instead of overwritten. Default 25.
25
+ */
26
+ recheckEvery?: number;
21
27
  };
22
28
  /**
23
29
  * Apply an approved subset of a patch plan through a connector.
package/dist/connector.js CHANGED
@@ -23,6 +23,124 @@ export async function applyPatchPlan(connector, plan, options) {
23
23
  const results = [];
24
24
  let attempted = 0;
25
25
  let applied = 0;
26
+ let appliedSinceRecheck = 0;
27
+ // Pass 0 — snapshot-backed checks: plan-level guards and per-record
28
+ // filter re-verification, both against a FRESH snapshot. Cross-record
29
+ // eligibility ("the account still has no open contractsent deal") cannot
30
+ // be checked through per-operation field reads.
31
+ //
32
+ // These checks are NOT one-shot: a concurrent edit can land mid-apply,
33
+ // after the initial verification but before later writes (the TOCTOU
34
+ // window). Providers offer no compare-and-swap, so the window cannot be
35
+ // closed — but it can be shrunk: re-run the snapshot checks after the
36
+ // first write and every `recheckEvery` writes, conflicting out any
37
+ // operation whose record went stale mid-run.
38
+ const needsSnapshot = ((plan.guards && plan.guards.length > 0) || plan.filter) && connector.fetchSnapshot;
39
+ const recheckEvery = Math.max(1, options.recheckEvery ?? 25);
40
+ const staleIds = new Set();
41
+ let guardFailure = null;
42
+ const refreshSnapshotChecks = async () => {
43
+ if (!needsSnapshot)
44
+ return;
45
+ const { evaluateGuard, eligibleIds } = await import("./bulkUpdate.js");
46
+ const liveSnapshot = await connector.fetchSnapshot();
47
+ if (plan.filter) {
48
+ const stillEligible = eligibleIds(liveSnapshot, plan.filter.objectType, plan.filter.where);
49
+ staleIds.clear();
50
+ for (const operation of plan.operations) {
51
+ if (!stillEligible.has(operation.objectId))
52
+ staleIds.add(operation.objectId);
53
+ }
54
+ }
55
+ for (const guard of plan.guards ?? []) {
56
+ const failure = evaluateGuard(liveSnapshot, guard);
57
+ if (failure) {
58
+ guardFailure = failure;
59
+ return;
60
+ }
61
+ }
62
+ };
63
+ await refreshSnapshotChecks();
64
+ if (guardFailure) {
65
+ for (const operation of plan.operations) {
66
+ results.push({
67
+ operationId: operation.id,
68
+ status: approved.has(operation.id) ? "conflict" : "skipped",
69
+ detail: approved.has(operation.id)
70
+ ? `${guardFailure} No operation in this plan was applied — re-plan against current data.`
71
+ : "Operation was not approved.",
72
+ });
73
+ }
74
+ return {
75
+ planId: plan.id,
76
+ provider: connector.provider,
77
+ startedAt,
78
+ finishedAt: new Date().toISOString(),
79
+ status: "rejected",
80
+ results,
81
+ };
82
+ }
83
+ const staleByFilter = plan.filter ? staleIds : null;
84
+ // Pass 1 — conflict detection. Re-read the written field (beforeValue
85
+ // compare-and-set) and any explicit preconditions. A conflicted operation
86
+ // never writes; if it carries a groupId, the whole group is poisoned so
87
+ // multi-record changes stay all-or-nothing.
88
+ const conflicts = new Map();
89
+ const poisonedGroups = new Set();
90
+ if (staleByFilter && staleByFilter.size > 0) {
91
+ for (const operation of plan.operations) {
92
+ if (!approved.has(operation.id) || !staleByFilter.has(operation.objectId))
93
+ continue;
94
+ conflicts.set(operation.id, {
95
+ operationId: operation.id,
96
+ status: "conflict",
97
+ detail: `Record ${operation.objectType}/${operation.objectId} no longer matches the plan's filter [${plan.filter.where.join(" AND ")}] — it changed since the plan was built. Re-plan against current data.`,
98
+ });
99
+ if (operation.groupId)
100
+ poisonedGroups.add(operation.groupId);
101
+ }
102
+ }
103
+ if (checkConflicts && connector.readField) {
104
+ for (const operation of plan.operations) {
105
+ if (!approved.has(operation.id) || conflicts.has(operation.id))
106
+ continue;
107
+ let conflict = null;
108
+ if (operation.field && FIELD_WRITE_OPERATIONS.has(operation.operation)) {
109
+ const current = await connector.readField(operation.objectType, operation.objectId, operation.field);
110
+ const expected = normalizeForComparison(operation.beforeValue);
111
+ const found = normalizeForComparison(current);
112
+ if (expected !== found) {
113
+ conflict = {
114
+ operationId: operation.id,
115
+ status: "conflict",
116
+ detail: `Value drifted since the plan was proposed: expected ${expected ?? "∅"}, found ${found ?? "∅"}. Re-run the audit.`,
117
+ providerData: { currentValue: current ?? null },
118
+ };
119
+ }
120
+ }
121
+ if (!conflict && operation.preconditions) {
122
+ for (const precondition of operation.preconditions) {
123
+ const current = await connector.readField(operation.objectType, operation.objectId, precondition.field);
124
+ const expected = normalizeForComparison(precondition.expectedValue);
125
+ const found = normalizeForComparison(current);
126
+ if (expected !== found) {
127
+ conflict = {
128
+ operationId: operation.id,
129
+ status: "conflict",
130
+ detail: `Precondition failed: ${precondition.field} was ${expected ?? "∅"} when the plan was built, found ${found ?? "∅"} now. The record changed — re-plan before writing.`,
131
+ providerData: { preconditionField: precondition.field, currentValue: current ?? null },
132
+ };
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ if (conflict) {
138
+ conflicts.set(operation.id, conflict);
139
+ if (operation.groupId)
140
+ poisonedGroups.add(operation.groupId);
141
+ }
142
+ }
143
+ }
26
144
  for (const operation of plan.operations) {
27
145
  if (!approved.has(operation.id)) {
28
146
  results.push({
@@ -41,30 +159,53 @@ export async function applyPatchPlan(connector, plan, options) {
41
159
  });
42
160
  continue;
43
161
  }
44
- if (checkConflicts &&
45
- connector.readField &&
46
- operation.field &&
47
- FIELD_WRITE_OPERATIONS.has(operation.operation)) {
48
- const current = await connector.readField(operation.objectType, operation.objectId, operation.field);
49
- const expected = normalizeForComparison(operation.beforeValue);
50
- const found = normalizeForComparison(current);
51
- if (expected !== found) {
52
- results.push({
53
- operationId: operation.id,
54
- status: "conflict",
55
- detail: `Value drifted since the plan was proposed: expected ${expected ?? "∅"}, found ${found ?? "∅"}. Re-run the audit.`,
56
- providerData: { currentValue: current ?? null },
57
- });
58
- continue;
59
- }
162
+ if (guardFailure) {
163
+ results.push({
164
+ operationId: operation.id,
165
+ status: "conflict",
166
+ detail: `${guardFailure} Detected mid-apply this and all remaining operations were not applied.`,
167
+ });
168
+ continue;
169
+ }
170
+ const conflict = conflicts.get(operation.id);
171
+ if (conflict) {
172
+ results.push(conflict);
173
+ continue;
174
+ }
175
+ if (staleByFilter && staleByFilter.has(operation.objectId)) {
176
+ results.push({
177
+ operationId: operation.id,
178
+ status: "conflict",
179
+ detail: `Record ${operation.objectType}/${operation.objectId} no longer matches the plan's filter [${plan.filter.where.join(" AND ")}] (changed mid-apply). Not applied — re-plan against current data.`,
180
+ });
181
+ if (operation.groupId)
182
+ poisonedGroups.add(operation.groupId);
183
+ continue;
184
+ }
185
+ if (operation.groupId && poisonedGroups.has(operation.groupId)) {
186
+ results.push({
187
+ operationId: operation.id,
188
+ status: "skipped",
189
+ detail: `Skipped: another operation in group ${operation.groupId} hit a conflict — the group applies all-or-nothing.`,
190
+ });
191
+ continue;
60
192
  }
61
193
  const resolved = override === undefined ? operation : { ...operation, afterValue: override };
62
194
  attempted += 1;
63
195
  try {
64
196
  const result = await connector.applyOperation(resolved);
65
197
  results.push(result);
66
- if (result.status === "applied")
198
+ if (result.status === "applied") {
67
199
  applied += 1;
200
+ appliedSinceRecheck += 1;
201
+ // shrink the TOCTOU window: first write fires a re-check (a
202
+ // concurrent editor reacting to our changes shows up immediately),
203
+ // then re-check on a fixed cadence
204
+ if (applied === 1 || appliedSinceRecheck >= recheckEvery) {
205
+ appliedSinceRecheck = 0;
206
+ await refreshSnapshotChecks();
207
+ }
208
+ }
68
209
  }
69
210
  catch (error) {
70
211
  results.push({
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { auditSnapshot, defaultPolicy } from "./audit.ts";
2
+ export { buildBulkUpdatePlan, parseWhere, type BulkUpdateOptions } from "./bulkUpdate.ts";
2
3
  export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, type FullstackgtmConfig, type LoadedConfig, } from "./config.ts";
3
4
  export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
4
5
  export { createHubspotConnector, type HubspotConnectorOptions } from "./connectors/hubspot.ts";
@@ -17,11 +18,13 @@ export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mapp
17
18
  export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
18
19
  export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, type CallDealSuggestion, type CallInsightType, type ExtractedCallInsight, type ParsedCall, type ParsedTranscriptSegment, } from "./calls.ts";
19
20
  export { sampleSnapshot } from "./sampleData.ts";
20
- export { DEFAULT_MODELS, DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, type CallScorecard, type LlmCredential, type LlmExtractedInsight, type LlmProvider, type Rubric, type ScoredDimension, } from "./llm.ts";
21
+ export { DEFAULT_MODELS, DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, type CallScorecard, type LlmCredential, type LlmExtractedInsight, type LlmProvider, type Rubric, type ScoredDimension, } from "./llm.ts";
21
22
  export { resolveRecord, type ResolveCandidate, type ResolveMatch, type ResolveResult } from "./resolve.ts";
22
- export { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, extractReadableText, loadCaptureTexts, loadMarketConfig, marketHome, normalizeForMatch, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, type CaptureEntry, type CaptureOptions, type ClaimFront, type ClaimIntensity, type FrontDrift, type FrontState, type MarketAxis, type MarketClaim, type MarketConfig, type MarketObservation, type MarketVendor, type ObservationConfidence, type ObservationSet, type ObservationStore, type SpanVerificationFailure, } from "./market.ts";
23
+ export { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, extractReadableText, loadCaptureTexts, loadMarketConfig, marketHome, normalizeForMatch, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, type CaptureEntry, type CaptureOptions, type ClaimFront, type ClaimIntensity, type FrontDrift, type FrontState, type MarketAxis, type MarketClaim, type MarketConfig, type MarketObservation, type MarketVendor, type ObservationConfidence, type ObservationSet, type ObservationStore, type ScaleSignal, type SpanVerificationFailure, } from "./market.ts";
23
24
  export { assessAxes, axesReportToText, axisPosition, messageBreadth, pcaTop2, pearson, type AxesReport, type AxisAssessment, type AxisPairing, type PrincipalComponent, } from "./marketAxes.ts";
24
25
  export { buildWorksheet, classifyMarket, type ClassifyMarketOptions, type ClassifyMarketResult, type MarketWorksheet, } from "./marketClassify.ts";
26
+ export { computeDirectives, computeOverlayStats, directivesToPlan, overlayToMarkdown, type CallDocument, type ClaimMentionStats, type DirectiveStat, type DirectiveType, type MarketDirective, type OverlayOptions, type OverlayStats, type VendorMentionStats, } from "./marketOverlay.ts";
27
+ export { computeScaleIndex, scaleReportToText, type ScaleReport, type VendorScale } from "./marketScale.ts";
25
28
  export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
26
29
  export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
27
30
  export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { auditSnapshot, defaultPolicy } from "./audit.js";
2
+ export { buildBulkUpdatePlan, parseWhere } from "./bulkUpdate.js";
2
3
  export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, } from "./config.js";
3
4
  export { applyPatchPlan } from "./connector.js";
4
5
  export { createHubspotConnector } from "./connectors/hubspot.js";
@@ -17,10 +18,12 @@ export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mapp
17
18
  export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
18
19
  export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, } from "./calls.js";
19
20
  export { sampleSnapshot } from "./sampleData.js";
20
- export { DEFAULT_MODELS, DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
21
+ export { DEFAULT_MODELS, DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
21
22
  export { resolveRecord } from "./resolve.js";
22
23
  export { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, extractReadableText, loadCaptureTexts, loadMarketConfig, marketHome, normalizeForMatch, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, } from "./market.js";
23
24
  export { assessAxes, axesReportToText, axisPosition, messageBreadth, pcaTop2, pearson, } from "./marketAxes.js";
24
25
  export { buildWorksheet, classifyMarket, } from "./marketClassify.js";
26
+ export { computeDirectives, computeOverlayStats, directivesToPlan, overlayToMarkdown, } from "./marketOverlay.js";
27
+ export { computeScaleIndex, scaleReportToText } from "./marketScale.js";
25
28
  export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
26
29
  export { suggestValues } from "./suggest.js";
package/dist/market.d.ts CHANGED
@@ -29,6 +29,30 @@ export type MarketClaim = {
29
29
  pricingStructure: string;
30
30
  /** Operational definition: how a reader judges LOUD vs QUIET vs ABSENT. */
31
31
  definition: string;
32
+ /**
33
+ * Exact terms buyers use for this claim, for deterministic mention
34
+ * matching against call transcripts (the overlay). No terms = no mention
35
+ * stats for this claim; matching is word-boundary, case-insensitive.
36
+ */
37
+ terms?: string[];
38
+ };
39
+ /**
40
+ * One public, citable scale signal for a vendor (G2 review count, LinkedIn
41
+ * headcount, disclosed revenue, self-reported customer count). The composite
42
+ * of several biased-in-different-directions signals sizes the report's
43
+ * bubbles — a RELATIVE scale index within the mapped set, never "market
44
+ * share" unqualified.
45
+ */
46
+ export type ScaleSignal = {
47
+ /** e.g. "g2_reviews", "linkedin_employees", "revenue_usd", "self_reported_customers". */
48
+ metric: string;
49
+ value: number;
50
+ unit: string;
51
+ sourceUrl: string;
52
+ /** Verbatim snippet containing the number — same evidence posture as observations. */
53
+ quote: string;
54
+ asOf: string;
55
+ caveat?: string;
32
56
  };
33
57
  export type MarketVendor = {
34
58
  id: string;
@@ -39,6 +63,10 @@ export type MarketVendor = {
39
63
  pricing: string | null;
40
64
  product: string[];
41
65
  };
66
+ /** Alternate names/spellings for deterministic mention matching. */
67
+ aliases?: string[];
68
+ /** Public scale signals; see ScaleSignal. */
69
+ scaleSignals?: ScaleSignal[];
42
70
  notes?: string;
43
71
  };
44
72
  export type MarketAxis = {
package/dist/market.js CHANGED
@@ -58,6 +58,9 @@ export function parseMarketConfig(raw) {
58
58
  if (axisIds.has(axis.id))
59
59
  throw new Error(`market config: duplicate axis id "${axis.id}"`);
60
60
  axisIds.add(axis.id);
61
+ if (!axis.negativePole || !axis.positivePole) {
62
+ throw new Error(`market config: axis "${axis.id}" needs negativePole and positivePole labels (the strategic map renders them as axis ends)`);
63
+ }
61
64
  for (const claimId of Object.keys(axis.claimScores ?? {})) {
62
65
  if (!claimIds.has(claimId)) {
63
66
  throw new Error(`market config: axis "${axis.id}" scores unknown claim "${claimId}"`);
@@ -0,0 +1,116 @@
1
+ import type { CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
2
+ import type { MarketConfig, ObservationSet } from "./market.ts";
3
+ /**
4
+ * The directive layer: the market map joined to the company's own ground
5
+ * truth. The map alone says what the category looks like; the overlay says
6
+ * what THIS company should do about it — and two companies running the same
7
+ * map see different directives, because the overlay is their own conversion
8
+ * fingerprint.
9
+ *
10
+ * Everything here is deterministic. Inputs are the observation store (front
11
+ * states), a CRM snapshot (won/lost deals), and call documents (transcript
12
+ * text, optionally linked to deals). Claim mentions are found by exact
13
+ * word-boundary term matching against each claim's configured `terms` —
14
+ * the same posture as the rest of the map: no model in the loop, every
15
+ * directive carries at least one observation and at least one CRM statistic
16
+ * with its sample size, and small samples refuse to become claims (the
17
+ * minimum-evidence thresholds are explicit options, not vibes).
18
+ */
19
+ export type CallDocument = {
20
+ id: string;
21
+ text: string;
22
+ /** Links the document to a deal for win/loss statistics; optional. */
23
+ dealId?: string;
24
+ occurredAt?: string;
25
+ };
26
+ export type ClaimMentionStats = {
27
+ claimId: string;
28
+ /** Documents whose text matches any of the claim's terms. */
29
+ mentionDocIds: string[];
30
+ /** Distinct deals among those documents (only docs with dealId count). */
31
+ mentionDealIds: string[];
32
+ wonDeals: number;
33
+ lostDeals: number;
34
+ /** won / (won + lost) among closed mentioned deals; null below any closure. */
35
+ winRateWhenMentioned: number | null;
36
+ };
37
+ export type VendorMentionStats = {
38
+ vendorId: string;
39
+ mentionDocIds: string[];
40
+ mentionDealIds: string[];
41
+ wonWhenMentioned: number;
42
+ lostWhenMentioned: number;
43
+ };
44
+ export type OverlayStats = {
45
+ documents: number;
46
+ documentsWithDeal: number;
47
+ deals: {
48
+ total: number;
49
+ closed: number;
50
+ won: number;
51
+ baselineWinRate: number | null;
52
+ };
53
+ claims: ClaimMentionStats[];
54
+ vendors: VendorMentionStats[];
55
+ };
56
+ export type DirectiveType = "occupy" | "promote" | "urgent" | "retreat";
57
+ export type DirectiveStat = {
58
+ name: string;
59
+ value: number | string;
60
+ n: number;
61
+ };
62
+ export type MarketDirective = {
63
+ id: string;
64
+ type: DirectiveType;
65
+ claimId: string;
66
+ title: string;
67
+ summary: string;
68
+ recommendation: string;
69
+ /** ≥1 observation id and ≥1 CRM stat — the spec's evidence-chain rule. */
70
+ observationIds: string[];
71
+ stats: DirectiveStat[];
72
+ };
73
+ export type OverlayOptions = {
74
+ /** Minimum mention documents before OCCUPY/PROMOTE may fire (default 3). */
75
+ minMentions?: number;
76
+ /** Minimum win-rate lift over baseline for PROMOTE (default 0.10). */
77
+ promoteLift?: number;
78
+ /** Minimum won deals in the corpus before RETREAT may fire (default 3). */
79
+ minWonDealsForRetreat?: number;
80
+ /** Prior run's observations: enables URGENT (front drift) directives. */
81
+ priorSet?: ObservationSet;
82
+ };
83
+ /**
84
+ * Deterministic claim/vendor mention statistics over a call corpus.
85
+ * Claims match on their configured `terms` (claims without terms simply have
86
+ * no mention stats — the directives that need mentions will not fire for
87
+ * them); vendors match on name + configured `aliases`.
88
+ */
89
+ export declare function computeOverlayStats(config: MarketConfig, snapshot: CanonicalGtmSnapshot, documents: CallDocument[]): OverlayStats;
90
+ /**
91
+ * Directive rules v1 — deterministic over (front states × overlay stats),
92
+ * with explicit minimum-evidence thresholds so small samples cannot mint
93
+ * strategy. Requires config.anchorVendor: directives are advice to someone.
94
+ *
95
+ * OCCUPY — open/vacant front the anchor doesn't own loudly, and buyers
96
+ * demonstrably talk about it (≥ minMentions documents).
97
+ * PROMOTE — anchor is quiet on a claim whose mentioned-deal win rate beats
98
+ * baseline by ≥ promoteLift (with ≥ minMentions mentioned deals).
99
+ * URGENT — a front the anchor is loud on drifted toward saturation since
100
+ * the prior run (requires priorSet).
101
+ * RETREAT — saturated front the anchor is loud on, with zero presence in
102
+ * won-deal conversations despite a corpus that contains wins.
103
+ */
104
+ export declare function computeDirectives(config: MarketConfig, set: ObservationSet, stats: OverlayStats, options?: OverlayOptions): MarketDirective[];
105
+ /**
106
+ * Emit directives as a standard dry-run patch plan: one approval-gated
107
+ * create_task per directive against a designated CRM record (the company's
108
+ * own account/deal record — directives are strategy tasks, and the CRM
109
+ * needs somewhere to hang them). Approving and applying goes through the
110
+ * normal plans → approve → apply gate; nothing here writes.
111
+ */
112
+ export declare function directivesToPlan(config: MarketConfig, set: ObservationSet, directives: MarketDirective[], target: {
113
+ objectType: "account" | "deal";
114
+ objectId: string;
115
+ }, now?: () => Date): PatchPlan;
116
+ export declare function overlayToMarkdown(stats: OverlayStats, directives: MarketDirective[]): string;