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 +27 -41
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/changelog.json +26 -0
- package/workspace/guide/reports.md +5 -1
- package/workspace/lib/report-cli.js +26 -6
- package/workspace/lib/report-engine.js +77 -20
- package/workspace/lib/report-export.js +155 -3
- package/workspace/lib/report-library.js +134 -0
- package/workspace/lib/report-pyramid.js +2 -0
- package/workspace/lib/report-slides.js +580 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/cozy-vw.css +1 -0
- package/workspace/public/pm/premium-report-read.js +24 -3
- package/workspace/skills/report-write.skill +4 -1
- package/workspace/test-lists/report-slides.json +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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
|
}
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T19:01:32Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/changelog.json
CHANGED
|
@@ -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-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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',
|
|
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))
|
|
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
|
|
686
|
-
const
|
|
687
|
-
|
|
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);
|