create-pmos 0.239.0 → 0.241.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-report-structure-real-test.js +211 -0
  7. package/workspace/c3-report-structure-test.js +382 -0
  8. package/workspace/guide/briefing.md +31 -3
  9. package/workspace/guide/index.md +2 -0
  10. package/workspace/guide/report-design.md +4 -0
  11. package/workspace/guide/report-structure.md +91 -0
  12. package/workspace/guide/reports-dashboards.md +1 -1
  13. package/workspace/guide/reports-set-up.md +2 -0
  14. package/workspace/lib/charts-store.js +124 -0
  15. package/workspace/lib/headless.js +17 -3
  16. package/workspace/lib/needs-providers.js +1 -0
  17. package/workspace/lib/report-structure-needs.js +64 -0
  18. package/workspace/lib/report-structure.js +515 -0
  19. package/workspace/lib/route-modules.js +3 -1
  20. package/workspace/lib/routes-brief-page.js +56 -5
  21. package/workspace/lib/routes-mentions.js +37 -0
  22. package/workspace/lib/routes-report-setups.js +11 -6
  23. package/workspace/lib/routes-report-structure.js +619 -0
  24. package/workspace/package-lock.json +2 -2
  25. package/workspace/package.json +1 -1
  26. package/workspace/public/pm/briefing-deck.js +1 -0
  27. package/workspace/public/pm/c3-brief-mentions.js +136 -0
  28. package/workspace/public/pm/c3-brief-needs.js +23 -0
  29. package/workspace/public/pm/c3-brief-pulse.js +1 -0
  30. package/workspace/public/pm/c3-brief-summary.js +139 -0
  31. package/workspace/public/pm/c3-demo-brief-page.js +20 -4
  32. package/workspace/public/pm/c3-demo-rd.js +4 -0
  33. package/workspace/public/pm/c3-design-studio.js +7 -3
  34. package/workspace/public/pm/c3-report-setup.js +3 -0
  35. package/workspace/public/pm/c3-report-structure.css +40 -0
  36. package/workspace/public/pm/c3-report-structure.js +455 -0
  37. package/workspace/public/pm/c3-reports.css +2 -0
  38. package/workspace/public/pm/c3-reports.js +22 -3
  39. package/workspace/public/pm/c3-shell.js +34 -2
  40. package/workspace/public/pm/console3.css +12 -0
  41. package/workspace/server.js +38 -9
  42. package/workspace/skills/report-source-check.skill +45 -0
  43. package/workspace/skills/report-structure-dry.skill +51 -0
  44. package/workspace/skills/report-structure.skill +61 -0
  45. package/workspace/skills/report-template.skill +10 -0
  46. package/workspace/test-files.json +2 -0
  47. package/workspace/test-lists/report-structure-backend.json +5 -0
  48. package/workspace/test-lists/report-structure-ui.json +11 -0
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-report-structure-test.js — the Structure step of the report studio, UI slice (initiative report-structure; mockup 43-reportstructure;
4
+ * API contract planning/prd/report-structure-contract.md). A real server and a real Chromium drive /c3.html?tab=rep; the routes of the
5
+ * contract (/api/report-structure*, /api/charts) and the report reads are answered by Playwright route interception, so the page is
6
+ * tested against the contract alone, not against the backend (c3-report-structure-real-test.js does that).
7
+ *
8
+ * entry the Ask PMOS answer (Shape it with PMOS sets the report up, then opens Structure), a set-up row (Shape the report, before Design
9
+ * how it looks) and the step bar of the Design studio (1 Structure, 2 Design)
10
+ * turn a first turn shows PMOS working, lands S1 with its questions as answer chips; a chip fills the box; a refine turn lands S2; the
11
+ * strip lists both; S1 can be shown again and Go back to S1 posts a revert
12
+ * plan What it answers; Sources in From PMOS and Outside PMOS with their state tags and actions (Open, Test pull, Check it works, How to
13
+ * connect); Charts tagged New chart / From Charts; a row opens to question, Used in, what PMOS reads, why and the test pull;
14
+ * Outline with chart chips and PMOS notes
15
+ * chart Discuss this chart fills the box; Remove it posts a remove and lands the next version
16
+ * source Add a source offers every From PMOS and Outside PMOS kind; a From PMOS source and a Chrome source are added; the Chrome check
17
+ * shows its steps with Needs you and Check again, and passes on the second check
18
+ * options Other structures: three side by side, the recommended first
19
+ * dry Try it with real data runs one job and shows headline, values, flags, could not get, options
20
+ * accept Use this structure posts the shown version; the setup sheet shows the saved structure with Refine the structure
21
+ * charts the Charts tab lists a data chart beside the built-ins with its source and Used in
22
+ * phone at 400 wide none of it scrolls sideways
23
+ * Set RX_SHOTS=<dir> to save screenshots at 1440 and 400 wide. No script error and no failed request may be raised. */
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const { spawn } = require('child_process');
27
+ const { findBrowser } = require('./lib/find-browser');
28
+ const T = require('./test-run').configure({ name: 'c3-report-structure' });
29
+
30
+ const BASE = T.BASE;
31
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
32
+ const until = async (fn, ms = 10000, step = 100) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
33
+ const results = [];
34
+ const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 400) : ''}`); };
35
+ let srv = null; let browser = null;
36
+ const finish = async (code) => {
37
+ if (browser) await browser.close().catch(() => {});
38
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
39
+ T.killTmux();
40
+ const bad = results.filter((x) => !x.cond).length;
41
+ console.log(`\n${results.length - bad}/${results.length} c3-report-structure assertions passed`);
42
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
43
+ };
44
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
45
+
46
+ /* ── the stub: the contract's routes, as a small state machine ── */
47
+ const SETUP = { id: 'app-monthly', name: 'MySPH app performance, monthly', source: 'work', output: 'doc', status: 'active', schedule: { cadence: 'monthly', dayOfMonth: 3, time: '09:00' }, label: 'Monthly on the 3rd 09:00, draft only', next_draft: '2026-11-03', last_issued: null, cycles: 0, copy: null };
48
+ const PROPOSAL = { name: 'MySPH app performance, monthly', purpose: 'Where to invest next quarter.', audience: 'Leadership', source: 'work', output: 'doc', schedule: { cadence: 'monthly', dayOfMonth: 3, time: '09:00' }, sources: ['Google Analytics'], sections: ['Headline'], learned_from: null, fixes: [] };
49
+ const src = (id, kind, name, o) => ({ id, kind, name, via: '', ref: '', use: '', state: 'reached', group: ['connector', 'link', 'upload', 'person', 'chrome', 'computer'].includes(kind) ? 'outside' : 'pmos', ...o });
50
+ const GA = src('ga', 'connector', 'Google Analytics 4: MySPH web + app', { via: 'Read through your Supermetrics connector', use: 'Test pull Mon 12 Oct 22:31. App streams iOS and Android, Sep 2026.' });
51
+ const BOARD = src('board', 'pmos-level', 'Board: MySPH', { via: 'What the report covers', use: 'Its initiatives, releases, key results and risks.' });
52
+ const DASH = src('dash', 'dashboard', 'Dashboard: MySPH health', { via: '2 of its 6 charts used', use: 'New users is already a chart there.' });
53
+ const PAST = src('past', 'past-report', 'Past report: none yet', { via: 'From the second cycle', use: 'From November, PMOS reads the October report.', state: 'later' });
54
+ const BQ = src('bq', 'connector', 'BigQuery: GA4 export', { via: 'Not connected', use: 'Optional. Only needed for retention older than 14 months.', state: 'not-connected' });
55
+ const CH_NEW = { id: 'cpa', chart: null, name: 'Cost per new user', source: 'ga', from: 'GA4 cost', how: 'Paid spend divided by new users from paid campaigns', compare: 'Previous month', flag: 'Up 15% or more', question: 'What do new users cost us?', query: 'GA4 Data API: metric advertiserAdCost; filter sessionMedium = cpc', why: 'Spend explains the drop in new users.' };
56
+ const CH_OLD = { id: 'new-users', chart: 'new-users', name: 'New users', source: 'ga', from: 'GA4 newUsers', how: 'Month total, app streams only', compare: 'Previous month', flag: 'Down 10% or more', question: 'Are we still bringing people in?', query: 'GA4 Data API: metric newUsers', why: 'The top of the funnel.' };
57
+ const CH_RET = { id: 'ret', chart: null, name: 'D30 retention', source: 'ga', from: 'GA4 cohort exploration', how: 'Share of each monthly cohort still active on day 30', compare: 'The last 3 cohorts', flag: 'Below 25%', question: 'Do they stay?', query: 'GA4 Data API runReport with cohortSpec', why: 'You asked for it in S2.' };
58
+ const SEC = (id, title, o) => ({ id, title, fixed: false, charts: [], notes: '', show: '', ...o });
59
+ const OPTIONS = [
60
+ { name: 'Goals first', sections: ['Headline', 'One block per Q4 objective', 'Where to invest'], good: 'A team that reviews against OKRs.', misses: 'Numbers outside an OKR disappear.', recommended: false },
61
+ { name: 'Funnel first', sections: ['Headline', 'New users, activation, subscriptions', 'Where to invest'], good: 'Leadership deciding where to invest.', misses: 'Less detail per feature.', recommended: true },
62
+ { name: 'Features first', sections: ['Headline', 'One block per feature', 'What to build next'], good: 'A product team deciding what to fix next.', misses: 'Leadership has to add the money side.', recommended: false },
63
+ ];
64
+ const plan1 = () => ({ purpose: { readers: 'Leadership: Gita Rahma and the product directors', decision: 'Where to invest next quarter', scope: 'MySPH app, iOS and Android', cadence: 'monthly', questions: ['Where do users drop between installing and paying?', 'What should we fund next quarter?'] },
65
+ sources: [BOARD, DASH, PAST, GA, BQ].map((s) => ({ ...s })), charts: [CH_OLD, CH_NEW].map((c) => ({ ...c })),
66
+ outline: [SEC('headline', 'Headline', { fixed: true, notes: 'The one thing leadership must know this month', show: 'Two sentences' }), SEC('funnel', 'The funnel this month', { charts: ['new-users', 'cpa'], notes: 'Where do users drop?', show: 'Funnel chart' }),
67
+ SEC('needs', 'Needs your call', { fixed: true, notes: 'Asks to leadership' }), SEC('sources', 'Sources', { fixed: true, notes: 'Sources reached and not reached' })], options: OPTIONS });
68
+ const Q1 = [{ q: 'Who reads it, and what do they decide with it?', chips: ['Leadership: where to invest next quarter', 'Product team: what to fix next', 'Both'] }, { q: 'Anything that must be in it?', chips: ['Revenue', 'Releases this month', 'Nothing more'] }];
69
+
70
+ function makeStub() {
71
+ const S = { versions: [], plans: {}, log: [], current: null, saved: null, running: null, ticks: 0, dry: null, dryResult: null, checks: {}, posts: [], gets: [], chartsRead: 0, polls: 0, setupMade: 0, designStep: 0 };
72
+ const now = () => '2026-10-12T22:' + String(10 + S.versions.length * 3 + S.log.length).padStart(2, '0') + ':00.000Z';
73
+ const land = (turn, plan, answer, extra) => { const n = S.versions.length + 1; S.versions.push({ n, at: now(), turn }); S.plans[n] = plan; S.current = n; S.log.push({ who: 'pmos', text: answer, at: now(), n, questions: [], options: [], ...(extra || {}) }); };
74
+ const view = () => {
75
+ if (S.running) { S.ticks++; if (S.ticks >= 2) { const r = S.running; S.running = null; r.done(); } }
76
+ return { ok: true, setup: { id: SETUP.id, name: SETUP.name, cadence: 'monthly' }, structure: { running: !!S.running, job: S.running ? { kind: S.running.kind } : null, current: S.current, saved: S.saved, versions: S.versions.slice(), log: S.log.slice(),
77
+ plan: S.current ? S.plans[S.current] : null, dry: S.dry, checks: S.checks, errors: [] } };
78
+ };
79
+ const chromeChecks = (pass) => ({ source: 'looker', kind: 'chrome', at: now(), running: false, ok: pass, steps: [
80
+ { id: 'mac', label: 'PMOS runs on this Mac', state: 'works', reason: 'The read runs on your MacBook.' }, { id: 'session', label: 'A session can start with Claude in Chrome', state: 'works', reason: 'PMOS started a test session.' },
81
+ { id: 'extension', label: 'Claude in Chrome answers', state: 'works', reason: 'The extension answered.' },
82
+ { id: 'signed-in', label: 'You are signed in to Looker Studio', state: pass ? 'works' : 'needs-you', reason: pass ? 'Opened the report as you.' : 'Chrome showed the Google sign-in page. Sign in once in Chrome yourself, then check again.' },
83
+ { id: 'read', label: 'PMOS can read the numbers', state: pass ? 'works' : 'waiting', reason: pass ? 'Read the table "Spend by campaign".' : 'Waits for the step above.' }] });
84
+ const fixedPlan = (n) => JSON.parse(JSON.stringify(S.plans[n]));
85
+ const route = async (route) => {
86
+ const req = route.request(); const u = new URL(req.url()); const p = u.pathname; const m = req.method();
87
+ const json = (b, status) => route.fulfill({ status: status || 200, contentType: 'application/json', body: JSON.stringify(b) });
88
+ const body = m === 'POST' ? JSON.parse(req.postData() || '{}') : null;
89
+ if (m === 'POST') S.posts.push({ path: p, body });
90
+ if (m === 'GET' && p === '/api/report-structure') { S.polls++; return json(view()); }
91
+ if (m === 'GET' && p === '/api/report-structure/version') return json({ ok: true, plan: S.plans[Number(u.searchParams.get('n'))] });
92
+ if (m === 'GET' && p === '/api/report-structure/dry') return json({ ok: true, dry: S.dryResult });
93
+ if (m === 'GET' && p === '/api/charts') { S.chartsRead++; return json({ ok: true, charts: [{ id: 'new-users', name: 'New users', question: 'Are we still bringing people in?', source: { kind: 'connector', name: 'Google Analytics 4', via: 'Supermetrics' }, from: 'GA4 newUsers', how: 'Month total', compare: 'Previous month', flag: 'Down 10% or more', query: 'GA4 Data API: metric newUsers', why: 'The top of the funnel.', users: [{ kind: 'dashboard', id: 'health', name: 'MySPH health' }, { kind: 'report', id: SETUP.id, name: SETUP.name }] }] }); }
94
+ if (m === 'POST' && p === '/api/report-structure/turn') {
95
+ if (S.running) return json({ ok: false, error: 'a report-structure job is already running' }, 409);
96
+ S.ticks = 0; S.log.push({ who: 'pm', text: body.text, at: now() });
97
+ let done;
98
+ if (!S.versions.length && !S.asked) done = () => { S.asked = true; S.log.push({ who: 'pmos', text: 'Before I propose a structure, two questions.', at: now(), questions: Q1, options: [] }); };
99
+ else if (!S.versions.length) done = () => land('From your answers', plan1(), 'Three ways to shape it. I recommend Funnel first. I drafted it as S1.', { options: OPTIONS });
100
+ else if (/^Test pull/.test(body.text)) done = () => { S.log.push({ who: 'pmos', text: 'Read it: both charts returned a number.', at: now(), questions: [], options: [] }); };
101
+ else done = () => { const pl = fixedPlan(S.current); pl.charts.push({ ...CH_RET }); pl.outline.splice(2, 0, SEC('ret', 'Retention by cohort', { charts: ['ret'], notes: 'Do the people we bring in stay?', show: 'Cohort table' })); land('Retention added', pl, 'S2: D30 retention added.'); };
102
+ S.running = { kind: 'report-structure', done };
103
+ return json({ ok: true, running: true });
104
+ }
105
+ if (m === 'POST' && p === '/api/report-structure/revert') { S.current = body.n; return json({ ok: true, structure: view().structure }); }
106
+ if (m === 'POST' && p === '/api/report-structure/remove') { const pl = fixedPlan(S.current); if (body.kind === 'chart') { pl.charts = pl.charts.filter((c) => c.id !== body.ref); pl.outline.forEach((o) => { o.charts = o.charts.filter((c) => c !== body.ref); }); } else pl.sources = pl.sources.filter((s) => s.id !== body.ref); land('Remove ' + body.ref, pl, 'Removed ' + body.ref + '.'); return json({ ok: true, structure: view().structure }); }
107
+ if (m === 'POST' && p === '/api/report-structure/source') {
108
+ const pl = fixedPlan(S.current); const s = body.source; const grp = ['connector', 'link', 'upload', 'person', 'chrome', 'computer'].includes(s.kind) ? 'outside' : 'pmos';
109
+ pl.sources.push({ via: '', ref: '', ...s, group: grp, state: s.kind === 'chrome' ? 'needs-you' : 'reached' });
110
+ land('Add ' + s.name, pl, s.name + ' added.');
111
+ if (s.kind === 'chrome') S.checks[s.id] = { ...chromeChecks(false), source: s.id };
112
+ return json({ ok: true, structure: view().structure });
113
+ }
114
+ if (m === 'POST' && p === '/api/report-structure/check') { S.checks[body.source] = { ...chromeChecks(true), source: body.source }; const pl = S.plans[S.current]; const s = pl.sources.find((x) => x.id === body.source); if (s) s.state = 'reached'; return json({ ok: true, check: S.checks[body.source] }); }
115
+ if (m === 'POST' && p === '/api/report-structure/dry') { S.ticks = 0; S.running = { kind: 'report-structure-dry', done: () => { S.dry = { run: 'r1', n: body.n || S.current, at: now(), ok: true };
116
+ S.dryResult = { n: S.dry.n, period: '2026-09', headline: 'New users fell 8.9% while cost per new user rose 19%.', values: [{ chart: 'new-users', value: '48,210', was: '52,930 in Aug', delta: '-8.9%', flag: true, note: 'down 10% or more is the rule' }, { chart: 'cpa', value: 'IDR 21,900', was: 'IDR 18,400 in Aug', delta: '+19%', flag: false, note: '' }], missing: [{ source: 'bq', reason: 'BigQuery is not connected.' }], options: ['Fix the first 30 days', 'Restart paid acquisition'] }; } }; return json({ ok: true, running: true }); }
117
+ if (m === 'POST' && p === '/api/report-structure/accept') { S.saved = body.n; return json({ ok: true, structure: view().structure, brief: 'structure/brief.md', charts: ['cpa'] }); }
118
+ if (m === 'GET' && p === '/api/report-setups') return json({ ok: true, setups: S.setupMade ? [SETUP] : [SETUP], findings: [], asks: S.askShown ? [{ id: 'ask1', text: 'Monthly product performance for leadership, numbers from Google Analytics', state: 'ready', proposal: PROPOSAL }] : [], finder: { cadence: 'off' } });
119
+ if (m === 'POST' && p === '/api/report-setups/setup') { S.setupMade++; return json({ ok: true, setup: SETUP, drafted: null }); }
120
+ if (m === 'GET' && p === '/api/report-documents') return json({ ok: true, docs: [] });
121
+ if (m === 'GET' && p === '/api/report-dashboards') return json({ ok: true, library: [{ id: 'north', title: 'North star', question: 'Are we moving?', source: 'PMOS ledgers' }], layouts: {}, defaults: { board: [], initiative: [] }, boards: [], initiatives: [], schedules: [] });
122
+ if (m === 'GET' && p === '/api/report-design') return json({ ok: true, setup: { id: SETUP.id, name: SETUP.name }, design: { running: false, current: null, saved: null, versions: [], log: [], samples: [], description: '', format: 'deck' } });
123
+ return route.continue();
124
+ };
125
+ return { S, route };
126
+ }
127
+
128
+ (async () => {
129
+ const exe = findBrowser();
130
+ if (!exe) { console.log('· no Chrome/Chromium on this machine, c3-report-structure suite skipped'); process.exit(0); }
131
+ let chromium;
132
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('· playwright-core not installed, c3-report-structure suite skipped'); process.exit(0); }
133
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
134
+ await T.waitForServer(BASE);
135
+ browser = await chromium.launch({ executablePath: exe });
136
+ require('./lib/c3-full-console').fullConsole(browser); // the desktop layout at phone width (c3-phone-companion)
137
+ const shots = process.env.RX_SHOTS || ''; if (shots) fs.mkdirSync(shots, { recursive: true });
138
+ const shot = async (pg, n) => { if (shots) await pg.screenshot({ path: path.join(shots, n + '.png') }); };
139
+
140
+ const errors = [];
141
+ const stub = makeStub(); const S = stub.S;
142
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
143
+ await ctx.route(/\/api\/(report-structure|charts|report-setups|report-documents|report-dashboards|report-design)/, stub.route);
144
+ const page = await ctx.newPage();
145
+ page.on('pageerror', (e) => errors.push('pageerror: ' + String((e && e.message) || e)));
146
+ page.on('console', (m) => { if (m.type() === 'error' && !/favicon|Failed to load resource/.test(m.text())) errors.push('console: ' + m.text()); });
147
+ page.on('response', (r) => { if (r.status() >= 400 && !/\/api\/(scan\/(?:today|portfolio)|settings|report-structure)\b/.test(r.url()) && !(r.status() === 409)) errors.push('http ' + r.status() + ' ' + r.url()); });
148
+ page.on('dialog', (d) => d.dismiss().catch(() => {}));
149
+ const text = (sel) => page.textContent(sel).catch(() => '');
150
+ const sent = (p) => S.posts.filter((x) => x.path === p);
151
+ const closeAll = async () => { for (let i = 0; i < 3 && await page.$('.sheet'); i++) { await page.keyboard.press('Escape'); await sleep(250); } };
152
+ const goReports = async () => { await page.goto(BASE + '/c3.html?tab=rep'); await page.waitForSelector('[data-rp3]'); await page.click('[data-rp3-tab="reports"]'); await page.waitForSelector('[data-rs-reg="' + SETUP.id + '"]', { timeout: 15000 }); };
153
+ const idle = () => page.waitForFunction(() => { const b = document.querySelector('[data-st-act="send"]'); return b && !b.disabled && !document.querySelector('[data-st-running]'); }, null, { timeout: 15000 });
154
+ const noOverflow = () => page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1);
155
+
156
+ /* ── entry points ── */
157
+ S.askShown = true;
158
+ await goReports();
159
+ ok('the set-up row has Shape the report before Design how it looks', await page.$$eval('[data-rs-reg="' + SETUP.id + '"] .acts button', (a) => { const t = a.map((b) => b.textContent.trim()); return t.indexOf('Shape the report') > -1 && t.indexOf('Shape the report') < t.indexOf('Design how it looks'); }));
160
+ await page.waitForSelector('[data-rs-card="ask1"]');
161
+ ok('an Ask PMOS answer has Shape it with PMOS', !!(await page.$('[data-rs-card="ask1"] [data-rs-act="shape"]')));
162
+ await page.click('[data-rs-card="ask1"] [data-rs-act="shape"]');
163
+ await page.waitForSelector('[data-st-studio]');
164
+ ok('Shape it with PMOS sets the report up, then opens the Structure step', sent('/api/report-setups/setup').length === 1 && sent('/api/report-setups/setup')[0].body.ask === 'ask1' && /Not saved yet/.test(await text('[data-st-studio]')));
165
+ ok('the step bar says 1 Structure, 2 Design with Structure pressed', await page.getAttribute('[data-st-step-btn="structure"]', 'aria-pressed') === 'true' && /2 Design/.test(await text('[data-st-steps]')));
166
+ ok('with no version yet the studio asks what the report is for and the Use button waits', !!(await page.$('[data-st-studio] .fold')) && await page.$eval('[data-st-act="save"]', (b) => b.disabled));
167
+ await closeAll();
168
+ S.askShown = false;
169
+ await goReports();
170
+ await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="shape"]');
171
+ await page.waitForSelector('[data-st-studio]');
172
+ ok('Shape the report on the row opens the same studio', /Not saved yet/.test(await text('[data-st-studio]')));
173
+ await closeAll();
174
+ await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="design"]');
175
+ await page.waitForSelector('[data-rd-studio]');
176
+ ok('the Design studio shows the step bar with 2 Design pressed', await page.getAttribute('[data-rd-step="design"]', 'aria-pressed') === 'true' && await page.getAttribute('[data-rd-step="structure"]', 'aria-pressed') === 'false');
177
+ await page.click('[data-rd-step="structure"]');
178
+ await page.waitForSelector('[data-st-studio]');
179
+ ok('1 Structure in the Design studio opens the Structure step', !!(await page.$('[data-st-steps]')));
180
+ await page.click('[data-st-step-btn="design"]');
181
+ await page.waitForSelector('[data-rd-studio]');
182
+ ok('2 Design in the Structure step opens the Design studio', true);
183
+ await page.click('[data-rd-step="structure"]');
184
+ await page.waitForSelector('[data-st-studio]');
185
+
186
+ /* ── a first turn: PMOS asks, answer chips ── */
187
+ await page.fill('[data-st-in]', 'I need a monthly product performance report for leadership. Get the numbers from Google Analytics.');
188
+ await page.click('[data-st-act="send"]');
189
+ ok('a turn posts the words as ONE turn', !!(await until(() => sent('/api/report-structure/turn').length === 1)) && /monthly product performance/.test(sent('/api/report-structure/turn')[0].body.text));
190
+ ok('while the job runs the conversation says PMOS is working and Make the next version waits', !!(await until(() => page.$('[data-st-running]'))));
191
+ await page.waitForSelector('[data-st-question]', { timeout: 15000 });
192
+ ok('PMOS asks its questions with answer chips', (await page.$$('[data-st-question]')).length === 2 && (await page.$$('[data-st-question] [data-st-act="chip"]')).length === 6);
193
+ await page.click('[data-st-act="chip"][data-v="Leadership: where to invest next quarter"]');
194
+ ok('a chip puts its answer in the box', (await page.inputValue('[data-st-in]')) === 'Leadership: where to invest next quarter.');
195
+ await page.click('[data-st-act="chip"][data-v="Revenue"]');
196
+ ok('a second chip adds to the box', /next quarter\. Revenue\.$/.test(await page.inputValue('[data-st-in]')));
197
+ await page.click('[data-st-act="send"]');
198
+ await page.waitForSelector('[data-st-v="1"]', { timeout: 15000 });
199
+ ok('the second turn lands S1', (await page.$$('[data-st-v]')).length === 1 && await page.getAttribute('[data-st-v="1"]', 'aria-pressed') === 'true');
200
+ await page.waitForSelector('[data-st-purpose]');
201
+ ok('the plan shows What it answers with readers, decision, scope and the questions', /Leadership: Gita Rahma/.test(await text('[data-st-purpose]')) && /Where to invest next quarter/.test(await text('[data-st-purpose]')) && /Where do users drop/.test(await text('[data-st-purpose]')));
202
+
203
+ /* ── sources ── */
204
+ const grp = (g) => page.$$eval('[data-st-group="' + g + '"] + ul > li', (a) => a.map((li) => li.getAttribute('data-st-src')));
205
+ ok('Sources are grouped: From PMOS and Outside PMOS', JSON.stringify(await grp('pmos')) === '["board","dash","past"]' && JSON.stringify(await grp('outside')) === '["ga","bq"]', JSON.stringify([await grp('pmos'), await grp('outside')]));
206
+ ok('each source shows its state', await page.getAttribute('[data-st-src="ga"] [data-st-state]', 'data-st-state') === 'reached' && /Not connected/.test(await text('[data-st-src="bq"] [data-st-state]')) && /First cycle: nothing to read/.test(await text('[data-st-src="past"] [data-st-state]')));
207
+ ok('the actions fit the source: Open, Test pull, How to connect', /Open/.test(await text('[data-st-src="board"] .acts')) && /Test pull/.test(await text('[data-st-src="ga"] .acts')) && /How to connect/.test(await text('[data-st-src="bq"] .acts')));
208
+ ok('a source says how many charts use it', /2 charts/.test(await text('[data-st-src="ga"]')) && !/ chart/.test(await text('[data-st-src="board"] .chips')));
209
+ await page.click('[data-st-src="ga"] [data-st-act="test"]');
210
+ ok('Test pull asks PMOS to read that source (one turn)', !!(await until(() => sent('/api/report-structure/turn').length === 3)) && /Test pull from Google Analytics 4/.test(sent('/api/report-structure/turn')[2].body.text));
211
+ await idle();
212
+ await page.click('[data-st-src="bq"] [data-st-act="connect"]');
213
+ ok('How to connect says where to connect it', /Settings, Connectors/.test(await text('[data-c3-toast]')));
214
+
215
+ /* ── charts ── */
216
+ ok('Charts are tagged New chart and From Charts', await text('[data-st-chart="cpa"] [data-st-tag]') === 'New chart' && await text('[data-st-chart="new-users"] [data-st-tag]') === 'From Charts' && /1 new, 1 from Charts/.test(await text('[data-st-metrics]')));
217
+ await page.click('[data-st-chart="new-users"] [data-st-act="chart"]');
218
+ await page.waitForSelector('[data-st-detail="new-users"]');
219
+ ok('a chart row opens to question, used in, what PMOS reads and why', /Are we still bringing people in/.test(await text('[data-st-q]')) && /GA4 Data API: metric newUsers/.test(await text('[data-st-query]')) && /top of the funnel/.test(await text('[data-st-why]')));
220
+ ok('Used in names the dashboard and report that use a chart from Charts', !!(await until(async () => /dashboard MySPH health/.test(await text('[data-st-used]')) && /this report/.test(await text('[data-st-used]')))), await text('[data-st-used]'));
221
+ ok('a chart from Charts has Open in Charts, a new chart does not', !!(await page.$('[data-st-detail="new-users"] [data-st-act="opencharts"]')));
222
+ await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
223
+ ok('only one row is open and a new chart says it is saved in Charts when you use the structure', !!(await page.$('[data-st-detail="cpa"]')) && !(await page.$('[data-st-detail="new-users"]')) && /Saved in Charts when you use this structure/.test(await text('[data-st-used]')) && !(await page.$('[data-st-detail="cpa"] [data-st-act="opencharts"]')));
224
+ ok('the test pull says it is not tried yet', /Not tried on this version/.test(await text('[data-st-testpull]')));
225
+ await shot(page, 'structure-logic-1440');
226
+ await page.click('[data-st-detail="cpa"] [data-st-act="discuss"]');
227
+ ok('Discuss this chart fills the box with the chart name and focuses it', (await page.inputValue('[data-st-in]')) === 'About Cost per new user: ' && await page.evaluate(() => document.activeElement && document.activeElement.hasAttribute('data-st-in')));
228
+
229
+ /* ── the outline ── */
230
+ ok('the Outline lists the sections in order with chart chips and PMOS notes', JSON.stringify(await page.$$eval('[data-st-section]', (a) => a.map((x) => x.getAttribute('data-st-section')))) === '["headline","funnel","needs","sources"]'
231
+ && /New users/.test(await text('[data-st-section="funnel"]')) && /PMOS notes/.test(await text('[data-st-section="funnel"]')) && !/PMOS notes/.test(await text('[data-st-section="headline"]')) && /In every report/.test(await text('[data-st-section="headline"]')));
232
+
233
+ /* ── a refine turn: S2 ── */
234
+ await idle();
235
+ await page.fill('[data-st-in]', 'Add retention by cohort.');
236
+ await page.click('[data-st-act="send"]');
237
+ await page.waitForSelector('[data-st-v="2"]', { timeout: 15000 });
238
+ ok('a refine turn lands S2 and the strip lists S1 and S2', (await page.$$('[data-st-v]')).length === 2 && await page.getAttribute('[data-st-v="2"]', 'aria-pressed') === 'true');
239
+ await page.waitForSelector('[data-st-chart="ret"]');
240
+ ok('S2 has the retention chart and a section that says New in S2', !!(await page.$('[data-st-chart="ret"]')) && /New in S2/.test(await text('[data-st-section="ret"]')) && !/New in S2/.test(await text('[data-st-section="funnel"]')));
241
+ await shot(page, 'structure-1440');
242
+ await page.$eval('[data-st-sources]', (n) => n.scrollIntoView({ block: 'start' })); await sleep(200); await shot(page, 'structure-sources-1440');
243
+ ok('the conversation names each version with a link', (await page.$$('[data-st-log] [data-st-act="ver"]')).length === 2);
244
+ await page.click('[data-st-v="1"]');
245
+ await until(async () => await page.getAttribute('[data-st-v="1"]', 'aria-pressed') === 'true' && !(await page.$('[data-st-chart="ret"]')));
246
+ ok('S1 shows again without the retention chart and says it is an older version', !(await page.$('[data-st-chart="ret"]')) && /older version/.test(await text('[data-st-vcap]')));
247
+ await page.click('[data-st-act="revert"]');
248
+ ok('Go back to S1 posts a revert for n 1', !!(await until(() => sent('/api/report-structure/revert').length === 1)) && sent('/api/report-structure/revert')[0].body.n === 1);
249
+ await until(async () => !(await page.$('[data-st-act="revert"]')));
250
+ await page.click('[data-st-v="2"]');
251
+ await page.waitForSelector('[data-st-chart="ret"]');
252
+ await page.click('[data-st-act="revert"]');
253
+ ok('going back to S2 the same way makes it current again', !!(await until(() => sent('/api/report-structure/revert').length === 2)) && sent('/api/report-structure/revert')[1].body.n === 2);
254
+ await until(async () => !(await page.$('[data-st-act="revert"]')));
255
+
256
+ /* ── Remove it ── */
257
+ await page.click('[data-st-chart="ret"] [data-st-act="chart"]');
258
+ await page.click('[data-st-detail="ret"] [data-st-act="drop"]');
259
+ ok('Remove it posts a remove of that chart', !!(await until(() => sent('/api/report-structure/remove').length === 1)) && sent('/api/report-structure/remove')[0].body.kind === 'chart' && sent('/api/report-structure/remove')[0].body.ref === 'ret');
260
+ await page.waitForSelector('[data-st-v="3"]', { timeout: 10000 });
261
+ ok('removing makes the next version, S3, without the chart', !(await page.$('[data-st-chart="ret"]')) && (await page.$$('[data-st-v]')).length === 3);
262
+
263
+ /* ── Add a source ── */
264
+ await page.click('[data-st-act="panel"][aria-expanded]');
265
+ await page.waitForSelector('[data-st-panel]');
266
+ const optKinds = await page.$$eval('[data-st-panel] [data-st-act="opt"]', (a) => a.map((b) => b.getAttribute('data-v')));
267
+ ok('Add a source offers every From PMOS and Outside PMOS kind', JSON.stringify(optKinds) === JSON.stringify(['pmos-level', 'dashboard', 'chart', 'document', 'past-report', 'connector', 'link', 'upload', 'person', 'chrome', 'computer']), optKinds.join(','));
268
+ ok('the panel has the two groups and says PMOS only reads', /From PMOS/.test(await text('[data-st-panel]')) && /Outside PMOS/.test(await text('[data-st-panel]')) && /PMOS only reads/.test(await text('[data-st-panel]')));
269
+ await shot(page, 'structure-pmos-1440');
270
+ await page.click('[data-st-panel] [data-st-act="opt"][data-v="document"]');
271
+ await page.waitForSelector('[data-st-form="document"]');
272
+ await page.click('[data-st-form="document"] [data-st-act="add"]');
273
+ ok('a form needs its name before it adds', sent('/api/report-structure/source').length === 0);
274
+ await page.fill('[data-st-form="document"] [data-st-field="name"]', 'MySPH strategy 2026');
275
+ await page.click('[data-st-form="document"] [data-st-act="add"]');
276
+ ok('a From PMOS source (a document) posts a source of that kind', !!(await until(() => sent('/api/report-structure/source').length === 1)) && sent('/api/report-structure/source')[0].body.source.kind === 'document' && sent('/api/report-structure/source')[0].body.source.name === 'MySPH strategy 2026' && !('state' in sent('/api/report-structure/source')[0].body.source));
277
+ await page.waitForSelector('[data-st-v="4"]', { timeout: 10000 });
278
+ ok('it joins the From PMOS group in S4', JSON.stringify(await grp('pmos')) === '["board","dash","past","mysph-strategy-2026"]', JSON.stringify(await grp('pmos')));
279
+ await page.click('[data-st-act="panel"][aria-expanded]');
280
+ await page.click('[data-st-panel] [data-st-act="opt"][data-v="chrome"]');
281
+ await page.waitForSelector('[data-st-form="chrome"]');
282
+ await page.fill('[data-st-form="chrome"] [data-st-field="name"]', 'Looker Studio: Paid acquisition');
283
+ await page.fill('[data-st-form="chrome"] [data-st-field="ref"]', 'not a link');
284
+ await page.click('[data-st-form="chrome"] [data-st-act="add"]');
285
+ ok('a Chrome page needs a link that starts with https://', /starts with https/.test(await text('[data-c3-toast]')) && !S.posts.some((x) => x.body && x.body.source && x.body.source.kind === 'chrome'));
286
+ await page.fill('[data-st-form="chrome"] [data-st-field="ref"]', 'https://lookerstudio.google.com/reporting/abc');
287
+ await page.click('[data-st-form="chrome"] [data-st-act="add"]');
288
+ await page.waitForSelector('[data-st-check]', { timeout: 10000 });
289
+ const lid = sent('/api/report-structure/source').pop().body.source.id;
290
+ ok('a Chrome source shows its check step by step', (await page.$$('[data-st-check] [data-st-step]')).length === 5 && await page.getAttribute('[data-st-check] [data-st-step="signed-in"]', 'data-st-step-state') === 'needs-you');
291
+ ok('a failing step says Needs you with its reason; the steps after it Wait', /Needs you/.test(await text('[data-st-step="signed-in"]')) && /Sign in once in Chrome/.test(await text('[data-st-step="signed-in"]')) && /Waiting/.test(await text('[data-st-step="read"]')) && /Works/.test(await text('[data-st-step="mac"]')));
292
+ ok('the check says one step needs you, what the session may do, and the 4 hour wait', /1 step needs you/.test(await text('[data-st-check-state]')) && /Only the browser tools/.test(await text('[data-st-check]')) && /up to 4 hours/.test(await text('[data-st-check]')));
293
+ ok('the source row is in Outside PMOS with Needs you and Check it works', /Needs you/.test(await text('[data-st-src="' + lid + '"] [data-st-state]')) && /Check it works/.test(await text('[data-st-src="' + lid + '"] .acts')) && /Re-checked 1 hour before each draft/.test(await text('[data-st-src="' + lid + '"]')));
294
+ await shot(page, 'structure-browser-1440');
295
+ ok('Check again is offered', /Check again/.test(await text('[data-st-check] [data-st-act="chkagain"]')));
296
+ await page.click('[data-st-check] [data-st-act="chkagain"]');
297
+ ok('Check again posts a check for that source', !!(await until(() => sent('/api/report-structure/check').length === 1)) && sent('/api/report-structure/check')[0].body.source === lid);
298
+ await until(async () => /All steps work/.test(await text('[data-st-check-state]')));
299
+ ok('the second check passes: every step Works and the source is Reached', /All steps work/.test(await text('[data-st-check-state]')) && (await page.$$eval('[data-st-check] [data-st-step-state="works"]', (a) => a.length)) === 5 && /Reached/.test(await text('[data-st-src="' + lid + '"] [data-st-state]')));
300
+ await shot(page, 'structure-browser-ok-1440');
301
+ await page.click('[data-st-src="' + lid + '"] [data-st-act="chkopen"]');
302
+ ok('Check it works on the row posts a check', !!(await until(() => sent('/api/report-structure/check').length === 2)));
303
+
304
+ /* ── Other structures ── */
305
+ await page.click('[data-st-view="options"]');
306
+ await page.waitForSelector('[data-st-options]');
307
+ ok('Other structures shows three side by side with the recommended first', JSON.stringify(await page.$$eval('[data-st-option]', (a) => a.map((x) => x.getAttribute('data-st-option')))) === '["Funnel first","Goals first","Features first"]' && /Recommended/.test(await text('[data-st-option="Funnel first"]')));
308
+ ok('each option has its sections, good for and misses', /GOOD FOR/.test(await text('[data-st-option="Goals first"]')) && /MISSES/.test(await text('[data-st-option="Goals first"]')) && (await page.$$('[data-st-option="Goals first"] li')).length === 3);
309
+ const cols = await page.$$eval('[data-st-option]', (a) => a.map((x) => Math.round(x.getBoundingClientRect().top)));
310
+ ok('the three options sit in one row at 1440 wide', cols.every((t) => Math.abs(t - cols[0]) < 4), cols.join(','));
311
+ await shot(page, 'structure-options-1440');
312
+ await page.click('[data-st-option="Goals first"] [data-st-act="choose"]');
313
+ ok('Use this instead puts the choice in the box and goes back to the plan', (await page.inputValue('[data-st-in]')) === 'Use the "Goals first" structure instead.' && await page.getAttribute('[data-st-view="plan"]', 'aria-pressed') === 'true');
314
+ await page.fill('[data-st-in]', '');
315
+
316
+ /* ── Try it with real data ── */
317
+ await page.click('[data-st-view="dry"]');
318
+ await page.waitForSelector('[data-st-nodry]');
319
+ ok('Try it with real data explains that it reads only and saves nothing', /saves nothing/.test(await text('[data-st-nodry]')));
320
+ await page.click('[data-st-act="dryrun"]');
321
+ ok('the run posts ONE dry job for the version on screen', !!(await until(() => sent('/api/report-structure/dry').length === 1)) && sent('/api/report-structure/dry')[0].body.n === 5);
322
+ ok('while it runs the view says so and nothing is saved', !!(await until(() => page.$('[data-st-dryrunning]'))));
323
+ await page.waitForSelector('[data-st-dry-headline]', { timeout: 15000 });
324
+ ok('the result shows the headline PMOS would write', /New users fell 8\.9%/.test(await text('[data-st-dry-headline]')));
325
+ ok('the values show each chart with its comparison', /New users/.test(await text('[data-st-dry-values]')) && /48,210/.test(await text('[data-st-dry-values]')) && /52,930 in Aug/.test(await text('[data-st-dry-values]')) && /\+19%/.test(await text('[data-st-dry-values]')));
326
+ ok('a flagged value is flagged and listed under Flags', /Flagged/.test(await text('[data-st-dry-values]')) && /New users/.test(await text('[data-st-dry-flags]')));
327
+ ok('what it could not get names the source and why', /BigQuery/.test(await text('[data-st-dry-missing]')) && /not connected/.test(await text('[data-st-dry-missing]')));
328
+ ok('the options PMOS would propose show, with the note that the PM decides', /Fix the first 30 days/.test(await text('[data-st-dry-options]')) && /PMOS never decides them/.test(await text('[data-st-dry-options]')));
329
+ await shot(page, 'structure-dry-1440');
330
+ await page.click('[data-st-view="plan"]');
331
+ await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
332
+ ok('a chart row now shows its test pull value', /IDR 21,900/.test(await text('[data-st-testpull]')) && /\+19%/.test(await text('[data-st-testpull]')));
333
+
334
+ /* ── Use this structure ── */
335
+ await page.click('[data-st-act="save"]');
336
+ ok('Use this structure posts the version on screen', !!(await until(() => sent('/api/report-structure/accept').length === 1)) && sent('/api/report-structure/accept')[0].body.n === 5);
337
+ await page.waitForSelector('[data-rd-brief]', { timeout: 10000 });
338
+ await page.waitForSelector('[data-st-saved-wrap]');
339
+ ok('the report setup sheet shows the saved structure beside the Design', /Structure S5/.test(await text('[data-st-saved-tag]')) && !!(await page.$('[data-rd-design]')));
340
+ ok('the saved structure lists what it answers, the sources, the charts and the outline', /Where to invest next quarter/.test(await text('[data-st-answers]')) && /MySPH strategy 2026/.test(await text('[data-st-brief]')) && /Cost per new user/.test(await text('[data-st-brief]')) && /Needs your call/.test(await text('[data-st-brief]')));
341
+ ok('it offers Refine the structure and Try it with real data', /Refine the structure/.test(await text('[data-st-brief]')) && /Try it with real data/.test(await text('[data-st-brief]')));
342
+ ok('it says a change applies from the next cycle and the Chrome source is re-checked', /applies from the next cycle/.test(await text('[data-st-saved-wrap]')) && /Looker Studio: Paid acquisition/.test(await text('[data-st-saved-wrap]')) && /waits up to 4 hours/.test(await text('[data-st-saved-wrap]')));
343
+ await shot(page, 'structure-saved-1440');
344
+ await page.click('[data-st-brief] [data-st-act="opendry"]');
345
+ await page.waitForSelector('[data-st-studio]');
346
+ ok('Try it with real data in the setup sheet opens the studio on that view', await page.getAttribute('[data-st-view="dry"]', 'aria-pressed') === 'true' && /Saved structure: S5/.test(await text('[data-st-studio]')));
347
+ await closeAll();
348
+
349
+ /* ── the Charts tab ── */
350
+ await goReports();
351
+ await page.click('[data-rp3-tab="charts"]');
352
+ await page.waitForSelector('[data-rp3-data-chart="new-users"]');
353
+ ok('the Charts tab lists a data chart beside the built-in ones', !!(await page.$('[data-rp3-lib] [data-home="north"]')) && !!(await page.$('[data-rp3-lib] [data-rp3-data-chart="new-users"]')) && /Data chart/.test(await text('[data-rp3-data-chart="new-users"]')));
354
+ ok('the data chart shows its source and Used in', /Google Analytics 4 through Supermetrics/.test(await text('[data-rp3-source="new-users"]')) && /Used in: .*MySPH health.*MySPH app performance, monthly/.test(await text('[data-rp3-data-chart="new-users"] [data-rp3-used]')));
355
+ ok('the Charts tab counts both kinds', /2/.test(await text('[data-rp3-tab="charts"]')));
356
+ await page.click('[data-rp3-data-chart="new-users"] .tt').catch(() => {});
357
+ await shot(page, 'structure-charts-tab-1440');
358
+
359
+ /* ── the saved structure and the studio at 400 wide ── */
360
+ await page.setViewportSize({ width: 400, height: 900 });
361
+ await goReports();
362
+ ok('at 400 wide the Reports tab does not scroll sideways', await noOverflow());
363
+ await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="shape"]');
364
+ await page.waitForSelector('[data-st-chart]');
365
+ ok('at 400 wide the Structure step does not scroll sideways', await noOverflow());
366
+ await shot(page, 'structure-400');
367
+ await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
368
+ await page.click('[data-st-act="panel"][aria-expanded]');
369
+ await page.waitForSelector('[data-st-panel]');
370
+ ok('at 400 wide the open chart and the Add a source panel do not scroll the page sideways', await noOverflow());
371
+ await shot(page, 'structure-pmos-400');
372
+ await page.click('[data-st-view="options"]');
373
+ ok('at 400 wide Other structures stack and do not scroll sideways', await noOverflow());
374
+ await shot(page, 'structure-options-400');
375
+ await page.click('[data-st-view="dry"]');
376
+ ok('at 400 wide the dry run does not scroll sideways', await noOverflow());
377
+ await shot(page, 'structure-dry-400');
378
+ await page.setViewportSize({ width: 1440, height: 1000 });
379
+
380
+ ok('no script error and no failed request', errors.length === 0, errors.join(' | '));
381
+ await finish();
382
+ })().catch((e) => { console.error(e); finish(1); });
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "The Briefing: today's brief"
4
- description: "The Briefing page of the console (/): the headline cards, Today, Needs you, Portfolio pulse, and what Done, Not yet and Drop do to an item."
4
+ description: "The Briefing page of the console (/): The brief, the headline cards, Today, Needs you, Mentions, Portfolio pulse, and what Done, Not yet and Drop do to an item."
5
5
  timestamp: 2026-10-14
6
6
  ---
7
7
 
8
8
  # The Briefing: today's brief
9
9
 
10
- The Briefing is the first page of the console at `/`. It has two columns. The left column holds the greeting, the headline cards and Needs you. The right column holds Today and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
10
+ The Briefing is the first page of the console at `/`. It has two columns. The left column holds the greeting, The brief, the headline cards, Needs you and Mentions. The right column holds Today and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
11
11
 
12
12
  Review is in the Work page, in its Review lens. Handled, What I could stop and Data you can lean on are in Reports, under Drafts.
13
13
 
@@ -35,6 +35,20 @@ Overdue cards come first, the most late first. The signal engine's cards come ne
35
35
 
36
36
  Above the cards, the greeting names the part of the day, for example "Good morning, Wawan". The name is the first word of the git user name on the computer where PMOS runs. When git holds no name, the greeting has none. The part of the day follows the clock. There is no button to change it. When the clock moves to a new part, the line under the greeting and the order of the cards change after you stop using the page for a minute.
37
37
 
38
+ ## The brief
39
+
40
+ The brief is one paragraph under the greeting, above the headline cards. Read it to know the day in a few seconds. It says, in this order:
41
+
42
+ 1. The one decision that the day turns on. When no row is a decision that only you can make, it names the first row of Needs you.
43
+ 2. How many things need you, how many are overdue, and the oldest overdue item.
44
+ 3. Who waits for your reply to a mention.
45
+ 4. How many meetings are left today, and the next meeting that still has a to-do before it.
46
+ 5. Which initiatives are off track or at risk, and how many are on track.
47
+
48
+ Each number and each title comes from a row on the page. The count of things that need you is the same as the count of Needs you. Press a title to go to its row. The line at the right of the title says when PMOS wrote the paragraph and which sections it read. When a section changes, for example when you mark an item done, the paragraph changes too. In the evening the paragraph says what is still open and how many meetings you held.
49
+
50
+ PMOS writes the paragraph from the rows on the page. It does not call a model and it does not write a record.
51
+
38
52
  ## Today
39
53
 
40
54
  Today starts with one line: the meeting that is on now or next, how many meetings are left, and how many of them are prepped, with a small ring. **Open calendar** opens the calendar. Under it, Today lists the meetings of the day, in time order. Each row shows:
@@ -73,6 +87,20 @@ Press a row to open it in place. A row of your own item has three actions. Each
73
87
 
74
88
  PMOS can see evidence that an outside item is done. For example, a meeting with the person is on the calendar. Then the row says "Looks done: a meeting with Ana is booked Tue 10:00. Confirm?" PMOS never marks the item done itself. The item changes only when you press **Done** or **Yes, it is done**.
75
89
 
90
+ ## Mentions
91
+
92
+ Mentions lists the places where someone mentioned you today: Slack, Jira, Confluence and mail. The list comes from today's morning brief. When there is no morning brief for today, Mentions says so.
93
+
94
+ The section is quiet. It shows one line that names who waits for your reply, for example "Priya in Slack and Dara in Jira are waiting on your reply." The header says how many need a reply. Press **Show all** to open the list. A tab for each source filters the list. Each row shows the person, the text they wrote, the source and the place, the time, and **needs your reply** or **for your information**. Each row has these actions:
95
+
96
+ - **Open in Slack** (or Jira, Confluence, Mail) opens the mention in a new tab. A mention with no link has no such button.
97
+ - **Add to Work** adds an item to your Work list, for example "Reply to Dara (Jira) by today".
98
+ - **Mark read** removes the row from the list. Press **Undo** in the notice to bring it back.
99
+
100
+ A mention that is already a row of Needs you does not show again in Mentions. That row in Needs you has a **mentioned you** tag.
101
+
102
+ PMOS does not reply, react or post in Slack, Jira, Confluence or mail.
103
+
76
104
  ## Portfolio pulse
77
105
 
78
106
  Portfolio pulse says how many initiatives need attention, then shows one bar for the status of the initiatives and one row for each initiative. An initiative shows when it has a promised date, or when PMOS holds a read of its Jira epic. A row from a Jira epic shows "3 of 8 done" and a bar. "Done" counts the tickets under the epic that are done in Jira. A row with blocked tickets says **Blocked**. The Jira read is the last Jira status check; the opened row says its date. When no initiative has a promised date or a Jira read, the pulse says so. **All initiatives** at the bottom opens the Initiatives page. Press a row to open it in place. It shows why the initiative has that status, the promised date against the forecast, and the Jira progress when PMOS holds it. The page does not change.
@@ -85,6 +113,6 @@ An item has its actions in one place only. On the Briefing, Today and Portfolio
85
113
 
86
114
  ## On a phone
87
115
 
88
- At 400 pixels wide the Briefing is one column (greeting, cards, Needs you, Today, Portfolio pulse) with no sideways scroll. Each button in an opened row is 44 pixels high.
116
+ At 400 pixels wide the Briefing is one column (greeting, The brief, cards, Needs you, Mentions, Today, Portfolio pulse) with no sideways scroll. Each button in an opened row is 44 pixels high.
89
117
 
90
118
  On a phone, every console page has the same top. The page title has Record at its right. The sunrise line is under it, then the tool buttons in one row. The five tabs share the width, and each tab shows an icon over its name, so Initiatives and Reports are always on screen. A count shows at the top right of its tab's icon. A person in a row shows as a small round letter, and you can tap anywhere in the 44-pixel square around it. A line that has a sentence and a due date wraps like normal text.
@@ -74,6 +74,8 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
74
74
  - [Report slides](report-slides.md): how the Reports page shows a report as the PM-OS biweekly slides, Present and Print / PDF, and how your notes become the words on the slides.
75
75
  - [Reports: dashboards of reports](reports-dashboards.md): the console Reports page, its dashboards at portfolio, board and initiative scope, how to edit one, how to send one as a report, and the Charts tab.
76
76
  - [Set up a report by asking, and the report finder](reports-set-up.md): how to ask PMOS to draft a report you send, how the finder finds reports you already send, and how a cycle becomes a document.
77
+ - [Shape what a report says: the Structure step](report-structure.md): how to decide with PMOS what a report answers, where each number comes from and how it is worked out, add a page in Chrome or an app as a source, and try the plan on real data.
78
+ - [Shape what a report says: the Structure step](report-structure.md): how to decide with PMOS what a report answers, where each number comes from and how it is worked out, add a page in Chrome or an app as a source, and try the plan on real data.
77
79
  - [Design how a report looks, and read it as a deck](report-design.md): how to design a report in the design studio, read each cycle as a deck or an interactive document, present it, and use speaker notes and notes on the slide.
78
80
  - [The document sheet](document-sheet.md): read a document as rich text, edit it, accept the agent's proposals section by section, read the edit log, and clear a stale document.
79
81
  - [Ask the agent from the page](ask-the-agent.md): the five presses that start an agent from the console, what the agent may do, and what stays yours.
@@ -13,6 +13,10 @@ There is no template to fill in. You describe the report, add samples, and refin
13
13
 
14
14
  ## Open the design studio
15
15
 
16
+ Design is step 2 of the report studio. Step 1 is Structure, which decides what the report says (see [Shape what a report says](report-structure.md)). Press **1 Structure** at the top of the studio to go to it.
17
+
18
+ Design is step 2 of the report studio. Step 1 is Structure, which decides what the report says (see [Shape what a report says](report-structure.md)). Press **1 Structure** at the top of the studio to go to it.
19
+
16
20
  Use one of these two buttons:
17
21
 
18
22
  - In the **Set up** list of the Reports tab, press **Design how it looks** on the row of the report.
@@ -0,0 +1,91 @@
1
+ ---
2
+ type: Guide
3
+ title: "Shape what a report says: the Structure step"
4
+ description: "How to decide with PMOS what a report answers, where each number comes from and how it is worked out, in the Structure step of the report studio, and how to try the plan on real data before you design it."
5
+ timestamp: 2026-10-14
6
+ ---
7
+
8
+ # Shape what a report says
9
+
10
+ The report studio has two steps: **1 Structure** and **2 Design**. Structure decides what the report says. Design decides how it looks. Do Structure first.
11
+
12
+ In Structure you talk with PMOS. You say what the report is for. PMOS asks what it needs and writes a report plan. The plan shows what the report answers, where each number comes from, how each number is worked out, and the order of the sections.
13
+
14
+ ## Open the Structure step
15
+
16
+ Use one of these:
17
+
18
+ - In **Ask PMOS** on the Reports tab, press **Shape it with PMOS** on the answer. PMOS sets the report up and opens Structure.
19
+ - In the **Set up** list, press **Shape the report** on the row of the report.
20
+ - In the Design studio, press **1 Structure** at the top.
21
+
22
+ To go to the Design studio from Structure, press **2 Design**.
23
+
24
+ ## Talk it through
25
+
26
+ 1. Write what the report is for, who reads it and what they decide with it. Press **Make the first version**.
27
+ 2. PMOS can ask questions first. Each question has answer chips. Press a chip to put the answer in the box. You can press more than one, and edit the words. Press **Make the next version**.
28
+ 3. PMOS makes **S1**. It can also propose up to three structures. Press **See the three structures side by side** to read them.
29
+ 4. Write what to change and press **Make the next version**. A turn that changes the plan makes a new version: S2, S3 and so on. A turn that only asks a question makes no new version.
30
+
31
+ A turn takes a minute or two. You can close the studio. The version is ready when you open it again.
32
+
33
+ ## Read the plan
34
+
35
+ The strip at the top right lists the versions. Press a version to read it. On an older version, press **Go back to** that version to build on it again. The later versions stay in the strip.
36
+
37
+ The plan has these parts:
38
+
39
+ - **What it answers.** The readers, the decision, the scope, how often, and the questions the report answers.
40
+ - **Sources.** Where the numbers come from. They are in two groups.
41
+ - **From PMOS.** A portfolio, board or initiative, a dashboard, a chart, a document, or a past report. A document gives PMOS context for its notes, never numbers. A past report gives last cycle's numbers. In the first cycle there is nothing to read.
42
+ - **Outside PMOS.** A connector you have (for example Google Analytics through Supermetrics), a link, a file you upload each cycle, a person who gives a number each cycle, a page read in Chrome, or an app on your Mac.
43
+ - Each source shows its state: **Reached**, **Not connected**, **First cycle: nothing to read** or **Needs you**. It also shows how it is read and how many charts use it. Press **Test pull** to ask PMOS to read it now. Press **Check it works** on a Chrome page or an app on your Mac. Press **How to connect** for a connector that is not connected. Press **Open** for a source in PMOS.
44
+ - **Charts.** Every number is a chart: one question, its source, how it is worked out, what it is compared with, and when it is flagged. A chart that PMOS proposes is tagged **New chart**. A chart that already exists in Reports is tagged **From Charts**. Press a chart to see the question, where it is used, exactly what PMOS reads, why PMOS proposed it and its test value. Press **Discuss this chart** to put its name in the box. Press **Open in Charts** to see a chart from Charts. Press **Remove it** to make the next version without the chart.
45
+ - **Outline.** The sections in order. Each section lists its charts and says if PMOS writes notes on them. **Headline**, **Needs your call** and **Sources** are in every report. You decide everything between them.
46
+
47
+ Press **Other structures** to read the structures PMOS proposed side by side. The one PMOS recommends is first. Press **Use this instead** to put the choice in the box, then press **Make the next version**.
48
+
49
+ ## Add a source
50
+
51
+ Press **Add a source** under the conversation. Choose the kind, write its name and press **Add**. A source from outside PMOS that needs a link asks for one. PMOS reads every source and never writes to one. A connector that your Claude settings do not allow stays closed.
52
+
53
+ ## Add a page in Chrome or an app on your Mac
54
+
55
+ Some numbers are in a page that has no connector, or in an app. PMOS can read a page with Claude in Chrome, or an app with computer use. Both run on your Mac, with your own sign-in.
56
+
57
+ When you add one, PMOS checks it step by step. Each step says **Works**, **Needs you** or **Waiting**, with a plain reason:
58
+
59
+ 1. PMOS runs on this Mac.
60
+ 2. A session can start with Claude in Chrome (or with computer use).
61
+ 3. Claude in Chrome answers (or macOS allows the screen and the mouse and keyboard).
62
+ 4. You are signed in.
63
+ 5. PMOS can read the numbers.
64
+
65
+ If a step says **Needs you**, do what it says, then press **Check again**. PMOS never types a password. The check session can only open the page, read it and scroll. It cannot submit a form or change a setting, and it closes its tab. Computer use runs only when your own Claude Code settings allow it. If they do not, the check says so.
66
+
67
+ When a draft is due, PMOS checks that source again. If the check fails, Work shows what to do. The draft waits up to 4 hours. After that it is drafted without that source and says so under Sources. PMOS never guesses a number.
68
+
69
+ ## Try it with real data
70
+
71
+ Press **Try it with real data** to run the plan on the last complete period, before any design. PMOS shows:
72
+
73
+ - the headline it would write,
74
+ - the value of each chart, with what it is compared with,
75
+ - the flags,
76
+ - what it could not get, and why,
77
+ - the options it would propose where the outline asks for a choice.
78
+
79
+ This only reads. It saves nothing in the report and sends nothing.
80
+
81
+ ## Save the structure
82
+
83
+ Press **Use this structure** to save the version on screen. PMOS saves each new chart in **Reports**, **Charts**. A chart from Charts is not copied: if you change it there, it changes everywhere it is used.
84
+
85
+ The report setup sheet then shows the saved structure with **Refine the structure** and **Try it with real data**. Each next draft follows it: the sections in order, each number worked out by the saved logic, and the sources it reached and did not reach listed under **Sources**. A change to the structure applies from the next cycle. A cycle that was already issued keeps the structure it was drafted with.
86
+
87
+ Then press **Design how it looks** to do step 2.
88
+
89
+ Nothing is sent from Structure.
90
+
91
+ See also [Design how a report looks](report-design.md), [Set up a report by asking](reports-set-up.md) and [Reports: dashboards of reports](reports-dashboards.md).