create-pmos 0.123.0 → 0.125.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/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/notes.md +1 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +100 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +415 -0
- package/workspace/public/pm/briefing-model.js +176 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +173 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +637 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +138 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1015 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1480 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-notes.js +25 -3
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +919 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +8 -0
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/* pm/c3-demo-init.js — the DEMO fixtures of the five Initiatives sub-views of /c3.html (Roadmap, Breakdown, Tickets, Documents, Signals):
|
|
2
|
+
* the console3 mockup's own sample content (console3.html lines 3640 to 3800 and 3900 to 4240), in the REAL response shapes of the routes
|
|
3
|
+
* the views read. c3-demo.js calls installInit() once, after it has wrapped fetch. This wrapper sits in front of that one: a request that
|
|
4
|
+
* has a handler below is answered here; any other goes on unchanged. Writes are answered with the real route's shape and change the
|
|
5
|
+
* in-memory fixture only (the Breakdown list is stateful, so an add, an edit and a delete show), never the server.
|
|
6
|
+
* Real mode never loads this file. Each view's handlers sit in its own section. */
|
|
7
|
+
import * as D from './c3-demo-data.js';
|
|
8
|
+
|
|
9
|
+
const GET = {}; // path -> (searchParams, url) => body
|
|
10
|
+
const POST = {}; // path -> (body) => body
|
|
11
|
+
const json = (body, status) => new Response(JSON.stringify(body), { status: status || 200, headers: { 'content-type': 'application/json' } });
|
|
12
|
+
let seq = 0;
|
|
13
|
+
export const nextId = (p) => p + '-demo' + (++seq);
|
|
14
|
+
export { D };
|
|
15
|
+
|
|
16
|
+
/* == decks == the Plan deck of each sub-view (the mockup's viewCards, console3.html lines 3800 to 3830, 4005 to 4010): GET /api/briefing?scope=init-<view> */
|
|
17
|
+
const K = { ok: 'var(--success)', warn: 'var(--warning)', bad: 'var(--destructive)', acc: 'var(--acc)', mfg: 'var(--muted-foreground)', opp: 'var(--purple)' };
|
|
18
|
+
const dk = (key, tag, pov, headline, sub, rich, facts, conf, options, nev) => ({ key, tag, pov, headline, sub, hero: { kind: 'rows', points: null, rich }, facts: [],
|
|
19
|
+
kpis: [[conf + '%', 'confidence', K.acc], [String(options.length), 'options', K.opp], [String(nev || Math.max(2, facts.length)), 'evidence', K.ok]], factsX: facts,
|
|
20
|
+
move: { suggested: { id: options[0][0], label: options[0][1], conf }, options: options.map(([id, label]) => ({ id, label })) } });
|
|
21
|
+
const bars = (b) => ({ kind: 'bars', bars: b });
|
|
22
|
+
const rows = (r) => ({ kind: 'rows', rows: r });
|
|
23
|
+
const tiles = (t) => ({ kind: 'tiles', tiles: t });
|
|
24
|
+
const DECKS = {
|
|
25
|
+
'init-roadmap': [
|
|
26
|
+
dk('rm1', 'Critical', 'PROJECT', 'Checkout v2 is the one Now bet at risk', 'The roadmap says 24 Oct. The path says 31 Oct.', bars([['Checkout v2', 0.6, K.warn, '60%'], ['SSO beta to general release', 0.8, K.ok, '80%'], ['Search relevance beta', 0.5, K.warn, '50%']]), [['flag', 'Now holds 3 bets, room for 4'], ['clock', 'Confidence is the agent\'s read']], 78, [['cut', 'Cut to 3 flows and hold 24 Oct'], ['keep', 'Keep it where it is']]),
|
|
27
|
+
dk('rm2', 'Opportunity', 'PRODUCT', 'Fix label printing could move up', 'Three of three calls name it. Interview 5 on 13 Oct decides.', rows([['search', '3 of 3 calls name labels', 'ok', 'evidence'], ['cal', 'Interview 5 on 13 Oct', '', 'decides'], ['flag', 'Returns contact rate 0.61', 'warn', 'goal 0.5']]), [['next', 'Pull forward if interview 5 agrees']], 76, [['pf', 'Pull it forward to Now'], ['k', 'Keep it where it is']]),
|
|
28
|
+
dk('rm3', 'Roadmap', 'PRODUCT', 'Now 3, Next 3, Later 2', 'Later is direction only. Dates there are not promises.', tiles([['3', 'Now', K.acc], ['3', 'Next', K.opp], ['2', 'Later', K.mfg]]), [['target', 'Each item names its goal']], 72, [['k', 'Keep the shape'], ['t', 'Ask the agent to trim Later']]),
|
|
29
|
+
dk('rm4', 'Critical', 'PRODUCT', 'Self-serve onboarding has no anchor', 'Unanchored work is not accepted. It has had no signal in 9 days.', rows([['alert', 'No KR or health budget', 'bad', ''], ['clock', 'No signal in 9 days', 'warn', 'stop rule passed']]), [['moon', 'Park it or anchor it']], 66, [['p', 'Park it'], ['k', 'Keep it where it is']]),
|
|
30
|
+
],
|
|
31
|
+
'init-breakdown': [
|
|
32
|
+
dk('wb1', 'Just in', 'PROJECT', 'The agent added 1 of 15 items and made 1 changes', 'Each carries its reason. Review the additions you did not ask for.', bars([['You', 14 / 15, K.acc, '14'], ['Agent', 1 / 15, K.opp, '1'], ['Team', 0, K.mfg, '0']]), [['spark', 'Proposals need your yes']], 82, [['k', 'Keep reviewing additions']]),
|
|
33
|
+
dk('wb2', 'Critical', 'PROJECT', 'Scope grew 62% in three additions', 'Checkout v2 went from 21 to 34 points. Two additions came from the agent, one from you.', { kind: 'num', big: '62', unit: '%', delta: '21 to 34 points', tone: K.warn, arr: [21, 21, 26, 29, 34], target: 40 }, [['scale', 'A fourth change re-baselines the plan']], 78, [['c', 'Freeze scope after the decision'], ['k', 'Keep going']]),
|
|
34
|
+
dk('wb3', 'Opportunity', 'PROJECT', 'A11y audit slot changed 2 times', 'Repeated changes to one item are a signal. Ask why before the next one.', rows([['clock', 'A11y audit slot', 'warn', 'Dara'], ['clock', 'Saved-card removal', 'warn', 'Agent']]), [['info', 'Reasons are in the row history']], 74, [['a', 'Ask why before the next change'], ['k', 'Leave it']]),
|
|
35
|
+
dk('wb4', 'Small win', 'PROJECT', 'Every item has an owner and a reason', 'No item was added or changed without a recorded why.', { kind: 'ring', done: 15, total: 15, cap: 'all items traced', sub: 'owner and reason' }, [['check', 'Audit trail complete']], 90, [['k', 'Nothing to do']]),
|
|
36
|
+
],
|
|
37
|
+
'init-tickets': [
|
|
38
|
+
dk('tk1', 'Critical', 'PROJECT', '6 of 11 tickets are late or blocked', 'The oldest is the vendor keys, two days with no reply.', bars([['Payments sandbox keys', 0.45, K.bad, '+2 d'], ['Saved-card removal', 0.45, K.bad, '+2 d'], ['Address autocomplete', 0.45, K.bad, '+2 d'], ['A11y audit slot', 0.95, K.bad, '+7 d']]), [['reply', 'Nudge Dara and Sam']], 78, [['n', 'Ask Dara for an update'], ['k', 'Leave it']], 4),
|
|
39
|
+
dk('tk2', 'Just in', 'PROJECT', 'The agent drafted 2 tickets from the whiteboard photo', 'Retry state and declined-card copy. Nothing reaches Jira until you accept.', rows([['plus', 'Retry state after a failed payment', 'warn', '3 pts'], ['plus', 'Error copy for declined cards', 'warn', '2 pts']]), [['alert', 'Accepting makes scope change four']], 70, [['h', 'Hold both until the decision'], ['a', 'Accept both'], ['d', 'Drop them']]),
|
|
40
|
+
dk('tk3', 'Small win', 'PROJECT', '2 of 17 tickets are done', 'Pricing copy and the SSO beta round closed this week.', { kind: 'ring', done: 2, total: 17, cap: '2 of 17', sub: 'tickets done' }, [['check', 'Velocity steady']], 88, [['k', 'Nothing to do']]),
|
|
41
|
+
dk('tk4', 'Opportunity', 'PROJECT', 'The a11y audit slot has nobody responsible', 'Lee is assigned, but the slot request is unowned. A fix costs one message.', rows([['alert', 'CHK-144 A11y audit slot', 'bad', 'no R'], ['user', 'Lee, Dara', '', 'proposed']]), [['org', 'Responsible and accountable are different people']], 74, [['a', 'Make Lee responsible, Dara accountable'], ['k', 'Leave it']]),
|
|
42
|
+
],
|
|
43
|
+
'init-documents': [
|
|
44
|
+
dk('dc1', 'Critical', 'PRODUCT', 'The Checkout PRD lists 4 flows, the cut may make it 3', 'Leadership saw v3. This is v5. Today\'s decision makes it v6.', rows([['doc', 'PRD v5: 4 flows', 'warn', 'stale'], ['scale', 'Decision at 14:00', 'bad', 'today'], ['spark', 'Agent drafts the v6 diff', 'ok', 'ready']]), [['out', 'Needs your approval before it is shared']], 78, [['dr', 'Have the agent draft the update'], ['sh', 'Share v5 with a note on the change'], ['lt', 'Later']], 3),
|
|
45
|
+
dk('dc2', 'Opportunity', 'PRODUCT', 'Peers convert better than you', 'Median peer 3.4%, top quartile 4.3%, you 3.1%. The benchmark is 10 days old.', bars([['Top quartile', 0.86, K.ok, '4.3%'], ['Peer median', 0.68, K.acc, '3.4%'], ['You', 0.62, K.warn, '3.1%']]), [['clock', 'Refresh before Friday\'s update']], 78, [['dr', 'Have the agent draft the update'], ['lt', 'Later']]),
|
|
46
|
+
dk('dc3', 'Just in', 'PRODUCT', 'Research: labels are the main pain', '3 of 3 calls name labels, 2 of 3 name the 30-day window, 0 mention support.', bars([['Label printing', 1, K.ok, '3 of 3'], ['30-day window', 0.67, K.warn, '2 of 3'], ['Support route', 0.04, 'var(--ring)', '0']]), [['notes', 'Interview 4 tests the support route']], 78, [['dr', 'Review the agent draft'], ['lt', 'Later']]),
|
|
47
|
+
dk('dc4', 'Critical', 'PRODUCT', '4 documents need attention', 'Features need a spec and a rubric before build. Discovery needs a brief.', tiles([['9', 'Current', K.ok], ['3', 'Stale', K.warn], ['2', 'Draft', K.acc], ['1', 'Missing', K.bad]]), [['shield', 'Gate: spec and rubric come first']], 80, [['d', 'Have the agent draft all of them'], ['k', 'Review one by one']]),
|
|
48
|
+
],
|
|
49
|
+
'init-signals': [
|
|
50
|
+
dk('sg1', 'Critical', 'PROJECT', 'Delay: Checkout v2 is 7 days late', 'Forecast drifted from 24 to 31 Oct in 3 weeks. Cause: scope, not capacity.', { kind: 'num', big: '7', unit: 'days', delta: 'vs promised 24 Oct', tone: K.bad, arr: [0, 0, 2, 4, 7, 7], target: 8 }, [['clock', 'More than 5 days: warn leadership']], 78, [['c', 'Cut to 3 flows and hold 24 Oct'], ['w', 'Warn leadership now with a new date'], ['h', 'Hold and watch']], 3),
|
|
51
|
+
dk('sg2', 'Critical', 'PROJECT', 'Scope creep: up 62% since the baseline', 'Three changes in four weeks: 21 points became 34. A fourth should re-baseline.', { kind: 'num', big: '62', unit: '%', delta: '21 to 34 points', tone: K.warn, arr: [21, 21, 26, 29, 34], target: 40 }, [['scale', 'Freeze scope after the decision']], 78, [['f', 'Freeze scope after the decision'], ['k', 'Keep going']]),
|
|
52
|
+
dk('sg3', 'Critical', 'PRODUCT', 'Conversion has fallen six weeks in a row', '3.5% to 3.1%. The pivot line is 3.0% for two weeks.', { kind: 'num', big: '3.1', unit: '%', delta: 'from 3.5%', tone: K.bad, arr: [3.5, 3.4, 3.4, 3.3, 3.3, 3.2, 3.1], target: 3.0 }, [['alert', 'Run the address-step test']], 76, [['x', 'Run the address-step test'], ['w', 'Watch 3 more days']]),
|
|
53
|
+
dk('sg4', 'Opportunity', 'PRODUCT', 'Enterprise pull is stronger than planned', '3 design partners live. 2 more asked for SCIM early.', { kind: 'ring', done: 3, total: 5, cap: '3 of 5 partners', sub: 'goal by December' }, [['up', 'Make Enterprise a Q1 bet at 5']], 74, [['q', 'Make Enterprise a Q1 bet'], ['k', 'Keep the plan']]),
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
GET['/api/briefing'] = (q) => {
|
|
57
|
+
const sc = q.get('scope') || '';
|
|
58
|
+
if (!DECKS[sc]) return undefined;
|
|
59
|
+
return { ok: true, lens: 'Plan', zoom: q.get('zoom') || 'week', cards: DECKS[sc].map((c) => ({ ...c })), ticker: [], banner: null, unread: 2 };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/* == roadmap == */
|
|
63
|
+
/* the mockup's ROAD (console3.html line 3669) as bets records: horizon and rank, parent_kr (a KR id, named in `krs`), confidence; `initiative` joins the
|
|
64
|
+
* repository row; q0 and q1 (quarter columns) and why are fields the demo adds for the chart and the case file. */
|
|
65
|
+
const KRS = [{ id: 'q4-2026:o1:kr1', title: 'Checkout conversion 4.0%' }, { id: 'q4-2026:o1:kr2', title: 'Returns contact rate 0.5' }, { id: 'q4-2026:o2:kr3', title: 'SSO adoption' }];
|
|
66
|
+
const bet = (rid, title, initiative, kr, horizon, rank, confidence, q0, q1, why, risk) => ({ rid, title, initiative, parent_kr: kr, horizon, rank, confidence, q0, q1, why: why || '', risk: risk || '' });
|
|
67
|
+
const BETS = [
|
|
68
|
+
{ ...bet('r1', 'Checkout v2', 'checkout', KRS[0].id, 'now', 1, 0.6, 0, 0, '', 'late'), options: [{ id: 'cut', l: 'Cut to 3 flows and hold 24 Oct', rec: true, fx: [['check', 'Holds 24 Oct', 'ok'], ['scale', 'Drops one flow', 'warn']], msg: 'Scenario set to the cut. Timeline and Calendar follow.' }] },
|
|
69
|
+
bet('r2', 'SSO beta to general release', 'sso', KRS[2].id, 'now', 2, 0.8, 0, 0, '', 'risk'),
|
|
70
|
+
bet('r3', 'Search relevance beta', 'search', KRS[0].id, 'now', 3, 0.5, 0, 0, '', 'risk'),
|
|
71
|
+
bet('r4', 'Fix label printing', 'returns', KRS[1].id, 'next', 1, 0.55, 1, 2, '3 of 3 calls name it. Interview 5 decides on 13 Oct.'),
|
|
72
|
+
bet('r5', 'Guided SSO setup', 'sso', KRS[2].id, 'next', 2, 0.6, 1, 1, 'Adoption lagged because setup took a call.'),
|
|
73
|
+
bet('r6', 'Address autocomplete', 'checkout', KRS[0].id, 'next', 3, 0.7, 1, 1, 'Top idea by score. The address step loses 45% of carts.'),
|
|
74
|
+
bet('r7', 'Payment wallets', 'checkout', KRS[0].id, 'later', 1, 0.3, 2, 3, 'A direction, not a plan.'),
|
|
75
|
+
bet('r8', 'Self-serve onboarding', 'onb', '', 'later', 2, 0.2, 2, 3, 'No signal in 9 days and no anchor.'),
|
|
76
|
+
];
|
|
77
|
+
const PEOPLE = [{ id: 'Wawan', points_per_week: 10 }, { id: 'Lee', points_per_week: 6 }, { id: 'Marta', points_per_week: 4 }];
|
|
78
|
+
const live = (h) => BETS.filter((b) => !b.off && b.horizon === h);
|
|
79
|
+
const capLine = () => ({ points_per_week: PEOPLE.reduce((n, p) => n + p.points_per_week, 0), people: PEOPLE.length, now: { used: live('now').length, limit: 4, free: Math.max(0, 4 - live('now').length) }, next: { used: live('next').length, limit: 4, free: Math.max(0, 4 - live('next').length) }, per_bet: 3.3, over: false });
|
|
80
|
+
const DATED = [['Payments sandbox keys', '2026-10-12', '2026-10-10', 1], ['SSO general release', '2026-10-14', '2026-10-14', 0], ['Index job fix', '2026-10-15', '2026-10-12', 1], ['Synthesis and stop rule', '2026-10-15', '2026-10-15', 0], ['Saved-card removal', '2026-10-19', '2026-10-17', 1], ['Address autocomplete', '2026-10-22', '2026-10-20', 1]].map(([t, fe, plan, bad]) => ({ t, fe, plan, bad: !!bad }));
|
|
81
|
+
GET['/api/c3-init/roadmap'] = () => ({ ok: true, dated: DATED, bets: BETS.map((b) => ({ ...b })), capacity: capLine(), people: PEOPLE.map((p) => ({ ...p })), krs: KRS });
|
|
82
|
+
POST['/api/roadmap-ops/bet/add'] = (b) => {
|
|
83
|
+
const title = String(b.title || '').trim(); if (!title) return { ok: false, error: 'a bet needs a title' };
|
|
84
|
+
if (!String(b.anchor || '').trim()) return { ok: false, error: 'an unanchored bet is refused: anchor it to a KR or to health:<id>', field: 'anchor' };
|
|
85
|
+
const h = b.horizon || 'later'; if ((h === 'now' || h === 'next') && live(h).length >= 4) return { ok: false, error: h + ' already holds 4 bets; add it to a later column or move one out first', full: h };
|
|
86
|
+
const rid = nextId('bet'); BETS.push(bet(rid, title, '', b.anchor, h, live(h).length + 1, typeof b.confidence === 'number' ? b.confidence : 0.5, h === 'now' ? 0 : h === 'next' ? 1 : 2, h === 'later' ? 3 : h === 'next' ? 1 : 0, 'Added by you. The agent will look for evidence.'));
|
|
87
|
+
return { ok: true, written: [rid], rid };
|
|
88
|
+
};
|
|
89
|
+
POST['/api/roadmap-ops/bet/move'] = (b) => {
|
|
90
|
+
const x = BETS.find((y) => y.rid === b.id && !y.off); if (!x) return { ok: false, error: 'no such bet on the roadmap: ' + b.id };
|
|
91
|
+
if (x.horizon === b.to) return { ok: false, error: 'the bet is already in ' + b.to };
|
|
92
|
+
if (b.to !== 'later' && !x.parent_kr) return { ok: false, error: 'anchor the bet before it goes to ' + b.to, field: 'anchor' };
|
|
93
|
+
const set = (y, h) => { y.horizon = h; y.q0 = h === 'now' ? 0 : h === 'next' ? 1 : 2; y.q1 = h === 'later' ? Math.max(y.q0, 3) : y.q0; };
|
|
94
|
+
let victim = null;
|
|
95
|
+
if (b.to !== 'later' && live(b.to).length >= 4) {
|
|
96
|
+
victim = live(b.to).slice().sort((p, q) => p.confidence - q.confidence)[0];
|
|
97
|
+
const to = b.to === 'now' ? 'next' : 'later';
|
|
98
|
+
if (!b.confirmSwap) return { ok: true, proposal: { kind: 'swap', move: { id: x.rid, title: x.title, from: x.horizon, to: b.to }, send_back: [{ id: victim.rid, title: victim.title, confidence: victim.confidence, from: b.to, to }], confirm: 'send { confirmSwap: true } to apply; nothing moved yet' } };
|
|
99
|
+
set(victim, to);
|
|
100
|
+
}
|
|
101
|
+
set(x, b.to);
|
|
102
|
+
return { ok: true, written: [x.rid], rid: x.rid, ...(victim ? { swapped: [{ id: victim.rid, title: victim.title }] } : {}) };
|
|
103
|
+
};
|
|
104
|
+
POST['/api/roadmap-ops/bet/take-off'] = (b) => {
|
|
105
|
+
const x = BETS.find((y) => y.rid === b.id && !y.off); if (!x) return { ok: false, error: 'no such bet on the roadmap: ' + b.id };
|
|
106
|
+
if (!String(b.reason || '').trim()) return { ok: false, error: 'a reason is required to take a bet off the roadmap', field: 'reason' };
|
|
107
|
+
x.off = true; x.off_reason = b.reason; return { ok: true, written: [x.rid], rid: x.rid };
|
|
108
|
+
};
|
|
109
|
+
GET['/api/roadmap-ops/bet/anchor-suggestions'] = (q) => ({ ok: true, id: q.get('id'), suggestions: [] });
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/* == breakdown == */
|
|
113
|
+
/* the mockup's PMOS-only work items (console3.html patch 25): 15 items, phase, source, owner, points. Stateful: an add, edit, delete and undo show. */
|
|
114
|
+
const ev = (kind, rid, by, why, at, title) => ({ kind, rid, why, at: '2026-10-' + at + 'T09:00:00.000Z', by, title: title || '' });
|
|
115
|
+
const it = (rid, title, phase, source, owner, points, initiative, done, deps) => ({ rid, title, phase, source, ...(owner ? { owner } : {}), points, initiative, ...(done ? { done: true } : {}), deps: deps || [] });
|
|
116
|
+
let ITEMS = [
|
|
117
|
+
it('PM-1', 'Address drop-off analysis', 'discovery', 'local', 'Agent', 3, 'checkout', true), it('PM-2', 'Retry flow whiteboard session', 'discovery', 'calendar', 'Wawan', 1, 'checkout', true),
|
|
118
|
+
it('PM-3', 'Checkout flow screens', 'design', 'figma', 'Lee', 5, 'checkout'), it('PM-4', 'Release notes', 'launch', 'local', 'Agent', 1, 'checkout', false, ['CHK-144']),
|
|
119
|
+
it('PM-5', 'Support and sales brief', 'launch', 'local', 'Agent', 2, 'checkout', true), it('PM-6', 'Retry state design', 'design', 'figma', 'Agent', 3, 'checkout'), it('PM-7', 'Error copy for declined cards', 'design', 'local', 'Agent', 2, 'checkout'),
|
|
120
|
+
it('RET-11', 'Interview 3', 'discovery', 'calendar', 'Wawan', 1, 'returns', true), it('RET-12', 'Interview 4 slot', 'discovery', 'gmail', 'Agent', 1, 'returns'), it('RET-13', 'Interview 5', 'discovery', 'calendar', 'Agent', 1, 'returns'), it('RET-14', 'Synthesis and stop rule', 'discovery', 'local', 'Agent', 2, 'returns'),
|
|
121
|
+
it('SSO-32', 'Beta feedback round', 'launch', 'local', 'Marta', 3, 'sso', true), it('SSO-33', 'Confirm date to Marta', 'launch', 'local', 'Agent', 1, 'sso'),
|
|
122
|
+
it('EN-421', 'Ranking weights v2', 'design', 'local', 'Wawan', 5, 'search', false, ['EN-418']),
|
|
123
|
+
it('ONB-2', 'Continue or park decision', 'discovery', 'notes', 'Agent', 1, 'onb'),
|
|
124
|
+
];
|
|
125
|
+
const EVENTS = [
|
|
126
|
+
...ITEMS.map((x) => ev('add', x.rid, x.rid === 'PM-1' ? 'claude-code/session' : 'human:wawan', x.rid === 'PM-1' ? 'The agent read the checkout recordings.' : 'Added by you.', '05', x.title)),
|
|
127
|
+
ev('edit', 'RET-12', 'claude-code/session', 'Slots held. Marta offered three slots. The agent held them.', '08', 'Interview 4 slot'),
|
|
128
|
+
];
|
|
129
|
+
const dayName = () => new Date().toISOString();
|
|
130
|
+
const loadOf = () => { const m = new Map(); ITEMS.filter((x) => !x.done).forEach((x) => m.set(x.owner || null, (m.get(x.owner || null) || 0) + (x.points || 1))); return m; };
|
|
131
|
+
const capOf = () => {
|
|
132
|
+
const m = loadOf(); const ids = [...new Set([...PEOPLE.map((p) => p.id), ...[...m.keys()].filter(Boolean)])];
|
|
133
|
+
const rows = ids.map((person) => { const capacity = ((PEOPLE.find((p) => p.id === person) || {}).points_per_week || 5) * 2; const used = m.get(person) || 0; return { person, pmos_points: used, capacity, jira_points: 0, over: used > capacity, free: Math.max(0, capacity - used) }; });
|
|
134
|
+
[['Dara', 8], ['Wawan', 10], ['Sam', 5]].forEach(([n, j]) => { const r = rows.find((x) => x.person === n); if (r) r.jira_points = j; else rows.push({ person: n, pmos_points: 0, capacity: 10, jira_points: j, over: false, free: 10 }); });
|
|
135
|
+
if (m.has(null)) rows.push({ person: null, pmos_points: m.get(null), capacity: 0, jira_points: 0, over: false, free: 0, unassigned: true });
|
|
136
|
+
return rows;
|
|
137
|
+
};
|
|
138
|
+
GET['/api/c3-init/breakdown'] = () => ({ ok: true, items: ITEMS.map((x) => ({ ...x })), phases: ['discovery', 'design', 'launch'], sources: ['local', 'figma', 'calendar', 'gmail', 'notes'], people: PEOPLE.map((p) => ({ ...p })), capacity: capOf(), history: EVENTS.slice(-8).reverse(), events: EVENTS.map((e) => ({ ...e })) });
|
|
139
|
+
POST['/api/roadmap-ops/breakdown/add'] = (b) => {
|
|
140
|
+
if (!String(b.title || '').trim()) return { ok: false, error: 'an item needs a title' };
|
|
141
|
+
if (!['discovery', 'design', 'launch'].includes(b.phase)) return { ok: false, error: 'phase must be discovery, design or launch' };
|
|
142
|
+
const rid = nextId('bd'); ITEMS.push(it(rid, String(b.title).trim(), b.phase, b.source || 'local', b.owner || '', b.points || 1, b.initiative || '')); EVENTS.push(ev('add', rid, 'human:wawan', 'Added by you.', '08', b.title)); EVENTS[EVENTS.length - 1].at = dayName();
|
|
143
|
+
return { ok: true, rid };
|
|
144
|
+
};
|
|
145
|
+
POST['/api/roadmap-ops/breakdown/edit'] = (b) => {
|
|
146
|
+
const x = ITEMS.find((y) => y.rid === b.rid); if (!x) return { ok: false, error: 'no such item: ' + b.rid };
|
|
147
|
+
const keys = Object.keys(b.fields || {}); Object.assign(x, b.fields || {}); EVENTS.push({ ...ev('edit', x.rid, 'human:wawan', 'edited ' + keys.join(', '), '08', x.title), at: dayName() });
|
|
148
|
+
return { ok: true };
|
|
149
|
+
};
|
|
150
|
+
let lastDeleted = null;
|
|
151
|
+
POST['/api/roadmap-ops/breakdown/delete'] = (b) => {
|
|
152
|
+
const i = ITEMS.findIndex((y) => y.rid === b.rid); if (i < 0) return { ok: false, error: 'no such item: ' + b.rid };
|
|
153
|
+
lastDeleted = { item: ITEMS[i], i }; ITEMS.splice(i, 1); return { ok: true, undo_ms: 8000 };
|
|
154
|
+
};
|
|
155
|
+
POST['/api/roadmap-ops/breakdown/undo'] = (b) => {
|
|
156
|
+
if (!lastDeleted || lastDeleted.item.rid !== b.rid) return { ok: false, error: 'nothing to undo for ' + b.rid };
|
|
157
|
+
ITEMS.splice(lastDeleted.i, 0, lastDeleted.item); lastDeleted = null; return { ok: true };
|
|
158
|
+
};
|
|
159
|
+
POST['/api/roadmap-ops/breakdown/dependency'] = (b) => {
|
|
160
|
+
const x = ITEMS.find((y) => y.rid === b.rid); if (!x) return { ok: false, error: 'no such item: ' + b.rid };
|
|
161
|
+
if (!/^[A-Z][A-Z0-9]+-\d+$/.test(String(b.jira || ''))) return { ok: false, error: 'a dependency is a Jira ticket key such as ABC-123' };
|
|
162
|
+
x.deps = b.remove ? x.deps.filter((d) => d !== b.jira) : [...new Set([...x.deps, b.jira])]; return { ok: true };
|
|
163
|
+
};
|
|
164
|
+
POST['/api/roadmap-ops/people'] = (b) => {
|
|
165
|
+
const p = PEOPLE.find((y) => y.id === b.id); if (p) p.points_per_week = Number(b.points_per_week); else PEOPLE.push({ id: b.id, points_per_week: Number(b.points_per_week) });
|
|
166
|
+
return { ok: true, people: PEOPLE.map((y) => ({ ...y })) };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
/* == tickets == */
|
|
171
|
+
/* GET /api/my-work: the mockup's 11 Jira tickets (TIX with src Jira, console3.html 3640 and 3904) as my-work items. owner, pts, forecast_due,
|
|
172
|
+
* blocker, critical and in_work are the mockup's extras the real read does not carry; raci and drafts are its two side cards. */
|
|
173
|
+
const jira = (key, title, category, owner, due, fc, pts, initiative, o) => ({ id: 'jira:' + key, source: 'jira', key, title, url: null, priority: null, status: category === 'done' ? 'Done' : category === 'doing' ? 'In progress' : 'To do',
|
|
174
|
+
category, due, estimate: null, blocks: 0, from: null, ask: null, thread: null, open: [], suggest: null, kind: 'agent', initiative, repo: null, ws: null, owner, pts, forecast_due: fc, ...(o || {}) });
|
|
175
|
+
GET['/api/my-work'] = () => ({ ok: true, today: '2026-10-08', read_at: null, counts: {}, banner: null, items: [
|
|
176
|
+
jira('CHK-141', 'Payments sandbox keys', 'doing', 'Dara', '2026-10-10', '2026-10-12', 2, 'checkout', { blocker: 'vendor, 2 days', critical: true, in_work: true }),
|
|
177
|
+
jira('EN-418', 'Index job fix', 'doing', 'Sam', '2026-10-12', '2026-10-15', 5, 'search', { blocker: 'platform review' }),
|
|
178
|
+
jira('CHK-142', 'Saved-card removal', 'todo', 'Wawan', '2026-10-17', '2026-10-19', 5, 'checkout', { critical: true }),
|
|
179
|
+
jira('CHK-143', 'Address autocomplete', 'todo', 'Dara', '2026-10-20', '2026-10-22', 5, 'checkout'),
|
|
180
|
+
jira('CHK-144', 'A11y audit slot', 'todo', 'Lee', '2026-10-24', '2026-10-31', 3, 'checkout', { blocker: 'no slot before 28 Oct', critical: true }),
|
|
181
|
+
jira('CHK-145', 'Ship Checkout v2', 'todo', 'Dara', '2026-10-24', '2026-10-31', 1, 'checkout', { critical: true }),
|
|
182
|
+
jira('SSO-31', 'SCIM docs', 'doing', 'Wawan', '2026-10-12', '2026-10-12', 5, 'sso'),
|
|
183
|
+
jira('SSO-34', 'SSO general release', 'todo', 'Dara', '2026-10-14', '2026-10-14', 1, 'sso'),
|
|
184
|
+
jira('EN-425', 'Relevance beta', 'todo', 'Sam', '2026-10-30', '2026-10-30', 3, 'search'),
|
|
185
|
+
jira('CHK-136', 'Payment form redesign', 'done', 'Dara', '2026-10-05', '2026-10-05', 8, 'checkout'),
|
|
186
|
+
jira('CHK-137', 'Tax and shipping display', 'done', 'Lee', '2026-10-05', '2026-10-05', 5, 'checkout'),
|
|
187
|
+
], raci: { people: ['Wawan', 'Dara', 'Lee', 'Sam', 'Priya'], rows: [
|
|
188
|
+
{ title: 'Payments sandbox keys', cells: ['', 'A', '', '', 'I'] }, { title: 'A11y audit slot', cells: ['', '', 'R', '', ''] },
|
|
189
|
+
{ title: 'Saved-card removal', cells: ['R', 'A', '', 'C', ''] }, { title: 'PRD update', cells: ['A', 'R', '', '', 'A'] }],
|
|
190
|
+
notes: [{ tone: 'bad', text: 'A11y audit slot has nobody responsible.' }, { tone: 'warn', text: 'PRD update has two accountable people.' }] },
|
|
191
|
+
drafts: [{ title: 'Retry state after a failed payment', pts: 3 }, { title: 'Error copy for declined cards', pts: 2 }] });
|
|
192
|
+
|
|
193
|
+
/* == documents == */
|
|
194
|
+
/* GET /api/docs/library (files, initiatives, kinds, edges, categories) with the mockup's 14 documents (IDOCS, console3.html 3652) and its graph nodes
|
|
195
|
+
* and edges (DXN and DXE, 4152). Extras the real library lacks: per file ty, v, st, why, up, node, col; a 4th edge element (the state); `cards`.
|
|
196
|
+
* POST /api/doc-health/check answers { ok, path, health: { passed, total, checks } } with the mockup's "N of M" per document. */
|
|
197
|
+
const DOCS = [ // id, name, type, initiative, version, updated, state, why, category
|
|
198
|
+
['p1', 'Checkout v2 PRD', 'PRD', 'checkout', 'v5', 'Wed 7 Oct', 'stale', 'Section 4 lists 4 flows. Today\'s decision may cut it to 3. Leadership saw v3.', 'spec', '2026-10-07'],
|
|
199
|
+
['r1', 'Checkout v2 rubric', 'Rubric', 'checkout', 'v2', 'Mon 5 Oct', 'current', '4 weighted dimensions, threshold 70%.', 'eval', '2026-10-05'],
|
|
200
|
+
['u1', 'Address drop-off analysis', 'Research', 'checkout', 'v1', 'Tue 6 Oct', 'current', 'The address step loses 45% of carts. Two recordings back it.', 'res', '2026-10-06'],
|
|
201
|
+
['b1', 'One-page checkout benchmark', 'Benchmark', 'checkout', 'v1', '28 Sep', 'stale', 'Peer median 3.4%. The data is 10 days old.', 'res', '2026-09-28'],
|
|
202
|
+
['g1', 'Checkout flow designs', 'Design', 'checkout', 'v3', 'Wed 7 Oct', 'current', 'Copy waits on the annual-plan label.', 'proto', '2026-10-07'],
|
|
203
|
+
['p2', 'Returns research plan', 'Brief', 'returns', 'v1', 'Mon 5 Oct', 'current', '5 interviews, 3 hypotheses, stop rule after interview 5.', 'spec', '2026-10-05'],
|
|
204
|
+
['u2', 'Interview notes 1 to 3', 'Research', 'returns', '3 of 5', 'Thu 8 Oct', 'draft', 'Labels lead. The support hypothesis is untested.', 'res', '2026-10-08'],
|
|
205
|
+
['b2', 'Returns experience benchmark', 'Benchmark', 'returns', '', '', 'missing', 'No peer data on label printing. The agent can draft it in about an hour.', 'res', null],
|
|
206
|
+
['p3', 'SSO PRD', 'PRD', 'sso', 'v4', 'Fri 2 Oct', 'current', 'Matches the beta scope.', 'spec', '2026-10-02'],
|
|
207
|
+
['r3', 'SSO rubric', 'Rubric', 'sso', 'v2', 'Fri 2 Oct', 'current', 'Beta scored 78% against 70%.', 'eval', '2026-10-02'],
|
|
208
|
+
['u3', 'Beta feedback synthesis', 'Research', 'sso', 'v1', 'Wed 7 Oct', 'current', 'Enterprise wants SCIM before audit logs.', 'res', '2026-10-07'],
|
|
209
|
+
['p4', 'Search relevance PRD', 'PRD', 'search', 'v2', 'Thu 1 Oct', 'current', 'Done criteria listed.', 'spec', '2026-10-01'],
|
|
210
|
+
['r4', 'Search relevance rubric', 'Rubric', 'search', 'v1', '', 'draft', 'Agent draft waits for your weights. Build is half done.', 'eval', null],
|
|
211
|
+
['p5', 'Onboarding quick brief', 'Brief', 'onb', 'v1', '29 Sep', 'stale', 'No update in 9 days. The stop rule has passed.', 'spec', '2026-09-29'],
|
|
212
|
+
];
|
|
213
|
+
const DNODES = [ // id, name, node type, column, category, initiative
|
|
214
|
+
['kr:conv', 'Checkout conversion', 'Goal', 0, 'goal', ''], ['kr:ret', 'Returns contact rate', 'Goal', 0, 'goal', ''], ['kr:sso', 'SSO adoption', 'Goal', 0, 'goal', ''],
|
|
215
|
+
['dec:scope', 'Scope decision, 14:00', 'Decision', 0, 'dec', ''], ['fc:q4', 'Q4 forecast, row 14', 'Data', 0, 'data', ''],
|
|
216
|
+
['t:checkout', '6 tickets, Checkout v2', 'Tickets', 4, 'tickets', 'checkout'], ['t:search', '3 tickets, Search relevance', 'Tickets', 4, 'tickets', 'search'], ['t:sso', '3 tickets, SSO', 'Tickets', 4, 'tickets', 'sso'],
|
|
217
|
+
];
|
|
218
|
+
const DCOL = { Research: 1, Benchmark: 1, PRD: 2, Brief: 2, Rubric: 3, Design: 3 };
|
|
219
|
+
const DFILES = [...DOCS.map(([id, name, ty, ini, v, up, st, why, cat, date]) => ({ id, path: 'planning/' + id + '-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '.md', title: name, category: cat, date, initiative: ini, ty, v, up, st, why, col: DCOL[ty] })),
|
|
220
|
+
...DNODES.map(([id, name, node, col, cat, ini]) => ({ id, path: id, title: name, category: cat, date: null, initiative: ini, node, col, st: 'node' }))];
|
|
221
|
+
const DKINDS = ['goal', 'measured by', 'informs', 'changes', 'numbers', 'rubric from', 'designs from', 'tickets from', 'implements', 'accepts', 'needs', 'label insight'];
|
|
222
|
+
const DEDGES = [['kr:conv', 'p1', 0, 'ok'], ['kr:conv', 'r1', 1, 'ok'], ['kr:ret', 'p2', 0, 'ok'], ['kr:sso', 'p3', 0, 'ok'], ['u1', 'p1', 2, 'ok'], ['b1', 'p1', 2, 'ok'], ['dec:scope', 'p1', 3, 'conflict'], ['fc:q4', 'p1', 4, 'conflict'],
|
|
223
|
+
['p1', 'r1', 5, 'drift'], ['p1', 'g1', 6, 'ok'], ['p1', 't:checkout', 7, 'ok'], ['g1', 't:checkout', 8, 'ok'], ['r1', 't:checkout', 9, 'ok'], ['u2', 'p2', 2, 'ok'], ['p2', 'b2', 10, 'missing'], ['u3', 'p3', 2, 'ok'], ['p3', 'r3', 5, 'ok'],
|
|
224
|
+
['p3', 't:sso', 7, 'ok'], ['p4', 'r4', 5, 'ok'], ['p4', 't:search', 7, 'ok'], ['u2', 'p1', 11, 'ok']];
|
|
225
|
+
const DIDX = Object.fromEntries(DFILES.map((f, i) => [f.id, i]));
|
|
226
|
+
const DHEALTH = { p1: [3, 6], b1: [2, 4], p5: [2, 4], r4: [2, 3], r1: [3, 3], u2: [3, 3], u1: [3, 3], g1: [3, 3], p2: [3, 3], p3: [3, 3], r3: [3, 3], u3: [3, 3], p4: [3, 3] };
|
|
227
|
+
const DCHECKS = ['fresh', 'decisions', 'data', 'complete', 'traced', 'readers'];
|
|
228
|
+
GET['/api/docs/library'] = () => ({ ok: true, key: 'demo', files: DFILES.map(({ id, ...f }) => f), edges: DEDGES.map(([a, b, k, st]) => [DIDX[a], DIDX[b], k, st]), kinds: DKINDS,
|
|
229
|
+
categories: [{ key: 'spec', name: 'Specs' }, { key: 'eval', name: 'Evals' }, { key: 'res', name: 'Research & plans' }, { key: 'proto', name: 'Prototypes' }, { key: 'goal', name: 'Goals' }, { key: 'dec', name: 'Decisions' }, { key: 'data', name: 'Data' }, { key: 'tickets', name: 'Tickets' }],
|
|
230
|
+
initiatives: { checkout: { title: 'Checkout v2', short: 'Checkout v2' }, returns: { title: 'Returns experience', short: 'Returns experience' }, sso: { title: 'SSO', short: 'SSO' }, search: { title: 'Search relevance', short: 'Search relevance' }, onb: { title: 'Self-serve onboarding', short: 'Self-serve onboarding' } },
|
|
231
|
+
unreadable: [], ws: 'demo',
|
|
232
|
+
cards: { benchmark: { sub: 'checkout conversion', rows: [['Top quartile', .86, 'var(--success)', '4.3%'], ['Peer median', .68, 'var(--acc)', '3.4%'], ['You', .62, 'var(--warning)', '3.1%'], ['Goal', .8, 'var(--ring)', '4.0%']] },
|
|
233
|
+
research: { sub: 'returns, 3 interviews', rows: [['Label printing', 1, 'var(--success)', '3 of 3'], ['30-day window', .67, 'var(--warning)', '2 of 3'], ['Support route', .04, 'var(--ring)', '0 of 3']] } } });
|
|
234
|
+
/* the text and the revisions of each document (the mockup's DOCTX, console3.html line 4045); a document with none gets the mockup's default: a summary and a details section */
|
|
235
|
+
const mdOf = (secs) => secs.map(([h, t]) => '## ' + h + '\n' + t + '\n').join('\n');
|
|
236
|
+
const DOCV = {
|
|
237
|
+
p1: [['Wawan', '2026-09-25', 'The version leadership saw.', [['Problem', 'Checkout converts at 3.5%. The address step loses carts.'], ['Goal', 'Raise checkout conversion to 4.0% by Q3 2027. Growth assumption 4%.'], ['Users', 'Mid-size merchants and their repeat shoppers.'], ['Scope', 'Three flows ship together: one-page address entry, wallet pay and guest checkout.']]],
|
|
238
|
+
['Wawan', '2026-10-05', 'Added saved-card removal and the risks.', [['Problem', 'Checkout converts at 3.1%, down from 3.5% in six weeks. The address step loses 45% of carts.'], ['Goal', 'Raise checkout conversion to 4.0% by Q3 2027. Growth assumption 4%.'], ['Users', 'Mid-size merchants and their repeat shoppers on desktop and mobile web.'], ['Scope', 'Four flows ship together: one-page address entry, saved-card removal, wallet pay and guest checkout.'], ['Risks', 'Vendor sandbox keys.']]],
|
|
239
|
+
['Wawan', '2026-10-07', 'Reworded the done criteria and added the audit risk.', [['Problem', 'Checkout converts at 3.1%, down from 3.5% in six weeks. The address step loses 45% of carts.'], ['Goal', 'Raise checkout conversion to 4.0% by Q3 2027. Growth assumption 4%.'], ['Users', 'Mid-size merchants and their repeat shoppers on desktop and mobile web.'], ['Scope', 'Four flows ship together: one-page address entry, saved-card removal, wallet pay and guest checkout.'], ['Done criteria', 'Conversion at or above 3.6% over 14 days. Accessibility audit passed. Payment errors below 0.5%.'], ['Risks', 'Vendor sandbox keys. Accessibility audit slot.']]]],
|
|
240
|
+
r1: [['Agent', '2026-10-05', 'Weights drafted from the PRD, confirmed by you.', [['Outcome, 30%', 'Conversion reaches 3.6% over 14 days.'], ['Quality, 25%', 'CI green, no payment errors above 0.5%.'], ['Adoption, 25%', 'At least 60% of merchants enable the new flow.'], ['Process, 20%', 'Spec and rubric before build. Review recorded.'], ['Threshold', '70%']]]],
|
|
241
|
+
b1: [['Agent', '2026-09-28', 'First benchmark of one-page checkout.', [['Summary', 'Peers with a one-page checkout convert at a median of 3.4%. The top quartile reaches 4.3%.'], ['Sources', 'Five public case studies and two analyst reports, read on 28 Sep.'], ['Caveat', 'Mix of verticals. Treat as a range.']]]],
|
|
242
|
+
u2: [['Agent', '2026-10-08', 'Notes from interviews 1 to 3.', [['Label printing', '3 of 3 customers say printing the return label is the hardest step.'], ['30-day window', '2 of 3 did not know the window existed.'], ['Support route', 'Not yet asked. Interview 4 tests it.']]]],
|
|
243
|
+
p2: [['Wawan', '2026-10-05', 'Research plan for returns.', [['Question', 'Where do returns hurt most for mid-size merchants?'], ['Hypotheses', 'Label printing is the main pain. Customers miss the 30-day window. Support is a workaround, not a cost.'], ['Method', 'Five customer interviews, one week.'], ['Stop rule', 'Park the work if fewer than 3 of 5 raise labels by 13 Oct.']]]],
|
|
244
|
+
};
|
|
245
|
+
const REV = {}; // path -> [{ rev, by, at, why, text }]
|
|
246
|
+
const revsOf = (path) => {
|
|
247
|
+
if (REV[path]) return REV[path];
|
|
248
|
+
const f = DFILES.find((x) => x.path === path); if (!f) return null;
|
|
249
|
+
const v = DOCV[f.id] || [['Agent', f.date || '2026-10-08', 'Current version.', [['Summary', f.why || 'No summary.'], ['Details', 'The detail lives in the source tool. This view shows the agent summary.']]]];
|
|
250
|
+
const vn = /^v(\d+)$/.exec(f.v || ''); const top = vn ? +vn[1] : 0;
|
|
251
|
+
REV[path] = v.map(([by, at, why, secs], i) => ({ rev: i + 1, ...(top ? { v: 'v' + (top - (v.length - 1 - i)) } : {}), by: /^Wawan$/.test(by) ? 'human:wawan' : 'claude-code/session', at: at + 'T09:00:00.000Z', why, text: mdOf(secs) }));
|
|
252
|
+
return REV[path];
|
|
253
|
+
};
|
|
254
|
+
const hashOf = (t) => { let h = 5381; for (const c of String(t)) h = ((h * 33) ^ c.charCodeAt(0)) >>> 0; return h.toString(16).padStart(8, '0') + String(t).length.toString(16).padStart(4, '0'); };
|
|
255
|
+
const nextVer = (v) => { const m = /^v(\d+)$/.exec(v || ''); return m ? 'v' + (+m[1] + 1) : 'v1'; };
|
|
256
|
+
const pathOf = (q) => q.get('path');
|
|
257
|
+
const withRev = (path, by, why, text) => { const R = revsOf(path); const f = DFILES.find((x) => x.path === path); f.v = nextVer(f.v); R.push({ rev: R.length + 1, ...(/^v\d+$/.test(f.v) ? { v: f.v } : {}), by, at: '2026-10-08T08:12:00.000Z', why, text }); f.st = 'current'; f.up = 'Thu 8 Oct'; f.date = '2026-10-08'; return R.length; };
|
|
258
|
+
GET['/api/doc'] = (q) => { const R = revsOf(pathOf(q)); return R ? R[R.length - 1].text : undefined; };
|
|
259
|
+
GET['/api/doc-sheet/revisions'] = (q) => { const R = revsOf(pathOf(q)); return R ? { ok: true, path: pathOf(q), revisions: R.map(({ text, ...r }) => ({ ...r, before_hash: '', after_hash: hashOf(text) })) } : { ok: false, error: 'could not read the document' }; };
|
|
260
|
+
GET['/api/doc-sheet/at'] = (q) => { const R = revsOf(pathOf(q)); const r = R && R.find((x) => x.rev === Number(q.get('rev'))); return r ? { ok: true, path: pathOf(q), rev: r.rev, text: r.text } : { ok: false, error: 'no such revision' }; };
|
|
261
|
+
POST['/api/doc-sheet/edit'] = (b) => {
|
|
262
|
+
const R = revsOf(b.path); if (!R) return { ok: false, error: 'could not read the document' };
|
|
263
|
+
if (typeof b.why !== 'string' || !b.why.trim()) return { ok: false, error: 'a why is required: say why the document changes' };
|
|
264
|
+
return { ok: true, path: b.path, rev: withRev(b.path, 'human:pm', b.why.trim(), b.text), logged: false };
|
|
265
|
+
};
|
|
266
|
+
const PROPS = {};
|
|
267
|
+
POST['/api/doc-sheet/propose'] = (b) => {
|
|
268
|
+
const R = revsOf(b.path); if (!R) return { ok: false, error: 'could not read the document' };
|
|
269
|
+
const id = nextId('prop'); PROPS[id] = { path: b.path, prompt: b.prompt, after: b.after, hash: hashOf(b.after), before: R[R.length - 1].text };
|
|
270
|
+
return { ok: true, id, state: 'proposed', hash: PROPS[id].hash, diff: [], voided_hash: null };
|
|
271
|
+
};
|
|
272
|
+
POST['/api/doc-sheet/accept'] = (b) => {
|
|
273
|
+
const p = PROPS[b.id]; if (!p || p.path !== b.path) return { ok: false, error: 'no such proposal' };
|
|
274
|
+
if (b.hash !== p.hash) return { ok: false, error: 'the hash does not match the proposal the PM saw', reason: 'hash mismatch' };
|
|
275
|
+
const R = revsOf(b.path); if (R[R.length - 1].text !== p.before) return { ok: false, error: 'stale: the document changed since this proposal was made', reason: 'stale' };
|
|
276
|
+
return { ok: true, path: b.path, rev: withRev(b.path, 'claude-code/session', p.prompt, p.after) };
|
|
277
|
+
};
|
|
278
|
+
POST['/api/doc-sheet/change-note'] = (b) => ({ ok: true, note: 'A change note for the owners of ' + (b.downstream || []).join(', ') });
|
|
279
|
+
|
|
280
|
+
/* health: the six checks (the mockup's docHealth, line 4059). Checkout v2 PRD is recomputed from its text; the others answer the fixed N of M. */
|
|
281
|
+
const HN = { fresh: 'Fresh', decisions: 'Matches the scope decision', data: 'Matches the forecast', complete: 'Complete', traced: 'Traced to a goal and tickets', readers: 'Readers have this version' };
|
|
282
|
+
POST['/api/doc-health/check'] = (b) => {
|
|
283
|
+
const f = DFILES.find((x) => x.path === b.path); const h = f && DHEALTH[f.id];
|
|
284
|
+
if (!f || !h) return { ok: false, error: 'file not found' };
|
|
285
|
+
if (f.id === 'p1') {
|
|
286
|
+
const t = revsOf(b.path).slice(-1)[0].text; const three = /three flows|3 flows/i.test(t); const six = /6%/.test(t) && !/4%/.test(t);
|
|
287
|
+
const C = [{ id: 'fresh', pass: true, reason: 'changed today, stale after 7 days' },
|
|
288
|
+
{ id: 'decisions', pass: three, reason: three ? 'Scope names 3 flows' : 'Scope lists 4 flows. Today\'s decision may cut it to 3', section: 'Scope', flag: 'Conflicts with today\'s decision', fix: 'Cut the scope to 3 flows to match the decision' },
|
|
289
|
+
{ id: 'data', pass: six, reason: six ? 'Growth 6% matches row 14' : 'PRD says 4%, the Q4 forecast row 14 says 6%', section: 'Goal', flag: 'Growth differs from the forecast', fix: 'Align the growth assumption with the Q4 forecast' },
|
|
290
|
+
{ id: 'complete', pass: true, reason: 'Required sections present' },
|
|
291
|
+
{ id: 'traced', pass: true, reason: 'Linked to the goal and its tickets' },
|
|
292
|
+
{ id: 'readers', pass: f.v === 'v3', reason: 'Leadership saw v3, this is ' + f.v, fix: 'Draft a short change note for leadership' }];
|
|
293
|
+
const passed = C.filter((c) => c.pass).length; DHEALTH.p1 = [passed, 6];
|
|
294
|
+
return { ok: true, path: b.path, health: { passed, total: 6, checks: C } };
|
|
295
|
+
}
|
|
296
|
+
return { ok: true, path: b.path, health: { passed: h[0], total: h[1], checks: DCHECKS.slice(0, h[1]).map((id, i) => ({ id, pass: i < h[0], reason: i < h[0] ? HN[id] + ': holds' : (f.why || 'does not hold'), ...(i < h[0] ? {} : { fix: 'Update it to match the current plan' }) })) } };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/* == signals == */
|
|
300
|
+
/* GET /api/signals: the mockup's SIGS (console3.html 2861 and 3899 on) as signal rules with one latest reading each, plus the optional narrative
|
|
301
|
+
* fields (title, evidence, action, when, spark, sev, initiative) and the response's head, charts and watch list that the real route does not send. */
|
|
302
|
+
const sigRule = (id, metric, op, threshold, severity, o) => ({ id, metric, op, threshold, severity, ...o });
|
|
303
|
+
/* the case file of each signal: the mockup's SIGC (console3.html line 2864), as the case model c3-case.js reads */
|
|
304
|
+
const sgc = (sit, conf, chg, opts, unsure) => ({ sit, conf, cta: 'Slide to confirm', chg, opts, unsure: unsure || '' });
|
|
305
|
+
const SIG_CASE = {
|
|
306
|
+
conv: sgc('At this slope conversion ends the quarter at 2.9% against 4.0%. The pivot line is 3.0% for two weeks.', 71, [['clock', 'Down 6 weeks in a row', 'Analytics', 'bad'], ['alert', 'Cart to address loses 45%', 'funnel', 'bad'], ['scale', 'Scope change may be the cause', 'Jira', 'warn']],
|
|
307
|
+
[{ id: 'x', l: 'Run the address-step experiment, freeze scope', rec: 1, msg: 'Experiment brief is drafting. Scope freeze goes into today\'s decision.', fx: [['check', 'Tests the cause before pivoting', 'ok'], ['clock', 'Result in 2 weeks']] }, { id: 'w', l: 'Wait two more weeks', msg: 'Watching. The agent re-checks in 2 weeks.', fx: [['alert', 'Reaches the pivot line unprepared', 'warn']] }, { id: 'p', l: 'Pivot the checkout bet now', msg: 'Pivot review requested for the Quarterly review.', fx: [['alert', 'Trigger not crossed yet', 'bad']] }], 'One quarter of data. A seasonal dip is not ruled out.'),
|
|
308
|
+
ret: sgc('Retention has not moved all quarter, while the clearest customer pain sits in returns.', 66, [['clock', 'Day 30 at 41.5% against 45%', 'Analytics', 'warn'], ['notes', '3 of 3 calls name label printing', 'Notes', 'ok'], ['cal', 'Interview 5 on 13 Oct', 'Calendar']],
|
|
309
|
+
[{ id: 'f', l: 'Pull Returns forward a quarter if interview 5 agrees', rec: 1, msg: 'Conditional change drafted for the Q1 plan.', fx: [['check', 'Moves effort to the strongest signal', 'ok']] }, { id: 'k', l: 'Keep the plan', msg: 'Plan unchanged.', fx: [['clock', 'Returns waits until Q1', 'warn']] }]),
|
|
310
|
+
slip: sgc('Sandbox keys and the a11y slot hold up the critical path. The ship date has moved from 24 to 31 Oct.', 78, [['clock', '7 days late on the critical path', 'Jira', 'bad'], ['alert', 'Sandbox keys 2 days overdue', 'Jira', 'bad'], ['scale', 'Cause: scope, not capacity', 'plan run 09:12', 'warn']],
|
|
311
|
+
[{ id: 'c', l: 'Cut to 3 flows and hold 24 Oct', rec: 1, msg: 'Scenario set to the cut. Timeline and Calendar follow.', fx: [['check', 'Holds 24 Oct', 'ok'], ['scale', 'Drops one flow', 'warn']] }, { id: 'w', l: 'Warn leadership now with a new date', msg: 'A note to leadership is drafted. It goes out only by your slide.', fx: [['out', 'Goes out only by your slide', 'ok']] }, { id: 'h', l: 'Hold and watch', msg: 'Watching.', fx: [['alert', 'The date keeps slipping', 'warn']] }]),
|
|
312
|
+
churn: sgc('Three scope changes in four weeks make any date a guess.', 74, [['scale', '3 changes in 4 weeks', 'Jira', 'warn'], ['doc', 'PRD at v5', 'Confluence']],
|
|
313
|
+
[{ id: 'z', l: 'Freeze scope after today\'s decision', rec: 1, msg: 'Scope freeze added to the decision follow-ups.', fx: [['check', 'Makes the date believable', 'ok']] }, { id: 'o', l: 'Stay flexible', msg: 'No freeze.', fx: [['alert', 'Date stays a guess', 'warn']] }]),
|
|
314
|
+
dep: sgc('The same vendor delay holds up two bets, so it is a portfolio risk and not a task risk.', 82, [['alert', 'Sandbox keys 2 days overdue', 'Jira', 'bad'], ['init', 'Blocks Checkout v2 and SSO billing', 'Initiatives', 'warn']],
|
|
315
|
+
[{ id: 'e', l: 'Escalate to the vendor lead today', rec: 1, msg: 'Escalation drafted for you to send.', fx: [['check', 'Unblocks both bets', 'ok']] }, { id: 'w', l: 'Wait until Friday', msg: 'Waiting.', fx: [['clock', 'Crosses 5 days', 'bad']] }]),
|
|
316
|
+
rel: sgc('Dates have lost trust. Ranges tell leadership the truth about uncertainty.', 69, [['report', '62% of dated promises kept', 'Reports', 'warn'], ['out', 'Next update to leadership: Fri 16 Oct', 'Calendar']],
|
|
317
|
+
[{ id: 'r', l: 'Switch to ranges for estimates', rec: 1, msg: 'Ranges will be used in the next update.', fx: [['check', 'Rebuilds trust', 'ok']] }, { id: 'd', l: 'Keep dates', msg: 'No change.', fx: [['alert', 'Risks another miss', 'warn']] }]),
|
|
318
|
+
gate: sgc('Build is 70% done and no rubric exists. The gate order says the rubric comes first.', 80, [['alert', 'No rubric for Search relevance', 'Documents', 'bad'], ['clock', 'Build is 70% done', 'Jira', 'warn']],
|
|
319
|
+
[{ id: 'r', l: 'Have the agent draft the rubric for your weights', rec: 1, msg: 'Draft started. It lands in Review as a diff you approve.', fx: [['check', 'You approve before anything counts', 'ok']] }, { id: 'h', l: 'Hold the merge until it exists', msg: 'Merge held.', fx: [['clock', 'Build waits', 'warn']] }]),
|
|
320
|
+
ent: sgc('Customers are pulling Enterprise forward faster than the plan assumes.', 77, [['user', '3 design partners live', 'Jira', 'ok'], ['mail', '2 asked for SCIM early', 'Gmail', 'ok'], ['target', '5 partners by Dec is the goal', 'OKR']],
|
|
321
|
+
[{ id: 'q', l: 'Add Enterprise as a Q1 bet', rec: 1, msg: 'Q1 bet drafted for the Quarterly review.', fx: [['check', 'Follows the pull', 'ok'], ['alert', 'Needs capacity from Retain', 'warn']] }, { id: 'h', l: 'Hold the plan', msg: 'No change.', fx: [['alert', 'May lose the window', 'warn']] }]),
|
|
322
|
+
};
|
|
323
|
+
const SIG_RULES = [
|
|
324
|
+
sigRule('conv', 'checkout.conversion', '<', 3.0, 'act', { title: 'Conversion has fallen six weeks in a row', evidence: '3.5% to 3.1%. The address step loses 45% of carts.', action: 'Below 3.0% for 2 weeks: pivot the checkout bet', when: 'about 2 weeks', spark: [3.5, 3.4, 3.4, 3.3, 3.3, 3.2, 3.1], initiative: 'checkout' }),
|
|
325
|
+
sigRule('ret', 'retention.day30', '<', 45, 'act', { title: 'Retention is flat while the top pain is returns', evidence: 'Day 30 at 41.5% against 45%. 3 of 3 calls name label printing.', action: 'No lift after interview 5: pull Returns forward', when: '5 days', spark: [41, 41.2, 41.4, 41.3, 41.5, 41.4, 41.5], initiative: 'returns' }),
|
|
326
|
+
sigRule('slip', 'checkout.critical_path_late_days', '>', 5, 'act', { title: 'Checkout v2 critical path is 7 days late', evidence: 'Sandbox keys and the a11y slot move the ship date from 24 to 31 Oct.', action: 'More than 5 days late: warn leadership', when: 'tripped', spark: [0, 0, 2, 4, 7, 7], initiative: 'checkout' }),
|
|
327
|
+
sigRule('churn', 'checkout.scope_changes', '>=', 4, 'watch', { title: 'Scope keeps changing on Checkout v2', evidence: 'Scope changed 3 times in 4 weeks.', action: 'A fourth change: re-baseline the plan', when: 'next change', spark: [1, 1, 2, 2, 3, 3, 3], initiative: 'checkout' }),
|
|
328
|
+
sigRule('dep', 'vendor.keys_overdue_days', '>', 5, 'watch', { title: 'One vendor blocks two bets', evidence: 'Sandbox keys hold up Checkout v2 and SSO billing.', action: 'More than 5 days overdue: escalate to the sponsor', when: '3 days', spark: [0, 0, 0, 1, 1, 2, 2], initiative: 'checkout,sso' }),
|
|
329
|
+
sigRule('rel', 'promises.kept_pct', '<', 55, 'watch', { title: 'Dated promises are slipping more often', evidence: '62% of dated promises kept this quarter against 80%.', action: 'Below 55%: give ranges, not dates', when: '2 to 3 weeks', spark: [85, 80, 78, 72, 68, 65, 62], initiative: 'checkout,sso,search,returns,onb' }),
|
|
330
|
+
sigRule('gate', 'search.build_without_rubric', '>', 0, 'watch', { title: 'Search relevance has no quality evidence yet', evidence: 'Build is 70% done. Quality, review and acceptance are all open.', action: 'Build without a rubric: block the merge', when: '3 days', spark: [0, 0, 0, 1, 1, 2], initiative: 'search' }),
|
|
331
|
+
sigRule('ent', 'enterprise.partners', '>=', 5, 'watch', { sev: 'up', title: 'Enterprise pull is stronger than planned', evidence: '3 design partners live. 2 more asked for SCIM early.', action: '5 partners by Dec: make Enterprise a Q1 bet', when: 'on course', spark: [0, 0, 1, 1, 2, 3, 3], initiative: 'sso' }),
|
|
332
|
+
];
|
|
333
|
+
GET['/api/signals'] = () => ({ ok: true, rules: SIG_RULES.map((r) => ({ ...r, case: SIG_CASE[r.id] })), readings: [{ metric: 'checkout.conversion', value: 3.1, at: '2026-10-08' }, { metric: 'checkout.critical_path_late_days', value: 7, at: '2026-10-08' }], trips: [{ rule_id: 'slip', severity: 'act', reading: { metric: 'checkout.critical_path_late_days', value: 7, at: '2026-10-08' }, at: '2026-10-08' }], open: [{ rule_id: 'slip', severity: 'act', op: '>', threshold: 5, reading: { metric: 'checkout.critical_path_late_days', value: 7, at: '2026-10-08' }, at: '2026-10-08' }],
|
|
334
|
+
head: { h: 'Checkout v2 is 7 days late and scope is up 62%', s: 'Both trace to scope, not capacity. Two rules have tripped.', ring: [4, 6] },
|
|
335
|
+
charts: { creep: { baseline: 21, points: [['14 Sep', 21, ''], ['25 Sep', 26, 'Saved-card removal'], ['30 Sep', 29, 'A11y audit'], ['5 Oct', 34, 'Address autocomplete'], ['8 Oct', 34, '']] }, delay: { promised: 24, points: [['14 Sep', 24], ['21 Sep', 24], ['28 Sep', 26], ['5 Oct', 28], ['8 Oct', 31]] } },
|
|
336
|
+
watch: [{ tone: 'ok', text: 'Gate order: no build before spec and rubric', note: '0 breaches' }, { tone: 'warn', text: 'Scope changes: a fourth re-baselines the plan', note: '3 of 4' }, { tone: 'bad', text: 'Critical path: more than 5 days late warns leadership', note: '7 days, tripped' },
|
|
337
|
+
{ tone: 'warn', text: 'Dependencies: over 5 days overdue escalates', note: '2 days' }, { tone: 'warn', text: 'Conversion below 3.0% for 2 weeks reviews the bet', note: '3.1%, 6 weeks down' }, { tone: 'ok', text: 'Correction rate rising 3 runs reverts to L2', note: '15%, falling' }] });
|
|
338
|
+
GET['/api/records/kr-metrics'] = () => ({ ok: true, kind: 'kr-metrics', records: [] });
|
|
339
|
+
|
|
340
|
+
/* == new initiative == */
|
|
341
|
+
/* POST /api/intake/draft: the mockup's three example lines (C3.EXN, console3.html line 4781) in the route's draft shape; any other line gets the engine's own honest default */
|
|
342
|
+
const EXN = { 'self-serve billing for teams': { lane: 'feature', confidence: 0.62, anchor_suggestion: 'Checkout conversion 4.0%', prior_rejections: [{ title: 'Usage-based billing was rejected on 12 Aug (planning/out-of-scope/billing.md). This idea is flat per-seat pricing.', path: 'planning/out-of-scope/billing.md' }] },
|
|
343
|
+
'why returns hurt': { lane: 'discovery', confidence: 0.84, anchor_suggestion: 'Returns contact rate 0.5', prior_rejections: [] },
|
|
344
|
+
'fix flaky ci on search': { lane: 'chore', confidence: 0.9, anchor_suggestion: 'health:ci-reliability', prior_rejections: [] } };
|
|
345
|
+
POST['/api/intake/draft'] = (b) => {
|
|
346
|
+
const line = String(b.line || '').trim(); if (!line) return { ok: false, error: 'line is required' };
|
|
347
|
+
const e = EXN[line.toLowerCase()] || { lane: 'feature', confidence: 0.45, anchor_suggestion: null, prior_rejections: [] };
|
|
348
|
+
return { ok: true, draft: { title: line, ...e, refused: false, refused_reason: null, stage: 'discover' } };
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/* == wrapper == */
|
|
352
|
+
export function installInit() {
|
|
353
|
+
/* what each initiative already has, per requirement of its stage (the mockup's C3.done, console3.html line 4695): the Lifecycle lens reads it in demo mode only */
|
|
354
|
+
const all = ['brief', 'evidence', 'cont', 'opts', 'bet', 'commit', 'prd', 'rub', 'plan'];
|
|
355
|
+
const tick = (l) => Object.fromEntries(l.map((k) => [k, 1]));
|
|
356
|
+
window.__c3reqDone = { checkout: tick(all), returns: tick(['brief', 'evidence']), sso: tick([...all, 'rev']), search: tick([...all, 'rev', 'rel']), onb: tick(['brief', 'evidence']) };
|
|
357
|
+
const prev = window.fetch;
|
|
358
|
+
window.fetch = (input, init) => {
|
|
359
|
+
try {
|
|
360
|
+
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
361
|
+
const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
362
|
+
const u = new URL(raw, location.origin);
|
|
363
|
+
if (u.origin === location.origin) {
|
|
364
|
+
if (method === 'GET' && GET[u.pathname]) { const out = GET[u.pathname](u.searchParams, u); if (typeof out === 'string') return Promise.resolve(new Response(out, { status: 200, headers: { 'content-type': 'text/markdown; charset=utf-8' } })); if (out !== undefined) return Promise.resolve(json(out)); }
|
|
365
|
+
if (method === 'POST' && POST[u.pathname]) {
|
|
366
|
+
let b = {}; try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; }
|
|
367
|
+
return Promise.resolve(json(POST[u.pathname](b)));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch { /* an unreadable request goes on as it is */ }
|
|
371
|
+
return prev(input, init);
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* pm/c3-demo-life.js — DEMO fixtures of the Lifecycle panels (/c3.html?demo=1): the mockup's own sample content (console3.html IDEAS 3439, HYPS 3451, GSTATE 3453, RUBRIC 3454, the
|
|
2
|
+
* lessons 3575, the correction rate 3604) in the REAL response shapes of the routes c3-lifecycle-panels.js reads:
|
|
3
|
+
* GET /api/records/ideas | hypotheses | decisions { ok, kind, records: [{ rid, ...fields }] } (scripts/records)
|
|
4
|
+
* GET /api/c3-life { ok, evals, corrections, capacity, sources, interviews } (workspace/lib/routes-c3-life.js; `interviews` is demo only)
|
|
5
|
+
* GET /api/console?name=ladder&id=<initiative> { ok, row: { steps: [...], next, blockers, gate_failures } } (the workflow engine's ladder)
|
|
6
|
+
* GET /api/console?name=radar { ok, data: { calibration } }: the mockup draws none, so none is invented
|
|
7
|
+
* c3-demo.js imports this file for its side effect: it wraps window.fetch BEFORE c3-demo.js captures the original, so an answer here sits innermost and every other demo answer
|
|
8
|
+
* (and the "every write answers { ok: true }" rule, which never reaches this file) stays as it was. Real mode never loads this file. */
|
|
9
|
+
const json = (body) => Promise.resolve(new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } }));
|
|
10
|
+
const rec = (kind, rid, f) => ({ rid, ...f, created: 'demo', updated: 'demo' });
|
|
11
|
+
|
|
12
|
+
const IDEAS = [
|
|
13
|
+
['a1', 'Address autocomplete', 'checkout', 8, 3, 0.75, 'agent', 'new', 'Typing is the slow part of the address step, where 45% of carts stop.'],
|
|
14
|
+
['a2', 'Show the 30-day window at checkout', 'returns', 6, 2, 0.7, 'agent', 'new', '2 of 3 customers did not know the return window.'],
|
|
15
|
+
['a3', 'Print the label for the customer', 'returns', 8, 5, 0.8, 'customer', 'new', '3 of 3 calls name label printing as the hardest part.'],
|
|
16
|
+
['a4', 'QR returns at the door, no printer', 'returns', 9, 7, 0.5, 'agent', 'new', 'Removes the printer entirely. Needs carrier support.'],
|
|
17
|
+
['a5', 'Instant exchange instead of refund', 'returns', 7, 8, 0.4, 'you', 'new', 'Keeps revenue, but touches payments and stock.'],
|
|
18
|
+
['a6', 'Saved cards in v2.1', 'checkout', 5, 3, 0.7, 'you', 'short', 'Cut from v2 scope. Easy once the billing code exists.'],
|
|
19
|
+
['a7', 'Annual-plan label test', 'checkout', 3, 1, 0.6, 'you', 'new', 'Cheap to test, small upside.'],
|
|
20
|
+
['a8', 'SCIM provisioning', 'sso', 8, 5, 0.8, 'customer', 'short', 'Two design partners asked for it early.'],
|
|
21
|
+
['a9', 'Audit logs for Enterprise', 'sso', 7, 6, 0.6, 'agent', 'new', 'Proposed by the agent from three support threads.'],
|
|
22
|
+
['a10', 'Support deflection bot', 'returns', 4, 4, 0.3, 'agent', 'parked', 'Low confidence. Support is a workaround, not yet a cost.'],
|
|
23
|
+
].map(([id, title, initiative, impact, effort, confidence, source, status, why]) => rec('ideas', id, { title, initiative, impact, effort, confidence, source, status, why }));
|
|
24
|
+
|
|
25
|
+
const EV = (n, d, w, at) => ({ src: 'planning/briefs/returns-interview-' + n + '.md', dir: d, w, at });
|
|
26
|
+
const HYPS = [
|
|
27
|
+
rec('hypotheses', 'h1', { title: 'Label printing is the main pain', initiative: 'returns', falsifier: 'Fewer than 3 of 5 interviews raise labels', review_by: '2026-10-13', evidence: [EV(1, 'supports', 1, '2026-10-06'), EV(2, 'supports', 1, '2026-10-07'), EV(3, 'supports', 1, '2026-10-08')] }),
|
|
28
|
+
rec('hypotheses', 'h2', { title: 'Customers miss the 30-day window', initiative: 'returns', falsifier: 'Most customers name the window unprompted', review_by: '2026-10-13', evidence: [{ src: 'planning/briefs/returns-survey-1.md', dir: 'refutes', w: 1, at: '2026-10-05' }, EV(1, 'supports', 1, '2026-10-06'), EV(3, 'supports', 1, '2026-10-08')] }),
|
|
29
|
+
rec('hypotheses', 'h3', { title: 'Support is a workaround, not a cost', initiative: 'returns', falsifier: 'Support hours per return exceed one', review_by: '2026-10-13', evidence: [] }),
|
|
30
|
+
rec('hypotheses', 'h4', { title: 'Slow typing causes the address drop', initiative: 'checkout', falsifier: 'Autofill users drop at the same rate', review_by: '2026-10-20', evidence: [EV(1, 'supports', 2, '2026-10-06'), EV(2, 'supports', 1, '2026-10-07')] }),
|
|
31
|
+
];
|
|
32
|
+
const SOURCES = {}; HYPS.forEach((h) => h.evidence.forEach((e) => { SOURCES[e.src] = true; }));
|
|
33
|
+
|
|
34
|
+
const DECISIONS = [
|
|
35
|
+
rec('decisions', 'd1', { case: 'Lesson', chose: 'recorded', why: 'Enterprise wants SCIM before audit logs.', lesson: 'Enterprise wants SCIM before audit logs.', tone: 'ok', initiative: 'sso' }),
|
|
36
|
+
rec('decisions', 'd2', { case: 'Lesson', chose: 'recorded', why: 'Adoption lagged because setup took a call. Guided setup is the fix.', lesson: 'Adoption lagged because setup took a call. Guided setup is the fix.', tone: 'warn', initiative: 'sso' }),
|
|
37
|
+
rec('decisions', 'd3', { case: 'Lesson', chose: 'recorded', why: 'Design partners respond within a day. Interviews fit a one-week loop.', lesson: 'Design partners respond within a day. Interviews fit a one-week loop.', tone: '', initiative: 'sso' }),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/* the mockup's RUBRIC (line 3454): [dimension, weight, score], threshold 0.70; the score is their weighted sum, as rScore() computes it */
|
|
41
|
+
const RUBRIC = [['Outcome', 0.30, 0.8], ['Quality', 0.25, 0.9], ['Adoption', 0.25, 0.6], ['Process', 0.20, 0.7]];
|
|
42
|
+
const EVALS = { sso: { run_id: 'sso-beta-run1', score: RUBRIC.reduce((a, r) => a + r[1] * r[2], 0), threshold: 0.70, verdict: 'PASS-WITH-FINDINGS', dimensions: Object.fromEntries(RUBRIC.map((r) => [r[0], r[2]])), dims: RUBRIC.map((r) => ({ dim: r[0], w: r[1], score: r[2] })), evaluator_model: 'claude-opus-5-5', recorded_at: '2026-10-07T12:00:00Z', findings: [] } };
|
|
43
|
+
const CORR = { runs: 600, corrections: 111, series: [22, 21, 19, 18, 16, 15].map((c, i) => ({ from: '2026-09-' + String(1 + i * 7).padStart(2, '0'), runs: 100, corrections: c })) };
|
|
44
|
+
|
|
45
|
+
/* the mockup's GSTATE (line 3453): 1 done, .5 the gate in progress, 0 not yet */
|
|
46
|
+
const GSTATE = { checkout: [1, 1, 0.5, 0, 0, 0], sso: [1, 1, 1, 1, 0.5, 0], search: [1, 1, 0.5, 0, 0, 0], returns: [0.5, 0, 0, 0, 0, 0], onb: [0.5, 0, 0, 0, 0, 0] };
|
|
47
|
+
const KEYS = ['spec', 'contract', 'build', 'quality', 'review', 'accept'];
|
|
48
|
+
const DAYS = ['2026-09-14', '2026-09-16', '2026-09-25', '2026-10-02', '2026-10-07', '2026-10-08'];
|
|
49
|
+
const WHAT = { spec: 'the PRD', contract: 'the rubric and the agreed done criteria', build: 'the build', quality: 'compile, lint and tests', review: 'the rubric review', accept: 'your acceptance' };
|
|
50
|
+
function ladder(id) {
|
|
51
|
+
const g = GSTATE[id] || GSTATE.checkout;
|
|
52
|
+
const steps = KEYS.map((key, i) => ({ key, label: key, status: g[i] === 1 ? 'done' : 'pending', evidence: g[i] === 1 ? WHAT[key] + ' exists' : g[i] > 0 ? WHAT[key] + ' is under way' : 'not started', next: null, blocking: false, basis: { rule: 'The ' + key + ' gate', file: '', source: 'file', first_seen: g[i] === 1 ? DAYS[i] : null } }));
|
|
53
|
+
const nx = steps.find((s) => s.status !== 'done');
|
|
54
|
+
return { ok: true, name: 'ladder', empty: false, row: { ok: true, id, steps, blockers: [], gate_failures: [], next: nx ? { step: nx.key, why: nx.evidence, action: 'finish ' + WHAT[nx.key] } : null } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const prev = window.fetch.bind(window);
|
|
58
|
+
window.fetch = (input, init) => {
|
|
59
|
+
try {
|
|
60
|
+
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
61
|
+
const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
62
|
+
const u = new URL(raw, location.origin);
|
|
63
|
+
if (method === 'GET' && u.origin === location.origin) {
|
|
64
|
+
const p = u.pathname;
|
|
65
|
+
if (p === '/api/records/ideas') return json({ ok: true, kind: 'ideas', records: IDEAS });
|
|
66
|
+
if (p === '/api/records/hypotheses') return json({ ok: true, kind: 'hypotheses', records: HYPS });
|
|
67
|
+
if (p === '/api/records/decisions') return json({ ok: true, kind: 'decisions', records: DECISIONS });
|
|
68
|
+
if (p === '/api/c3-life') return json({ ok: true, evals: EVALS, corrections: CORR, capacity: { points: 14, basis: 'Team of 4 across 2 sprints' }, sources: SOURCES, interviews: [['Tue 6', 'done'], ['Wed 7', 'done'], ['Thu 8', 'now'], ['Mon 12', 'next'], ['Tue 13', 'next']].map(([label, state]) => ({ label, state })), as_of: '2026-10-08T08:12:00Z' });
|
|
69
|
+
if (p === '/api/console') {
|
|
70
|
+
const name = u.searchParams.get('name');
|
|
71
|
+
if (name === 'ladder') return json(ladder(u.searchParams.get('id') || 'checkout'));
|
|
72
|
+
if (name === 'radar') return json({ ok: true, name: 'radar', empty: false, source: 'file-mode', data: { calibration: null, watermelons: [], flags: 0 } });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch { /* an unreadable request goes on as it is */ }
|
|
76
|
+
return prev(input, init);
|
|
77
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* pm/c3-demo-misc2.js — DEMO ONLY (/c3.html?demo=1): the sample content for three panels the content audit found missing (planning/research/console3-content-parity.md), in the REAL
|
|
2
|
+
* response shapes. Real mode never loads this file. installMisc2() is called from installDemo() in c3-demo.js, after the briefing fixtures, and wraps window.fetch once:
|
|
3
|
+
* GET /api/briefing (the Briefing's own scope) → adds `stop`: [{ icon, text }], the agent's proposals of what to stop (c3-misc-panels.js stopCard shows them in the Reflect lens only).
|
|
4
|
+
* The real route sends no `stop`, so a real Reflect lens draws no such panel.
|
|
5
|
+
* GET /api/pm-items → lists.today is the mockup's Today (console3.html wItems, line 3215): the work not yet done that is due today, and the inbox items
|
|
6
|
+
* due today. Those are the same item objects as in lists.triage, which is how the mockup's one item list shows them in both lenses.
|
|
7
|
+
* This puts "Say yes to Sam", which has no job, in Today, so the grouped list ends with "No job yet".
|
|
8
|
+
* window.__c3repix.INM → the initiative names the Project report's forecast card uses (mockup INM, line 3438).
|
|
9
|
+
* The Project forecast chart itself needs no fixture here: it reads /api/c3-timeline, which c3-demo-time.js answers. */
|
|
10
|
+
|
|
11
|
+
import { pmItems } from './c3-demo-data.js';
|
|
12
|
+
|
|
13
|
+
/* The fixture's work set for today also holds five Review drafts the mockup does not list in Today. The Work view reads /api/pm-items before the fetch wrapper below exists,
|
|
14
|
+
* so the array (the very one pmItems() returns) is cut in place, once, when this file loads. The inbox items due today join Today in pm-work-view.js. */
|
|
15
|
+
{ const T = pmItems().lists.today; const keep = T.filter((x) => /^Cut Checkout v2/.test(x.title)); T.splice(0, T.length, ...keep); }
|
|
16
|
+
|
|
17
|
+
const STOP = [
|
|
18
|
+
{ icon: 'cal', text: 'Skip Sprint planning, send the decision in writing' },
|
|
19
|
+
{ icon: 'user', text: 'Let Marta pick the interview slot' },
|
|
20
|
+
{ icon: 'check', text: 'Auto-apply duplicate closes after 2 clean weeks' },
|
|
21
|
+
];
|
|
22
|
+
let done = false;
|
|
23
|
+
export function installMisc2() {
|
|
24
|
+
if (done) return; done = true;
|
|
25
|
+
if (window.__c3repix) window.__c3repix.INM = { checkout: 'Checkout v2', returns: 'Returns experience', sso: 'SSO', search: 'Search relevance', onb: 'Self-serve onboarding' };
|
|
26
|
+
const prev = window.fetch.bind(window);
|
|
27
|
+
window.fetch = async (input, init) => {
|
|
28
|
+
const res = await prev(input, init);
|
|
29
|
+
try {
|
|
30
|
+
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
31
|
+
const u = new URL(raw, location.origin);
|
|
32
|
+
const method = String((init && init.method) || 'GET').toUpperCase();
|
|
33
|
+
if (u.origin !== location.origin || method !== 'GET') return res;
|
|
34
|
+
if (u.pathname === '/api/briefing' && (!u.searchParams.get('scope') || u.searchParams.get('scope') === 'brief')) {
|
|
35
|
+
const j = await res.clone().json();
|
|
36
|
+
if (j && j.ok !== false) return new Response(JSON.stringify({ ...j, stop: STOP }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
37
|
+
} else if (u.pathname === '/api/pm-items') {
|
|
38
|
+
const j = await res.clone().json();
|
|
39
|
+
if (j && j.lists) {
|
|
40
|
+
return new Response(JSON.stringify({ ...j, lens_text: { ...(j.lens_text || {}), today: ['6 committed today, 1 overdue', 'Best focus window is 11:15. 2 items wait on others.'] } }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch { /* an unreadable answer goes through as it is */ }
|
|
44
|
+
return res;
|
|
45
|
+
};
|
|
46
|
+
}
|