create-pmos 0.532.0 → 0.533.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.532.0",
3
+ "version": "0.533.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.533.0",
37
+ "at": "2026-11-04T19:35:12.134Z",
38
+ "f": 4,
39
+ "prs": [
40
+ {
41
+ "n": 1249,
42
+ "t": "Component slots on report slides",
43
+ "s": "Component slots on report slides",
44
+ "k": "new"
45
+ }
46
+ ]
47
+ },
35
48
  {
36
49
  "v": "0.532.0",
37
50
  "at": "2026-11-04T19:01:35.763Z",
@@ -406,19 +419,6 @@
406
419
  "k": "fix"
407
420
  }
408
421
  ]
409
- },
410
- {
411
- "v": "0.513.0",
412
- "at": "2026-11-03T15:46:14.696Z",
413
- "f": 4,
414
- "prs": [
415
- {
416
- "n": 1203,
417
- "t": "Typography roles with Plus Jakarta Sans, two weights, four sizes",
418
- "s": "Typography roles with Plus Jakarta Sans, two weights, four sizes",
419
- "k": "new"
420
- }
421
- ]
422
422
  }
423
423
  ]
424
424
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@c8e1d796
1
+ kernel-source: pmos@2120ce89
2
2
  assembled: 2026-11-04
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-11-04T19:01:32Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T19:35:08Z"}
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-04T19:01:32Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T19:35:08Z"}
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-04T19:01:32Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T19:35:08Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "v": "0.533.0",
4
+ "at": "2026-11-04T19:35:12.134Z",
5
+ "f": 4,
6
+ "prs": [
7
+ {
8
+ "n": 1249,
9
+ "t": "Component slots on report slides",
10
+ "s": "Component slots on report slides",
11
+ "k": "new"
12
+ }
13
+ ]
14
+ },
2
15
  {
3
16
  "v": "0.532.0",
4
17
  "at": "2026-11-04T19:01:35.763Z",
@@ -94,6 +94,8 @@ pmos report issue biweekly 2026-10-16
94
94
 
95
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
96
 
97
+ A slide can also hold a PMOS component instead of a layout: today, the KR metric tile of the Insights page, as a line, bars, a meter or a number. PMOS draws it from the KR metric record when the report is written and keeps a dated snapshot with the report, so an old issue shows the figures of its day. The slide says when the snapshot was taken, and the PowerPoint file draws the same tile.
98
+
97
99
  Every command accepts `--root <dir>` to name the repository. `draft` and `list` also accept `--json`.
98
100
 
99
101
  ## The window
@@ -208,7 +208,7 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
208
208
  }
209
209
 
210
210
  /** The report engine's verbs (lib/report-engine.js). An agent session and the PM may run them; issuing stays the PM's (`pmos report issue`). */
211
- function engineVerb(verb, { pos, flags, root, json, say, fail, isTTY, env, stdin, now }) {
211
+ async function engineVerb(verb, { pos, flags, root, json, say, fail, isTTY, env, stdin, now }) {
212
212
  const RE = require('./report-engine.js');
213
213
  const id = pos[0];
214
214
  if (!id || !RE.ID_RE.test(id)) { say(`usage: pmos report ${verb} <id> … (the id is lower case letters, digits and dashes, for example scarlet-assessment)`); return 2; }
@@ -252,7 +252,16 @@ function engineVerb(verb, { pos, flags, root, json, say, fail, isTTY, env, stdin
252
252
  }
253
253
  if (verb === 'write') {
254
254
  const r = readFrom(); if (r.error) return fail(r.error, 2);
255
- const w = RE.write(root, id, day, r.body, { actor: who, now: clock });
255
+ // component slots: PMOS reads each record and draws its component now, and the snapshot is kept with the report (lib/report-slots.js)
256
+ const snapshots = {};
257
+ const secs = r.body && Array.isArray(r.body.sections) ? r.body.sections : [];
258
+ for (const [i, s] of secs.entries()) {
259
+ if (!s || s.layout !== 'component' || !s.data || typeof s.data !== 'object') continue;
260
+ const sn = await require('./report-slots.js').snapshotOf(root, s.data, { now: clock || Date.now() });
261
+ if (!sn.ok) return fail(`sections[${i}] (component): ${sn.error}`);
262
+ snapshots[i] = sn.snapshot;
263
+ }
264
+ const w = RE.write(root, id, day, r.body, { actor: who, now: clock, snapshots });
256
265
  if (!w.ok) {
257
266
  if (json) { say(JSON.stringify({ ok: false, error: w.error, checks: w.checks || null, none: !!w.none })); return 1; }
258
267
  say(`✗ ${w.error}`);
@@ -703,7 +703,8 @@ function deckOf(report, { brief = null, readings = null, look = null } = {}) {
703
703
  const data = RS.mapLayout(s.layout, s.data, r);
704
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
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) });
706
+ const snap = s.layout === 'component' && isObj(data.snapshot) ? data.snapshot : null;
707
+ 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: snap ? `Source: PMOS ${snap.kind === 'kr' ? 'KR metric record' : 'record'} ${snap.ref}, last reading ${full(snap.readAt)}` : srcOf(ids) });
707
708
  continue;
708
709
  }
