create-pmos 0.154.0 → 0.155.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.154.0",
3
+ "version": "0.155.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@c6cee1a
1
+ kernel-source: pmos@8593af2
2
2
  assembled: 2026-10-12
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-12T04:39:11Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T04:46:53Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-12T04:39:11Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T04:46:53Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-12T04:39:11Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T04:46:53Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "The document sheet"
4
- description: "How to read a document as rich text, edit it, review the agent's proposals section by section, read the edit log, and clear a stale document."
4
+ description: "How to read a document as rich text with its outline, edit it, review the agent's proposals section by section, read the edit log, keep its Confluence and Google Doc copies in step, see the documents around it, and clear a stale document."
5
5
  timestamp: 2026-10-12
6
6
  ---
7
7
 
@@ -16,7 +16,8 @@ Read shows the document as formatted text: headings, bold, links, tables, checkl
16
16
  - Each section shows who changed it last: a person, or the agent and the person who accepted it, with the revision and the day. Select the name to open that revision in the edit log.
17
17
  - **Show who wrote what** colours each section: blue for a person, purple for the agent.
18
18
  - **Markdown source** shows the file as text. **Copy the markdown** copies it.
19
- - **Contents** goes to a section.
19
+ - The **outline** beside the text lists the headings, nested by level. The section you read is marked as you scroll. Select a heading to go to it, or the arrow beside a heading to fold the headings under it. A document with more than 12 headings has a **Find a heading** box. An orange dot marks a section that cites a source that changed. A purple dot marks a section that an open proposal changes.
20
+ - **Hide the outline** hides it, and the sheet keeps that choice in this browser. On a narrow screen the outline is a button, **Outline**, that names the section you read and opens the list over the text.
20
21
  - When an open proposal changes a section, a note shows above that section. Select **Show change** to see the difference in place.
21
22
 
22
23
  ## Edit
@@ -53,6 +54,20 @@ A document is stale when a document that it links to changed after it. The sheet
53
54
  - Edit the sections, or ask the agent for an update.
54
55
  - If the document is still right, select **Mark as still current** and write why. The mark counts as a change, so the banner clears.
55
56
 
57
+ ## Copies in Confluence and Google Docs
58
+
59
+ The header shows one chip for each copy of the document in Confluence or Google Docs, with its state: in step, behind (the copy has changes), PMOS ahead (PMOS has changes), both changed, or not checked. Select a chip to open **Copies in other tools**. Each copy shows its state, who changed it last and when, and when it was last checked.
60
+
61
+ - **Check now** starts an agent session that reads the copies with your connectors. The states change when it finishes.
62
+ - **Open in Confluence** or **Open in Google Doc** opens the copy.
63
+ - When the copy is behind or both changed, **Bring their change in as a proposal** asks the agent to read the copy and draft its changes as a proposal. Review it in Proposals, section by section. PMOS keeps a fingerprint of the copy, not its text, so the proposal is where you see the difference.
64
+ - When PMOS is ahead, **Update the Google Doc to this version** (or the Confluence page) starts an agent session that writes the copy. You have five seconds to select **Undo**.
65
+ - **Link a copy** takes the address of a Confluence page or a Google Doc. The sheet finds the tool from the address.
66
+
67
+ PMOS never overwrites the document from a copy.
68
+
56
69
  ## Health and Relations
57
70
 
58
- Health shows the checks of the document and how many pass. Relations shows the documents around this one.
71
+ Health shows the checks of the document and how many pass.
72
+
73
+ Relations shows a graph of the documents around this one: the documents it uses on the left, its initiative and goals on top, and the documents that use it and its copies on the right. A dashed orange line shows a document that is out of step. Select a document to open it. The lists under the graph name the same documents. From Initiatives › Documents, **Open the full graph in Docs** shows the whole graph.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.154.0",
3
+ "version": "0.155.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.154.0",
9
+ "version": "0.155.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.154.0",
3
+ "version": "0.155.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",
@@ -584,14 +584,15 @@ function handleMore(S, method, path, p) {
584
584
  return okb({ board: { name: 'Demo board' }, releases: S.releases.filter((x) => x.board === p.board).map((x) => ({ id: x.id, name: x.name, type: 'both', initiative_ids: x.scope.slice(), deploy_at: x.date + 'T11:00:00', release_at: x.date + 'T12:00:00', accepted_at: x.status === 'Released' ? x.date + 'T12:30:00' : null, gapFlags: [], tags: { detected: [], reason: null }, summary: [], status: x.status, board: x.board })) });
585
585
  }
586
586
  if (path === '/api/docs/library') {
587
- const files = [];
587
+ const files = []; const edges = []; // the research informs the PRD; the rubric comes from the PRD (the document sheet's Relations graph)
588
588
  for (const r of feat(p.board)) {
589
- const h = hash(r.id); const slug = r.id.toLowerCase();
589
+ const h = hash(r.id); const slug = r.id.toLowerCase(); const at0 = files.length;
590
590
  if (r.spec) files.push({ path: 'planning/ib/' + slug + '-prd.md', title: r.title + ' PRD', category: 'spec', date: D(-(h % 20) - 1), initiative: r.id });
591
591
  files.push({ path: 'planning/ib/' + slug + '-research.md', title: r.title + ' research', category: 'res', date: D(-(h % 50) - 5), initiative: r.id });
592
592
  if (catOfRow(S, r) === 'Do') files.push({ path: 'planning/ib/' + slug + '-rubric.md', title: r.title + ' rubric', category: 'eval', date: D(-(h % 9) - 1), initiative: r.id });
593
+ if (r.spec) { edges.push([at0 + 1, at0, 0]); if (files.length === at0 + 3) edges.push([at0, at0 + 2, 1]); }
593
594
  }
594
- return okb({ key: 'demo', files, edges: [], kinds: [], categories: [{ key: 'spec', name: 'Specs' }, { key: 'eval', name: 'Evals' }, { key: 'res', name: 'Research & plans' }], initiatives: {} });
595
+ return okb({ key: 'demo', files, edges, kinds: ['informs', 'rubric from'], categories: [{ key: 'spec', name: 'Specs' }, { key: 'eval', name: 'Evals' }, { key: 'res', name: 'Research & plans' }], initiatives: {} });
595
596
  }
596
597
  return undefined;
597
598
  }
@@ -316,6 +316,22 @@ POST['/api/doc-sheet/reject'] = (b) => {
316
316
  };
317
317
  POST['/api/doc-sheet/why'] = (b) => { const R = revsOf(b.path); const r = R && R.find((x) => x.rev === Number(b.rev)); if (!r) return { ok: false, error: 'no such revision' }; if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; r.why_added = b.why.trim(); r.why_added_at = '2026-10-08T08:12:00.000Z'; return { ok: true, rev: r.rev }; };
318
318
  POST['/api/doc-sheet/mark-current'] = (b) => { if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; DEMO_EVENTS.push({ path: b.path, kind: 'marked', at: '2026-10-08T08:12:00.000Z', why: b.why.trim(), by: 'pm' }); return { ok: true, at: '2026-10-08T08:12:00.000Z' }; };
