create-pmos 0.530.0 → 0.532.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.530.0",
3
+ "version": "0.532.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -32,6 +32,32 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.532.0",
37
+ "at": "2026-11-04T19:01:35.763Z",
38
+ "f": 4,
39
+ "prs": [
40
+ {
41
+ "n": 1248,
42
+ "t": "Report templates T02–T08",
43
+ "s": "Report templates T02–T08",
44
+ "k": "new"
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "v": "0.531.0",
50
+ "at": "2026-11-04T18:44:50.726Z",
51
+ "f": 4,
52
+ "prs": [
53
+ {
54
+ "n": 1247,
55
+ "t": "Report library frame and 23 slide layouts",
56
+ "s": "Report library frame and 23 slide layouts",
57
+ "k": "new"
58
+ }
59
+ ]
60
+ },
35
61
  {
36
62
  "v": "0.530.0",
37
63
  "at": "2026-11-04T14:38:26.528Z",
@@ -393,46 +419,6 @@
393
419
  "k": "new"
394
420
  }
395
421
  ]
396
- },
397
- {
398
- "v": "0.512.0",
399
- "at": "2026-11-03T15:33:10.629Z",
400
- "f": 4,
401
- "prs": [
402
- {
403
- "n": 1204,
404
- "t": "Suggest the next prompt and sharpen a rough one in the chat",
405
- "s": "Suggest the next prompt and sharpen a rough one in the chat",
406
- "k": "new"
407
- }
408
- ]
409
- },
410
- {
411
- "v": "0.511.0",
412
- "at": "2026-11-03T15:10:29.930Z",
413
- "f": 4,
414
- "prs": [
415
- {
416
- "n": 1202,
417
- "t": "A closed initiative is never late on the Board",
418
- "s": "A closed initiative is never late on the Board",
419
- "k": "fix"
420
- },
421
- {
422
- "n": 1200,
423
- "t": "Open the chat on the Board, and dock panels left or right",
424
- "s": "Open the chat on the Board, and dock panels left or right",
425
- "k": "new"
426
- },
427
- {
428
- "n": 1201,
429
- "t": "Settings Updates tells each change as Claude's plain-language story (design E)",
430
- "s": "Settings Updates tells each change as Claude's plain-language story (design E)",
431
- "k": "new",
432
- "h": "Updates read like a story",
433
- "d": "Updates listed pull request titles and numbers, grouped by kind, which was hard to skim. Updates opens with a short summary of the week, then the newest version, then each change as a plain story, with a dot for New and Fixed."
434
- }
435
- ]
436
422
  }
437
423
  ]
