create-pmos 0.124.0 → 0.126.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.
Files changed (129) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/briefing-cards.md +40 -0
  15. package/workspace/guide/index.md +1 -0
  16. package/workspace/guide/layout.md +2 -0
  17. package/workspace/lib/engine/nine-stages.js +81 -0
  18. package/workspace/lib/engine/workflow-state.js +2 -0
  19. package/workspace/lib/facts-async.js +51 -0
  20. package/workspace/lib/item-steps.js +52 -0
  21. package/workspace/lib/jira-status.js +8 -3
  22. package/workspace/lib/jobs-to-be-done.js +52 -0
  23. package/workspace/lib/needs-overdue.js +41 -0
  24. package/workspace/lib/outbox-store.js +87 -0
  25. package/workspace/lib/pm-items.js +115 -0
  26. package/workspace/lib/route-modules.js +21 -0
  27. package/workspace/lib/routes-briefing.js +109 -0
  28. package/workspace/lib/routes-c3-init.js +53 -0
  29. package/workspace/lib/routes-c3-life.js +119 -0
  30. package/workspace/lib/routes-c3-strategy.js +169 -0
  31. package/workspace/lib/routes-c3-timeline.js +191 -0
  32. package/workspace/lib/routes-calendar-import.js +60 -0
  33. package/workspace/lib/routes-calendar-views.js +61 -0
  34. package/workspace/lib/routes-doc-health.js +79 -0
  35. package/workspace/lib/routes-doc-sheet.js +169 -0
  36. package/workspace/lib/routes-forecast.js +85 -0
  37. package/workspace/lib/routes-hooks.js +121 -0
  38. package/workspace/lib/routes-initiatives.js +3 -0
  39. package/workspace/lib/routes-intake.js +24 -0
  40. package/workspace/lib/routes-outbox.js +97 -0
  41. package/workspace/lib/routes-pm-items.js +216 -0
  42. package/workspace/lib/routes-records.js +61 -0
  43. package/workspace/lib/routes-releases.js +129 -0
  44. package/workspace/lib/routes-report-facts.js +84 -0
  45. package/workspace/lib/routes-report-families.js +32 -0
  46. package/workspace/lib/routes-roadmap-ops.js +141 -0
  47. package/workspace/lib/routes-runs.js +119 -0
  48. package/workspace/lib/routes-signals.js +50 -0
  49. package/workspace/lib/run-answers.js +69 -0
  50. package/workspace/package-lock.json +2 -2
  51. package/workspace/package.json +1 -1
  52. package/workspace/public/c3.html +26 -0
  53. package/workspace/public/pm/briefing-deck.js +413 -0
  54. package/workspace/public/pm/briefing-model.js +211 -0
  55. package/workspace/public/pm/c3-breakdown.js +240 -0
  56. package/workspace/public/pm/c3-brief.js +278 -0
  57. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  58. package/workspace/public/pm/c3-case.js +185 -0
  59. package/workspace/public/pm/c3-daypart.js +176 -0
  60. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  61. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  62. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  63. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  64. package/workspace/public/pm/c3-demo-data.js +560 -0
  65. package/workspace/public/pm/c3-demo-init.js +373 -0
  66. package/workspace/public/pm/c3-demo-life.js +77 -0
  67. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  68. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  69. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  70. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  71. package/workspace/public/pm/c3-demo-term.js +153 -0
  72. package/workspace/public/pm/c3-demo-time.js +49 -0
  73. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  74. package/workspace/public/pm/c3-demo-work2.js +13 -0
  75. package/workspace/public/pm/c3-demo.js +113 -0
  76. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  77. package/workspace/public/pm/c3-dock.js +180 -0
  78. package/workspace/public/pm/c3-docs.js +290 -0
  79. package/workspace/public/pm/c3-expand.js +96 -0
  80. package/workspace/public/pm/c3-init-extras.js +170 -0
  81. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  82. package/workspace/public/pm/c3-misc-panels.js +89 -0
  83. package/workspace/public/pm/c3-plan-panels.js +163 -0
  84. package/workspace/public/pm/c3-plan.js +192 -0
  85. package/workspace/public/pm/c3-roadmap.js +249 -0
  86. package/workspace/public/pm/c3-sheet.js +107 -0
  87. package/workspace/public/pm/c3-shell.js +674 -0
  88. package/workspace/public/pm/c3-signals.js +196 -0
  89. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  90. package/workspace/public/pm/c3-swipe.js +230 -0
  91. package/workspace/public/pm/c3-terminal-data.js +26 -0
  92. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  93. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  94. package/workspace/public/pm/c3-terminal-results.js +141 -0
  95. package/workspace/public/pm/c3-terminal.js +52 -0
  96. package/workspace/public/pm/c3-tickets.js +163 -0
  97. package/workspace/public/pm/c3-timeline.js +201 -0
  98. package/workspace/public/pm/c3-toast.js +16 -0
  99. package/workspace/public/pm/calendar-views.js +1014 -0
  100. package/workspace/public/pm/console3-lift.md +220 -0
  101. package/workspace/public/pm/console3.css +1547 -0
  102. package/workspace/public/pm/dest-initiatives.js +16 -3
  103. package/workspace/public/pm/dest-mywork.js +8 -1
  104. package/workspace/public/pm/dest-reports.js +47 -1
  105. package/workspace/public/pm/dest-today.js +55 -1
  106. package/workspace/public/pm/doc-health-panel.js +110 -0
  107. package/workspace/public/pm/doc-rail.js +9 -3
  108. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  109. package/workspace/public/pm/lifecycle-lens.js +261 -0
  110. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  111. package/workspace/public/pm/outbox-panel.js +125 -0
  112. package/workspace/public/pm/pm-work-view.js +895 -0
  113. package/workspace/public/pm/releases-view.js +248 -0
  114. package/workspace/public/pm/report-families-view.js +503 -0
  115. package/workspace/public/pm/run-card.js +244 -0
  116. package/workspace/public/pm/sessions-panel.js +149 -0
  117. package/workspace/public/pm/shell-bell.js +162 -0
  118. package/workspace/public/pm/shell-model.js +83 -0
  119. package/workspace/public/pm/shell-phone.js +124 -0
  120. package/workspace/public/pm/shell-readaloud.js +53 -0
  121. package/workspace/public/pm/shell-recording.js +110 -0
  122. package/workspace/public/pm/shell-ticker.js +90 -0
  123. package/workspace/public/pm/slide-confirm.js +163 -0
  124. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  125. package/workspace/public/pm/today-day.js +8 -2
  126. package/workspace/public/pm/tokens.css +1 -0
  127. package/workspace/server.js +5 -2
  128. package/workspace/test-files.json +21 -3
  129. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -0,0 +1,476 @@