319
+ /* the copies in other tools (GET /api/docs, lib/doc-sync.js signals; initiative c3-doc-sheet-v2): Checkout v2 PRD has a Confluence copy that is behind
320
+ * and a Google Doc that PMOS is ahead of; the drop-off analysis has a Confluence copy in step. Check marks them read now; sync and link are kept in memory. */
321
+ const pOf = (id) => DFILES.find((f) => f.id === id).path;
322
+ const COPIES = {
323
+ [pOf('p1')]: [{ kind: 'confluence', label: 'Confluence', target: 'acme.atlassian.net/wiki/spaces/PM/pages/48213/Checkout+v2+PRD', url: null, title: 'Checkout v2 PRD', signal: 'behind', basis: 'text', words: 'Confluence has a change PMOS does not have: Dana edited the Scope section on Thu 8 Oct.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-08T06:55:00.000Z', remote_modified_by: 'Dana Ruiz', linked_at: '2026-09-25T10:00:00.000Z', syncing: null, sync_failed: null },
324
+ { kind: 'google', label: 'Google Docs', target: 'docs.google.com/document/d/1Xc2Checkout', url: null, title: 'Checkout v2 PRD (shared with Design)', signal: 'ahead', basis: 'text', words: 'PMOS has changes the Google Doc does not have: the done criteria and the audit risk.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-05T16:20:00.000Z', remote_modified_by: 'Wawan', linked_at: '2026-10-01T09:00:00.000Z', syncing: null, sync_failed: null }],
325
+ [pOf('u1')]: [{ kind: 'confluence', label: 'Confluence', target: 'acme.atlassian.net/wiki/spaces/PM/pages/48390', url: null, title: 'Address drop-off analysis', signal: 'same', basis: 'text', words: 'Both have the same text.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-06T11:00:00.000Z', remote_modified_by: 'Wawan', linked_at: '2026-10-06T11:05:00.000Z', syncing: null, sync_failed: null }] };
326
+ GET['/api/docs'] = () => ({ ok: true, docs: Object.entries(COPIES).map(([path, links]) => ({ path, title: (DFILES.find((f) => f.path === path) || {}).title || path, links: links.map((l) => ({ ...l })), attention: links.some((l) => l.signal !== 'same') })), summary: {}, open: null, session: null, connectors: {} });
327
+ POST['/api/docs/check'] = () => { for (const L of Object.values(COPIES)) for (const l of L) { l.checked_at = '2026-10-08T08:12:00.000Z'; l.checked_by = 'agent'; } return { ok: true, session: 'demo-check' }; };
328
+ POST['/api/docs/sync'] = (b) => { const l = (COPIES[b.path] || []).find((x) => x.kind === b.kind); if (!l) return { ok: false, error: b.path + ' has no such copy' }; if (!['pmos', 'copy'].includes(b.use)) return { ok: false, error: 'say which version to use: pmos or copy' }; l.syncing = { run: null, use: b.use, started_at: '2026-10-08T08:12:00.000Z' }; return { ok: true, session: 'demo-sync' }; };
329
+ POST['/api/docs/link'] = (b) => {
330
+ if (!['google', 'confluence'].includes(b.kind) || !String(b.target || '').trim()) return { ok: false, error: 'not a Google Docs or Confluence address' };
331
+ const L = COPIES[b.path] || (COPIES[b.path] = []); if (L.some((x) => x.kind === b.kind)) return { ok: false, error: 'that document already has a ' + b.kind + ' copy' };
332
+ L.push({ kind: b.kind, label: b.kind === 'google' ? 'Google Docs' : 'Confluence', target: b.target, url: null, title: '', signal: 'unchecked', basis: null, words: 'No check has read this copy yet.', checked_at: null, checked_by: null, remote_modified_at: null, remote_modified_by: null, linked_at: '2026-10-08T08:12:00.000Z', syncing: null, sync_failed: null });
333
+ return { ok: true };
334
+ };
319
335
  POST['/api/doc-sheet/change-note'] = (b) => ({ ok: true, note: 'A change note for the owners of ' + (b.downstream || []).join(', ') });
320
336
 
321
337
  /* health: the six checks (the mockup's docHealth, line 4059). Checkout v2 PRD is recomputed from its text; the others answer the fixed N of M. */
@@ -1,12 +1,14 @@
1
1
  /* pm/c3-doc-sheet.js — the DOCUMENT SHEET of /c3.html (Initiatives › Documents, an initiative's Docs, Reports). The right-hand sheet from
2
2
  * c3-sheet.js, up to 780px, a bottom sheet on a phone. Initiative c3-doc-sheet-rich: the approved mockup (planning/prd/c3-doc-sheet-rich-prd.md).
3
3
  *
4
- * openDocSheet({ ws, doc, mode, prompt, relations(doc, openOther), onChanged, generated, secExtra }) doc = { path, name, ty, v, st, ini, iniName }
4
+ * openDocSheet({ ws, doc, mode, prompt, relations(doc, openOther), onChanged, generated, secExtra, library(), openGraph(doc) }) doc = { path, name, ty, v, st, ini, iniName }
5
5
  *
6
6
  * Six modes (the segment under the header; the keys are kept for the callers):
7
7
  * read Read the markdown rendered (pm/c3-md.js): the front matter as properties, each section with who changed it last (from the
8
- * revisions), "Show who wrote what", the Markdown source with a copy button, a Contents menu, the stale banner, and the
9
- * open proposals pinned to the sections they change, with "Show change" for the diff in place.
8
+ * revisions), "Show who wrote what", the Markdown source with a copy button, the stale banner, and the open proposals
9
+ * pinned to the sections they change, with "Show change" for the diff in place. The OUTLINE (v2) beside it: the headings
10
+ * nested by level, the section in view marked as the page scrolls, groups that fold, a filter past 12 headings, a dot on
11
+ * stale and proposal sections; Hide is kept in this browser (localStorage); under 900px it is a button over the text.
10
12
  * edit Edit a rich-text editor of the whole document (toolbar, Rich | Markdown over the same draft), the front matter as fields; a
11
13
  * changed section is marked; saving needs a reason (POST /api/doc-sheet/edit). A section nobody touched is written back
12
14
  * exactly as it was, so opening and saving without a change writes nothing.
@@ -18,13 +20,20 @@
18
20
  * change that has none (POST /api/doc-sheet/why).
19
21
  * health Health POST /api/doc-health/check: the checks, N of M; a failing Fresh check lists the sources that changed, and "Mark as still
20
22
  * current" records a reason (POST /api/doc-sheet/mark-current).
21
- * links Relations the document graph around this document, from the caller.
23
+ * links Relations the graph around this document (v2): its sources on the left, its initiative and goals on top, the documents that
24
+ * use it and its copies on the right, a drift link dashed; a node opens that document. It reads `library()` from the
25
+ * caller, else GET /api/docs/library; the caller's `relations` list, when given, is kept below the graph.
26
+ * COPIES (v2): GET /api/docs gives the document's Confluence and Google Doc links with their signal (lib/doc-sync.js): a chip per copy in
27
+ * the header, and the "Copies in other tools" panel: Check now (POST /api/docs/check), Open in the tool, Bring their change in as a
28
+ * proposal (the ask run reads the copy; PMOS keeps hashes, not the copy's text, so the proposal is the difference), Update the copy to
29
+ * this version (five seconds to undo, then POST /api/docs/sync with use: pmos), and Link a copy (POST /api/docs/link).
22
30
  * Nothing is invented: a part with no data says so. The demo (?demo=1) answers the routes from fixtures.
23
31
  *
24
32
  * Two options for a report, which is a document with no stored file yet (report-families-view.js):
25
33
  * generated = { text, why, health } Read shows the generated text; Edit and Proposals say why they need a stored document; Health shows `health`.
26
34
  * secExtra(name) a node under each section in Read: the report's claims and where they come from.
27
- * Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName, parseFront, setFront, composeSections. */
35
+ * Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName, parseFront, setFront, composeSections,
36
+ * copyKindOf, citesIn, outlineOf, hoodOf. */
28
37
  import { el } from './dom.js';
29
38
  import { pmGet, pmPost } from './http.js';
30
39
  import { c3ic } from './pm-work-view.js';
