create-pmos 0.509.0 → 0.510.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.509.0",
3
+ "version": "0.510.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -32,6 +32,25 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.510.0",
37
+ "at": "2026-11-03T14:29:03.666Z",
38
+ "f": 3,
39
+ "prs": [
40
+ {
41
+ "n": 1197,
42
+ "t": "Clean Updates lines: no, one PR number, no repeated title, right group",
43
+ "s": "Clean Updates lines: no, one PR number, no repeated title, right group",
44
+ "k": "fix"
45
+ },
46
+ {
47
+ "n": 1199,
48
+ "t": "A Board product head's name opens its page in the Viewer",
49
+ "s": "A Board product head's name opens its page in the Viewer",
50
+ "k": "new"
51
+ }
52
+ ]
53
+ },
35
54
  {
36
55
  "v": "0.509.0",
37
56
  "at": "2026-11-03T14:11:23.227Z",
@@ -440,19 +459,6 @@
440
459
  "k": "fix"
441
460
  }
442
461
  ]
443
- },
444
- {
445
- "v": "0.490.0",
446
- "at": "2026-11-02T08:52:24.978Z",
447
- "f": 3,
448
- "prs": [
449
- {
450
- "n": 1153,
451
- "t": "Sign-in session takes keys; login check names the login",
452
- "s": "Sign-in session takes keys; login check names the login",
453
- "k": "fix"
454
- }
455
- ]
456
462
  }
457
463
  ]
458
464
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@be5277db
1
+ kernel-source: pmos@c10daf2f
2
2
  assembled: 2026-11-03
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-11-03T14:11:20Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T14:29:00Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-11-03T14:11:20Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T14:29:00Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-11-03T14:11:20Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T14:29:00Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -99,6 +99,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
99
99
  ok('O pressing the band dispatches bd:program with the program id and opens no initiative panel', (await events(p)).join() === 'project-ascent' && await p.evaluate(() => !/Initiative/.test(((window.czViewer && window.czViewer.current()) || {}).kind || '')), JSON.stringify(await events(p)));
100
100
  await p.keyboard.press('Escape'); await p.waitForFunction(() => { const v = document.querySelector('#czViewer'); return !v || v.hidden; });
101
101
 
102
+ // batch 26 item 3: a product head's tile and name open its page in Home's Viewer; the chevron is its own button and folds
103
+ const ph = await p.evaluate(() => { const o = document.querySelector('#ibBody .bt-prd [data-prodopen]'), f = document.querySelector('#ibBody .bt-prd [data-fold]'); return { open: !!o && !o.contains(f) && /^Open IDPF, a product$/.test(o.getAttribute('aria-label')) && !!o.querySelector('.bd-ti'), fold: !!f && f.hasAttribute('aria-expanded') }; });
104
+ ok('P a product head: the tile and name are one button that opens the product, the fold is a separate button', ph.open && ph.fold, JSON.stringify(ph));
102
105
  await p.click('#ibBody .bt-pg [data-fold]');
103
106
  const f1 = await p.evaluate(() => ({ kids: document.querySelectorAll('#ibBody .bt-kid').length, band: !!document.querySelector('#ibBody .bt-pg'), exp: document.querySelector('#ibBody .bt-pg [data-fold]').getAttribute('aria-expanded'), st: localStorage.getItem('pmos.board.fold') }));
104
107
  ok('F the program group folds to its band row, and the fold is stored', f1.kids === 0 && f1.band && f1.exp === 'false' && /g:project-ascent/.test(f1.st), JSON.stringify(f1));
