@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,3416 +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/baselines/metrics-benchmarks.ts
|
|
9
|
-
function motionBenchmarkLabel(motion) {
|
|
10
|
-
return MOTION_LABELS[motion ?? "mid_market"];
|
|
11
|
-
}
|
|
12
|
-
var METRICS_BENCHMARKS, MOTION_LABELS;
|
|
13
|
-
var init_metrics_benchmarks = __esm({
|
|
14
|
-
"src/baselines/metrics-benchmarks.ts"() {
|
|
15
|
-
"use strict";
|
|
16
|
-
METRICS_BENCHMARKS = {
|
|
17
|
-
plg: {
|
|
18
|
-
nrr: { green: 110, yellow: 100 },
|
|
19
|
-
grr: { green: 85, yellow: 75 },
|
|
20
|
-
win_rate: { green: 25, yellow: 15 },
|
|
21
|
-
pipeline_coverage: { green: 4, yellow: 2.5 },
|
|
22
|
-
magic_number: { green: 1, yellow: 0.75 },
|
|
23
|
-
payback_months: { green: 12, yellow: 18 }
|
|
24
|
-
},
|
|
25
|
-
smb_velocity: {
|
|
26
|
-
nrr: { green: 105, yellow: 95 },
|
|
27
|
-
grr: { green: 88, yellow: 78 },
|
|
28
|
-
win_rate: { green: 22, yellow: 12 },
|
|
29
|
-
pipeline_coverage: { green: 3.5, yellow: 2 },
|
|
30
|
-
magic_number: { green: 0.9, yellow: 0.6 },
|
|
31
|
-
payback_months: { green: 14, yellow: 20 }
|
|
32
|
-
},
|
|
33
|
-
mid_market: {
|
|
34
|
-
nrr: { green: 100, yellow: 90 },
|
|
35
|
-
grr: { green: 90, yellow: 80 },
|
|
36
|
-
win_rate: { green: 20, yellow: 12 },
|
|
37
|
-
pipeline_coverage: { green: 3, yellow: 2 },
|
|
38
|
-
magic_number: { green: 0.75, yellow: 0.5 },
|
|
39
|
-
payback_months: { green: 16, yellow: 22 }
|
|
40
|
-
},
|
|
41
|
-
enterprise: {
|
|
42
|
-
nrr: { green: 95, yellow: 85 },
|
|
43
|
-
grr: { green: 92, yellow: 82 },
|
|
44
|
-
win_rate: { green: 15, yellow: 8 },
|
|
45
|
-
pipeline_coverage: { green: 2.5, yellow: 1.5 },
|
|
46
|
-
magic_number: { green: 0.6, yellow: 0.4 },
|
|
47
|
-
payback_months: { green: 18, yellow: 24 }
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
MOTION_LABELS = {
|
|
51
|
-
plg: "PLG",
|
|
52
|
-
smb_velocity: "SMB Velocity",
|
|
53
|
-
mid_market: "Mid-Market",
|
|
54
|
-
enterprise: "Enterprise"
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// src/data/metric-definitions.ts
|
|
60
|
-
function pctBand(metric, motion) {
|
|
61
|
-
const m = motion ?? "mid_market";
|
|
62
|
-
const t = METRICS_BENCHMARKS[m][metric];
|
|
63
|
-
return `${motionBenchmarkLabel(m)} green \u2265${t.green}${metric === "pipeline_coverage" ? "x" : "%"}, yellow \u2265${t.yellow}${metric === "pipeline_coverage" ? "x" : "%"}`;
|
|
64
|
-
}
|
|
65
|
-
function monthsBand(motion) {
|
|
66
|
-
const m = motion ?? "mid_market";
|
|
67
|
-
const t = METRICS_BENCHMARKS[m].payback_months;
|
|
68
|
-
return `${motionBenchmarkLabel(m)} green \u2264${t.green}mo, yellow \u2264${t.yellow}mo`;
|
|
69
|
-
}
|
|
70
|
-
function magicBand(motion) {
|
|
71
|
-
const m = motion ?? "mid_market";
|
|
72
|
-
const t = METRICS_BENCHMARKS[m].magic_number;
|
|
73
|
-
return `${motionBenchmarkLabel(m)} green \u2265${t.green}, yellow \u2265${t.yellow}`;
|
|
74
|
-
}
|
|
75
|
-
function getMetricExplainer(id) {
|
|
76
|
-
return BY_ID.get(id);
|
|
77
|
-
}
|
|
78
|
-
function resolveMetricId(query) {
|
|
79
|
-
const q = query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
80
|
-
if (!q) return void 0;
|
|
81
|
-
if (BY_ID.has(q)) return q;
|
|
82
|
-
const direct = ALIAS_INDEX.get(q);
|
|
83
|
-
if (direct) return direct;
|
|
84
|
-
const norm = q.replace(/[-\s]+/g, "_");
|
|
85
|
-
if (BY_ID.has(norm)) return norm;
|
|
86
|
-
return ALIAS_INDEX.get(norm);
|
|
87
|
-
}
|
|
88
|
-
function getCoreDeckExplainers() {
|
|
89
|
-
return CORE_DECK_IDS.map((id) => BY_ID.get(id)).filter(Boolean);
|
|
90
|
-
}
|
|
91
|
-
var VITALS, SAAS, METRIC_DEFINITIONS, CORE_DECK_IDS, BY_ID, ALIAS_INDEX, VITAL_IDS, SAAS_METRIC_IDS;
|
|
92
|
-
var init_metric_definitions = __esm({
|
|
93
|
-
"src/data/metric-definitions.ts"() {
|
|
94
|
-
"use strict";
|
|
95
|
-
init_metrics_benchmarks();
|
|
96
|
-
VITALS = [
|
|
97
|
-
{
|
|
98
|
-
id: "freshness",
|
|
99
|
-
kind: "vital",
|
|
100
|
-
label: "Freshness",
|
|
101
|
-
group: "Vital Signs",
|
|
102
|
-
tagline: "Does the CRM report which records are still active?",
|
|
103
|
-
how_computed: "NTRP computes a weighted average of people, organizations, and opportunities with recent activity. Open opportunities must also not be past-due. Default windows: people and organizations 90 days, opportunities 30 days. Weights are 35 / 30 / 35.",
|
|
104
|
-
formula_lines: [
|
|
105
|
-
"freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
|
|
106
|
-
"people/orgs fresh if activity within 90d",
|
|
107
|
-
"opps fresh if activity within 30d AND not past-due"
|
|
108
|
-
],
|
|
109
|
-
meaning: "Board question: how much of this pipeline is real versus fiction? Dollar value equals the sum of amount on stale opportunities: pipeline at risk.",
|
|
110
|
-
expert_read: "Cut by owner and by stage first. Freshness reds concentrate on people or process. They rarely spread evenly. In a long-cycle enterprise motion, 30 quiet days can be normal cadence. In a velocity motion, 30 quiet days is a dead deal. A sudden cliff usually means a broken integration or a departed rep. It is not gradual decay. Check this false positive: bulk-imported records that nobody has touched yet.",
|
|
111
|
-
deepdive: [
|
|
112
|
-
"Status bands: green 80 or more, yellow 60 or more, red below 60. Motion presets can change the windows.",
|
|
113
|
-
'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
|
|
114
|
-
"Layer 1 of the gating stack. A red score here limits trust in later layers.",
|
|
115
|
-
"Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when the score is below 60.",
|
|
116
|
-
"Levers: stale-deal alert at N quiet days. Weekly hygiene scrub. Enrichment refresh on quiet records. Signal-triggered reactivation for paid dormant accounts."
|
|
117
|
-
],
|
|
118
|
-
visual: {
|
|
119
|
-
kind: "bars",
|
|
120
|
-
caption: "Example component mix. Higher bars are fresher.",
|
|
121
|
-
bars: [
|
|
122
|
-
{ label: "People", value: 72, tone: "yellow" },
|
|
123
|
-
{ label: "Organizations", value: 81, tone: "green" },
|
|
124
|
-
{ label: "Opportunities", value: 44, tone: "red" }
|
|
125
|
-
]
|
|
126
|
-
},
|
|
127
|
-
play_id: "clean-dead-pipeline",
|
|
128
|
-
dollar_label: "pipeline at risk",
|
|
129
|
-
audience: {
|
|
130
|
-
board: "Freshness answers whether the pipeline number is real. Low freshness means forecast risk. Stale deals inflate coverage and hide the true gap.",
|
|
131
|
-
ops: "Score equals weighted recency across people, organizations, and opportunities. Cut by owner and stage. Set a stale-deal alert and a weekly scrub. Play: Clean Dead Pipeline."
|
|
132
|
-
},
|
|
133
|
-
aliases: ["data freshness", "stale", "zombie deals", "crm freshness"]
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
id: "flow_rate",
|
|
137
|
-
kind: "vital",
|
|
138
|
-
label: "Flow Rate",
|
|
139
|
-
group: "Vital Signs",
|
|
140
|
-
tagline: "How fast do deals move, and where do they stop?",
|
|
141
|
-
how_computed: "NTRP sets a base score from average open-deal age versus max_days. It then applies a penalty of up to 20 for the share of stuck deals. A deal is stuck when it has no update beyond stuck_days, or a past-due close. Status uses average open age, not the score alone.",
|
|
142
|
-
formula_lines: [
|
|
143
|
-
"base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
|
|
144
|
-
"score = base \u2212 stuckSharePenalty (\u226420)",
|
|
145
|
-
"stuck = no update > stuck_days OR past-due close"
|
|
146
|
-
],
|
|
147
|
-
meaning: "Board question: is next quarter slipping because deals are stuck? Dollar value equals the amount stuck in pipeline.",
|
|
148
|
-
expert_read: "Cut by stage age, not only deal age. Find the stage where deals stop. That is usually one stage. Compare stuck-deal age to this company's own median cycle. Do not use a generic norm. Stuck deals plus past-due close dates signal optimistic forecasting. That is a credibility problem before it is a revenue problem.",
|
|
149
|
-
deepdive: [
|
|
150
|
-
"Status uses average open age. Green is 45 days or less. Yellow is 90 days or less. Else red. Defaults: max_days 120, stuck_days 60.",
|
|
151
|
-
'Dollar translation: sum of amount on stuck deals \u2192 "stuck in pipeline".',
|
|
152
|
-
"Layer 2 of the gating stack, with Drop Rate.",
|
|
153
|
-
"Trigger play: Unstick the Pipeline (unstick-pipeline) when the score is weak.",
|
|
154
|
-
"Levers: stage-age report. Past-due close cleanup. Progression plans on stuck deals. Forecast hygiene on optimistic close dates."
|
|
155
|
-
],
|
|
156
|
-
visual: {
|
|
157
|
-
kind: "funnel",
|
|
158
|
-
caption: "Example stage ages. Find the stage where deals stop.",
|
|
159
|
-
funnel: [
|
|
160
|
-
{ label: "Discovery", widthPct: 100 },
|
|
161
|
-
{ label: "Qualify", widthPct: 78 },
|
|
162
|
-
{ label: "Propose", widthPct: 55 },
|
|
163
|
-
{ label: "Negotiate", widthPct: 22 },
|
|
164
|
-
{ label: "Closed", widthPct: 12 }
|
|
165
|
-
]
|
|
166
|
-
},
|
|
167
|
-
play_id: "unstick-pipeline",
|
|
168
|
-
dollar_label: "stuck in pipeline",
|
|
169
|
-
audience: {
|
|
170
|
-
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.",
|
|
171
|
-
ops: "Find the stage with collapsing advancement and age. Clear past-due closes. Write progression plans on stuck deals. Play: Unstick the Pipeline."
|
|
172
|
-
},
|
|
173
|
-
aliases: ["flow rate", "deal velocity", "stuck deals", "stuck pipeline"]
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
id: "drop_rate",
|
|
177
|
-
kind: "vital",
|
|
178
|
-
label: "Drop Rate",
|
|
179
|
-
group: "Vital Signs",
|
|
180
|
-
tagline: "Where do leads drop between systems?",
|
|
181
|
-
how_computed: "NTRP blends cross-system retention and opportunity retention. Cross-system retention is marketing people also present in sales. Opportunity retention is open opportunities that are not abandoned. Default weights: cross-system 60 percent, opportunity retention 40 percent. Abandoned means open opportunities with no activity in 30 days.",
|
|
182
|
-
formula_lines: [
|
|
183
|
-
"score = crossSystemRetention\xD70.6 + oppRetention\xD70.4",
|
|
184
|
-
"cross-system = marketing people also in sales CRM",
|
|
185
|
-
"abandoned = open opps with no activity in 30d"
|
|
186
|
-
],
|
|
187
|
-
meaning: "Board question: how much pipeline did we pay for and never work? Dollar value = droppedCount \xD7 conversionRate \xD7 avgDealSize \u2014 est. lost at handoff.",
|
|
188
|
-
expert_read: "This is almost always a systems failure. Causes include routing rules, unassigned territories, dead rep queues, or a sync gap between marketing and CRM. It is 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.",
|
|
189
|
-
deepdive: [
|
|
190
|
-
"Status bands: green 80 or more, yellow 60 or more, red below 60.",
|
|
191
|
-
'Dollar translation: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
|
|
192
|
-
"Layer 2 of the gating stack, with Flow Rate.",
|
|
193
|
-
"Trigger play: Fix the Handoff Gap (fix-handoff-gap) when drop is high.",
|
|
194
|
-
"Levers: source-level handoff audit. Routing and sync repair. Time-to-first-touch SLA. Weekly marketing-only-leads report."
|
|
195
|
-
],
|
|
196
|
-
visual: {
|
|
197
|
-
kind: "funnel",
|
|
198
|
-
caption: "Example handoff funnel. The leak is usually one or two sources.",
|
|
199
|
-
funnel: [
|
|
200
|
-
{ label: "Marketing leads", widthPct: 100 },
|
|
201
|
-
{ label: "In sales CRM", widthPct: 62 },
|
|
202
|
-
{ label: "Assigned + touched", widthPct: 41 },
|
|
203
|
-
{ label: "Active opportunities", widthPct: 28 }
|
|
204
|
-
]
|
|
205
|
-
},
|
|
206
|
-
play_id: "fix-handoff-gap",
|
|
207
|
-
dollar_label: "est. lost at handoff",
|
|
208
|
-
audience: {
|
|
209
|
-
board: "Drop Rate prices the handoff leak. Budget is already spent on leads that never reach a working rep. This is usually a systems failure, not a people failure.",
|
|
210
|
-
ops: "Audit by source. Fix routing, sync, and dead queues. Set a time-to-first-touch measure. Play: Fix the Handoff Gap."
|
|
211
|
-
},
|
|
212
|
-
aliases: ["drop rate", "handoff", "handoff gap", "lead leak", "marketing sales handoff"]
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
id: "signal_to_noise",
|
|
216
|
-
kind: "vital",
|
|
217
|
-
label: "Signal:Noise",
|
|
218
|
-
group: "Vital Signs",
|
|
219
|
-
tagline: "How much activity is aimed at deals that can still close?",
|
|
220
|
-
how_computed: "Over a 90-day lookback, NTRP computes (signal activities / all activities) \xD7 100. Signal is activity linked to an open opportunity, a pipeline person, or a pipeline organization.",
|
|
221
|
-
formula_lines: [
|
|
222
|
-
"score = (signalCount / activityCount) \xD7 100",
|
|
223
|
-
"signal = linked to open opp / pipeline person / pipeline org",
|
|
224
|
-
"lookback = trailing 90 days"
|
|
225
|
-
],
|
|
226
|
-
meaning: "Board question: are we burning capacity on dead accounts? Dollar value = noiseCount \xD7 hours_per_activity \xD7 rep_hourly_cost \u2014 misdirected effort.",
|
|
227
|
-
expert_read: "Cut by rep and by account status. Noise usually means reps work dead accounts they already know. 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. That is often a hygiene artifact.",
|
|
228
|
-
deepdive: [
|
|
229
|
-
"Status bands: green 65 or more, yellow 40 or more, red below 40.",
|
|
230
|
-
"Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost).",
|
|
231
|
-
"Layer 3 of the gating stack. Trust Freshness, Flow Rate, and Drop Rate before you read activity efficiency.",
|
|
232
|
-
"Trigger play: Retarget Misdirected Effort (retarget-effort) when the score is low.",
|
|
233
|
-
"Levers: refresh account lists. Signal-based targeting. Stop logging against closed or unlinked records. Coverage-model redesign."
|
|
234
|
-
],
|
|
235
|
-
visual: {
|
|
236
|
-
kind: "split",
|
|
237
|
-
caption: "Example activity mix. Signal versus noise.",
|
|
238
|
-
bars: [
|
|
239
|
-
{ label: "Signal", value: 38, tone: "green" },
|
|
240
|
-
{ label: "Noise", value: 62, tone: "red" }
|
|
241
|
-
]
|
|
242
|
-
},
|
|
243
|
-
play_id: "retarget-effort",
|
|
244
|
-
dollar_label: "misdirected effort",
|
|
245
|
-
audience: {
|
|
246
|
-
board: "Signal:Noise prices wasted capacity. Persistent noise is usually a coverage-model problem, not a coaching problem. Reps work dead accounts they already know.",
|
|
247
|
-
ops: "Score equals the percent of activities linked to live pipeline. Cut by rep and account status. Refresh targeting. Play: Retarget Misdirected Effort."
|
|
248
|
-
},
|
|
249
|
-
aliases: ["signal to noise", "signal:noise", "s/n", "activity efficiency", "noise"]
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
id: "thread_depth",
|
|
253
|
-
kind: "vital",
|
|
254
|
-
label: "Thread Depth",
|
|
255
|
-
group: "Vital Signs",
|
|
256
|
-
tagline: "How fragile is the pipeline if one champion stops?",
|
|
257
|
-
how_computed: "Percent of open deals with at least multi_thread_threshold distinct people active in the last 90 days. The default threshold is 2. People include opportunity-direct contacts and same-organization activity.",
|
|
258
|
-
formula_lines: [
|
|
259
|
-
"score = % open deals with \u22652 active people (90d)",
|
|
260
|
-
"people counted via opp contacts + same-org activity",
|
|
261
|
-
"threshold configurable (default 2)"
|
|
262
|
-
],
|
|
263
|
-
meaning: "Board question: how much revenue is lost if one contact changes jobs? Dollar value equals the sum of amount on single-threaded deals.",
|
|
264
|
-
expert_read: "Weight by deal size. 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. There is no second contact.",
|
|
265
|
-
deepdive: [
|
|
266
|
-
"Status bands: green 65 or more, yellow 40 or more, red below 40.",
|
|
267
|
-
'Dollar translation: sum of amount on single-threaded deals \u2192 "single-threaded".',
|
|
268
|
-
"Layer 4 of the gating stack. Read this last, after the upstream vital signs.",
|
|
269
|
-
"Trigger play: Multi-Thread Your Deals (multi-thread-deals) when depth is low.",
|
|
270
|
-
"Levers: buying-committee map. Warm internal referral first. CRM contact roles. Mid-stage single-thread alerts. Champion job-change signals."
|
|
271
|
-
],
|
|
272
|
-
visual: {
|
|
273
|
-
kind: "bars",
|
|
274
|
-
caption: "Example: multi-threaded versus single-threaded open deals",
|
|
275
|
-
bars: [
|
|
276
|
-
{ label: "Multi-threaded", value: 34, tone: "green" },
|
|
277
|
-
{ label: "Single-threaded", value: 66, tone: "red" }
|
|
278
|
-
]
|
|
279
|
-
},
|
|
280
|
-
play_id: "multi-thread-deals",
|
|
281
|
-
dollar_label: "single-threaded",
|
|
282
|
-
audience: {
|
|
283
|
-
board: "Thread Depth is resilience risk. One single-threaded mega-deal outweighs ten small ones. Late-cycle single-threading is a leading indicator of slipped quarters.",
|
|
284
|
-
ops: "Score equals the percent of open deals with 2 or more active contacts in 90 days. Map the buying committee. Alert on mid-stage singles. Play: Multi-Thread Your Deals."
|
|
285
|
-
},
|
|
286
|
-
aliases: ["thread depth", "multithreading", "multi-thread", "single-threaded", "buying committee"]
|
|
287
|
-
}
|
|
288
|
-
];
|
|
289
|
-
SAAS = [
|
|
290
|
-
// —— Revenue ——
|
|
291
|
-
{
|
|
292
|
-
id: "arr",
|
|
293
|
-
kind: "saas",
|
|
294
|
-
label: "ARR",
|
|
295
|
-
group: "Revenue",
|
|
296
|
-
tagline: "How big is the revenue engine, and from where?",
|
|
297
|
-
how_computed: "Sum of amount on closed-won opportunities in the dataset. This is pipeline-inferred ARR when a pure subscription ledger is not available.",
|
|
298
|
-
formula_lines: [
|
|
299
|
-
"ARR \u2248 \u03A3 amount on closed-won opportunities",
|
|
300
|
-
"New + Expansion = growth \xB7 Churned + Contraction = leakage"
|
|
301
|
-
],
|
|
302
|
-
meaning: "Board question: how fast are we growing, and from where? Always decompose growth into new versus expansion. The mix is the story.",
|
|
303
|
-
expert_read: "Always decompose growth into new versus expansion. The mix is the story. Prefer this company's own trailing history over any external prior. A number below its reliability_gate is a hypothesis, not a fact.",
|
|
304
|
-
deepdive: [
|
|
305
|
-
"Companion metrics: New ARR, Expansion ARR, Churned ARR, Contraction ARR.",
|
|
306
|
-
"Estimation method may be ledger, pipeline_inferred, or snapshot. Read confidence and reliability_gate.",
|
|
307
|
-
"Cross-check with Freshness before you trust ARR growth stories that rest on zombie deals."
|
|
308
|
-
],
|
|
309
|
-
visual: {
|
|
310
|
-
kind: "waterfall",
|
|
311
|
-
caption: "Example ARR walk. Growth versus leakage.",
|
|
312
|
-
waterfall: [
|
|
313
|
-
{ label: "Starting", delta: 100, cumulative: 100 },
|
|
314
|
-
{ label: "+ New", delta: 18, cumulative: 118 },
|
|
315
|
-
{ label: "+ Expansion", delta: 12, cumulative: 130 },
|
|
316
|
-
{ label: "\u2212 Contraction", delta: -4, cumulative: 126 },
|
|
317
|
-
{ label: "\u2212 Churned", delta: -8, cumulative: 118 }
|
|
318
|
-
]
|
|
319
|
-
},
|
|
320
|
-
audience: {
|
|
321
|
-
board: "ARR is the size of the engine. The story is the mix: new versus expansion growth, and how much leakage (churn plus contraction) reduced it.",
|
|
322
|
-
ops: "NTRP computes \u03A3 closed-won amounts. This is pipeline-inferred when there is no ledger. Decompose into new, expansion, churned, and contraction before you brief anyone."
|
|
323
|
-
},
|
|
324
|
-
aliases: ["annual recurring revenue", "revenue"]
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
id: "new_arr",
|
|
328
|
-
kind: "saas",
|
|
329
|
-
label: "New ARR",
|
|
330
|
-
group: "Revenue",
|
|
331
|
-
tagline: "How much growth came from new customers?",
|
|
332
|
-
how_computed: "Closed-won tagged New Business. If tags are missing, first closed-won deal per organization.",
|
|
333
|
-
formula_lines: [
|
|
334
|
-
"New ARR = \u03A3 closed-won tagged New Business",
|
|
335
|
-
"fallback: first closed-won deal per organization"
|
|
336
|
-
],
|
|
337
|
-
meaning: "Board question: is growth coming from the top of funnel, or from the installed base?",
|
|
338
|
-
expert_read: "Rising New ARR with falling Expansion usually means land-and-expand is underpowered. The lever is packaging or CS motion, not only sales capacity.",
|
|
339
|
-
deepdive: [
|
|
340
|
-
"Pair with Expansion ARR. The mix shows which motion carries growth.",
|
|
341
|
-
"Tag quality matters. Untagged deals use the first-deal-per-organization heuristic."
|
|
342
|
-
],
|
|
343
|
-
visual: {
|
|
344
|
-
kind: "bars",
|
|
345
|
-
caption: "Example growth mix",
|
|
346
|
-
bars: [
|
|
347
|
-
{ label: "New ARR", value: 60, tone: "accent" },
|
|
348
|
-
{ label: "Expansion ARR", value: 40, tone: "green" }
|
|
349
|
-
]
|
|
350
|
-
},
|
|
351
|
-
audience: {
|
|
352
|
-
board: "New ARR is net-new logos. Read it next to Expansion ARR. A healthy mix beats a one-sided engine.",
|
|
353
|
-
ops: "Prefer CRM New Business tags. Otherwise use the first closed-won per organization. Watch tag hygiene."
|
|
354
|
-
},
|
|
355
|
-
aliases: ["new business arr", "new logo arr"]
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
id: "expansion_arr",
|
|
359
|
-
kind: "saas",
|
|
360
|
-
label: "Expansion ARR",
|
|
361
|
-
group: "Revenue",
|
|
362
|
-
tagline: "How much extra revenue comes from existing customers?",
|
|
363
|
-
how_computed: "Closed-won tagged Expansion. If tags are missing, later closed-won deals per organization after the first win.",
|
|
364
|
-
formula_lines: [
|
|
365
|
-
"Expansion ARR = \u03A3 closed-won tagged Expansion",
|
|
366
|
-
"fallback: later closed-won deals per organization"
|
|
367
|
-
],
|
|
368
|
-
meaning: "Board question: is the installed base compounding?",
|
|
369
|
-
expert_read: "Expansion is the cheapest growth. Weak Expansion with strong New ARR is a land-only motion. The usual lever is packaging, CS capacity, or product attach.",
|
|
370
|
-
deepdive: [
|
|
371
|
-
"Feeds NRR as the upside term.",
|
|
372
|
-
"Compare to Contraction. Net expansion equals expansion minus contraction."
|
|
373
|
-
],
|
|
374
|
-
visual: {
|
|
375
|
-
kind: "bars",
|
|
376
|
-
caption: "Example: expansion versus contraction",
|
|
377
|
-
bars: [
|
|
378
|
-
{ label: "Expansion", value: 70, tone: "green" },
|
|
379
|
-
{ label: "Contraction", value: 25, tone: "yellow" }
|
|
380
|
-
]
|
|
381
|
-
},
|
|
382
|
-
audience: {
|
|
383
|
-
board: "Expansion ARR is installed-base compounding. It is the cheapest growth when it works.",
|
|
384
|
-
ops: "Tagged Expansion or subsequent wins per organization. Pair with Contraction before you celebrate net expansion."
|
|
385
|
-
},
|
|
386
|
-
aliases: ["upsell", "upsell arr", "cross-sell"]
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
id: "churned_arr",
|
|
390
|
-
kind: "saas",
|
|
391
|
-
label: "Churned ARR",
|
|
392
|
-
group: "Revenue",
|
|
393
|
-
tagline: "How much revenue left?",
|
|
394
|
-
how_computed: "NTRP sums historical closed-won amounts for organizations that have historical wins, no win in the trailing 12 months, and no active open opportunity.",
|
|
395
|
-
formula_lines: [
|
|
396
|
-
"Churned ARR = \u03A3 historical wins for orgs with",
|
|
397
|
-
" no win in trailing 12mo AND no active open opp"
|
|
398
|
-
],
|
|
399
|
-
meaning: "Board question: how leaky is the bucket before expansion hides it? With Contraction, this is the GRR downside.",
|
|
400
|
-
expert_read: "Pipeline-inferred churn is a hypothesis. Confirm with billing status when that data is available. A spike often clusters in one segment or cohort.",
|
|
401
|
-
deepdive: [
|
|
402
|
-
"Feeds GRR and NRR as the churn term.",
|
|
403
|
-
"Cut by segment or motion before you treat it as a company-wide PMF problem."
|
|
404
|
-
],
|
|
405
|
-
visual: {
|
|
406
|
-
kind: "bars",
|
|
407
|
-
caption: "Example leakage mix",
|
|
408
|
-
bars: [
|
|
409
|
-
{ label: "Churned", value: 55, tone: "red" },
|
|
410
|
-
{ label: "Contraction", value: 30, tone: "yellow" }
|
|
411
|
-
]
|
|
412
|
-
},
|
|
413
|
-
audience: {
|
|
414
|
-
board: "Churned ARR is full logo loss. With Contraction it sets the floor of the business (GRR).",
|
|
415
|
-
ops: "Heuristic: historical winners with no trailing-12 win and no open opportunity. Validate against billing when you can."
|
|
416
|
-
},
|
|
417
|
-
aliases: ["churn", "logo churn", "churned revenue"]
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
id: "contraction_arr",
|
|
421
|
-
kind: "saas",
|
|
422
|
-
label: "Contraction ARR",
|
|
423
|
-
group: "Revenue",
|
|
424
|
-
tagline: "How much less did existing customers buy?",
|
|
425
|
-
how_computed: "Organizations with 2 or more wins where the latest amount is less than the prior. NTRP sums the negative deltas.",
|
|
426
|
-
formula_lines: [
|
|
427
|
-
"Contraction = \u03A3 (prior \u2212 latest) where latest < prior",
|
|
428
|
-
"requires \u22652 closed-won deals per organization"
|
|
429
|
-
],
|
|
430
|
-
meaning: "Board question: are we shrinking inside the base while logos stay?",
|
|
431
|
-
expert_read: "Contraction is often packaging, seat-reduction, or downgrade. The owner is different from logo churn. The same NRR can be a churn problem or a no-expansion problem.",
|
|
432
|
-
deepdive: [
|
|
433
|
-
"Feeds GRR and NRR.",
|
|
434
|
-
"This metric needs multi-deal history per organization. Thin history understates contraction."
|
|
435
|
-
],
|
|
436
|
-
visual: {
|
|
437
|
-
kind: "waterfall",
|
|
438
|
-
caption: "Example: contraction reduces the base",
|
|
439
|
-
waterfall: [
|
|
440
|
-
{ label: "Prior", delta: 100, cumulative: 100 },
|
|
441
|
-
{ label: "Latest", delta: -18, cumulative: 82 }
|
|
442
|
-
]
|
|
443
|
-
},
|
|
444
|
-
audience: {
|
|
445
|
-
board: "Contraction is silent shrink inside retained logos. It is often packaging or seats, not a cancelled contract.",
|
|
446
|
-
ops: "Requires 2 or more wins per organization with a down-round. Pair with Expansion for net expansion."
|
|
447
|
-
},
|
|
448
|
-
aliases: ["downgrade", "seat reduction", "contraction"]
|
|
449
|
-
},
|
|
450
|
-
// —— Retention ——
|
|
451
|
-
{
|
|
452
|
-
id: "nrr",
|
|
453
|
-
kind: "saas",
|
|
454
|
-
label: "Net Revenue Retention",
|
|
455
|
-
group: "Retention",
|
|
456
|
-
tagline: "Would this business grow if sales stopped selling?",
|
|
457
|
-
how_computed: "startingArr = ARR + Churned + Contraction \u2212 Expansion. NRR = ((starting \u2212 Churned \u2212 Contraction + Expansion) / starting) \xD7 100.",
|
|
458
|
-
formula_lines: [
|
|
459
|
-
"starting = ARR + churned + contraction \u2212 expansion",
|
|
460
|
-
"NRR = (starting \u2212 churned \u2212 contraction + expansion) / starting \xD7 100",
|
|
461
|
-
"NRR = 100% + expansion% \u2212 contraction% \u2212 churn%"
|
|
462
|
-
],
|
|
463
|
-
meaning: "Board question: would this business grow if sales stopped selling? A value above 100% means growth from existing customers.",
|
|
464
|
-
expert_read: "Decompose before you judge. The same 95% can be a churn problem (product/PMF) or a no-expansion problem (packaging/motion). Those have different owners. Priors by segment: about 97% SMB, about 108% mid-market, about 118% enterprise medians. 110% or more is a strong signal at any stage.",
|
|
465
|
-
deepdive: [
|
|
466
|
-
"Always show the waterfall: +expansion \u2212contraction \u2212churn.",
|
|
467
|
-
"GRR is the floor. NRR adds expansion on top.",
|
|
468
|
-
"On pipeline-only data, treat NRR as a hypothesis. Check confidence and reliability_gate."
|
|
469
|
-
],
|
|
470
|
-
visual: {
|
|
471
|
-
kind: "waterfall",
|
|
472
|
-
caption: "Example NRR walk from 100%",
|
|
473
|
-
waterfall: [
|
|
474
|
-
{ label: "100%", delta: 100, cumulative: 100 },
|
|
475
|
-
{ label: "+ Expansion", delta: 14, cumulative: 114 },
|
|
476
|
-
{ label: "\u2212 Contraction", delta: -4, cumulative: 110 },
|
|
477
|
-
{ label: "\u2212 Churn", delta: -6, cumulative: 104 }
|
|
478
|
-
]
|
|
479
|
-
},
|
|
480
|
-
audience: {
|
|
481
|
-
board: "NRR above 100% means the base compounds without new logos. Decompose before you judge. The same number can have different owners.",
|
|
482
|
-
ops: "NRR = 100 + expansion \u2212 contraction \u2212 churn. Motion benchmarks calibrate green and yellow bands. Check reliability_gate on pipeline-inferred data."
|
|
483
|
-
},
|
|
484
|
-
aliases: ["net revenue retention", "net retention", "ndr"],
|
|
485
|
-
benchmarkHint: (motion) => pctBand("nrr", motion)
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
id: "grr",
|
|
489
|
-
kind: "saas",
|
|
490
|
-
label: "Gross Revenue Retention",
|
|
491
|
-
group: "Retention",
|
|
492
|
-
tagline: "How leaky is the bucket before expansion hides it?",
|
|
493
|
-
how_computed: "GRR = ((startingArr \u2212 Churned \u2212 Contraction) / startingArr) \xD7 100. Expansion is excluded on purpose.",
|
|
494
|
-
formula_lines: [
|
|
495
|
-
"starting = ARR + churned + contraction \u2212 expansion",
|
|
496
|
-
"GRR = (starting \u2212 churned \u2212 contraction) / starting \xD7 100"
|
|
497
|
-
],
|
|
498
|
-
meaning: "Board question: how leaky is the bucket before expansion hides it? Prior: above 90% is healthy. Above 95% is strong for enterprise.",
|
|
499
|
-
expert_read: "GRR is the honesty metric. Expansion can make NRR look fine while GRR erodes. Always read both.",
|
|
500
|
-
deepdive: [
|
|
501
|
-
"GRR never includes Expansion. That is the point.",
|
|
502
|
-
"Owners: product and CS for churn. Packaging for contraction."
|
|
503
|
-
],
|
|
504
|
-
visual: {
|
|
505
|
-
kind: "gauge",
|
|
506
|
-
caption: "Example GRR. This is the floor of the business.",
|
|
507
|
-
gauge: 92
|
|
508
|
-
},
|
|
509
|
-
audience: {
|
|
510
|
-
board: "GRR is the floor: churn plus contraction only. Expansion cannot hide a leaky bucket here.",
|
|
511
|
-
ops: "Exclude Expansion by design. Pair with NRR. Diagnose churn versus contraction separately."
|
|
512
|
-
},
|
|
513
|
-
aliases: ["gross revenue retention", "gross retention"],
|
|
514
|
-
benchmarkHint: (motion) => pctBand("grr", motion)
|
|
515
|
-
},
|
|
516
|
-
// —— Pipeline ——
|
|
517
|
-
{
|
|
518
|
-
id: "pipeline_coverage",
|
|
519
|
-
kind: "saas",
|
|
520
|
-
label: "Pipeline Coverage",
|
|
521
|
-
group: "Pipeline",
|
|
522
|
-
tagline: "Is next quarter already at risk?",
|
|
523
|
-
how_computed: "Open pipeline amount divided by trailing-90-day closed-won amount.",
|
|
524
|
-
formula_lines: [
|
|
525
|
-
"Coverage = openPipeline / trailing_90d_won",
|
|
526
|
-
"required \u2248 1 / win_rate (discount for time left)"
|
|
527
|
-
],
|
|
528
|
-
meaning: "Board question: is next quarter already at risk? Priors scale with cycle length: about 3x for velocity/SMB, 4 to 5x for enterprise.",
|
|
529
|
-
expert_read: "Coverage has no meaning without win rate. Required coverage is about 1 / win rate, discounted for time left in the period. Inflated stages and zombie deals fake coverage. Cross-check with Freshness before you trust it.",
|
|
530
|
-
deepdive: [
|
|
531
|
-
"Always pair with Win Rate and Freshness.",
|
|
532
|
-
"Weighted Pipeline is the credibility-adjusted companion."
|
|
533
|
-
],
|
|
534
|
-
visual: {
|
|
535
|
-
kind: "gauge",
|
|
536
|
-
caption: "Example coverage versus a 3x target",
|
|
537
|
-
gauge: 72,
|
|
538
|
-
bars: [
|
|
539
|
-
{ label: "Open pipeline", value: 75, tone: "accent" },
|
|
540
|
-
{ label: "Trailing won (scaled)", value: 25, tone: "neutral" }
|
|
541
|
-
]
|
|
542
|
-
},
|
|
543
|
-
audience: {
|
|
544
|
-
board: "Coverage answers whether next quarter already has too little pipeline. Fake coverage from zombies is worse than an honest gap.",
|
|
545
|
-
ops: "open / trailing-90d won. Required \u2248 1/win_rate. Cross-check Freshness before you brief."
|
|
546
|
-
},
|
|
547
|
-
aliases: ["coverage", "pipeline coverage", "pipe coverage"],
|
|
548
|
-
benchmarkHint: (motion) => pctBand("pipeline_coverage", motion)
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
id: "weighted_pipeline",
|
|
552
|
-
kind: "saas",
|
|
553
|
-
label: "Weighted Pipeline",
|
|
554
|
-
group: "Pipeline",
|
|
555
|
-
tagline: "What is the pipeline worth after stage probability?",
|
|
556
|
-
how_computed: "Sum of amount \xD7 stage probability for open deals. NTRP uses CRM Probability when present. Else it uses stage defaults.",
|
|
557
|
-
formula_lines: [
|
|
558
|
-
"Weighted = \u03A3 (amount \xD7 stageProbability)",
|
|
559
|
-
"trust \u2264 stage discipline deserves"
|
|
560
|
-
],
|
|
561
|
-
meaning: "Board question: what should we forecast from open pipeline?",
|
|
562
|
-
expert_read: "Trust this number only as much as stage discipline deserves. Inflated late stages make Weighted Pipeline a fiction.",
|
|
563
|
-
deepdive: [
|
|
564
|
-
"Compare to unweighted open pipeline. A large gap means optimistic stages.",
|
|
565
|
-
"Pair with Flow Rate for stuck late stages."
|
|
566
|
-
],
|
|
567
|
-
visual: {
|
|
568
|
-
kind: "bars",
|
|
569
|
-
caption: "Example: open versus weighted",
|
|
570
|
-
bars: [
|
|
571
|
-
{ label: "Open pipeline", value: 100, tone: "neutral" },
|
|
572
|
-
{ label: "Weighted", value: 42, tone: "accent" }
|
|
573
|
-
]
|
|
574
|
-
},
|
|
575
|
-
audience: {
|
|
576
|
-
board: "Weighted Pipeline is the credibility-adjusted forecast input. It is only as good as stage discipline.",
|
|
577
|
-
ops: "\u03A3 amount \xD7 probability. Audit stage probabilities when weighted is far below open."
|
|
578
|
-
},
|
|
579
|
-
aliases: ["weighted pipe", "probability-weighted pipeline"]
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
id: "pipeline_created",
|
|
583
|
-
kind: "saas",
|
|
584
|
-
label: "Pipeline Created (90d)",
|
|
585
|
-
group: "Pipeline",
|
|
586
|
-
tagline: "How much new pipeline did we generate recently?",
|
|
587
|
-
how_computed: "Sum of amounts for opportunities created in the last 90 days.",
|
|
588
|
-
formula_lines: ["Pipeline Created = \u03A3 amount where created_at within 90d"],
|
|
589
|
-
meaning: "Board question: is the top of funnel still filling?",
|
|
590
|
-
expert_read: "Falling created pipeline with flat coverage is a future miss. Coverage is lagging. Created is leading.",
|
|
591
|
-
deepdive: [
|
|
592
|
-
"This is a leading indicator for next-quarter coverage.",
|
|
593
|
-
"Cut by source or segment to find where creation stalled."
|
|
594
|
-
],
|
|
595
|
-
visual: {
|
|
596
|
-
kind: "bars",
|
|
597
|
-
caption: "Example: created versus needed",
|
|
598
|
-
bars: [
|
|
599
|
-
{ label: "Created (90d)", value: 55, tone: "yellow" },
|
|
600
|
-
{ label: "Target pace", value: 80, tone: "green" }
|
|
601
|
-
]
|
|
602
|
-
},
|
|
603
|
-
audience: {
|
|
604
|
-
board: "Pipeline Created is a leading indicator. When coverage lags, the miss is already in motion.",
|
|
605
|
-
ops: "\u03A3 amounts on opportunities created in 90 days. Cut by source when it dips."
|
|
606
|
-
},
|
|
607
|
-
aliases: ["pipe gen", "pipeline generation", "created pipeline"]
|
|
608
|
-
},
|
|
609
|
-
{
|
|
610
|
-
id: "pipeline_velocity",
|
|
611
|
-
kind: "saas",
|
|
612
|
-
label: "Pipeline Velocity",
|
|
613
|
-
group: "Pipeline",
|
|
614
|
-
tagline: "Revenue throughput per day. Four levers, one number.",
|
|
615
|
-
how_computed: "(openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays. Requires 3 or more dated closed-won deals. Unit: $/day.",
|
|
616
|
-
formula_lines: [
|
|
617
|
-
"Velocity = (openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays",
|
|
618
|
-
"four levers: #opps \xB7 deal size \xB7 win rate \xB7 cycle days"
|
|
619
|
-
],
|
|
620
|
-
meaning: "Board question: which lever moved when throughput changed? This is the most decision-ready pipeline metric.",
|
|
621
|
-
expert_read: "When velocity changes, name which lever moved. A win-rate rise on falling opportunity volume is qualification tightening, not improvement.",
|
|
622
|
-
deepdive: [
|
|
623
|
-
"Needs 3 or more dated wins. Otherwise this metric is unavailable.",
|
|
624
|
-
"Pairs with Flow Rate (cycle) and Win Rate (conversion)."
|
|
625
|
-
],
|
|
626
|
-
visual: {
|
|
627
|
-
kind: "levers",
|
|
628
|
-
caption: "Four levers. Name which one moved.",
|
|
629
|
-
levers: ["# Open opps", "Avg deal size", "Win rate", "Cycle days"]
|
|
630
|
-
},
|
|
631
|
-
audience: {
|
|
632
|
-
board: "Velocity is throughput. When it moves, name the lever: volume, size, win rate, or cycle.",
|
|
633
|
-
ops: "(opps \xD7 avgDeal \xD7 winRate) / cycleDays. Diagnose the moved lever before you recommend a play."
|
|
634
|
-
},
|
|
635
|
-
aliases: ["velocity", "pipeline velocity", "throughput"]
|
|
636
|
-
},
|
|
637
|
-
// —— Sales efficiency ——
|
|
638
|
-
{
|
|
639
|
-
id: "win_rate",
|
|
640
|
-
kind: "saas",
|
|
641
|
-
label: "Win Rate",
|
|
642
|
-
group: "Sales Efficiency",
|
|
643
|
-
tagline: "Of decided deals, how often do we win?",
|
|
644
|
-
how_computed: "closed-won / (won + lost) \xD7 100.",
|
|
645
|
-
formula_lines: ["Win Rate = won / (won + lost) \xD7 100"],
|
|
646
|
-
meaning: "Board question: are we converting the pipeline we create? Priors: 25\u201335% SMB, 18\u201325% mid-market, 12\u201318% enterprise on qualified opportunities.",
|
|
647
|
-
expert_read: "A rising win rate on falling opportunity volume is qualification tightening, not improvement. Check the denominator.",
|
|
648
|
-
deepdive: [
|
|
649
|
-
"Required coverage is about 1 / win rate.",
|
|
650
|
-
"Cut by segment or source before company-wide coaching."
|
|
651
|
-
],
|
|
652
|
-
visual: {
|
|
653
|
-
kind: "split",
|
|
654
|
-
caption: "Example decided deals",
|
|
655
|
-
bars: [
|
|
656
|
-
{ label: "Won", value: 28, tone: "green" },
|
|
657
|
-
{ label: "Lost", value: 72, tone: "red" }
|
|
658
|
-
]
|
|
659
|
-
},
|
|
660
|
-
audience: {
|
|
661
|
-
board: "Win Rate is conversion of decided deals. A rising win rate with falling volume is often tighter qualification, not better selling.",
|
|
662
|
-
ops: "won/(won+lost). Check the denominator. Motion benchmarks set green and yellow bands."
|
|
663
|
-
},
|
|
664
|
-
aliases: ["close rate", "winrate", "win %"],
|
|
665
|
-
benchmarkHint: (motion) => pctBand("win_rate", motion)
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
id: "avg_deal_size",
|
|
669
|
-
kind: "saas",
|
|
670
|
-
label: "Avg Deal Size",
|
|
671
|
-
group: "Sales Efficiency",
|
|
672
|
-
tagline: "What does a typical win look like?",
|
|
673
|
-
how_computed: "Mean amount on closed-won opportunities.",
|
|
674
|
-
formula_lines: ["Avg Deal = mean(closed-won amount)"],
|
|
675
|
-
meaning: "Board question: are we selling the motion we think we are?",
|
|
676
|
-
expert_read: "Deal size drifting down while volume rises often means mix shift into a lower segment. That is not always a problem. It does change coverage math.",
|
|
677
|
-
deepdive: [
|
|
678
|
-
"Feeds Pipeline Velocity and LTV proxy.",
|
|
679
|
-
"Cut by segment. Averages hide bimodal motions."
|
|
680
|
-
],
|
|
681
|
-
visual: {
|
|
682
|
-
kind: "bars",
|
|
683
|
-
caption: "Example size mix by segment",
|
|
684
|
-
bars: [
|
|
685
|
-
{ label: "SMB", value: 30, tone: "neutral" },
|
|
686
|
-
{ label: "Mid-market", value: 55, tone: "accent" },
|
|
687
|
-
{ label: "Enterprise", value: 90, tone: "green" }
|
|
688
|
-
]
|
|
689
|
-
},
|
|
690
|
-
audience: {
|
|
691
|
-
board: "Avg Deal Size should match the motion you claim. Mix shift changes coverage and capacity math.",
|
|
692
|
-
ops: "Mean closed-won amount. Segment before you coach on size."
|
|
693
|
-
},
|
|
694
|
-
aliases: ["average deal size", "asp", "acv"]
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
id: "avg_sales_cycle",
|
|
698
|
-
kind: "saas",
|
|
699
|
-
label: "Avg Sales Cycle",
|
|
700
|
-
group: "Sales Efficiency",
|
|
701
|
-
tagline: "How long from create to close on wins?",
|
|
702
|
-
how_computed: "Mean days from created_at to close date on dated closed-won deals.",
|
|
703
|
-
formula_lines: ["Avg Cycle = mean(close_date \u2212 created_at) on dated wins"],
|
|
704
|
-
meaning: "Board question: is the cycle stretching? That is the earliest soft signal of deal-quality decay.",
|
|
705
|
-
expert_read: "Cycle stretching past the motion's norm is the earliest soft signal of deal-quality decay. Pair with Flow Rate stuck stages.",
|
|
706
|
-
deepdive: [
|
|
707
|
-
"Feeds Pipeline Velocity as the denominator.",
|
|
708
|
-
"Needs dated wins. Missing close dates understate or omit this metric."
|
|
709
|
-
],
|
|
710
|
-
visual: {
|
|
711
|
-
kind: "bars",
|
|
712
|
-
caption: "Example cycle versus motion norm",
|
|
713
|
-
bars: [
|
|
714
|
-
{ label: "Your cycle", value: 78, tone: "yellow" },
|
|
715
|
-
{ label: "Motion norm", value: 55, tone: "green" }
|
|
716
|
-
]
|
|
717
|
-
},
|
|
718
|
-
audience: {
|
|
719
|
-
board: "Cycle stretch is an early soft signal that quality or process is slipping. This appears before the miss shows in bookings.",
|
|
720
|
-
ops: "Mean create to close on dated wins. Investigate the stage that aged."
|
|
721
|
-
},
|
|
722
|
-
aliases: ["sales cycle", "cycle length", "time to close"]
|
|
723
|
-
},
|
|
724
|
-
{
|
|
725
|
-
id: "stage_conversion",
|
|
726
|
-
kind: "saas",
|
|
727
|
-
label: "Stage Conversion",
|
|
728
|
-
group: "Sales Efficiency",
|
|
729
|
-
tagline: "Where in the stage model does advancement collapse?",
|
|
730
|
-
how_computed: "From metadata.stage_history stage advances when present. Otherwise a win-rate proxy.",
|
|
731
|
-
formula_lines: [
|
|
732
|
-
"Preferred: advancement rates from stage_history",
|
|
733
|
-
"Fallback: win-rate proxy when history is missing"
|
|
734
|
-
],
|
|
735
|
-
meaning: "Board question: which single stage is starving everything downstream?",
|
|
736
|
-
expert_read: "Find the one stage where conversion collapses. That is the process problem. Everything downstream is starvation.",
|
|
737
|
-
deepdive: [
|
|
738
|
-
"Best with stage_history metadata. Otherwise treat this as a proxy.",
|
|
739
|
-
"Pairs with Flow Rate stage-age cuts."
|
|
740
|
-
],
|
|
741
|
-
visual: {
|
|
742
|
-
kind: "funnel",
|
|
743
|
-
caption: "Example: find the collapse",
|
|
744
|
-
funnel: [
|
|
745
|
-
{ label: "Stage 1\u21922", widthPct: 100 },
|
|
746
|
-
{ label: "Stage 2\u21923", widthPct: 72 },
|
|
747
|
-
{ label: "Stage 3\u21924", widthPct: 28 },
|
|
748
|
-
{ label: "Stage 4\u2192Close", widthPct: 18 }
|
|
749
|
-
]
|
|
750
|
-
},
|
|
751
|
-
audience: {
|
|
752
|
-
board: "Stage Conversion names the bottleneck stage. One collapse starves every stage after it.",
|
|
753
|
-
ops: "Prefer stage_history advances. Fix the collapse stage before you coach downstream reps."
|
|
754
|
-
},
|
|
755
|
-
aliases: ["stage conversion", "stage advance", "conversion by stage"]
|
|
756
|
-
},
|
|
757
|
-
// —— Unit economics ——
|
|
758
|
-
{
|
|
759
|
-
id: "ltv_proxy",
|
|
760
|
-
kind: "saas",
|
|
761
|
-
label: "LTV (Proxy)",
|
|
762
|
-
group: "Unit Economics",
|
|
763
|
-
tagline: "Rough lifetime value from deal size and GRR.",
|
|
764
|
-
how_computed: "avgDeal / ((100 \u2212 GRR) / 100) when GRR is below 100. This metric is unavailable when GRR is 100% or more, or missing.",
|
|
765
|
-
formula_lines: [
|
|
766
|
-
"LTV \u2248 avgDeal / churnRate",
|
|
767
|
-
"churnRate = (100 \u2212 GRR) / 100 (requires GRR < 100)"
|
|
768
|
-
],
|
|
769
|
-
meaning: "Board question: what is a customer roughly worth over their life?",
|
|
770
|
-
expert_read: "This is a proxy, not a cohort LTV. Use it for direction, not capital allocation.",
|
|
771
|
-
deepdive: [
|
|
772
|
-
"Unavailable when GRR is 100 or more, or missing.",
|
|
773
|
-
"Pairs with CAC for LTV:CAC when spend data exists."
|
|
774
|
-
],
|
|
775
|
-
visual: {
|
|
776
|
-
kind: "gauge",
|
|
777
|
-
caption: "Example LTV proxy. Directional only.",
|
|
778
|
-
gauge: 68
|
|
779
|
-
},
|
|
780
|
-
audience: {
|
|
781
|
-
board: "LTV Proxy is directional from deal size and GRR. It is not a cohort LTV. Use it for orientation, not capital decisions.",
|
|
782
|
-
ops: "avgDeal / ((100\u2212GRR)/100). Needs GRR below 100. Prefer cohort math when billing data arrives."
|
|
783
|
-
},
|
|
784
|
-
aliases: ["ltv", "lifetime value"]
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
id: "cac",
|
|
788
|
-
kind: "saas",
|
|
789
|
-
label: "CAC",
|
|
790
|
-
group: "Unit Economics",
|
|
791
|
-
tagline: "Customer acquisition cost. This metric needs spend data.",
|
|
792
|
-
how_computed: "This metric requires campaign or sales spend data. It is currently unavailable on CRM-only datasets.",
|
|
793
|
-
formula_lines: ["CAC = sales & marketing spend / new customers", "(requires spend data \u2014 not in CRM-only exports)"],
|
|
794
|
-
meaning: "Board question: what does a new logo cost to win?",
|
|
795
|
-
expert_read: "Without spend, NTRP cannot invent CAC. Connect campaign spend or finance exports to unlock unit economics.",
|
|
796
|
-
deepdive: [
|
|
797
|
-
"Always unavailable on CRM-only demos. That is expected.",
|
|
798
|
-
"Unlocks LTV:CAC, Payback, and Magic Number when spend lands."
|
|
799
|
-
],
|
|
800
|
-
visual: { kind: "none", caption: "Needs campaign spend or a finance export" },
|
|
801
|
-
audience: {
|
|
802
|
-
board: "CAC is locked until spend data is connected. CRM alone cannot price acquisition.",
|
|
803
|
-
ops: "Bring campaign or S&M spend. Until then, unit-econ metrics stay unavailable by design."
|
|
804
|
-
},
|
|
805
|
-
aliases: ["customer acquisition cost", "acquisition cost"]
|
|
806
|
-
},
|
|
807
|
-
{
|
|
808
|
-
id: "ltv_cac_ratio",
|
|
809
|
-
kind: "saas",
|
|
810
|
-
label: "LTV:CAC Ratio",
|
|
811
|
-
group: "Unit Economics",
|
|
812
|
-
tagline: "Does acquisition spend return enough value?",
|
|
813
|
-
how_computed: "LTV proxy divided by CAC. Unavailable without spend (CAC).",
|
|
814
|
-
formula_lines: ["LTV:CAC = LTV_proxy / CAC", "(requires CAC)"],
|
|
815
|
-
meaning: "Board question: do we earn enough lifetime value per dollar spent to acquire?",
|
|
816
|
-
expert_read: "In the efficiency era, boards weigh LTV:CAC and payback as heavily as growth. Classic rule of thumb is 3x or more. Motion and gross margin matter.",
|
|
817
|
-
deepdive: ["Blocked on CAC. See LTV Proxy and CAC."],
|
|
818
|
-
visual: { kind: "none", caption: "This metric needs CAC (spend data)" },
|
|
819
|
-
audience: {
|
|
820
|
-
board: "LTV:CAC is the acquisition ROI story. It is available once spend is connected.",
|
|
821
|
-
ops: "LTV_proxy / CAC. Unlocks with spend import."
|
|
822
|
-
},
|
|
823
|
-
aliases: ["ltv cac", "ltv/cac", "ltv to cac"]
|
|
824
|
-
},
|
|
825
|
-
{
|
|
826
|
-
id: "payback_months",
|
|
827
|
-
kind: "saas",
|
|
828
|
-
label: "Payback Months",
|
|
829
|
-
group: "Unit Economics",
|
|
830
|
-
tagline: "How many months to recover CAC?",
|
|
831
|
-
how_computed: "This metric requires CAC and spend. Lower is better.",
|
|
832
|
-
formula_lines: ["Payback \u2248 CAC / (monthly gross profit per customer)", "(requires spend data)"],
|
|
833
|
-
meaning: "Board question: how fast does acquisition spend return? Efficiency-era prior: under 18 months is often healthy.",
|
|
834
|
-
expert_read: "Boards now weigh payback (under 18 months) as heavily as growth in many motions.",
|
|
835
|
-
deepdive: ["Blocked on CAC. Benchmarks exist per motion once data lands."],
|
|
836
|
-
visual: { kind: "none", caption: "This metric needs CAC (spend data)" },
|
|
837
|
-
audience: {
|
|
838
|
-
board: "Payback is how fast CAC returns. Efficiency-era boards often want under 18 months.",
|
|
839
|
-
ops: "Requires CAC. Motion green and yellow bands apply when available."
|
|
840
|
-
},
|
|
841
|
-
aliases: ["payback", "cac payback"],
|
|
842
|
-
benchmarkHint: (motion) => monthsBand(motion)
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
id: "magic_number",
|
|
846
|
-
kind: "saas",
|
|
847
|
-
label: "Magic Number",
|
|
848
|
-
group: "Unit Economics",
|
|
849
|
-
tagline: "Sales efficiency: net new ARR per sales dollar.",
|
|
850
|
-
how_computed: "This metric requires sales spend. Classic form: net new ARR (quarter) / prior-quarter S&M spend.",
|
|
851
|
-
formula_lines: [
|
|
852
|
-
"Magic Number \u2248 Net New ARR(q) / S&M spend(q\u22121)",
|
|
853
|
-
"(requires spend data)"
|
|
854
|
-
],
|
|
855
|
-
meaning: "Board question: how efficiently does sales spend produce net new ARR? Prior: above 0.75 is often healthy. Above 1 is strong.",
|
|
856
|
-
expert_read: "In the efficiency era, a magic number above 0.75 is weighed alongside growth. Without spend, NTRP keeps this unavailable. NTRP does not invent it.",
|
|
857
|
-
deepdive: ["Blocked on spend. Benchmarks per motion are ready when data lands."],
|
|
858
|
-
visual: { kind: "none", caption: "This metric needs S&M spend data" },
|
|
859
|
-
audience: {
|
|
860
|
-
board: "Magic Number prices sales efficiency. It is available once S&M spend is connected.",
|
|
861
|
-
ops: "Net new ARR / prior S&M. Motion benchmarks apply when spend lands."
|
|
862
|
-
},
|
|
863
|
-
aliases: ["sales magic number", "sales efficiency magic number"],
|
|
864
|
-
benchmarkHint: (motion) => magicBand(motion)
|
|
865
|
-
}
|
|
866
|
-
];
|
|
867
|
-
METRIC_DEFINITIONS = [...VITALS, ...SAAS];
|
|
868
|
-
CORE_DECK_IDS = [
|
|
869
|
-
"arr",
|
|
870
|
-
"nrr",
|
|
871
|
-
"grr",
|
|
872
|
-
"pipeline_coverage",
|
|
873
|
-
"win_rate",
|
|
874
|
-
"pipeline_velocity",
|
|
875
|
-
"freshness",
|
|
876
|
-
"flow_rate",
|
|
877
|
-
"drop_rate",
|
|
878
|
-
"signal_to_noise",
|
|
879
|
-
"thread_depth"
|
|
880
|
-
];
|
|
881
|
-
BY_ID = new Map(METRIC_DEFINITIONS.map((m) => [m.id, m]));
|
|
882
|
-
ALIAS_INDEX = (() => {
|
|
883
|
-
const idx = /* @__PURE__ */ new Map();
|
|
884
|
-
for (const m of METRIC_DEFINITIONS) {
|
|
885
|
-
idx.set(m.id.toLowerCase(), m.id);
|
|
886
|
-
idx.set(m.label.toLowerCase(), m.id);
|
|
887
|
-
for (const a of m.aliases ?? []) {
|
|
888
|
-
idx.set(a.toLowerCase(), m.id);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
idx.set("signal-to-noise", "signal_to_noise");
|
|
892
|
-
idx.set("signal:noise", "signal_to_noise");
|
|
893
|
-
idx.set("flow-rate", "flow_rate");
|
|
894
|
-
idx.set("drop-rate", "drop_rate");
|
|
895
|
-
idx.set("thread-depth", "thread_depth");
|
|
896
|
-
return idx;
|
|
897
|
-
})();
|
|
898
|
-
VITAL_IDS = [
|
|
899
|
-
"freshness",
|
|
900
|
-
"flow_rate",
|
|
901
|
-
"drop_rate",
|
|
902
|
-
"signal_to_noise",
|
|
903
|
-
"thread_depth"
|
|
904
|
-
];
|
|
905
|
-
SAAS_METRIC_IDS = SAAS.map((m) => m.id);
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
// src/data/guide-slides.ts
|
|
910
|
-
function normalize(raw) {
|
|
911
|
-
return raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/_/g, "-");
|
|
912
|
-
}
|
|
913
|
-
function isGuideSectionQuery(raw) {
|
|
914
|
-
const q = normalize(raw);
|
|
915
|
-
return GUIDE_SECTION_ALIASES.includes(q);
|
|
916
|
-
}
|
|
917
|
-
function resolveGuideId(raw) {
|
|
918
|
-
const q = normalize(raw);
|
|
919
|
-
if (!q) return null;
|
|
920
|
-
if (isGuideSectionQuery(q)) return GUIDE_DECK_IDS[0];
|
|
921
|
-
const underscored = q.replace(/-/g, "_");
|
|
922
|
-
if (GUIDE_DECK_IDS.includes(underscored)) {
|
|
923
|
-
return underscored;
|
|
924
|
-
}
|
|
925
|
-
return SLIDE_ALIASES[q] ?? SLIDE_ALIASES[underscored] ?? null;
|
|
926
|
-
}
|
|
927
|
-
function getGuideSlide(id) {
|
|
928
|
-
return BY_ID2.get(id);
|
|
929
|
-
}
|
|
930
|
-
function listGuideSlides() {
|
|
931
|
-
return GUIDE_DECK_IDS.map((id) => BY_ID2.get(id));
|
|
932
|
-
}
|
|
933
|
-
var GUIDE_DECK_IDS, GUIDE_SECTION_ALIASES, SLIDE_ALIASES, TALK, ASK, HANDOFF, LOOP, BY_ID2;
|
|
934
|
-
var init_guide_slides = __esm({
|
|
935
|
-
"src/data/guide-slides.ts"() {
|
|
936
|
-
"use strict";
|
|
937
|
-
GUIDE_DECK_IDS = ["talk", "ask", "handoff", "loop"];
|
|
938
|
-
GUIDE_SECTION_ALIASES = [
|
|
939
|
-
"guide",
|
|
940
|
-
"how",
|
|
941
|
-
"howto",
|
|
942
|
-
"how-to",
|
|
943
|
-
"how_to",
|
|
944
|
-
"use",
|
|
945
|
-
"using",
|
|
946
|
-
"nav",
|
|
947
|
-
"navigate",
|
|
948
|
-
"userflow",
|
|
949
|
-
"user-guide",
|
|
950
|
-
"userguide"
|
|
951
|
-
];
|
|
952
|
-
SLIDE_ALIASES = {
|
|
953
|
-
talk: "talk",
|
|
954
|
-
chat: "talk",
|
|
955
|
-
type: "talk",
|
|
956
|
-
speak: "talk",
|
|
957
|
-
conversation: "talk",
|
|
958
|
-
ask: "ask",
|
|
959
|
-
connect: "ask",
|
|
960
|
-
glossary: "ask",
|
|
961
|
-
keyless: "ask",
|
|
962
|
-
enter: "ask",
|
|
963
|
-
definitions: "ask",
|
|
964
|
-
handoff: "handoff",
|
|
965
|
-
inbox: "handoff",
|
|
966
|
-
skill: "handoff",
|
|
967
|
-
claude: "handoff",
|
|
968
|
-
ship: "handoff",
|
|
969
|
-
pickup: "handoff",
|
|
970
|
-
chatgpt: "handoff",
|
|
971
|
-
cursor: "handoff",
|
|
972
|
-
export: "handoff",
|
|
973
|
-
loop: "loop",
|
|
974
|
-
strategy: "loop",
|
|
975
|
-
strategist: "loop",
|
|
976
|
-
playbook: "loop",
|
|
977
|
-
remember: "loop",
|
|
978
|
-
sessions: "loop",
|
|
979
|
-
progress: "loop"
|
|
980
|
-
};
|
|
981
|
-
TALK = {
|
|
982
|
-
id: "talk",
|
|
983
|
-
label: "Talk to NTRP",
|
|
984
|
-
tagline: "Type English. Confirm the scope. Load data. Then listen.",
|
|
985
|
-
visual: {
|
|
986
|
-
kind: "funnel",
|
|
987
|
-
caption: "Bare Enter submits the dim \u23CE hint at these gates. If nothing is armed, Enter does nothing.",
|
|
988
|
-
funnel: [
|
|
989
|
-
{ label: "Type a question", widthPct: 100 },
|
|
990
|
-
{ label: "\u23CE yes (scope)", widthPct: 78 },
|
|
991
|
-
{ label: "Load data", widthPct: 56 },
|
|
992
|
-
{ label: "\u23CE go ahead", widthPct: 34 }
|
|
993
|
-
]
|
|
994
|
-
},
|
|
995
|
-
lines: [
|
|
996
|
-
'You do not need a slash. Type the question you need. Examples: "is our retention real for the board?" or "pipeline health".',
|
|
997
|
-
"",
|
|
998
|
-
"NTRP restates the question as a scope card. Confirm with \u23CE yes, or type yes. NTRP listens. NTRP does not invent a different question.",
|
|
999
|
-
"",
|
|
1000
|
-
"When the dataset is empty, type \u23CE use demo data. Or paste a CSV path. Or type /ingest. When the gap card shows that the formulas can compute, type \u23CE go ahead.",
|
|
1001
|
-
"",
|
|
1002
|
-
"Vital signs and SaaS metrics compute without an AI key. Every score that has a dollar translation shows it."
|
|
1003
|
-
],
|
|
1004
|
-
deepdive: [
|
|
1005
|
-
"Power-user slash commands still work. They stay hidden from /help: /new, /diagnose, /metrics, /ingest, /session.",
|
|
1006
|
-
'Type "use demo data" to load the hidden_crisis scenario. Company profile is optional. In scripts, type /demo --no-profile.',
|
|
1007
|
-
'After compute, the prompt becomes ask \u203A. Brief is the default depth. Type "go deep" when you want the long read.',
|
|
1008
|
-
"Type /home to show phase status. Type /help to list conversation shortcuts. /help does not list every command."
|
|
1009
|
-
]
|
|
1010
|
-
};
|
|
1011
|
-
ASK = {
|
|
1012
|
-
id: "ask",
|
|
1013
|
-
label: "After the numbers",
|
|
1014
|
-
tagline: 'Glossary is free. "Our ARR" is compute. Narrative needs /connect.',
|
|
1015
|
-
visual: {
|
|
1016
|
-
kind: "split",
|
|
1017
|
-
caption: "Possessives such as our, my, and the team's skip the glossary. They go to compute or scope.",
|
|
1018
|
-
bars: [
|
|
1019
|
-
{ label: "what is ARR?", value: 55, tone: "accent" },
|
|
1020
|
-
{ label: "our ARR", value: 45, tone: "neutral" }
|
|
1021
|
-
]
|
|
1022
|
-
},
|
|
1023
|
-
lines: [
|
|
1024
|
-
'"what is ARR?" and "how is freshness calculated?" answer from the built-in glossary. No key. No data.',
|
|
1025
|
-
"",
|
|
1026
|
-
'"what is our ARR?" and "why is this red?" need a loaded dataset. AI findings and /ask need a stored key. Type /connect and paste any provider key. NTRP detects it.',
|
|
1027
|
-
"",
|
|
1028
|
-
"Type /deepdive to replay this tour. Type /deepdive freshness for one slide. You can also type nrr, arr, or another id. Type /deepdive list for the catalog. Type /deepdive guide for this how-to section.",
|
|
1029
|
-
"",
|
|
1030
|
-
"When the prompt shows a dim \u23CE hint, bare Enter submits that action. Examples: \u23CE yes, \u23CE use demo data, \u23CE go ahead, \u23CE /connect. If there is no hint, Enter does nothing."
|
|
1031
|
-
],
|
|
1032
|
-
deepdive: [
|
|
1033
|
-
"Type /connect ollama for a keyless local model. Type /connect --base-url <url> --id <name> for any OpenAI-compatible endpoint.",
|
|
1034
|
-
"Type /model refresh to re-discover models. A retired model self-heals on the first 404.",
|
|
1035
|
-
"First-run skip of this tour does not complete it. A home \u2691 chip can bring you back after the first analysis.",
|
|
1036
|
-
"Tab completes /deepdive <metric>. Finding cards and playbook triggers also link here."
|
|
1037
|
-
]
|
|
1038
|
-
};
|
|
1039
|
-
HANDOFF = {
|
|
1040
|
-
id: "handoff",
|
|
1041
|
-
label: "Ship work to Claude",
|
|
1042
|
-
tagline: "Teach the inbox once. Later, tell Claude to pick it up.",
|
|
1043
|
-
visual: {
|
|
1044
|
-
kind: "layer_stack",
|
|
1045
|
-
caption: "You and Claude find and open the file. NTRP only writes.",
|
|
1046
|
-
layers: [
|
|
1047
|
-
{ label: "Set a pickup folder (onboard or /inbox set)" },
|
|
1048
|
-
{ label: "Paste the finder skill once (/inbox skill)", highlight: true },
|
|
1049
|
-
{ label: "/handoff writes files. There is no paste block." },
|
|
1050
|
-
{ label: '"Pick up the latest NTRP handoff."' }
|
|
1051
|
-
]
|
|
1052
|
-
},
|
|
1053
|
-
lines: [
|
|
1054
|
-
'Type "ship a board deck" or type /handoff. Files land in ~/Documents/Claude/ntrp-inbox by default. Type /inbox set to change the folder.',
|
|
1055
|
-
"",
|
|
1056
|
-
"During company setup, or any time, type /inbox skill. Paste a standing finder into Claude, ChatGPT, or Cursor once. That skill tells the tool to follow latest-handoff.md and INDEX.md.",
|
|
1057
|
-
"",
|
|
1058
|
-
'After that, each /handoff prints "Handoff ready". You will not get a paste block every write. Tell Claude: pick up the latest NTRP handoff.',
|
|
1059
|
-
"",
|
|
1060
|
-
"If you skipped onboard, type /inbox set ~/Documents/Claude/ntrp-inbox. Then type /inbox skill. Type /handoff skill to reprint the same finder."
|
|
1061
|
-
],
|
|
1062
|
-
deepdive: [
|
|
1063
|
-
"Inbox copies: latest-handoff.md, latest-pickup.md, SKILL.md, INDEX.md. Dated files also live under ~/.ntrp/exports/.",
|
|
1064
|
-
"Type /exports to list writes. Type /inbox show to print the folder and the latest pointer. Type /handoff --print to show the prompt body in the terminal.",
|
|
1065
|
-
"Audience-framed Metric definitions append to decks and reports. Claude then has the same glossary you walked.",
|
|
1066
|
-
"Type /end to close a session. NTRP writes a transcript plus a 1-page context brief under ~/.ntrp/sessions/."
|
|
1067
|
-
]
|
|
1068
|
-
};
|
|
1069
|
-
LOOP = {
|
|
1070
|
-
id: "loop",
|
|
1071
|
-
label: "Stay in the loop",
|
|
1072
|
-
tagline: "Diagnose \u2192 plan \u2192 review \u2192 remember. NTRP learns your business.",
|
|
1073
|
-
visual: {
|
|
1074
|
-
kind: "layer_stack",
|
|
1075
|
-
caption: "Stethoscope, not hospital. Observe and recommend. Never prescribe surgery.",
|
|
1076
|
-
layers: [
|
|
1077
|
-
{ label: "Listen (/diagnose, /metrics)" },
|
|
1078
|
-
{ label: 'Plan ("how should we fix this?" / /strategy)', highlight: true },
|
|
1079
|
-
{ label: "Review (/strategy review, /playbook)" },
|
|
1080
|
-
{ label: "Remember (/remember, /rate, ANALYST.md)" }
|
|
1081
|
-
]
|
|
1082
|
-
},
|
|
1083
|
-
lines: [
|
|
1084
|
-
'Type "how should we fix this?" or type /strategy. NTRP builds a measurable plan with milestones and dollar-anchored ranges. Type /strategy review to check those against later vital signs.',
|
|
1085
|
-
"",
|
|
1086
|
-
"When a vital sign is red, type /playbook. NTRP names the matching play. Outcomes from reviews annotate the catalog with what hit here.",
|
|
1087
|
-
"",
|
|
1088
|
-
"Type /remember to store a durable fact. Type /rate bad <reason> to write a calibration. Optional ~/.ntrp/ANALYST.md is standing voice and priorities. It never overrides safety rules.",
|
|
1089
|
-
"",
|
|
1090
|
-
"Commands: /sessions show, /session <id> pickup, /end (transcript plus brief), /home, /progress, /help."
|
|
1091
|
-
],
|
|
1092
|
-
deepdive: [
|
|
1093
|
-
'Type "build me a game plan" before analysis. NTRP queues the strategist and resumes after compute.',
|
|
1094
|
-
"Interactive sessions distill 5 or fewer durable facts on close when an LLM key is stored. One-shot commands do not bank hours or distill.",
|
|
1095
|
-
"Type /scratch to factory-reset data and config. progress.json and install.json survive unless you pass --include-progress.",
|
|
1096
|
-
"Credits in /progress accrue in interactive ntrp only."
|
|
1097
|
-
]
|
|
1098
|
-
};
|
|
1099
|
-
BY_ID2 = /* @__PURE__ */ new Map([
|
|
1100
|
-
[TALK.id, TALK],
|
|
1101
|
-
[ASK.id, ASK],
|
|
1102
|
-
[HANDOFF.id, HANDOFF],
|
|
1103
|
-
[LOOP.id, LOOP]
|
|
1104
|
-
]);
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
// src/output/formatters.ts
|
|
1109
|
-
var init_formatters = __esm({
|
|
1110
|
-
"src/output/formatters.ts"() {
|
|
1111
|
-
"use strict";
|
|
1112
|
-
}
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
// src/ui/theme.ts
|
|
1116
|
-
import chalk from "chalk";
|
|
1117
|
-
function paint(token, text) {
|
|
1118
|
-
if (token === "dim") return chalk.dim(text);
|
|
1119
|
-
return chalk.hex(TOKENS[token])(text);
|
|
1120
|
-
}
|
|
1121
|
-
function bold(text) {
|
|
1122
|
-
return chalk.bold(text);
|
|
1123
|
-
}
|
|
1124
|
-
function badge(label, tone = "muted") {
|
|
1125
|
-
const normalized = ` ${label.toUpperCase()} `;
|
|
1126
|
-
if (tone === "muted") return chalk.dim(normalized);
|
|
1127
|
-
const color = BADGE_TONE_COLORS[tone];
|
|
1128
|
-
if (chalk.level >= 2) {
|
|
1129
|
-
return chalk.bgHex(color).hex(BADGE_TEXT).bold(normalized);
|
|
1130
|
-
}
|
|
1131
|
-
return chalk.hex(color)(normalized);
|
|
1132
|
-
}
|
|
1133
|
-
function sectionHeading(label) {
|
|
1134
|
-
return `${paint("accent", "\u25B8")} ${paint("accent", bold(label))}`;
|
|
1135
|
-
}
|
|
1136
|
-
function statusDot(status) {
|
|
1137
|
-
if (status === "neutral") return chalk.dim("\u25CB");
|
|
1138
|
-
return chalk.hex(STATUS[status])("\u25CF");
|
|
1139
|
-
}
|
|
1140
|
-
function scoreBar(score, status, width = 14) {
|
|
1141
|
-
const filled = Math.round(score / 100 * width);
|
|
1142
|
-
const color = chalk.hex(STATUS[status]);
|
|
1143
|
-
let filledPart = "";
|
|
1144
|
-
for (let i = 0; i < filled; i++) {
|
|
1145
|
-
filledPart += i % 2 === 0 ? "\u2588" : "\u2593";
|
|
1146
|
-
}
|
|
1147
|
-
const emptyPart = "\u2591".repeat(width - filled);
|
|
1148
|
-
return color(filledPart) + chalk.dim(emptyPart);
|
|
1149
|
-
}
|
|
1150
|
-
var STATUS, TOKENS, BADGE_TONE_COLORS, BADGE_TEXT;
|
|
1151
|
-
var init_theme = __esm({
|
|
1152
|
-
"src/ui/theme.ts"() {
|
|
1153
|
-
"use strict";
|
|
1154
|
-
init_formatters();
|
|
1155
|
-
STATUS = {
|
|
1156
|
-
green: "#22c55e",
|
|
1157
|
-
yellow: "#eab308",
|
|
1158
|
-
red: "#ef4444",
|
|
1159
|
-
neutral: "#64748b"
|
|
1160
|
-
};
|
|
1161
|
-
TOKENS = {
|
|
1162
|
-
accent: "#14b8a6",
|
|
1163
|
-
accentBright: "#2dd4bf",
|
|
1164
|
-
border: "#334155",
|
|
1165
|
-
borderMuted: "#1e293b",
|
|
1166
|
-
dim: "#64748b",
|
|
1167
|
-
text: "#e2e8f0",
|
|
1168
|
-
info: "#3b82f6",
|
|
1169
|
-
...STATUS,
|
|
1170
|
-
success: STATUS.green,
|
|
1171
|
-
warning: STATUS.yellow,
|
|
1172
|
-
error: STATUS.red
|
|
1173
|
-
};
|
|
1174
|
-
BADGE_TONE_COLORS = {
|
|
1175
|
-
success: TOKENS.success,
|
|
1176
|
-
warning: TOKENS.warning,
|
|
1177
|
-
error: TOKENS.error,
|
|
1178
|
-
info: TOKENS.info,
|
|
1179
|
-
accent: TOKENS.accent
|
|
1180
|
-
};
|
|
1181
|
-
BADGE_TEXT = "#0f172a";
|
|
1182
|
-
}
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
// src/ui/layout.ts
|
|
1186
|
-
function stripAnsi(text) {
|
|
1187
|
-
return text.replace(ANSI_RE, "");
|
|
1188
|
-
}
|
|
1189
|
-
function visibleWidth(text) {
|
|
1190
|
-
return stripAnsi(text).length;
|
|
1191
|
-
}
|
|
1192
|
-
function padRight(text, width) {
|
|
1193
|
-
const gap = Math.max(0, width - visibleWidth(text));
|
|
1194
|
-
return `${text}${" ".repeat(gap)}`;
|
|
1195
|
-
}
|
|
1196
|
-
function truncateVisible(text, maxVisible, ellipsis = "\u2026") {
|
|
1197
|
-
if (visibleWidth(text) <= maxVisible) return text;
|
|
1198
|
-
if (maxVisible <= ellipsis.length) return stripAnsi(text).slice(0, maxVisible);
|
|
1199
|
-
const target = maxVisible - ellipsis.length;
|
|
1200
|
-
let visible = 0;
|
|
1201
|
-
let i = 0;
|
|
1202
|
-
let sawAnsi = false;
|
|
1203
|
-
while (i < text.length && visible < target) {
|
|
1204
|
-
if (text[i] === "\x1B") {
|
|
1205
|
-
const match = text.slice(i).match(/^\u001B\[[0-9;]*m/);
|
|
1206
|
-
if (match) {
|
|
1207
|
-
i += match[0].length;
|
|
1208
|
-
sawAnsi = true;
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
visible++;
|
|
1213
|
-
i++;
|
|
1214
|
-
}
|
|
1215
|
-
const reset = sawAnsi ? "\x1B[0m" : "";
|
|
1216
|
-
return text.slice(0, i) + reset + ellipsis;
|
|
1217
|
-
}
|
|
1218
|
-
function wrapWords(text, maxW) {
|
|
1219
|
-
const words = text.split(/\s+/).filter(Boolean);
|
|
1220
|
-
const lines = [];
|
|
1221
|
-
let cur = "";
|
|
1222
|
-
for (let word of words) {
|
|
1223
|
-
if (visibleWidth(word) > maxW) {
|
|
1224
|
-
if (cur) {
|
|
1225
|
-
lines.push(cur);
|
|
1226
|
-
cur = "";
|
|
1227
|
-
}
|
|
1228
|
-
word = truncateVisible(word, maxW, maxW > 3 ? "\u2026" : "");
|
|
1229
|
-
}
|
|
1230
|
-
const test = cur ? `${cur} ${word}` : word;
|
|
1231
|
-
if (cur && visibleWidth(test) > maxW) {
|
|
1232
|
-
lines.push(cur);
|
|
1233
|
-
cur = word;
|
|
1234
|
-
} else {
|
|
1235
|
-
cur = test;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
if (cur) lines.push(cur);
|
|
1239
|
-
return lines.length ? lines : [""];
|
|
1240
|
-
}
|
|
1241
|
-
function termWidth() {
|
|
1242
|
-
return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
|
|
1243
|
-
}
|
|
1244
|
-
function resolveCardWidth(opts = {}) {
|
|
1245
|
-
const { min = 60, max = 100, margin = 4 } = opts;
|
|
1246
|
-
const usable = Math.max(20, termWidth() - margin);
|
|
1247
|
-
return Math.max(Math.min(min, usable), Math.min(usable, max));
|
|
1248
|
-
}
|
|
1249
|
-
var ANSI_RE;
|
|
1250
|
-
var init_layout = __esm({
|
|
1251
|
-
"src/ui/layout.ts"() {
|
|
1252
|
-
"use strict";
|
|
1253
|
-
ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
// src/ui/slides.ts
|
|
1258
|
-
import chalk2 from "chalk";
|
|
1259
|
-
function slideCardWidth() {
|
|
1260
|
-
return resolveCardWidth({ min: 64, max: 100, margin: 4 });
|
|
1261
|
-
}
|
|
1262
|
-
function tonePaint(tone = "accent") {
|
|
1263
|
-
switch (tone) {
|
|
1264
|
-
case "green":
|
|
1265
|
-
return chalk2.hex("#22c55e");
|
|
1266
|
-
case "yellow":
|
|
1267
|
-
return chalk2.hex("#eab308");
|
|
1268
|
-
case "red":
|
|
1269
|
-
return chalk2.hex("#ef4444");
|
|
1270
|
-
case "neutral":
|
|
1271
|
-
return chalk2.dim;
|
|
1272
|
-
default:
|
|
1273
|
-
return (t) => paint("accent", t);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
function renderBarRow(bar, barWidth, labelW) {
|
|
1277
|
-
const fill = Math.max(0, Math.min(barWidth, Math.round(bar.value / 100 * barWidth)));
|
|
1278
|
-
const body = "\u2588".repeat(fill) + "\u2591".repeat(barWidth - fill);
|
|
1279
|
-
const colored = tonePaint(bar.tone)(body);
|
|
1280
|
-
const label = padRight(truncateVisible(bar.label, labelW), labelW);
|
|
1281
|
-
const pct = String(Math.round(bar.value)).padStart(3);
|
|
1282
|
-
return `${label} ${colored} ${chalk2.dim(pct)}`;
|
|
1283
|
-
}
|
|
1284
|
-
function renderBars(bars, inner) {
|
|
1285
|
-
const labelW = Math.min(18, Math.max(...bars.map((b) => visibleWidth(b.label)), 8));
|
|
1286
|
-
const barWidth = Math.max(8, Math.min(28, inner - labelW - 6));
|
|
1287
|
-
return bars.map((b) => renderBarRow(b, barWidth, labelW));
|
|
1288
|
-
}
|
|
1289
|
-
function renderFunnel(steps, inner) {
|
|
1290
|
-
const maxBar = Math.max(12, Math.min(40, inner - 22));
|
|
1291
|
-
const lines = [];
|
|
1292
|
-
for (const step of steps) {
|
|
1293
|
-
const w = Math.max(2, Math.round(step.widthPct / 100 * maxBar));
|
|
1294
|
-
const bar = paint("accent", "\u2588".repeat(w));
|
|
1295
|
-
const label = truncateVisible(step.label, Math.max(8, inner - maxBar - 8));
|
|
1296
|
-
lines.push(`${padRight(label, Math.min(18, inner - maxBar - 6))} ${bar} ${chalk2.dim(`${step.widthPct}%`)}`);
|
|
1297
|
-
}
|
|
1298
|
-
return lines;
|
|
1299
|
-
}
|
|
1300
|
-
function renderWaterfall(steps, inner) {
|
|
1301
|
-
const maxAbs = Math.max(...steps.map((s) => Math.abs(s.cumulative)), 1);
|
|
1302
|
-
const barW = Math.max(10, Math.min(28, inner - 28));
|
|
1303
|
-
const lines = [];
|
|
1304
|
-
for (const step of steps) {
|
|
1305
|
-
const fill = Math.max(1, Math.round(Math.abs(step.cumulative) / maxAbs * barW));
|
|
1306
|
-
const bar = step.delta >= 0 ? chalk2.hex("#22c55e")("\u2588".repeat(fill)) : chalk2.hex("#ef4444")("\u2588".repeat(fill));
|
|
1307
|
-
const deltaStr = step.delta > 0 ? `+${step.delta}` : step.delta < 0 ? `${step.delta}` : `${step.delta}`;
|
|
1308
|
-
const deltaPainted = step.delta > 0 ? chalk2.hex("#22c55e")(deltaStr.padStart(5)) : step.delta < 0 ? chalk2.hex("#ef4444")(deltaStr.padStart(5)) : chalk2.dim(deltaStr.padStart(5));
|
|
1309
|
-
const label = padRight(truncateVisible(step.label, 14), 14);
|
|
1310
|
-
lines.push(`${label} ${deltaPainted} ${bar} ${chalk2.dim(`\u2192 ${step.cumulative}`)}`);
|
|
1311
|
-
}
|
|
1312
|
-
return lines;
|
|
1313
|
-
}
|
|
1314
|
-
function renderLayerStack(layers, inner) {
|
|
1315
|
-
const lines = [];
|
|
1316
|
-
for (let i = 0; i < layers.length; i++) {
|
|
1317
|
-
const layer = layers[i];
|
|
1318
|
-
const marker = layer.highlight ? paint("accent", "\u25C6") : chalk2.dim("\u25C7");
|
|
1319
|
-
const text = layer.highlight ? bold(layer.label) : chalk2.dim(layer.label);
|
|
1320
|
-
lines.push(`${marker} ${truncateVisible(text, inner - 4)}`);
|
|
1321
|
-
if (i < layers.length - 1) {
|
|
1322
|
-
lines.push(chalk2.dim(" \u2502"));
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
return lines;
|
|
1326
|
-
}
|
|
1327
|
-
function renderLevers(levers, inner) {
|
|
1328
|
-
const cell = Math.floor((inner - 9) / 2);
|
|
1329
|
-
const lines = [];
|
|
1330
|
-
lines.push(chalk2.dim("\u250C" + "\u2500".repeat(Math.max(8, cell)) + "\u2510 \u250C" + "\u2500".repeat(Math.max(8, cell)) + "\u2510"));
|
|
1331
|
-
for (let i = 0; i < levers.length; i += 2) {
|
|
1332
|
-
const a = padRight(truncateVisible(levers[i] ?? "", cell - 2), cell - 2);
|
|
1333
|
-
const b = padRight(truncateVisible(levers[i + 1] ?? "", cell - 2), cell - 2);
|
|
1334
|
-
lines.push(
|
|
1335
|
-
`${paint("accent", "\u2502")} ${a} ${paint("accent", "\u2502")} ${paint("accent", "\u2502")} ${b} ${paint("accent", "\u2502")}`
|
|
1336
|
-
);
|
|
1337
|
-
}
|
|
1338
|
-
lines.push(chalk2.dim("\u2514" + "\u2500".repeat(Math.max(8, cell)) + "\u2518 \u2514" + "\u2500".repeat(Math.max(8, cell)) + "\u2518"));
|
|
1339
|
-
return lines;
|
|
1340
|
-
}
|
|
1341
|
-
function renderGauge(score, inner, status) {
|
|
1342
|
-
const st = status ?? (score >= 80 ? "green" : score >= 60 ? "yellow" : "red");
|
|
1343
|
-
const bar = scoreBar(score, st === "neutral" ? "yellow" : st, Math.min(28, inner - 12));
|
|
1344
|
-
return [`${statusDot(st)} ${bar} ${bold(String(Math.round(score)))}`];
|
|
1345
|
-
}
|
|
1346
|
-
function renderSplit(bars, inner) {
|
|
1347
|
-
if (bars.length < 2) return renderBars(bars, inner);
|
|
1348
|
-
const total = bars.reduce((s, b) => s + b.value, 0) || 100;
|
|
1349
|
-
const width = Math.max(16, Math.min(40, inner - 4));
|
|
1350
|
-
let used = 0;
|
|
1351
|
-
const parts = [];
|
|
1352
|
-
for (let i = 0; i < bars.length; i++) {
|
|
1353
|
-
const b = bars[i];
|
|
1354
|
-
const w = i === bars.length - 1 ? width - used : Math.max(1, Math.round(b.value / total * width));
|
|
1355
|
-
used += w;
|
|
1356
|
-
parts.push(tonePaint(b.tone)("\u2588".repeat(w)));
|
|
1357
|
-
}
|
|
1358
|
-
const legend = bars.map((b) => `${tonePaint(b.tone)("\u25CF")} ${b.label} ${chalk2.dim(`${Math.round(b.value)}%`)}`).join(" ");
|
|
1359
|
-
return [parts.join(""), truncateVisible(legend, inner)];
|
|
1360
|
-
}
|
|
1361
|
-
function renderVisual(visual, inner) {
|
|
1362
|
-
const lines = [];
|
|
1363
|
-
switch (visual.kind) {
|
|
1364
|
-
case "bars":
|
|
1365
|
-
if (visual.bars?.length) lines.push(...renderBars(visual.bars, inner));
|
|
1366
|
-
break;
|
|
1367
|
-
case "funnel":
|
|
1368
|
-
if (visual.funnel?.length) lines.push(...renderFunnel(visual.funnel, inner));
|
|
1369
|
-
break;
|
|
1370
|
-
case "waterfall":
|
|
1371
|
-
if (visual.waterfall?.length) lines.push(...renderWaterfall(visual.waterfall, inner));
|
|
1372
|
-
break;
|
|
1373
|
-
case "layer_stack":
|
|
1374
|
-
if (visual.layers?.length) lines.push(...renderLayerStack(visual.layers, inner));
|
|
1375
|
-
break;
|
|
1376
|
-
case "levers":
|
|
1377
|
-
if (visual.levers?.length) lines.push(...renderLevers(visual.levers, inner));
|
|
1378
|
-
break;
|
|
1379
|
-
case "gauge":
|
|
1380
|
-
lines.push(...renderGauge(visual.gauge ?? 50, inner));
|
|
1381
|
-
if (visual.bars?.length) lines.push(...renderBars(visual.bars, inner));
|
|
1382
|
-
break;
|
|
1383
|
-
case "split":
|
|
1384
|
-
if (visual.bars?.length) lines.push(...renderSplit(visual.bars, inner));
|
|
1385
|
-
break;
|
|
1386
|
-
case "none":
|
|
1387
|
-
default:
|
|
1388
|
-
break;
|
|
1389
|
-
}
|
|
1390
|
-
if (visual.caption) {
|
|
1391
|
-
lines.push(chalk2.dim(truncateVisible(visual.caption, inner)));
|
|
1392
|
-
}
|
|
1393
|
-
return lines;
|
|
1394
|
-
}
|
|
1395
|
-
function kindBadge(explainer) {
|
|
1396
|
-
if (explainer.kind === "vital") return badge("VITAL", "accent");
|
|
1397
|
-
return badge("SAAS", "info");
|
|
1398
|
-
}
|
|
1399
|
-
function statusToTone(status) {
|
|
1400
|
-
if (status === "green") return "success";
|
|
1401
|
-
if (status === "yellow") return "warning";
|
|
1402
|
-
if (status === "red") return "error";
|
|
1403
|
-
return "muted";
|
|
1404
|
-
}
|
|
1405
|
-
function pushWrapped(out, text, inner, indent = "") {
|
|
1406
|
-
for (const w of wrapWords(text, inner - indent.length)) {
|
|
1407
|
-
out.push(indent + w);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
function buildSlideContent(explainer, opts = {}) {
|
|
1411
|
-
const width = slideCardWidth();
|
|
1412
|
-
const inner = width - 4;
|
|
1413
|
-
const lines = [];
|
|
1414
|
-
const title = opts.titleOverride ?? (explainer ? explainer.label : "Metrics");
|
|
1415
|
-
if (explainer) {
|
|
1416
|
-
const headerBits = [
|
|
1417
|
-
kindBadge(explainer),
|
|
1418
|
-
chalk2.dim(explainer.group)
|
|
1419
|
-
];
|
|
1420
|
-
if (opts.index != null && opts.total != null) {
|
|
1421
|
-
headerBits.push(chalk2.dim(`slide ${opts.index}/${opts.total}`));
|
|
1422
|
-
}
|
|
1423
|
-
lines.push(headerBits.join(chalk2.dim(" \xB7 ")));
|
|
1424
|
-
lines.push(chalk2.dim(explainer.tagline));
|
|
1425
|
-
lines.push("");
|
|
1426
|
-
if (opts.live) {
|
|
1427
|
-
const live = opts.live;
|
|
1428
|
-
const tone = statusToTone(live.status);
|
|
1429
|
-
const liveLine = `${statusDot(live.status)} ${bold("Your reading:")} ${bold(live.formatted)} ` + badge(String(live.status), tone);
|
|
1430
|
-
lines.push(truncateVisible(liveLine, inner));
|
|
1431
|
-
if (live.dollarLine) {
|
|
1432
|
-
lines.push(chalk2.dim(` $ ${live.dollarLine}`));
|
|
1433
|
-
}
|
|
1434
|
-
if (live.benchmarkNote) {
|
|
1435
|
-
lines.push(chalk2.dim(` ${live.benchmarkNote}`));
|
|
1436
|
-
}
|
|
1437
|
-
lines.push("");
|
|
1438
|
-
} else {
|
|
1439
|
-
const hint = explainer.benchmarkHint?.(opts.motion);
|
|
1440
|
-
if (hint) {
|
|
1441
|
-
lines.push(chalk2.dim(`Benchmark \xB7 ${hint}`));
|
|
1442
|
-
lines.push("");
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
const visual = opts.visualOverride ?? explainer.visual;
|
|
1446
|
-
const visLines = renderVisual(visual, inner);
|
|
1447
|
-
if (visLines.length) {
|
|
1448
|
-
lines.push(...visLines);
|
|
1449
|
-
lines.push("");
|
|
1450
|
-
}
|
|
1451
|
-
lines.push(sectionHeading("What it means"));
|
|
1452
|
-
pushWrapped(lines, explainer.meaning, inner, " ");
|
|
1453
|
-
lines.push("");
|
|
1454
|
-
if (!opts.skipFormula) {
|
|
1455
|
-
lines.push(sectionHeading("How it's calculated"));
|
|
1456
|
-
pushWrapped(lines, explainer.how_computed, inner, " ");
|
|
1457
|
-
for (const f of explainer.formula_lines) {
|
|
1458
|
-
lines.push(paint("accent", ` ${f}`));
|
|
1459
|
-
}
|
|
1460
|
-
lines.push("");
|
|
1461
|
-
}
|
|
1462
|
-
if (opts.deepdive) {
|
|
1463
|
-
lines.push(sectionHeading("Deep dive"));
|
|
1464
|
-
pushWrapped(lines, explainer.expert_read, inner, " ");
|
|
1465
|
-
lines.push("");
|
|
1466
|
-
for (const bullet of explainer.deepdive) {
|
|
1467
|
-
pushWrapped(lines, `\xB7 ${bullet}`, inner, " ");
|
|
1468
|
-
}
|
|
1469
|
-
if (explainer.play_id) {
|
|
1470
|
-
lines.push("");
|
|
1471
|
-
lines.push(
|
|
1472
|
-
chalk2.dim(" Play: ") + paint("accent", explainer.play_id)
|
|
1473
|
-
);
|
|
1474
|
-
}
|
|
1475
|
-
lines.push("");
|
|
1476
|
-
}
|
|
1477
|
-
} else if (opts.visualOverride) {
|
|
1478
|
-
const visLines = renderVisual(opts.visualOverride, inner);
|
|
1479
|
-
if (visLines.length) {
|
|
1480
|
-
lines.push(...visLines);
|
|
1481
|
-
lines.push("");
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
if (opts.extraLines?.length) {
|
|
1485
|
-
for (const line of opts.extraLines) {
|
|
1486
|
-
if (line === "") lines.push("");
|
|
1487
|
-
else pushWrapped(lines, line, inner);
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
return { title, lines, width, inner };
|
|
1491
|
-
}
|
|
1492
|
-
function renderMetricSlide(explainer, opts = {}) {
|
|
1493
|
-
const { title, lines, width, inner } = buildSlideContent(explainer, opts);
|
|
1494
|
-
const border = (s) => paint("border", s);
|
|
1495
|
-
const termW = termWidth();
|
|
1496
|
-
const outerPad = " ".repeat(Math.max(0, Math.floor((termW - width) / 2)));
|
|
1497
|
-
const out = [];
|
|
1498
|
-
out.push("");
|
|
1499
|
-
out.push(`${outerPad}${border(`\u256D${"\u2500".repeat(width - 2)}\u256E`)}`);
|
|
1500
|
-
out.push(
|
|
1501
|
-
`${outerPad}${border("\u2502 ")}${padRight(sectionHeading(title), inner)}${border(" \u2502")}`
|
|
1502
|
-
);
|
|
1503
|
-
out.push(`${outerPad}${border(`\u251C${"\u2500".repeat(width - 2)}\u2524`)}`);
|
|
1504
|
-
for (const row of lines) {
|
|
1505
|
-
out.push(
|
|
1506
|
-
`${outerPad}${border("\u2502 ")}${padRight(truncateVisible(row, inner), inner)}${border(" \u2502")}`
|
|
1507
|
-
);
|
|
1508
|
-
}
|
|
1509
|
-
const footer = opts.footer ?? (opts.deepdive ? `\u23CE next \xB7 b back \xB7 q quit` : `\u23CE next \xB7 /deepdive more \xB7 b back \xB7 q quit`);
|
|
1510
|
-
out.push(`${outerPad}${border(`\u251C${"\u2500".repeat(width - 2)}\u2524`)}`);
|
|
1511
|
-
out.push(
|
|
1512
|
-
`${outerPad}${border("\u2502 ")}${padRight(chalk2.dim(truncateVisible(footer, inner)), inner)}${border(" \u2502")}`
|
|
1513
|
-
);
|
|
1514
|
-
out.push(`${outerPad}${border(`\u2570${"\u2500".repeat(width - 2)}\u256F`)}`);
|
|
1515
|
-
out.push("");
|
|
1516
|
-
if (opts.asLines) return out;
|
|
1517
|
-
for (const line of out) console.log(line);
|
|
1518
|
-
}
|
|
1519
|
-
function measureSlideWidth(lines) {
|
|
1520
|
-
return Math.max(0, ...lines.map(visibleWidth));
|
|
1521
|
-
}
|
|
1522
|
-
var init_slides = __esm({
|
|
1523
|
-
"src/ui/slides.ts"() {
|
|
1524
|
-
"use strict";
|
|
1525
|
-
init_theme();
|
|
1526
|
-
init_layout();
|
|
1527
|
-
}
|
|
1528
|
-
});
|
|
1529
|
-
|
|
1530
|
-
// src/ai/llm/thread-compat.ts
|
|
1531
|
-
var init_thread_compat = __esm({
|
|
1532
|
-
"src/ai/llm/thread-compat.ts"() {
|
|
1533
|
-
"use strict";
|
|
1534
|
-
}
|
|
1535
|
-
});
|
|
1536
|
-
|
|
1537
|
-
// src/io/context.ts
|
|
1538
|
-
var init_context = __esm({
|
|
1539
|
-
"src/io/context.ts"() {
|
|
1540
|
-
"use strict";
|
|
1541
|
-
}
|
|
1542
|
-
});
|
|
1543
|
-
|
|
1544
|
-
// src/config/store.ts
|
|
1545
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
1546
|
-
import { homedir } from "os";
|
|
1547
|
-
import { join, resolve } from "path";
|
|
1548
|
-
function ntrpHome() {
|
|
1549
|
-
return NTRP_DIR;
|
|
1550
|
-
}
|
|
1551
|
-
function chmodQuiet(path, mode) {
|
|
1552
|
-
try {
|
|
1553
|
-
chmodSync(path, mode);
|
|
1554
|
-
} catch {
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
function ensureDir() {
|
|
1558
|
-
if (!existsSync(NTRP_DIR)) {
|
|
1559
|
-
mkdirSync(NTRP_DIR, { recursive: true, mode: 448 });
|
|
1560
|
-
}
|
|
1561
|
-
chmodQuiet(NTRP_DIR, 448);
|
|
1562
|
-
}
|
|
1563
|
-
function writePrivateFile(path, contents) {
|
|
1564
|
-
ensureDir();
|
|
1565
|
-
writeFileSync(path, contents, { encoding: "utf-8", mode: 384 });
|
|
1566
|
-
chmodQuiet(path, 384);
|
|
1567
|
-
}
|
|
1568
|
-
function loadConfig() {
|
|
1569
|
-
if (cachedConfig) return cachedConfig;
|
|
1570
|
-
ensureDir();
|
|
1571
|
-
if (!existsSync(CONFIG_PATH)) {
|
|
1572
|
-
cachedConfig = {};
|
|
1573
|
-
return cachedConfig;
|
|
1574
|
-
}
|
|
1575
|
-
try {
|
|
1576
|
-
cachedConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
1577
|
-
} catch {
|
|
1578
|
-
cachedConfig = {};
|
|
1579
|
-
}
|
|
1580
|
-
return cachedConfig;
|
|
1581
|
-
}
|
|
1582
|
-
function saveConfig(config) {
|
|
1583
|
-
ensureDir();
|
|
1584
|
-
writePrivateFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
1585
|
-
cachedConfig = config;
|
|
1586
|
-
}
|
|
1587
|
-
function getConfigValue(key) {
|
|
1588
|
-
if (key === "api-key") return loadConfig()["api-key"];
|
|
1589
|
-
if (key === "license-key") return process.env.NTRP_LICENSE_KEY ?? loadConfig()["license-key"];
|
|
1590
|
-
const config = loadConfig();
|
|
1591
|
-
return config[key];
|
|
1592
|
-
}
|
|
1593
|
-
function setConfigValue(key, value) {
|
|
1594
|
-
const config = loadConfig();
|
|
1595
|
-
config[key] = value;
|
|
1596
|
-
saveConfig(config);
|
|
1597
|
-
}
|
|
1598
|
-
function deleteConfigValue(key) {
|
|
1599
|
-
const config = loadConfig();
|
|
1600
|
-
delete config[key];
|
|
1601
|
-
saveConfig(config);
|
|
1602
|
-
}
|
|
1603
|
-
var NTRP_DIR, CONFIG_PATH, cachedConfig;
|
|
1604
|
-
var init_store = __esm({
|
|
1605
|
-
"src/config/store.ts"() {
|
|
1606
|
-
"use strict";
|
|
1607
|
-
NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join(homedir(), ".ntrp");
|
|
1608
|
-
CONFIG_PATH = join(NTRP_DIR, "config.json");
|
|
1609
|
-
cachedConfig = null;
|
|
1610
|
-
}
|
|
1611
|
-
});
|
|
1612
|
-
|
|
1613
|
-
// src/services/terminal-capture.ts
|
|
1614
|
-
var init_terminal_capture = __esm({
|
|
1615
|
-
"src/services/terminal-capture.ts"() {
|
|
1616
|
-
"use strict";
|
|
1617
|
-
}
|
|
1618
|
-
});
|
|
1619
|
-
|
|
1620
|
-
// src/output/redact-write.ts
|
|
1621
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1622
|
-
import { dirname } from "path";
|
|
1623
|
-
var init_redact_write = __esm({
|
|
1624
|
-
"src/output/redact-write.ts"() {
|
|
1625
|
-
"use strict";
|
|
1626
|
-
init_terminal_capture();
|
|
1627
|
-
}
|
|
1628
|
-
});
|
|
1629
|
-
|
|
1630
|
-
// src/output/path-safety.ts
|
|
1631
|
-
import { homedir as homedir2 } from "os";
|
|
1632
|
-
import { resolve as resolve2, sep } from "path";
|
|
1633
|
-
var NTRP_HOME;
|
|
1634
|
-
var init_path_safety = __esm({
|
|
1635
|
-
"src/output/path-safety.ts"() {
|
|
1636
|
-
"use strict";
|
|
1637
|
-
init_store();
|
|
1638
|
-
NTRP_HOME = ntrpHome();
|
|
1639
|
-
}
|
|
1640
|
-
});
|
|
1641
|
-
|
|
1642
|
-
// src/services/export-kinds.ts
|
|
1643
|
-
var init_export_kinds = __esm({
|
|
1644
|
-
"src/services/export-kinds.ts"() {
|
|
1645
|
-
"use strict";
|
|
1646
|
-
}
|
|
1647
|
-
});
|
|
1648
|
-
|
|
1649
|
-
// src/services/handoff-skill.ts
|
|
1650
|
-
import { mkdirSync as mkdirSync3 } from "fs";
|
|
1651
|
-
import { basename, join as join2 } from "path";
|
|
1652
|
-
import { homedir as homedir3 } from "os";
|
|
1653
|
-
import chalk3 from "chalk";
|
|
1654
|
-
var init_handoff_skill = __esm({
|
|
1655
|
-
"src/services/handoff-skill.ts"() {
|
|
1656
|
-
"use strict";
|
|
1657
|
-
init_redact_write();
|
|
1658
|
-
init_store();
|
|
1659
|
-
init_theme();
|
|
1660
|
-
init_export_kinds();
|
|
1661
|
-
}
|
|
1662
|
-
});
|
|
1663
|
-
|
|
1664
|
-
// src/services/exports-registry.ts
|
|
1665
|
-
import {
|
|
1666
|
-
appendFileSync,
|
|
1667
|
-
copyFileSync,
|
|
1668
|
-
cpSync,
|
|
1669
|
-
existsSync as existsSync2,
|
|
1670
|
-
mkdirSync as mkdirSync4,
|
|
1671
|
-
readFileSync as readFileSync2,
|
|
1672
|
-
readdirSync,
|
|
1673
|
-
renameSync,
|
|
1674
|
-
rmSync,
|
|
1675
|
-
statSync,
|
|
1676
|
-
writeFileSync as writeFileSync3
|
|
1677
|
-
} from "fs";
|
|
1678
|
-
import { basename as basename2, dirname as dirname2, join as join3, resolve as resolve3, sep as sep2 } from "path";
|
|
1679
|
-
import { randomUUID } from "crypto";
|
|
1680
|
-
var init_exports_registry = __esm({
|
|
1681
|
-
"src/services/exports-registry.ts"() {
|
|
1682
|
-
"use strict";
|
|
1683
|
-
init_redact_write();
|
|
1684
|
-
init_store();
|
|
1685
|
-
init_path_safety();
|
|
1686
|
-
init_export_kinds();
|
|
1687
|
-
init_handoff_skill();
|
|
1688
|
-
init_export_kinds();
|
|
1689
|
-
}
|
|
1690
|
-
});
|
|
1691
|
-
|
|
1692
|
-
// src/services/context-doc.ts
|
|
1693
|
-
import { writeFileSync as writeFileSync4 } from "fs";
|
|
1694
|
-
var init_context_doc = __esm({
|
|
1695
|
-
"src/services/context-doc.ts"() {
|
|
1696
|
-
"use strict";
|
|
1697
|
-
init_context2();
|
|
1698
|
-
init_formatters();
|
|
1699
|
-
init_store();
|
|
1700
|
-
init_exports_registry();
|
|
1701
|
-
init_terminal_capture();
|
|
1702
|
-
}
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
// src/services/transcript.ts
|
|
1706
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync5, rmSync as rmSync2 } from "fs";
|
|
1707
|
-
import { join as join4 } from "path";
|
|
1708
|
-
var init_transcript = __esm({
|
|
1709
|
-
"src/services/transcript.ts"() {
|
|
1710
|
-
"use strict";
|
|
1711
|
-
init_context2();
|
|
1712
|
-
init_terminal_capture();
|
|
1713
|
-
}
|
|
1714
|
-
});
|
|
1715
|
-
|
|
1716
|
-
// src/cli/context.ts
|
|
1717
|
-
import { basename as basename3, join as join5, resolve as resolve4, sep as sep3 } from "path";
|
|
1718
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, rmSync as rmSync3 } from "fs";
|
|
1719
|
-
import { homedir as homedir4 } from "os";
|
|
1720
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
1721
|
-
function ntrpHomeDir() {
|
|
1722
|
-
return process.env.NTRP_HOME ? resolve4(process.env.NTRP_HOME) : join5(homedir4(), ".ntrp");
|
|
1723
|
-
}
|
|
1724
|
-
function getSessionsDir() {
|
|
1725
|
-
const dir = join5(ntrpHomeDir(), "sessions");
|
|
1726
|
-
if (!existsSync4(dir)) {
|
|
1727
|
-
mkdirSync5(dir, { recursive: true });
|
|
1728
|
-
}
|
|
1729
|
-
return dir;
|
|
1730
|
-
}
|
|
1731
|
-
function isValidSessionId(id) {
|
|
1732
|
-
return SESSION_ID_RE.test(id);
|
|
1733
|
-
}
|
|
1734
|
-
function listSessions(opts) {
|
|
1735
|
-
const dir = getSessionsDir();
|
|
1736
|
-
const entries = [];
|
|
1737
|
-
try {
|
|
1738
|
-
const files = readdirSync2(dir).filter((name) => name.endsWith(".json")).map((name) => {
|
|
1739
|
-
const filePath = join5(dir, name);
|
|
1740
|
-
return { name, filePath, mtime: statSync2(filePath).mtimeMs };
|
|
1741
|
-
}).sort((a, b) => b.mtime - a.mtime);
|
|
1742
|
-
const filesToRead = opts?.limit ? files.slice(0, opts.limit) : files;
|
|
1743
|
-
for (const { name, filePath, mtime } of filesToRead) {
|
|
1744
|
-
const id = basename3(name, ".json");
|
|
1745
|
-
if (!isValidSessionId(id)) continue;
|
|
1746
|
-
try {
|
|
1747
|
-
const raw = readFileSync4(filePath, "utf-8");
|
|
1748
|
-
const session = JSON.parse(raw);
|
|
1749
|
-
entries.push({
|
|
1750
|
-
id,
|
|
1751
|
-
created_at: session.created_at,
|
|
1752
|
-
ended_at: session.ended_at,
|
|
1753
|
-
exchange_count: session.exchange_count ?? Math.floor(session.messages.length / 2),
|
|
1754
|
-
summary: session.summary,
|
|
1755
|
-
name: session.name,
|
|
1756
|
-
stage: session.stage,
|
|
1757
|
-
dataset: session.dataset,
|
|
1758
|
-
deliverables: session.deliverables,
|
|
1759
|
-
analysis: session.analysis,
|
|
1760
|
-
scope: session.scope,
|
|
1761
|
-
mtime
|
|
1762
|
-
});
|
|
1763
|
-
} catch {
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
} catch {
|
|
1767
|
-
return [];
|
|
1768
|
-
}
|
|
1769
|
-
entries.sort((a, b) => b.mtime - a.mtime);
|
|
1770
|
-
if (opts?.limit) return entries.slice(0, opts.limit);
|
|
1771
|
-
return entries;
|
|
1772
|
-
}
|
|
1773
|
-
var STALE_SESSION_MS, SESSION_ID_RE;
|
|
1774
|
-
var init_context2 = __esm({
|
|
1775
|
-
"src/cli/context.ts"() {
|
|
1776
|
-
"use strict";
|
|
1777
|
-
init_thread_compat();
|
|
1778
|
-
init_context();
|
|
1779
|
-
init_context_doc();
|
|
1780
|
-
init_transcript();
|
|
1781
|
-
STALE_SESSION_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
1782
|
-
SESSION_ID_RE = /^\d{4}-\d{2}-\d{2}-[a-f0-9]{4}$/i;
|
|
1783
|
-
}
|
|
1784
|
-
});
|
|
1785
|
-
|
|
1786
|
-
// src/config/profile.ts
|
|
1787
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync7, existsSync as existsSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1788
|
-
import { join as join6 } from "path";
|
|
1789
|
-
var NTRP_DIR2, PROFILE_PATH;
|
|
1790
|
-
var init_profile = __esm({
|
|
1791
|
-
"src/config/profile.ts"() {
|
|
1792
|
-
"use strict";
|
|
1793
|
-
init_store();
|
|
1794
|
-
NTRP_DIR2 = ntrpHome();
|
|
1795
|
-
PROFILE_PATH = join6(NTRP_DIR2, "profile.json");
|
|
1796
|
-
}
|
|
1797
|
-
});
|
|
1798
|
-
|
|
1799
|
-
// src/conversation/keyless-definitions.ts
|
|
1800
|
-
import chalk4 from "chalk";
|
|
1801
|
-
function isPossessiveMetricAsk(input) {
|
|
1802
|
-
return POSSESSIVE_RE.test(input.trim());
|
|
1803
|
-
}
|
|
1804
|
-
function isDefinitionAsk(input) {
|
|
1805
|
-
const line = input.trim();
|
|
1806
|
-
if (!line) return false;
|
|
1807
|
-
if (isPossessiveMetricAsk(line)) return false;
|
|
1808
|
-
return DEFINITION_RE.test(line) || MEAN_RE.test(line);
|
|
1809
|
-
}
|
|
1810
|
-
function extractDefinitionQuery(input) {
|
|
1811
|
-
const line = input.trim().replace(/[?.!]+$/, "");
|
|
1812
|
-
const mean = line.match(MEAN_RE);
|
|
1813
|
-
if (mean?.[1]) return cleanQuery(mean[1]);
|
|
1814
|
-
const how = line.match(HOW_CALC_RE);
|
|
1815
|
-
if (how?.[1]) return cleanQuery(how[1]);
|
|
1816
|
-
const what = line.match(WHAT_IS_RE);
|
|
1817
|
-
if (what?.[1]) return cleanQuery(what[1]);
|
|
1818
|
-
return void 0;
|
|
1819
|
-
}
|
|
1820
|
-
function cleanQuery(raw) {
|
|
1821
|
-
return raw.replace(/^(?:a|an|the|our|my)\s+/i, "").replace(/\b(metric|score|number|vital(?:\s+sign)?|kpi)\b/gi, "").replace(/\s+/g, " ").trim();
|
|
1822
|
-
}
|
|
1823
|
-
function matchDefinitionExplainer(input) {
|
|
1824
|
-
if (!isDefinitionAsk(input)) return void 0;
|
|
1825
|
-
const query = extractDefinitionQuery(input);
|
|
1826
|
-
if (!query) return void 0;
|
|
1827
|
-
const id = resolveMetricId(query);
|
|
1828
|
-
if (!id) {
|
|
1829
|
-
const tokens = query.split(/\s+/);
|
|
1830
|
-
for (let n = tokens.length; n >= 1; n--) {
|
|
1831
|
-
for (let i = 0; i + n <= tokens.length; i++) {
|
|
1832
|
-
const slice = tokens.slice(i, i + n).join(" ");
|
|
1833
|
-
const hit = resolveMetricId(slice);
|
|
1834
|
-
if (hit) return getMetricExplainer(hit);
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
return void 0;
|
|
1838
|
-
}
|
|
1839
|
-
return getMetricExplainer(id);
|
|
1840
|
-
}
|
|
1841
|
-
var POSSESSIVE_RE, DEFINITION_RE, MEAN_RE, HOW_CALC_RE, WHAT_IS_RE;
|
|
1842
|
-
var init_keyless_definitions = __esm({
|
|
1843
|
-
"src/conversation/keyless-definitions.ts"() {
|
|
1844
|
-
"use strict";
|
|
1845
|
-
init_context2();
|
|
1846
|
-
init_profile();
|
|
1847
|
-
init_metric_definitions();
|
|
1848
|
-
init_theme();
|
|
1849
|
-
init_layout();
|
|
1850
|
-
POSSESSIVE_RE = /\b(our|my|we|us|the company'?s|this (company|business|org|pipeline)|current|actual|latest)\b/i;
|
|
1851
|
-
DEFINITION_RE = /^(?:(?:please|can you|could you)\s+)?(?:what\s+(?:is|are|does)|what'?s|whats|define|explain|describe|how\s+(?:is|are|do(?:es)?)\s+(?:.+?\s+)?(?:calculated|computed|measured|defined)|how\s+do(?:es)?\s+(?:.+?\s+)?(?:work|get calculated)|tell me about|meaning of)\b/i;
|
|
1852
|
-
MEAN_RE = /\bwhat does\b(.+?)\bmean\b/i;
|
|
1853
|
-
HOW_CALC_RE = /\bhow (?:is|are|do(?:es)?)\b(.+?)\b(?:calculated|computed|measured|defined|work)\b/i;
|
|
1854
|
-
WHAT_IS_RE = /\b(?:what(?:'s|s)?|define|explain|describe|tell me about|meaning of)\s+(.+?)(?:\?|$)/i;
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
|
|
1858
|
-
// src/services/metric-explainers.ts
|
|
1859
|
-
function normalizeAudience(audience) {
|
|
1860
|
-
if (!audience) return "board";
|
|
1861
|
-
const a = String(audience).toLowerCase();
|
|
1862
|
-
if (a === "ops" || a === "operations" || a === "operator" || a === "team") {
|
|
1863
|
-
return "ops";
|
|
1864
|
-
}
|
|
1865
|
-
return "board";
|
|
1866
|
-
}
|
|
1867
|
-
function audienceLabel(audience) {
|
|
1868
|
-
return audience === "ops" ? "ops" : "board / exec";
|
|
1869
|
-
}
|
|
1870
|
-
function statusRankFrom(status) {
|
|
1871
|
-
if (status === "red") return 0;
|
|
1872
|
-
if (status === "yellow") return 1;
|
|
1873
|
-
if (status === "green") return 2;
|
|
1874
|
-
return 3;
|
|
1875
|
-
}
|
|
1876
|
-
function collectCandidates(bundle, opts) {
|
|
1877
|
-
const prefer = new Set(opts.prefer ?? []);
|
|
1878
|
-
const map = /* @__PURE__ */ new Map();
|
|
1879
|
-
const upsert = (id, priority, status) => {
|
|
1880
|
-
if (!getMetricExplainer(id)) return;
|
|
1881
|
-
const existing = map.get(id);
|
|
1882
|
-
const rank = statusRankFrom(status);
|
|
1883
|
-
if (!existing) {
|
|
1884
|
-
map.set(id, { id, priority, statusRank: rank });
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
existing.priority = Math.min(existing.priority, priority);
|
|
1888
|
-
existing.statusRank = Math.min(existing.statusRank, rank);
|
|
1889
|
-
};
|
|
1890
|
-
const health = bundle?.diagnosis?.health;
|
|
1891
|
-
const fromDiag = opts.vitals ?? health?.vital_signs ?? [];
|
|
1892
|
-
const gating = opts.prefer?.[0] ?? health?.gating_vital_sign;
|
|
1893
|
-
if (gating) upsert(String(gating), 0, "red");
|
|
1894
|
-
for (const vs of fromDiag) {
|
|
1895
|
-
const id = vs.vital_sign;
|
|
1896
|
-
upsert(id, prefer.has(id) ? 0 : 1, vs.status);
|
|
1897
|
-
}
|
|
1898
|
-
const rawMetrics = opts.metrics ?? bundle?.metrics?.metrics ?? [];
|
|
1899
|
-
let sawMetrics = rawMetrics.length > 0;
|
|
1900
|
-
for (const row of rawMetrics) {
|
|
1901
|
-
const id = String(row.metric ?? row.metric ?? "");
|
|
1902
|
-
if (!id) continue;
|
|
1903
|
-
const status = String(row.status ?? row.status ?? "");
|
|
1904
|
-
const value = row.value ?? row.value;
|
|
1905
|
-
const unavailable = row.unavailable_reason ?? row.unavailable_reason;
|
|
1906
|
-
if (value == null && unavailable) continue;
|
|
1907
|
-
upsert(id, prefer.has(id) ? 0 : 2, status);
|
|
1908
|
-
}
|
|
1909
|
-
if (sawMetrics || bundle?.metrics) {
|
|
1910
|
-
for (const id of ["arr", "nrr", "pipeline_coverage", "win_rate"]) {
|
|
1911
|
-
if (!map.has(id) && getMetricExplainer(id)) {
|
|
1912
|
-
upsert(id, 3, "neutral");
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
if (map.size === 0) {
|
|
1917
|
-
for (const id of ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"]) {
|
|
1918
|
-
upsert(id, 4, "neutral");
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
return [...map.values()].sort((a, b) => {
|
|
1922
|
-
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
1923
|
-
if (a.statusRank !== b.statusRank) return a.statusRank - b.statusRank;
|
|
1924
|
-
return a.id.localeCompare(b.id);
|
|
1925
|
-
});
|
|
1926
|
-
}
|
|
1927
|
-
function formatEntry(explainer, audience) {
|
|
1928
|
-
const framing = audience === "ops" ? explainer.audience.ops : explainer.audience.board;
|
|
1929
|
-
const lines = [];
|
|
1930
|
-
lines.push(`### ${explainer.label} (\`${explainer.id}\`)`);
|
|
1931
|
-
lines.push("");
|
|
1932
|
-
lines.push(framing);
|
|
1933
|
-
lines.push("");
|
|
1934
|
-
if (audience === "ops") {
|
|
1935
|
-
lines.push("**How it is calculated**");
|
|
1936
|
-
lines.push("");
|
|
1937
|
-
for (const f of explainer.formula_lines) {
|
|
1938
|
-
lines.push(`- \`${f}\``);
|
|
1939
|
-
}
|
|
1940
|
-
lines.push("");
|
|
1941
|
-
} else {
|
|
1942
|
-
lines.push(`*${explainer.tagline}*`);
|
|
1943
|
-
lines.push("");
|
|
1944
|
-
}
|
|
1945
|
-
return lines;
|
|
1946
|
-
}
|
|
1947
|
-
function buildDefinitionsAppendix(bundle, opts = {}) {
|
|
1948
|
-
const audience = normalizeAudience(opts.audience);
|
|
1949
|
-
const cap = opts.cap ?? DEFAULT_CAP;
|
|
1950
|
-
const candidates = collectCandidates(bundle, opts).slice(0, cap);
|
|
1951
|
-
if (candidates.length === 0) return "";
|
|
1952
|
-
const lines = [];
|
|
1953
|
-
lines.push(`## Metric definitions (for the ${audienceLabel(audience)})`);
|
|
1954
|
-
lines.push("");
|
|
1955
|
-
lines.push(
|
|
1956
|
-
audience === "ops" ? "Formula-first brief for operators who execute the plan. Full slides: `/deepdive <metric>`." : "Meaning-first brief for the room. Full slides: `/deepdive <metric>`."
|
|
1957
|
-
);
|
|
1958
|
-
lines.push("");
|
|
1959
|
-
for (const c of candidates) {
|
|
1960
|
-
const explainer = getMetricExplainer(c.id);
|
|
1961
|
-
if (!explainer) continue;
|
|
1962
|
-
lines.push(...formatEntry(explainer, audience));
|
|
1963
|
-
}
|
|
1964
|
-
return lines.join("\n");
|
|
1965
|
-
}
|
|
1966
|
-
function injectDefinitionsAppendix(markdown, appendix) {
|
|
1967
|
-
if (!appendix.trim()) return markdown;
|
|
1968
|
-
const block = appendix.trimEnd() + "\n";
|
|
1969
|
-
const footerIdx = markdown.lastIndexOf("\n---\n");
|
|
1970
|
-
if (footerIdx >= 0) {
|
|
1971
|
-
return markdown.slice(0, footerIdx + 1) + "\n" + block + "\n" + markdown.slice(footerIdx + 1);
|
|
1972
|
-
}
|
|
1973
|
-
return markdown.trimEnd() + "\n\n" + block + "\n";
|
|
1974
|
-
}
|
|
1975
|
-
var DEFAULT_CAP;
|
|
1976
|
-
var init_metric_explainers = __esm({
|
|
1977
|
-
"src/services/metric-explainers.ts"() {
|
|
1978
|
-
"use strict";
|
|
1979
|
-
init_metric_definitions();
|
|
1980
|
-
DEFAULT_CAP = 8;
|
|
1981
|
-
}
|
|
1982
|
-
});
|
|
1983
|
-
|
|
1984
|
-
// src/ui/spinner.ts
|
|
1985
|
-
import ora from "ora";
|
|
1986
|
-
var init_spinner = __esm({
|
|
1987
|
-
"src/ui/spinner.ts"() {
|
|
1988
|
-
"use strict";
|
|
1989
|
-
}
|
|
1990
|
-
});
|
|
1991
|
-
|
|
1992
|
-
// src/config/install.ts
|
|
1993
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
1994
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
1995
|
-
import { join as join7 } from "path";
|
|
1996
|
-
function installPath() {
|
|
1997
|
-
return join7(ntrpHome(), "install.json");
|
|
1998
|
-
}
|
|
1999
|
-
function ensureDir2() {
|
|
2000
|
-
const dir = ntrpHome();
|
|
2001
|
-
if (!existsSync6(dir)) {
|
|
2002
|
-
mkdirSync7(dir, { recursive: true });
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
function isValidInstall(value) {
|
|
2006
|
-
if (!value || typeof value !== "object") return false;
|
|
2007
|
-
const r = value;
|
|
2008
|
-
return r.schema_version === 1 && typeof r.install_id === "string" && r.install_id.length > 0 && typeof r.created_at === "string";
|
|
2009
|
-
}
|
|
2010
|
-
function ensureInstall() {
|
|
2011
|
-
if (cachedInstall) return cachedInstall;
|
|
2012
|
-
const path = installPath();
|
|
2013
|
-
if (existsSync6(path)) {
|
|
2014
|
-
try {
|
|
2015
|
-
const parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
2016
|
-
if (isValidInstall(parsed)) {
|
|
2017
|
-
cachedInstall = parsed;
|
|
2018
|
-
return parsed;
|
|
2019
|
-
}
|
|
2020
|
-
} catch {
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
const record = {
|
|
2024
|
-
schema_version: 1,
|
|
2025
|
-
install_id: randomUUID3(),
|
|
2026
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2027
|
-
};
|
|
2028
|
-
ensureDir2();
|
|
2029
|
-
writeFileSync8(path, JSON.stringify(record, null, 2) + "\n");
|
|
2030
|
-
cachedInstall = record;
|
|
2031
|
-
return record;
|
|
2032
|
-
}
|
|
2033
|
-
function getInstallId() {
|
|
2034
|
-
return ensureInstall().install_id;
|
|
2035
|
-
}
|
|
2036
|
-
var cachedInstall;
|
|
2037
|
-
var init_install = __esm({
|
|
2038
|
-
"src/config/install.ts"() {
|
|
2039
|
-
"use strict";
|
|
2040
|
-
init_store();
|
|
2041
|
-
cachedInstall = null;
|
|
2042
|
-
}
|
|
2043
|
-
});
|
|
2044
|
-
|
|
2045
|
-
// src/config/progress-migrate.ts
|
|
2046
|
-
import { existsSync as existsSync7, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync9 } from "fs";
|
|
2047
|
-
import { join as join8 } from "path";
|
|
2048
|
-
function legacyStatePath() {
|
|
2049
|
-
return join8(ntrpHome(), "state.json");
|
|
2050
|
-
}
|
|
2051
|
-
function legacyStateBackupPath() {
|
|
2052
|
-
return join8(ntrpHome(), "state.json.bak");
|
|
2053
|
-
}
|
|
2054
|
-
function progressPath() {
|
|
2055
|
-
return join8(ntrpHome(), "progress.json");
|
|
2056
|
-
}
|
|
2057
|
-
function isValidLegacyState(value) {
|
|
2058
|
-
if (!value || typeof value !== "object") return false;
|
|
2059
|
-
const s = value;
|
|
2060
|
-
return s.schema_version === 1 && typeof s.total_minutes_saved === "number" && Array.isArray(s.credits) && Array.isArray(s.milestones_unlocked);
|
|
2061
|
-
}
|
|
2062
|
-
function migrateLegacyStateIfNeeded(installId) {
|
|
2063
|
-
if (existsSync7(progressPath())) return null;
|
|
2064
|
-
const legacyPath = legacyStatePath();
|
|
2065
|
-
if (!existsSync7(legacyPath)) return null;
|
|
2066
|
-
try {
|
|
2067
|
-
const parsed = JSON.parse(readFileSync7(legacyPath, "utf-8"));
|
|
2068
|
-
if (!isValidLegacyState(parsed)) return null;
|
|
2069
|
-
const { schema_version: _v, ...rest } = parsed;
|
|
2070
|
-
const progress = {
|
|
2071
|
-
...rest,
|
|
2072
|
-
schema_version: 2,
|
|
2073
|
-
install_id: installId
|
|
2074
|
-
};
|
|
2075
|
-
writeFileSync9(progressPath(), JSON.stringify(progress, null, 2) + "\n");
|
|
2076
|
-
try {
|
|
2077
|
-
renameSync2(legacyPath, legacyStateBackupPath());
|
|
2078
|
-
} catch {
|
|
2079
|
-
}
|
|
2080
|
-
return progress;
|
|
2081
|
-
} catch {
|
|
2082
|
-
return null;
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
var init_progress_migrate = __esm({
|
|
2086
|
-
"src/config/progress-migrate.ts"() {
|
|
2087
|
-
"use strict";
|
|
2088
|
-
init_store();
|
|
2089
|
-
}
|
|
2090
|
-
});
|
|
2091
|
-
|
|
2092
|
-
// src/whimsy/usage-backfill.ts
|
|
2093
|
-
function isoWeekKey(d) {
|
|
2094
|
-
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
2095
|
-
const day = date.getUTCDay() || 7;
|
|
2096
|
-
date.setUTCDate(date.getUTCDate() + 4 - day);
|
|
2097
|
-
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
|
2098
|
-
const weekNo = Math.ceil(((date.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
2099
|
-
return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
|
|
2100
|
-
}
|
|
2101
|
-
function actionBase(action) {
|
|
2102
|
-
return action.split(":")[0] ?? action;
|
|
2103
|
-
}
|
|
2104
|
-
function bumpWeekly(weekly, week, patch) {
|
|
2105
|
-
const idx = weekly.findIndex((w) => w.week === week);
|
|
2106
|
-
const row = idx >= 0 ? { ...weekly[idx] } : { week, minutes_saved: 0, llm_calls: 0, actions: 0 };
|
|
2107
|
-
if (patch.minutes_saved) row.minutes_saved += patch.minutes_saved;
|
|
2108
|
-
if (patch.actions) row.actions += patch.actions;
|
|
2109
|
-
return idx >= 0 ? weekly.map((w, i) => i === idx ? row : w) : [...weekly, row];
|
|
2110
|
-
}
|
|
2111
|
-
function rebuildFromCredits(credits) {
|
|
2112
|
-
let diagnoses = 0;
|
|
2113
|
-
let metrics_runs = 0;
|
|
2114
|
-
let deliverables = 0;
|
|
2115
|
-
let nl_exchanges = 0;
|
|
2116
|
-
let weekly = [];
|
|
2117
|
-
let first_active_at;
|
|
2118
|
-
let last_active_at;
|
|
2119
|
-
for (const credit of credits) {
|
|
2120
|
-
if (!first_active_at || credit.at < first_active_at) first_active_at = credit.at;
|
|
2121
|
-
if (!last_active_at || credit.at > last_active_at) last_active_at = credit.at;
|
|
2122
|
-
const base = actionBase(credit.action);
|
|
2123
|
-
if (base === "diagnose" || base === "diagnose_findings") diagnoses++;
|
|
2124
|
-
if (base === "metrics" || base === "metrics_findings") metrics_runs++;
|
|
2125
|
-
if (base === "deliverable" || base === "deliverable_deck") deliverables++;
|
|
2126
|
-
if (base === "nl_answer") nl_exchanges++;
|
|
2127
|
-
const week = isoWeekKey(new Date(credit.at));
|
|
2128
|
-
weekly = bumpWeekly(weekly, week, { minutes_saved: credit.minutes, actions: 1 });
|
|
2129
|
-
}
|
|
2130
|
-
return {
|
|
2131
|
-
first_active_at,
|
|
2132
|
-
last_active_at,
|
|
2133
|
-
diagnoses,
|
|
2134
|
-
metrics_runs,
|
|
2135
|
-
deliverables,
|
|
2136
|
-
nl_exchanges,
|
|
2137
|
-
weekly
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
function mergeWeekly(existing, fromCredits) {
|
|
2141
|
-
const byWeek = /* @__PURE__ */ new Map();
|
|
2142
|
-
for (const row of fromCredits) {
|
|
2143
|
-
byWeek.set(row.week, { ...row });
|
|
2144
|
-
}
|
|
2145
|
-
for (const row of existing) {
|
|
2146
|
-
const prior = byWeek.get(row.week);
|
|
2147
|
-
if (prior) {
|
|
2148
|
-
byWeek.set(row.week, {
|
|
2149
|
-
week: row.week,
|
|
2150
|
-
minutes_saved: Math.max(prior.minutes_saved, row.minutes_saved),
|
|
2151
|
-
actions: Math.max(prior.actions, row.actions),
|
|
2152
|
-
llm_calls: row.llm_calls
|
|
2153
|
-
});
|
|
2154
|
-
} else {
|
|
2155
|
-
byWeek.set(row.week, { ...row });
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
return [...byWeek.values()].sort((a, b) => a.week.localeCompare(b.week));
|
|
2159
|
-
}
|
|
2160
|
-
function migrateUsageIfNeeded(state) {
|
|
2161
|
-
if (state.credits.length === 0) return { state, changed: false };
|
|
2162
|
-
if (state.usage?.first_active_at) return { state, changed: false };
|
|
2163
|
-
const fromCredits = rebuildFromCredits(state.credits);
|
|
2164
|
-
const prior = state.usage;
|
|
2165
|
-
const usage = {
|
|
2166
|
-
sessions_closed: prior?.sessions_closed ?? 0,
|
|
2167
|
-
llm_calls: prior?.llm_calls ?? 0,
|
|
2168
|
-
input_tokens: prior?.input_tokens ?? 0,
|
|
2169
|
-
output_tokens: prior?.output_tokens ?? 0,
|
|
2170
|
-
...fromCredits,
|
|
2171
|
-
weekly: mergeWeekly(prior?.weekly ?? [], fromCredits.weekly)
|
|
2172
|
-
};
|
|
2173
|
-
return { state: { ...state, usage }, changed: true };
|
|
2174
|
-
}
|
|
2175
|
-
var init_usage_backfill = __esm({
|
|
2176
|
-
"src/whimsy/usage-backfill.ts"() {
|
|
2177
|
-
"use strict";
|
|
2178
|
-
}
|
|
2179
|
-
});
|
|
2180
|
-
|
|
2181
|
-
// src/config/progress.ts
|
|
2182
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
2183
|
-
import { join as join9 } from "path";
|
|
2184
|
-
function progressPath2() {
|
|
2185
|
-
return join9(ntrpHome(), "progress.json");
|
|
2186
|
-
}
|
|
2187
|
-
function legacyStatePath2() {
|
|
2188
|
-
return join9(ntrpHome(), "state.json");
|
|
2189
|
-
}
|
|
2190
|
-
function legacyStateBackupPath2() {
|
|
2191
|
-
return join9(ntrpHome(), "state.json.bak");
|
|
2192
|
-
}
|
|
2193
|
-
function ensureDir3() {
|
|
2194
|
-
const dir = ntrpHome();
|
|
2195
|
-
if (!existsSync8(dir)) {
|
|
2196
|
-
mkdirSync8(dir, { recursive: true });
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
function emptyProgress(installId) {
|
|
2200
|
-
return {
|
|
2201
|
-
schema_version: 2,
|
|
2202
|
-
install_id: installId,
|
|
2203
|
-
total_minutes_saved: 0,
|
|
2204
|
-
credits: [],
|
|
2205
|
-
milestones_unlocked: []
|
|
2206
|
-
};
|
|
2207
|
-
}
|
|
2208
|
-
function isValidProgress(value) {
|
|
2209
|
-
if (!value || typeof value !== "object") return false;
|
|
2210
|
-
const s = value;
|
|
2211
|
-
return s.schema_version === 2 && typeof s.install_id === "string" && typeof s.total_minutes_saved === "number" && Array.isArray(s.credits) && Array.isArray(s.milestones_unlocked);
|
|
2212
|
-
}
|
|
2213
|
-
function reconcileInstallId(state) {
|
|
2214
|
-
const localId = getInstallId();
|
|
2215
|
-
if (state.install_id === localId) return { state, changed: false };
|
|
2216
|
-
if (!installMismatchWarned) {
|
|
2217
|
-
installMismatchWarned = true;
|
|
2218
|
-
console.warn(
|
|
2219
|
-
" progress.json install_id did not match this machine \u2014 rebound to local install."
|
|
2220
|
-
);
|
|
2221
|
-
}
|
|
2222
|
-
return { state: { ...state, install_id: localId }, changed: true };
|
|
2223
|
-
}
|
|
2224
|
-
function readProgressFile() {
|
|
2225
|
-
const path = progressPath2();
|
|
2226
|
-
if (!existsSync8(path)) return { state: null, changed: false };
|
|
2227
|
-
try {
|
|
2228
|
-
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
2229
|
-
if (!isValidProgress(parsed)) return { state: null, changed: false };
|
|
2230
|
-
return reconcileInstallId(parsed);
|
|
2231
|
-
} catch {
|
|
2232
|
-
return { state: null, changed: false };
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
function loadProgress() {
|
|
2236
|
-
ensureInstall();
|
|
2237
|
-
const installId = getInstallId();
|
|
2238
|
-
let state = null;
|
|
2239
|
-
let changed = false;
|
|
2240
|
-
const fromFile = readProgressFile();
|
|
2241
|
-
if (fromFile.state) {
|
|
2242
|
-
state = fromFile.state;
|
|
2243
|
-
changed = fromFile.changed;
|
|
2244
|
-
}
|
|
2245
|
-
if (!state) {
|
|
2246
|
-
const migrated = migrateLegacyStateIfNeeded(installId);
|
|
2247
|
-
if (migrated) {
|
|
2248
|
-
state = migrated;
|
|
2249
|
-
changed = true;
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
if (!state) {
|
|
2253
|
-
state = emptyProgress(installId);
|
|
2254
|
-
changed = true;
|
|
2255
|
-
}
|
|
2256
|
-
const { state: usageMigrated, changed: usageChanged } = migrateUsageIfNeeded(state);
|
|
2257
|
-
state = usageMigrated;
|
|
2258
|
-
if (usageChanged) changed = true;
|
|
2259
|
-
if (changed) saveProgress(state);
|
|
2260
|
-
return state;
|
|
2261
|
-
}
|
|
2262
|
-
function saveProgress(state) {
|
|
2263
|
-
ensureDir3();
|
|
2264
|
-
const next = {
|
|
2265
|
-
...state,
|
|
2266
|
-
schema_version: 2,
|
|
2267
|
-
install_id: getInstallId()
|
|
2268
|
-
};
|
|
2269
|
-
writeFileSync10(progressPath2(), JSON.stringify(next, null, 2) + "\n");
|
|
2270
|
-
}
|
|
2271
|
-
function wipeProgressFiles() {
|
|
2272
|
-
installMismatchWarned = false;
|
|
2273
|
-
for (const path of [progressPath2(), legacyStatePath2(), legacyStateBackupPath2()]) {
|
|
2274
|
-
if (existsSync8(path)) {
|
|
2275
|
-
unlinkSync2(path);
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
function unlockProgressMilestone(id) {
|
|
2280
|
-
const state = loadProgress();
|
|
2281
|
-
if (state.milestones_unlocked.includes(id)) return false;
|
|
2282
|
-
saveProgress({
|
|
2283
|
-
...state,
|
|
2284
|
-
milestones_unlocked: [...state.milestones_unlocked, id]
|
|
2285
|
-
});
|
|
2286
|
-
return true;
|
|
2287
|
-
}
|
|
2288
|
-
var installMismatchWarned;
|
|
2289
|
-
var init_progress = __esm({
|
|
2290
|
-
"src/config/progress.ts"() {
|
|
2291
|
-
"use strict";
|
|
2292
|
-
init_install();
|
|
2293
|
-
init_progress_migrate();
|
|
2294
|
-
init_store();
|
|
2295
|
-
init_usage_backfill();
|
|
2296
|
-
installMismatchWarned = false;
|
|
2297
|
-
}
|
|
2298
|
-
});
|
|
2299
|
-
|
|
2300
|
-
// src/whimsy/time-milestones.ts
|
|
2301
|
-
var init_time_milestones = __esm({
|
|
2302
|
-
"src/whimsy/time-milestones.ts"() {
|
|
2303
|
-
"use strict";
|
|
2304
|
-
}
|
|
2305
|
-
});
|
|
2306
|
-
|
|
2307
|
-
// src/whimsy/time-perspectives.ts
|
|
2308
|
-
var TIME_PERSPECTIVES;
|
|
2309
|
-
var init_time_perspectives = __esm({
|
|
2310
|
-
"src/whimsy/time-perspectives.ts"() {
|
|
2311
|
-
"use strict";
|
|
2312
|
-
TIME_PERSPECTIVES = [
|
|
2313
|
-
{ 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 },
|
|
2314
|
-
{ id: "rush_2112", category: "music", reference_hours: 0.33, label: "2112", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 2, max_ratio: 200 },
|
|
2315
|
-
{ id: "bohemian_rhapsody", category: "music", reference_hours: 0.1, label: "Bohemian Rhapsody", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 5, max_ratio: 500 },
|
|
2316
|
-
{ id: "stairway", category: "music", reference_hours: 0.13, label: "Stairway to Heaven", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 5, max_ratio: 400 },
|
|
2317
|
-
{ 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 },
|
|
2318
|
-
{ id: "abbey_road", category: "music", reference_hours: 0.8, label: "Abbey Road", template: "\u2248 {ratio}\xD7 through {label}", min_ratio: 1, max_ratio: 200 },
|
|
2319
|
-
{ 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 },
|
|
2320
|
-
{ 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 },
|
|
2321
|
-
{ 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 },
|
|
2322
|
-
{ 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 },
|
|
2323
|
-
{ 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 },
|
|
2324
|
-
{ id: "blockbuster", category: "film", reference_hours: 2.1, label: "average blockbusters", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 100 },
|
|
2325
|
-
{ 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 },
|
|
2326
|
-
{ id: "scorsese", category: "film", reference_hours: 3.5, label: "Goodfellas", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 80 },
|
|
2327
|
-
{ id: "godfather", category: "film", reference_hours: 6.5, label: "the Godfather saga", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 20 },
|
|
2328
|
-
{ 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 },
|
|
2329
|
-
{ 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 },
|
|
2330
|
-
{ 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 },
|
|
2331
|
-
{ 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 },
|
|
2332
|
-
{ 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 },
|
|
2333
|
-
{ id: "soccer_match", category: "sports", reference_hours: 1.75, label: "Premier League matches", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 150 },
|
|
2334
|
-
{ id: "marathon", category: "sports", reference_hours: 2, label: "marathons at world-record pace", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 80 },
|
|
2335
|
-
{ 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 },
|
|
2336
|
-
{ id: "superbowl", category: "sports", reference_hours: 3.5, label: "Super Bowls", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 80 },
|
|
2337
|
-
{ 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 },
|
|
2338
|
-
{ id: "wimbledon", category: "sports", reference_hours: 5, label: "Wimbledon finals", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 30 },
|
|
2339
|
-
{ 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 },
|
|
2340
|
-
{ id: "olympics", category: "sports", reference_hours: 250, label: "Summer Olympics broadcast hours", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 2, max_ratio: 10 },
|
|
2341
|
-
{ 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 },
|
|
2342
|
-
{ id: "iss_orbit", category: "cosmos", reference_hours: 1.5, label: "ISS orbits", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 2, max_ratio: 200 },
|
|
2343
|
-
{ 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 },
|
|
2344
|
-
{ 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 },
|
|
2345
|
-
{ 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 },
|
|
2346
|
-
{ 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 },
|
|
2347
|
-
{ id: "earth_rotation", category: "cosmos", reference_hours: 24, label: "Earth rotations", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.1, max_ratio: 50 },
|
|
2348
|
-
{ 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 },
|
|
2349
|
-
{ id: "lunar_month", category: "cosmos", reference_hours: 708, label: "a lunar cycle", template: "\u2248 {pct}% of {label}", min_ratio: 0.05, max_ratio: 2 },
|
|
2350
|
-
{ 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 },
|
|
2351
|
-
{ 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 },
|
|
2352
|
-
{ id: "standup", category: "gtm", reference_hours: 0.25, label: "daily standups", template: "\u2248 {ratio}\xD7 skipped {label}", min_ratio: 4, max_ratio: 500 },
|
|
2353
|
-
{ id: "quick_sync", category: "gtm", reference_hours: 0.5, label: "avoided 'quick syncs'", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 2, max_ratio: 200 },
|
|
2354
|
-
{ id: "pipeline_review", category: "gtm", reference_hours: 1, label: "weekly pipeline reviews", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 200 },
|
|
2355
|
-
{ id: "forecast_call", category: "gtm", reference_hours: 1.5, label: "forecast calls", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 150 },
|
|
2356
|
-
{ id: "pivot_spiral", category: "gtm", reference_hours: 2, label: "spreadsheet pivot-table spirals", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 1, max_ratio: 100 },
|
|
2357
|
-
{ 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 },
|
|
2358
|
-
{ id: "crm_cleanup", category: "gtm", reference_hours: 6, label: "CRM hygiene sprints", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.5, max_ratio: 40 },
|
|
2359
|
-
{ id: "qbr_prep", category: "gtm", reference_hours: 8, label: "QBR prep blocks", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 20 },
|
|
2360
|
-
{ id: "board_deck", category: "gtm", reference_hours: 12, label: "board deck builds", template: "\u2248 {ratio}\xD7 {label}", min_ratio: 0.3, max_ratio: 30 },
|
|
2361
|
-
{ 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 },
|
|
2362
|
-
{ 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 }
|
|
2363
|
-
];
|
|
2364
|
-
}
|
|
2365
|
-
});
|
|
2366
|
-
|
|
2367
|
-
// src/whimsy/time-bank-whimsy.ts
|
|
2368
|
-
var init_time_bank_whimsy = __esm({
|
|
2369
|
-
"src/whimsy/time-bank-whimsy.ts"() {
|
|
2370
|
-
"use strict";
|
|
2371
|
-
}
|
|
2372
|
-
});
|
|
2373
|
-
|
|
2374
|
-
// src/whimsy/perspective-rotation.ts
|
|
2375
|
-
var PERSPECTIVE_ROTATE_CALENDAR_MS;
|
|
2376
|
-
var init_perspective_rotation = __esm({
|
|
2377
|
-
"src/whimsy/perspective-rotation.ts"() {
|
|
2378
|
-
"use strict";
|
|
2379
|
-
PERSPECTIVE_ROTATE_CALENDAR_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2380
|
-
}
|
|
2381
|
-
});
|
|
2382
|
-
|
|
2383
|
-
// src/whimsy/usage-stats.ts
|
|
2384
|
-
var init_usage_stats = __esm({
|
|
2385
|
-
"src/whimsy/usage-stats.ts"() {
|
|
2386
|
-
"use strict";
|
|
2387
|
-
init_progress();
|
|
2388
|
-
}
|
|
2389
|
-
});
|
|
2390
|
-
|
|
2391
|
-
// src/whimsy/time-bank.ts
|
|
2392
|
-
import chalk5 from "chalk";
|
|
2393
|
-
var init_time_bank = __esm({
|
|
2394
|
-
"src/whimsy/time-bank.ts"() {
|
|
2395
|
-
"use strict";
|
|
2396
|
-
init_progress();
|
|
2397
|
-
init_theme();
|
|
2398
|
-
init_time_milestones();
|
|
2399
|
-
init_time_perspectives();
|
|
2400
|
-
init_time_bank_whimsy();
|
|
2401
|
-
init_perspective_rotation();
|
|
2402
|
-
init_usage_stats();
|
|
2403
|
-
}
|
|
2404
|
-
});
|
|
2405
|
-
|
|
2406
|
-
// src/data/playbook.ts
|
|
2407
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9, appendFileSync as appendFileSync2 } from "fs";
|
|
2408
|
-
import { join as join10 } from "path";
|
|
2409
|
-
var init_playbook = __esm({
|
|
2410
|
-
"src/data/playbook.ts"() {
|
|
2411
|
-
"use strict";
|
|
2412
|
-
init_store();
|
|
2413
|
-
}
|
|
2414
|
-
});
|
|
2415
|
-
|
|
2416
|
-
// src/memory/play-outcomes.ts
|
|
2417
|
-
import { existsSync as existsSync10, readFileSync as readFileSync10, appendFileSync as appendFileSync3 } from "fs";
|
|
2418
|
-
import { join as join11 } from "path";
|
|
2419
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
2420
|
-
var init_play_outcomes = __esm({
|
|
2421
|
-
"src/memory/play-outcomes.ts"() {
|
|
2422
|
-
"use strict";
|
|
2423
|
-
init_store();
|
|
2424
|
-
}
|
|
2425
|
-
});
|
|
2426
|
-
|
|
2427
|
-
// src/workflows/registry.ts
|
|
2428
|
-
var init_registry = __esm({
|
|
2429
|
-
"src/workflows/registry.ts"() {
|
|
2430
|
-
"use strict";
|
|
2431
|
-
}
|
|
2432
|
-
});
|
|
2433
|
-
|
|
2434
|
-
// src/ai/untrusted.ts
|
|
2435
|
-
var UNTRUSTED_MARKER_NAME, UNTRUSTED_MARKER_END_NAME, MARKER_SPOOF_PATTERN;
|
|
2436
|
-
var init_untrusted = __esm({
|
|
2437
|
-
"src/ai/untrusted.ts"() {
|
|
2438
|
-
"use strict";
|
|
2439
|
-
UNTRUSTED_MARKER_NAME = "EXTERNAL_UNTRUSTED_CONTENT";
|
|
2440
|
-
UNTRUSTED_MARKER_END_NAME = "END_EXTERNAL_UNTRUSTED_CONTENT";
|
|
2441
|
-
MARKER_SPOOF_PATTERN = new RegExp(
|
|
2442
|
-
`<{2,}\\s*/?\\s*(?:${UNTRUSTED_MARKER_NAME}|${UNTRUSTED_MARKER_END_NAME})[^>]*>{2,}`,
|
|
2443
|
-
"gi"
|
|
2444
|
-
);
|
|
2445
|
-
}
|
|
2446
|
-
});
|
|
2447
|
-
|
|
2448
|
-
// src/ai/prompt-parts.ts
|
|
2449
|
-
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
2450
|
-
import { join as join12 } from "path";
|
|
2451
|
-
var init_prompt_parts = __esm({
|
|
2452
|
-
"src/ai/prompt-parts.ts"() {
|
|
2453
|
-
"use strict";
|
|
2454
|
-
init_profile();
|
|
2455
|
-
init_store();
|
|
2456
|
-
init_playbook();
|
|
2457
|
-
init_play_outcomes();
|
|
2458
|
-
init_registry();
|
|
2459
|
-
init_untrusted();
|
|
2460
|
-
}
|
|
2461
|
-
});
|
|
2462
|
-
|
|
2463
|
-
// src/ai/llm/providers.ts
|
|
2464
|
-
var init_providers = __esm({
|
|
2465
|
-
"src/ai/llm/providers.ts"() {
|
|
2466
|
-
"use strict";
|
|
2467
|
-
init_store();
|
|
2468
|
-
}
|
|
2469
|
-
});
|
|
2470
|
-
|
|
2471
|
-
// src/config/llm-config.ts
|
|
2472
|
-
var init_llm_config = __esm({
|
|
2473
|
-
"src/config/llm-config.ts"() {
|
|
2474
|
-
"use strict";
|
|
2475
|
-
init_providers();
|
|
2476
|
-
init_store();
|
|
2477
|
-
}
|
|
2478
|
-
});
|
|
2479
|
-
|
|
2480
|
-
// src/ai/llm/gate.ts
|
|
2481
|
-
var init_gate = __esm({
|
|
2482
|
-
"src/ai/llm/gate.ts"() {
|
|
2483
|
-
"use strict";
|
|
2484
|
-
init_llm_config();
|
|
2485
|
-
}
|
|
2486
|
-
});
|
|
2487
|
-
|
|
2488
|
-
// src/ai/repl-api.ts
|
|
2489
|
-
var init_repl_api = __esm({
|
|
2490
|
-
"src/ai/repl-api.ts"() {
|
|
2491
|
-
"use strict";
|
|
2492
|
-
init_gate();
|
|
2493
|
-
}
|
|
2494
|
-
});
|
|
2495
|
-
|
|
2496
|
-
// src/ai/explore-mode.ts
|
|
2497
|
-
var init_explore_mode = __esm({
|
|
2498
|
-
"src/ai/explore-mode.ts"() {
|
|
2499
|
-
"use strict";
|
|
2500
|
-
init_context2();
|
|
2501
|
-
}
|
|
2502
|
-
});
|
|
2503
|
-
|
|
2504
|
-
// src/ai/llm/models-cache.ts
|
|
2505
|
-
var CACHE_TTL_MS;
|
|
2506
|
-
var init_models_cache = __esm({
|
|
2507
|
-
"src/ai/llm/models-cache.ts"() {
|
|
2508
|
-
"use strict";
|
|
2509
|
-
init_store();
|
|
2510
|
-
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2511
|
-
}
|
|
2512
|
-
});
|
|
2513
|
-
|
|
2514
|
-
// src/ai/llm/catalog.ts
|
|
2515
|
-
var ENTRIES, byId;
|
|
2516
|
-
var init_catalog = __esm({
|
|
2517
|
-
"src/ai/llm/catalog.ts"() {
|
|
2518
|
-
"use strict";
|
|
2519
|
-
init_models_cache();
|
|
2520
|
-
ENTRIES = [
|
|
2521
|
-
{
|
|
2522
|
-
id: "claude-opus-4-6",
|
|
2523
|
-
provider: "anthropic",
|
|
2524
|
-
tier: "high",
|
|
2525
|
-
status: "active",
|
|
2526
|
-
successor_id: null,
|
|
2527
|
-
supports_tools: true,
|
|
2528
|
-
max_context_tokens: 2e5,
|
|
2529
|
-
display_name: "Claude Opus 4.6",
|
|
2530
|
-
relative_cost: 3
|
|
2531
|
-
},
|
|
2532
|
-
{
|
|
2533
|
-
id: "claude-sonnet-4-5-20250929",
|
|
2534
|
-
provider: "anthropic",
|
|
2535
|
-
tier: "medium",
|
|
2536
|
-
status: "active",
|
|
2537
|
-
successor_id: null,
|
|
2538
|
-
supports_tools: true,
|
|
2539
|
-
max_context_tokens: 2e5,
|
|
2540
|
-
display_name: "Claude Sonnet 4.5",
|
|
2541
|
-
relative_cost: 2
|
|
2542
|
-
},
|
|
2543
|
-
{
|
|
2544
|
-
id: "claude-haiku-4-5-20251001",
|
|
2545
|
-
provider: "anthropic",
|
|
2546
|
-
tier: "low",
|
|
2547
|
-
status: "active",
|
|
2548
|
-
successor_id: null,
|
|
2549
|
-
supports_tools: true,
|
|
2550
|
-
max_context_tokens: 2e5,
|
|
2551
|
-
display_name: "Claude Haiku 4.5",
|
|
2552
|
-
relative_cost: 1
|
|
2553
|
-
},
|
|
2554
|
-
{
|
|
2555
|
-
id: "gpt-4.1",
|
|
2556
|
-
provider: "openai",
|
|
2557
|
-
tier: "high",
|
|
2558
|
-
status: "active",
|
|
2559
|
-
successor_id: null,
|
|
2560
|
-
supports_tools: true,
|
|
2561
|
-
max_context_tokens: 1047576,
|
|
2562
|
-
display_name: "GPT-4.1",
|
|
2563
|
-
relative_cost: 3
|
|
2564
|
-
},
|
|
2565
|
-
{
|
|
2566
|
-
id: "gpt-4.1-mini",
|
|
2567
|
-
provider: "openai",
|
|
2568
|
-
tier: "medium",
|
|
2569
|
-
status: "active",
|
|
2570
|
-
successor_id: null,
|
|
2571
|
-
supports_tools: true,
|
|
2572
|
-
max_context_tokens: 1047576,
|
|
2573
|
-
display_name: "GPT-4.1 Mini",
|
|
2574
|
-
relative_cost: 2
|
|
2575
|
-
},
|
|
2576
|
-
{
|
|
2577
|
-
id: "gpt-4.1-nano",
|
|
2578
|
-
provider: "openai",
|
|
2579
|
-
tier: "low",
|
|
2580
|
-
status: "active",
|
|
2581
|
-
successor_id: null,
|
|
2582
|
-
supports_tools: true,
|
|
2583
|
-
max_context_tokens: 1047576,
|
|
2584
|
-
display_name: "GPT-4.1 Nano",
|
|
2585
|
-
relative_cost: 1
|
|
2586
|
-
}
|
|
2587
|
-
];
|
|
2588
|
-
byId = new Map(ENTRIES.map((e) => [e.id, e]));
|
|
2589
|
-
}
|
|
2590
|
-
});
|
|
2591
|
-
|
|
2592
|
-
// src/ai/llm/surfaces.ts
|
|
2593
|
-
var init_surfaces = __esm({
|
|
2594
|
-
"src/ai/llm/surfaces.ts"() {
|
|
2595
|
-
"use strict";
|
|
2596
|
-
}
|
|
2597
|
-
});
|
|
2598
|
-
|
|
2599
|
-
// src/ai/llm/session-state.ts
|
|
2600
|
-
var init_session_state = __esm({
|
|
2601
|
-
"src/ai/llm/session-state.ts"() {
|
|
2602
|
-
"use strict";
|
|
2603
|
-
init_llm_config();
|
|
2604
|
-
init_catalog();
|
|
2605
|
-
init_surfaces();
|
|
2606
|
-
}
|
|
2607
|
-
});
|
|
2608
|
-
|
|
2609
|
-
// src/conversation/recommended-action.ts
|
|
2610
|
-
var init_recommended_action = __esm({
|
|
2611
|
-
"src/conversation/recommended-action.ts"() {
|
|
2612
|
-
"use strict";
|
|
2613
|
-
init_repl_api();
|
|
2614
|
-
init_phase();
|
|
2615
|
-
}
|
|
2616
|
-
});
|
|
2617
|
-
|
|
2618
|
-
// src/conversation/phase.ts
|
|
2619
|
-
import chalk6 from "chalk";
|
|
2620
|
-
var init_phase = __esm({
|
|
2621
|
-
"src/conversation/phase.ts"() {
|
|
2622
|
-
"use strict";
|
|
2623
|
-
init_context2();
|
|
2624
|
-
init_repl_api();
|
|
2625
|
-
init_explore_mode();
|
|
2626
|
-
init_session_state();
|
|
2627
|
-
init_theme();
|
|
2628
|
-
init_recommended_action();
|
|
2629
|
-
}
|
|
2630
|
-
});
|
|
2631
|
-
|
|
2632
|
-
// src/cli/args.ts
|
|
2633
|
-
import { basename as basename4 } from "path";
|
|
2634
|
-
var init_args = __esm({
|
|
2635
|
-
"src/cli/args.ts"() {
|
|
2636
|
-
"use strict";
|
|
2637
|
-
}
|
|
2638
|
-
});
|
|
2639
|
-
|
|
2640
|
-
// src/cli/global-admin.ts
|
|
2641
|
-
var init_global_admin = __esm({
|
|
2642
|
-
"src/cli/global-admin.ts"() {
|
|
2643
|
-
"use strict";
|
|
2644
|
-
init_args();
|
|
2645
|
-
}
|
|
2646
|
-
});
|
|
2647
|
-
|
|
2648
|
-
// src/cli/post-action.ts
|
|
2649
|
-
var init_post_action = __esm({
|
|
2650
|
-
"src/cli/post-action.ts"() {
|
|
2651
|
-
"use strict";
|
|
2652
|
-
}
|
|
2653
|
-
});
|
|
2654
|
-
|
|
2655
|
-
// src/cli/repl-globals.ts
|
|
2656
|
-
var init_repl_globals = __esm({
|
|
2657
|
-
"src/cli/repl-globals.ts"() {
|
|
2658
|
-
"use strict";
|
|
2659
|
-
}
|
|
2660
|
-
});
|
|
2661
|
-
|
|
2662
|
-
// src/ui/banner.ts
|
|
2663
|
-
import chalk8 from "chalk";
|
|
2664
|
-
var init_banner = __esm({
|
|
2665
|
-
"src/ui/banner.ts"() {
|
|
2666
|
-
"use strict";
|
|
2667
|
-
init_theme();
|
|
2668
|
-
init_layout();
|
|
2669
|
-
}
|
|
2670
|
-
});
|
|
2671
|
-
|
|
2672
|
-
// src/license/trial-policy.ts
|
|
2673
|
-
var init_trial_policy = __esm({
|
|
2674
|
-
"src/license/trial-policy.ts"() {
|
|
2675
|
-
"use strict";
|
|
2676
|
-
}
|
|
2677
|
-
});
|
|
2678
|
-
|
|
2679
|
-
// src/license/upgrade-whimsy.ts
|
|
2680
|
-
var init_upgrade_whimsy = __esm({
|
|
2681
|
-
"src/license/upgrade-whimsy.ts"() {
|
|
2682
|
-
"use strict";
|
|
2683
|
-
init_trial_policy();
|
|
2684
|
-
}
|
|
2685
|
-
});
|
|
2686
|
-
|
|
2687
|
-
// src/license/normalize.ts
|
|
2688
|
-
var init_normalize = __esm({
|
|
2689
|
-
"src/license/normalize.ts"() {
|
|
2690
|
-
"use strict";
|
|
2691
|
-
}
|
|
2692
|
-
});
|
|
2693
|
-
|
|
2694
|
-
// src/license/lemonsqueezy.ts
|
|
2695
|
-
import { hostname } from "os";
|
|
2696
|
-
var init_lemonsqueezy = __esm({
|
|
2697
|
-
"src/license/lemonsqueezy.ts"() {
|
|
2698
|
-
"use strict";
|
|
2699
|
-
}
|
|
2700
|
-
});
|
|
2701
|
-
|
|
2702
|
-
// src/license/verify.ts
|
|
2703
|
-
var init_verify = __esm({
|
|
2704
|
-
"src/license/verify.ts"() {
|
|
2705
|
-
"use strict";
|
|
2706
|
-
init_store();
|
|
2707
|
-
init_trial_policy();
|
|
2708
|
-
init_upgrade_whimsy();
|
|
2709
|
-
init_normalize();
|
|
2710
|
-
init_lemonsqueezy();
|
|
2711
|
-
}
|
|
2712
|
-
});
|
|
2713
|
-
|
|
2714
|
-
// src/cli/prompts.ts
|
|
2715
|
-
import { createInterface } from "readline/promises";
|
|
2716
|
-
import { clearLine, cursorTo } from "readline";
|
|
2717
|
-
import { StringDecoder } from "string_decoder";
|
|
2718
|
-
import chalk9 from "chalk";
|
|
2719
|
-
var init_prompts = __esm({
|
|
2720
|
-
"src/cli/prompts.ts"() {
|
|
2721
|
-
"use strict";
|
|
2722
|
-
init_repl_globals();
|
|
2723
|
-
init_theme();
|
|
2724
|
-
}
|
|
2725
|
-
});
|
|
2726
|
-
|
|
2727
|
-
// src/ui/open-browser.ts
|
|
2728
|
-
import { spawn } from "child_process";
|
|
2729
|
-
import { platform } from "os";
|
|
2730
|
-
var init_open_browser = __esm({
|
|
2731
|
-
"src/ui/open-browser.ts"() {
|
|
2732
|
-
"use strict";
|
|
2733
|
-
}
|
|
2734
|
-
});
|
|
2735
|
-
|
|
2736
|
-
// src/license/upgrade.ts
|
|
2737
|
-
import chalk10 from "chalk";
|
|
2738
|
-
var init_upgrade = __esm({
|
|
2739
|
-
"src/license/upgrade.ts"() {
|
|
2740
|
-
"use strict";
|
|
2741
|
-
init_prompts();
|
|
2742
|
-
init_store();
|
|
2743
|
-
init_banner();
|
|
2744
|
-
init_theme();
|
|
2745
|
-
init_verify();
|
|
2746
|
-
init_trial_policy();
|
|
2747
|
-
init_upgrade_whimsy();
|
|
2748
|
-
init_open_browser();
|
|
2749
|
-
}
|
|
2750
|
-
});
|
|
2751
|
-
|
|
2752
|
-
// src/license/activation.ts
|
|
2753
|
-
import chalk11 from "chalk";
|
|
2754
|
-
var init_activation = __esm({
|
|
2755
|
-
"src/license/activation.ts"() {
|
|
2756
|
-
"use strict";
|
|
2757
|
-
init_banner();
|
|
2758
|
-
init_theme();
|
|
2759
|
-
init_verify();
|
|
2760
|
-
init_trial_policy();
|
|
2761
|
-
init_upgrade();
|
|
2762
|
-
}
|
|
2763
|
-
});
|
|
2764
|
-
|
|
2765
|
-
// src/license/gate.ts
|
|
2766
|
-
var init_gate2 = __esm({
|
|
2767
|
-
"src/license/gate.ts"() {
|
|
2768
|
-
"use strict";
|
|
2769
|
-
}
|
|
2770
|
-
});
|
|
2771
|
-
|
|
2772
|
-
// src/cli/dispatch.ts
|
|
2773
|
-
import chalk12 from "chalk";
|
|
2774
|
-
var init_dispatch = __esm({
|
|
2775
|
-
"src/cli/dispatch.ts"() {
|
|
2776
|
-
"use strict";
|
|
2777
|
-
init_args();
|
|
2778
|
-
init_global_admin();
|
|
2779
|
-
init_post_action();
|
|
2780
|
-
init_repl_globals();
|
|
2781
|
-
init_registry();
|
|
2782
|
-
init_theme();
|
|
2783
|
-
init_activation();
|
|
2784
|
-
init_gate2();
|
|
2785
|
-
init_upgrade();
|
|
2786
|
-
}
|
|
2787
|
-
});
|
|
2788
|
-
|
|
2789
|
-
// src/baselines/profile-presets.ts
|
|
2790
|
-
var init_profile_presets = __esm({
|
|
2791
|
-
"src/baselines/profile-presets.ts"() {
|
|
2792
|
-
"use strict";
|
|
2793
|
-
}
|
|
2794
|
-
});
|
|
2795
|
-
|
|
2796
|
-
// src/baselines/defaults.ts
|
|
2797
|
-
var init_defaults = __esm({
|
|
2798
|
-
"src/baselines/defaults.ts"() {
|
|
2799
|
-
"use strict";
|
|
2800
|
-
}
|
|
2801
|
-
});
|
|
2802
|
-
|
|
2803
|
-
// src/baselines/resolve.ts
|
|
2804
|
-
var init_resolve = __esm({
|
|
2805
|
-
"src/baselines/resolve.ts"() {
|
|
2806
|
-
"use strict";
|
|
2807
|
-
init_defaults();
|
|
2808
|
-
init_profile_presets();
|
|
2809
|
-
}
|
|
2810
|
-
});
|
|
2811
|
-
|
|
2812
|
-
// src/cli/argparse.ts
|
|
2813
|
-
var init_argparse = __esm({
|
|
2814
|
-
"src/cli/argparse.ts"() {
|
|
2815
|
-
"use strict";
|
|
2816
|
-
}
|
|
2817
|
-
});
|
|
2818
|
-
|
|
2819
|
-
// src/ui/markdown.ts
|
|
2820
|
-
import chalk13 from "chalk";
|
|
2821
|
-
import Table from "cli-table3";
|
|
2822
|
-
var init_markdown = __esm({
|
|
2823
|
-
"src/ui/markdown.ts"() {
|
|
2824
|
-
"use strict";
|
|
2825
|
-
init_layout();
|
|
2826
|
-
}
|
|
2827
|
-
});
|
|
2828
|
-
|
|
2829
|
-
// src/commands/profile.ts
|
|
2830
|
-
import chalk14 from "chalk";
|
|
2831
|
-
var init_profile2 = __esm({
|
|
2832
|
-
"src/commands/profile.ts"() {
|
|
2833
|
-
"use strict";
|
|
2834
|
-
init_store();
|
|
2835
|
-
init_profile();
|
|
2836
|
-
init_profile_presets();
|
|
2837
|
-
init_defaults();
|
|
2838
|
-
init_resolve();
|
|
2839
|
-
init_argparse();
|
|
2840
|
-
init_markdown();
|
|
2841
|
-
init_theme();
|
|
2842
|
-
}
|
|
2843
|
-
});
|
|
2844
|
-
|
|
2845
|
-
// src/db/connection.ts
|
|
2846
|
-
import { dirname as dirname3, join as join13, resolve as resolve5 } from "path";
|
|
2847
|
-
var NTRP_DIR3, DEFAULT_DB_PATH, DB_PATH_PINNED;
|
|
2848
|
-
var init_connection = __esm({
|
|
2849
|
-
"src/db/connection.ts"() {
|
|
2850
|
-
"use strict";
|
|
2851
|
-
init_store();
|
|
2852
|
-
NTRP_DIR3 = process.env.NTRP_HOME ? resolve5(process.env.NTRP_HOME) : join13(process.env.HOME ?? "", ".ntrp");
|
|
2853
|
-
DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ? resolve5(process.env.NTRP_DB_PATH) : join13(NTRP_DIR3, "ntrp.duckdb");
|
|
2854
|
-
DB_PATH_PINNED = !!process.env.NTRP_DB_PATH;
|
|
2855
|
-
}
|
|
2856
|
-
});
|
|
2857
|
-
|
|
2858
|
-
// src/db/queries.ts
|
|
2859
|
-
var init_queries = __esm({
|
|
2860
|
-
"src/db/queries.ts"() {
|
|
2861
|
-
"use strict";
|
|
2862
|
-
init_connection();
|
|
2863
|
-
init_formatters();
|
|
2864
|
-
init_connection();
|
|
2865
|
-
}
|
|
2866
|
-
});
|
|
2867
|
-
|
|
2868
|
-
// src/db/schema.ts
|
|
2869
|
-
var init_schema = __esm({
|
|
2870
|
-
"src/db/schema.ts"() {
|
|
2871
|
-
"use strict";
|
|
2872
|
-
init_connection();
|
|
2873
|
-
}
|
|
2874
|
-
});
|
|
2875
|
-
|
|
2876
|
-
// src/config/update-check.ts
|
|
2877
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync12, unlinkSync as unlinkSync3, writeFileSync as writeFileSync11 } from "fs";
|
|
2878
|
-
import { join as join14 } from "path";
|
|
2879
|
-
var init_update_check = __esm({
|
|
2880
|
-
"src/config/update-check.ts"() {
|
|
2881
|
-
"use strict";
|
|
2882
|
-
init_store();
|
|
2883
|
-
}
|
|
2884
|
-
});
|
|
2885
|
-
|
|
2886
|
-
// src/version.ts
|
|
2887
|
-
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
2888
|
-
import { dirname as dirname4, join as join15 } from "path";
|
|
2889
|
-
import { fileURLToPath } from "url";
|
|
2890
|
-
var init_version = __esm({
|
|
2891
|
-
"src/version.ts"() {
|
|
2892
|
-
"use strict";
|
|
2893
|
-
}
|
|
2894
|
-
});
|
|
2895
|
-
|
|
2896
|
-
// src/update/registry.ts
|
|
2897
|
-
var init_registry2 = __esm({
|
|
2898
|
-
"src/update/registry.ts"() {
|
|
2899
|
-
"use strict";
|
|
2900
|
-
init_update_check();
|
|
2901
|
-
init_version();
|
|
2902
|
-
}
|
|
2903
|
-
});
|
|
2904
|
-
|
|
2905
|
-
// src/conversation/metric-tour.ts
|
|
2906
|
-
import chalk17 from "chalk";
|
|
2907
|
-
function hasCompletedMetricsTour() {
|
|
2908
|
-
return Boolean(getConfigValue(TOUR_COMPLETED_KEY));
|
|
2909
|
-
}
|
|
2910
|
-
function markMetricsTourCompleted() {
|
|
2911
|
-
if (hasCompletedMetricsTour()) return;
|
|
2912
|
-
setConfigValue(TOUR_COMPLETED_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
2913
|
-
}
|
|
2914
|
-
function hasSkippedMetricsTour() {
|
|
2915
|
-
return Boolean(getConfigValue(TOUR_SKIPPED_KEY));
|
|
2916
|
-
}
|
|
2917
|
-
function markMetricsTourSkipped() {
|
|
2918
|
-
if (hasSkippedMetricsTour()) return;
|
|
2919
|
-
setConfigValue(TOUR_SKIPPED_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
2920
|
-
}
|
|
2921
|
-
function hasSeenDeepdiveHomeNudge() {
|
|
2922
|
-
return Boolean(getConfigValue(HOME_NUDGE_SEEN_KEY));
|
|
2923
|
-
}
|
|
2924
|
-
function markDeepdiveHomeNudgeSeen() {
|
|
2925
|
-
if (hasSeenDeepdiveHomeNudge()) return;
|
|
2926
|
-
setConfigValue(HOME_NUDGE_SEEN_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
2927
|
-
}
|
|
2928
|
-
function hasAnalysisForDeepdiveNudge(ctx) {
|
|
2929
|
-
if (ctx.stage === "analyzed") return true;
|
|
2930
|
-
if (ctx.analysis.completed.length > 0) return true;
|
|
2931
|
-
try {
|
|
2932
|
-
return listSessions({ limit: 20 }).some(
|
|
2933
|
-
(s) => s.stage === "analyzed" || (s.analysis?.completed?.length ?? 0) > 0
|
|
2934
|
-
);
|
|
2935
|
-
} catch {
|
|
2936
|
-
return false;
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
function getDeepdiveNudge(ctx) {
|
|
2940
|
-
if (hasCompletedMetricsTour() || hasSeenDeepdiveHomeNudge()) return null;
|
|
2941
|
-
if (!hasAnalysisForDeepdiveNudge(ctx)) return null;
|
|
2942
|
-
return {
|
|
2943
|
-
text: "Tour \u2014 what the numbers mean, and how to use NTRP",
|
|
2944
|
-
command: "/deepdive"
|
|
2945
|
-
};
|
|
2946
|
-
}
|
|
2947
|
-
function metricItems() {
|
|
2948
|
-
return getCoreDeckExplainers().map((e) => ({ kind: "metric", explainer: e }));
|
|
2949
|
-
}
|
|
2950
|
-
function guideItems() {
|
|
2951
|
-
return listGuideSlides().map((s) => ({ kind: "guide", slide: s }));
|
|
2952
|
-
}
|
|
2953
|
-
function fullDeck() {
|
|
2954
|
-
return [{ kind: "intro" }, ...metricItems(), ...guideItems(), { kind: "close" }];
|
|
2955
|
-
}
|
|
2956
|
-
function itemId(item) {
|
|
2957
|
-
if (item.kind === "metric") return item.explainer.id;
|
|
2958
|
-
if (item.kind === "guide") return item.slide.id;
|
|
2959
|
-
return void 0;
|
|
2960
|
-
}
|
|
2961
|
-
function buildDeck(opts) {
|
|
2962
|
-
if (opts.singleSlide && opts.startAt) {
|
|
2963
|
-
const guide = getGuideSlide(opts.startAt);
|
|
2964
|
-
if (guide) return [{ kind: "guide", slide: guide }];
|
|
2965
|
-
const explainer2 = getMetricExplainer(opts.startAt);
|
|
2966
|
-
if (!explainer2) return [];
|
|
2967
|
-
return [{ kind: "metric", explainer: explainer2 }];
|
|
2968
|
-
}
|
|
2969
|
-
const full = fullDeck();
|
|
2970
|
-
if (!opts.startAt) return full;
|
|
2971
|
-
const idx = full.findIndex((item) => itemId(item) === opts.startAt);
|
|
2972
|
-
if (idx >= 0) return full.slice(idx);
|
|
2973
|
-
const explainer = getMetricExplainer(opts.startAt);
|
|
2974
|
-
if (explainer) {
|
|
2975
|
-
return [
|
|
2976
|
-
{ kind: "metric", explainer },
|
|
2977
|
-
...getCoreDeckExplainers().filter((e) => e.id !== explainer.id).map((e) => ({ kind: "metric", explainer: e })),
|
|
2978
|
-
...guideItems(),
|
|
2979
|
-
{ kind: "close" }
|
|
2980
|
-
];
|
|
2981
|
-
}
|
|
2982
|
-
return full;
|
|
2983
|
-
}
|
|
2984
|
-
function describeTourDeck(opts = {}) {
|
|
2985
|
-
return buildDeck(opts).map((item) => ({ kind: item.kind, id: itemId(item) }));
|
|
2986
|
-
}
|
|
2987
|
-
var TOUR_COMPLETED_KEY, TOUR_SKIPPED_KEY, HOME_NUDGE_SEEN_KEY, METRICS_TOUR_MILESTONE_ID;
|
|
2988
|
-
var init_metric_tour = __esm({
|
|
2989
|
-
"src/conversation/metric-tour.ts"() {
|
|
2990
|
-
"use strict";
|
|
2991
|
-
init_context2();
|
|
2992
|
-
init_prompts();
|
|
2993
|
-
init_store();
|
|
2994
|
-
init_progress();
|
|
2995
|
-
init_profile();
|
|
2996
|
-
init_metric_definitions();
|
|
2997
|
-
init_guide_slides();
|
|
2998
|
-
init_slides();
|
|
2999
|
-
init_theme();
|
|
3000
|
-
TOUR_COMPLETED_KEY = "metrics-tour-completed";
|
|
3001
|
-
TOUR_SKIPPED_KEY = "metrics-tour-skipped";
|
|
3002
|
-
HOME_NUDGE_SEEN_KEY = "metrics-tour-home-nudge-seen";
|
|
3003
|
-
METRICS_TOUR_MILESTONE_ID = "metrics_tour";
|
|
3004
|
-
}
|
|
3005
|
-
});
|
|
3006
|
-
|
|
3007
|
-
// src/conversation/deepdive-smoke.ts
|
|
3008
|
-
init_metric_definitions();
|
|
3009
|
-
init_guide_slides();
|
|
3010
|
-
init_slides();
|
|
3011
|
-
init_keyless_definitions();
|
|
3012
|
-
init_metric_explainers();
|
|
3013
|
-
|
|
3014
|
-
// src/cli/repl.ts
|
|
3015
|
-
init_spinner();
|
|
3016
|
-
init_context2();
|
|
3017
|
-
init_transcript();
|
|
3018
|
-
init_time_bank();
|
|
3019
|
-
init_prompt_parts();
|
|
3020
|
-
init_store();
|
|
3021
|
-
init_phase();
|
|
3022
|
-
init_recommended_action();
|
|
3023
|
-
import { createInterface as createInterface2 } from "readline/promises";
|
|
3024
|
-
import { clearLine as clearLine2, cursorTo as cursorTo2 } from "readline";
|
|
3025
|
-
import chalk16 from "chalk";
|
|
3026
|
-
import { join as join16 } from "path";
|
|
3027
|
-
|
|
3028
|
-
// src/conversation/loop-guard.ts
|
|
3029
|
-
init_theme();
|
|
3030
|
-
import chalk7 from "chalk";
|
|
3031
|
-
|
|
3032
|
-
// src/cli/repl.ts
|
|
3033
|
-
init_dispatch();
|
|
3034
|
-
init_post_action();
|
|
3035
|
-
init_global_admin();
|
|
3036
|
-
init_repl_globals();
|
|
3037
|
-
init_registry();
|
|
3038
|
-
init_theme();
|
|
3039
|
-
init_layout();
|
|
3040
|
-
init_banner();
|
|
3041
|
-
|
|
3042
|
-
// src/ui/welcome.ts
|
|
3043
|
-
init_context2();
|
|
3044
|
-
init_phase();
|
|
3045
|
-
init_layout();
|
|
3046
|
-
init_theme();
|
|
3047
|
-
init_banner();
|
|
3048
|
-
init_store();
|
|
3049
|
-
init_profile();
|
|
3050
|
-
init_profile2();
|
|
3051
|
-
init_verify();
|
|
3052
|
-
init_queries();
|
|
3053
|
-
init_schema();
|
|
3054
|
-
import chalk15 from "chalk";
|
|
3055
|
-
|
|
3056
|
-
// src/cli/repl.ts
|
|
3057
|
-
init_registry2();
|
|
3058
|
-
|
|
3059
|
-
// src/conversation/deepdive-complete.ts
|
|
3060
|
-
init_metric_definitions();
|
|
3061
|
-
init_guide_slides();
|
|
3062
|
-
var SUBCOMMANDS = ["list", "tour", "start", "ls", "catalog"];
|
|
3063
|
-
var GUIDE_COMPLETE = [
|
|
3064
|
-
...GUIDE_SECTION_ALIASES,
|
|
3065
|
-
...GUIDE_DECK_IDS,
|
|
3066
|
-
"inbox",
|
|
3067
|
-
"claude",
|
|
3068
|
-
"skill",
|
|
3069
|
-
"strategy"
|
|
3070
|
-
];
|
|
3071
|
-
function deepdiveArgCandidates() {
|
|
3072
|
-
const ids = METRIC_DEFINITIONS.map((m) => m.id);
|
|
3073
|
-
const coreSet = new Set(CORE_DECK_IDS);
|
|
3074
|
-
const core = CORE_DECK_IDS.filter((id) => ids.includes(id));
|
|
3075
|
-
const rest = ids.filter((id) => !coreSet.has(id));
|
|
3076
|
-
return [...SUBCOMMANDS, ...GUIDE_COMPLETE, ...core, ...rest];
|
|
3077
|
-
}
|
|
3078
|
-
function completeDeepdiveLine(line) {
|
|
3079
|
-
const match = /^(?:\/)?deepdive(\s+)(.*)$/i.exec(line);
|
|
3080
|
-
if (!match) return null;
|
|
3081
|
-
const partial = match[2] ?? "";
|
|
3082
|
-
const lower = partial.toLowerCase();
|
|
3083
|
-
const hits = deepdiveArgCandidates().filter((c) => c.startsWith(lower));
|
|
3084
|
-
return [hits, partial];
|
|
3085
|
-
}
|
|
3086
|
-
function deepdiveGhostSuffix(line) {
|
|
3087
|
-
const result = completeDeepdiveLine(line);
|
|
3088
|
-
if (!result) return null;
|
|
3089
|
-
const [hits, partial] = result;
|
|
3090
|
-
if (hits.length === 0) return null;
|
|
3091
|
-
if (hits.length === 1) {
|
|
3092
|
-
const only = hits[0];
|
|
3093
|
-
return only === partial ? null : only.slice(partial.length);
|
|
3094
|
-
}
|
|
3095
|
-
let common = hits[0];
|
|
3096
|
-
for (const hit of hits) {
|
|
3097
|
-
let i = 0;
|
|
3098
|
-
while (i < common.length && i < hit.length && common[i] === hit[i]) i++;
|
|
3099
|
-
common = common.slice(0, i);
|
|
3100
|
-
}
|
|
3101
|
-
return common.length > partial.length ? common.slice(partial.length) : null;
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
// src/cli/repl.ts
|
|
3105
|
-
var GHOST_HINTS = {
|
|
3106
|
-
orient: [
|
|
3107
|
-
'try "pipeline health"',
|
|
3108
|
-
"try /deepdive",
|
|
3109
|
-
"try /deepdive guide",
|
|
3110
|
-
'try "is our retention real for the board?"',
|
|
3111
|
-
'try "what is the most expensive problem to solve?"',
|
|
3112
|
-
'try "board deck on Q3"'
|
|
3113
|
-
],
|
|
3114
|
-
explore: [
|
|
3115
|
-
'try "what is ARR?"',
|
|
3116
|
-
"try /deepdive freshness",
|
|
3117
|
-
'try "how should we fix this?"',
|
|
3118
|
-
'try "which segment is weakest?"',
|
|
3119
|
-
'try "ship a board deck"'
|
|
3120
|
-
]
|
|
3121
|
-
};
|
|
3122
|
-
|
|
3123
|
-
// src/conversation/deepdive-smoke.ts
|
|
3124
|
-
init_metric_tour();
|
|
3125
|
-
init_store();
|
|
3126
|
-
init_progress();
|
|
3127
|
-
function stubCtx(overrides = {}) {
|
|
3128
|
-
return {
|
|
3129
|
-
sessionId: "smoke",
|
|
3130
|
-
sessionFile: "/tmp/ntrp-deepdive-smoke-session.json",
|
|
3131
|
-
oneShot: true,
|
|
3132
|
-
execution: { mode: "interactive" },
|
|
3133
|
-
snapshot: { computeResult: null, divergences: [] },
|
|
3134
|
-
messages: [],
|
|
3135
|
-
conversation: [],
|
|
3136
|
-
stage: "new",
|
|
3137
|
-
deliverables: [],
|
|
3138
|
-
analysis: { primary: "gtm_health", completed: [] },
|
|
3139
|
-
wizardDepth: 0,
|
|
3140
|
-
...overrides
|
|
3141
|
-
};
|
|
3142
|
-
}
|
|
3143
|
-
function assert(cond, msg) {
|
|
3144
|
-
if (!cond) throw new Error(msg);
|
|
3145
|
-
}
|
|
3146
|
-
function section(name) {
|
|
3147
|
-
console.log(` \xB7 ${name}`);
|
|
3148
|
-
}
|
|
3149
|
-
section("registry covers all vitals + SaaS ids");
|
|
3150
|
-
for (const id of VITAL_IDS) {
|
|
3151
|
-
assert(getMetricExplainer(id), `missing vital explainer: ${id}`);
|
|
3152
|
-
}
|
|
3153
|
-
for (const id of SAAS_METRIC_IDS) {
|
|
3154
|
-
assert(getMetricExplainer(id), `missing saas explainer: ${id}`);
|
|
3155
|
-
}
|
|
3156
|
-
assert(METRIC_DEFINITIONS.length === VITAL_IDS.length + SAAS_METRIC_IDS.length, "count mismatch");
|
|
3157
|
-
assert(VITAL_IDS.length === 5, "expected 5 vitals");
|
|
3158
|
-
assert(SAAS_METRIC_IDS.length === 20, `expected 20 saas, got ${SAAS_METRIC_IDS.length}`);
|
|
3159
|
-
section("core deck order");
|
|
3160
|
-
assert(CORE_DECK_IDS.length === 11, `core deck should be 11, got ${CORE_DECK_IDS.length}`);
|
|
3161
|
-
for (const id of CORE_DECK_IDS) {
|
|
3162
|
-
assert(getMetricExplainer(id), `core deck missing: ${id}`);
|
|
3163
|
-
}
|
|
3164
|
-
assert(getCoreDeckExplainers().length === CORE_DECK_IDS.length, "core deck resolve failed");
|
|
3165
|
-
section("how-to guide slides after vitals");
|
|
3166
|
-
assert(GUIDE_DECK_IDS.length === 4, `guide deck should be 4, got ${GUIDE_DECK_IDS.length}`);
|
|
3167
|
-
assert(GUIDE_DECK_IDS.join(",") === "talk,ask,handoff,loop", "guide order talk \u2192 ask \u2192 handoff \u2192 loop");
|
|
3168
|
-
for (const id of GUIDE_DECK_IDS) {
|
|
3169
|
-
const slide = getGuideSlide(id);
|
|
3170
|
-
assert(slide, `guide slide missing: ${id}`);
|
|
3171
|
-
assert(slide.lines.length > 0, `empty guide body: ${id}`);
|
|
3172
|
-
assert(slide.deepdive.length > 0, `empty guide deepdive: ${id}`);
|
|
3173
|
-
}
|
|
3174
|
-
assert(listGuideSlides().length === GUIDE_DECK_IDS.length, "listGuideSlides length");
|
|
3175
|
-
assert(isGuideSectionQuery("guide"), "guide is a section alias");
|
|
3176
|
-
assert(isGuideSectionQuery("how to"), "how to is a section alias");
|
|
3177
|
-
assert(resolveGuideId("guide") === "talk", "guide section starts at talk");
|
|
3178
|
-
assert(resolveGuideId("handoff") === "handoff", "handoff id");
|
|
3179
|
-
assert(resolveGuideId("inbox") === "handoff", "inbox \u2192 handoff");
|
|
3180
|
-
assert(resolveGuideId("claude") === "handoff", "claude \u2192 handoff");
|
|
3181
|
-
assert(resolveGuideId("skill") === "handoff", "skill \u2192 handoff");
|
|
3182
|
-
assert(resolveGuideId("strategy") === "loop", "strategy \u2192 loop");
|
|
3183
|
-
assert(resolveGuideId("connect") === "ask", "connect \u2192 ask");
|
|
3184
|
-
assert(resolveGuideId("arr") === null, "arr is not a guide id");
|
|
3185
|
-
assert(resolveGuideId("freshness") === null, "freshness is not a guide id");
|
|
3186
|
-
var handoffSlide = getGuideSlide("handoff");
|
|
3187
|
-
assert(/inbox skill|paste/i.test(handoffSlide.lines.join("\n")), "handoff slide teaches one-time paste");
|
|
3188
|
-
assert(/latest-handoff|pick up the latest/i.test(handoffSlide.lines.join("\n")), "handoff slide teaches pickup phrase");
|
|
3189
|
-
{
|
|
3190
|
-
const painted = renderMetricSlide(null, {
|
|
3191
|
-
asLines: true,
|
|
3192
|
-
titleOverride: handoffSlide.label,
|
|
3193
|
-
visualOverride: handoffSlide.visual,
|
|
3194
|
-
skipFormula: true,
|
|
3195
|
-
extraLines: [handoffSlide.tagline, "", ...handoffSlide.lines]
|
|
3196
|
-
});
|
|
3197
|
-
const blob = painted.join("\n");
|
|
3198
|
-
assert(/Paste the finder skill once/i.test(blob), "handoff visual renders the one-time paste step");
|
|
3199
|
-
}
|
|
3200
|
-
section("full tour deck: intro + 11 metrics + 4 guides + close");
|
|
3201
|
-
{
|
|
3202
|
-
const full = describeTourDeck();
|
|
3203
|
-
assert(full.length === 17, `full tour should be 17 slides, got ${full.length}`);
|
|
3204
|
-
assert(full[0]?.kind === "intro", "deck starts with intro");
|
|
3205
|
-
assert(full[1]?.id === "arr", "first metric is arr");
|
|
3206
|
-
assert(full[11]?.id === "thread_depth", "last metric is thread_depth (vitals end)");
|
|
3207
|
-
assert(full[12]?.kind === "guide" && full[12]?.id === "talk", "how-to starts after vitals");
|
|
3208
|
-
assert(full[13]?.id === "ask", "ask follows talk");
|
|
3209
|
-
assert(full[14]?.id === "handoff", "handoff is in the how-to section");
|
|
3210
|
-
assert(full[15]?.id === "loop", "loop closes the how-to section");
|
|
3211
|
-
assert(full[16]?.kind === "close", "deck ends with close");
|
|
3212
|
-
const fromGuide = describeTourDeck({ startAt: "talk" });
|
|
3213
|
-
assert(fromGuide[0]?.id === "talk", "startAt talk skips metrics");
|
|
3214
|
-
assert(fromGuide.some((i) => i.id === "handoff"), "section tour still includes handoff");
|
|
3215
|
-
assert(fromGuide.at(-1)?.kind === "close", "section tour still closes");
|
|
3216
|
-
const one = describeTourDeck({ startAt: "handoff", singleSlide: true });
|
|
3217
|
-
assert(one.length === 1 && one[0]?.id === "handoff", "singleSlide handoff is just that card");
|
|
3218
|
-
}
|
|
3219
|
-
section("alias resolution");
|
|
3220
|
-
assert(resolveMetricId("NRR") === "nrr", "NRR alias");
|
|
3221
|
-
assert(resolveMetricId("signal:noise") === "signal_to_noise", "signal:noise");
|
|
3222
|
-
assert(resolveMetricId("thread depth") === "thread_depth", "thread depth");
|
|
3223
|
-
assert(resolveMetricId("annual recurring revenue") === "arr", "ARR alias");
|
|
3224
|
-
assert(resolveMetricId("nope-metric-xyz") === void 0, "unknown should miss");
|
|
3225
|
-
section("slides render at widths without throw");
|
|
3226
|
-
var widths = [60, 80, 120];
|
|
3227
|
-
var originals = process.stdout.columns;
|
|
3228
|
-
for (const w of widths) {
|
|
3229
|
-
Object.defineProperty(process.stdout, "columns", { value: w, configurable: true });
|
|
3230
|
-
for (const explainer of METRIC_DEFINITIONS) {
|
|
3231
|
-
const { lines, width } = buildSlideContent(explainer, {
|
|
3232
|
-
index: 1,
|
|
3233
|
-
total: 11,
|
|
3234
|
-
deepdive: true
|
|
3235
|
-
});
|
|
3236
|
-
assert(width <= Math.max(20, w), `card wider than term at ${w}: ${explainer.id} \u2192 ${width}`);
|
|
3237
|
-
const painted = renderMetricSlide(explainer, {
|
|
3238
|
-
asLines: true,
|
|
3239
|
-
deepdive: false,
|
|
3240
|
-
index: 1,
|
|
3241
|
-
total: 11
|
|
3242
|
-
});
|
|
3243
|
-
const maxVis = measureSlideWidth(painted);
|
|
3244
|
-
assert(maxVis <= Math.max(w + 4, width + 8), `painted too wide for ${explainer.id} @${w}: ${maxVis}`);
|
|
3245
|
-
assert(lines.length > 0, `empty slide content for ${explainer.id}`);
|
|
3246
|
-
renderVisual(explainer.visual, Math.max(20, width - 4));
|
|
3247
|
-
}
|
|
3248
|
-
for (const slide of listGuideSlides()) {
|
|
3249
|
-
const painted = renderMetricSlide(null, {
|
|
3250
|
-
asLines: true,
|
|
3251
|
-
titleOverride: slide.label,
|
|
3252
|
-
visualOverride: slide.visual,
|
|
3253
|
-
skipFormula: true,
|
|
3254
|
-
extraLines: [slide.tagline, "", ...slide.lines],
|
|
3255
|
-
index: 1,
|
|
3256
|
-
total: 17
|
|
3257
|
-
});
|
|
3258
|
-
const maxVis = measureSlideWidth(painted);
|
|
3259
|
-
assert(maxVis <= Math.max(w + 4, 108), `guide painted too wide for ${slide.id} @${w}: ${maxVis}`);
|
|
3260
|
-
assert(painted.length > 0, `empty guide slide: ${slide.id}`);
|
|
3261
|
-
renderVisual(slide.visual, Math.max(20, 60));
|
|
3262
|
-
assert(
|
|
3263
|
-
painted.some((l) => l.includes(slide.label)),
|
|
3264
|
-
`guide card missing title: ${slide.id}`
|
|
3265
|
-
);
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
if (originals != null) {
|
|
3269
|
-
Object.defineProperty(process.stdout, "columns", { value: originals, configurable: true });
|
|
3270
|
-
}
|
|
3271
|
-
section("keyless definition matcher");
|
|
3272
|
-
assert(isDefinitionAsk("what is ARR?"), "what is ARR");
|
|
3273
|
-
assert(isDefinitionAsk("how is freshness calculated?"), "how calculated");
|
|
3274
|
-
assert(isDefinitionAsk("what does NRR mean?"), "what does mean");
|
|
3275
|
-
assert(isDefinitionAsk("define thread depth"), "define");
|
|
3276
|
-
assert(!isDefinitionAsk("what is our ARR?"), "possessive our");
|
|
3277
|
-
assert(isPossessiveMetricAsk("what is our ARR?"), "possessive detect");
|
|
3278
|
-
assert(!isDefinitionAsk("what is my NRR looking like"), "possessive my");
|
|
3279
|
-
assert(matchDefinitionExplainer("what is ARR?")?.id === "arr", "match ARR");
|
|
3280
|
-
assert(matchDefinitionExplainer("how is drop rate calculated?")?.id === "drop_rate", "match drop");
|
|
3281
|
-
assert(matchDefinitionExplainer("what is our ARR?") === void 0, "possessive no match");
|
|
3282
|
-
assert(extractDefinitionQuery("what is pipeline coverage?")?.toLowerCase().includes("pipeline"), "extract");
|
|
3283
|
-
section("definitions appendix");
|
|
3284
|
-
var boardAppendix = buildDefinitionsAppendix(null, {
|
|
3285
|
-
audience: "board",
|
|
3286
|
-
vitals: [
|
|
3287
|
-
{
|
|
3288
|
-
vital_sign: "freshness",
|
|
3289
|
-
score: 29,
|
|
3290
|
-
status: "red",
|
|
3291
|
-
components: {},
|
|
3292
|
-
entity_details: [],
|
|
3293
|
-
dollar_value: 31e5,
|
|
3294
|
-
dollar_label: "pipeline at risk"
|
|
3295
|
-
},
|
|
3296
|
-
{
|
|
3297
|
-
vital_sign: "flow_rate",
|
|
3298
|
-
score: 55,
|
|
3299
|
-
status: "yellow",
|
|
3300
|
-
components: {},
|
|
3301
|
-
entity_details: [],
|
|
3302
|
-
dollar_value: 1e6,
|
|
3303
|
-
dollar_label: "stuck in pipeline"
|
|
3304
|
-
}
|
|
3305
|
-
],
|
|
3306
|
-
prefer: ["freshness"],
|
|
3307
|
-
metrics: [
|
|
3308
|
-
{
|
|
3309
|
-
metric: "nrr",
|
|
3310
|
-
label: "Net Revenue Retention",
|
|
3311
|
-
group: "Retention",
|
|
3312
|
-
value: 95,
|
|
3313
|
-
formatted: "95%",
|
|
3314
|
-
status: "yellow",
|
|
3315
|
-
components: {}
|
|
3316
|
-
},
|
|
3317
|
-
{
|
|
3318
|
-
metric: "arr",
|
|
3319
|
-
label: "ARR",
|
|
3320
|
-
group: "Revenue",
|
|
3321
|
-
value: 24e5,
|
|
3322
|
-
formatted: "$2.4M",
|
|
3323
|
-
status: "green",
|
|
3324
|
-
components: {}
|
|
3325
|
-
}
|
|
3326
|
-
]
|
|
3327
|
-
});
|
|
3328
|
-
assert(boardAppendix.includes("Metric definitions"), "appendix heading");
|
|
3329
|
-
assert(boardAppendix.includes("Freshness"), "includes gating vital");
|
|
3330
|
-
assert(boardAppendix.includes("board"), "board framing label");
|
|
3331
|
-
assert(!boardAppendix.toLowerCase().includes("formula-first"), "board is meaning-first");
|
|
3332
|
-
var opsAppendix = buildDefinitionsAppendix(null, {
|
|
3333
|
-
audience: "ops",
|
|
3334
|
-
prefer: ["freshness"],
|
|
3335
|
-
vitals: [
|
|
3336
|
-
{
|
|
3337
|
-
vital_sign: "freshness",
|
|
3338
|
-
score: 29,
|
|
3339
|
-
status: "red",
|
|
3340
|
-
components: {},
|
|
3341
|
-
entity_details: [],
|
|
3342
|
-
dollar_value: null,
|
|
3343
|
-
dollar_label: null
|
|
3344
|
-
}
|
|
3345
|
-
]
|
|
3346
|
-
});
|
|
3347
|
-
assert(opsAppendix.includes("How it's calculated") || opsAppendix.includes("ops"), "ops framing");
|
|
3348
|
-
var injected = injectDefinitionsAppendix(
|
|
3349
|
-
"# Report\n\nbody\n\n---\n*Generated by ntrp-cli*\n",
|
|
3350
|
-
boardAppendix
|
|
3351
|
-
);
|
|
3352
|
-
assert(injected.includes("Metric definitions"), "inject keeps appendix");
|
|
3353
|
-
assert(injected.indexOf("Metric definitions") < injected.indexOf("*Generated by ntrp-cli*"), "before footer");
|
|
3354
|
-
section("ghost hints teach /deepdive");
|
|
3355
|
-
assert(
|
|
3356
|
-
(GHOST_HINTS.orient ?? []).some((h) => h.includes("/deepdive")),
|
|
3357
|
-
"orient ghost includes /deepdive"
|
|
3358
|
-
);
|
|
3359
|
-
assert(
|
|
3360
|
-
(GHOST_HINTS.orient ?? []).some((h) => h.includes("/deepdive guide")),
|
|
3361
|
-
"orient ghost includes /deepdive guide"
|
|
3362
|
-
);
|
|
3363
|
-
assert(
|
|
3364
|
-
(GHOST_HINTS.explore ?? []).some((h) => h.includes("/deepdive")),
|
|
3365
|
-
"explore ghost includes /deepdive"
|
|
3366
|
-
);
|
|
3367
|
-
section("getDeepdiveNudge one-shot home chip + skip defer");
|
|
3368
|
-
deleteConfigValue("metrics-tour-completed");
|
|
3369
|
-
deleteConfigValue("metrics-tour-skipped");
|
|
3370
|
-
deleteConfigValue("metrics-tour-home-nudge-seen");
|
|
3371
|
-
var cold = stubCtx({ stage: "new" });
|
|
3372
|
-
assert(getDeepdiveNudge(cold) === null, "no nudge before analysis");
|
|
3373
|
-
markMetricsTourSkipped();
|
|
3374
|
-
assert(hasSkippedMetricsTour(), "skip recorded");
|
|
3375
|
-
assert(!hasCompletedMetricsTour(), "skip does not complete tour");
|
|
3376
|
-
assert(getDeepdiveNudge(cold) === null, "still no nudge before analysis after skip");
|
|
3377
|
-
var analyzed = stubCtx({
|
|
3378
|
-
stage: "analyzed",
|
|
3379
|
-
analysis: { primary: "gtm_health", completed: ["gtm_health"] }
|
|
3380
|
-
});
|
|
3381
|
-
assert(getDeepdiveNudge(analyzed)?.command === "/deepdive", "nudge after analysis when incomplete");
|
|
3382
|
-
markDeepdiveHomeNudgeSeen();
|
|
3383
|
-
assert(hasSeenDeepdiveHomeNudge(), "home nudge marked seen");
|
|
3384
|
-
assert(getDeepdiveNudge(analyzed) === null, "nudge suppressed after seen");
|
|
3385
|
-
deleteConfigValue("metrics-tour-home-nudge-seen");
|
|
3386
|
-
markMetricsTourCompleted();
|
|
3387
|
-
assert(hasCompletedMetricsTour(), "tour marked completed");
|
|
3388
|
-
assert(getDeepdiveNudge(analyzed) === null, "nudge suppressed after tour completed");
|
|
3389
|
-
section("deepdive tab/ghost completion");
|
|
3390
|
-
{
|
|
3391
|
-
const [hits, partial] = completeDeepdiveLine("/deepdive fr");
|
|
3392
|
-
assert(partial === "fr", "completer partial is fr");
|
|
3393
|
-
assert(hits.includes("freshness"), "completer returns freshness for /deepdive fr");
|
|
3394
|
-
assert(!hits.includes("nrr"), "fr does not match nrr");
|
|
3395
|
-
}
|
|
3396
|
-
assert(completeDeepdiveLine("/deepdive") === null, "bare /deepdive is command-level");
|
|
3397
|
-
assert(deepdiveGhostSuffix("/deepdive fr") === "eshness" || (deepdiveGhostSuffix("/deepdive fr") ?? "").startsWith("e"), "ghost extends fr");
|
|
3398
|
-
assert(completeDeepdiveLine("/deepdive list")?.[0].includes("list"), "list subcommand completes");
|
|
3399
|
-
{
|
|
3400
|
-
const [guideHits] = completeDeepdiveLine("/deepdive gu");
|
|
3401
|
-
assert(guideHits.includes("guide"), "completer returns guide for /deepdive gu");
|
|
3402
|
-
}
|
|
3403
|
-
{
|
|
3404
|
-
const [handoffHits] = completeDeepdiveLine("/deepdive ha");
|
|
3405
|
-
assert(handoffHits.includes("handoff"), "completer returns handoff for /deepdive ha");
|
|
3406
|
-
}
|
|
3407
|
-
section("metrics_tour progress milestone");
|
|
3408
|
-
wipeProgressFiles();
|
|
3409
|
-
assert(unlockProgressMilestone(METRICS_TOUR_MILESTONE_ID), "milestone unlocks once");
|
|
3410
|
-
assert(!unlockProgressMilestone(METRICS_TOUR_MILESTONE_ID), "milestone unlock is idempotent");
|
|
3411
|
-
assert(
|
|
3412
|
-
loadProgress().milestones_unlocked.includes(METRICS_TOUR_MILESTONE_ID),
|
|
3413
|
-
"milestone persisted"
|
|
3414
|
-
);
|
|
3415
|
-
console.log("PASS deepdive-smoke (unit)");
|
|
3416
|
-
//# sourceMappingURL=deepdive-smoke.js.map
|