@@ -183,10 +192,63 @@ const md = (text, extra) => el('div', { class: 'ds-md' + (extra ? ' ' + extra :
183
192
  const btn = (cls, attrs, kids, onClick) => c3btn(cls, attrs, kids, onClick);
184
193
  const headLevel = (s) => (/^(#+)/.exec(s.prefix || '') || ['', ''])[1].length;
185
194
 
186
- export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra }) {
195
+ /* ── v2 (c3-doc-sheet-v2): the copies in other tools, the outline, the graph ── */
196
+ /* a copy's state (lib/doc-sync.js signal): [label, icon, class, sentence, chip word] */
197
+ const SIG = {
198
+ same: ['In step', 'check', 'ds-good', 'Both have the same text.', 'in step'],
199
+ ahead: ['PMOS is ahead', 'up', 'ds-acc', 'PMOS has changes the copy does not have.', 'PMOS ahead'],
200
+ behind: ['Behind', 'alert', 'ds-warn', 'The copy has changes PMOS does not have.', 'behind'],
201
+ diverged: ['Both changed', 'alert', 'ds-bad', 'Both changed since they last matched.', 'both changed'],
202
+ unchecked: ['Not checked', 'info', '', 'No check has read this copy yet.', 'not checked'],
203
+ unreadable: ['Cannot open', 'alert', 'ds-bad', 'The last check could not open the copy.', 'cannot open'],
204
+ 'missing-remote': ['Not found', 'alert', 'ds-bad', 'The copy is not there any more.', 'not found'],
205
+ 'missing-repo': ['Not in PMOS', 'alert', 'ds-bad', 'This file is not in the repository any more.', 'not in PMOS'] };
206
+ const SYNCING = ['Updating', 'clock', 'ds-acc', 'A session is updating this copy.', 'updating'];
207
+ const KLABEL = { google: 'Google Doc', confluence: 'Confluence' };
208
+ const sigOf = (l) => (l.syncing ? SYNCING : SIG[l.signal] || SIG.unchecked);
209
+ const labelOf = (l) => KLABEL[l.kind] || l.label || l.kind;
210
+ /** Which tool a pasted address names (the server checks it again): 'confluence', 'google' or null. */
211
+ export function copyKindOf(target) {
212
+ const t = String(target || '');
213
+ if (/atlassian|confluence|\/wiki\/|pageId=/i.test(t)) return 'confluence';
214
+ if (/google|docs\.|drive\./i.test(t)) return 'google';
215
+ return null;
216
+ }
217
+ /** The names of the sections that cite a path (or its file name). */
218
+ export function citesIn(secs, p) {
219
+ const b = String(p).split('/').pop();
220
+ return secs.filter((s) => s.name !== 'Front matter' && (s.text.includes(p) || (b && s.text.includes(b)))).map((s) => s.name);
221
+ }
222
+ /** The outline: the named sections, nested from the top level found (1 to 3). A single title heading on top is left out. */
223
+ export function outlineOf(secs) {
224
+ let named = secs.filter((s) => s.prefix && headLevel(s) > 0);
225
+ if (named.length && headLevel(named[0]) === 1 && named.filter((s) => headLevel(s) === 1).length === 1) named = named.slice(1);
226
+ if (!named.length) return [];
227
+ const base = Math.min(...named.map(headLevel));
228
+ return named.map((s) => ({ s, lv: Math.min(3, headLevel(s) - base + 1) }));
229
+ }
230
+ /** The neighbourhood of a document in a library response: { me, src, out } with each edge's state (the Docs page's rule when it has none). */
231
+ export function hoodOf(lib, path) {
232
+ if (!lib || !Array.isArray(lib.files)) return null;
233
+ const me = lib.files.findIndex((f) => f.path === path);
234
+ const kinds = lib.kinds || []; const src = []; const out = [];
235
+ const st = (a, b, k, s) => s || (['link', 'cites', 'anchor'].includes(kinds[k]) && a.date && b.date && a.date > b.date ? 'drift' : 'ok');
236
+ if (me >= 0) for (const [a, b, k, s] of lib.edges || []) {
237
+ if (b === me && a !== me && lib.files[a]) src.push({ f: lib.files[a], kind: kinds[k] || '', st: st(lib.files[a], lib.files[me], k, s) });
238
+ else if (a === me && b !== me && lib.files[b]) out.push({ f: lib.files[b], kind: kinds[k] || '', st: st(lib.files[me], lib.files[b], k, s) });
239
+ }
240
+ return { me: me >= 0 ? lib.files[me] : null, src, out };
241
+ }
242
+ const OL_KEY = 'pmos.c3.docOutline';
243
+ const olGet = () => { try { return localStorage.getItem(OL_KEY) !== 'off'; } catch { return true; } };
244
+ const olSet = (on) => { try { localStorage.setItem(OL_KEY, on ? 'on' : 'off'); } catch { /* no storage here */ } };
245
+ const ESTATE = { ok: 'in step', drift: 'out of step', conflict: 'conflict', missing: 'missing', ahead: 'PMOS ahead' };
246
+
247
+ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra, library, openGraph }) {
187
248
  const S = { mode: MODE[mode] || mode, prompt, text: null, err: '', revs: null, texts: {}, health: null, hAsk: false, busy: false, changed: false,
188
249
  meta: null, metaFor: null, props: null, propsErr: '', log: null, logErr: '', asking: null, closed: false, who: false, src: false, preview: {},
189
- pick: {}, rejFor: null, rejWhy: '', whyFor: null, whyText: '', staleWhy: null, logF: 'all', cmp: null, cmpNode: null, flashRev: null, ed: null, leave: null };
250
+ pick: {}, rejFor: null, rejWhy: '', whyFor: null, whyText: '', staleWhy: null, logF: 'all', cmp: null, cmpNode: null, flashRev: null, ed: null, leave: null,
251
+ copies: null, copiesErr: '', syncOpen: false, linking: false, linkUrl: '', pend: {}, checkRun: null, lib: undefined, ol: olGet(), olOpen: false, olq: '', olShut: {}, cur: null };
190
252
  const demo = isDemo();
191
253
  const base = { ws, path: doc.path };
192
254
  let ui = null;
@@ -237,7 +299,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
237
299
  const reloadAll = async () => {
238
300
  S.revs = null; S.meta = null; S.metaFor = null; S.texts = {}; S.health = null; S.hAsk = false; S.log = null;
239
301
  await loadText(); draw();
240
- await Promise.all([loadRevs(), loadProps(), loadHealth()]); loadMeta(); if (S.mode === 'hist') { await loadLog(); } draw();
302
+ await Promise.all([loadRevs(), loadProps(), loadHealth(), loadCopies()]); loadMeta(); if (S.mode === 'hist') { await loadLog(); } draw();
241
303
  };
242
304
 
243
305
  /* ── drawing helpers ── */
@@ -266,7 +328,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
266
328
  return el('div', { class: 'chips2 ds-meta', 'data-doc-meta': '' },
267
329
  el('span', { class: 'chip2 ' + st[1] }, c3ic(st[2], 'sm'), st[0]), el('span', { class: 'chip2' }, doc.ty || 'Document'), doc.v ? el('span', { class: 'chip2 mono' }, doc.v) : null,
268
330
  h && h.total ? btn('chip2 lk ' + hcls(h), { 'data-doc-hchip': '' }, [c3ic('shield', 'sm'), h.passed + ' of ' + h.total + ' checks'], () => go('health')) : null,
269
- el('span', { class: 'chip2' }, c3ic('init', 'sm'), doc.iniName || doc.ini || 'Portfolio'),
331
+ el('span', { class: 'chip2' }, c3ic('init', 'sm'), doc.iniName || doc.ini || 'Portfolio'), copyChips(),
270
332
  last && !generated ? btn('chip2 lk', { 'data-doc-last': '' }, [c3ic(isAgentRev(last) ? 'spark' : 'user', 'sm'), 'Last change: ', el('b', null, whoOf(last) + ','), ' ' + dayName(last.at)], () => go('hist')) : null);
271
333
  }
272
334
  function seg() {
@@ -285,6 +347,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
285
347
  if (m === 'hist') { S.cmp = null; loadLog().then(() => { draw(); if (S.flashRev != null) { goEl('[data-log-rev="' + S.flashRev + '"]'); S.flashRev = null; } }); }
286
348
  if (m === 'ask' && S.props === null) loadProps().then(draw);
287
349
  if (m === 'read') loadMeta();
350
+ if (m === 'links') loadLib();
288
351
  draw();
289
352
  const sb = ui && ui.body.closest('.sb'); if (sb) sb.scrollTop = 0;
290
353
  }
@@ -294,7 +357,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
294
357
  const f = fresh(); if (!f || generated) return null;
295
358
  const secs = S.text ? splitDoc(S.text) : [];
296
359
  const srcs = Array.isArray(f.sources) ? f.sources : [];
297
- const cites = (p) => { const b = String(p).split('/').pop(); return secs.filter((s) => s.name !== 'Front matter' && (s.text.includes(p) || (b && s.text.includes(b)))).map((s) => s.name); };
360
+ const cites = (p) => citesIn(secs, p);
298
361
  const op = openProps();
299
362
  if (S.staleWhy !== null) {
300
363
  const inp = el('input', { type: 'text', id: 'dstalewhy', 'data-doc-stalewhy': '', placeholder: 'For example: the launch date is not agreed yet', 'aria-label': 'Why it is still current', value: S.staleWhy, onInput: (e) => { S.staleWhy = e.target.value; } });
@@ -318,6 +381,243 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
318
381
  await loadHealth(); draw(); say('Marked as current. The reason is in the edit log.');
319
382
  }
320
383
 
384
+ /* ── v2: the copies in other tools (GET /api/docs: the links and their signals, lib/doc-sync.js) ── */
385
+ const loadCopies = async () => {
386
+ if (generated) { S.copies = []; return; }
387
+ const j = await pmGet('/api/docs?' + q({ ws })).catch(() => null);
388
+ if (!j || !j.ok) { S.copies = S.copies || []; S.copiesErr = (j && j.error) || 'The copies could not be read.'; return; }
389
+ const d = (j.docs || []).find((x) => x.path === doc.path);
390
+ S.copies = d ? d.links || [] : []; S.copiesErr = ''; S.checkRun = j.session || j.open || null;
391
+ };
392
+ const copyChips = () => {
393
+ if (generated || S.copies == null) return null;
394
+ if (!S.copies.length) return btn('chip2 lk', { 'data-doc-copychip': '', 'aria-expanded': String(S.syncOpen) }, [c3ic('link', 'sm'), 'Link a Confluence or Google Doc copy'], () => toggleCopies());
395
+ return S.copies.map((l) => { const g = sigOf(l); return btn('chip2 lk ' + g[2], { 'data-doc-copychip': l.kind, 'aria-expanded': String(S.syncOpen), title: l.words || g[3] }, [c3ic(g[1], 'sm'), labelOf(l) + ': ' + g[4]], () => toggleCopies()); });
396
+ };
397
+ function toggleCopies() {
398
+ S.syncOpen = !S.syncOpen;
399
+ if (S.syncOpen && S.mode === 'edit') { go('read'); return; }
400
+ draw(); const sb = ui && ui.body.closest('.sb'); if (sb && S.syncOpen) sb.scrollTop = 0;
401
+ }
402
+ function copiesPanel() {
403
+ if (!S.syncOpen || generated) return null;
404
+ const L = S.copies || [];
405
+ const rows = L.map((l) => {
406
+ const g = sigOf(l); const name = labelOf(l); const pend = S.pend[l.kind];
407
+ const acts = [btn('ds-btn', { 'data-copy-check': l.kind, disabled: !!S.checkRun, 'aria-disabled': S.checkRun ? 'true' : null }, [c3ic(S.checkRun ? 'clock' : 'undo', 'sm'), S.checkRun ? 'Checking' : 'Check now'], () => checkNow()),
408
+ l.url ? el('a', { class: 'ds-btn', href: l.url, target: '_blank', rel: 'noopener', 'data-copy-open': l.kind }, c3ic('out', 'sm'), 'Open in ' + name) : null];
409
+ if (!l.syncing && (l.signal === 'behind' || l.signal === 'diverged')) acts.push(btn('ds-btn ag', { 'data-copy-take': l.kind }, [c3ic('spark', 'sm'), 'Bring their change in as a proposal'], () => takeCopy(l)));
410
+ if (!l.syncing && l.signal === 'ahead') acts.push(pend ? btn('ds-btn', { 'data-copy-undo': l.kind }, [c3ic('undo', 'sm'), 'Undo the update'], () => undoPush(l))
411
+ : btn('ds-btn pri', { 'data-copy-push': l.kind }, [c3ic('up', 'sm'), 'Update the ' + name + ' to this version'], () => pushCopy(l)));
412
+ const changed = l.remote_modified_at ? 'changed ' + whenOf(l.remote_modified_at) + (l.remote_modified_by ? ' by ' + l.remote_modified_by : '') : '';
413
+ return el('li', { 'data-copy': l.kind },
414
+ el('div', { class: 'l1' }, c3ic('doc', 'sm'), el('b', null, l.title || name), el('span', { class: 'tag ' + g[2], 'data-copy-state': l.syncing ? 'syncing' : l.signal || 'unchecked' }, c3ic(g[1], 'sm'), g[0])),
415
+ el('div', { class: 'ds-mono' }, [name, l.target, changed].filter(Boolean).join(' · ')),
416
+ el('p', null, l.words || g[3]),
417
+ el('div', { class: 'ds-note' }, l.checked_at ? 'Last checked ' + whenOf(l.checked_at) + (l.checked_by ? ' by ' + byName(l.checked_by) : '') + '.' : 'Not checked yet.'),
418
+ l.syncing ? el('div', { class: 'fold', role: 'status', 'data-copy-syncing': l.kind }, c3ic('clock', 'sm'), 'A session is updating ' + (l.syncing.use === 'copy' ? 'PMOS from the ' + name : 'the ' + name + ' to this version') + (l.syncing.started_at ? '. It started ' + whenOf(l.syncing.started_at) + '.' : '.')) : null,
419
+ l.sync_failed ? el('div', { class: 'fold w', role: 'alert', 'data-copy-failed': l.kind }, c3ic('alert', 'sm'), 'The last update did not finish' + (l.sync_failed.reason ? ': ' + l.sync_failed.reason : '.')) : null,
420
+ pend ? el('div', { class: 'fold', role: 'status', 'data-copy-pending': l.kind }, c3ic('clock', 'sm'), 'The update starts in five seconds. Undo stops it.') : null,
421
+ el('div', { class: 'ds-acts' }, acts));
422
+ });
423
+ const linkRow = S.linking ? el('div', { class: 'ds-whyadd ds-linkadd' },
424
+ el('input', { type: 'text', id: 'dcopyurl', 'data-copy-url': '', placeholder: 'Paste a Confluence page or Google Doc link', 'aria-label': 'Address of the copy', value: S.linkUrl, onInput: (e) => { S.linkUrl = e.target.value; } }),
425
+ btn('ds-btn pri', { 'data-copy-linkok': '' }, 'Link', () => linkCopy()), btn('ds-btn', {}, 'Cancel', () => { S.linking = false; draw(); })) : null;
426
+ return el('section', { class: 'ds-sync', 'aria-label': 'Copies in other tools', 'data-doc-copies': '' },
427
+ el('div', { class: 'hd' }, el('b', null, 'Copies in other tools'), el('span', { class: 'ds-acts' },
428
+ btn('ds-btn', { 'data-copy-link': '' }, [c3ic('link', 'sm'), L.length ? 'Link another copy' : 'Link a copy'], () => { S.linking = true; S.linkUrl = ''; draw(); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); }),
429
+ btn('ds-btn', { 'aria-label': 'Close the copies', 'data-copy-close': '' }, c3ic('x', 'sm'), () => { S.syncOpen = false; draw(); }))),
430
+ S.copiesErr ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.copiesErr) : null, linkRow,
431
+ L.length ? el('ul', null, rows) : el('p', { class: 'ds-note', 'data-copy-none': '' }, 'This document has no copy in Confluence or Google Docs yet. Link one to see whether it is in step.'),
432
+ el('p', { class: 'ds-note' }, 'PMOS never overwrites this document from a copy: a change in the copy comes in as a proposal you accept by section. Updating a copy starts one agent session, and you have five seconds to undo.'));
433
+ }
434
+ async function checkNow() {
435
+ const r = await pmPost('/api/docs/check', { ws }).catch(() => null);
436
+ if (!r || !r.ok) { say((r && r.error) || 'The check did not start.'); return; }
437
+ S.checkRun = { id: r.session }; draw();
438
+ say('Checking the copies. An agent session reads them with your connectors; the states change when it finishes.');
439
+ for (let i = 0; i < 120 && !S.closed && S.checkRun; i++) {
440
+ await new Promise((ok) => setTimeout(ok, 5000));
441
+ if (S.closed) return;
442
+ await loadCopies(); drawBg();
443
+ }
444
+ }
445
+ /* a change in the copy comes in as a proposal: the read-only ask run (#637) reads the copy and drafts it by section */
446
+ function takeCopy(l) {
447
+ S.prompt = 'Read the ' + labelOf(l) + ' copy of this document (' + l.target + ') and propose its changes here, section by section. Keep the PMOS text where only PMOS changed.';
448
+ S.syncOpen = false; go('ask');
449
+ if (demo) askDemo(); else askReal();
450
+ }
451
+ function pushCopy(l) {
452
+ const name = labelOf(l);
453
+ S.pend[l.kind] = setTimeout(async () => {
454
+ delete S.pend[l.kind];
455
+ const r = await pmPost('/api/docs/sync', { ws, path: doc.path, kind: l.kind, use: 'pmos' }).catch(() => null);
456
+ if (!r || !r.ok) say((r && r.error) || 'The update did not start.');
457
+ else say('Updating the ' + name + '. A session writes it with your connectors.');
458
+ await loadCopies(); drawBg();
459
+ }, 5000);
460
+ draw();
461
+ }
462
+ function undoPush(l) { clearTimeout(S.pend[l.kind]); delete S.pend[l.kind]; draw(); say('Stopped. The ' + labelOf(l) + ' is not updated.'); }
463
+ async function linkCopy() {
464
+ const t = String(S.linkUrl || '').trim(); const kind = copyKindOf(t);
465
+ if (!kind) { say('Paste a Confluence page or Google Doc link.'); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); return; }
466
+ const r = await pmPost('/api/docs/link', { ws, path: doc.path, kind, target: t }).catch(() => null);
467
+ if (!r || !r.ok) { say((r && r.error) || 'The copy was not linked.'); return; }
468
+ S.linking = false; S.linkUrl = ''; await loadCopies(); draw(); say('Linked the ' + KLABEL[kind] + ' copy. Check now reads it.');
469
+ }
470
+
471
+ /* ── v2: the outline (Read) ── */
472
+ const staleNames = () => {
473
+ const f = fresh(); if (!f || !S.text) return new Set();
474
+ const secs = splitDoc(S.text); const out = new Set();
475
+ (Array.isArray(f.sources) ? f.sources : []).forEach((x) => citesIn(secs, x.path).forEach((n) => out.add(n)));
476
+ return out;
477
+ };
478
+ function outlineNav(items) {
479
+ const qq = S.olq.trim().toLowerCase(); const stale = staleNames(); const prop = new Set(openProps().flatMap((p) => p.sections.map((c) => c.name)));
480
+ const rows = []; let hideUnder = null;
481
+ items.forEach((it, k) => {
482
+ const { s, lv } = it;
483
+ if (hideUnder !== null && lv > hideUnder) return;
484
+ hideUnder = null;
485
+ if (qq && !s.name.toLowerCase().includes(qq)) return;
486
+ let n = 0; for (let j = k + 1; j < items.length && items[j].lv > lv; j++) n++;
487
+ const shut = !qq && !!S.olShut[s.i]; if (shut) hideUnder = lv;
488
+ const on = S.cur === s.i;
489
+ rows.push(el('li', { class: 'l' + lv + (on ? ' on' : ''), 'data-ol-i': String(s.i) },
490
+ n && !qq ? btn('tw', { 'aria-label': (shut ? 'Show' : 'Hide') + ' the ' + plural(n, 'heading') + ' under ' + s.name, 'aria-expanded': String(!shut), 'data-ol-fold': String(s.i) }, c3ic('next', 'sm'), () => { S.olShut[s.i] = !shut; redrawOl(); }) : el('span', { class: 'tw' }),
491
+ btn('go', { 'data-ol-go': String(s.i), 'aria-current': on ? 'location' : null }, [el('span', null, s.name),
492
+ stale.has(s.name) ? el('i', { class: 'mdot w', title: 'A source changed after this section', 'data-ol-stale': '' }) : null,
493
+ prop.has(s.name) ? el('i', { class: 'mdot p', title: 'The agent proposes a change here', 'data-ol-prop': '' }) : null], () => jumpTo(s.i))));
494
+ });
495
+ return el('nav', { class: 'ds-ol', 'aria-label': 'Outline', 'data-doc-outline': '' },
496
+ el('div', { class: 'hd' }, el('b', null, 'Outline'), el('span', { class: 'ds-mono' }, plural(items.length, 'heading'))),
497
+ items.length > 12 ? el('input', { type: 'text', id: 'dolq', 'data-ol-find': '', placeholder: 'Find a heading', 'aria-label': 'Find a heading', value: S.olq, onInput: (e) => { S.olq = e.target.value; redrawOl(); } }) : null,
498
+ el('ul', null, rows.length ? rows : el('li', { class: 'none ds-note' }, 'No heading matches.')),
499
+ stale.size || prop.size ? el('div', { class: 'key ds-note' }, stale.size ? el('span', null, el('i', { class: 'mdot w' }), 'stale') : null, prop.size ? el('span', null, el('i', { class: 'mdot p' }), 'proposal') : null) : null);
500
+ }
501
+ function redrawOl() {
502
+ const n = ui && ui.body.querySelector('.ds-ol'); if (!n || S.text == null) return;
503
+ const f = document.activeElement && document.activeElement.id === 'dolq' ? document.activeElement.selectionStart : null;
504
+ n.replaceWith(outlineNav(outlineOf(splitDoc(S.text)))); placeOl();
505
+ if (f != null) { const i = ui.body.querySelector('#dolq'); if (i) { i.focus(); try { i.setSelectionRange(f, f); } catch { /* not a text field */ } } }
506
+ }
507
+ /* where the sticky header ends: a section scrolled to lands below it, and the outline sticks under it */
508
+ /* (a sticky element sticks inside the scroller's padding: its edge from the scroller's top adds the padding) */
509
+ const stickBottom = () => { const t = ui && ui.body.querySelector('.ds-top'); return t ? (parseFloat(getComputedStyle(t).top) || 0) + t.offsetHeight : 0; };
510
+ const topEdge = () => { const sb = ui && ui.body.closest('.sb'); return stickBottom() + (sb ? parseFloat(getComputedStyle(sb).paddingTop) || 0 : 0); };
511
+ function placeOl() {
512
+ const nav = ui && ui.body.querySelector('.ds-ol'); const sb = ui && ui.body.closest('.sb'); if (!nav || !sb) return;
513
+ const h = stickBottom() + 8;
514
+ nav.style.setProperty('--in-ds-oltop', h + 'px'); nav.style.setProperty('--in-ds-olmax', Math.max(160, sb.clientHeight - h - 16) + 'px');
515
+ }
516
+ function jumpTo(i) {
517
+ const sb = ui && ui.body.closest('.sb'); const n = ui && ui.body.querySelector('[data-dsec-i="' + i + '"]'); if (!sb || !n) return;
518
+ sb.scrollTop += n.getBoundingClientRect().top - sb.getBoundingClientRect().top - topEdge() - 8;
519
+ n.classList.remove('flash'); void n.offsetWidth; n.classList.add('flash');
520
+ S.cur = i; S.pinCur = i; S.olOpen = false; const lay = ui.body.querySelector('.ds-rlay'); if (lay) lay.classList.remove('olopen');
521
+ const b = ui.body.querySelector('[data-ol-btn]'); if (b) b.setAttribute('aria-expanded', 'false');
522
+ markCur();
523
+ }
524
+ function trackCur() {
525
+ if (S.mode !== 'read' || !ui) return;
526
+ const sb = ui.body.closest('.sb'); const nav = ui.body.querySelector('.ds-ol'); if (!sb || !nav) return;
527
+ const ids = new Set([...nav.querySelectorAll('[data-ol-i]')].map((x) => x.getAttribute('data-ol-i')));
528
+ const lim = sb.getBoundingClientRect().top + topEdge() + 24; let cur = null;
529
+ for (const s of ui.body.querySelectorAll('.ds-rmain [data-dsec-i]')) {
530
+ const k = s.getAttribute('data-dsec-i');
531
+ if (s.getBoundingClientRect().top <= lim) { if (ids.has(k)) cur = +k; } else break;
532
+ }
533
+ if (cur == null) { const f = nav.querySelector('[data-ol-i]'); cur = f ? +f.getAttribute('data-ol-i') : null; }
534
+ /* a section pressed near the end cannot scroll to the top: it stays the marked one while it is on screen */
535
+ if (S.pinCur != null) {
536
+ const p = ui.body.querySelector('[data-dsec-i="' + S.pinCur + '"]'); const r = p && p.getBoundingClientRect(); const b = sb.getBoundingClientRect();
537
+ if (r && r.top >= b.top + topEdge() - 8 && r.top < b.bottom - 40) cur = S.pinCur; else S.pinCur = null;
538
+ }
539
+ if (cur !== S.cur) { S.cur = cur; markCur(); }
540
+ }
541
+ function markCur() {
542
+ const nav = ui && ui.body.querySelector('.ds-ol'); if (!nav) return;
543
+ nav.querySelectorAll('[data-ol-i]').forEach((li) => { const on = +li.getAttribute('data-ol-i') === S.cur; li.classList.toggle('on', on); const g = li.querySelector('[data-ol-go]'); if (g) { if (on) g.setAttribute('aria-current', 'location'); else g.removeAttribute('aria-current'); } });
544
+ const secs = S.text ? splitDoc(S.text) : []; const c = secs.find((s) => s.i === S.cur);
545
+ const ob = ui.body.querySelector('[data-ol-btn] b'); if (ob) ob.textContent = c ? c.name : '';
546
+ const on = nav.querySelector('li.on');
547
+ if (on && nav.scrollHeight > nav.clientHeight) { const t = on.offsetTop - nav.offsetTop; if (t < nav.scrollTop || t > nav.scrollTop + nav.clientHeight - 30) nav.scrollTop = t - nav.clientHeight / 3; }
548
+ }
549
+
550
+ /* ── v2: Relations, the graph around this document (from the caller's library, else GET /api/docs/library) ── */
551
+ const loadLib = async () => {
552
+ if (S.lib !== undefined) return;
553
+ S.lib = null;
554
+ const j = await (library ? Promise.resolve().then(library) : pmGet('/api/docs/library?' + q({ ws }))).catch(() => null);
555
+ S.lib = j && j.ok ? j : { err: (j && j.error) || 'The document graph could not be read.' };
556
+ drawBg();
557
+ };
558
+ const catName = (c) => { const k = ((S.lib && S.lib.categories) || []).find((x) => x.key === c); return k ? k.name : c || 'Document'; };
559
+ const fileDoc = (f) => ({ path: f.path, name: f.title || f.path, ty: f.ty || catName(f.category), v: f.v || '', st: STATE[f.st] ? f.st : 'current', ini: f.initiative || '',
560
+ iniName: (S.lib && S.lib.initiatives && S.lib.initiatives[f.initiative] && (S.lib.initiatives[f.initiative].short || S.lib.initiatives[f.initiative].title)) || f.initiative || '' });
561
+ function graphNodes() {
562
+ const H = hoodOf(S.lib && !S.lib.err ? S.lib : null, doc.path) || { src: [], out: [] };
563
+ const isGoal = (f) => f.category === 'goal' || f.node === 'Goal';
564
+ const nd = (e, side) => { const f = e.f; const file = !f.node && f.st !== 'node' && f.st !== 'missing';
565
+ return { side, name: f.title || f.path, sub: [...new Set([e.kind, f.node || (f.date ? dayName(f.date) : '')].filter(Boolean).map((x) => x.toLowerCase() === String(e.kind).toLowerCase() ? e.kind : x))].join(' · '), st: e.st, path: f.path, open: file ? fileDoc(f) : null }; };
566
+ const top = H.src.filter((e) => isGoal(e.f)).map((e) => nd(e, 'top'));
567
+ if (doc.ini || doc.iniName) top.unshift({ side: 'top', name: doc.iniName || doc.ini, sub: 'initiative', st: 'ok', init: true });
568
+ const left = H.src.filter((e) => !isGoal(e.f)).map((e) => nd(e, 'left'));
569
+ const right = H.out.map((e) => nd(e, 'right')).concat((S.copies || []).map((l) => ({ side: 'right', name: labelOf(l) + ' copy', sub: sigOf(l)[4], url: l.url, kind: l.kind,
570
+ st: l.signal === 'behind' || l.signal === 'diverged' ? 'drift' : l.signal === 'ahead' ? 'ahead' : l.signal === 'same' ? 'ok' : 'missing' })));
571
+ return { top, left, right };
572
+ }
573
+ function graphEl(G) {
574
+ const CAP = 6;
575
+ const node = (n) => {
576
+ const kids = [el('b', null, n.name), el('small', null, n.sub)];
577
+ const a = { 'data-gn': n.side, 'data-gn-st': n.st, 'data-gn-copy': n.kind || null };
578
+ if (n.url) return el('a', { class: 'ds-gn ' + n.st, href: n.url, target: '_blank', rel: 'noopener', ...a }, ...kids);
579
+ if (n.open) return btn('ds-gn ' + n.st, { 'data-gn-path': n.path, title: 'Open ' + n.name, ...a }, kids, () => S.open(n.open));
580
+ return el('div', { class: 'ds-gn x ' + n.st, ...a }, ...kids);
581
+ };
582
+ const col = (list) => [list.slice(0, CAP).map(node), list.length > CAP ? el('div', { class: 'ds-note ds-gmore' }, '+' + (list.length - CAP) + ' more, listed below') : null];
583
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'ds-gsvg'); svg.setAttribute('aria-hidden', 'true');
584
+ const wrap = el('div', { class: 'ds-graph', role: 'group', 'aria-label': 'This document, its sources on the left, its initiative above, the documents that use it and its copies on the right', 'data-doc-graph': '' }, svg,
585
+ G.top.length ? el('div', { class: 'ds-gtop' }, col(G.top)) : null,
586
+ el('div', { class: 'ds-gmid' }, el('div', { class: 'ds-gcol' }, G.left.length ? col(G.left) : el('div', { class: 'ds-note' }, 'No source recorded')),
587
+ el('div', { class: 'ds-gn me', 'data-gn': 'me' }, el('b', null, doc.name), el('small', null, 'this document' + (doc.v ? ' · ' + doc.v : ''))),
588
+ el('div', { class: 'ds-gcol' }, G.right.length ? col(G.right) : el('div', { class: 'ds-note' }, 'Nothing uses it yet'))),
589
+ el('div', { class: 'key ds-note' }, el('span', null, el('i', { class: 'ln ok' }), 'in step'), el('span', null, el('i', { class: 'ln drift' }), 'out of step'), el('span', null, el('i', { class: 'ln ahead' }), 'PMOS ahead of the copy'),
590
+ openGraph ? btn('chip2 lk', { 'data-doc-fullgraph': '' }, [c3ic('tree', 'sm'), 'Open the full graph in Docs'], () => { ui.close(); openGraph(doc); }) : null));
591
+ if (typeof ResizeObserver === 'function') new ResizeObserver(() => drawEdges(wrap)).observe(wrap);
592
+ return wrap;
593
+ }
594
+ function drawEdges(wrap) {
595
+ const svg = wrap.querySelector('.ds-gsvg'); const me = wrap.querySelector('[data-gn="me"]'); if (!svg || !me || !wrap.isConnected) return;
596
+ const W = wrap.getBoundingClientRect(); const M = me.getBoundingClientRect();
597
+ svg.setAttribute('viewBox', '0 0 ' + Math.round(W.width) + ' ' + Math.round(W.height)); svg.setAttribute('width', String(Math.round(W.width))); svg.setAttribute('height', String(Math.round(W.height)));
598
+ const paths = [...wrap.querySelectorAll('[data-gn]:not([data-gn="me"])')].map((n) => {
599
+ const R = n.getBoundingClientRect(); const side = n.getAttribute('data-gn');
600
+ const [fx, fy, tx, ty] = side === 'left' ? [R.right, R.top + R.height / 2, M.left, M.top + M.height / 2] : side === 'right' ? [M.right, M.top + M.height / 2, R.left, R.top + R.height / 2] : [R.left + R.width / 2, R.bottom, M.left + M.width / 2, M.top];
601
+ const x1 = fx - W.left; const y1 = fy - W.top; const x2 = tx - W.left; const y2 = ty - W.top;
602
+ const d = side === 'top' ? 'M' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + (y1 + y2) / 2 + ', ' + x2 + ' ' + (y1 + y2) / 2 + ', ' + x2 + ' ' + y2 : 'M' + x1 + ' ' + y1 + ' C ' + (x1 + x2) / 2 + ' ' + y1 + ', ' + (x1 + x2) / 2 + ' ' + y2 + ', ' + x2 + ' ' + y2;
603
+ const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p.setAttribute('d', d); p.setAttribute('class', 'e ' + (n.getAttribute('data-gn-st') || 'ok')); return p;
604
+ });
605
+ svg.replaceChildren(...paths);
606
+ }
607
+ function linksBody() {
608
+ if (S.lib === undefined) loadLib();
609
+ if (S.lib == null) return el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the document graph.');
610
+ const G = graphNodes(); const any = G.left.length || G.right.length || G.top.some((n) => !n.init);
611
+ const rel = (h, list) => list.length ? [el('h3', { class: 'ds-h' }, h), el('ul', { class: 'ds-rel' }, list.map((n) => el('li', { 'data-rel': n.path || n.kind || n.name },
612
+ c3ic(n.url ? 'out' : n.init ? 'init' : 'doc', 'sm'),
613
+ n.open ? btn('ds-rlk', { 'data-rel-open': n.path }, [n.name, el('small', null, n.sub)], () => S.open(n.open)) : n.url ? el('a', { class: 'ds-rlk', href: n.url, target: '_blank', rel: 'noopener' }, n.name, el('small', null, n.sub)) : el('span', { class: 'ds-rlk' }, n.name, el('small', null, n.sub)),
614
+ n.st && n.st !== 'ok' ? el('span', { class: 'tag ' + (n.st === 'ahead' ? 'ds-acc' : n.st === 'drift' ? 'ds-warn' : 'ds-bad') }, ESTATE[n.st] || n.st) : el('span'))))] : null;
615
+ return el('div', { class: 'ds' },
616
+ S.lib.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.lib.err) : null,
617
+ any ? graphEl(G) : el('div', { class: 'fold', 'data-doc-norel': '' }, c3ic('info', 'sm'), 'No relation is recorded for this document.'),
618
+ relations ? relations(doc, (d) => { if (S.open) S.open(d); }) : [rel('Sources', G.left), rel('Goals and initiative', G.top.filter((n) => !n.init)), rel('Used by and copies', G.right)]);
619
+ }
620
+
321
621
  /* ── Read ── */