709
710
  // 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
@@ -795,7 +796,7 @@ function make(root, id, { slot, ask, readers, sources, template, add, gaps, addF
795
796
  }
796
797
 
797
798
  /** Steps 4–6: the writer's report.json, checked, then the document rendered. A report with no answer, or a figure that is not a reading, is refused. */
798
- function write(root, id, slot, report, { now = Date.now() } = {}) {
799
+ function write(root, id, slot, report, { now = Date.now(), snapshots = {} } = {}) {
799
800
  const abs = path.resolve(String(root));
800
801
  let P; try { P = paths(abs, id, slot); } catch (e) { return refuse(400, e.message); }
801
802
  if (isIssued(abs, id, slot)) return refuse(409, `${id} ${slot} is issued; it is frozen`);
@@ -805,7 +806,10 @@ function write(root, id, slot, report, { now = Date.now() } = {}) {
805
806
  const pre = checkReport(report, { brief, readings });
806
807
  const hardFail = pre.filter((c) => c.hard && !c.pass);
807
808
  if (hardFail.length) return refuse(422, hardFail.map((c) => `${c.id}: ${c.reason}`).join(' · '), { checks: pre });
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() };
809
+ // a component slot's snapshot comes from PMOS (opts.snapshots, by section index: the CLI reads the record and draws it), never from the writer
810
+ const snaps = isObj(snapshots) ? snapshots : {};
811
+ const slotOf = (s, i) => (s.layout === 'component' && isObj(s.data) ? { ...s, data: { ...Object.fromEntries(Object.entries(s.data).filter(([k]) => k !== 'snapshot')), ...(snaps[i] ? { snapshot: snaps[i] } : {}) } } : s);
812
+ const clean = { ...report, sections: report.sections.map((s, i) => ({ ...slotOf(s, i), 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() };
809
813
  const md = renderMarkdown(clean, { brief, readings, now });
810
814
  const checks = checkReport(clean, { brief, readings, markdown: md });
811
815
  writeJson(P.report.abs, clean);
@@ -512,6 +512,22 @@ function b2Parts(s, { shape, text }) {
512
512
  for (const [a, b] of d.edges) { const p = pos.get(a); const q = pos.get(b); const x1 = p.x + nw; const y1 = p.y + nh / 2; const x2 = q.x; const y2 = q.y + nh / 2; shape(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1), { geom: 'line', line: '#9FB8E0', lw: 2, flip: y2 < y1 }); }
513
513
  d.nodes.forEach((n) => { const p = pos.get(n.id); const t = tn(n.tone); const [bg, ink] = t === 'bad' ? PILLC.bad : t === 'watch' || t === 'late' ? PILLC.watch : ['#FFFFFF', C.ink]; shape(p.x, p.y, nw, nh, { geom: 'roundRect', adj: round(14, nw, nh), fill: bg, line: bg === '#FFFFFF' ? '#DCDDE0' : null });
514
514
  text(p.x + 8, p.y + 18, nw - 16, 20, n.label, o({ size: 15, bold: true, color: ink, align: 'ctr' })); });
515
+ } else if (L === 'component' && d.snapshot && d.snapshot.kind === 'kr') {
516
+ // PMOS's KR metric tile, from the snapshot kept with the report: the value, its readings as bars from zero, the target line, the date
517
+ const sn = d.snapshot; const bw = Math.min(W, 760); const bh = 300;
518
+ shape(X, y, bw, bh, { geom: 'roundRect', adj: round(18, bw, bh), fill: C.well });
519
+ text(X + 22, y + 18, bw - 200, 20, sn.title, o({ size: 13, bold: true, color: C.ink2 }));
520
+ if (sn.chip) pill(X + bw - 22 - Math.min(160, 18 + sn.chip.length * 7), y + 16, sn.tone, sn.chip);
521
+ text(X + 22, y + 42, bw - 44, 56, sn.valueText || String(sn.value), o({ size: 40, bold: true, color: C.ink }));
522
+ text(X + 22, y + 100, bw - 44, 18, ['on ' + RS.dmy(sn.readAt), sn.targetText].filter(Boolean).join(' · '), o({ size: 13, color: C.ink3 }));
523
+ const ser = (sn.series || []).slice(-24); const hi = Math.max(...ser.map((p) => p.v), sn.target != null ? sn.target : 0) || 1; const px = X + 22; const pw = bw - 44; const py = y + 130; const ph = 130;
524
+ const cw = pw / Math.max(1, ser.length);
525
+ ser.forEach((p, i) => { const h = Math.max(1, (Math.max(0, p.v) / hi) * ph); shape(px + i * cw + cw * 0.2, py + ph - h, cw * 0.6, h, { fill: i === ser.length - 1 ? C.ink : '#C9CBCF' }); });
526
+ shape(px, py + ph, pw, 1, { fill: C.hair });
527
+ if (sn.target != null && sn.target >= 0) shape(px, py + ph - (sn.target / hi) * ph, pw, 1, { line: C.ink3, lw: 1, dash: true, name: 'Target' });
528
+ if (ser.length) { text(px, py + ph + 6, 200, 16, RS.dmy(ser[0].at), o({ size: 11, color: C.ink3 })); text(px + pw - 200, py + ph + 6, 200, 16, RS.dmy(ser[ser.length - 1].at), o({ size: 11, color: C.ink3, align: 'r' })); }
529
+ const when = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})$/.exec(String(sn.at)); const label = `Snapshot ${when ? RS.dmy(when[1]) + ' ' + when[2] : sn.at} · KR metric record ${sn.ref}`;
530
+ pill(X, y + bh + 12, 'none', label, Math.min(W, 30 + label.length * 6.6));
515
531
  } else plain();
516
532
  if (ins) {
517
533
  shape(902, y, 1, BOT - y, { fill: C.hair, name: 'Insight rule' });
@@ -33,7 +33,7 @@
33
33
  */
34
34
 
35
35
  const LAYOUTS = Object.freeze(['overview', 'tiles', 'ledger', 'timeline', 'compare', 'lists', 'risks', 'ranked', 'decisions', 'quotes', 'actions',
36
- 'agenda', 'heatmap', 'network', 'schedule', 'donut', 'bubbles', 'people', 'statement', 'sources', 'minutes', 'focus', 'next', 'blocks']);
36
+ 'agenda', 'heatmap', 'network', 'schedule', 'donut', 'bubbles', 'people', 'statement', 'sources', 'minutes', 'focus', 'next', 'blocks', 'component']);
37
37
  const TONES = Object.freeze(['ok', 'watch', 'late', 'bad', 'info', 'none']);
38
38
  const MONS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
39
39
  const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
@@ -91,12 +91,13 @@ const SPEC = {
91
91
  focus: { gate: { obj: { title: 't', text: 't' } }, steps: { max: 4, min: 1, item: { label: 't', title: 't', text: 't' } } },
92
92
  next: { date: 'date', time: 't', items: { max: 6, min: 1, item: { length: 't', text: 't' } } },
93
93
  blocks: { blocks: { max: 12, min: 1, raw: true } },
94
+ component: {},
94
95
  };
95
96
  const REQUIRED = {
96
97
  overview: ['cols'], tiles: ['tiles'], ledger: ['rows'], timeline: ['rows'], compare: ['left', 'right'], lists: ['left', 'right'], risks: ['rows'],
97
98
  ranked: ['rows'], decisions: ['rows'], quotes: ['quotes'], actions: ['rows'], agenda: ['rows'], heatmap: ['cols', 'rows'], network: ['nodes', 'edges'],
98
99
  schedule: ['cols', 'rows'], donut: ['parts'], bubbles: ['points'], people: ['people'], statement: ['text'], sources: [], minutes: ['rows'],
99
- focus: ['steps'], next: ['items'], blocks: ['blocks'],
100
+ focus: ['steps'], next: ['items'], blocks: ['blocks'], component: [],
100
101
  };
101
102
  const TEXT_MAX = 400;
102
103
 
@@ -133,6 +134,7 @@ function checkSpec(spec, data, at) {
133
134
  function layoutError(name, data) {
134
135
  if (!LAYOUTS.includes(name)) return `layout is one of ${LAYOUTS.join(', ')}`;
135
136
  if (!isObj(data)) return `data is the ${name} layout's object`;
137
+ if (name === 'component') return require('./report-slots.js').slotError(data);
136
138
  for (const k of REQUIRED[name]) if (data[k] == null || (Array.isArray(data[k]) && !data[k].length) || (isStr(data[k]) && !data[k].trim())) return `the ${name} layout needs ${k}`;
137
139
  const e = checkSpec(SPEC[name], data, 'data');
138
140
  if (e) return e;
@@ -154,7 +156,7 @@ function walk(spec, data, fn) {
154
156
  return out;
155
157
  }
156
158
  /** A copy of the data with fn applied to every text the writer wrote (the blocks layout is the engine's own and is left as it is). */
157
- const mapLayout = (name, data, fn) => (SPEC[name] && name !== 'blocks' ? walk(SPEC[name], data, fn) : data);
159
+ const mapLayout = (name, data, fn) => (SPEC[name] && name !== 'blocks' && name !== 'component' ? walk(SPEC[name], data, fn) : data);
158
160
  /** Every text the writer wrote in the data. */
159
161
  function textsOfLayout(name, data) { const out = []; mapLayout(name, data, (t) => { out.push(t); return t; }); return out; }
160
162
 
@@ -188,6 +190,7 @@ function linesOf(name, d) {
188
190
  case 'focus': return [d.gate ? join(d.gate.title, d.gate.text) : ''].concat(L(d.steps, (s) => join(s.label, s.title, s.text))).filter(Boolean);
189
191
  case 'next': return [join(d.date ? longDate(d.date) : '', d.time)].concat(L(d.items, (x) => join(x.length, x.text))).filter(Boolean);
190
192
  case 'blocks': return L(d.blocks, (b) => (b.kind === 'p' ? b.text : b.kind === 'list' ? b.items.join(' · ') : b.kind === 'table' ? b.rows.map((r) => r.join(' | ')).join(' · ') : b.kind === 'bars' ? b.rows.map((r) => `${r.label}: ${r.text}`).join(' · ') : '')).filter(Boolean);
193
+ case 'component': return d.snapshot ? require('./report-slots.js').snapshotLines(d.snapshot) : [`The ${d.kind || ''} component ${d.ref || ''} is drawn when the report is written`];
191
194
  default: return [];
192
195
  }
193
196
  }
@@ -302,6 +305,13 @@ const BODY = {
302
305
  return `<div class="next-row">${when}<div class="fl"><div class="lb pb6">Draft agenda</div>${items}</div></div>`;
303
306
  },
304
307
  blocks(d) { return `<div class="blk">${d.blocks.map(blockHtml).join('')}</div>`; },
308
+ component(d) {
309
+ // PMOS's own component, as the snapshot PMOS took when the report was written (lib/report-slots.js); the writer never supplies this markup
310
+ const s = d.snapshot;
311
+ if (!isObj(s) || !isStr(s.html)) return `<div class="slot"><p class="ink2">This component is drawn from its record when the report is written.</p></div>`;
312
+ const when = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})$/.exec(str(s.at));
313
+ return `<div class="slot">${s.html}<span class="snap">Snapshot ${esc(when ? `${dmy(when[1])} ${when[2]}` : s.at)} · ${s.kind === 'kr' ? 'KR metric record' : 'record'} ${esc(s.ref)}</span></div>`;
314
+ },
305
315
  };
