create-pmos 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/bin/pmos.js +15 -1
  16. package/workspace/cut-stub.js +40 -0
  17. package/workspace/first-paint-audit.js +145 -0
  18. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  19. package/workspace/fixtures/initiatives-api/list.json +176 -0
  20. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  21. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  22. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  23. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  24. package/workspace/fixtures/report-api/draft.json +177 -0
  25. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  26. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  27. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  28. package/workspace/guide/board.md +81 -0
  29. package/workspace/guide/breakdown.md +154 -0
  30. package/workspace/guide/command-palette.md +75 -0
  31. package/workspace/guide/index.md +27 -17
  32. package/workspace/guide/initiatives-roadmap.md +130 -0
  33. package/workspace/guide/initiatives-screen.md +89 -0
  34. package/workspace/guide/initiatives-timeline.md +57 -0
  35. package/workspace/guide/integrated-work.md +94 -0
  36. package/workspace/guide/knowledge-graph.md +3 -68
  37. package/workspace/guide/layout.md +98 -32
  38. package/workspace/guide/live-notes.md +3 -3
  39. package/workspace/guide/pm-only-acts.md +89 -0
  40. package/workspace/guide/pm-only-lines.md +61 -0
  41. package/workspace/guide/reports-page.md +80 -0
  42. package/workspace/guide/reports.md +73 -0
  43. package/workspace/guide/timeline.md +70 -0
  44. package/workspace/guide/today.md +85 -0
  45. package/workspace/guide/validate-with-typesafe.md +1 -1
  46. package/workspace/lib/actor.js +91 -0
  47. package/workspace/lib/board-cache.js +32 -2
  48. package/workspace/lib/board-cli.js +108 -0
  49. package/workspace/lib/board-fold.js +232 -0
  50. package/workspace/lib/breakdown-export.js +80 -0
  51. package/workspace/lib/capture.js +98 -0
  52. package/workspace/lib/engine/plan-items-stage.js +271 -0
  53. package/workspace/lib/engine/workflow-state.js +28 -2
  54. package/workspace/lib/factory-join.js +158 -0
  55. package/workspace/lib/grant-mint.js +113 -0
  56. package/workspace/lib/health-rules-factory.js +92 -0
  57. package/workspace/lib/health-rules-plan.js +164 -0
  58. package/workspace/lib/health.js +79 -0
  59. package/workspace/lib/initiative-cli.js +222 -0
  60. package/workspace/lib/initiative-display.js +183 -0
  61. package/workspace/lib/initiative-needs.js +127 -0
  62. package/workspace/lib/initiative-rows.js +206 -0
  63. package/workspace/lib/initiative-writer.js +141 -0
  64. package/workspace/lib/issues.js +206 -0
  65. package/workspace/lib/item-cli.js +195 -0
  66. package/workspace/lib/item-spans.js +137 -0
  67. package/workspace/lib/measure-cli.js +100 -0
  68. package/workspace/lib/measurements.js +189 -0
  69. package/workspace/lib/mobile-nav.js +57 -53
  70. package/workspace/lib/needs-providers.js +120 -0
  71. package/workspace/lib/placements.js +440 -0
  72. package/workspace/lib/plan-items.js +922 -0
  73. package/workspace/lib/plan-ledgers.js +85 -0
  74. package/workspace/lib/plan-rules.js +72 -0
  75. package/workspace/lib/plan-tree.js +367 -0
  76. package/workspace/lib/pm-guard.js +174 -0
  77. package/workspace/lib/pm-lines-needs.js +331 -0
  78. package/workspace/lib/pmos-verbs.js +66 -0
  79. package/workspace/lib/proactive.js +7 -4
  80. package/workspace/lib/report-cli.js +152 -0
  81. package/workspace/lib/report-draft.js +579 -0
  82. package/workspace/lib/report-needs.js +213 -0
  83. package/workspace/lib/route-modules.js +128 -0
  84. package/workspace/lib/routes-board.js +91 -0
  85. package/workspace/lib/routes-capture.js +52 -0
  86. package/workspace/lib/routes-initiatives.js +371 -0
  87. package/workspace/lib/routes-reports.js +163 -0
  88. package/workspace/lib/routes-timeline.js +197 -0
  89. package/workspace/lib/routes-today.js +32 -0
  90. package/workspace/lib/today-cli.js +99 -0
  91. package/workspace/lib/today.js +333 -0
  92. package/workspace/lib/triage.js +163 -0
  93. package/workspace/lib/verify-work-cli.js +81 -0
  94. package/workspace/lib/work-check-needs.js +99 -0
  95. package/workspace/package-lock.json +1 -1
  96. package/workspace/package.json +5 -5
  97. package/workspace/public/index.html +29 -19
  98. package/workspace/public/pm/app.js +299 -30
  99. package/workspace/public/pm/bd-board.js +113 -0
  100. package/workspace/public/pm/bd-drag.js +59 -0
  101. package/workspace/public/pm/bd-model.js +152 -0
  102. package/workspace/public/pm/bd-timeline.js +233 -0
  103. package/workspace/public/pm/bd-tree.js +244 -0
  104. package/workspace/public/pm/board-copy.js +57 -0
  105. package/workspace/public/pm/breakdown-api.js +58 -0
  106. package/workspace/public/pm/breakdown-copy.js +138 -0
  107. package/workspace/public/pm/components.css +3054 -0
  108. package/workspace/public/pm/cycle-review.js +125 -0
  109. package/workspace/public/pm/data.js +40 -187
  110. package/workspace/public/pm/dest-board.js +438 -0
  111. package/workspace/public/pm/dest-initiatives.js +134 -0
  112. package/workspace/public/pm/dest-reports.js +270 -0
  113. package/workspace/public/pm/dest-settings.js +217 -0
  114. package/workspace/public/pm/dest-today.js +176 -0
  115. package/workspace/public/pm/dialogs.js +88 -0
  116. package/workspace/public/pm/display-word.js +56 -0
  117. package/workspace/public/pm/dock.js +105 -0
  118. package/workspace/public/pm/fixture.js +8 -227
  119. package/workspace/public/pm/frame-copy.js +94 -0
  120. package/workspace/public/pm/handoff-box.js +82 -0
  121. package/workspace/public/pm/http.js +88 -0
  122. package/workspace/public/pm/icons.js +53 -0
  123. package/workspace/public/pm/initiative-list.js +218 -0
  124. package/workspace/public/pm/initiative-sheet.js +261 -0
  125. package/workspace/public/pm/initiatives-api.js +59 -0
  126. package/workspace/public/pm/initiatives-copy.js +215 -0
  127. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  128. package/workspace/public/pm/initiatives-timeline.js +410 -0
  129. package/workspace/public/pm/initiatives-ui.js +123 -0
  130. package/workspace/public/pm/item-sheet.js +251 -0
  131. package/workspace/public/pm/keep-card.js +204 -0
  132. package/workspace/public/pm/lifecycle.js +7 -1
  133. package/workspace/public/pm/mobile.css +49 -20
  134. package/workspace/public/pm/mobile.js +157 -29
  135. package/workspace/public/pm/modes-live.js +7 -883
  136. package/workspace/public/pm/modes.js +32 -1024
  137. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  138. package/workspace/public/pm/next-step-ui.js +3 -2
  139. package/workspace/public/pm/palette-copy.js +51 -0
  140. package/workspace/public/pm/palette.js +312 -0
  141. package/workspace/public/pm/panel-breakdown.js +702 -0
  142. package/workspace/public/pm/panel-cockpit.js +63 -295
  143. package/workspace/public/pm/panel-detail.js +25 -172
  144. package/workspace/public/pm/panel-initiatives.js +1 -19
  145. package/workspace/public/pm/panel-repos.js +31 -81
  146. package/workspace/public/pm/panel-tabs.js +197 -110
  147. package/workspace/public/pm/panel-trees.js +5 -2
  148. package/workspace/public/pm/portfolio-report.js +61 -0
  149. package/workspace/public/pm/primitives.js +656 -0
  150. package/workspace/public/pm/print.css +33 -0
  151. package/workspace/public/pm/provider-cards.js +136 -0
  152. package/workspace/public/pm/registry.js +194 -0
  153. package/workspace/public/pm/release-card.js +110 -0
  154. package/workspace/public/pm/report-view.js +309 -0
  155. package/workspace/public/pm/reports-copy.js +114 -0
  156. package/workspace/public/pm/roadmap.js +271 -0
  157. package/workspace/public/pm/sig-watch.js +41 -0
  158. package/workspace/public/pm/state.js +8 -4
  159. package/workspace/public/pm/status-banner.js +13 -5
  160. package/workspace/public/pm/tab-doc.js +2 -4
  161. package/workspace/public/pm/tab-term.js +12 -2
  162. package/workspace/public/pm/tab-web.js +3 -2
  163. package/workspace/public/pm/timeline-copy.js +48 -0
  164. package/workspace/public/pm/today-cards.js +244 -0
  165. package/workspace/public/pm/today-copy.js +97 -0
  166. package/workspace/public/pm/tokens.css +16 -18
  167. package/workspace/public/pm/topbar.js +102 -0
  168. package/workspace/public/pm/triage-card.js +60 -0
  169. package/workspace/public/pm/undo-hold.js +103 -0
  170. package/workspace/public/pm/view-addrepo.js +2 -1
  171. package/workspace/public/pm/view-livenotes.js +3 -1
  172. package/workspace/public/pm/view-today.js +7 -162
  173. package/workspace/public/vendor/Sortable.min.js +2 -0
  174. package/workspace/public/vendor/lucide-sprite.js +79 -0
  175. package/workspace/replaced-tests.json +2864 -0
  176. package/workspace/run-tests.js +117 -6
  177. package/workspace/server.js +312 -36
  178. package/workspace/sheet-answer.js +61 -0
  179. package/workspace/skills/measure.skill +41 -0
  180. package/workspace/skills/report-biweekly.skill +23 -25
  181. package/workspace/skills/tracker-refresh.skill +50 -0
  182. package/workspace/test-files.json +20 -4
  183. package/workspace/test-lists/base.json +6 -0
  184. package/workspace/test-lists/run2-a.json +6 -0
  185. package/workspace/test-lists/run2-b.json +13 -0
  186. package/workspace/test-lists/run2-c.json +19 -0
  187. package/workspace/test-lists/run2-d.json +22 -0
  188. package/workspace/test-lists/run3-0.json +5 -0
  189. package/workspace/test-lists/run3-a.json +5 -0
  190. package/workspace/test-lists/run3-b.json +6 -0
  191. package/workspace/test-lists/run3-c.json +15 -0
  192. package/workspace/test-lists/run3-d.json +6 -0
  193. package/workspace/test-lists/run4-a.json +8 -0
  194. package/workspace/test-lists/run4-b.json +6 -0
  195. package/workspace/test-lists/run4-c.json +13 -0
  196. package/workspace/test-lists/run4-d.json +9 -0
  197. package/workspace/test-lists/run5-a.json +6 -0
  198. package/workspace/test-lists/run5-b.json +6 -0
  199. package/workspace/test-lists/run5-c.json +15 -0
  200. package/workspace/test-run.js +27 -1
  201. package/workspace/third-party/README.md +44 -0
  202. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  203. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  204. package/workspace/design-parity.js +0 -673
  205. package/workspace/public/pm/tab-task.js +0 -86
  206. package/workspace/public/pm/view-guide.js +0 -141
  207. package/workspace/public/pm/view-knowledge.js +0 -225
  208. package/workspace/public/pm/view-memory.js +0 -122
  209. package/workspace/public/pm/view-portfolio.js +0 -153
  210. package/workspace/public/pm/view-reports.js +0 -190
  211. package/workspace/public/pm/view-scheduled.js +0 -161
  212. package/workspace/public/pm/view-skills.js +0 -216
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
  /* run-tests.js — run the workspace suites side by side.
4
4
  *
5
- * node run-tests.js <group> [--jobs=N] [--shard=I/N] [--list] [--verbose]
5
+ * node run-tests.js <group> [--jobs=N] [--shard=I/N] [--changed[=REF]] [--list] [--verbose]
6
6
  * node run-tests.js --check
7
7
  *
8
8
  * Groups come from test-files.json: unit (test.js + modules), modules, panels, ui, browser
@@ -17,8 +17,20 @@
17
17
  * file in exactly one shard), and --check fails when a suite on disk is in no list, so a new suite
18
18
  * that nobody wired in is a red build rather than a suite that runs nowhere.
19
19
  *
20
+ * --changed is for the LOCAL inner loop only. It narrows the group to the suites a diff against REF
21
+ * (default origin/main) can reach, and runs the whole group whenever it cannot tell. CI never passes
22
+ * it: Workspace CI runs every suite on every PR, so a suite --changed leaves out still runs before
23
+ * the merge. See changedSuites() for the rule.
24
+ *
20
25
  * Output: a suite's own output is buffered and printed whole when it finishes, so lines from two
21
26
  * suites never interleave. A passing suite prints its last lines; --verbose prints all of it.
27
+ *
28
+ * Fragments (pm-os-absorb Runs 2 to 5). Builders who work at the same time add their suites in a file
29
+ * of their own, test-lists/<name>.json, never in test-files.json: two slices that each append to the
30
+ * same JSON array always conflict. A fragment has the keys modules, panels, exclusive and seconds of
31
+ * test-files.json, plus `helpers` (a -test.js file that is a helper, not a suite, with the reason, as
32
+ * in ELSEWHERE). This file merges every fragment into the lists before it does anything else, so
33
+ * --check, --shard and every group see one list. A fragment that is not valid fails every command.
22
34
  */