322
622
  function propsBlock(secs, editing) {
323
623
  const s0 = secs.find((s) => s.prefix === ''); const fm = s0 && parseFront(s0.text);
@@ -334,11 +634,14 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
334
634
  if (S.text == null) return S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the document.');
335
635
  const secs = splitDoc(S.text);
336
636
  if (!S.text.trim()) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
337
- const named = secs.filter((s) => s.prefix);
338
- const toc = el('select', { class: 'ds-sel', 'aria-label': 'Contents', 'data-doc-toc': '', onChange: (e) => { const v = e.target.value; e.target.value = ''; if (v !== '') goEl('[data-dsec-i="' + v + '"]'); } },
339
- el('option', { value: '' }, 'Contents'), named.map((s) => el('option', { value: String(s.i) }, (headLevel(s) > 2 ? ' ' : '') + s.name)));
637
+ const items = outlineOf(secs); const olOn = items.length >= 2 && !S.src;
638
+ if (S.cur == null && items.length) S.cur = items[0].s.i;
639
+ const curName = (items.find((x) => x.s.i === S.cur) || items[0] || { s: { name: '' } }).s.name;
640
+ const olCtl = olOn ? el('span', { class: 'ds-acts' },
641
+ btn('ds-btn ds-olbtn', { 'aria-expanded': String(S.olOpen), 'data-ol-btn': '' }, [c3ic('ul', 'sm'), 'Outline · ', el('b', null, curName)], () => { S.olOpen = !S.olOpen; const lay = ui.body.querySelector('.ds-rlay'); if (lay) lay.classList.toggle('olopen', S.olOpen); const b = ui.body.querySelector('[data-ol-btn]'); if (b) b.setAttribute('aria-expanded', String(S.olOpen)); }),
642
+ btn('ds-btn ds-olhide', { 'aria-pressed': String(!S.ol), 'data-ol-hide': '' }, [c3ic('ul', 'sm'), S.ol ? 'Hide the outline' : 'Show the outline'], () => { S.ol = !S.ol; olSet(S.ol); draw(); })) : el('span');
340
643
  const tg = (id, label, on, set) => el('label', { class: 'ds-tg' }, el('input', { type: 'checkbox', checked: on, 'data-doc-toggle': id, onChange: (e) => { set(e.target.checked); draw(); } }), label);
341
- const bar = el('div', { class: 'ds-rbar' }, toc, el('span', { class: 'ds-acts' },
644
+ const bar = el('div', { class: 'ds-rbar' }, olCtl, el('span', { class: 'ds-acts' },
342
645
  generated ? null : tg('who', 'Show who wrote what', S.who, (v) => { S.who = v; }), tg('src', 'Markdown source', S.src, (v) => { S.src = v; })));
343
646
  const legend = S.who ? el('div', { class: 'ds-legend' }, el('span', null, el('i', { class: 'h' }), 'Written by a person'), el('span', null, el('i', { class: 'a' }), 'Written by the agent, accepted by a person')) : null;
344
647
  if (S.src) {
@@ -368,8 +671,9 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
368
671
  });
369
672
  const adds = openProps().flatMap((p) => p.sections.filter((c) => c.kind === 'added' && S.preview[p.id]).map((c) => el('div', { class: 'ds-sec a prev', 'data-dsec-new': c.name },
370
673
  el('div', { class: 'ds-pin' }, el('span', null, c3ic('spark', 'sm'), 'The proposal adds a section: ' + c.name)), el('div', { class: 'ds-sec-h' }, el('h2', null, c.name)), md(diffMd(null, c.after), 'ds-body'))));
371
- return el('div', { class: 'ds' + (S.who ? ' who' : '') }, bar, legend, generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null,
372
- staleBanner(), propsBlock(secs, false), cells, adds);
674
+ const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, staleBanner(), propsBlock(secs, false), cells, adds];
675
+ return el('div', { class: 'ds' + (S.who ? ' who' : '') }, bar, legend,
676
+ olOn ? el('div', { class: 'ds-rlay' + (S.ol ? '' : ' nool') + (S.olOpen ? ' olopen' : '') }, outlineNav(items), el('div', { class: 'ds-rmain' }, main)) : el('div', { class: 'ds-rmain' }, main));
373
677
  }
