create-pmos 0.134.0 → 0.135.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 +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +16 -1
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-capture.js +9 -1
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +10 -3
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +55 -9
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +1 -1
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +99 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +29 -8
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +46 -9
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +12 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +10 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* stage-tools.js — the records the PM edits per initiative and stage (problem statement, RICE, launch checklist, kill
|
|
3
|
+
* conditions, learnings), and the sprint burndown from the Jira snapshot (initiative stage-tools-records, PRD R1, R2, R4, R6).
|
|
4
|
+
*
|
|
5
|
+
* TOOLS · LEDGER = 'initiative-tools.jsonl'
|
|
6
|
+
* checkData(tool, data) → { ok: true, data } | { ok: false, error } the shape of one save; unknown fields dropped
|
|
7
|
+
* readLedger(file) → Map<initiative, Map<tool, { data, at, by }>> the last valid line per initiative and tool
|
|
8
|
+
* recordsOf(folded, initiative) → { problem, rice, launch, kill, learnings } each { data, at, by } or null; rice adds score
|
|
9
|
+
* appendTool(file, { initiative, tool, data }, actor, { now }) → the line written
|
|
10
|
+
* riceScore(data) → reach × impact × confidence/100 ÷ effort, one decimal
|
|
11
|
+
* riceRank(folded, ids) → [{ id, score, rank, reach, impact, confidence, effort, at }] highest score first
|
|
12
|
+
* buildDeliverOn(boardsFile, board) → false only when the board's own line says buildDeliver: false
|
|
13
|
+
* burndown(tickets, { sprint, today }) → { available: true, sprint, committed, done, left, ... } | { available: false, reason }
|
|
14
|
+
*
|
|
15
|
+
* THE LEDGER is one append-only file per repository, in that repository's state folder (the PM's choice, 2026-10-11): a
|
|
16
|
+
* save appends { v: 1, at, by, ev: 'tool', initiative, tool, data }. Nothing is rewritten. `by` is the guard's actor, never
|
|
17
|
+
* a field of the body. A line that does not parse, or whose data fails the shape check, is skipped by the read.
|
|
18
|
+
* The factory never writes this file (D65): the only writer is the page route, behind pm-guard requirePagePress.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const LEDGER = 'initiative-tools.jsonl';
|
|
24
|
+
const TOOLS = Object.freeze(['problem', 'rice', 'launch', 'kill', 'learnings']);
|
|
25
|
+
const IMPACTS = Object.freeze([0.25, 0.5, 1, 2, 3]);
|
|
26
|
+
const KILL_STATES = Object.freeze(['watching', 'tripped', 'cleared']);
|
|
27
|
+
const LIMITS = Object.freeze({ launch: 40, kill: 20, learnings: 60 });
|
|
28
|
+
const TEXT_MAX = { problem: 4000, item: 500 };
|
|
29
|
+
const ITEM_ID = /^[A-Za-z0-9][\w-]{0,39}$/;
|
|
30
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
31
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
32
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
33
|
+
const textOk = (v, max) => typeof v === 'string' && v.trim().length >= 1 && v.length <= max && !/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(v);
|
|
34
|
+
|
|
35
|
+
function checkItems(tool, items, one) {
|
|
36
|
+
if (!Array.isArray(items)) return { ok: false, error: `${tool} needs items: a list` };
|
|
37
|
+
if (items.length > LIMITS[tool]) return { ok: false, error: `${tool} holds at most ${LIMITS[tool]} items` };
|
|
38
|
+
const out = []; const seen = new Set();
|
|
39
|
+
for (let i = 0; i < items.length; i++) {
|
|
40
|
+
const it = items[i];
|
|
41
|
+
if (!isObj(it)) return { ok: false, error: `${tool} item ${i + 1} is not an object` };
|
|
42
|
+
if (!ITEM_ID.test(String(it.id || ''))) return { ok: false, error: `${tool} item ${i + 1} needs an id (letters, digits, - or _)` };
|
|
43
|
+
if (seen.has(it.id)) return { ok: false, error: `${tool} item id ${it.id} is used twice` };
|
|
44
|
+
seen.add(it.id);
|
|
45
|
+
if (!textOk(it.text, TEXT_MAX.item)) return { ok: false, error: `${tool} item ${i + 1} needs text of 1 to ${TEXT_MAX.item} characters on one line` };
|
|
46
|
+
if (/[\r\n]/.test(it.text)) return { ok: false, error: `${tool} item ${i + 1} text must be one line` };
|
|
47
|
+
const r = one(it, i);
|
|
48
|
+
if (typeof r === 'string') return { ok: false, error: r };
|
|
49
|
+
out.push({ id: it.id, text: it.text.trim(), ...r });
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, data: { items: out } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The shape of one save. Unknown fields are dropped, so a record carries only what the read promises. */
|
|
55
|
+
function checkData(tool, data) {
|
|
56
|
+
if (!TOOLS.includes(tool)) return { ok: false, error: `tool must be one of ${TOOLS.join(', ')}` };
|
|
57
|
+
if (!isObj(data)) return { ok: false, error: 'data must be an object' };
|
|
58
|
+
if (tool === 'problem') {
|
|
59
|
+
if (!textOk(data.text, TEXT_MAX.problem)) return { ok: false, error: `problem needs text of 1 to ${TEXT_MAX.problem} characters` };
|
|
60
|
+
return { ok: true, data: { text: data.text.trim() } };
|
|
61
|
+
}
|
|
62
|
+
if (tool === 'rice') {
|
|
63
|
+
const { reach, impact, confidence, effort } = data;
|
|
64
|
+
if (!num(reach) || reach < 0) return { ok: false, error: 'rice reach must be a number of 0 or more' };
|
|
65
|
+
if (!IMPACTS.includes(impact)) return { ok: false, error: `rice impact must be one of ${IMPACTS.join(', ')}` };
|
|
66
|
+
if (!num(confidence) || confidence < 0 || confidence > 100) return { ok: false, error: 'rice confidence must be a percentage from 0 to 100' };
|
|
67
|
+
if (!num(effort) || effort <= 0) return { ok: false, error: 'rice effort must be a number above 0 (person-months)' };
|
|
68
|
+
return { ok: true, data: { reach, impact, confidence, effort } };
|
|
69
|
+
}
|
|
70
|
+
if (tool === 'launch') return checkItems(tool, data.items, (it, i) => (typeof it.done === 'boolean' ? { done: it.done } : `launch item ${i + 1} needs done: true or false`));
|
|
71
|
+
if (tool === 'kill') return checkItems(tool, data.items, (it, i) => (KILL_STATES.includes(it.status) ? { status: it.status } : `kill item ${i + 1} needs status: ${KILL_STATES.join(', ')}`));
|
|
72
|
+
return checkItems(tool, data.items, (it, i) => (typeof it.date === 'string' && DATE_RE.test(it.date) ? { date: it.date } : `learnings item ${i + 1} needs date: YYYY-MM-DD`));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readLedger(file) {
|
|
76
|
+
const out = new Map();
|
|
77
|
+
let text = '';
|
|
78
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return out; }
|
|
79
|
+
for (const raw of text.split('\n')) {
|
|
80
|
+
if (!raw.trim()) continue;
|
|
81
|
+
let e;
|
|
82
|
+
try { e = JSON.parse(raw); } catch { continue; }
|
|
83
|
+
if (!isObj(e) || e.ev !== 'tool' || typeof e.initiative !== 'string' || !e.initiative) continue;
|
|
84
|
+
const c = checkData(e.tool, e.data);
|
|
85
|
+
if (!c.ok) continue;
|
|
86
|
+
if (!out.has(e.initiative)) out.set(e.initiative, new Map());
|
|
87
|
+
out.get(e.initiative).set(e.tool, { data: c.data, at: typeof e.at === 'string' ? e.at : null, by: typeof e.by === 'string' ? e.by : null });
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function riceScore(d) {
|
|
93
|
+
if (!d || !num(d.effort) || d.effort <= 0) return null;
|
|
94
|
+
return Math.round((d.reach * d.impact * (d.confidence / 100) / d.effort) * 10) / 10;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function recordsOf(folded, initiative) {
|
|
98
|
+
const mine = folded.get(initiative) || new Map();
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const t of TOOLS) {
|
|
101
|
+
const r = mine.get(t);
|
|
102
|
+
out[t] = r ? { ...r, data: t === 'rice' ? { ...r.data, score: riceScore(r.data) } : r.data } : null;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function appendTool(file, { initiative, tool, data }, actor, { now } = {}) {
|
|
108
|
+
const line = { v: 1, at: new Date(now == null ? Date.now() : now).toISOString(), by: (actor && actor.by) || 'pm:unknown', ev: 'tool', initiative, tool, data };
|
|
109
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
110
|
+
fs.appendFileSync(file, JSON.stringify(line) + '\n');
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function riceRank(folded, ids) {
|
|
115
|
+
const want = Array.isArray(ids) && ids.length ? new Set(ids) : null;
|
|
116
|
+
const rows = [];
|
|
117
|
+
for (const [id, tools] of folded) {
|
|
118
|
+
if (want && !want.has(id)) continue;
|
|
119
|
+
const r = tools.get('rice');
|
|
120
|
+
if (!r) continue;
|
|
121
|
+
rows.push({ id, score: riceScore(r.data), ...r.data, at: r.at });
|
|
122
|
+
}
|
|
123
|
+
rows.sort((a, b) => (b.score - a.score) || a.id.localeCompare(b.id));
|
|
124
|
+
let rank = 0; let last = null;
|
|
125
|
+
rows.forEach((r, i) => { if (r.score !== last) { rank = i + 1; last = r.score; } r.rank = rank; });
|
|
126
|
+
return rows;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The per-board "Build and deliver" switch, from the board's own lines in boards.jsonl (kind 'board'). The last line that
|
|
130
|
+
* names it wins. Missing file, missing board or a board that never set it: on. */
|
|
131
|
+
function buildDeliverOn(boardsFile, board) {
|
|
132
|
+
if (!boardsFile || !board) return true;
|
|
133
|
+
let text = '';
|
|
134
|
+
try { text = fs.readFileSync(boardsFile, 'utf8'); } catch { return true; }
|
|
135
|
+
let on = true;
|
|
136
|
+
for (const raw of text.split('\n')) {
|
|
137
|
+
if (!raw.trim()) continue;
|
|
138
|
+
let e;
|
|
139
|
+
try { e = JSON.parse(raw); } catch { continue; }
|
|
140
|
+
if (isObj(e) && e.kind === 'board' && e.board === board && typeof e.buildDeliver === 'boolean') on = e.buildDeliver;
|
|
141
|
+
}
|
|
142
|
+
return on;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── burndown ──────────────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
const DAY = 86400000;
|
|
147
|
+
const dayNum = (d) => Date.UTC(+d.slice(0, 4), +d.slice(5, 7) - 1, +d.slice(8, 10)) / DAY;
|
|
148
|
+
const dayStr = (n) => new Date(n * DAY).toISOString().slice(0, 10);
|
|
149
|
+
const r1 = (x) => Math.round(x * 10) / 10;
|
|
150
|
+
|
|
151
|
+
/** The sprint to draw: the named one, else the active one, else the latest that has dates. Sprints are keyed by id when
|
|
152
|
+
* Jira gave one, else by name. */
|
|
153
|
+
function pickSprint(tickets, name) {
|
|
154
|
+
const by = new Map();
|
|
155
|
+
for (const t of tickets) {
|
|
156
|
+
const s = t && t.sprint;
|
|
157
|
+
if (!s || !s.name) continue;
|
|
158
|
+
const k = s.id || s.name;
|
|
159
|
+
const cur = by.get(k) || { name: s.name, state: s.state, id: s.id || null, start: null, end: null, completed: null };
|
|
160
|
+
for (const f of ['start', 'end', 'completed']) if (s[f] && !cur[f]) cur[f] = s[f];
|
|
161
|
+
by.set(k, cur);
|
|
162
|
+
}
|
|
163
|
+
const all = [...by.values()];
|
|
164
|
+
if (name) return all.find((s) => s.name === name) || null;
|
|
165
|
+
return all.find((s) => s.state === 'active') || all.filter((s) => s.start).sort((a, b) => (b.start > a.start ? 1 : -1))[0] || all[0] || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Remaining points per day over the sprint, from the tickets' current sprint membership. Committed is the points of every
|
|
170
|
+
* ticket in the sprint; a ticket counts as burned on its resolved date (or, when Done with no date, on the sprint's
|
|
171
|
+
* completion date for a closed sprint). Scope added or removed mid-sprint is not visible: the snapshot has no history.
|
|
172
|
+
*/
|
|
173
|
+
function burndown(tickets, { sprint: name, today } = {}) {
|
|
174
|
+
const list = Array.isArray(tickets) ? tickets : [];
|
|
175
|
+
const s = pickSprint(list, name);
|
|
176
|
+
if (!s) return { available: false, reason: name ? `no ticket is in the sprint ${name}` : 'no ticket is in a sprint' };
|
|
177
|
+
if (!s.start || !s.end) return { available: false, reason: `the Jira read has no dates for ${s.name}: the next Jira status check adds them`, sprint: s };
|
|
178
|
+
const inSprint = list.filter((t) => t.sprint && (s.id ? t.sprint.id === s.id : t.sprint.name === s.name));
|
|
179
|
+
const pointed = inSprint.filter((t) => num(t.points));
|
|
180
|
+
if (!pointed.length) return { available: false, reason: `no ticket in ${s.name} has story points`, sprint: s, tickets: inSprint.length, unpointed: inSprint.length };
|
|
181
|
+
const t0 = dayNum(s.start); const t1 = dayNum(s.end);
|
|
182
|
+
if (t1 < t0) return { available: false, reason: `${s.name} ends before it starts`, sprint: s };
|
|
183
|
+
const now = dayNum(today || new Date().toISOString().slice(0, 10));
|
|
184
|
+
const burnDay = (t) => {
|
|
185
|
+
if (t.category !== 'done') return null;
|
|
186
|
+
if (t.resolved) return dayNum(t.resolved);
|
|
187
|
+
return s.completed ? dayNum(s.completed) : now;
|
|
188
|
+
};
|
|
189
|
+
const committed = pointed.reduce((a, t) => a + t.points, 0);
|
|
190
|
+
const span = Math.max(1, t1 - t0);
|
|
191
|
+
const last = Math.min(t1, now);
|
|
192
|
+
const series = [];
|
|
193
|
+
for (let d = t0; d <= t1; d++) {
|
|
194
|
+
const ideal = r1(committed * (1 - (d - t0) / span));
|
|
195
|
+
if (d > last) { series.push({ date: dayStr(d), remaining: null, ideal }); continue; }
|
|
196
|
+
const burned = pointed.reduce((a, t) => { const b = burnDay(t); return b != null && b <= d ? a + t.points : a; }, 0);
|
|
197
|
+
series.push({ date: dayStr(d), remaining: r1(committed - burned), ideal });
|
|
198
|
+
}
|
|
199
|
+
const done = pointed.filter((t) => t.category === 'done').reduce((a, t) => a + t.points, 0);
|
|
200
|
+
const left = r1(committed - done);
|
|
201
|
+
const daysLeft = Math.max(0, t1 - now);
|
|
202
|
+
return {
|
|
203
|
+
available: true, sprint: s, committed: r1(committed), done: r1(done), left, daysLeft,
|
|
204
|
+
needPerDay: daysLeft > 0 ? r1(left / daysLeft) : null, series, tickets: inSprint.length, unpointed: inSprint.length - pointed.length,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { LEDGER, TOOLS, IMPACTS, KILL_STATES, LIMITS, checkData, readLedger, recordsOf, appendTool, riceScore, riceRank, buildDeliverOn, burndown, pickSprint };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// The workflow engine already decides which layout a repo has (scripts/workflow-state.js layout()): the PMOS repo keeps its
|
|
4
4
|
// ledgers under planning/runs, a kit tenant keeps them under pmos/state. Readers must ask here and never hard-code planning/.
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { workspaceLib } = require('../workspace-lib.js');
|
|
6
7
|
|
|
7
8
|
function pathsFor(root) {
|
|
8
9
|
const base = path.resolve(root || process.cwd());
|
|
@@ -33,7 +34,7 @@ function planItemToFact(it) {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function planItemFacts(text) {
|
|
36
|
-
const P = require('
|
|
37
|
+
const P = require(workspaceLib('plan-items.js')); // a checkout's workspace/lib, or the package's own lib
|
|
37
38
|
const parsed = P.parseText(text);
|
|
38
39
|
const folded = P.fold(parsed.lines || parsed);
|
|
39
40
|
const list = Array.isArray(folded.items) ? folded.items : Object.values(folded.items || {});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* workspace-lib.js — where a workspace/lib/ module is, for a script that runs in a checkout AND as a copy in a workspace
|
|
3
|
+
* package (incident workspace-package-script-closure).
|
|
4
|
+
*
|
|
5
|
+
* require(workspaceLib('plan-items.js'))
|
|
6
|
+
*
|
|
7
|
+
* Some scripts share a fold with the workspace (the plan items, the item steps). In a checkout that module is at
|
|
8
|
+
* workspace/lib/<name>. In a package the script is a copy that scripts/vendor-workspace-engine.js put in lib/engine/,
|
|
9
|
+
* and this file is lib/engine/workspace-lib.js, so the module is the bundle's own lib/<name>, one folder up. A plain relative
|
|
10
|
+
* require of ../workspace/lib/x.js resolves in the first place only, and the vendor step refuses it.
|
|
11
|
+
*
|
|
12
|
+
* Both places are tried, the checkout first. When neither exists the checkout path is returned, and require() names it.
|
|
13
|
+
*/
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
function workspaceLib(name) {
|
|
18
|
+
const n = String(name || '');
|
|
19
|
+
if (!/^[a-z0-9][a-z0-9-]*\.js$/.test(n)) throw new Error(`workspaceLib: '${n}' must be a file name in workspace/lib/, such as 'plan-items.js'`);
|
|
20
|
+
const candidates = [path.join(__dirname, '..', 'workspace', 'lib', n), path.join(__dirname, '..', n)];
|
|
21
|
+
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { workspaceLib };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* lib/work-evidence.js — "Looks done?" proof for work done outside PMOS (initiative console3-looks-done).
|
|
3
|
+
*
|
|
4
|
+
* evidenceFor(item, { events, notes, now }) → string | null one sentence, at most 240 characters
|
|
5
|
+
* withEvidence(items, { events, notes, now }) → items offline items gain `evidence` when they have none
|
|
6
|
+
*
|
|
7
|
+
* Pure: no I/O, no model. An offline item has a `person`. Proof is a calendar event that has ended and names the person, or
|
|
8
|
+
* a note that is not private and names the person (its title, meeting title or summary lines; the body is never read), both
|
|
9
|
+
* after the item was created. The PM confirms; nothing closes the item. */
|
|
10
|
+
|
|
11
|
+
const MAX = 240;
|
|
12
|
+
const MIN_FIRST = 3;
|
|
13
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
const day = (t) => new Date(t).toISOString().slice(0, 10);
|
|
15
|
+
const clip = (s) => (s.length > MAX ? s.slice(0, MAX - 1).trimEnd() + '…' : s);
|
|
16
|
+
|
|
17
|
+
/** A matcher for a person: the full name, or the first name as a whole word (3 letters or more). */
|
|
18
|
+
function matcher(person) {
|
|
19
|
+
const name = String(person || '').trim().replace(/\s+/g, ' ');
|
|
20
|
+
if (!name) return null;
|
|
21
|
+
const parts = [esc(name)];
|
|
22
|
+
const first = name.split(' ')[0];
|
|
23
|
+
if (first.length >= MIN_FIRST) parts.push(esc(first));
|
|
24
|
+
return new RegExp(`(^|[^\\p{L}\\p{N}])(?:${parts.join('|')})(?![\\p{L}\\p{N}])`, 'iu');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ms = (t) => { const v = Date.parse(t || ''); return Number.isNaN(v) ? null : v; };
|
|
28
|
+
|
|
29
|
+
function calendarProof(re, since, now, events) {
|
|
30
|
+
let best = null;
|
|
31
|
+
for (const e of events || []) {
|
|
32
|
+
const start = ms(e.at || e.start);
|
|
33
|
+
if (start === null || start < since) continue;
|
|
34
|
+
const end = ms(e.end) === null ? start : ms(e.end);
|
|
35
|
+
if (end > now) continue;
|
|
36
|
+
if (!re.test([e.title, ...(e.attendees || [])].join('\n'))) continue;
|
|
37
|
+
if (!best || start > best.start) best = { start, title: String(e.title || '').trim() };
|
|
38
|
+
}
|
|
39
|
+
return best && best.title ? `You had "${best.title}" on ${day(best.start)}.` : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function noteProof(re, since, now, notes) {
|
|
43
|
+
let best = null;
|
|
44
|
+
for (const n of notes || []) {
|
|
45
|
+
if (!n || n.private) continue;
|
|
46
|
+
const at = ms(n.updated_at) === null ? ms(n.created_at) : ms(n.updated_at);
|
|
47
|
+
if (at === null || at < since || at > now) continue;
|
|
48
|
+
const hay = [n.title, n.meeting && n.meeting.title, ...(n.summary_lines || [])].filter(Boolean).join('\n');
|
|
49
|
+
if (!re.test(hay)) continue;
|
|
50
|
+
if (!best || at > best.at) best = { at, title: String(n.title || (n.meeting && n.meeting.title) || 'Untitled note').trim() };
|
|
51
|
+
}
|
|
52
|
+
return best ? `Your note "${best.title}" from ${day(best.at)} mentions them.` : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function evidenceFor(item, { events = [], notes = [], now = Date.now() } = {}) {
|
|
56
|
+
if (!item || item.kind !== 'offline' || item.done) return null;
|
|
57
|
+
if (typeof item.evidence === 'string' && item.evidence.trim()) return item.evidence;
|
|
58
|
+
const re = matcher(item.person);
|
|
59
|
+
if (!re) return null;
|
|
60
|
+
const since = ms(item.created_at) === null ? 0 : ms(item.created_at);
|
|
61
|
+
const parts = [calendarProof(re, since, now, events), noteProof(re, since, now, notes)].filter(Boolean);
|
|
62
|
+
return parts.length ? clip(parts.join(' ')) : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function withEvidence(items, ctx = {}) {
|
|
66
|
+
return (items || []).map((it) => {
|
|
67
|
+
if (!it || it.kind !== 'offline') return it;
|
|
68
|
+
const ev = evidenceFor(it, ctx);
|
|
69
|
+
return ev && ev !== it.evidence ? { ...it, evidence: ev } : it;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { evidenceFor, withEvidence, matcher, MAX };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.135.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.135.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* Not registered with the registry (Today has no view registry): dest-today.js imports mountBriefing/disposeBriefing. */
|
|
13
13
|
import { el } from './dom.js';
|
|
14
14
|
import { createIdleGate, watchIdle, applyDayPart } from './c3-daypart.js';
|
|
15
|
-
import { LENSES, TAG_STYLE, PART_STYLE,
|
|
15
|
+
import { LENSES, TAG_STYLE, PART_STYLE, PARTS, partByKey, partFor, cardView, nextCards, swipeDir, sizeFor, caseModelOf, rawOption } from './briefing-model.js';
|
|
16
16
|
import { BX, caseFile, toast } from './c3-brief.js';
|
|
17
17
|
import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
|
|
18
18
|
import { listenButton } from './shell-readaloud.js';
|
|
19
|
+
import { onBriefPage } from './c3-brief-data.js';
|
|
20
|
+
import { button } from './primitives.js';
|
|
19
21
|
|
|
20
22
|
const NS = 'http://www.w3.org/2000/svg';
|
|
21
23
|
const roots = new WeakMap(); // host -> state
|
|
@@ -149,16 +151,23 @@ function hero(v) {
|
|
|
149
151
|
|
|
150
152
|
/* ── one card: the mockup's cardHtml (line 2324): .fc > .ff (.tg + .cf) and .fb (.fh + .case) ──────────────────────── */
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
/* the tag's style, coloured by the card's tone when the engine sends one (briefing-model.js TONE_COLOR) */
|
|
155
|
+
const tagStyle = (v) => { const T = TAG_STYLE[v.tag] || TAG_STYLE['Just in']; return v.toneColor ? { ...T, color: v.toneColor } : T; };
|
|
153
156
|
const pct = (x) => Math.round(Math.max(0, Math.min(1, x)) * 100);
|
|
154
157
|
|
|
155
158
|
function eyebrow(v) {
|
|
156
159
|
const T = tagStyle(v);
|
|
157
160
|
if (v.eyebrow) return [el('span', { class: 'tg', style: '--tc:var(--muted-foreground)' }, c3ic(v.eyebrow.icon, 'sm'), v.eyebrow.label), v.pov ? el('span', { class: 'tag pov' }, v.pov) : null];
|
|
158
|
-
const tg = el('span', { class: 'tg' + (T.filled ? ' f' : ''), title: T.q }, c3ic(v.icon || T.icon, 'sm'), v.tag);
|
|
161
|
+
const tg = el('span', { class: 'tg' + (T.filled ? ' f' : ''), title: T.q }, c3ic(v.icon || T.icon, 'sm'), v.kind || v.tag);
|
|
159
162
|
return v.pov ? [tg, el('span', { class: 'tag pov' }, v.pov)] : tg;
|
|
160
163
|
}
|
|
161
164
|
|
|
165
|
+
/* One number tile. Its colour is a token name ('var(--warning)' gives class k-warning), set by console3.css, so no inline style is needed. */
|
|
166
|
+
const kpTile = (v, l, c) => {
|
|
167
|
+
const m = /^var\(--([a-z-]+)\)$/.exec(String(c || '')); const tone = m ? ({ success: 'foreground', purple: 'acc' }[m[1]] || m[1]) : 'acc'; // the colour vocabulary has no green or purple tile: success reads neutral, purple takes the accent
|
|
168
|
+
return el('div', { class: 'kp k-' + tone }, el('b', null, v), el('span', null, l));
|
|
169
|
+
};
|
|
170
|
+
|
|
162
171
|
/* the mockup's .kp tiles (line 765): numbers the card carries, only the ones it carries */
|
|
163
172
|
function kpis(v) {
|
|
164
173
|
const t = v.terms || {};
|
|
@@ -166,7 +175,15 @@ function kpis(v) {
|
|
|
166
175
|
if (t.impact != null) a.push([pct(t.impact) + '%', 'impact', 'var(--acc)']);
|
|
167
176
|
if (t.urgency != null) a.push([pct(t.urgency) + '%', 'urgency', 'var(--warning)']);
|
|
168
177
|
if (t.score != null) a.push([String(Math.round(t.score * 100) / 100), 'score', 'var(--success)']);
|
|
169
|
-
return a.slice(0, 3).map((k) =>
|
|
178
|
+
return a.slice(0, 3).map((k) => kpTile(k[0], k[1], k[2]));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* The front's right side (the mockup's UI.deck): the number tiles the card carries (.kp). A card that carries no number keeps its picture (.hero), so the
|
|
182
|
+
* front is never empty. The tiles are a row of their own, not a second slot, so a card draws one or the other. */
|
|
183
|
+
function cfh(v) {
|
|
184
|
+
const tiles = v.kpisX ? v.kpisX.map((k) => kpTile(k.v, k.l, k.c)) : kpis(v);
|
|
185
|
+
if (tiles.length) return el('div', { class: 'cfh', 'data-kp': String(tiles.length) }, el('div', { class: 'chips2 kps' }, ...tiles));
|
|
186
|
+
return el('div', { class: 'cfh' }, el('div', { class: 'hero', 'data-hero': v.hero.kind }, hero(v)));
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
function front(v) {
|
|
@@ -179,8 +196,7 @@ function front(v) {
|
|
|
179
196
|
el('div', null, eyebrow(v)),
|
|
180
197
|
el('div', { class: 'cf' },
|
|
181
198
|
el('div', { class: 'cfa' }, el('h2', null, v.headline), v.sub ? el('div', { class: 'cfs' }, v.sub) : v.why ? el('div', { class: 'cfs', 'data-bf-why': '' }, 'why shown: ' + v.why) : null),
|
|
182
|
-
|
|
183
|
-
el('div', { class: 'cfk' }, ...(v.kpisX ? v.kpisX.map((k) => el('div', { class: 'kp', style: '--c:' + (k.c || 'var(--acc)') }, el('b', null, k.v), el('span', null, k.l))) : kpis(v))),
|
|
199
|
+
cfh(v),
|
|
184
200
|
el('div', { class: 'cff' }, el('div', { class: 'cfx' }, ...facts), rec)));
|
|
185
201
|
}
|
|
186
202
|
|
|
@@ -232,7 +248,7 @@ function card(S, v, i, n) {
|
|
|
232
248
|
}, front(v), el('span', { class: 'flipg', 'aria-hidden': 'true' }, c3ic('undo', 'sm')), bk);
|
|
233
249
|
bk.bind(c);
|
|
234
250
|
/* named by its own headline (aria-labelledby) rather than a long aria-label: the card's name is its text, and the position is its description */
|
|
235
|
-
const hd = c.querySelector('h2'); if (hd) { hd.id = 'fc-h-' + String(v.key || i).replace(/\W+/g, '-') + '-' + i; c.setAttribute('aria-labelledby', hd.id); } else c.setAttribute('aria-label', v.tag + ': ' + v.headline);
|
|
251
|
+
const hd = c.querySelector('h2'); if (hd) { hd.id = 'fc-h-' + String(v.key || i).replace(/\W+/g, '-') + '-' + i; c.setAttribute('aria-labelledby', hd.id); } else c.setAttribute('aria-label', (v.kind || v.tag) + ': ' + v.headline);
|
|
236
252
|
return c;
|
|
237
253
|
}
|
|
238
254
|
|
|
@@ -244,9 +260,10 @@ function flipTo(S, c, force) {
|
|
|
244
260
|
if (!c.classList.contains('on') && !c.classList.contains('back')) { deck.scrollTo({ left: c.offsetLeft - deck.children[0].offsetLeft, behavior: reduced() ? 'auto' : 'smooth' }); return; }
|
|
245
261
|
const toBack = force !== undefined ? force : !c.classList.contains('back');
|
|
246
262
|
deck.querySelectorAll('.fc.back').forEach((o) => { if (o !== c) { o.classList.remove('back'); o.setAttribute('aria-expanded', 'false'); } });
|
|
247
|
-
/*
|
|
248
|
-
*
|
|
263
|
+
/* The flip is a CSS transition of rotateY (console3.css .flipcard, about 350 ms). The front has a fixed size; the back grows to fit its content (console3 foundation),
|
|
264
|
+
* so the options and the slide are never clipped. Sibling cards keep their boxes. prefers-reduced-motion turns the transition off. */
|
|
249
265
|
c.classList.toggle('back', toBack); c.setAttribute('aria-expanded', String(toBack));
|
|
266
|
+
if (toBack && c.scrollIntoView) c.scrollIntoView({ block: 'nearest', behavior: reduced() ? 'auto' : 'smooth' }); // the back grows to its content: bring its foot into view
|
|
250
267
|
S.flip = toBack ? c.dataset.bfCard || '1' : null;
|
|
251
268
|
}
|
|
252
269
|
|
|
@@ -323,15 +340,37 @@ async function load(S, lens) {
|
|
|
323
340
|
else { const old = S.root.querySelector('.stance'); if (old) old.replaceWith(stance(S)); } // another lens arrived: update the switch, keep the open card
|
|
324
341
|
}
|
|
325
342
|
|
|
326
|
-
/* the stance bar (the mockup's stanceBar
|
|
343
|
+
/* the stance bar (the mockup's stanceBar): the greeting for the part of the day, the lens and its line, the five-part day switch and the read-aloud
|
|
344
|
+
* button. The part follows the clock; a tap on the switch previews another part (its lens, its cards) and the preview drops when the clock moves
|
|
345
|
+
* to another part. The name is the PM's first name when PMOS holds one (GET /api/brief-page `me`); without one the greeting stands alone. */
|
|
327
346
|
function stance(S) {
|
|
328
347
|
const P = PART_STYLE[S.lens] || PART_STYLE.Plan;
|
|
348
|
+
const part = partByKey(S.preview || S.clockPart);
|
|
349
|
+
const seg = el('div', { class: 'seg dp', role: 'group', 'aria-label': 'Part of the day', title: 'Follows the clock. Tap to preview another part of the day.' },
|
|
350
|
+
...PARTS.map((x) => {
|
|
351
|
+
const b = button({ label: x.label, variant: 'ghost', size: 'sm', onClick: () => setPart(S, x.k), attrs: { 'aria-pressed': String(x.k === part.k), 'data-pg-dp': x.k, title: x.label + ' · ' + x.at + (x.k === S.clockPart ? ' (now)' : ''), 'aria-label': x.label } });
|
|
352
|
+
b.replaceChildren(c3ic(x.icon, 'sm')); // the icon alone; the label is the aria-label
|
|
353
|
+
return b;
|
|
354
|
+
}));
|
|
329
355
|
return el('div', { class: 'stance', 'data-bf': 'lens-bar' },
|
|
330
|
-
el('span', { class: 'ki' }, c3ic(
|
|
331
|
-
el('div', null, el('b', { title: 'Follows the clock.' }, S.
|
|
356
|
+
el('span', { class: 'ki' }, c3ic(part.icon)),
|
|
357
|
+
el('div', null, el('b', { title: 'Follows the clock: this is the part of the day it is now.' }, part.greet + (S.me ? ', ' + S.me : '')), el('span', { class: 'dim' }, S.lens + ': ' + P.sub)),
|
|
358
|
+
seg,
|
|
332
359
|
listenButton(() => { const x = S.data[S.lens]; return x && x.ok ? x.cards[S.i] || x.cards[0] : null; })); // the mockup's speaker (data-a=listen) reads the card on top
|
|
333
360
|
}
|
|
334
361
|
|
|
362
|
+
/* Preview one part of the day: its lens, its cards. Nothing is saved; the clock's own part comes back by tapping it, or when the clock moves on. */
|
|
363
|
+
function setPart(S, k) {
|
|
364
|
+
const part = partByKey(k);
|
|
365
|
+
S.preview = k === S.clockPart ? null : k;
|
|
366
|
+
const lens = part.lens;
|
|
367
|
+
const changed = lens !== S.lens;
|
|
368
|
+
S.lens = lens; S.i = 0; S.flip = null;
|
|
369
|
+
if (changed) load(S, lens);
|
|
370
|
+
draw(S);
|
|
371
|
+
if (S.ctx.onLens) S.ctx.onLens(lens);
|
|
372
|
+
}
|
|
373
|
+
|
|
335
374
|
/* the mockup's running line (.tick > .tkt, lines 539 to 545): the engine's ticker strings, twice so the marquee loops */
|
|
336
375
|
function ticker(items) {
|
|
337
376
|
const one = (hide) => items.map((t) => el('span', { class: 'tki', 'aria-hidden': hide ? 'true' : null }, el('b', { style: 'color:var(--acc)' }, c3ic('bolt', 'sm')), String(t)));
|
|
@@ -340,7 +379,12 @@ function ticker(items) {
|
|
|
340
379
|
|
|
341
380
|
function draw(S) {
|
|
342
381
|
const d = S.data[S.lens];
|
|
343
|
-
|
|
382
|
+
/* a page's headline deck (ctx.bare, the Calendar tab): no lens row, and nothing at all until cards of its own scope arrive. ctx.scope
|
|
383
|
+
* is then required of the answer, so a server that does not know the scope never fills the page with another page's cards. */
|
|
384
|
+
const bare = !!(S.ctx && S.ctx.bare);
|
|
385
|
+
if (bare && (!d || !d.ok || d.scope !== S.ctx.scope || !(d.cards || []).length)) { S.root.replaceChildren(); S.root.hidden = true; S.flip = null; return; }
|
|
386
|
+
if (bare) S.root.hidden = false;
|
|
387
|
+
const kids = bare ? [] : [stance(S)];
|
|
344
388
|
if (!d) kids.push(el('p', { class: 'dim mono', style: small, role: 'status' }, 'Reading the repository'));
|
|
345
389
|
else if (!d.ok) kids.push(el('p', { class: 'dim mono', style: small, role: 'status', 'data-bf': 'error' }, 'The briefing could not be read: ' + d.error));
|
|
346
390
|
else {
|
|
@@ -370,16 +414,18 @@ export function mountBriefing(host, ctx) {
|
|
|
370
414
|
ensureC3Css();
|
|
371
415
|
const root = el('section', { class: 'c3 brief', 'data-board': 'briefing', 'aria-label': 'Briefing' });
|
|
372
416
|
host.replaceChildren(root);
|
|
373
|
-
const
|
|
374
|
-
|
|
417
|
+
const clockPart = partFor(new Date()).k;
|
|
418
|
+
const clock = partByKey(clockPart).lens;
|
|
419
|
+
S = { host, root, ctx: ctx || {}, lens: clock, clock, clockPart, preview: null, me: null, zoom: 'week', i: 0, flip: null, data: {}, dead: false };
|
|
375
420
|
S.stopPhone = c3Phone(root);
|
|
376
421
|
roots.set(host, S);
|
|
377
422
|
draw(S);
|
|
378
423
|
if (!host.closest('[data-daypart]')) applyDayPart(root); // a page without the console3 shell still gets the day part tokens
|
|
379
424
|
/* The clock moves the lens, but never under the reader: a new part waits for 60 s without pointer or key activity and for every card to be turned back. */
|
|
380
|
-
const gate = createIdleGate({ current: S.
|
|
381
|
-
apply: (
|
|
425
|
+
const gate = createIdleGate({ current: S.clockPart, part: (ms) => partFor(new Date(ms)).k, busy: () => !!S.flip || !!S.root.querySelector('.fc.back'),
|
|
426
|
+
apply: (k) => { if (S.dead) return; const l = partByKey(k).lens; S.clockPart = k; S.preview = null; S.clock = l; S.lens = l; S.i = 0; S.flip = null; load(S, l); draw(S); if (S.ctx.onLens) S.ctx.onLens(l); } });
|
|
382
427
|
S.idle = watchIdle(gate);
|
|
428
|
+
if (!(S.ctx && S.ctx.bare)) onBriefPage((j) => { const me = j && j.ok !== false && j.me ? String(j.me) : null; if (me === S.me || S.dead) return; S.me = me; const old = S.root.querySelector('.stance'); if (old) old.replaceWith(stance(S)); }); // the greeting's name
|
|
383
429
|
if (S.ctx.onLens) S.ctx.onLens(S.lens); // the page may follow the lens from the start (Reflect: "Carries over", "What I could stop")
|
|
384
430
|
// the shown lens first, then the others for their preview line
|
|
385
431
|
load(S, S.lens).then(() => Promise.all(LENSES.filter((l) => l !== S.lens).map((l) => load(S, l))));
|
|
@@ -15,6 +15,26 @@ export const TAG_STYLE = {
|
|
|
15
15
|
'Small win': { key: 'small', icon: 'up', color: 'var(--success)', filled: false, q: 'Small win: progress you can bank in one gesture.' },
|
|
16
16
|
'Big win': { key: 'big', icon: 'flag', color: 'var(--success)', filled: true, q: 'Big win: a milestone worth telling others.' },
|
|
17
17
|
};
|
|
18
|
+
/* A card's tone (scripts/signals/kinds.js) colours it: the engine names open kinds ("Deadline moved", "Blocked", "Risk"), so the
|
|
19
|
+
* colour comes from the tone, not from the kind. Only health tones have a colour of their own (the colour vocabulary); a positive
|
|
20
|
+
* or neutral card, or one without a tone, keeps its tag's colour. */
|
|
21
|
+
export const TONES = ['critical', 'warning', 'positive', 'neutral'];
|
|
22
|
+
export const TONE_COLOR = { critical: 'var(--destructive)', warning: 'var(--warning)' };
|
|
23
|
+
|
|
24
|
+
/* The five parts of the day (console3 Briefing stance switch). The lens is the part's signal ranking: evening and night both read Reflect. */
|
|
25
|
+
export const PARTS = [
|
|
26
|
+
{ k: 'morning', lens: 'Plan', icon: 'sun', label: 'Morning', at: '08:00', greet: 'Good morning' },
|
|
27
|
+
{ k: 'midday', lens: 'Unblock', icon: 'bolt', label: 'Midday', at: '12:30', greet: 'Good afternoon' },
|
|
28
|
+
{ k: 'afternoon', lens: 'Close', icon: 'check', label: 'Afternoon', at: '16:00', greet: 'Good afternoon' },
|
|
29
|
+
{ k: 'evening', lens: 'Reflect', icon: 'moon', label: 'Evening', at: '18:30', greet: 'Good evening' },
|
|
30
|
+
{ k: 'night', lens: 'Reflect', icon: 'star', label: 'Night', at: '22:00', greet: 'Good night' },
|
|
31
|
+
];
|
|
32
|
+
export const partByKey = (k) => PARTS.find((p) => p.k === k) || PARTS[0];
|
|
33
|
+
/** The part of the day for a clock time (morning 5 to 11, midday 11 to 14, afternoon 14 to 17, evening 17 to 21, night 21 to 5). */
|
|
34
|
+
export function partFor(date) {
|
|
35
|
+
const h = (date instanceof Date ? date : new Date(date)).getHours();
|
|
36
|
+
return partByKey(h >= 5 && h < 11 ? 'morning' : h >= 11 && h < 14 ? 'midday' : h >= 14 && h < 17 ? 'afternoon' : h >= 17 && h < 21 ? 'evening' : 'night');
|
|
37
|
+
}
|
|
18
38
|
/* The stance bar of each lens: icon and the line under its name (console3.html line 2206). */
|
|
19
39
|
export const PART_STYLE = {
|
|
20
40
|
Plan: { icon: 'sun', sub: 'Decide what gates the day' },
|
|
@@ -27,21 +47,24 @@ export const PART_STYLE = {
|
|
|
27
47
|
* (pm-work-view.js SPRITE, `c3i-*`). The five starting tags map to the icons TAG_STYLE gives them. No model call, no network. */
|
|
28
48
|
export const NEUTRAL_ICON = 'info';
|
|
29
49
|
export const ICON_RULES = [
|
|
30
|
-
[/\bsmall win\b|\bprogress\b|\bimproved?\b/, 'up'],
|
|
50
|
+
[/\bsmall win\b|\bquick win\b|\bprogress\b|\bimproved?\b|\bcapacity\b|\bfree\b/, 'up'],
|
|
31
51
|
[/\bbig win\b|\bwin\b|\bmilestone\b|\blaunch|\bship(ped)?\b|\brelease/, 'flag'],
|
|
32
52
|
[/\bcritical\b|\brisks?\b|\bblock|\bincident|\boutage|\boverdue\b|\bescalat|\bregress|\bfail/, 'alert'],
|
|
33
53
|
[/\bsecurity\b|\bcompliance\b|\bprivacy\b|\bpolicy\b/, 'shield'],
|
|
34
54
|
[/\bdeadline|\bdue\b|\bmoved\b|\bslip|\bdelay|\bschedule|\bwaiting\b|\bwaits?\b/, 'clock'],
|
|
35
|
-
[/\bdecision|\bapprov|\btrade-?off|\bchoose|\bchoice/, 'scale'],
|
|
55
|
+
[/\bdecision|\bapprov|\btrade-?off|\bchoose|\bchoice|\bconflict|\bmismatch|\bdisagree/, 'scale'],
|
|
36
56
|
[/\bopportunit|\bidea\b|\binsight|\bsuggest/, 'spark'],
|
|
37
57
|
[/\bjira\b|\bticket|\bissue|\bnew from\b|\binbound\b/, 'inbox'],
|
|
38
58
|
[/\bmeeting|\bcalendar|\bevent\b|\bagenda/, 'cal'],
|
|
39
|
-
[/\bmail|\bemail|\breply\b|\bmessage/, 'mail'],
|
|
40
|
-
[/\bperson|\bpeople|\bstakeholder|\bowner\b|\bteam\b|\bcustomer/, 'user'],
|
|
41
|
-
[/\bokr\b|\bgoal|\bobjective|\bkey result|\btarget\b/, 'target'],
|
|
59
|
+
[/\bmail|\bemail|\breply\b|\bmessage|\bpromise|\btold\b/, 'mail'],
|
|
60
|
+
[/\bperson|\bpeople|\bstakeholder|\bowner\b|\bowns?\b|\bnobody\b|\bteam\b|\bcustomer/, 'user'],
|
|
61
|
+
[/\bokr\b|\bgoal|\bobjective|\bkey result|\btarget\b|\banchor/, 'target'],
|
|
42
62
|
[/\bmetric|\bkpi\b|\btrend|\breport|\bnumber|\bburn/, 'report'],
|
|
43
63
|
[/\bpull request|\bpr\b|\bmerge|\bbranch|\bcommit|\bcode\b|\brepo/, 'branch'],
|
|
44
64
|
[/\bdoc\b|\bdocs\b|\bspec\b|\bprd\b|\bnote/, 'doc'],
|
|
65
|
+
[/\bplan\b|\bplans\b|\broadmap|\bforecast|\btoo much\b|\boverload|\bload\b/, 'cal'],
|
|
66
|
+
[/\bquestion|\bunknown|\bhypothes|\bevidence|\bresearch/, 'search'],
|
|
67
|
+
[/\bsort\b|\btriage|\border\b|\brank/, 'filter'],
|
|
45
68
|
[/\bjust in\b|\bnew\b|\bnews\b|\bupdate|\bsignal|\bchanged?\b/, 'bolt'],
|
|
46
69
|
];
|
|
47
70
|
/** The icon name for a card of this kind and signal type. Pure. Unknown or empty gives the neutral icon. */
|
|
@@ -88,6 +111,10 @@ export function cardView(card) {
|
|
|
88
111
|
kind: String(c.kind || c.tag || tag),
|
|
89
112
|
icon: iconFor(c.kind || c.tag || tag, c.signal || c.signalType || c.type),
|
|
90
113
|
tag, mark: TAG_MARK[tag], pinned: !!c.pinned,
|
|
114
|
+
tone: TONES.includes(c.tone) ? c.tone : null,
|
|
115
|
+
toneColor: TONE_COLOR[c.tone] || null,
|
|
116
|
+
/* what a card of this kind must say (the engine's template); null on cards that carry none */
|
|
117
|
+
template: c.template && typeof c.template === 'object' && c.template.text ? { text: String(c.template.text), gaps: Array.isArray(c.template.gaps) ? c.template.gaps.map(String) : [] } : null,
|
|
91
118
|
headline: String(c.headline || ''),
|
|
92
119
|
hero: { kind, points: Array.isArray(h.points) ? h.points.map(num).filter((x) => x !== null) : null },
|
|
93
120
|
facts: (Array.isArray(c.facts) ? c.facts : []).filter(Boolean).map(String).slice(0, 3),
|
|
@@ -144,6 +171,7 @@ export const rawOption = (m, id) => ((m && (m.opts || m.options)) || []).find((o
|
|
|
144
171
|
export const NO_ROUTE = 'No channel connected, and no route on this page carries it out. The suggestion stays text; nothing is sent or changed.';
|
|
145
172
|
|
|
146
173
|
/** The case of one engine card (v = cardView(card)). Pure; every string comes from a card field. */
|
|
174
|
+
const GAP_WORDS = { what: 'what happened', impact: 'the impact', when: 'by when', who: 'who it waits on', move: 'the move' };
|
|
147
175
|
const OUTWARD = /\b(send|tell|warn|reply|replies|email|message|notify|invite|ask)\b/i;
|
|
148
176
|
export function caseModelOf(v, o = {}) {
|
|
149
177
|
const reg = v && v.key && registered('card', v.key);
|
|
@@ -156,9 +184,10 @@ export function caseModelOf(v, o = {}) {
|
|
|
156
184
|
}));
|
|
157
185
|
const dl = c.deadline;
|
|
158
186
|
return {
|
|
159
|
-
id: 'card:' + (c.key || ''), tc: (TAG_STYLE[c.tag] || TAG_STYLE['Just in']).color,
|
|
187
|
+
id: 'card:' + (c.key || ''), tc: c.toneColor || (TAG_STYLE[c.tag] || TAG_STYLE['Just in']).color,
|
|
160
188
|
situation: c.waitsFor ? 'Waiting on ' + c.waitsFor + '.' : c.sub || c.headline, evidence: ev, options: opts,
|
|
161
|
-
confidence: c.conf != null ? c.conf : null,
|
|
189
|
+
confidence: c.conf != null ? c.conf : null,
|
|
190
|
+
unsure: [c.unsure, c.template && c.template.gaps.length ? 'Not known yet: ' + c.template.gaps.map((g) => GAP_WORDS[g] || g).join(', ') + '.' : ''].filter(Boolean).join(' '),
|
|
162
191
|
ifYouWait: dl ? (dl.days == null ? 'No deadline is known: ' + dl.text : dl.text) : '', why: c.why || '',
|
|
163
192
|
outward: !sim, outboxReason: NO_ROUTE,
|
|
164
193
|
};
|