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.
Files changed (209) 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 +16 -1
  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-capture.js +9 -1
  83. package/workspace/lib/notes-final.js +38 -13
  84. package/workspace/lib/notes-replay.js +125 -0
  85. package/workspace/lib/notes-screen.js +180 -0
  86. package/workspace/lib/notes-whisper.js +10 -3
  87. package/workspace/lib/outbox-store.js +10 -6
  88. package/workspace/lib/pmos-verbs.js +3 -1
  89. package/workspace/lib/proactive-cli.js +7 -0
  90. package/workspace/lib/proactive.js +15 -2
  91. package/workspace/lib/repo-script.js +36 -0
  92. package/workspace/lib/report-dashboards.js +5 -2
  93. package/workspace/lib/report-frame.js +122 -0
  94. package/workspace/lib/report-schedules.js +113 -0
  95. package/workspace/lib/route-modules.js +9 -2
  96. package/workspace/lib/routes-boards.js +734 -0
  97. package/workspace/lib/routes-brief-page.js +13 -4
  98. package/workspace/lib/routes-briefing.js +22 -11
  99. package/workspace/lib/routes-c3-init.js +4 -3
  100. package/workspace/lib/routes-c3-life.js +2 -1
  101. package/workspace/lib/routes-c3-reports.js +96 -13
  102. package/workspace/lib/routes-c3-strategy.js +4 -3
  103. package/workspace/lib/routes-c3-timeline.js +3 -2
  104. package/workspace/lib/routes-calendar-import.js +3 -2
  105. package/workspace/lib/routes-calendar-views.js +9 -4
  106. package/workspace/lib/routes-doc-health.js +2 -1
  107. package/workspace/lib/routes-doc-sheet.js +2 -1
  108. package/workspace/lib/routes-forecast.js +2 -1
  109. package/workspace/lib/routes-hooks.js +3 -2
  110. package/workspace/lib/routes-initiatives.js +2 -1
  111. package/workspace/lib/routes-intake.js +2 -1
  112. package/workspace/lib/routes-notes.js +55 -9
  113. package/workspace/lib/routes-outbox.js +23 -3
  114. package/workspace/lib/routes-pm-items.js +19 -6
  115. package/workspace/lib/routes-records.js +3 -2
  116. package/workspace/lib/routes-releases.js +2 -1
  117. package/workspace/lib/routes-report-facts.js +4 -3
  118. package/workspace/lib/routes-report-families.js +2 -1
  119. package/workspace/lib/routes-roadmap-ops.js +5 -4
  120. package/workspace/lib/routes-runs.js +6 -5
  121. package/workspace/lib/routes-settings.js +51 -0
  122. package/workspace/lib/routes-signals.js +3 -2
  123. package/workspace/lib/routes-stage-tools.js +220 -0
  124. package/workspace/lib/run-answers.js +2 -1
  125. package/workspace/lib/settings-store.js +226 -0
  126. package/workspace/lib/stage-tools.js +208 -0
  127. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  128. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  129. package/workspace/lib/work-evidence.js +73 -0
  130. package/workspace/package-lock.json +2 -2
  131. package/workspace/package.json +1 -1
  132. package/workspace/public/pm/briefing-deck.js +63 -17
  133. package/workspace/public/pm/briefing-model.js +36 -7
  134. package/workspace/public/pm/c3-ambient.js +100 -0
  135. package/workspace/public/pm/c3-brief-needs.js +26 -6
  136. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  137. package/workspace/public/pm/c3-brief-today.js +56 -21
  138. package/workspace/public/pm/c3-brief.js +2 -1
  139. package/workspace/public/pm/c3-calendar.js +161 -42
  140. package/workspace/public/pm/c3-calpop.js +5 -5
  141. package/workspace/public/pm/c3-demo-boards.js +649 -0
  142. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  143. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  144. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  145. package/workspace/public/pm/c3-demo-data.js +17 -27
  146. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  147. package/workspace/public/pm/c3-demo.js +2 -1
  148. package/workspace/public/pm/c3-dock.js +1 -1
  149. package/workspace/public/pm/c3-ib-docs.js +80 -0
  150. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  151. package/workspace/public/pm/c3-ib-items.js +201 -0
  152. package/workspace/public/pm/c3-ib-releases.js +51 -0
  153. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  154. package/workspace/public/pm/c3-ib-triage.js +178 -0
  155. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  156. package/workspace/public/pm/c3-ini-records.js +197 -0
  157. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  158. package/workspace/public/pm/c3-ini-ui.js +229 -0
  159. package/workspace/public/pm/c3-ini-work.js +139 -0
  160. package/workspace/public/pm/c3-initiatives.css +218 -0
  161. package/workspace/public/pm/c3-initiatives.js +741 -0
  162. package/workspace/public/pm/c3-plan.js +2 -2
  163. package/workspace/public/pm/c3-reports.css +47 -47
  164. package/workspace/public/pm/c3-reports.js +223 -185
  165. package/workspace/public/pm/c3-settings.css +74 -0
  166. package/workspace/public/pm/c3-settings.js +703 -0
  167. package/workspace/public/pm/c3-shell.js +102 -36
  168. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  169. package/workspace/public/pm/c3-terminal.js +2 -2
  170. package/workspace/public/pm/calendar-views.js +1 -1
  171. package/workspace/public/pm/components.css +319 -213
  172. package/workspace/public/pm/console3.css +99 -23
  173. package/workspace/public/pm/dest-notes.js +26 -4
  174. package/workspace/public/pm/notes-screen-capture.js +165 -0
  175. package/workspace/public/pm/notes-screen-view.js +98 -0
  176. package/workspace/public/pm/panel-trees.js +5 -2
  177. package/workspace/public/pm/pm-work-view.js +233 -146
  178. package/workspace/public/pm/shell-bell.js +3 -2
  179. package/workspace/public/pm/shell-recording.js +29 -8
  180. package/workspace/public/pm/tokens.css +2 -2
  181. package/workspace/public/pm/view-notes.js +46 -9
  182. package/workspace/public/pm/welcome.js +197 -89
  183. package/workspace/public/welcome.html +6 -4
  184. package/workspace/server.js +105 -6
  185. package/workspace/skills/day-check.skill +1 -1
  186. package/workspace/skills/jira-sync.skill +6 -4
  187. package/workspace/skills/morning-brief.skill +1 -1
  188. package/workspace/test-files.json +12 -3
  189. package/workspace/test-lists/c3-ib-items.json +9 -0
  190. package/workspace/test-lists/c3-ib-more.json +9 -0
  191. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  192. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  193. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  194. package/workspace/test-lists/console3-briefing.json +9 -0
  195. package/workspace/test-lists/console3-foundation.json +9 -0
  196. package/workspace/test-lists/console3-reports.json +3 -3
  197. package/workspace/test-lists/meeting-notes.json +10 -4
  198. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  199. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  200. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  201. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  202. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  203. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  204. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  205. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  206. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  207. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  208. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  209. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+ /*
3
+ * breakdown-ops.js — Breakdown is PMOS-only, non-development work (console3 wave E1b; HANDOFF 14f). Tickets are Jira only.
4
+ *
5
+ * Three phases (discovery, design, launch); each item has a source (local, figma, calendar, gmail, notes), an owner and points.
6
+ * An item can wait on a Jira ticket: a `dependency` event holds the ticket key, and planInput() turns the items into the
7
+ * { id: { deps, dur } } shape that scripts/derived/plan.js plan() reads (so release notes wait on the audit ticket).
8
+ *
9
+ * STORE. One append-only JSONL, <root>/breakdown-pmos.jsonl, written only through scripts/trust/ledger.js append. The writer
10
+ * is apply(); every other function is pure. Lines: { id (ULID), kind: add|edit|delete|undo|dependency, rid, fields?, snapshot?,
11
+ * corrects?, why, at, by, via }. `by` and `via` come from the call path (the ledger), never from a caller.
12
+ *
13
+ * JIRA IS READ-ONLY HERE. Jira-owned fields (title, owner, status) of an item a Jira ticket represents change only as a
14
+ * PROPOSAL ({ proposal:true }, no event). Such an item cannot be deleted here: the answer is unlink advice. Jira engineering
15
+ * load enters capacityLoad() as data and is returned as data; no function writes it.
16
+ */
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const ledger = require('./trust/ledger.js');
20
+
21
+ const PHASES = ['discovery', 'design', 'launch'];
22
+ const SOURCES = ['local', 'figma', 'calendar', 'gmail', 'notes'];
23
+ const JIRA_OWNED = ['title', 'owner', 'status'];
24
+ const UNDO_MS = 8000;
25
+ const DEFAULT_PPW = 5; // same default as workspace/lib/routes-forecast.js for an owner with no entry
26
+ const JIRA_KEY = /^[A-Z][A-Z0-9]+-\d+$/;
27
+ const FILE = 'breakdown-pmos.jsonl';
28
+
29
+ const refuse = (error, extra) => ({ ok: false, error, ...(extra || {}) });
30
+ const pts = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
31
+ const fileOf = (root) => path.join(root, FILE);
32
+ const readLines = (root) => { try { return ledger.read(fileOf(root)); } catch { return []; } };
33
+
34
+ /** fold(lines) -> items in first-add order. delete removes, undo restores the snapshot, dependency edits `deps` (Jira keys). */
35
+ function fold(lines) {
36
+ const by = new Map();
37
+ const deletes = new Map();
38
+ for (const e of lines) {
39
+ if (!e || !e.rid) continue;
40
+ if (e.kind === 'add') by.set(e.rid, { rid: e.rid, deps: [], ...e.fields });
41
+ else if (e.kind === 'edit' && by.has(e.rid)) Object.assign(by.get(e.rid), e.fields);
42
+ else if (e.kind === 'dependency' && by.has(e.rid) && e.fields && e.fields.jira) {
43
+ const it = by.get(e.rid), k = e.fields.jira;
44
+ it.deps = e.fields.remove ? it.deps.filter((d) => d !== k) : [...new Set([...it.deps, k])];
45
+ } else if (e.kind === 'delete' && by.has(e.rid)) { deletes.set(e.id, e.snapshot || by.get(e.rid)); by.delete(e.rid); }
46
+ else if (e.kind === 'undo' && deletes.has(e.corrects)) by.set(e.rid, { ...deletes.get(e.corrects) });
47
+ }
48
+ return [...by.values()];
49
+ }
50
+ const list = (root) => fold(readLines(root));
51
+
52
+ function checkFields(f, { partial }) {
53
+ if ('title' in f || !partial) if (!String(f.title || '').trim()) return 'an item needs a title';
54
+ if ('phase' in f || !partial) if (!PHASES.includes(f.phase)) return 'phase must be discovery, design or launch';
55
+ if ('source' in f) {
56
+ if (String(f.source).toLowerCase() === 'jira') return 'Jira tickets live in Tickets; Breakdown holds PMOS-only work';
57
+ if (!SOURCES.includes(f.source)) return 'source must be local, figma, calendar, gmail or notes';
58
+ }
59
+ if ('points' in f && f.points !== null && !pts(f.points)) return 'points must be a number above 0';
60
+ return null;
61
+ }
62
+
63
+ const newRid = () => 'bd-' + ledger.newId().slice(-8).toLowerCase();
64
+
65
+ /** addItem(items, { title, phase, source?, owner?, points?, initiative?, jira_key? }) -> { ok, rid, events } */
66
+ function addItem(items, input) {
67
+ const i = input || {};
68
+ const fields = { title: i.title, phase: i.phase, source: i.source || 'local' };
69
+ const err = checkFields({ ...fields, ...(i.points != null ? { points: i.points } : {}) }, { partial: false });
70
+ if (err) return refuse(err);
71
+ fields.title = String(i.title).trim();
72
+ for (const k of ['owner', 'points', 'initiative', 'jira_key']) if (i[k] != null && i[k] !== '') fields[k] = i[k];
73
+ if (fields.jira_key && !JIRA_KEY.test(fields.jira_key)) return refuse('jira_key must look like ABC-123');
74
+ const rid = i.rid || newRid();
75
+ if ((items || []).some((x) => x.rid === rid)) return refuse('item exists: ' + rid);
76
+ return { ok: true, rid, events: [{ kind: 'add', rid, fields, why: 'item added' }] };
77
+ }
78
+
79
+ /**
80
+ * editItem(items, rid, fields) -> { ok, events, proposal?, proposals? }
81
+ * title, owner and status of an item a Jira ticket represents are Jira's: they come back as { proposal:true, proposals:[...] }
82
+ * with NO event. The other fields (phase, points, source) still apply.
83
+ */
84
+ function editItem(items, rid, fields) {
85
+ const it = (items || []).find((x) => x.rid === rid);
86
+ if (!it) return refuse('no such item: ' + rid);
87
+ const f = { ...(fields || {}) };
88
+ delete f.rid; delete f.deps;
89
+ const out = { ...f };
90
+ const proposals = [];
91
+ if (it.jira_key) {
92
+ for (const k of [...JIRA_OWNED, 'jira_key']) {
93
+ if (k in out && out[k] !== it[k]) { proposals.push({ field: k, from: it[k] === undefined ? null : it[k], to: out[k] }); }
94
+ delete out[k];
95
+ }
96
+ }
97
+ const err = checkFields(out, { partial: true });
98
+ if (err) return refuse(err);
99
+ const keys = Object.keys(out).filter((k) => out[k] !== it[k]);
100
+ const events = keys.length ? [{ kind: 'edit', rid, fields: Object.fromEntries(keys.map((k) => [k, out[k]])), why: 'edited ' + keys.join(', ') }] : [];
101
+ if (!events.length && !proposals.length) return refuse('nothing to change');
102
+ return { ok: true, events, ...(proposals.length ? { proposal: true, proposals, note: 'Jira owns these fields; the change is a proposal the PM sends by slide, nothing was applied' } : {}) };
103
+ }
104
+
105
+ /** deleteItem(items, rid, { now }) -> a delete event with a snapshot; undo for UNDO_MS. A Jira-backed item gets unlink advice. */
106
+ function deleteItem(items, rid, opts = {}) {
107
+ const it = (items || []).find((x) => x.rid === rid);
108
+ if (!it) return refuse('no such item: ' + rid);
109
+ if (it.jira_key) return refuse('a Jira ticket represents this item, so it cannot be deleted here', { jira_backed: true, advice: `Unlink ${it.jira_key} from the item, or close the ticket in Jira; Breakdown does not delete what Jira holds` });
110
+ const now = opts.now || new Date();
111
+ return { ok: true, events: [{ kind: 'delete', rid, snapshot: { ...it }, why: 'item deleted', at: now.toISOString() }], undo_ms: UNDO_MS };
112
+ }
113
+
114
+ /** undoEvent(lines, rid, now) -> { ok, events } while the last delete of rid is younger than 8 seconds; else a refusal. */
115
+ function undoEvent(lines, rid, now = new Date()) {
116
+ const del = [...lines].reverse().find((e) => e && e.rid === rid && (e.kind === 'delete' || e.kind === 'undo'));
117
+ if (!del || del.kind === 'undo') return refuse('nothing to undo for ' + rid);
118
+ if (now.getTime() - new Date(del.at).getTime() > UNDO_MS) return refuse('the undo window (8 seconds) has passed', { expired: true });
119
+ return { ok: true, events: [{ kind: 'undo', rid, corrects: del.id, why: 'delete undone' }] };
120
+ }
121
+
122
+ /** setDependency(items, rid, jiraKey, { remove }) -> a dependency event; the item then waits on that ticket. */
123
+ function setDependency(items, rid, jiraKey, opts = {}) {
124
+ const it = (items || []).find((x) => x.rid === rid);
125
+ if (!it) return refuse('no such item: ' + rid);
126
+ if (!JIRA_KEY.test(String(jiraKey || ''))) return refuse('a dependency is a Jira ticket key such as ABC-123');
127
+ const has = it.deps.includes(jiraKey);
128
+ if (opts.remove ? !has : has) return refuse(opts.remove ? 'no such dependency' : 'already depends on ' + jiraKey);
129
+ return { ok: true, events: [{ kind: 'dependency', rid, fields: { jira: jiraKey, ...(opts.remove ? { remove: true } : {}) }, why: (opts.remove ? 'stopped waiting on ' : 'waits on ') + jiraKey }] };
130
+ }
131
+
132
+ /**
133
+ * planInput(items, jira, { daysPerPoint }) -> { id: { deps, dur } } for scripts/derived/plan.js plan(). `jira` is read-only data,
134
+ * { 'ABC-1': { days, done } }. A Jira key an item waits on becomes a node with the ticket's remaining days (0 when done or unknown).
135
+ */
136
+ function planInput(items, jira, opts = {}) {
137
+ const dpp = opts.daysPerPoint || 1;
138
+ const s = {};
139
+ for (const it of items || []) {
140
+ s[it.rid] = { deps: [...it.deps], dur: it.done ? 0 : (it.points || 1) * dpp };
141
+ for (const k of it.deps) if (!s[k]) { const j = (jira || {})[k] || {}; s[k] = { deps: [], dur: j.done ? 0 : Number(j.days) || 0 }; }
142
+ }
143
+ return s;
144
+ }
145
+
146
+ /**
147
+ * capacityLoad(items, people, weeks, jiraLoad) -> [{ person, pmos_points, capacity, jira_points, over, free }]
148
+ * Compares the PMOS points of each owner to points_per_week * weeks. jiraLoad ({ person: points }) is engineering load, shown
149
+ * read-only and never subtracted or written. Unassigned points come back as person null.
150
+ */
151
+ function capacityLoad(items, people, weeks = 2, jiraLoad = {}) {
152
+ const jira = Object.freeze({ ...jiraLoad });
153
+ const ppw = new Map((people || []).map((p) => [p.id, Number(p.points_per_week) || DEFAULT_PPW]));
154
+ const load = new Map();
155
+ for (const it of items || []) {
156
+ if (it.done) continue;
157
+ const who = it.owner || null;
158
+ load.set(who, (load.get(who) || 0) + (Number(it.points) || 1));
159
+ }
160
+ const ids = [...new Set([...ppw.keys(), ...[...load.keys()].filter(Boolean)])];
161
+ const rows = ids.map((person) => {
162
+ const capacity = (ppw.get(person) || DEFAULT_PPW) * weeks, used = load.get(person) || 0;
163
+ return { person, pmos_points: used, capacity, jira_points: jira[person] || 0, over: used > capacity, free: Math.max(0, capacity - used) };
164
+ });
165
+ if (load.has(null)) rows.push({ person: null, pmos_points: load.get(null), capacity: 0, jira_points: 0, over: false, free: 0, unassigned: true });
166
+ return rows;
167
+ }
168
+
169
+ /** apply(root, events, actor, { now }) -> { ok, written } | { ok:false, error }. The one writer; ledger.append validates each line. */
170
+ function apply(root, events, actor, opts = {}) {
171
+ const now = opts.now || new Date();
172
+ fs.mkdirSync(root, { recursive: true });
173
+ const written = [];
174
+ try {
175
+ for (const e of events) written.push(ledger.append(fileOf(root), { id: ledger.newId(), at: now.toISOString(), ...e }, actor));
176
+ } catch (err) { return { ok: false, error: err.message, written }; }
177
+ return { ok: true, written };
178
+ }
179
+
180
+ module.exports = { PHASES, SOURCES, JIRA_OWNED, UNDO_MS, FILE, fileOf, readLines, fold, list, addItem, editItem, deleteItem, undoEvent, setDependency, planInput, capacityLoad, apply };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+ /* calendar-import.js — cached calendar metadata, supplied by a caller (console3 wave E2a).
3
+ *
4
+ * PMOS holds no calendar credentials. An agent session with the PM's own connector fetches the events and hands them
5
+ * here; nothing in this file calls a network. An import is untrusted data (scripts/trust/import-guard.js): hidden text
6
+ * is stripped, every line is datamarked, the raw text is hashed, and the only claim an import can make is 'reported'.
7
+ *
8
+ * importEvents(rawEvents, { fetchedAt, sensitivity }) → [event] pure; a missing or unknown sensitivity is refused
9
+ * foldEvents(lines) → [event] current events; the PM's notes survive a refresh
10
+ * appendImport(file, events, actor, now) → lines ledger append (scripts/trust/ledger.js)
11
+ * appendNote(file, eventId, text, actor, now) → line a PM note on one event
12
+ * readLines(file) · cacheMarkdown(events, dir, now) the Markdown cache is the PM's trust/import-calendar.js, unchanged
13
+ */
14
+ const fs = require('fs');
15
+ const crypto = require('crypto');
16
+ const path = require('path');
17
+ const guard = require('./trust/import-guard.js');
18
+ const L = require('./trust/ledger.js');
19
+ const sampleCache = require('./trust/import-calendar.js');
20
+
21
+ const SENSITIVITY = Object.freeze(['public', 'internal', 'confidential', 'private']);
22
+ const HIDDEN = [/<!--[\s\S]*?-->/g, /[​-‏⁠]/g, /[\u{E0000}-\u{E007F}]/gu];
23
+
24
+ const strip = (s) => HIDDEN.reduce((t, re) => t.replace(re, ''), String(s == null ? '' : s));
25
+ const mark = (s) => s.split('\n').map((l) => '▒ ' + l).join('\n');
26
+ const nameOf = (a) => (typeof a === 'string' ? a : (a && (a.name || a.displayName)) || '');
27
+
28
+ function importEvents(rawEvents, opts = {}) {
29
+ if (!SENSITIVITY.includes(opts.sensitivity)) throw new Error('an import needs a sensitivity label: ' + SENSITIVITY.join(', '));
30
+ if (!Array.isArray(rawEvents)) throw new Error('events must be a list');
31
+ const fetchedAt = opts.fetchedAt || new Date().toISOString();
32
+ return rawEvents.map((e, i) => {
33
+ const at = e && (e.at || e.start);
34
+ if (!e || !e.id || !e.title || !at) throw new Error(`event ${i}: id, title and start are required`);
35
+ const attendeesRaw = (e.attendees || []).map(nameOf).filter(Boolean);
36
+ const rawText = [e.title, ...attendeesRaw].join('\n');
37
+ const findings = guard.scan(rawText);
38
+ const title = strip(e.title).replace(/\s+/g, ' ').trim();
39
+ const attendees = attendeesRaw.map((a) => strip(a).replace(/\s+/g, ' ').trim());
40
+ return {
41
+ id: String(e.id), at: String(at), end: e.end ? String(e.end) : null, title, attendees,
42
+ initiative: e.initiative ? String(e.initiative) : null, agent_prep_notes: '',
43
+ fetched_at: fetchedAt, external: true, sensitivity: opts.sensitivity,
44
+ label: 'reported', trust: 'untrusted', marked: mark([title, ...attendees].join('\n')),
45
+ findings, raw_sha256: crypto.createHash('sha256').update(rawText).digest('hex'),
46
+ };
47
+ });
48
+ }
49
+
50
+ // Lines: { kind:'calendar_event', ...event } replaces the cached part; { kind:'calendar_note', event_id, text } adds a PM note.
51
+ function foldEvents(lines) {
52
+ const by = new Map();
53
+ for (const l of lines || []) {
54
+ if (!l) continue;
55
+ if (l.kind === 'calendar_event' && l.event && l.event.id) {
56
+ const prev = by.get(l.event.id);
57
+ by.set(l.event.id, { ...l.event, agent_prep_notes: prev ? prev.agent_prep_notes : l.event.agent_prep_notes || '', pm_notes: prev ? prev.pm_notes : [] });
58
+ } else if (l.kind === 'calendar_note' && by.has(l.event_id)) {
59
+ by.get(l.event_id).pm_notes.push({ text: l.text, at: l.at, id: l.id });
60
+ }
61
+ }
62
+ return [...by.values()].sort((a, b) => String(a.at).localeCompare(String(b.at)));
63
+ }
64
+
65
+ const readLines = (file) => L.read(file);
66
+
67
+ function appendImport(file, events, actor, now) {
68
+ fs.mkdirSync(path.dirname(file), { recursive: true });
69
+ return events.map((event) => L.append(file, { id: L.newId(), kind: 'calendar_event', why: 'calendar import (reported, untrusted)', at: now || new Date().toISOString(), event }, actor));
70
+ }
71
+
72
+ function appendNote(file, eventId, text, actor, now) {
73
+ const t = String(text || '').trim();
74
+ if (!t) throw new Error('the note needs text');
75
+ fs.mkdirSync(path.dirname(file), { recursive: true });
76
+ return L.append(file, { id: L.newId(), kind: 'calendar_note', why: 'PM note on an imported event', event_id: eventId, text: t, at: now || new Date().toISOString() }, actor);
77
+ }
78
+
79
+ // The PM's Markdown cache writer, unchanged; events are mapped to its shape. (It labels files 'internal'; the ledger holds the real label.)
80
+ function cacheMarkdown(events, dir, now) {
81
+ return sampleCache.importEvents(events.map((e) => ({ id: e.id, title: e.title, start: e.at, end: e.end || e.at, attendees: e.attendees, initiative: e.initiative })), dir, now);
82
+ }
83
+
84
+ module.exports = { SENSITIVITY, importEvents, foldEvents, appendImport, appendNote, readLines, cacheMarkdown };
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+ /* calendar-model.js — the pure model behind the Calendar views (console3 wave C2).
3
+ *
4
+ * rangeFor(view, date) → { view, date, start, end, days } ; end is the last day, inclusive
5
+ * bucketEvents(events, view, date) → { range, unit, cells: [{ key, start, end, events[] }] }
6
+ * ganttRows(initiatives, view, date, opts) → rows: planned against forecast against promised, with a status
7
+ * strategyLens(initiatives, krs, today) → { north_star, deviations, pivot, reason }
8
+ *
9
+ * No I/O and no clock: every date comes in as 'YYYY-MM-DD'. Weeks start on Monday; a quarter is a calendar quarter.
10
+ * Nothing here invents a number: a plan, a forecast or a KR metric that was not supplied is reported as missing.
11
+ *
12
+ * SLIP RULE. slip_days = forecast end minus planned end. Tolerance = the initiative's slip_tolerance_days, else 10 percent
13
+ * of the planned duration (rounded, at least 1 day when a duration is known). slip <= 0 is on time; 0 < slip < tolerance is
14
+ * on time; slip = tolerance up to twice tolerance is at risk; slip beyond twice tolerance is delayed. An initiative past
15
+ * its promised date with no end (not done, no actual end) is overdue, which wins over the slip statuses.
16
+ *
17
+ * DEVIATION RULE. A KR's expected value is a straight line from baseline (at start) to target (at date). EARLY_WARNING is
18
+ * the shortfall, as a share of the baseline-to-target span, at which a KR is flagged: expected minus actual >= 10 percent.
19
+ * PIVOT RULE. A KR signals a pivot when it carries at least PIVOT_CHECKS (3) dated checks, each behind the line by the
20
+ * early-warning share, and the latest shortfall is no smaller than the first (it is not closing). */
21
+
22
+ const V = require('./derived/derive.js');
23
+
24
+ /* BACKED BY THE PM'S SAMPLE: the slip tolerance (derived/derive.js). REPO-WRITTEN, the sample has no equivalent: the Day to Year
25
+ * ranges and buckets, the on time / at risk / delayed bands, and the strategy lens (deviation and pivot rules). */
26
+ const DAY = 864e5;
27
+ const EARLY_WARNING = 0.10;
28
+ const PIVOT_CHECKS = 3;
29
+ const VIEWS = ['day', 'week', 'month', 'quarter', 'year'];
30
+
31
+ const pad = (n) => String(n).padStart(2, '0');
32
+ const isDate = (s) => typeof s === 'string' && /^\d{4}-\d{2}-\d{2}/.test(s) && !Number.isNaN(Date.parse(s.slice(0, 10) + 'T00:00:00Z'));
33
+ const ms = (s) => Date.parse(String(s).slice(0, 10) + 'T00:00:00Z');
34
+ const fmt = (t) => new Date(t).toISOString().slice(0, 10);
35
+ const addDays = (s, n) => fmt(ms(s) + n * DAY);
36
+ const diffDays = (a, b) => Math.round((ms(a) - ms(b)) / DAY);
37
+ const ymd = (y, m, d) => `${y}-${pad(m)}-${pad(d)}`;
38
+ const lastOfMonth = (y, m) => fmt(Date.UTC(y, m, 0)); // m is 1-based; day 0 of the next month
39
+
40
+ function rangeFor(view, date) {
41
+ if (!VIEWS.includes(view)) throw new Error('unknown view: ' + view);
42
+ if (!isDate(date)) throw new Error('date must be YYYY-MM-DD');
43
+ const d = date.slice(0, 10);
44
+ const y = Number(d.slice(0, 4)); const m = Number(d.slice(5, 7));
45
+ let start; let end;
46
+ if (view === 'day') { start = d; end = d; }
47
+ else if (view === 'week') { const dow = (new Date(ms(d)).getUTCDay() + 6) % 7; start = addDays(d, -dow); end = addDays(start, 6); }
48
+ else if (view === 'month') { start = ymd(y, m, 1); end = lastOfMonth(y, m); }
49
+ else if (view === 'quarter') { const q = Math.floor((m - 1) / 3); start = ymd(y, q * 3 + 1, 1); end = lastOfMonth(y, q * 3 + 3); }
50
+ else { start = ymd(y, 1, 1); end = ymd(y, 12, 31); }
51
+ return { view, date: d, start, end, days: diffDays(end, start) + 1 };
52
+ }
53
+
54
+ /* the cell unit of a view: a day view and a week view hold days, a month holds days, a quarter holds weeks, a year months */
55
+ const UNIT = { day: 'day', week: 'day', month: 'day', quarter: 'week', year: 'month' };
56
+
57
+ function cellsFor(range, unit) {
58
+ const cells = [];
59
+ if (unit === 'day') for (let d = range.start; d <= range.end; d = addDays(d, 1)) cells.push({ key: d, start: d, end: d });
60
+ else if (unit === 'week') {
61
+ for (let s = rangeFor('week', range.start).start; s <= range.end; s = addDays(s, 7)) cells.push({ key: s, start: s, end: addDays(s, 6) });
62
+ } else {
63
+ for (let mo = 1; mo <= 12; mo++) {
64
+ const y = Number(range.start.slice(0, 4));
65
+ const s = ymd(y, mo, 1); const e = lastOfMonth(y, mo);
66
+ if (e >= range.start && s <= range.end) cells.push({ key: s.slice(0, 7), start: s, end: e });
67
+ }
68
+ }
69
+ return cells.map((c) => ({ ...c, events: [] }));
70
+ }
71
+
72
+ /* An event lands in every cell its span touches: an all-day event (a date with no time) in its day, a multi-day event in
73
+ * each day it covers. Events with no usable `at` are dropped; events outside the range are not placed. */
74
+ function bucketEvents(events, view, date) {
75
+ const range = rangeFor(view, date);
76
+ const unit = UNIT[view];
77
+ const cells = cellsFor(range, unit);
78
+ for (const e of events || []) {
79
+ if (!e || !isDate(e.at)) continue;
80
+ const a = String(e.at).slice(0, 10);
81
+ let b = e.end && isDate(e.end) ? String(e.end).slice(0, 10) : a;
82
+ // an end at exactly 00:00 of a later day is exclusive (the usual all-day convention), so it does not spill into that day
83
+ if (b > a && /T00:00(:00)?/.test(String(e.end)) ) b = addDays(b, -1);
84
+ if (b < a) b = a;
85
+ for (const c of cells) if (a <= c.end && b >= c.start) c.events.push(e);
86
+ }
87
+ return { range, unit, cells };
88
+ }
89
+
90
+ /* The slip tolerance is the sample's (derived/derive.js tolerance, from facts.js): the initiative's own slip_tolerance_days,
91
+ * else 10 percent of the planned duration, at least 1 day. Called with the planned duration as a start and a promise that far
92
+ * apart. Unlike the older code, an explicit 0 falls back to the default (the sample reads the field with ||). */
93
+ function toleranceOf(i, plannedDays) {
94
+ const explicit = i.slip_tolerance_days != null && i.slip_tolerance_days !== '' && Number.isFinite(Number(i.slip_tolerance_days));
95
+ if (!explicit && plannedDays == null) return null;
96
+ return V.tolerance(explicit ? i : {}, '1970-01-01', addDays('1970-01-01', plannedDays || 0));
97
+ }
98
+
99
+ function ganttRows(initiatives, view, date, opts = {}) {
100
+ const range = rangeFor(view, date);
101
+ const today = isDate(opts.today) ? opts.today.slice(0, 10) : range.date;
102
+ const rows = [];
103
+ for (const i of initiatives || []) {
104
+ if (!i || !i.id) continue;
105
+ const promised = isDate(i.promised) ? i.promised.slice(0, 10) : null;
106
+ let pStart = i.planned && isDate(i.planned.start) ? i.planned.start.slice(0, 10) : (isDate(i.planned_start) ? i.planned_start.slice(0, 10) : null);
107
+ let pEnd = i.planned && isDate(i.planned.end) ? i.planned.end.slice(0, 10) : (isDate(i.planned_end) ? i.planned_end.slice(0, 10) : promised);
108
+ const pd = i.plannedDays != null ? Number(i.plannedDays) : (i.planned_days != null ? Number(i.planned_days) : null);
109
+ if (!pStart && pEnd && Number.isFinite(pd)) pStart = addDays(pEnd, -pd);
110
+ const fStart = i.forecast && isDate(i.forecast.start) ? i.forecast.start.slice(0, 10) : null;
111
+ const fEnd = i.forecast && typeof i.forecast === 'object' ? (isDate(i.forecast.end) ? i.forecast.end.slice(0, 10) : null) : (isDate(i.forecast) ? String(i.forecast).slice(0, 10) : null);
112
+ const duration = pStart && pEnd ? Math.max(0, diffDays(pEnd, pStart)) : (Number.isFinite(pd) ? pd : null);
113
+ const tol = toleranceOf(i, duration);
114
+ const slip = pEnd && fEnd ? diffDays(fEnd, pEnd) : 0;
115
+ const ended = !!(i.done || i.actual_end);
116
+ let status;
117
+ if (promised && !ended && promised < today) status = 'overdue';
118
+ else if (!pEnd || !fEnd) status = 'no plan';
119
+ else if (slip <= 0) status = 'on time';
120
+ else if (tol == null || slip < tol) status = 'on time';
121
+ else if (slip <= 2 * tol) status = 'at risk';
122
+ else status = 'delayed';
123
+ // keep a row when its bar touches the range, or when it has no dates at all (so a missing plan is shown, not dropped)
124
+ const lo = pStart || fStart || pEnd || fEnd || promised; const hi = [pEnd, fEnd, promised].filter(Boolean).sort().pop() || lo;
125
+ if (lo && !(lo <= range.end && hi >= range.start)) continue;
126
+ rows.push({
127
+ id: i.id, title: i.title || i.id,
128
+ planned: pEnd || pStart ? { start: pStart, end: pEnd } : null,
129
+ forecast: fEnd || fStart ? { start: fStart || pStart, end: fEnd } : null,
130
+ promised, status, slip_days: slip, tolerance_days: tol,
131
+ });
132
+ }
133
+ return rows;
134
+ }
135
+
136
+ function dayOf(x) { return isDate(x) ? String(x).slice(0, 10) : null; }
137
+
138
+ /* expected value on the straight line from baseline at start to target at date, clamped to the period */
139
+ function expectedAt(kr, at) {
140
+ const start = dayOf(kr.start) || rangeFor('quarter', kr.date).start;
141
+ const span = diffDays(kr.date, start);
142
+ const f = span <= 0 ? 1 : Math.min(1, Math.max(0, diffDays(at, start) / span));
143
+ return kr.baseline + (kr.target - kr.baseline) * f;
144
+ }
145
+
146
+ function strategyLens(initiatives, krs, today) {
147
+ const inis = (initiatives || []).filter(Boolean);
148
+ const list = Array.isArray(krs) ? krs.filter(Boolean) : [];
149
+ const out = {
150
+ north_star: { progress: null, scored: 0, total: list.length, initiatives: { total: inis.length, done: inis.filter((i) => i.done).length } },
151
+ deviations: [], unscored: [], pivot: { signal: false, reason: '' }, reason: '',
152
+ };
153
+ if (!list.length) {
154
+ out.reason = 'No key results were supplied, so nothing is scored.';
155
+ out.pivot.reason = out.reason;
156
+ return out;
157
+ }
158
+ const num = (v) => v !== null && v !== undefined && v !== '' && Number.isFinite(Number(v));
159
+ const progresses = []; const pivotKrs = [];
160
+ for (const kr of list) {
161
+ const name = kr.id || kr.title || kr.kr || 'kr';
162
+ const ok = kr.metric && num(kr.baseline) && num(kr.target) && num(kr.actual) && isDate(kr.date) && Number(kr.target) !== Number(kr.baseline);
163
+ if (!ok) { out.unscored.push({ kr: name, status: 'no metric' }); continue; }
164
+ const k = { ...kr, baseline: Number(kr.baseline), target: Number(kr.target), actual: Number(kr.actual) };
165
+ const at = isDate(today) ? today.slice(0, 10) : k.date;
166
+ const span = k.target - k.baseline;
167
+ const expected = expectedAt(k, at);
168
+ const shortfall = (expected - k.actual) / Math.abs(span) * Math.sign(span); // positive when behind the line
169
+ progresses.push(Math.min(1, Math.max(0, (k.actual - k.baseline) / span)));
170
+ out.deviations.push({ kr: name, metric: k.metric, expected: +expected.toFixed(4), actual: k.actual, shortfall: +shortfall.toFixed(4), early_warning: shortfall >= EARLY_WARNING });
171
+ const checks = (Array.isArray(kr.checks) ? kr.checks : []).filter((c) => c && isDate(c.date) && num(c.actual)).sort((a, b) => ms(a.date) - ms(b.date));
172
+ if (checks.length >= PIVOT_CHECKS) {
173
+ const sf = checks.map((c) => (expectedAt(k, c.date.slice(0, 10)) - Number(c.actual)) / Math.abs(span) * Math.sign(span));
174
+ if (sf.every((v) => v >= EARLY_WARNING) && sf[sf.length - 1] >= sf[0]) pivotKrs.push(name);
175
+ }
176
+ }
177
+ out.north_star.scored = progresses.length;
178
+ out.north_star.progress = progresses.length ? +(progresses.reduce((a, b) => a + b, 0) / progresses.length).toFixed(4) : null;
179
+ if (!progresses.length) out.reason = 'Every key result is missing a metric, so nothing is scored.';
180
+ if (pivotKrs.length) out.pivot = { signal: true, reason: `${pivotKrs.join(', ')} behind the line at ${PIVOT_CHECKS} or more checks and not closing.` };
181
+ else out.pivot.reason = progresses.length ? 'No key result has been behind the line at three checks in a row.' : out.reason;
182
+ return out;
183
+ }
184
+
185
+ module.exports = { VIEWS, EARLY_WARNING, PIVOT_CHECKS, rangeFor, bucketEvents, ganttRows, strategyLens, cellsFor };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+ /* card-state.js — the run card's logic, a thin adapter over scripts/reply-contract/reply-contract.js (the PM's sample).
3
+ * The state is the contract's fold phase, never re-derived here; the moves are the contract's nextMoves; a correction
4
+ * is the contract's applyMove record. What is local: card shaping, swipes, fallbacks, the coverage share.
5
+ * cardFor(runLines, { run, ctx, extra, ended, error }) · nextMoves(result) · correctionEvent(kind, reason, run, now)
6
+ * swipeAction(direction, card) · fallbackFor(kind) · coverageShare(runs) · permissionCard · questionCard
7
+ */
8
+ const C = require('./reply-contract/reply-contract');
9
+ const { foldReplies } = require('./replies');
10
+
11
+ const REJECT_REASONS = C.REJECT.slice();
12
+ const REVISE_OPTIONS = Object.keys(C.REVISE);
13
+ const COVERAGE_LIMIT = 0.1;
14
+ const DENY_MESSAGE = 'waiting for the PM';
15
+ const PM_PRESS = { by: 'pm', via: 'ui' };
16
+
17
+ const TITLES = {
18
+ queued: 'Starting', not_started: 'Not started', working: 'Working', asking: 'Needs your answer', permission: 'Needs your permission',
19
+ attention: 'Needs the terminal', paused: 'Paused', quiet: 'Quiet', degraded: 'Plain text only', failed: 'Failed', done: 'Result',
20
+ };
21
+
22
+ // The card for one run. `extra` = lines from other files (the PM's answers); `ended`/`error` close the run from outside.
23
+ function cardFor(runLines = [], { run, ctx, extra = [], ended = false, error = false } = {}) {
24
+ let lines = runLines.concat(extra);
25
+ const r = run || require('./replies').runOf(lines);
26
+ if ((ended || error) && !lines.some((l) => l && l.kind === 'end')) lines = lines.concat([{ kind: 'end', run: r.id, session: r.session, exit: error ? 1 : 0 }]);
27
+ const vm = foldReplies(lines, r, ctx);
28
+ const card = {
29
+ state: vm.phase, title: TITLES[vm.phase] || vm.phase, steps: vm.steps, ask: vm.ask, permission: vm.permission, attention: vm.attention,
30
+ failure: vm.failure, recover: vm.recover, problems: vm.problems, unlabelled: vm.unlabelled, fallbackText: vm.fallbackText,
31
+ result: vm.result, claims: (vm.result && vm.result.claims) || [], blocked: vm.failure && vm.failure.code === 'blocked' ? vm.failure.why : null,
32
+ nextMoves: [], fallback: null,
33
+ };
34
+ if (vm.result) { card.nextMoves = nextMoves(vm.result); card.revise = REVISE_OPTIONS; card.reject = REJECT_REASONS; }
35
+ if (vm.recover.includes('open_session')) card.fallback = fallbackFor(vm.phase === 'degraded' ? 'plain' : 'terminal');
36
+ return card;
37
+ }
38
+
39
+ // The moves under a result: { verb, by, label, prompt? }, the contract's own.
40
+ function nextMoves(result, opts) {
41
+ return C.nextMoves(result || {}, opts).moves;
42
+ }
43
+
44
+ // An append-only correction event. Every Reject and Revise is one; the caller appends it with appendOutbox.
45
+ function correctionEvent(kind, reason, run, now = new Date()) {
46
+ if (kind !== 'reject' && kind !== 'revise') throw new Error('a correction is a reject or a revise');
47
+ const m = C.applyMove(kind, reason, { id: run || null }, PM_PRESS);
48
+ if (m.error) throw new Error(m.error);
49
+ return { type: 'correction', kind, reason, run: run || null, counted: true, at: new Date(now).toISOString(), record: m.record };
50
+ }
51
+
52
+ // Right = the card's primary action; left = open the reply tray. Neither reaches the outbox.
53
+ function swipeAction(direction, card = {}) {
54
+ if (direction === 'left') return { action: 'open-reply-tray', outward: false };
55
+ if (direction === 'right') return { action: card.state === 'done' && (card.nextMoves || []).some((m) => m.verb === 'accept') ? 'accept' : 'none', outward: false };
56
+ return { action: 'none', outward: false };
57
+ }
58
+
59
+ function fallbackFor(kind) {
60
+ if (kind === 'plain' || kind === 'tier2' || kind === 2) {
61
+ return { tier: 2, shown: 'plain text', checked: false, canBackClaim: false, note: 'The agent did not use the format, so nothing here is checked.' };
62
+ }
63
+ return { tier: 3, shown: 'terminal only', checked: false, canBackClaim: false, action: 'Open the session' };
64
+ }
65
+
66
+ // runs: [{ tier: 1|2|3 }]. The PRD: above about 10% the contract is wrong. The number is returned, never hidden.
67
+ function coverageShare(runs = []) {
68
+ const n = runs.length;
69
+ const low = runs.filter((r) => r && (r.tier === 2 || r.tier === 3)).length;
70
+ const share = n ? low / n : 0;
71
+ return { share, low, runs: n, overLimit: share > COVERAGE_LIMIT, limit: COVERAGE_LIMIT };
72
+ }
73
+
74
+ function permissionCard(request = {}) {
75
+ return {
76
+ kind: 'permission', command: request.command || '', cwd: request.cwd || '', why: request.why || '',
77
+ options: ['allow once', 'allow always for this branch', 'deny'],
78
+ onDeny: { behavior: 'deny', message: DENY_MESSAGE }, denyMessage: DENY_MESSAGE,
79
+ };
80
+ }
81
+
82
+ // The CLI question tool's input, through the contract's askFromTool (up to 4 questions, 6 options each).
83
+ function questionCard(input = {}) {
84
+ const qs = Array.isArray(input) ? input : input.questions || [];
85
+ const r = C.askFromTool({ questions: qs.map((q) => ({ ...q, options: (q.options || []).map((o) => (typeof o === 'string' ? { label: o } : o)) })) });
86
+ return { kind: 'question', questions: r.asks || [], truncated: qs.length > 4, error: r.error || null, onDeny: { behavior: 'deny', message: DENY_MESSAGE } };
87
+ }
88
+
89
+ module.exports = {
90
+ REJECT_REASONS, REVISE_OPTIONS, COVERAGE_LIMIT, DENY_MESSAGE, TITLES,
91
+ cardFor, nextMoves, correctionEvent, swipeAction, fallbackFor, coverageShare, permissionCard, questionCard,
92
+ };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+ /* derive.js — the PM's derived-view expressions from sample/reader.js and sample/facts.js, lifted into pure functions.
3
+ * reader.js is a script that runs on load against one portfolio root; the repo modules need the same expressions over data
4
+ * they are given. Every expression here is copied from reader.js / facts.js (the section number is named at each one) and
5
+ * only parameterised. derived.test.js runs both on the generated sample portfolio and asserts they agree, so the two
6
+ * copies cannot drift. When the sample changes, change reader.js/facts.js first, then this file.
7
+ */
8
+ const P = require('./plan');
9
+ const { fold, plan, day, fmt, D } = P;
10
+ const days = (a, b) => Math.round((a - b) / D);
11
+
12
+ /* reader.js section 1: one row per check (snap): forecast date, slip against the promise, scope points. */
13
+ const pts = (s) => Object.values(s).reduce((a, t) => a + t.pts, 0);
14
+ function timeline(events, start, promised) {
15
+ const snaps = [...new Set(events.map((e) => e.snap))].sort();
16
+ return snaps.map((c) => { const s = fold(events, c), p = plan(s, start); return { check: c, forecast: fmt(p.forecast), slip: days(p.forecast, day(promised)), points: pts(s) }; });
17
+ }
18
+
19
+ /* facts.js: the slip tolerance is the initiative's own field, else 10 percent of the planned duration (at least 1 day). */
20
+ const tolerance = (init, start, promised) => +init.slip_tolerance_days || Math.max(1, Math.round(0.1 * Math.round((day(promised) - day(start)) / D)));
21
+
22
+ /* facts.js: critical path = the tickets with slack 0 against the forecast. */
23
+ function critical(state, fc) {
24
+ const succ = {};
25
+ Object.entries(state).forEach(([k, t]) => t.deps.forEach((d) => (succ[d] = succ[d] || []).push(k)));
26
+ const tail = (k) => Math.max(0, ...(succ[k] || []).map((s) => state[s].dur + tail(s)));
27
+ return new Set(Object.keys(state).filter((k) => fc.end[k] + tail(k) * D === fc.forecast));
28
+ }
29
+
30
+ /* facts.js ablate(): remove one event, recompute; the difference is what that event cost. */
31
+ function attribution(events, start, firstSnap) {
32
+ const fc = plan(fold(events), start);
33
+ const out = [];
34
+ for (const e of events) {
35
+ if (e.snap === firstSnap) continue;
36
+ try { out.push({ ev: e, days: Math.round((fc.forecast - plan(fold(events.filter((x) => x !== e)), start).forecast) / D) }); } catch { out.push({ ev: e, days: null }); }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ /* reader.js section 3: document graph edges. docs: { id: { type, rev, sources: [{id, rev}], ... } }; findings: open findings. */
42
+ const edgeState = (docs, src, seen) => { const u = docs[src]; if (!u) return 'missing'; if (u.rev <= seen) return 'in sync'; return ['Decision', 'Benchmark'].includes(u.type) ? 'conflict' : 'out of sync'; };
43
+ function docGraph(docs, findings) {
44
+ const edges = [];
45
+ for (const [id, d] of Object.entries(docs)) for (const s of d.sources || []) edges.push({ from: s.id, to: id, state: edgeState(docs, s.id, s.rev) });
46
+ for (const f of (findings || []).filter((x) => x.status === 'open')) {
47
+ const e = edges.find((x) => x.from === f.vs && x.to === f.doc);
48
+ if (e) { e.state = 'conflict'; e.reason = 'finding'; } else edges.push({ from: f.vs, to: f.doc, state: 'conflict', reason: 'finding: ' + f.quote });
49
+ }
50
+ return edges;
51
+ }
52
+
53
+ /* reader.js section 4: the six health checks for one document. need: { <type>: [required headings] } (doc-types.json
54
+ * `required`); readers: doc-readers records { doc, who, rev }; now: 'YYYY-MM-DD'. Doc bodies are in docs[id]._body. */
55
+ function docHealth(id, docs, edges, readers, need, now) {
56
+ const d = docs[id], my = edges.filter((e) => e.to === id), kind = (i) => (docs[i] || {}).type, chk = {};
57
+ chk.fresh = !d.stale_after || d.stale_after >= now;
58
+ chk.decisions = !my.some((e) => kind(e.from) === 'Decision' && e.state !== 'in sync');
59
+ chk.data = !my.some((e) => kind(e.from) === 'Benchmark' && e.state !== 'in sync') && chk.fresh;
60
+ chk.complete = (need[d.type] || []).every((h) => (d._body || '').includes('## ' + h));
61
+ const reachKR = (i, seen = new Set()) => { if (seen.has(i) || !docs[i]) return false; seen.add(i); if (docs[i].type === 'KeyResult') return true; return (docs[i].sources || []).some((x) => reachKR(x.id, seen)); };
62
+ const citedBy = Object.entries(docs).filter(([, x]) => (x.sources || []).some((y) => y.id === id)).map(([k]) => k);
63
+ chk.traced = (['Research', 'Benchmark'].includes(d.type) ? citedBy.some((k) => reachKR(k)) : reachKR(id)) && (d.type !== 'PRD' || (d.tickets || []).length > 0);
64
+ const lastR = {}; (readers || []).filter((r) => r.doc === id).forEach((r) => (lastR[r.who] = r.rev));
65
+ chk.readers = Object.values(lastR).every((r) => r >= d.rev);
66
+ return chk;
67
+ }
68
+
69
+ /* reader.js section 7: what each person was told against what is true now. messages: sent.jsonl records;
70
+ * pl: plan(state,start) of the current state. Subjects: 'ship-date' and 'ticket:<id>'. */
71
+ const truthOf = (subj, pl) => (subj === 'ship-date' ? fmt(pl.forecast) : subj.startsWith('ticket:') && pl.end[subj.slice(7)] !== undefined ? fmt(pl.end[subj.slice(7)]) : null);
72
+ function heldVsTrue(messages, pl) {
73
+ const rows = [];
74
+ for (const m of messages) for (const c of m.claims) {
75
+ const t = truthOf(c.subject, pl), gap = t == null ? null : days(day(t), day(c.value));
76
+ rows.push({ to: m.to, subject: c.subject, told: c.value, true: t, gap, state: gap === null ? 'unknown' : gap ? gap + 'd apart' : 'in sync' });
77
+ }
78
+ return rows;
79
+ }
80
+
81
+ /* gen.js shared/trust/claim-rules.json: the evidence labels and the per-destination strictness trust.checkReport reads. */
82
+ const CLAIM_RULES = {
83
+ basis: {
84
+ fact: { needs: ['source'] }, cited: { needs: ['source'] }, reported: { needs: ['by', 'at'] }, estimate: { needs: ['method', 'range'] },
85
+ assumption: { needs: ['owner', 'falsifier', 'review_by'] }, judgement: { needs: ['by'] } },
86
+ destinations: {
87
+ leadership: { allow: ['fact', 'cited', 'reported', 'estimate', 'assumption', 'judgement'], unlabelled: 'block', incomplete: 'block', stale: 'block', overdue_assumption: 'block', label_visible: true },
88
+ customer: { allow: ['fact', 'cited'], unlabelled: 'block', incomplete: 'block', stale: 'block', overdue_assumption: 'block', label_visible: false },
89
+ 'internal-draft': { allow: ['fact', 'cited', 'reported', 'estimate', 'assumption', 'judgement'], unlabelled: 'warn', incomplete: 'warn', stale: 'warn', overdue_assumption: 'warn', label_visible: true } },
90
+ };
91
+
92
+ module.exports = { timeline, tolerance, critical, attribution, docGraph, docHealth, truthOf, heldVsTrue, CLAIM_RULES, pts };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+ // Re-export: the canonical copy is scripts/trust/plan.js (the PM's sample, ported once).
3
+ module.exports = require('../trust/plan.js');
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+ // Re-export: the canonical copy is scripts/trust/trust.js (the PM's sample, ported once).
3
+ module.exports = require('../trust/trust.js');