create-pmos 0.535.0 → 0.536.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.535.0",
3
+ "version": "0.536.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,19 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.536.0",
37
+ "at": "2026-11-05T10:03:22.177Z",
38
+ "f": 4,
39
+ "prs": [
40
+ {
41
+ "n": 1252,
42
+ "t": "Apply an order to a report draft, and save my own",
43
+ "s": "Apply an order to a report draft, and save my own",
44
+ "k": "new"
45
+ }
46
+ ]
47
+ },
35
48
  {
36
49
  "v": "0.535.0",
37
50
  "at": "2026-11-05T09:07:13.086Z",
@@ -398,27 +411,6 @@
398
411
  "k": "new"
399
412
  }
400
413
  ]
401
- },
402
- {
403
- "v": "0.516.0",
404
- "at": "2026-11-04T00:00:07.601Z",
405
- "f": 4,
406
- "prs": [
407
- {
408
- "n": 1209,
409
- "t": "Runbook of what Claude does behind the scenes, in the guide",
410
- "s": "Runbook of what Claude does behind the scenes, in the guide",
411
- "k": "x",
412
- "h": "See what Claude does behind the scenes",
413
- "d": "Claude's background jobs were explained one page at a time, and nothing listed them all together. One guide page lists every job Claude runs by itself: when it runs, what it reads, where you see the result, how to stop it, and what Claude fixes without asking."
414
- },
415
- {
416
- "n": 1208,
417
- "t": "Reports that need you come first, one ✓ each (redesign R)",
418
- "s": "Reports that need you come first, one ✓ each (redesign R)",
419
- "k": "new"
420
- }
421
- ]
422
414
  }
423
415
  ]
424
416
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@8d1f39b1
1
+ kernel-source: pmos@6d70978e
2
2
  assembled: 2026-11-05
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-05T09:07:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-05T10:03:18Z"}
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-05T09:07:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-05T10:03:18Z"}
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-05T09:07:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-05T10:03:18Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "v": "0.536.0",
4
+ "at": "2026-11-05T10:03:22.177Z",
5
+ "f": 4,
6
+ "prs": [
7
+ {
8
+ "n": 1252,
9
+ "t": "Apply an order to a report draft, and save my own",
10
+ "s": "Apply an order to a report draft, and save my own",
11
+ "k": "new"
12
+ }
13
+ ]
14
+ },
2
15
  {
3
16
  "v": "0.535.0",
4
17
  "at": "2026-11-05T09:07:13.086Z",
@@ -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-04
5
+ timestamp: 2026-11-05
6
6
  ---
7
7
 
8
8
  # Reports: draft and issue a cadence report
@@ -65,6 +65,16 @@ When you draft a slot again, every note stays as you wrote it, next to its initi
65
65
 
66
66
  When you ask Claude in a chat to change a report, the change goes to the latest draft of that report, in its own file. The report's session edits only the notes that the change is about, even when they have text, and keeps every other note and every number as it is. It keeps each caveat (a sentence that says something is provisional, not decided or unverified), also when you ask for shorter text, and it tells you which notes it changed and quotes any sentence it removed. It drafts the same slot again only when you ask for fresh numbers. It never drafts a new slot for today. A change to a number, a date, a section or a slide is not made in the draft: the session tells you which setting to change in the template or the studio. If the latest cycle is issued, it is frozen, and the change is not passed on.
67
67
 
68
+ ## Orders: the slide order of a draft
69
+
70
+ An order is a default order of slides, for example the Weekly Review order. The writer keeps every reading, retitles each part as a finding, and adds or drops a part when the findings say so. PMOS has seven orders. You can also save your own.
71
+
72
+ - **Claude's pick.** When an order fits the question of a draft better than its own order, a ✦ shows under the slides. It names the order and the parts that it adds (in blue) or drops (struck through). Press ✓ to write the draft again in that order. Press ✕ to hide the ✦. No ✦ shows on an issued report, on a draft that started from an order, or when your readers agreed the order.
73
+ - **The orders.** The grid icon under the summary opens the orders: the draft as it is now, your orders, then the seven PMOS orders. Each card shows its first slides and its number of slides. Press a card to see all its slides, its parts and what it changes in your draft. Press ✓ to write the draft again in that order.
74
+ - **Agreed order.** When the readers agreed the order of the last issued report, today's draft stays as agreed. ✓ then reads "Use from the next cycle": the next draft starts in the order that you picked.
75
+ - **The name and Undo.** The order's name shows under the summary, from the draft to the issued report. For one day after a redraft, the Undo icon puts the draft back as it was. An issued report is frozen: it has no Undo.
76
+ - **My orders.** The bookmark icon under the summary saves the draft's order as yours. When you change a draft that started from an order, a ✦ asks if you want to save it. The bookmark under the slide saves one slide's layout to My slides. Only the parts and the slide layouts are saved, never the numbers or the words. Your orders are private and show first, marked Mine. You can rename and delete only your own orders. To use a saved slide in a draft is not possible yet.
77
+
68
78
  ## The commands
69
79
 
70
80
  `pmos report draft <cadence> [<slot>]` writes the draft to `planning/reports/<cadence>/<slot>.md`. The slot is a date, YYYY-MM-DD. If you do not give a slot, the slot is today. If a draft for the slot exists, the command stops and writes nothing. To draft the slot again and keep every note, add `--redraft`. To fix the clock, add `--now <time>`: two drafts with the same inputs and the same `--now` are the same, byte for byte.
@@ -55,6 +55,7 @@ const READING_ID_RE = /^[a-z0-9][a-z0-9._-]{0,80}$/;
55
55
  const SOURCE_ID_RE = /^[a-z0-9][a-z0-9._-]{0,40}$/;
56
56
  const TOKEN_RE = /\{\{r:([a-z0-9][a-z0-9._-]{0,80})(?:\|(of|n|value|label))?\}\}/g;
57
57
  const DAY = 86400000;
58
+ const NEXT_ORDER = 'order-next.json'; // the order the PM chose for the next cycle (lib/report-orders.js)
58
59
  const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
59
60
  const CYCLE_DAYS = { daily: 1, weekly: 7, biweekly: 14, 'bi-weekly': 14, monthly: 30, quarterly: 91 };
60
61
  const LIMITS = Object.freeze({ readings: 2000, findings: 60, sections: 30, blocks: 40, rows: 60, cols: 12, text: 2000, title: 240, rules: 60 });
@@ -205,6 +206,14 @@ function issuedSlots(root, id) {
205
206
  /** The headings of a document the engine did not write: the agreed outline of an older issue. */
206
207
  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
208
 
209
+ /** null when o is a usable order { id, name, outline: [{ part, layout }] } (1 to 30 parts, each layout one of lib/report-slides.js LAYOUTS), else why. */
210
+ function orderError(o) {
211
+ if (!isObj(o) || !/^[a-z0-9][a-z0-9:-]{0,60}$/.test(str(o.id)) || !str(o.name).trim()) return 'an order has an id and a name';
212
+ if (!Array.isArray(o.outline) || !o.outline.length || o.outline.length > 30) return 'an order has 1 to 30 parts';
213
+ const L = require('./report-slides.js').LAYOUTS;
214
+ const bad = o.outline.find((p) => !isObj(p) || !str(p.part).trim() || !L.includes(str(p.layout)));
215
+ return bad ? `part ${JSON.stringify(bad && bad.part)} has no known layout` : null;
216
+ }
208
217
  function briefOf(root, id, { slot, ask, readers, sources, template, now = Date.now() } = {}) {
209
218
  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
219
  const today = slot || dayOf(now);
@@ -281,10 +290,20 @@ function briefOf(root, id, { slot, ask, readers, sources, template, now = Date.n
281
290
  from.push(LP.md.rel);
282
291
  }
283
292
  // 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
293
+ // template is an id of lib/report-library.js TEMPLATES, or an order already resolved ({ id, name, outline }: a saved "my order" or the brief's own,
294
+ // kept across a re-read). chosen: the PM pressed ✓ on it for this cycle, so it wins over the agreed outline (batch 27 item 16, lib/report-orders.js).
295
+ // an order the PM chose on an agreed outline "from the next cycle" (lib/report-orders.js setNext): a later slot starts from it
296
+ if (!template) { const nx = readJson(path.join(paths(abs, id).dir, NEXT_ORDER)); if (nx && isObj(nx.order) && isDate(nx.from) && today > nx.from) template = { ...nx.order, chosen: true }; }
284
297
  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 })) }; }
298
+ const t = isObj(template) ? orderError(template) ? null : template : require('./report-library.js').templateOf(str(template));
299
+ if (!t) return refuse(400, isObj(template) ? `the order is not usable: ${orderError(template)}` : `there is no report template ${template}. The templates: ${require('./report-library.js').TEMPLATES.map((x) => x.id).join(', ')}`);
300
+ const chosen = !!(isObj(template) && template.chosen);
301
+ if (!brief.agreed_outline.length || chosen) {
302
+ from.push(`template:${t.id}`);
303
+ brief.template = { id: str(t.id), name: str(t.name), outline: t.outline.map((o) => ({ part: str(o.part), layout: str(o.layout) })), ...(chosen ? { chosen: true } : {}) };
304
+ // the PM chose another order over the agreed one: the agreed outline is kept as an example only
305
+ if (chosen && brief.agreed_outline.length) { brief.earlier_outline = brief.agreed_outline; brief.agreed_outline = []; }
306
+ }
288
307
  }
289
308
  brief.rules = rulesOf(abs, id);
290
309
  for (const r of brief.rules) if (r.kind === 'voice' || r.kind === 'wording') brief.voice.push(r.text);
@@ -778,7 +797,7 @@ function make(root, id, { slot, ask, readers, sources, template, add, gaps, addF
778
797
  const old = readJson(P.brief.abs);
779
798
  // a re-read keeps the brief's chat question and named sources unless new ones are given
780
799
  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),
781
- template: template || (old && old.template ? old.template.id : undefined),
800
+ template: template || (old && old.template ? (!old.template.chosen && require('./report-library.js').templateOf(old.template.id) ? old.template.id : old.template) : undefined),
782
801
  sources: [...(old ? old.sources.filter((s) => s.kind === 'pack' || s.kind === 'connector' || s.kind === 'document') : []), ...(sources || [])] });
783
802
  if (!b.ok) return b;
784
803
  const rd = readingsOf(abs, b.brief, { add, gaps, keep: readJson(P.readings.abs), now });
