@contractspec/module.lifecycle-core 1.56.1 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/stage-scorer.test.d.ts +2 -0
- package/dist/__tests__/stage-scorer.test.d.ts.map +1 -0
- package/dist/adapters/analytics-adapter.d.ts +7 -11
- package/dist/adapters/analytics-adapter.d.ts.map +1 -1
- package/dist/adapters/intent-adapter.d.ts +5 -9
- package/dist/adapters/intent-adapter.d.ts.map +1 -1
- package/dist/adapters/questionnaire-adapter.d.ts +7 -11
- package/dist/adapters/questionnaire-adapter.d.ts.map +1 -1
- package/dist/browser/index.js +427 -0
- package/dist/collectors/signal-collector.d.ts +17 -21
- package/dist/collectors/signal-collector.d.ts.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +427 -5
- package/dist/node/index.js +427 -0
- package/dist/orchestrator/lifecycle-orchestrator.d.ts +16 -19
- package/dist/orchestrator/lifecycle-orchestrator.d.ts.map +1 -1
- package/dist/planning/milestone-planner.d.ts +5 -9
- package/dist/planning/milestone-planner.d.ts.map +1 -1
- package/dist/scoring/stage-scorer.d.ts +18 -19
- package/dist/scoring/stage-scorer.d.ts.map +1 -1
- package/package.json +20 -15
- package/dist/collectors/signal-collector.js +0 -65
- package/dist/collectors/signal-collector.js.map +0 -1
- package/dist/data/milestones-catalog.js +0 -74
- package/dist/data/milestones-catalog.js.map +0 -1
- package/dist/data/stage-weights.js +0 -170
- package/dist/data/stage-weights.js.map +0 -1
- package/dist/orchestrator/lifecycle-orchestrator.js +0 -53
- package/dist/orchestrator/lifecycle-orchestrator.js.map +0 -1
- package/dist/planning/milestone-planner.js +0 -17
- package/dist/planning/milestone-planner.js.map +0 -1
- package/dist/scoring/stage-scorer.js +0 -64
- package/dist/scoring/stage-scorer.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,428 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/collectors/signal-collector.ts
|
|
3
|
+
import {
|
|
4
|
+
CapitalPhase,
|
|
5
|
+
CompanyPhase,
|
|
6
|
+
ProductPhase
|
|
7
|
+
} from "@contractspec/lib.lifecycle";
|
|
8
|
+
var DEFAULT_AXES = {
|
|
9
|
+
product: ProductPhase.Sketch,
|
|
10
|
+
company: CompanyPhase.Solo,
|
|
11
|
+
capital: CapitalPhase.Bootstrapped
|
|
12
|
+
};
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
class StageSignalCollector {
|
|
15
|
+
options;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
async collect(input = {}) {
|
|
20
|
+
const axes = {
|
|
21
|
+
...DEFAULT_AXES,
|
|
22
|
+
...input.axes ?? {}
|
|
23
|
+
};
|
|
24
|
+
const metricsSnapshots = [];
|
|
25
|
+
const aggregatedSignals = [...input.signals ?? []];
|
|
26
|
+
const questionnaireAnswers = {
|
|
27
|
+
...input.questionnaireAnswers ?? {}
|
|
28
|
+
};
|
|
29
|
+
if (input.metrics) {
|
|
30
|
+
metricsSnapshots.push(input.metrics);
|
|
31
|
+
}
|
|
32
|
+
if (this.options.analyticsAdapter) {
|
|
33
|
+
const result = await this.options.analyticsAdapter.fetch();
|
|
34
|
+
if (result.axes)
|
|
35
|
+
Object.assign(axes, result.axes);
|
|
36
|
+
if (result.metrics)
|
|
37
|
+
metricsSnapshots.push(result.metrics);
|
|
38
|
+
if (result.signals)
|
|
39
|
+
aggregatedSignals.push(...result.signals);
|
|
40
|
+
}
|
|
41
|
+
if (this.options.questionnaireAdapter) {
|
|
42
|
+
const result = await this.options.questionnaireAdapter.fetch();
|
|
43
|
+
if (result.axes)
|
|
44
|
+
Object.assign(axes, result.axes);
|
|
45
|
+
if (result.signals)
|
|
46
|
+
aggregatedSignals.push(...result.signals);
|
|
47
|
+
Object.assign(questionnaireAnswers, result.answers);
|
|
48
|
+
}
|
|
49
|
+
if (this.options.intentAdapter) {
|
|
50
|
+
const result = await this.options.intentAdapter.fetch();
|
|
51
|
+
if (result.signals)
|
|
52
|
+
aggregatedSignals.push(...result.signals);
|
|
53
|
+
}
|
|
54
|
+
const metrics = mergeMetricSnapshots(metricsSnapshots);
|
|
55
|
+
return {
|
|
56
|
+
axes,
|
|
57
|
+
metrics,
|
|
58
|
+
signals: dedupeSignals(aggregatedSignals),
|
|
59
|
+
questionnaireAnswers: Object.keys(questionnaireAnswers).length ? questionnaireAnswers : undefined
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var mergeMetricSnapshots = (snapshots) => snapshots.reduce((acc, snapshot) => {
|
|
64
|
+
Object.entries(snapshot ?? {}).forEach(([key, value]) => {
|
|
65
|
+
if (value !== undefined && value !== null) {
|
|
66
|
+
acc[key] = value;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return acc;
|
|
70
|
+
}, {});
|
|
71
|
+
var dedupeSignals = (signals) => {
|
|
72
|
+
const seen = new Set;
|
|
73
|
+
return signals.filter((signal) => {
|
|
74
|
+
if (!signal.id)
|
|
75
|
+
return true;
|
|
76
|
+
if (seen.has(signal.id))
|
|
77
|
+
return false;
|
|
78
|
+
seen.add(signal.id);
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
// src/scoring/stage-scorer.ts
|
|
83
|
+
import {
|
|
84
|
+
LIFECYCLE_STAGE_META,
|
|
85
|
+
LifecycleStage
|
|
86
|
+
} from "@contractspec/lib.lifecycle";
|
|
87
|
+
// src/data/stage-weights.json
|
|
88
|
+
var stage_weights_default = {
|
|
89
|
+
Exploration: {
|
|
90
|
+
base: 0.35,
|
|
91
|
+
metrics: {
|
|
92
|
+
activeUsers: { weight: 0.3, direction: "lte", threshold: 5 },
|
|
93
|
+
customerCount: { weight: 0.2, direction: "lte", threshold: 3 },
|
|
94
|
+
teamSize: { weight: 0.1, direction: "lte", threshold: 3 }
|
|
95
|
+
},
|
|
96
|
+
signalKinds: {
|
|
97
|
+
qualitative: 0.4,
|
|
98
|
+
metric: 0.1
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
ProblemSolutionFit: {
|
|
102
|
+
base: 0.4,
|
|
103
|
+
metrics: {
|
|
104
|
+
activeUsers: { weight: 0.2, direction: "gte", threshold: 5 },
|
|
105
|
+
customerCount: { weight: 0.2, direction: "gte", threshold: 3 }
|
|
106
|
+
},
|
|
107
|
+
signalKinds: {
|
|
108
|
+
qualitative: 0.3,
|
|
109
|
+
event: 0.1
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
MvpEarlyTraction: {
|
|
113
|
+
base: 0.45,
|
|
114
|
+
metrics: {
|
|
115
|
+
activeUsers: { weight: 0.25, direction: "gte", threshold: 25 },
|
|
116
|
+
weeklyActiveUsers: {
|
|
117
|
+
weight: 0.25,
|
|
118
|
+
direction: "gte",
|
|
119
|
+
threshold: 20
|
|
120
|
+
},
|
|
121
|
+
retentionRate: { weight: 0.2, direction: "gte", threshold: 0.25 }
|
|
122
|
+
},
|
|
123
|
+
signalKinds: {
|
|
124
|
+
metric: 0.15,
|
|
125
|
+
event: 0.1
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
ProductMarketFit: {
|
|
129
|
+
base: 0.5,
|
|
130
|
+
metrics: {
|
|
131
|
+
retentionRate: {
|
|
132
|
+
weight: 0.35,
|
|
133
|
+
direction: "gte",
|
|
134
|
+
threshold: 0.45
|
|
135
|
+
},
|
|
136
|
+
monthlyRecurringRevenue: {
|
|
137
|
+
weight: 0.25,
|
|
138
|
+
direction: "gte",
|
|
139
|
+
threshold: 1e4
|
|
140
|
+
},
|
|
141
|
+
customerCount: { weight: 0.2, direction: "gte", threshold: 30 }
|
|
142
|
+
},
|
|
143
|
+
signalKinds: {
|
|
144
|
+
metric: 0.15,
|
|
145
|
+
event: 0.1
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
GrowthScaleUp: {
|
|
149
|
+
base: 0.55,
|
|
150
|
+
metrics: {
|
|
151
|
+
retentionRate: { weight: 0.2, direction: "gte", threshold: 0.55 },
|
|
152
|
+
monthlyRecurringRevenue: {
|
|
153
|
+
weight: 0.3,
|
|
154
|
+
direction: "gte",
|
|
155
|
+
threshold: 50000
|
|
156
|
+
},
|
|
157
|
+
teamSize: { weight: 0.15, direction: "gte", threshold: 15 }
|
|
158
|
+
},
|
|
159
|
+
signalKinds: {
|
|
160
|
+
event: 0.2,
|
|
161
|
+
metric: 0.15
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
ExpansionPlatform: {
|
|
165
|
+
base: 0.6,
|
|
166
|
+
metrics: {
|
|
167
|
+
monthlyRecurringRevenue: {
|
|
168
|
+
weight: 0.35,
|
|
169
|
+
direction: "gte",
|
|
170
|
+
threshold: 150000
|
|
171
|
+
},
|
|
172
|
+
customerCount: { weight: 0.2, direction: "gte", threshold: 100 },
|
|
173
|
+
teamSize: { weight: 0.15, direction: "gte", threshold: 40 }
|
|
174
|
+
},
|
|
175
|
+
signalKinds: {
|
|
176
|
+
event: 0.2,
|
|
177
|
+
milestone: 0.1
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
MaturityRenewal: {
|
|
181
|
+
base: 0.6,
|
|
182
|
+
metrics: {
|
|
183
|
+
monthlyRecurringRevenue: {
|
|
184
|
+
weight: 0.25,
|
|
185
|
+
direction: "gte",
|
|
186
|
+
threshold: 250000
|
|
187
|
+
},
|
|
188
|
+
teamSize: { weight: 0.15, direction: "gte", threshold: 80 },
|
|
189
|
+
burnMultiple: { weight: 0.2, direction: "lte", threshold: 1.5 }
|
|
190
|
+
},
|
|
191
|
+
signalKinds: {
|
|
192
|
+
event: 0.2,
|
|
193
|
+
milestone: 0.15
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/scoring/stage-scorer.ts
|
|
199
|
+
var DEFAULT_WEIGHTS = stage_weights_default;
|
|
200
|
+
|
|
201
|
+
class StageScorer {
|
|
202
|
+
weights;
|
|
203
|
+
constructor(weights = DEFAULT_WEIGHTS) {
|
|
204
|
+
this.weights = weights;
|
|
205
|
+
}
|
|
206
|
+
score(input) {
|
|
207
|
+
const kindStrength = evaluateSignalKinds(input.signals);
|
|
208
|
+
const scores = Object.values(LifecycleStage).filter(isStageValue).map((stage) => {
|
|
209
|
+
const stageName = LifecycleStage[stage];
|
|
210
|
+
const config = this.weights[stageName] ?? { base: 0.5 };
|
|
211
|
+
let score = config.base ?? 0.5;
|
|
212
|
+
let contributions = 0;
|
|
213
|
+
const totalPossible = Object.keys(config.metrics ?? {}).length + Object.keys(config.signalKinds ?? {}).length || 1;
|
|
214
|
+
const supportingSignals = [];
|
|
215
|
+
if (config.metrics) {
|
|
216
|
+
Object.entries(config.metrics).forEach(([metricKey, metricConfig]) => {
|
|
217
|
+
const value = input.metrics[metricKey];
|
|
218
|
+
if (value === undefined || value === null)
|
|
219
|
+
return;
|
|
220
|
+
if (passesThreshold(value, metricConfig)) {
|
|
221
|
+
score += metricConfig.weight;
|
|
222
|
+
contributions += 1;
|
|
223
|
+
supportingSignals.push(`metric:${metricKey}`);
|
|
224
|
+
} else {
|
|
225
|
+
score += metricConfig.weight * 0.25;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (config.signalKinds) {
|
|
230
|
+
Object.entries(config.signalKinds).forEach(([kind, weight]) => {
|
|
231
|
+
const strength = kindStrength[kind] ?? 0;
|
|
232
|
+
if (strength > 0 && typeof weight === "number") {
|
|
233
|
+
score += weight;
|
|
234
|
+
contributions += 1;
|
|
235
|
+
supportingSignals.push(`signal:${kind}`);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
score = clamp(score, 0, 1.25);
|
|
240
|
+
const confidence = clamp(contributions / totalPossible, 0.1, 1);
|
|
241
|
+
return {
|
|
242
|
+
stage,
|
|
243
|
+
score,
|
|
244
|
+
confidence,
|
|
245
|
+
supportingSignals
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
return scores.sort((a, b) => {
|
|
249
|
+
if (b.score === a.score) {
|
|
250
|
+
return b.confidence - a.confidence;
|
|
251
|
+
}
|
|
252
|
+
return b.score - a.score;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
var passesThreshold = (value, config) => {
|
|
257
|
+
const direction = config.direction ?? "gte";
|
|
258
|
+
if (direction === "gte") {
|
|
259
|
+
return value >= config.threshold;
|
|
260
|
+
}
|
|
261
|
+
return value <= config.threshold;
|
|
262
|
+
};
|
|
263
|
+
var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
264
|
+
var evaluateSignalKinds = (signals) => signals.reduce((acc, signal) => {
|
|
265
|
+
const key = signal.kind ?? "unknown";
|
|
266
|
+
acc[key] = (acc[key] ?? 0) + (signal.weight ?? 1);
|
|
267
|
+
return acc;
|
|
268
|
+
}, {});
|
|
269
|
+
var isStageValue = (value) => typeof value === "number" && (value in LIFECYCLE_STAGE_META);
|
|
270
|
+
// src/orchestrator/lifecycle-orchestrator.ts
|
|
271
|
+
import {
|
|
272
|
+
LIFECYCLE_STAGE_META as LIFECYCLE_STAGE_META2,
|
|
273
|
+
LifecycleStage as LifecycleStage2
|
|
274
|
+
} from "@contractspec/lib.lifecycle";
|
|
275
|
+
|
|
276
|
+
class LifecycleOrchestrator {
|
|
277
|
+
collector;
|
|
278
|
+
scorer;
|
|
279
|
+
planner;
|
|
280
|
+
constructor(options) {
|
|
281
|
+
this.collector = options.collector;
|
|
282
|
+
this.scorer = options.scorer;
|
|
283
|
+
this.planner = options.milestonePlanner;
|
|
284
|
+
}
|
|
285
|
+
async run(input) {
|
|
286
|
+
const collected = await this.collector.collect(input);
|
|
287
|
+
const scorecard = this.scorer.score({
|
|
288
|
+
metrics: collected.metrics,
|
|
289
|
+
signals: collected.signals
|
|
290
|
+
});
|
|
291
|
+
const top = scorecard[0] ?? fallbackScore();
|
|
292
|
+
const meta = LIFECYCLE_STAGE_META2[top.stage];
|
|
293
|
+
return {
|
|
294
|
+
stage: top.stage,
|
|
295
|
+
confidence: top.confidence,
|
|
296
|
+
axes: collected.axes,
|
|
297
|
+
signals: collected.signals,
|
|
298
|
+
metrics: toMetricRecord(collected.metrics),
|
|
299
|
+
gaps: meta.focusAreas.slice(0, 3),
|
|
300
|
+
focusAreas: meta.focusAreas,
|
|
301
|
+
scorecard,
|
|
302
|
+
generatedAt: new Date().toISOString()
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
getUpcomingMilestones(stage, completedMilestoneIds = [], limit = 5) {
|
|
306
|
+
if (!this.planner)
|
|
307
|
+
return [];
|
|
308
|
+
return this.planner.getUpcoming(stage, completedMilestoneIds, limit);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
var fallbackScore = () => ({
|
|
312
|
+
stage: LifecycleStage2.Exploration,
|
|
313
|
+
score: 0.3,
|
|
314
|
+
confidence: 0.3,
|
|
315
|
+
supportingSignals: []
|
|
316
|
+
});
|
|
317
|
+
var toMetricRecord = (snapshot) => Object.entries(snapshot ?? {}).reduce((acc, [key, value]) => {
|
|
318
|
+
if (typeof value === "number") {
|
|
319
|
+
acc[key] = value;
|
|
320
|
+
}
|
|
321
|
+
return acc;
|
|
322
|
+
}, {});
|
|
323
|
+
// src/data/milestones-catalog.json
|
|
324
|
+
var milestones_catalog_default = [
|
|
325
|
+
{
|
|
326
|
+
id: "stage0-problem-statement",
|
|
327
|
+
stage: 0,
|
|
328
|
+
category: "product",
|
|
329
|
+
title: "Write the pain statement",
|
|
330
|
+
description: "Capture the clearest description of the top problem in the customer\u2019s own words.",
|
|
331
|
+
priority: 1,
|
|
332
|
+
actionItems: [
|
|
333
|
+
"Interview at least 5 ideal customers",
|
|
334
|
+
"Synthesize quotes into a one-page brief"
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "stage1-prototype-loop",
|
|
339
|
+
stage: 1,
|
|
340
|
+
category: "product",
|
|
341
|
+
title: "Prototype feedback loop",
|
|
342
|
+
description: "Ship a clickable prototype and gather 3 rounds of feedback.",
|
|
343
|
+
priority: 2,
|
|
344
|
+
actionItems: [
|
|
345
|
+
"Create a low-fidelity prototype",
|
|
346
|
+
"Schedule standing feedback calls"
|
|
347
|
+
]
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: "stage2-activation",
|
|
351
|
+
stage: 2,
|
|
352
|
+
category: "operations",
|
|
353
|
+
title: "Activation checklist",
|
|
354
|
+
description: "Define the minimum steps required for a new user to succeed.",
|
|
355
|
+
priority: 1,
|
|
356
|
+
actionItems: [
|
|
357
|
+
"Document onboarding flow",
|
|
358
|
+
"Instrument activation analytics"
|
|
359
|
+
]
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: "stage3-retention-narrative",
|
|
363
|
+
stage: 3,
|
|
364
|
+
category: "product",
|
|
365
|
+
title: "Retention narrative",
|
|
366
|
+
description: "Create the before/after story that proves why users stay.",
|
|
367
|
+
priority: 2,
|
|
368
|
+
actionItems: [
|
|
369
|
+
"Interview 3 retained users",
|
|
370
|
+
"Publish a one-pager with concrete metrics"
|
|
371
|
+
]
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: "stage4-growth-loop",
|
|
375
|
+
stage: 4,
|
|
376
|
+
category: "growth",
|
|
377
|
+
title: "Install a growth loop",
|
|
378
|
+
description: "Stand up a repeatable acquisition \u2192 activation \u2192 referral motion.",
|
|
379
|
+
priority: 1,
|
|
380
|
+
actionItems: [
|
|
381
|
+
"Define loop metrics",
|
|
382
|
+
"Assign owners for each stage",
|
|
383
|
+
"Review weekly"
|
|
384
|
+
]
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "stage5-platform-blueprint",
|
|
388
|
+
stage: 5,
|
|
389
|
+
category: "product",
|
|
390
|
+
title: "Platform blueprint",
|
|
391
|
+
description: "Align on APIs, integrations, and governance for partners.",
|
|
392
|
+
priority: 2,
|
|
393
|
+
actionItems: [
|
|
394
|
+
"Create integration scoring rubric",
|
|
395
|
+
"Publish partner onboarding checklist"
|
|
396
|
+
]
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "stage6-renewal-ops",
|
|
400
|
+
stage: 6,
|
|
401
|
+
category: "operations",
|
|
402
|
+
title: "Renewal operating rhythm",
|
|
403
|
+
description: "Decide whether to optimize, reinvest, or sunset each major surface.",
|
|
404
|
+
priority: 1,
|
|
405
|
+
actionItems: [
|
|
406
|
+
"Hold quarterly renewal review",
|
|
407
|
+
"Document reinvestment bets"
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
// src/planning/milestone-planner.ts
|
|
413
|
+
class LifecycleMilestonePlanner {
|
|
414
|
+
milestones;
|
|
415
|
+
constructor(customCatalog) {
|
|
416
|
+
this.milestones = customCatalog ?? milestones_catalog_default;
|
|
417
|
+
}
|
|
418
|
+
getUpcoming(stage, completedIds = [], limit = 5) {
|
|
419
|
+
const completedSet = new Set(completedIds);
|
|
420
|
+
return this.milestones.filter((milestone) => milestone.stage === stage && !completedSet.has(milestone.id)).sort((a, b) => a.priority - b.priority).slice(0, limit);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
export {
|
|
424
|
+
StageSignalCollector,
|
|
425
|
+
StageScorer,
|
|
426
|
+
LifecycleOrchestrator,
|
|
427
|
+
LifecycleMilestonePlanner
|
|
428
|
+
};
|