@open-product-primer/cli 0.11.0 → 0.12.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/cli.js CHANGED
@@ -10,6 +10,7 @@ const update_1 = require("./commands/update");
10
10
  const doctor_1 = require("./commands/doctor");
11
11
  const migrate_1 = require("./commands/migrate");
12
12
  const measure_1 = require("./commands/measure");
13
+ const ovw_1 = require("./commands/ovw");
13
14
  const package_json_1 = __importDefault(require("../package.json"));
14
15
  const program = new commander_1.Command();
15
16
  program
@@ -21,4 +22,5 @@ program.addCommand((0, update_1.updateCommand)());
21
22
  program.addCommand((0, doctor_1.doctorCommand)());
22
23
  program.addCommand((0, migrate_1.migrateCommand)());
23
24
  program.addCommand((0, measure_1.measureCommand)());
25
+ program.addCommand((0, ovw_1.ovwCommand)());
24
26
  program.parse();
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function ovwCommand(): Command;
@@ -0,0 +1,263 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ovwCommand = ovwCommand;
40
+ const commander_1 = require("commander");
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const yaml = __importStar(require("js-yaml"));
45
+ function resolveBetDecisionPath(betsDir, betId) {
46
+ if (!fs.existsSync(betsDir))
47
+ return null;
48
+ const entries = fs.readdirSync(betsDir, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ if (entry.isDirectory() && entry.name.startsWith(betId)) {
51
+ const candidate = path.join(betsDir, entry.name, 'bet-decision.md');
52
+ if (fs.existsSync(candidate))
53
+ return candidate;
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ function extractDoorType(content) {
59
+ if (/\[x\]\s*2-way door/i.test(content))
60
+ return '2-way';
61
+ if (/\[x\]\s*1-way door/i.test(content))
62
+ return '1-way';
63
+ return null;
64
+ }
65
+ function extractRiskLevel(content, label) {
66
+ const match = content.match(new RegExp(`\\*\\*${label}\\*\\*:\\s*(Low|Medium|High)`, 'i'));
67
+ if (!match)
68
+ return null;
69
+ const raw = match[1];
70
+ if (raw.toLowerCase() === 'low')
71
+ return 'Low';
72
+ if (raw.toLowerCase() === 'medium')
73
+ return 'Medium';
74
+ if (raw.toLowerCase() === 'high')
75
+ return 'High';
76
+ return null;
77
+ }
78
+ function extractRiskProfile(content) {
79
+ return {
80
+ value: extractRiskLevel(content, 'Value risk'),
81
+ usability: extractRiskLevel(content, 'Usability risk'),
82
+ feasibility: extractRiskLevel(content, 'Feasibility risk'),
83
+ viability: extractRiskLevel(content, 'Business viability risk'),
84
+ };
85
+ }
86
+ function loadBetMeta(betsDir, betId) {
87
+ const decisionPath = resolveBetDecisionPath(betsDir, betId);
88
+ if (!decisionPath)
89
+ return { doorType: null, risks: null };
90
+ try {
91
+ const content = fs.readFileSync(decisionPath, 'utf-8');
92
+ return {
93
+ doorType: extractDoorType(content),
94
+ risks: extractRiskProfile(content),
95
+ };
96
+ }
97
+ catch {
98
+ return { doorType: null, risks: null };
99
+ }
100
+ }
101
+ function abbreviate(level) {
102
+ if (level === 'Low')
103
+ return 'L';
104
+ if (level === 'Medium')
105
+ return 'M';
106
+ if (level === 'High')
107
+ return 'H';
108
+ return '?';
109
+ }
110
+ function renderInlineMeta(meta) {
111
+ if (!meta.doorType && !meta.risks)
112
+ return '[risk: unknown]';
113
+ const door = meta.doorType ?? '?-way';
114
+ if (!meta.risks)
115
+ return `[${door} | risk: unknown]`;
116
+ const { value, usability, feasibility, viability } = meta.risks;
117
+ return `[${door} | value:${abbreviate(value)} usability:${abbreviate(usability)} feasibility:${abbreviate(feasibility)} viability:${abbreviate(viability)}]`;
118
+ }
119
+ function renderBlockers(bet) {
120
+ if (!bet.blocked_by || bet.blocked_by.length === 0)
121
+ return '';
122
+ return ` [blocked by: ${bet.blocked_by.join(', ')}]`;
123
+ }
124
+ function renderLane(label, bets, betsDir, showMeta) {
125
+ console.log(chalk_1.default.bold.cyan(`\n── ${label.toUpperCase()} ──`));
126
+ if (bets.length === 0) {
127
+ console.log(' (empty)');
128
+ return;
129
+ }
130
+ for (const bet of bets) {
131
+ if (showMeta) {
132
+ const meta = loadBetMeta(betsDir, bet.id);
133
+ const inline = renderInlineMeta(meta);
134
+ const blockers = renderBlockers(bet);
135
+ console.log(` ${bet.id} ${bet.title} ${inline}${blockers}`);
136
+ }
137
+ else {
138
+ const blockers = renderBlockers(bet);
139
+ console.log(` ${bet.id} ${bet.title}${blockers}`);
140
+ }
141
+ }
142
+ }
143
+ function isElevated(level) {
144
+ return level === 'Medium' || level === 'High';
145
+ }
146
+ function buildAdvisory(board, betsDir) {
147
+ const nudges = [];
148
+ const now = board.now ?? [];
149
+ const next = board.next ?? [];
150
+ // Board-shape rules
151
+ if (now.length === 0) {
152
+ nudges.push('Now lane is empty — consider promoting a bet from next');
153
+ }
154
+ if (now.length > 3) {
155
+ nudges.push(`Focus risk: now lane has ${now.length} active bets — consider narrowing`);
156
+ }
157
+ for (const bet of now) {
158
+ const blockers = bet.blocked_by ?? [];
159
+ const inFlightIds = new Set([...now.map((b) => b.id), ...next.map((b) => b.id)]);
160
+ for (const blocker of blockers) {
161
+ if (!inFlightIds.has(blocker)) {
162
+ nudges.push(`${bet.id} may be stuck — its blocker ${blocker} is not in a flight lane`);
163
+ }
164
+ }
165
+ }
166
+ // Door-type sequencing rules
167
+ const inFlightIds = new Set([...now.map((b) => b.id), ...next.map((b) => b.id)]);
168
+ const metaCache = new Map();
169
+ const getMeta = (betId) => {
170
+ if (!metaCache.has(betId))
171
+ metaCache.set(betId, loadBetMeta(betsDir, betId));
172
+ return metaCache.get(betId);
173
+ };
174
+ const nowMetas = now.map((b) => ({ bet: b, meta: getMeta(b.id) }));
175
+ const nextMetas = next.map((b) => ({ bet: b, meta: getMeta(b.id) }));
176
+ const inFlightMetas = [...nowMetas, ...nextMetas];
177
+ // 2-way bets in flight that unlock 1-way bets (used to check unrisker presence)
178
+ const twowayUnlocks = new Set();
179
+ for (const { bet, meta } of inFlightMetas) {
180
+ if (meta.doorType === '2-way') {
181
+ for (const unlockedId of bet.unlocks ?? [])
182
+ twowayUnlocks.add(unlockedId);
183
+ }
184
+ }
185
+ const allNow1Way = nowMetas.length > 0 && nowMetas.every(({ meta }) => meta.doorType === '1-way');
186
+ if (allNow1Way) {
187
+ nudges.push('Your now lane has no 2-way door bets — you are in full-commitment mode with no reversible fallback');
188
+ }
189
+ for (const { bet, meta } of nowMetas) {
190
+ if (meta.doorType === '1-way' && !twowayUnlocks.has(bet.id)) {
191
+ nudges.push(`${bet.id} is a 1-way door with no 2-way door unrisker in flight — consider sequencing a reversible spike first`);
192
+ }
193
+ }
194
+ // 2-way door blocked by a 1-way door (inverted order)
195
+ for (const { bet, meta } of inFlightMetas) {
196
+ if (meta.doorType === '2-way') {
197
+ for (const blockerId of bet.blocked_by ?? []) {
198
+ const blockerMeta = getMeta(blockerId);
199
+ if (blockerMeta.doorType === '1-way') {
200
+ nudges.push(`${bet.id} (2-way) is blocked by ${blockerId} (1-way) — order may be inverted; 2-way doors should precede 1-way doors`);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ // Risk advisory rules for now lane
206
+ for (const { bet, meta } of nowMetas) {
207
+ if (!meta.risks)
208
+ continue;
209
+ const { value, usability, feasibility, viability } = meta.risks;
210
+ if (isElevated(value) && meta.doorType === '1-way') {
211
+ nudges.push(`${bet.id}: high commitment with unvalidated value — consider a 2-way door discovery bet first`);
212
+ }
213
+ if (isElevated(feasibility)) {
214
+ nudges.push(`${bet.id}: feasibility risk is elevated — ensure a spike or prototype is in plan before full build`);
215
+ }
216
+ if (isElevated(usability)) {
217
+ nudges.push(`${bet.id}: usability risk is elevated — plan for user testing before shipping`);
218
+ }
219
+ if (isElevated(viability)) {
220
+ nudges.push(`${bet.id}: business viability risk is elevated — align with stakeholders before committing`);
221
+ }
222
+ }
223
+ return nudges;
224
+ }
225
+ function ovwCommand() {
226
+ return new commander_1.Command('ovw')
227
+ .description('Show the sequencing board with door type, risk profile, and advisory guidance')
228
+ .action(() => {
229
+ const projectRoot = process.cwd();
230
+ const sequencePath = path.join(projectRoot, 'oprim', 'sequence.yaml');
231
+ const betsDir = path.join(projectRoot, 'oprim', 'bets');
232
+ if (!fs.existsSync(sequencePath)) {
233
+ console.error("No oprim/sequence.yaml found — run 'oprim init' first");
234
+ process.exit(1);
235
+ return;
236
+ }
237
+ let board;
238
+ try {
239
+ board = yaml.load(fs.readFileSync(sequencePath, 'utf-8')) ?? {};
240
+ }
241
+ catch {
242
+ console.error('Failed to parse oprim/sequence.yaml');
243
+ process.exit(1);
244
+ return;
245
+ }
246
+ const lanes = [
247
+ { key: 'now', label: 'now', showMeta: true },
248
+ { key: 'next', label: 'next', showMeta: true },
249
+ { key: 'later', label: 'later', showMeta: false },
250
+ { key: 'backlog', label: 'backlog', showMeta: false },
251
+ ];
252
+ for (const { key, label, showMeta } of lanes) {
253
+ renderLane(label, board[key] ?? [], betsDir, showMeta);
254
+ }
255
+ const nudges = buildAdvisory(board, betsDir);
256
+ if (nudges.length > 0) {
257
+ console.log(chalk_1.default.bold.yellow('\n── ADVISORY ──'));
258
+ for (const nudge of nudges) {
259
+ console.log(` • ${nudge}`);
260
+ }
261
+ }
262
+ });
263
+ }
@@ -426,6 +426,15 @@ If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
426
426
  ### 4. Gather content
427
427
  Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date (YYYY-MM-DD), Why now, Alternatives considered, Expected outcomes (metric: baseline → target in timeframe), Kill criteria / rollback trigger, PDR links (optional).
428
428
 
429
+ Then ask about reversibility:
430
+ - "Is this a **2-way door** (reversible — easy to undo, safe to try) or a **1-way door** (hard to reverse — requires high confidence)?"
431
+
432
+ Then ask about each of the four risk dimensions (Low / Medium / High + short rationale):
433
+ - "**Value risk**: Will users/customers actually use or buy this? (Low / Medium / High — and why?)"
434
+ - "**Usability risk**: Can users figure out how to use it without help? (Low / Medium / High — and why?)"
435
+ - "**Feasibility risk**: Can we build this with our current skills, time, and technology? (Low / Medium / High — and why?)"
436
+ - "**Business viability risk**: Does this solution work for the business (revenue, legal, ops)? (Low / Medium / High — and why?)"
437
+
429
438
  ### 5. Write oprim/bets/BET-NNN-<slug>/bet-decision.md
430
439
  \`\`\`
431
440
  # Decision: BET-NNN <title>
@@ -437,6 +446,16 @@ Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date
437
446
  - Owner: <owner>
438
447
  - Review date: <review date>
439
448
 
449
+ ## Door type
450
+ - [<x if 2-way>] 2-way door (reversible — safe to try, easy to undo)
451
+ - [<x if 1-way>] 1-way door (hard to reverse — requires higher confidence before committing)
452
+
453
+ ## Risk profile
454
+ - **Value risk**: <Low / Medium / High> — <rationale>
455
+ - **Usability risk**: <Low / Medium / High> — <rationale>
456
+ - **Feasibility risk**: <Low / Medium / High> — <rationale>
457
+ - **Business viability risk**: <Low / Medium / High> — <rationale>
458
+
440
459
  ## Why now
441
460
  <why-now as bullet list>
442
461
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",