@@ -790,6 +809,8 @@ function make(root, id, { slot, ask, readers, sources, template, add, gaps, addF
790
809
  const fd = findingsOf(b.brief, rd, { prior, add: keepFiled.concat(Array.isArray(addFindings) ? addFindings : []) });
791
810
  if (fd.ok === false) return fd;
792
811
  writeJson(P.brief.abs, b.brief);
812
+ // the order chosen for the next cycle is used once: this brief now carries it, so the record goes
813
+ if (b.brief.template && b.brief.template.chosen) { const nf = path.join(P.dir, NEXT_ORDER); const nx = readJson(nf); if (nx && isObj(nx.order) && nx.order.id === b.brief.template.id && day > nx.from) { try { fs.unlinkSync(nf); } catch { /* already gone */ } } }
793
814
  writeJson(P.readings.abs, { at: rd.at, readings: rd.readings, gaps: rd.gaps });
794
815
  writeJson(P.findings.abs, fd);
795
816
  return { ok: true, brief: b.brief, readings: { readings: rd.readings, gaps: rd.gaps }, findings: fd, paths: { brief: P.brief.rel, readings: P.readings.rel, findings: P.findings.rel, report: P.report.rel, md: P.md.rel } };
@@ -834,7 +855,7 @@ function engineDeckOf(absMd, look) {
834
855
  }
835
856
 
836
857
  module.exports = {
837
- ID_RE, BEYOND, BEYOND_KEYS, VOICE, RULE_KINDS, BLOCK_KINDS, LIMITS,
838
- paths, briefOf, readingsOf, findingsOf, resolve, tokensIn, bareFigures, checkReport, shapeError, renderMarkdown, deckOf, make, write,
858
+ ID_RE, NEXT_ORDER, BEYOND, BEYOND_KEYS, VOICE, RULE_KINDS, BLOCK_KINDS, LIMITS,
859
+ paths, briefOf, orderError, readingsOf, findingsOf, resolve, tokensIn, bareFigures, checkReport, shapeError, renderMarkdown, deckOf, make, write,
839
860
  engineReportOf, engineDeckOf, rulesOf, addRule, removeRule, closeAsk, parseSource, sourcesFromText, full, valueText,
840
861
  };
@@ -131,4 +131,4 @@ function templateDeck(id, o = {}) {
131
131
  /** The library page: every layout, or one template (o.template). */
132
132
  const libraryHtml = (o = {}) => RS.deckHtml(o.template ? templateDeck(o.template, o) : sampleDeck(o));
133
133
 
134
- module.exports = { sampleDeck, libraryHtml, TEMPLATES, templateOf, templateCard, templateDeck };
134
+ module.exports = { sampleDeck, libraryHtml, TEMPLATES, templateOf, templateCard, templateDeck, NAMES };
@@ -0,0 +1,218 @@
1
+ 'use strict';
2
+ /* report-orders.js — apply an order to a report draft, and keep my own orders (batch 27 item 16, agreed by Wawan 2026-11-05 00:53 "Build the
3
+ * recommendation"; main canvas boards TpA, TpD, TpE; brief /mnt/project-files/feedback-batch-27/item16/build-brief.md).
4
+ *
5
+ * The app says "order", never "template": T<n> already names a report's saved settings (lib/report-templates.js). An ORDER is a default order of
6
+ * slide layouts, { id, name, outline: [{ part, layout }] }: the seven of lib/report-library.js TEMPLATES, or one the PM saved ("Mine"). It is never
7
+ * a fixed structure: the writer retitles each part as a finding and adds or drops a part when the findings say so.
8
+ *
9
+ * partsOf(report, brief) → [{ part, layout }] the draft's order: one per section, named by its order's part or its layout (pure)
10
+ * diff(now, order) → { adds: [part], drops: [part] } by layout; the title and sources slides every deck has are left out (pure)
11
+ * nowOf(brief, report) → { kind: 'agreed'|'order'|'standard', id, name, parts, mine, edited, changes } the draft's order (pure)
12
+ * pick(brief, report) → { id, name, adds, drops } | null the ✦: the order that fits the report's question, by plain rules (pure)
13
+ * orderOf(id, mine) → an order by id: a TEMPLATES id or a saved "mine-<n>" (mine = fold(...).orders)
14
+ * previewDeck(order) → the order drawn with placeholder words, one slide per part (lib/report-slides.js deckHtml takes it)
15
+ * snapshot / undoOf / undo the draft as it was before a redraft, kept one day (Undo)
16
+ * setNext / nextOf an order chosen on an agreed outline, used from the next cycle (lib/report-engine.js briefOf reads it)
17
+ * MY ORDERS (person level, private): <runtimeDir>/report-orders.jsonl, append only
18
+ * { ev: 'save', id: 'mine-<n>', kind: 'order'|'slide', name, outline, from: { report, slot }, at } { ev: 'rename', id, name, at } { ev: 'remove', id, at }
19
+ * fold(lines) → { orders, slides } (newest first) · readLines(dir) · save(dir, o) · rename(dir, id, name) · remove(dir, id)
20
+ *
21
+ * WHAT IS SAVED. Parts and slide layouts only. Never a number, a sentence or a reading: those belong to one cycle and would be stale. A part's
22
+ * name is its order's part name, else its layout's name ("Ledger"); never the section's title, which is a finding.
23
+ */
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const crypto = require('crypto');
27
+ const RL = require('./report-library.js');
28
+
29
+ const DAY = 86400000;
30
+ const UNDO_FOR = DAY; // the previous draft is kept one day (TpA 3)
31
+ const DRAFTING_FOR = 30 * 60000; // a redraft that has not landed after 30 minutes is no longer "redrafting"
32
+ const ALWAYS = new Set(['overview', 'sources']); // the engine draws the title and the sources slides on every deck
33
+ const LEDGER = 'report-orders.jsonl';
34
+ const MINE_RE = /^mine-[1-9][0-9]{0,5}$/;
35
+ const FILES = ['.md', '.report.json', '.brief.json', '.checks.json', '.findings.json'];
36
+ const str = (v) => (typeof v === 'string' ? v : v == null ? '' : String(v));
37
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
38
+ const nameOfLayout = (l) => (RL.NAMES[l] || (l === 'component' ? 'PMOS component' : 'Text'));
39
+ const RE = () => require('./report-engine.js');
40
+
41
+ /* ── the draft's order (pure) ── */
42
+ /** One part per section of the engine's report, in order. The name comes from the order the draft began with (the first unused part of the same
43
+ * layout), else from the layout. A section's own title is a finding, so it is never a part name. */
44
+ function partsOf(report, brief) {
45
+ const secs = isObj(report) && Array.isArray(report.sections) ? report.sections : [];
46
+ const pool = (brief && brief.template && Array.isArray(brief.template.outline) ? brief.template.outline : []).map((p) => ({ ...p, used: false }));
47
+ return secs.map((s) => {
48
+ const layout = str(s.layout) || 'blocks';
49
+ const m = pool.find((p) => !p.used && p.layout === layout);
50
+ if (m) m.used = true;
51
+ return { part: m ? m.part : nameOfLayout(layout), layout };
52
+ });
53
+ }
54
+ const core = (parts) => (parts || []).filter((p) => !ALWAYS.has(p.layout));
55
+ /** What an order adds to the draft and what it drops, matched by layout (a layout used twice counts twice). */
56
+ function diff(now, order) {
57
+ const left = core(now).map((p) => ({ ...p, hit: false }));
58
+ const adds = [];
59
+ for (const p of core(order)) { const m = left.find((x) => !x.hit && x.layout === p.layout); if (m) m.hit = true; else adds.push(p.part); }
60
+ return { adds, drops: left.filter((x) => !x.hit).map((x) => x.part) };
61
+ }
62
+ const same = (d) => !d.adds.length && !d.drops.length;
63
+ const isMine = (id) => MINE_RE.test(str(id));
64
+ /** The draft's order: agreed with its readers, an order (edited or not), or the standard one the writer chose. */
65
+ function nowOf(brief, report) {
66
+ const parts = partsOf(report, brief);
67
+ const b = brief || {};
68
+ if (Array.isArray(b.agreed_outline) && b.agreed_outline.length) return { kind: 'agreed', id: 'agreed', name: 'Agreed order', parts, mine: false, edited: false, changes: { adds: [], drops: [] }, since: b.last_issue || null };
69
+ if (b.template && b.template.id) {
70
+ const ch = report ? diff(b.template.outline, parts) : { adds: [], drops: [] }; // what the draft added to its order (adds) and dropped (drops)
71
+ return { kind: 'order', id: str(b.template.id), name: str(b.template.name), parts, mine: isMine(b.template.id), edited: !!report && !same(ch), changes: { adds: ch.adds, drops: ch.drops } };
72
+ }
73
+ return { kind: 'standard', id: 'standard', name: 'Standard order', parts, mine: false, edited: false, changes: { adds: [], drops: [] } };
74
+ }
75
+
76
+ /* ── the ✦ pick (pure). Plain rules over the brief's words, first match wins; no model. Silent (null) when the outline is agreed, when the draft
77
+ * already began from an order, or when the order would change nothing. Claude's own pick while drafting is not proven yet (owed: 5 real drafts). */
78
+ const RULES = [
79
+ ['incident-review', /\b(incident|outage|post-?mortem|downtime|sev ?[0-4]|root cause)\b/],
80
+ ['meeting-recap', /\b(meeting|recap|minutes|stand-?up|retro(spective)?|workshop)\b/],
81
+ ['decision-brief', /\b(decide|decision|should we|go\/no-go|approve|approval)\b/],
82
+ ['program-kickoff', /\b(kick-?off|charter|new program(me)?|program(me)? start)\b/],
83
+ ['portfolio-snapshot', /\b(portfolio|programs|programmes|across products)\b/],
84
+ ['quarterly-review', /\b(quarter(ly)?|q[1-4]|okr review|qbr)\b/],
85
+ ['weekly-review', /\b(weekly|this week|week in review|last week)\b/],
86
+ ];
87
+ function pick(brief, report) {
88
+ const b = brief || {};
89
+ if ((Array.isArray(b.agreed_outline) && b.agreed_outline.length) || (b.template && b.template.id)) return null;
90
+ const words = [b.title, b.question, ...(Array.isArray(b.questions) ? b.questions : []), b.readers].map(str).join(' \n ').toLowerCase();
91
+ let id = (RULES.find(([, re]) => re.test(words)) || [])[0] || null;
92
+ if (!id && b.recurring && b.cycle_days === 7) id = 'weekly-review';
93
+ if (!id && b.recurring && b.cycle_days >= 80) id = 'quarterly-review';
94
+ const t = id ? RL.templateOf(id) : null;
95
+ if (!t) return null;
96
+ const d = diff(partsOf(report, b), t.outline);
97
+ return report && same(d) ? null : { id: t.id, name: t.name, ...d };
98
+ }
99
+
100
+ /* ── orders by id, and their pictures ── */
101
+ function asOrder(o) { return { id: o.id, name: o.name, outline: o.outline.map((p) => ({ part: p.part, layout: p.layout })) }; }
102
+ function orderOf(id, mine = []) {
103
+ const t = RL.templateOf(str(id));
104
+ if (t) return asOrder(t);
105
+ const m = isMine(id) ? (mine || []).find((x) => x.id === id) : null;
106
+ return m ? asOrder(m) : null;
107
+ }
108
+ /** One line under a card: what the order is for (a PMOS order) or when it was saved (mine). */
109
+ function subOf(id, mine) { const t = RL.templateOf(id); if (t) return t.use; const m = (mine || []).find((x) => x.id === id); return m ? 'Saved ' + str(m.at).slice(0, 10) : ''; }
110
+ /** The order with placeholder words: one slide per part, in its layout (the library's sample of that layout, titled by the part). */
111
+ function previewDeck(order, o = {}) {
112
+ if (!order) return null;
113
+ const lib = RL.sampleDeck(o).slides;
114
+ const slides = order.outline.map((p) => {
115
+ const s = lib.find((x) => x.layout === p.layout) || { ...lib.find((x) => x.layout === 'statement'), layout: 'statement' };
116
+ return { ...s, eyebrow: `${order.name} · ${p.part}`, title: p.part, sub: s.title, source: 'Order · placeholder words' };
117
+ });
118
+ slides.forEach((s, i) => { s.n = i + 1; });
119
+ return { name: order.name, today: o.today || '2026-11-04', slides };
120
+ }
121
+
122
+ /* ── Undo: the draft before a redraft, kept one day ── */
123
+ const sha = (t) => crypto.createHash('sha256').update(String(t)).digest('hex').slice(0, 16);
124
+ const undoDir = (P, slot) => path.join(P.dir, '.undo', slot);
125
+ function readJson(f) { try { const o = JSON.parse(fs.readFileSync(f, 'utf8')); return isObj(o) ? o : null; } catch { return null; } }
126
+ /** Copy the draft's files aside before a redraft. One snapshot per slot: Undo gives back the draft as it was before the NEWEST redraft. */
127
+ function snapshot(root, id, slot, { order = null, now = Date.now() } = {}) {
128
+ const P = RE().paths(root, id, slot);
129
+ const dir = undoDir(P, slot);
130
+ fs.rmSync(dir, { recursive: true, force: true });
131
+ fs.mkdirSync(dir, { recursive: true });
132
+ const had = [];
133
+ for (const ext of FILES) { const f = path.join(P.dir, slot + ext); if (fs.existsSync(f)) { fs.copyFileSync(f, path.join(dir, slot + ext)); had.push(ext); } }
134
+ let md = ''; try { md = fs.readFileSync(P.md.abs, 'utf8'); } catch { /* no document yet */ }
135
+ const meta = { at: new Date(now).toISOString(), order: order ? { id: order.id, name: order.name } : null, had, md: sha(md) };
136
+ fs.writeFileSync(path.join(dir, 'meta.json'), JSON.stringify(meta) + '\n');
137
+ return meta;
138
+ }
139
+ /** The Undo of a slot: { at, until, order, state: 'drafting'|'done'|'late' } while its day lasts and the slot is not issued, else null.
140
+ * drafting = the document has not changed since the press (the session still writes); late = it has not changed after 30 minutes. */
141
+ function undoOf(root, id, slot, { now = Date.now(), issued = false } = {}) {
142
+ if (issued) return null;
143
+ let P; try { P = RE().paths(root, id, slot); } catch { return null; }
144
+ const meta = readJson(path.join(undoDir(P, slot), 'meta.json'));
145
+ if (!meta || !Number.isFinite(Date.parse(meta.at))) return null;
146
+ const at = Date.parse(meta.at);
147
+ if (now - at > UNDO_FOR) return null;
148
+ let md = ''; try { md = fs.readFileSync(P.md.abs, 'utf8'); } catch { /* gone */ }
149
+ const changed = sha(md) !== meta.md;
150
+ return { at: meta.at, until: new Date(at + UNDO_FOR).toISOString(), order: meta.order, state: changed ? 'done' : now - at > DRAFTING_FOR ? 'late' : 'drafting' };
151
+ }
152
+ /** Put the draft back as it was before the redraft: the files it had come back, the files it did not have go. */
153
+ function undo(root, id, slot, o = {}) {
154
+ const u = undoOf(root, id, slot, o);
155
+ if (!u) return { ok: false, status: 409, error: 'there is nothing to undo on this draft' };
156
+ const P = RE().paths(root, id, slot); const dir = undoDir(P, slot);
157
+ const meta = readJson(path.join(dir, 'meta.json'));
158
+ for (const ext of FILES) {
159
+ const to = path.join(P.dir, slot + ext);
160
+ if (meta.had.includes(ext)) fs.copyFileSync(path.join(dir, slot + ext), to);
161
+ else fs.rmSync(to, { force: true });
162
+ }
163
+ fs.rmSync(dir, { recursive: true, force: true });
164
+ return { ok: true, order: u.order };
165
+ }
166
+ const dropUndo = (root, id, slot) => { try { const P = RE().paths(root, id, slot); fs.rmSync(undoDir(P, slot), { recursive: true, force: true }); } catch { /* none */ } };
167
+
168
+ /* ── an order for the next cycle (an agreed outline: today's draft stays as agreed) ── */
169
+ function setNext(root, id, order, from, { now = Date.now(), by = 'pm' } = {}) {
170
+ const P = RE().paths(root, id);
171
+ fs.mkdirSync(P.dir, { recursive: true });
172
+ fs.writeFileSync(path.join(P.dir, RE().NEXT_ORDER), JSON.stringify({ order: asOrder(order), from, at: new Date(now).toISOString(), by }) + '\n');
173
+ return { ok: true };
174
+ }
175
+ function nextOf(root, id) { try { const x = readJson(path.join(RE().paths(root, id).dir, RE().NEXT_ORDER)); return x && isObj(x.order) ? { id: x.order.id, name: x.order.name, from: x.from } : null; } catch { return null; } }
176
+
177
+ /* ── my orders: person level, private ── */
178
+ const ledger = (dir) => path.join(path.resolve(String(dir)), LEDGER);
179
+ function readLines(dir) {
180
+ let t; try { t = fs.readFileSync(ledger(dir), 'utf8'); } catch { return []; }
181
+ return t.split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(isObj);
182
+ }
183
+ function fold(lines) {
184
+ const m = new Map();
185
+ for (const e of lines || []) {
186
+ if (e.ev === 'save' && isMine(e.id) && Array.isArray(e.outline)) m.set(e.id, { id: e.id, kind: e.kind === 'slide' ? 'slide' : 'order', name: str(e.name), outline: e.outline, from: e.from || null, at: str(e.at) });
187
+ else if (e.ev === 'rename' && m.has(e.id)) m.get(e.id).name = str(e.name);
188
+ else if (e.ev === 'remove') m.delete(e.id);
189
+ }
190
+ const all = [...m.values()].reverse();
191
+ return { orders: all.filter((x) => x.kind === 'order'), slides: all.filter((x) => x.kind === 'slide') };
192
+ }
193
+ const cleanName = (n) => str(n).replace(/\{\{r:[^}]*\}\}/g, '').replace(/\s+/g, ' ').trim().slice(0, 80);
194
+ /** Save an order (kind 'order', the whole deck's parts) or one slide (kind 'slide', one part). Only parts and layouts are kept. */
195
+ function save(dir, { kind = 'order', name, outline, from = null }, { now = Date.now() } = {}) {
196
+ const nm = cleanName(name);
197
+ if (!nm) return { ok: false, status: 400, error: 'say its name' };
198
+ const parts = (Array.isArray(outline) ? outline : []).map((p) => ({ part: cleanName(p && p.part).slice(0, 60) || nameOfLayout(p && p.layout), layout: str(p && p.layout) }));
199
+ const err = RE().orderError({ id: 'mine-1', name: nm, outline: parts });
200
+ if (err) return { ok: false, status: 400, error: err };
201
+ if (kind === 'slide' && parts.length !== 1) return { ok: false, status: 400, error: 'a slide is one part' };
202
+ const lines = readLines(dir);
203
+ const n = lines.reduce((x, e) => Math.max(x, +(/^mine-(\d+)$/.exec(str(e.id)) || [0, 0])[1]), 0) + 1;
204
+ const e = { ev: 'save', id: 'mine-' + n, kind: kind === 'slide' ? 'slide' : 'order', name: nm, outline: parts, from: isObj(from) ? { report: str(from.report), slot: str(from.slot) } : null, at: new Date(now).toISOString() };
205
+ fs.mkdirSync(path.resolve(String(dir)), { recursive: true });
206
+ fs.appendFileSync(ledger(dir), JSON.stringify(e) + '\n');
207
+ return { ok: true, id: e.id, saved: fold(lines.concat(e)) };
208
+ }
209
+ function change(dir, ev, id, extra, now) {
210
+ if (!isMine(id) || ![...fold(readLines(dir)).orders, ...fold(readLines(dir)).slides].some((x) => x.id === id)) return { ok: false, status: 404, error: 'there is no saved order ' + id };
211
+ fs.appendFileSync(ledger(dir), JSON.stringify({ ev, id, ...extra, at: new Date(now).toISOString() }) + '\n');
212
+ return { ok: true };
213
+ }
214
+ function rename(dir, id, name, { now = Date.now() } = {}) { const nm = cleanName(name); if (!nm) return { ok: false, status: 400, error: 'say its name' }; return change(dir, 'rename', id, { name: nm }, now); }
215
+ function remove(dir, id, { now = Date.now() } = {}) { return change(dir, 'remove', id, {}, now); }
216
+
217
+ module.exports = { ALWAYS, LEDGER, UNDO_FOR, partsOf, diff, nowOf, pick, orderOf, subOf, previewDeck, isMine, nameOfLayout,
218
+ snapshot, undoOf, undo, dropUndo, setNext, nextOf, readLines, fold, save, rename, remove };
@@ -39,6 +39,7 @@ const MODULES = Object.freeze([
39
39
  'routes-capture', // Run 3 (3C): ⌘K capture
40
40
  'routes-board', // Run 4 (4D): the cross-initiative Board, Release
41
41
  'routes-reports', // Run 5 (5C): drafts, Issue, notes
42
+ 'routes-report-orders', // batch 27 item 16: apply an order to a report draft, Undo, my saved orders (canvas TpA, TpD, TpE)
42
43
  'routes-report-flow', // batch 22 item 14: the report building flow (template versions, the report's own session, review)
43
44
  'routes-notes', // meeting-notes Run 1: the Notes view (typed notes, dictation, transcripts, summaries)
44
45
  'routes-my-work', // initiative my-work: the ranked list of work assigned to the PM, the brief, Read again
@@ -307,4 +307,4 @@ async function handle(req, res, url, ctx) {
307
307
  return r.ok ? send({ status: 200, body: r }) : fail(r);
308
308
  }
309
309
 
310
- module.exports = { prefixes, handle, stateOf, askPrompt, draftWords };
310
+ module.exports = { prefixes, handle, stateOf, askPrompt, draftWords, ask };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+ /* routes-report-orders.js — apply an order to a report draft, Undo, and my saved orders (batch 27 item 16; main canvas TpA, TpD, TpE;
3
+ * lib/report-orders.js). The app says "order", never "template".
4
+ *
5
+ * GET /api/report-orders?ws&path → { ok, id, slot, issued, engine, now, pick, next, orders, slides, undo } what the draft's order is,
6
+ * the ✦ (pick), every order with what it adds and drops, my orders first, my slides, and the Undo
7
+ * GET /api/report-orders/preview?ws&order → text/html, the order drawn with placeholder words (one slide per part), sanitized
8
+ * POST /api/report-orders/apply { ws, path, order } → { ok, redraft: { session, started|passed } } | { ok, next: { id, name } }
9
+ * A draft: the draft is copied aside (Undo, one day), the brief takes the order (lib/report-engine.js make),
10
+ * and the report's own session is asked to write it again in that order, keeping its readings. An agreed outline:
11
+ * nothing changes today; the order is used from the next cycle. An issued cycle is refused 409 (it is frozen).
12
+ * POST /api/report-orders/undo { ws, path } → { ok } the draft as it was before the redraft (its day only)
13
+ * POST /api/report-orders/save { ws, path, name? } → { ok, id } the draft's parts and layouts as my order (never its numbers or words)
14
+ * POST /api/report-orders/slide { ws, path, n } → { ok, id, name } slide n (from 0) of the draft's deck as my slide: its layout and part
15
+ * POST /api/report-orders/rename { ws, id, name } → { ok }
16
+ * POST /api/report-orders/remove { ws, id } → { ok }
17
+ *
18
+ * MY ORDERS are the PM's own: they live in the per-user runtime directory (ctx.runtimeDir), not in any repository, and no other person reads them.
19
+ * WHO WRITES. Every POST passes pm-guard requirePagePress; a request with an agent session header is refused first (403). Nothing here sends
20
+ * anything outward; the redraft runs in the report's own session (lib/routes-report-flow.js ask), which never issues.
21
+ */
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const guard = require('./pm-guard.js');
25
+ const RO = require('./report-orders.js');
26
+ const RE = require('./report-engine.js');
27
+ const RL = require('./report-library.js');
28
+ const issues = require('./issues.js');
29
+
30
+ const prefixes = ['/api/report-orders'];
31
+ const POSTS = ['apply', 'undo', 'save', 'slide', 'rename', 'remove'];
32
+ const text = (v) => (typeof v === 'string' ? v : '');
33
+ const bad = (status, error) => ({ status, body: { ok: false, error } });
34
+ const PATH_RE = /(?:^|\/)reports\/([a-z][a-z0-9-]{0,39})\/(\d{4}-\d{2}-\d{2})\.md$/;
35
+ function wsOf(ctx, id) { const w = ctx.getWs(id || null); return w && (!id || w.id === id) ? w : null; }
36
+ const mineDir = (ctx) => ctx.runtimeDir || require('./state-root.js').defaultRoot();
37
+ const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
38
+
39
+ /** The draft a path names: { id, slot, P, issued } | { error }. The path must be a report document that exists. */
40
+ function draftOf(w, rel) {
41
+ const m = PATH_RE.exec(text(rel));
42
+ if (!m || text(rel).split('/').includes('..')) return { error: bad(400, 'path must be a report draft') };
43
+ let P; try { P = RE.paths(w.root, m[1], m[2]); } catch (e) { return { error: bad(400, e.message) }; }
44
+ if (!fs.existsSync(P.md.abs)) return { error: bad(404, 'no such report') };
45
+ let iss = false; try { iss = issues.isIssued(issues.readIssues(w.root), m[1], m[2]); } catch { iss = false; }
46
+ return { id: m[1], slot: m[2], P, issued: iss };
47
+ }
48
+ function setupName(w, id) { try { const RS = require('./report-setups.js'); const s = RS.fold(RS.readLines(RS.ledgerFile(w.root))).setups.get(id); return (s && s.name) || ''; } catch { return ''; } }
49
+ function reportName(w, id, brief) { return setupName(w, id) || (brief && brief.title) || id; }
50
+ /** A saved order's first name: the recurring report's ("Product weekly order"), else the order it began as ("Weekly Review, my way"), else
51
+ * "My order". Never an ad hoc report's title: that is its question, a sentence with dates in it. */
52
+ function savedName(w, id, brief) { const s = setupName(w, id); if (s) return s + ' order'; return brief && brief.template && brief.template.name ? brief.template.name.replace(/, my way$/, '') + ', my way' : 'My order'; }
53
+ const card = (o, now, mine, extra = {}) => ({ id: o.id, name: o.name, sub: RO.subOf(o.id, mine), n: o.outline.length, parts: o.outline, ...RO.diff(now.parts, o.outline), ...extra });
54
+ /** The whole state of one draft's order. */
55
+ function stateOf(ctx, w, d) {
56
+ const brief = readJson(d.P.brief.abs), report = readJson(d.P.report.abs);
57
+ const now = RO.nowOf(brief, report);
58
+ const my = RO.fold(RO.readLines(mineDir(ctx)));
59
+ const pk = d.issued ? null : RO.pick(brief, report);
60
+ const orders = my.orders.map((o) => card(o, now, my.orders, { mine: true }))
61
+ .concat(RL.TEMPLATES.map((t) => card({ id: t.id, name: t.name, outline: t.outline }, now, my.orders)));
62
+ return { ok: true, id: d.id, slot: d.slot, issued: d.issued, engine: !!report, name: reportName(w, d.id, brief),
63
+ now, pick: pk, next: RO.nextOf(w.root, d.id), orders, slides: my.slides.map((s) => ({ id: s.id, name: s.name, part: s.outline[0].part, layout: s.outline[0].layout, at: s.at })),
64
+ undo: RO.undoOf(w.root, d.id, d.slot, { issued: d.issued }) };
65
+ }
66
+ /** The words the report's session gets for a redraft in an order. */
67
+ function redraftWords(id, slot, rel, order) {
68
+ return `Write this draft again in the "${order.name}" order (${rel}, slot ${slot}). This change IS a redraft, of this slot only. `
69
+ + `The brief (${slot}.brief.json) now carries the order as its template: follow skills/report-write.skill from step 3, with the order's parts as the starting outline, `
70
+ + `each part in its slide layout and retitled as a finding; add or drop a part when the findings say so. Keep every reading and every finding already filed: `
71
+ + `run \`pmos report make ${id} --slot ${slot}\` only when a source is missing. Then run \`pmos report write ${id} --slot ${slot} --from <file>\`.`;
72
+ }
73
+ /** The deck slide n as one saved part: its layout, and a part name that is never a finding. */
74
+ function slidePart(d, n) {
75
+ const deck = RE.engineDeckOf(d.P.md.abs, null);
76
+ if (!deck) return { error: bad(409, 'this draft was written before the report engine: its slides have no layouts to save') };
77
+ const s = deck.slides[n];
78
+ if (!s) return { error: bad(404, 'no such slide') };
79
+ const fixed = { title: ['Title', 'overview'], answer: ['The answer', s.layout], beyond: ['Beyond the question', 'agenda'], ask: ['Asks', 'decisions'], sources: ['Sources', 'sources'] };
80
+ if (fixed[s.kind]) return { part: fixed[s.kind][0], layout: fixed[s.kind][1] || 'statement' };
81
+ const report = readJson(d.P.report.abs), brief = readJson(d.P.brief.abs);
82
+ const parts = RO.partsOf(report, brief);
83
+ // the section slides come after the title and answer slides, in the report's order (a long text section may take several slides)
84
+ const k = deck.slides.slice(0, n + 1).filter((x) => x.kind === 'section' && !/\(continued\)$/.test(x.title)).length - 1;
85
+ const p = parts[k] || { part: RO.nameOfLayout(s.layout), layout: s.layout };
86
+ return { part: p.part, layout: p.layout };
87
+ }
88
+
89
+ async function handle(req, res, url, ctx) {
90
+ const p = url.pathname;
91
+ const send = (r) => { ctx.json(res, r.body, r.status); return true; };
92
+ const sub = p.replace(/^\/api\/report-orders\/?/, '');
93
+ if (req.method === 'GET') {
94
+ const w = wsOf(ctx, url.searchParams.get('ws'));
95
+ if (!w) return send(bad(404, 'no such workspace'));
96
+ if (sub === 'preview') {
97
+ const o = RO.orderOf(text(url.searchParams.get('order')), RO.fold(RO.readLines(mineDir(ctx))).orders);
98
+ if (!o) return send(bad(404, 'no such order'));
99
+ const html = require('./report-design.js').sanitize(require('./report-slides.js').deckHtml(RO.previewDeck(o)));
100
+ res.writeHead && res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
101
+ if (res.end) res.end(html); else { res.status = 200; res.body = html; }
102
+ return true;
103
+ }
104
+ if (sub !== '') return send(bad(404, 'no such route'));
105
+ const d = draftOf(w, url.searchParams.get('path'));
106
+ if (d.error) return send(d.error);
107
+ return send({ status: 200, body: stateOf(ctx, w, d) });
108
+ }
109
+ if (!POSTS.includes(sub)) return send(bad(404, 'no such route'));
110
+ if (req.method !== 'POST') return send(bad(405, 'POST only'));
111
+ if (req.headers && req.headers['x-pmos-session']) return send(bad(403, 'a session cannot change a report: the PM presses on the page'));
112
+ const read = await ctx.readJson(req);
113
+ if (!read.ok) return send(bad(400, 'the body is not JSON'));
114
+ const body = read.body || {};
115
+ const g = guard.requirePagePress(req, body);
116
+ if (!g.ok) return send(bad(g.status, g.error));
117
+ const w = wsOf(ctx, text(body.ws));
118
+ if (!w) return send(bad(404, 'no such workspace'));
119
+ const dir = mineDir(ctx);
120
+
121
+ if (sub === 'rename') { const r = RO.rename(dir, text(body.id), text(body.name)); return send(r.ok ? { status: 200, body: { ok: true } } : bad(r.status, r.error)); }
122
+ if (sub === 'remove') { const r = RO.remove(dir, text(body.id)); return send(r.ok ? { status: 200, body: { ok: true } } : bad(r.status, r.error)); }
123
+
124
+ const d = draftOf(w, body.path);
125
+ if (d.error) return send(d.error);
126
+ if (sub === 'save') {
127
+ const report = readJson(d.P.report.abs), brief = readJson(d.P.brief.abs);
128
+ if (!report) return send(bad(409, 'this draft was written before the report engine: its slides have no layouts to save'));
129
+ const outline = [{ part: 'Title', layout: 'overview' }].concat(RO.partsOf(report, brief), [{ part: 'Sources', layout: 'sources' }]);
130
+ const r = RO.save(dir, { kind: 'order', name: text(body.name) || savedName(w, d.id, brief), outline, from: { report: d.id, slot: d.slot } });
131
+ return send(r.ok ? { status: 200, body: { ok: true, id: r.id } } : bad(r.status, r.error));
132
+ }
133
+ if (sub === 'slide') {
134
+ const n = Number(body.n);
135
+ if (!Number.isInteger(n) || n < 0) return send(bad(400, 'n is the slide, from 0'));
136
+ const sp = slidePart(d, n);
137
+ if (sp.error) return send(sp.error);
138
+ const r = RO.save(dir, { kind: 'slide', name: sp.part, outline: [sp], from: { report: d.id, slot: d.slot } });
139
+ return send(r.ok ? { status: 200, body: { ok: true, id: r.id, name: sp.part, layout: sp.layout } } : bad(r.status, r.error));
140
+ }
141
+ if (d.issued) return send(bad(409, `the ${d.slot} report is issued, so it is frozen. The next cycle starts from its order`));
142
+ if (sub === 'undo') { const r = RO.undo(w.root, d.id, d.slot); return send(r.ok ? { status: 200, body: { ok: true, order: r.order } } : bad(r.status, r.error)); }
143
+
144
+ // apply
145
+ const order = RO.orderOf(text(body.order), RO.fold(RO.readLines(dir)).orders);
146
+ if (!order) return send(bad(404, 'no such order'));
147
+ const brief = readJson(d.P.brief.abs);
148
+ if (brief && Array.isArray(brief.agreed_outline) && brief.agreed_outline.length) {
149
+ RO.setNext(w.root, d.id, order, d.slot, { by: g.actor });
150
+ return send({ status: 200, body: { ok: true, next: { id: order.id, name: order.name } } });
151
+ }
152
+ RO.snapshot(w.root, d.id, d.slot, { order });
153
+ const m = RE.make(w.root, d.id, { slot: d.slot, template: RL.templateOf(order.id) ? order.id : order });
154
+ if (!m.ok) { RO.undo(w.root, d.id, d.slot); return send(bad(m.status || 400, m.error)); }
155
+ const rel = d.P.md.rel;
156
+ const r = require('./routes-report-flow.js').ask(ctx, w, d.id, redraftWords(d.id, d.slot, rel, order), rel);
157
+ if (!r.ok) { RO.undo(w.root, d.id, d.slot); return send(bad(r.status || 409, r.error)); }
158
+ return send({ status: 200, body: { ok: true, redraft: { session: r.session, started: !!r.started, passed: !!r.passed }, order: { id: order.id, name: order.name } } });
159
+ }
160
+
161
+ module.exports = { prefixes, handle, stateOf, redraftWords };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.535.0",
3
+ "version": "0.536.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.535.0",
9
+ "version": "0.536.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/sdk": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.535.0",
3
+ "version": "0.536.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -310,3 +310,43 @@ button.rp-cc { cursor: pointer; }
310
310
  .rf-lk .rf-fix small { color: var(--cz-ink3); font-size: var(--fs-meta); line-height: var(--lh-meta); }
311
311
  .rf-tplx { padding: 10px 12px; border: 1px solid var(--cz-hair); border-radius: 14px; background: var(--cz-bluebg); }
312
312
  .rf-hv em.rf-rest { color: var(--cz-ink2); }
313
+
314
+ /* ── the orders of a draft (batch 27 item 16; canvas TpA, TpD, TpE; report-orders-ui.js) ── */
315
+ .ro-row { align-items: center; }
316
+ .ro-row .ro-ib.rp-tl { width: 30px; height: 30px; }
317
+ .ro-next { color: var(--cz-ink3); }
318
+ .ro-ass { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; padding: 10px 12px; border-radius: 14px; border: 1px solid var(--cz-blue); background: var(--cz-card); font-size: var(--fs-body); line-height: var(--lh-body); }
319
+ .ro-ass p { margin: 0; }
320
+ .ro-st { color: var(--cz-blueink); font-weight: var(--fw-strong); }
321
+ .vz .ro-note { margin: 0; font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--cz-ink3); }
322
+ .ro-pts { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; }
323
+ .ro-pt { display: inline-flex; padding: 2px 8px; border-radius: 8px; border: 1px solid var(--cz-hair); background: var(--cz-well); color: var(--cz-ink2); font-size: var(--fs-badge); line-height: var(--lh-badge); }
324
+ .ro-pt.add, .ro-pt.on { border-color: var(--cz-blue); color: var(--cz-blueink); font-weight: var(--fw-strong); background: var(--cz-card); }
325
+ .ro-pt.cut { border-style: dashed; color: var(--cz-ink3); text-decoration: line-through; background: none; }
326
+ .ro-acts { display: flex; justify-content: flex-end; align-items: center; gap: 6px; }
327
+ .ro-acts .rp-pri { margin: 0; width: 36px; height: 36px; }
328
+ .ro-gal { display: flex; flex-direction: column; gap: 10px; }
329
+ .ro-gh { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
330
+ .ro-gh small { display: block; color: var(--cz-ink3); font-size: var(--fs-meta); line-height: var(--lh-meta); }
331
+ .ro-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px; }
332
+ .ro-card { display: flex; flex-direction: column; gap: 6px; text-align: left; padding: 10px; border-radius: 14px; border: 1px solid var(--cz-hair); background: var(--cz-card); color: var(--cz-ink); cursor: pointer; font: inherit; }
333
+ .ro-card:hover { background: var(--cz-well); }
334
+ .ro-card:focus-visible, .ro-sl:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; }
335
+ .ro-card.now { border-color: var(--cz-blue); }
336
+ .ro-card small, .ro-sl small { color: var(--cz-ink3); font-size: var(--fs-meta); line-height: var(--lh-meta); }
337
+ .ro-cn { display: flex; align-items: center; gap: 6px; font-size: var(--fs-row); line-height: var(--lh-row); }
338
+ .ro-tag { display: inline-flex; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--cz-hair); font-size: var(--fs-badge); line-height: var(--lh-badge); color: var(--cz-ink2); font-weight: var(--fw-strong); }
339
+ .ro-tag.ro-star { color: var(--cz-blueink); border-color: var(--cz-blue); }
340
+ .ro-pics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
341
+ .ro-pic { display: block; position: relative; aspect-ratio: 16 / 9; border-radius: 4px; overflow: hidden; background: var(--cz-well); box-shadow: inset 0 0 0 1px var(--cz-hair); }
342
+ .ro-slides { display: flex; flex-wrap: wrap; gap: 10px; }
343
+ .ro-sl { display: flex; flex-direction: column; gap: 4px; width: 120px; padding: 0; border: 0; background: none; cursor: pointer; text-align: left; font: inherit; }
344
+ .ro-sl .ro-pics { grid-template-columns: 1fr; }
345
+ .ro-big { position: relative; aspect-ratio: 16 / 9; }
346
+ .ro-big .ro-pic { position: absolute; inset: 0; }
347
+ .ro-ren { display: flex; align-items: center; gap: 6px; }
348
+ .ro-ren input { flex: 1; min-width: 0; height: 34px; padding: 0 10px; border-radius: 10px; border: 1px solid var(--cz-hair); background: var(--cz-card); color: var(--cz-ink); font: inherit; }
349
+ .ro-ren .rp-pri { margin: 0; }
350
+ @media (max-width: 560px) { .ro-grid { grid-template-columns: 1fr; } }
351
+ .ro-ib.rp-pri { background: var(--cz-blue); color: var(--cz-onsolid); border-radius: 999px; }
352
+ .ro-ib.rp-pri:hover { background: var(--cz-blue); filter: brightness(1.08); color: var(--cz-onsolid); }
@@ -57,6 +57,7 @@ import { presentInRecording } from './cozy-rec-bridge.js'; // while a meeting
57
57
  import { placeholderHtml } from './cozy-placeholder.js';
58
58
  import { vzStyle } from './premium-vw-open.js';
59
59
  import { reviewHtml, chatOf as rfChatOf, ICON as RF_ICON } from './report-flow.js'; // Claude's review and the template icon (batch 22 item 14, canvas RfH2, RfH6)
60
+ import * as OU from './report-orders-ui.js'; // batch 27 item 16: the orders of a draft (canvas TpA, TpD, TpE)
60
61
  import { adler } from './cozy-adler.js'; // registers window.czViews; vzStyle adds the Viewer's sheet (cozy-vw.css)
61
62
  import { cycleGroups, cycleName, briefModel, briefGlanceHtml, briefConfirmHtml, briefWhyHtml, briefLooksHtml, paperModel, paperHtml, liveModel, liveHtml, presentHtml, dayMon, reportSummary,
62
63
  rpSumHtml, rpBarHtml, cadenceWords, goesOutWords, FORMATS, RP_ICONS } from './rp-parts.js';
@@ -73,7 +74,7 @@ const P = {
73
74
  warn: '<path d="M12 4l9 16H3zM12 10v4M12 17h.01"/>', bin: '<path d="M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13"/>', box: '<path d="M4 5h16v4H4zM5 9v10h14V9M10 13h4"/>', lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>',
74
75
  redo2: '<path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/>', undo: '<path d="M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3"/>', redo: '<path d="m15 14 5-5-5-5M20 9H9a5 5 0 0 0 0 10h3"/>',
75
76
  people: '<path d="M9 11a3.5 3.5 0 1 0 0-7a3.5 3.5 0 1 0 0 7M2.5 20a6.5 6.5 0 0 1 13 0M16 4.5a3.5 3.5 0 0 1 0 6.5M18 14a6 6 0 0 1 3.5 6"/>', list: '<path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/>',
76
- plus: '<path d="M12 5v14M5 12h14"/>', dl: '<path d="M12 4v11M7 10l5 5 5-5M5 20h14"/>', live: '<path d="M3 17l5-5 4 4 8-8M16 8h4v4"/>', layout: '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M4 10h16M10 10v10"/>',
77
+ plus: '<path d="M12 5v14M5 12h14"/>', grid: '<rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1"/><rect x="4" y="13" width="7" height="7" rx="1"/><rect x="13" y="13" width="7" height="7" rx="1"/>', mark: '<path d="M7 4h10v16l-5-4-5 4z"/>', dl: '<path d="M12 4v11M7 10l5 5 5-5M5 20h14"/>', live: '<path d="M3 17l5-5 4 4 8-8M16 8h4v4"/>', layout: '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M4 10h16M10 10v10"/>',
77
78
  theme: '<path d="M12 3a9 9 0 1 0 0 18c1 0 1.5-.8 1.5-1.6 0-1-.8-1.4-.8-2.4 0-1 .8-1.6 1.8-1.6H17a4 4 0 0 0 4-4c0-4.6-4-8.4-9-8.4z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10.5" cy="7.5" r="1"/><circle cx="15" cy="7.5" r="1"/>',
78
79
  };
79
80
  const ic = (k, n) => svg(P[k], n);
@@ -437,7 +438,7 @@ function deckHtml() {
437
438
  if (!L.length) return '<p class="vz-empty">This cycle has no slides. Read it as a page.</p>';
438
439
  const i = R.i = Math.max(0, Math.min(R.i, L.length - 1));
439
440
  return `<div class="vz-slide" tabindex="0" role="group" aria-roledescription="slide" aria-label="Slide ${i + 1} of ${L.length}: ${esc(L[i].h)}. Use the arrow keys to change slide." data-r="slide">${slideCard(L[i])}</div>`
440
- + `<div class="rp-nav">${ib('prev', ic('prev', 15), 'Previous slide', { off: i === 0 })}<span class="rp-count" aria-live="polite">Slide ${i + 1} of ${L.length}</span>${ib('next', ic('next', 15), 'Next slide', { off: i === L.length - 1 })}</div>`
441
+ + `<div class="rp-nav">${ib('prev', ic('prev', 15), 'Previous slide', { off: i === 0 })}<span class="rp-count" aria-live="polite">Slide ${i + 1} of ${L.length}</span>${ib('next', ic('next', 15), 'Next slide', { off: i === L.length - 1 })}${R.pyr && !R.cyc && R.ord && R.ord.engine && !R.edit ? ib('ord|slide', ic('mark', 15), 'Save this slide\'s layout to My slides', { cls: ' ro-slmark', off: R.busy }) : ''}</div>`
441
442
  + `<div class="vz-thumbs rp-thumbs" role="group" aria-label="Slides">`
442
443
  + L.map((x, j) => `<button type="button" class="vz-th rp-th${x.k === 'title' ? ' rp-tt' : ''}" data-r="go|${j}"${j === i ? ' aria-current="true"' : ''} aria-label="Slide ${j + 1}: ${esc(x.h)}" title="${j + 1} · ${esc(x.h)}"><span>${slideCard(x, true)}</span></button>`).join('')
443
444
  + '</div>';
@@ -473,11 +474,11 @@ function rfhHtml() {
473
474
  R.archived ? null : { act: 'share', label: 'Share', icon: 'share', exp: R.pop === 'share', pop: 'dialog' },
474
475
  { act: 'dlm', label: 'Download', icon: 'dl', exp: R.pop === 'dl' }] });
