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/CHANGELOG.md +78 -1
- package/dist/bulkUpdate.d.ts +37 -0
- package/dist/bulkUpdate.js +315 -0
- package/dist/cli.js +161 -2
- package/dist/connector.d.ts +6 -0
- package/dist/connector.js +158 -17
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/market.d.ts +28 -0
- package/dist/market.js +3 -0
- package/dist/marketOverlay.d.ts +116 -0
- package/dist/marketOverlay.js +258 -0
- package/dist/marketReport.js +16 -3
- package/dist/marketScale.d.ts +42 -0
- package/dist/marketScale.js +68 -0
- package/dist/mcp.js +13 -2
- package/dist/types.d.ts +44 -0
- package/package.json +1 -1
- package/src/bulkUpdate.ts +375 -0
- package/src/cli.ts +183 -2
- package/src/connector.ts +169 -23
- package/src/index.ts +18 -0
- package/src/market.ts +32 -0
- package/src/marketOverlay.ts +410 -0
- package/src/marketReport.ts +20 -4
- package/src/marketScale.ts +111 -0
- package/src/mcp.ts +15 -2
- package/src/types.ts +39 -0
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/connector.d.ts
CHANGED
|
@@ -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 (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|