create-pmos 0.192.0 → 0.194.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 (47) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/okf/core/concepts/frontmatter-format.md +3 -2
  4. package/payload/planning/evals/eval-calibration.md +1 -1
  5. package/payload/planning/okrs/health-budgets.md +1 -1
  6. package/payload/state/discoveries.md +1 -1
  7. package/workspace/c3-core-docs-test.js +152 -0
  8. package/workspace/guide/console-calendar.md +1 -1
  9. package/workspace/guide/core-documents.md +49 -0
  10. package/workspace/guide/index.md +2 -1
  11. package/workspace/guide/initiative-boards.md +14 -0
  12. package/workspace/lib/core-docs-needs.js +86 -0
  13. package/workspace/lib/core-docs.js +675 -0
  14. package/workspace/lib/coredoc-cli.js +90 -0
  15. package/workspace/lib/doc-category.js +1 -1
  16. package/workspace/lib/driver-cli.js +16 -2
  17. package/workspace/lib/driver-decide.js +8 -1
  18. package/workspace/lib/driver-facts.js +8 -2
  19. package/workspace/lib/driver-run.js +6 -0
  20. package/workspace/lib/driver.js +32 -4
  21. package/workspace/lib/headless.js +13 -3
  22. package/workspace/lib/needs-providers.js +1 -0
  23. package/workspace/lib/pmos-verbs.js +3 -0
  24. package/workspace/lib/route-modules.js +1 -0
  25. package/workspace/lib/routes-core-docs.js +171 -0
  26. package/workspace/lib/routes-driver.js +1 -0
  27. package/workspace/lib/routes-meeting-focus.js +23 -5
  28. package/workspace/lib/vendor/scripts/migrate-layout.js +1 -1
  29. package/workspace/lib/vendor/workspace/lib/doc-category.js +1 -1
  30. package/workspace/package-lock.json +2 -2
  31. package/workspace/package.json +1 -1
  32. package/workspace/public/pm/c3-brief-needs.js +3 -0
  33. package/workspace/public/pm/c3-core-docs.css +18 -0
  34. package/workspace/public/pm/c3-core-docs.js +234 -0
  35. package/workspace/public/pm/c3-demo-coredocs.js +211 -0
  36. package/workspace/public/pm/c3-demo.js +2 -1
  37. package/workspace/public/pm/c3-driver.js +6 -4
  38. package/workspace/public/pm/c3-focus.js +6 -4
  39. package/workspace/public/pm/c3-ib-docs.js +24 -14
  40. package/workspace/public/pm/c3-ii-tabs.js +15 -8
  41. package/workspace/public/pm/c3-initiatives.js +29 -2
  42. package/workspace/public/pm/c3-shell.js +1 -1
  43. package/workspace/public/pm/shell-recording.js +2 -3
  44. package/workspace/server.js +15 -2
  45. package/workspace/skills/initiative-drive.skill +6 -2
  46. package/workspace/test-lists/c3-core-docs.json +9 -0
  47. package/workspace/test-lists/core-documents-backend.json +5 -0