1
+ /* pm/c3-lifecycle-panels.js — the panels the Lifecycle lens draws for each stage, as the console3 mockup draws them (console3.html: iniInfo 3577, iniLeft 3582, iniRight 3592,
2
+ * ideaRow 3563, hypRow 3565, gateCard 3573, evMatrix 3572, scoreCard 3574, lessonsCard 3575, scatter 3455, hRubric 3487, c3needs 4713, the Build lens 4760 to 4768).
3
+ *
4
+ * lensPanels(V, lens, scope) -> { head: { icon, h, s, ring } | null, left: [nodes], after: [nodes], right: [nodes] }
5
+ * left goes under the initiative's lifecycle line and above "What <stage> needs"; after goes below it (the Build lens); right is the second column.
6
+ * ensureLife(V) starts the reads once and draws again when they land. V.life holds what came back.
7
+ *
8
+ * DATA, all real, all read-only until a PM press:
9
+ * ideas, hypotheses, decisions GET /api/records/<kind> (scripts/records; add forms POST /api/records/<kind>/add through pmPost)
10
+ * gates GET /api/console?name=ladder&id= (the engine's steps; one initiative per call, so only the ones shown are asked for)
11
+ * evaluations, corrections GET /api/c3-life (workspace/lib/routes-c3-life.js)
12
+ * calibration GET /api/console?name=radar (the evaluator's kappa against PM labels, D40)
13
+ * capacity, work items, tickets GET /api/c3-init/roadmap, /api/c3-init/breakdown, /api/forecast
14
+ * Where a record does not exist the panel says what is missing and how to add it. No number is invented: an idea with a missing figure has no score, a gate the engine does not
15
+ * claim is drawn as unknown, a rubric with no recorded weights is unweighted. The maths is lifecycle-lens-model.js (ports of scripts/trust/reader.js 11 to 16).
16
+ * The demo (/c3.html?demo=1) answers the same routes from c3-demo-life.js, in the same shapes. Built with el(); every string from data is a text node. */
17
+ import { el } from './dom.js';
18
+ import { pmGet, pmPost } from './http.js';
19
+ import { c3ic } from './pm-work-view.js';
20
+ import { openSheet } from './c3-sheet.js';
21
+ import { renderCase } from './c3-case.js';
22
+ import { card, hBars, rowShell, caseNode, markDone, say, short, dayLabel } from './c3-init-extras.js';
23
+ import { progressCard } from './c3-plan-panels.js';
24
+ import { releasePanels } from './releases-view.js';
25
+ import { ideaScore, normIdea, rankIdeas, capacityUsed, normHyp, hypStatus, HYP_LABEL, GATES, gateLadder, gateCell, normEval, evalVerdict, correctionRate, nineOf, stageBucket } from './lifecycle-lens-model.js';
26
+
27
+ const isDemo = () => !!window.__c3demo;
28
+ const SVGNS = 'http://www.w3.org/2000/svg';
29
+ const sv = (tag, attrs, ...kids) => { const n = document.createElementNS(SVGNS, tag); for (const k of Object.keys(attrs || {})) if (attrs[k] != null) n.setAttribute(k, attrs[k]); for (const c of kids) if (c) n.appendChild(c); return n; };
30
+ const svt = (x, y, t, a) => { const n = sv('text', { x, y, ...(a || {}) }); n.textContent = t; return n; };
31
+ const ST_COLOR = { new: ['var(--acc)', 'new'], short: ['var(--acc)', 'shortlisted'], committed: ['var(--success)', 'committed'], parked: ['var(--muted-foreground)', 'parked'] };
32
+ const HYP_COLOR = { supported: 'var(--success)', mixed: 'var(--warning)', untested: 'var(--ring)', refuted: 'var(--destructive)', open: 'var(--ring)' };
33
+ const DIM = 'text-transform:none;letter-spacing:0';
34
+ const pct = (n) => Math.round(n * 100);
35
+
36
+ /* ── the reads ── */
37
+ const rdOk = (r) => !!(r && r.ok);
38
+ const nameOf = (V, id) => { const r = (V.ctx.rows || []).find((x) => x.id === id); return r ? r.title : id || ''; };
39
+
40
+ /** Start the reads once. V.life: { state: 'idle'|'loading'|'ready', ideas, hyps, decisions, evals, corr, sources, calib, capacity, items, ladders, tickets, err }. */
41
+ export function ensureLife(V) {
42
+ const L = V.life || (V.life = { state: 'idle', st: {}, ladders: {}, tickets: {}, err: {} });
43
+ if (L.state === 'idle') { L.state = 'loading'; load(V, L); }
44
+ return L;
45
+ }
46
+ export function reloadLife(V) { if (V.life) V.life.state = 'idle'; if (!V.disposed) V.render(); }
47
+
48
+ async function load(V, L) {
49
+ const ws = V.ctx.ws ? '&ws=' + encodeURIComponent(V.ctx.ws) : '';
50
+ const [ideas, hyps, decs, life, radar, rm, bd] = await Promise.all([
51
+ pmGet('/api/records/ideas'), pmGet('/api/records/hypotheses'), pmGet('/api/records/decisions'), pmGet('/api/c3-life'),
52
+ pmGet('/api/console?name=radar' + ws), pmGet('/api/c3-init/roadmap'), pmGet('/api/c3-init/breakdown'),
53
+ ]);
54
+ if (V.disposed) return;
55
+ L.err = {};
56
+ L.ideas = rdOk(ideas) ? (ideas.records || []).map(normIdea) : null; if (!rdOk(ideas)) L.err.ideas = (ideas && ideas.error) || 'The ideas could not be read.';
57
+ L.hyps = rdOk(hyps) ? (hyps.records || []).map(normHyp) : null; if (!rdOk(hyps)) L.err.hyps = (hyps && hyps.error) || 'The hypotheses could not be read.';
58
+ L.decisions = rdOk(decs) ? decs.records || [] : null;
59
+ L.life = rdOk(life) ? life : null; if (!L.life) L.err.life = 'The evaluations and the corrections are not served by this server yet (GET /api/c3-life). Restart the workspace to load the route.';
60
+ L.calib = rdOk(radar) && radar.data ? { ...(radar.data.calibration || {}), source: radar.source || '' } : null; if (L.calib && !radar.data.calibration) L.calib = { none: true, source: radar.source || '' };
61
+ L.bets = rdOk(rm) ? rm.bets || [] : null;
62
+ L.capacity = rdOk(rm) ? rm.capacity || null : null; L.people = rdOk(rm) ? rm.people || [] : [];
63
+ L.items = rdOk(bd) ? bd.items || [] : null;
64
+ L.state = 'ready';
65
+ V.render();
66
+ }
67
+
68
+ /** One initiative's ladder (GET /api/console?name=ladder). Asked for once, only for the initiatives a panel shows. */
69
+ function ensureLadder(V, id) {
70
+ const L = V.life; const cur = L.ladders[id];
71
+ if (cur) return cur;
72
+ const ws = V.ctx.ws ? '&ws=' + encodeURIComponent(V.ctx.ws) : '';
73
+ const slot = L.ladders[id] = { state: 'loading', row: null };
74
+ pmGet('/api/console?name=ladder&id=' + encodeURIComponent(id) + ws).then((r) => {
75
+ if (V.disposed) return;
76
+ slot.state = rdOk(r) && !r.empty && r.row ? 'ready' : 'none'; slot.row = rdOk(r) && r.row ? r.row : null; slot.err = rdOk(r) ? '' : (r && r.error) || 'The ladder could not be read.';
77
+ V.render();
78
+ });
79
+ return slot;
80
+ }
81
+ /** Jira-side tickets of one initiative (GET /api/forecast?initiative=): the Build lens. */
82
+ function ensureTickets(V, id) {
83
+ const L = V.life; if (L.tickets[id]) return L.tickets[id];
84
+ const slot = L.tickets[id] = { state: 'loading', items: [] };
85
+ pmGet('/api/forecast?initiative=' + encodeURIComponent(id)).then((r) => {
86
+ if (V.disposed) return;
87
+ slot.state = rdOk(r) ? 'ready' : 'none'; slot.items = rdOk(r) ? r.items || [] : []; slot.err = rdOk(r) ? '' : (r && r.error) || '';
88
+ V.render();
89
+ });
90
+ return slot;
91
+ }
92
+
93
+ /* ── pieces ── */
94
+ const inScope = (scope) => (x) => !scope || x.init === scope;
95
+ const loading = (what) => el('li', { class: 'fold', 'data-life-loading': '' }, c3ic('clock', 'sm'), 'Reading ' + what + '…');
96
+ const note = (icon, text, attr) => el('li', { class: 'fold', ...(attr || {}) }, c3ic(icon, 'sm'), text);
97
+ const chip2 = (icon, text, color, title) => el('span', { class: 'chip2', style: color ? 'color:' + color : null, title: title || null }, icon ? c3ic(icon, 'sm') : null, text);
98
+ const section = (icon, title, count, sub, rows, attrs) => el('section', { class: 'card sect', ...(attrs || {}) },
99
+ el('div', { class: 'sh2' }, el('h2', null, c3ic(icon, 'sm'), title, count == null ? null : [' ', el('span', { class: 'bdg g' }, String(count))]), sub ? el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, sub) : null),
100
+ el('ul', { class: 'list' }, rows));
101
+
102
+ /** The mockup's hNum: a number, a delta line and the trend against a target. arr = the series, target = the line. */
103
+ function numCard(big, unit, delta, tone, arr, target) {
104
+ const W = 340; const H = 60; const all = target == null ? arr : arr.concat([target]); const mn = Math.min(...all); const mx = Math.max(...all);
105
+ const X = (i) => 4 + i * (W - 8) / Math.max(1, arr.length - 1); const Y = (v) => H - 6 - (v - mn) / (mx - mn || 1) * (H - 26);
106
+ const p = arr.map((v, i) => X(i) + ',' + Y(v)).join(' ');
107
+ const s = sv('svg', { viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Trend: ' + arr.join(', ') });
108
+ if (target != null) s.append(sv('line', { x1: 4, x2: W - 4, y1: Y(target), y2: Y(target), stroke: 'var(--ring)', 'stroke-dasharray': '4 4' }), svt(W - 4, Y(target) - 4, 'target ' + target + unit, { 'text-anchor': 'end' }));
109
+ s.append(sv('polygon', { points: `4,${H - 4} ${p} ${W - 4},${H - 4}`, fill: tone, 'fill-opacity': '.1' }), sv('polyline', { class: 'draw', pathLength: 1, points: p, fill: 'none', stroke: tone, 'stroke-width': 3, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }),
110
+ sv('circle', { cx: X(arr.length - 1), cy: Y(arr[arr.length - 1]), r: 4.5, fill: tone, stroke: 'var(--card)', 'stroke-width': 2 }));
111
+ return el('div', { class: 'hn' }, el('div', { class: 'a' }, el('b', null, big), el('span', null, unit)), el('div', { class: 'd', style: 'color:' + tone }, delta), s);
112
+ }
113
+
114
+ /** The mockup's hSegs: a row of coloured blocks with a value and a label. a = [[label, value, colour]]. */
115
+ function segs(a, unit) {
116
+ const W = 340; const H = 84; const tot = a.reduce((t, r) => t + r[1], 0) || 1; let x = 0;
117
+ const s = sv('svg', { viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': a.map((r) => r[0] + ' ' + r[1]).join(', ') });
118
+ a.forEach((r, i) => { const w = (W - (a.length - 1) * 4) * (r[1] / tot);
119
+ s.append(sv('rect', { class: 'grow', style: 'animation-delay:' + (i * 90) + 'ms', x, y: 20, width: w, height: 30, rx: 9, fill: r[2], opacity: '.92' }), svt(x + w / 2, 39, r[1] + (unit || ''), { 'text-anchor': 'middle', style: 'fill:#fff;font-weight:500' }), svt(x + w / 2, 68, short(r[0], Math.max(5, Math.floor(w / 6.5))), { 'text-anchor': 'middle' }));
120
+ x += w + 4; });
121
+ s.append(svt(0, 12, 'total ' + tot + (unit || ''), {}));
122
+ return s;
123
+ }
124
+
125
+ /** The mockup's scatter (line 3455): impact up, effort across, a bubble per idea sized by confidence. Pressing a dot opens that idea's row. */
126
+ function scatter(V, list) {
127
+ const W = 560; const H = 250; const x0 = 40; const x1 = W - 16; const y0 = 14; const y1 = H - 34;
128
+ const X = (v) => x0 + (v - 1) / 9 * (x1 - x0); const Y = (v) => y1 - (v - 1) / 9 * (y1 - y0);
129
+ const s = sv('svg', { class: 'sct', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Ideas by impact and effort' });
130
+ s.append(sv('rect', { x: x0, y: y0, width: X(5.5) - x0, height: Y(5.5) - y0, fill: 'var(--success)', 'fill-opacity': '.08' }),
131
+ svt(x0 + 6, y0 + 14, 'quick wins'), svt(x1 - 6, y0 + 14, 'big bets', { 'text-anchor': 'end' }), svt(x0 + 6, y1 - 8, 'fill-ins'), svt(x1 - 6, y1 - 8, 'money pits', { 'text-anchor': 'end' }),
132
+ sv('line', { x1: X(5.5), x2: X(5.5), y1: y0, y2: y1, stroke: 'var(--border)' }), sv('line', { x1: x0, x2: x1, y1: Y(5.5), y2: Y(5.5), stroke: 'var(--border)' }),
133
+ sv('line', { x1: x0, x2: x1, y1: y1, y2: y1, stroke: 'var(--ring)' }), sv('line', { x1: x0, x2: x0, y1: y0, y2: y1, stroke: 'var(--ring)' }),
134
+ svt((x0 + x1) / 2, H - 8, 'effort, low to high', { 'text-anchor': 'middle' }), svt(12, (y0 + y1) / 2, 'impact', { transform: `rotate(-90 12 ${(y0 + y1) / 2})`, 'text-anchor': 'middle' }));
135
+ list.filter((x) => ideaScore(x) != null).forEach((x, i) => {
136
+ const c = x.st === 'parked' ? 'var(--ring)' : x.st === 'committed' ? 'var(--success)' : x.st === 'short' ? 'var(--acc)' : x.src === 'agent' ? 'var(--purple)' : 'var(--muted-foreground)';
137
+ const t = sv('title'); t.textContent = x.n + ': impact ' + x.imp + ', effort ' + x.eff + ', confidence ' + pct(x.conf) + '%';
138
+ const dot = sv('circle', { class: 'pop', style: 'animation-delay:' + (i * 50) + 'ms', cx: X(Math.min(10, Math.max(1, x.eff))), cy: Y(Math.min(10, Math.max(1, x.imp))), r: 7 + x.conf * 9, fill: c, 'fill-opacity': '.65', stroke: 'var(--card)', 'stroke-width': 2, 'data-idea-dot': x.id }, t); // a convenience as in the mockup, not a control: the idea's row is the control (44px, keys, swipe)
139
+ const go = () => { V.open = V.open || {}; V.open['idea' + x.id] = true; V.render(); const row = V.root.querySelector('[data-idea="' + (window.CSS && CSS.escape ? CSS.escape(x.id) : x.id) + '"]'); if (row && row.scrollIntoView) row.scrollIntoView({ block: 'center' }); };
140
+ dot.addEventListener('click', go);
141
+ s.append(dot);
142
+ });
143
+ return el('div', null, s, el('div', { class: 'pgl' }, el('span', { style: 'color:var(--acc)' }, '● shortlisted'), el('span', { style: 'color:var(--success)' }, '● committed'), el('span', { style: 'color:var(--purple)' }, '● from the agent'), el('span', null, '● yours or customers\''), el('span', null, 'bubble size = confidence')));
144
+ }
145
+
146
+ /* ── writes: a record edit through pmPost, with the page's undo ── */
147
+ async function patch(V, kind, rid, fields, why) {
148
+ const r = await pmPost('/api/records/' + kind + '/edit', { rid, fields, why });
149
+ return r && r.ok ? { ok: true } : { ok: false, error: (r && r.error) || 'Not recorded.' };
150
+ }
151
+ /** Set an idea's status. The page shows it at once (V.life.st); a real record is read again after the write; the demo keeps the local one. */
152
+ async function setIdeaStatus(V, x, status, msg) {
153
+ const L = V.life; const prev = L.st[x.id] || x.st;
154
+ const r = await patch(V, 'ideas', x.id, { status }, 'Set from the lifecycle lens');
155
+ if (!r.ok) { say(r.error); return false; }
156
+ L.st[x.id] = status; if (!isDemo()) { x.st = status; }
157
+ const undo = async () => { const u = await patch(V, 'ideas', x.id, { status: prev }, 'Undone from the lifecycle lens'); if (u.ok) { L.st[x.id] = prev; if (!isDemo()) x.st = prev; V.render(); } };
158
+ say(msg, undo); V.render();
159
+ return undo;
160
+ }
161
+ const withSt = (V, list) => list.map((x) => (V.life.st[x.id] ? { ...x, st: V.life.st[x.id] } : x));
162
+
163
+ /* ── the add forms of the empty states: one record through pmPost ── */
164
+ function field(label, input) { return el('label', null, label, input); }
165
+ function addForm(V, kind, spec) {
166
+ const open = !!(V.addOpen && V.addOpen[kind]);
167
+ const err = el('div', { class: 'err', role: 'alert' });
168
+ const inputs = {};
169
+ const mk = (name, type, attrs) => { const i = el('input', { type, name, 'aria-label': name, ...(attrs || {}) }); inputs[name] = i; return i; };
170
+ const rowsIni = (V.ctx.rows || []).slice(0, 12);
171
+ const sel = el('select', { name: 'initiative', 'aria-label': 'initiative' }, el('option', { value: '' }, 'No initiative'), ...rowsIni.map((r) => el('option', { value: r.id }, short(r.title, 40))));
172
+ const body = spec.fields(mk, field);
173
+ const form = el('form', { class: 'lfm', 'data-life-form': kind, onSubmit: async (e) => {
174
+ e.preventDefault(); err.textContent = '';
175
+ const f = spec.collect(inputs); if (f.error) { err.textContent = f.error; return; }
176
+ if (sel.value) f.fields.initiative = sel.value;
177
+ const r = await pmPost('/api/records/' + kind + '/add', { fields: f.fields, why: 'Added from the lifecycle lens' });
178
+ if (!r || !r.ok) { err.textContent = (r && r.error) || 'Not recorded.'; return; }
179
+ say(isDemo() ? spec.done + ' (demo: nothing is saved)' : spec.done);
180
+ if (isDemo()) { V.life[spec.key] = (V.life[spec.key] || []).concat([spec.local({ ...f.fields, rid: 'demo-' + Date.now() })]); V.addOpen[kind] = false; V.render(); } else { V.addOpen[kind] = false; reloadLife(V); }
181
+ } }, ...body, field('Initiative', sel), err, el('button', { type: 'submit', class: 'pill', style: 'min-height:44px' }, spec.button));
182
+ V.addOpen = V.addOpen || {};
183
+ return el('li', { class: 'fold', 'data-life-add': kind, style: 'display:grid;gap:8px' },
184
+ el('div', { style: 'display:flex;gap:8px;align-items:center;flex-wrap:wrap' }, c3ic('plus', 'sm'), el('button', { type: 'button', class: 'pill', style: 'min-height:44px', 'aria-expanded': String(open), 'data-life-add-btn': kind, onClick: () => { V.addOpen[kind] = !V.addOpen[kind]; V.render(); } }, spec.open), el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, spec.how)),
185
+ open ? form : null);
186
+ }
187
+ const num = (v) => (v === '' || v == null ? NaN : Number(v));
188
+ const IDEA_FORM = { key: 'ideas', open: 'Add an idea', button: 'Save the idea', done: 'Idea added.', how: 'Needs a title, impact, effort and confidence. It is written to planning/records/ideas.jsonl.', local: normIdea,
189
+ fields: (mk, f) => [f('Title', mk('title', 'text', { required: '', placeholder: 'What would we do?' })), el('div', { class: 'row2' }, f('Impact (1 to 10)', mk('impact', 'number', { min: 1, max: 10, step: 'any', required: '' })), f('Effort (above 0)', mk('effort', 'number', { min: 0.1, step: 'any', required: '' })), f('Confidence (0 to 1)', mk('confidence', 'number', { min: 0, max: 1, step: 'any', required: '' })))],
190
+ collect: (i) => { const t = i.title.value.trim(); const impact = num(i.impact.value); const effort = num(i.effort.value); const confidence = num(i.confidence.value);
191
+ if (!t) return { error: 'A title is needed.' }; if (!Number.isFinite(impact) || !Number.isFinite(effort) || effort <= 0 || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) return { error: 'Impact, effort (above 0) and confidence (0 to 1) must be numbers.' };
192
+ return { fields: { title: t, impact, effort, confidence, status: 'new', source: 'you' } }; } };
193
+ const HYP_FORM = { key: 'hyps', open: 'Add a hypothesis', button: 'Save the hypothesis', done: 'Hypothesis added.', how: 'Needs a statement, what would prove it wrong, and a review date. Evidence is added to the record as it comes in.', local: normHyp,
194
+ fields: (mk, f) => [f('Statement', mk('title', 'text', { required: '' })), f('What result would prove it wrong?', mk('falsifier', 'text', { required: '' })), f('Review by', mk('review_by', 'date', { required: '' }))],
195
+ collect: (i) => { const t = i.title.value.trim(); const fa = i.falsifier.value.trim(); const d = i.review_by.value;
196
+ if (!t || !fa || !/^\d{4}-\d{2}-\d{2}$/.test(d)) return { error: 'A statement, a falsifier and a review date are needed.' };
197
+ return { fields: { title: t, falsifier: fa, review_by: d, evidence: [] } }; } };
198
+ const LESSON_FORM = { key: 'decisions', open: 'Add a lesson', button: 'Save the lesson', done: 'Lesson added.', how: 'Written to planning/records/decisions.jsonl as a decision with a lesson.', local: (f) => ({ ...f }),
199
+ fields: (mk, f) => [f('What we learned', mk('lesson', 'text', { required: '' }))],
200
+ collect: (i) => { const t = i.lesson.value.trim(); if (!t) return { error: 'Say what was learned.' }; return { fields: { case: 'Lesson', chose: 'recorded', why: t, lesson: t, tone: '' } }; } };
201
+
202
+ /* ── DISCOVER: Hypotheses, the evidence matrix, Interviews ── */
203
+ const dayName = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); if (!m) return ''; const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])); return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getUTCDay()] + ' ' + (+m[3]); };
204
+ const baseName = (s) => String(s || '').split('/').pop().replace(/\.[a-z]+$/i, '');
205
+
206
+ function hypCase(V, h, hs) {
207
+ const n = hs.supports; const tot = h.ev.length;
208
+ const chg = [['notes', n + ' supporting source' + (n === 1 ? '' : 's') + (hs.refutes ? ', ' + hs.refutes + ' against' : ''), 'sources', n ? 'ok' : ''], ['target', 'Status: ' + HYP_LABEL[hs.shown], 'now'], ['alert', 'Falsifier: ' + (h.falsifier || 'none recorded'), 'falsifier', h.falsifier ? '' : 'bad']];
209
+ if (hs.untraced.length) chg.push(['alert', 'Untraced: ' + hs.untraced.slice(0, 3).join(', ') + (hs.untraced.length > 3 ? ' and ' + (hs.untraced.length - 3) + ' more' : '') + '. These sources are not files in the repository.', 'untraced', 'warn']);
210
+ if (h.review_by) chg.push(['clock', 'Review by ' + dayLabel(h.review_by), 'review']);
211
+ return { sit: 'Evidence so far: ' + n + ' of ' + tot + ' sources support it.' + (hs.status !== hs.shown && hs.status === 'open' ? ' The bar for supported is a net weight of 3.' : ''), conf: hs.shown === 'untested' ? 60 : 80, chg, cta: 'Slide to confirm',
212
+ opts: [{ id: 't', l: 'Add a question to the next interview', rec: hs.shown === 'untested', outward: !isDemo(), fx: [['check', 'Tests it this week', 'ok']] },
213
+ { id: 's', l: 'Mark it supported', fx: [['check', 'Recorded as your call; the derived status stays as the evidence says', 'ok']] },
214
+ { id: 'r', l: 'Mark it refuted', fx: [['x', 'Closes the question', 'warn']] }],
215
+ outboxReason: 'Adding a question goes to the interview guide, and this page has no route to the calendar yet. Nothing is written.' };
216
+ }
217
+ function hypRow(V, h, known, ncols) {
218
+ const hs = hypStatus(h, known); const col = HYP_COLOR[hs.shown]; const key = 'hyp' + h.id; const call = (V.life.call || {})[h.id];
219
+ const decide = async (o) => {
220
+ if (o.id === 't') { say('Question added to the next interview guide.'); markDone(V, 'hyp:' + h.id, 'Question added.'); return; }
221
+ const v = o.id === 's' ? 'supported' : 'refuted';
222
+ const r = await patch(V, 'hypotheses', h.id, { pm_call: v }, 'Marked ' + v + ' from the lifecycle lens');
223
+ if (!r.ok) { say(r.error); return; }
224
+ (V.life.call = V.life.call || {})[h.id] = v; say('Marked ' + v + '.', async () => { const u = await patch(V, 'hypotheses', h.id, { pm_call: '' }, 'Undone'); if (u.ok) { delete V.life.call[h.id]; V.render(); } }); markDone(V, 'hyp:' + h.id, 'Marked ' + v + '. Recorded as your call.');
225
+ };
226
+ return rowShell(V, { key, attrs: { 'data-hyp': h.id, 'data-id': h.id }, icon: hs.shown === 'supported' ? 'check' : hs.shown === 'mixed' ? 'alert' : hs.shown === 'refuted' ? 'x' : 'info', iconColor: col, title: h.t, hint: false,
227
+ chips: [chip2(null, HYP_LABEL[hs.shown], col), el('span', { class: 'chip2' }, hs.supports + ' of ' + Math.max(h.ev.length, ncols) + ' sources'), hs.untraced.length ? el('span', { class: 'st warn', 'data-untraced': '', title: hs.untraced.join(', ') }, c3ic('alert', 'sm'), hs.untraced.length + ' untraced') : null,
228
+ call ? chip2('user', 'you: ' + call) : null, h.init ? el('span', { class: 'tag' }, nameOf(V, h.init)) : null],
229
+ xpContent: caseNode(V, 'hyp:' + h.id, hypCase(V, h, hs), { onConfirm: decide }) });
230
+ }
231
+ function hypothesesCard(V, L, scope) {
232
+ if (!L.hyps) return section('target', 'Hypotheses', null, null, [note('alert', L.err.hyps || 'The hypotheses could not be read.', { 'data-life-error': '' })], { 'data-life-panel': 'hypotheses' });
233
+ const list = L.hyps.filter(inScope(scope)); const known = L.life ? L.life.sources || {} : {};
234
+ const ncols = Math.min(3, sourceCols(list).length);
235
+ const rows = list.map((h) => hypRow(V, h, known, ncols));
236
+ return section('target', 'Hypotheses', list.length, null, [...rows, !list.length ? note('target', L.hyps.length ? 'No hypothesis is recorded for this initiative.' : 'No hypotheses are recorded yet. A hypothesis is a statement plus the result that would prove it wrong; adding evidence moves it to supported or refuted.', { 'data-life-empty': 'hypotheses' }) : null, addForm(V, 'hypotheses', HYP_FORM)], { 'data-life-panel': 'hypotheses' });
237
+ }
238
+ /** The sources the evidence matrix columns stand for: the last three distinct sources named by the hypotheses of the scope, in the order they were recorded. */
239
+ const sourceCols = (hyps) => { const at = {}; const cols = []; hyps.forEach((h) => h.ev.forEach((e) => { if (!cols.includes(e.src)) { cols.push(e.src); at[e.src] = e.at || ''; } })); return cols.map((c, i) => [c, i]).sort((a, b) => (at[a[0]] < at[b[0]] ? -1 : at[a[0]] > at[b[0]] ? 1 : a[1] - b[1])).map((x) => x[0]); };
240
+ function evidenceMatrix(V, L, scope) {
241
+ const H = (L.hyps || []).filter(inScope(scope));
242
+ const cols = sourceCols(H);
243
+ if (!cols.length) return card('notes', 'Evidence matrix', 'which source supports which hypothesis', el('p', { class: 'dim', 'data-life-empty': 'evidence' }, 'No evidence is recorded yet. Evidence is a list on the hypothesis: { src, dir: supports or refutes, w }.'));
244
+ const shown = cols.slice(-3);
245
+ const today = new Date().toISOString().slice(0, 10);
246
+ const head = (s) => { const at = ((L.hyps || []).flatMap((h) => h.ev).find((e) => e.src === s) || {}).at; return at && at.slice(0, 10) === today ? 'Today' : dayName(at) || short(baseName(s), 10); };
247
+ const grid = el('div', { class: 'evm', role: 'table', 'aria-label': 'Evidence matrix', style: 'grid-template-columns:minmax(0,1.6fr) repeat(' + shown.length + ',minmax(0,.5fr))' }, el('span'), ...shown.map((s) => el('span', { class: 'h', title: s }, head(s))),
248
+ H.map((h) => [el('span', null, short(h.t, 40)), ...shown.map((s) => { const e = h.ev.find((x) => x.src === s); return el('span', { class: 'c' }, el('i', { class: e ? (e.dir === 'refutes' ? 'x' : '') : 'o', title: e ? e.dir : 'no signal' })); })]));
249
+ return card('notes', 'Evidence matrix', 'which source supports which hypothesis', grid, el('div', { class: 'pgl' }, el('span', { style: 'color:var(--success)' }, '● supports'), H.some((h) => h.ev.some((e) => e.dir === 'refutes' && shown.includes(e.src))) ? el('span', { style: 'color:var(--destructive)' }, '● refutes') : null, el('span', null, '◌ no signal'), cols.length > shown.length ? el('span', null, 'last ' + shown.length + ' of ' + cols.length + ' sources') : null));
250
+ }
251
+ function interviewsCard(V, L) {
252
+ const iv = L.life && Array.isArray(L.life.interviews) ? L.life.interviews : null;
253
+ if (!iv || !iv.length) return card('cal', 'Interviews', '', el('p', { class: 'dim', 'data-life-empty': 'interviews' }, 'No interviews are tied to this initiative. Interviews are calendar events: add them in the Calendar, tagged to the initiative.'));
254
+ const col = { done: 'var(--success)', now: 'var(--acc)', next: 'var(--ring)' };
255
+ return card('cal', 'Interviews', '', segs(iv.map((x) => [x.label, 1, col[x.state] || col.next])));
256
+ }
257
+
258
+ /* ── IDEATE and PRIORITIZE: Ideas, Ranked by score, the scatter, Capacity ── */
259
+ function ideaCase(V, x, ranked) {
260
+ const sc = ideaScore(x); const top = ranked.slice(0, 3).some((r) => r.id === x.id);
261
+ const chg = [['target', 'Impact ' + x.imp + ' of 10, effort ' + x.eff + ' of 10', sc == null ? 'no score' : 'score ' + sc], ['user', 'From ' + x.src, 'source'], ['spark', 'Confidence ' + pct(x.conf) + '%', 'agent']];
262
+ const spike = x.conf < 0.6 && x.imp >= 7;
263
+ return { sit: x.why || 'No reason is recorded for this idea.', conf: pct(x.conf), chg, cta: 'Slide to confirm',
264
+ opts: [{ id: 'sh', l: 'Shortlist it', rec: top, fx: [['check', 'Joins the prioritization', 'ok']] }, { id: 'cm', l: 'Commit it to the plan', fx: [['flag', 'Gets scope and an owner', 'ok']] },
265
+ { id: 'sp', l: 'Run a 1-day spike first', rec: spike, outward: !isDemo(), fx: [['clock', 'Cheap test of the assumption', 'ok']] }, { id: 'pk', l: 'Park it', rec: !top && x.conf < 0.5 && x.imp < 6, fx: [['moon', 'Out of the way', 'warn']] }],
266
+ outboxReason: 'A spike is scoped by an agent session, and this page cannot start one. Ask for it in your terminal.' };
267
+ }
268
+ function ideaRow(V, x, ranked, lens) {
269
+ const sc = ideaScore(x); const st = ST_COLOR[x.st] || ST_COLOR.new;
270
+ const decide = (o) => { const to = { sh: ['short', 'Shortlisted.'], cm: ['committed', 'Committed. It appears in Plan as work to break down.'], pk: ['parked', 'Parked.'] }[o.id];
271
+ if (!to) { say('Spike scoped as a draft.'); markDone(V, 'idea:' + x.id, 'Spike scoped as a draft.'); return; }
272
+ setIdeaStatus(V, x, to[0], to[1]).then((u) => { if (u) markDone(V, 'idea:' + x.id, to[1], async () => { await u(); }); }); };
273
+ const swipe = (status, msg) => () => { setIdeaStatus(V, x, status, msg); };
274
+ return rowShell(V, { key: 'idea' + x.id, attrs: { 'data-idea': x.id, 'data-id': x.id }, icon: x.src === 'agent' ? 'spark' : x.src === 'customer' ? 'user' : 'notes', iconColor: x.src === 'agent' ? 'var(--purple)' : null, title: x.n,
275
+ sw: { labelRight: lens === 'prioritize' ? 'commit it' : 'shortlist it', labelLeft: 'park it', iconRight: 'check', iconLeft: 'moon', onRight: swipe(lens === 'prioritize' ? 'committed' : 'short', lens === 'prioritize' ? 'Committed to the plan.' : 'Shortlisted.'), onLeft: swipe('parked', 'Parked.') },
276
+ chips: [el('span', { class: 'chip2', title: 'Score: impact x confidence / effort' }, c3ic('scale', 'sm'), sc == null ? 'no score' : String(sc)), el('span', { class: 'chip2' }, 'impact ' + x.imp), el('span', { class: 'chip2' }, 'effort ' + x.eff), chip2(null, st[1], st[0]), x.init ? el('span', { class: 'tag' }, nameOf(V, x.init)) : null],
277
+ xpContent: caseNode(V, 'idea:' + x.id, ideaCase(V, x, ranked), { onConfirm: decide }) });
278
+ }
279
+ function ideasCard(V, L, scope, lens) {
280
+ if (!L.ideas) return section('spark', lens === 'prioritize' ? 'Ranked by score' : 'Ideas', null, null, [note('alert', L.err.ideas || 'The ideas could not be read.', { 'data-life-error': '' })], { 'data-life-panel': lens });
281
+ const all = withSt(V, L.ideas).filter(inScope(scope)); const ranked = rankIdeas(all);
282
+ const list = lens === 'prioritize' ? ranked : all.slice().sort((a, b) => (ideaScore(b) ?? -1) - (ideaScore(a) ?? -1));
283
+ const empty = !list.length ? note('spark', L.ideas.length ? 'No idea is recorded for this initiative.' : 'No ideas are recorded yet. An idea is a title with impact, effort and confidence; the score is impact x confidence / effort.', { 'data-life-empty': 'ideas' }) : null;
284
+ return section(lens === 'prioritize' ? 'scale' : 'spark', lens === 'prioritize' ? 'Ranked by score' : 'Ideas', list.length, lens === 'prioritize' ? 'impact x confidence / effort' : null, [...list.map((x) => ideaRow(V, x, ranked, lens)), empty, addForm(V, 'ideas', IDEA_FORM)], { 'data-life-panel': lens });
285
+ }
286
+ function scatterCard(V, L, scope, lens) {
287
+ if (!L.ideas) return null;
288
+ const all = withSt(V, L.ideas).filter(inScope(scope)); const list = lens === 'prioritize' ? rankIdeas(all) : all;
289
+ if (!list.filter((x) => ideaScore(x) != null).length) return card('spark', 'Impact and effort', '', el('p', { class: 'dim', 'data-life-empty': 'scatter' }, 'Nothing to plot yet. Each idea is a dot once it has impact and effort.'));
290
+ return card(lens === 'prioritize' ? 'scale' : 'spark', 'Impact and effort', lens === 'prioritize' ? '' : 'tap a dot to open it', scatter(V, list));
291
+ }
292
+ const CAP_COLORS = ['var(--acc)', 'var(--success)', 'var(--purple)', 'var(--warning)'];
293
+ function capacityOf(L) { const c = L.life && L.life.capacity; return c && Number.isFinite(c.points) ? c : null; }
294
+ function capacityCard(V, L, scope) {
295
+ const all = withSt(V, L.ideas || []).filter(inScope(scope)); const held = rankIdeas(all).filter((x) => x.st === 'short' || x.st === 'committed'); const cap = capacityOf(L);
296
+ const body = [];
297
+ if (held.length) body.push(segs(held.map((x, i) => [x.n, x.eff, CAP_COLORS[i % 4]]), ' pts'));
298
+ else body.push(el('p', { class: 'dim', 'data-life-empty': 'capacity-held' }, 'No idea is shortlisted or committed yet.'));
299
+ body.push(cap ? el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Capacity is ' + cap.points + ' effort points' + (cap.basis ? ': ' + cap.basis.charAt(0).toLowerCase() + cap.basis.slice(1) : '') + '.')
300
+ : el('div', { class: 'dim', style: 'font-size:var(--fs-2)', 'data-life-empty': 'capacity' }, 'No capacity is recorded, so none is assumed. Add the people and their points a week in the Roadmap view.'));
301
+ return card('user', 'Capacity', '', ...body);
302
+ }
303
+
304
+ /* ── MONITOR: Quality gates ── */
305
+ function gateCase(V, id, lad) {
306
+ const nm = nameOf(V, id); const nx = lad.next;
307
+ const chg = lad.gates.map((g) => [g.state === 'done' ? 'check' : g.state === 'na' ? 'moon' : g.state === 'unknown' ? 'info' : 'clock', g.name + ': ' + (g.state === 'done' ? 'passed' : g.state === 'next' ? 'next' : g.state === 'na' ? 'not required' : g.state === 'unknown' ? 'not claimed by the engine' : 'not yet') + (g.evidence ? '. ' + g.evidence : ''), g.key, g.state === 'done' ? 'ok' : g.state === 'next' ? 'warn' : '']);
308
+ lad.violations.forEach((v) => chg.push(['alert', 'Order violation: ' + v, 'order', 'bad']));
309
+ return { sit: nx ? (lad.done ? lad.done + ' gate' + (lad.done === 1 ? '' : 's') + ' passed, so ' + nx.name + ' is next. ' : '') + 'Asking for the ' + nx.name + ' gate early surfaces problems while they are cheap.' : 'Every gate that applies is passed.', conf: 72, chg, cta: 'Slide to request',
310
+ opts: nx ? [{ id: 'r', l: 'Request an early ' + nx.name, rec: 1, outward: !isDemo(), fx: [['check', 'Finds problems early', 'ok']] }, { id: 'w', l: 'Wait for ' + (lad.gates.find((g) => g.key === 'build' && g.state === 'next') ? 'build to finish' : 'the work to reach it'), fx: [['clock', 'Later feedback', 'warn']] }] : [],
311
+ outboxReason: 'A gate is requested from an agent session, and this page cannot start one. Ask for it in your terminal.', go: ['open', id, 'Open ' + nm] };
312
+ }
313
+ function gateRow(V, id, slot) {
314
+ const nm = nameOf(V, id);
315
+ if (slot.state === 'loading') return el('div', { class: 'gtr', 'data-life-gate': id }, el('b', null, nm), el('span', { class: 'dim' }, c3ic('clock', 'sm'), 'Reading the gates…'), el('span'));
316
+ if (!slot.row) return el('div', { class: 'gtr', 'data-life-gate': id }, el('b', null, nm), el('span', { class: 'dim' }, c3ic('alert', 'sm'), slot.err || 'No record for this initiative, so no gate is claimed.'), el('span', { class: 'chip-s', style: '--c:var(--muted-foreground)' }, 'unknown'));
317
+ const lad = gateLadder(slot.row);
318
+ const open = () => { const model = gateCase(V, id, lad); const sh = openSheet({ title: nm + ': gates', icon: 'shield', body: () => renderCase({ ...model, id: 'gate:' + id }, { outbox: false, onConfirm: (r) => { say(r.option.id === 'r' ? 'Early review requested.' : 'Waiting.'); sh.close(); }, onGo: () => { sh.close(); if (V.ctx.openInitiative) V.ctx.openInitiative(V.ctx.repo, id); } }) }); };
319
+ const bar = el('div', { class: 'hbs', style: 'gap:2px' }, el('div', { class: 'gt', role: 'img', 'aria-label': 'Quality gates', 'aria-description': lad.gates.map((g) => g.name + ' ' + g.state).join(', ') }, lad.gates.map((g) => el('i', { class: gateCell(g), title: g.name + ': ' + g.state + (g.evidence ? '. ' + g.evidence : '') }))),
320
+ el('div', { class: 'gtl' }, GATES.map(([n]) => el('span', null, n))));
321
+ return el('div', { class: 'gtr', 'data-life-gate': id, role: 'button', tabindex: '0', style: 'cursor:pointer;min-height:44px', title: 'Open the case', onClick: open, onKeydown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } } },
322
+ el('b', null, nm), bar, el('span', { class: 'chip-s', style: '--c:' + (lad.violations.length ? 'var(--destructive)' : 'var(--muted-foreground)') }, lad.done + ' of ' + lad.counted));
323
+ }
324
+ function gatesCard(V, L, scope, rows) {
325
+ const ids = scope ? [scope] : pickGateRows(rows).map((r) => r.id);
326
+ if (!ids.length) return card('shield', 'Quality gates', 'enforced in order', el('p', { class: 'dim', 'data-life-empty': 'gates' }, 'No initiative to show gates for.'));
327
+ const slots = ids.map((id) => ensureLadder(V, id));
328
+ const extra = [];
329
+ ids.forEach((id, i) => { const s = slots[i]; if (!s.row) return; const lad = gateLadder(s.row);
330
+ lad.violations.forEach((v) => extra.push(el('li', { class: 'bad' }, c3ic('alert', 'sm'), el('span', null, nameOf(V, id) + ': ' + v + '.'))));
331
+ if (lad.next) extra.push(el('li', null, c3ic('next', 'sm'), el('span', null, nameOf(V, id) + ': next gate is ' + lad.next.name + (lad.nextAction && lad.nextAction.action ? '. ' + short(lad.nextAction.action.charAt(0).toUpperCase() + lad.nextAction.action.slice(1), 140) : '') + '.')));
332
+ else extra.push(el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, nameOf(V, id) + ': every gate that applies is passed.')));
333
+ });
334
+ return card('shield', 'Quality gates', 'enforced in order', ...ids.map((id, i) => gateRow(V, id, slots[i])),
335
+ el('div', { class: 'pgl' }, el('span', { style: 'color:var(--success)' }, '▮ passed'), el('span', null, '◐ next'), el('span', null, '▯ not yet'), el('span', null, '┅ not required'), el('span', null, '▨ the engine does not claim it')),
336
+ extra.length ? el('ul', { class: 'chgl', 'data-life-gate-notes': '' }, extra) : null);
337
+ }
338
+ /** The initiatives the gates card shows when no initiative is chosen: the ones in flight (Build to Evaluate) first, then the rest in row order. At most four: each is one engine run. */
339
+ function pickGateRows(rows) {
340
+ const inFlight = (r) => ['build', 'deliver', 'monitor', 'evaluate'].includes(stageBucket(nineOf(r)));
341
+ return rows.filter(inFlight).concat(rows.filter((r) => !inFlight(r))).slice(0, 4);
342
+ }
343
+
344
+ /* ── EVALUATE: the scorecard, What we learned, the correction rate ── */
345
+ function rubricBars(ev) {
346
+ const thr = ev.threshold;
347
+ return el('div', { class: 'sc' }, ev.dims.map((d) => { const c = d.score >= 0.8 ? 'var(--success)' : d.score >= 0.65 ? 'var(--warning)' : 'var(--destructive)';
348
+ return el('div', { class: 'scr', style: '--c:' + c }, el('div', { class: 'l' }, el('span', null, d.name + (d.w != null ? ' · ' + pct(d.w) + '%' : '')), el('i', { class: 'mono', style: 'font-style:normal' }, String(pct(d.score)))),
349
+ el('div', { class: 'tk' }, el('u', { style: 'width:' + d.score * 100 + '%' }), thr != null ? el('s', { style: 'left:' + thr * 100 + '%' }) : null)); }));
350
+ }
351
+ function calibrationLine(L) {
352
+ const c = L.calib;
353
+ if (!c) return el('li', { 'data-life-calib': 'none' }, c3ic('info', 'sm'), el('span', null, 'Calibration is not read: the radar surface did not answer. The Review gate stays advisory until Cohen\'s kappa is at least 0.60 over 10 or more labeled pairs (D40).'));
354
+ if (c.none || c.kappa == null) return el('li', { 'data-life-calib': 'none' }, c3ic('info', 'sm'), el('span', null, 'No calibration figure is recorded. The Review gate stays advisory until kappa is at least 0.60 over 10 or more PM-labeled pairs (D40); your acceptance is the binding call.'));
355
+ const ok = !!c.meets_d40;
356
+ return el('li', { class: ok ? 'ok' : 'warn', 'data-life-calib': ok ? 'calibrated' : 'advisory' }, c3ic(ok ? 'check' : 'alert', 'sm'),
357
+ el('span', null, 'Evaluator against your labels: kappa ' + c.kappa + (c.n != null ? ' over ' + c.n + ' pairs' : '') + (c.tnr != null ? ', TNR ' + c.tnr : '') + '. ' + (ok ? 'Meets the D40 standard.' : 'Below the D40 standard (kappa at least 0.60, n at least 10, healthy TNR): the verdict is advisory.') + (c.source ? ' Source: ' + c.source + '.' : '')));
358
+ }
359
+ function scoreCard(V, L, scope, rows) {
360
+ if (!L.life) return card('check', 'Evaluation scorecard', 'weighted, against a threshold', el('p', { class: 'dim', 'data-life-error': '' }, L.err.life || 'The evaluations could not be read.'));
361
+ const evals = L.life.evals || {};
362
+ const cand = (scope ? rows.filter((r) => r.id === scope) : rows.slice().sort((a, b) => (stageBucket(nineOf(b)) === 'evaluate') - (stageBucket(nineOf(a)) === 'evaluate'))).filter((r) => evals[r.id]);
363
+ if (!cand.length) return card('check', 'Evaluation scorecard', 'weighted, against a threshold', el('p', { class: 'dim', 'data-life-empty': 'scorecard' }, (scope ? nameOf(V, scope) + ' has no evaluation recorded.' : 'No evaluation is recorded for these initiatives.') + ' The evaluator writes one row per run to planning/runs/evals.jsonl: a score, a threshold and a score for each dimension.'));
364
+ return cand.slice(0, scope ? 1 : 2).map((r) => {
365
+ const ev = normEval(evals[r.id]); const vd = evalVerdict(ev);
366
+ if (!ev || !vd) return card('check', 'Evaluation scorecard', r.title, el('p', { class: 'dim' }, 'The latest evaluation has no usable score.'));
367
+ const ok = vd.passes; const flags = [];
368
+ if (vd.watermelon) flags.push(el('li', { class: 'warn', 'data-life-watermelon': '' }, c3ic('alert', 'sm'), el('span', null, 'Watermelon guard: it passes at ' + pct(ev.score) + '%, but ' + vd.weakest.name + ' scores ' + pct(vd.weakest.score) + '%. Read that dimension before accepting.')));
369
+ flags.push(el('li', { class: vd.weakest && vd.weakest.score < 0.7 ? 'warn' : '' }, c3ic(vd.weakest && vd.weakest.score < 0.7 ? 'alert' : 'info', 'sm'), el('span', null, vd.weakest ? 'Weakest dimension: ' + vd.weakest.name + ' at ' + pct(vd.weakest.score) + '.' : 'No dimension scores are recorded.')));
370
+ if (!ev.weighted && ev.dims.length) flags.push(el('li', null, c3ic('info', 'sm'), el('span', null, 'No weights are recorded for this rubric, so the dimensions are shown unweighted; the score is the evaluator\'s own.')));
371
+ flags.push(calibrationLine(L));
372
+ return el('section', { class: 'card', 'data-life-panel': 'scorecard', 'data-life-eval': r.id }, el('h3', null, c3ic('check'), 'Evaluation scorecard ', el('span', { class: 'dim', style: DIM }, short(r.title, 34) + ' · weighted, against a threshold')),
373
+ ev.dims.length ? rubricBars(ev) : null,
374
+ el('div', { class: 'chips2', style: 'justify-content:space-between' }, el('b', { style: 'font-size:var(--fs-4)' }, pct(ev.score) + '%'), el('span', { class: 'st ' + (ok ? 'ok' : 'warn') }, c3ic(ok ? 'check' : 'alert', 'sm'), (ok === null ? 'No threshold recorded' : ok ? (ev.verdict && /FINDING/i.test(ev.verdict) ? 'Passes with findings' : 'Passes') : 'Below threshold') + (ev.threshold != null ? ' · threshold ' + pct(ev.threshold) + '%' : ''))),
375
+ el('ul', { class: 'chgl' }, flags), ev.model ? el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'Evaluator ' + ev.model + (ev.at ? ' · ' + ev.at.slice(0, 10) : '')) : null);
376
+ });
377
+ }
378
+ function lessonsCard(V, L, scope, rows) {
379
+ const out = [];
380
+ (L.decisions || []).filter((d) => d.lesson && (!scope || d.initiative === scope)).forEach((d) => out.push([d.tone === 'warn' ? 'warn' : d.tone === 'ok' ? 'ok' : '', d.tone === 'warn' ? 'alert' : d.tone === 'ok' ? 'check' : 'info', String(d.lesson)]));
381
+ const evals = (L.life && L.life.evals) || {};
382
+ rows.filter((r) => !scope || r.id === scope).forEach((r) => ((evals[r.id] && evals[r.id].findings) || []).forEach((f) => out.push([/should|block|must/i.test(f.severity) ? 'warn' : '', /should|block|must/i.test(f.severity) ? 'alert' : 'info', (scope ? '' : r.title + ': ') + f.text])));
383
+ return el('section', { class: 'card', 'data-life-panel': 'learned' }, el('h3', null, c3ic('spark'), 'What we learned'),
384
+ out.length ? el('ul', { class: 'chgl' }, out.slice(0, 6).map((l) => el('li', { class: l[0] }, c3ic(l[1], 'sm'), el('span', null, short(l[2], 260))))) : el('p', { class: 'dim', 'data-life-empty': 'learned' }, 'Nothing is recorded yet. A lesson comes from the evaluation findings or from a decision recorded with a lesson.'),
385
+ el('ul', { class: 'list', style: 'margin-top:8px' }, addForm(V, 'decisions', LESSON_FORM)));
386
+ }
387
+ function correctionCard(V, L) {
388
+ if (!L.life) return card('up', 'Human correction rate', '', el('p', { class: 'dim', 'data-life-error': '' }, L.err.life || 'The corrections could not be read.'));
389
+ const c = correctionRate(L.life.corrections && L.life.corrections.series);
390
+ if (!c) return card('up', 'Human correction rate', '', el('p', { class: 'dim', 'data-life-empty': 'corrections' }, 'No run records a human correction count yet. Each run in planning/runs/agent-runs.jsonl carries human_corrections.'));
391
+ const tone = c.improved ? 'var(--success)' : 'var(--warning)';
392
+ const per100 = c.vals.map((v) => Math.round(v * 100)); const first = per100[0]; const last = per100[per100.length - 1];
393
+ return card('up', 'Human correction rate', 'corrections per 100 agent runs, lower is better', numCard(String(last), ' per 100 runs', per100.length > 1 ? (c.improved ? 'down from ' : 'up from ') + first + ' at the start' : 'one period of runs so far', tone, per100.length > 1 ? per100 : [last, last], null),
394
+ el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, L.life.corrections.runs + ' runs, ' + L.life.corrections.corrections + ' corrections in all. Each point is a week of runs.'));
395
+ }
396
+
397
+ /* ── PLAN: Work breakdown, who holds the work ── */
398
+ function wbsCard(V, L, scope) {
399
+ if (!L.items) return section('tree', 'Work breakdown', null, null, [note('alert', 'The breakdown could not be read.', { 'data-life-error': '' })], { 'data-life-panel': 'wbs' });
400
+ const list = L.items.filter((x) => !scope || x.initiative === scope);
401
+ const rows = list.map((c) => { const done = !!c.done; const col = done ? 'var(--success)' : 'var(--muted-foreground)';
402
+ return el('li', { class: 'nrow', 'data-row': '', tabindex: '0', 'data-wbs': c.rid }, el('span', { class: 'ki', style: 'color:' + col }, c3ic(done ? 'check' : 'moon')),
403
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, c.title), el('div', { class: 'chips' }, chip2(null, done ? 'done' : 'to do', col), c.owner ? el('span', { class: 'chip2' }, c.owner) : el('span', { class: 'st warn' }, c3ic('alert', 'sm'), 'no owner'), c.points ? el('span', { class: 'chip2' }, c.points + ' pts') : null, c.initiative ? el('span', { class: 'tag' }, nameOf(V, c.initiative)) : null))); });
404
+ return section('tree', 'Work breakdown', list.length, 'from the Breakdown view', [...rows, !rows.length ? note('tree', 'No work items yet. Add them in the Breakdown view; each gets an owner and points.', { 'data-life-empty': 'wbs' }) : null], { 'data-life-panel': 'wbs' });
405
+ }
406
+ function whoCard(V, L, scope) {
407
+ const list = (L.items || []).filter((x) => !scope || x.initiative === scope);
408
+ if (!list.length) return card('org', 'Who does what', 'owner of each work item', el('p', { class: 'dim', 'data-life-empty': 'who' }, 'No work items, so no owners. Roles beyond the owner (accountable, consulted, informed) are not recorded anywhere yet.'));
409
+ const people = []; list.forEach((x) => { if (x.owner && !people.includes(x.owner)) people.push(x.owner); });
410
+ const shown = people.slice(0, 5);
411
+ const grid = el('div', { class: 'rcg', style: 'grid-template-columns:minmax(0,1.6fr) repeat(' + shown.length + ',minmax(0,.5fr))' }, el('span'), ...shown.map((p) => el('span', { class: 'h', title: p }, p.slice(0, 3))),
412
+ list.slice(0, 8).map((x) => [el('span', null, short(x.title, 34)), ...shown.map((p) => el('span', { class: 'c ' + (x.owner === p ? 'R' : '') }, x.owner === p ? 'R' : ''))]));
413
+ const gaps = list.filter((x) => !x.owner);
414
+ return card('org', 'Who does what', 'the owner of each work item (R)', grid, el('ul', { class: 'chgl' }, gaps.length ? gaps.slice(0, 4).map((x) => el('li', { class: 'bad' }, c3ic('alert', 'sm'), el('span', null, x.title + ' has nobody responsible.'))) : el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, 'Every work item has an owner.'))));
415
+ }
416
+
417
+ /* ── BUILD: tickets, merged by initiative, the factory boundary ── */
418
+ function buildPanels(V, L, scope, rows) {
419
+ const ids = scope ? [scope] : rows.slice(0, 6).map((r) => r.id);
420
+ const slots = ids.map((id) => ensureTickets(V, id));
421
+ const tix = []; ids.forEach((id, i) => slots[i].items.forEach((t) => tix.push({ ...t, ini: id })));
422
+ const pending = slots.some((s) => s.state === 'loading');
423
+ const merged = (it) => it.state === 'done';
424
+ const tickets = section('branch', 'Build tickets', tix.length, 'Jira, read-only here', [...tix.map((t) => el('li', { class: 'nrow', 'data-row': '', tabindex: '0', 'data-ticket': t.id }, el('span', { class: 'ki', style: 'color:' + (merged(t) ? 'var(--success)' : 'var(--muted-foreground)') }, c3ic(merged(t) ? 'check' : t.state === 'doing' ? 'clock' : 'moon')),
425
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, t.title), el('div', { class: 'chips' }, el('span', { class: 'chip2 mono' }, t.id), chip2(null, t.state === 'done' ? 'done' : t.state === 'doing' ? 'doing' : 'to do'), t.forecast && t.forecast.slip_days ? el('span', { class: 'st warn' }, c3ic('alert', 'sm'), t.forecast.slip_days + ' d late') : null, el('span', { class: 'tag' }, nameOf(V, t.ini)))))),
426
+ pending ? loading('the tickets') : null, !tix.length && !pending ? note('check', 'No tickets are read for these initiatives. Tickets come from Jira; connect it in Settings.', { 'data-life-empty': 'tickets' }) : null], { 'data-life-panel': 'tickets' });
427
+ const bars = ids.map((id, i) => { const its = slots[i].items; const d = its.filter(merged).length; return its.length ? [short(nameOf(V, id), 24), d / its.length || 0.03, CAP_COLORS[i % 4], d + ' of ' + its.length] : null; }).filter(Boolean);
428
+ const right = [card('branch', 'Merged by initiative', '', bars.length ? hBars(bars) : el('p', { class: 'dim', 'data-life-empty': 'merged' }, pending ? 'Reading the tickets…' : 'No tickets to count.'), el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'A ticket counts when it is done in Jira. Merging needs required CI to be green.')),
429
+ el('section', { class: 'card' }, el('h3', null, c3ic('shield'), 'Factory boundary'), el('ul', { class: 'chgl' }, el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, 'Opens pull requests only')), el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, 'Never writes a control-graph record')), el('li', { class: 'warn' }, c3ic('alert', 'sm'), el('span', null, 'A failed rubric review sends the work back, not forward'))))];
430
+ const total = tix.length; const done = tix.filter(merged).length;
431
+ return { after: [tickets], right, head: { icon: 'branch', h: pending && !total ? 'Reading the build tickets' : done + ' of ' + total + ' build tickets are done' + (scope ? ' for ' + nameOf(V, scope) : ' across the initiatives'), s: 'The factory opens pull requests and holds no merge rights. CI and review decide.', ring: [done, Math.max(1, total)] } };
432
+ }
433
+
434
+ /* ── the header of each lens (iniInfo, line 3577) and the panels per lens ── */
435
+ export function lensPanels(V, lens, scope) {
436
+ const L = ensureLife(V); const rows = V.ctx.rows || [];
437
+ const out = { head: null, left: [], after: [], right: [] };
438
+ const wait = L.state !== 'ready';
439
+ if (wait) { out.left = [section('info', 'Reading the records', null, null, [loading('ideas, hypotheses, evaluations')])]; return out; }
440
+ const ideas = withSt(V, L.ideas || []).filter(inScope(scope)); const hyps = (L.hyps || []).filter(inScope(scope));
441
+ if (lens === 'discover') {
442
+ const hs = hyps.map((h) => hypStatus(h, L.life ? L.life.sources || {} : {}));
443
+ out.head = { icon: 'search', h: hyps.length ? hyps.length + ' hypothes' + (hyps.length === 1 ? 'is' : 'es') + ': ' + hs.filter((s) => s.shown === 'supported').length + ' supported, ' + hs.filter((s) => s.shown === 'untested').length + ' untested' : 'No hypotheses are recorded yet', s: hyps.length ? 'A hypothesis is supported at a net evidence weight of 3 and refuted at -2. Evidence that points to a missing source is flagged untraced.' : 'Add a hypothesis with what would prove it wrong. Evidence moves it to supported or refuted.', ring: [hs.filter((s) => s.shown === 'supported').length, Math.max(1, hyps.length)] };
444
+ out.left = [hypothesesCard(V, L, scope)]; out.right = [evidenceMatrix(V, L, scope), interviewsCard(V, L)];
445
+ } else if (lens === 'ideate') {
446
+ out.head = { icon: 'spark', h: ideas.length ? ideas.length + ' ideas, ' + ideas.filter((x) => x.src === 'agent').length + ' proposed by the agent' : 'No ideas are recorded yet', s: 'Score by impact, confidence and effort, then shortlist.', ring: [ideas.filter((x) => x.st === 'short' || x.st === 'committed').length, Math.max(1, ideas.length)] };
447
+ out.left = [ideasCard(V, L, scope, 'ideate')]; out.right = [scatterCard(V, L, scope, 'ideate')];
448
+ } else if (lens === 'prioritize') {
449
+ const cap = capacityOf(L); const r = rankIdeas(ideas); const used = capacityUsed(ideas, cap && cap.points);
450
+ out.head = { icon: 'scale', h: r.length + ' ranked ideas' + (cap ? ' against capacity of ' + cap.points : ', no capacity recorded'), s: used.cap != null && used.over ? 'You hold ' + used.used + ' effort points, ' + used.over + ' over capacity. Commit the top three and park the rest.' : 'Commit the top three and park the rest.', ring: [ideas.filter((x) => x.st === 'committed').length, 3] };
451
+ out.left = [ideasCard(V, L, scope, 'prioritize')]; out.right = [scatterCard(V, L, scope, 'prioritize'), capacityCard(V, L, scope)];
452
+ } else if (lens === 'plan') {
453
+ const items = (L.items || []).filter((x) => !scope || x.initiative === scope);
454
+ const owned = items.filter((x) => x.owner).length;
455
+ out.head = { icon: 'tree', h: items.length ? items.length + ' work items, ' + items.filter((x) => x.done).length + ' done' : 'No work items yet', s: items.length ? (items.length - owned ? (items.length - owned) + ' work item' + (items.length - owned === 1 ? ' lacks' : 's lack') + ' an owner.' : 'Every work item has an owner.') : 'Add work items in the Breakdown view.', ring: [items.filter((x) => x.done).length, Math.max(1, items.length)] };
456
+ out.left = [wbsCard(V, L, scope)]; out.right = [whoCard(V, L, scope), progressCard(V, 'Path and dates', '', { cap: 6, uid: 'plan', onOpen: (id) => V.ctx.openInitiative && V.ctx.openInitiative(V.ctx.repo, id) })];
457
+ } else if (lens === 'monitor') {
458
+ const ids = scope ? [scope] : pickGateRows(rows).map((r) => r.id);
459
+ const lads = ids.map((id) => L.ladders[id]).filter((s) => s && s.row).map((s) => gateLadder(s.row));
460
+ out.head = lads.length ? { icon: 'report', h: lads.reduce((a, l) => a + l.done, 0) + ' of ' + lads.reduce((a, l) => a + l.counted, 0) + ' gates passed across ' + lads.length + ' initiative' + (lads.length === 1 ? '' : 's'), s: lads.some((l) => l.violations.length) ? 'Order violations: ' + lads.flatMap((l) => l.violations).slice(0, 2).join('; ') + '.' : 'Gates are enforced in order: spec and rubric before build.', ring: [lads.reduce((a, l) => a + l.done, 0), Math.max(1, lads.reduce((a, l) => a + l.counted, 0))] }
461
+ : { icon: 'report', h: 'Reading the quality gates', s: 'Each initiative\'s gates come from the workflow engine.', ring: [0, 1] };
462
+ out.left = [gatesCard(V, L, scope, rows)]; out.right = [progressCard(V, 'Progress and timing', '', { cap: 8, uid: 'monitor', onOpen: (id) => V.ctx.openInitiative && V.ctx.openInitiative(V.ctx.repo, id) })];
463
+ } else if (lens === 'evaluate') {
464
+ const evals = (L.life && L.life.evals) || {}; const cand = (scope ? rows.filter((r) => r.id === scope) : rows).filter((r) => evals[r.id]);
465
+ const first = cand[0] ? normEval(evals[cand[0].id]) : null; const vd = first ? evalVerdict(first) : null;
466
+ out.head = first && vd ? { icon: 'check', h: cand[0].title + (vd.passes ? ' passes' + (/FINDING/i.test(first.verdict) ? ' with findings' : '') : ' is below its threshold') + ' at ' + pct(first.score) + '%', s: (vd.weakest ? vd.weakest.name + ' is weakest. ' : '') + 'Decide whether to scale, iterate or stop.', ring: [pct(first.score), 100] }
467
+ : { icon: 'check', h: 'No evaluation is recorded' + (scope ? ' for ' + nameOf(V, scope) : ''), s: 'The evaluator writes one row per run to planning/runs/evals.jsonl.', ring: [0, 100] };
468
+ out.left = [...[].concat(scoreCard(V, L, scope, rows)), lessonsCard(V, L, scope, rows)]; out.right = [correctionCard(V, L)];
469
+ } else if (lens === 'deliver') {
470
+ const rp = releasePanels(V, scope); out.left = rp.left; out.right = rp.right;
471
+ } else if (lens === 'build') {
472
+ const b = buildPanels(V, L, scope, rows); out.head = b.head; out.after = b.after; out.right = b.right;
473
+ }
474
+ out.right = out.right.filter(Boolean);
475
+ return out;
476
+ }