475
476
  const view = R.err ? `<p class="vz-msg" data-tone="bad" role="alert">The document could not be read: ${esc(R.err)}</p>` : !R.W ? '<p class="vz-empty" role="status">Reading the report.</p>'
476
- : R.view === 'doc' ? pageHtml() : deckHtml();
477
+ : R.gal ? galView() : (R.view === 'doc' ? pageHtml() : deckHtml()) + ordAssHtml();
477
478
  const conf = confirm && R.BF ? czSections([{ id: 'confirm', title: 'Please confirm', count: confirm, open: true, render: () => briefConfirmHtml(R.BF, { issued: R.issued, busy: R.busy }) + (R.bfMsg ? `<p class="vz-msg" role="status">${esc(R.bfMsg)}</p>` : '') }], { key: 'report' }) : '';
478
479
  const pri = R.archived ? { act: 'unarch|' + R.row.path, label: 'Bring back', icon: 'back' } : R.issued ? null : { act: 'issue', label: 'Issue it', icon: 'send' };
479
480
  const foot = pri ? `<div class="rp-foot"><button type="button" class="rp-pri" data-r="${esc(pri.act)}" aria-label="${esc(pri.label)}" title="${esc(pri.label)}"${R.busy ? ' disabled' : ''}><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${RP_ICONS[pri.icon]}"/></svg></button></div>` : '';
