@open-product-primer/cli 0.1.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/LICENSE +21 -0
- package/README.md +31 -0
- package/bin/oprim.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +22 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +176 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +131 -0
- package/dist/commands/measure.d.ts +2 -0
- package/dist/commands/measure.js +207 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +81 -0
- package/dist/lib/detect.d.ts +10 -0
- package/dist/lib/detect.js +72 -0
- package/dist/lib/install-agent.d.ts +7 -0
- package/dist/lib/install-agent.js +421 -0
- package/dist/lib/measure.d.ts +55 -0
- package/dist/lib/measure.js +250 -0
- package/dist/lib/scaffold.d.ts +4 -0
- package/dist/lib/scaffold.js +60 -0
- package/dist/lib/templates.d.ts +6 -0
- package/dist/lib/templates.js +120 -0
- package/package.json +60 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.computeDateWindow = computeDateWindow;
|
|
37
|
+
exports.generateAmplitudeDefinition = generateAmplitudeDefinition;
|
|
38
|
+
exports.generateBigQuerySQL = generateBigQuerySQL;
|
|
39
|
+
exports.classifyStatus = classifyStatus;
|
|
40
|
+
exports.writeRunResult = writeRunResult;
|
|
41
|
+
exports.runAmplitudeMetric = runAmplitudeMetric;
|
|
42
|
+
exports.runBigQueryMetric = runBigQueryMetric;
|
|
43
|
+
exports.scanCriteriaForSourceType = scanCriteriaForSourceType;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const yaml = __importStar(require("js-yaml"));
|
|
47
|
+
// ─── Date window ──────────────────────────────────────────────────────────────
|
|
48
|
+
function computeDateWindow(launchDate, timeframe) {
|
|
49
|
+
if (!launchDate)
|
|
50
|
+
return null;
|
|
51
|
+
const match = timeframe.match(/^(\d+)\s+days/);
|
|
52
|
+
if (!match)
|
|
53
|
+
return null;
|
|
54
|
+
const days = parseInt(match[1], 10);
|
|
55
|
+
const start = new Date(launchDate + 'T00:00:00Z');
|
|
56
|
+
const end = new Date(launchDate + 'T00:00:00Z');
|
|
57
|
+
end.setUTCDate(end.getUTCDate() + days);
|
|
58
|
+
return {
|
|
59
|
+
start: start.toISOString().slice(0, 10),
|
|
60
|
+
end: end.toISOString().slice(0, 10),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// ─── Amplitude generator ──────────────────────────────────────────────────────
|
|
64
|
+
function generateAmplitudeDefinition(metric) {
|
|
65
|
+
const def = metric.source.definition;
|
|
66
|
+
const window = computeDateWindow(metric.launch_date ?? null, metric.timeframe);
|
|
67
|
+
const isDenominator = !!def.denominator_event;
|
|
68
|
+
const events = [{ event_type: def.event }];
|
|
69
|
+
if (isDenominator && def.denominator_event) {
|
|
70
|
+
events.push({ event_type: def.denominator_event });
|
|
71
|
+
}
|
|
72
|
+
const result = {
|
|
73
|
+
chart_type: isDenominator ? 'funnel' : 'event_count',
|
|
74
|
+
events,
|
|
75
|
+
aggregation: def.aggregation,
|
|
76
|
+
};
|
|
77
|
+
if (window) {
|
|
78
|
+
result['window'] = window;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
result['_comment'] = 'launch_date required to compute window';
|
|
82
|
+
}
|
|
83
|
+
const segmentFilters = [];
|
|
84
|
+
if (metric.segment)
|
|
85
|
+
segmentFilters.push(metric.segment);
|
|
86
|
+
result['segment_filters'] = segmentFilters;
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
// ─── BigQuery SQL generator ───────────────────────────────────────────────────
|
|
90
|
+
function generateBigQuerySQL(metric) {
|
|
91
|
+
const def = metric.source.definition;
|
|
92
|
+
const window = computeDateWindow(metric.launch_date ?? null, metric.timeframe);
|
|
93
|
+
const windowComment = window
|
|
94
|
+
? `-- Window: ${window.start} to ${window.end}`
|
|
95
|
+
: '-- launch_date required to compute window';
|
|
96
|
+
const header = [
|
|
97
|
+
`-- Metric: ${metric.name}`,
|
|
98
|
+
`-- Baseline: ${metric.baseline} | Target: ${metric.target}`,
|
|
99
|
+
windowComment,
|
|
100
|
+
].join('\n');
|
|
101
|
+
const dateFilter = window
|
|
102
|
+
? ` AND DATE(timestamp) BETWEEN '${window.start}' AND '${window.end}'`
|
|
103
|
+
: ` -- AND DATE(timestamp) BETWEEN <start> AND <end> -- set launch_date to enable`;
|
|
104
|
+
const agg = buildSQLAggregation(def);
|
|
105
|
+
if (def.denominator_query) {
|
|
106
|
+
const numerator = `SELECT ${agg}\nFROM \`${def.table}\`\nWHERE (${def.filter})\n${dateFilter}`;
|
|
107
|
+
const indented = numerator.split('\n').join('\n ');
|
|
108
|
+
return `${header}\nSELECT (\n ${indented}\n) / (\n ${def.denominator_query}\n)`;
|
|
109
|
+
}
|
|
110
|
+
return `${header}\nSELECT ${agg}\nFROM \`${def.table}\`\nWHERE (${def.filter})\n${dateFilter}`;
|
|
111
|
+
}
|
|
112
|
+
function buildSQLAggregation(def) {
|
|
113
|
+
switch (def.aggregation) {
|
|
114
|
+
case 'count_distinct':
|
|
115
|
+
return `COUNT(DISTINCT ${def.metric_column})`;
|
|
116
|
+
case 'count':
|
|
117
|
+
return `COUNT(${def.metric_column})`;
|
|
118
|
+
case 'avg':
|
|
119
|
+
return `AVG(${def.metric_column})`;
|
|
120
|
+
default:
|
|
121
|
+
return `SUM(${def.metric_column})`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ─── Status classification ────────────────────────────────────────────────────
|
|
125
|
+
function classifyStatus(actual, target) {
|
|
126
|
+
if (actual === null)
|
|
127
|
+
return 'pending';
|
|
128
|
+
return actual >= target ? 'hit' : 'missed';
|
|
129
|
+
}
|
|
130
|
+
// ─── Run result writer ────────────────────────────────────────────────────────
|
|
131
|
+
function writeRunResult(betId, measurementsDir, results, runDate) {
|
|
132
|
+
const data = {
|
|
133
|
+
bet_id: betId,
|
|
134
|
+
run_date: runDate,
|
|
135
|
+
metrics: results.map((r) => ({
|
|
136
|
+
id: r.id,
|
|
137
|
+
name: r.name,
|
|
138
|
+
source: r.source,
|
|
139
|
+
actual: r.actual,
|
|
140
|
+
target: r.target,
|
|
141
|
+
status: r.status,
|
|
142
|
+
notes: r.notes,
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
145
|
+
fs.writeFileSync(path.join(measurementsDir, `run-${runDate}.yaml`), yaml.dump(data, { indent: 2 }), 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
// ─── Amplitude execution ──────────────────────────────────────────────────────
|
|
148
|
+
async function runAmplitudeMetric(definitionPath, apiKey) {
|
|
149
|
+
const def = JSON.parse(fs.readFileSync(definitionPath, 'utf-8'));
|
|
150
|
+
if (!def.window) {
|
|
151
|
+
return { actual: null, notes: 'No date window — set launch_date in criteria.yaml' };
|
|
152
|
+
}
|
|
153
|
+
const startDate = def.window.start.replace(/-/g, '');
|
|
154
|
+
const endDate = def.window.end.replace(/-/g, '');
|
|
155
|
+
const auth = 'Basic ' + Buffer.from(`${apiKey}:`).toString('base64');
|
|
156
|
+
let response;
|
|
157
|
+
if (def.chart_type === 'funnel') {
|
|
158
|
+
response = await fetch('https://amplitude.com/api/2/funnels', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { Authorization: auth, 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
e: def.events,
|
|
163
|
+
m: def.aggregation === 'unique_users' ? 'UNIQUE' : 'TOTALS',
|
|
164
|
+
start: startDate,
|
|
165
|
+
end: endDate,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const params = new URLSearchParams({
|
|
171
|
+
e: JSON.stringify({ event_type: def.events[0]?.event_type }),
|
|
172
|
+
m: def.aggregation === 'unique_users' ? 'UNIQUE' : 'TOTALS',
|
|
173
|
+
start: startDate,
|
|
174
|
+
end: endDate,
|
|
175
|
+
i: '30',
|
|
176
|
+
});
|
|
177
|
+
response = await fetch(`https://amplitude.com/api/2/events/segmentation?${params}`, {
|
|
178
|
+
headers: { Authorization: auth },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const text = await response.text();
|
|
183
|
+
return { actual: null, notes: `Amplitude API ${response.status}: ${text.slice(0, 200)}` };
|
|
184
|
+
}
|
|
185
|
+
const data = (await response.json());
|
|
186
|
+
const actual = extractAmplitudeScalar(data, def.chart_type);
|
|
187
|
+
return { actual, notes: actual === null ? 'Could not extract scalar from Amplitude response' : null };
|
|
188
|
+
}
|
|
189
|
+
function extractAmplitudeScalar(data, chartType) {
|
|
190
|
+
if (chartType === 'funnel') {
|
|
191
|
+
const steps = data?.data?.steps;
|
|
192
|
+
if (steps && steps.length > 0) {
|
|
193
|
+
const rate = steps[steps.length - 1]?.overall_conversion_rate;
|
|
194
|
+
return typeof rate === 'number' ? rate : null;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const series = data?.data?.series;
|
|
199
|
+
if (series && series.length > 0 && series[0].length > 0) {
|
|
200
|
+
const val = series[0][0]?.value;
|
|
201
|
+
return typeof val === 'number' ? val : null;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
// ─── BigQuery execution ───────────────────────────────────────────────────────
|
|
206
|
+
async function runBigQueryMetric(sqlPath) {
|
|
207
|
+
let BQMod;
|
|
208
|
+
try {
|
|
209
|
+
BQMod = await Promise.resolve().then(() => __importStar(require('@google-cloud/bigquery')));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return { actual: null, notes: '@google-cloud/bigquery not installed' };
|
|
213
|
+
}
|
|
214
|
+
const sql = fs.readFileSync(sqlPath, 'utf-8');
|
|
215
|
+
const bq = new BQMod.BigQuery();
|
|
216
|
+
const [job] = await bq.createQueryJob({ query: sql });
|
|
217
|
+
const [rows] = await job.getQueryResults();
|
|
218
|
+
if (!rows || rows.length === 0) {
|
|
219
|
+
return { actual: null, notes: 'BigQuery query returned zero rows' };
|
|
220
|
+
}
|
|
221
|
+
const firstValue = Object.values(rows[0])[0];
|
|
222
|
+
if (typeof firstValue === 'number')
|
|
223
|
+
return { actual: firstValue, notes: null };
|
|
224
|
+
if (firstValue !== null && firstValue !== undefined) {
|
|
225
|
+
const parsed = parseFloat(String(firstValue));
|
|
226
|
+
if (!isNaN(parsed))
|
|
227
|
+
return { actual: parsed, notes: null };
|
|
228
|
+
}
|
|
229
|
+
return { actual: null, notes: 'Could not extract scalar from BigQuery result' };
|
|
230
|
+
}
|
|
231
|
+
// ─── Criteria scanner (used by doctor) ───────────────────────────────────────
|
|
232
|
+
function scanCriteriaForSourceType(projectRoot, sourceType) {
|
|
233
|
+
const betsDir = path.join(projectRoot, 'primer', 'bets');
|
|
234
|
+
if (!fs.existsSync(betsDir))
|
|
235
|
+
return false;
|
|
236
|
+
for (const entry of fs.readdirSync(betsDir)) {
|
|
237
|
+
const criteriaPath = path.join(betsDir, entry, 'criteria.yaml');
|
|
238
|
+
if (!fs.existsSync(criteriaPath))
|
|
239
|
+
continue;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = yaml.load(fs.readFileSync(criteriaPath, 'utf-8'));
|
|
242
|
+
if (parsed?.metrics?.some((m) => m.source?.type === sourceType))
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// skip malformed criteria files
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function ensureDir(dir: string): void;
|
|
2
|
+
export declare function writeFileIfAbsent(filePath: string, content: string): boolean;
|
|
3
|
+
export declare function writeFile(filePath: string, content: string): void;
|
|
4
|
+
export declare function fileExists(filePath: string): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ensureDir = ensureDir;
|
|
37
|
+
exports.writeFileIfAbsent = writeFileIfAbsent;
|
|
38
|
+
exports.writeFile = writeFile;
|
|
39
|
+
exports.fileExists = fileExists;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
function ensureDir(dir) {
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function writeFileIfAbsent(filePath, content) {
|
|
48
|
+
if (fs.existsSync(filePath))
|
|
49
|
+
return false;
|
|
50
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
51
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function writeFile(filePath, content) {
|
|
55
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
56
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
function fileExists(filePath) {
|
|
59
|
+
return fs.existsSync(filePath);
|
|
60
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean): string;
|
|
2
|
+
export declare const sequenceTemplate = "wip_limits:\n now: 2\n\nnow: []\nnext: []\nlater: []\nbacklog: []\n";
|
|
3
|
+
export declare const pdrTemplate = "# PDR-XXX: <Decision title>\n\n## Status\nProposed | Accepted | Deprecated | Superseded by PDR-YYY\n\n## Context\n<What forced this decision?>\n\n## Decision\n<Clear statement of what is decided>\n\n## Alternatives considered\n- <Alternative A and why rejected>\n- <Alternative B and why rejected>\n\n## Consequences\n- Positive: <...>\n- Trade-offs: <...>\n- Follow-ups: <...>\n\n## Evidence\n- <Research link>\n- <Data link>\n\n## Related\n- Bets: <BET-IDs>\n- OpenSpec: <change paths>\n- Supersedes: <PDR-ID or none>\n";
|
|
4
|
+
export declare const betDecisionTemplate = "# Decision: BET-XXX <Bet title>\n\n## Status\n- Decision: Build now | Defer | Kill\n- Date: YYYY-MM-DD\n- Owner: <name>\n- Review date: YYYY-MM-DD\n\n## Why now\n- <prioritization rationale>\n\n## Alternatives considered\n- <alternative + reason>\n\n## Expected outcomes\n- <metric: baseline -> target in timeframe>\n\n## Kill criteria / rollback trigger\n- <condition and action>\n\n## Links\n- PDRs: <PDR-IDs>\n- OpenSpec change: <path once promoted>\n";
|
|
5
|
+
export declare const criteriaTemplate = "metrics:\n - id: metric_id\n name: \"Metric name\"\n baseline: 0.00\n target: 0.00\n timeframe: \"30 days post-launch\"\n launch_date: \"YYYY-MM-DD\"\n source:\n type: amplitude\n definition:\n event: event_name\n aggregation: unique_users\n denominator_event: null\n segment: null\n";
|
|
6
|
+
export declare const kpiReviewTemplate = "# KPI Review: BET-XXX\n\n**Review date:** YYYY-MM-DD\n**Reviewed by:** <name>\n\n| Metric | Baseline | Target | Actual | Status |\n|--------|----------|--------|--------|--------|\n| <metric name> | - | - | - | pending |\n\n## Decision quality\n<Did outcomes validate the decision? What would you do differently?>\n\n## Actions\n- [ ] Update bet-decision outcome section\n- [ ] Update affected PDRs\n- [ ] Re-sequence impacted bets\n";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.kpiReviewTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
4
|
+
exports.configTemplate = configTemplate;
|
|
5
|
+
function configTemplate(projectName, openspecEnabled, graphifyEnabled) {
|
|
6
|
+
return `version: 1
|
|
7
|
+
project:
|
|
8
|
+
name: "${projectName}"
|
|
9
|
+
agents: []
|
|
10
|
+
integrations:
|
|
11
|
+
openspec:
|
|
12
|
+
enabled: ${openspecEnabled}
|
|
13
|
+
changes_dir: openspec/changes
|
|
14
|
+
graphify:
|
|
15
|
+
enabled: ${graphifyEnabled}
|
|
16
|
+
graph_dir: graphify-out
|
|
17
|
+
measurement:
|
|
18
|
+
amplitude:
|
|
19
|
+
enabled: false
|
|
20
|
+
bigquery:
|
|
21
|
+
enabled: false
|
|
22
|
+
metabase_url: null
|
|
23
|
+
sequencing:
|
|
24
|
+
wip_limits:
|
|
25
|
+
now: 2
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
exports.sequenceTemplate = `wip_limits:
|
|
29
|
+
now: 2
|
|
30
|
+
|
|
31
|
+
now: []
|
|
32
|
+
next: []
|
|
33
|
+
later: []
|
|
34
|
+
backlog: []
|
|
35
|
+
`;
|
|
36
|
+
exports.pdrTemplate = `# PDR-XXX: <Decision title>
|
|
37
|
+
|
|
38
|
+
## Status
|
|
39
|
+
Proposed | Accepted | Deprecated | Superseded by PDR-YYY
|
|
40
|
+
|
|
41
|
+
## Context
|
|
42
|
+
<What forced this decision?>
|
|
43
|
+
|
|
44
|
+
## Decision
|
|
45
|
+
<Clear statement of what is decided>
|
|
46
|
+
|
|
47
|
+
## Alternatives considered
|
|
48
|
+
- <Alternative A and why rejected>
|
|
49
|
+
- <Alternative B and why rejected>
|
|
50
|
+
|
|
51
|
+
## Consequences
|
|
52
|
+
- Positive: <...>
|
|
53
|
+
- Trade-offs: <...>
|
|
54
|
+
- Follow-ups: <...>
|
|
55
|
+
|
|
56
|
+
## Evidence
|
|
57
|
+
- <Research link>
|
|
58
|
+
- <Data link>
|
|
59
|
+
|
|
60
|
+
## Related
|
|
61
|
+
- Bets: <BET-IDs>
|
|
62
|
+
- OpenSpec: <change paths>
|
|
63
|
+
- Supersedes: <PDR-ID or none>
|
|
64
|
+
`;
|
|
65
|
+
exports.betDecisionTemplate = `# Decision: BET-XXX <Bet title>
|
|
66
|
+
|
|
67
|
+
## Status
|
|
68
|
+
- Decision: Build now | Defer | Kill
|
|
69
|
+
- Date: YYYY-MM-DD
|
|
70
|
+
- Owner: <name>
|
|
71
|
+
- Review date: YYYY-MM-DD
|
|
72
|
+
|
|
73
|
+
## Why now
|
|
74
|
+
- <prioritization rationale>
|
|
75
|
+
|
|
76
|
+
## Alternatives considered
|
|
77
|
+
- <alternative + reason>
|
|
78
|
+
|
|
79
|
+
## Expected outcomes
|
|
80
|
+
- <metric: baseline -> target in timeframe>
|
|
81
|
+
|
|
82
|
+
## Kill criteria / rollback trigger
|
|
83
|
+
- <condition and action>
|
|
84
|
+
|
|
85
|
+
## Links
|
|
86
|
+
- PDRs: <PDR-IDs>
|
|
87
|
+
- OpenSpec change: <path once promoted>
|
|
88
|
+
`;
|
|
89
|
+
exports.criteriaTemplate = `metrics:
|
|
90
|
+
- id: metric_id
|
|
91
|
+
name: "Metric name"
|
|
92
|
+
baseline: 0.00
|
|
93
|
+
target: 0.00
|
|
94
|
+
timeframe: "30 days post-launch"
|
|
95
|
+
launch_date: "YYYY-MM-DD"
|
|
96
|
+
source:
|
|
97
|
+
type: amplitude
|
|
98
|
+
definition:
|
|
99
|
+
event: event_name
|
|
100
|
+
aggregation: unique_users
|
|
101
|
+
denominator_event: null
|
|
102
|
+
segment: null
|
|
103
|
+
`;
|
|
104
|
+
exports.kpiReviewTemplate = `# KPI Review: BET-XXX
|
|
105
|
+
|
|
106
|
+
**Review date:** YYYY-MM-DD
|
|
107
|
+
**Reviewed by:** <name>
|
|
108
|
+
|
|
109
|
+
| Metric | Baseline | Target | Actual | Status |
|
|
110
|
+
|--------|----------|--------|--------|--------|
|
|
111
|
+
| <metric name> | - | - | - | pending |
|
|
112
|
+
|
|
113
|
+
## Decision quality
|
|
114
|
+
<Did outcomes validate the decision? What would you do differently?>
|
|
115
|
+
|
|
116
|
+
## Actions
|
|
117
|
+
- [ ] Update bet-decision outcome section
|
|
118
|
+
- [ ] Update affected PDRs
|
|
119
|
+
- [ ] Re-sequence impacted bets
|
|
120
|
+
`;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-product-primer/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"product",
|
|
7
|
+
"decisions",
|
|
8
|
+
"sequencing",
|
|
9
|
+
"kpi",
|
|
10
|
+
"openspec",
|
|
11
|
+
"oprim"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Eshane",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/eshraw/open-product-primer.git",
|
|
18
|
+
"directory": "packages/cli"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/eshraw/open-product-primer/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/eshraw/open-product-primer#readme",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"open-product-primer": "bin/oprim.js",
|
|
29
|
+
"oprim": "bin/oprim.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin",
|
|
33
|
+
"dist",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"dev": "tsc --watch",
|
|
40
|
+
"start": "node dist/cli.js",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@google-cloud/bigquery": "^8.3.1",
|
|
46
|
+
"@inquirer/prompts": "^7.0.0",
|
|
47
|
+
"chalk": "^5.3.0",
|
|
48
|
+
"commander": "^12.0.0",
|
|
49
|
+
"js-yaml": "^4.1.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/js-yaml": "^4.0.9",
|
|
53
|
+
"@types/node": "^20.0.0",
|
|
54
|
+
"typescript": "^5.4.0",
|
|
55
|
+
"vitest": "^4.1.7"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18"
|
|
59
|
+
}
|
|
60
|
+
}
|