fullstackgtm 0.43.0 → 0.45.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +378 -1
  2. package/README.md +37 -14
  3. package/dist/audit.d.ts +3 -1
  4. package/dist/audit.js +29 -2
  5. package/dist/cli/audit.d.ts +15 -0
  6. package/dist/cli/audit.js +392 -0
  7. package/dist/cli/auth.d.ts +80 -0
  8. package/dist/cli/auth.js +500 -0
  9. package/dist/cli/call.d.ts +1 -0
  10. package/dist/cli/call.js +373 -0
  11. package/dist/cli/capabilities.d.ts +30 -0
  12. package/dist/cli/capabilities.js +197 -0
  13. package/dist/cli/draft.d.ts +7 -0
  14. package/dist/cli/draft.js +87 -0
  15. package/dist/cli/enrich.d.ts +8 -0
  16. package/dist/cli/enrich.js +788 -0
  17. package/dist/cli/fix.d.ts +33 -0
  18. package/dist/cli/fix.js +344 -0
  19. package/dist/cli/help.d.ts +25 -0
  20. package/dist/cli/help.js +609 -0
  21. package/dist/cli/icp.d.ts +7 -0
  22. package/dist/cli/icp.js +229 -0
  23. package/dist/cli/init.d.ts +7 -0
  24. package/dist/cli/init.js +58 -0
  25. package/dist/cli/market.d.ts +1 -0
  26. package/dist/cli/market.js +391 -0
  27. package/dist/cli/plans.d.ts +5 -0
  28. package/dist/cli/plans.js +454 -0
  29. package/dist/cli/schedule.d.ts +8 -0
  30. package/dist/cli/schedule.js +479 -0
  31. package/dist/cli/shared.d.ts +70 -0
  32. package/dist/cli/shared.js +331 -0
  33. package/dist/cli/signals.d.ts +7 -0
  34. package/dist/cli/signals.js +412 -0
  35. package/dist/cli/suggest.d.ts +49 -0
  36. package/dist/cli/suggest.js +135 -0
  37. package/dist/cli/tam.d.ts +9 -0
  38. package/dist/cli/tam.js +387 -0
  39. package/dist/cli/ui.d.ts +121 -0
  40. package/dist/cli/ui.js +375 -0
  41. package/dist/cli.d.ts +1 -57
  42. package/dist/cli.js +104 -4556
  43. package/dist/connector.d.ts +15 -0
  44. package/dist/connector.js +35 -0
  45. package/dist/connectors/hubspot.d.ts +3 -1
  46. package/dist/connectors/hubspot.js +10 -3
  47. package/dist/connectors/outboxChannel.d.ts +64 -0
  48. package/dist/connectors/outboxChannel.js +170 -0
  49. package/dist/connectors/prospectSources.d.ts +22 -0
  50. package/dist/connectors/prospectSources.js +43 -0
  51. package/dist/connectors/salesforce.d.ts +3 -1
  52. package/dist/connectors/salesforce.js +12 -5
  53. package/dist/connectors/signalSources.d.ts +105 -0
  54. package/dist/connectors/signalSources.js +264 -0
  55. package/dist/connectors/theirstack.d.ts +84 -0
  56. package/dist/connectors/theirstack.js +125 -0
  57. package/dist/icp.d.ts +52 -4
  58. package/dist/icp.js +112 -35
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +1 -0
  61. package/dist/init.js +3 -0
  62. package/dist/judge.d.ts +2 -0
  63. package/dist/judge.js +6 -0
  64. package/dist/judgeEval.d.ts +7 -0
  65. package/dist/judgeEval.js +8 -1
  66. package/dist/marketClassify.d.ts +2 -0
  67. package/dist/marketClassify.js +7 -1
  68. package/dist/mcp-bin.js +2 -2
  69. package/dist/mcp.js +259 -166
  70. package/dist/runReport.d.ts +26 -0
  71. package/dist/runReport.js +15 -0
  72. package/dist/schedule.d.ts +80 -2
  73. package/dist/schedule.js +272 -5
  74. package/dist/signals.d.ts +54 -0
  75. package/dist/signals.js +64 -0
  76. package/dist/spoolFiles.d.ts +44 -0
  77. package/dist/spoolFiles.js +114 -0
  78. package/dist/tam.d.ts +225 -0
  79. package/dist/tam.js +470 -0
  80. package/dist/types.d.ts +11 -0
  81. package/docs/api.md +91 -11
  82. package/docs/outbox-format.md +92 -0
  83. package/docs/recipes.md +37 -0
  84. package/docs/roadmap-to-1.0.md +31 -1
  85. package/docs/signal-spool-format.md +175 -0
  86. package/docs/tam.md +195 -0
  87. package/llms.txt +83 -11
  88. package/package.json +1 -1
  89. package/skills/fullstackgtm/SKILL.md +4 -3
  90. package/src/audit.ts +27 -1
  91. package/src/cli/audit.ts +447 -0
  92. package/src/cli/auth.ts +549 -0
  93. package/src/cli/call.ts +398 -0
  94. package/src/cli/capabilities.ts +215 -0
  95. package/src/cli/draft.ts +101 -0
  96. package/src/cli/enrich.ts +885 -0
  97. package/src/cli/fix.ts +372 -0
  98. package/src/cli/help.ts +664 -0
  99. package/src/cli/icp.ts +265 -0
  100. package/src/cli/init.ts +65 -0
  101. package/src/cli/market.ts +423 -0
  102. package/src/cli/plans.ts +523 -0
  103. package/src/cli/schedule.ts +526 -0
  104. package/src/cli/shared.ts +375 -0
  105. package/src/cli/signals.ts +434 -0
  106. package/src/cli/suggest.ts +151 -0
  107. package/src/cli/tam.ts +435 -0
  108. package/src/cli/ui.ts +426 -0
  109. package/src/cli.ts +103 -5170
  110. package/src/connector.ts +46 -0
  111. package/src/connectors/hubspot.ts +14 -2
  112. package/src/connectors/outboxChannel.ts +202 -0
  113. package/src/connectors/prospectSources.ts +57 -0
  114. package/src/connectors/salesforce.ts +18 -2
  115. package/src/connectors/signalSources.ts +314 -0
  116. package/src/connectors/theirstack.ts +170 -0
  117. package/src/icp.ts +120 -34
  118. package/src/index.ts +32 -0
  119. package/src/init.ts +3 -0
  120. package/src/judge.ts +7 -0
  121. package/src/judgeEval.ts +8 -1
  122. package/src/marketClassify.ts +8 -1
  123. package/src/mcp-bin.ts +2 -2
  124. package/src/mcp.ts +130 -57
  125. package/src/runReport.ts +39 -0
  126. package/src/schedule.ts +330 -7
  127. package/src/signals.ts +90 -0
  128. package/src/spoolFiles.ts +116 -0
  129. package/src/tam.ts +654 -0
  130. package/src/types.ts +12 -0