438
424
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@5a2765e8
1
+ kernel-source: pmos@c8e1d796
2
2
  assembled: 2026-11-04
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-11-04T14:38:23Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T19:01:32Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-11-04T14:38:23Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T19:01:32Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-11-04T14:38:23Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T19:01:32Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,4 +1,30 @@
1
1
  [
2
+ {
3
+ "v": "0.532.0",
4
+ "at": "2026-11-04T19:01:35.763Z",
5
+ "f": 4,
6
+ "prs": [
7
+ {
8
+ "n": 1248,
9
+ "t": "Report templates T02–T08",
10
+ "s": "Report templates T02–T08",
11
+ "k": "new"
12
+ }
13
+ ]
14
+ },
15
+ {
16
+ "v": "0.531.0",
17
+ "at": "2026-11-04T18:44:50.726Z",
18
+ "f": 4,
19
+ "prs": [
20
+ {
21
+ "n": 1247,
22
+ "t": "Report library frame and 23 slide layouts",
23
+ "s": "Report library frame and 23 slide layouts",
24
+ "k": "new"
25
+ }
26
+ ]
27
+ },
2
28
  {
3
29
  "v": "0.530.0",
4
30
  "at": "2026-11-04T14:38:26.528Z",
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Reports: draft and issue a cadence report"
4
4
  description: "How pmos report drafts a cadence report with every number, how the notes are kept when you draft again, and how you issue and freeze a report."
5
- timestamp: 2026-11-02
5
+ timestamp: 2026-11-04
6
6
  ---
7
7
 
8
8
  # Reports: draft and issue a cadence report
@@ -90,6 +90,10 @@ pmos report issue biweekly 2026-10-16
90
90
 
91
91
  `pmos report finder` shows the report finder setting, or changes it.
92
92
 
93
+ `pmos report library` writes one page with every slide layout of the report library, filled with placeholder words. Open it in a browser to see the layouts a report can use. Add `--out <file>` to choose the file.
94
+
95
+ The library also has seven report templates: weekly-review, program-kickoff, meeting-recap, portfolio-snapshot, quarterly-review, decision-brief and incident-review. A template is a default order of slides, not a fixed structure. `pmos report library --template <id>` draws one template with placeholder words. `pmos report make <id> --template <id>` starts a report from it: the writer uses its order and adds or drops slides when the findings say so. A recurring report keeps the outline its readers agreed, so the template is used only for a report that has none.
96
+
93
97
  Every command accepts `--root <dir>` to name the repository. `draft` and `list` also accept `--json`.
94
98
 
95
99
  ## The window
@@ -37,10 +37,10 @@ const actorLib = require('./actor.js');
37
37
  const RS = require('./report-setups.js');
38
38
  const { parseArgs } = require('./schedule-cli');
39
39
 
40
- const COMMANDS = { report: ['make', 'read', 'find', 'write', 'check', 'rule', 'draft', 'issue', 'list', 'propose', 'definitions', 'setups', 'finder'] };
40
+ const COMMANDS = { report: ['make', 'read', 'find', 'write', 'check', 'rule', 'draft', 'issue', 'list', 'propose', 'definitions', 'setups', 'finder', 'library'] };
41
41
  const USAGE = [
42
42
  'usage: pmos report make|read|find|write|check|rule — the report engine: brief, read, find, answer, render (any report, any shape)',
43
- ' pmos report make <id> [--ask "<the question>"] [--readers "<who>"] [--source <kind:ref>,…] [--slot <date>]',
43
+ ' pmos report make <id> [--ask "<the question>"] [--readers "<who>"] [--source <kind:ref>,…] [--template <id>] [--slot <date>]',
44
44
  ' Steps 1 to 3: derive the brief (readers, question, window, sources; no section list), read every source PMOS reaches,',
45
45
  ' and find what changed. An ad hoc report needs --ask. Sources: pmos · pack:<state file of readings> · connector:<name> · document:<path>.',
46
46
  ' pmos report read <id> [--slot <date>] --from <file|->',
@@ -54,6 +54,9 @@ const USAGE = [
54
54
  ' pmos report check <id> [--slot <date>] the checks of the last write: substance first, then form',
55
55
  ' pmos report rule <id> list | add <voice|wording|off|keep> "<text or part>" | remove <n>',
56
56
  ' What the readers taught this report: a voice or wording rule, or a part beyond the ask turned off or kept.',
57
+ 'usage: pmos report library [--template <id>] [--out <file>] [--json] — the slide library: every layout in the report frame, with placeholder words',
58
+ ' Writes one HTML page (default: report-library.html in the current folder). Open it in a browser to check the look.',
59
+ ' --template draws one report template (its card, then each part); --json lists the templates and the layouts.',
57
60
  'usage: pmos report draft|issue|list — cadence reports (for example the biweekly)',
58
61
  ' pmos report draft <cadence> [<slot>] [--now <time>] [--redraft]',
59
62
  ' Write the draft for a slot (default: today) with every number. Refuses a slot that has a draft;',
@@ -74,9 +77,9 @@ const USAGE = [
74
77
  ' every command: [--json] [--root <dir>]. The cadence is the report skill id without "report-", for example biweekly.',
75
78
  ];
76
79
  const BOOL = new Set(['json', 'redraft', 'help']);
77
- const VALUE = new Set(['now', 'root', 'ask', 'readers', 'source', 'slot', 'from']);
78
- const ALLOWED = { make: ['json', 'now', 'root', 'ask', 'readers', 'source', 'slot'], read: ['json', 'now', 'root', 'slot', 'from', 'source'], find: ['json', 'now', 'root', 'slot', 'from'],
79
- write: ['json', 'now', 'root', 'slot', 'from'], check: ['json', 'root', 'slot'], rule: ['json', 'root'], draft: ['json', 'redraft', 'now', 'root'], issue: ['root'], list: ['json', 'root'], propose: ['json', 'root'], definitions: ['json', 'root'], setups: ['json', 'root'], finder: ['json', 'root'] };
80
+ const VALUE = new Set(['now', 'root', 'ask', 'readers', 'source', 'slot', 'from', 'out', 'template']);
81
+ const ALLOWED = { make: ['json', 'now', 'root', 'ask', 'readers', 'source', 'slot', 'template'], read: ['json', 'now', 'root', 'slot', 'from', 'source'], find: ['json', 'now', 'root', 'slot', 'from'],
82
+ write: ['json', 'now', 'root', 'slot', 'from'], check: ['json', 'root', 'slot'], rule: ['json', 'root'], draft: ['json', 'redraft', 'now', 'root'], issue: ['root'], list: ['json', 'root'], propose: ['json', 'root'], definitions: ['json', 'root'], setups: ['json', 'root'], finder: ['json', 'root'], library: ['json', 'out', 'template'] };
80
83
  const CADENCE_RE = /^[a-z][a-z0-9-]{0,39}$/;
81
84
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
82
85
 
@@ -145,6 +148,23 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
145
148
  return 0;
146
149
  }
147
150
 
151
+ if (verb === 'library') {
152
+ // the slide library as one page: sanitized as a report render is, with the workspace face inlined so the file shows it anywhere
153
+ const out = path.resolve(cwd || process.cwd(), typeof flags.out === 'string' && flags.out ? flags.out : 'report-library.html');
154
+ const D = require('./report-design.js');
155
+ const LB = require('./report-library.js');
156
+ if (flags.template !== undefined && !LB.templateOf(String(flags.template))) return fail(`there is no report template ${flags.template}. The templates: ${LB.TEMPLATES.map((t) => t.id).join(', ')}`, 2);
157
+ if (json && flags.out === undefined && flags.template === undefined) {
158
+ say(JSON.stringify({ ok: true, layouts: require('./report-slides.js').LAYOUTS.filter((x) => x !== 'blocks'), templates: LB.TEMPLATES.map((t) => ({ id: t.id, name: t.name, use: t.use, readers: t.readers, cadence: t.cadence, outline: t.outline })) }));
159
+ return 0;
160
+ }
161
+ let face = '';
162
+ try { face = `<style>@font-face{font-family:"Plus Jakarta Sans";font-weight:200 800;src:url(data:font/woff2;base64,${fs.readFileSync(path.join(__dirname, '..', 'public', 'vendor', 'plus-jakarta-sans-var.woff2')).toString('base64')}) format("woff2")}body{margin:0;background:#888}.pmos-slide{margin:0 auto 12px}</style>`; } catch { face = ''; }
163
+ const html = D.sanitize(LB.libraryHtml(flags.template ? { template: String(flags.template) } : {}));
164
+ try { fs.writeFileSync(out, html.replace('<style>', face + '<style>')); } catch (e) { return fail(`could not write ${out}: ${String((e && e.message) || e).slice(0, 160)}`); }
165
+ if (json) say(JSON.stringify({ ok: true, path: out })); else say(`Wrote ${out}`);
166
+ return 0;
167
+ }
148
168
  if (['make', 'read', 'find', 'write', 'check', 'rule'].includes(verb)) return engineVerb(verb, { pos, flags, root, json, say, fail, isTTY, env, stdin, now });
149
169
  if (verb === 'definitions' || verb === 'setups' || verb === 'propose' || verb === 'finder') return reportWorkspace(verb, { pos, flags, root, json, say, fail, cwd, isTTY, env, stdin, now });
150
170
 
@@ -219,7 +239,7 @@ function engineVerb(verb, { pos, flags, root, json, say, fail, isTTY, env, stdin
219
239
  return 0;
220
240
  };
221
241
  if (verb === 'make') {
222
- const m = RE.make(root, id, { slot: day, ask: flags.ask, readers: flags.readers, sources, now: clock });
242
+ const m = RE.make(root, id, { slot: day, ask: flags.ask, readers: flags.readers, sources, template: flags.template, now: clock });
223
243
  if (!m.ok) return fail(m.error);
224
244
  return summary(m);
225
245
  }
@@ -205,7 +205,7 @@ function issuedSlots(root, id) {
205
205
  /** The headings of a document the engine did not write: the agreed outline of an older issue. */
206
206
  const headingsOf = (md) => str(md).replace(/^---[\s\S]*?\n---\n?/, '').split('\n').filter((l) => /^## /.test(l)).map((l) => l.slice(3).trim()).filter(Boolean);
207
207
 
208
- function briefOf(root, id, { slot, ask, readers, sources, now = Date.now() } = {}) {
208
+ function briefOf(root, id, { slot, ask, readers, sources, template, now = Date.now() } = {}) {
209
209
  if (!ID_RE.test(str(id))) return refuse(400, 'the report id is lower case letters, digits and dashes, at most 40, starting with a letter');
210
210
  const today = slot || dayOf(now);
211
211
  if (!isDate(today)) return refuse(400, 'the slot is a date, YYYY-MM-DD');
@@ -213,7 +213,7 @@ function briefOf(root, id, { slot, ask, readers, sources, now = Date.now() } = {
213
213
  const P = paths(abs, id, today);
214
214
  const from = [];
215
215
  const brief = { v: 1, id, slot: today, title: null, readers: null, question: null, questions: [], decision: null, window: null, sources: [], recurring: false,
216
- cycle_days: null, agreed_outline: [], earlier_outline: [], last_issue: null, carried_asks: [], rules: [], voice: VOICE.slice(), from };
216
+ cycle_days: null, agreed_outline: [], earlier_outline: [], template: null, last_issue: null, carried_asks: [], rules: [], voice: VOICE.slice(), from };
217
217
 
218
218
  // the definition: readers, the question (its purpose), the sources, the cadence
219
219
  let def = null; let defText = '';
@@ -280,6 +280,12 @@ function briefOf(root, id, { slot, ask, readers, sources, now = Date.now() } = {
280
280
  }
281
281
  from.push(LP.md.rel);
282
282
  }
283
+ // a report template (lib/report-library.js) is a default order of slide layouts for a report with no agreed outline; the agreed one wins
284
+ if (template) {
285
+ const t = require('./report-library.js').templateOf(str(template));
286
+ if (!t) return refuse(400, `there is no report template ${template}. The templates: ${require('./report-library.js').TEMPLATES.map((x) => x.id).join(', ')}`);
287
+ if (!brief.agreed_outline.length) { from.push(`template:${t.id}`); brief.template = { id: t.id, name: t.name, outline: t.outline.map((o) => ({ part: o.part, layout: o.layout })) }; }
288
+ }
283
289
  brief.rules = rulesOf(abs, id);
284
290
  for (const r of brief.rules) if (r.kind === 'voice' || r.kind === 'wording') brief.voice.push(r.text);
285
291
  return { ok: true, brief };
@@ -456,6 +462,10 @@ function textsOf(report) {
456
462
  if (isObj(report.answer)) { add('answer', report.answer.text); (report.answer.supports || []).forEach((s, i) => add(`answer.supports[${i}]`, s)); }
457
463
  (report.sections || []).forEach((s, i) => {
458
464
  add(`sections[${i}].title`, s.title);
465
+ add(`sections[${i}].eyebrow`, s.eyebrow); add(`sections[${i}].sub`, s.sub);
466
+ // a section bound to a library layout (lib/report-slides.js): every text in its data and its insight is the writer's, so it is checked too
467
+ if (s.layout) { try { require('./report-slides.js').textsOfLayout(s.layout, s.data).forEach((t) => add(`sections[${i}].data`, t)); } catch { /* shapeError names it */ } }
468
+ textsOfInsight(s.insight).forEach((t) => add(`sections[${i}].insight`, t));
459
469
  (s.blocks || []).forEach((b, j) => {
460
470
  const w = `sections[${i}].blocks[${j}]`;
461
471
  add(w, b.text); (b.items || []).forEach((x) => add(w, x));
@@ -475,11 +485,20 @@ function shapeError(report) {
475
485
  if (report.answer.supports != null && (!Array.isArray(report.answer.supports) || report.answer.supports.length > 5 || report.answer.supports.some((s) => !isStr(s)))) return 'answer.supports is a list of at most 5 sentences';
476
486
  if (!Array.isArray(report.sections) || report.sections.length > LIMITS.sections) return `sections is a list of at most ${LIMITS.sections} (any titles, any order: the writer chooses)`;
477
487
  const seen = new Set();
478
- for (const [i, s] of report.sections.entries()) {
488
+ for (let [i, s] of report.sections.entries()) {
479
489
  if (!isObj(s) || !isStr(s.title) || !s.title.trim() || /[\r\n]/.test(s.title)) return `sections[${i}] has a one-line title`;
480
490
  const id = str(s.id) || slugOf(s.title);
481
491
  if (seen.has(id)) return `two sections share the id ${id}`;
482
492
  seen.add(id);
493
+ if (s.layout != null) {
494
+ const RS = require('./report-slides.js');
495
+ if (!RS.LAYOUTS.includes(s.layout) || s.layout === 'blocks') return `sections[${i}].layout is one of ${RS.LAYOUTS.filter((x) => x !== 'blocks').join(', ')} (or leave it out and write blocks)`;
496
+ const e = RS.layoutError(s.layout, s.data);
497
+ if (e) return `sections[${i}] (${s.layout}): ${e}`;
498
+ if (s.blocks == null) s = { ...s, blocks: [] };
499
+ }
500
+ if (s.insight != null && (!isObj(s.insight) || (s.insight.points != null && (!Array.isArray(s.insight.points) || s.insight.points.length > 4 || s.insight.points.some((x) => !isObj(x) || !isStr(x.text)))))) return `sections[${i}].insight is { head, text, points: [{ tone, text }] (at most 4), note }`;
501
+ for (const k of ['eyebrow', 'sub']) if (s[k] != null && (!isStr(s[k]) || /[\r\n]/.test(s[k]) || s[k].length > LIMITS.title)) return `sections[${i}].${k} is one line`;
483
502
  if (!Array.isArray(s.blocks) || s.blocks.length > LIMITS.blocks) return `sections[${i}].blocks is a list of at most ${LIMITS.blocks}`;
484
503
  for (const [j, b] of s.blocks.entries()) {
485
504
  if (!isObj(b) || !BLOCK_KINDS.includes(b.kind)) return `sections[${i}].blocks[${j}].kind is one of ${BLOCK_KINDS.join(', ')}`;
@@ -613,7 +632,17 @@ function renderMarkdown(report, { brief = null, readings = null, now = Date.now(
613
632
  }
614
633
  for (const s of report.sections) {
615
634
  lines.push(`## ${r(s.title)}`, '');
616
- for (const b of s.blocks) lines.push(blockMd(b, null, byId, brief), '');
635
+ if (s.layout && !(s.blocks || []).length) {
636
+ // a section bound to a library layout and no blocks: the document holds the layout's lines and its insight, sourced like a block
637
+ const RS = require('./report-slides.js');
638
+ const texts = RS.textsOfLayout(s.layout, s.data).concat(textsOfInsight(s.insight));
639
+ const ls = RS.linesOf(s.layout, RS.mapLayout(s.layout, s.data, r));
640
+ if (ls.length) lines.push(ls.map((l) => `- ${l}`).join('\n'), '');
641
+ if (isObj(s.insight) && (s.insight.head || s.insight.text)) lines.push(`**${r(str(s.insight.head)) || 'Insight'}.** ${r(str(s.insight.text))}`.trim(), '');
642
+ const src = sourceLine(texts.flatMap(tokensIn), byId, brief);
643
+ if (src) lines.push(`*${src}*`, '');
644
+ }
645
+ for (const b of s.blocks || []) lines.push(blockMd(b, null, byId, brief), '');
617
646
  }
618
647
  for (const p of beyondParts(report)) {
619
648
  lines.push(`## ${p.label}`, '');
@@ -644,50 +673,77 @@ function periodOf(brief, R) {
644
673
  const ats = R.map((x) => x.at).sort();
645
674
  return 'Data of ' + span(ats[0], ats[ats.length - 1]);
646
675
  }
647
- /** The deck of an engine report: the same model, drawn as slides (lib/report-pyramid.js deckHtml and lib/report-export.js pptx draw it). */
676
+ /** The deck of an engine report: the same model, drawn as slides in the report library's frame (B2, lib/report-slides.js; batch 27 item 14).
677
+ * Each slide keeps its kind (title, answer, section, beyond, ask, sources) and names its layout: the title is the overview with the sections as its
678
+ * agenda, the answer lists its supports with the ask in the insight column, a section is drawn by the layout it names (else its blocks), the parts
679
+ * beyond the question are a numbered list, the ask is the decisions layout and the sources close the deck. lib/report-pyramid.js deckHtml and
680
+ * lib/report-export.js pptx draw it. */
648
681
  function deckOf(report, { brief = null, readings = null, look = null } = {}) {
682
+ const RS = require('./report-slides.js');
649
683
  const R = Array.isArray(readings && readings.readings) ? readings.readings : [];
650
684
  const byId = new Map(R.map((r) => [r.id, r]));
651
685
  const r = (t) => String(resolve(t, byId));
652
686
  const srcOf = (ids) => sourceLine(ids, byId, brief).replace(/\.$/, '');
653
687
  const slides = [];
688
+ const deckName = (brief && brief.title) || '';
654
689
  const titleText = clip(r(report.title), 160);
655
- slides.push({ kind: 'title', tag: [brief && brief.title, brief && brief.readers ? `for ${brief.readers}` : ''].filter(Boolean).join(' · ') || 'Report', title: titleText, size: titleText.length > 60 ? 48 : 64,
656
- sub: brief ? periodOf(brief, R) : '', source: srcOf(tokensIn(report.title)) });
657
- const supports = (report.answer.supports || []).map((s) => { const t = r(s); const m = /^([^:]{3,80}):\s+(.+)$/.exec(t); return m ? { head: m[1], text: m[2] } : { head: t, text: '' }; });
658
- const ask = report.ask ? { question: r(report.ask.text), who: report.ask.who ? r(report.ask.who) : (brief && brief.readers) || '', by: report.ask.by ? full(report.ask.by) : null, options: (report.ask.options || []).map(r), more: '' } : null;
659
- slides.push({ kind: 'answer', tag: 'The answer', title: clip(r(report.answer.text), 200), lead: '', supports, ask,
690
+ const firstLine = (s) => { const ins = isObj(s.insight) ? str(s.insight.head) || str(s.insight.text) : ''; const p = (s.blocks || []).find((b) => b && b.kind === 'p'); return ins || str(s.sub) || (p ? str(p.text) : ''); };
691
+ const agenda = report.sections.slice(0, 4).map((s) => ({ head: clip(r(s.title), 80), text: clip(r(firstLine(s)), 110) }));
692
+ slides.push({ kind: 'title', layout: agenda.length ? 'overview' : 'statement', eyebrow: [brief && brief.title, brief && brief.readers ? `for ${brief.readers}` : ''].filter(Boolean).join(' · ') || 'Report',
693
+ title: titleText, sub: brief ? periodOf(brief, R) : '', data: agenda.length ? { lead: clip(r(report.answer.text), 200), cols: agenda } : { text: r(report.answer.text) }, source: srcOf(tokensIn(report.title).concat(tokensIn(report.answer.text))) });
694
+ const supports = (report.answer.supports || []).map((s) => { const t = r(s); const m = /^([^:]{3,80}):\s+(.+)$/.exec(t); return m ? { title: m[1], text: m[2].charAt(0).toUpperCase() + m[2].slice(1) } : { title: t, text: '' }; });
695
+ const ask = report.ask ? { question: r(report.ask.text), who: report.ask.who ? r(report.ask.who) : (brief && brief.readers) || '', by: report.ask.by || null, options: (report.ask.options || []).map(r) } : null;
696
+ const askInsight = ask ? { head: 'Your call', text: ask.question, points: [], note: [ask.who ? `For ${ask.who}` : '', ask.by ? `by ${full(ask.by)}` : ''].filter(Boolean).join(' · ') } : null;
697
+ slides.push({ kind: 'answer', layout: supports.length ? 'agenda' : 'statement', eyebrow: 'The answer', title: clip(r(report.answer.text), 200), sub: '',
698
+ data: supports.length ? { rows: supports.slice(0, 5) } : { text: r(report.answer.text) }, insight: askInsight,
660
699
  source: srcOf(tokensIn(report.answer.text).concat(...(report.answer.supports || []).map(tokensIn))) });
661
700
  for (const s of report.sections) {
701
+ if (s.layout && !RS.layoutError(s.layout, s.data)) {
702
+ // the writer bound this section to a library layout: its texts are resolved here, its insight goes in the side column
703
+ const data = RS.mapLayout(s.layout, s.data, r);
704
+ const ins = isObj(s.insight) ? { head: r(str(s.insight.head)), text: r(str(s.insight.text)), points: (Array.isArray(s.insight.points) ? s.insight.points : []).map((x) => ({ tone: x.tone, text: r(str(x.text)) })), note: r(str(s.insight.note)) } : null;
705
+ const ids = RS.textsOfLayout(s.layout, s.data).concat(textsOfInsight(s.insight)).flatMap(tokensIn);
706
+ slides.push({ kind: 'section', layout: s.layout, eyebrow: s.eyebrow ? r(s.eyebrow) : '', title: clip(r(s.title), 160), sub: s.sub ? r(s.sub) : '', data, insight: ins, source: srcOf(ids) });
707
+ continue;
708
+ }
662
709
  // a section goes on as many slides as its blocks need: a long table is cut at 8 rows, and a block that would run under the footer starts a new slide
663
710
  const chunks = [[]]; let used = 0;
664
711
  const put = (b) => { const h = blockHeight(b); if (chunks[chunks.length - 1].length && used + h > SLIDE_ROOM) { chunks.push([]); used = 0; } chunks[chunks.length - 1].push(b); used += h; };
665
- for (const b of s.blocks) {
712
+ for (const b of s.blocks || []) {
666
713
  if (b.kind === 'table' && b.rows.length > 8) for (let i = 0; i < b.rows.length; i += 8) put({ ...b, rows: b.rows.slice(i, i + 8) });
667
714
  else put(b);
668
715
  }
669
716
  const real = chunks.filter((c) => c.length);
670
717
  real.forEach((blocks, i) => {
671
718
  const ids = blocks.flatMap((b) => textsOf({ title: '', answer: { text: '' }, sections: [{ title: '', blocks: [b] }] }).flatMap((t) => tokensIn(t.text)));
672
- slides.push({ kind: 'section', title: clip(r(s.title), 160) + (i ? ' (continued)' : ''), blocks: blocks.map((b) => drawBlock(b, r, byId)), source: srcOf(ids) });
719
+ slides.push({ kind: 'section', layout: 'blocks', eyebrow: '', title: clip(r(s.title), 160) + (i ? ' (continued)' : ''), sub: '', data: { blocks: blocks.map((b) => drawBlock(b, r, byId)) }, blocks: blocks.map((b) => drawBlock(b, r, byId)), source: srcOf(ids) });
673
720
  });
674
721
  }
675
722
  const parts = beyondParts(report);
676
723
  for (let i = 0; i < parts.length; i += 4) {
677
724
  const chunk = parts.slice(i, i + 4);
678
725
  // the title is a finding, as on every slide: the first part's own sentence
679
- slides.push({ kind: 'beyond', tag: 'Beyond the question', title: clip(r(chunk[0].texts[0]), 160), items: chunk.map((p) => ({ head: p.label, text: clip(p.texts.map(r).join(' '), 260) })),
726
+ slides.push({ kind: 'beyond', layout: 'agenda', eyebrow: 'Beyond the question', title: clip(r(chunk[0].texts[0]), 160), sub: '',
727
+ data: { rows: chunk.map((p) => ({ title: p.label, text: clip(p.texts.map(r).join(' '), 260) })) }, items: chunk.map((p) => ({ head: p.label, text: clip(p.texts.map(r).join(' '), 260) })),
680
728
  source: srcOf(chunk.flatMap((p) => p.texts.flatMap(tokensIn))) });
681
729
  }
682
- if (ask && (ask.options.length || ask.who)) slides.push({ kind: 'ask', tag: 'Needs your call', title: clip(ask.by ? `Decide by ${ask.by}: ${ask.question.replace(/\?$/, '')}` : ask.question, 200), ask, source: 'Source: the readings above' });
730
+ if (ask && (ask.options.length || ask.who)) {
731
+ const rows = ask.options.length ? ask.options.slice(0, 3).map((o, i) => ({ question: `Option ${'ABC'[i]}: ${o}`, who: ask.who, by: ask.by || undefined })) : [{ question: ask.question, who: ask.who, by: ask.by || undefined }];
732
+ slides.push({ kind: 'ask', layout: 'decisions', eyebrow: 'Needs your call', title: clip(ask.by ? `Decide by ${full(ask.by)}: ${ask.question.replace(/\?$/, '')}` : ask.question, 200), sub: ask.options.length ? ask.question : '',
733
+ data: { rows }, source: 'Source: the readings above' });
734
+ }
683
735
  const names = new Map(((brief && brief.sources) || []).map((x) => [x.id, x.name]));
684
736
  const used = new Set(textsOf(report).flatMap((t) => tokensIn(t.text)));
685
- const reached = [...new Set(R.filter((x) => used.has(x.id)).map((x) => x.source))].map((s) => `${names.get(s) || s}, read ${[...new Set(R.filter((x) => x.source === s && used.has(x.id)).map((x) => x.at))].sort().map(full).join(', ')}`);
686
- const nr = (Array.isArray(readings && readings.gaps) ? readings.gaps : []).map((g) => ({ source: names.get(g.source) || g.source, why: g.reason }));
687
- slides.push({ kind: 'sources', title: `Every number comes from ${reached.length === 1 ? 'one source' : `${reached.length} sources`}${nr.length ? `; ${nr.length === 1 ? 'one source was' : `${nr.length} sources were`} not reached` : ''}`, reached, notReached: nr, source: 'Source: the readings of this report' });
737
+ const srcIds = [...new Set(R.filter((x) => used.has(x.id)).map((x) => x.source))];
738
+ const lastAt = (s) => R.filter((x) => x.source === s && used.has(x.id)).map((x) => x.at).filter((a) => /^\d{4}-\d{2}-\d{2}$/.test(str(a))).sort().pop();
739
+ const read = srcIds.map((s) => ({ name: names.get(s) || s, ...(lastAt(s) ? { at: lastAt(s) } : {}) }));
740
+ const nr = (Array.isArray(readings && readings.gaps) ? readings.gaps : []).map((g) => ({ name: names.get(g.source) || g.source, why: str(g.reason) }));
741
+ slides.push({ kind: 'sources', layout: 'sources', eyebrow: 'Sources', title: `${read.length ? `Every number comes from ${read.length === 1 ? 'one source' : `${read.length} sources`}` : 'No reading is used on these slides'}${nr.length ? `; ${nr.length === 1 ? 'one source was' : `${nr.length} sources were`} not reached` : ''}`, sub: '',
742
+ data: { read: read.slice(0, 6), missing: nr.slice(0, 6) }, reached: read.map((x) => x.name + (x.at ? ', read ' + full(x.at) : '')), notReached: nr.map((x) => ({ source: x.name, why: x.why })), source: 'Source: the readings of this report' });
688
743
  slides.forEach((s, i) => { s.n = i + 1; });
689
- return { slides, look: look || null, name: r(report.title), engine: true };
744
+ return { slides, look: look || null, name: r(report.title), deck: deckName, today: (brief && brief.slot) || null, engine: true, frame: 'b2' };
690
745
  }
746
+ function textsOfInsight(ins) { return isObj(ins) ? [ins.head, ins.text, ins.note].concat((Array.isArray(ins.points) ? ins.points : []).map((x) => x && x.text)).filter(isStr) : []; }
691
747
  /** The room under a slide's title, and what a block takes of it (1280 x 720 frames, lib/report-pyramid.js css). */
692
748
  const SLIDE_ROOM = 440;
693
749
  function blockHeight(b) {
@@ -713,7 +769,7 @@ function drawBlock(b, r, byId) {
713
769
  // ── the steps, written ──────────────────────────────────────────────────────────────────────────────────────────────
714
770
  function isIssued(root, id, slot) { return issuedSlots(root, id).includes(slot); }
715
771
  /** Steps 1–3: the brief, the readings, the findings, written next to the draft. Filed readings and findings are kept across a re-read. */
716
- function make(root, id, { slot, ask, readers, sources, add, gaps, addFindings, now = Date.now() } = {}) {
772
+ function make(root, id, { slot, ask, readers, sources, template, add, gaps, addFindings, now = Date.now() } = {}) {
717
773
  const abs = path.resolve(String(root));
718
774
  const day = slot || dayOf(now);
719
775
  let P; try { P = paths(abs, id, day); } catch (e) { return refuse(400, e.message); }
@@ -721,6 +777,7 @@ function make(root, id, { slot, ask, readers, sources, add, gaps, addFindings, n
721
777
  const old = readJson(P.brief.abs);
722
778
  // a re-read keeps the brief's chat question and named sources unless new ones are given
723
779
  const b = briefOf(abs, id, { slot: day, now, ask: ask || (old && old.from && old.from.includes('chat') ? old.question : undefined), readers: readers || (old && old.from && old.from.includes('readers') ? old.readers : undefined),
780
+ template: template || (old && old.template ? old.template.id : undefined),
724
781
  sources: [...(old ? old.sources.filter((s) => s.kind === 'pack' || s.kind === 'connector' || s.kind === 'document') : []), ...(sources || [])] });
725
782
  if (!b.ok) return b;
726
783
  const rd = readingsOf(abs, b.brief, { add, gaps, keep: readJson(P.readings.abs), now });
@@ -748,7 +805,7 @@ function write(root, id, slot, report, { now = Date.now() } = {}) {
748
805
  const pre = checkReport(report, { brief, readings });
749
806
  const hardFail = pre.filter((c) => c.hard && !c.pass);
750
807
  if (hardFail.length) return refuse(422, hardFail.map((c) => `${c.id}: ${c.reason}`).join(' · '), { checks: pre });
751
- const clean = { ...report, sections: report.sections.map((s) => ({ ...s, id: str(s.id) || slugOf(s.title) })), carried: brief.carried_asks || [], v: 1, at: new Date(now).toISOString() };
808
+ const clean = { ...report, sections: report.sections.map((s) => ({ ...s, blocks: Array.isArray(s.blocks) ? s.blocks : [], id: str(s.id) || slugOf(s.title) })), carried: brief.carried_asks || [], v: 1, at: new Date(now).toISOString() };
752
809
  const md = renderMarkdown(clean, { brief, readings, now });
753
810
  const checks = checkReport(clean, { brief, readings, markdown: md });
754
811
  writeJson(P.report.abs, clean);