@@ -1,4 +1,23 @@
1
1
  [
2
+ {
3
+ "v": "0.510.0",
4
+ "at": "2026-11-03T14:29:03.666Z",
5
+ "f": 3,
6
+ "prs": [
7
+ {
8
+ "n": 1197,
9
+ "t": "Clean Updates lines: no, one PR number, no repeated title, right group",
10
+ "s": "Clean Updates lines: no, one PR number, no repeated title, right group",
11
+ "k": "fix"
12
+ },
13
+ {
14
+ "n": 1199,
15
+ "t": "A Board product head's name opens its page in the Viewer",
16
+ "s": "A Board product head's name opens its page in the Viewer",
17
+ "k": "new"
18
+ }
19
+ ]
20
+ },
2
21
  {
3
22
  "v": "0.509.0",
4
23
  "at": "2026-11-03T14:11:23.227Z",
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Programs on the Board: All products, groupings and the program panel"
4
4
  description: "A program groups initiatives across products. How it shows on the Board (stage track, Kanban, timeline and List), the All products mode, the By product grouping, the program panel and the side panel width."
5
- timestamp: 2026-11-01
5
+ timestamp: 2026-11-03
6
6
  ---
7
7
 
8
8
  # Programs on the Board
@@ -18,6 +18,7 @@ A cancelled or archived initiative leaves its program's views: the band, the pro
18
18
  - The product picker is the same chip and list as on Home (see [Home](home.md)). It shows the tiles of the ticked products, or **All**. When the list closes, the Board shows the ticked products, merged as on All products. The pick is the saved setting that Home and Insights share, so a reload keeps it, and a product that you untick here goes from Home too.
19
19
  - An address with `?ws=`, `?board=`, `?all=1` or `?pick=` is a one-off view: the Board shows what the address names and does not change the saved pick. A change in the list saves the pick and opens the Board without those parts.
20
20
  - A program comes first. Its band shows the stage spread of its initiatives, the next milestone and how many initiatives are ready for it. Under the band, the initiatives sit under a small heading for each product. Then each product has its own group.
21
+ - A product's group head works like a program's: press the product's tile or name to open its page in Home's Viewer. Press the chevron to fold the group. This works in every view: the stage track, Kanban, the timeline and List. On a shared board, the product's name is plain text.
21
22
  - To group by product only, open **Filter** and choose **By product**. A program member then shows a small layers icon and stays under its product in every view, the timeline included. A chip shows while By product is on.
22
23
  - **Kanban** shows programs as swimlanes, then products. Press a column header to fold the column to a strip with its count. An empty column starts folded. The Board remembers each fold.
23
24
  - The **timeline** draws a program row that brackets its initiatives' bars, on every span. Program milestones are diamonds. Milestones that are close together merge into one diamond with a count.
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Settings in console3: one home for each setting"
4
4
  description: "How to open Settings in console3, what each section holds, what the scope tags and the environment notes mean, and where the settings are kept."
5
- timestamp: 2026-11-01
5
+ timestamp: 2026-11-03
6
6
  ---
7
7
 
8
8
  # Settings in console3: one home for each setting
@@ -70,7 +70,7 @@ Open **Updates**. PMOS asks the npm registry for the latest version at once, and
70
70
  Under the update card, **Updates** shows what changed in the published kits. **This week** is open: it is the one you need. A week runs Monday to Sunday. Every earlier week, last week included, is folded into **Archive**. Press **Archive** to list the earlier weeks (**Week of** a date), each with its counts, and press a week to read it. When there is nothing for this week yet, the newest week with changes is the open one.
71
71
 
72
72
  - Each week has up to three groups: **New** (a feature), **Improved** (a faster or better thing that already existed) and **Fixed** (a problem that is gone). A group with nothing in it is not shown, and a week with nothing to show is not listed.
73
- - Each line is a bold name and one sentence, copied from the pull request that made the change, with its number at the end. Work that you cannot see in PMOS (notes, tests, run logs) is left out.
73
+ - Each line is a bold name and one sentence, copied from the pull request that made the change, with its number at the end. When the name and the sentence say the same thing, the line shows it once. Work that you cannot see in PMOS (notes, tests, run logs) is left out, and so are notes for the people who build PMOS, such as "[skip publish]" or "(batch 25 item 11)".
74
74
  - A week shows **Ready to install** when one of its kits is newer than the version you run.
75
75
  - A kit published before PMOS kept the kind of each change has no kind. Its title decides: a word such as "fix" or "no longer" puts it under **Fixed**, and anything else under **Improved**.
76
76
  - **What's new** is not a section of its own any more. The button on the update card in **General** opens **Updates**.
@@ -122,12 +122,24 @@ const npxArgs = (src, version, rest) => (src && src.kind === 'local'
122
122
  ? ['-y', `--package=${src.path}`, '--', PKG_NAME, ...rest]
123
123
  : ['-y', `${PKG_NAME}@${version}`, ...rest]);
124
124
 
125
- /** One changelog entry as the page may show it, or null: a version, a time, and PRs with a number, a summary and, from
126
- * kit-changelog format 2, a title. */
125
+ /** One changelog entry as the page may show it, or null: a version, a time, and PRs with a number, a summary, from
126
+ * kit-changelog format 2 a title, and from format 3 a kind (k: new, imp, fix or x), which decides New, Improved or Fixed.
127
+ * Kits published before kit-changelog dropped "[skip publish]" kept it in the text and lost the PR number to an inline
128
+ * "(#123)"; both are repaired here, so every line ends in one muted number. */
129
+ const KINDS = new Set(['new', 'imp', 'fix', 'x']);
130
+ const SKIP_TAG = /\s*\[(?:skip|no)[ -][a-z]+\]/gi;
131
+ const PR_REF = /\s*\(#(\d+)\)/;
132
+ const WORK_TAG = /\s*\((?:batch \d+|canvas |D\d+\b)[^()]*\)\s*$/i; // "(batch 25 item 11)", "(canvas TplA)", "(D105)": how it was made, not what it does
133
+ const tidy = (t) => t.replace(SKIP_TAG, '').replace(new RegExp(PR_REF.source, 'g'), '').replace(/\s+/g, ' ').trim().replace(WORK_TAG, '').trim();
134
+ function cleanPr(p) {
135
+ const t = typeof p.t === 'string' ? p.t : '', ref = PR_REF.exec(t) || PR_REF.exec(p.s);
136
+ const n = Number.isInteger(p.n) ? p.n : ref ? Number(ref[1]) : null;
137
+ const tt = tidy(t).slice(0, 200), s = tidy(p.s).slice(0, 400);
138
+ return { n, ...(tt ? { t: tt } : {}), s: s || tt, ...(KINDS.has(p.k) ? { k: p.k } : {}) };
139
+ }
127
140
  function cleanEntry(e) {
128
141
  if (!e || !validVersion(e.v)) return null;
129
- const prs = (Array.isArray(e.prs) ? e.prs : []).filter((p) => p && typeof p.s === 'string' && p.s.trim())
130
- .map((p) => ({ n: Number.isInteger(p.n) ? p.n : null, ...(typeof p.t === 'string' && p.t.trim() ? { t: p.t.trim().slice(0, 200) } : {}), s: p.s.trim().slice(0, 400) }));
142
+ const prs = (Array.isArray(e.prs) ? e.prs : []).filter((p) => p && typeof p.s === 'string' && p.s.trim()).map(cleanPr).filter((p) => p.s);
131
143
  return { v: e.v, at: typeof e.at === 'string' && !Number.isNaN(Date.parse(e.at)) ? e.at : null, prs };
132
144
  }
133
145
  const cleanList = (list) => (Array.isArray(list) ? list.map(cleanEntry).filter(Boolean) : []);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.509.0",
3
+ "version": "0.510.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.509.0",
9
+ "version": "0.510.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/sdk": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.509.0",
3
+ "version": "0.510.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",
@@ -10,8 +10,8 @@
10
10
  * readyHtml(m, whole) a milestone's readiness as small diamonds, one per initiative it waits on
11
11
  * mergeBoards(list) All products: one Board model from every product's model, each row keeping its own product
12
12
  * bandHtml(g, st, o) the stage track's program row: tile, title, products, your count, its spread, its next milestones
13
- * prodHtml(g, o) · subHtml(name) a product group's head row (tile, name, goal dots, chevron) and a program's product sub-row
14
- * prodCell(g, dots) the head's fold button alone (the timeline's product lane, batch 11 item 17); dots replace the goal dots
13
+ * prodHtml(g, o) · subHtml(name) a product group's head row (tile and name open the product, goal dots, chevron folds) and a program's product sub-row
14
+ * prodCell(g, dots) the head's open (tile, name) and fold buttons alone (the timeline's product lane, batch 11 item 17); dots replace the goal dots
15
15
  * goalsFrom(fn) · goalsHtml(name) the goal dots of a product head, from Home's source (pm/home-list.js goalsOf); fn(name) gives its rows
16
16
  *
17
17
  * A program has no stage of its own (PM, 2026-10-23): the band shows only what its initiatives say. With one product picked, the program shows
@@ -173,9 +173,14 @@ export function prodHtml(g, cls = 'bt-prd') {
173
173
  /** The fold button of a product head alone: the timeline's product lane (batch 11 item 17, PlmA1Board) puts it in the name cell, its track beside it. */
174
174
  export function prodCell(g, dots) {
175
175
  // the calm timeline (batch 13 item 4) passes its own dots: none while open, one per initiative off track while folded
176
+ // the tile and name open the product's page in the Viewer (batch 26 item 3, as Home's header and a program's title do); the rest folds
176
177
  const open = !folded(g.key), gl = dots !== undefined ? dots : open ? goalsHtml(g.name) : '';
177
- return `<button type="button" class="bd-prdb" data-fold="${esc(g.key)}" aria-expanded="${open}" aria-label="${esc(g.name)}, ${g.rows.length} ${plural(g.rows.length, 'initiative', 'initiatives')}. ${open ? 'Fold' : 'Show'}">${tileHtml(g.name)}<span>${esc(g.name)}</span>${gl}${open || dots !== undefined ? '' : `<span class="n">${g.rows.length}</span>`}${SV(open ? CHEV_D : CHEV_R)}</button>`;
178
+ return `<span class="bd-prdb">${prodOpen(g, 'bd-prdo')}<button type="button" class="bd-prdf" data-fold="${esc(g.key)}" aria-expanded="${open}" aria-label="${esc(g.name)}, ${g.rows.length} ${plural(g.rows.length, 'initiative', 'initiatives')}. ${open ? 'Fold' : 'Show'}" title="${open ? 'Fold' : 'Show'}">${gl}${open || dots !== undefined ? '' : `<span class="n">${g.rows.length}</span>`}${SV(open ? CHEV_D : CHEV_R)}</button></span>`;
178
179
  }
180
+ /** A product's workspace, from its rows (All products: each row keeps its own); '' when the rows carry none (the page uses its own board's). */
181
+ export const prodWs = (g) => ((g.rows || []).find((r) => r && r.ws) || {}).ws || '';
182
+ /** A product head's tile and name as one button: it opens that product's page in the Viewer (pm/premium-board.js data-prodopen). */
183
+ export const prodOpen = (g, cls) => `<button type="button" class="${cls}" data-prodopen="${esc(g.name)}" data-prodws="${esc(prodWs(g))}" aria-label="Open ${esc(g.name)}, a product">${tileHtml(g.name)}<span>${esc(g.name)}</span></button>`;
179
184
  /** The goal dots of a product's head (PgBoard .gd), from the same source as Home's product header (pm/home-list.js goalsOf): one per KR or
180
185
  * budget its open initiatives anchor to, green on track, amber at risk. With no goal anchored, nothing is drawn. The page sets the rows. */
181
186
  let ROWS_OF = null;
@@ -191,11 +196,11 @@ export const subHtml = (name, cls = 'bt-psb') => `<div class="${cls}" role="row"
191
196
  export const memberIcon = (programs, id) => { const p = (programs || []).find((x) => x.id === id); return p ? `<span class="bd-pgi" role="img" aria-label="In ${esc(p.title)}" title="In ${esc(p.title)}">${SV(PROG)}</span>` : ''; };
192
197
 
193
198
  /** A Kanban swimlane's head (PgKanban .lh): a program's tile, title, your count and its next milestones merged into one counted diamond with
194
- * the readiness of the first; a product's tile and name. Both fold with the chevron; the program's title opens the program. */
199
+ * the readiness of the first; a product's tile and name. Both fold with the chevron; the title opens the program or the product. */
195
200
  export function laneHtml(g, st) {
196
201
  const open = !folded(g.key);
197
202
  const fb = (label) => `<button type="button" class="bd-ib sm bd-fold" data-fold="${esc(g.key)}" aria-expanded="${open}" aria-label="${open ? 'Fold' : 'Show'} ${esc(label)}" title="${open ? 'Fold' : 'Show'}">${SV(open ? CHEV_D : CHEV_R)}</button>`;
198
- if (g.kind !== 'program') return `<button type="button" class="bd-lho" data-fold="${esc(g.key)}" aria-expanded="${open}" aria-label="${esc(g.name)}, ${g.rows.length} ${plural(g.rows.length, 'initiative', 'initiatives')}. ${open ? 'Fold' : 'Show'}">${tileHtml(g.name)}<span>${esc(g.name)}</span></button><span class="bd-sp"></span>${open ? goalsHtml(g.name) : ''}${fb(g.name)}`;
203
+ if (g.kind !== 'program') return `${prodOpen(g, 'bd-lho')}<span class="bd-sp"></span>${open ? goalsHtml(g.name) : ''}${fb(g.name)}`;
199
204
  const p = g.program, c = nextCluster(st);
200
205
  const ms = c ? `<span class="bd-pm2${c.late ? ' late' : ''}" title="${esc(c.items.map((m) => m.name + (m.day ? ' ' + m.day : '')).join(' · '))}"><i class="bd-dm"></i><b>${esc(c.label)}</b>${esc(c.range)}${readyHtml(c.first, whole(st))}</span>` : '';
201
206
  return `<button type="button" class="bd-lho" data-prog="${esc(p.id)}" aria-haspopup="dialog" aria-label="${esc(p.title)}, a program. Open the program">${progTile()}<span>${esc(p.title)}</span></button>${yoursHtml(st)}<span class="bd-sp"></span>${ms}${fb(p.title)}`;
@@ -1085,7 +1085,7 @@ html[data-theme="dark"] body.bd-page { --bd-prog: rgb(180,170,240); --bd-progbg:
1085
1085
  .bd-rdy i { display: block; width: 6px; height: 6px; border-radius: 1px; transform: rotate(45deg); background: var(--bd-plan); }
1086
1086
  .bd-rdy i.on { background: var(--bd-prog); }
1087
1087
  .bd-fold { color: var(--cz-ink3); }
1088
- .bd-fold:focus-visible, .bt-pgo:focus-visible, .bd-prdb:focus-visible, .bd-lho:focus-visible, .tg-pgo:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 10px; }
1088
+ .bd-fold:focus-visible, .bt-pgo:focus-visible, .bd-prdo:focus-visible, .bd-prdf:focus-visible, .bd-lho:focus-visible, .tg-pgo:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 10px; }
1089
1089
  /* the grouping control in the toolbar (PgBoardProduct: "By product" on in violet) */
1090
1090
  .bd-grpw2 { position: relative; display: inline-flex; align-items: center; }
1091
1091
  .bd-grpw2[hidden] { display: none; }
@@ -1119,7 +1119,13 @@ html[data-theme="dark"] body.bd-page { --bd-prog: rgb(180,170,240); --bd-progbg:
1119
1119
  .bt-psb > span, .tg-psb > span { display: inline-flex; align-items: center; gap: 8px; }
1120
1120
  .bt-psb::before, .tg-psb::before { content: ""; position: absolute; left: 26px; top: 0; bottom: 0; width: 2px; background: var(--bd-progln); }
1121
1121
  .bt-prd, .tg-prd { display: flex; align-items: center; padding: 8px 14px; background: var(--bd-row); border-top: 1px solid var(--cz-hair); }
1122
- .bd-prdb { all: unset; box-sizing: border-box; cursor: pointer; flex: 1; display: flex; align-items: center; gap: 10px; font-size: var(--fs-3); font-weight: 800; color: var(--cz-ink); min-height: 28px; }
1122
+ .bd-prdb { all: unset; box-sizing: border-box; flex: 1; display: flex; align-items: center; gap: 10px; font-size: var(--fs-3); font-weight: 800; color: var(--cz-ink); min-height: 28px; }
1123
+ /* the tile and name open the product's page (batch 26 item 3, as Home's header); the rest of the head, its dots, count and chevron, folds it */
1124
+ .bd-prdo, .bd-prdf { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; gap: 10px; min-height: 28px; }
1125
+ .bd-prdo { flex: 0 1 auto; min-width: 0; }
1126
+ .bd-prdo > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1127
+ .bd-prdf { flex: 1; align-self: stretch; justify-content: flex-end; }
1128
+ @media (hover: hover) { .bd-prdo:hover > span:last-child, .bd-lh.pd .bd-lho:hover > span:last-child { color: var(--cz-blueink); } }
1123
1129
  .bd-prdb svg { width: 15px; height: 15px; stroke-width: 2; color: var(--cz-ink3); margin-left: auto; }
1124
1130
  /* a product head's goal dots (PgBoard .gd): Home's source and look (cozy-today.css .hv-gd) */
1125
1131
  .bd-prdb .n { font-size: var(--fs-2); font-weight: 700; color: var(--cz-ink3); margin-left: auto; }
@@ -639,6 +639,8 @@ $('#ibBody').addEventListener('click', (e) => {
639
639
  const tk = e.target.closest('[data-tk]'); if (tk) { openTicket(tk.dataset.ini, tk.dataset.tk, tk); return; }
640
640
  if (e.target.closest('[data-act], [data-hold], [data-ms-pick], [data-fc-pick]')) return; // pm/bd-acts.js handles its own presses (a ⏸ on the timeline opens its hold there; a ◇ its milestones, pm/bd-pin-card.js)
641
641
  const pg = e.target.closest('[data-prog]'); if (pg) { PG.openProgram(pg.dataset.prog, pg); return; }
642
+ // a product head's tile and name (batch 26 item 3): that product's page in Home's Viewer, the one place the product page is drawn
643
+ const pd = e.target.closest('[data-prodopen]'); if (pd) { location.assign(productHref(pd.dataset.prodws)); return; }
642
644
  // a counted milestone ◇ lists its milestones right under it; on a phone the row's bar lane opens them in a sheet (batch 11 item 3)
643
645
  const mc = e.target.closest('[data-ms-row]'); if (mc) { msPop(mc, rowOf(mc.dataset.msRow), mc.dataset.msIds.split(' ')); return; }
644
646
  // a product lane's counted ◇ lists its milestones with their initiatives; on a phone a tap on the lane lists them all in a sheet (batch 11 item 17)
@@ -693,6 +695,8 @@ function askHref(r, q) {
693
695
  const ws = (r && r.ws) || M.board.ws; if (ws) p.set('ws', ws);
694
696
  return '/premium.html?' + p.toString();
695
697
  }
698
+ /** A product's page (Home's Viewer, home-app.js openProductPage): Home on that product's workspace with ?ini=product. ws '': this board's own. */
699
+ const productHref = (ws) => { const w = ws || (M && !M.board.all ? M.board.ws : ''); return '/premium.html?' + (w ? 'ws=' + encodeURIComponent(w) + '&' : '') + 'ini=product'; };
696
700
  const pageHref = (r) => { const ws = r.ws || M.board.ws, b = r.boardId || M.board.id; return '/premium-initiative.html?id=' + encodeURIComponent(r.id) + (ws ? '&ws=' + encodeURIComponent(ws) + '&board=' + encodeURIComponent(b) : ''); };
697
701
 
698
702
  /* ── the initiative and its Jira ticket open in the Viewer's shell A, floating over the page (pm/bd-ini-panel.js; canvas XvABoard, XvOptAJira).
@@ -47,7 +47,9 @@ const REMOVE = [
47
47
  * milestone opens the program's charter in the workspace; a share does not hold the charter. */
48
48
  const PLAIN = ['button.dl-e[aria-label*="Program milestone"]:not([aria-label*="press to open"])', 'button.cz-vw-rn', 'button.iv-tt', 'button.iv-dc', 'button.iv-stc', 'button.iv-dtc', 'button.iv-wf', 'button.iv-cb', 'button.tg-end',
49
49
  // the program page (batch 21 item 2): a task's check box, a goal (its Viewer is in Intelligence), a report (its file is the publisher's), a program check
50
- 'button.pp-ck', 'button.pv-gr', 'button.db-rep', 'button.pp-dm'];
50
+ 'button.pp-ck', 'button.pv-gr', 'button.db-rep', 'button.pp-dm',
51
+ // a product head's tile and name (batch 26 item 3): its page is in the publisher's Home, which a share does not hold
52
+ 'button[data-prodopen]'];
51
53
  /** A fold whose body goes with it: Documents and Linked are not listed. */
52
54
  const WITH_BODY = ['[data-iv-fold="docs"]', '[data-iv-docs]', '[data-iv-fold="linked"]'];
53
55
 
@@ -15,9 +15,13 @@ export function kindOf(p) {
15
15
  if (p && (p.k === 'new' || p.k === 'imp' || p.k === 'fix' || p.k === 'x')) return p.k;
16
16
  return FIX.test((p && (p.t || p.s)) || '') ? 'fix' : 'imp';
17
17
  }
18
+ /** A title and a summary are the same line when they differ only in code ticks, case, spacing or the closing stop
19
+ * ("`pmos x` names y" and "Pmos x names y."): the line is then the title once, without its ticks, so a command keeps its own case. */
20
+ const same = (a, b) => { const f = (x) => x.replace(/`/g, '').replace(/[.!?]+$/, '').replace(/\s+/g, ' ').trim().toLowerCase(); return f(a) === f(b); };
18
21
  export function lineOf(p) {
19
- const s = String((p && p.s) || ''), t = String((p && p.t) || '');
20
- return t && t !== s ? { b: t, s } : { b: '', s };
22
+ const s = String((p && p.s) || ''), t = String((p && p.t) || '').replace(/`([^`]*)`/g, '$1');
23
+ if (!t) return { b: '', s };
24
+ return same(t, s) ? { b: '', s: t } : { b: t, s };
21
25
  }
22
26
  const mondayOf = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate() - ((d.getDay() + 6) % 7));
23
27
  const keyOf = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');