@@ -55,14 +55,32 @@ function linesOf(runtimeDir, keys) {
55
55
 
56
56
  /* ── the meeting, its note and its brief ──────────────────────────────────────────────────────────────────────────── */
57
57
 
58
+ const minuteOf = (t) => { const ms = Date.parse(t); return Number.isFinite(ms) ? Math.floor(ms / 60000) : null; };
59
+ const normTitle = (t) => require('./calendar-memory.js').normTitle(t);
60
+ /** A meeting named by its start and title (a recording whose meeting has no stored id). The start is compared as an instant, not as
61
+ * text: a note keeps its meeting's start in UTC ("…T07:00:00.000Z") while the calendar writes it with the local offset ("…T15:00:00+08:00").
62
+ * Same minute and same title first, then the only meeting of that title on that local day (a meeting moved after the note linked it). */
63
+ function findByStart(events, by) {
64
+ const m = minuteOf(by.at); if (m === null) return null;
65
+ const rows = (events || []).filter((e) => e && e.id && minuteOf(e.at) !== null);
66
+ const title = by.title ? normTitle(by.title) : '';
67
+ const atMin = rows.filter((e) => minuteOf(e.at) === m);
68
+ const both = title ? atMin.filter((e) => normTitle(e.title) === title) : atMin;
69
+ if (both.length) return both[0];
70
+ if (!title) return null;
71
+ const day = localDay(m * 60000);
72
+ const named = rows.filter((e) => normTitle(e.title) === title && localDay(Date.parse(e.at)) === day);
73
+ return named.length === 1 ? named[0] : null;
74
+ }
58
75
  function findEvent(events, id, date, by) {
59
- if (!id && by && by.at) { // a meeting without a stored id: its start and title name it
60
- const hit = (events || []).filter((e) => e && e.id && e.at === String(by.at) && (!by.title || e.title === String(by.title)));
61
- return hit[0] || null;
62
- }
76
+ if (!id && by && by.at) return findByStart(events, by);
63
77
  const rows = (events || []).filter((e) => e && e.id && (String(e.id) === id || baseId(e.id) === baseId(id)));
64
78
  const onDay = rows.filter((e) => !date || String(e.at || '').slice(0, 10) === date);
65
- return onDay.find((e) => String(e.id) === id) || onDay[0] || null;
79
+ const hit = onDay.find((e) => String(e.id) === id) || onDay[0] || null;
80
+ if (hit) return hit;
81
+ // a date that is the UTC day of the start, not its local day (an early-morning meeting east of UTC): the start names it
82
+ if (by && by.at) return findByStart(events, by);
83
+ return rows.find((e) => String(e.id) === id) || null; // an occurrence id names one meeting whatever the day says
66
84
  }
67
85
  /** A recording that is not on the calendar (a talk in the corridor): the note stands in for the meeting. It has no end, no people
68
86
  * and no brief, so focus shows what is said, PMOS's notes and Captured, and no topics from an invite. */
@@ -67,7 +67,7 @@ function registry() {
67
67
  try {
68
68
  REGISTRY = new Set(fs.readFileSync(path.join(PMOS_ROOT, 'scripts', 'frontmatter_registry.txt'), 'utf8')
69
69
  .split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')));
70
- } catch { REGISTRY = new Set(['Concept', 'ADR', 'Template', 'Playbook', 'Reference', 'Initiative', 'PRD', 'Story', 'Rubric', 'OKR', 'Radar', 'Checklist', 'Calibration', 'Research', 'Design', 'Prototype', 'Handover', 'Decision', 'Principle', 'Debt', 'Discovery', 'Guide']); }
70
+ } catch { REGISTRY = new Set(['Concept', 'ADR', 'Template', 'Playbook', 'Reference', 'Initiative', 'PRD', 'Story', 'Rubric', 'OKR', 'Radar', 'Checklist', 'Calibration', 'Research', 'Design', 'Prototype', 'Handover', 'Review', 'Decision', 'Principle', 'Debt', 'Discovery', 'Guide']); }
71
71
  return REGISTRY;
72
72
  }
73
73
 
@@ -47,7 +47,7 @@ const TYPE_TO_CATEGORY = {
47
47
  Concept: 'knowledge', ADR: 'spec-artifact', Template: 'other', Playbook: 'knowledge', Reference: null,
48
48
  Initiative: 'spec-artifact', PRD: 'prd', Story: 'spec-artifact', Rubric: 'spec-artifact', OKR: 'spec-artifact',
49
49
  Radar: 'report', Checklist: 'other', Calibration: 'report', Research: null, Design: 'spec-artifact',
50
- Prototype: 'other', Handover: 'report',
50
+ Prototype: 'other', Handover: 'report', Review: 'report',
51
51
  Decision: 'spec-artifact', Principle: 'knowledge', Debt: 'report', Discovery: 'report', Guide: 'knowledge',
52
52
  };
53
53
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.192.0",
3
+ "version": "0.194.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.192.0",
9
+ "version": "0.194.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.192.0",
3
+ "version": "0.194.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",
@@ -21,6 +21,7 @@ import { cardView, TAG_STYLE } from './briefing-model.js';
21
21
  import { onBriefPage } from './c3-brief-data.js';
22
22
  import { button, ensureComponentsCss } from './primitives.js';
23
23
  import { loadDriverNeeds, needsRow as driverRow } from './c3-driver.js';
24
+ import { needsLoc } from './c3-core-docs.js';
24
25
 
25
26
  const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
26
27
  const post = (path, body) => pmPost(path, body).catch(() => ({ ok: false }));
@@ -175,6 +176,8 @@ export function needsSection(S) {
175
176
  /* the agent's rows: drafts waiting on the PM, a target the PM is blocking. A press opens the initiative (its Overview holds the strip) or Work. */
176
177
  let agentSig = '';
177
178
  const goAgent = (n) => {
179
+ const docsLoc = n.kind === 'core-doc-drafts' ? needsLoc(n) : null;
180
+ if (docsLoc) { try { const u = new URL(location.href); u.searchParams.set('init', docsLoc); history.replaceState(history.state, '', u); } catch { /* no history */ } if (hooks.go) hooks.go({ action: 'goinit' }); try { window.dispatchEvent(new CustomEvent('pmos:ini-go', { detail: { loc: docsLoc } })); } catch { /* the page reads the address when it mounts */ } return; }
178
181
  if (n.initiative_id) { try { const u = new URL(location.href); u.searchParams.set('init', 'ini:' + encodeURIComponent(n.ws) + ':' + encodeURIComponent(n.initiative_id) + ':overview'); history.replaceState(history.state, '', u); } catch { /* no history */ } }
179
182
  if (hooks.go) hooks.go({ action: n.initiative_id ? 'goinit' : 'gowork' });
180
183
  };
@@ -0,0 +1,18 @@
1
+ /* pm/c3-core-docs.css — the core documents of a level (c3-core-docs.js): the agent colour for a draft, the actions under a row, the
2
+ * panel above a draft in the document sheet. Scoped to .cd-host and .cd-panel; tokens only. */
3
+ .c3 .cd-host { display: contents; }
4
+ .c3 .cd-host .nrow > .acts { grid-column: 2; display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-start; align-items: flex-start; }
5
+ .c3 .cd-host .st.cd-ag, .c3 .cd-panel .st.cd-ag { color: var(--agent); border-color: color-mix(in oklab, var(--agent) 45%, transparent); background: color-mix(in oklab, var(--agent) 8%, transparent); }
6
+ .c3 .cd-host .chip2.cd-agc { color: var(--agent); }
7
+ .c3 .cd-panel { display: grid; gap: var(--sp-2); min-width: 0; }
8
+ .c3 .cd-panel .fold { border-color: color-mix(in oklab, var(--agent) 45%, transparent); }
9
+ .c3 .cd-oh { margin-top: var(--sp-3); }
10
+ /* The quiet pages (c3-quiet.css) hide teaching sentences and every chip after the second. The approved mockup of the Docs views keeps both:
11
+ * a row says what the document is for, who reads it, how often it changes and who writes it. */
12
+ .c3 .cd-host .ini-note, .c3 .cd-panel .ini-note { display: block !important; }
13
+ .c3 .cd-host .nrow .nb > .chips > :nth-child(n+3) { display: inline-flex !important; }
14
+ .c3 [data-ini="pf-docs"] .ini-lhm .dim { display: block !important; }
15
+ .c3 .cd-host .sect .sh2 .ini-meta { display: inline !important; }
16
+ /* the heading above the board's other documents: the look of a section heading, without a card around the search and the groups */
17
+ .c3 .cd-host .cd-oh { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: var(--sp-3) 0 var(--sp-2); border-bottom: 1px solid var(--border); }
18
+ .c3 .cd-host .cd-oh h2 { margin: 0; font: 500 var(--fs-2) var(--mono); color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .06em; display: flex; gap: 8px; align-items: center; }
@@ -0,0 +1,234 @@
1
+ /* pm/c3-core-docs.js — the core documents of a level (initiative core-documents, PRD section 6; the approved mockup 43-doc-levels.js).
2
+ * Each level answers one question for one reader: Portfolio, Board, Initiative. Its Docs view starts with the level's core set, from
3
+ * GET /api/core-docs?ws&level=portfolio|board|initiative&board&id { ok, level, scope, question, reader, rhythm, build, wfTpl, stage, cat,
4
+ * summary, docs: [entry + state] }
5
+ * POST /api/core-docs/draft { ws, scope, key } { ok, run } Draft it, and Redraft ("Ask PMOS to change it")
6
+ * POST /api/core-docs/issue { ws, scope, key, slot } { ok } Issue it
7
+ * The server holds the sets, the states and the drafts; this file draws them and holds nothing.
8
+ *
9
+ * coreDocs(env) -> { left, right, dispose } two hosts to put in the columns of cols(): the sections and the cards. env =
10
+ * { api, level, board?, id?, ws, today, toast, nav: { tab(tab), tool(tool), reports() }, boardRow?, iniDocs?: { docs, initiatives } }
11
+ * needsLoc(row) -> the `init=` address of the Docs view a Briefing row (kind core-doc-drafts) opens
12
+ * segBadge(n) the count badge of the Portfolio's Documents segment
13
+ * A press on a row opens `open`: { path } the document sheet, { tab } the board tab, { tool } the initiative's Overview tool, { reports } Reports.
14
+ * Review the draft opens the draft in the document sheet. The sheet holds the actions: a panel above the draft (docAbove, the sheet's own hook)
15
+ * with Issue it and Ask PMOS to change it, which asks for a new draft. Nothing is sent to anyone; the PM's press issues it. */
16
+ import { el } from './dom.js';
17
+ import { icon, pbtn, lk, chip, tag, st, note, sect, card, list, nrow, plural, day, ago, sw, chgl, mono, put, ensureIniCss } from './c3-ini-ui.js';
18
+
19
+ const BY = { you: 'You write it', proposes: 'PMOS proposes', drafts: 'PMOS drafts it', files: 'PMOS files it', watches: 'PMOS watches it', keeps: 'PMOS keeps it current', live: 'Live view', reviews: 'PMOS reviews, you accept' };
20
+ const AGENT_BY = ['drafts', 'proposes', 'files', 'watches', 'keeps', 'reviews'];
21
+ const CAT_IC = { Discover: 'search', Plan: 'cal', Do: 'bolt', Learn: 'up' };
22
+ const CAT_ORDER = ['Discover', 'Plan', 'Do', 'Learn'];
23
+ const POLL_MS = 4000;
24
+ const PENDING_MS = 1500;
25
+
26
+ function ensureCss() {
27
+ if (typeof document === 'undefined' || document.querySelector('link[data-c3-cd]')) return;
28
+ ensureIniCss();
29
+ const l = document.createElement('link');
30
+ l.rel = 'stylesheet'; l.href = new URL('./c3-core-docs.css', import.meta.url).href; l.setAttribute('data-c3-cd', '');
31
+ document.head.appendChild(l);
32
+ }
33
+
34
+ /** The count badge of the Portfolio's Documents segment (hidden at 0). */
35
+ export function segBadge(n) { return el('span', { class: 'bdg', 'data-cd': 'seg-badge', ...(n ? {} : { hidden: '' }) }, String(n || 0)); }
36
+
37
+ /** The address of the Docs view a Briefing row opens, as the `init=` value (c3-initiatives.js parseLoc). Null when the row names no scope. */
38
+ export function needsLoc(row) {
39
+ const r = row || {};
40
+ const raw = String(r.scope || (/(?:^|:)(portfolio|board:[^:|]+|ini(?:tiative)?:[^:|]+)$/.exec(String(r.run_id || '')) || [])[1] || '');
41
+ if (raw == 'portfolio') return 'docs';
42
+ let m = /^board:(.+)$/.exec(raw);
43
+ if (m) return 'board:' + encodeURIComponent(m[1]) + ':docs';
44
+ m = /^ini(?:tiative)?:(.+)$/.exec(raw);
45
+ const id = m ? m[1] : r.initiative_id;
46
+ if (id) return 'ini:' + encodeURIComponent(r.board || r.ws || '') + ':' + encodeURIComponent(id) + ':docs';
47
+ return null;
48
+ }
49
+
50
+ /* ── the sheet's draft panel ── */
51
+ const DRAFTS = new Map(); // path -> { ws, api, scope, key, slot, toast, ui, done }
52
+ const NODES = new WeakMap();
53
+ let hooked = null;
54
+ function hookSheet() {
55
+ if (!hooked) hooked = import('./c3-doc-sheet.js').then((m) => { m.docAbove.push(({ doc, key }) => { const e = DRAFTS.get(doc.path); if (!e) return null; if (!NODES.has(key)) NODES.set(key, draftPanel(e)); return NODES.get(key); }); return m; });
56
+ return hooked;
57
+ }
58
+ function draftPanel(e) {
59
+ const msg = el('div', { class: 'fold w', role: 'alert', hidden: '', 'data-cd': 'panel-error' });
60
+ const fail = (t) => { msg.textContent = t; msg.hidden = false; };
61
+ let busy = false;
62
+ const act = (path, body, done) => async () => {
63
+ if (busy) return; busy = true; msg.hidden = true;
64
+ const r = await e.api.post(path, { scope: e.scope, key: e.key, ...body }).catch(() => null);
65
+ busy = false;
66
+ if (!r || !r.ok) { fail((r && r.error) || 'no answer'); return; }
67
+ done(); if (e.ui && e.ui.close) e.ui.close();
68
+ if (e.done) e.done();
69
+ };
70
+ const issue = act('/api/core-docs/issue', { slot: e.slot }, () => { DRAFTS.delete(e.path); e.toast('Issued. Nothing was sent to anyone.'); });
71
+ const redraft = act('/api/core-docs/draft', {}, () => { e.toast('PMOS is drafting it again. The new draft replaces this one when it is ready.'); });
72
+ return el('div', { class: 'cd-panel', 'data-cd': 'draft-panel' },
73
+ el('div', { class: 'fold' }, icon('spark', 'sm'), el('span', null, ' PMOS drafted this from its sources. Nothing is sent. Read it, change what you like, then issue it.')),
74
+ el('div', { class: 'chips2' }, pbtn('primary', 'Issue it', 'check', { 'data-cd-issue': e.key, onclick: issue }), pbtn('outline', 'Ask PMOS to change it', 'spark', { 'data-cd-redraft': e.key, onclick: redraft })),
75
+ msg);
76
+ }
77
+
78
+ /* ── rows ── */
79
+ const summaryLine = (docs) => {
80
+ const n = (s) => docs.filter((d) => d.st === s).length;
81
+ return [n('draft') ? plural(n('draft'), 'draft') + ' to review' : '', n('due') ? n('due') + ' due' : '', n('missing') ? n('missing') + ' not started' : ''].filter(Boolean).join(', ') || 'Everything is up to date';
82
+ };
83
+ const isAgent = (d) => d.running || d.st === 'draft';
84
+
85
+ function stChip(d) {
86
+ if (d.running) return el('span', { class: 'st cd-ag', 'data-cd-state': 'running' }, icon('spark', 'sm'), 'Agent drafting');
87
+ const a = { 'data-cd-state': d.st };
88
+ switch (d.st) {
89
+ case 'ok': return st('ok', 'check', 'Up to date', a);
90
+ case 'due': return st('warn', 'clock', d.due ? 'Due ' + day(d.due) : 'Due now', a);
91
+ case 'draft': return el('span', { class: 'st cd-ag', ...a }, icon('spark', 'sm'), 'Draft ready' + (d.due ? ', due ' + day(d.due) : ''));
92
+ case 'missing': return st('bad', 'alert', 'Not started', a);
93
+ case 'stale': return st('warn', 'clock', 'Out of date', a);
94
+ default: return el('span', { class: 'chip2 dim', ...a }, 'Comes later');
95
+ }
96
+ }
97
+ const byChip = (k) => (AGENT_BY.includes(k) ? el('span', { class: 'chip2 cd-agc' }, icon('spark', 'sm'), BY[k]) : chip(icon(k === 'live' ? 'eye' : 'user', 'sm'), BY[k] || k));
98
+
99
+ function pathNameOf(env, d) { return d.title + (env.scopeLabel ? ' · ' + env.scopeLabel : ''); }
100
+
101
+ async function openSheetFor(env, d, path, draft) {
102
+ try {
103
+ const m = await hookSheet();
104
+ const ui = m.openDocSheet({ ws: env.ws, doc: { path, name: pathNameOf(env, d), ty: draft ? 'Draft' : 'Document', v: '', st: draft ? 'draft' : d.st, ini: env.level === 'initiative' ? env.id : '', iniName: '' },
105
+ library: () => env.api.get('/api/docs/library', env.board ? { board: env.board } : {}) });
106
+ if (draft) { const e = DRAFTS.get(path); if (e) e.ui = ui; }
107
+ return ui;
108
+ } catch { env.toast('The document sheet could not open. The file is ' + path + '.'); return null; }
109
+ }
110
+
111
+ function press(env, d, redraw) {
112
+ const o = d.open || null;
113
+ if (!o) return null;
114
+ if (o.path) return () => openSheetFor(env, d, o.path, false);
115
+ if (o.tab) return () => env.nav.tab(o.tab);
116
+ if (o.tool) return () => env.nav.tool(o.tool);
117
+ if (o.reports) return () => env.nav.reports();
118
+ return null;
119
+ }
120
+
121
+ function review(env, d, redraw) {
122
+ const p = d.draft && d.draft.path;
123
+ if (!p) return () => env.toast('The draft has no file yet. Try again in a moment.');
124
+ return () => {
125
+ DRAFTS.set(p, { path: p, ws: env.ws, api: env.api, scope: env.scope, key: d.key, slot: d.draft.slot, toast: env.toast, ui: null, done: redraw });
126
+ openSheetFor(env, d, p, true);
127
+ };
128
+ }
129
+
130
+ async function draftIt(env, d, redraw) {
131
+ const r = await env.api.post('/api/core-docs/draft', { scope: env.scope, key: d.key }).catch(() => null);
132
+ if (!r || !r.ok) { env.toast('The draft did not start: ' + ((r && r.error) || 'no answer') + '.'); return; }
133
+ env.toast('PMOS is drafting the ' + d.title.toLowerCase() + '. It lands here as a draft for you to review.');
134
+ redraw();
135
+ }
136
+
137
+ function coreRow(env, d, redraw) {
138
+ const open = press(env, d, redraw);
139
+ const meta = [d.when && !['later', 'missing'].includes(d.st) ? chip(icon('clock', 'sm'), (d.st === 'draft' ? 'drafted ' : 'updated ') + ago(d.when, env.today)) : null,
140
+ d.flag ? st('warn', 'alert', d.flag) : null, d.staleWhy && d.st === 'stale' ? chip(d.staleWhy) : null];
141
+ let action = null;
142
+ if (!d.running && d.st === 'draft') action = pbtn('primary', 'Review the draft', 'spark', { 'data-cd-review': d.key, onclick: review(env, d, redraw) });
143
+ else if (!d.running && d.kind === 'doc' && d.by !== 'you' && (d.st === 'missing' || d.st === 'stale')) action = pbtn('outline', 'Draft it', 'spark', { 'data-cd-draft': d.key, onclick: () => draftIt(env, d, redraw) });
144
+ return nrow({ ic: d.ic || 'doc', hot: isAgent(d) || d.st === 'missing', title: [d.title, d.isNew ? [' ', tag('new')] : null], onOpen: open || undefined, openLabel: open ? 'Open ' + d.title : undefined,
145
+ titleAttrs: { 'data-cd-open': d.key }, sub: d['for'] || '',
146
+ chips: [stChip(d), d.cadence ? chip(icon('cal', 'sm'), d.cadence) : null, d.reader ? chip(icon('user', 'sm'), d.reader) : null, byChip(d.by), ...meta],
147
+ right: [action], attrs: { 'data-cd-row': d.key, 'data-cd-st': d.running ? 'running' : d.st } });
148
+ }
149
+
150
+ /* ── the sections ── */
151
+ function coreSection(env, data, redraw) {
152
+ return sect({ ic: 'doc', title: 'Core documents', count: data.docs.length, attrs: { 'data-cd': 'core' }, right: mono(summaryLine(data.docs)) },
153
+ list(data.docs.map((d) => coreRow(env, d, redraw)), { 'data-cd': 'core-list' }));
154
+ }
155
+
156
+ function iniSections(env, data, redraw) {
157
+ const cur = data.cat || 'Plan';
158
+ return CAT_ORDER.map((c) => {
159
+ const ds = data.docs.filter((d) => d.cat === c);
160
+ if (!ds.length) return null;
161
+ const ic = CAT_IC[c];
162
+ const a = (extra) => ({ 'data-cd-cat': c, ...extra });
163
+ if (ds.every((d) => d.st === 'later')) return sect({ ic, title: c, count: ds.length, attrs: a({ 'data-cd-fold': 'later' }), right: chip('Comes later') },
164
+ el('div', { class: 'chips2' }, ...ds.map((d) => el('span', { class: 'chip2 dim' }, icon(d.ic || 'doc', 'sm'), d.title))));
165
+ if (c !== cur && ds.every((d) => d.st === 'ok' && !d.running)) return sect({ ic, title: c, count: ds.length, attrs: a({ 'data-cd-fold': 'done' }), right: st('ok', 'check', 'All ' + ds.length + ' up to date') },
166
+ el('div', { class: 'chips2' }, ...ds.map((d) => { const o = press(env, d, redraw); return o ? lk({ onclick: o, 'data-cd-chip': d.key }, icon(d.ic || 'doc', 'sm'), d.title) : el('span', { class: 'chip2' }, icon(d.ic || 'doc', 'sm'), d.title); })));
167
+ return sect({ ic, title: c + (c === cur ? ' · now' : ''), count: ds.length, attrs: a({ 'data-cd-fold': 'open' }), right: chip(sw(c), ds.filter((d) => d.st === 'ok').length + ' of ' + ds.length + ' done') },
168
+ list(ds.map((d) => coreRow(env, d, redraw))));
169
+ }).filter(Boolean);
170
+ }
171
+
172
+ /* ── the cards of the right column ── */
173
+ const levelCard = (data, extra) => card({ ic: 'info', title: 'What this level is for', attrs: { 'data-cd': 'level' } },
174
+ el('p', { class: 'ini-p' }, el('b', null, data.question || '')),
175
+ note('Read by ' + (data.reader || 'the team') + '. Changes ' + (data.rhythm || 'as it changes') + '.' + (data.level == 'portfolio' ? ' Boards and initiatives link up to it through their key result, so nothing here repeats what a board says.' : '')),
176
+ ...extra);
177
+
178
+ function draftsCard(env, data, redraw) {
179
+ const ds = data.docs.filter((d) => d.st === 'draft' && !d.running);
180
+ return card({ ic: 'spark', title: 'Drafts waiting for you', attrs: { 'data-cd': 'drafts' } },
181
+ ds.length ? chgl(ds.map((d) => el('li', null, icon('spark', 'sm'), el('span', null, lk({ onclick: review(env, d, redraw), 'data-cd-wait': d.key }, d.title), d.due ? ' due ' + day(d.due) : ''), tag('Briefing'))))
182
+ : note('No draft is waiting.'),
183
+ note('Drafts also show as one Briefing card. Nothing is sent until you issue it.'));
184
+ }
185
+ const connectCard = () => card({ ic: 'init', title: 'How the levels connect', attrs: { 'data-cd': 'connect' } }, chgl([
186
+ el('li', null, icon('alert', 'sm'), el('span', null, 'Risks are written once, on the initiative. Board risks and cross-board risks collect them.')),
187
+ el('li', null, icon('up', 'sm'), el('span', null, 'Each initiative\'s outcome review feeds the board retro and the quarterly review.')),
188
+ el('li', null, icon('report', 'sm'), el('span', null, 'Status reports at every level come from the same dashboard, so they agree.'))]));
189
+
190
+ function rightCards(env, data, redraw) {
191
+ if (data.level == 'portfolio') return [levelCard(data, []), draftsCard(env, data, redraw), connectCard()];
192
+ if (data.level === 'board') {
193
+ const b = env.boardRow || {};
194
+ const ini = env.iniDocs;
195
+ return [levelCard(data, [note(data.build ? 'Build and deliver is on, so the set adds the sprint review and release notes' + (b.jira ? ' from Jira ' + b.jira : '') + '.' : 'Build and deliver is off, so there is no sprint review or release notes. Progress comes from PMOS tasks.')]),
196
+ card({ ic: 'doc', title: 'Initiative documents', attrs: { 'data-cd': 'ini-docs' } },
197
+ note('Each initiative keeps its own PRD, rubric, risks and outcome review on its Docs tab.' + (ini ? ' ' + plural(ini.docs, 'document') + ' across ' + plural(ini.initiatives, 'initiative') + '.' : '')))];
198
+ }
199
+ return [levelCard(data, [note('The set grows with the workflow: each stage adds the documents it needs, and later stages wait.'), data.wfTpl === 'research' ? note('Research only: nothing gets built, so there is no PRD, rubric, design or launch checklist.') : null]),
200
+ card({ ic: 'alert', title: 'Order rules', attrs: { 'data-cd': 'order' } }, note('A missing PRD or rubric before Build shows as a warning on the initiative, never a block. The CI gate still needs a PRD and a rubric before a pull request merges.'))];
201
+ }
202
+
203
+ /** The two hosts of a level's core documents. Reads the set, draws it, and reads it again after a press and while an agent drafts. */
204
+ export function coreDocs(env) {
205
+ ensureCss();
206
+ const left = el('div', { class: 'cd-host', 'data-cd-host': 'left' }, note('Reading the core documents'));
207
+ const right = el('div', { class: 'cd-host', 'data-cd-host': 'right' });
208
+ let dead = false; let timer = null; let seq = 0; let last = null;
209
+ const get = () => env.api.get('/api/core-docs', { level: env.level, board: env.level == 'portfolio' ? '' : env.board, id: env.level === 'initiative' ? env.id : '' });
210
+ async function load() {
211
+ const token = ++seq;
212
+ const r = await get().catch(() => null);
213
+ if (dead || token !== seq) return;
214
+ clearTimeout(timer);
215
+ if (r && r.pending) { // the initiative rows are still loading: say so and ask again
216
+ put(left, note('Reading the core documents')); put(right);
217
+ timer = setTimeout(() => { if (!dead && left.isConnected) load(); }, PENDING_MS); return;
218
+ }
219
+ if (!r || !r.ok || !Array.isArray(r.docs)) {
220
+ put(left, el('div', { class: 'fold w', role: 'alert', 'data-cd': 'error' }, icon('alert', 'sm'), 'The core documents could not be read: ' + ((r && r.error) || 'no answer') + '.'));
221
+ put(right); return;
222
+ }
223
+ const redraw = () => { load(); };
224
+ last = { r, redraw };
225
+ put(left, ...(env.level === 'initiative' ? iniSections({ ...env, scope: r.scope }, r, redraw) : [coreSection({ ...env, scope: r.scope }, r, redraw)]));
226
+ put(right, ...rightCards({ ...env, scope: r.scope }, r, redraw));
227
+ if (env.onData) env.onData(r);
228
+ if (r.docs.some((d) => d.running)) timer = setTimeout(() => { if (!dead && left.isConnected) load(); }, POLL_MS);
229
+ }
230
+ load();
231
+ return { left, right, dispose() { dead = true; clearTimeout(timer); }, reload: load,
232
+ /** The counts of the board's other documents, for the Initiative documents card. */
233
+ setIniDocs(v) { env.iniDocs = v; if (last) put(right, ...rightCards({ ...env, scope: last.r.scope }, last.r, last.redraw)); } };
234
+ }
@@ -0,0 +1,211 @@
1
+ /* pm/c3-demo-coredocs.js — DEMO ONLY (/c3.html?demo=1): the core documents of each level (initiative core-documents; the approved mockup
2
+ * 43-doc-levels.js). It answers GET /api/core-docs with the shape of the route (the three sets, a state for each entry, drafts), the three
3
+ * POSTs (draft, issue, drop) and the documents the rows open (GET /api/doc and the sheet's revisions, proposals and log for
4
+ * planning/core-docs/...). A write changes the in-memory state only. A draft that was asked for shows "Agent drafting" for a few seconds, then
5
+ * becomes a draft to review; an issue turns a draft into an up-to-date document.
6
+ *
7
+ * The mockup's boards and initiatives are mapped to the ones this demo holds (c3-demo-boards.js): Microsite (Build and deliver off) is
8
+ * "Office move"; IDPF-15 (in Build) is IDPF-12; IDPF-21 (research only, Discover) is IDPF-21; IDPF-09 (in Evaluate, with an outcome review
9
+ * draft) is IDPF-62, an Evaluate initiative of the same board. Dates come from the demo's today.
10
+ * Wiring: c3-demo.js calls installCoreDocs() after installBoards(), so this wrapper is outermost. Real mode never loads this file. */
11
+ import { TODAY } from './c3-demo-data.js';
12
+
13
+ const DAY = 86400000;
14
+ const ymd = (n) => { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(TODAY); const t = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]) + n * DAY); return t.toISOString().slice(0, 10); };
15
+
16
+ const SCOPE_Q = { portfolio: 'Are we betting on the right things?', board: 'Is this area moving, and what comes next?', initiative: 'What are we building, why, and did it work?' };
17
+ const READER = { portfolio: 'leadership', board: 'the board owner, the team and the engineering lead', initiative: 'you, the builders and the reviewers' };
18
+ const RHYTHM = { portfolio: 'each quarter, with a monthly pulse', board: 'every sprint or two weeks', initiative: 'at each stage' };
19
+
20
+ /* the core sets: key, title, ic, for, reader, cadence, by, kind (doc | live | report), cat, isNew, state, when, due, flag, home */
21
+ const PF = [
22
+ { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'you', st: 'ok', w: -9, flag: '2 key results do not trace to it', isNew: true },
23
+ { key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', st: 'ok', w: -12 },
24
+ { key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', st: 'draft', w: 0, due: 4 },
25
+ { key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'draft', w: 0, due: 4, isNew: true },
26
+ { key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', st: 'ok', w: -1, kind: 'report' },
27
+ { key: 'status', title: 'Portfolio status report', ic: 'report', for: 'One page: progress on key results, what changed, what needs a decision.', reader: 'Leadership', cadence: 'Every two weeks', by: 'drafts', st: 'due', w: -10, due: 4, kind: 'report' },
28
+ { key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', st: 'ok', w: -2 },
29
+ { key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', st: 'ok', w: -30, flag: '1 tripped' },
30
+ { key: 'prisks', title: 'Cross-board risks', ic: 'alert', for: 'The few risks that span boards, rolled up from the risks each initiative records.', reader: 'Leadership, board owners', cadence: 'Monthly', by: 'drafts', st: 'ok', w: -3, isNew: true },
31
+ { key: 'quarter', title: 'Quarterly review', ic: 'up', for: 'What landed, what moved the key results, and what we learned. Built from outcome reviews.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'ok', w: -6, isNew: true },
32
+ { key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', st: 'ok', w: -20 },
33
+ ];
34
+ const BD = [
35
+ { key: 'charter', title: 'Board charter', ic: 'flag', for: 'Why this board exists: purpose, owner, the key results it serves, scope in and out, how work flows.', reader: 'Owner, team, new joiners', cadence: 'When the board changes', by: 'drafts', st: 'ok', w: -15, isNew: true },
36
+ { key: 'backlog', title: 'Roadmap and backlog', ic: 'todo', for: 'Initiatives by stage, horizon and owner.', reader: 'Team, stakeholders', cadence: 'Live', by: 'live', st: 'ok', kind: 'live', tab: 'initiatives' },
37
+ { key: 'intake', title: 'Intake log', ic: 'inbox', for: 'Asks received, accepted or declined, and why.', reader: 'Owner, requesters', cadence: 'Live', by: 'live', st: 'ok', kind: 'live', tab: 'triage' },
38
+ { key: 'bstatus', title: 'Board status report', ic: 'report', for: 'Progress, slips, asks and what needs a decision, for the owner\'s manager.', reader: 'Stakeholders', cadence: 'Every two weeks', by: 'drafts', st: 'draft', w: 0, due: 2, kind: 'report' },
39
+ { key: 'capacity', title: 'Capacity plan', ic: 'user', for: 'Who works on the board, their points a week, and whether the plan fits.', reader: 'Owner, eng lead', cadence: 'Each sprint', by: 'drafts', st: 'ok', w: -6, flag: 'Ana is at 112%', isNew: true },
40
+ { key: 'metrics', title: 'Board metrics', ic: 'up', for: 'The few numbers this board moves, with one definition each.', reader: 'Team', cadence: 'Quarterly', by: 'keeps', st: 'ok', w: -40 },
41
+ { key: 'brisks', title: 'Board risks', ic: 'alert', for: 'Risks on this board\'s initiatives and the waits on other boards. Written on each initiative, collected here.', reader: 'Owner', cadence: 'Every two weeks', by: 'drafts', st: 'ok', w: -1, isNew: true },
42
+ { key: 'sprint', title: 'Sprint review', ic: 'clock', for: 'What the sprint committed and finished, what slipped and why.', reader: 'Team, stakeholders', cadence: 'Each sprint', by: 'drafts', st: 'draft', w: 0, build: true, isNew: true },
43
+ { key: 'release', title: 'Release notes', ic: 'out', for: 'What each release holds, every line cited.', reader: 'Team, support, customers', cadence: 'Each release', by: 'drafts', st: 'due', due: 3, build: true, kind: 'live', tab: 'releases' },
44
+ { key: 'retro', title: 'Board retro', ic: 'undo', for: 'Patterns across this board\'s initiatives, from their learnings and outcome reviews.', reader: 'Team', cadence: 'Quarterly', by: 'drafts', st: 'ok', w: -6, isNew: true },
45
+ ];
46
+ const IN = [
47
+ { key: 'record', title: 'Initiative record', ic: 'init', cat: 'Discover', for: 'Identity, anchor, lane and stage, with links to everything else. The gate checks it.', by: 'drafts' },
48
+ { key: 'problem', title: 'Problem statement', ic: 'notes', cat: 'Discover', for: 'The problem, who has it and the evidence.', by: 'drafts', tool: 'problem' },
49
+ { key: 'research', title: 'Research notes', ic: 'search', cat: 'Discover', for: 'Interviews, data pulls and anything else the problem rests on.', by: 'drafts' },
50
+ { key: 'rice', title: 'Priority (RICE)', ic: 'scale', cat: 'Plan', for: 'Why now, compared with the rest of the board.', by: 'proposes', tool: 'rice' },
51
+ { key: 'prd', title: 'PRD', ic: 'doc', cat: 'Plan', for: 'What and why, requirements and done criteria.', by: 'drafts' },
52
+ { key: 'rubric', title: 'Eval rubric', ic: 'check', cat: 'Plan', for: 'How the result will be judged, agreed before the build.', by: 'drafts' },
53
+ { key: 'design', title: 'Design and mockup', ic: 'eye', cat: 'Plan', for: 'The approved look and flows.', by: 'drafts' },
54
+ { key: 'stories', title: 'User stories and ADRs', ic: 'tree', cat: 'Plan', for: 'Slices the builders pick up, and technical decisions with their reasons.', by: 'drafts' },
55
+ { key: 'risks', title: 'Risks', ic: 'alert', cat: 'Do', for: 'What could sink it. Rolls up to the board and the portfolio.', by: 'drafts', tool: 'risks' },
56
+ { key: 'launch', title: 'Launch checklist and comms', ic: 'out', cat: 'Do', for: 'What must be true to ship, and who hears about it.', by: 'drafts', tool: 'release' },
57
+ { key: 'review', title: 'Review verdict and acceptance', ic: 'shield', cat: 'Do', for: 'The review gate result and your sign-off.', by: 'reviews' },
58
+ { key: 'update', title: 'Status update', ic: 'report', cat: 'Do', for: 'A short update for stakeholders while it is active.', by: 'drafts', kind: 'report' },
59
+ { key: 'explainer', title: 'Explainer', ic: 'info', cat: 'Learn', for: 'What changed, in plain words, for support and new readers.', by: 'drafts' },
60
+ { key: 'outcome', title: 'Outcome review', ic: 'up', cat: 'Learn', for: 'Did it move the key result, and what we learned. Feeds the quarterly review.', by: 'drafts', isNew: true },
61
+ ];
62
+ const CAT_ORDER = ['Discover', 'Plan', 'Do', 'Learn'];
63
+ /* states the mockup gives its demo initiatives; anything not listed comes from the stage */
64
+ const INI_STATE = {
65
+ 'IDPF-12': { record: 'ok', problem: 'ok', research: 'ok', rice: 'ok', prd: 'ok', rubric: 'ok', design: 'ok', stories: 'ok', risks: 'ok', launch: 'due', review: 'later', update: 'draft' },
66
+ 'IDPF-21': { record: 'ok', problem: 'draft', research: 'due' },
67
+ 'IDPF-62': { record: 'ok', problem: 'ok', research: 'ok', rice: 'ok', prd: 'ok', rubric: 'ok', design: 'ok', stories: 'ok', risks: 'ok', launch: 'ok', review: 'ok', update: 'ok', explainer: 'ok', outcome: 'draft' },
68
+ };
69
+
70
+ /* the documents the rows open: the mockup's, as markdown; any other entry gets a short generic body */
71
+ const BODY = {
72
+ invest: '## Headline\nIDPF takes 46% of the effort and Payments 28%. Upkeep is 22% against a 20% budget. Two key results get no effort this quarter.\n\n## By board\n- IDPF: 46%\n- Payments: 28%\n- Growth: 14%\n- Office move: 6%\n- Hiring Q4: 4%\n\n## Feature work against upkeep\n- Feature (anchored to a key result): 78%\n- Upkeep (health budgets): 22% of a 20% budget\n\n## Worth a look\n- KR 2.1 Activation has three initiatives on two boards. One shared definition of "activated" would save rework.\n- KR 3.3 and KR 5.1 have no initiative at all.\n',
73
+ sprint: '## Headline\nThe sprint committed 44 points and finished 36 (82%). Two stories slipped, both waiting on QA.\n\n## Finished\n- IDPF-1175 Setup step 2: first API key\n- IDPF-1210 Spike: token bucket per partner\n\n## Carried over\n- IDPF-1182 Workspace provisioning API, 5 points: waiting for QA\n- IDPF-1190 QA test plan for beta, 3 points: blocked, no QA this sprint\n\n## For the next sprint\n- Add QA capacity before committing the beta.\n',
74
+ charter: '## Purpose\nGive developers a platform they can set up and build on alone: identity, keys, limits and the first API call.\n\n## Serves\n- KR 2.1 Activation to 45%\n- KR 1.3 Partner uptime 99.9%\n\n## Scope\n- In: sign-in, workspaces, keys, limits, SDKs\n- Out: billing (Payments board)\n\n## How work flows\nBuild and deliver is on. Two-week sprints. New asks arrive in Triage.\n',
75
+ outcome: '## The bet\nIf developers can create and test a key on one page, more of them make a first API call within one day.\n\n## Did it move the key result?\nYes. First API call within one day rose from 52% to 61% in the five weeks after release (target for the initiative: 60%).\n\n## Kill conditions\nNone tripped.\n\n## What we learned\n- A test call on the same page mattered more than the new layout.\n- We should have measured rotation before; we have no baseline.\n\n## What next\n- Close the initiative as Hit.\n',
76
+ };
77
+ const PRETTY = { portfolio: 'portfolio', board: 'board', initiative: 'initiative' };
78
+
79
+ const slugOf = (scope) => scope.replace(/[^A-Za-z0-9]+/g, '-').toLowerCase();
80
+ const pathOf = (scope, key, slot) => 'planning/core-docs/' + slugOf(scope) + '/' + key + '/' + slot + '.md';
81
+ const parseScope = (scope) => { const m = /^(portfolio|board|initiative):?(.*)$/.exec(String(scope || '')); return m ? { level: m[1], id: m[2] } : null; };
82
+
83
+ /* ── state: what a press changed ── */
84
+ const OV = new Map(); // scope|key -> { st, when, draft, running, issued }
85
+ const ovKey = (scope, key) => scope + '|' + key;
86
+
87
+ function boardInfo(id) {
88
+ const S = typeof window !== 'undefined' ? window.__c3boards : null;
89
+ const b = S && S.boards ? S.boards.find((x) => x.id === id) : null;
90
+ return b ? { id, name: b.name, build: !!b.build, jira: b.jira || '', owner: b.owner } : { id, name: id, build: id === 'idpf', jira: '', owner: 'You' };
91
+ }
92
+ function iniInfo(id) {
93
+ const S = typeof window !== 'undefined' ? window.__c3boards : null;
94
+ const r = S && S.rows ? S.rows.find((x) => x.id === id) : null;
95
+ if (!r) return null;
96
+ const b = S.boards.find((x) => x.id === r.board);
97
+ const wf = r.workflow || (b && b.workflow) || [];
98
+ const stage = (wf.find((s) => s.name === r.stage) || {});
99
+ return { id, board: r.board, stage: r.stage, cat: stage.cat || 'Plan', wfTpl: r.template || (b && b.template) || 'product' };
100
+ }
101
+
102
+ function entryOf(scope, e, base) {
103
+ const o = OV.get(ovKey(scope, e.key)) || {};
104
+ const stt = o.st || base.st;
105
+ const when = o.when !== undefined ? o.when : base.when;
106
+ const draft = stt === 'draft' ? (o.draft || { slot: ymd(0), path: pathOf(scope, e.key, ymd(0)) }) : null;
107
+ let open = null;
108
+ if (e.tab) open = { tab: e.tab }; else if (e.tool) open = { tool: e.tool }; else if (e.kind === 'report') open = { reports: true };
109
+ else if (stt === 'draft') open = { path: draft.path };
110
+ else if (stt === 'ok' || stt === 'due' || stt === 'stale') open = when ? { path: pathOf(scope, e.key, when) } : null;
111
+ return { key: e.key, title: e.title, ic: e.ic, for: e.for, reader: e.reader || null, cadence: e.cadence || null, by: e.by, kind: e.kind || 'doc', cat: e.cat || null, isNew: !!e.isNew,
112
+ st: stt, when: when || null, due: base.due != null ? (typeof base.due === 'number' ? ymd(base.due) : base.due) : null, flag: o.st ? null : base.flag || null, staleWhy: null, open, draft, running: !!o.running };
113
+ }
114
+ const summaryOf = (docs) => { const n = (s) => docs.filter((d) => d.st === s).length; return { draft: n('draft'), due: n('due'), missing: n('missing'), stale: n('stale') }; };
115
+
116
+ function setOf(scope) {
117
+ const sc = parseScope(scope);
118
+ if (!sc) return null;
119
+ const head = { ok: true, level: sc.level, scope, question: SCOPE_Q[sc.level], reader: READER[sc.level], rhythm: RHYTHM[sc.level], build: null, wfTpl: null, stage: null, cat: null };
120
+ let docs;
121
+ if (sc.level == 'portfolio') docs = PF.map((e) => entryOf(scope, e, { ...e, when: e.w != null ? ymd(e.w) : null }));
122
+ else if (sc.level === 'board') {
123
+ const b = boardInfo(sc.id); head.build = b.build;
124
+ docs = BD.filter((e) => !e.build || b.build).map((e) => {
125
+ const idpf = sc.id === 'idpf';
126
+ const st0 = e.key === 'charter' && sc.id === 'office' ? 'missing' : idpf ? e.st : (e.st === 'draft' ? 'ok' : e.st);
127
+ return entryOf(scope, e, { ...e, st: st0, when: st0 === 'missing' ? null : (e.w != null ? ymd(e.w) : null), flag: idpf ? e.flag : null });
128
+ });
129
+ } else {
130
+ const i = iniInfo(sc.id); if (!i) return { ok: false, error: 'No initiative ' + sc.id };
131
+ const b = boardInfo(i.board); head.build = b.build; head.stage = i.stage; head.cat = i.cat; head.wfTpl = i.wfTpl;
132
+ const cur = CAT_ORDER.indexOf(i.cat); const given = INI_STATE[sc.id] || {}; const research = i.wfTpl === 'research';
133
+ docs = IN.filter((e) => (b.build || !['rubric', 'stories', 'review'].includes(e.key) || i.board === 'idpf') && (!research || ['record', 'problem', 'research', 'rice', 'risks', 'update', 'outcome'].includes(e.key))).map((e) => {
134
+ const at = CAT_ORDER.indexOf(e.cat);
135
+ const st0 = given[e.key] || (at > cur ? 'later' : at === cur ? 'due' : 'ok');
136
+ return entryOf(scope, e, { ...e, st: st0, when: st0 === 'ok' ? ymd(-(at + 2) * 6) : st0 === 'draft' ? ymd(0) : null });
137
+ });
138
+ }
139
+ return { ...head, summary: summaryOf(docs), docs };
140
+ }
141
+
142
+ function bodyText(scope, key, slot) {
143
+ const sc = parseScope(scope) || {};
144
+ const all = [...PF, ...BD, ...IN]; const e = all.find((x) => x.key === key) || { title: key, for: '' };
145
+ const title = e.title + (sc.level === 'board' ? ' · ' + boardInfo(sc.id).name : sc.level === 'initiative' ? ' · ' + sc.id : '');
146
+ let body = BODY[key];
147
+ if (key === 'charter' && sc.id === 'office') body = '## Purpose\nMove the Jakarta office in 2027 with one contractor, and keep the team working through it.\n\n## Serves\n- Project milestone: lease signed\n\n## How work flows\nProject workflow. Build and deliver is off: the work is PMOS tasks, no Jira.\n';
148
+ if (!body) body = '## Summary\n' + (e.for || 'The document.') + '\n\n## Sources\n- Records PMOS reads for this ' + (PRETTY[sc.level] || 'level') + '\n';
149
+ const draftedBy = '---\ntitle: ' + title + '\nsource: core-docs:' + key + '\nslot: ' + slot + '\n---\n';
150
+ return draftedBy + '# ' + title + '\n\n' + body;
151
+ }
152
+
153
+ const json = (b, status) => new Response(JSON.stringify(b), { status: status || 200, headers: { 'content-type': 'application/json' } });
154
+ const KEYED = (p) => /^planning\/core-docs\/([^/]+)\/([^/]+)\/(\d{4}-\d{2}-\d{2})\.md$/.exec(String(p || ''));
155
+
156
+ function answer(method, path, q, body) {
157
+ if (path === '/api/core-docs' && method === 'GET') {
158
+ const level = q.get('level');
159
+ const scope = level == 'portfolio' ? 'portfolio' : level === 'board' ? 'board:' + (q.get('board') || '') : 'initiative:' + (q.get('id') || '');
160
+ const out = setOf(scope);
161
+ return out && out.ok ? json(out) : json(out || { ok: false, error: 'No such level.' }, out ? 404 : 400);
162
+ }
163
+ if (path.startsWith('/api/core-docs/') && method !== 'GET') {
164
+ const k = ovKey(body.scope, body.key);
165
+ if (path === '/api/core-docs/draft') {
166
+ const cur = OV.get(k) || {};
167
+ if (cur.running) return json({ ok: false, error: 'A draft is already running for this document.' }, 409);
168
+ const prev = (setOf(body.scope) || { docs: [] }).docs.find((d) => d.key === body.key) || {};
169
+ OV.set(k, { ...cur, running: true, st: prev.st === 'draft' ? 'draft' : cur.st || prev.st });
170
+ setTimeout(() => { OV.set(k, { st: 'draft', when: ymd(0), draft: { slot: ymd(0), path: pathOf(body.scope, body.key, ymd(0)) }, running: false }); }, 5000);
171
+ return json({ ok: true, run: 'demo-core-doc-' + body.key });
172
+ }
173
+ if (path === '/api/core-docs/issue') { OV.set(k, { st: 'ok', when: ymd(0), running: false }); return json({ ok: true }); }
174
+ if (path === '/api/core-docs/drop') { OV.set(k, { st: 'missing', when: null, running: false }); return json({ ok: true }); }
175
+ }
176
+ /* the documents the rows open */
177
+ const m = KEYED(q.get('path') || (body && body.path));
178
+ if (m) {
179
+ const slug = m[1]; const key = m[2]; const slot = m[3];
180
+ const scope = slug == 'portfolio' ? 'portfolio' : slug.startsWith('board-') ? 'board:' + slug.slice(6) : 'initiative:' + (/^initiative-(.+)$/.exec(slug) || [])[1];
181
+ const sc = scope.startsWith('initiative:') ? 'initiative:' + String(scope.slice(11)).toUpperCase() : scope;
182
+ if (path === '/api/doc') return new Response(bodyText(sc, key, slot), { status: 200, headers: { 'content-type': 'text/markdown; charset=utf-8' } });
183
+ const p = q.get('path') || body.path;
184
+ if (path === '/api/doc-sheet/revisions') return json({ ok: true, path: p, revisions: [{ rev: 1, by: 'claude-code/session', at: slot + 'T08:10:00.000Z', why: 'Drafted from the sources', before_hash: '', after_hash: '' }] });
185
+ if (path === '/api/doc-sheet/proposals') return json({ ok: true, path: p, proposals: [] });
186
+ if (path === '/api/doc-sheet/log') return json({ ok: true, path: p, revisions: [{ rev: 1, by: 'claude-code/session', at: slot + 'T08:10:00.000Z', why: 'Drafted from the sources', via: 'sheet', text_by: 'agent', before_hash: '', after_hash: '' }], events: [] });
187
+ if (path === '/api/doc-health/check') return json({ ok: true, path: p, health: { passed: 3, total: 3, checks: [{ id: 'fresh', pass: true, reason: 'drafted today' }, { id: 'complete', pass: true, reason: 'Required sections present' }, { id: 'traced', pass: true, reason: 'Sources listed' }] } });
188
+ }
189
+ return null;
190
+ }
191
+
192
+ let installed = false;
193
+ export function installCoreDocs() {
194
+ if (installed || typeof window === 'undefined') return;
195
+ installed = true;
196
+ const prev = window.fetch;
197
+ window.fetch = (input, init) => {
198
+ try {
199
+ const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
200
+ const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
201
+ const u = new URL(raw, location.origin);
202
+ if (u.origin === location.origin && (u.pathname.startsWith('/api/core-docs') || u.pathname.startsWith('/api/doc'))) {
203
+ let b = {};
204
+ if (method !== 'GET') { try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; } }
205
+ const out = answer(method, u.pathname, u.searchParams, b);
206
+ if (out) return Promise.resolve(out);
207
+ }
208
+ } catch { /* an unreadable request goes on as it is */ }
209
+ return prev(input, init);
210
+ };
211
+ }
@@ -13,6 +13,7 @@ import * as D from './c3-demo-data.js';
13
13
  import { installInit } from './c3-demo-init.js';
14
14
  import { focusGet, focusPost } from './c3-demo-focus.js'; // Meeting focus: the mockup's Partner API review (GET /api/meeting-focus, its ask poll, and the topic and ask POSTs)
15
15
  import { installBoards } from './c3-demo-boards.js'; // Initiatives boards: answers every /api/boards GET and POST (installed outermost, in installDemo)
16
+ import { installCoreDocs } from './c3-demo-coredocs.js'; // Core documents of each level: GET /api/core-docs and its POSTs (installed outermost, after installBoards)
16
17
  import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3calix (calendar-views.js reads it in demo only)
17
18
  import './c3-demo-cal3.js'; // Calendar Quarter and Year: answers GET /api/c3-strategy (chained under this file's fetch wrapper)
18
19
  import './c3-demo-rep-ix.js'; // Reports fixtures: sets window.__c3repix (report-families-view.js reads it in demo only)
@@ -123,4 +124,4 @@ export function demoPill() {
123
124
  }
124
125
 
125
126
  export const DEMO_WS = D.WS_ID;
126
- export function installDemo() { freezeClock(); install(); installRep3(); installInit(); installBoards(); const repReady = import('./c3-demo-rep2.js').then((m) => { m.installRep2(); return import('./c3-demo-rs.js'); }).then((m) => m.installRs()); window.__c3demo = true; window.__c3demoReady = Promise.all([repReady, import('./c3-demo-brief-ix.js')]).then(([, m]) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()).then(() => import('./c3-demo-brief-page.js')).then((m) => m.installBriefPage()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)
127
+ export function installDemo() { freezeClock(); install(); installRep3(); installInit(); installBoards(); installCoreDocs(); const repReady = import('./c3-demo-rep2.js').then((m) => { m.installRep2(); return import('./c3-demo-rs.js'); }).then((m) => m.installRs()); window.__c3demo = true; window.__c3demoReady = Promise.all([repReady, import('./c3-demo-brief-ix.js')]).then(([, m]) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()).then(() => import('./c3-demo-brief-page.js')).then((m) => m.installBriefPage()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)