374
678
  const short = (t) => { const s = String(t || '').replace(/\s+/g, ' ').trim(); return s.length > 90 ? s.slice(0, 88) + '…' : s; };
375
679
 
@@ -709,8 +1013,6 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
709
1013
  el('p', { class: 'ds-note' }, 'Health answers one question: can I trust this document without rereading it? The checks run again every time the text changes.'));
710
1014
  }
711
1015
 
712
- const linksBody = () => (relations ? relations(doc, (d) => { if (S.open) S.open(d); }) : el('div', { class: 'fold' }, c3ic('info', 'sm'), 'No relation is recorded for this document.'));
713
-
714
1016
  /* a load that finished in the background must not redraw under a hand that is typing */
715
1017
  const drawBg = () => { const a = document.activeElement; if (ui && a && ui.body.contains(a) && a.closest('input,textarea,[contenteditable=true]')) { refreshTop(); return; } draw(); };
716
1018
  const top = () => el('div', { class: 'ds-top' }, head(), seg(), S.mode === 'edit' && S.ed ? toolbar() : null,
@@ -723,21 +1025,26 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
723
1025
  const body = S.mode === 'read' ? readBody() : S.mode === 'edit' ? editBody() : S.mode === 'ask' ? propsBody() : S.mode === 'hist' ? logBody() : S.mode === 'health' ? healthBody() : linksBody();
724
1026
  const keep = ui.body.querySelector('textarea:focus,input[type=text]:focus');
725
1027
  const had = keep ? { id: keep.id, s: keep.selectionStart } : null;
726
- ui.body.replaceChildren(el('div', { class: 'dpn ds' }, top(), body));
1028
+ ui.body.replaceChildren(el('div', { class: 'dpn ds' }, top(), copiesPanel(), body));
1029
+ const sh = ui.body.closest('.sheet'); if (sh) sh.classList.toggle('dwx', S.mode === 'read' && S.ol && !!ui.body.querySelector('.ds-ol'));
1030
+ const sb = ui.body.closest('.sb');
1031
+ if (sb && !sb.__dsTrack) { sb.__dsTrack = true; sb.addEventListener('scroll', trackCur, { passive: true }); }
1032
+ if (S.mode === 'read') { placeOl(); trackCur(); }
1033
+ if (S.mode === 'links') { const g = ui.body.querySelector('.ds-graph'); if (g) requestAnimationFrame(() => drawEdges(g)); }
727
1034
  if (had && had.id) { const n = ui.body.querySelector('#' + had.id); if (n) { n.focus(); try { n.setSelectionRange(had.s, had.s); } catch { /* not a text field */ } } }
728
1035
  if (S.mode === 'ask' && S.openProp) { const id = S.openProp; S.openProp = null; goEl('[data-doc-prop-card="' + id + '"]'); }
729
1036
  }