480
- return `<div class="vz-rpb rp-rfh">${R.conf ? confHtml() : ''}${chips}${st}${R.W && !R.err ? tools : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${conf}${askHtml()}${foot}</div>`;
481
+ return `<div class="vz-rpb rp-rfh">${R.conf ? confHtml() : ''}${chips}${st}${R.W && !R.err ? ordRowHtml() + tools : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${conf}${askHtml()}${foot}</div>`;
481
482
  }
482
483
  function bodyHtml() {
483
484
  if (!R.edit && rfhOn()) return rfhHtml();
@@ -488,8 +489,85 @@ function bodyHtml() {
488
489
  + (R.view === 'deck' ? '' : `<div class="vz-r1">${WAIT('Page editor')}</div>`) + '<div class="vz-ed" data-vz-ed></div></div>';
489
490
  }
490
491
  const view = R.err ? `<p class="vz-msg" data-tone="bad" role="alert">The document could not be read: ${esc(R.err)}</p>` : !R.W ? '<p class="vz-empty" role="status">Reading the report.</p>'
491
- : R.view === 'deck' ? deckHtml() : R.view === 'live' ? liveView() : pageHtml();
492
- return `<div class="vz-rpb">${R.conf ? confHtml() : R.W ? sumHtml() : ''}${R.W && !R.err ? cutsHtml() : ''}${R.msg ? `<p class="vz-msg" role="status" data-tone="${R.msg.tone || ''}">${esc(R.msg.text)}</p>` : ''}${R.edit ? '' : waitHtml()}${R.W && !R.err ? barHtml() : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${R.W && !R.err ? foldsHtml() : ''}${askHtml()}</div>`;
492
+ : R.gal ? galView() : (R.view === 'deck' ? deckHtml() : R.view === 'live' ? liveView() : pageHtml()) + ordAssHtml();
493
+ return `<div class="vz-rpb">${R.conf ? confHtml() : R.W ? sumHtml() + ordRowHtml() : ''}${R.W && !R.err ? cutsHtml() : ''}${R.msg ? `<p class="vz-msg" role="status" data-tone="${R.msg.tone || ''}">${esc(R.msg.text)}</p>` : ''}${R.edit ? '' : waitHtml()}${R.W && !R.err ? barHtml() : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${R.W && !R.err ? foldsHtml() : ''}${askHtml()}</div>`;
494
+ }
495
+ /* ── the orders of a draft (batch 27 item 16; canvas TpA, TpD, TpE; report-orders-ui.js, GET /api/report-orders) ── */
496
+ const ORD_HIDE = 'pmos-report-orders-hidden';
497
+ function ordHidden() { try { return new Set(JSON.parse(localStorage.getItem(ORD_HIDE) || '[]')); } catch { return new Set(); } }
498
+ const PICS = new Map(); // order id → its slides drawn small with placeholder words (GET /api/report-orders/preview), 'wait' while read
499
+ function ordOpts() {
500
+ return { esc, ic, slideCard, busy: R.busy, issued: R.issued, hidden: R.ordHide, now: R.pyr || R.cyc ? slides() : null,
501
+ pics: (id) => { const x = PICS.get(R.ws + '|' + id); return Array.isArray(x) ? x : null; } };
502
+ }
503
+ const ordRowHtml = () => (R.ord ? OU.rowHtml(R.ord, ordOpts()) : '');
504
+ const ordAssHtml = () => (R.ord && !R.edit ? OU.assHtml(R.ord, ordOpts()) : '');
505
+ function galView() { readPics(); return OU.galHtml(R.ord, R.gal, ordOpts()); }
506
+ /** The pictures of the orders the gallery shows, each read once and kept (they hold placeholder words only). */
507
+ function readPics() {
508
+ const me = R, S = R.ord; if (!S || !S.ok) return;
509
+ const ids = R.gal && R.gal.sel ? [R.gal.sel] : S.orders.map((o) => o.id).concat((S.slides || []).map((x) => x.id));
510
+ for (const id of ids) {
511
+ const k = R.ws + '|' + id; if (id === 'now' || PICS.has(k)) continue;
512
+ PICS.set(k, 'wait');
513
+ textOf('/api/report-orders/preview?' + q({ ws: R.ws, order: id })).then((h) => { const x = splitRender(h); PICS.set(k, x.slides.map((y) => ({ k: 'frame', h: y.h, html: y.html, css: x.style }))); })
514
+ .catch(() => PICS.set(k, [])).then(() => { if (R === me && R.gal) draw(); });
515
+ }
516
+ }
517
+ const dropPics = () => { for (const k of [...PICS.keys()]) if (/\|mine-/.test(k)) PICS.delete(k); };
518
+ let ordT = null;
519
+ async function readOrders() {
520
+ const me = R; if (!R || !R.row.path) return;
521
+ const j = await Promise.resolve(pmGet('/api/report-orders?' + q({ ws: R.ws, path: R.row.path }))).catch(() => null);
522
+ if (R !== me) return;
523
+ R.ord = j && j.ok ? j : null;
524
+ if (R.gal && !R.ord) R.gal = null;
525
+ draw();
526
+ // while the report's session writes the draft again, read the state until it lands (the file watch redraws the slides)
527
+ clearTimeout(ordT);
528
+ if (R.ord && R.ord.undo && R.ord.undo.state === 'drafting') ordT = setTimeout(() => { if (R === me) readOrders(); }, 6000);
529
+ }
530
+ async function ordAct(k, v) {
531
+ const me = R, S = R.ord;
532
+ if (k === 'gal') { R.gal = { sel: null }; R.pop = null; return draw(); }
533
+ if (k === 'close') { R.gal = null; return draw(); }
534
+ if (k === 'back') { R.gal = { sel: null }; return draw(); }
535
+ if (k === 'open') { R.gal = { sel: v, i: 0 }; return draw(); }
536
+ if (k === 'at') { if (R.gal) R.gal.i = +v; return draw(); }
537
+ if (k === 'hide') { R.ordHide.add(OU.hideKey(S, v)); try { localStorage.setItem(ORD_HIDE, JSON.stringify([...R.ordHide].slice(-200))); } catch { /* kept for this visit */ } return draw(); }
538
+ if (k === 'ren') { const o = S && [...S.orders, ...(S.slides || [])].find((x) => x.id === v); if (R.gal && o) R.gal.ren = o.name; draw(); const f = bodyEl() && bodyEl().querySelector('#roName'); if (f) f.focus(); return; }
539
+ if (R.busy) return;
540
+ const post = async (sub, body) => { R.busy = true; draw(); const r = await pmPost('/api/report-orders/' + sub, { ws: R.ws, ...body }).catch(() => null); if (R !== me) return null; R.busy = false; return r || { ok: false, error: 'the page could not reach PMOS' }; };
541
+ if (k === 'apply') {
542
+ const r = await post('apply', { path: R.row.path, order: v }); if (!r) return;
543
+ if (!r.ok) return say('The order was not applied: ' + r.error + '.', 'bad');
544
+ R.gal = null;
545
+ await readOrders(); if (R !== me) return;
546
+ return say(r.next ? `From the next cycle this report starts in the ${r.next.name} order. Today's draft stays as agreed.` : `Claude is writing this draft again in the ${r.order.name} order. Undo puts this one back, for one day.`);
547
+ }
548
+ if (k === 'undo') {
549
+ const r = await post('undo', { path: R.row.path }); if (!r) return;
550
+ if (!r.ok) return say('Nothing was undone: ' + r.error + '.', 'bad');
551
+ await reread(); if (R !== me) return; await readOrders(); if (R !== me) return;
552
+ return say('The draft is back as it was before the redraft.');
553
+ }
554
+ if (k === 'save' || k === 'slide') {
555
+ const r = await post(k, k === 'save' ? { path: R.row.path } : { path: R.row.path, n: R.i }); if (!r) return;
556
+ if (!r.ok) return say('Nothing was saved: ' + r.error + '.', 'bad');
557
+ R.ordHide.add(OU.hideKey(S, 'edit')); dropPics(); await readOrders(); if (R !== me) return;
558
+ return say(k === 'save' ? 'Saved to My orders. It is first in the orders, marked Mine. Only its parts and slide layouts are kept.' : `Saved to My slides: ${r.name}, its layout only.`);
559
+ }
560
+ if (k === 'renok') {
561
+ const r = await post('rename', { id: v, name: (R.gal && R.gal.ren) || '' }); if (!r) return;
562
+ if (!r.ok) return say('The name did not change: ' + r.error + '.', 'bad');
563
+ if (R.gal) R.gal.ren = null; dropPics(); return readOrders();
564
+ }
565
+ if (k === 'del') {
566
+ const r = await post('remove', { id: v }); if (!r) return;
567
+ if (!r.ok) return say('It was not deleted: ' + r.error + '.', 'bad');
568
+ R.gal = { sel: null }; dropPics(); await readOrders(); if (R !== me) return;
569
+ return say('Deleted from your orders. No report changed.');
570
+ }
493
571
  }