23
35
  const fs = require('fs');
24
36
  const os = require('os');
@@ -26,7 +38,41 @@ const path = require('path');
26
38
  const { spawn } = require('child_process');
27
39
 
28
40
  const DIR = __dirname;
29
- const FILES = JSON.parse(fs.readFileSync(path.join(DIR, 'test-files.json'), 'utf8'));
41
+ const FRAG_DIR = path.join(DIR, 'test-lists');
42
+ const FRAG_KEYS = ['_comment', 'modules', 'panels', 'exclusive', 'seconds', 'helpers'];
43
+ const fragProblems = [];
44
+ const fragHelpers = {};
45
+ /** test-files.json with every test-lists/*.json fragment merged in (see the header). Problems go to fragProblems. */
46
+ function loadFiles() {
47
+ const files = JSON.parse(fs.readFileSync(path.join(DIR, 'test-files.json'), 'utf8'));
48
+ let names = [];
49
+ try { names = fs.readdirSync(FRAG_DIR).filter((n) => n.endsWith('.json')).sort(); } catch { /* no fragments yet */ }
50
+ for (const n of names) {
51
+ const where = `test-lists/${n}`;
52
+ if (!/^[a-z0-9][a-z0-9-]*\.json$/.test(n)) fragProblems.push(`${where}: a fragment is named <lowercase-id>.json, for example run2-a.json`);
53
+ let frag;
54
+ try { frag = JSON.parse(fs.readFileSync(path.join(FRAG_DIR, n), 'utf8')); } catch (e) { fragProblems.push(`${where} is not valid JSON — ${e.message}`); continue; }
55
+ if (!frag || typeof frag !== 'object' || Array.isArray(frag)) { fragProblems.push(`${where} is not a JSON object`); continue; }
56
+ for (const k of Object.keys(frag)) if (!FRAG_KEYS.includes(k)) fragProblems.push(`${where}: unknown key "${k}" (the keys are ${FRAG_KEYS.slice(1).join(', ')})`);
57
+ for (const k of ['modules', 'panels', 'exclusive']) {
58
+ if (frag[k] === undefined) continue;
59
+ if (!Array.isArray(frag[k]) || !frag[k].every((f) => typeof f === 'string')) { fragProblems.push(`${where}: "${k}" is a list of file names`); continue; }
60
+ files[k] = [...(files[k] || []), ...frag[k]];
61
+ }
62
+ for (const [f, v] of Object.entries(frag.seconds || {})) {
63
+ if (typeof v !== 'number' || !(v > 0)) fragProblems.push(`${where}: seconds of ${f} is a positive number`);
64
+ else if (files.seconds && f in files.seconds) fragProblems.push(`${where}: seconds of ${f} is already recorded elsewhere`);
65
+ else files.seconds = { ...(files.seconds || {}), [f]: v };
66
+ }
67
+ for (const [f, why] of Object.entries(frag.helpers || {})) {
68
+ if (typeof why !== 'string' || !why.trim()) fragProblems.push(`${where}: helper ${f} needs a reason`);
69
+ else if (fragHelpers[f]) fragProblems.push(`${where}: helper ${f} is already named by ${fragHelpers[f].where}`);
70
+ else fragHelpers[f] = { why, where };
71
+ }
72
+ }
73
+ return files;
74
+ }
75
+ const FILES = loadFiles();
30
76
  const GROUPS = {
31
77
  unit: [...FILES.unit, ...FILES.modules],
32
78
  modules: FILES.modules,
@@ -38,7 +84,14 @@ const GROUPS = {
38
84
  // Suites that run elsewhere on purpose, so --check does not count them as unwired.
39
85
  const ELSEWHERE = {
40
86
  'offline-tarball.test.js': 'npm run test:offline (needs a packed tarball)',
87
+ // A helper module, not a suite: the Breakdown suites require it (pm-os-absorb Run 1). Its name keeps it out of the
88
+ // tarball (BUNDLE_EXCLUDES has 'pm-*-test.js'), which is why it ends in -test.js.
89
+ 'pm-breakdown-stub-test.js': 'not a suite — the helpers pm-breakdown-test.js, pm-breakdown-phone-test.js and first-paint-count.test.js require',
41
90
  };
91
+ for (const [f, h] of Object.entries(fragHelpers)) {
92
+ if (ELSEWHERE[f]) fragProblems.push(`${h.where}: helper ${f} is already exempt in run-tests.js`);
93
+ else ELSEWHERE[f] = `${h.why} (${h.where})`;
94
+ }
42
95
  const PER_FILE_TIMEOUT_MS = 10 * 60 * 1000;
43
96
 
44
97
  const args = process.argv.slice(2);
@@ -65,12 +118,60 @@ function shardOf(list, spec) {
65
118
  return out[i - 1];
66
119
  }
67
120
 
121
+ // Files every page or every suite loads. A change to one can break any suite, so --changed runs
122
+ // the whole group. A changed file no suite can be traced to is treated the same way (see below).
123
+ const WIDE = new Set([
124
+ 'server.js', 'test-run.js', 'run-tests.js', 'test-files.json', 'package.json', 'package-lock.json',
125
+ 'public/pm/app.js', 'public/pm/state.js', 'public/pm/dom.js', 'public/pm/data.js',
126
+ 'public/pm/tokens.css', 'public/pm/fixture.js', 'public/pm/mobile.css', 'lib/find-browser.js',
127
+ ]);
128
+
129
+ /* The suites in `list` that the diff against `ref` can reach. A suite is selected when its own file
130
+ * changed, when its source names a changed file (`panel-tabs` in pm-tabs-test.js), or when its file
131
+ * name carries the changed file's stem with any panel-/view-/tab- prefix removed (view-memory.js ->
132
+ * pm-r29-memory-test.js). It errs toward running MORE: a WIDE file, or a changed workspace file that
133
+ * no suite can be traced to, selects the whole list. Returns { files, suites, why }. */
134
+ function changedSuites(list, ref) {
135
+ const git = (...a) => {
136
+ const r = require('child_process').spawnSync('git', a, { cwd: DIR, encoding: 'utf8' });
137
+ return r.status === 0 ? r.stdout : null;
138
+ };
139
+ const top = (git('rev-parse', '--show-toplevel') || '').trim();
140
+ if (!top) return { files: [], suites: list, why: 'not a git checkout, so every suite runs' };
141
+ const base = (git('merge-base', 'HEAD', ref) || '').trim();
142
+ if (!base) return { files: [], suites: list, why: `no merge base with ${ref} (fetch it first), so every suite runs` };
143
+ const prefix = path.relative(top, DIR).split(path.sep).join('/') + '/';
144
+ const names = [
145
+ git('diff', '--name-only', base), // committed on this branch and uncommitted changes
146
+ git('ls-files', '--others', '--exclude-standard', '--full-name', DIR),
147
+ ].join('\n').split('\n').map((f) => f.trim()).filter(Boolean);
148
+ const files = [...new Set(names)].filter((f) => f.startsWith(prefix)).map((f) => f.slice(prefix.length));
149
+ if (!files.length) return { files, suites: [], why: `nothing under workspace/ differs from ${ref}` };
150
+ const wide = files.filter((f) => WIDE.has(f));
151
+ if (wide.length) return { files, suites: list, why: `${wide.join(', ')} changed, and every suite depends on it` };
152
+ const source = new Map(list.map((f) => [f, fs.existsSync(path.join(DIR, f)) ? fs.readFileSync(path.join(DIR, f), 'utf8') : '']));
153
+ const picked = new Set();
154
+ const untraced = [];
155
+ for (const f of files) {
156
+ if (source.has(f)) { picked.add(f); continue; }
157
+ if (/^(guide|docs)\/|\.md$/.test(f)) continue; // prose: no suite asserts on it except guide.test.js, a unit suite
158
+ const stem = path.basename(f).replace(/\.[^.]+$/, '');
159
+ const key = stem.replace(/^(panel|view|tab)-/, '');
160
+ const hits = list.filter((s) => source.get(s).includes(stem) || s.includes(`-${key}-`) || s.includes(`-${key}.`));
161
+ if (!hits.length) untraced.push(f);
162
+ for (const h of hits) picked.add(h);
163
+ }
164
+ if (untraced.length) return { files, suites: list, why: `no suite names ${untraced.slice(0, 3).join(', ')}${untraced.length > 3 ? ' …' : ''}, so every suite runs` };
165
+ return { files, suites: list.filter((f) => picked.has(f)), why: `${files.length} changed file(s) reach these suites` };
166
+ }
167
+
68
168
  function check() {
69
169
  const listed = new Set(GROUPS.all);
70
- const problems = [];
170
+ const problems = [...fragProblems];
71
171
  const dupes = GROUPS.all.filter((f, k) => GROUPS.all.indexOf(f) !== k);
72
- for (const f of new Set(dupes)) problems.push(`${f} is listed more than once in test-files.json`);
73
- for (const f of listed) if (!fs.existsSync(path.join(DIR, f))) problems.push(`${f} is listed in test-files.json but does not exist`);
172
+ for (const f of new Set(dupes)) problems.push(`${f} is listed more than once in test-files.json and test-lists/`);
173
+ for (const f of listed) if (!fs.existsSync(path.join(DIR, f))) problems.push(`${f} is listed in test-files.json or test-lists/ but does not exist`);
174
+ for (const f of Object.keys(ELSEWHERE)) if (listed.has(f)) problems.push(`${f} is exempt as a helper but is also listed as a suite`);
74
175
  for (const f of FILES.exclusive || []) if (!listed.has(f)) problems.push(`${f} is marked exclusive but is in no test list`);
75
176
  const onDisk = fs.readdirSync(DIR).filter((f) => f === 'test.js' || /(\.test\.(js|mjs)|-test\.js)$/.test(f));
76
177
  for (const f of onDisk) if (!listed.has(f) && !ELSEWHERE[f]) problems.push(`${f} is a suite on disk that no test list runs. Add it to test-files.json.`);
@@ -79,7 +180,8 @@ function check() {
79
180
  if (parts.length !== GROUPS[g].length || new Set(parts).size !== GROUPS[g].length) problems.push(`--shard does not partition "${g}" into ${n}`);
80
181
  }
81
182
  if (problems.length) { for (const p of problems) console.error(`✗ ${p}`); process.exitCode = 1; return; }
82
- console.log(`✓ ${listed.size} suites listed, each once, each on disk; ${onDisk.length - Object.keys(ELSEWHERE).length} suites on disk, all listed; shards partition every group.`);
183
+ const frags = fs.existsSync(FRAG_DIR) ? fs.readdirSync(FRAG_DIR).filter((n) => n.endsWith('.json')).length : 0;
184
+ console.log(`✓ ${listed.size} suites listed, each once, each on disk; ${onDisk.filter((f) => !ELSEWHERE[f]).length} suites on disk, all listed; shards partition every group; ${frags} fragment(s) in test-lists/.`);
83
185
  }
84
186
 
85
187
  function runOne(file, verbose) {
@@ -107,6 +209,7 @@ function runOne(file, verbose) {
107
209
 
108
210
  async function main() {
109
211
  if (flag('check')) return check();
212
+ if (fragProblems.length) { for (const p of fragProblems) console.error(`✗ ${p}`); process.exitCode = 2; return; }
110
213
  if (!group || !GROUPS[group]) {
111
214
  console.error(`usage: node run-tests.js <${Object.keys(GROUPS).join('|')}> [--jobs=N] [--shard=I/N] [--list] [--verbose]\n node run-tests.js --check`);
112
215
  process.exitCode = 2; return;
@@ -114,6 +217,14 @@ async function main() {
114
217
  let list = GROUPS[group];
115
218
  const shard = flag('shard');
116
219
  list = shard ? shardOf(list, shard) : bySlowest(list);
220
+ const changed = flag('changed');
221
+ if (changed) {
222
+ const ref = changed === true ? 'origin/main' : changed;
223
+ const pick = changedSuites(list, ref);
224
+ console.log(`run-tests: --changed against ${ref}: ${pick.why}. ${pick.suites.length} of ${list.length} suites run here; CI runs all ${list.length}.`);
225
+ list = pick.suites;
226
+ if (!list.length) return;
227
+ }
117
228
  if (flag('list')) { console.log(list.join('\n')); return; }
118
229
  const cpus = typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length;
119
230
  // A browser suite runs a server, a tmux daemon and a Chromium of its own, and several of them assert