730
1037
 
731
1038
  document.addEventListener('selectionchange', keepRange);
732
1039
  ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; document.removeEventListener('selectionchange', keepRange); if (S.changed && onChanged) onChanged(); } });
733
- S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); };
1040
+ S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); else openDocSheet({ ws, doc: d, library, openGraph }); };
734
1041
  ui.setOpener = (fn) => { S.openOther = fn; };
735
1042
  draw();
736
1043
  (async () => {
737
1044
  await loadText();
738
1045
  if (S.mode === 'edit') startEdit();
739
1046
  drawBg();
740
- await Promise.all([loadRevs(), loadProps(), loadHealth()]);
1047
+ await Promise.all([loadRevs(), loadProps(), loadHealth(), loadCopies()]);
741
1048
  if (S.mode === 'hist') await loadLog();
742
1049
  drawBg(); loadMeta();
743
1050
  })();
@@ -58,7 +58,8 @@ export async function badge(ctx) {
58
58
  function openDoc(V, d, ask) {
59
59
  if (!d || d.st === 'missing' || d.node) return;
60
60
  const ui = openDocSheet({ ws: V.ctx.ws, doc: { path: d.path, name: d.name, ty: d.ty, v: d.v, st: d.st, ini: d.ini, iniName: iniName(V, d.ini) }, mode: ask ? 'ask' : 'read', prompt: ask || '',
61
- relations: (doc, openOther) => relationsOf(V, d, openOther), onChanged: () => { delete V.health[d.path]; V.asking = false; load(V); } });
61
+ relations: (doc, openOther) => relationsOf(V, d, openOther), library: () => V.lib, openGraph: () => { V.view = 'graph'; V.sel = d.i; V.render(); },
62
+ onChanged: () => { delete V.health[d.path]; V.asking = false; load(V); } });
62
63
  ui.setOpener((o) => openDoc(V, o));
