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,175 @@
1
+ 'use strict';
2
+ // console3 wave B4: the four report families, their section builders and the claims check. Pure: no I/O, no send.
3
+ // Evidence labels (HANDOFF section 11): fact | cited | reported | estimate | assumption | judgement.
4
+ const { whoHoldsWhat } = require('./truth-table');
5
+ const TR = require('./derived/trust.js');
6
+ const { CLAIM_RULES } = require('./derived/derive.js');
7
+
8
+ // BACKED BY THE PM'S SAMPLE: the claims check is derived/trust.js checkReport() under the sample's rules (derive.CLAIM_RULES,
9
+ // gen.js claim-rules.json): label, required fields, destination strictness, untrusted import, source exists, source current,
10
+ // overdue assumption, and the omitted-tripped-signal rule. This file only maps a claim to the sample's shape.
11
+ // REPO-WRITTEN, no sample equivalent: the four families and their variants, the section builders, and the send summary wording.
12
+
13
+ const LABELS = ['fact', 'cited', 'reported', 'estimate', 'assumption', 'judgement'];
14
+ const REQUIRED = Object.fromEntries(Object.entries(CLAIM_RULES.basis).map(([k, v]) => [k, v.needs.slice()])); // CHANGED to the sample: assumption needs review_by, judgement needs by
15
+ const present = (v) => v !== undefined && v !== null && String(v).trim() !== '';
16
+
17
+ // gate: 'block' = an unlabelled or incomplete claim blocks sending; 'strict' = block, and only fact and cited are allowed;
18
+ // 'warn' = internal draft, the check warns and never blocks.
19
+ const FAMILIES = [
20
+ { id: 'project', name: 'Project', intent: 'Decide and unblock', purpose: 'Let the audience decide something or remove a blocker.',
21
+ cadence: 'on demand, at each decision point',
22
+ variants: [
23
+ { id: 'leadership', name: 'Leadership', audience: 'Leadership', gate: 'block' },
24
+ { id: 'engineering', name: 'Engineering', audience: 'Engineering', gate: 'warn' },
25
+ { id: 'customer-success', name: 'Customer success', audience: 'Customer success', gate: 'strict', external: true },
26
+ ],
27
+ sections: ['headline', 'progress', 'risks', 'decisions_needed', 'blockers'] },
28
+ { id: 'product', name: 'Product', intent: 'Align and invest', purpose: 'Align the audience on outcomes and where to invest.',
29
+ cadence: 'per planning cycle',
30
+ variants: [
31
+ { id: 'executives', name: 'Executives', audience: 'Executives', gate: 'block' },
32
+ { id: 'product-council', name: 'Product council', audience: 'Product council', gate: 'block' },
33
+ { id: 'team', name: 'Team', audience: 'Team', gate: 'warn' },
34
+ ],
35
+ sections: ['outcomes', 'bets', 'tradeoffs', 'investment_asks'] },
36
+ { id: 'communications', name: 'Communications', intent: 'Inform, enable, ask', purpose: 'Tell people what they need, give them what they need to act, and ask.',
37
+ cadence: 'on demand',
38
+ variants: [
39
+ { id: 'message-queue', name: 'Message queue', audience: 'Per message', gate: 'warn' },
40
+ { id: 'who-holds-what', name: 'Who holds what', audience: 'PM', gate: 'warn' },
41
+ { id: 'external', name: 'External message', audience: 'External', gate: 'strict', external: true, needs_approval: true },
42
+ ],
43
+ sections: ['message_queue', 'who_holds_what', 'asks'], needs_approval_external: true },
44
+ { id: 'periodic', name: 'Periodic', intent: 'Rhythm', purpose: 'Keep a steady rhythm; one template, with the change since last time.',
45
+ cadence: 'weekly, monthly, quarterly, per retro',
46
+ variants: [
47
+ { id: 'weekly-digest', name: 'Weekly digest', audience: 'Team', gate: 'warn', cadence: 'weekly' },
48
+ { id: 'monthly-review', name: 'Monthly review', audience: 'Leadership', gate: 'block', cadence: 'monthly' },
49
+ { id: 'quarterly-board-update', name: 'Quarterly board update', audience: 'Board', gate: 'block', cadence: 'quarterly' },
50
+ { id: 'retro', name: 'Retro', audience: 'Team', gate: 'warn', cadence: 'per retro' },
51
+ ],
52
+ sections: ['since_last_time', 'highlights', 'metrics', 'next'], template: 'same template; change since last time' },
53
+ ];
54
+
55
+ const familyOf = (id) => FAMILIES.find((f) => f.id === id) || null;
56
+ const variantOf = (f, id) => (f && f.variants.find((v) => v.id === id)) || null;
57
+
58
+ /* ── section builders: pure, over a facts object the caller supplies ─────────────────────────────────────────────── */
59
+ // facts[<section>] is a list of items { id?, text, label, source, ...label fields, from_import?, stale? }.
60
+ // A builder copies what is given. It never adds a claim and never fills in a label.
61
+ const CLAIM_KEYS = ['id', 'text', 'label', 'source', 'by', 'at', 'method', 'range', 'owner', 'falsifier', 'review_date', 'author', 'from_import', 'stale'];
62
+ const pick = (it) => { const c = {}; for (const k of CLAIM_KEYS) if (it[k] !== undefined) c[k] = it[k]; if (c.label === undefined) c.label = null; if (c.source === undefined) c.source = null; return c; };
63
+ const fromList = (key, title) => (facts = {}) => ({ title, claims: (Array.isArray(facts[key]) ? facts[key] : []).filter((x) => x && typeof x === 'object' && x.text).map(pick) });
64
+
65
+ // "Since last time": a claim for each id whose value differs between facts.previous and facts.current (both { id: { text, value, source } }).
66
+ function sinceLastTime(facts = {}) {
67
+ const prev = facts.previous || {}, cur = facts.current || {};
68
+ const claims = [];
69
+ for (const id of Object.keys(cur)) {
70
+ const c = cur[id] || {};
71
+ if (JSON.stringify(prev[id] && prev[id].value) === JSON.stringify(c.value)) continue;
72
+ const was = prev[id] ? ` (was ${JSON.stringify(prev[id].value)})` : ' (new)';
73
+ claims.push({ ...pick({ ...c, id, text: `${c.text || id}: ${JSON.stringify(c.value)}${was}` }), label: c.label === undefined ? null : c.label });
74
+ }
75
+ return { title: 'Since last time', claims };
76
+ }
77
+
78
+ function whoHoldsWhatSection(facts = {}) {
79
+ const rows = whoHoldsWhat(facts.messages || [], facts.truth || {});
80
+ const claims = [];
81
+ for (const p of rows) for (const c of p.claims) {
82
+ claims.push({ id: `${p.person}:${c.id}`, text: `${p.person} holds "${c.text || c.id}": ${c.state}`, label: 'fact', source: 'truth-table:' + c.id, stale: c.state === 'stale', contradicted: c.state === 'contradicted' });
83
+ }
84
+ return { title: 'Who holds what', claims };
85
+ }
86
+
87
+ const BUILDERS = {
88
+ headline: fromList('headline', 'Headline'), progress: fromList('progress', 'Progress'), risks: fromList('risks', 'Risks'),
89
+ decisions_needed: fromList('decisions_needed', 'Decisions needed'), blockers: fromList('blockers', 'Blockers'),
90
+ outcomes: fromList('outcomes', 'Outcomes'), bets: fromList('bets', 'Bets'), tradeoffs: fromList('tradeoffs', 'Trade-offs'),
91
+ investment_asks: fromList('investment_asks', 'Investment asks'),
92
+ message_queue: fromList('message_queue', 'Message queue'), who_holds_what: whoHoldsWhatSection, asks: fromList('asks', 'Asks'),
93
+ since_last_time: sinceLastTime, highlights: fromList('highlights', 'Highlights'), metrics: fromList('metrics', 'Metrics'), next: fromList('next', 'Next'),
94
+ };
95
+
96
+ function buildFamily(familyId, facts = {}) {
97
+ const f = familyOf(familyId);
98
+ if (!f) throw new Error('unknown family: ' + familyId);
99
+ return f.sections.map((name) => ({ name, ...BUILDERS[name](facts) }));
100
+ }
101
+
102
+ /* ── the claims check ────────────────────────────────────────────────────────────────────────────────────────────── */
103
+ // gate 'block' = leadership rules, 'strict' = customer rules, 'warn' = internal-draft rules.
104
+ const DEST = { block: 'leadership', strict: 'customer', warn: 'internal-draft' };
105
+
106
+ // A repo claim in the sample's shape. The older field names are still read: review_date for review_by, author for by.
107
+ // A stale claim cites its source at revision 1 while the source is at revision 2, which is how the sample marks it stale.
108
+ function toSample(c, opts) {
109
+ const src = c && present(c.source) ? String(c.source) : '';
110
+ const stale = !!(c && (c.stale || (opts.staleKeys || []).includes(src)));
111
+ const absent = (opts.absentKeys || []).includes(src) || (Array.isArray(opts.knownKeys) && !opts.knownKeys.includes(src));
112
+ const sc = c ? { id: c.id, basis: c.label || undefined, claim: c.text, by: c.by || c.author, at: c.at, method: c.method, range: c.range,
113
+ owner: c.owner, falsifier: c.falsifier, review_by: c.review_by || c.review_date, covers: c.covers } : {};
114
+ sc.source = c && c.from_import ? 'import:' + src : (stale && src && !absent ? src + '@1' : src);
115
+ const docs = src && !absent ? { [src]: { rev: stale ? 2 : 1 } } : (src ? {} : { '': { rev: 0 } });
116
+ return { sc, docs, stale, src, absent };
117
+ }
118
+ const ctxOf = (docs, opts) => ({ snaps: [], last: '', docs, now: opts.now || '0000-00-00', tripped: opts.tripped || [] });
119
+
120
+ // opts: { gate, knownKeys?, absentKeys?, staleKeys?, now? }
121
+ function checkClaim(c, opts = {}) {
122
+ const gate = opts.gate || 'warn';
123
+ const m = toSample(c, opts);
124
+ const row = TR.checkReport({ sections: [m.sc] }, CLAIM_RULES, ctxOf(m.docs, opts), DEST[gate] || DEST.warn).rows[0];
125
+ const why = row.verdict === 'ok' ? '' : row.why;
126
+ const problems = !why ? [] : /^missing /.test(why) ? why.slice(8).split(', ').map((k) => 'missing ' + k) : [why];
127
+ const fabricated = row.verdict === 'block' && /does not exist|no such check/.test(why);
128
+ if (fabricated && m.src && !/not found/.test(why)) problems.push('source not found: ' + m.src);
129
+ return { ok: row.verdict === 'ok', verdict: row.verdict, problems, fabricated, stale: m.stale && /is now rev/.test(why) };
130
+ }
131
+
132
+ function claimsCheck(sections = [], opts = {}) {
133
+ return (sections || []).map((s) => {
134
+ const out = { name: s.name || null, title: s.title || null, traced: 0, stale_source: 0, needs_input: 0, fabricated: 0, problems: [] };
135
+ (s.claims || []).forEach((c, i) => {
136
+ const r = checkClaim(c, opts);
137
+ if (r.fabricated) out.fabricated++;
138
+ if (r.ok) out.traced++;
139
+ else if (r.stale && r.verdict === 'warn') out.stale_source++; // an internal draft only warns about a stale source
140
+ else { out.needs_input++; out.problems.push({ index: i, text: (c && c.text) || null, problems: r.problems }); }
141
+ });
142
+ return out;
143
+ });
144
+ }
145
+
146
+ // Whether sending is blocked, and why. A fabricated source always blocks. Otherwise the variant's gate decides.
147
+ function sendCheck({ family, variant, sections, knownKeys, absentKeys, staleKeys, tripped, now } = {}) {
148
+ const f = familyOf(family);
149
+ if (!f) return { ok: false, error: 'unknown family' };
150
+ const v = variantOf(f, variant) || f.variants[0];
151
+ const opts = { gate: v.gate, knownKeys, absentKeys, staleKeys, now };
152
+ const per = claimsCheck(sections, opts);
153
+ // the sample's bad-news rule: a leadership or customer report must cover every tripped signal (a claim's `covers` list)
154
+ let omitted = [];
155
+ if (Array.isArray(tripped) && tripped.length) {
156
+ const all = (sections || []).flatMap((s) => (s.claims || []).map((c) => toSample(c, opts)));
157
+ const rows = TR.checkReport({ sections: all.map((m) => m.sc) }, CLAIM_RULES, ctxOf(Object.assign({}, ...all.map((m) => m.docs)), { ...opts, tripped }), DEST[v.gate]).rows;
158
+ omitted = rows.filter((r) => r.id === 'omitted').map((r) => r.claim);
159
+ }
160
+ const fabricated = per.reduce((n, s) => n + s.fabricated, 0);
161
+ const needs = per.reduce((n, s) => n + s.needs_input, 0);
162
+ const reasons = [];
163
+ if (fabricated) reasons.push(`${fabricated} claim${fabricated > 1 ? 's' : ''} cite a source that does not exist`);
164
+ if (needs && v.gate !== 'warn') reasons.push(`${needs} claim${needs > 1 ? 's' : ''} need input before this ${v.name} report can be sent`);
165
+ if (omitted.length) reasons.push(omitted[0]);
166
+ const warnings = [];
167
+ if (needs && v.gate === 'warn') warnings.push(`${needs} claim${needs > 1 ? 's' : ''} need input (internal draft: warning only)`);
168
+ const stale = per.reduce((n, s) => n + s.stale_source, 0);
169
+ if (stale) warnings.push(`${stale} claim${stale > 1 ? 's' : ''} rest on a stale source`);
170
+ const approval = !!(v.needs_approval || v.external);
171
+ if (approval) warnings.push('external message: needs the PM approval before it is sent');
172
+ return { ok: true, family: f.id, variant: v.id, gate: v.gate, sections: per, blocked: reasons.length > 0, reasons, warnings, needs_approval: approval };
173
+ }
174
+
175
+ module.exports = { LABELS, REQUIRED, FAMILIES, BUILDERS, familyOf, variantOf, buildFamily, checkClaim, claimsCheck, sendCheck };
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+ /*
3
+ * roadmap-ops.js — the Roadmap's writes (console3 wave E1b; HANDOFF 14e/14f). Pure functions over the folded bets
4
+ * (scripts/records/index.js, kind 'bets'); nothing here touches a file except apply(), which goes through
5
+ * scripts/records, which goes through scripts/trust/ledger.js (the PM's writer).
6
+ *
7
+ * A bet: { rid, title, horizon: now|next|later, rank, parent_kr, confidence (0..1), off?, off_reason? }.
8
+ * Every function returns { ok:false, error } or { ok:true, ... }. A result that writes carries `events`:
9
+ * { op:'add', fields, why } | { op:'edit', rid, fields, why }, applied by apply(root, events, actor).
10
+ * A swap is only a PROPOSAL ({ ok:true, proposal:{...} }, no events). The PM confirms it by calling moveBet again with
11
+ * { confirmSwap: true }; nothing is moved silently.
12
+ */
13
+ const path = require('path');
14
+ const records = require('./records/index.js');
15
+
16
+ const LIMIT = 4; // bet limit for Now and Next (HANDOFF 14f)
17
+ const COLUMNS = ['now', 'next', 'later'];
18
+ const HEALTH = /^health:[a-z0-9][a-z0-9-]*$/;
19
+ const KR = /^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$/; // q3-2026:o1:kr1
20
+
21
+ const refuse = (error, extra) => ({ ok: false, error, ...(extra || {}) });
22
+ const live = (bets) => (bets || []).filter((b) => b && !b.off);
23
+ const inColumn = (bets, h) => live(bets).filter((b) => b.horizon === h);
24
+ const nextRank = (bets, h) => inColumn(bets, h).reduce((m, b) => Math.max(m, b.rank || 0), 0) + 1;
25
+ const sure = (b) => (typeof b.confidence === 'number' ? b.confidence : 0.5); // an unrated bet counts as 50 percent
26
+
27
+ function anchorOk(anchor, krs) {
28
+ const a = String(anchor || '').trim();
29
+ if (!a) return false;
30
+ if (HEALTH.test(a) || KR.test(a)) return true;
31
+ return (krs || []).some((k) => (k && (k.id || k.rid || k)) === a);
32
+ }
33
+
34
+ /** addBet(bets, { title, anchor, horizon, confidence?, initiative? }, { krs?, limit? }) */
35
+ function addBet(bets, input, opts = {}) {
36
+ const i = input || {};
37
+ const limit = opts.limit || LIMIT;
38
+ const title = String(i.title || '').trim();
39
+ if (!title) return refuse('a bet needs a title');
40
+ const horizon = i.horizon || 'later';
41
+ if (!COLUMNS.includes(horizon)) return refuse('horizon must be now, next or later');
42
+ if (!anchorOk(i.anchor, opts.krs)) return refuse('an unanchored bet is refused: anchor it to a KR or to health:<id>', { field: 'anchor' });
43
+ if ((horizon === 'now' || horizon === 'next') && inColumn(bets, horizon).length >= limit) {
44
+ return refuse(`${horizon} already holds ${limit} bets; add it to a later column or move one out first`, { full: horizon });
45
+ }
46
+ const fields = { title, horizon, rank: nextRank(bets, horizon), parent_kr: String(i.anchor).trim() };
47
+ if (typeof i.confidence === 'number') fields.confidence = i.confidence;
48
+ if (i.initiative) fields.initiative = i.initiative;
49
+ return { ok: true, events: [{ op: 'add', fields, why: 'bet added to ' + horizon }] };
50
+ }
51
+
52
+ const back = (h) => (h === 'now' ? 'next' : h === 'next' ? 'later' : null);
53
+
54
+ /**
55
+ * moveBet(bets, id, toHorizon, capacity = {now:4,next:4}, { confirmSwap, krs })
56
+ * Any column to any column. When the target is full: without confirmSwap, a proposal naming the LEAST SURE bet of the
57
+ * target (lowest confidence; ties go to the lowest-ranked) and where it goes (one column back, cascading when that column is
58
+ * full too); with confirmSwap, the events for the whole chain.
59
+ */
60
+ function moveBet(bets, id, toHorizon, capacity, opts = {}) {
61
+ const cap = { now: LIMIT, next: LIMIT, ...(capacity || {}) };
62
+ const bet = live(bets).find((b) => b.rid === id);
63
+ if (!bet) return refuse('no such bet on the roadmap: ' + id);
64
+ if (!COLUMNS.includes(toHorizon)) return refuse('horizon must be now, next or later');
65
+ if (bet.horizon === toHorizon) return refuse('the bet is already in ' + toHorizon);
66
+ const promoting = toHorizon !== 'later';
67
+ if (promoting && !anchorOk(bet.parent_kr, opts.krs)) return refuse('anchor the bet before it goes to ' + toHorizon, { field: 'anchor' });
68
+
69
+ // Build the chain of bets that must step back to make room, working on a copy of the columns.
70
+ const col = {}; for (const h of COLUMNS) col[h] = inColumn(bets, h).filter((b) => b.rid !== id);
71
+ const chain = [];
72
+ let h = toHorizon;
73
+ while (h !== 'later' && col[h].length >= (cap[h] === undefined ? LIMIT : cap[h])) {
74
+ const victim = [...col[h]].sort((a, b) => sure(a) - sure(b) || (b.rank || 0) - (a.rank || 0))[0];
75
+ const to = back(h);
76
+ chain.push({ id: victim.rid, title: victim.title, confidence: victim.confidence == null ? null : victim.confidence, from: h, to });
77
+ col[h] = col[h].filter((b) => b.rid !== victim.rid);
78
+ col[to].push(victim);
79
+ h = to;
80
+ }
81
+ if (chain.length && !opts.confirmSwap) {
82
+ return { ok: true, proposal: { kind: 'swap', move: { id, title: bet.title, from: bet.horizon, to: toHorizon }, send_back: chain, confirm: 'send { confirmSwap: true } to apply; nothing moved yet' } };
83
+ }
84
+ // Apply back to front, so each target column has room when its bet arrives.
85
+ const events = [];
86
+ const rank = {}; for (const c of COLUMNS) rank[c] = nextRank(bets, c);
87
+ const place = (rid, to, why) => events.push({ op: 'edit', rid, fields: { horizon: to, rank: rank[to]++ }, why });
88
+ for (const c of [...chain].reverse()) place(c.id, c.to, `sent back to ${c.to} to make room for ${bet.title}`);
89
+ place(id, toHorizon, `moved from ${bet.horizon} to ${toHorizon}`);
90
+ return { ok: true, events, ...(chain.length ? { swapped: chain } : {}) };
91
+ }
92
+
93
+ const STOP = new Set(['the', 'a', 'an', 'of', 'to', 'for', 'and', 'in', 'on', 'with', 'by', 'is', 'at', 'or', 'new', 'q1', 'q2', 'q3', 'q4']);
94
+ const words = (s) => new Set(String(s || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP.has(w)));
95
+
96
+ /**
97
+ * anchorSuggestions(bet, krs, healthBudgets) -> [{ anchor, title, kind, overlap:[words], score }], best first.
98
+ * krs: [{ id, title }], healthBudgets: [{ id, title }] (id 'tech' or 'health:tech'). Word overlap only; empty when none.
99
+ */
100
+ function anchorSuggestions(bet, krs, healthBudgets) {
101
+ const mine = words(bet && bet.title);
102
+ const out = [];
103
+ const consider = (anchor, title, kind) => {
104
+ const overlap = [...words(title)].filter((w) => mine.has(w));
105
+ if (overlap.length) out.push({ anchor, title, kind, overlap, score: overlap.length });
106
+ };
107
+ for (const k of krs || []) consider(k.id || k.rid, k.title || k.metric, 'kr');
108
+ for (const h of healthBudgets || []) consider(String(h.id).startsWith('health:') ? h.id : 'health:' + h.id, h.title || h.id, 'health');
109
+ return out.filter((s) => s.anchor).sort((a, b) => b.score - a.score || String(a.anchor).localeCompare(String(b.anchor)));
110
+ }
111
+
112
+ /** takeOff(bets, id, reason): off the roadmap, with a required reason. The record stays (the fold keeps `off`). */
113
+ function takeOff(bets, id, reason) {
114
+ const bet = live(bets).find((b) => b.rid === id);
115
+ if (!bet) return refuse('no such bet on the roadmap: ' + id);
116
+ const why = String(reason || '').trim();
117
+ if (!why) return refuse('a reason is required to take a bet off the roadmap', { field: 'reason' });
118
+ return { ok: true, events: [{ op: 'edit', rid: id, fields: { off: true, off_reason: why, off_from: bet.horizon }, why }] };
119
+ }
120
+
121
+ /**
122
+ * capacityLine(bets, people, { limit }) -> bet capacity derived from people capacity (never typed).
123
+ * people: [{ id, points_per_week }]. Each live Now or Next bet is a share of the team's weekly points.
124
+ */
125
+ function capacityLine(bets, people, opts = {}) {
126
+ const limit = opts.limit || LIMIT;
127
+ const total = (people || []).reduce((s, p) => s + (Number(p.points_per_week) || 0), 0);
128
+ const now = inColumn(bets, 'now').length, next = inColumn(bets, 'next').length;
129
+ const active = now + next;
130
+ return {
131
+ points_per_week: total, people: (people || []).length,
132
+ now: { used: now, limit, free: Math.max(0, limit - now) },
133
+ next: { used: next, limit, free: Math.max(0, limit - next) },
134
+ per_bet: active ? Math.round((total / active) * 10) / 10 : total,
135
+ over: now > limit || next > limit,
136
+ };
137
+ }
138
+
139
+ /** apply(root, events, actor) -> { ok, written } | { ok:false, reasons }. The only writer here; it is scripts/records. */
140
+ function apply(root, events, actor) {
141
+ const written = [];
142
+ for (const e of events) {
143
+ const r = e.op === 'add' ? records.add(root, 'bets', e.fields, actor, e.why) : records.edit(root, 'bets', e.rid, e.fields, actor, e.why);
144
+ if (!r.ok) return { ok: false, reasons: r.reasons, written };
145
+ written.push(r.rid);
146
+ }
147
+ return { ok: true, written };
148
+ }
149
+
150
+ module.exports = { LIMIT, COLUMNS, addBet, moveBet, anchorSuggestions, takeOff, capacityLine, apply, anchorOk, live };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+ // Run 9: the sessions list. Pure. Session shape: { id, origin, cardId, startedAt, endedAt }; times are ms or ISO.
3
+ // Groups: onCard (running, tied to a card), startedByMe (running, PM-started), past (ended in the last 7 days, newest first),
4
+ // automatic (every automatic origin, running or ended). The UI hides `automatic` unless AUTOMATIC_HIDDEN_BY_DEFAULT is switched off.
5
+ const PAST_DAYS = 7;
6
+ const AUTOMATIC_HIDDEN_BY_DEFAULT = true;
7
+ const t = (v) => (v == null || v === '' ? NaN : new Date(v).getTime());
8
+ const isAutomaticOrigin = (o) => !!o && o !== 'manual' && o !== 'pm';
9
+
10
+ function groupSessions(sessions, now = Date.now()) {
11
+ const n = t(now);
12
+ const out = { onCard: [], startedByMe: [], past: [], automatic: [], automaticHidden: AUTOMATIC_HIDDEN_BY_DEFAULT };
13
+ for (const s of Array.isArray(sessions) ? sessions : []) {
14
+ if (!s) continue;
15
+ if (isAutomaticOrigin(s.origin)) { out.automatic.push(s); continue; }
16
+ const ended = !Number.isNaN(t(s.endedAt));
17
+ if (ended) {
18
+ if (n - t(s.endedAt) <= PAST_DAYS * 864e5) out.past.push(s);
19
+ } else if (s.cardId) out.onCard.push(s);
20
+ else out.startedByMe.push(s);
21
+ }
22
+ const when = (s) => t(s.endedAt) || t(s.startedAt) || 0;
23
+ out.past.sort((a, b) => when(b) - when(a));
24
+ return out;
25
+ }
26
+
27
+ module.exports = { PAST_DAYS, AUTOMATIC_HIDDEN_BY_DEFAULT, isAutomaticOrigin, groupSessions };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+ /* signal-rules.js — the Signals page records (console3 wave E2a), from STRUCTURE.md: rules, metric readings, trips.
3
+ *
4
+ * Append-only lines in one ledger file (scripts/trust/ledger.js):
5
+ * { kind:'signal_rule', rule:{id,metric,op,threshold,severity} } latest line per id wins
6
+ * { kind:'signal_reading', metric, value, at }
7
+ * { kind:'signal_trip', rule_id, severity, reading:{metric,value,at}, at } a rule tripping on a reading
8
+ *
9
+ * evaluateRules(rules, readings) → trips pure; each rule is tested against the LATEST reading of its metric
10
+ * foldSignals(lines) → { rules, readings (latest per metric), trips, open } open = tripping on the latest reading
11
+ * tripsSince(trips, date) → trips at or after date ('YYYY-MM-DD' or ISO)
12
+ * trippedSince(trips, date) → rule ids, de-duplicated: the `tripped` list scripts/report-families.js sendCheck takes,
13
+ * so a report that omits one is blocked ('Report omits tripped signal')
14
+ * appendRule / appendReading ledger writers; a reading also appends a trip when it starts tripping a rule */
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const L = require('./trust/ledger.js');
18
+
19
+ const OPS = Object.freeze(['<', '<=', '>', '>=', '==']);
20
+ const SEVERITIES = Object.freeze(['watch', 'act', 'critical']);
21
+ const CMP = { '<': (a, b) => a < b, '<=': (a, b) => a <= b, '>': (a, b) => a > b, '>=': (a, b) => a >= b, '==': (a, b) => a === b };
22
+
23
+ function validateRule(r) {
24
+ const e = [];
25
+ if (!r || typeof r !== 'object') return ['a rule is an object'];
26
+ if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(String(r.id || ''))) e.push('id must be letters, digits, . _ - (max 64)');
27
+ if (!String(r.metric || '').trim()) e.push('metric is required');
28
+ if (!OPS.includes(r.op)) e.push('op must be one of ' + OPS.join(' '));
29
+ if (typeof r.threshold !== 'number' || !Number.isFinite(r.threshold)) e.push('threshold must be a number');
30
+ if (!SEVERITIES.includes(r.severity)) e.push('severity must be one of ' + SEVERITIES.join(', '));
31
+ return e;
32
+ }
33
+ const tripsOn = (rule, value) => typeof value === 'number' && Number.isFinite(value) && CMP[rule.op](value, rule.threshold);
34
+
35
+ function latestByMetric(readings) {
36
+ const m = new Map();
37
+ for (const r of readings || []) {
38
+ if (!r || r.metric == null) continue;
39
+ const cur = m.get(r.metric);
40
+ if (!cur || String(r.at) >= String(cur.at)) m.set(r.metric, r);
41
+ }
42
+ return m;
43
+ }
44
+
45
+ function evaluateRules(rules, readings) {
46
+ const latest = latestByMetric(readings);
47
+ const out = [];
48
+ for (const rule of rules || []) {
49
+ const rd = latest.get(rule.metric);
50
+ if (rd && tripsOn(rule, rd.value)) out.push({ rule_id: rule.id, severity: rule.severity, op: rule.op, threshold: rule.threshold, reading: { metric: rd.metric, value: rd.value, at: rd.at }, at: rd.at });
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function foldSignals(lines) {
56
+ const rules = new Map(); const readings = []; const trips = [];
57
+ for (const l of lines || []) {
58
+ if (!l) continue;
59
+ if (l.kind === 'signal_rule' && l.rule) rules.set(l.rule.id, l.rule);
60
+ else if (l.kind === 'signal_reading') readings.push({ metric: l.metric, value: l.value, at: l.at });
61
+ else if (l.kind === 'signal_trip') trips.push({ rule_id: l.rule_id, severity: l.severity, reading: l.reading, at: l.at });
62
+ }
63
+ const ruleList = [...rules.values()];
64
+ return { rules: ruleList, readings: [...latestByMetric(readings).values()], trips, open: evaluateRules(ruleList, readings) };
65
+ }
66
+
67
+ const tripsSince = (trips, date) => (trips || []).filter((t) => t && String(t.at) >= String(date));
68
+ const trippedSince = (trips, date) => [...new Set(tripsSince(trips, date).map((t) => t.rule_id))];
69
+
70
+ function appendRule(file, rule, actor, now) {
71
+ const errs = validateRule(rule);
72
+ if (errs.length) throw new Error(errs.join('; '));
73
+ fs.mkdirSync(path.dirname(file), { recursive: true });
74
+ const r = { id: rule.id, metric: String(rule.metric).trim(), op: rule.op, threshold: rule.threshold, severity: rule.severity };
75
+ return L.append(file, { id: L.newId(), kind: 'signal_rule', why: 'signal rule set by the PM', at: now, rule: r }, actor);
76
+ }
77
+
78
+ // Appends the reading, then a trip line for each rule that this reading newly trips (it did not trip on the reading before).
79
+ function appendReading(file, reading, actor, now) {
80
+ if (!String(reading.metric || '').trim()) throw new Error('metric is required');
81
+ if (typeof reading.value !== 'number' || !Number.isFinite(reading.value)) throw new Error('value must be a number');
82
+ const at = reading.at || now;
83
+ fs.mkdirSync(path.dirname(file), { recursive: true });
84
+ const before = foldSignals(L.read(file));
85
+ const wasOpen = new Set(before.open.map((t) => t.rule_id));
86
+ const line = L.append(file, { id: L.newId(), kind: 'signal_reading', why: 'metric reading', metric: reading.metric, value: reading.value, at }, actor);
87
+ const trips = [];
88
+ for (const t of evaluateRules(before.rules, [...L.read(file).filter((x) => x.kind === 'signal_reading')])) {
89
+ if (t.reading.at !== at || t.reading.metric !== reading.metric || wasOpen.has(t.rule_id)) continue;
90
+ trips.push(L.append(file, { id: L.newId(), kind: 'signal_trip', why: 'rule tripped on a reading', rule_id: t.rule_id, severity: t.severity, reading: t.reading, at }, actor));
91
+ }
92
+ return { line, trips };
93
+ }
94
+
95
+ module.exports = { OPS, SEVERITIES, validateRule, evaluateRules, foldSignals, tripsSince, trippedSince, appendRule, appendReading };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+ // Signal engine, step 8: card anatomy. A card is a plain object the UI can render:
3
+ // tag, headline (computed numbers), hero (one, chosen by data shape), facts (at most 3), move (suggested move
4
+ // with options, or null), deadline (days + source, falling back to the promise date), why (score terms), absorbs,
5
+ // kind + tone + template (kinds.js: what this kind must say, the parts it says and the parts it cannot), source.
6
+
7
+ const { rankOptions } = require('./options');
8
+ const { weakest } = require('./detectors');
9
+ const K = require('./kinds');
10
+
11
+ // Hero by data shape. A series of 3 or more points is a line; two values to compare are two bars.
12
+ const HERO = { series: 'line', compare: 'two-bars', rows: 'rows', ring: 'ring', bars: 'bars', graph: 'mini-graph', number: 'big-number' };
13
+ function heroFor(head) {
14
+ if (Array.isArray(head.series) && head.series.length >= 3) return { kind: 'line', points: head.series };
15
+ if (head.shape === 'series') return { kind: 'big-number' };
16
+ return { kind: HERO[head.shape] || 'big-number' };
17
+ }
18
+
19
+ function deadlineOf(c, tolerance) {
20
+ if (c.needsDeadline) return { days: null, source: 'unknown', text: 'unknown - the agent asks: when does this need deciding?' };
21
+ const d = c.deadlineDays;
22
+ const when = d < 1 ? 'within a day' : Math.round(d) + ' days';
23
+ return { days: d, source: c.deadlineSource, text: `${when} because of ${c.deadlineSource}${c.promiseMissed && tolerance != null ? ` | promise to others missed beyond ${tolerance}d tolerance` : ''}` };
24
+ }
25
+
26
+ function buildCard(c, ctx) {
27
+ const head = c.head;
28
+ const tol = head.tolerance != null ? head.tolerance : null;
29
+ const w = c.weights;
30
+ const pinned = ctx.pinned.includes(c);
31
+ const move = head.options && head.options.length
32
+ ? rankOptions(head.options, { stance: ctx.stance, holders: (ctx.holdersOf && ctx.holdersOf(head.initiative)) || 0, weak: weakest(ctx.rubric) })
33
+ : null;
34
+ const k = K.kindOf(c);
35
+ const card = {
36
+ key: c.key, tag: c.tag, kind: k.kind, tone: K.toneOf(c, k), signal: K.baseId(head.id) || null, headline: c.title, hero: heroFor(head),
37
+ facts: (c.facts || []).filter(Boolean).slice(0, 3),
38
+ move: move && {
39
+ suggested: move.rec, options: move.opts, hold: move.hold, moveDate: move.move, closeCall: move.close, stance: ctx.stance,
40
+ note: 'confidence not shown: no track record for this kind of decision yet',
41
+ },
42
+ deadline: deadlineOf(c, tol),
43
+ waitsFor: c.blockedBy || null,
44
+ pinned,
45
+ why: {
46
+ pinned, impact: c.impact, urgency: c.urgency, irreversible: c.irrev, novelty: c.novelty, lensFit: c.lensFit, positive: c.positive, score: c.score,
47
+ text: `${pinned ? '[pinned: Critical and due within 3 days] ' : ''}impact ${c.impact.toFixed(2)} x ${w.impact} + urgency ${c.urgency.toFixed(2)} x ${w.urgency} + irreversible ${c.irrev} x ${w.irreversible}, x novelty ${c.novelty} x ${ctx.lens || 'any'} fit ${c.lensFit}${c.positive ? ' x 0.8 (positive)' : ''} = ${c.score}`,
48
+ },
49
+ absorbs: c.members.map((m) => m.id),
50
+ page: c.page, entity: c.entity, score: c.score,
51
+ source: { page: c.page, entity: c.entity || null },
52
+ };
53
+ card.template = { must: k.must, text: k.says, ...K.fillTemplate(card, k.must) };
54
+ return card;
55
+ }
56
+
57
+ module.exports = { buildCard, heroFor, deadlineOf };