@@ -0,0 +1,423 @@
1
+ // Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { createFilePlanStore } from "../planStore.ts";
6
+ import type { ParsedCall } from "../calls.ts";
7
+ import { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, loadCaptureTexts, loadMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, type ObservationSet } from "../market.ts";
8
+ import { assessAxes, axesReportToText } from "../marketAxes.ts";
9
+ import { computeDirectives, computeOverlayStats, directivesToPlan, overlayToMarkdown, type CallDocument } from "../marketOverlay.ts";
10
+ import { computeScaleIndex, scaleReportToText } from "../marketScale.ts";
11
+ import { suggestMarketConfig } from "../marketTaxonomy.ts";
12
+ import { buildWorksheet, classifyMarket } from "../marketClassify.ts";
13
+ import { marketMapToHtml, marketMapToMarkdown } from "../marketReport.ts";
14
+ import type { CanonicalGtmSnapshot } from "../types.ts";
15
+ import { isSpoolPath, readSpoolPath } from "../spoolFiles.ts";
16
+ import { numericOption, option, repeatedOption, requireLlmCredential, saveRequested } from "./shared.ts";
17
+ import { createStatusLine } from "./ui.ts";
18
+ import { unknownSubcommandError } from "./suggest.ts";
19
+
20
+
21
+ /**
22
+ * The market map: claim taxonomy in a reviewable config file, page captures
23
+ * and append-only observations under the profile home, deterministic front
24
+ * states and reports computed from the store. Intensity readings enter as
25
+ * proposals through two channels — `classify` (LLM, bring-your-own-key, the
26
+ * call-intelligence pattern) and `worksheet`/`observe` (an agent or human
27
+ * fills the worksheet) — and BOTH pass the same mechanical gate: every quoted
28
+ * span is verified verbatim against the stored capture it cites.
29
+ */
30
+ const MARKET_SUBCOMMANDS = [
31
+ "init", "capture", "classify", "worksheet", "observe", "fronts", "axes", "overlay", "scale", "report", "refresh",
32
+ ];
33
+
34
+ export async function marketCommand(args: string[]) {
35
+ const [subcommand, ...rest] = args;
36
+ const configPath = () => resolve(process.cwd(), option(rest, "--config") ?? "market.config.json");
37
+
38
+ // Catch --help anywhere before loadMarketConfig/credential checks run —
39
+ // several subcommands (capture, refresh) have side effects on bare invocation.
40
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || rest.includes("--help") || rest.includes("-h")) {
41
+ console.log(`Usage:
42
+ market init --category <name> [--out <path>] write a starter market.config.json
43
+ market init --category <name> --auto --vendor <url> [--vendor <url>...] [--anchor <url>] [--max-claims n]
44
+ LLM-propose vendors + claim taxonomy from seed pages (needs an API key)
45
+ market capture [--config <path>] [--run <label>]
46
+ market classify [--run <label>] [--capture-run <label>] [--vendor <id>] [--model m] [--out <path>]
47
+ market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
48
+ market observe --from <observations.json|sets.jsonl|spool-dir> [--unverified]
49
+ market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
50
+ market axes [--config <path>] [--run <label>] [--json]
51
+ market overlay --snapshot <crm.json> [--calls <parsed.json|manifest.json>]... [--prior-run <label>]
52
+ [--min-mentions N] [--promote-lift X] [--json] [--save --task-account <id>|--task-deal <id>]
53
+ market scale [--config <path>] [--json]
54
+ market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
55
+ market refresh [--run <label>] [--model m] capture → classify → fronts drift → HTML report
56
+
57
+ overlay is the directive layer: joins the map to YOUR CRM ground truth and
58
+ emits OCCUPY / PROMOTE / URGENT / RETREAT directives, each carrying ≥1
59
+ observation and ≥1 CRM statistic with its sample size. Claim mentions are
60
+ deterministic word-boundary matches of each claim's "terms" against call
61
+ documents (call parse output); small samples refuse to become strategy
62
+ (--min-mentions, default 3). --save turns directives into approval-gated
63
+ create_task operations through the normal plans → approve → apply gate.
64
+
65
+ scale prints the relative scale index that sizes the report's bubbles when
66
+ vendors carry scaleSignals (citable review counts / headcount / revenue —
67
+ a within-set index, never "market share" unqualified).
68
+
69
+ axes runs the axis-discovery math: PCA over the vendor × claim intensity
70
+ matrix (PC1 = the category's primary axis, PC2 = the max-differentiation
71
+ direction orthogonal to it), triangulation of configured axes against the
72
+ PCs, and an orthogonality screen (|r|>0.75 = one axis twice). Axes live in
73
+ the config as claim-scoring rubrics; the report's strategic map and axis
74
+ lab render from them.
75
+
76
+ classify uses your Anthropic/OpenAI key (like call parse) to read the stored
77
+ captures and propose intensity readings; worksheet is the no-key path (an
78
+ agent or human fills it, submits via observe). Either way, every quoted span
79
+ is verified character-for-character against the capture it cites before the
80
+ observation is accepted — quotes that aren't on the page bounce.
81
+
82
+ The taxonomy (vendors + claims) is config you review and version; captures
83
+ and observations live under ~/.fullstackgtm/market/<category> (profile-scoped,
84
+ one client's category intel never bleeds into another's). Front states are
85
+ recomputed deterministically on every invocation — never stored.`);
86
+ return;
87
+ }
88
+
89
+ if (subcommand === "init") {
90
+ const category = option(rest, "--category");
91
+ if (!category) throw new Error("market init requires --category <name>");
92
+ const outPath = resolve(process.cwd(), option(rest, "--out") ?? "market.config.json");
93
+ if (existsSync(outPath)) throw new Error(`${outPath} already exists — refusing to overwrite`);
94
+
95
+ if (rest.includes("--auto")) {
96
+ const vendorUrls = repeatedOption(rest, "--vendor");
97
+ if (vendorUrls.length === 0) {
98
+ throw new Error("market init --auto requires at least one --vendor <url> (the competitor homepages to seed from)");
99
+ }
100
+ const anchorUrl = option(rest, "--anchor");
101
+ const credential = await requireLlmCredential("market classify");
102
+ console.error(`Capturing ${vendorUrls.length} seed page(s) and proposing a claim taxonomy with ${credential.provider}…`);
103
+ const { config, unreadableVendorIds, model } = await suggestMarketConfig({
104
+ category,
105
+ vendors: vendorUrls.map((url) => ({ url, anchor: anchorUrl ? url === anchorUrl : false })),
106
+ llm: { ...credential, model: option(rest, "--model") ?? undefined },
107
+ maxClaims: numericOption(rest, "--max-claims"),
108
+ });
109
+ writeFileSync(outPath, `${JSON.stringify(config, null, 2)}\n`);
110
+ if (unreadableVendorIds.length > 0) {
111
+ console.error(`Note: no readable text for ${unreadableVendorIds.join(", ")} — excluded from taxonomy grounding.`);
112
+ }
113
+ console.log(
114
+ `Wrote ${outPath}: ${config.vendors.length} vendors, ${config.claims.length} proposed claims (${model}). Review it, then: fullstackgtm market refresh`,
115
+ );
116
+ return;
117
+ }
118
+
119
+ writeFileSync(outPath, `${JSON.stringify(starterMarketConfig(category), null, 2)}\n`);
120
+ console.log(`Wrote ${outPath}. Fill in vendors and claims, then: fullstackgtm market capture`);
121
+ return;
122
+ }
123
+
124
+ // Validate the subcommand BEFORE loading market.config.json — a typo'd
125
+ // subcommand used to die with the config's ENOENT instead of a did-you-mean.
126
+ if (!MARKET_SUBCOMMANDS.includes(subcommand)) {
127
+ throw unknownSubcommandError("market", subcommand, MARKET_SUBCOMMANDS);
128
+ }
129
+
130
+ const config = loadMarketConfig(configPath());
131
+ const store = createFileObservationStore(config.category);
132
+
133
+ if (subcommand === "capture") {
134
+ const result = await captureMarket(config, { runLabel: option(rest, "--run") ?? "run-1" });
135
+ for (const entry of result.entries) {
136
+ const flag = entry.captureHash && entry.textChars > 500 ? "" : " <-- thin/empty";
137
+ console.log(
138
+ `${entry.vendorId.padEnd(16)} ${entry.kind.padEnd(8)} ${String(entry.httpStatus ?? "ERR").padEnd(4)} ${String(entry.textChars).padStart(7)} chars ${entry.url}${flag}`,
139
+ );
140
+ }
141
+ console.log(`\nmanifest: ${result.manifestPath}`);
142
+ return;
143
+ }
144
+
145
+ if (subcommand === "observe") {
146
+ const fromPath = option(rest, "--from");
147
+ if (!fromPath) throw new Error("market observe requires --from <observations.json>");
148
+ // Additive spool support (docs/signal-spool-format.md): a *.jsonl file (one
149
+ // ObservationSet envelope per line) or a directory of *.jsonl / *.json spool
150
+ // files, each envelope validated + appended through the SAME gates as the
151
+ // single-file path. A plain .json file parses exactly as before.
152
+ const abs = resolve(process.cwd(), fromPath);
153
+ const sets: ObservationSet[] = isSpoolPath(abs)
154
+ ? readSpoolPath(abs, "market observe --from").map((entry) => entry.row as unknown as ObservationSet)
155
+ : [JSON.parse(readFileSync(abs, "utf8")) as ObservationSet];
156
+ for (const set of sets) {
157
+ const problems = validateObservationSet(config, set);
158
+ if (problems.length > 0) {
159
+ console.error(`Rejected: ${problems.length} problem(s)`);
160
+ for (const problem of problems.slice(0, 20)) console.error(` - ${problem}`);
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+ if (!rest.includes("--unverified")) {
165
+ const { textByHash } = loadCaptureTexts(config.category);
166
+ const failures = verifyEvidenceSpans(set.observations, textByHash);
167
+ if (failures.length > 0) {
168
+ console.error(`Rejected: ${failures.length} evidence span(s) failed verification against the stored captures`);
169
+ for (const failure of failures.slice(0, 20)) {
170
+ console.error(` - ${failure.vendorId} × ${failure.claimId}: ${failure.problem}`);
171
+ }
172
+ console.error("Quotes must be copied verbatim from the captured pages. (--unverified skips this gate when the captures genuinely live elsewhere.)");
173
+ process.exitCode = 1;
174
+ return;
175
+ }
176
+ }
177
+ await store.append(set);
178
+ console.log(`Appended ${set.runLabel}: ${set.observations.length} observations (${set.extractor})`);
179
+ }
180
+ return;
181
+ }
182
+
183
+ if (subcommand === "worksheet") {
184
+ const vendorId = option(rest, "--vendor");
185
+ if (!vendorId) throw new Error("market worksheet requires --vendor <id>");
186
+ const worksheet = buildWorksheet(config, vendorId, { captureRun: option(rest, "--capture-run") ?? undefined });
187
+ const outPath = option(rest, "--out");
188
+ const payload = `${JSON.stringify(worksheet, null, 2)}\n`;
189
+ if (outPath) {
190
+ writeFileSync(resolve(process.cwd(), outPath), payload);
191
+ console.log(`Wrote ${outPath} (${worksheet.pages.length} captured pages, ${worksheet.claims.length} claims)`);
192
+ } else {
193
+ console.log(payload);
194
+ }
195
+ return;
196
+ }
197
+
198
+ if (subcommand === "classify") {
199
+ const credential = await requireLlmCredential("market classify");
200
+ const vendorFilter = option(rest, "--vendor");
201
+ const outPath = option(rest, "--out");
202
+ if (vendorFilter && !outPath) {
203
+ throw new Error(
204
+ "market classify --vendor produces a partial set (coverage validation would reject it) — pass --out <path> to inspect/merge it by hand",
205
+ );
206
+ }
207
+ const classifyStatus = createStatusLine();
208
+ let result: Awaited<ReturnType<typeof classifyMarket>>;
209
+ try {
210
+ result = await classifyMarket(config, {
211
+ llm: { ...credential, model: option(rest, "--model") ?? undefined },
212
+ runLabel: option(rest, "--run") ?? option(rest, "--capture-run") ?? "run-1",
213
+ captureRun: option(rest, "--capture-run") ?? undefined,
214
+ vendors: vendorFilter ? [vendorFilter] : undefined,
215
+ onVendor: classifyStatus.active
216
+ ? (done, total, vendorId) =>
217
+ classifyStatus.set(`Classifying vendor ${done + 1}/${total} · ${vendorId} · ${credential.provider}`)
218
+ : undefined,
219
+ });
220
+ } finally {
221
+ classifyStatus.done();
222
+ }
223
+ if (result.retriedVendorIds.length > 0) {
224
+ console.error(`Span verification bounced ${result.retriedVendorIds.join(", ")} once; retry passed.`);
225
+ }
226
+ if (outPath) {
227
+ writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(result.set, null, 2)}\n`);
228
+ console.log(`Wrote ${outPath}: ${result.set.observations.length} verified observations (${result.set.extractor})`);
229
+ return;
230
+ }
231
+ const problems = validateObservationSet(config, result.set);
232
+ if (problems.length > 0) {
233
+ throw new Error(`Classified set failed validation: ${problems.slice(0, 5).join("; ")}`);
234
+ }
235
+ await store.append(result.set);
236
+ console.log(
237
+ `Appended ${result.set.runLabel}: ${result.set.observations.length} observations, every span verified (${result.set.extractor})`,
238
+ );
239
+ return;
240
+ }
241
+
242
+ if (subcommand === "refresh") {
243
+ const credential = await requireLlmCredential("market classify");
244
+ const runLabel = option(rest, "--run") ?? `run-${new Date().toISOString().slice(0, 10)}`;
245
+ const prior = await store.latest();
246
+ console.log(`Capturing ${config.vendors.length} vendors as ${runLabel}…`);
247
+ const captured = await captureMarket(config, { runLabel });
248
+ const failed = captured.entries.filter((entry) => !entry.captureHash);
249
+ if (failed.length > 0) console.log(`${failed.length} page(s) failed/empty — affected cells will verify against remaining pages or read unobservable.`);
250
+ console.log(`Classifying with ${credential.provider}…`);
251
+ const refreshStatus = createStatusLine();
252
+ let result: Awaited<ReturnType<typeof classifyMarket>>;
253
+ try {
254
+ result = await classifyMarket(config, {
255
+ llm: { ...credential, model: option(rest, "--model") ?? undefined },
256
+ runLabel,
257
+ captureRun: runLabel,
258
+ onVendor: refreshStatus.active
259
+ ? (done, total, vendorId) =>
260
+ refreshStatus.set(`Classifying vendor ${done + 1}/${total} · ${vendorId} · ${credential.provider}`)
261
+ : undefined,
262
+ });
263
+ } finally {
264
+ refreshStatus.done();
265
+ }
266
+ await store.append(result.set);
267
+ const fronts = computeFrontStates(config, result.set);
268
+ if (prior) {
269
+ const drift = diffFrontStates(computeFrontStates(config, prior), fronts);
270
+ if (drift.length === 0) console.log(`No front changes since ${prior.runLabel}.`);
271
+ for (const change of drift) console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
272
+ }
273
+ const outPath = option(rest, "--out") ?? `${config.category}-${runLabel}.html`;
274
+ writeFileSync(resolve(process.cwd(), outPath), marketMapToHtml(config, result.set));
275
+ console.log(`Wrote ${outPath}`);
276
+ return;
277
+ }
278
+
279
+ const loadSet = async (): Promise<ObservationSet> => {
280
+ const runLabel = option(rest, "--run");
281
+ const set = runLabel ? await store.get(runLabel) : await store.latest();
282
+ if (!set) {
283
+ throw new Error(
284
+ runLabel
285
+ ? `No observation run "${runLabel}" for ${config.category}`
286
+ : `No observations stored for ${config.category} — run market observe --from <file> first`,
287
+ );
288
+ }
289
+ return set;
290
+ };
291
+
292
+ if (subcommand === "fronts") {
293
+ const set = await loadSet();
294
+ const fronts = computeFrontStates(config, set);
295
+ const priorLabel = option(rest, "--diff");
296
+ const prior = priorLabel ? await store.get(priorLabel) : null;
297
+ if (priorLabel && !prior) throw new Error(`No observation run "${priorLabel}" to diff against`);
298
+ const drift = prior ? diffFrontStates(computeFrontStates(config, prior), fronts) : null;
299
+ if (rest.includes("--json")) {
300
+ console.log(JSON.stringify({ runLabel: set.runLabel, fronts, drift }, null, 2));
301
+ return;
302
+ }
303
+ for (const front of fronts) {
304
+ const owner = front.state === "owned" ? ` → ${front.loudVendorIds[0]}` : "";
305
+ console.log(`${front.state.toUpperCase().padEnd(10)} ${front.claimId}${owner}`);
306
+ }
307
+ if (drift) {
308
+ console.log("");
309
+ if (drift.length === 0) console.log(`No front changes since ${priorLabel}.`);
310
+ for (const change of drift) console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
311
+ }
312
+ return;
313
+ }
314
+
315
+ if (subcommand === "report") {
316
+ const set = await loadSet();
317
+ const format = option(rest, "--format") ?? "md";
318
+ const output = format === "html" ? marketMapToHtml(config, set) : marketMapToMarkdown(config, set);
319
+ const outPath = option(rest, "--out");
320
+ if (outPath) {
321
+ writeFileSync(resolve(process.cwd(), outPath), output);
322
+ console.log(`Wrote ${outPath}`);
323
+ } else {
324
+ console.log(output);
325
+ }
326
+ return;
327
+ }
328
+
329
+ if (subcommand === "axes") {
330
+ const set = await loadSet();
331
+ const report = assessAxes(config, set);
332
+ if (rest.includes("--json")) {
333
+ console.log(JSON.stringify(report, null, 2));
334
+ return;
335
+ }
336
+ console.log(axesReportToText(report));
337
+ return;
338
+ }
339
+
340
+ if (subcommand === "scale") {
341
+ const report = computeScaleIndex(config);
342
+ if (rest.includes("--json")) {
343
+ console.log(JSON.stringify(report, null, 2));
344
+ return;
345
+ }
346
+ console.log(scaleReportToText(config, report));
347
+ return;
348
+ }
349
+
350
+ if (subcommand === "overlay") {
351
+ const set = await loadSet();
352
+ const snapshotPath = option(rest, "--snapshot");
353
+ if (!snapshotPath) {
354
+ throw new Error(
355
+ "market overlay requires --snapshot <canonical-snapshot.json> (fullstackgtm snapshot --out it first) — directives need CRM ground truth",
356
+ );
357
+ }
358
+ const snapshot = JSON.parse(readFileSync(resolve(process.cwd(), snapshotPath), "utf8")) as CanonicalGtmSnapshot;
359
+
360
+ // --calls accepts ParsedCall JSON files (from `call parse --out`) and/or
361
+ // manifest arrays [{path, dealId?}] linking calls to deals. Repeatable.
362
+ const documents: CallDocument[] = [];
363
+ const addParsedCall = (parsedPath: string, dealId?: string) => {
364
+ const parsed = JSON.parse(readFileSync(resolve(process.cwd(), parsedPath), "utf8")) as ParsedCall & {
365
+ segments?: Array<{ text?: string }>;
366
+ };
367
+ const text = [
368
+ ...(parsed.segments ?? []).map((segment) => segment.text ?? ""),
369
+ ...(parsed.insights ?? []).map((insight) => `${insight.text ?? ""} ${insight.evidence ?? ""}`),
370
+ ].join("\n");
371
+ documents.push({ id: parsed.id ?? parsedPath, text, dealId, occurredAt: parsed.evidence?.[0]?.capturedAt });
372
+ };
373
+ for (let i = 0; i < rest.length; i += 1) {
374
+ if (rest[i] !== "--calls") continue;
375
+ const callsPath = rest[i + 1];
376
+ if (!callsPath) throw new Error("--calls needs a path");
377
+ const raw = JSON.parse(readFileSync(resolve(process.cwd(), callsPath), "utf8"));
378
+ if (Array.isArray(raw)) {
379
+ for (const entry of raw as Array<{ path: string; dealId?: string }>) addParsedCall(entry.path, entry.dealId);
380
+ } else {
381
+ addParsedCall(callsPath);
382
+ }
383
+ }
384
+
385
+ const priorLabel = option(rest, "--prior-run");
386
+ const priorSet = priorLabel ? await store.get(priorLabel) : null;
387
+ if (priorLabel && !priorSet) throw new Error(`No observation run "${priorLabel}" for URGENT drift`);
388
+
389
+ const stats = computeOverlayStats(config, snapshot, documents);
390
+ const directives = computeDirectives(config, set, stats, {
391
+ minMentions: numericOption(rest, "--min-mentions") ?? undefined,
392
+ promoteLift: numericOption(rest, "--promote-lift") ?? undefined,
393
+ priorSet: priorSet ?? undefined,
394
+ });
395
+
396
+ if (rest.includes("--json")) {
397
+ console.log(JSON.stringify({ stats, directives }, null, 2));
398
+ return;
399
+ }
400
+ console.log(overlayToMarkdown(stats, directives));
401
+
402
+ if (saveRequested(rest)) {
403
+ const taskAccount = option(rest, "--task-account");
404
+ const taskDeal = option(rest, "--task-deal");
405
+ if (!taskAccount && !taskDeal) {
406
+ throw new Error(
407
+ "--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)",
408
+ );
409
+ }
410
+ const plan = directivesToPlan(
411
+ config,
412
+ set,
413
+ directives,
414
+ taskDeal ? { objectType: "deal", objectId: taskDeal } : { objectType: "account", objectId: taskAccount as string },
415
+ );
416
+ const stored = await createFilePlanStore().save(plan);
417
+ console.log(`Saved plan ${stored.plan.id} (${directives.length} directive task(s); approve via \`plans approve\`)`);
418
+ }
419
+ return;
420
+ }
421
+
422
+ throw unknownSubcommandError("market", subcommand, MARKET_SUBCOMMANDS);
423
+ }