63
64
  }
64
65
  const iniName = (V, id) => { const i = V.lib && V.lib.initiatives && V.lib.initiatives[id]; return i ? i.short || i.title : id || ''; };
@@ -30,7 +30,9 @@ export async function loadDocs(ctx, idSet) {
30
30
  async function openDoc(ctx, d, titles) {
31
31
  try {
32
32
  const { openDocSheet } = await import('./c3-doc-sheet.js');
33
- openDocSheet({ ws: ctx.ws, doc: { path: d.path, name: d.name, ty: d.ty, v: '', st: d.st, ini: d.ini, iniName: titles.get(d.ini) || d.ini } });
33
+ const ui = openDocSheet({ ws: ctx.ws, doc: { path: d.path, name: d.name, ty: d.ty, v: '', st: d.st, ini: d.ini, iniName: titles.get(d.ini) || d.ini },
34
+ library: () => ctx.api.get('/api/docs/library', { board: ctx.board.id }) });
35
+ ui.setOpener((o) => openDoc(ctx, { path: o.path, name: o.name, ty: o.ty, st: o.st, ini: o.ini }, titles));
34
36
  } catch { ctx.toast('The document sheet could not open. The file is ' + d.path + '.'); }
35
37
  }
36
38
 
@@ -2073,6 +2073,89 @@
2073
2073
  }
2074
2074
  .c3 .ds input[type=checkbox]{width:auto;min-height:0;padding:0;margin:0;flex:none;border-radius:0}
2075
2075
 
