@sonnechasser/ntrp 0.3.5 → 1.3.6
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/LICENSE +24 -0
- package/README.md +4 -2
- package/dist/index.js +25779 -23174
- package/dist/mcp/server.js +5208 -2982
- package/package.json +9 -3
- package/dist/ai/findings-stream-smoke.js +0 -185
- package/dist/ai/findings-stream-smoke.js.map +0 -1
- package/dist/ai/guardrails-smoke.js +0 -23005
- package/dist/ai/guardrails-smoke.js.map +0 -1
- package/dist/conversation/deepdive-smoke.js +0 -3010
- package/dist/conversation/deepdive-smoke.js.map +0 -1
- package/dist/conversation/loop-guard-smoke.js +0 -33868
- package/dist/conversation/loop-guard-smoke.js.map +0 -1
- package/dist/demo/whimsy-smoke.js +0 -400
- package/dist/demo/whimsy-smoke.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/investigation/quality-eval-cli.js +0 -23025
- package/dist/investigation/quality-eval-cli.js.map +0 -1
- package/dist/investigation/verbosity-cli.js +0 -22837
- package/dist/investigation/verbosity-cli.js.map +0 -1
- package/dist/mcp/server.js.map +0 -1
- package/dist/services/exports-registry-smoke.js +0 -858
- package/dist/services/exports-registry-smoke.js.map +0 -1
- package/dist/services/transcript-smoke.js +0 -1034
- package/dist/services/transcript-smoke.js.map +0 -1
- package/dist/strategist/strategist-smoke.js +0 -3001
- package/dist/strategist/strategist-smoke.js.map +0 -1
- package/dist/whimsy/time-bank-smoke.js +0 -33497
- package/dist/whimsy/time-bank-smoke.js.map +0 -1
|
@@ -1,3001 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
process.noDeprecation = true;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
// src/ui/spinner.ts
|
|
9
|
-
import ora from "ora";
|
|
10
|
-
var init_spinner = __esm({
|
|
11
|
-
"src/ui/spinner.ts"() {
|
|
12
|
-
"use strict";
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
// src/ai/llm/thread-compat.ts
|
|
17
|
-
var init_thread_compat = __esm({
|
|
18
|
-
"src/ai/llm/thread-compat.ts"() {
|
|
19
|
-
"use strict";
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// src/io/context.ts
|
|
24
|
-
var init_context = __esm({
|
|
25
|
-
"src/io/context.ts"() {
|
|
26
|
-
"use strict";
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// src/output/formatters.ts
|
|
31
|
-
var init_formatters = __esm({
|
|
32
|
-
"src/output/formatters.ts"() {
|
|
33
|
-
"use strict";
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// src/config/store.ts
|
|
38
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
39
|
-
import { homedir } from "os";
|
|
40
|
-
import { join, resolve } from "path";
|
|
41
|
-
function ntrpHome() {
|
|
42
|
-
return NTRP_DIR;
|
|
43
|
-
}
|
|
44
|
-
function getMemoryDir() {
|
|
45
|
-
const dir = join(NTRP_DIR, "memory");
|
|
46
|
-
if (!existsSync(dir)) {
|
|
47
|
-
mkdirSync(dir, { recursive: true });
|
|
48
|
-
}
|
|
49
|
-
return dir;
|
|
50
|
-
}
|
|
51
|
-
var NTRP_DIR, CONFIG_PATH;
|
|
52
|
-
var init_store = __esm({
|
|
53
|
-
"src/config/store.ts"() {
|
|
54
|
-
"use strict";
|
|
55
|
-
NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join(homedir(), ".ntrp");
|
|
56
|
-
CONFIG_PATH = join(NTRP_DIR, "config.json");
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// src/output/path-safety.ts
|
|
61
|
-
import { homedir as homedir2 } from "os";
|
|
62
|
-
import { resolve as resolve2, sep } from "path";
|
|
63
|
-
var NTRP_HOME;
|
|
64
|
-
var init_path_safety = __esm({
|
|
65
|
-
"src/output/path-safety.ts"() {
|
|
66
|
-
"use strict";
|
|
67
|
-
init_store();
|
|
68
|
-
NTRP_HOME = ntrpHome();
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// src/services/exports-registry.ts
|
|
73
|
-
import {
|
|
74
|
-
appendFileSync,
|
|
75
|
-
copyFileSync,
|
|
76
|
-
cpSync,
|
|
77
|
-
existsSync as existsSync2,
|
|
78
|
-
mkdirSync as mkdirSync2,
|
|
79
|
-
readFileSync as readFileSync2,
|
|
80
|
-
readdirSync,
|
|
81
|
-
renameSync,
|
|
82
|
-
rmSync,
|
|
83
|
-
statSync,
|
|
84
|
-
writeFileSync as writeFileSync2
|
|
85
|
-
} from "fs";
|
|
86
|
-
import { basename, dirname, join as join2, resolve as resolve3, sep as sep2 } from "path";
|
|
87
|
-
import { randomUUID } from "crypto";
|
|
88
|
-
var init_exports_registry = __esm({
|
|
89
|
-
"src/services/exports-registry.ts"() {
|
|
90
|
-
"use strict";
|
|
91
|
-
init_store();
|
|
92
|
-
init_path_safety();
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// src/services/terminal-capture.ts
|
|
97
|
-
var init_terminal_capture = __esm({
|
|
98
|
-
"src/services/terminal-capture.ts"() {
|
|
99
|
-
"use strict";
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// src/services/context-doc.ts
|
|
104
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
105
|
-
var init_context_doc = __esm({
|
|
106
|
-
"src/services/context-doc.ts"() {
|
|
107
|
-
"use strict";
|
|
108
|
-
init_context2();
|
|
109
|
-
init_formatters();
|
|
110
|
-
init_store();
|
|
111
|
-
init_exports_registry();
|
|
112
|
-
init_terminal_capture();
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// src/services/transcript.ts
|
|
117
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
|
|
118
|
-
import { join as join3 } from "path";
|
|
119
|
-
var init_transcript = __esm({
|
|
120
|
-
"src/services/transcript.ts"() {
|
|
121
|
-
"use strict";
|
|
122
|
-
init_context2();
|
|
123
|
-
init_terminal_capture();
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// src/cli/context.ts
|
|
128
|
-
import { basename as basename2, join as join4, resolve as resolve4, sep as sep3 } from "path";
|
|
129
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, rmSync as rmSync3 } from "fs";
|
|
130
|
-
import { homedir as homedir3 } from "os";
|
|
131
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
132
|
-
function isAnalysisReady(ctx) {
|
|
133
|
-
if (ctx.stage !== "analyzed" && ctx.stage !== "delivered" || ctx.analysis.completed.length === 0) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
if (!ctx.dataset) return false;
|
|
137
|
-
const counts = ctx.dataset.counts ?? {};
|
|
138
|
-
return Object.values(counts).some((n) => n > 0);
|
|
139
|
-
}
|
|
140
|
-
function defaultSessionAnalysis(primary = "gtm_health") {
|
|
141
|
-
return { primary, completed: [] };
|
|
142
|
-
}
|
|
143
|
-
var STALE_SESSION_MS;
|
|
144
|
-
var init_context2 = __esm({
|
|
145
|
-
"src/cli/context.ts"() {
|
|
146
|
-
"use strict";
|
|
147
|
-
init_thread_compat();
|
|
148
|
-
init_context();
|
|
149
|
-
init_context_doc();
|
|
150
|
-
init_transcript();
|
|
151
|
-
STALE_SESSION_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// src/config/profile.ts
|
|
156
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
157
|
-
import { join as join5 } from "path";
|
|
158
|
-
var NTRP_DIR2, PROFILE_PATH;
|
|
159
|
-
var init_profile = __esm({
|
|
160
|
-
"src/config/profile.ts"() {
|
|
161
|
-
"use strict";
|
|
162
|
-
init_store();
|
|
163
|
-
NTRP_DIR2 = ntrpHome();
|
|
164
|
-
PROFILE_PATH = join5(NTRP_DIR2, "profile.json");
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// src/db/connection.ts
|
|
169
|
-
import { dirname as dirname2, join as join6, resolve as resolve5 } from "path";
|
|
170
|
-
var NTRP_DIR3, DEFAULT_DB_PATH, DB_PATH_PINNED;
|
|
171
|
-
var init_connection = __esm({
|
|
172
|
-
"src/db/connection.ts"() {
|
|
173
|
-
"use strict";
|
|
174
|
-
NTRP_DIR3 = process.env.NTRP_HOME ? resolve5(process.env.NTRP_HOME) : join6(process.env.HOME ?? "", ".ntrp");
|
|
175
|
-
DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ? resolve5(process.env.NTRP_DB_PATH) : join6(NTRP_DIR3, "ntrp.duckdb");
|
|
176
|
-
DB_PATH_PINNED = !!process.env.NTRP_DB_PATH;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// src/db/queries.ts
|
|
181
|
-
var init_queries = __esm({
|
|
182
|
-
"src/db/queries.ts"() {
|
|
183
|
-
"use strict";
|
|
184
|
-
init_connection();
|
|
185
|
-
init_formatters();
|
|
186
|
-
init_connection();
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// src/ui/theme.ts
|
|
191
|
-
import chalk from "chalk";
|
|
192
|
-
var STATUS, TOKENS, BADGE_TONE_COLORS;
|
|
193
|
-
var init_theme = __esm({
|
|
194
|
-
"src/ui/theme.ts"() {
|
|
195
|
-
"use strict";
|
|
196
|
-
init_formatters();
|
|
197
|
-
STATUS = {
|
|
198
|
-
green: "#22c55e",
|
|
199
|
-
yellow: "#eab308",
|
|
200
|
-
red: "#ef4444",
|
|
201
|
-
neutral: "#64748b"
|
|
202
|
-
};
|
|
203
|
-
TOKENS = {
|
|
204
|
-
accent: "#14b8a6",
|
|
205
|
-
accentBright: "#2dd4bf",
|
|
206
|
-
border: "#334155",
|
|
207
|
-
borderMuted: "#1e293b",
|
|
208
|
-
dim: "#64748b",
|
|
209
|
-
text: "#e2e8f0",
|
|
210
|
-
info: "#3b82f6",
|
|
211
|
-
...STATUS,
|
|
212
|
-
success: STATUS.green,
|
|
213
|
-
warning: STATUS.yellow,
|
|
214
|
-
error: STATUS.red
|
|
215
|
-
};
|
|
216
|
-
BADGE_TONE_COLORS = {
|
|
217
|
-
success: TOKENS.success,
|
|
218
|
-
warning: TOKENS.warning,
|
|
219
|
-
error: TOKENS.error,
|
|
220
|
-
info: TOKENS.info,
|
|
221
|
-
accent: TOKENS.accent
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// src/services/session-analysis.ts
|
|
227
|
-
var init_session_analysis = __esm({
|
|
228
|
-
"src/services/session-analysis.ts"() {
|
|
229
|
-
"use strict";
|
|
230
|
-
init_queries();
|
|
231
|
-
init_profile();
|
|
232
|
-
init_formatters();
|
|
233
|
-
init_theme();
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// src/baselines/metrics-benchmarks.ts
|
|
238
|
-
function motionBenchmarkLabel(motion) {
|
|
239
|
-
return MOTION_LABELS[motion ?? "mid_market"];
|
|
240
|
-
}
|
|
241
|
-
var METRICS_BENCHMARKS, MOTION_LABELS;
|
|
242
|
-
var init_metrics_benchmarks = __esm({
|
|
243
|
-
"src/baselines/metrics-benchmarks.ts"() {
|
|
244
|
-
"use strict";
|
|
245
|
-
METRICS_BENCHMARKS = {
|
|
246
|
-
plg: {
|
|
247
|
-
nrr: { green: 110, yellow: 100 },
|
|
248
|
-
grr: { green: 85, yellow: 75 },
|
|
249
|
-
win_rate: { green: 25, yellow: 15 },
|
|
250
|
-
pipeline_coverage: { green: 4, yellow: 2.5 },
|
|
251
|
-
magic_number: { green: 1, yellow: 0.75 },
|
|
252
|
-
payback_months: { green: 12, yellow: 18 }
|
|
253
|
-
},
|
|
254
|
-
smb_velocity: {
|
|
255
|
-
nrr: { green: 105, yellow: 95 },
|
|
256
|
-
grr: { green: 88, yellow: 78 },
|
|
257
|
-
win_rate: { green: 22, yellow: 12 },
|
|
258
|
-
pipeline_coverage: { green: 3.5, yellow: 2 },
|
|
259
|
-
magic_number: { green: 0.9, yellow: 0.6 },
|
|
260
|
-
payback_months: { green: 14, yellow: 20 }
|
|
261
|
-
},
|
|
262
|
-
mid_market: {
|
|
263
|
-
nrr: { green: 100, yellow: 90 },
|
|
264
|
-
grr: { green: 90, yellow: 80 },
|
|
265
|
-
win_rate: { green: 20, yellow: 12 },
|
|
266
|
-
pipeline_coverage: { green: 3, yellow: 2 },
|
|
267
|
-
magic_number: { green: 0.75, yellow: 0.5 },
|
|
268
|
-
payback_months: { green: 16, yellow: 22 }
|
|
269
|
-
},
|
|
270
|
-
enterprise: {
|
|
271
|
-
nrr: { green: 95, yellow: 85 },
|
|
272
|
-
grr: { green: 92, yellow: 82 },
|
|
273
|
-
win_rate: { green: 15, yellow: 8 },
|
|
274
|
-
pipeline_coverage: { green: 2.5, yellow: 1.5 },
|
|
275
|
-
magic_number: { green: 0.6, yellow: 0.4 },
|
|
276
|
-
payback_months: { green: 18, yellow: 24 }
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
MOTION_LABELS = {
|
|
280
|
-
plg: "PLG",
|
|
281
|
-
smb_velocity: "SMB Velocity",
|
|
282
|
-
mid_market: "Mid-Market",
|
|
283
|
-
enterprise: "Enterprise"
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// src/data/metric-definitions.ts
|
|
289
|
-
function pctBand(metric, motion) {
|
|
290
|
-
const m = motion ?? "mid_market";
|
|
291
|
-
const t = METRICS_BENCHMARKS[m][metric];
|
|
292
|
-
return `${motionBenchmarkLabel(m)} green \u2265${t.green}${metric === "pipeline_coverage" ? "x" : "%"}, yellow \u2265${t.yellow}${metric === "pipeline_coverage" ? "x" : "%"}`;
|
|
293
|
-
}
|
|
294
|
-
function monthsBand(motion) {
|
|
295
|
-
const m = motion ?? "mid_market";
|
|
296
|
-
const t = METRICS_BENCHMARKS[m].payback_months;
|
|
297
|
-
return `${motionBenchmarkLabel(m)} green \u2264${t.green}mo, yellow \u2264${t.yellow}mo`;
|
|
298
|
-
}
|
|
299
|
-
function magicBand(motion) {
|
|
300
|
-
const m = motion ?? "mid_market";
|
|
301
|
-
const t = METRICS_BENCHMARKS[m].magic_number;
|
|
302
|
-
return `${motionBenchmarkLabel(m)} green \u2265${t.green}, yellow \u2265${t.yellow}`;
|
|
303
|
-
}
|
|
304
|
-
var VITALS, SAAS, METRIC_DEFINITIONS, BY_ID, ALIAS_INDEX, SAAS_METRIC_IDS;
|
|
305
|
-
var init_metric_definitions = __esm({
|
|
306
|
-
"src/data/metric-definitions.ts"() {
|
|
307
|
-
"use strict";
|
|
308
|
-
init_metrics_benchmarks();
|
|
309
|
-
VITALS = [
|
|
310
|
-
{
|
|
311
|
-
id: "freshness",
|
|
312
|
-
kind: "vital",
|
|
313
|
-
label: "Freshness",
|
|
314
|
-
group: "Vital Signs",
|
|
315
|
-
tagline: "Is your CRM telling the truth about what's alive?",
|
|
316
|
-
how_computed: "Weighted average of people, organizations, and opportunities with recent activity (and open opps not past-due). Defaults: people/orgs 90-day window, opps 30-day window; weights 35/30/35.",
|
|
317
|
-
formula_lines: [
|
|
318
|
-
"freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
|
|
319
|
-
"people/orgs fresh if activity within 90d",
|
|
320
|
-
"opps fresh if activity within 30d AND not past-due"
|
|
321
|
-
],
|
|
322
|
-
meaning: 'Board question: "how much of this pipeline is real vs fiction?" Dollar value = sum of amount on stale opportunities \u2014 pipeline at risk.',
|
|
323
|
-
expert_read: "Cut by owner and by stage first \u2014 freshness reds concentrate on people or process, rarely evenly. In a long-cycle enterprise motion 30 quiet days can be normal cadence; in a velocity motion it's a dead deal. A sudden cliff usually means a broken integration or a departed rep, not gradual decay. False positive to check: bulk-imported records nobody has touched yet.",
|
|
324
|
-
deepdive: [
|
|
325
|
-
"Status: green \u226580, yellow \u226560, red below 60 (motion presets can shift windows).",
|
|
326
|
-
'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
|
|
327
|
-
"Layer 1 of the gating stack \u2014 a red here bounds what you can trust downstream.",
|
|
328
|
-
"Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when score < 60.",
|
|
329
|
-
"Levers: stale-deal alert at N quiet days, weekly hygiene scrub, enrichment refresh on quiet records, signal-triggered reactivation for paid-for dormant accounts."
|
|
330
|
-
],
|
|
331
|
-
visual: {
|
|
332
|
-
kind: "bars",
|
|
333
|
-
caption: "Exemplar component mix (higher = fresher)",
|
|
334
|
-
bars: [
|
|
335
|
-
{ label: "People", value: 72, tone: "yellow" },
|
|
336
|
-
{ label: "Organizations", value: 81, tone: "green" },
|
|
337
|
-
{ label: "Opportunities", value: 44, tone: "red" }
|
|
338
|
-
]
|
|
339
|
-
},
|
|
340
|
-
play_id: "clean-dead-pipeline",
|
|
341
|
-
dollar_label: "pipeline at risk",
|
|
342
|
-
audience: {
|
|
343
|
-
board: "Freshness answers whether the pipeline number is real. Low freshness means forecast risk \u2014 stale deals inflate coverage and hide the true gap.",
|
|
344
|
-
ops: "Score = weighted recency across people/orgs/opps. Cut by owner and stage; install a stale-deal alert and weekly scrub. Play: Clean Dead Pipeline."
|
|
345
|
-
},
|
|
346
|
-
aliases: ["data freshness", "stale", "zombie deals", "crm freshness"]
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
id: "flow_rate",
|
|
350
|
-
kind: "vital",
|
|
351
|
-
label: "Flow Rate",
|
|
352
|
-
group: "Vital Signs",
|
|
353
|
-
tagline: "How fast do deals actually move \u2014 and where do they die?",
|
|
354
|
-
how_computed: "Base score from average open-deal age vs max_days, then a penalty (up to \u221220) for the share of stuck deals (no update beyond stuck_days, or past-due close). Status is driven by average open age, not the score alone.",
|
|
355
|
-
formula_lines: [
|
|
356
|
-
"base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
|
|
357
|
-
"score = base \u2212 stuckSharePenalty (\u226420)",
|
|
358
|
-
"stuck = no update > stuck_days OR past-due close"
|
|
359
|
-
],
|
|
360
|
-
meaning: 'Board question: "is next quarter slipping because deals are stuck?" Dollar value = amount stuck in pipeline.',
|
|
361
|
-
expert_read: "Cut by stage-age, not just deal-age \u2014 find the stage where deals go to die (usually one). Compare stuck-deal age to this company's own median cycle, not a generic norm. Stuck + past-due close dates together signal happy-ears forecasting, a credibility problem before it's a revenue problem.",
|
|
362
|
-
deepdive: [
|
|
363
|
-
"Status from avg open age: \u226445d green, \u226490d yellow, else red (defaults; max_days 120, stuck_days 60).",
|
|
364
|
-
'Dollar translation: sum of amount on stuck deals \u2192 "stuck in pipeline".',
|
|
365
|
-
"Layer 2 of the gating stack (with Drop Rate).",
|
|
366
|
-
"Trigger play: Unstick the Pipeline (unstick-pipeline) when score is weak.",
|
|
367
|
-
"Levers: stage-age report, past-due close cleanup, progression plans on stuck deals, forecast hygiene on happy-ears dates."
|
|
368
|
-
],
|
|
369
|
-
visual: {
|
|
370
|
-
kind: "funnel",
|
|
371
|
-
caption: "Exemplar stage ages \u2014 find the stage where deals go to die",
|
|
372
|
-
funnel: [
|
|
373
|
-
{ label: "Discovery", widthPct: 100 },
|
|
374
|
-
{ label: "Qualify", widthPct: 78 },
|
|
375
|
-
{ label: "Propose", widthPct: 55 },
|
|
376
|
-
{ label: "Negotiate", widthPct: 22 },
|
|
377
|
-
{ label: "Closed", widthPct: 12 }
|
|
378
|
-
]
|
|
379
|
-
},
|
|
380
|
-
play_id: "unstick-pipeline",
|
|
381
|
-
dollar_label: "stuck in pipeline",
|
|
382
|
-
audience: {
|
|
383
|
-
board: "Flow Rate is velocity risk. Stuck pipeline with past-due closes is a credibility problem for the forecast before it is a revenue miss.",
|
|
384
|
-
ops: "Find the stage with collapsing advancement and age. Clear past-due closes, write progression plans on stuck deals. Play: Unstick the Pipeline."
|
|
385
|
-
},
|
|
386
|
-
aliases: ["flow rate", "deal velocity", "stuck deals", "stuck pipeline"]
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
id: "drop_rate",
|
|
390
|
-
kind: "vital",
|
|
391
|
-
label: "Drop Rate",
|
|
392
|
-
group: "Vital Signs",
|
|
393
|
-
tagline: "Where do leads vanish between systems?",
|
|
394
|
-
how_computed: "Blend of cross-system retention (marketing people also present in sales) and opportunity retention (open opps not abandoned). Defaults weight cross-system 60% / opp retention 40%. Abandoned = open opps with no activity in 30 days.",
|
|
395
|
-
formula_lines: [
|
|
396
|
-
"score = crossSystemRetention\xD70.6 + oppRetention\xD70.4",
|
|
397
|
-
"cross-system = marketing people also in sales CRM",
|
|
398
|
-
"abandoned = open opps with no activity in 30d"
|
|
399
|
-
],
|
|
400
|
-
meaning: 'Board question: "how much pipeline are we paying for and never working?" Dollar value = droppedCount \xD7 conversionRate \xD7 avgDealSize \u2014 est. lost at handoff.',
|
|
401
|
-
expert_read: "This is almost always a systems failure \u2014 routing rules, unassigned territories, dead rep queues, or a sync gap between marketing and CRM \u2014 not lazy reps. First cut by lead source; the leak usually concentrates in one or two sources. The cheapest pipeline this business can buy is the leads it already paid for.",
|
|
402
|
-
deepdive: [
|
|
403
|
-
"Status: green \u226580, yellow \u226560, red below 60.",
|
|
404
|
-
'Dollar translation: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
|
|
405
|
-
"Layer 2 of the gating stack (with Flow Rate).",
|
|
406
|
-
"Trigger play: Fix the Handoff Gap (fix-handoff-gap) when drop is high.",
|
|
407
|
-
"Levers: source-level handoff audit, routing + sync repair, time-to-first-touch SLA, weekly marketing-only-leads report."
|
|
408
|
-
],
|
|
409
|
-
visual: {
|
|
410
|
-
kind: "funnel",
|
|
411
|
-
caption: "Exemplar handoff funnel \u2014 the leak is usually one or two sources",
|
|
412
|
-
funnel: [
|
|
413
|
-
{ label: "Marketing leads", widthPct: 100 },
|
|
414
|
-
{ label: "In sales CRM", widthPct: 62 },
|
|
415
|
-
{ label: "Assigned + touched", widthPct: 41 },
|
|
416
|
-
{ label: "Active opportunities", widthPct: 28 }
|
|
417
|
-
]
|
|
418
|
-
},
|
|
419
|
-
play_id: "fix-handoff-gap",
|
|
420
|
-
dollar_label: "est. lost at handoff",
|
|
421
|
-
audience: {
|
|
422
|
-
board: "Drop Rate prices the handoff leak \u2014 budget already spent on leads that never reach a working rep. Usually a systems failure, not a people failure.",
|
|
423
|
-
ops: "Audit by source, fix routing/sync/dead queues, instrument time-to-first-touch. Play: Fix the Handoff Gap."
|
|
424
|
-
},
|
|
425
|
-
aliases: ["drop rate", "handoff", "handoff gap", "lead leak", "marketing sales handoff"]
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
id: "signal_to_noise",
|
|
429
|
-
kind: "vital",
|
|
430
|
-
label: "Signal:Noise",
|
|
431
|
-
group: "Vital Signs",
|
|
432
|
-
tagline: "How much activity is aimed at deals that can still close?",
|
|
433
|
-
how_computed: "Over a 90-day lookback: (signal activities / all activities) \xD7 100. Signal = activity linked to an open opportunity, a pipeline person, or a pipeline organization.",
|
|
434
|
-
formula_lines: [
|
|
435
|
-
"score = (signalCount / activityCount) \xD7 100",
|
|
436
|
-
"signal = linked to open opp / pipeline person / pipeline org",
|
|
437
|
-
"lookback = trailing 90 days"
|
|
438
|
-
],
|
|
439
|
-
meaning: 'Board question: "are we burning capacity on dead water?" Dollar value = noiseCount \xD7 hours_per_activity \xD7 rep_hourly_cost \u2014 misdirected effort.',
|
|
440
|
-
expert_read: "Cut by rep and by account status \u2014 noise usually means reps fishing in the pond they can see (dead accounts they know) because targeting and account lists are stale. Persistent noise is a coverage-model problem, not a coaching problem. Check whether activity is logged against closed or unlinked records \u2014 often a hygiene artifact.",
|
|
441
|
-
deepdive: [
|
|
442
|
-
"Status: green \u226565, yellow \u226540, red below 40.",
|
|
443
|
-
"Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost).",
|
|
444
|
-
"Layer 3 of the gating stack \u2014 trust Freshness / Flow / Drop before reading activity efficiency.",
|
|
445
|
-
"Trigger play: Retarget Misdirected Effort (retarget-effort) when score is low.",
|
|
446
|
-
"Levers: refresh account lists, signal-based targeting, stop logging against closed/unlinked records, coverage-model redesign."
|
|
447
|
-
],
|
|
448
|
-
visual: {
|
|
449
|
-
kind: "split",
|
|
450
|
-
caption: "Exemplar activity mix \u2014 signal vs noise",
|
|
451
|
-
bars: [
|
|
452
|
-
{ label: "Signal", value: 38, tone: "green" },
|
|
453
|
-
{ label: "Noise", value: 62, tone: "red" }
|
|
454
|
-
]
|
|
455
|
-
},
|
|
456
|
-
play_id: "retarget-effort",
|
|
457
|
-
dollar_label: "misdirected effort",
|
|
458
|
-
audience: {
|
|
459
|
-
board: "Signal:Noise prices wasted capacity. Persistent noise is usually a coverage-model problem, not a coaching problem \u2014 reps fish in dead ponds they already know.",
|
|
460
|
-
ops: "Score = % of activities linked to live pipeline. Cut by rep and account status; refresh targeting. Play: Retarget Misdirected Effort."
|
|
461
|
-
},
|
|
462
|
-
aliases: ["signal to noise", "signal:noise", "s/n", "activity efficiency", "noise"]
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
id: "thread_depth",
|
|
466
|
-
kind: "vital",
|
|
467
|
-
label: "Thread Depth",
|
|
468
|
-
group: "Vital Signs",
|
|
469
|
-
tagline: "How fragile is the pipeline if one champion goes dark?",
|
|
470
|
-
how_computed: "Percent of open deals with at least multi_thread_threshold (default 2) distinct people active in the last 90 days (opp-direct contacts + same-org activity).",
|
|
471
|
-
formula_lines: [
|
|
472
|
-
"score = % open deals with \u22652 active people (90d)",
|
|
473
|
-
"people counted via opp contacts + same-org activity",
|
|
474
|
-
"threshold configurable (default 2)"
|
|
475
|
-
],
|
|
476
|
-
meaning: 'Board question: "how much revenue dies if one contact changes jobs?" Dollar value = sum of amount on single-threaded deals.',
|
|
477
|
-
expert_read: "Weight by deal size \u2014 one single-threaded mega-deal outweighs ten small ones. Single-threading late in the cycle is far more dangerous than early. In enterprise motions, thread depth is a leading indicator of slipped quarters: champions change jobs, and there's no second door in.",
|
|
478
|
-
deepdive: [
|
|
479
|
-
"Status: green \u226565, yellow \u226540, red below 40.",
|
|
480
|
-
'Dollar translation: sum of amount on single-threaded deals \u2192 "single-threaded".',
|
|
481
|
-
"Layer 4 of the gating stack \u2014 read last, after the upstream vitals.",
|
|
482
|
-
"Trigger play: Multi-Thread Your Deals (multi-thread-deals) when depth is low.",
|
|
483
|
-
"Levers: buying-committee map, warm internal referral first, CRM contact roles, mid-stage single-thread alerts, champion job-change signals."
|
|
484
|
-
],
|
|
485
|
-
visual: {
|
|
486
|
-
kind: "bars",
|
|
487
|
-
caption: "Exemplar \u2014 multi-threaded vs single-threaded open deals",
|
|
488
|
-
bars: [
|
|
489
|
-
{ label: "Multi-threaded", value: 34, tone: "green" },
|
|
490
|
-
{ label: "Single-threaded", value: 66, tone: "red" }
|
|
491
|
-
]
|
|
492
|
-
},
|
|
493
|
-
play_id: "multi-thread-deals",
|
|
494
|
-
dollar_label: "single-threaded",
|
|
495
|
-
audience: {
|
|
496
|
-
board: "Thread Depth is resilience risk. One single-threaded mega-deal outweighs ten small ones \u2014 late-cycle single-threading is a leading indicator of slipped quarters.",
|
|
497
|
-
ops: "Score = % of open deals with \u22652 active contacts in 90d. Map the buying committee; alert on mid-stage singles. Play: Multi-Thread Your Deals."
|
|
498
|
-
},
|
|
499
|
-
aliases: ["thread depth", "multithreading", "multi-thread", "single-threaded", "buying committee"]
|
|
500
|
-
}
|
|
501
|
-
];
|
|
502
|
-
SAAS = [
|
|
503
|
-
// —— Revenue ——
|
|
504
|
-
{
|
|
505
|
-
id: "arr",
|
|
506
|
-
kind: "saas",
|
|
507
|
-
label: "ARR",
|
|
508
|
-
group: "Revenue",
|
|
509
|
-
tagline: "How big is the revenue engine \u2014 and from where?",
|
|
510
|
-
how_computed: "Sum of amount on closed-won opportunities in the dataset (pipeline-inferred ARR when a pure subscription ledger is unavailable).",
|
|
511
|
-
formula_lines: [
|
|
512
|
-
"ARR \u2248 \u03A3 amount on closed-won opportunities",
|
|
513
|
-
"New + Expansion = growth \xB7 Churned + Contraction = leakage"
|
|
514
|
-
],
|
|
515
|
-
meaning: 'Board question: "how fast are we growing, and from where?" Always decompose growth into new vs expansion \u2014 the mix is the story.',
|
|
516
|
-
expert_read: "Always decompose growth into new vs expansion \u2014 the mix is the story. Instrument trust: prefer this company's own trailing history over any external prior; a number below its reliability gate is a hypothesis, not a fact.",
|
|
517
|
-
deepdive: [
|
|
518
|
-
"Companion metrics: New ARR, Expansion ARR, Churned ARR, Contraction ARR.",
|
|
519
|
-
"Estimation method may be ledger, pipeline_inferred, or snapshot \u2014 read confidence + reliability_gate.",
|
|
520
|
-
"Cross-check with Freshness before trusting ARR growth stories built on zombie deals."
|
|
521
|
-
],
|
|
522
|
-
visual: {
|
|
523
|
-
kind: "waterfall",
|
|
524
|
-
caption: "Exemplar ARR walk \u2014 growth vs leakage",
|
|
525
|
-
waterfall: [
|
|
526
|
-
{ label: "Starting", delta: 100, cumulative: 100 },
|
|
527
|
-
{ label: "+ New", delta: 18, cumulative: 118 },
|
|
528
|
-
{ label: "+ Expansion", delta: 12, cumulative: 130 },
|
|
529
|
-
{ label: "\u2212 Contraction", delta: -4, cumulative: 126 },
|
|
530
|
-
{ label: "\u2212 Churned", delta: -8, cumulative: 118 }
|
|
531
|
-
]
|
|
532
|
-
},
|
|
533
|
-
audience: {
|
|
534
|
-
board: "ARR is the size of the engine. The story is the mix \u2014 new vs expansion growth, and how much leakage (churn + contraction) ate it.",
|
|
535
|
-
ops: "Computed as \u03A3 closed-won amounts (pipeline-inferred when no ledger). Decompose into new / expansion / churned / contraction before briefing anyone."
|
|
536
|
-
},
|
|
537
|
-
aliases: ["annual recurring revenue", "revenue"]
|
|
538
|
-
},
|
|
539
|
-
{
|
|
540
|
-
id: "new_arr",
|
|
541
|
-
kind: "saas",
|
|
542
|
-
label: "New ARR",
|
|
543
|
-
group: "Revenue",
|
|
544
|
-
tagline: "How much growth came from brand-new customers?",
|
|
545
|
-
how_computed: "Closed-won tagged New Business, or first closed-won deal per organization when tags are missing.",
|
|
546
|
-
formula_lines: [
|
|
547
|
-
"New ARR = \u03A3 closed-won tagged New Business",
|
|
548
|
-
"fallback: first closed-won deal per organization"
|
|
549
|
-
],
|
|
550
|
-
meaning: 'Board question: "is growth coming from the top of funnel, or are we farming the base?"',
|
|
551
|
-
expert_read: "Rising New ARR with falling Expansion usually means land-and-expand is underpowered \u2014 packaging or CS motion, not just sales capacity.",
|
|
552
|
-
deepdive: [
|
|
553
|
-
"Pair with Expansion ARR \u2014 the mix tells you which motion is carrying growth.",
|
|
554
|
-
"Tag quality matters: untagged deals fall into the first-deal-per-org heuristic."
|
|
555
|
-
],
|
|
556
|
-
visual: {
|
|
557
|
-
kind: "bars",
|
|
558
|
-
caption: "Exemplar growth mix",
|
|
559
|
-
bars: [
|
|
560
|
-
{ label: "New ARR", value: 60, tone: "accent" },
|
|
561
|
-
{ label: "Expansion ARR", value: 40, tone: "green" }
|
|
562
|
-
]
|
|
563
|
-
},
|
|
564
|
-
audience: {
|
|
565
|
-
board: "New ARR is net-new logos. Read it next to Expansion \u2014 a healthy mix beats a one-sided engine.",
|
|
566
|
-
ops: "Prefer CRM New Business tags; otherwise first closed-won per org. Watch tag hygiene."
|
|
567
|
-
},
|
|
568
|
-
aliases: ["new business arr", "new logo arr"]
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
id: "expansion_arr",
|
|
572
|
-
kind: "saas",
|
|
573
|
-
label: "Expansion ARR",
|
|
574
|
-
group: "Revenue",
|
|
575
|
-
tagline: "How much are existing customers buying more?",
|
|
576
|
-
how_computed: "Closed-won tagged Expansion, or later closed-won deals per organization after the first win.",
|
|
577
|
-
formula_lines: [
|
|
578
|
-
"Expansion ARR = \u03A3 closed-won tagged Expansion",
|
|
579
|
-
"fallback: later closed-won deals per organization"
|
|
580
|
-
],
|
|
581
|
-
meaning: 'Board question: "is the installed base compounding?"',
|
|
582
|
-
expert_read: "Expansion is the cheapest growth. Weak Expansion with strong New ARR is a land-only motion \u2014 packaging, CS capacity, or product attach is usually the lever.",
|
|
583
|
-
deepdive: [
|
|
584
|
-
"Feeds NRR as the upside term.",
|
|
585
|
-
"Compare to Contraction \u2014 net expansion = expansion \u2212 contraction."
|
|
586
|
-
],
|
|
587
|
-
visual: {
|
|
588
|
-
kind: "bars",
|
|
589
|
-
caption: "Exemplar \u2014 expansion vs contraction",
|
|
590
|
-
bars: [
|
|
591
|
-
{ label: "Expansion", value: 70, tone: "green" },
|
|
592
|
-
{ label: "Contraction", value: 25, tone: "yellow" }
|
|
593
|
-
]
|
|
594
|
-
},
|
|
595
|
-
audience: {
|
|
596
|
-
board: "Expansion ARR is installed-base compounding \u2014 the cheapest growth when it works.",
|
|
597
|
-
ops: "Tagged Expansion or subsequent wins per org. Pair with Contraction before celebrating net expansion."
|
|
598
|
-
},
|
|
599
|
-
aliases: ["upsell", "upsell arr", "cross-sell"]
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
id: "churned_arr",
|
|
603
|
-
kind: "saas",
|
|
604
|
-
label: "Churned ARR",
|
|
605
|
-
group: "Revenue",
|
|
606
|
-
tagline: "How much revenue walked out the door?",
|
|
607
|
-
how_computed: "Organizations with historical wins, no win in the trailing 12 months, and no active open opportunity \u2014 sum of their historical closed-won amounts.",
|
|
608
|
-
formula_lines: [
|
|
609
|
-
"Churned ARR = \u03A3 historical wins for orgs with",
|
|
610
|
-
" no win in trailing 12mo AND no active open opp"
|
|
611
|
-
],
|
|
612
|
-
meaning: `Board question: "how leaky is the bucket before expansion papers over it?" (with Contraction, this is GRR's downside).`,
|
|
613
|
-
expert_read: "Pipeline-inferred churn is a hypothesis \u2014 confirm with billing status when available. A spike often clusters in one segment or cohort.",
|
|
614
|
-
deepdive: [
|
|
615
|
-
"Feeds GRR and NRR as the churn term.",
|
|
616
|
-
"Cut by segment / motion before treating it as a company-wide PMF problem."
|
|
617
|
-
],
|
|
618
|
-
visual: {
|
|
619
|
-
kind: "bars",
|
|
620
|
-
caption: "Exemplar leakage mix",
|
|
621
|
-
bars: [
|
|
622
|
-
{ label: "Churned", value: 55, tone: "red" },
|
|
623
|
-
{ label: "Contraction", value: 30, tone: "yellow" }
|
|
624
|
-
]
|
|
625
|
-
},
|
|
626
|
-
audience: {
|
|
627
|
-
board: "Churned ARR is full logo loss. With Contraction it sets the floor of the business (GRR).",
|
|
628
|
-
ops: "Heuristic: historical winners with no trailing-12 win and no open opp. Validate against billing when you can."
|
|
629
|
-
},
|
|
630
|
-
aliases: ["churn", "logo churn", "churned revenue"]
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
id: "contraction_arr",
|
|
634
|
-
kind: "saas",
|
|
635
|
-
label: "Contraction ARR",
|
|
636
|
-
group: "Revenue",
|
|
637
|
-
tagline: "How much did existing customers buy less?",
|
|
638
|
-
how_computed: "Organizations with \u22652 wins where the latest amount is less than the prior \u2014 sum of the negative deltas.",
|
|
639
|
-
formula_lines: [
|
|
640
|
-
"Contraction = \u03A3 (prior \u2212 latest) where latest < prior",
|
|
641
|
-
"requires \u22652 closed-won deals per organization"
|
|
642
|
-
],
|
|
643
|
-
meaning: 'Board question: "are we quietly shrinking inside the base while logos stay?"',
|
|
644
|
-
expert_read: "Contraction is often packaging, seat-reduction, or downgrade \u2014 different owner than logo churn. Same NRR can be a churn problem or a no-expansion problem.",
|
|
645
|
-
deepdive: [
|
|
646
|
-
"Feeds GRR and NRR.",
|
|
647
|
-
"Needs multi-deal history per org \u2014 thin history understates contraction."
|
|
648
|
-
],
|
|
649
|
-
visual: {
|
|
650
|
-
kind: "waterfall",
|
|
651
|
-
caption: "Exemplar \u2014 contraction digs into the base",
|
|
652
|
-
waterfall: [
|
|
653
|
-
{ label: "Prior", delta: 100, cumulative: 100 },
|
|
654
|
-
{ label: "Latest", delta: -18, cumulative: 82 }
|
|
655
|
-
]
|
|
656
|
-
},
|
|
657
|
-
audience: {
|
|
658
|
-
board: "Contraction is silent shrink inside retained logos \u2014 often packaging or seats, not a cancelled contract.",
|
|
659
|
-
ops: "Requires \u22652 wins per org with a down-round. Pair with Expansion for net expansion."
|
|
660
|
-
},
|
|
661
|
-
aliases: ["downgrade", "seat reduction", "contraction"]
|
|
662
|
-
},
|
|
663
|
-
// —— Retention ——
|
|
664
|
-
{
|
|
665
|
-
id: "nrr",
|
|
666
|
-
kind: "saas",
|
|
667
|
-
label: "Net Revenue Retention",
|
|
668
|
-
group: "Retention",
|
|
669
|
-
tagline: "Would this business grow if sales stopped selling?",
|
|
670
|
-
how_computed: "startingArr = ARR + Churned + Contraction \u2212 Expansion; NRR = ((starting \u2212 Churned \u2212 Contraction + Expansion) / starting) \xD7 100.",
|
|
671
|
-
formula_lines: [
|
|
672
|
-
"starting = ARR + churned + contraction \u2212 expansion",
|
|
673
|
-
"NRR = (starting \u2212 churned \u2212 contraction + expansion) / starting \xD7 100",
|
|
674
|
-
"NRR = 100% + expansion% \u2212 contraction% \u2212 churn%"
|
|
675
|
-
],
|
|
676
|
-
meaning: 'Board question: "would this business grow if sales stopped selling?" >100% means growing from existing customers.',
|
|
677
|
-
expert_read: "Decompose before judging: the same 95% can be a churn problem (product/PMF) or a no-expansion problem (packaging/motion) with different owners. Priors by segment: ~97% SMB, ~108% mid-market, ~118% enterprise medians; 110%+ is a strong signal at any stage.",
|
|
678
|
-
deepdive: [
|
|
679
|
-
"Always show the waterfall: +expansion \u2212contraction \u2212churn.",
|
|
680
|
-
"GRR is the floor; NRR adds expansion on top.",
|
|
681
|
-
"On pipeline-only data, treat as a hypothesis \u2014 check confidence / reliability_gate."
|
|
682
|
-
],
|
|
683
|
-
visual: {
|
|
684
|
-
kind: "waterfall",
|
|
685
|
-
caption: "Exemplar NRR walk from 100%",
|
|
686
|
-
waterfall: [
|
|
687
|
-
{ label: "100%", delta: 100, cumulative: 100 },
|
|
688
|
-
{ label: "+ Expansion", delta: 14, cumulative: 114 },
|
|
689
|
-
{ label: "\u2212 Contraction", delta: -4, cumulative: 110 },
|
|
690
|
-
{ label: "\u2212 Churn", delta: -6, cumulative: 104 }
|
|
691
|
-
]
|
|
692
|
-
},
|
|
693
|
-
audience: {
|
|
694
|
-
board: "NRR >100% means the base compounds without new logos. Decompose before judging \u2014 same number, different owners.",
|
|
695
|
-
ops: "NRR = 100 + expansion \u2212 contraction \u2212 churn. Motion benchmarks calibrate green/yellow bands. Check reliability_gate on pipeline-inferred data."
|
|
696
|
-
},
|
|
697
|
-
aliases: ["net revenue retention", "net retention", "ndr"],
|
|
698
|
-
benchmarkHint: (motion) => pctBand("nrr", motion)
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
id: "grr",
|
|
702
|
-
kind: "saas",
|
|
703
|
-
label: "Gross Revenue Retention",
|
|
704
|
-
group: "Retention",
|
|
705
|
-
tagline: "How leaky is the bucket before expansion papers over it?",
|
|
706
|
-
how_computed: "GRR = ((startingArr \u2212 Churned \u2212 Contraction) / startingArr) \xD7 100 \u2014 expansion is excluded on purpose.",
|
|
707
|
-
formula_lines: [
|
|
708
|
-
"starting = ARR + churned + contraction \u2212 expansion",
|
|
709
|
-
"GRR = (starting \u2212 churned \u2212 contraction) / starting \xD7 100"
|
|
710
|
-
],
|
|
711
|
-
meaning: 'Board question: "how leaky is the bucket before expansion papers over it?" Prior: >90% healthy, >95% strong for enterprise.',
|
|
712
|
-
expert_read: "GRR is the honesty metric. Expansion can make NRR look fine while GRR is quietly eroding \u2014 always read both.",
|
|
713
|
-
deepdive: [
|
|
714
|
-
"GRR never includes Expansion \u2014 that is the point.",
|
|
715
|
-
"Owners: product/CS for churn, packaging for contraction."
|
|
716
|
-
],
|
|
717
|
-
visual: {
|
|
718
|
-
kind: "gauge",
|
|
719
|
-
caption: "Exemplar GRR \u2014 floor of the business",
|
|
720
|
-
gauge: 92
|
|
721
|
-
},
|
|
722
|
-
audience: {
|
|
723
|
-
board: "GRR is the floor \u2014 churn + contraction only. Expansion cannot paper over a leaky bucket here.",
|
|
724
|
-
ops: "Exclude Expansion by design. Pair with NRR; diagnose churn vs contraction separately."
|
|
725
|
-
},
|
|
726
|
-
aliases: ["gross revenue retention", "gross retention"],
|
|
727
|
-
benchmarkHint: (motion) => pctBand("grr", motion)
|
|
728
|
-
},
|
|
729
|
-
// —— Pipeline ——
|
|
730
|
-
{
|
|
731
|
-
id: "pipeline_coverage",
|
|
732
|
-
kind: "saas",
|
|
733
|
-
label: "Pipeline Coverage",
|
|
734
|
-
group: "Pipeline",
|
|
735
|
-
tagline: "Is next quarter already at risk?",
|
|
736
|
-
how_computed: "Open pipeline amount \xF7 trailing-90-day closed-won amount.",
|
|
737
|
-
formula_lines: [
|
|
738
|
-
"Coverage = openPipeline / trailing_90d_won",
|
|
739
|
-
"required \u2248 1 / win_rate (discount for time left)"
|
|
740
|
-
],
|
|
741
|
-
meaning: 'Board question: "is next quarter already at risk?" Priors scale with cycle length: ~3x velocity/SMB, 4\u20135x enterprise.',
|
|
742
|
-
expert_read: "Coverage means nothing without win rate: required coverage \u2248 1 / win rate, discounted for time left in period. Inflated stages and zombie deals fake coverage \u2014 cross-check with Freshness before trusting it.",
|
|
743
|
-
deepdive: [
|
|
744
|
-
"Always pair with Win Rate and Freshness.",
|
|
745
|
-
"Weighted Pipeline is the credibility-adjusted cousin."
|
|
746
|
-
],
|
|
747
|
-
visual: {
|
|
748
|
-
kind: "gauge",
|
|
749
|
-
caption: "Exemplar coverage vs a 3x target",
|
|
750
|
-
gauge: 72,
|
|
751
|
-
bars: [
|
|
752
|
-
{ label: "Open pipeline", value: 75, tone: "accent" },
|
|
753
|
-
{ label: "Trailing won (scaled)", value: 25, tone: "neutral" }
|
|
754
|
-
]
|
|
755
|
-
},
|
|
756
|
-
audience: {
|
|
757
|
-
board: "Coverage answers whether next quarter is already under-piped. Fake coverage from zombies is worse than an honest gap.",
|
|
758
|
-
ops: "open / trailing-90d won. Required \u2248 1/win_rate. Cross-check Freshness before briefing."
|
|
759
|
-
},
|
|
760
|
-
aliases: ["coverage", "pipeline coverage", "pipe coverage"],
|
|
761
|
-
benchmarkHint: (motion) => pctBand("pipeline_coverage", motion)
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
id: "weighted_pipeline",
|
|
765
|
-
kind: "saas",
|
|
766
|
-
label: "Weighted Pipeline",
|
|
767
|
-
group: "Pipeline",
|
|
768
|
-
tagline: "What is the pipeline worth after stage probability?",
|
|
769
|
-
how_computed: "Sum of amount \xD7 stage probability for open deals (CRM Probability when present, else stage defaults).",
|
|
770
|
-
formula_lines: [
|
|
771
|
-
"Weighted = \u03A3 (amount \xD7 stageProbability)",
|
|
772
|
-
"trust \u2264 stage discipline deserves"
|
|
773
|
-
],
|
|
774
|
-
meaning: 'Board question: "what should we actually forecast from open pipe?"',
|
|
775
|
-
expert_read: "Trust it only as much as stage discipline deserves. Inflated late stages make weighted pipeline a fiction.",
|
|
776
|
-
deepdive: [
|
|
777
|
-
"Compare to unweighted open pipeline \u2014 a huge gap means optimistic stages.",
|
|
778
|
-
"Pair with Flow Rate (stuck late stages)."
|
|
779
|
-
],
|
|
780
|
-
visual: {
|
|
781
|
-
kind: "bars",
|
|
782
|
-
caption: "Exemplar \u2014 open vs weighted",
|
|
783
|
-
bars: [
|
|
784
|
-
{ label: "Open pipeline", value: 100, tone: "neutral" },
|
|
785
|
-
{ label: "Weighted", value: 42, tone: "accent" }
|
|
786
|
-
]
|
|
787
|
-
},
|
|
788
|
-
audience: {
|
|
789
|
-
board: "Weighted Pipeline is the credibility-adjusted forecast input \u2014 only as good as stage discipline.",
|
|
790
|
-
ops: "\u03A3 amount \xD7 probability. Audit stage probabilities when weighted << open."
|
|
791
|
-
},
|
|
792
|
-
aliases: ["weighted pipe", "probability-weighted pipeline"]
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
id: "pipeline_created",
|
|
796
|
-
kind: "saas",
|
|
797
|
-
label: "Pipeline Created (90d)",
|
|
798
|
-
group: "Pipeline",
|
|
799
|
-
tagline: "How much new pipe did we generate recently?",
|
|
800
|
-
how_computed: "Sum of amounts for opportunities created in the last 90 days.",
|
|
801
|
-
formula_lines: ["Pipeline Created = \u03A3 amount where created_at within 90d"],
|
|
802
|
-
meaning: 'Board question: "is the top of funnel still filling?"',
|
|
803
|
-
expert_read: "Falling created pipeline with flat coverage is a future miss \u2014 coverage is lagging; created is leading.",
|
|
804
|
-
deepdive: [
|
|
805
|
-
"Leading indicator for next-quarter coverage.",
|
|
806
|
-
"Cut by source / segment to find where creation stalled."
|
|
807
|
-
],
|
|
808
|
-
visual: {
|
|
809
|
-
kind: "bars",
|
|
810
|
-
caption: "Exemplar \u2014 created vs needed",
|
|
811
|
-
bars: [
|
|
812
|
-
{ label: "Created (90d)", value: 55, tone: "yellow" },
|
|
813
|
-
{ label: "Target pace", value: 80, tone: "green" }
|
|
814
|
-
]
|
|
815
|
-
},
|
|
816
|
-
audience: {
|
|
817
|
-
board: "Pipeline Created is a leading indicator \u2014 coverage lagging means the miss is already in motion.",
|
|
818
|
-
ops: "\u03A3 amounts on opps created in 90d. Cut by source when it dips."
|
|
819
|
-
},
|
|
820
|
-
aliases: ["pipe gen", "pipeline generation", "created pipeline"]
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
id: "pipeline_velocity",
|
|
824
|
-
kind: "saas",
|
|
825
|
-
label: "Pipeline Velocity",
|
|
826
|
-
group: "Pipeline",
|
|
827
|
-
tagline: "Revenue throughput per day \u2014 four levers, one number.",
|
|
828
|
-
how_computed: "(openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays \u2014 requires \u22653 dated closed-won deals. Unit: $/day.",
|
|
829
|
-
formula_lines: [
|
|
830
|
-
"Velocity = (openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays",
|
|
831
|
-
"four levers: #opps \xB7 deal size \xB7 win rate \xB7 cycle days"
|
|
832
|
-
],
|
|
833
|
-
meaning: 'Board question: "which lever moved when throughput changed?" The most decision-ready pipeline metric.',
|
|
834
|
-
expert_read: "When velocity changes, name WHICH lever moved. A win-rate rise on falling opp volume is qualification tightening, not improvement.",
|
|
835
|
-
deepdive: [
|
|
836
|
-
"Needs \u22653 dated wins \u2014 otherwise unavailable.",
|
|
837
|
-
"Pairs with Flow Rate (cycle) and Win Rate (conversion)."
|
|
838
|
-
],
|
|
839
|
-
visual: {
|
|
840
|
-
kind: "levers",
|
|
841
|
-
caption: "Four levers \u2014 say which one moved",
|
|
842
|
-
levers: ["# Open opps", "Avg deal size", "Win rate", "Cycle days"]
|
|
843
|
-
},
|
|
844
|
-
audience: {
|
|
845
|
-
board: "Velocity is throughput. When it moves, demand the lever \u2014 volume, size, win rate, or cycle \u2014 not a shrug.",
|
|
846
|
-
ops: "(opps \xD7 avgDeal \xD7 winRate) / cycleDays. Diagnose the moved lever before prescribing."
|
|
847
|
-
},
|
|
848
|
-
aliases: ["velocity", "pipeline velocity", "throughput"]
|
|
849
|
-
},
|
|
850
|
-
// —— Sales efficiency ——
|
|
851
|
-
{
|
|
852
|
-
id: "win_rate",
|
|
853
|
-
kind: "saas",
|
|
854
|
-
label: "Win Rate",
|
|
855
|
-
group: "Sales Efficiency",
|
|
856
|
-
tagline: "Of decided deals, how often do we win?",
|
|
857
|
-
how_computed: "closed-won / (won + lost) \xD7 100.",
|
|
858
|
-
formula_lines: ["Win Rate = won / (won + lost) \xD7 100"],
|
|
859
|
-
meaning: 'Board question: "are we converting the pipe we create?" Priors: 25\u201335% SMB, 18\u201325% mid-market, 12\u201318% enterprise on qualified opps.',
|
|
860
|
-
expert_read: "A rising win rate on falling opp volume is qualification tightening, not improvement \u2014 check the denominator.",
|
|
861
|
-
deepdive: [
|
|
862
|
-
"Required coverage \u2248 1 / win rate.",
|
|
863
|
-
"Cut by segment / source before company-wide coaching."
|
|
864
|
-
],
|
|
865
|
-
visual: {
|
|
866
|
-
kind: "split",
|
|
867
|
-
caption: "Exemplar decided deals",
|
|
868
|
-
bars: [
|
|
869
|
-
{ label: "Won", value: 28, tone: "green" },
|
|
870
|
-
{ label: "Lost", value: 72, tone: "red" }
|
|
871
|
-
]
|
|
872
|
-
},
|
|
873
|
-
audience: {
|
|
874
|
-
board: "Win Rate is conversion of decided deals. Rising win rate with falling volume is often tighter qualification, not better selling.",
|
|
875
|
-
ops: "won/(won+lost). Check the denominator. Motion benchmarks set green/yellow bands."
|
|
876
|
-
},
|
|
877
|
-
aliases: ["close rate", "winrate", "win %"],
|
|
878
|
-
benchmarkHint: (motion) => pctBand("win_rate", motion)
|
|
879
|
-
},
|
|
880
|
-
{
|
|
881
|
-
id: "avg_deal_size",
|
|
882
|
-
kind: "saas",
|
|
883
|
-
label: "Avg Deal Size",
|
|
884
|
-
group: "Sales Efficiency",
|
|
885
|
-
tagline: "What does a typical win look like?",
|
|
886
|
-
how_computed: "Mean amount on closed-won opportunities.",
|
|
887
|
-
formula_lines: ["Avg Deal = mean(closed-won amount)"],
|
|
888
|
-
meaning: 'Board question: "are we selling the motion we think we are?"',
|
|
889
|
-
expert_read: "Deal size drifting down while volume rises often means mix shift into a lower segment \u2014 not always a problem, but it changes coverage math.",
|
|
890
|
-
deepdive: [
|
|
891
|
-
"Feeds Pipeline Velocity and LTV proxy.",
|
|
892
|
-
"Cut by segment \u2014 averages hide bimodal motions."
|
|
893
|
-
],
|
|
894
|
-
visual: {
|
|
895
|
-
kind: "bars",
|
|
896
|
-
caption: "Exemplar \u2014 size mix by segment",
|
|
897
|
-
bars: [
|
|
898
|
-
{ label: "SMB", value: 30, tone: "neutral" },
|
|
899
|
-
{ label: "Mid-market", value: 55, tone: "accent" },
|
|
900
|
-
{ label: "Enterprise", value: 90, tone: "green" }
|
|
901
|
-
]
|
|
902
|
-
},
|
|
903
|
-
audience: {
|
|
904
|
-
board: "Avg Deal Size should match the motion you claim. Mix shift changes coverage and capacity math.",
|
|
905
|
-
ops: "Mean closed-won amount. Segment before coaching on size."
|
|
906
|
-
},
|
|
907
|
-
aliases: ["average deal size", "asp", "acv"]
|
|
908
|
-
},
|
|
909
|
-
{
|
|
910
|
-
id: "avg_sales_cycle",
|
|
911
|
-
kind: "saas",
|
|
912
|
-
label: "Avg Sales Cycle",
|
|
913
|
-
group: "Sales Efficiency",
|
|
914
|
-
tagline: "How long from create to close on wins?",
|
|
915
|
-
how_computed: "Mean days from created_at to close date on dated closed-won deals.",
|
|
916
|
-
formula_lines: ["Avg Cycle = mean(close_date \u2212 created_at) on dated wins"],
|
|
917
|
-
meaning: 'Board question: "is the cycle stretching \u2014 the earliest soft signal of deal-quality decay?"',
|
|
918
|
-
expert_read: "Cycle stretching past the motion's norm is the earliest soft signal of deal-quality decay. Pair with Flow Rate stuck stages.",
|
|
919
|
-
deepdive: [
|
|
920
|
-
"Feeds Pipeline Velocity as the denominator.",
|
|
921
|
-
"Needs dated wins \u2014 missing close dates understate/omit."
|
|
922
|
-
],
|
|
923
|
-
visual: {
|
|
924
|
-
kind: "bars",
|
|
925
|
-
caption: "Exemplar cycle vs motion norm",
|
|
926
|
-
bars: [
|
|
927
|
-
{ label: "Your cycle", value: 78, tone: "yellow" },
|
|
928
|
-
{ label: "Motion norm", value: 55, tone: "green" }
|
|
929
|
-
]
|
|
930
|
-
},
|
|
931
|
-
audience: {
|
|
932
|
-
board: "Cycle stretch is an early soft signal that quality or process is slipping \u2014 before the miss shows in bookings.",
|
|
933
|
-
ops: "Mean create\u2192close on dated wins. Investigate the stage that aged."
|
|
934
|
-
},
|
|
935
|
-
aliases: ["sales cycle", "cycle length", "time to close"]
|
|
936
|
-
},
|
|
937
|
-
{
|
|
938
|
-
id: "stage_conversion",
|
|
939
|
-
kind: "saas",
|
|
940
|
-
label: "Stage Conversion",
|
|
941
|
-
group: "Sales Efficiency",
|
|
942
|
-
tagline: "Where in the stage model does advancement collapse?",
|
|
943
|
-
how_computed: "From metadata.stage_history stage advances when present; otherwise a win-rate proxy.",
|
|
944
|
-
formula_lines: [
|
|
945
|
-
"Preferred: advancement rates from stage_history",
|
|
946
|
-
"Fallback: win-rate proxy when history is missing"
|
|
947
|
-
],
|
|
948
|
-
meaning: 'Board question: "which single stage is starving everything downstream?"',
|
|
949
|
-
expert_read: "Find the one stage where conversion collapses \u2014 that's the process problem; everything downstream is starvation.",
|
|
950
|
-
deepdive: [
|
|
951
|
-
"Best with stage_history metadata; otherwise treat as proxy.",
|
|
952
|
-
"Pairs with Flow Rate stage-age cuts."
|
|
953
|
-
],
|
|
954
|
-
visual: {
|
|
955
|
-
kind: "funnel",
|
|
956
|
-
caption: "Exemplar \u2014 find the collapse",
|
|
957
|
-
funnel: [
|
|
958
|
-
{ label: "Stage 1\u21922", widthPct: 100 },
|
|
959
|
-
{ label: "Stage 2\u21923", widthPct: 72 },
|
|
960
|
-
{ label: "Stage 3\u21924", widthPct: 28 },
|
|
961
|
-
{ label: "Stage 4\u2192Close", widthPct: 18 }
|
|
962
|
-
]
|
|
963
|
-
},
|
|
964
|
-
audience: {
|
|
965
|
-
board: "Stage Conversion names the bottleneck stage \u2014 one collapse starves every stage after it.",
|
|
966
|
-
ops: "Prefer stage_history advances. Fix the collapse stage before coaching downstream reps."
|
|
967
|
-
},
|
|
968
|
-
aliases: ["stage conversion", "stage advance", "conversion by stage"]
|
|
969
|
-
},
|
|
970
|
-
// —— Unit economics ——
|
|
971
|
-
{
|
|
972
|
-
id: "ltv_proxy",
|
|
973
|
-
kind: "saas",
|
|
974
|
-
label: "LTV (Proxy)",
|
|
975
|
-
group: "Unit Economics",
|
|
976
|
-
tagline: "Rough lifetime value from deal size and GRR.",
|
|
977
|
-
how_computed: "avgDeal / ((100 \u2212 GRR) / 100) when GRR < 100. Unavailable when GRR is 100%+ or missing.",
|
|
978
|
-
formula_lines: [
|
|
979
|
-
"LTV \u2248 avgDeal / churnRate",
|
|
980
|
-
"churnRate = (100 \u2212 GRR) / 100 (requires GRR < 100)"
|
|
981
|
-
],
|
|
982
|
-
meaning: 'Board question: "what is a customer roughly worth over their life?"',
|
|
983
|
-
expert_read: "This is a proxy \u2014 not a cohort LTV. Use it for direction, not capital allocation.",
|
|
984
|
-
deepdive: [
|
|
985
|
-
"Unavailable when GRR \u2265 100 or missing.",
|
|
986
|
-
"Pairs with CAC for LTV:CAC when spend data exists."
|
|
987
|
-
],
|
|
988
|
-
visual: {
|
|
989
|
-
kind: "gauge",
|
|
990
|
-
caption: "Exemplar LTV proxy (directional)",
|
|
991
|
-
gauge: 68
|
|
992
|
-
},
|
|
993
|
-
audience: {
|
|
994
|
-
board: "LTV Proxy is directional from deal size and GRR \u2014 not a cohort LTV. Use for orientation, not capital decisions.",
|
|
995
|
-
ops: "avgDeal / ((100\u2212GRR)/100). Needs GRR < 100. Prefer cohort math when billing data arrives."
|
|
996
|
-
},
|
|
997
|
-
aliases: ["ltv", "lifetime value"]
|
|
998
|
-
},
|
|
999
|
-
{
|
|
1000
|
-
id: "cac",
|
|
1001
|
-
kind: "saas",
|
|
1002
|
-
label: "CAC",
|
|
1003
|
-
group: "Unit Economics",
|
|
1004
|
-
tagline: "Customer acquisition cost \u2014 needs spend data.",
|
|
1005
|
-
how_computed: "Requires campaign / sales spend data. Currently unavailable on CRM-only datasets.",
|
|
1006
|
-
formula_lines: ["CAC = sales & marketing spend / new customers", "(requires spend data \u2014 not in CRM-only exports)"],
|
|
1007
|
-
meaning: 'Board question: "what does a new logo cost to win?"',
|
|
1008
|
-
expert_read: "Without spend, NTRP cannot invent CAC. Wire campaign spend or finance exports to unlock unit economics.",
|
|
1009
|
-
deepdive: [
|
|
1010
|
-
"Always unavailable on CRM-only demos \u2014 expected.",
|
|
1011
|
-
"Unlocks LTV:CAC, Payback, Magic Number when spend lands."
|
|
1012
|
-
],
|
|
1013
|
-
visual: { kind: "none", caption: "Needs campaign spend / finance export" },
|
|
1014
|
-
audience: {
|
|
1015
|
-
board: "CAC is locked until spend data is connected \u2014 CRM alone cannot price acquisition.",
|
|
1016
|
-
ops: "Bring campaign or S&M spend. Until then unit-econ metrics stay unavailable by design."
|
|
1017
|
-
},
|
|
1018
|
-
aliases: ["customer acquisition cost", "acquisition cost"]
|
|
1019
|
-
},
|
|
1020
|
-
{
|
|
1021
|
-
id: "ltv_cac_ratio",
|
|
1022
|
-
kind: "saas",
|
|
1023
|
-
label: "LTV:CAC Ratio",
|
|
1024
|
-
group: "Unit Economics",
|
|
1025
|
-
tagline: "Is acquisition spend earning its keep?",
|
|
1026
|
-
how_computed: "LTV proxy \xF7 CAC. Unavailable without spend (CAC).",
|
|
1027
|
-
formula_lines: ["LTV:CAC = LTV_proxy / CAC", "(requires CAC)"],
|
|
1028
|
-
meaning: 'Board question: "do we earn enough lifetime value per dollar spent to acquire?"',
|
|
1029
|
-
expert_read: "Efficiency era: boards weigh LTV:CAC and payback as heavily as growth. Classic rule of thumb \u22653x, but motion and gross margin matter.",
|
|
1030
|
-
deepdive: ["Blocked on CAC. See LTV Proxy and CAC."],
|
|
1031
|
-
visual: { kind: "none", caption: "Needs CAC (spend data)" },
|
|
1032
|
-
audience: {
|
|
1033
|
-
board: "LTV:CAC is the acquisition ROI story \u2014 available once spend is wired.",
|
|
1034
|
-
ops: "LTV_proxy / CAC. Unlocks with spend import."
|
|
1035
|
-
},
|
|
1036
|
-
aliases: ["ltv cac", "ltv/cac", "ltv to cac"]
|
|
1037
|
-
},
|
|
1038
|
-
{
|
|
1039
|
-
id: "payback_months",
|
|
1040
|
-
kind: "saas",
|
|
1041
|
-
label: "Payback Months",
|
|
1042
|
-
group: "Unit Economics",
|
|
1043
|
-
tagline: "How many months to recover CAC?",
|
|
1044
|
-
how_computed: "Requires CAC / spend. Lower is better.",
|
|
1045
|
-
formula_lines: ["Payback \u2248 CAC / (monthly gross profit per customer)", "(requires spend data)"],
|
|
1046
|
-
meaning: 'Board question: "how fast does acquisition spend return?" Efficiency era prior: <18 months often healthy.',
|
|
1047
|
-
expert_read: "Boards now weigh payback (<18mo) as heavily as growth in many motions.",
|
|
1048
|
-
deepdive: ["Blocked on CAC. Benchmarks exist per motion once data lands."],
|
|
1049
|
-
visual: { kind: "none", caption: "Needs CAC (spend data)" },
|
|
1050
|
-
audience: {
|
|
1051
|
-
board: "Payback is how fast CAC returns. Efficiency-era boards often want <18 months.",
|
|
1052
|
-
ops: "Requires CAC. Motion green/yellow bands apply when available."
|
|
1053
|
-
},
|
|
1054
|
-
aliases: ["payback", "cac payback"],
|
|
1055
|
-
benchmarkHint: (motion) => monthsBand(motion)
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
id: "magic_number",
|
|
1059
|
-
kind: "saas",
|
|
1060
|
-
label: "Magic Number",
|
|
1061
|
-
group: "Unit Economics",
|
|
1062
|
-
tagline: "Sales efficiency \u2014 net new ARR per sales dollar.",
|
|
1063
|
-
how_computed: "Requires sales spend. Classic form: net new ARR (quarter) / prior-quarter S&M spend.",
|
|
1064
|
-
formula_lines: [
|
|
1065
|
-
"Magic Number \u2248 Net New ARR(q) / S&M spend(q\u22121)",
|
|
1066
|
-
"(requires spend data)"
|
|
1067
|
-
],
|
|
1068
|
-
meaning: 'Board question: "how efficiently does sales spend produce net new ARR?" Prior: >0.75 often healthy; >1 strong.',
|
|
1069
|
-
expert_read: "Efficiency era: magic number >0.75 is weighed alongside growth. Without spend, NTRP keeps this unavailable rather than inventing it.",
|
|
1070
|
-
deepdive: ["Blocked on spend. Benchmarks per motion ready when data lands."],
|
|
1071
|
-
visual: { kind: "none", caption: "Needs S&M spend data" },
|
|
1072
|
-
audience: {
|
|
1073
|
-
board: "Magic Number prices sales efficiency. Available once S&M spend is connected.",
|
|
1074
|
-
ops: "Net new ARR / prior S&M. Motion benchmarks apply when spend lands."
|
|
1075
|
-
},
|
|
1076
|
-
aliases: ["sales magic number", "sales efficiency magic number"],
|
|
1077
|
-
benchmarkHint: (motion) => magicBand(motion)
|
|
1078
|
-
}
|
|
1079
|
-
];
|
|
1080
|
-
METRIC_DEFINITIONS = [...VITALS, ...SAAS];
|
|
1081
|
-
BY_ID = new Map(METRIC_DEFINITIONS.map((m) => [m.id, m]));
|
|
1082
|
-
ALIAS_INDEX = (() => {
|
|
1083
|
-
const idx = /* @__PURE__ */ new Map();
|
|
1084
|
-
for (const m of METRIC_DEFINITIONS) {
|
|
1085
|
-
idx.set(m.id.toLowerCase(), m.id);
|
|
1086
|
-
idx.set(m.label.toLowerCase(), m.id);
|
|
1087
|
-
for (const a of m.aliases ?? []) {
|
|
1088
|
-
idx.set(a.toLowerCase(), m.id);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
idx.set("signal-to-noise", "signal_to_noise");
|
|
1092
|
-
idx.set("signal:noise", "signal_to_noise");
|
|
1093
|
-
idx.set("flow-rate", "flow_rate");
|
|
1094
|
-
idx.set("drop-rate", "drop_rate");
|
|
1095
|
-
idx.set("thread-depth", "thread_depth");
|
|
1096
|
-
return idx;
|
|
1097
|
-
})();
|
|
1098
|
-
SAAS_METRIC_IDS = SAAS.map((m) => m.id);
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
// src/services/metric-explainers.ts
|
|
1103
|
-
var init_metric_explainers = __esm({
|
|
1104
|
-
"src/services/metric-explainers.ts"() {
|
|
1105
|
-
"use strict";
|
|
1106
|
-
init_metric_definitions();
|
|
1107
|
-
}
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
// src/conversation/handoff-draft.ts
|
|
1111
|
-
function isShipIntent(input) {
|
|
1112
|
-
const line = input.trim();
|
|
1113
|
-
if (/\?\s*$/.test(line) || QUESTION_LEAD_RE.test(line)) return false;
|
|
1114
|
-
return SHIP_INTENT_RE.test(line);
|
|
1115
|
-
}
|
|
1116
|
-
var QUESTION_LEAD_RE, SHIP_INTENT_RE;
|
|
1117
|
-
var init_handoff_draft = __esm({
|
|
1118
|
-
"src/conversation/handoff-draft.ts"() {
|
|
1119
|
-
"use strict";
|
|
1120
|
-
init_profile();
|
|
1121
|
-
init_session_analysis();
|
|
1122
|
-
init_metric_explainers();
|
|
1123
|
-
QUESTION_LEAD_RE = /^\s*(what|why|how|when|where|who|which|is|are|was|were|do|does|did|explain|tell me|help me understand)\b/i;
|
|
1124
|
-
SHIP_INTENT_RE = /\b(ship|export|deliver|write[- ]?up|board memo|action plan|turn (this|it|that) into|(draft|create|make|build|prepare|generate|send)\s+(me\s+)?(a\s+|the\s+)?hand[- ]?off|hand[- ]?off\s+(prompt|doc|document|plan))\b/i;
|
|
1125
|
-
}
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
// src/ai/llm/providers.ts
|
|
1129
|
-
var init_providers = __esm({
|
|
1130
|
-
"src/ai/llm/providers.ts"() {
|
|
1131
|
-
"use strict";
|
|
1132
|
-
init_store();
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
// src/config/llm-config.ts
|
|
1137
|
-
var init_llm_config = __esm({
|
|
1138
|
-
"src/config/llm-config.ts"() {
|
|
1139
|
-
"use strict";
|
|
1140
|
-
init_providers();
|
|
1141
|
-
init_store();
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
// src/ai/llm/gate.ts
|
|
1146
|
-
var init_gate = __esm({
|
|
1147
|
-
"src/ai/llm/gate.ts"() {
|
|
1148
|
-
"use strict";
|
|
1149
|
-
init_llm_config();
|
|
1150
|
-
}
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
// src/ai/repl-api.ts
|
|
1154
|
-
var init_repl_api = __esm({
|
|
1155
|
-
"src/ai/repl-api.ts"() {
|
|
1156
|
-
"use strict";
|
|
1157
|
-
init_gate();
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
// src/cli/repl-globals.ts
|
|
1162
|
-
var init_repl_globals = __esm({
|
|
1163
|
-
"src/cli/repl-globals.ts"() {
|
|
1164
|
-
"use strict";
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
// src/cli/prompts.ts
|
|
1169
|
-
import { createInterface } from "readline/promises";
|
|
1170
|
-
import { clearLine, cursorTo } from "readline";
|
|
1171
|
-
import { StringDecoder } from "string_decoder";
|
|
1172
|
-
import chalk2 from "chalk";
|
|
1173
|
-
var init_prompts = __esm({
|
|
1174
|
-
"src/cli/prompts.ts"() {
|
|
1175
|
-
"use strict";
|
|
1176
|
-
init_repl_globals();
|
|
1177
|
-
init_theme();
|
|
1178
|
-
}
|
|
1179
|
-
});
|
|
1180
|
-
|
|
1181
|
-
// src/pipeline/segments.ts
|
|
1182
|
-
var init_segments = __esm({
|
|
1183
|
-
"src/pipeline/segments.ts"() {
|
|
1184
|
-
"use strict";
|
|
1185
|
-
init_connection();
|
|
1186
|
-
init_queries();
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
// src/baselines/defaults.ts
|
|
1191
|
-
var init_defaults = __esm({
|
|
1192
|
-
"src/baselines/defaults.ts"() {
|
|
1193
|
-
"use strict";
|
|
1194
|
-
}
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
// src/baselines/profile-presets.ts
|
|
1198
|
-
var init_profile_presets = __esm({
|
|
1199
|
-
"src/baselines/profile-presets.ts"() {
|
|
1200
|
-
"use strict";
|
|
1201
|
-
}
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
// src/baselines/resolve.ts
|
|
1205
|
-
var init_resolve = __esm({
|
|
1206
|
-
"src/baselines/resolve.ts"() {
|
|
1207
|
-
"use strict";
|
|
1208
|
-
init_defaults();
|
|
1209
|
-
init_profile_presets();
|
|
1210
|
-
}
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
// src/vitals/shared.ts
|
|
1214
|
-
var init_shared = __esm({
|
|
1215
|
-
"src/vitals/shared.ts"() {
|
|
1216
|
-
"use strict";
|
|
1217
|
-
}
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
// src/vitals/freshness.ts
|
|
1221
|
-
var init_freshness = __esm({
|
|
1222
|
-
"src/vitals/freshness.ts"() {
|
|
1223
|
-
"use strict";
|
|
1224
|
-
init_defaults();
|
|
1225
|
-
init_shared();
|
|
1226
|
-
}
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
|
-
// src/vitals/flow-rate.ts
|
|
1230
|
-
var init_flow_rate = __esm({
|
|
1231
|
-
"src/vitals/flow-rate.ts"() {
|
|
1232
|
-
"use strict";
|
|
1233
|
-
init_defaults();
|
|
1234
|
-
init_shared();
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
// src/vitals/drop-rate.ts
|
|
1239
|
-
var init_drop_rate = __esm({
|
|
1240
|
-
"src/vitals/drop-rate.ts"() {
|
|
1241
|
-
"use strict";
|
|
1242
|
-
init_defaults();
|
|
1243
|
-
init_shared();
|
|
1244
|
-
}
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
// src/vitals/signal-to-noise.ts
|
|
1248
|
-
var init_signal_to_noise = __esm({
|
|
1249
|
-
"src/vitals/signal-to-noise.ts"() {
|
|
1250
|
-
"use strict";
|
|
1251
|
-
init_defaults();
|
|
1252
|
-
init_store();
|
|
1253
|
-
init_shared();
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
// src/vitals/thread-depth.ts
|
|
1258
|
-
var init_thread_depth = __esm({
|
|
1259
|
-
"src/vitals/thread-depth.ts"() {
|
|
1260
|
-
"use strict";
|
|
1261
|
-
init_defaults();
|
|
1262
|
-
init_shared();
|
|
1263
|
-
}
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
// src/vitals/health-score.ts
|
|
1267
|
-
var LAYERS;
|
|
1268
|
-
var init_health_score = __esm({
|
|
1269
|
-
"src/vitals/health-score.ts"() {
|
|
1270
|
-
"use strict";
|
|
1271
|
-
init_segments();
|
|
1272
|
-
init_resolve();
|
|
1273
|
-
init_freshness();
|
|
1274
|
-
init_flow_rate();
|
|
1275
|
-
init_drop_rate();
|
|
1276
|
-
init_signal_to_noise();
|
|
1277
|
-
init_thread_depth();
|
|
1278
|
-
init_connection();
|
|
1279
|
-
init_queries();
|
|
1280
|
-
LAYERS = [
|
|
1281
|
-
{ layer: 1, signs: ["freshness"] },
|
|
1282
|
-
{ layer: 2, signs: ["flow_rate", "drop_rate"] },
|
|
1283
|
-
{ layer: 3, signs: ["signal_to_noise"] },
|
|
1284
|
-
{ layer: 4, signs: ["thread_depth"] }
|
|
1285
|
-
];
|
|
1286
|
-
}
|
|
1287
|
-
});
|
|
1288
|
-
|
|
1289
|
-
// src/pipeline/divergence.ts
|
|
1290
|
-
var init_divergence = __esm({
|
|
1291
|
-
"src/pipeline/divergence.ts"() {
|
|
1292
|
-
"use strict";
|
|
1293
|
-
}
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
// src/db/schema.ts
|
|
1297
|
-
var init_schema = __esm({
|
|
1298
|
-
"src/db/schema.ts"() {
|
|
1299
|
-
"use strict";
|
|
1300
|
-
init_connection();
|
|
1301
|
-
}
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
// src/metrics/classify-source.ts
|
|
1305
|
-
var init_classify_source = __esm({
|
|
1306
|
-
"src/metrics/classify-source.ts"() {
|
|
1307
|
-
"use strict";
|
|
1308
|
-
}
|
|
1309
|
-
});
|
|
1310
|
-
|
|
1311
|
-
// src/metrics/helpers.ts
|
|
1312
|
-
var init_helpers = __esm({
|
|
1313
|
-
"src/metrics/helpers.ts"() {
|
|
1314
|
-
"use strict";
|
|
1315
|
-
init_metrics_benchmarks();
|
|
1316
|
-
}
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
// src/metrics/coverage.ts
|
|
1320
|
-
var init_coverage = __esm({
|
|
1321
|
-
"src/metrics/coverage.ts"() {
|
|
1322
|
-
"use strict";
|
|
1323
|
-
init_helpers();
|
|
1324
|
-
init_classify_source();
|
|
1325
|
-
}
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
// src/metrics/context.ts
|
|
1329
|
-
var init_context3 = __esm({
|
|
1330
|
-
"src/metrics/context.ts"() {
|
|
1331
|
-
"use strict";
|
|
1332
|
-
init_metrics_benchmarks();
|
|
1333
|
-
init_profile();
|
|
1334
|
-
init_health_score();
|
|
1335
|
-
init_queries();
|
|
1336
|
-
init_classify_source();
|
|
1337
|
-
init_coverage();
|
|
1338
|
-
}
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
// src/metrics/revenue.ts
|
|
1342
|
-
var init_revenue = __esm({
|
|
1343
|
-
"src/metrics/revenue.ts"() {
|
|
1344
|
-
"use strict";
|
|
1345
|
-
init_helpers();
|
|
1346
|
-
init_formatters();
|
|
1347
|
-
}
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
// src/metrics/retention.ts
|
|
1351
|
-
var init_retention = __esm({
|
|
1352
|
-
"src/metrics/retention.ts"() {
|
|
1353
|
-
"use strict";
|
|
1354
|
-
init_revenue();
|
|
1355
|
-
init_helpers();
|
|
1356
|
-
init_metrics_benchmarks();
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
// src/metrics/pipeline.ts
|
|
1361
|
-
var init_pipeline = __esm({
|
|
1362
|
-
"src/metrics/pipeline.ts"() {
|
|
1363
|
-
"use strict";
|
|
1364
|
-
init_helpers();
|
|
1365
|
-
init_formatters();
|
|
1366
|
-
init_metrics_benchmarks();
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
|
|
1370
|
-
// src/metrics/sales-efficiency.ts
|
|
1371
|
-
var init_sales_efficiency = __esm({
|
|
1372
|
-
"src/metrics/sales-efficiency.ts"() {
|
|
1373
|
-
"use strict";
|
|
1374
|
-
init_helpers();
|
|
1375
|
-
init_formatters();
|
|
1376
|
-
init_metrics_benchmarks();
|
|
1377
|
-
}
|
|
1378
|
-
});
|
|
1379
|
-
|
|
1380
|
-
// src/metrics/unit-economics.ts
|
|
1381
|
-
var init_unit_economics = __esm({
|
|
1382
|
-
"src/metrics/unit-economics.ts"() {
|
|
1383
|
-
"use strict";
|
|
1384
|
-
init_formatters();
|
|
1385
|
-
}
|
|
1386
|
-
});
|
|
1387
|
-
|
|
1388
|
-
// src/metrics/confidence.ts
|
|
1389
|
-
var init_confidence = __esm({
|
|
1390
|
-
"src/metrics/confidence.ts"() {
|
|
1391
|
-
"use strict";
|
|
1392
|
-
}
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
// src/metrics/periods.ts
|
|
1396
|
-
var init_periods = __esm({
|
|
1397
|
-
"src/metrics/periods.ts"() {
|
|
1398
|
-
"use strict";
|
|
1399
|
-
init_helpers();
|
|
1400
|
-
}
|
|
1401
|
-
});
|
|
1402
|
-
|
|
1403
|
-
// src/metrics/compute.ts
|
|
1404
|
-
var init_compute = __esm({
|
|
1405
|
-
"src/metrics/compute.ts"() {
|
|
1406
|
-
"use strict";
|
|
1407
|
-
init_segments();
|
|
1408
|
-
init_queries();
|
|
1409
|
-
init_context3();
|
|
1410
|
-
init_revenue();
|
|
1411
|
-
init_retention();
|
|
1412
|
-
init_pipeline();
|
|
1413
|
-
init_sales_efficiency();
|
|
1414
|
-
init_unit_economics();
|
|
1415
|
-
init_confidence();
|
|
1416
|
-
init_periods();
|
|
1417
|
-
init_context3();
|
|
1418
|
-
}
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
// src/metrics/insights.ts
|
|
1422
|
-
var init_insights = __esm({
|
|
1423
|
-
"src/metrics/insights.ts"() {
|
|
1424
|
-
"use strict";
|
|
1425
|
-
init_coverage();
|
|
1426
|
-
}
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
// src/ai/explore-mode.ts
|
|
1430
|
-
var init_explore_mode = __esm({
|
|
1431
|
-
"src/ai/explore-mode.ts"() {
|
|
1432
|
-
"use strict";
|
|
1433
|
-
init_context2();
|
|
1434
|
-
}
|
|
1435
|
-
});
|
|
1436
|
-
|
|
1437
|
-
// src/ai/llm/models-cache.ts
|
|
1438
|
-
var CACHE_TTL_MS;
|
|
1439
|
-
var init_models_cache = __esm({
|
|
1440
|
-
"src/ai/llm/models-cache.ts"() {
|
|
1441
|
-
"use strict";
|
|
1442
|
-
init_store();
|
|
1443
|
-
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1444
|
-
}
|
|
1445
|
-
});
|
|
1446
|
-
|
|
1447
|
-
// src/ai/llm/catalog.ts
|
|
1448
|
-
var ENTRIES, byId;
|
|
1449
|
-
var init_catalog = __esm({
|
|
1450
|
-
"src/ai/llm/catalog.ts"() {
|
|
1451
|
-
"use strict";
|
|
1452
|
-
init_models_cache();
|
|
1453
|
-
ENTRIES = [
|
|
1454
|
-
{
|
|
1455
|
-
id: "claude-opus-4-6",
|
|
1456
|
-
provider: "anthropic",
|
|
1457
|
-
tier: "high",
|
|
1458
|
-
status: "active",
|
|
1459
|
-
successor_id: null,
|
|
1460
|
-
supports_tools: true,
|
|
1461
|
-
max_context_tokens: 2e5,
|
|
1462
|
-
display_name: "Claude Opus 4.6",
|
|
1463
|
-
relative_cost: 3
|
|
1464
|
-
},
|
|
1465
|
-
{
|
|
1466
|
-
id: "claude-sonnet-4-5-20250929",
|
|
1467
|
-
provider: "anthropic",
|
|
1468
|
-
tier: "medium",
|
|
1469
|
-
status: "active",
|
|
1470
|
-
successor_id: null,
|
|
1471
|
-
supports_tools: true,
|
|
1472
|
-
max_context_tokens: 2e5,
|
|
1473
|
-
display_name: "Claude Sonnet 4.5",
|
|
1474
|
-
relative_cost: 2
|
|
1475
|
-
},
|
|
1476
|
-
{
|
|
1477
|
-
id: "claude-haiku-4-5-20251001",
|
|
1478
|
-
provider: "anthropic",
|
|
1479
|
-
tier: "low",
|
|
1480
|
-
status: "active",
|
|
1481
|
-
successor_id: null,
|
|
1482
|
-
supports_tools: true,
|
|
1483
|
-
max_context_tokens: 2e5,
|
|
1484
|
-
display_name: "Claude Haiku 4.5",
|
|
1485
|
-
relative_cost: 1
|
|
1486
|
-
},
|
|
1487
|
-
{
|
|
1488
|
-
id: "gpt-4.1",
|
|
1489
|
-
provider: "openai",
|
|
1490
|
-
tier: "high",
|
|
1491
|
-
status: "active",
|
|
1492
|
-
successor_id: null,
|
|
1493
|
-
supports_tools: true,
|
|
1494
|
-
max_context_tokens: 1047576,
|
|
1495
|
-
display_name: "GPT-4.1",
|
|
1496
|
-
relative_cost: 3
|
|
1497
|
-
},
|
|
1498
|
-
{
|
|
1499
|
-
id: "gpt-4.1-mini",
|
|
1500
|
-
provider: "openai",
|
|
1501
|
-
tier: "medium",
|
|
1502
|
-
status: "active",
|
|
1503
|
-
successor_id: null,
|
|
1504
|
-
supports_tools: true,
|
|
1505
|
-
max_context_tokens: 1047576,
|
|
1506
|
-
display_name: "GPT-4.1 Mini",
|
|
1507
|
-
relative_cost: 2
|
|
1508
|
-
},
|
|
1509
|
-
{
|
|
1510
|
-
id: "gpt-4.1-nano",
|
|
1511
|
-
provider: "openai",
|
|
1512
|
-
tier: "low",
|
|
1513
|
-
status: "active",
|
|
1514
|
-
successor_id: null,
|
|
1515
|
-
supports_tools: true,
|
|
1516
|
-
max_context_tokens: 1047576,
|
|
1517
|
-
display_name: "GPT-4.1 Nano",
|
|
1518
|
-
relative_cost: 1
|
|
1519
|
-
}
|
|
1520
|
-
];
|
|
1521
|
-
byId = new Map(ENTRIES.map((e) => [e.id, e]));
|
|
1522
|
-
}
|
|
1523
|
-
});
|
|
1524
|
-
|
|
1525
|
-
// src/ai/llm/surfaces.ts
|
|
1526
|
-
var init_surfaces = __esm({
|
|
1527
|
-
"src/ai/llm/surfaces.ts"() {
|
|
1528
|
-
"use strict";
|
|
1529
|
-
}
|
|
1530
|
-
});
|
|
1531
|
-
|
|
1532
|
-
// src/ai/llm/session-state.ts
|
|
1533
|
-
var init_session_state = __esm({
|
|
1534
|
-
"src/ai/llm/session-state.ts"() {
|
|
1535
|
-
"use strict";
|
|
1536
|
-
init_llm_config();
|
|
1537
|
-
init_catalog();
|
|
1538
|
-
init_surfaces();
|
|
1539
|
-
}
|
|
1540
|
-
});
|
|
1541
|
-
|
|
1542
|
-
// src/conversation/recommended-action.ts
|
|
1543
|
-
var init_recommended_action = __esm({
|
|
1544
|
-
"src/conversation/recommended-action.ts"() {
|
|
1545
|
-
"use strict";
|
|
1546
|
-
init_repl_api();
|
|
1547
|
-
init_phase();
|
|
1548
|
-
}
|
|
1549
|
-
});
|
|
1550
|
-
|
|
1551
|
-
// src/conversation/phase.ts
|
|
1552
|
-
import chalk3 from "chalk";
|
|
1553
|
-
function sessionHasData(ctx) {
|
|
1554
|
-
const counts = ctx.dataset?.counts ?? {};
|
|
1555
|
-
return Object.values(counts).some((n) => (n ?? 0) > 0);
|
|
1556
|
-
}
|
|
1557
|
-
function resolveConversationPhase(ctx) {
|
|
1558
|
-
if (ctx.deliverIntent) return "deliver";
|
|
1559
|
-
if (ctx.computeInProgress) return "compute";
|
|
1560
|
-
if (ctx.strategistState && ctx.strategistState.step !== "awaiting_analysis" && ctx.strategistState.step !== "awaiting_connect") {
|
|
1561
|
-
return "strategize";
|
|
1562
|
-
}
|
|
1563
|
-
if (isAnalysisReady(ctx)) return "explore";
|
|
1564
|
-
const scope = ctx.scope;
|
|
1565
|
-
if (scope?.confirmed_at) {
|
|
1566
|
-
if (!sessionHasData(ctx)) return "awaiting_data";
|
|
1567
|
-
if (ctx.stage !== "analyzed") return "awaiting_data";
|
|
1568
|
-
}
|
|
1569
|
-
if (scope?.intent_summary && !scope.confirmed_at) return "scope";
|
|
1570
|
-
return "orient";
|
|
1571
|
-
}
|
|
1572
|
-
var init_phase = __esm({
|
|
1573
|
-
"src/conversation/phase.ts"() {
|
|
1574
|
-
"use strict";
|
|
1575
|
-
init_context2();
|
|
1576
|
-
init_repl_api();
|
|
1577
|
-
init_explore_mode();
|
|
1578
|
-
init_session_state();
|
|
1579
|
-
init_theme();
|
|
1580
|
-
init_recommended_action();
|
|
1581
|
-
}
|
|
1582
|
-
});
|
|
1583
|
-
|
|
1584
|
-
// src/conversation/gap-audit.ts
|
|
1585
|
-
var init_gap_audit = __esm({
|
|
1586
|
-
"src/conversation/gap-audit.ts"() {
|
|
1587
|
-
"use strict";
|
|
1588
|
-
init_connection();
|
|
1589
|
-
init_queries();
|
|
1590
|
-
init_profile();
|
|
1591
|
-
init_compute();
|
|
1592
|
-
init_insights();
|
|
1593
|
-
init_phase();
|
|
1594
|
-
}
|
|
1595
|
-
});
|
|
1596
|
-
|
|
1597
|
-
// src/strategies/library.ts
|
|
1598
|
-
import { writeFileSync as writeFileSync7 } from "fs";
|
|
1599
|
-
import { join as join7 } from "path";
|
|
1600
|
-
import { stringify as stringifyYaml } from "yaml";
|
|
1601
|
-
var init_library = __esm({
|
|
1602
|
-
"src/strategies/library.ts"() {
|
|
1603
|
-
"use strict";
|
|
1604
|
-
init_store();
|
|
1605
|
-
}
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
|
-
// src/io/types.ts
|
|
1609
|
-
var init_types = __esm({
|
|
1610
|
-
"src/io/types.ts"() {
|
|
1611
|
-
"use strict";
|
|
1612
|
-
}
|
|
1613
|
-
});
|
|
1614
|
-
|
|
1615
|
-
// src/io/errors.ts
|
|
1616
|
-
var init_errors = __esm({
|
|
1617
|
-
"src/io/errors.ts"() {
|
|
1618
|
-
"use strict";
|
|
1619
|
-
init_types();
|
|
1620
|
-
}
|
|
1621
|
-
});
|
|
1622
|
-
|
|
1623
|
-
// src/services/strategist.ts
|
|
1624
|
-
import { createHash } from "crypto";
|
|
1625
|
-
var init_strategist = __esm({
|
|
1626
|
-
"src/services/strategist.ts"() {
|
|
1627
|
-
"use strict";
|
|
1628
|
-
init_schema();
|
|
1629
|
-
init_queries();
|
|
1630
|
-
init_health_score();
|
|
1631
|
-
init_divergence();
|
|
1632
|
-
init_gap_audit();
|
|
1633
|
-
init_library();
|
|
1634
|
-
init_errors();
|
|
1635
|
-
init_types();
|
|
1636
|
-
init_formatters();
|
|
1637
|
-
init_formatters();
|
|
1638
|
-
}
|
|
1639
|
-
});
|
|
1640
|
-
|
|
1641
|
-
// src/ai/llm/types.ts
|
|
1642
|
-
var init_types2 = __esm({
|
|
1643
|
-
"src/ai/llm/types.ts"() {
|
|
1644
|
-
"use strict";
|
|
1645
|
-
}
|
|
1646
|
-
});
|
|
1647
|
-
|
|
1648
|
-
// src/config/install.ts
|
|
1649
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
1650
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
1651
|
-
import { join as join8 } from "path";
|
|
1652
|
-
var init_install = __esm({
|
|
1653
|
-
"src/config/install.ts"() {
|
|
1654
|
-
"use strict";
|
|
1655
|
-
init_store();
|
|
1656
|
-
}
|
|
1657
|
-
});
|
|
1658
|
-
|
|
1659
|
-
// src/config/progress-migrate.ts
|
|
1660
|
-
import { existsSync as existsSync7, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync9 } from "fs";
|
|
1661
|
-
import { join as join9 } from "path";
|
|
1662
|
-
var init_progress_migrate = __esm({
|
|
1663
|
-
"src/config/progress-migrate.ts"() {
|
|
1664
|
-
"use strict";
|
|
1665
|
-
init_store();
|
|
1666
|
-
}
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
|
-
// src/whimsy/usage-backfill.ts
|
|
1670
|
-
var init_usage_backfill = __esm({
|
|
1671
|
-
"src/whimsy/usage-backfill.ts"() {
|
|
1672
|
-
"use strict";
|
|
1673
|
-
}
|
|
1674
|
-
});
|
|
1675
|
-
|
|
1676
|
-
// src/config/progress.ts
|
|
1677
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
1678
|
-
import { join as join10 } from "path";
|
|
1679
|
-
var init_progress = __esm({
|
|
1680
|
-
"src/config/progress.ts"() {
|
|
1681
|
-
"use strict";
|
|
1682
|
-
init_install();
|
|
1683
|
-
init_progress_migrate();
|
|
1684
|
-
init_store();
|
|
1685
|
-
init_usage_backfill();
|
|
1686
|
-
}
|
|
1687
|
-
});
|
|
1688
|
-
|
|
1689
|
-
// src/whimsy/usage-stats.ts
|
|
1690
|
-
var init_usage_stats = __esm({
|
|
1691
|
-
"src/whimsy/usage-stats.ts"() {
|
|
1692
|
-
"use strict";
|
|
1693
|
-
init_progress();
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
|
|
1697
|
-
// src/ai/llm/errors.ts
|
|
1698
|
-
var init_errors2 = __esm({
|
|
1699
|
-
"src/ai/llm/errors.ts"() {
|
|
1700
|
-
"use strict";
|
|
1701
|
-
init_types2();
|
|
1702
|
-
}
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
// src/ai/llm/adapters/anthropic.ts
|
|
1706
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
1707
|
-
var init_anthropic = __esm({
|
|
1708
|
-
"src/ai/llm/adapters/anthropic.ts"() {
|
|
1709
|
-
"use strict";
|
|
1710
|
-
init_errors2();
|
|
1711
|
-
init_types2();
|
|
1712
|
-
}
|
|
1713
|
-
});
|
|
1714
|
-
|
|
1715
|
-
// src/ai/llm/adapters/openai-compat.ts
|
|
1716
|
-
import OpenAI from "openai";
|
|
1717
|
-
var init_openai_compat = __esm({
|
|
1718
|
-
"src/ai/llm/adapters/openai-compat.ts"() {
|
|
1719
|
-
"use strict";
|
|
1720
|
-
init_errors2();
|
|
1721
|
-
init_types2();
|
|
1722
|
-
}
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
// src/ai/llm/http.ts
|
|
1726
|
-
var init_http = __esm({
|
|
1727
|
-
"src/ai/llm/http.ts"() {
|
|
1728
|
-
"use strict";
|
|
1729
|
-
}
|
|
1730
|
-
});
|
|
1731
|
-
|
|
1732
|
-
// src/ai/llm/ranking.ts
|
|
1733
|
-
var init_ranking = __esm({
|
|
1734
|
-
"src/ai/llm/ranking.ts"() {
|
|
1735
|
-
"use strict";
|
|
1736
|
-
}
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
|
-
// src/ai/llm/discovery.ts
|
|
1740
|
-
var init_discovery = __esm({
|
|
1741
|
-
"src/ai/llm/discovery.ts"() {
|
|
1742
|
-
"use strict";
|
|
1743
|
-
init_llm_config();
|
|
1744
|
-
init_http();
|
|
1745
|
-
init_models_cache();
|
|
1746
|
-
init_providers();
|
|
1747
|
-
init_ranking();
|
|
1748
|
-
}
|
|
1749
|
-
});
|
|
1750
|
-
|
|
1751
|
-
// src/ai/llm/heal.ts
|
|
1752
|
-
var init_heal = __esm({
|
|
1753
|
-
"src/ai/llm/heal.ts"() {
|
|
1754
|
-
"use strict";
|
|
1755
|
-
init_store();
|
|
1756
|
-
init_discovery();
|
|
1757
|
-
init_models_cache();
|
|
1758
|
-
}
|
|
1759
|
-
});
|
|
1760
|
-
|
|
1761
|
-
// src/ai/llm/resolver.ts
|
|
1762
|
-
var init_resolver = __esm({
|
|
1763
|
-
"src/ai/llm/resolver.ts"() {
|
|
1764
|
-
"use strict";
|
|
1765
|
-
init_llm_config();
|
|
1766
|
-
init_catalog();
|
|
1767
|
-
init_session_state();
|
|
1768
|
-
}
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
// src/ai/llm/failover.ts
|
|
1772
|
-
var init_failover = __esm({
|
|
1773
|
-
"src/ai/llm/failover.ts"() {
|
|
1774
|
-
"use strict";
|
|
1775
|
-
init_usage_stats();
|
|
1776
|
-
init_anthropic();
|
|
1777
|
-
init_openai_compat();
|
|
1778
|
-
init_catalog();
|
|
1779
|
-
init_discovery();
|
|
1780
|
-
init_errors2();
|
|
1781
|
-
init_heal();
|
|
1782
|
-
init_models_cache();
|
|
1783
|
-
init_providers();
|
|
1784
|
-
init_types2();
|
|
1785
|
-
init_resolver();
|
|
1786
|
-
}
|
|
1787
|
-
});
|
|
1788
|
-
|
|
1789
|
-
// src/ai/web-search.ts
|
|
1790
|
-
var init_web_search = __esm({
|
|
1791
|
-
"src/ai/web-search.ts"() {
|
|
1792
|
-
"use strict";
|
|
1793
|
-
init_store();
|
|
1794
|
-
}
|
|
1795
|
-
});
|
|
1796
|
-
|
|
1797
|
-
// src/ai/tool-schemas.ts
|
|
1798
|
-
var init_tool_schemas = __esm({
|
|
1799
|
-
"src/ai/tool-schemas.ts"() {
|
|
1800
|
-
"use strict";
|
|
1801
|
-
init_web_search();
|
|
1802
|
-
}
|
|
1803
|
-
});
|
|
1804
|
-
|
|
1805
|
-
// src/ai/privacy.ts
|
|
1806
|
-
import { homedir as homedir4 } from "os";
|
|
1807
|
-
import { join as join11 } from "path";
|
|
1808
|
-
var AUDIT_DIR;
|
|
1809
|
-
var init_privacy = __esm({
|
|
1810
|
-
"src/ai/privacy.ts"() {
|
|
1811
|
-
"use strict";
|
|
1812
|
-
AUDIT_DIR = join11(homedir4(), ".ntrp", "audit");
|
|
1813
|
-
}
|
|
1814
|
-
});
|
|
1815
|
-
|
|
1816
|
-
// src/ai/untrusted.ts
|
|
1817
|
-
var UNTRUSTED_MARKER_NAME, UNTRUSTED_MARKER_END_NAME, MARKER_SPOOF_PATTERN;
|
|
1818
|
-
var init_untrusted = __esm({
|
|
1819
|
-
"src/ai/untrusted.ts"() {
|
|
1820
|
-
"use strict";
|
|
1821
|
-
UNTRUSTED_MARKER_NAME = "EXTERNAL_UNTRUSTED_CONTENT";
|
|
1822
|
-
UNTRUSTED_MARKER_END_NAME = "END_EXTERNAL_UNTRUSTED_CONTENT";
|
|
1823
|
-
MARKER_SPOOF_PATTERN = new RegExp(
|
|
1824
|
-
`<{2,}\\s*/?\\s*(?:${UNTRUSTED_MARKER_NAME}|${UNTRUSTED_MARKER_END_NAME})[^>]*>{2,}`,
|
|
1825
|
-
"gi"
|
|
1826
|
-
);
|
|
1827
|
-
}
|
|
1828
|
-
});
|
|
1829
|
-
|
|
1830
|
-
// src/ai/tool-handlers.ts
|
|
1831
|
-
var init_tool_handlers = __esm({
|
|
1832
|
-
"src/ai/tool-handlers.ts"() {
|
|
1833
|
-
"use strict";
|
|
1834
|
-
init_connection();
|
|
1835
|
-
init_privacy();
|
|
1836
|
-
init_web_search();
|
|
1837
|
-
init_untrusted();
|
|
1838
|
-
}
|
|
1839
|
-
});
|
|
1840
|
-
|
|
1841
|
-
// src/ai/loop-guard.ts
|
|
1842
|
-
var init_loop_guard = __esm({
|
|
1843
|
-
"src/ai/loop-guard.ts"() {
|
|
1844
|
-
"use strict";
|
|
1845
|
-
}
|
|
1846
|
-
});
|
|
1847
|
-
|
|
1848
|
-
// src/ai/thread.ts
|
|
1849
|
-
var PRUNED_TOOL_RESULT;
|
|
1850
|
-
var init_thread = __esm({
|
|
1851
|
-
"src/ai/thread.ts"() {
|
|
1852
|
-
"use strict";
|
|
1853
|
-
PRUNED_TOOL_RESULT = JSON.stringify({
|
|
1854
|
-
pruned: true,
|
|
1855
|
-
note: "Old tool result cleared to free context \u2014 call the tool again if you still need it."
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
});
|
|
1859
|
-
|
|
1860
|
-
// src/data/playbook.ts
|
|
1861
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9, appendFileSync as appendFileSync2 } from "fs";
|
|
1862
|
-
import { join as join12 } from "path";
|
|
1863
|
-
function playsPath() {
|
|
1864
|
-
return join12(getMemoryDir(), PLAYS_FILE);
|
|
1865
|
-
}
|
|
1866
|
-
function getCustomPlays() {
|
|
1867
|
-
const path = playsPath();
|
|
1868
|
-
if (!existsSync9(path)) return [];
|
|
1869
|
-
const out = [];
|
|
1870
|
-
for (const line of readFileSync9(path, "utf-8").split("\n")) {
|
|
1871
|
-
const trimmed = line.trim();
|
|
1872
|
-
if (!trimmed) continue;
|
|
1873
|
-
try {
|
|
1874
|
-
const play = JSON.parse(trimmed);
|
|
1875
|
-
out.push({ ...play, source: "learned" });
|
|
1876
|
-
} catch {
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
return out;
|
|
1880
|
-
}
|
|
1881
|
-
function getAllPlays() {
|
|
1882
|
-
return [...PLAYBOOK, ...getCustomPlays()];
|
|
1883
|
-
}
|
|
1884
|
-
function getPlaybook() {
|
|
1885
|
-
return getAllPlays();
|
|
1886
|
-
}
|
|
1887
|
-
function matchTriggeredPlays(vitals2, layers) {
|
|
1888
|
-
const bySign = new Map(vitals2.map((v) => [v.vital_sign, v]));
|
|
1889
|
-
const out = [];
|
|
1890
|
-
for (const layer of layers) {
|
|
1891
|
-
for (const sign of layer.signs) {
|
|
1892
|
-
const vital = bySign.get(sign);
|
|
1893
|
-
if (!vital) continue;
|
|
1894
|
-
const fires = vital.status === "red" || vital.score < VITAL_TRIGGER_THRESHOLDS[sign];
|
|
1895
|
-
if (!fires) continue;
|
|
1896
|
-
for (const play of getAllPlays()) {
|
|
1897
|
-
if (play.trigger_vital_sign === sign) {
|
|
1898
|
-
out.push({ play, vital, layer: layer.layer });
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
return out;
|
|
1904
|
-
}
|
|
1905
|
-
var PLAYBOOK, PLAYS_FILE, VITAL_TRIGGER_THRESHOLDS;
|
|
1906
|
-
var init_playbook = __esm({
|
|
1907
|
-
"src/data/playbook.ts"() {
|
|
1908
|
-
"use strict";
|
|
1909
|
-
init_store();
|
|
1910
|
-
PLAYBOOK = [
|
|
1911
|
-
{
|
|
1912
|
-
id: "multi-thread-deals",
|
|
1913
|
-
name: "Multi-Thread Your Deals",
|
|
1914
|
-
trigger_vital_sign: "thread_depth",
|
|
1915
|
-
trigger_condition: "thread_depth score < 2 (most deals have \u22641 active contact)",
|
|
1916
|
-
why: "Single-threaded deals die when your one contact goes dark, changes roles, or loses budget authority. Every deal needs at least 2 active contacts to survive \u2014 and late-stage single-threading is a leading indicator of a slipped quarter.",
|
|
1917
|
-
steps: [
|
|
1918
|
-
"Pull single-threaded deals weighted by amount \u2014 one exposed mega-deal outranks ten small ones",
|
|
1919
|
-
"Enrich the buying committee for each: map champion, economic buyer, and technical evaluator (enrichment waterfall or manual research)",
|
|
1920
|
-
"Multi-thread through the existing contact first \u2014 a warm internal referral beats a cold second thread",
|
|
1921
|
-
"Log every new contact with a role against the opportunity so thread depth is measured, not remembered",
|
|
1922
|
-
"Install the mechanism: an alert when any deal past mid-stage has one active contact, and a job-change signal on champions so you hear about departures before the deal goes quiet"
|
|
1923
|
-
],
|
|
1924
|
-
tools_that_help: ["Buying-committee enrichment (waterfall)", "Job-change signal tracking", "CRM contact roles", "Single-thread alerts"],
|
|
1925
|
-
expected_outcome: "Thread depth score rises above threshold; single-threaded deal count drops by 50%+ within 2 weeks; zero late-stage deals with one thread"
|
|
1926
|
-
},
|
|
1927
|
-
{
|
|
1928
|
-
id: "clean-dead-pipeline",
|
|
1929
|
-
name: "Clean Dead Pipeline",
|
|
1930
|
-
trigger_vital_sign: "freshness",
|
|
1931
|
-
trigger_condition: "freshness score < 60",
|
|
1932
|
-
why: "Stale accounts and zombie deals inflate your pipeline number but deliver zero revenue. They corrupt the forecast, and they hide the real coverage math \u2014 you can't fix what the CRM is lying about. Clearing them is also the cheapest pipeline you'll ever source: those records are already paid for.",
|
|
1933
|
-
steps: [
|
|
1934
|
-
"Split the stale pool into saveable vs already-dead: contacted-recently-enough-to-revive vs fiction to clear",
|
|
1935
|
-
"Saveable deals: contact within 48 hours with a specific reason to talk, or move to Closed Lost \u2014 inaction is the worst choice",
|
|
1936
|
-
"Dead-but-paid-for records: route into a signal-triggered reactivation track (funding, hiring, job-change, site-visit triggers) instead of deleting them",
|
|
1937
|
-
"Stale organizations: re-verify ICP fit before re-working; archive what no longer fits so reps stop fishing in dead water",
|
|
1938
|
-
"Install the mechanism: a stale-deal alert at N quiet days (calibrated to this motion's cycle), a weekly 15-minute hygiene scrub, and enrichment refresh on records that go quiet"
|
|
1939
|
-
],
|
|
1940
|
-
tools_that_help: ["CRM bulk update", "Signal-based reactivation triggers", "Enrichment refresh (waterfall)", "Pipeline hygiene cadence"],
|
|
1941
|
-
expected_outcome: "Freshness score jumps 20+ points; forecast reflects reality; reactivation track produces meetings at a fraction of cold-acquisition cost"
|
|
1942
|
-
},
|
|
1943
|
-
{
|
|
1944
|
-
id: "fix-handoff-gap",
|
|
1945
|
-
name: "Fix the Handoff Gap",
|
|
1946
|
-
trigger_vital_sign: "drop_rate",
|
|
1947
|
-
trigger_condition: "drop_rate score indicates >30% of marketing leads not reaching sales",
|
|
1948
|
-
why: "Every lead that marketing generates but sales never sees is wasted budget and lost revenue. The marketing\u2192sales handoff is the #1 leak in most GTM motions \u2014 and it is almost always a systems failure (routing, sync, dead queues), not a people failure.",
|
|
1949
|
-
steps: [
|
|
1950
|
-
"Audit the leak by source: which lead sources exist only in marketing systems and never reach the CRM or a rep queue? The leak usually concentrates in one or two sources",
|
|
1951
|
-
"Trace the routing path end-to-end: assignment rules, territory coverage, inactive-rep queues, and the marketing\u2192CRM sync itself \u2014 find where records fall on the floor",
|
|
1952
|
-
"Fix the pipes: repair routing gaps, reassign orphaned queues, and dedupe/enrich records so routing has the fields it needs to route",
|
|
1953
|
-
"Set the SLA and instrument it: time-to-first-touch on handed-off leads, with a report someone owns",
|
|
1954
|
-
"Install the mechanism: an automated weekly marketing-only-leads report and an alert when any source's handoff rate degrades \u2014 so the leak can't quietly reopen"
|
|
1955
|
-
],
|
|
1956
|
-
tools_that_help: ["Lead routing audit", "Enrichment waterfall (routing fields)", "SLA dashboard", "Handoff-degradation alerts"],
|
|
1957
|
-
expected_outcome: "Drop rate improves 15+ points; marketing-only lead count drops by 60%+; time-to-first-touch inside SLA"
|
|
1958
|
-
},
|
|
1959
|
-
{
|
|
1960
|
-
id: "retarget-effort",
|
|
1961
|
-
name: "Retarget Misdirected Effort",
|
|
1962
|
-
trigger_vital_sign: "signal_to_noise",
|
|
1963
|
-
trigger_condition: "signal_to_noise score < 50% (majority of activities not linked to pipeline)",
|
|
1964
|
-
why: "When reps spend more than half their time on activities unconnected to open pipeline, they're burning hours that could be closing deals. Persistent noise is a targeting-system problem \u2014 reps fish in the pond they can see because the account lists are stale \u2014 not a coaching problem.",
|
|
1965
|
-
steps: [
|
|
1966
|
-
"Cut noisy activity by rep and account status: dead accounts, closed deals, unlinked admin \u2014 name what dominates",
|
|
1967
|
-
"Fix the pond, not the fishing: rebuild rep focus lists from ICP fit and live signals (intent, hiring, funding, usage) instead of memory",
|
|
1968
|
-
"Route signals to reps in the channel they already work in, so the next action is the scored account, not the familiar one",
|
|
1969
|
-
"Set the ratio target and instrument it: 80% of weekly activities touch open pipeline or scored accounts, on a per-rep report",
|
|
1970
|
-
"Automate or delete the noise-generating busywork (logging, list building, manual research) so the time actually moves to pipeline"
|
|
1971
|
-
],
|
|
1972
|
-
tools_that_help: ["Activity reports by rep", "ICP/propensity scoring", "Signal routing to rep channels", "Enrichment automation"],
|
|
1973
|
-
expected_outcome: "Signal-to-noise ratio improves to 70%+; rep hours shift measurably from dead accounts to scored pipeline"
|
|
1974
|
-
},
|
|
1975
|
-
{
|
|
1976
|
-
id: "unstick-pipeline",
|
|
1977
|
-
name: "Unstick the Pipeline",
|
|
1978
|
-
trigger_vital_sign: "flow_rate",
|
|
1979
|
-
trigger_condition: "flow_rate score < 50 (high average deal age or many stuck deals)",
|
|
1980
|
-
why: "Stuck deals block revenue and demoralize reps. A deal that hasn't moved in 14+ days (calibrate to this motion's cycle) is either dead or needs intervention \u2014 and stuck deals with past-due close dates are a forecast-credibility problem before they're a revenue problem.",
|
|
1981
|
-
steps: [
|
|
1982
|
-
"Pull stuck deals sorted by amount, and find the stage where they cluster \u2014 there is usually one stage where deals go to die",
|
|
1983
|
-
"For each stuck deal: name the blocker (no next step, waiting on prospect, internal approval, missing stakeholder) \u2014 'stuck' is a symptom, the blocker is the work",
|
|
1984
|
-
"Create a specific next action with a deadline for each; deals with no plausible next action get triaged to Closed Lost so the forecast tells the truth",
|
|
1985
|
-
"Fix the stage, not just the deals: add exit criteria and a required-next-step field to the stage where deals cluster",
|
|
1986
|
-
"Install the mechanism: an aging alert at the motion-calibrated threshold and automatic manager escalation past 2x median stage duration"
|
|
1987
|
-
],
|
|
1988
|
-
tools_that_help: ["Deal inspection reports", "Stage exit criteria", "Aging alerts", "Manager escalation workflow"],
|
|
1989
|
-
expected_outcome: "Flow rate score improves 15+ points; stuck deal count drops by 40%+ within 2 weeks; the die-stage conversion measurably improves"
|
|
1990
|
-
},
|
|
1991
|
-
{
|
|
1992
|
-
id: "reduce-logo-churn",
|
|
1993
|
-
name: "Reduce Logo Churn",
|
|
1994
|
-
trigger_metric: "grr",
|
|
1995
|
-
trigger_lens: "revenue_metrics",
|
|
1996
|
-
trigger_condition: "GRR below motion benchmark or churned ARR elevated",
|
|
1997
|
-
why: "Revenue leaking from existing customers is the most expensive problem \u2014 you already paid to acquire them.",
|
|
1998
|
-
steps: [
|
|
1999
|
-
"Identify churned and at-risk accounts from retention metrics",
|
|
2000
|
-
"Segment churn by deal size, tenure, and product usage patterns",
|
|
2001
|
-
"Launch save plays for accounts showing contraction signals",
|
|
2002
|
-
"Audit renewal process: timing, stakeholders, and success criteria",
|
|
2003
|
-
"Implement early-warning triggers 90 days before renewal"
|
|
2004
|
-
],
|
|
2005
|
-
tools_that_help: ["CS platform", "Renewal calendar", "NPS/CSAT surveys"],
|
|
2006
|
-
expected_outcome: "GRR improves toward motion benchmark within 2 quarters"
|
|
2007
|
-
},
|
|
2008
|
-
{
|
|
2009
|
-
id: "accelerate-expansion",
|
|
2010
|
-
name: "Accelerate Expansion",
|
|
2011
|
-
trigger_metric: "nrr",
|
|
2012
|
-
trigger_lens: "revenue_metrics",
|
|
2013
|
-
trigger_condition: "NRR below 100% with low expansion ARR",
|
|
2014
|
-
why: "Growing from installed base is cheaper than new logo acquisition \u2014 low expansion means untapped wallet share.",
|
|
2015
|
-
steps: [
|
|
2016
|
-
"List accounts with single-product adoption and upsell potential",
|
|
2017
|
-
"Map expansion triggers (seat growth, new use cases, tier upgrades)",
|
|
2018
|
-
"Assign expansion targets to CS and AE teams by account tier",
|
|
2019
|
-
"Create packaged upsell offers with clear ROI narratives",
|
|
2020
|
-
"Track expansion pipeline separately from new business"
|
|
2021
|
-
],
|
|
2022
|
-
tools_that_help: ["Account plans", "Usage analytics", "Expansion playbooks"],
|
|
2023
|
-
expected_outcome: "Expansion ARR grows 20%+ quarter over quarter"
|
|
2024
|
-
},
|
|
2025
|
-
{
|
|
2026
|
-
id: "fix-renewal-process",
|
|
2027
|
-
name: "Fix the Renewal Process",
|
|
2028
|
-
trigger_metric: "contraction_arr",
|
|
2029
|
-
trigger_lens: "revenue_metrics",
|
|
2030
|
-
trigger_condition: "Contraction ARR > 0",
|
|
2031
|
-
why: "Downgrades are usually a process failure \u2014 late engagement, wrong stakeholders, or missing value proof.",
|
|
2032
|
-
steps: [
|
|
2033
|
-
"Pull all contraction events and categorize root cause",
|
|
2034
|
-
"Standardize renewal timeline: 120/90/60/30-day checkpoints",
|
|
2035
|
-
"Ensure economic buyer is engaged before renewal date",
|
|
2036
|
-
"Build ROI recap deck template for every renewal",
|
|
2037
|
-
"Escalate contractions >20% to leadership review"
|
|
2038
|
-
],
|
|
2039
|
-
tools_that_help: ["Renewal workflow", "QBR templates", "Value realization reports"],
|
|
2040
|
-
expected_outcome: "Contraction ARR drops 50%+ within 2 quarters"
|
|
2041
|
-
},
|
|
2042
|
-
{
|
|
2043
|
-
id: "rebalance-pipeline-mix",
|
|
2044
|
-
name: "Rebalance Pipeline Mix",
|
|
2045
|
-
trigger_metric: "pipeline_coverage",
|
|
2046
|
-
trigger_lens: "revenue_metrics",
|
|
2047
|
-
trigger_condition: "Pipeline coverage red while win rate is healthy",
|
|
2048
|
-
why: "Strong win rate with weak coverage means qualification works but top-of-funnel is starving the machine.",
|
|
2049
|
-
steps: [
|
|
2050
|
-
"Compare pipeline created vs closed-won by source and segment",
|
|
2051
|
-
"Identify segments with coverage below benchmark",
|
|
2052
|
-
"Shift marketing and SDR effort toward under-covered segments",
|
|
2053
|
-
"Set weekly pipeline-created targets by rep",
|
|
2054
|
-
"Review discounting and stage inflation masking thin pipeline"
|
|
2055
|
-
],
|
|
2056
|
-
tools_that_help: ["Pipeline analytics", "Marketing attribution", "Capacity planning"],
|
|
2057
|
-
expected_outcome: "Pipeline coverage reaches motion benchmark within 90 days"
|
|
2058
|
-
},
|
|
2059
|
-
{
|
|
2060
|
-
id: "compress-sales-cycle",
|
|
2061
|
-
name: "Compress the Sales Cycle",
|
|
2062
|
-
trigger_metric: "avg_sales_cycle",
|
|
2063
|
-
trigger_lens: "revenue_metrics",
|
|
2064
|
-
trigger_condition: "Avg sales cycle exceeds profile sales_cycle_days by 50%+",
|
|
2065
|
-
why: "Deals aging past your motion's norm tie up capacity and push revenue into future quarters.",
|
|
2066
|
-
steps: [
|
|
2067
|
-
"Analyze cycle time by stage \u2014 find where deals stall longest",
|
|
2068
|
-
"Implement stage-exit criteria with required next steps",
|
|
2069
|
-
"Introduce mutual action plans for deals past midpoint",
|
|
2070
|
-
"Escalate deals exceeding 2x median cycle to manager review",
|
|
2071
|
-
"Remove low-probability aged deals to free rep capacity"
|
|
2072
|
-
],
|
|
2073
|
-
tools_that_help: ["Stage duration reports", "MAP templates", "Deal coaching"],
|
|
2074
|
-
expected_outcome: "Median cycle time drops 20%+ within one quarter"
|
|
2075
|
-
},
|
|
2076
|
-
{
|
|
2077
|
-
id: "improve-magic-number",
|
|
2078
|
-
name: "Improve Magic Number",
|
|
2079
|
-
trigger_metric: "magic_number",
|
|
2080
|
-
trigger_lens: "revenue_metrics",
|
|
2081
|
-
trigger_condition: "Magic number below motion benchmark (when spend data available)",
|
|
2082
|
-
why: "Low S&M efficiency means you're buying growth too expensively \u2014 burn rate outpaces sustainable unit economics.",
|
|
2083
|
-
steps: [
|
|
2084
|
-
"Calculate magic number by channel and segment",
|
|
2085
|
-
"Cut spend on channels with magic number below 0.5",
|
|
2086
|
-
"Double down on highest-efficiency acquisition motions",
|
|
2087
|
-
"Align CAC targets to motion-specific payback thresholds",
|
|
2088
|
-
"Review rep ramp time and quota attainment curves"
|
|
2089
|
-
],
|
|
2090
|
-
tools_that_help: ["Finance model", "Channel ROI dashboard", "CAC by source"],
|
|
2091
|
-
expected_outcome: "Magic number improves toward benchmark within 2 quarters"
|
|
2092
|
-
}
|
|
2093
|
-
];
|
|
2094
|
-
PLAYS_FILE = "plays.jsonl";
|
|
2095
|
-
VITAL_TRIGGER_THRESHOLDS = {
|
|
2096
|
-
freshness: 60,
|
|
2097
|
-
flow_rate: 50,
|
|
2098
|
-
drop_rate: 70,
|
|
2099
|
-
signal_to_noise: 50,
|
|
2100
|
-
thread_depth: 60
|
|
2101
|
-
};
|
|
2102
|
-
}
|
|
2103
|
-
});
|
|
2104
|
-
|
|
2105
|
-
// src/memory/play-outcomes.ts
|
|
2106
|
-
import { existsSync as existsSync10, readFileSync as readFileSync10, appendFileSync as appendFileSync3 } from "fs";
|
|
2107
|
-
import { join as join13 } from "path";
|
|
2108
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
2109
|
-
var init_play_outcomes = __esm({
|
|
2110
|
-
"src/memory/play-outcomes.ts"() {
|
|
2111
|
-
"use strict";
|
|
2112
|
-
init_store();
|
|
2113
|
-
}
|
|
2114
|
-
});
|
|
2115
|
-
|
|
2116
|
-
// src/workflows/registry.ts
|
|
2117
|
-
var init_registry = __esm({
|
|
2118
|
-
"src/workflows/registry.ts"() {
|
|
2119
|
-
"use strict";
|
|
2120
|
-
}
|
|
2121
|
-
});
|
|
2122
|
-
|
|
2123
|
-
// src/ai/prompt-parts.ts
|
|
2124
|
-
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
2125
|
-
import { join as join14 } from "path";
|
|
2126
|
-
var init_prompt_parts = __esm({
|
|
2127
|
-
"src/ai/prompt-parts.ts"() {
|
|
2128
|
-
"use strict";
|
|
2129
|
-
init_profile();
|
|
2130
|
-
init_store();
|
|
2131
|
-
init_playbook();
|
|
2132
|
-
init_play_outcomes();
|
|
2133
|
-
init_registry();
|
|
2134
|
-
}
|
|
2135
|
-
});
|
|
2136
|
-
|
|
2137
|
-
// src/ai/strategist-prompt.ts
|
|
2138
|
-
var init_strategist_prompt = __esm({
|
|
2139
|
-
"src/ai/strategist-prompt.ts"() {
|
|
2140
|
-
"use strict";
|
|
2141
|
-
init_prompt_parts();
|
|
2142
|
-
}
|
|
2143
|
-
});
|
|
2144
|
-
|
|
2145
|
-
// src/ai/json-response.ts
|
|
2146
|
-
function stripJsonFences(text) {
|
|
2147
|
-
const trimmed = text.trim();
|
|
2148
|
-
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
2149
|
-
if (fenced) return fenced[1].trim();
|
|
2150
|
-
return trimmed.replace(/```(?:json)?\s*/gi, "").replace(/```/g, "").trim();
|
|
2151
|
-
}
|
|
2152
|
-
var init_json_response = __esm({
|
|
2153
|
-
"src/ai/json-response.ts"() {
|
|
2154
|
-
"use strict";
|
|
2155
|
-
}
|
|
2156
|
-
});
|
|
2157
|
-
|
|
2158
|
-
// src/ai/strategist-validate.ts
|
|
2159
|
-
function parseJsonObjectFromText(text) {
|
|
2160
|
-
const cleaned = stripJsonFences(text);
|
|
2161
|
-
const start = cleaned.indexOf("{");
|
|
2162
|
-
if (start === -1) return null;
|
|
2163
|
-
let depth = 0;
|
|
2164
|
-
let inString = false;
|
|
2165
|
-
let escaped = false;
|
|
2166
|
-
let end = -1;
|
|
2167
|
-
for (let i = start; i < cleaned.length; i++) {
|
|
2168
|
-
const ch = cleaned[i];
|
|
2169
|
-
if (escaped) {
|
|
2170
|
-
escaped = false;
|
|
2171
|
-
continue;
|
|
2172
|
-
}
|
|
2173
|
-
if (ch === "\\") {
|
|
2174
|
-
if (inString) escaped = true;
|
|
2175
|
-
continue;
|
|
2176
|
-
}
|
|
2177
|
-
if (ch === '"') {
|
|
2178
|
-
inString = !inString;
|
|
2179
|
-
continue;
|
|
2180
|
-
}
|
|
2181
|
-
if (inString) continue;
|
|
2182
|
-
if (ch === "{") depth++;
|
|
2183
|
-
else if (ch === "}") {
|
|
2184
|
-
depth--;
|
|
2185
|
-
if (depth === 0) {
|
|
2186
|
-
end = i;
|
|
2187
|
-
break;
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
if (end === -1) return null;
|
|
2192
|
-
try {
|
|
2193
|
-
const parsed = JSON.parse(cleaned.slice(start, end + 1));
|
|
2194
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2195
|
-
} catch {
|
|
2196
|
-
return null;
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
function extractNumbers(text) {
|
|
2200
|
-
const out = [];
|
|
2201
|
-
for (const match of text.matchAll(NUMBER_RE)) {
|
|
2202
|
-
const base = Number(match[1].replace(/,/g, ""));
|
|
2203
|
-
if (!Number.isFinite(base)) continue;
|
|
2204
|
-
const suffix = match[2]?.toLowerCase();
|
|
2205
|
-
out.push(suffix ? base * (SUFFIX_MULTIPLIER[suffix] ?? 1) : base);
|
|
2206
|
-
}
|
|
2207
|
-
return out;
|
|
2208
|
-
}
|
|
2209
|
-
function numbersMatch(a, b) {
|
|
2210
|
-
if (a === b) return true;
|
|
2211
|
-
if (a === 0 || b === 0) return Math.abs(a - b) < 0.5;
|
|
2212
|
-
return Math.abs(a - b) / Math.max(Math.abs(a), Math.abs(b)) <= 0.02;
|
|
2213
|
-
}
|
|
2214
|
-
function baselineAppearsInEvidence(value, evidenceNumbers) {
|
|
2215
|
-
const valueNumbers = extractNumbers(value);
|
|
2216
|
-
if (valueNumbers.length === 0) return false;
|
|
2217
|
-
return valueNumbers.some((v) => evidenceNumbers.some((e) => numbersMatch(v, e)));
|
|
2218
|
-
}
|
|
2219
|
-
function isKnownInstrument(measuredBy) {
|
|
2220
|
-
const lower = measuredBy.toLowerCase();
|
|
2221
|
-
return INSTRUMENT_TOKENS.some((token) => lower.includes(token));
|
|
2222
|
-
}
|
|
2223
|
-
function parseIsoDate(value) {
|
|
2224
|
-
if (typeof value !== "string") return null;
|
|
2225
|
-
const match = value.trim().match(ISO_DATE_RE);
|
|
2226
|
-
if (!match) return null;
|
|
2227
|
-
const date = /* @__PURE__ */ new Date(`${match[1]}-${match[2]}-${match[3]}T00:00:00Z`);
|
|
2228
|
-
return Number.isNaN(date.getTime()) ? null : date;
|
|
2229
|
-
}
|
|
2230
|
-
function toIso(date) {
|
|
2231
|
-
return date.toISOString().slice(0, 10);
|
|
2232
|
-
}
|
|
2233
|
-
function addDays(date, days) {
|
|
2234
|
-
const out = new Date(date);
|
|
2235
|
-
out.setUTCDate(out.getUTCDate() + days);
|
|
2236
|
-
return out;
|
|
2237
|
-
}
|
|
2238
|
-
function normalizeDate(value, today2, fallbackDays) {
|
|
2239
|
-
const parsed = parseIsoDate(value);
|
|
2240
|
-
if (parsed && parsed.getTime() >= today2.getTime()) {
|
|
2241
|
-
return { iso: toIso(parsed), repaired: false };
|
|
2242
|
-
}
|
|
2243
|
-
return { iso: toIso(addDays(today2, fallbackDays)), repaired: true };
|
|
2244
|
-
}
|
|
2245
|
-
function str(value) {
|
|
2246
|
-
return typeof value === "string" ? value.trim() : "";
|
|
2247
|
-
}
|
|
2248
|
-
function strArray(value) {
|
|
2249
|
-
if (!Array.isArray(value)) return [];
|
|
2250
|
-
return value.map(str).filter(Boolean);
|
|
2251
|
-
}
|
|
2252
|
-
function num(value, fallback2) {
|
|
2253
|
-
const n = typeof value === "number" ? value : Number(value);
|
|
2254
|
-
return Number.isFinite(n) ? n : fallback2;
|
|
2255
|
-
}
|
|
2256
|
-
function enumValue(value, allowed, fallback2) {
|
|
2257
|
-
return typeof value === "string" && allowed.includes(value) ? value : fallback2;
|
|
2258
|
-
}
|
|
2259
|
-
function hasNumber(text) {
|
|
2260
|
-
return extractNumbers(text).length > 0;
|
|
2261
|
-
}
|
|
2262
|
-
function validateOutcome(raw, label, evidenceNumbers, today2, fallbackDays) {
|
|
2263
|
-
if (!raw || typeof raw !== "object") {
|
|
2264
|
-
return { outcome: null, measurable: false, notes: [`${label}: missing \u2014 excluded from targets`] };
|
|
2265
|
-
}
|
|
2266
|
-
const record = raw;
|
|
2267
|
-
const metric = str(record.metric);
|
|
2268
|
-
const baseline = str(record.baseline);
|
|
2269
|
-
const targetRange = str(record.target_range);
|
|
2270
|
-
const measuredBy = str(record.measured_by);
|
|
2271
|
-
if (!metric) {
|
|
2272
|
-
return { outcome: null, measurable: false, notes: [`${label}: no metric named \u2014 excluded from targets`] };
|
|
2273
|
-
}
|
|
2274
|
-
const notes = [];
|
|
2275
|
-
let measurable = true;
|
|
2276
|
-
if (!hasNumber(baseline)) {
|
|
2277
|
-
measurable = false;
|
|
2278
|
-
notes.push(`${label} "${metric}": baseline is not a number \u2014 demoted to assumption`);
|
|
2279
|
-
} else if (!baselineAppearsInEvidence(baseline, evidenceNumbers)) {
|
|
2280
|
-
measurable = false;
|
|
2281
|
-
notes.push(`${label} "${metric}": baseline ${baseline} not found in grounded evidence \u2014 demoted to assumption`);
|
|
2282
|
-
}
|
|
2283
|
-
if (!hasNumber(targetRange)) {
|
|
2284
|
-
measurable = false;
|
|
2285
|
-
notes.push(`${label} "${metric}": target range has no number \u2014 demoted to assumption`);
|
|
2286
|
-
}
|
|
2287
|
-
if (!measuredBy || !isKnownInstrument(measuredBy)) {
|
|
2288
|
-
measurable = false;
|
|
2289
|
-
notes.push(`${label} "${metric}": measured_by "${measuredBy || "(empty)"}" is not a known instrument \u2014 demoted to assumption`);
|
|
2290
|
-
}
|
|
2291
|
-
const checkDate = normalizeDate(record.check_date, today2, fallbackDays);
|
|
2292
|
-
if (checkDate.repaired) {
|
|
2293
|
-
notes.push(`${label} "${metric}": check date repaired to ${checkDate.iso}`);
|
|
2294
|
-
}
|
|
2295
|
-
return {
|
|
2296
|
-
outcome: {
|
|
2297
|
-
metric,
|
|
2298
|
-
baseline: baseline || "unknown",
|
|
2299
|
-
target_range: targetRange || "unquantified",
|
|
2300
|
-
check_date: checkDate.iso,
|
|
2301
|
-
measured_by: measuredBy || "unspecified"
|
|
2302
|
-
},
|
|
2303
|
-
measurable,
|
|
2304
|
-
notes
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
|
-
function validateMilestones(raw, today2, issues, workstreamTitle) {
|
|
2308
|
-
const out = [];
|
|
2309
|
-
if (Array.isArray(raw)) {
|
|
2310
|
-
for (const item of raw) {
|
|
2311
|
-
if (!item || typeof item !== "object") continue;
|
|
2312
|
-
const record = item;
|
|
2313
|
-
const label = str(record.label);
|
|
2314
|
-
const verification = str(record.verification);
|
|
2315
|
-
if (!label) continue;
|
|
2316
|
-
const due = normalizeDate(record.due, today2, 14);
|
|
2317
|
-
if (due.repaired) {
|
|
2318
|
-
issues.push(`Milestone "${label}" (${workstreamTitle}): due date repaired to ${due.iso}`);
|
|
2319
|
-
}
|
|
2320
|
-
if (!verification) {
|
|
2321
|
-
issues.push(`Milestone "${label}" (${workstreamTitle}): no verification method \u2014 flagged`);
|
|
2322
|
-
}
|
|
2323
|
-
out.push({ label, due: due.iso, verification: verification || "Verification method not specified \u2014 define before activating" });
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
return out;
|
|
2327
|
-
}
|
|
2328
|
-
function validateDeliverables(raw, today2) {
|
|
2329
|
-
const out = [];
|
|
2330
|
-
if (Array.isArray(raw)) {
|
|
2331
|
-
for (const item of raw) {
|
|
2332
|
-
if (!item || typeof item !== "object") continue;
|
|
2333
|
-
const record = item;
|
|
2334
|
-
const label = str(record.label);
|
|
2335
|
-
if (!label) continue;
|
|
2336
|
-
out.push({
|
|
2337
|
-
label,
|
|
2338
|
-
kind: enumValue(record.kind, ["artifact", "process_change", "decision"], "artifact"),
|
|
2339
|
-
due: normalizeDate(record.due, today2, 21).iso
|
|
2340
|
-
});
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
return out;
|
|
2344
|
-
}
|
|
2345
|
-
function validateContingency(raw, today2) {
|
|
2346
|
-
if (!raw || typeof raw !== "object") return null;
|
|
2347
|
-
const record = raw;
|
|
2348
|
-
const trigger = str(record.trigger);
|
|
2349
|
-
const fallback2 = str(record.fallback);
|
|
2350
|
-
if (!trigger || !fallback2) return null;
|
|
2351
|
-
return {
|
|
2352
|
-
trigger,
|
|
2353
|
-
trigger_check_date: normalizeDate(record.trigger_check_date, today2, 21).iso,
|
|
2354
|
-
fallback: fallback2
|
|
2355
|
-
};
|
|
2356
|
-
}
|
|
2357
|
-
function validPlayIds(value) {
|
|
2358
|
-
const allowed = new Set(getPlaybook().map((play) => play.id));
|
|
2359
|
-
return strArray(value).filter((id) => allowed.has(id));
|
|
2360
|
-
}
|
|
2361
|
-
function validateStrategistPlan(raw, opts) {
|
|
2362
|
-
const today2 = parseIsoDate(opts.todayIso) ?? /* @__PURE__ */ new Date();
|
|
2363
|
-
const evidenceNumbers = extractNumbers(opts.evidenceText);
|
|
2364
|
-
const issues = [];
|
|
2365
|
-
const demotedAssumptions = [];
|
|
2366
|
-
let measurableTargets = 0;
|
|
2367
|
-
let totalTargets = 0;
|
|
2368
|
-
const rawWorkstreams = Array.isArray(raw.workstreams) ? raw.workstreams : [];
|
|
2369
|
-
const workstreams = [];
|
|
2370
|
-
for (const [index, item] of rawWorkstreams.entries()) {
|
|
2371
|
-
if (!item || typeof item !== "object") continue;
|
|
2372
|
-
const record = item;
|
|
2373
|
-
const title = str(record.title) || `Workstream ${index + 1}`;
|
|
2374
|
-
const outcomeCheck = validateOutcome(record.expected_outcome, "Expected outcome", evidenceNumbers, today2, 30);
|
|
2375
|
-
totalTargets += outcomeCheck.outcome ? 1 : 0;
|
|
2376
|
-
if (outcomeCheck.measurable) measurableTargets++;
|
|
2377
|
-
for (const note of outcomeCheck.notes) issues.push(`${title}: ${note}`);
|
|
2378
|
-
if (outcomeCheck.outcome && !outcomeCheck.measurable) {
|
|
2379
|
-
demotedAssumptions.push(
|
|
2380
|
-
`Unverified expectation (${title}): ${outcomeCheck.outcome.metric} ${outcomeCheck.outcome.baseline} -> ${outcomeCheck.outcome.target_range} \u2014 quantify against grounded data before treating as a target`
|
|
2381
|
-
);
|
|
2382
|
-
}
|
|
2383
|
-
const leadingIndicators = [];
|
|
2384
|
-
if (Array.isArray(record.leading_indicators)) {
|
|
2385
|
-
for (const li of record.leading_indicators) {
|
|
2386
|
-
const check = validateOutcome(li, "Leading indicator", evidenceNumbers, today2, 14);
|
|
2387
|
-
if (!check.outcome) continue;
|
|
2388
|
-
totalTargets++;
|
|
2389
|
-
if (check.measurable) {
|
|
2390
|
-
measurableTargets++;
|
|
2391
|
-
leadingIndicators.push(check.outcome);
|
|
2392
|
-
} else {
|
|
2393
|
-
for (const note of check.notes) issues.push(`${title}: ${note}`);
|
|
2394
|
-
demotedAssumptions.push(
|
|
2395
|
-
`Unverified indicator (${title}): ${check.outcome.metric} \u2014 not measurable as stated`
|
|
2396
|
-
);
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
let milestones = validateMilestones(record.milestones, today2, issues, title);
|
|
2401
|
-
if (milestones.length === 0 && outcomeCheck.outcome) {
|
|
2402
|
-
milestones = [{
|
|
2403
|
-
label: `Outcome check: ${outcomeCheck.outcome.metric}`,
|
|
2404
|
-
due: outcomeCheck.outcome.check_date,
|
|
2405
|
-
verification: `${outcomeCheck.outcome.measured_by} shows ${outcomeCheck.outcome.target_range}`
|
|
2406
|
-
}];
|
|
2407
|
-
issues.push(`${title}: no dated milestones \u2014 derived one from the expected outcome`);
|
|
2408
|
-
}
|
|
2409
|
-
if (!outcomeCheck.outcome && milestones.length === 0) {
|
|
2410
|
-
issues.push(`${title}: no measurable outcome and no milestones \u2014 workstream demoted to assumption`);
|
|
2411
|
-
demotedAssumptions.push(`Dropped workstream "${title}" \u2014 had no measurable outcome or dated milestone`);
|
|
2412
|
-
continue;
|
|
2413
|
-
}
|
|
2414
|
-
const contingency = validateContingency(record.contingency, today2);
|
|
2415
|
-
if (!contingency) {
|
|
2416
|
-
issues.push(`${title}: contingency missing or incomplete \u2014 flagged for definition at first review`);
|
|
2417
|
-
}
|
|
2418
|
-
workstreams.push({
|
|
2419
|
-
order: num(record.order, workstreams.length + 1),
|
|
2420
|
-
title,
|
|
2421
|
-
problem: str(record.problem) || "Problem statement not captured",
|
|
2422
|
-
rationale: str(record.rationale) || "Sequence rationale not captured",
|
|
2423
|
-
play_ids: validPlayIds(record.play_ids),
|
|
2424
|
-
actions: strArray(record.actions),
|
|
2425
|
-
effort_hours: Math.max(0, num(record.effort_hours, 8)),
|
|
2426
|
-
milestones,
|
|
2427
|
-
deliverables: validateDeliverables(record.deliverables, today2),
|
|
2428
|
-
expected_outcome: outcomeCheck.outcome ?? {
|
|
2429
|
-
metric: "unspecified",
|
|
2430
|
-
baseline: "unknown",
|
|
2431
|
-
target_range: "unquantified",
|
|
2432
|
-
check_date: toIso(addDays(today2, 30)),
|
|
2433
|
-
measured_by: "unspecified"
|
|
2434
|
-
},
|
|
2435
|
-
leading_indicators: leadingIndicators,
|
|
2436
|
-
contingency: contingency ?? {
|
|
2437
|
-
trigger: "Define trigger at first review",
|
|
2438
|
-
trigger_check_date: toIso(addDays(today2, 21)),
|
|
2439
|
-
fallback: "Define fallback at first review"
|
|
2440
|
-
}
|
|
2441
|
-
});
|
|
2442
|
-
}
|
|
2443
|
-
if (workstreams.length === 0) return null;
|
|
2444
|
-
workstreams.sort((a, b) => a.order - b.order);
|
|
2445
|
-
workstreams.forEach((ws, i) => {
|
|
2446
|
-
ws.order = i + 1;
|
|
2447
|
-
});
|
|
2448
|
-
const capped = workstreams.slice(0, 5);
|
|
2449
|
-
if (workstreams.length > 5) {
|
|
2450
|
-
issues.push(`Plan proposed ${workstreams.length} workstreams \u2014 capped to 5 (focus beats coverage)`);
|
|
2451
|
-
}
|
|
2452
|
-
const rawConfidence = Math.min(1, Math.max(0, num(raw.confidence, 0.6)));
|
|
2453
|
-
const confidence = Math.max(0.2, rawConfidence - demotedAssumptions.length * 0.05);
|
|
2454
|
-
const plan = {
|
|
2455
|
-
title: str(raw.title) || "Untitled Strategy",
|
|
2456
|
-
objective: str(raw.objective) || "Objective not restated",
|
|
2457
|
-
summary_30k: str(raw.summary_30k) || str(raw.hypothesis) || "Executive summary not captured",
|
|
2458
|
-
hypothesis: str(raw.hypothesis) || "If the workstreams execute in order, the objective metrics should move within their target ranges.",
|
|
2459
|
-
target_segment: str(raw.target_segment) || "Whole pipeline",
|
|
2460
|
-
priority: enumValue(raw.priority, ["low", "medium", "high"], "medium"),
|
|
2461
|
-
review_cadence: str(raw.review_cadence) || "Weekly",
|
|
2462
|
-
confidence,
|
|
2463
|
-
constraints: strArray(raw.constraints),
|
|
2464
|
-
assumptions: [...strArray(raw.assumptions), ...demotedAssumptions],
|
|
2465
|
-
risks: strArray(raw.risks),
|
|
2466
|
-
workstreams: capped
|
|
2467
|
-
};
|
|
2468
|
-
return { plan, issues, measurableTargets, totalTargets };
|
|
2469
|
-
}
|
|
2470
|
-
function buildGroundedFallbackPlan(input) {
|
|
2471
|
-
const today2 = parseIsoDate(input.todayIso) ?? /* @__PURE__ */ new Date();
|
|
2472
|
-
const triggered2 = matchTriggeredPlays(input.vitals, LAYERS);
|
|
2473
|
-
const issues = [
|
|
2474
|
-
"LLM plan JSON invalid \u2014 using grounded fallback from triggered plays and live vitals"
|
|
2475
|
-
];
|
|
2476
|
-
const sources = triggered2.length > 0 ? triggered2.slice(0, 3) : input.vitals.slice().sort((a, b) => a.score - b.score).slice(0, 2).map((vital) => {
|
|
2477
|
-
const play = getPlaybook().find((p) => p.trigger_vital_sign === vital.vital_sign) ?? getPlaybook()[0];
|
|
2478
|
-
return { play, vital, layer: 1 };
|
|
2479
|
-
});
|
|
2480
|
-
const workstreams = sources.map(({ play, vital }, index) => {
|
|
2481
|
-
const score = Math.round(vital.score);
|
|
2482
|
-
const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value).toLocaleString("en-US")}` : null;
|
|
2483
|
-
const baseline = dollar ?? String(score);
|
|
2484
|
-
const targetLow = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value * 0.4).toLocaleString("en-US")}` : String(Math.min(100, score + 20));
|
|
2485
|
-
const targetHigh = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value * 0.6).toLocaleString("en-US")}` : String(Math.min(100, score + 35));
|
|
2486
|
-
const checkDate = toIso(addDays(today2, 21 + index * 7));
|
|
2487
|
-
const outcome = {
|
|
2488
|
-
metric: vital.vital_sign,
|
|
2489
|
-
baseline,
|
|
2490
|
-
target_range: `${baseline} -> ${targetLow}-${targetHigh}`,
|
|
2491
|
-
check_date: checkDate,
|
|
2492
|
-
measured_by: `${vital.vital_sign} vital sign`
|
|
2493
|
-
};
|
|
2494
|
-
return {
|
|
2495
|
-
order: index + 1,
|
|
2496
|
-
title: play.name,
|
|
2497
|
-
problem: `${vital.vital_sign} score ${score} (${vital.status})${dollar ? ` \u2014 ${dollar} ${vital.dollar_label ?? ""}`.trimEnd() : ""}`,
|
|
2498
|
-
rationale: index === 0 ? "Layer-order first: clean or unblock the gating vital before downstream work" : "Next in dependency order after the prior workstream",
|
|
2499
|
-
play_ids: [play.id],
|
|
2500
|
-
actions: play.steps.slice(0, 3),
|
|
2501
|
-
effort_hours: 8 + index * 4,
|
|
2502
|
-
milestones: [
|
|
2503
|
-
{
|
|
2504
|
-
label: `Check ${vital.vital_sign} movement`,
|
|
2505
|
-
due: checkDate,
|
|
2506
|
-
verification: `${vital.vital_sign} score moves toward ${targetLow}-${targetHigh} (baseline ${baseline})`
|
|
2507
|
-
}
|
|
2508
|
-
],
|
|
2509
|
-
deliverables: [
|
|
2510
|
-
{
|
|
2511
|
-
label: `${play.name} triage list`,
|
|
2512
|
-
kind: "artifact",
|
|
2513
|
-
due: toIso(addDays(today2, 7 + index * 7))
|
|
2514
|
-
}
|
|
2515
|
-
],
|
|
2516
|
-
expected_outcome: outcome,
|
|
2517
|
-
leading_indicators: [],
|
|
2518
|
-
contingency: {
|
|
2519
|
-
trigger: `${vital.vital_sign} flat or worse at first check`,
|
|
2520
|
-
trigger_check_date: checkDate,
|
|
2521
|
-
fallback: "Descope to the single highest-dollar entity cohort and re-run /strategy review"
|
|
2522
|
-
}
|
|
2523
|
-
};
|
|
2524
|
-
});
|
|
2525
|
-
const gating = input.gatingVitalSign ?? sources[0]?.vital.vital_sign ?? "freshness";
|
|
2526
|
-
const varLabel = input.totalValueAtRisk != null && input.totalValueAtRisk > 0 ? `$${Math.round(input.totalValueAtRisk).toLocaleString("en-US")} at risk` : "material pipeline dollars at risk";
|
|
2527
|
-
const plan = {
|
|
2528
|
-
title: "Grounded recovery plan",
|
|
2529
|
-
objective: input.objective,
|
|
2530
|
-
summary_30k: `${gating} is the gating pressure (${varLabel}). This fallback sequences ${workstreams.length} playbook workstream(s) in layer order so data trust and handoffs unlock pipeline movement. Treat baselines as live vital readings; refine with /strategy after the first review.`,
|
|
2531
|
-
hypothesis: "If plays execute in layer order against the live vital scores, the objective metrics should move into the stated ranges within one review cycle.",
|
|
2532
|
-
target_segment: "Whole pipeline",
|
|
2533
|
-
priority: "high",
|
|
2534
|
-
review_cadence: "Weekly",
|
|
2535
|
-
confidence: 0.45,
|
|
2536
|
-
constraints: ["Generated without a validated LLM plan JSON \u2014 confirm capacity before staffing"],
|
|
2537
|
-
assumptions: ["Outcome ranges are heuristic halves/increments of live vitals, not model-authored forecasts"],
|
|
2538
|
-
risks: ["Fallback plans lack stress-test revisions \u2014 run /strategy once the engine emits valid JSON"],
|
|
2539
|
-
workstreams
|
|
2540
|
-
};
|
|
2541
|
-
return {
|
|
2542
|
-
plan,
|
|
2543
|
-
issues,
|
|
2544
|
-
measurableTargets: workstreams.length,
|
|
2545
|
-
totalTargets: workstreams.length
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
var NUMBER_RE, SUFFIX_MULTIPLIER, INSTRUMENT_TOKENS, ISO_DATE_RE;
|
|
2549
|
-
var init_strategist_validate = __esm({
|
|
2550
|
-
"src/ai/strategist-validate.ts"() {
|
|
2551
|
-
"use strict";
|
|
2552
|
-
init_playbook();
|
|
2553
|
-
init_health_score();
|
|
2554
|
-
init_json_response();
|
|
2555
|
-
NUMBER_RE = /\$?\s*(\d[\d,]*\.?\d*)\s*(m|k|b|million|thousand|billion)?\b/gi;
|
|
2556
|
-
SUFFIX_MULTIPLIER = {
|
|
2557
|
-
k: 1e3,
|
|
2558
|
-
thousand: 1e3,
|
|
2559
|
-
m: 1e6,
|
|
2560
|
-
million: 1e6,
|
|
2561
|
-
b: 1e9,
|
|
2562
|
-
billion: 1e9
|
|
2563
|
-
};
|
|
2564
|
-
INSTRUMENT_TOKENS = [
|
|
2565
|
-
// vital signs + components
|
|
2566
|
-
"freshness",
|
|
2567
|
-
"flow_rate",
|
|
2568
|
-
"flow rate",
|
|
2569
|
-
"drop_rate",
|
|
2570
|
-
"drop rate",
|
|
2571
|
-
"signal_to_noise",
|
|
2572
|
-
"signal-to-noise",
|
|
2573
|
-
"signal to noise",
|
|
2574
|
-
"thread_depth",
|
|
2575
|
-
"thread depth",
|
|
2576
|
-
"health score",
|
|
2577
|
-
"vital",
|
|
2578
|
-
"gating",
|
|
2579
|
-
"score",
|
|
2580
|
-
// SaaS metrics lane
|
|
2581
|
-
"arr",
|
|
2582
|
-
"nrr",
|
|
2583
|
-
"grr",
|
|
2584
|
-
"net revenue retention",
|
|
2585
|
-
"gross revenue retention",
|
|
2586
|
-
"win rate",
|
|
2587
|
-
"pipeline coverage",
|
|
2588
|
-
"weighted pipeline",
|
|
2589
|
-
"velocity",
|
|
2590
|
-
"deal size",
|
|
2591
|
-
"sales cycle",
|
|
2592
|
-
"conversion",
|
|
2593
|
-
"ltv",
|
|
2594
|
-
"cac",
|
|
2595
|
-
"payback",
|
|
2596
|
-
"magic number",
|
|
2597
|
-
"churn",
|
|
2598
|
-
"expansion",
|
|
2599
|
-
"retention",
|
|
2600
|
-
// countable pipeline entities + states
|
|
2601
|
-
"count",
|
|
2602
|
-
"stale",
|
|
2603
|
-
"stuck",
|
|
2604
|
-
"single-threaded",
|
|
2605
|
-
"single threaded",
|
|
2606
|
-
"zombie",
|
|
2607
|
-
"dollar",
|
|
2608
|
-
"pipeline at risk",
|
|
2609
|
-
"opportunit",
|
|
2610
|
-
"deal",
|
|
2611
|
-
"activity",
|
|
2612
|
-
"activities",
|
|
2613
|
-
"contact",
|
|
2614
|
-
"lead",
|
|
2615
|
-
"account",
|
|
2616
|
-
"segment",
|
|
2617
|
-
"handoff",
|
|
2618
|
-
"past-due",
|
|
2619
|
-
"past due",
|
|
2620
|
-
// the review instrument itself
|
|
2621
|
-
"strategy review",
|
|
2622
|
-
"metric_readings",
|
|
2623
|
-
"vital_sign_readings",
|
|
2624
|
-
"entity count"
|
|
2625
|
-
];
|
|
2626
|
-
ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
2627
|
-
}
|
|
2628
|
-
});
|
|
2629
|
-
|
|
2630
|
-
// src/ai/strategist.ts
|
|
2631
|
-
function describePlanValidationFailure(text, evidenceText, todayIso) {
|
|
2632
|
-
const raw = parseJsonObjectFromText(text);
|
|
2633
|
-
if (!raw) {
|
|
2634
|
-
if (text.includes("{") && !text.trim().endsWith("}")) {
|
|
2635
|
-
return "JSON object appears truncated (increase brevity: \u22643 workstreams) or incomplete";
|
|
2636
|
-
}
|
|
2637
|
-
return "not parseable as a JSON object (prose or truncated output)";
|
|
2638
|
-
}
|
|
2639
|
-
const result = validateStrategistPlan(raw, { evidenceText, todayIso });
|
|
2640
|
-
if (!result) {
|
|
2641
|
-
return "JSON parsed but no usable workstreams remained after measurability checks (need \u22651 workstream with measurable outcome or dated milestone)";
|
|
2642
|
-
}
|
|
2643
|
-
return "unknown validation failure";
|
|
2644
|
-
}
|
|
2645
|
-
var init_strategist2 = __esm({
|
|
2646
|
-
"src/ai/strategist.ts"() {
|
|
2647
|
-
"use strict";
|
|
2648
|
-
init_types2();
|
|
2649
|
-
init_failover();
|
|
2650
|
-
init_surfaces();
|
|
2651
|
-
init_llm_config();
|
|
2652
|
-
init_repl_api();
|
|
2653
|
-
init_tool_schemas();
|
|
2654
|
-
init_web_search();
|
|
2655
|
-
init_tool_handlers();
|
|
2656
|
-
init_loop_guard();
|
|
2657
|
-
init_thread();
|
|
2658
|
-
init_strategist_prompt();
|
|
2659
|
-
init_strategist_validate();
|
|
2660
|
-
init_strategist_prompt();
|
|
2661
|
-
}
|
|
2662
|
-
});
|
|
2663
|
-
|
|
2664
|
-
// src/ui/layout.ts
|
|
2665
|
-
var init_layout = __esm({
|
|
2666
|
-
"src/ui/layout.ts"() {
|
|
2667
|
-
"use strict";
|
|
2668
|
-
}
|
|
2669
|
-
});
|
|
2670
|
-
|
|
2671
|
-
// src/output/strategy-brief.ts
|
|
2672
|
-
import chalk4 from "chalk";
|
|
2673
|
-
var init_strategy_brief = __esm({
|
|
2674
|
-
"src/output/strategy-brief.ts"() {
|
|
2675
|
-
"use strict";
|
|
2676
|
-
init_theme();
|
|
2677
|
-
init_layout();
|
|
2678
|
-
}
|
|
2679
|
-
});
|
|
2680
|
-
|
|
2681
|
-
// src/output/llm-attribution.ts
|
|
2682
|
-
import chalk5 from "chalk";
|
|
2683
|
-
var init_llm_attribution = __esm({
|
|
2684
|
-
"src/output/llm-attribution.ts"() {
|
|
2685
|
-
"use strict";
|
|
2686
|
-
init_catalog();
|
|
2687
|
-
}
|
|
2688
|
-
});
|
|
2689
|
-
|
|
2690
|
-
// src/whimsy/time-milestones.ts
|
|
2691
|
-
var init_time_milestones = __esm({
|
|
2692
|
-
"src/whimsy/time-milestones.ts"() {
|
|
2693
|
-
"use strict";
|
|
2694
|
-
}
|
|
2695
|
-
});
|
|
2696
|
-
|
|
2697
|
-
// src/whimsy/time-perspectives.ts
|
|
2698
|
-
var TIME_PERSPECTIVES;
|
|
2699
|
-
var init_time_perspectives = __esm({
|
|
2700
|
-
"src/whimsy/time-perspectives.ts"() {
|
|
2701
|
-
"use strict";
|
|
2702
|
-
TIME_PERSPECTIVES = [
|
|
2703
|
-
{ id: "dsotm", category: "music", reference_hours: 0.74, label: "Dark Side of the Moon", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 1, max_ratio: 200 },
|
|
2704
|
-
{ id: "rush_2112", category: "music", reference_hours: 0.33, label: "2112", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 2, max_ratio: 200 },
|
|
2705
|
-
{ id: "bohemian_rhapsody", category: "music", reference_hours: 0.1, label: "Bohemian Rhapsody", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 5, max_ratio: 500 },
|
|
2706
|
-
{ id: "stairway", category: "music", reference_hours: 0.13, label: "Stairway to Heaven", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 5, max_ratio: 400 },
|
|
2707
|
-
{ id: "podcast_binge", category: "music", reference_hours: 0.75, label: "hour-long podcast episodes", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 2, max_ratio: 300 },
|
|
2708
|
-
{ id: "abbey_road", category: "music", reference_hours: 0.8, label: "Abbey Road", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 1, max_ratio: 200 },
|
|
2709
|
-
{ id: "iron_maiden_set", category: "music", reference_hours: 2, label: "an Iron Maiden marathon set", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 50 },
|
|
2710
|
-
{ id: "festival_set", category: "music", reference_hours: 1.5, label: "main-stage festival sets", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 100 },
|
|
2711
|
-
{ id: "jazz_club", category: "music", reference_hours: 3, label: "late-night jazz sets", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 80 },
|
|
2712
|
-
{ id: "ring_cycle", category: "music", reference_hours: 15, label: "Wagner's Ring Cycle", template: "\u2248 {pct}% of {label}", min_ratio: 0.1, max_ratio: 2 },
|
|
2713
|
-
{ id: "shrek", category: "film", reference_hours: 1.5, label: "Shrek (the first one)", template: "\u2248 {ratio}\xD7 watching {label}", min_ratio: 1, max_ratio: 150 },
|
|
2714
|
-
{ id: "blockbuster", category: "film", reference_hours: 2.1, label: "average blockbusters", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 100 },
|
|
2715
|
-
{ id: "dune_two", category: "film", reference_hours: 2.75, label: "Dune: Part Two", template: "\u2248 {ratio}\xD7 in theater for {label}", min_ratio: 1, max_ratio: 100 },
|
|
2716
|
-
{ id: "scorsese", category: "film", reference_hours: 3.5, label: "Goodfellas", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 80 },
|
|
2717
|
-
{ id: "godfather", category: "film", reference_hours: 6.5, label: "the Godfather saga", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 20 },
|
|
2718
|
-
{ id: "lotr_extended", category: "film", reference_hours: 11.4, label: "the LOTR extended trilogy", template: "Longer than all of {label}", min_ratio: 1, max_ratio: 50 },
|
|
2719
|
-
{ id: "cooking_brisket", category: "film", reference_hours: 12, label: "low-and-slow brisket cooks", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 30 },
|
|
2720
|
-
{ id: "the_office", category: "film", reference_hours: 68, label: "The Office (full series)", template: "\u2248 {ratio}\xD7 bingeing {label}", min_ratio: 5, max_ratio: 200 },
|
|
2721
|
-
{ id: "marvel_marathon", category: "film", reference_hours: 50, label: "an MCU Phase One marathon", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 30 },
|
|
2722
|
-
{ id: "around_world", category: "film", reference_hours: 1920, label: "Around the World in 80 Days (fictionally)", template: "\u2248 {pct}% of {label}", min_ratio: 0.3, max_ratio: 1 },
|
|
2723
|
-
{ id: "soccer_match", category: "sports", reference_hours: 1.75, label: "Premier League matches", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 150 },
|
|
2724
|
-
{ id: "marathon", category: "sports", reference_hours: 2, label: "marathons at world-record pace", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 80 },
|
|
2725
|
-
{ id: "baseball_game", category: "sports", reference_hours: 3, label: "nine-inning baseball games", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 80 },
|
|
2726
|
-
{ id: "superbowl", category: "sports", reference_hours: 3.5, label: "Super Bowls", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 80 },
|
|
2727
|
-
{ id: "nfl_game", category: "sports", reference_hours: 3.25, label: "NFL games (with commercials)", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 50 },
|
|
2728
|
-
{ id: "wimbledon", category: "sports", reference_hours: 5, label: "Wimbledon finals", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 30 },
|
|
2729
|
-
{ id: "tour_stage", category: "sports", reference_hours: 4.5, label: "Tour de France stages", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 60 },
|
|
2730
|
-
{ id: "olympics", category: "sports", reference_hours: 250, label: "Summer Olympics broadcast hours", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 2, max_ratio: 10 },
|
|
2731
|
-
{ id: "moon_light", category: "cosmos", reference_hours: 1.3 / 3600, label: "a beam of light Earth \u2192 Moon", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1e3, max_ratio: 1e6 },
|
|
2732
|
-
{ id: "iss_orbit", category: "cosmos", reference_hours: 1.5, label: "ISS orbits", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 2, max_ratio: 200 },
|
|
2733
|
-
{ id: "light_sun", category: "cosmos", reference_hours: 8.3, label: "solar light crossing to Earth", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 200 },
|
|
2734
|
-
{ id: "sleep_cycle", category: "cosmos", reference_hours: 8, label: "full nights of sleep", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 150 },
|
|
2735
|
-
{ id: "red_eye", category: "cosmos", reference_hours: 5.5, label: "transcontinental red-eyes", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 100 },
|
|
2736
|
-
{ id: "mayfly", category: "cosmos", reference_hours: 24, label: "a mayfly's entire adult life", template: "\u2248 {pct}% of {label}", min_ratio: 0.1, max_ratio: 2 },
|
|
2737
|
-
{ id: "earth_rotation", category: "cosmos", reference_hours: 24, label: "Earth rotations", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.1, max_ratio: 50 },
|
|
2738
|
-
{ id: "jupiter_storm", category: "cosmos", reference_hours: 150, label: "Jupiter's Great Red Spot rotation", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 15 },
|
|
2739
|
-
{ id: "lunar_month", category: "cosmos", reference_hours: 708, label: "a lunar cycle", template: "\u2248 {pct}% of {label}", min_ratio: 0.05, max_ratio: 2 },
|
|
2740
|
-
{ id: "mars_transit", category: "cosmos", reference_hours: 5110, label: "a one-way Mars transit (optimistic)", template: "\u2248 {pct}% of {label}", min_ratio: 1e-3, max_ratio: 5 },
|
|
2741
|
-
{ id: "calendar_year", category: "cosmos", reference_hours: 8760, label: "all the hours in a calendar year", template: "\u2248 {pct}% of {label}", min_ratio: 0.05, max_ratio: 0.2 },
|
|
2742
|
-
{ id: "standup", category: "gtm", reference_hours: 0.25, label: "daily standups", template: "\u2248 {ratio}\xD7 skipped {label}", min_ratio: 4, max_ratio: 500 },
|
|
2743
|
-
{ id: "quick_sync", category: "gtm", reference_hours: 0.5, label: "avoided 'quick syncs'", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 2, max_ratio: 200 },
|
|
2744
|
-
{ id: "pipeline_review", category: "gtm", reference_hours: 1, label: "weekly pipeline reviews", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 200 },
|
|
2745
|
-
{ id: "forecast_call", category: "gtm", reference_hours: 1.5, label: "forecast calls", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 150 },
|
|
2746
|
-
{ id: "pivot_spiral", category: "gtm", reference_hours: 2, label: "spreadsheet pivot-table spirals", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 100 },
|
|
2747
|
-
{ id: "win_loss", category: "gtm", reference_hours: 4, label: "win/loss interview blocks", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 50 },
|
|
2748
|
-
{ id: "crm_cleanup", category: "gtm", reference_hours: 6, label: "CRM hygiene sprints", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 40 },
|
|
2749
|
-
{ id: "qbr_prep", category: "gtm", reference_hours: 8, label: "QBR prep blocks", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 20 },
|
|
2750
|
-
{ id: "board_deck", category: "gtm", reference_hours: 12, label: "board deck builds", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 30 },
|
|
2751
|
-
{ id: "semester", category: "gtm", reference_hours: 400, label: "a college semester of analyst coverage", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 5 },
|
|
2752
|
-
{ id: "business_year", category: "gtm", reference_hours: 2e3, label: "a full-time analyst year", template: "\u2248 {pct}% of {label}", min_ratio: 0.2, max_ratio: 1 }
|
|
2753
|
-
];
|
|
2754
|
-
}
|
|
2755
|
-
});
|
|
2756
|
-
|
|
2757
|
-
// src/whimsy/time-bank-whimsy.ts
|
|
2758
|
-
var init_time_bank_whimsy = __esm({
|
|
2759
|
-
"src/whimsy/time-bank-whimsy.ts"() {
|
|
2760
|
-
"use strict";
|
|
2761
|
-
}
|
|
2762
|
-
});
|
|
2763
|
-
|
|
2764
|
-
// src/whimsy/perspective-rotation.ts
|
|
2765
|
-
var PERSPECTIVE_ROTATE_CALENDAR_MS;
|
|
2766
|
-
var init_perspective_rotation = __esm({
|
|
2767
|
-
"src/whimsy/perspective-rotation.ts"() {
|
|
2768
|
-
"use strict";
|
|
2769
|
-
PERSPECTIVE_ROTATE_CALENDAR_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2770
|
-
}
|
|
2771
|
-
});
|
|
2772
|
-
|
|
2773
|
-
// src/whimsy/time-bank.ts
|
|
2774
|
-
import chalk6 from "chalk";
|
|
2775
|
-
var init_time_bank = __esm({
|
|
2776
|
-
"src/whimsy/time-bank.ts"() {
|
|
2777
|
-
"use strict";
|
|
2778
|
-
init_progress();
|
|
2779
|
-
init_theme();
|
|
2780
|
-
init_time_milestones();
|
|
2781
|
-
init_time_perspectives();
|
|
2782
|
-
init_time_bank_whimsy();
|
|
2783
|
-
init_perspective_rotation();
|
|
2784
|
-
init_usage_stats();
|
|
2785
|
-
}
|
|
2786
|
-
});
|
|
2787
|
-
|
|
2788
|
-
// src/conversation/strategist-flow.ts
|
|
2789
|
-
import chalk7 from "chalk";
|
|
2790
|
-
function isStrategistIntent(input) {
|
|
2791
|
-
const line = input.trim();
|
|
2792
|
-
if (!line) return false;
|
|
2793
|
-
if (isShipIntent(line)) return false;
|
|
2794
|
-
return STRATEGIST_INTENT_RE.test(line);
|
|
2795
|
-
}
|
|
2796
|
-
function extractObjectiveSeed(input) {
|
|
2797
|
-
const cleaned = input.trim().replace(/^(hey|ok|okay|please|can you|could you|help me|let'?s|i want to|i'?d like to|i need to)\s+/i, "").replace(/^(build|draft|make|create|put together)\s+(me\s+)?(a\s+|the\s+)?(game\s?plan|battle\s?plan|strategy|plan|roadmap)\s*(to|for|around|on)?\s*/i, "").replace(/^strategize\s+(about|around|on|for)?\s*/i, "").trim();
|
|
2798
|
-
return cleaned.length >= 8 ? cleaned : input.trim();
|
|
2799
|
-
}
|
|
2800
|
-
var STRATEGIST_INTENT_RE;
|
|
2801
|
-
var init_strategist_flow = __esm({
|
|
2802
|
-
"src/conversation/strategist-flow.ts"() {
|
|
2803
|
-
"use strict";
|
|
2804
|
-
init_spinner();
|
|
2805
|
-
init_context2();
|
|
2806
|
-
init_handoff_draft();
|
|
2807
|
-
init_repl_api();
|
|
2808
|
-
init_theme();
|
|
2809
|
-
init_prompts();
|
|
2810
|
-
init_health_score();
|
|
2811
|
-
init_divergence();
|
|
2812
|
-
init_strategist();
|
|
2813
|
-
init_strategist2();
|
|
2814
|
-
init_strategy_brief();
|
|
2815
|
-
init_llm_attribution();
|
|
2816
|
-
init_time_bank();
|
|
2817
|
-
init_formatters();
|
|
2818
|
-
STRATEGIST_INTENT_RE = /\b(strateg(y|ize|ic)|game\s?plan|battle\s?plan|roadmap|(build|draft|make|create|put together)\s+(me\s+)?(a\s+|the\s+)?plan\b|plan\s+(to|for)\s+(fix|improv|reduc|recover|hit|reach|get|grow|turn)|how\s+(should|do|can)\s+we\s+(fix|approach|tackle|attack|prioritize|sequence|turn\s+(this|it)\s+around)|what\s+should\s+we\s+(do|fix|tackle|prioritize|focus\s+on)\s+(first|next)|what\s+order\s+should|where\s+(do|should)\s+we\s+start)\b/i;
|
|
2819
|
-
}
|
|
2820
|
-
});
|
|
2821
|
-
|
|
2822
|
-
// src/strategist/strategist-smoke.ts
|
|
2823
|
-
init_strategist_flow();
|
|
2824
|
-
init_phase();
|
|
2825
|
-
init_context2();
|
|
2826
|
-
init_strategist_validate();
|
|
2827
|
-
init_strategist2();
|
|
2828
|
-
init_playbook();
|
|
2829
|
-
init_health_score();
|
|
2830
|
-
var failures = [];
|
|
2831
|
-
function assert(cond, msg) {
|
|
2832
|
-
if (!cond) failures.push(msg);
|
|
2833
|
-
}
|
|
2834
|
-
assert(isStrategistIntent("how should we fix drop rate before the board?"), "strategist intent: how should we fix");
|
|
2835
|
-
assert(isStrategistIntent("build me a game plan to recover pipeline"), "strategist intent: game plan");
|
|
2836
|
-
assert(isStrategistIntent("what should we prioritize first"), "strategist intent: prioritize");
|
|
2837
|
-
assert(!isStrategistIntent("ship a board deck"), "ship intent must not match strategist");
|
|
2838
|
-
assert(!isStrategistIntent("what is our NRR?"), "descriptive question must not match strategist");
|
|
2839
|
-
var seed = extractObjectiveSeed("help me build a plan to fix stale pipeline");
|
|
2840
|
-
assert(seed.includes("stale pipeline"), "objective seed strips lead-in verbs");
|
|
2841
|
-
function mockCtx(partial) {
|
|
2842
|
-
return {
|
|
2843
|
-
sessionId: "2026-07-03-test",
|
|
2844
|
-
sessionFile: "/tmp/test.json",
|
|
2845
|
-
oneShot: false,
|
|
2846
|
-
execution: { mode: "interactive", output: "terminal", color: true, progress: true, quiet: false },
|
|
2847
|
-
snapshot: { computeResult: null, divergences: [] },
|
|
2848
|
-
messages: [],
|
|
2849
|
-
conversation: [],
|
|
2850
|
-
stage: "analyzed",
|
|
2851
|
-
deliverables: [],
|
|
2852
|
-
analysis: defaultSessionAnalysis(),
|
|
2853
|
-
wizardDepth: 0,
|
|
2854
|
-
attachments: [],
|
|
2855
|
-
deliverIntent: false,
|
|
2856
|
-
computeInProgress: false,
|
|
2857
|
-
dataset: { counts: { opportunities: 10 } },
|
|
2858
|
-
...partial
|
|
2859
|
-
};
|
|
2860
|
-
}
|
|
2861
|
-
assert(
|
|
2862
|
-
resolveConversationPhase(mockCtx({ strategistState: { step: "objective_confirm", objective: "fix freshness" } })) === "strategize",
|
|
2863
|
-
"objective_confirm \u2192 strategize phase"
|
|
2864
|
-
);
|
|
2865
|
-
assert(
|
|
2866
|
-
resolveConversationPhase(
|
|
2867
|
-
mockCtx({
|
|
2868
|
-
strategistState: { step: "awaiting_analysis", objective: "fix pipeline" },
|
|
2869
|
-
stage: "new",
|
|
2870
|
-
scope: { intent_summary: "health", primary_lens: "gtm_health", confirmed_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2871
|
-
})
|
|
2872
|
-
) === "awaiting_data",
|
|
2873
|
-
"awaiting_analysis rides normal funnel (not strategize)"
|
|
2874
|
-
);
|
|
2875
|
-
var evidence = JSON.stringify({
|
|
2876
|
-
vital_signs: { freshness: { score: 29, dollar_value: 31e5 } }
|
|
2877
|
-
});
|
|
2878
|
-
var today = "2026-07-03";
|
|
2879
|
-
var validPlan = {
|
|
2880
|
-
title: "Q4 Pipeline Recovery",
|
|
2881
|
-
objective: "Cut stale pipeline before board",
|
|
2882
|
-
summary_30k: "Freshness gates everything.",
|
|
2883
|
-
hypothesis: "Cleaning stale deals unlocks flow fixes.",
|
|
2884
|
-
target_segment: "Enterprise pipeline",
|
|
2885
|
-
priority: "high",
|
|
2886
|
-
review_cadence: "Weekly",
|
|
2887
|
-
confidence: 0.7,
|
|
2888
|
-
constraints: ["6 rep-hours/week"],
|
|
2889
|
-
assumptions: [],
|
|
2890
|
-
risks: ["Rep capacity"],
|
|
2891
|
-
workstreams: [
|
|
2892
|
-
{
|
|
2893
|
-
order: 1,
|
|
2894
|
-
title: "Clean dead pipeline",
|
|
2895
|
-
problem: "Stale deals block trust in the pipeline number",
|
|
2896
|
-
rationale: "Freshness is the gating vital sign",
|
|
2897
|
-
play_ids: ["clean-dead-pipeline"],
|
|
2898
|
-
actions: ["Pull stale list", "Re-engage or close"],
|
|
2899
|
-
effort_hours: 12,
|
|
2900
|
-
milestones: [{ label: "Stale list triaged", due: "2026-07-17", verification: "stale count < 40" }],
|
|
2901
|
-
deliverables: [{ label: "Re-engagement sequence", kind: "artifact", due: "2026-07-24" }],
|
|
2902
|
-
expected_outcome: {
|
|
2903
|
-
metric: "freshness score",
|
|
2904
|
-
baseline: "29",
|
|
2905
|
-
target_range: "45\u201355",
|
|
2906
|
-
check_date: "2026-08-01",
|
|
2907
|
-
measured_by: "freshness vital sign score"
|
|
2908
|
-
},
|
|
2909
|
-
leading_indicators: [
|
|
2910
|
-
{
|
|
2911
|
-
metric: "stale deal count",
|
|
2912
|
-
baseline: "120",
|
|
2913
|
-
target_range: "80\u201390",
|
|
2914
|
-
check_date: "2026-07-20",
|
|
2915
|
-
measured_by: "freshness entity_details stale count"
|
|
2916
|
-
}
|
|
2917
|
-
],
|
|
2918
|
-
contingency: {
|
|
2919
|
-
trigger: "stale count flat by week 2",
|
|
2920
|
-
trigger_check_date: "2026-07-20",
|
|
2921
|
-
fallback: "Descope to top-2 segments only"
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
]
|
|
2925
|
-
};
|
|
2926
|
-
var validated = validateStrategistPlan(validPlan, { evidenceText: evidence + " score 29 dollar 3100000 stale 120", todayIso: today });
|
|
2927
|
-
assert(validated !== null, "valid plan parses");
|
|
2928
|
-
assert(validated.plan.workstreams.length === 1, "valid plan keeps workstream");
|
|
2929
|
-
assert(validated.measurableTargets >= 1, "valid plan has measurable targets");
|
|
2930
|
-
assert(isKnownInstrument("freshness vital sign score"), "freshness is known instrument");
|
|
2931
|
-
var vaguePlan = {
|
|
2932
|
-
...validPlan,
|
|
2933
|
-
workstreams: [
|
|
2934
|
-
{
|
|
2935
|
-
...validPlan.workstreams[0],
|
|
2936
|
-
expected_outcome: {
|
|
2937
|
-
metric: "team morale",
|
|
2938
|
-
baseline: "low",
|
|
2939
|
-
target_range: "much better",
|
|
2940
|
-
check_date: "2026-08-01",
|
|
2941
|
-
measured_by: "gut feel"
|
|
2942
|
-
}
|
|
2943
|
-
}
|
|
2944
|
-
]
|
|
2945
|
-
};
|
|
2946
|
-
var demoted = validateStrategistPlan(vaguePlan, { evidenceText: evidence, todayIso: today });
|
|
2947
|
-
assert(demoted !== null, "vague plan still returns structure");
|
|
2948
|
-
assert(demoted.plan.assumptions.length > 0, "vague outcome demoted to assumptions");
|
|
2949
|
-
var vitals = ["freshness", "flow_rate", "thread_depth"].map((sign) => ({
|
|
2950
|
-
vital_sign: sign,
|
|
2951
|
-
score: sign === "freshness" ? 29 : sign === "flow_rate" ? 40 : 70,
|
|
2952
|
-
status: sign === "freshness" ? "red" : "yellow",
|
|
2953
|
-
dollar_value: sign === "freshness" ? 31e5 : null,
|
|
2954
|
-
dollar_label: sign === "freshness" ? "pipeline at risk" : null
|
|
2955
|
-
}));
|
|
2956
|
-
var triggered = matchTriggeredPlays(vitals, LAYERS);
|
|
2957
|
-
assert(triggered.length >= 1, "keyless skeleton triggers at least one play");
|
|
2958
|
-
assert(triggered[0].layer === 1, "first triggered play respects LAYERS order (freshness first)");
|
|
2959
|
-
var nums = extractNumbers("$3.1M stale pipeline, 120 deals");
|
|
2960
|
-
assert(nums.some((n) => n >= 3e6), "extractNumbers parses $3.1M");
|
|
2961
|
-
assert(
|
|
2962
|
-
describePlanValidationFailure("not json at all", evidence, today).includes("not parseable"),
|
|
2963
|
-
"describePlanValidationFailure: prose \u2192 not parseable"
|
|
2964
|
-
);
|
|
2965
|
-
assert(
|
|
2966
|
-
describePlanValidationFailure('{"title":"x","workstreams":[]}', evidence, today).includes("no usable workstreams"),
|
|
2967
|
-
"describePlanValidationFailure: empty workstreams \u2192 demoted message"
|
|
2968
|
-
);
|
|
2969
|
-
assert(
|
|
2970
|
-
describePlanValidationFailure('{"title":"cut off", "workstreams": [{"title":', evidence, today).includes("truncated"),
|
|
2971
|
-
"describePlanValidationFailure: truncated JSON \u2192 truncated message"
|
|
2972
|
-
);
|
|
2973
|
-
var fallback = buildGroundedFallbackPlan({
|
|
2974
|
-
objective: "Cut stale pipeline in half before Q4",
|
|
2975
|
-
vitals: ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"].map(
|
|
2976
|
-
(sign) => ({
|
|
2977
|
-
vital_sign: sign,
|
|
2978
|
-
score: sign === "freshness" ? 29 : sign === "flow_rate" ? 40 : 70,
|
|
2979
|
-
status: sign === "freshness" ? "red" : sign === "flow_rate" ? "red" : "yellow",
|
|
2980
|
-
dollar_value: sign === "freshness" ? 31e5 : sign === "flow_rate" ? 12e5 : null,
|
|
2981
|
-
dollar_label: sign === "freshness" ? "pipeline at risk" : sign === "flow_rate" ? "stuck in pipeline" : null
|
|
2982
|
-
})
|
|
2983
|
-
),
|
|
2984
|
-
todayIso: today,
|
|
2985
|
-
gatingVitalSign: "freshness",
|
|
2986
|
-
totalValueAtRisk: 43e5
|
|
2987
|
-
});
|
|
2988
|
-
assert(fallback.plan.workstreams.length >= 1, "grounded fallback has workstreams");
|
|
2989
|
-
assert(fallback.plan.objective.includes("stale pipeline"), "grounded fallback keeps objective");
|
|
2990
|
-
assert(
|
|
2991
|
-
fallback.plan.workstreams.some((ws) => ws.play_ids.includes("clean-dead-pipeline")),
|
|
2992
|
-
"grounded fallback links freshness play"
|
|
2993
|
-
);
|
|
2994
|
-
assert(fallback.issues.some((i) => i.includes("grounded fallback")), "grounded fallback records issue");
|
|
2995
|
-
if (failures.length > 0) {
|
|
2996
|
-
console.error("FAIL strategist-smoke:");
|
|
2997
|
-
for (const f of failures) console.error(" -", f);
|
|
2998
|
-
process.exit(1);
|
|
2999
|
-
}
|
|
3000
|
-
console.log("PASS strategist-smoke");
|
|
3001
|
-
//# sourceMappingURL=strategist-smoke.js.map
|