494
572
  /* ── Claude's review (batch 22 item 14, canvas RfH2): the checks PMOS runs on the draft, as ✦ fixes the report's own session makes ── */
495
573
  function rvHtml() {
@@ -605,9 +683,10 @@ function wire(b) {
605
683
  e.preventDefault(); act(t.dataset.r.split('|'), t);
606
684
  };
607
685
  b.onchange = (e) => { if (e.target.dataset.r === 'stime') R.sch.time = e.target.value || '09:00'; };
608
- b.oninput = (e) => { if (e.target.dataset.r === 'shpass') R.shr.pass = e.target.value; };
686
+ b.oninput = (e) => { if (e.target.dataset.r === 'shpass') R.shr.pass = e.target.value; if (e.target.dataset.r === 'ord-name' && R.gal) R.gal.ren = e.target.value; };
609
687
  b.onkeydown = (e) => {
610
688
  if (e.key === 'Escape' && R.pop) { e.preventDefault(); e.stopPropagation(); R.pop = null; draw(); return; }
689
+ if (e.key === 'Escape' && R.gal) { e.preventDefault(); e.stopPropagation(); R.gal = R.gal.sel ? { sel: null } : null; draw(); return; }
611
690
  if (!R.edit && R.view === 'deck' && /^Arrow(Left|Right)$/.test(e.key) && !e.target.closest('input')) { const L = slides().length, j = R.i + (e.key === 'ArrowRight' ? 1 : -1); if (j >= 0 && j < L) { e.preventDefault(); R.i = j; draw(); const s = bodyEl().querySelector('.vz-slide'); if (s) s.focus(); } }
612
691
  };
613
692
  /* a phone swipes the slide (RpPhone "swipe for the next") */
@@ -634,6 +713,7 @@ export function fit(root) { root.querySelectorAll('.vz-fr').forEach((c) => { if
634
713
  const say = (text, tone) => { R.msg = { text, tone }; draw(); const me = R, m = R.msg; if (tone !== 'bad') setTimeout(() => { if (R === me && R.msg === m) { R.msg = null; draw(); } }, 9000); };
635
714
  async function act([k, v], t) {
636
715
  if (k === 'rv-do' || k === 'rv-no' || k === 'rv-all') return rvAct(k, v);
716
+ if (k === 'ord') return ordAct(v, t.dataset.r.split('|').slice(2).join('|'));
637
717
  if (k === 'pop') { R.pop = v || null; return draw(); }
638
718
  if (k === 'cut') return cutTo(v);
639
719
  if (k === 'bf') return briefAct(v, t.dataset.r.split('|').slice(2).join('|'));
@@ -1030,7 +1110,7 @@ export async function openReport(row, hand = {}) {
1030
1110
  if (R) present(false);
1031
1111
  const me = R = { row, ws: hand.ws || '', id: c.cadence, name: hand.name || row.name || c.cadence || 'Report', audience: hand.audience || '', issued: row.state === 'issued', archived: !!row.archived,
1032
1112
  text: '', err: '', W: null, view: 'deck', BF: null, bfRaw: null, bfMsg: '', LV: null, blank: false, i: 0, cyc: null, pyr: null, setup: undefined, pop: null, conf: false, checks: null, issueErr: '', busy: false, ask: '', askErr: '', edit: false, msg: null,
1033
- sch: schedPick(null), schErr: '', shr: null, rv: null, rvHide: new Set(), rvSent: new Set(), shares: null, more: [], hand, cuts: hand.cuts ? { on: 'team', team: null, sponsors: null, err: '' } : null, cycles: (hand.cycles || []).slice(), onIssue: hand.onIssue || null, onChange: hand.onChange || null, vid: 'report:' + row.path };
1113
+ sch: schedPick(null), schErr: '', shr: null, ord: null, gal: null, ordHide: ordHidden(), rv: null, rvHide: new Set(), rvSent: new Set(), shares: null, more: [], hand, cuts: hand.cuts ? { on: 'team', team: null, sponsors: null, err: '' } : null, cycles: (hand.cycles || []).slice(), onIssue: hand.onIssue || null, onChange: hand.onChange || null, vid: 'report:' + row.path };
1034
1114
  const head = me.head = () => ({ id: me.vid, kind: 'Report' + (me.audience ? ' · ' + me.audience : ''), title: me.name, icon: me.view === 'doc' ? 'doc' : 'report', full: true, url: viewUrl(row, me.ws), more: me.more,
1035
1115
  // the template behind one icon (batch 22 item 14, canvas RfH1): format, readers, when it goes out, and its history
1036
1116
  presses: me.setup ? [{ act: 'chat', label: 'Talk to Claude about this report', icon: RF_ICON.chat }, { act: 'template', label: 'Template: format, readers, look, sources', icon: RF_ICON.tune }] : [],
@@ -1048,6 +1128,7 @@ export async function openReport(row, hand = {}) {
1048
1128
  if (R !== me) return me;
1049
1129
  readBrief(); // the Brief tab's count shows at once
1050
1130
  readReview();
1131
+ readOrders();
1051
1132
  if (hand.say) me.msg = { text: hand.say };
1052
1133
  if (vCur() && vCur().id === me.vid && (vCur().title !== me.name || me.audience || me.setup)) { me.reopen = true; vOpen(head()); me.reopen = false; } // the name and audience are known now
1053
1134
  draw();
@@ -1067,7 +1148,7 @@ async function watchFile(me) {
1067
1148
  if (R !== me || !h) continue;
1068
1149
  // a session writes the draft in many steps: read it when the file has been still for one more check, so a burst of writes is one redraw
1069
1150
  if (seen && h !== seen) { seen = h; pending = true; continue; }
1070
- if (pending) { pending = false; await reread(); if (R === me && !me.edit && !me.pop) { me.fromFile = true; draw(); me.fromFile = false; readReview(); } }
1151
+ if (pending) { pending = false; await reread(); if (R === me && !me.edit && !me.pop) { me.fromFile = true; draw(); me.fromFile = false; readReview(); readOrders(); } }
1071
1152
  seen = h;
1072
1153
  }
1073
1154
  }
@@ -0,0 +1,115 @@
1
+ /* pm/report-orders-ui.js — the orders of a report draft in the report Viewer (batch 27 item 16; main canvas TpA, TpD, TpE). Pure html builders;
2
+ * premium-report-read.js holds the state and the presses (data-r="ord|…") and reads GET /api/report-orders (lib/routes-report-orders.js).
3
+ * The app says "order", never "template".
4
+ *
5
+ * rowHtml(S, o) the order line in the summary: the order's name as a chip (zero is silence for the standard order), the grid icon that
6
+ * opens the gallery (a draft), the bookmark (save as my order), Undo while its day lasts, and the next cycle's order
7
+ * assHtml(S, o) the one ✦ under the slide: Claude's pick (✕ · grid · ✓ redraft) | "Redrafting in …" | after the PM edited an order,
8
+ * "You changed this order … Save it as yours?" (✕ · bookmark). Nothing to say → ''.
9
+ * galHtml(S, G, o) the gallery: Now, then Mine, then the seven PMOS orders, each with the pictures of its first four layouts and its slide
10
+ * count; My slides; a pressed card shows its slides, its parts, what it adds or drops, and ✓ (Redraft in this order, or
11
+ * Use from the next cycle on an agreed outline). Rename and Delete on Mine only.
12
+ * S = the GET answer · G = { sel, i, ren } · o = { esc, ic, slideCard, pics(id) → [slide] | null, now: [slide], busy, hidden: Set, issued }
13
+ */
14
+
15
+ const keyOf = (S, k) => k + ':' + (S && S.slot) + ':' + ((S && S.pick && S.pick.id) || (S && S.now && S.now.id) || '');
16
+ export const hideKey = keyOf;
17
+ const words = (L) => (L.length <= 1 ? L.join('') : L.slice(0, -1).join(', ') + ' and ' + L[L.length - 1]);
18
+ const ALWAYS = new Set(['overview', 'sources']);
19
+ /** "adds Why, Scope and People; drops The week" (pure). */
20
+ export function diffWords(d) {
21
+ const a = (d && d.adds) || [], r = (d && d.drops) || [];
22
+ if (!a.length && !r.length) return 'Same parts as your draft.';
23
+ return 'Compared with your draft: ' + [a.length ? 'adds ' + words(a) : '', r.length ? 'drops ' + words(r) : ''].filter(Boolean).join('; ') + '.';
24
+ }
25
+ const btn = (o, act, icon, label, cls = '') => `<button type="button" class="rp-tl vz-hv ro-ib${cls}" data-r="${o.esc(act)}" aria-label="${o.esc(label)}" title="${o.esc(label)}"${o.busy ? ' disabled' : ''}>${o.ic(icon, 16)}</button>`;
26
+
27
+ /** The order line under the summary. */
28
+ export function rowHtml(S, o) {
29
+ if (!S || !S.ok) return '';
30
+ const e = o.esc, n = S.now || {}, out = [];
31
+ const named = n.kind !== 'standard';
32
+ if (!o.issued) out.push(`<button type="button" class="rp-mc ro-chip" data-r="ord|gal" aria-label="${e(named ? n.name + ': see the orders' : 'See the orders')}" title="See the orders">${o.ic('grid', 14)}${named ? e(n.name) + (n.edited ? ', edited' : '') : ''}</button>`);
33
+ else if (named) out.push(`<span class="rp-mc ro-chip">${o.ic('grid', 14)}${e(n.name)}</span>`);
34
+ if (S.next) out.push(`<span class="rp-mc ro-next">Next cycle: ${e(S.next.name)}</span>`);
35
+ if (S.engine) out.push(btn(o, 'ord|save', 'mark', 'Save as my order'));
36
+ if (S.undo && S.undo.state !== 'drafting' && !o.issued) out.push(btn(o, 'ord|undo', 'undo', 'Undo the redraft'));
37
+ return `<div class="rp-mcs ro-row">${out.join('')}</div>`;
38
+ }
39
+
40
+ const parts = (o, list, adds, cls) => list.filter((p) => !ALWAYS.has(p.layout)).map((p) => `<span class="ro-pt${adds && adds.includes(p.part) ? ' add' : ''}${cls || ''}">${o.esc(p.part)}</span>`).join('');
41
+ /** The one ✦ under the slide. */
42
+ export function assHtml(S, o) {
43
+ if (!S || !S.ok || o.issued) return '';
44
+ const e = o.esc, h = o.hidden || new Set();
45
+ const U = S.undo;
46
+ if (U && U.state !== 'done' && U.order) {
47
+ const late = U.state === 'late';
48
+ return `<div class="ro-ass" role="status"><p><b class="ro-st" aria-hidden="true">✦</b> ${late ? 'The redraft in the ' + e(U.order.name) + ' order has not landed yet.' : 'Redrafting in the ' + e(U.order.name) + ' order.'}</p>`
49
+ + `<p class="ro-note">${late ? 'Undo puts the draft back as it was.' : 'It takes a few minutes. Every reading stays; the new draft shows here when it lands.'}</p></div>`;
50
+ }
51
+ if (S.pick && !h.has(keyOf(S, 'pick'))) {
52
+ const ord = (S.orders || []).find((x) => x.id === S.pick.id);
53
+ if (!ord) return '';
54
+ return `<div class="ro-ass" role="group" aria-label="Claude's pick"><p><b class="ro-st" aria-hidden="true">✦</b> The ${e(ord.name)} order fits this report's question.</p>`
55
+ + `<p class="ro-pts">${parts(o, ord.parts, S.pick.adds)}${(S.pick.drops || []).map((x) => `<span class="ro-pt cut">${e(x)}</span>`).join('')}</p>`
56
+ + `<p class="ro-note">Blue is added${(S.pick.drops || []).length ? ', struck through is dropped' : ''}. Every reading stays.</p>`
57
+ + `<div class="ro-acts">${btn(o, 'ord|hide|pick', 'x', 'Not now')}${btn(o, 'ord|open|' + ord.id, 'grid', 'See it with the other orders')}${btn(o, 'ord|apply|' + ord.id, 'check', 'Redraft in this order', ' rp-pri')}</div></div>`;
58
+ }
59
+ const n = S.now || {};
60
+ if (S.engine && n.kind === 'order' && n.edited && !h.has(keyOf(S, 'edit'))) {
61
+ const c = n.changes || {}, said = [(c.drops || []).length ? 'dropped ' + words(c.drops.map((x) => '“' + x + '”')) : '', (c.adds || []).length ? 'added ' + words(c.adds.map((x) => '“' + x + '”')) : ''].filter(Boolean).join(', ');
62
+ return `<div class="ro-ass" role="group" aria-label="Save your order"><p><b class="ro-st" aria-hidden="true">✦</b> You changed the ${e(n.name)} order${said ? ': ' + e(said) : ''}. Save it as yours?</p>`
63
+ + `<p class="ro-pts">${parts(o, n.parts, c.adds)}</p><p class="ro-note">It saves the parts and their slide layouts. Not the numbers or the words.</p>`
64
+ + `<div class="ro-acts">${btn(o, 'ord|hide|edit', 'x', 'Not now')}${btn(o, 'ord|save', 'mark', 'Save as my order', ' rp-pri')}</div></div>`;
65
+ }
66
+ return '';
67
+ }
68
+
69
+ /** Up to four pictures of an order's layouts (real slides, drawn small). Until they are read: grey boxes. */
70
+ function pics(o, L, n = 4) {
71
+ if (!L) return `<span class="ro-pics" aria-hidden="true">${'<span class="ro-pic ro-wait"></span>'.repeat(n)}</span>`;
72
+ return `<span class="ro-pics" aria-hidden="true">${L.slice(0, n).map((s) => `<span class="ro-pic">${o.slideCard(s, true)}</span>`).join('')}</span>`;
73
+ }
74
+ const more = (n) => n + (n === 1 ? ' slide' : ' slides') + (n > 4 ? ' · +' + (n - 4) + ' more' : '');
75
+ function cardHtml(S, o, c) {
76
+ const e = o.esc, mark = c.now ? '<span class="ro-tag">Now</span>' : c.mine ? '<span class="ro-tag">Mine</span>' : S.pick && S.pick.id === c.id ? '<span class="ro-tag ro-star" aria-label="Claude\'s pick">✦</span>' : '';
77
+ return `<button type="button" class="ro-card${c.now ? ' now' : ''}" data-r="ord|open|${e(c.id)}"${c.now ? ' aria-current="true"' : ''}>${pics(o, c.now ? o.now : o.pics(c.id))}`
78
+ + `<span class="ro-cn">${mark}<b>${e(c.name)}</b></span><small>${more(c.n)}</small></button>`;
79
+ }
80
+ /** The gallery (TpD), or one order pressed open. */
81
+ export function galHtml(S, G, o) {
82
+ if (!S || !S.ok) return '<p class="vz-empty" role="status">Reading the orders.</p>';
83
+ const e = o.esc;
84
+ const close = `<button type="button" class="rp-tl vz-hv ro-ib" data-r="ord|close" aria-label="Close the orders" title="Close">${o.ic('x', 16)}</button>`;
85
+ if (G.sel) return detailHtml(S, G, o);
86
+ const n = S.now || {};
87
+ const head = S.now.kind === 'agreed'
88
+ ? `<p class="ro-note">Your readers agreed this order${n.since ? ' when the ' + e(n.since) + ' report went out' : ''}. A pick here is used from the next cycle; today's draft stays as agreed.</p>`
89
+ : S.pick ? `<p class="ro-note"><b class="ro-st" aria-hidden="true">✦</b> The ${e((S.orders.find((x) => x.id === S.pick.id) || {}).name || '')} order fits this report's question. Press a card to see it, or pick another.</p>` : '';
90
+ const cards = [{ id: 'now', now: true, name: n.name, n: (o.now || []).length || n.parts.length }].concat(S.orders.filter((x) => x.mine), S.orders.filter((x) => !x.mine));
91
+ const sl = (S.slides || []).length ? `<p class="rp-lbl">My slides</p><div class="ro-slides">${S.slides.map((s) => `<button type="button" class="ro-sl" data-r="ord|open|${e(s.id)}">${pics(o, o.pics(s.id), 1)}<small>${e(s.name)}</small></button>`).join('')}</div>` : '';
92
+ return `<div class="ro-gal" role="region" aria-label="Orders"><div class="ro-gh"><b>Orders</b>${close}</div>${head}`
93
+ + `<div class="ro-grid">${cards.map((c) => cardHtml(S, o, c)).join('')}</div>${sl}`
94
+ + `<p class="ro-note">An order is a default order of slides. The writer keeps every reading and may add or drop a part.</p></div>`;
95
+ }
96
+ function detailHtml(S, G, o) {
97
+ const e = o.esc;
98
+ const isNow = G.sel === 'now';
99
+ const slide = (S.slides || []).find((x) => x.id === G.sel);
100
+ const ord = isNow ? { id: 'now', name: S.now.name, n: (o.now || []).length, parts: S.now.parts } : slide ? { id: slide.id, name: slide.name, n: 1, parts: [{ part: slide.part, layout: slide.layout }], mine: true, slide: true } : S.orders.find((x) => x.id === G.sel);
101
+ if (!ord) return '';
102
+ const L = isNow ? o.now : o.pics(ord.id);
103
+ const i = Math.max(0, Math.min(G.i || 0, L ? L.length - 1 : 0));
104
+ const big = L && L[i] ? `<div class="vz-slide ro-big">${o.slideCard(L[i], false)}</div>` : '<div class="vz-slide ro-big"><span class="ro-pic ro-wait"></span></div>';
105
+ const strip = L && L.length > 1 ? `<div class="vz-thumbs rp-thumbs ro-strip" role="group" aria-label="Slides">${L.map((s, j) => `<button type="button" class="vz-th rp-th" data-r="ord|at|${j}"${j === i ? ' aria-current="true"' : ''} aria-label="Slide ${j + 1}: ${e(s.h)}"><span>${o.slideCard(s, true)}</span></button>`).join('')}</div>` : '';
106
+ const agreed = S.now.kind === 'agreed';
107
+ const sub = isNow ? 'Now' : ord.slide ? 'My slide · its layout only' : (ord.mine ? 'Mine · ' : '') + more(ord.n).split(' · ')[0] + (ord.sub ? ' · ' + ord.sub : '');
108
+ const diff = isNow || ord.slide ? '' : `<p class="ro-note">${e(diffWords(ord))}${agreed ? ' From the next cycle: today\'s draft stays as agreed.' : ' Every reading stays.'}</p>`;
109
+ const ren = G.ren ? `<div class="ro-ren"><label class="vz-lab" for="roName">Name</label><input id="roName" data-r="ord-name" value="${e(G.ren)}" maxlength="80"><button type="button" class="rp-tl vz-hv ro-ib rp-pri" data-r="ord|renok|${e(ord.id)}" aria-label="Keep this name" title="Keep this name"${o.busy ? ' disabled' : ''}>${o.ic('check', 16)}</button></div>` : '';
110
+ const acts = [btn(o, 'ord|back', 'prev', 'Back to the orders')];
111
+ if (ord.mine) acts.push(btn(o, 'ord|del|' + ord.id, 'bin', 'Delete'), btn(o, 'ord|ren|' + ord.id, 'pen', 'Rename'));
112
+ if (!isNow && !ord.slide && !o.issued) acts.push(btn(o, 'ord|apply|' + ord.id, 'check', agreed ? 'Use from the next cycle' : 'Redraft in this order', ' rp-pri'));
113
+ return `<div class="ro-gal ro-one" role="region" aria-label="${e(ord.name)}"><div class="ro-gh"><span><b>${e(ord.name)}</b><small>${e(sub)}</small></span></div>${big}${strip}`
114
+ + `<p class="ro-pts">${ord.parts.map((p, j) => `<span class="ro-pt${!isNow && j === i ? ' on' : ''}">${e(p.part)}</span>`).join('')}</p>${diff}${ren}<div class="ro-acts">${acts.join('')}</div></div>`;
115
+ }
@@ -33,7 +33,7 @@ Procedure:
33
33
  5. Choose the outline for these readers and this question. Any sections, any titles, any order.
34
34
  - A recurring report keeps `agreed_outline`, the sections of its last issued issue. Add a section only when a finding needs one, drop one only when it would be empty, and say so in `outline_note` (one line).
35
35
  - `earlier_outline` is the shape of an issue written before the engine (the old fixed frame). It is an example, not an agreement: keep what served the readers.
36
- - `template` is set when the PM started the report from a report template (`pmos report make <id> --template <name>`; `pmos report library --json` lists them). Its `outline` is a default order of parts, each with its slide layout: use it as the starting outline, retitle each part as a finding, and add or drop a part when the findings say so. An agreed outline wins over a template.
36
+ - `template` is set when the PM started the report from a report template (`pmos report make <id> --template <name>`; `pmos report library --json` lists them). Its `outline` is a default order of parts, each with its slide layout: use it as the starting outline, retitle each part as a finding, and add or drop a part when the findings say so. An agreed outline wins over a template, except when `template.chosen` is true: the PM picked that order for this cycle, so use it, and read `earlier_outline` as an example only. The template can also be an order the PM saved (its id starts with `mine-`) or one the PM applied from the report's orders; it reads the same way.
37
37
  - Each section title is a finding, not a label: "Sign-up stalls before submit", not "Sign-up". Reading only the titles tells the story.
38
38
  - Shapes other reports took, as examples and never a menu: a decision (the lean · the options and what each costs · what could reverse it · your call); a watch (today against yesterday and the week · the drop that moved most · why); a stakeholder update (are the dates on track · what each team needs and by when · open decisions · what could stop it); a review (the summary for decision · the outcomes by segment · the challenges and answers · method); did-it-work (what was promised · what we got · the verdict).
39
39
  6. Write `<slot>.report.json` (in a scratch file, then `pmos report write <id> --from <file>`):
@@ -0,0 +1,11 @@
1
+ {
2
+ "_comment": "Batch 27 item 16: apply an order to a report draft (the ✦ pick, the orders gallery's state and pictures, the redraft in the report's own session, Undo for one day, the next cycle's order on an agreed outline) and my saved orders and slides (per-user ledger). Node only.",
3
+ "modules": [
4
+ "report-orders.test.js",
5
+ "report-orders-page.test.mjs"
6
+ ],
7
+ "seconds": {
8
+ "report-orders.test.js": 2,
9
+ "report-orders-page.test.mjs": 1
10
+ }
11
+ }