create-pmos 0.133.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.
Files changed (212) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/bin/notes-replay.js +235 -0
  7. package/workspace/c3-briefing-test.js +130 -0
  8. package/workspace/c3-foundation-test.js +159 -0
  9. package/workspace/c3-ib-items-test.js +105 -0
  10. package/workspace/c3-ib-more-test.js +120 -0
  11. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  12. package/workspace/c3-ini-harness.js +66 -0
  13. package/workspace/c3-initiatives-test.js +140 -0
  14. package/workspace/c3-one-home-test.js +14 -1
  15. package/workspace/c3-reports-test.js +57 -18
  16. package/workspace/c3-settings-test.js +139 -0
  17. package/workspace/c3-work-test.js +42 -10
  18. package/workspace/fixtures/one-home-baseline.json +1 -9
  19. package/workspace/guide/briefing-cards.md +15 -3
  20. package/workspace/guide/briefing.md +12 -8
  21. package/workspace/guide/calendar-popup.md +2 -2
  22. package/workspace/guide/console-calendar.md +39 -10
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-boards.md +65 -0
  25. package/workspace/guide/jira.md +3 -1
  26. package/workspace/guide/layout.md +2 -0
  27. package/workspace/guide/my-work.md +5 -3
  28. package/workspace/guide/notes.md +35 -2
  29. package/workspace/guide/reports-dashboards.md +29 -5
  30. package/workspace/guide/settings.md +68 -0
  31. package/workspace/guide/welcome.md +10 -10
  32. package/workspace/lib/calendar-cli.js +3 -2
  33. package/workspace/lib/calendar-layers.js +21 -10
  34. package/workspace/lib/calendar-tidy.js +230 -0
  35. package/workspace/lib/engine/boards.js +542 -0
  36. package/workspace/lib/engine/breakdown-ops.js +180 -0
  37. package/workspace/lib/engine/calendar-import.js +84 -0
  38. package/workspace/lib/engine/calendar-model.js +185 -0
  39. package/workspace/lib/engine/card-state.js +92 -0
  40. package/workspace/lib/engine/derived/derive.js +92 -0
  41. package/workspace/lib/engine/derived/plan.js +3 -0
  42. package/workspace/lib/engine/derived/trust.js +3 -0
  43. package/workspace/lib/engine/doc-health.js +142 -0
  44. package/workspace/lib/engine/doc-sheet.js +75 -0
  45. package/workspace/lib/engine/intake.js +78 -0
  46. package/workspace/lib/engine/outbox.js +86 -0
  47. package/workspace/lib/engine/plan-forecast.js +342 -0
  48. package/workspace/lib/engine/promises.js +29 -0
  49. package/workspace/lib/engine/records/index.js +114 -0
  50. package/workspace/lib/engine/releases.js +96 -0
  51. package/workspace/lib/engine/replies.js +99 -0
  52. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  53. package/workspace/lib/engine/report-families.js +175 -0
  54. package/workspace/lib/engine/roadmap-ops.js +150 -0
  55. package/workspace/lib/engine/sessions-list.js +27 -0
  56. package/workspace/lib/engine/signal-rules.js +95 -0
  57. package/workspace/lib/engine/signals/cards.js +57 -0
  58. package/workspace/lib/engine/signals/detectors.js +370 -0
  59. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  60. package/workspace/lib/engine/signals/facts.js +197 -0
  61. package/workspace/lib/engine/signals/index.js +33 -0
  62. package/workspace/lib/engine/signals/kinds.js +110 -0
  63. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  64. package/workspace/lib/engine/signals/options.js +24 -0
  65. package/workspace/lib/engine/signals/score.js +72 -0
  66. package/workspace/lib/engine/signals/select.js +47 -0
  67. package/workspace/lib/engine/step-planner.js +65 -0
  68. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  69. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  70. package/workspace/lib/engine/trust/import-guard.js +36 -0
  71. package/workspace/lib/engine/trust/ledger.js +53 -0
  72. package/workspace/lib/engine/trust/outbox.js +45 -0
  73. package/workspace/lib/engine/trust/plan.js +5 -0
  74. package/workspace/lib/engine/trust/trust.js +21 -0
  75. package/workspace/lib/engine/truth-table.js +57 -0
  76. package/workspace/lib/engine/workspace-lib.js +24 -0
  77. package/workspace/lib/factory-join.js +1 -1
  78. package/workspace/lib/facts-async.js +4 -4
  79. package/workspace/lib/headless.js +95 -0
  80. package/workspace/lib/import-overlay.js +2 -1
  81. package/workspace/lib/jira-status.js +30 -3
  82. package/workspace/lib/notes-audio.js +28 -9
  83. package/workspace/lib/notes-capture.js +136 -0
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-live-guard.js +117 -0
  86. package/workspace/lib/notes-replay.js +125 -0
  87. package/workspace/lib/notes-screen.js +180 -0
  88. package/workspace/lib/notes-whisper.js +139 -16
  89. package/workspace/lib/notes.js +4 -2
  90. package/workspace/lib/outbox-store.js +10 -6
  91. package/workspace/lib/pmos-verbs.js +3 -1
  92. package/workspace/lib/proactive-cli.js +7 -0
  93. package/workspace/lib/proactive.js +15 -2
  94. package/workspace/lib/repo-script.js +36 -0
  95. package/workspace/lib/report-dashboards.js +5 -2
  96. package/workspace/lib/report-frame.js +122 -0
  97. package/workspace/lib/report-schedules.js +113 -0
  98. package/workspace/lib/route-modules.js +9 -2
  99. package/workspace/lib/routes-boards.js +734 -0
  100. package/workspace/lib/routes-brief-page.js +13 -4
  101. package/workspace/lib/routes-briefing.js +22 -11
  102. package/workspace/lib/routes-c3-init.js +4 -3
  103. package/workspace/lib/routes-c3-life.js +2 -1
  104. package/workspace/lib/routes-c3-reports.js +96 -13
  105. package/workspace/lib/routes-c3-strategy.js +4 -3
  106. package/workspace/lib/routes-c3-timeline.js +3 -2
  107. package/workspace/lib/routes-calendar-import.js +3 -2
  108. package/workspace/lib/routes-calendar-views.js +9 -4
  109. package/workspace/lib/routes-doc-health.js +2 -1
  110. package/workspace/lib/routes-doc-sheet.js +2 -1
  111. package/workspace/lib/routes-forecast.js +2 -1
  112. package/workspace/lib/routes-hooks.js +3 -2
  113. package/workspace/lib/routes-initiatives.js +2 -1
  114. package/workspace/lib/routes-intake.js +2 -1
  115. package/workspace/lib/routes-notes.js +113 -15
  116. package/workspace/lib/routes-outbox.js +23 -3
  117. package/workspace/lib/routes-pm-items.js +19 -6
  118. package/workspace/lib/routes-records.js +3 -2
  119. package/workspace/lib/routes-releases.js +2 -1
  120. package/workspace/lib/routes-report-facts.js +4 -3
  121. package/workspace/lib/routes-report-families.js +2 -1
  122. package/workspace/lib/routes-roadmap-ops.js +5 -4
  123. package/workspace/lib/routes-runs.js +6 -5
  124. package/workspace/lib/routes-settings.js +51 -0
  125. package/workspace/lib/routes-signals.js +3 -2
  126. package/workspace/lib/routes-stage-tools.js +220 -0
  127. package/workspace/lib/run-answers.js +2 -1
  128. package/workspace/lib/settings-store.js +226 -0
  129. package/workspace/lib/stage-tools.js +208 -0
  130. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  131. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  132. package/workspace/lib/work-evidence.js +73 -0
  133. package/workspace/package-lock.json +2 -2
  134. package/workspace/package.json +1 -1
  135. package/workspace/public/pm/briefing-deck.js +63 -17
  136. package/workspace/public/pm/briefing-model.js +36 -7
  137. package/workspace/public/pm/c3-ambient.js +100 -0
  138. package/workspace/public/pm/c3-brief-needs.js +26 -6
  139. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  140. package/workspace/public/pm/c3-brief-today.js +56 -21
  141. package/workspace/public/pm/c3-brief.js +2 -1
  142. package/workspace/public/pm/c3-calendar.js +161 -42
  143. package/workspace/public/pm/c3-calpop.js +5 -5
  144. package/workspace/public/pm/c3-demo-boards.js +649 -0
  145. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  146. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  147. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  148. package/workspace/public/pm/c3-demo-data.js +17 -27
  149. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  150. package/workspace/public/pm/c3-demo.js +2 -1
  151. package/workspace/public/pm/c3-dock.js +1 -1
  152. package/workspace/public/pm/c3-ib-docs.js +80 -0
  153. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  154. package/workspace/public/pm/c3-ib-items.js +201 -0
  155. package/workspace/public/pm/c3-ib-releases.js +51 -0
  156. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  157. package/workspace/public/pm/c3-ib-triage.js +178 -0
  158. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  159. package/workspace/public/pm/c3-ini-records.js +197 -0
  160. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  161. package/workspace/public/pm/c3-ini-ui.js +229 -0
  162. package/workspace/public/pm/c3-ini-work.js +139 -0
  163. package/workspace/public/pm/c3-initiatives.css +218 -0
  164. package/workspace/public/pm/c3-initiatives.js +741 -0
  165. package/workspace/public/pm/c3-plan.js +2 -2
  166. package/workspace/public/pm/c3-reports.css +47 -47
  167. package/workspace/public/pm/c3-reports.js +223 -185
  168. package/workspace/public/pm/c3-settings.css +74 -0
  169. package/workspace/public/pm/c3-settings.js +703 -0
  170. package/workspace/public/pm/c3-shell.js +102 -36
  171. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  172. package/workspace/public/pm/c3-terminal.js +2 -2
  173. package/workspace/public/pm/calendar-views.js +26 -13
  174. package/workspace/public/pm/components.css +319 -213
  175. package/workspace/public/pm/console3.css +118 -23
  176. package/workspace/public/pm/dest-notes.js +26 -4
  177. package/workspace/public/pm/notes-screen-capture.js +165 -0
  178. package/workspace/public/pm/notes-screen-view.js +98 -0
  179. package/workspace/public/pm/panel-trees.js +5 -2
  180. package/workspace/public/pm/pm-work-view.js +233 -146
  181. package/workspace/public/pm/shell-bell.js +3 -2
  182. package/workspace/public/pm/shell-recording.js +148 -17
  183. package/workspace/public/pm/tokens.css +2 -2
  184. package/workspace/public/pm/view-notes.js +53 -11
  185. package/workspace/public/pm/welcome.js +197 -89
  186. package/workspace/public/welcome.html +6 -4
  187. package/workspace/server.js +105 -6
  188. package/workspace/skills/day-check.skill +1 -1
  189. package/workspace/skills/jira-sync.skill +6 -4
  190. package/workspace/skills/morning-brief.skill +1 -1
  191. package/workspace/test-files.json +14 -3
  192. package/workspace/test-lists/c3-ib-items.json +9 -0
  193. package/workspace/test-lists/c3-ib-more.json +9 -0
  194. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  195. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  196. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  197. package/workspace/test-lists/console3-briefing.json +9 -0
  198. package/workspace/test-lists/console3-foundation.json +9 -0
  199. package/workspace/test-lists/console3-reports.json +3 -3
  200. package/workspace/test-lists/meeting-notes.json +12 -4
  201. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  202. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  203. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  204. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  205. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  206. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  207. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  208. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  209. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  210. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  211. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  212. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+ /* calendar-tidy.js — the tidy-up suggestions of the console3 Calendar page (initiative calendar-tidy-up, R1 to R6).
3
+ *
4
+ * tidyFrom({ range, today, nowMs, events, eventsSource, inis, gantt, planItems }) → { tidy, tidy_total, tidy_note }
5
+ *
6
+ * PURE and READ ONLY. No I/O, no clock, no model call, and no write function: a suggestion is a proposal in words. PMOS holds
7
+ * no calendar credentials, so nothing here can move, decline or send anything (R3).
8
+ *
9
+ * INPUT (plain data that routes-calendar-views.js already holds):
10
+ * range = { start, end } 'YYYY-MM-DD'
11
+ * events = imported events ({ id, at, end, title, initiative, prep, prep_done, steps }); all of them, this filters
12
+ * inis = the initiative facts ({ id, title, done, status, promised, stopped:{state}, horizon })
13
+ * gantt = model.ganttRows rows ({ id, status, promised })
14
+ * planItems = [{ id, initiative, title, due, milestone, state }]
15
+ *
16
+ * THE RULES (PRD R2). Each gives at most one suggestion per pair or item.
17
+ * overlap bad two timed events that overlap
18
+ * no-buffer warn two timed events on the same day with a gap of 0 to 9 minutes (10 minutes or more is fine)
19
+ * prep-risk warn calendar-layers prepRisks: less than PER_STEP (30) free minutes for each prep step left
20
+ * dead-initiative warn an event about an initiative that is done, parked, on hold, closed or cancelled
21
+ * review-early info a review, sync or checkpoint event before a milestone or promised date of its initiative that
22
+ * is not done (the nearest one is named)
23
+ * likely-delay bad a milestone or promised date in range of an initiative that is overdue or delayed
24
+ * warn the same, for an initiative at risk
25
+ * heavy-day info a working day with more than 6 hours (360 minutes) of timed meetings; exactly 6 hours is fine
26
+ * All-day events (a date with no time) take no time, so the time rules leave them out.
27
+ *
28
+ * OUTPUT. Sorted by severity (bad, warn, info), then date, then id; capped at TIDY_CAP; tidy_total counts all. tidy_note
29
+ * names each rule that could not run, with the reason (R5): no events in the range, or no initiative data. */
30
+ const L = require('./calendar-layers.js');
31
+
32
+ const TIDY_CAP = 40;
33
+ const BUFFER_MIN = 10;
34
+ const HEAVY_MIN = 6 * 60;
35
+ const SEVERITY = ['bad', 'warn', 'info'];
36
+ const RULES = ['overlap', 'no-buffer', 'prep-risk', 'dead-initiative', 'review-early', 'likely-delay', 'heavy-day'];
37
+ const EVENT_RULES = ['overlap', 'no-buffer', 'prep-risk', 'dead-initiative', 'review-early', 'heavy-day'];
38
+ const INI_RULES = ['dead-initiative', 'review-early', 'likely-delay'];
39
+ const REVIEW_RE = /review|sync|checkpoint/i;
40
+
41
+ const pad = (n) => String(n).padStart(2, '0');
42
+ const isDay = (s) => typeof s === 'string' && /^\d{4}-\d{2}-\d{2}/.test(s) && !Number.isNaN(Date.parse(s.slice(0, 10) + 'T00:00:00Z'));
43
+ const day10 = (s) => String(s).slice(0, 10);
44
+ const str = (v) => (typeof v === 'string' ? v.trim() : '');
45
+ const hhmm = (ms) => { const m = L.minuteOfDay(ms); return `${pad(Math.floor(m / 60))}:${pad(m % 60)}`; };
46
+ const dow = (s) => new Date(Date.parse(s + 'T00:00:00Z')).getUTCDay();
47
+ const isWorkday = (s) => dow(s) >= 1 && dow(s) <= 5;
48
+ const duration = (min) => { const h = Math.floor(min / 60); const m = min % 60; return m ? `${h} h ${m} min` : `${h} h`; };
49
+ const titleOf = (e) => str(e && e.title) || 'a meeting';
50
+ const evRef = (e) => ({ type: 'event', id: String(e.id) });
51
+ const iniRef = (id) => ({ type: 'initiative', id: String(id) });
52
+
53
+ /** The state that makes an initiative's meetings pointless, as a word, or null while it is live. */
54
+ function deadWord(ini) {
55
+ if (!ini) return null;
56
+ if (ini.done || ini.status === 'done') return 'done';
57
+ const st = ini.stopped && str(ini.stopped.state).toLowerCase();
58
+ if (st === 'on-hold' || st === 'held' || st === 'on hold') return 'on hold';
59
+ if (st === 'closed' || st === 'cancelled' || st === 'parked') return st;
60
+ const words = [ini.horizon, ini.status, ini.stage, ini.lifecycle && ini.lifecycle.state].map((x) => str(x).toLowerCase());
61
+ if (words.includes('parked')) return 'parked';
62
+ if (words.some((w) => w === 'on hold' || w === 'on-hold' || w === 'held')) return 'on hold';
63
+ return null;
64
+ }
65
+
66
+ /** The worst delay status of an initiative from its facts and its gantt row: 'overdue' | 'delayed' | 'at risk' | null. */
67
+ function delayWord(ini, row) {
68
+ const order = ['overdue', 'delayed', 'at risk'];
69
+ const found = [ini && ini.status, row && row.status].map((x) => str(x).toLowerCase()).filter((x) => order.includes(x));
70
+ if (!found.length) return null;
71
+ return order.find((o) => found.includes(o));
72
+ }
73
+
74
+ /** The dated milestones of each initiative that are not done: its promised date and its plan-item milestones. */
75
+ function milestonesOf(inis, planItems) {
76
+ const out = new Map();
77
+ const add = (id, m) => { if (!out.has(id)) out.set(id, []); out.get(id).push(m); };
78
+ for (const i of inis) {
79
+ if (!i || !i.id || i.done || !isDay(i.promised)) continue;
80
+ add(String(i.id), { date: day10(i.promised), title: `the promised date of ${str(i.title) || i.id}`, short: 'the promised date', ref: iniRef(i.id), key: `ini:${i.id}:promised` });
81
+ }
82
+ for (const p of planItems || []) {
83
+ if (!p || !p.id || !p.initiative || !p.milestone || p.state === 'done' || !isDay(p.due)) continue;
84
+ add(String(p.initiative), { date: day10(p.due), title: `the milestone ${str(p.title) || p.id}`, short: `the milestone ${str(p.title) || p.id}`, ref: { type: 'plan-item', id: String(p.id) }, key: `plan:${p.id}` });
85
+ }
86
+ for (const list of out.values()) list.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : a.key < b.key ? -1 : 1));
87
+ return out;
88
+ }
89
+
90
+ function tidyFrom(input = {}) {
91
+ const range = input.range || {};
92
+ const today = input.today;
93
+ const inis = Array.isArray(input.inis) ? input.inis.filter((i) => i && i.id) : [];
94
+ const iniById = new Map(inis.map((i) => [String(i.id), i]));
95
+ const rowById = new Map((input.gantt || []).filter((r) => r && r.id).map((r) => [String(r.id), r]));
96
+ const inRange = (d) => d >= range.start && d <= range.end;
97
+
98
+ // the events inside the range, each with its day and, when it has a time, its span
99
+ const events = [];
100
+ for (const e of input.events || []) {
101
+ const at = e && (e.at || e.start);
102
+ if (!e || !e.id || !isDay(at)) continue;
103
+ const sp = L.spanOf(e);
104
+ const date = sp ? L.localDay(sp.s) : day10(at);
105
+ if (!inRange(date)) continue;
106
+ events.push({ e, date, sp });
107
+ }
108
+ const timed = events.filter((x) => x.sp).sort((a, b) => a.sp.s - b.sp.s || b.sp.f - a.sp.f || (String(a.e.id) < String(b.e.id) ? -1 : 1));
109
+
110
+ const note = {};
111
+ if (!events.length) {
112
+ const why = input.eventsSource === 'none' ? 'no calendar is imported, so there are no events to check' : 'no events fall inside this range';
113
+ for (const r of EVENT_RULES) note[r] = why;
114
+ }
115
+ if (!inis.length) for (const r of INI_RULES) if (!note[r]) note[r] = 'no initiative status was read, so initiatives cannot be checked';
116
+
117
+ const out = new Map();
118
+ const put = (s) => { if (!out.has(s.id)) out.set(s.id, s); };
119
+
120
+ // overlap and no-buffer: every pair of timed events on the same day
121
+ const byDay = new Map();
122
+ for (const x of timed) { if (!byDay.has(x.date)) byDay.set(x.date, []); byDay.get(x.date).push(x); }
123
+ for (const [date, list] of byDay) {
124
+ for (let i = 0; i < list.length; i++) {
125
+ for (let j = i + 1; j < list.length; j++) {
126
+ const a = list[i]; const b = list[j]; // a starts first (or at the same time and runs longer)
127
+ const pair = [String(a.e.id), String(b.e.id)].sort().join('+');
128
+ if (b.sp.s < a.sp.f) {
129
+ put({ id: `overlap:${pair}`, rule: 'overlap', severity: 'bad', date,
130
+ title: `${titleOf(a.e)} overlaps ${titleOf(b.e)}`,
131
+ why: `${titleOf(a.e)} runs from ${hhmm(a.sp.s)} to ${hhmm(a.sp.f)} and ${titleOf(b.e)} starts at ${hhmm(b.sp.s)}, so you cannot attend both in full.`,
132
+ suggestion: `Move ${titleOf(b.e)} to ${hhmm(a.sp.f)} or shorten ${titleOf(a.e)}.`,
133
+ refs: [evRef(a.e), evRef(b.e)] });
134
+ continue;
135
+ }
136
+ const gap = Math.round((b.sp.s - a.sp.f) / 60000);
137
+ if (gap >= 0 && gap < BUFFER_MIN && L.localDay(a.sp.f) === date) {
138
+ // only the next event after a counts: a gap to a later one is never smaller than the gap to the next one
139
+ const between = list.some((c, k) => k !== i && k !== j && c.sp.s >= a.sp.f && c.sp.s < b.sp.s);
140
+ if (between) continue;
141
+ put({ id: `no-buffer:${pair}`, rule: 'no-buffer', severity: 'warn', date,
142
+ title: `No break between ${titleOf(a.e)} and ${titleOf(b.e)}`,
143
+ why: gap === 0
144
+ ? `${titleOf(b.e)} starts at ${hhmm(b.sp.s)}, the minute ${titleOf(a.e)} ends. You have no time to move, take notes or prepare.`
145
+ : `${titleOf(b.e)} starts ${gap} ${gap === 1 ? 'minute' : 'minutes'} after ${titleOf(a.e)} ends. That is less than ${BUFFER_MIN} minutes to move, take notes or prepare.`,
146
+ suggestion: `Move ${titleOf(b.e)} to ${hhmm(a.sp.f + BUFFER_MIN * 60000)} or end ${titleOf(a.e)} ${BUFFER_MIN - gap} ${BUFFER_MIN - gap === 1 ? 'minute' : 'minutes'} earlier.`,
147
+ refs: [evRef(a.e), evRef(b.e)] });
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ // prep-risk: the calendar-memory rule, for the events in range
154
+ if (events.length) {
155
+ for (const r of L.prepRisks(events.map((x) => x.e), today, input.nowMs)) {
156
+ put({ id: `prep-risk:${r.id}`, rule: 'prep-risk', severity: 'warn', date: r.date,
157
+ title: `Little time to prepare for ${r.title}`,
158
+ why: `${r.title} has ${r.left} prep ${r.left === 1 ? 'step' : 'steps'} left, and only ${Math.round(r.free)} minutes of free working time remain before it. Each step needs about 30 minutes.`,
159
+ suggestion: `Block time before ${r.title} for the prep, or move ${r.title} to a later day.`,
160
+ refs: [{ type: 'event', id: String(r.id) }] });
161
+ }
162
+ }
163
+
164
+ // dead-initiative and review-early: an event about an initiative
165
+ const miles = milestonesOf(inis, input.planItems);
166
+ for (const { e, date } of events) {
167
+ const id = e.initiative ? String(e.initiative) : null;
168
+ const ini = id ? iniById.get(id) : null;
169
+ if (!ini) continue;
170
+ const name = str(ini.title) || id;
171
+ const dead = deadWord(ini);
172
+ if (dead) {
173
+ put({ id: `dead-initiative:${e.id}`, rule: 'dead-initiative', severity: 'warn', date,
174
+ title: `${titleOf(e)} is about an initiative that is ${dead}`,
175
+ why: `${titleOf(e)} is linked to ${name}, which is ${dead}.`,
176
+ suggestion: `Cancel ${titleOf(e)} or give it a new purpose, because ${name} is ${dead}.`,
177
+ refs: [evRef(e), iniRef(id)] });
178
+ continue;
179
+ }
180
+ if (REVIEW_RE.test(titleOf(e))) {
181
+ const next = (miles.get(id) || []).find((m) => m.date > date);
182
+ if (next) {
183
+ put({ id: `review-early:${e.id}`, rule: 'review-early', severity: 'info', date,
184
+ title: `${titleOf(e)} comes before what it reviews`,
185
+ why: `${titleOf(e)} is on ${date}, but ${next.title} is on ${next.date} and is not done yet.`,
186
+ suggestion: `Move ${titleOf(e)} to ${next.date} or later, or keep it as a progress check and say so in the invite.`,
187
+ refs: [evRef(e), next.ref, ...(next.ref.type === 'initiative' ? [] : [iniRef(id)])] });
188
+ }
189
+ }
190
+ }
191
+
192
+ // likely-delay: a milestone or promised date in range of an initiative that is late already
193
+ for (const [id, list] of miles) {
194
+ const ini = iniById.get(id);
195
+ if (!ini || deadWord(ini)) continue;
196
+ const late = delayWord(ini, rowById.get(id));
197
+ if (!late) continue;
198
+ const name = str(ini.title) || id;
199
+ for (const m of list) {
200
+ if (!inRange(m.date)) continue;
201
+ put({ id: `likely-delay:${m.key}`, rule: 'likely-delay', severity: late === 'at risk' ? 'warn' : 'bad', date: m.date,
202
+ title: `${m.short.charAt(0).toUpperCase() + m.short.slice(1)} of ${name} is likely to slip`,
203
+ why: `${name} is ${late}, and ${m.title} is on ${m.date}.`,
204
+ suggestion: `Agree a new date for ${m.short} with the people who depend on it, or cut its scope to keep ${m.date}.`,
205
+ refs: m.ref.type === 'initiative' ? [m.ref] : [m.ref, iniRef(id)] });
206
+ }
207
+ }
208
+
209
+ // heavy-day: more than six hours of timed meetings on a working day (overlaps count once)
210
+ for (const [date, list] of byDay) {
211
+ if (!isWorkday(date)) continue;
212
+ const iv = L.minutesOn(list.map((x) => x.sp), date).map((m) => [m.from, m.to]).sort((a, b) => a[0] - b[0]);
213
+ let busy = 0; let cur = null;
214
+ for (const [s, f] of iv) { if (cur && s <= cur[1]) cur[1] = Math.max(cur[1], f); else { if (cur) busy += cur[1] - cur[0]; cur = [s, f]; } }
215
+ if (cur) busy += cur[1] - cur[0];
216
+ if (busy <= HEAVY_MIN) continue;
217
+ const longest = list.slice().sort((a, b) => (b.sp.f - b.sp.s) - (a.sp.f - a.sp.s))[0];
218
+ put({ id: `heavy-day:${date}`, rule: 'heavy-day', severity: 'info', date,
219
+ title: `${duration(busy)} of meetings on ${date}`,
220
+ why: `More than 6 hours of meetings on ${date} leave little time for focused work.`,
221
+ suggestion: `Move or shorten one meeting on ${date}, for example ${titleOf(longest.e)}, to free a block of focus time.`,
222
+ refs: list.map((x) => evRef(x.e)) });
223
+ }
224
+
225
+ const all = [...out.values()].sort((a, b) => SEVERITY.indexOf(a.severity) - SEVERITY.indexOf(b.severity)
226
+ || (a.date < b.date ? -1 : a.date > b.date ? 1 : 0) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
227
+ return { tidy: all.slice(0, TIDY_CAP), tidy_total: all.length, tidy_note: note };
228
+ }
229
+
230
+ module.exports = { tidyFrom, deadWord, delayWord, TIDY_CAP, BUFFER_MIN, HEAVY_MIN, RULES, SEVERITY };