306
316
  const initials = (name) => str(name).split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join('');
307
317
  const dueText = (ymd, today) => { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(str(ymd)); return m ? `${DAYS[new Date(ms(ymd)).getUTCDay()].slice(0, 3)} ${+m[3]} ${MONS[+m[2] - 1]}${today && m[1] !== str(today).slice(0, 4) ? ' ' + m[1] : ''}` : str(ymd); };
@@ -574,6 +584,7 @@ width:1280px;height:720px;position:relative;overflow:hidden;box-sizing:border-bo
574
584
  .rs .kr,.rs .lrow,.rs .tb,.rs .btn,.rs .sb{transition:background-color .2s ease,box-shadow .2s ease}
575
585
  .rs .kr:hover,.rs .lrow:hover{background:rgba(244,243,240,.7)}
576
586
  .rs .tb:hover{box-shadow:0 2px 8px -2px rgba(42,46,53,.3)}
587
+ ${require('./report-slots.js').CSS}
577
588
  @media (prefers-reduced-motion:reduce){.rs *{animation:none!important;transition:none!important}}
578
589
  @media print{.rs *{animation:none!important;transition:none!important}.rs .ctl{display:none}}`;
579
590
 
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+ /* report-slots.js — component slots on report slides (batch 27 item 14, PR 3; brief option B, "Component slots"). A slide can hold one of
3
+ * PMOS's own components, drawn by PMOS's own code from a PMOS record, never by the writer. The writer names the component and the record;
4
+ * PMOS reads the record when the report is written and keeps a DATED SNAPSHOT with the report, so an old issue stays true.
5
+ *
6
+ * KINDS → the components a slot can hold today (each proven in the real reader; the rest were dropped)
7
+ * slotError(data) → null | a sentence: what is wrong with a section's { kind, ref, form } (pure)
8
+ * snapshotOf(root, data, { now }) → Promise<{ ok, snapshot } | { ok: false, error }>: reads the record, draws the component
9
+ * snapshotLines(snapshot) → [line]: the component as plain lines, for the document and the PowerPoint file (pure)
10
+ * CSS → the component's own styles, scoped to a slot (the workspace tokens, written out)
11
+ *
12
+ * A snapshot is { kind, ref, form, title, at, html, value, unit, readAt, series: [{ at, v }], target, chip, tone }. `html` is PMOS's markup:
13
+ * the report sanitizer still runs over the whole deck. The writer cannot pass a snapshot: lib/report-engine.js write() drops one it is given.
14
+ *
15
+ * WHICH COMPONENTS (the brief's proof rule: any component that fails the proof in the real reader is dropped, not faked):
16
+ * kr the KR metric tile of the Insights page (public/pm/premium-chart-draw.js tileHtml, from lib/insights-premium.js chartOf), in its four
17
+ * forms: line, bars, meter, number. Pure strings; runs in Node; proven inside the reader's empty sandbox.
18
+ * Dropped for now: the Board Timeline (bd-span.js needs the board's row model, its CSS and a mounted live copy; the library's own timeline
19
+ * layout stands in), Document and Viewer (they mount DOM; they cannot be drawn into a sandboxed slide).
20
+ */
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const KINDS = Object.freeze(['kr']);
25
+ const FORMS = Object.freeze(['line', 'bars', 'meter', 'number']);
26
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
27
+ const str = (v) => (typeof v === 'string' ? v : v == null ? '' : String(v));
28
+
29
+ function slotError(d) {
30
+ if (!isObj(d)) return 'data is the component\'s object: { kind, ref, form }';
31
+ for (const k of Object.keys(d)) if (!['kind', 'ref', 'form', 'snapshot'].includes(k)) return `data.${k} is not part of a component slot; it takes kind, ref and form`;
32
+ if (!KINDS.includes(d.kind)) return `data.kind is one of ${KINDS.join(', ')} (the components PMOS can draw on a slide)`;
33
+ if (!/^[A-Za-z0-9_.:-]{1,100}$/.test(str(d.ref))) return 'data.ref is the id of the record the component shows (a KR metric record id)';
34
+ if (d.form != null && !FORMS.includes(d.form)) return `data.form is one of ${FORMS.join(', ')}`;
35
+ return null;
36
+ }
37
+
38
+ function recordsDir(root) {
39
+ try { const p = require(require('./repo-script.js').repoScript('signals/layout-paths.js')).pathsFor(root).recordsDir; if (p && fs.existsSync(p)) return p; } catch { /* fall through */ }
40
+ return path.join(root, 'planning', 'records');
41
+ }
42
+ const stamp = (now) => { const d = new Date(now); return `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 16)}`; };
43
+
44
+ /** Read the record and draw the component. Async: the drawing code is the browser's own module, imported unchanged. */
45
+ async function snapshotOf(root, d, { now = Date.now() } = {}) {
46
+ const e = slotError(d); if (e) return { ok: false, error: e };
47
+ if (d.kind === 'kr') {
48
+ let krs = [];
49
+ try { krs = require(require('./repo-script.js').repoScript('records/index.js')).list(recordsDir(root), 'kr-metrics'); } catch (x) { return { ok: false, error: `the KR metric records could not be read: ${str(x && x.message).slice(0, 120)}` }; }
50
+ const k = krs.find((x) => x.rid === d.ref);
51
+ if (!k) return { ok: false, error: `there is no KR metric record ${d.ref}. The records: ${krs.map((x) => x.rid).slice(0, 12).join(', ') || 'none'}` };
52
+ const today = new Date(now).toISOString().slice(0, 10);
53
+ const o = require('./routes-c3-strategy.js').outcomeOf(k, today);
54
+ const c = require('./insights-premium.js').chartOf(o);
55
+ if (!c) return { ok: false, error: `the KR metric ${d.ref} has fewer than two readings, so it has no chart yet` };
56
+ // Imported as source, not by path: the file has no imports of its own, and a path import makes newer Node print a module-type warning.
57
+ const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'pm', 'premium-chart-draw.js'));
58
+ const draw = await import('data:text/javascript;base64,' + src.toString('base64'));
59
+ const form = d.form || 'line';
60
+ const html = draw.tileHtml({ ...c, form }).replace(/\stabindex="0"/g, '');
61
+ const last = o.readings[o.readings.length - 1];
62
+ const tone = c.chipKind === 'ok' ? 'ok' : c.chipKind === 'warn' ? 'watch' : c.chipKind === 'bad' ? 'bad' : 'none';
63
+ return { ok: true, snapshot: { kind: 'kr', ref: d.ref, form, title: c.title, at: stamp(now), html, value: last.v, unit: o.unit || '', readAt: last.at,
64
+ series: o.readings.map((r) => ({ at: r.at, v: r.v })), target: o.target, chip: c.chip, tone, valueText: c.v, targetText: c.targetTxt } };
65
+ }
66
+ return { ok: false, error: `no component ${d.kind}` };
67
+ }
68
+
69
+ /** The component as lines: the document and the PowerPoint file (dates in full). */
70
+ function snapshotLines(s) {
71
+ if (!isObj(s)) return [];
72
+ const RS = require('./report-slides.js');
73
+ if (s.kind === 'kr') return [[s.title, s.valueText, s.targetText, s.chip].filter(Boolean).join(' · '), `Snapshot of the KR metric record ${s.ref}, last reading ${RS.dmy(s.readAt)}, taken ${RS.dmy(s.at.slice(0, 10))} ${s.at.slice(11)}`];
74
+ return [];
75
+ }
76
+
77
+ /* The tile's own rules (public/pm/premium.css), scoped to a slot, with the workspace tokens they read written out. No child combinator:
78
+ * the report sanitizer escapes `>` in CSS. */
79
+ const CSS = `.rs .slot{--well:#F4F3F0;--ink:#2A2E35;--ink-2:#4A4F58;--ink-3:#646A74;--hair:rgba(42,46,53,.08);--ok:#3E6E49;--ember:#B8860B;--ember-ink:#8A5A00;--bad:#A8322A;--fs-display:44px;--lh-display:1.1;--fs-meta:13px;--lh-meta:1.4;--fs-badge:12px;--lh-badge:1.2;--fw-strong:600;display:flex;flex-direction:column;gap:10px;flex:1;min-height:0}
80
+ .rs .slot .mt1{background:var(--well);border-radius:18px;padding:18px 22px 16px;display:grid;gap:4px;align-content:start}
81
+ .rs .slot .mt1 span{font-size:var(--fs-meta);color:var(--ink-3);line-height:var(--lh-meta)}
82
+ .rs .slot .mt1 b{font-size:var(--fs-display);font-weight:600;letter-spacing:-.03em;line-height:1.15}
83
+ .rs .slot .mt1 em{font-style:normal;font-size:var(--fs-meta);color:var(--ink-3);line-height:var(--lh-meta)}
84
+ .rs .slot .mt1.warn em{color:var(--ember-ink)}.rs .slot .mt1.bad em{color:var(--bad)}.rs .slot .mt1.ok em{color:var(--ok)}
85
+ .rs .slot .mlab{display:flex;align-items:center;gap:8px;font-weight:600;color:var(--ink-2)}
86
+ .rs .slot .mchip{font-style:normal;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;background:#fff;color:var(--ink-2)}
87
+ .rs .slot .mchip.ok{color:var(--ok)}.rs .slot .mchip.warn{color:var(--ember-ink)}
88
+ .rs .slot .mchg{font-size:var(--fs-meta)}.rs .slot .mchg.good{color:var(--ok)}.rs .slot .mchg.bad{color:var(--bad)}
89
+ .rs .slot .mfoot{display:flex;justify-content:space-between;font-size:12px;color:var(--ink-3)}.rs .slot .mfoot i{font-style:normal}
90
+ .rs .slot svg{width:100%;height:auto;display:block;margin-top:8px;overflow:visible}
91
+ .rs .slot .mch .ar{fill:rgba(42,46,53,.08);stroke:none}
92
+ .rs .slot .mch .ln2{fill:none;stroke:var(--ink);stroke-width:1.8;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke}
93
+ .rs .slot .mch .tg{stroke:var(--ink-3);stroke-dasharray:3 3;stroke-width:1;vector-effect:non-scaling-stroke}
94
+ .rs .slot .mch .cx{display:none}
95
+ .rs .slot .mch .dt{fill:var(--ink)}.rs .slot .mch.ok .dt{fill:var(--ok)}.rs .slot .mch.warn .dt{fill:var(--ember)}.rs .slot .mch.bad .dt{fill:var(--bad)}
96
+ .rs .slot .mfm .mval{font-size:var(--fs-display);line-height:var(--lh-display)}
97
+ .rs .slot .mfm .nspk{width:100%;height:auto;display:block;margin-top:8px;overflow:visible}
98
+ .rs .slot .mfm .nspk path{fill:none;stroke:var(--ink-2);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}
99
+ .rs .slot .bplot{position:relative;display:flex;align-items:stretch;gap:2px;height:160px;margin-top:10px;border-bottom:1px solid var(--hair)}
100
+ .rs .slot .bw{flex:1 1 0;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;min-width:0;position:relative}
101
+ .rs .slot .bw i{display:block;width:min(24px,64%);border-radius:4px 4px 0 0}
102
+ .rs .slot .bw i.bg{background:rgba(42,46,53,.2)}.rs .slot .bw i.bk{background:var(--ink)}
103
+ .rs .slot .bw .bl{font-size:12px!important;font-weight:600;margin-bottom:3px;white-space:nowrap;letter-spacing:0}
104
+ .rs .slot .btg{position:absolute;left:0;right:0;border-top:1px dashed var(--ink-3);height:0}
105
+ .rs .slot .mtrk{position:relative;height:12px;border-radius:999px;background:rgba(42,46,53,.1);margin:16px 0 8px}
106
+ .rs .slot .mfil{position:absolute;inset:0 auto 0 0;border-radius:999px;background:var(--ink)}.rs .slot .mfil.ok{background:var(--ok)}.rs .slot .mfil.warn{background:var(--ember)}
107
+ .rs .slot .mtgt{position:absolute;top:-5px;bottom:-5px;width:2px;background:var(--ink);border-radius:2px}
108
+ .rs .slot .mnote,.rs .slot .mwhy{font-size:var(--fs-meta);color:var(--ink-3)}
109
+ .rs .snap{align-self:flex-start;display:inline-flex;align-items:center;height:22px;padding:0 9px;border-radius:999px;font-size:12px;font-weight:600;background:var(--well);color:var(--ink-3)}`;
110
+
111
+ module.exports = { KINDS, FORMS, slotError, snapshotOf, snapshotLines, CSS };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.532.0",
3
+ "version": "0.533.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.532.0",
9
+ "version": "0.533.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.532.0",
3
+ "version": "0.533.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",
@@ -39,6 +39,7 @@ Procedure:
39
39
  6. Write `<slot>.report.json` (in a scratch file, then `pmos report write <id> --from <file>`):
40
40
  `{"title":"…{{r:…}}…","answer":{"text":"…","supports":["Label: …{{r:…}}…"]},"sections":[{"id":"…","title":"…","blocks":[{"kind":"p","text":"…"},{"kind":"list","items":["…"]},{"kind":"table","columns":["…"],"rows":[["…"]]},{"kind":"bars","title":"…","rows":[["label","{{r:id}}"]]},{"kind":"options","options":[{"name":"…","text":"…"}]},{"kind":"quote","text":"…"}]}],"ask":{"text":"…","who":"…","by":"YYYY-MM-DD","options":["…"]},"beyond":{"why":"…","so_what":"…","now_what":"…","since":"…","would_change":"…","unknown":["…"],"else":["…"],"next_question":"…"},"questions":[{"question":"<a question of the brief>","answered_in":"<section id or answer>","gap":"<why it cannot be answered>"}],"outline_note":"…"}`.
41
41
  - A section can take a slide layout from the report library instead of blocks: `{"title":"…","layout":"<name>","data":{…},"insight":{"head":"…","text":"…","points":[{"tone":"ok|watch|late|bad|info|none","text":"…"}],"note":"…"},"eyebrow":"…","sub":"…"}`. The names: overview, tiles, ledger, timeline, compare, lists, risks, ranked, decisions, quotes, actions, agenda, heatmap, network, schedule, donut, bubbles, people, statement, sources, minutes, focus, next. Run `pmos report library` to see each one with its data; `pmos report write` names a field that does not fit. Pick the layout that fits the finding, never to fill a slide. Every text in `data` and `insight` is checked like any other: a figure is a reading token. A bar's `share`, a `target` mark and a bubble's `x`, `y` and `size` are numbers from 0 to 1: take them from the readings. The insight is optional; leave it out when the slide says it already.
42
+ - A section can also hold a PMOS component, drawn by PMOS from a PMOS record: `{"title":"…","layout":"component","data":{"kind":"kr","ref":"<KR metric record id>","form":"line|bars|meter|number"}}`. Today the one component is `kr`, the KR metric tile of the Insights page; the record needs two readings or more. Do not write the drawing: `pmos report write` reads the record, draws the tile and keeps a dated snapshot with the report, so an old issue keeps the figures of its day. It refuses a record that is not there and names the records that are.
42
43
  - `ask` is null when the readers have nothing to decide. When it is a call with no better option on the evidence, give the options and say "your call".
43
44
  - BEYOND THE ASK. Answer the readers' next three questions before they ask: why (the cause, from the evidence), so what (the effect on the target and its date), now what (the recommendation, who, by), since the last issue (what moved, and each carried ask), what would change it (the one or two readings that would flip the recommendation), what we don't know (the gaps, counted, never padded), what else is here (at most two findings from a named source the readers did not ask about, when their question depends on them) and the next question (the split they will ask for next). Each part cites a reading. Leave a part out when no reading backs it, and leave out a part the brief's rules turn off.
44
45
  - `questions`: map each question of the brief after the first to the section that answers it, or give the gap.