create-pmos 0.50.0 → 0.52.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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/design-parity.js +8 -0
- package/workspace/guide/layout.md +9 -1
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +2 -1
- package/workspace/public/pm/panel-cockpit.js +14 -0
- package/workspace/public/pm/panel-detail.js +13 -2
- package/workspace/public/pm/panel-tabs.js +4 -2
- package/workspace/public/pm/tokens.css +21 -3
- package/workspace/run-tests.js +61 -1
- package/workspace/test-files.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-05T21:40:45Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-05T21:40:45Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-05T21:40:45Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -201,6 +201,14 @@ const EXCLUSIONS = {
|
|
|
201
201
|
{ what: 'the height of the artifacts section', only: ['box'], design: { own: 'Artifacts', up: 1 }, app: { sel: '[data-cockpit="artifacts"]' } },
|
|
202
202
|
],
|
|
203
203
|
},
|
|
204
|
+
{
|
|
205
|
+
name: 'column fold controls',
|
|
206
|
+
why: 'The PM could not fold the list and the detail away (2026-10-05, "i cannot minimize column 2 and 3"). Each column has a '
|
|
207
|
+
+ '"⟨" control that folds it to the 36px strip, which opens it again. The board draws no fold control. Both float over the '
|
|
208
|
+
+ 'column (the list\'s over the right end of "New initiative", the detail\'s at its top right), so nothing in the column moves.',
|
|
209
|
+
design: null,
|
|
210
|
+
app: { sel: '[data-fold]' },
|
|
211
|
+
},
|
|
204
212
|
{
|
|
205
213
|
name: 'tab: second line',
|
|
206
214
|
why: 'A tab names its initiative and session under its title ("<initiative> · session <id>"): after a reload the PM could not '
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: layout"
|
|
4
4
|
description: "The three workspace columns: navigation, detail and tabs, and what each one holds."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-05
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: layout
|
|
@@ -92,12 +92,20 @@ What a row in a list opens:
|
|
|
92
92
|
|
|
93
93
|
## Folding
|
|
94
94
|
|
|
95
|
+
On a desktop screen, the list and the detail each have a `⟨` button. The list's button is at the right of `+ New initiative`. The detail's button is at its top right. Press it to fold that column to a thin strip. Press the strip to open the column again. The rail does not fold.
|
|
96
|
+
|
|
97
|
+
In the older layout:
|
|
98
|
+
|
|
95
99
|
- Choose the arrow in the header of the list to fold it to one line under the repositories. Choose the line to open it again.
|
|
96
100
|
- Fold the navigation column to a thin strip. The list folds with it. Choose the strip to open it again.
|
|
97
101
|
- Fold the detail column to its strip in the same way.
|
|
98
102
|
|
|
99
103
|
The workspace remembers which columns are open and how wide the navigation column is.
|
|
100
104
|
|
|
105
|
+
## Light theme
|
|
106
|
+
|
|
107
|
+
In the light theme, the tabs column stays dark, because a terminal is dark in both themes. The tab strip and the bar under the tabs use light text on that dark band.
|
|
108
|
+
|
|
101
109
|
## On a phone
|
|
102
110
|
|
|
103
111
|
A phone shows one panel at a time. The bottom bar moves between them.
|
package/workspace/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"test:panels": "node run-tests.js panels",
|
|
20
20
|
"test:offline": "node offline-tarball.test.js",
|
|
21
21
|
"test:browser": "node run-tests.js browser",
|
|
22
|
+
"test:changed": "node run-tests.js all --changed",
|
|
22
23
|
"test:list-check": "node run-tests.js --check"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
@@ -446,15 +446,29 @@ export function renderWorkList(host) {
|
|
|
446
446
|
// repository, so it has no files to list and does not offer the toggle (Files would only say it read nothing).
|
|
447
447
|
const filesOn = !FIXTURE && !!state.filesOpen;
|
|
448
448
|
if (filesOn) renderFiles(filesHost);
|
|
449
|
+
host.style.position = 'relative'; // anchors the fold control (foldList)
|
|
449
450
|
setChildren(host, [
|
|
450
451
|
el('div', { style: 'flex:none;display:flex;flex-direction:column;gap:12px' }, stateHealthBanner(), header, manageRow, switcher, FIXTURE ? null : filesToggle(), filesOn ? null : filterBox),
|
|
451
452
|
// the rows take their own height and scroll only when they outgrow the column; "New initiative" sits at the bottom either way
|
|
452
453
|
filesOn ? filesHost : el('div', { 'data-list': 'rows', style: 'flex:0 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:2px' }, groupsNodes),
|
|
453
454
|
el('div', { style: 'flex:none;margin-top:auto;display:flex;flex-direction:column;gap:8px' }, el('div', { style: 'height:1px;background:var(--border)' }), addInit),
|
|
455
|
+
foldList(),
|
|
454
456
|
]);
|
|
455
457
|
return host;
|
|
456
458
|
}
|
|
457
459
|
|
|
460
|
+
/** The list's fold control. The column folds to the 36px strip app.js draws ("initiatives ⟩"), which opens it again. It floats over
|
|
461
|
+
* the right end of the "New initiative" row (the host's 12px padding, then that 40px row), so nothing in the column moves. The
|
|
462
|
+
* board has no fold control; design-parity names it as an exclusion. */
|
|
463
|
+
function foldList() {
|
|
464
|
+
return el('button', {
|
|
465
|
+
'data-fold': 'list', 'aria-label': 'hide the list', title: 'hide the list',
|
|
466
|
+
onClick: () => setState({ col1: false }),
|
|
467
|
+
style: 'position:absolute;right:12px;bottom:17px;height:30px;padding:0 8px;border-radius:6px;font-size:14px;color:var(--muted-foreground)',
|
|
468
|
+
hover: 'color:var(--foreground);background:var(--accent)',
|
|
469
|
+
}, '⟨');
|
|
470
|
+
}
|
|
471
|
+
|
|
458
472
|
/** The tab to show when the PM goes back to Work from a view: the last one that is not a view, else none. */
|
|
459
473
|
function lastNonView() {
|
|
460
474
|
const t = [...(state.tabs || [])].reverse().find((x) => x.kind !== 'view');
|
|
@@ -1170,7 +1170,8 @@ const CK_SEP = () => el('div', { style: 'height:1px;background:var(--border)' })
|
|
|
1170
1170
|
/** Title, summary and chips: the lane, the anchor and the flow, as pills. */
|
|
1171
1171
|
function cockpitTitle(name, tag, pills) {
|
|
1172
1172
|
return [
|
|
1173
|
-
|
|
1173
|
+
// the right padding keeps a long name clear of the column's fold control (renderDetail)
|
|
1174
|
+
el('div', { style: 'font:600 17px var(--sans);overflow-wrap:anywhere;padding-right:30px' }, iv(name)),
|
|
1174
1175
|
el('div', { style: 'color:var(--muted-foreground)' }, iv(tag)),
|
|
1175
1176
|
el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, pills.map((pl) => el('span', { style: CK_BADGE }, iv(pl.t)))),
|
|
1176
1177
|
];
|
|
@@ -1643,7 +1644,17 @@ export function renderDetail(host) {
|
|
|
1643
1644
|
el('button', { 'data-focus': 'navigate', onClick: () => setState({ layoutFocus: false }), style: 'margin-left:auto;font:500 11px var(--sans);border:1px solid var(--border);border-radius:6px;padding:2px 8px', hover: 'background:var(--accent)' }, 'Navigate'));
|
|
1644
1645
|
return setChildren(host, [bar, ...body]);
|
|
1645
1646
|
}
|
|
1646
|
-
|
|
1647
|
+
/* The cockpit's initiative has no title bar, so its fold control floats at the column's top right. The column folds to the 36px
|
|
1648
|
+
* strip app.js draws ("initiative ⟩"), which opens it again. Nothing in the column moves (design-parity names the control). */
|
|
1649
|
+
host.style.position = bare ? 'relative' : '';
|
|
1650
|
+
return setChildren(host, bare ? [...body, foldDetail] : [stickyHeader, ...body]);
|
|
1647
1651
|
}
|
|
1648
1652
|
|
|
1653
|
+
const foldDetail = el('button', {
|
|
1654
|
+
'data-fold': 'detail', 'aria-label': 'hide the detail', title: 'hide the detail',
|
|
1655
|
+
onClick: () => setState({ col2: false }),
|
|
1656
|
+
style: 'position:absolute;top:10px;right:10px;height:28px;padding:0 8px;border-radius:6px;font-size:14px;color:var(--muted-foreground)',
|
|
1657
|
+
hover: 'color:var(--foreground);background:var(--accent)',
|
|
1658
|
+
}, '⟨');
|
|
1659
|
+
|
|
1649
1660
|
export default renderDetail;
|
|
@@ -335,7 +335,9 @@ const CHIP_STYLE = 'flex:none;align-self:center;font-family:var(--mono);font-siz
|
|
|
335
335
|
const SPLIT_STYLE = 'flex:none;margin-left:auto;align-self:center;font-family:var(--mono);font-size:11px;color:oklch(0.85 0.01 214);padding:5px 12px;min-height:30px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%);white-space:nowrap';
|
|
336
336
|
const CHIP_HOVER = 'border-color:oklch(0.56 0.021 213.5)';
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
/* `data-term-chrome`: the strip and the bottom bar sit on --term-bg, which is dark in BOTH themes (D41), so they take the dark
|
|
339
|
+
* theme's tokens in light mode too (tokens.css). Without it the light theme's dark text was drawn on the dark band, unreadable. */
|
|
340
|
+
const tabBar = el('div', { 'data-tabs': 'bar', 'data-term-chrome': '', style: BAR_STYLE, onWheel: stripWheel });
|
|
339
341
|
|
|
340
342
|
/* ── the cockpit's tab strip (workspace-cockpit-nav Run 4, planning/design/cockpit-mockup/WsA.dc.html) ─────────────────────────
|
|
341
343
|
* On a desktop the strip is the mockup's: a 44px band whose tabs sit on its floor, the tab in front a card with rounded top corners,
|
|
@@ -676,7 +678,7 @@ const colTitleLabel = iv('scratchpad');
|
|
|
676
678
|
const copyLabel = iv('copy');
|
|
677
679
|
|
|
678
680
|
const bottomBar = el('div', {
|
|
679
|
-
'data-tabs': 'bottom',
|
|
681
|
+
'data-tabs': 'bottom', 'data-term-chrome': '',
|
|
680
682
|
style: 'display:flex;gap:10px;align-items:center;padding:5px 12px;border-top:1px solid oklch(1 0 0 / 10%);font-size:11px;color:oklch(0.6 0.02 213)',
|
|
681
683
|
},
|
|
682
684
|
el('button', {
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* VERBATIM from the design prototype. Do not "improve" a value here: the acceptance bar for this
|
|
5
5
|
* surface is a mechanical diff against that prototype, so an edit to a token is a defect.
|
|
6
6
|
*
|
|
7
|
+
* One deliberate exception, asked for by the PM (2026-10-05, "some font not visible on light mode"): five LIGHT-theme text tokens
|
|
8
|
+
* are darker than the prototype's — --muted-foreground (0.56 → 0.48), --success, --warning, --info and --destructive — so that
|
|
9
|
+
* secondary text, status badges and warnings reach about 4.5:1 on the light surfaces. The dark theme, which the parity board
|
|
10
|
+
* compares, is untouched.
|
|
11
|
+
*
|
|
7
12
|
* The faces are VENDORED (D62: the workspace is a local companion app that must render correctly
|
|
8
13
|
* with no network at all). The prototype linked Google Fonts; a CDN font is network egress and is
|
|
9
14
|
* forbidden here, so the same three IBM Plex Mono weights and the Space Grotesk variable face are
|
|
@@ -57,14 +62,27 @@
|
|
|
57
62
|
[data-pm-theme="light"]{
|
|
58
63
|
--background:oklch(1 0 0);--foreground:oklch(0.148 0.004 228.8);
|
|
59
64
|
--card:oklch(1 0 0);--secondary:oklch(0.967 0.001 286.375);
|
|
60
|
-
--muted:oklch(0.963 0.002 197.1);--muted-foreground:oklch(0.
|
|
65
|
+
--muted:oklch(0.963 0.002 197.1);--muted-foreground:oklch(0.48 0.021 213.5);
|
|
61
66
|
--accent:oklch(0.963 0.002 197.1);--border:oklch(0.925 0.005 214.3);--input:oklch(0.925 0.005 214.3);
|
|
62
67
|
--ring:oklch(0.723 0.014 214.4);--sidebar:oklch(0.987 0.002 197.1);
|
|
63
68
|
--brand:oklch(0.488 0.243 264.376);--brand-wash:color-mix(in oklab, oklch(0.488 0.243 264.376) 7%, transparent);
|
|
64
|
-
--success:oklch(0.
|
|
65
|
-
--purple:oklch(0.5 0.2 300);--destructive:oklch(0.
|
|
69
|
+
--success:oklch(0.46 0.13 152);--warning:oklch(0.48 0.11 72);--info:oklch(0.47 0.09 195);
|
|
70
|
+
--purple:oklch(0.5 0.2 300);--destructive:oklch(0.5 0.21 27.325);
|
|
66
71
|
--term-bg:oklch(0.148 0.004 228.8);
|
|
67
72
|
}
|
|
73
|
+
/* The tab strip and the bottom bar of the tabs panel sit on --term-bg, dark in BOTH themes (D41). In the light theme they take the
|
|
74
|
+
dark theme's tokens, so their text and controls stay readable. Copied from the :root block above; keep the two in step. */
|
|
75
|
+
[data-pm-theme="light"] [data-term-chrome]{
|
|
76
|
+
--background:oklch(0.148 0.004 228.8);--foreground:oklch(0.987 0.002 197.1);
|
|
77
|
+
--card:oklch(0.218 0.008 223.9);--secondary:oklch(0.274 0.006 286.033);
|
|
78
|
+
--muted:oklch(0.275 0.011 216.9);--muted-foreground:oklch(0.723 0.014 214.4);
|
|
79
|
+
--accent:oklch(0.275 0.011 216.9);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 15%);
|
|
80
|
+
--ring:oklch(0.56 0.021 213.5);--sidebar:oklch(0.218 0.008 223.9);
|
|
81
|
+
--brand:oklch(0.623 0.214 259.815);--brand-wash:color-mix(in oklab, oklch(0.623 0.214 259.815) 12%, transparent);
|
|
82
|
+
--success:oklch(0.76 0.17 152);--warning:oklch(0.82 0.15 82);--info:oklch(0.79 0.11 195);
|
|
83
|
+
--purple:oklch(0.72 0.15 300);--destructive:oklch(0.704 0.191 22.216);
|
|
84
|
+
color:var(--foreground);
|
|
85
|
+
}
|
|
68
86
|
@keyframes pmblink{0%,49%{opacity:1}50%,100%{opacity:0}}
|
|
69
87
|
*{box-sizing:border-box}
|
|
70
88
|
body{margin:0;background:oklch(0.148 0.004 228.8);-webkit-font-smoothing:antialiased}
|
package/workspace/run-tests.js
CHANGED
|
@@ -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,6 +17,11 @@
|
|
|
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.
|
|
22
27
|
*/
|
|
@@ -65,6 +70,53 @@ function shardOf(list, spec) {
|
|
|
65
70
|
return out[i - 1];
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
// Files every page or every suite loads. A change to one can break any suite, so --changed runs
|
|
74
|
+
// the whole group. A changed file no suite can be traced to is treated the same way (see below).
|
|
75
|
+
const WIDE = new Set([
|
|
76
|
+
'server.js', 'test-run.js', 'run-tests.js', 'test-files.json', 'package.json', 'package-lock.json',
|
|
77
|
+
'public/pm/app.js', 'public/pm/state.js', 'public/pm/dom.js', 'public/pm/data.js',
|
|
78
|
+
'public/pm/tokens.css', 'public/pm/fixture.js', 'public/pm/mobile.css', 'lib/find-browser.js',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
/* The suites in `list` that the diff against `ref` can reach. A suite is selected when its own file
|
|
82
|
+
* changed, when its source names a changed file (`panel-tabs` in pm-tabs-test.js), or when its file
|
|
83
|
+
* name carries the changed file's stem with any panel-/view-/tab- prefix removed (view-memory.js ->
|
|
84
|
+
* pm-r29-memory-test.js). It errs toward running MORE: a WIDE file, or a changed workspace file that
|
|
85
|
+
* no suite can be traced to, selects the whole list. Returns { files, suites, why }. */
|
|
86
|
+
function changedSuites(list, ref) {
|
|
87
|
+
const git = (...a) => {
|
|
88
|
+
const r = require('child_process').spawnSync('git', a, { cwd: DIR, encoding: 'utf8' });
|
|
89
|
+
return r.status === 0 ? r.stdout : null;
|
|
90
|
+
};
|
|
91
|
+
const top = (git('rev-parse', '--show-toplevel') || '').trim();
|
|
92
|
+
if (!top) return { files: [], suites: list, why: 'not a git checkout, so every suite runs' };
|
|
93
|
+
const base = (git('merge-base', 'HEAD', ref) || '').trim();
|
|
94
|
+
if (!base) return { files: [], suites: list, why: `no merge base with ${ref} (fetch it first), so every suite runs` };
|
|
95
|
+
const prefix = path.relative(top, DIR).split(path.sep).join('/') + '/';
|
|
96
|
+
const names = [
|
|
97
|
+
git('diff', '--name-only', base), // committed on this branch and uncommitted changes
|
|
98
|
+
git('ls-files', '--others', '--exclude-standard', '--full-name', DIR),
|
|
99
|
+
].join('\n').split('\n').map((f) => f.trim()).filter(Boolean);
|
|
100
|
+
const files = [...new Set(names)].filter((f) => f.startsWith(prefix)).map((f) => f.slice(prefix.length));
|
|
101
|
+
if (!files.length) return { files, suites: [], why: `nothing under workspace/ differs from ${ref}` };
|
|
102
|
+
const wide = files.filter((f) => WIDE.has(f));
|
|
103
|
+
if (wide.length) return { files, suites: list, why: `${wide.join(', ')} changed, and every suite depends on it` };
|
|
104
|
+
const source = new Map(list.map((f) => [f, fs.existsSync(path.join(DIR, f)) ? fs.readFileSync(path.join(DIR, f), 'utf8') : '']));
|
|
105
|
+
const picked = new Set();
|
|
106
|
+
const untraced = [];
|
|
107
|
+
for (const f of files) {
|
|
108
|
+
if (source.has(f)) { picked.add(f); continue; }
|
|
109
|
+
if (/^(guide|docs)\/|\.md$/.test(f)) continue; // prose: no suite asserts on it except guide.test.js, a unit suite
|
|
110
|
+
const stem = path.basename(f).replace(/\.[^.]+$/, '');
|
|
111
|
+
const key = stem.replace(/^(panel|view|tab)-/, '');
|
|
112
|
+
const hits = list.filter((s) => source.get(s).includes(stem) || s.includes(`-${key}-`) || s.includes(`-${key}.`));
|
|
113
|
+
if (!hits.length) untraced.push(f);
|
|
114
|
+
for (const h of hits) picked.add(h);
|
|
115
|
+
}
|
|
116
|
+
if (untraced.length) return { files, suites: list, why: `no suite names ${untraced.slice(0, 3).join(', ')}${untraced.length > 3 ? ' …' : ''}, so every suite runs` };
|
|
117
|
+
return { files, suites: list.filter((f) => picked.has(f)), why: `${files.length} changed file(s) reach these suites` };
|
|
118
|
+
}
|
|
119
|
+
|
|
68
120
|
function check() {
|
|
69
121
|
const listed = new Set(GROUPS.all);
|
|
70
122
|
const problems = [];
|
|
@@ -114,6 +166,14 @@ async function main() {
|
|
|
114
166
|
let list = GROUPS[group];
|
|
115
167
|
const shard = flag('shard');
|
|
116
168
|
list = shard ? shardOf(list, shard) : bySlowest(list);
|
|
169
|
+
const changed = flag('changed');
|
|
170
|
+
if (changed) {
|
|
171
|
+
const ref = changed === true ? 'origin/main' : changed;
|
|
172
|
+
const pick = changedSuites(list, ref);
|
|
173
|
+
console.log(`run-tests: --changed against ${ref}: ${pick.why}. ${pick.suites.length} of ${list.length} suites run here; CI runs all ${list.length}.`);
|
|
174
|
+
list = pick.suites;
|
|
175
|
+
if (!list.length) return;
|
|
176
|
+
}
|
|
117
177
|
if (flag('list')) { console.log(list.join('\n')); return; }
|
|
118
178
|
const cpus = typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length;
|
|
119
179
|
// A browser suite runs a server, a tmux daemon and a Chromium of its own, and several of them assert
|