2076
+ /* the document sheet v2 (initiative c3-doc-sheet-v2): the outline, the copies in other tools, the Relations graph */
2077
+ @media (min-width:761px){.c3 .sheet.dw.dwx{width:min(1100px,calc(100% - 24px))}}
2078
+ .c3 .ds-acc{color:var(--acc)}
2079
+ .c3 .ds-rmain{display:grid;gap:var(--sp-3);min-width:0}
2080
+ .c3 .ds-rlay{display:grid;grid-template-columns:232px minmax(0,1fr);gap:var(--sp-4);align-items:start}
2081
+ .c3 .ds-rlay.nool{grid-template-columns:minmax(0,1fr)}
2082
+ .c3 .ds-rlay.nool .ds-ol{display:none}
2083
+ .c3 .ds-ol{position:sticky;top:var(--in-ds-oltop,0);max-height:var(--in-ds-olmax,60vh);overflow:auto;display:grid;gap:6px;align-content:start;padding:10px 8px 10px 0;border-right:1px solid var(--border);font-size:var(--fs-2)}
2084
+ .c3 .ds-ol .hd{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding-left:8px}
2085
+ .c3 .ds-ol .hd .ds-mono{font-size:var(--fs-1)}
2086
+ .c3 .ds-ol input{margin-left:8px;width:calc(100% - 8px);font:var(--fs-2) var(--mono);padding:5px 8px;border:1px solid var(--border);border-radius:6px;background:var(--background);color:var(--foreground)}
2087
+ .c3 .ds-ol ul{list-style:none;margin:0;padding:0;display:grid}
2088
+ .c3 .ds-ol li{display:grid;grid-template-columns:18px minmax(0,1fr);align-items:start}
2089
+ .c3 .ds-ol li.l2 .go{padding-left:18px}
2090
+ .c3 .ds-ol li.l3 .go{padding-left:32px}
2091
+ .c3 .ds-ol .tw{width:18px;min-height:26px;display:grid;place-items:center;color:var(--muted-foreground);background:none;border:0;padding:0;border-radius:6px}
2092
+ .c3 .ds-ol .tw[aria-expanded="true"] .ic{transform:rotate(90deg)}
2093
+ .c3 .ds-ol .go{display:flex;align-items:flex-start;gap:6px;width:100%;text-align:left;background:none;border:0;border-left:2px solid transparent;padding:4px 8px;border-radius:0;color:var(--muted-foreground);line-height:1.35;font:inherit;min-height:0;justify-content:flex-start}
2094
+ .c3 .ds-ol .go span{flex:1;min-width:0}
2095
+ .c3 .ds-ol li.l1 .go{color:var(--foreground)}
2096
+ .c3 .ds-ol li.on .go{color:var(--acc);border-left-color:var(--acc);background:color-mix(in oklab,var(--acc) 8%,transparent);font-weight:500}
2097
+ .c3 .ds-ol .mdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-top:6px;flex:none}
2098
+ .c3 .ds-ol .mdot.w{background:var(--warning)}
2099
+ .c3 .ds-ol .mdot.p{background:var(--agent)}
2100
+ .c3 .ds-ol .key{display:flex;flex-wrap:wrap;gap:4px 10px;padding:6px 0 0 8px;border-top:1px solid var(--border);font-size:var(--fs-1)}
2101
+ .c3 .ds-ol .key span{display:inline-flex;gap:4px;align-items:center}
2102
+ .c3 .ds-ol .key .mdot{margin:0}
2103
+ .c3 .ds-ol .none{padding:4px 8px}
2104
+ .c3 .ds-olbtn{display:none}
2105
+ .c3 .ds-olbtn b{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
2106
+ @media (hover:hover){.c3 .ds-ol .go:hover{background:var(--muted);color:var(--foreground)}.c3 .ds-gn:not(.x):not(.me):hover{border-color:var(--ring)}}
2107
+ @media (max-width:900px){
2108
+ .c3 .ds-rlay{grid-template-columns:minmax(0,1fr)}
2109
+ .c3 .ds-olhide{display:none}
2110
+ .c3 .ds-olbtn{display:inline-flex;max-width:100%}
2111
+ .c3 .ds-ol{display:none;z-index:3;max-height:60vh;border:1px solid var(--ring);border-radius:8px;background:var(--card);padding:10px 8px;box-shadow:0 8px 24px color-mix(in oklab,var(--foreground) 14%,transparent)}
2112
+ .c3 .ds-rlay.olopen .ds-ol,.c3 .ds-rlay.nool.olopen .ds-ol{display:grid}
2113
+ }
2114
+ .c3 .ds-sync{display:grid;gap:8px;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--card);min-width:0}
2115
+ .c3 .ds-sync .hd{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}
2116
+ .c3 .ds-sync ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}
2117
+ .c3 .ds-sync li{display:grid;gap:4px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--background);min-width:0}
2118
+ .c3 .ds-sync .l1{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
2119
+ .c3 .ds-sync .l1 .tag{margin-left:auto;display:inline-flex;gap:4px;align-items:center}
2120
+ .c3 .ds-sync p{margin:0;font-size:var(--fs-2);line-height:1.5}
2121
+ .c3 .ds-sync .ds-mono{font-size:var(--fs-1);overflow-wrap:anywhere}
2122
+ .c3 .ds-sync a.ds-btn{text-decoration:none}
2123
+ .c3 .ds-linkadd input{border-color:var(--border)}
2124
+ .c3 .ds-graph{position:relative;border:1px solid var(--border);border-radius:8px;padding:12px;background:var(--card);display:grid;gap:18px;min-width:0}
2125
+ .c3 .ds-gsvg{position:absolute;left:0;top:0;pointer-events:none;overflow:visible}
2126
+ .c3 .ds-gsvg .e{fill:none;stroke:var(--ring);stroke-width:1.5}
2127
+ .c3 .ds-gsvg .e.drift{stroke:var(--warning);stroke-dasharray:5 4}
2128
+ .c3 .ds-gsvg .e.conflict{stroke:var(--destructive);stroke-dasharray:5 4}
2129
+ .c3 .ds-gsvg .e.missing{stroke:var(--muted-foreground);stroke-dasharray:2 4}
2130
+ .c3 .ds-gsvg .e.ahead{stroke:var(--acc)}
2131
+ .c3 .ds-gtop{display:flex;justify-content:center;gap:12px;flex-wrap:wrap;position:relative}
2132
+ .c3 .ds-gmid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr) minmax(0,1fr);gap:40px;align-items:center;position:relative}
2133
+ .c3 .ds-gcol{display:grid;gap:10px;min-width:0}
2134
+ .c3 .ds-gn{display:grid;gap:2px;text-align:left;padding:6px 10px;border:1px solid var(--border);border-radius:8px;background:var(--background);color:var(--foreground);min-width:0;position:relative;text-decoration:none;font:inherit;min-height:0;justify-content:stretch}
2135
+ .c3 .ds-gtop .ds-gn{min-width:140px;max-width:220px}
2136
+ .c3 .ds-gn b{font-weight:500;font-size:var(--fs-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2137
+ .c3 .ds-gn small{font:var(--fs-1) var(--mono);color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2138
+ .c3 .ds-gn.drift small{color:var(--warning)}
2139
+ .c3 .ds-gn.conflict small{color:var(--destructive)}
2140
+ .c3 .ds-gn.ahead small{color:var(--acc)}
2141
+ .c3 .ds-gn.me{border-color:var(--acc);border-width:1.5px;background:color-mix(in oklab,var(--acc) 10%,var(--background));padding:10px 12px}
2142
+ .c3 .ds-gmore{font-size:var(--fs-1)}
2143
+ .c3 .ds-graph .key{display:flex;flex-wrap:wrap;gap:4px 14px;align-items:center;font-size:var(--fs-1);position:relative}
2144
+ .c3 .ds-graph .key span{display:inline-flex;gap:6px;align-items:center}
2145
+ .c3 .ds-graph .key .ln{width:14px;height:2px;display:inline-block;background:var(--ring)}
2146
+ .c3 .ds-graph .key .ln.drift{background:var(--warning)}
2147
+ .c3 .ds-graph .key .ln.ahead{background:var(--acc)}
2148
+ .c3 .ds-graph .key .chip2{margin-left:auto}
2149
+ .c3 .ds-rel{list-style:none;margin:0;padding:0;display:grid;gap:4px}
2150
+ .c3 .ds-rel li{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border)}
2151
+ .c3 .ds-rlk{display:grid;gap:2px;text-align:left;background:none;border:0;padding:0;color:var(--foreground);font:inherit;text-decoration:none;min-height:0;justify-content:start}
2152
+ .c3 .ds-rlk small{font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
2153
+ @media (max-width:640px){
2154
+ .c3 .ds-gmid{gap:16px}
2155
+ .c3 .ds-graph{padding:8px}
2156
+ }
2157
+ @media (pointer:coarse){.c3 .ds-ol .go{min-height:44px}.c3 .ds-ol .tw{min-height:44px}.c3 .ds-gn{min-height:44px}}
2158
+
2076
2159
  /* the card before a meeting (initiative c3-meeting-card) */
2077
2160
  .c3 .cal3-ctx .cal3-p{margin:0;font-size:var(--fs-2);line-height:1.5;color:var(--foreground)}
2078
2161
  .c3 .cal3-ctx.cal3-gist{padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--muted)}
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "Initiative c3-doc-sheet-v2: the console3 document sheet outline, the copies in other tools, and the Relations graph. The browser suite runs the done-criteria.",
3
+ "modules": [],
4
+ "panels": ["pm-c3-doc-sheet-v2-test.js"],
5
+ "seconds": { "pm-c3-doc-sheet-v2-test.js": 60 }
6
+ }