@sonnechasser/ntrp 1.3.5 → 1.3.8
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 +23484 -23393
- package/dist/mcp/server.js +3725 -3000
- package/package.json +8 -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 -25584
- package/dist/ai/guardrails-smoke.js.map +0 -1
- package/dist/conversation/deepdive-smoke.js +0 -3416
- package/dist/conversation/deepdive-smoke.js.map +0 -1
- package/dist/conversation/loop-guard-smoke.js +0 -37627
- package/dist/conversation/loop-guard-smoke.js.map +0 -1
- package/dist/demo/whimsy-smoke.js +0 -692
- package/dist/demo/whimsy-smoke.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/investigation/quality-eval-cli.js +0 -24564
- package/dist/investigation/quality-eval-cli.js.map +0 -1
- package/dist/investigation/verbosity-cli.js +0 -24373
- 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 -1205
- package/dist/services/exports-registry-smoke.js.map +0 -1
- package/dist/services/transcript-smoke.js +0 -1101
- package/dist/services/transcript-smoke.js.map +0 -1
- package/dist/strategist/strategist-smoke.js +0 -3073
- package/dist/strategist/strategist-smoke.js.map +0 -1
- package/dist/whimsy/time-bank-smoke.js +0 -36126
- package/dist/whimsy/time-bank-smoke.js.map +0 -1
|
@@ -1,692 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
process.noDeprecation = true;
|
|
3
|
-
|
|
4
|
-
// src/demo/seed.ts
|
|
5
|
-
function mulberry32(seed) {
|
|
6
|
-
let a = seed | 0;
|
|
7
|
-
return () => {
|
|
8
|
-
a = a + 1831565813 | 0;
|
|
9
|
-
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
10
|
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
11
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function createSeededRandom(seed) {
|
|
15
|
-
const raw = mulberry32(seed);
|
|
16
|
-
const rng = {
|
|
17
|
-
next: raw,
|
|
18
|
-
nextInt(min, max) {
|
|
19
|
-
return Math.floor(raw() * (max - min + 1)) + min;
|
|
20
|
-
},
|
|
21
|
-
nextFloat(min, max) {
|
|
22
|
-
return raw() * (max - min) + min;
|
|
23
|
-
},
|
|
24
|
-
pick(arr) {
|
|
25
|
-
if (arr.length === 0) {
|
|
26
|
-
throw new Error("Cannot pick from an empty array");
|
|
27
|
-
}
|
|
28
|
-
return arr[Math.floor(raw() * arr.length)];
|
|
29
|
-
},
|
|
30
|
-
pickN(arr, n) {
|
|
31
|
-
const copy = [...arr];
|
|
32
|
-
rng.shuffle(copy);
|
|
33
|
-
return copy.slice(0, Math.min(n, copy.length));
|
|
34
|
-
},
|
|
35
|
-
shuffle(arr) {
|
|
36
|
-
for (let i = arr.length - 1; i > 0; i--) {
|
|
37
|
-
const j = Math.floor(raw() * (i + 1));
|
|
38
|
-
const current = arr[i];
|
|
39
|
-
arr[i] = arr[j];
|
|
40
|
-
arr[j] = current;
|
|
41
|
-
}
|
|
42
|
-
return arr;
|
|
43
|
-
},
|
|
44
|
-
chance(probability) {
|
|
45
|
-
return raw() < probability;
|
|
46
|
-
},
|
|
47
|
-
uuid() {
|
|
48
|
-
const bytes = Array.from({ length: 16 }, () => Math.floor(raw() * 256));
|
|
49
|
-
bytes[6] = bytes[6] & 15 | 64;
|
|
50
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
51
|
-
const hex = bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
52
|
-
return [
|
|
53
|
-
hex.slice(0, 8),
|
|
54
|
-
hex.slice(8, 12),
|
|
55
|
-
hex.slice(12, 16),
|
|
56
|
-
hex.slice(16, 20),
|
|
57
|
-
hex.slice(20, 32)
|
|
58
|
-
].join("-");
|
|
59
|
-
},
|
|
60
|
-
date(start, end) {
|
|
61
|
-
const s = start.getTime();
|
|
62
|
-
const e = end.getTime();
|
|
63
|
-
return new Date(s + raw() * (e - s));
|
|
64
|
-
},
|
|
65
|
-
weightedPick(items, weights) {
|
|
66
|
-
if (items.length === 0) {
|
|
67
|
-
throw new Error("Cannot pick from an empty weighted item list");
|
|
68
|
-
}
|
|
69
|
-
const total = weights.reduce((sum, w) => sum + w, 0);
|
|
70
|
-
let r = raw() * total;
|
|
71
|
-
for (let i = 0; i < items.length; i++) {
|
|
72
|
-
r -= weights[i] ?? 0;
|
|
73
|
-
if (r <= 0) return items[i];
|
|
74
|
-
}
|
|
75
|
-
return items[items.length - 1];
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
return rng;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// src/demo/whimsy-names.ts
|
|
82
|
-
var WHIMSY_FIGURES = [
|
|
83
|
-
// ── Music (band members, real names — no stage names) ──
|
|
84
|
-
{ first: "James", last: "Hetfield", category: "music" },
|
|
85
|
-
{ first: "Lars", last: "Ulrich", category: "music" },
|
|
86
|
-
{ first: "Kirk", last: "Hammett", category: "music" },
|
|
87
|
-
{ first: "Robert", last: "Trujillo", category: "music" },
|
|
88
|
-
{ first: "Myles", last: "Kennedy", category: "music" },
|
|
89
|
-
{ first: "Mark", last: "Tremonti", category: "music" },
|
|
90
|
-
{ first: "Scott", last: "Stapp", category: "music" },
|
|
91
|
-
{ first: "Dave", last: "Grohl", category: "music" },
|
|
92
|
-
{ first: "Taylor", last: "Hawkins", category: "music" },
|
|
93
|
-
{ first: "Eddie", last: "Vedder", category: "music" },
|
|
94
|
-
{ first: "Mike", last: "McCready", category: "music" },
|
|
95
|
-
{ first: "Stone", last: "Gossard", category: "music" },
|
|
96
|
-
{ first: "Geddy", last: "Lee", category: "music" },
|
|
97
|
-
{ first: "Alex", last: "Lifeson", category: "music" },
|
|
98
|
-
{ first: "Neil", last: "Peart", category: "music" },
|
|
99
|
-
{ first: "Robert", last: "Plant", category: "music" },
|
|
100
|
-
{ first: "Jimmy", last: "Page", category: "music" },
|
|
101
|
-
{ first: "Brian", last: "May", category: "music" },
|
|
102
|
-
{ first: "Roger", last: "Taylor", category: "music" },
|
|
103
|
-
{ first: "Adam", last: "Jones", category: "music" },
|
|
104
|
-
{ first: "Danny", last: "Carey", category: "music" },
|
|
105
|
-
{ first: "Chris", last: "Cornell", category: "music" },
|
|
106
|
-
{ first: "Angus", last: "Young", category: "music" },
|
|
107
|
-
{ first: "Chad", last: "Smith", category: "music" },
|
|
108
|
-
{ first: "Sammy", last: "Hagar", category: "music" },
|
|
109
|
-
{ first: "Maynard", last: "Keenan", category: "music" },
|
|
110
|
-
{ first: "Thom", last: "Yorke", category: "music" },
|
|
111
|
-
{ first: "Tony", last: "Iommi", category: "music" },
|
|
112
|
-
{ first: "Billie", last: "Armstrong", category: "music" },
|
|
113
|
-
{ first: "Bruce", last: "Dickinson", category: "music" },
|
|
114
|
-
{ first: "Stevie", last: "Nicks", category: "music" },
|
|
115
|
-
{ first: "Jon", last: "Jovi", category: "music" },
|
|
116
|
-
{ first: "Richie", last: "Sambora", category: "music" },
|
|
117
|
-
// ── Sports (legends, broadly uncontroversial) ──
|
|
118
|
-
{ first: "Michael", last: "Jordan", category: "sports" },
|
|
119
|
-
{ first: "Wayne", last: "Gretzky", category: "sports" },
|
|
120
|
-
{ first: "Roger", last: "Federer", category: "sports" },
|
|
121
|
-
{ first: "Serena", last: "Williams", category: "sports" },
|
|
122
|
-
{ first: "Lionel", last: "Messi", category: "sports" },
|
|
123
|
-
{ first: "Peyton", last: "Manning", category: "sports" },
|
|
124
|
-
{ first: "Derek", last: "Jeter", category: "sports" },
|
|
125
|
-
{ first: "Sidney", last: "Crosby", category: "sports" },
|
|
126
|
-
{ first: "Mia", last: "Hamm", category: "sports" },
|
|
127
|
-
{ first: "Larry", last: "Bird", category: "sports" },
|
|
128
|
-
{ first: "Steph", last: "Curry", category: "sports" },
|
|
129
|
-
{ first: "Patrick", last: "Mahomes", category: "sports" },
|
|
130
|
-
{ first: "Jackie", last: "Robinson", category: "sports" },
|
|
131
|
-
{ first: "Bonnie", last: "Blair", category: "sports" },
|
|
132
|
-
{ first: "Peggy", last: "Fleming", category: "sports" },
|
|
133
|
-
{ first: "Tiger", last: "Woods", category: "sports" },
|
|
134
|
-
{ first: "Simone", last: "Biles", category: "sports" },
|
|
135
|
-
{ first: "Venus", last: "Williams", category: "sports" },
|
|
136
|
-
{ first: "Tom", last: "Brady", category: "sports" },
|
|
137
|
-
{ first: "Naomi", last: "Osaka", category: "sports" },
|
|
138
|
-
{ first: "Rafael", last: "Nadal", category: "sports" },
|
|
139
|
-
// ── Film / TV (well-known actors) ──
|
|
140
|
-
{ first: "Tom", last: "Hanks", category: "film" },
|
|
141
|
-
{ first: "Meryl", last: "Streep", category: "film" },
|
|
142
|
-
{ first: "Denzel", last: "Washington", category: "film" },
|
|
143
|
-
{ first: "Harrison", last: "Ford", category: "film" },
|
|
144
|
-
{ first: "Sigourney", last: "Weaver", category: "film" },
|
|
145
|
-
{ first: "Keanu", last: "Reeves", category: "film" },
|
|
146
|
-
{ first: "Morgan", last: "Freeman", category: "film" },
|
|
147
|
-
{ first: "Bryan", last: "Cranston", category: "film" },
|
|
148
|
-
{ first: "Jeff", last: "Goldblum", category: "film" },
|
|
149
|
-
{ first: "Cate", last: "Blanchett", category: "film" },
|
|
150
|
-
{ first: "Viola", last: "Davis", category: "film" },
|
|
151
|
-
{ first: "Jodie", last: "Foster", category: "film" },
|
|
152
|
-
{ first: "Sandra", last: "Bullock", category: "film" },
|
|
153
|
-
{ first: "Idris", last: "Elba", category: "film" },
|
|
154
|
-
{ first: "Emma", last: "Stone", category: "film" },
|
|
155
|
-
{ first: "Steve", last: "Carell", category: "film" },
|
|
156
|
-
{ first: "Jennifer", last: "Aniston", category: "film" },
|
|
157
|
-
{ first: "Matt", last: "Damon", category: "film" },
|
|
158
|
-
{ first: "Julia", last: "Roberts", category: "film" },
|
|
159
|
-
{ first: "Leonardo", last: "DiCaprio", category: "film" },
|
|
160
|
-
{ first: "Aaron", last: "Paul", category: "film" },
|
|
161
|
-
{ first: "Gillian", last: "Anderson", category: "film" }
|
|
162
|
-
];
|
|
163
|
-
function createFigureDrawer(rng, categories) {
|
|
164
|
-
const pool = categories && categories.length > 0 ? WHIMSY_FIGURES.filter((f) => categories.includes(f.category)) : [...WHIMSY_FIGURES];
|
|
165
|
-
const shuffled = rng.shuffle([...pool]);
|
|
166
|
-
let idx = 0;
|
|
167
|
-
return {
|
|
168
|
-
next() {
|
|
169
|
-
if (idx >= shuffled.length) return null;
|
|
170
|
-
return shuffled[idx++];
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// src/demo/scenarios.ts
|
|
176
|
-
var BASELINE = {
|
|
177
|
-
enterpriseRatio: 0.2,
|
|
178
|
-
midMarketRatio: 0.3,
|
|
179
|
-
smbRatio: 0.5,
|
|
180
|
-
staleContactRatio: 0.15,
|
|
181
|
-
staleContactRatioEnterprise: 0.2,
|
|
182
|
-
staleContactRatioSmb: 0.1,
|
|
183
|
-
pastCloseDateRatio: 0.1,
|
|
184
|
-
staleDays: 120,
|
|
185
|
-
mqlDropRatio: 0.1,
|
|
186
|
-
qualifiedNoOutreachRatio: 0.1,
|
|
187
|
-
stuckDealRatio: 0.1,
|
|
188
|
-
stuckInNegotiationDays: 45,
|
|
189
|
-
avgDaysPerStageEnterprise: 25,
|
|
190
|
-
avgDaysPerStageSmb: 8,
|
|
191
|
-
activityVolumeMultiplier: 1,
|
|
192
|
-
noiseActivityRatio: 0.15,
|
|
193
|
-
singleThreadRatio: 0.2,
|
|
194
|
-
loneWolfRepIndex: null,
|
|
195
|
-
loneWolfSingleThreadRatio: 0,
|
|
196
|
-
freshnessGapDays: 90
|
|
197
|
-
};
|
|
198
|
-
var SCENARIOS = {
|
|
199
|
-
hidden_crisis: {
|
|
200
|
-
...BASELINE,
|
|
201
|
-
key: "hidden_crisis",
|
|
202
|
-
label: "The Hidden Crisis",
|
|
203
|
-
description: "Overall health looks yellow but Enterprise is deep red, masked by strong SMB numbers.",
|
|
204
|
-
story: "Your aggregate numbers look okay \u2014 but when you break it by segment, Enterprise is dying. 60% of enterprise contacts have gone dark, deals are single-threaded, and SMB is carrying the average.",
|
|
205
|
-
hook: "SMB is carrying the average while Enterprise dies quietly.",
|
|
206
|
-
staleContactRatio: 0.3,
|
|
207
|
-
staleContactRatioEnterprise: 0.6,
|
|
208
|
-
staleContactRatioSmb: 0.1,
|
|
209
|
-
singleThreadRatio: 0.5,
|
|
210
|
-
enterpriseRatio: 0.3,
|
|
211
|
-
midMarketRatio: 0.3,
|
|
212
|
-
smbRatio: 0.4
|
|
213
|
-
},
|
|
214
|
-
leaky_bucket: {
|
|
215
|
-
...BASELINE,
|
|
216
|
-
key: "leaky_bucket",
|
|
217
|
-
label: "The Leaky Bucket",
|
|
218
|
-
description: "Marketing generates plenty of leads but 40% vanish at handoff to sales.",
|
|
219
|
-
story: "Marketing is doing its job \u2014 MQLs are flowing. But 40% of qualified leads never show up in sales workflows. They're falling through the cracks at handoff, and nobody's noticing because marketing reports MQL count and sales reports pipeline value.",
|
|
220
|
-
hook: "MQLs flow in, then 40% vanish at the sales handoff.",
|
|
221
|
-
mqlDropRatio: 0.4,
|
|
222
|
-
qualifiedNoOutreachRatio: 0.35,
|
|
223
|
-
staleContactRatio: 0.2
|
|
224
|
-
},
|
|
225
|
-
stale_pipeline: {
|
|
226
|
-
...BASELINE,
|
|
227
|
-
key: "stale_pipeline",
|
|
228
|
-
label: "The Stale Pipeline",
|
|
229
|
-
description: "Big pipeline number but half the deals are zombies stuck in late stages.",
|
|
230
|
-
story: "The pipeline report says $5M. But look closer: half those deals have close dates in the past, 40% are stuck in Negotiation for 120+ days, and nobody's touching them. You're forecasting on fiction.",
|
|
231
|
-
hook: "Half the pipeline is zombies \u2014 you're forecasting on fiction.",
|
|
232
|
-
pastCloseDateRatio: 0.5,
|
|
233
|
-
stuckDealRatio: 0.4,
|
|
234
|
-
stuckInNegotiationDays: 120,
|
|
235
|
-
staleContactRatio: 0.25,
|
|
236
|
-
staleDays: 90
|
|
237
|
-
},
|
|
238
|
-
lone_wolf: {
|
|
239
|
-
...BASELINE,
|
|
240
|
-
key: "lone_wolf",
|
|
241
|
-
label: "The Lone Wolf",
|
|
242
|
-
description: "One rep has great numbers but every single deal is single-threaded.",
|
|
243
|
-
story: "Your top rep is crushing it on paper \u2014 biggest pipeline, highest close rate. But every deal has exactly one contact. One champion goes on vacation, gets promoted, or leaves, and the entire pipeline collapses.",
|
|
244
|
-
hook: "Top rep, huge pipeline, one contact per deal \u2014 one exit from collapse.",
|
|
245
|
-
loneWolfRepIndex: 0,
|
|
246
|
-
loneWolfSingleThreadRatio: 1,
|
|
247
|
-
singleThreadRatio: 0.15
|
|
248
|
-
},
|
|
249
|
-
busy_bees: {
|
|
250
|
-
...BASELINE,
|
|
251
|
-
key: "busy_bees",
|
|
252
|
-
label: "The Busy Bees",
|
|
253
|
-
description: "High activity volume across the team, but most of it hits dead ends.",
|
|
254
|
-
story: "Your team is busy. Activity metrics look great \u2014 calls are up, emails are up, meetings are up. But 60% of that activity is aimed at contacts with no associated pipeline. Reps are spraying, not aiming.",
|
|
255
|
-
hook: "Reps are spraying, not aiming.",
|
|
256
|
-
activityVolumeMultiplier: 3,
|
|
257
|
-
noiseActivityRatio: 0.6,
|
|
258
|
-
staleContactRatio: 0.2
|
|
259
|
-
},
|
|
260
|
-
even_keel: {
|
|
261
|
-
...BASELINE,
|
|
262
|
-
key: "even_keel",
|
|
263
|
-
label: "The Even Keel",
|
|
264
|
-
description: "A reasonably healthy book \u2014 enough yellow to listen, not a five-alarm fire.",
|
|
265
|
-
story: "Most numbers sit in a normal band. A few contacts have gone quiet, a handful of deals are slow, activity is mostly on-pipeline. This is what 'fine' looks like on the stethoscope \u2014 useful when you want to evaluate NTRP without a manufactured crisis.",
|
|
266
|
-
hook: "Reasonably healthy \u2014 enough signal to listen, not a crisis."
|
|
267
|
-
},
|
|
268
|
-
compound_pain: {
|
|
269
|
-
...BASELINE,
|
|
270
|
-
key: "compound_pain",
|
|
271
|
-
label: "The Compound Fracture",
|
|
272
|
-
description: "Several vitals are red at once \u2014 stale pipeline, leaky handoff, noisy activity, thin threads.",
|
|
273
|
-
story: "This isn't one problem. Enterprise contacts have gone dark, MQLs vanish at handoff, late-stage deals are zombies, and a lot of activity never touches pipeline. The gating logic has to pick a first red \u2014 that's the point of this book.",
|
|
274
|
-
hook: "Several vitals red at once \u2014 the stethoscope has to pick a first listen.",
|
|
275
|
-
enterpriseRatio: 0.3,
|
|
276
|
-
midMarketRatio: 0.3,
|
|
277
|
-
smbRatio: 0.4,
|
|
278
|
-
staleContactRatio: 0.35,
|
|
279
|
-
staleContactRatioEnterprise: 0.55,
|
|
280
|
-
staleContactRatioSmb: 0.15,
|
|
281
|
-
pastCloseDateRatio: 0.35,
|
|
282
|
-
staleDays: 100,
|
|
283
|
-
mqlDropRatio: 0.3,
|
|
284
|
-
qualifiedNoOutreachRatio: 0.25,
|
|
285
|
-
stuckDealRatio: 0.3,
|
|
286
|
-
stuckInNegotiationDays: 90,
|
|
287
|
-
activityVolumeMultiplier: 2,
|
|
288
|
-
noiseActivityRatio: 0.4,
|
|
289
|
-
singleThreadRatio: 0.4
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
var SCENARIO_LIST = Object.values(SCENARIOS);
|
|
293
|
-
function getScenario(key) {
|
|
294
|
-
const scenario = SCENARIOS[key];
|
|
295
|
-
if (!scenario) {
|
|
296
|
-
throw new Error(`Unknown scenario: ${key}. Valid: ${Object.keys(SCENARIOS).join(", ")}`);
|
|
297
|
-
}
|
|
298
|
-
return scenario;
|
|
299
|
-
}
|
|
300
|
-
var NAMED_DEMO_SCENARIOS = [
|
|
301
|
-
"hidden_crisis",
|
|
302
|
-
"leaky_bucket",
|
|
303
|
-
"stale_pipeline",
|
|
304
|
-
"lone_wolf",
|
|
305
|
-
"busy_bees",
|
|
306
|
-
"even_keel",
|
|
307
|
-
"compound_pain"
|
|
308
|
-
];
|
|
309
|
-
function resolveScenarioInput(raw) {
|
|
310
|
-
const input = raw?.trim();
|
|
311
|
-
if (!input) return void 0;
|
|
312
|
-
if (input === "research_blend") return "research_blend";
|
|
313
|
-
if (NAMED_DEMO_SCENARIOS.includes(input)) {
|
|
314
|
-
return input;
|
|
315
|
-
}
|
|
316
|
-
const n = Number(input);
|
|
317
|
-
if (Number.isInteger(n) && n >= 1 && n <= NAMED_DEMO_SCENARIOS.length) {
|
|
318
|
-
return NAMED_DEMO_SCENARIOS[n - 1];
|
|
319
|
-
}
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
var RANDOM_POOL = [...NAMED_DEMO_SCENARIOS];
|
|
323
|
-
|
|
324
|
-
// src/config/store.ts
|
|
325
|
-
import { homedir } from "os";
|
|
326
|
-
import { join, resolve } from "path";
|
|
327
|
-
var NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join(homedir(), ".ntrp");
|
|
328
|
-
var CONFIG_PATH = join(NTRP_DIR, "config.json");
|
|
329
|
-
function ntrpHome() {
|
|
330
|
-
return NTRP_DIR;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// src/config/profile.ts
|
|
334
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
335
|
-
import { join as join2 } from "path";
|
|
336
|
-
var NTRP_DIR2 = ntrpHome();
|
|
337
|
-
var PROFILE_PATH = join2(NTRP_DIR2, "profile.json");
|
|
338
|
-
|
|
339
|
-
// src/demo/scenario-fit.ts
|
|
340
|
-
var MOTION_FIT_CHOICES = [
|
|
341
|
-
{
|
|
342
|
-
value: "plg",
|
|
343
|
-
label: "Product-led / self-serve",
|
|
344
|
-
description: "Users start themselves; sales assists or expands"
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
value: "smb_velocity",
|
|
348
|
-
label: "High-volume SMB",
|
|
349
|
-
description: "Fast cycles, lots of small deals, outbound or inbound machine"
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
value: "mid_market",
|
|
353
|
-
label: "Mid-market, structured process",
|
|
354
|
-
description: "A real sales cycle, a few stakeholders, moderate ACV"
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
value: "enterprise",
|
|
358
|
-
label: "Enterprise, long cycles",
|
|
359
|
-
description: "Large deals, many buyers, quarters not weeks"
|
|
360
|
-
}
|
|
361
|
-
];
|
|
362
|
-
var DEAL_BAND_CHOICES = [
|
|
363
|
-
{
|
|
364
|
-
value: "velocity",
|
|
365
|
-
label: "Under ~$15K, days to a couple of weeks",
|
|
366
|
-
description: "Velocity / transactional"
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
value: "core",
|
|
370
|
-
label: "~$15K\u2013$50K, a few weeks",
|
|
371
|
-
description: "Core SMB"
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
value: "mid",
|
|
375
|
-
label: "~$50K\u2013$150K, 1\u20133 months",
|
|
376
|
-
description: "Classic mid-market"
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
value: "enterprise",
|
|
380
|
-
label: "$150K+, a quarter or more",
|
|
381
|
-
description: "Enterprise / strategic"
|
|
382
|
-
}
|
|
383
|
-
];
|
|
384
|
-
var MATRIX = {
|
|
385
|
-
plg: {
|
|
386
|
-
velocity: "leaky_bucket",
|
|
387
|
-
core: "leaky_bucket",
|
|
388
|
-
mid: "hidden_crisis",
|
|
389
|
-
enterprise: "hidden_crisis"
|
|
390
|
-
},
|
|
391
|
-
smb_velocity: {
|
|
392
|
-
velocity: "busy_bees",
|
|
393
|
-
core: "busy_bees",
|
|
394
|
-
mid: "leaky_bucket",
|
|
395
|
-
enterprise: "lone_wolf"
|
|
396
|
-
},
|
|
397
|
-
mid_market: {
|
|
398
|
-
velocity: "busy_bees",
|
|
399
|
-
core: "stale_pipeline",
|
|
400
|
-
mid: "stale_pipeline",
|
|
401
|
-
enterprise: "hidden_crisis"
|
|
402
|
-
},
|
|
403
|
-
enterprise: {
|
|
404
|
-
velocity: "lone_wolf",
|
|
405
|
-
core: "lone_wolf",
|
|
406
|
-
mid: "hidden_crisis",
|
|
407
|
-
enterprise: "hidden_crisis"
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
var MOTION_ONLY = {
|
|
411
|
-
plg: "leaky_bucket",
|
|
412
|
-
smb_velocity: "busy_bees",
|
|
413
|
-
mid_market: "stale_pipeline",
|
|
414
|
-
enterprise: "hidden_crisis"
|
|
415
|
-
};
|
|
416
|
-
var BAND_ONLY = {
|
|
417
|
-
velocity: "busy_bees",
|
|
418
|
-
core: "leaky_bucket",
|
|
419
|
-
mid: "stale_pipeline",
|
|
420
|
-
enterprise: "hidden_crisis"
|
|
421
|
-
};
|
|
422
|
-
var KEYWORD_HINTS = [
|
|
423
|
-
{ scenario: "even_keel", patterns: [/\beven keel\b/, /\breasonably healthy\b/, /\bno crisis\b/, /\bjust evaluating\b/, /\bgreen[- ]field\b/] },
|
|
424
|
-
{ scenario: "compound_pain", patterns: [/\beverything('?s| is) (on fire|red|broken)\b/, /\bcompound\b/, /\ball (five )?vitals\b/, /\bmultiple problems\b/] },
|
|
425
|
-
{ scenario: "leaky_bucket", patterns: [/\bhandoff\b/, /\bmqls?\b/, /\bleak/, /\bdrop[- ]rate/, /\bvanish/, /\brouting\b/, /\bmarketing.?sales\b/] },
|
|
426
|
-
{ scenario: "stale_pipeline", patterns: [/\bzombie/, /\bstale\b/, /\bpast[- ]due\b/, /\bforecast(ing)? on fiction\b/, /\bstuck in negotiation\b/, /\bquiet deals?\b/] },
|
|
427
|
-
{ scenario: "lone_wolf", patterns: [/\bsingle[- ]thread/, /\blone wolf\b/, /\bone contact\b/, /\bchampion leaves\b/] },
|
|
428
|
-
{ scenario: "busy_bees", patterns: [/\bspray/, /\bnois(e|y)\b/, /\bmisdirected\b/, /\bactivity (volume|metrics)\b/, /\bbusy bees\b/, /\bnot (on|hitting) pipeline\b/] },
|
|
429
|
-
{ scenario: "hidden_crisis", patterns: [/\bhidden crisis\b/, /\benterprise (is )?(dying|red|stale)\b/, /\bsegment.{0,20}mask/, /\baverages? (look|looks) (fine|okay|yellow)\b/] }
|
|
430
|
-
];
|
|
431
|
-
function dealBandFromCycleDays(days) {
|
|
432
|
-
if (days == null || !Number.isFinite(days) || days <= 0) return void 0;
|
|
433
|
-
if (days <= 21) return "velocity";
|
|
434
|
-
if (days <= 45) return "core";
|
|
435
|
-
if (days <= 90) return "mid";
|
|
436
|
-
return "enterprise";
|
|
437
|
-
}
|
|
438
|
-
function dealBandFromAverageDealSize(raw) {
|
|
439
|
-
if (!raw) return void 0;
|
|
440
|
-
const t = raw.trim().toLowerCase();
|
|
441
|
-
if (!t) return void 0;
|
|
442
|
-
const match = t.match(/(\d+(?:\.\d+)?)\s*(k|m|million|thousand)?/i);
|
|
443
|
-
if (!match) return void 0;
|
|
444
|
-
let n = Number(match[1]);
|
|
445
|
-
if (!Number.isFinite(n)) return void 0;
|
|
446
|
-
const unit = (match[2] ?? "").toLowerCase();
|
|
447
|
-
if (unit === "k" || unit === "thousand") n *= 1e3;
|
|
448
|
-
else if (unit === "m" || unit === "million") n *= 1e6;
|
|
449
|
-
else if (n > 0 && n < 500) n *= 1e3;
|
|
450
|
-
if (n < 15e3) return "velocity";
|
|
451
|
-
if (n < 5e4) return "core";
|
|
452
|
-
if (n < 15e4) return "mid";
|
|
453
|
-
return "enterprise";
|
|
454
|
-
}
|
|
455
|
-
function signalsFromProfile(profile) {
|
|
456
|
-
if (!profile) return {};
|
|
457
|
-
const text = [
|
|
458
|
-
profile.industry,
|
|
459
|
-
profile.product_description,
|
|
460
|
-
profile.target_customer,
|
|
461
|
-
profile.user_scope,
|
|
462
|
-
profile.custom_context
|
|
463
|
-
].filter(Boolean).join("\n");
|
|
464
|
-
return {
|
|
465
|
-
salesMotion: profile.sales_motion,
|
|
466
|
-
dealBand: dealBandFromAverageDealSize(profile.average_deal_size) ?? dealBandFromCycleDays(profile.sales_cycle_days),
|
|
467
|
-
cycleDays: profile.sales_cycle_days,
|
|
468
|
-
text
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
function keywordHits(text) {
|
|
472
|
-
const hits = {};
|
|
473
|
-
const hay = text.toLowerCase();
|
|
474
|
-
for (const { scenario, patterns } of KEYWORD_HINTS) {
|
|
475
|
-
let n = 0;
|
|
476
|
-
for (const re of patterns) {
|
|
477
|
-
if (re.test(hay)) n++;
|
|
478
|
-
}
|
|
479
|
-
if (n > 0) hits[scenario] = n;
|
|
480
|
-
}
|
|
481
|
-
return hits;
|
|
482
|
-
}
|
|
483
|
-
function matrixPick(motion, band) {
|
|
484
|
-
if (motion && band) return MATRIX[motion][band];
|
|
485
|
-
if (motion) return MOTION_ONLY[motion];
|
|
486
|
-
if (band) return BAND_ONLY[band];
|
|
487
|
-
return "even_keel";
|
|
488
|
-
}
|
|
489
|
-
function reasonFor(scenario, signals, via) {
|
|
490
|
-
const s = getScenario(scenario);
|
|
491
|
-
if (via === "keywords") {
|
|
492
|
-
return `Your notes sound like ${s.label} \u2014 ${s.hook}`;
|
|
493
|
-
}
|
|
494
|
-
const motion = signals.salesMotion;
|
|
495
|
-
const band = signals.dealBand;
|
|
496
|
-
if (motion && band) {
|
|
497
|
-
return `${labelMotion(motion)} with ${labelBand(band)} deals maps to ${s.label}.`;
|
|
498
|
-
}
|
|
499
|
-
if (motion) return `${labelMotion(motion)} books usually show up as ${s.label}.`;
|
|
500
|
-
if (band) return `${labelBand(band)} deals usually show up as ${s.label}.`;
|
|
501
|
-
return `${s.label} is the even-keeled starting book when we don't know the motion yet.`;
|
|
502
|
-
}
|
|
503
|
-
function labelMotion(m) {
|
|
504
|
-
return MOTION_FIT_CHOICES.find((c) => c.value === m)?.label ?? m;
|
|
505
|
-
}
|
|
506
|
-
function labelBand(b) {
|
|
507
|
-
return DEAL_BAND_CHOICES.find((c) => c.value === b)?.label ?? b;
|
|
508
|
-
}
|
|
509
|
-
function inferDemoScenario(signals) {
|
|
510
|
-
const text = signals.text?.trim() ?? "";
|
|
511
|
-
if (text) {
|
|
512
|
-
const hits = keywordHits(text);
|
|
513
|
-
let best;
|
|
514
|
-
let bestN = 0;
|
|
515
|
-
for (const id of NAMED_DEMO_SCENARIOS) {
|
|
516
|
-
const n = hits[id] ?? 0;
|
|
517
|
-
if (n > bestN) {
|
|
518
|
-
best = id;
|
|
519
|
-
bestN = n;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (best && bestN > 0) {
|
|
523
|
-
return { scenario: best, reason: reasonFor(best, signals, "keywords"), source: "heuristic" };
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const scenario = matrixPick(signals.salesMotion, signals.dealBand);
|
|
527
|
-
return { scenario, reason: reasonFor(scenario, signals, "matrix"), source: "heuristic" };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// src/ai/json-response.ts
|
|
531
|
-
function stripJsonFences(text) {
|
|
532
|
-
const trimmed = text.trim();
|
|
533
|
-
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
534
|
-
if (fenced) return fenced[1].trim();
|
|
535
|
-
return trimmed.replace(/```(?:json)?\s*/gi, "").replace(/```/g, "").trim();
|
|
536
|
-
}
|
|
537
|
-
function parseJsonArrayFromText(text) {
|
|
538
|
-
const cleaned = stripJsonFences(text);
|
|
539
|
-
const start = cleaned.indexOf("[");
|
|
540
|
-
if (start === -1) return null;
|
|
541
|
-
let depth = 0;
|
|
542
|
-
let end = -1;
|
|
543
|
-
for (let i = start; i < cleaned.length; i++) {
|
|
544
|
-
if (cleaned[i] === "[") depth++;
|
|
545
|
-
else if (cleaned[i] === "]") {
|
|
546
|
-
depth--;
|
|
547
|
-
if (depth === 0) {
|
|
548
|
-
end = i;
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
if (end === -1) return null;
|
|
554
|
-
try {
|
|
555
|
-
const parsed = JSON.parse(cleaned.slice(start, end + 1));
|
|
556
|
-
return Array.isArray(parsed) ? parsed : null;
|
|
557
|
-
} catch {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// src/demo/whimsy-smoke.ts
|
|
563
|
-
function assert(condition, message) {
|
|
564
|
-
if (!condition) {
|
|
565
|
-
console.error(`FAIL: ${message}`);
|
|
566
|
-
process.exit(1);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
function testNoRepeats() {
|
|
570
|
-
const rng = createSeededRandom(42);
|
|
571
|
-
const drawer = createFigureDrawer(rng);
|
|
572
|
-
const seen = /* @__PURE__ */ new Set();
|
|
573
|
-
let count = 0;
|
|
574
|
-
for (let fig = drawer.next(); fig !== null; fig = drawer.next()) {
|
|
575
|
-
const key = `${fig.first}|${fig.last}`;
|
|
576
|
-
assert(!seen.has(key), `duplicate figure drawn: ${fig.first} ${fig.last}`);
|
|
577
|
-
seen.add(key);
|
|
578
|
-
count++;
|
|
579
|
-
}
|
|
580
|
-
assert(count === WHIMSY_FIGURES.length, `expected ${WHIMSY_FIGURES.length} unique draws, got ${count}`);
|
|
581
|
-
}
|
|
582
|
-
function testDeterministicOrder() {
|
|
583
|
-
const draw = (seed) => {
|
|
584
|
-
const drawer = createFigureDrawer(createSeededRandom(seed));
|
|
585
|
-
return Array.from({ length: 5 }, () => {
|
|
586
|
-
const f = drawer.next();
|
|
587
|
-
return f ? `${f.first} ${f.last}` : "";
|
|
588
|
-
});
|
|
589
|
-
};
|
|
590
|
-
assert(
|
|
591
|
-
JSON.stringify(draw(7)) === JSON.stringify(draw(7)),
|
|
592
|
-
"same seed should produce identical rep name order"
|
|
593
|
-
);
|
|
594
|
-
assert(
|
|
595
|
-
JSON.stringify(draw(7)) !== JSON.stringify(draw(8)),
|
|
596
|
-
"different seeds should shuffle differently"
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
function testCategoryFilter() {
|
|
600
|
-
const rng = createSeededRandom(123);
|
|
601
|
-
const drawer = createFigureDrawer(rng, ["music"]);
|
|
602
|
-
for (let fig = drawer.next(); fig !== null; fig = drawer.next()) {
|
|
603
|
-
assert(fig.category === "music", `expected music, got ${fig.category} for ${fig.first} ${fig.last}`);
|
|
604
|
-
}
|
|
605
|
-
const musicCount = WHIMSY_FIGURES.filter((f) => f.category === "music").length;
|
|
606
|
-
assert(musicCount > 0, "music pool should not be empty");
|
|
607
|
-
}
|
|
608
|
-
function testCategoriesPresent() {
|
|
609
|
-
const categories = ["music", "sports", "film"];
|
|
610
|
-
for (const cat of categories) {
|
|
611
|
-
assert(
|
|
612
|
-
WHIMSY_FIGURES.some((f) => f.category === cat),
|
|
613
|
-
`pool missing category: ${cat}`
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
function testEmailSafeLastNames() {
|
|
618
|
-
for (const fig of WHIMSY_FIGURES) {
|
|
619
|
-
assert(!fig.last.includes(" "), `${fig.first} ${fig.last} has multi-word last name`);
|
|
620
|
-
assert(fig.first.length > 0 && fig.last.length > 0, "empty name part");
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
function testResolveScenarioInput() {
|
|
624
|
-
assert(resolveScenarioInput(void 0) === void 0, "blank -> random");
|
|
625
|
-
assert(resolveScenarioInput("hidden_crisis") === "hidden_crisis", "key passthrough");
|
|
626
|
-
assert(resolveScenarioInput("2") === "leaky_bucket", "2 -> second scenario");
|
|
627
|
-
assert(resolveScenarioInput("6") === "even_keel", "6 -> even_keel");
|
|
628
|
-
assert(resolveScenarioInput("7") === "compound_pain", "7 -> compound_pain");
|
|
629
|
-
assert(resolveScenarioInput("99") === null, "out of range -> invalid");
|
|
630
|
-
assert(resolveScenarioInput("not-a-scenario") === null, "garbage -> invalid");
|
|
631
|
-
}
|
|
632
|
-
function testInferDemoScenario() {
|
|
633
|
-
assert(NAMED_DEMO_SCENARIOS.length === 7, "seven named books of business");
|
|
634
|
-
assert(dealBandFromCycleDays(14) === "velocity", "short cycle \u2192 velocity");
|
|
635
|
-
assert(dealBandFromCycleDays(60) === "mid", "60d \u2192 mid");
|
|
636
|
-
assert(dealBandFromCycleDays(120) === "enterprise", "120d \u2192 enterprise");
|
|
637
|
-
assert(dealBandFromAverageDealSize("$12K") === "velocity", "12k \u2192 velocity");
|
|
638
|
-
assert(dealBandFromAverageDealSize("$80K ACV") === "mid", "80k \u2192 mid");
|
|
639
|
-
assert(dealBandFromAverageDealSize("$250K+") === "enterprise", "250k \u2192 enterprise");
|
|
640
|
-
const plg = inferDemoScenario({ salesMotion: "plg", dealBand: "velocity" });
|
|
641
|
-
assert(plg.scenario === "leaky_bucket", `plg+velocity \u2192 leaky_bucket, got ${plg.scenario}`);
|
|
642
|
-
const ent = inferDemoScenario({ salesMotion: "enterprise", dealBand: "enterprise" });
|
|
643
|
-
assert(ent.scenario === "hidden_crisis", `enterprise+enterprise \u2192 hidden_crisis, got ${ent.scenario}`);
|
|
644
|
-
const smb = inferDemoScenario({ salesMotion: "smb_velocity", dealBand: "velocity" });
|
|
645
|
-
assert(smb.scenario === "busy_bees", `smb velocity \u2192 busy_bees, got ${smb.scenario}`);
|
|
646
|
-
const mm = inferDemoScenario({ salesMotion: "mid_market", dealBand: "mid" });
|
|
647
|
-
assert(mm.scenario === "stale_pipeline", `mid-market \u2192 stale_pipeline, got ${mm.scenario}`);
|
|
648
|
-
const none = inferDemoScenario({});
|
|
649
|
-
assert(none.scenario === "even_keel", `empty signals \u2192 even_keel, got ${none.scenario}`);
|
|
650
|
-
const handoff = inferDemoScenario({
|
|
651
|
-
salesMotion: "enterprise",
|
|
652
|
-
dealBand: "enterprise",
|
|
653
|
-
text: "MQLs vanish at the marketing-sales handoff"
|
|
654
|
-
});
|
|
655
|
-
assert(handoff.scenario === "leaky_bucket", `handoff keywords beat enterprise matrix, got ${handoff.scenario}`);
|
|
656
|
-
const wolf = inferDemoScenario({ text: "every deal is single-threaded with one contact" });
|
|
657
|
-
assert(wolf.scenario === "lone_wolf", `single-thread text \u2192 lone_wolf, got ${wolf.scenario}`);
|
|
658
|
-
const fromProfile = inferDemoScenario(
|
|
659
|
-
signalsFromProfile({
|
|
660
|
-
schema_version: 1,
|
|
661
|
-
company_name: "Acme",
|
|
662
|
-
industry: "B2B SaaS",
|
|
663
|
-
product_description: "Workflow software",
|
|
664
|
-
target_customer: "Enterprise IT",
|
|
665
|
-
sales_motion: "enterprise",
|
|
666
|
-
average_deal_size: "$200K",
|
|
667
|
-
sales_cycle_days: 120,
|
|
668
|
-
created_at: "",
|
|
669
|
-
updated_at: ""
|
|
670
|
-
})
|
|
671
|
-
);
|
|
672
|
-
assert(fromProfile.scenario === "hidden_crisis", `enterprise profile \u2192 hidden_crisis, got ${fromProfile.scenario}`);
|
|
673
|
-
}
|
|
674
|
-
function testParseJsonArrayFromText() {
|
|
675
|
-
assert(parseJsonArrayFromText("[]")?.length === 0, "empty array");
|
|
676
|
-
const fenced = parseJsonArrayFromText('Here you go:\n```json\n[{"a":1}]\n```');
|
|
677
|
-
assert(Array.isArray(fenced) && fenced[0].a === 1, "fenced array");
|
|
678
|
-
assert(parseJsonArrayFromText("not json") === null, "garbage -> null");
|
|
679
|
-
const balanced = parseJsonArrayFromText('Note:\n[{"text":"$727K pipeline at risk"}]');
|
|
680
|
-
assert(Array.isArray(balanced) && balanced.length === 1, "preamble + array");
|
|
681
|
-
assert(stripJsonFences("```json\n[]\n```") === "[]", "strip fences");
|
|
682
|
-
}
|
|
683
|
-
testNoRepeats();
|
|
684
|
-
testDeterministicOrder();
|
|
685
|
-
testCategoryFilter();
|
|
686
|
-
testCategoriesPresent();
|
|
687
|
-
testEmailSafeLastNames();
|
|
688
|
-
testResolveScenarioInput();
|
|
689
|
-
testInferDemoScenario();
|
|
690
|
-
testParseJsonArrayFromText();
|
|
691
|
-
console.log(`whimsy smoke passed (${WHIMSY_FIGURES.length} figures)`);
|
|
692
|
-
//# sourceMappingURL=whimsy-smoke.js.map
|