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,228 @@
|
|
|
1
|
+
/* pm/lifecycle-lens-model.js — the pure half of the Lifecycle lens (console3 wave B1). No DOM, so node can import it.
|
|
2
|
+
*
|
|
3
|
+
* Stage is DERIVED, never set. The lens reads each initiative's `nine_stages` (scripts/nine-stages.js: one of
|
|
4
|
+
* done | current | pending | no-signal | skipped per stage). The roadmap payload does not carry `nine_stages` yet, so
|
|
5
|
+
* `nineOf` falls back to a small MIRROR of scripts/nine-stages.js (STAGES, LANE_STAGES, SIGNALLED and the status rule),
|
|
6
|
+
* fed by the rung the row's `stage.id` names. The mirror is approximate: the row's stage is the plan-tree group, not the
|
|
7
|
+
* engine's raw rung, so an accepted initiative cannot be told apart from one in Evaluate. When the server sends
|
|
8
|
+
* `nine_stages`, that wins and the mirror is not used.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const STAGES = ['discover', 'ideate', 'strategize', 'prioritize', 'plan', 'build', 'deliver', 'monitor', 'evaluate'];
|
|
12
|
+
export const LABELS = { discover: 'Discover', ideate: 'Ideate', strategize: 'Strategize', prioritize: 'Prioritize', plan: 'Plan', build: 'Build', deliver: 'Deliver', monitor: 'Monitor', evaluate: 'Evaluate' };
|
|
13
|
+
// Mirror of scripts/nine-stages.js LANE_STAGES and SIGNALLED.
|
|
14
|
+
const LANE_STAGES = {
|
|
15
|
+
feature: STAGES,
|
|
16
|
+
discovery: ['discover', 'ideate', 'strategize', 'prioritize', 'evaluate'],
|
|
17
|
+
chore: ['plan', 'build', 'evaluate'],
|
|
18
|
+
incident: ['build', 'deliver', 'monitor', 'evaluate'],
|
|
19
|
+
content: ['ideate', 'plan', 'build', 'deliver'],
|
|
20
|
+
prototype: ['discover', 'ideate', 'evaluate'],
|
|
21
|
+
};
|
|
22
|
+
const SIGNALLED = new Set(['discover', 'plan', 'build', 'evaluate']);
|
|
23
|
+
// The plan-tree group id of a row's stage, as an engine rung rank (nine-stages.js RANK). Unknown groups read intake.
|
|
24
|
+
const GROUP_RANK = { intake: 0, spec: 1, brief: 1, draft: 1, contract: 2, build: 3, evaluate: 4, handoff: 4 };
|
|
25
|
+
|
|
26
|
+
/** The nine statuses of one row: its own `nine_stages` when the server sent them, else the mirror. */
|
|
27
|
+
export function nineOf(row) {
|
|
28
|
+
if (row && Array.isArray(row.nine_stages) && row.nine_stages.length) return row.nine_stages;
|
|
29
|
+
const lane = row && row.lane;
|
|
30
|
+
const laneSet = new Set(LANE_STAGES[lane] || STAGES);
|
|
31
|
+
const gid = row && row.stage && row.stage.id;
|
|
32
|
+
const rank = GROUP_RANK[gid] ?? 0;
|
|
33
|
+
const done = { discover: rank >= 1, plan: rank >= 2, build: rank >= 3, evaluate: rank >= 4 };
|
|
34
|
+
let cur = false;
|
|
35
|
+
return STAGES.map((key) => {
|
|
36
|
+
let status;
|
|
37
|
+
if (!laneSet.has(key)) status = 'skipped';
|
|
38
|
+
else if (!SIGNALLED.has(key)) status = 'no-signal';
|
|
39
|
+
else if (done[key]) status = 'done';
|
|
40
|
+
else status = 'pending';
|
|
41
|
+
if (status === 'pending' && !cur) { status = 'current'; cur = true; }
|
|
42
|
+
return { key, label: LABELS[key], status };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The stage an initiative sits in: the first `current`; else the last `done`; else null (nothing shows a stage yet). */
|
|
47
|
+
export function stageBucket(nine) {
|
|
48
|
+
const list = Array.isArray(nine) ? nine : [];
|
|
49
|
+
const cur = list.find((s) => s.status === 'current');
|
|
50
|
+
if (cur) return cur.key;
|
|
51
|
+
let last = null;
|
|
52
|
+
for (const s of list) if (s.status === 'done') last = s.key;
|
|
53
|
+
return last;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Group rows by stage: { buckets: { discover: [row…], … }, unplaced: [row…] }. `scope` is an initiative id or null for all. */
|
|
57
|
+
export function bucketRows(rows, scope) {
|
|
58
|
+
const buckets = Object.fromEntries(STAGES.map((k) => [k, []]));
|
|
59
|
+
const unplaced = [];
|
|
60
|
+
for (const r of rows || []) {
|
|
61
|
+
if (scope && r.id !== scope) continue;
|
|
62
|
+
const k = stageBucket(nineOf(r));
|
|
63
|
+
if (k && buckets[k]) buckets[k].push(r); else unplaced.push(r);
|
|
64
|
+
}
|
|
65
|
+
return { buckets, unplaced };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** How one stage reads across the scoped rows: 'no-signal' when every row that uses it has no signal, 'skipped' when every row skips it. */
|
|
69
|
+
export function stageNote(key, rows, scope) {
|
|
70
|
+
const sts = (rows || []).filter((r) => !scope || r.id === scope).map((r) => (nineOf(r).find((s) => s.key === key) || {}).status);
|
|
71
|
+
if (!sts.length) return null;
|
|
72
|
+
if (sts.every((s) => s === 'skipped')) return 'skipped';
|
|
73
|
+
if (sts.every((s) => s === 'skipped' || s === 'no-signal')) return 'no-signal';
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── what each stage needs (console3.html C3.REQ, line 4687): the requirement rows of the lens, each with who does it ── */
|
|
78
|
+
export const REQ = {
|
|
79
|
+
discover: [['brief', 'Discovery brief with a stop rule', 'agent'], ['evidence', 'Evidence from 3 or more sources', 'agent'], ['cont', 'Continue or park', 'pm']],
|
|
80
|
+
ideate: [['opts', 'Two or more options, one recommended', 'agent']],
|
|
81
|
+
strategize: [['bet', 'Bet statement tied to the anchor', 'agent']],
|
|
82
|
+
prioritize: [['commit', 'Commit: it fits Now capacity', 'pm']],
|
|
83
|
+
plan: [['prd', 'PRD with done criteria', 'agent'], ['rub', 'Rubric and pre-run contract', 'agent'], ['plan', 'Breakdown reviewed', 'pm']],
|
|
84
|
+
build: [['tix', 'Build tickets merged, required CI green', 'factory'], ['rev', 'Review against the rubric', 'agent']],
|
|
85
|
+
deliver: [['rel', 'Release shipped', 'pm']],
|
|
86
|
+
monitor: [['out', 'Outcome check after 2 weeks', 'agent']],
|
|
87
|
+
evaluate: [['verd', 'Verdict: what we learned, next bet seeded', 'pm']],
|
|
88
|
+
};
|
|
89
|
+
/** Who does a requirement: [icon, word]. */
|
|
90
|
+
export const reqWho = (w) => (w === 'pm' ? ['user', 'you'] : w === 'factory' ? ['branch', 'factory'] : ['spark', 'agent']);
|
|
91
|
+
/** The agent's or factory's requirements of the same stage that are not done yet: a PM decision waits for them (c3prevPending). `isDone(key)` says what is done. */
|
|
92
|
+
export const prevPending = (stage, key, isDone) => (REQ[stage] || []).filter((r) => r[2] !== 'pm' && r[0] !== key && !isDone(r[0]));
|
|
93
|
+
/** What the agent is asked to draft, per requirement: [the offer, what it is]. */
|
|
94
|
+
export const DRAFT_TEXT = {
|
|
95
|
+
brief: ['Draft the discovery brief', 'A one-page brief with a stop rule'], evidence: ['Gather the evidence', 'Three or more sources, each cited'], opts: ['Draft the options', 'Two or more, one recommended, with the cost of each'],
|
|
96
|
+
bet: ['Draft the bet statement', 'Tied to the anchor'], prd: ['Draft the PRD', 'Done criteria first'], rub: ['Draft the rubric and pre-run contract', 'The rubric is the spec; the contract is agreed before any build'],
|
|
97
|
+
rev: ['Review the build against the rubric', 'Outcome only, weighted, against the threshold'], out: ['Run the outcome check', 'Two weeks after release, from the data'],
|
|
98
|
+
};
|
|
99
|
+
/** The PM's decision per requirement: [yes label, yes effect], [no label, no effect], the sentence. */
|
|
100
|
+
export const DECIDE_TEXT = {
|
|
101
|
+
cont: [['Continue to the next stage', 'The stop rule stays in force'], ['Park it, reason recorded', 'Frees the discovery slot'], 'The stop rule is the test. Read the evidence and decide: a close call is a reason to ask for more, not to guess.'],
|
|
102
|
+
commit: [['Commit it to Now', 'It fits Now capacity'], ['Keep it in Prioritize', 'Waits for capacity'], 'Now has a limit. The least sure bet goes back if it fills.'],
|
|
103
|
+
plan: [['Approve the plan', 'Breakdown, owners and dates are agreed'], ['Send the breakdown back', 'The agent revises it'], 'Check that every work item has an owner before you approve.'],
|
|
104
|
+
rel: [['Release it', 'The release is recorded as shipped by your slide'], ['Hold it', 'The date moves; promises are re-told'], 'Release needs its readiness checks and an outside change in Jira, which needs its own slide.'],
|
|
105
|
+
verd: [['Record the verdict: iterate', 'The next bet is seeded from the lesson'], ['Record the verdict: stop', 'Frees the capacity'], 'The verdict is a human judgement; the agent only drafts the evidence.'],
|
|
106
|
+
};
|
|
107
|
+
/** The stages a lane uses, in order (the lane's own list). */
|
|
108
|
+
export const laneStages = (lane) => LANE_STAGES[lane] || STAGES;
|
|
109
|
+
|
|
110
|
+
/* ── the derived panels of the lens (console3 wave L1). Ports of scripts/trust/reader.js (the PM's sample), sections 11 to 16:
|
|
111
|
+
* 11 PRIORITIZE score = impact x confidence / effort, capacity in bets
|
|
112
|
+
* 12 DISCOVER hypothesis status from weighted evidence; evidence whose source is not a known file is untraced
|
|
113
|
+
* 14 GATE LADDER the six gates in order, the next gate, order violations
|
|
114
|
+
* 15 RUBRIC the weighted score against the threshold, the weakest dimension, the watermelon guard
|
|
115
|
+
* 16 NORTH STAR corrections per agent run, lower is better
|
|
116
|
+
* Pure: no DOM, no fetch. Real records feed them; nothing here invents a number. ── */
|
|
117
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
118
|
+
|
|
119
|
+
/** The score of an idea (reader.js section 11: impact x confidence / effort, two decimals). null when a figure is missing. */
|
|
120
|
+
export function ideaScore(x) {
|
|
121
|
+
if (!x || !isNum(x.imp) || !isNum(x.conf) || !isNum(x.eff) || x.eff <= 0) return null;
|
|
122
|
+
return Math.round(x.imp * x.conf / x.eff * 100) / 100;
|
|
123
|
+
}
|
|
124
|
+
/** An ideas record (scripts/records: title, impact, effort, confidence; optional initiative, source, status, why) as the lens draws it. */
|
|
125
|
+
export function normIdea(r) {
|
|
126
|
+
const o = r || {};
|
|
127
|
+
const st = ['new', 'short', 'committed', 'parked'].includes(o.status) ? o.status : o.status === 'shortlisted' ? 'short' : 'new';
|
|
128
|
+
return { id: String(o.rid || o.id || ''), n: String(o.title || ''), init: String(o.initiative || ''), imp: Number(o.impact), eff: Number(o.effort), conf: Number(o.confidence),
|
|
129
|
+
src: ['agent', 'customer', 'you'].includes(o.source) ? o.source : 'you', st, why: String(o.why || '') };
|
|
130
|
+
}
|
|
131
|
+
/** The ideas that are ranked: not parked, best score first (reader.js 11). */
|
|
132
|
+
export const rankIdeas = (ideas) => (ideas || []).filter((x) => x.st !== 'parked' && ideaScore(x) != null).sort((a, b) => ideaScore(b) - ideaScore(a));
|
|
133
|
+
/** Capacity: the effort the PM holds (shortlisted and committed) against the capacity figure. cap = null when none is recorded. */
|
|
134
|
+
export function capacityUsed(ideas, cap) {
|
|
135
|
+
const used = (ideas || []).filter((x) => x.st === 'short' || x.st === 'committed').reduce((a, x) => a + (isNum(x.eff) ? x.eff : 0), 0);
|
|
136
|
+
return { used, cap: isNum(cap) ? cap : null, over: isNum(cap) ? Math.max(0, used - cap) : null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A hypotheses record as the lens draws it. `evidence` is [{ src, dir: 'supports'|'refutes', w }]. */
|
|
140
|
+
export function normHyp(r) {
|
|
141
|
+
const o = r || {};
|
|
142
|
+
const ev = (Array.isArray(o.evidence) ? o.evidence : []).filter((e) => e && typeof e === 'object').map((e) => ({ src: String(e.src || ''), dir: e.dir === 'refutes' ? 'refutes' : 'supports', w: isNum(Number(e.w)) ? Number(e.w) : 1, at: e.at ? String(e.at) : '' }));
|
|
143
|
+
return { id: String(o.rid || o.id || ''), t: String(o.title || ''), init: String(o.initiative || ''), falsifier: String(o.falsifier || ''), review_by: String(o.review_by || ''), ev };
|
|
144
|
+
}
|
|
145
|
+
/** The status of a hypothesis, as reader.js section 12 derives it: net = sum of the weights (supports plus, refutes minus); net >= 3 is supported,
|
|
146
|
+
* net <= -2 is refuted, anything between is open. `known` is a Set (or object) of the sources that exist; a source outside it is untraced.
|
|
147
|
+
* The reader has three statuses. The page splits "open" for display only, so the PM can tell an empty hypothesis from a contested one:
|
|
148
|
+
* `shown` is untested (no evidence), mixed (evidence both ways), or open (one-sided but below the bar). `status` stays the reader's. */
|
|
149
|
+
export function hypStatus(h, known) {
|
|
150
|
+
const ev = (h && h.ev) || [];
|
|
151
|
+
const net = ev.reduce((a, e) => a + (e.dir === 'supports' ? e.w : -e.w), 0);
|
|
152
|
+
const status = net >= 3 ? 'supported' : net <= -2 ? 'refuted' : 'open';
|
|
153
|
+
const has = known instanceof Set ? (s) => known.has(s) : known ? (s) => !!known[s] : () => false;
|
|
154
|
+
const untraced = ev.filter((e) => !has(e.src)).map((e) => e.src || '(no source)');
|
|
155
|
+
const sup = ev.filter((e) => e.dir === 'supports').length; const ref = ev.length - sup;
|
|
156
|
+
const shown = status !== 'open' ? status : !ev.length ? 'untested' : sup && ref ? 'mixed' : 'open';
|
|
157
|
+
return { status, shown, net, untraced, supports: sup, refutes: ref };
|
|
158
|
+
}
|
|
159
|
+
export const HYP_LABEL = { supported: 'Supported', refuted: 'Refuted', mixed: 'Mixed', untested: 'Untested', open: 'Open' };
|
|
160
|
+
|
|
161
|
+
/** The six gates the mockup draws, and the engine step each reads (scripts/workflow-state.js steps; the rule text of each step is its `basis.rule`):
|
|
162
|
+
* Spec = spec, Rubric = contract (done criteria and test method agreed, D13), Build = build, Quality = quality, Review = review, Acceptance = accept. */
|
|
163
|
+
export const GATES = [['Spec', 'spec'], ['Rubric', 'contract'], ['Build', 'build'], ['Quality', 'quality'], ['Review', 'review'], ['Acceptance', 'accept']];
|
|
164
|
+
const dayOf = (v) => { const m = /^(\d{4}-\d{2}-\d{2})/.exec(String(v || '')); return m ? m[1] : ''; };
|
|
165
|
+
/** The gate ladder from one initiative's ladder row (the steps of `GET /api/console?name=ladder`). Each gate: { name, key, state, evidence, rule, date }.
|
|
166
|
+
* state is done | next (the first gate not done, the one in progress) | open (not yet) | na (cut by the PM or not required) | unknown (the engine does not claim it, e.g. CI decides).
|
|
167
|
+
* A violation is a gate done before an earlier gate that is not done (reader.js 14 `viol`): "Build before Rubric"; with first-seen dates, also a gate done on an earlier day than the gate before it. */
|
|
168
|
+
export function gateLadder(row) {
|
|
169
|
+
const steps = (row && Array.isArray(row.steps)) ? row.steps : [];
|
|
170
|
+
const by = Object.fromEntries(steps.map((s) => [s.key, s]));
|
|
171
|
+
let nextAt = -1;
|
|
172
|
+
const gates = GATES.map(([name, key], i) => {
|
|
173
|
+
const s = by[key] || null;
|
|
174
|
+
let state = !s ? 'unknown' : s.status === 'done' ? 'done' : s.status === 'na' ? 'na' : s.status === 'unknown' ? 'unknown' : 'open';
|
|
175
|
+
if (state === 'open' && nextAt < 0) { nextAt = i; state = 'next'; }
|
|
176
|
+
return { name, key, state, evidence: s ? String(s.evidence || '') : 'the engine returned no step for this gate', rule: s && s.basis ? String(s.basis.rule || '') : '', date: s && s.basis ? dayOf(s.basis.first_seen) : '', blocking: !!(s && s.blocking) };
|
|
177
|
+
});
|
|
178
|
+
const viol = [];
|
|
179
|
+
gates.forEach((g, i) => {
|
|
180
|
+
if (g.state !== 'done') return;
|
|
181
|
+
gates.slice(0, i).forEach((p) => {
|
|
182
|
+
if (p.state === 'na' || p.state === 'unknown') return;
|
|
183
|
+
if (p.state !== 'done') viol.push(g.name + ' before ' + p.name);
|
|
184
|
+
else if (g.date && p.date && g.date < p.date) viol.push(g.name + ' before ' + p.name + ' (' + g.date + ' against ' + p.date + ')');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
const next = gates.find((g) => g.state === 'next') || null;
|
|
188
|
+
return { gates, next, violations: viol, done: gates.filter((g) => g.state === 'done').length, counted: gates.filter((g) => g.state !== 'na').length, nextAction: row && row.next ? row.next : null, blockers: (row && row.blockers) || [], failures: (row && row.gate_failures) || [] };
|
|
189
|
+
}
|
|
190
|
+
/** The cells of the mockup's `.gt` bar: 'd' done, 'h' next (half), '' open, 'na', 'u' (unknown). */
|
|
191
|
+
export const gateCell = (g) => (g.state === 'done' ? 'd' : g.state === 'next' ? 'h' : g.state === 'na' ? 'na' : g.state === 'unknown' ? 'u' : '');
|
|
192
|
+
|
|
193
|
+
/** An evaluation row (planning/runs/evals.jsonl: score, threshold or pass_threshold, dimensions as { name: score }, verdict, evaluator_model) as the scorecard.
|
|
194
|
+
* The recorded score is shown as recorded. A weight is used only when a row carries one (`dims` as [{ dim, w, score }], the sample's form); otherwise the
|
|
195
|
+
* dimensions are unweighted and the page says so. Dimensions that are not numbers are dropped. */
|
|
196
|
+
export function normEval(e) {
|
|
197
|
+
if (!e || typeof e !== 'object') return null;
|
|
198
|
+
let dims = [];
|
|
199
|
+
if (Array.isArray(e.dims)) dims = e.dims.filter((d) => d && isNum(d.score)).map((d) => ({ name: String(d.dim || d.name || ''), w: isNum(d.w) ? d.w : null, score: d.score }));
|
|
200
|
+
else if (e.dimensions && typeof e.dimensions === 'object') dims = Object.entries(e.dimensions).filter(([, v]) => isNum(v)).map(([k, v]) => ({ name: k, w: null, score: v }));
|
|
201
|
+
const weighted = dims.length && dims.every((d) => d.w != null);
|
|
202
|
+
const computed = weighted ? dims.reduce((a, d) => a + d.w * d.score, 0) : dims.length ? dims.reduce((a, d) => a + d.score, 0) / dims.length : null;
|
|
203
|
+
const score = isNum(e.score) ? e.score : computed;
|
|
204
|
+
const thr = isNum(e.threshold) ? e.threshold : isNum(e.pass_threshold) ? e.pass_threshold : null;
|
|
205
|
+
return { score, threshold: thr, dims, weighted: !!weighted, verdict: String(e.verdict || ''), model: String(e.evaluator_model || ''), at: String(e.recorded_at || e.at || ''), run: String(e.run_id || e.run || '') };
|
|
206
|
+
}
|
|
207
|
+
/** The verdict of a scorecard: passes (score at or over the threshold), the weakest dimension, and the watermelon guard (reader.js 15: a pass while a dimension scores under 0.6). */
|
|
208
|
+
export function evalVerdict(ev) {
|
|
209
|
+
if (!ev || ev.score == null) return null;
|
|
210
|
+
const weakest = ev.dims.slice().sort((a, b) => a.score - b.score)[0] || null;
|
|
211
|
+
const passes = ev.threshold != null ? ev.score >= ev.threshold : null;
|
|
212
|
+
const watermelon = !!(passes && weakest && weakest.score < 0.6);
|
|
213
|
+
return { passes, weakest, watermelon, below: ev.threshold != null && !passes };
|
|
214
|
+
}
|
|
215
|
+
/** The newest evaluation per initiative from a list of rows (recorded_at, then file order). */
|
|
216
|
+
export function latestEvals(rows) {
|
|
217
|
+
const out = {};
|
|
218
|
+
(rows || []).forEach((r, i) => { const id = r && r.initiative_id; if (!id) return; const at = String(r.recorded_at || r.at || ''); const p = out[id]; if (!p || at >= p.at) out[id] = { at, i, row: r }; });
|
|
219
|
+
return Object.fromEntries(Object.entries(out).map(([k, v]) => [k, v.row]));
|
|
220
|
+
}
|
|
221
|
+
/** Corrections per run (reader.js 16, lower is better), two decimals, for each bucket and the change from the first to the last. */
|
|
222
|
+
export function correctionRate(series) {
|
|
223
|
+
const s = (series || []).filter((x) => x && x.runs > 0);
|
|
224
|
+
if (!s.length) return null;
|
|
225
|
+
const rate = (x) => Math.round(x.corrections / x.runs * 100) / 100;
|
|
226
|
+
const vals = s.map(rate);
|
|
227
|
+
return { now: vals[vals.length - 1], from: vals[0], vals, improved: vals[vals.length - 1] < vals[0] };
|
|
228
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/* pm/lifecycle-lens.js — the Lifecycle lens of Initiatives (console3 wave B1; R4b: on the console3 mockup's own markup and CSS).
|
|
2
|
+
*
|
|
3
|
+
* The mockup (prototype/pmos-console-handoff/mockups/console3.html) is the UI. Lifted: the view bar `.calbar` with the nine stages
|
|
4
|
+
* as `.seg` buttons (icon, label, `.bdg.g` count) and the scope as `.chips2` pills (iniHead, line 3576); the lens header `.lh`
|
|
5
|
+
* (iniLensHead, line 3579); `.calwrap > .calbody > .brief.sbb > .bcols > .bcol.l/.r` (vIni, line 3600); the card deck `.cdeck >
|
|
6
|
+
* .deck > .fc` with the dots `.dk .dkd` (iniDeck, line 3580); the lifecycle line `.lc` for one initiative (lifecycle, line 3487;
|
|
7
|
+
* the mockup's seven columns are nine here, as the mockup's own override at c3:20 makes them); and the stage's list as
|
|
8
|
+
* `section.card.sect > .sh2 + ul.list > li.nrow` with a case file in `.xp > .xi` (c3needs, line 4714). Styles: pm/console3.css.
|
|
9
|
+
*
|
|
10
|
+
* What is real: the rows of the Initiatives view ctx (the roadmap horizons rows) and the nine stages derived from them
|
|
11
|
+
* (lifecycle-lens-model.js). Stage is derived, never set; a stage with no signal reads "no signal yet", never done or pending;
|
|
12
|
+
* a stage the lane skips reads skipped. Park (on hold) and Kill (cancelled) go through setLifecycle with a reason, as the sheet's
|
|
13
|
+
* lifecycle menu does. `nine_stages` is not in that payload yet, so the model mirrors scripts/nine-stages.js; the page says so.
|
|
14
|
+
* Not lifted: the mockup's invented cards (hypotheses, scores, ideas, charts, lines 3490 to 3562) and its right-column charts;
|
|
15
|
+
* the Build and Deliver lists of Jira tickets and releases (Releases is its own view).
|
|
16
|
+
* What a stage needs (c3needs, line 4713): one row per requirement of the stage (REQ in the model), for each initiative at it, each with a
|
|
17
|
+
* case file in place: an agent's requirement offers "Have the agent: draft it" and "Not yet"; the PM's decision offers its two answers;
|
|
18
|
+
* the factory's is read-only. This page cannot start an agent or record a stage decision, so in real mode those slides are drawn
|
|
19
|
+
* disabled with the reason; the demo (?demo=1) lets the slide mark it done. Park or Kill goes through the lifecycle route with the reason
|
|
20
|
+
* as the option. Swipe right or left and the keys e, n, j, k work on every row. A deck card per initiative at the stage (five at most; the rest are in
|
|
21
|
+
* the list) replaces the mockup's five invented cards.
|
|
22
|
+
* Self-registers as the 'lifecycle' Initiatives view, like Timeline and Releases.
|
|
23
|
+
*/
|
|
24
|
+
import { el } from './dom.js';
|
|
25
|
+
import { ensureComponentsCss } from './primitives.js';
|
|
26
|
+
import { registerInitiativesView } from './registry.js';
|
|
27
|
+
import { setLifecycle } from './initiatives-api.js';
|
|
28
|
+
import { STAGES, LABELS, bucketRows, stageNote, nineOf, REQ, reqWho, prevPending, DRAFT_TEXT, DECIDE_TEXT } from './lifecycle-lens-model.js';
|
|
29
|
+
import { rowShell, caseNode, markDone, say } from './c3-init-extras.js';
|
|
30
|
+
import { lensPanels } from './c3-lifecycle-panels.js';
|
|
31
|
+
import { strategyPanel } from './c3-strategy-panels.js';
|
|
32
|
+
import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
|
|
33
|
+
|
|
34
|
+
const views = new WeakMap();
|
|
35
|
+
const PILLS = 7; // the mockup's scope row has five initiatives; a repository has hundreds, so the row shows the first few and says so
|
|
36
|
+
const LIST_MAX = 20; // a stage's list shows this many rows, then "Show all"
|
|
37
|
+
const ICON = { discover: 'search', ideate: 'spark', strategize: 'flag', prioritize: 'scale', plan: 'tree', build: 'branch', deliver: 'out', monitor: 'report', evaluate: 'check' }; // ILI, lines 3437 and 4686
|
|
38
|
+
const short = (t, n) => { t = String(t || ''); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
39
|
+
|
|
40
|
+
/* ── the mockup's Strategize lens (console3.html nsChart line 2833, sigCard 2883, matCard 2894), drawn only when ctx.strategy is given. The
|
|
41
|
+
* roadmap route does not send one; the /c3.html demo mode does (the mockup's north star series, early signals and maturity table). ───── */
|
|
42
|
+
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
43
|
+
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; };
|
|
44
|
+
const svt = (x, y, t, a) => { const n = sv('text', { x, y, ...(a || {}) }); n.textContent = t; return n; };
|
|
45
|
+
const SEV = { watch: ['Watch', 'var(--acc)', 'eye'], drift: ['Drift', 'var(--warning)', 'clock'], pivot: ['Pivot', 'var(--destructive)', 'alert'], up: ['Upside', 'var(--success)', 'up'] };
|
|
46
|
+
|
|
47
|
+
function miniSpark(a, c) {
|
|
48
|
+
const W = 64; const H = 26; const mn = Math.min(...a); const mx = Math.max(...a); const X = (i) => 3 + i * (W - 6) / (a.length - 1); const Y = (v) => H - 4 - (v - mn) / (mx - mn || 1) * (H - 8);
|
|
49
|
+
return sv('svg', { class: 'spk', viewBox: `0 0 ${W} ${H}`, 'aria-hidden': 'true', style: 'width:64px;height:26px' }, sv('polyline', { points: a.map((v, i) => X(i) + ',' + Y(v)).join(' '), fill: 'none', stroke: c, 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }), sv('circle', { cx: X(a.length - 1), cy: Y(a[a.length - 1]), r: 3, fill: c }));
|
|
50
|
+
}
|
|
51
|
+
function signalsCard(list) {
|
|
52
|
+
const order = ['pivot', 'drift', 'watch', 'up'];
|
|
53
|
+
return el('section', { class: 'card', 'data-lc-strategy': 'signals' }, el('h3', null, c3ic('alert'), 'Early signals ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'before the numbers say it')),
|
|
54
|
+
el('ul', { class: 'sgl', style: 'list-style:none;margin:0;padding:0' }, list.slice().sort((a, b) => order.indexOf(a.sev) - order.indexOf(b.sev)).slice(0, 4).map((g) => { const V = SEV[g.sev];
|
|
55
|
+
return el('li', { class: 'sgr', style: '--c:' + V[1] }, el('span', { class: 'ki', style: 'color:' + V[1] }, c3ic(V[2])),
|
|
56
|
+
el('div', { style: 'min-width:0' }, el('div', { class: 'st1' }, g.t), el('div', { class: 'st2' }, g.ev), el('div', { class: 'st3' }, el('span', { class: 'sv' }, V[0]), el('span', null, c3ic('target', 'sm'), g.thr), el('span', null, c3ic('clock', 'sm'), g.ttt))), miniSpark(g.sp, V[1])); })));
|
|
57
|
+
}
|
|
58
|
+
/** The mockup's lifecycle(id) line: one cell per stage; done is `d`, the current one `n`. */
|
|
59
|
+
function lcLine(row) {
|
|
60
|
+
const nine = nineOf(row);
|
|
61
|
+
return el('div', { class: 'lc', role: 'img', 'aria-label': 'Lifecycle' }, nine.map((s) => el('div', {
|
|
62
|
+
class: s.status === 'done' ? 'd' : s.status === 'current' ? 'n' : '', title: s.label + ': ' + (s.status === 'no-signal' ? 'no signal yet' : s.status),
|
|
63
|
+
'data-lc-cell': s.key, 'data-status': s.status, style: s.status === 'skipped' ? 'opacity:.4' : null,
|
|
64
|
+
}, el('i'), (s.label || s.key).slice(0, 5))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One deck card for an initiative at the stage: the mockup's card (`.fc > .cf > .cfa + .cfh + .cff`) with real fields only. */
|
|
68
|
+
function deckCard(V, row, i, n) {
|
|
69
|
+
const nine = nineOf(row);
|
|
70
|
+
const signalled = nine.filter((s) => s.status === 'done' || s.status === 'current' || s.status === 'pending');
|
|
71
|
+
const done = nine.filter((s) => s.status === 'done').length;
|
|
72
|
+
const cur = nine.find((s) => s.status === 'current');
|
|
73
|
+
const open = () => V.ctx.openInitiative(V.ctx.repo, row.id);
|
|
74
|
+
return el('article', { class: 'fc lnk' + (i === 0 ? ' on' : ''), tabindex: '0', role: 'button', 'aria-label': 'Open ' + row.title + ', card ' + (i + 1) + ' of ' + n,
|
|
75
|
+
'data-lc-deck': row.id, style: '--tc:var(--acc)', onClick: open, onKeydown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } } },
|
|
76
|
+
el('div', { class: 'ff' },
|
|
77
|
+
el('div', null, el('span', { class: 'tg f', style: '--tc:var(--acc)' }, c3ic('init', 'sm'), row.lane || 'feature')),
|
|
78
|
+
el('div', { class: 'cf' },
|
|
79
|
+
el('div', { class: 'cfa' }, el('h2', null, row.title)),
|
|
80
|
+
el('div', { class: 'cfh' }, el('div', { class: 'hero', 'data-hero': 'ring' }, c3ring(done, Math.max(1, signalled.length), 64, done + '/' + signalled.length))),
|
|
81
|
+
el('div', { class: 'cff' },
|
|
82
|
+
el('div', { class: 'cfx' }, el('span', null, c3ic('clock', 'sm'), cur ? 'now: ' + cur.label : 'all signalled stages are done')),
|
|
83
|
+
el('span', { class: 'cfr n' }, 'Tap to open it ', c3ic('next', 'sm'))))));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ── what a stage needs: one row per requirement, each with a case file (console3.html c3needs, line 4713; C3.cases.rq line 4719) ── */
|
|
87
|
+
const isDemo = () => !!window.__c3demo;
|
|
88
|
+
/** Is a requirement done for an initiative at this stage? The nine-stage rule says a stage that is done has all its requirements; at the current stage nothing is
|
|
89
|
+
* recorded per requirement, so none is shown done unless the PM's slide has just recorded it (the demo also carries the mockup's own table). */
|
|
90
|
+
function reqDone(V, row, L, key) {
|
|
91
|
+
const st = (nineOf(row).find((x) => x.key === L) || {}).status;
|
|
92
|
+
if (st === 'done') return true;
|
|
93
|
+
if ((V.rq || {})[row.id + '|' + key]) return true;
|
|
94
|
+
const d = window.__c3reqDone && window.__c3reqDone[row.id]; return !!(d && d[key]);
|
|
95
|
+
}
|
|
96
|
+
const openIt = (V, row) => V.ctx.openInitiative(V.ctx.repo, row.id);
|
|
97
|
+
|
|
98
|
+
function rqCase(V, row, L, r) {
|
|
99
|
+
const [key, text, who] = r; const nm = row.title; const stg = LABELS[L]; const demo = isDemo();
|
|
100
|
+
const done = reqDone(V, row, L, key);
|
|
101
|
+
const lane = nineOf(row); void lane;
|
|
102
|
+
const base = [['init', nm + ', ' + (row.lane || 'feature') + ' lane', 'Lane'], ['target', 'Anchor: ' + (row.anchor || 'none'), 'Anchor', row.anchor ? 'ok' : 'bad']];
|
|
103
|
+
const go = ['open', row.id, 'Open ' + nm];
|
|
104
|
+
if (done) return { sit: text + ' is done for ' + nm + '.', conf: 90, chg: [['check', 'Counted in the derived stage', '', 'ok']], opts: [], go };
|
|
105
|
+
if (who === 'factory') return { sit: 'The factory builds from the tickets and opens pull requests. Its progress fills this by itself. You cannot tick it.', conf: 88, chg: [['branch', 'Merged work is counted from Jira, read-only', 'Jira, CI', 'ok'], ['shield', 'The factory is propose-only: it never merges and never writes this record', 'D65']], opts: [], go };
|
|
106
|
+
if (who === 'agent') {
|
|
107
|
+
const t = DRAFT_TEXT[key] || ['Draft it', ''];
|
|
108
|
+
return { sit: 'Nothing has this yet. An agent drafts it and you read it before it counts. ' + nm + ' stays at ' + stg + ' until it exists.', conf: 82, chg: base, cta: 'Slide to start the draft', go,
|
|
109
|
+
outboxReason: 'An agent session does the drafting, and this page cannot start one. Ask it in your terminal; the draft comes back for you to read.',
|
|
110
|
+
opts: [{ id: 'go', l: 'Have the agent: ' + t[0].toLowerCase(), rec: 1, outward: !demo, fx: [['spark', t[1], 'ok'], ['shield', 'Inside the repo; nothing goes outside', 'ok']] }, { id: 'nt', l: 'Not yet', fx: [['clock', 'The stage stays at ' + stg, 'warn']] }] };
|
|
111
|
+
}
|
|
112
|
+
const pend = prevPending(L, key, (k) => reqDone(V, row, L, k));
|
|
113
|
+
if (pend.length) return { sit: 'This is your decision, and it comes after the agent’s drafts. Read them first: approving without reading is how errors go out.', conf: 70, chg: [['clock', 'Waiting for: ' + pend.map((q) => q[1]).join('; '), 'agent', 'warn']], opts: [], go };
|
|
114
|
+
const d = DECIDE_TEXT[key] || [['Decide yes', ''], ['Not yet', ''], 'This is your decision.'];
|
|
115
|
+
return { sit: d[2], conf: key === 'cont' ? 62 : 78, chg: [['init', nm + ' · ' + stg, 'Stage'], base[1]], cta: 'Slide to decide', unsure: key === 'cont' ? 'It is a close call.' : '', ign: 'The initiative stays at ' + stg + '.', go,
|
|
116
|
+
outboxReason: 'This page has no route to record that decision yet, so it stays a proposal. Nothing is written.',
|
|
117
|
+
opts: [{ id: 'ok', l: d[0][0], rec: 1, outward: !demo, fx: [['check', d[0][1], 'ok']] }, { id: 'no', l: d[1][0], fx: [['clock', d[1][1], 'warn']] }] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A requirement row. Swipe right starts the draft (the agent's requirements only); swipe left leaves it. Both say what they did; a draft only starts where the page can start one. */
|
|
121
|
+
function rqRow(V, row, L, r) {
|
|
122
|
+
const key = row.id + '|' + r[0]; const w = reqWho(r[2]); const done = reqDone(V, row, L, r[0]); const scope = V.scope;
|
|
123
|
+
const decide = (o) => {
|
|
124
|
+
if (o.id === 'go' || o.id === 'ok') { V.rq = V.rq || {}; V.rq[key] = 1; const m = o.id === 'go' ? 'Drafted. It waits in Review for you.' : o.l + '. Recorded as your decision.'; say(m, () => { delete V.rq[key]; V.render(); }); markDone(V, 'rq:' + key, m, () => { delete V.rq[key]; }); } else markDone(V, 'rq:' + key, 'Left as it is.');
|
|
125
|
+
};
|
|
126
|
+
const sw = { labelRight: r[2] === 'agent' ? 'start the draft' : 'open it first', labelLeft: 'leave it', iconRight: 'spark', iconLeft: 'moon',
|
|
127
|
+
onRight: () => say(r[2] === 'agent' && !done ? (isDemo() ? 'Open it, pick the draft, then slide: a draft starts only from its case file.' : 'An agent session does the drafting. This page cannot start one.') : done ? 'Already done.' : 'Open it first: a decision needs its case file.'),
|
|
128
|
+
onLeft: () => say('Left as it is.') };
|
|
129
|
+
const pend = !done && r[2] === 'pm' && prevPending(L, r[0], (k) => reqDone(V, row, L, k)).length;
|
|
130
|
+
return rowShell(V, { key: 'rq' + key, attrs: { 'data-rq': key, 'data-id': 'rq|' + key, 'data-lc-card': row.id }, icon: done ? 'check' : w[0], iconColor: done ? 'var(--success)' : null, sw,
|
|
131
|
+
titleNode: el('span', { class: 'tt', style: done ? 'text-decoration:line-through;color:var(--muted-foreground)' : '' }, r[1]),
|
|
132
|
+
xpContent: caseNode(V, 'rq:' + key, rqCase(V, row, L, r), { onConfirm: decide, onGo: () => openIt(V, row) }),
|
|
133
|
+
chips: [el('span', { class: 'tag' }, w[1]), scope ? null : el('span', { class: 'chip2' }, c3ic('init', 'sm'), row.title), pend ? el('span', { class: 'chip2' }, c3ic('clock', 'sm'), 'after the agent’s drafts') : null] });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Park it or kill it (console3.html C3.cases.ini, line 4745): the reason is the option, so a reason is always recorded. Both go through the same lifecycle route as the initiative sheet. */
|
|
137
|
+
function parkRow(V, row) {
|
|
138
|
+
const key = 'par|' + row.id; const demo = isDemo();
|
|
139
|
+
const mk = (id, why, kill) => ({ id, l: (kill ? 'Kill it: ' : 'Park it: ') + why, fx: [['moon', kill ? 'Closed for good' : 'Comes back to the planning list', 'warn']], state: kill ? 'cancelled' : 'on-hold', why });
|
|
140
|
+
const opts = [{ id: 'keep', l: 'Keep it going', rec: 1, fx: [['check', 'Nothing changes', 'ok']] }, mk('p1', 'not now'), mk('p2', 'no evidence'), mk('p3', 'replaced'), mk('k1', 'out of scope', true)];
|
|
141
|
+
const decide = async (o) => {
|
|
142
|
+
if (o.id === 'keep') { markDone(V, 'rq:' + key, 'Kept going.'); return; }
|
|
143
|
+
const out = await setLifecycle(V.ctx.ws, row.id, o.state, o.why);
|
|
144
|
+
if (!out || !out.ok) { say((out && out.error) || 'Not recorded.'); return; }
|
|
145
|
+
try { window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ }
|
|
146
|
+
const undo = async () => { await setLifecycle(V.ctx.ws, row.id, 'active', 'Undone from the lifecycle lens'); try { window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ } };
|
|
147
|
+
say((o.state === 'cancelled' ? 'Killed: ' : 'Parked: ') + o.why + '.', undo); markDone(V, 'rq:' + key, (o.state === 'cancelled' ? 'Killed: ' : 'Parked: ') + o.why + '. Undo reopens it.', undo);
|
|
148
|
+
};
|
|
149
|
+
void demo;
|
|
150
|
+
return rowShell(V, { key: 'rq' + key, attrs: { 'data-lc-park-row': row.id, 'data-id': 'par|' + row.id }, icon: 'moon', hint: false,
|
|
151
|
+
titleNode: el('span', { class: 'tt' }, 'Park it or kill it'),
|
|
152
|
+
xpContent: caseNode(V, 'rq:' + key, { sit: 'Stopping is a decision too. Pick the reason; it is recorded and the agent reads it before suggesting the idea again.', conf: 74, chg: [['init', row.title, 'Initiative']], opts, cta: 'Slide to record it', go: ['open', row.id, 'Open ' + row.title] }, { onConfirm: decide, onGo: () => openIt(V, row) }),
|
|
153
|
+
chips: [el('span', { class: 'tag' }, 'you'), el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'a reason is always recorded'), V.scope ? null : el('span', { class: 'chip2' }, c3ic('init', 'sm'), row.title)] });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function render(V) {
|
|
157
|
+
const rows = V.ctx.rows || [];
|
|
158
|
+
const scope = V.scope;
|
|
159
|
+
const { buckets, unplaced } = bucketRows(rows, scope);
|
|
160
|
+
const ST = V.ctx.strategy || null; // optional (demo): the mockup's Strategize lens content; the lens opens on it
|
|
161
|
+
if (!V.lens) V.lens = ST ? 'strategize' : STAGES.find((k) => buckets[k].length) || 'discover';
|
|
162
|
+
const lens = V.lens;
|
|
163
|
+
const stratOn = lens === 'strategize'; // the Calendar's strategy panels (c3-strategy-panels.js), as the mockup draws them under Strategize (iniLeft 3586, iniRight 3595)
|
|
164
|
+
const list = buckets[lens];
|
|
165
|
+
const note = stageNote(lens, rows, scope);
|
|
166
|
+
|
|
167
|
+
const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Initiative lifecycle', style: 'overflow-x:auto;max-width:100%' }, STAGES.map((k) => {
|
|
168
|
+
const n = buckets[k].length;
|
|
169
|
+
return el('button', { type: 'button', 'aria-pressed': String(lens === k), 'data-lc-stage': k, 'data-lc-note': stageNote(k, rows, scope) || '', onClick: () => { V.lens = k; render(V); } },
|
|
170
|
+
c3ic(ICON[k], 'sm'), LABELS[k], n ? [' ', el('span', { class: 'bdg g' }, String(n))] : null);
|
|
171
|
+
}));
|
|
172
|
+
const pills = el('div', { class: 'chips2', 'data-lc-scope': '' },
|
|
173
|
+
el('button', { type: 'button', class: 'pill', 'aria-pressed': String(!scope), onClick: () => { V.scope = null; V.lens = null; render(V); } }, 'All'),
|
|
174
|
+
...rows.slice(0, PILLS).map((r) => el('button', { type: 'button', class: 'pill', 'aria-pressed': String(scope === r.id), 'data-lc-pill': r.id, title: r.title, onClick: () => { V.scope = r.id; V.lens = null; render(V); } }, short(r.title, 14))),
|
|
175
|
+
rows.length > PILLS ? el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)', 'data-lc-more': '' }, '+' + (rows.length - PILLS) + ' more in the list') : null);
|
|
176
|
+
|
|
177
|
+
const placed = STAGES.reduce((n, k) => n + buckets[k].length, 0);
|
|
178
|
+
const PN = stratOn ? null : lensPanels(V, lens, scope); // the panels the mockup draws for this stage (c3-lifecycle-panels.js)
|
|
179
|
+
const HD = PN && PN.head;
|
|
180
|
+
const head = stratOn && ST ? el('div', { class: 'lh', 'data-lc-derived': '' }, el('span', { class: 'ki' }, c3ic('flag')), el('div', { style: 'min-width:0' }, el('b', null, ST.head.h), el('div', { class: 'dim' }, ST.head.s)), c3ring(ST.head.ring[0], ST.head.ring[1], 44, ST.head.ring[0] + '/' + ST.head.ring[1]))
|
|
181
|
+
: HD ? el('div', { class: 'lh', 'data-lc-derived': '', 'data-lc-head': lens }, el('span', { class: 'ki' }, c3ic(HD.icon)), el('div', { style: 'min-width:0' }, el('b', null, HD.h), el('div', { class: 'dim' }, HD.s)), c3ring(HD.ring[0], Math.max(1, HD.ring[1]), 44, HD.ring[0] + '/' + HD.ring[1]))
|
|
182
|
+
: el('div', { class: 'lh', 'data-lc-derived': '' },
|
|
183
|
+
el('span', { class: 'ki' }, c3ic(ICON[lens])),
|
|
184
|
+
el('div', { style: 'min-width:0' },
|
|
185
|
+
el('b', null, note === 'skipped' ? LABELS[lens] + ': skipped by the lane' : note === 'no-signal' ? LABELS[lens] + ': no signal yet' : list.length + ' initiative' + (list.length === 1 ? '' : 's') + ' at ' + LABELS[lens]),
|
|
186
|
+
el('div', { class: 'dim' }, 'Stage is derived from the records; nothing here sets it.')),
|
|
187
|
+
c3ring(placed, Math.max(1, scope ? 1 : rows.length), 44, placed + '/' + (scope ? 1 : rows.length)));
|
|
188
|
+
|
|
189
|
+
const sctx = { goInit: (id) => V.ctx.openInitiative(V.ctx.repo, id) };
|
|
190
|
+
const deckCards = list.slice(0, 5);
|
|
191
|
+
const deck = deckCards.length ? el('section', { class: 'cdeck', 'data-n': '1' },
|
|
192
|
+
el('div', { class: 'deck', tabindex: '-1', role: 'group', 'aria-label': 'Initiatives at ' + LABELS[lens] }, deckCards.map((r, i) => deckCard(V, r, i, deckCards.length))),
|
|
193
|
+
deckCards.length > 1 ? el('div', { class: 'dk', role: 'group', 'aria-label': 'Cards' }, deckCards.map((r, i) => el('button', { type: 'button', class: 'dkd', 'aria-label': 'Card ' + (i + 1), 'aria-current': String(i === 0) }))) : null) : null;
|
|
194
|
+
if (deck) {
|
|
195
|
+
const d = deck.querySelector('.deck');
|
|
196
|
+
d.addEventListener('scroll', () => {
|
|
197
|
+
const f = d.children[0]; if (!f) return;
|
|
198
|
+
const i = Math.max(0, Math.min(d.children.length - 1, Math.round(d.scrollLeft / (f.offsetWidth + 12))));
|
|
199
|
+
[].forEach.call(d.children, (c, j) => c.classList.toggle('on', j === i));
|
|
200
|
+
deck.querySelectorAll('.dkd').forEach((x, j) => x.setAttribute('aria-current', String(j === i)));
|
|
201
|
+
});
|
|
202
|
+
deck.querySelectorAll('.dkd').forEach((b, j) => b.addEventListener('click', () => { const c = d.children[j]; if (c) d.scrollTo({ left: c.offsetLeft - d.children[0].offsetLeft, behavior: 'smooth' }); }));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const scoped = scope ? rows.find((r) => r.id === scope) : null;
|
|
206
|
+
const lcCard = scoped ? el('section', { class: 'card' }, el('h3', null, c3ic('init'), scoped.title + ' ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'lifecycle')), lcLine(scoped)) : null;
|
|
207
|
+
const reqRows = [];
|
|
208
|
+
const needRows = scope ? rows.filter((r) => r.id === scope && (nineOf(r).find((x) => x.key === lens) || {}).status !== 'skipped') : list; // the mockup (c3needs, line 4715): a chosen initiative shows its rows for any stage its lane uses
|
|
209
|
+
for (const row of needRows) { REQ[lens].forEach((r) => reqRows.push(() => rqRow(V, row, lens, r))); reqRows.push(() => parkRow(V, row)); }
|
|
210
|
+
const shown = V.all ? reqRows : reqRows.slice(0, LIST_MAX);
|
|
211
|
+
const needs = el('section', { class: 'card sect', 'data-lc-list': lens },
|
|
212
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('check', 'sm'), 'What ' + LABELS[lens] + ' needs'), el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'stage is derived, not chosen')),
|
|
213
|
+
el('ul', { class: 'list' }, needRows.length ? [...shown.map((f) => f()),
|
|
214
|
+
reqRows.length > LIST_MAX && !V.all ? el('li', { class: 'fold' }, el('button', { type: 'button', class: 'pill', 'data-lc-showall': '', onClick: () => { V.all = true; render(V); } }, 'Show all ' + reqRows.length)) : null]
|
|
215
|
+
: el('li', { class: 'fold', 'data-lc-empty': '' }, c3ic(note === 'skipped' ? 'moon' : 'check', 'sm'),
|
|
216
|
+
note === 'skipped' || scope ? 'The ' + ((rows.find((r) => r.id === scope) || {}).lane || 'feature') + ' lane skips ' + LABELS[lens] + '.' : note === 'no-signal' ? 'No signal yet for ' + LABELS[lens] + '. Nothing is done or pending until a record shows it.' : 'No initiative is at ' + LABELS[lens] + ' now.')));
|
|
217
|
+
const right = [
|
|
218
|
+
...(PN && PN.right.length ? [...PN.right, ...(lens === 'monitor' && ST && ST.signals ? [signalsCard(ST.signals.slice(0, 3))] : [])] : lens === 'monitor' && ST && ST.signals ? [signalsCard(ST.signals.slice(0, 3))] : [el('section', { class: 'card' }, el('h3', null, c3ic('info'), 'How the stage is derived'),
|
|
219
|
+
el('ul', { class: 'chgl' },
|
|
220
|
+
el('li', null, c3ic('check', 'sm'), el('span', null, 'Discover, Plan, Build and Evaluate read from the records.')),
|
|
221
|
+
el('li', null, c3ic('clock', 'sm'), el('span', null, 'The other stages have no signal yet, so none is shown done or pending.')),
|
|
222
|
+
el('li', null, c3ic('moon', 'sm'), el('span', null, 'A stage the lane skips reads skipped.'))))]),
|
|
223
|
+
unplaced.length ? el('section', { class: 'card', 'data-lc-unplaced': '' }, el('h3', null, c3ic('alert'), 'No stage shown yet'),
|
|
224
|
+
el('ul', { class: 'chgl' }, unplaced.map((r) => el('li', null, c3ic('init', 'sm'), el('span', null, r.title))))) : null,
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
V.root.replaceChildren(el('div', { class: 'calwrap' },
|
|
228
|
+
el('div', { class: 'calbar' }, pills),
|
|
229
|
+
el('div', { class: 'calbar', style: 'flex-wrap:wrap;gap:8px' }, seg),
|
|
230
|
+
el('div', { class: 'calbody' },
|
|
231
|
+
head, V.ctx.links || null,
|
|
232
|
+
V.notice ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), V.notice) : null,
|
|
233
|
+
stratOn && rows.length
|
|
234
|
+
? el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, V.ctx.plan || deck, lcCard, strategyPanel('maturity', sctx), strategyPanel('bets', sctx), needs), el('div', { class: 'bcol r' }, strategyPanel('north', sctx, { view: 'year', plain: true }), strategyPanel('signals', sctx, { n: 4 }), strategyPanel('synergy', sctx))))
|
|
235
|
+
: rows.length
|
|
236
|
+
? el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, V.ctx.plan || deck, lcCard, ...(PN ? PN.left : []), needs, ...(PN ? PN.after : [])), el('div', { class: 'bcol r' }, right)))
|
|
237
|
+
: el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, V.ctx.plan || null, el('div', { class: 'fold' }, c3ic('check', 'sm'), 'No initiatives yet.')))))));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function mountLifecycle(host, ctx) {
|
|
241
|
+
let V = views.get(host);
|
|
242
|
+
if (V) { V.ctx = ctx; render(V); if (host.firstChild !== V.root) host.replaceChildren(V.root); return; }
|
|
243
|
+
ensureComponentsCss();
|
|
244
|
+
ensureC3Css();
|
|
245
|
+
V = { ctx, scope: null, lens: null, open: {}, notice: null, root: el('div', { class: 'c3', 'data-in-view': 'lifecycle' }) };
|
|
246
|
+
V.render = () => render(V);
|
|
247
|
+
V.stopPhone = c3Phone(V.root);
|
|
248
|
+
views.set(host, V);
|
|
249
|
+
host.replaceChildren(V.root);
|
|
250
|
+
render(V);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function disposeLifecycle(host) {
|
|
254
|
+
const V = views.get(host);
|
|
255
|
+
if (!V) return;
|
|
256
|
+
views.delete(host);
|
|
257
|
+
if (V.stopPhone) V.stopPhone();
|
|
258
|
+
if (V.root.parentNode === host) V.root.remove();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
registerInitiativesView('lifecycle', { label: 'Lifecycle', mount: mountLifecycle, dispose: disposeLifecycle });
|