rvlib-shipkit 1.9.0 → 2.0.1
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/CHANGELOG.md +38 -0
- package/package.json +1 -1
- package/src/check.ts +5 -2
- package/src/checks/SK001-package-json-shape.check.ts +47 -12
- package/src/checks/SK013-one-way-in-policy.check.ts +8 -7
- package/src/checks/SK017-vscode-hub-task.check.ts +109 -106
- package/src/checks/SK019-tests-run-in-units.check.ts +2 -1
- package/src/checks/SK027-agent-wiki-links.check.ts +5 -2
- package/src/checks/SK029-explicit-check-files.check.ts +25 -8
- package/src/checks/SK034-commit-gates-armed.check.ts +90 -0
- package/src/cli/cmd_Ci.ts +3 -3
- package/src/cli/cmd_Commands.ts +21 -5
- package/src/cli/cmd_Hooks.ts +6 -2
- package/src/cli/cmd_Init.ts +1 -1
- package/src/cli/cmd_Serve.ts +1 -2
- package/src/cli/cmd_Start.ts +1 -1
- package/src/cli.ts +2 -4
- package/src/core/budget.ts +6 -8
- package/src/core/bun-builtins.ts +36 -0
- package/src/core/bun-tool-cli.ts +23 -0
- package/src/core/canonical-scripts.ts +6 -4
- package/src/core/check-ctx.ts +104 -10
- package/src/core/check-file-names.ts +2 -3
- package/src/core/check-pass.ts +17 -1
- package/src/core/check-runner.ts +19 -6
- package/src/core/config-rewrite.ts +27 -6
- package/src/core/discover-checks.ts +30 -10
- package/src/core/floor-resolve.ts +16 -7
- package/src/core/job-run.ts +2 -2
- package/src/core/layout.ts +4 -1
- package/src/core/legacy-shapes.ts +40 -0
- package/src/core/one-lib.ts +1 -1
- package/src/core/one-way-in-policy.ts +106 -13
- package/src/core/pre-commit-discovery.ts +106 -0
- package/src/core/retired-checks.ts +2 -0
- package/src/core/run-ci.ts +16 -10
- package/src/core/run-fix.ts +9 -0
- package/src/core/run-fixers.ts +91 -35
- package/src/core/runs-matrix.ts +7 -0
- package/src/core/slots.ts +1 -1
- package/src/core/vendored-read.ts +39 -0
- package/src/core/vendored.ts +82 -0
- package/src/core/waivers.ts +125 -0
- package/src/core/when-block.ts +25 -185
- package/src/core/when.ts +30 -13
- package/src/core/wire.ts +17 -1
- package/src/fleet/update-fleet.ts +11 -2
- package/src/git-bootstrap.ts +1 -1
- package/src/home/attach.ts +3 -5
- package/src/home/insights.ts +6 -1
- package/src/home/pm-config.ts +2 -2
- package/src/home/run-daemon.ts +2 -2
- package/src/home/run-unit-action.ts +2 -2
- package/src/hooks/commit-message.ts +2 -14
- package/src/hooks/hook-gates.ts +38 -0
- package/src/hooks/leak-guard.ts +8 -3
- package/src/hooks/one-way-in-guard.ts +8 -2
- package/src/hooks/root-identity.ts +9 -6
- package/src/hooks/run-hooks.ts +72 -41
- package/src/hooks/staged.ts +49 -0
- package/src/hooks/tree-shrink.ts +41 -0
- package/src/init-repo.ts +0 -42
- package/src/ledger/check-table.ts +1 -1
- package/src/ledger/gate-checks.ts +45 -9
- package/src/ledger/ledger.ts +6 -1
- package/src/ledger/repo-view.ts +14 -4
- package/src/ledger/toolchain-identity.ts +4 -2
- package/src/mv/mv-md.ts +6 -1
- package/src/problems/extract.ts +27 -5
- package/src/problems/problem-line.ts +2 -3
- package/src/release/run-release.ts +13 -8
- package/src/repo-config.ts +44 -27
- package/src/scaffold/files/bunfig.toml +11 -0
- package/src/scaffold/files/vscode-settings.json +1 -2
- package/src/scaffold/jsonc-spans.ts +16 -0
- package/src/scaffold/project-scaffold.ts +43 -77
- package/src/serve/hub-app/check-view.tsx +4 -4
- package/src/serve/hub-app/check-words.ts +1 -0
- package/src/serve/hub-app/checks-svc.ts +1 -2
- package/src/serve/hub-app/home.html +0 -1
- package/src/serve/hub-app/hub-types.ts +0 -2
- package/src/serve/hub-app/index.html +0 -1
- package/src/serve/hub-app/project-list.css +1 -2
- package/src/serve/hub-app/tree-build.ts +3 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-port.ts +3 -3
- package/src/serve/hub-runtime.ts +0 -3
- package/src/serve/serve-hub.ts +1 -16
- package/src/serve/tracked-watch.ts +1 -1
- package/src/start-repo.ts +15 -3
- package/src/suite/judge-suites.ts +29 -28
- package/src/suite/select.ts +2 -0
- package/src/suite/suite-cli.ts +6 -3
- package/src/suite/suite-cpu-preload.ts +7 -11
- package/src/suite/test-units.ts +7 -4
- package/src/ts/config.ts +12 -5
- package/src/ts/imports.ts +3 -1
- package/src/tui/build-steps-panel.ts +1 -1
- package/src/wiki/scan-md.ts +16 -9
- package/src/checks/SK009-commit-ai-tells.check.ts +0 -131
- package/src/checks/SK014-prose-ste.check.ts +0 -78
- package/src/cli/cmd_Prose.ts +0 -74
- package/src/prose/prose-view.ts +0 -90
- package/src/prose/run-prose.ts +0 -187
- package/src/prose/ste.ts +0 -552
- package/src/scaffold/files/vscode-tasks.json +0 -24
- package/src/serve/hub-app/prose-hits-st.ts +0 -125
- package/src/serve/hub-app/prose-hits-view.tsx +0 -176
- package/src/serve/hub-app/prose-hits.css +0 -21
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
// package.json rewrite reformats the file it edits.
|
|
11
11
|
// Templates live beside this file in files/ and ship in the npm tarball, so
|
|
12
12
|
// none of them may name a machine-local tool or a private path.
|
|
13
|
-
import { existsSync,
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
14
14
|
import { dirname, join } from 'path'
|
|
15
15
|
import { defaultHubPort } from '../serve/hub-port.ts'
|
|
16
|
-
import { hubProblemMatcher } from '../problems/problem-line.ts'
|
|
17
16
|
|
|
18
17
|
const FILES = join(import.meta.dir, 'files')
|
|
19
18
|
|
|
@@ -36,15 +35,14 @@ export type ScaffoldOpts = {
|
|
|
36
35
|
claudePolicy?: boolean
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
/** every dep the house toolchain needs for the scripts below to run
|
|
40
|
-
|
|
38
|
+
/** every dep the house toolchain needs for the scripts below to run. The
|
|
39
|
+
* linter and the formatter are bun's own, declared in bunfig.toml */
|
|
40
|
+
export const TOOLCHAIN = ['typescript', '@types/bun'] as const
|
|
41
41
|
|
|
42
42
|
/** script bodies for a fresh repo whose only source dir is src/. A stack
|
|
43
|
-
* scaffold (vite, hono) adds dev/build on top
|
|
44
|
-
*
|
|
43
|
+
* scaffold (vite, hono) adds dev/build on top. No `lint` or `fmt`: a script
|
|
44
|
+
* of either name would shadow `bun lint` and `bun fmt` */
|
|
45
45
|
const SCRIPTS: Record<string, string> = {
|
|
46
|
-
lint: 'oxlint src/',
|
|
47
|
-
fmt: 'oxfmt src/',
|
|
48
46
|
typecheck: 'tsc --noEmit',
|
|
49
47
|
}
|
|
50
48
|
|
|
@@ -104,64 +102,12 @@ function put(root: string, rel: string, body: string): boolean {
|
|
|
104
102
|
return true
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
export type ErgonomicsOpts = {
|
|
108
|
-
/** repo name, substituted into the templates */
|
|
109
|
-
name: string
|
|
110
|
-
/** the port the hub actually listens on, resolved by the caller from the
|
|
111
|
-
* repo config (`serve.port`, else the name-derived default), so the task
|
|
112
|
-
* advertises a url something answers on */
|
|
113
|
-
port: number
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** the folder-open story, and the ONE file of it a repo shipkit did not create
|
|
117
|
-
* can be handed unasked.
|
|
118
|
-
*
|
|
119
|
-
* What lands is `tasks.json` alone. `settings.json` turns eslint off and hands
|
|
120
|
-
* formatting to an oxc extension, so writing it into a repo that lints with
|
|
121
|
-
* eslint silences the linter that repo chose: that is an answer about what the
|
|
122
|
-
* repo IS, and a repo with a history of its own already gave it. A tasks file
|
|
123
|
-
* is the opposite: with no `folderOpen` task nothing starts at all, so there
|
|
124
|
-
* is no existing answer to overrule.
|
|
125
|
-
*
|
|
126
|
-
* What lands INSIDE it is only what is true of every wired repo: the hub,
|
|
127
|
-
* whose script `init` writes, carrying the problem matcher that turns every
|
|
128
|
-
* check's findings into the problems pane. There is no type checker task, the
|
|
129
|
-
* hub runs `typecheck` like any other check. Every watcher a stack earns (a
|
|
130
|
-
* dev server, css) stays the analysis step `build/vscode-tasks.md` describes,
|
|
131
|
-
* and the caller says so out loud.
|
|
132
|
-
*
|
|
133
|
-
* Create-only: an adopted repo often carries a `.vscode/` from whatever it was
|
|
134
|
-
* forked from, and that one wins byte for byte (rule SK017 then repairs its
|
|
135
|
-
* hub task in place). Returns the paths it wrote. */
|
|
136
|
-
export function editorErgonomics(root: string, opts: ErgonomicsOpts): string[] {
|
|
137
|
-
// a symlinked .vscode is a dotfiles pattern pointing at a SHARED dir, and
|
|
138
|
-
// writing through it lands files outside the repo being wired
|
|
139
|
-
if (isLink(join(root, '.vscode'))) return []
|
|
140
|
-
const body = tasksTemplate({ NAME: opts.name, PORT: String(opts.port) })
|
|
141
|
-
return put(root, '.vscode/tasks.json', body) ? ['.vscode/tasks.json'] : []
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const isLink = (path: string): boolean => {
|
|
145
|
-
try {
|
|
146
|
-
return lstatSync(path).isSymbolicLink()
|
|
147
|
-
} catch {
|
|
148
|
-
return false
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** the tasks template, the hub's problem matcher rendered in from the one
|
|
153
|
-
* module the hub prints through, so the two can never disagree */
|
|
154
|
-
export function tasksTemplate(vars: Record<string, string>): string {
|
|
155
|
-
return template('vscode-tasks.json', vars).replace(/^(\s*)(.*)\{\{HUB_PROBLEM_MATCHER\}\}/m, (_all, indent: string, head: string) =>
|
|
156
|
-
`${indent}${head}${JSON.stringify(hubProblemMatcher(), null, 3).split('\n').join(`\n${indent}`)}`)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
105
|
export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
160
106
|
const p = opts.print ?? console.log
|
|
161
107
|
// the hub resolves its port from repo.config, whose name `init` takes from
|
|
162
108
|
// the CALLER first and the manifest only after (init-repo.ts repoName). A
|
|
163
|
-
// dir that arrived holding a forked package.json would otherwise get
|
|
164
|
-
//
|
|
109
|
+
// dir that arrived holding a forked package.json would otherwise get an
|
|
110
|
+
// agent index advertising a port nothing listens on, so the caller that
|
|
165
111
|
// knows the name it is about to wire passes the port it resolves
|
|
166
112
|
const port = opts.port ?? defaultHubPort(packageName(root) ?? opts.name)
|
|
167
113
|
const vars = {
|
|
@@ -175,15 +121,32 @@ export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
|
175
121
|
|
|
176
122
|
write('src/index.ts', 'export {}\n')
|
|
177
123
|
write('tsconfig.json', template('tsconfig.json', vars))
|
|
178
|
-
|
|
179
|
-
// settings.json only here: it turns eslint off and
|
|
180
|
-
//
|
|
124
|
+
write('bunfig.toml', template('bunfig.toml', vars))
|
|
125
|
+
// settings.json only here: it turns eslint off and keeps tsserver from
|
|
126
|
+
// formatting, which is a decision only the repo's creator gets to make
|
|
181
127
|
write('.vscode/settings.json', template('vscode-settings.json', vars))
|
|
182
128
|
write('.claude/skills/verify/SKILL.md', template('verify-skill.md', vars))
|
|
129
|
+
writeHouseFiles(write, vars, opts.claudePolicy === true, written, p)
|
|
130
|
+
|
|
131
|
+
addScripts(root, written, p)
|
|
132
|
+
addIgnoreLines(root, written)
|
|
133
|
+
|
|
134
|
+
if (written.length > 0) p(`ok: scaffolded ${written.length} file(s): ${written.slice(0, 6).join(', ')}${written.length > 6 ? `, +${written.length - 6} more` : ''}`)
|
|
135
|
+
// the skips are the interesting half on a half-built dir: a pre-existing
|
|
136
|
+
// .claude/settings.json means no attribution silencer, and that one only
|
|
137
|
+
// counts from the first commit on
|
|
138
|
+
if (skipped.length > 0) p(`skip: ${skipped.length} file(s) already present, left untouched: ${skipped.join(', ')}`)
|
|
139
|
+
return written
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** the half of the day-one shape a house brings, whatever the repo already
|
|
143
|
+
* is: the agent wiki its CLAUDE.md routes at, the reflections folder, and
|
|
144
|
+
* for a caller that declared a house the .claude/ policy files. Create-only */
|
|
145
|
+
function writeHouseFiles(write: (rel: string, body: string) => void, vars: Record<string, string>, claudePolicy: boolean, written: string[], p: (line: string) => void): void {
|
|
183
146
|
// a permission grant and a git-history policy are decisions about someone
|
|
184
147
|
// else's repo, so they land only for a caller that declared a house, and
|
|
185
148
|
// the grant is named out loud the one time it is written
|
|
186
|
-
if (
|
|
149
|
+
if (claudePolicy) {
|
|
187
150
|
write('.claude/settings.json', template('claude-settings.json', vars))
|
|
188
151
|
write('.claude/settings.local.json', template('claude-settings-local.json', vars))
|
|
189
152
|
if (written.includes('.claude/settings.local.json')) p(`ok: .claude/ policy written — agents are pre-approved for ${grantSummary()} in this repo, and the harness writes no attribution trailer`)
|
|
@@ -197,15 +160,18 @@ export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
|
197
160
|
// gitignored by policy, so it never ships: it exists so an agent hitting
|
|
198
161
|
// friction with the toolchain finds the place to write it down
|
|
199
162
|
write('.shipkit/reflections/README.md', REFLECTIONS_README)
|
|
163
|
+
}
|
|
200
164
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
165
|
+
/** an ADOPTED repo (one with history) whose start declares a house: the
|
|
166
|
+
* include lines pull it into the house contract, so it gets the house half
|
|
167
|
+
* of the day-one shape and nothing that says what the repo IS. Create-only,
|
|
168
|
+
* so every file the repo already holds wins. Returns the paths it wrote */
|
|
169
|
+
export function adoptHouse(root: string, opts: { name: string; today?: string; print?: (line: string) => void }): string[] {
|
|
170
|
+
const p = opts.print ?? console.log
|
|
171
|
+
const vars = { NAME: opts.name, PORT: String(defaultHubPort(opts.name)), DATE: opts.today ?? new Date().toISOString().slice(0, 10) }
|
|
172
|
+
const written: string[] = []
|
|
173
|
+
writeHouseFiles((rel, body) => { if (put(root, rel, body)) written.push(rel) }, vars, true, written, p)
|
|
174
|
+
if (written.length > 0) p(`ok: adopted into the house, ${written.length} file(s) seeded: ${written.join(', ')}`)
|
|
209
175
|
return written
|
|
210
176
|
}
|
|
211
177
|
|
|
@@ -227,14 +193,14 @@ What broke, which page or lib, what you expected. Gitignored on purpose,
|
|
|
227
193
|
transient by design: it is harvested, then deleted.
|
|
228
194
|
`
|
|
229
195
|
|
|
230
|
-
/** the
|
|
196
|
+
/** the baseline scripts, added only where the key is free */
|
|
231
197
|
function addScripts(root: string, written: string[], p: (line: string) => void): void {
|
|
232
198
|
const path = join(root, 'package.json')
|
|
233
|
-
if (!existsSync(path)) return void p('WARN no package.json here — the
|
|
199
|
+
if (!existsSync(path)) return void p('WARN no package.json here — the typecheck script was not written')
|
|
234
200
|
const raw = readFileSync(path, 'utf8')
|
|
235
201
|
let pkg: { scripts?: Record<string, string> }
|
|
236
202
|
try { pkg = JSON.parse(raw) as { scripts?: Record<string, string> } }
|
|
237
|
-
catch (e) { return void p(`WARN package.json is unparseable (${String(e).slice(0, 80)}) — the
|
|
203
|
+
catch (e) { return void p(`WARN package.json is unparseable (${String(e).slice(0, 80)}) — the typecheck script was not written, repair it then re-run`) }
|
|
238
204
|
pkg.scripts ??= {}
|
|
239
205
|
let added = 0
|
|
240
206
|
for (const [k, v] of Object.entries(SCRIPTS)) if (pkg.scripts[k] == null) { pkg.scripts[k] = v; added++ }
|
|
@@ -13,7 +13,6 @@ import type { RepoSt } from './repo-st.ts'
|
|
|
13
13
|
import type { SlowSuite, CheckFacts, RecentRun } from './hub-types.ts'
|
|
14
14
|
import { renderMd, renderMdInline } from './md.ts'
|
|
15
15
|
import { nextRun } from './next-run.ts'
|
|
16
|
-
import { ProseHits } from './prose-hits-view.tsx'
|
|
17
16
|
import { stampWords, TimeAgo, TimeAhead } from './time-ago.tsx'
|
|
18
17
|
import { checkName, commandOf, docPath, editorHref, lastRun, rememberedFact, repoPath, whatFacts, type ShortPath } from './check-words.ts'
|
|
19
18
|
|
|
@@ -23,8 +22,9 @@ const SOURCE_CHIP: Record<CheckFacts['whenSource'], string> = {
|
|
|
23
22
|
off: 'repo.config.ts',
|
|
24
23
|
when: 'when (retired)',
|
|
25
24
|
ruleExempt: 'ruleExempt (retired)',
|
|
25
|
+
'hooks.disable': 'hooks.disable (retired)',
|
|
26
26
|
'check-file': 'the check file',
|
|
27
|
-
defaults: 'defaults',
|
|
27
|
+
defaults: 'defaults (retired)',
|
|
28
28
|
default: 'derived',
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -33,7 +33,8 @@ const SOURCE_WORD: Record<CheckFacts['whenSource'], string> = {
|
|
|
33
33
|
when: 'turned off by the retired `when` key, which `shipkit ci` ports to `off` (SK029)',
|
|
34
34
|
ruleExempt: 'turned off by the retired `ruleExempt` key, which `shipkit ci` ports to `off` (SK029)',
|
|
35
35
|
'check-file': 'declared by the check file itself: every context it runs in, no default',
|
|
36
|
-
|
|
36
|
+
'hooks.disable': 'turned off by the retired `hooks.disable` key, which `shipkit ci` ports to `off` (SK029)',
|
|
37
|
+
defaults: 'turned off by the retired `defaults: { <slot>: false }`, which `shipkit ci` ports to `off` (SK029)',
|
|
37
38
|
default: 'a row with no file of its own: the code that derives it writes every context',
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -359,7 +360,6 @@ export const CheckCard = observer(({ app, id }: { app: RepoSt; id: string }) =>
|
|
|
359
360
|
</div>
|
|
360
361
|
<WhereItRuns app={app} u={u} />
|
|
361
362
|
{u.build && u.checkId === 'test' ? <SlowSuites app={app} /> : null}
|
|
362
|
-
{u.checkId === 'SK014' ? <ProseHits app={app} /> : null}
|
|
363
363
|
<LastOutput app={app} lines={u.lastLines} />
|
|
364
364
|
</div>
|
|
365
365
|
)
|
|
@@ -100,8 +100,7 @@ export class ChecksSvc {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** ONE write path for the switch
|
|
104
|
-
* lists alike: it edits repo.config.ts in place and leaves it dirty to be
|
|
103
|
+
/** ONE write path for the switch and the slow suite form alike: it edits repo.config.ts in place and leaves it dirty to be
|
|
105
104
|
* reviewed. Resolves true when the server took the edit */
|
|
106
105
|
async writeGate(edit: Record<string, unknown>, said: string): Promise<boolean> {
|
|
107
106
|
if (this.gateBusy) return false
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
<title>home</title>
|
|
7
7
|
<link rel="stylesheet" href="./hub.css" />
|
|
8
8
|
<link rel="stylesheet" href="./project-list.css" />
|
|
9
|
-
<link rel="stylesheet" href="./prose-hits.css" />
|
|
10
9
|
<link rel="stylesheet" href="./home-strip.css" />
|
|
11
10
|
<link rel="stylesheet" href="./planned.css" />
|
|
12
11
|
<link rel="stylesheet" href="./when.css" />
|
|
@@ -145,8 +145,6 @@ export type GateBlock = {
|
|
|
145
145
|
tests: { budgetMs?: number; slow?: SlowSuite[]; serial?: { file: string; why: string }[] }
|
|
146
146
|
declaredOutside: boolean
|
|
147
147
|
dirty: boolean
|
|
148
|
-
/** the prose words block inside `prose: {` (core/when-block.ts) */
|
|
149
|
-
wordsState?: 'missing-file' | 'missing-block' | 'stale' | 'current' | 'no-prose'
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
/** one check finishing, as the run stream sends it */
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
<title>hub</title>
|
|
7
7
|
<link rel="stylesheet" href="./hub.css" />
|
|
8
8
|
<link rel="stylesheet" href="./project-list.css" />
|
|
9
|
-
<link rel="stylesheet" href="./prose-hits.css" />
|
|
10
9
|
<link rel="stylesheet" href="./home-strip.css" />
|
|
11
10
|
<link rel="stylesheet" href="./planned.css" />
|
|
12
11
|
<link rel="stylesheet" href="./when.css" />
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/* the project list, the page's left panel. Its classes are pj
|
|
2
|
-
sheet of the page already styles .plist (prose-hits.css). It fills its
|
|
1
|
+
/* the project list, the page's left panel. Its classes are pj*. It fills its
|
|
3
2
|
panel (hub-app.tsx, react-resizable-panels), whose width is the only one:
|
|
4
3
|
this sheet sizes the rows and never the list. Colours are hub.css tokens only, spacing is its scale:
|
|
5
4
|
tests/hub-css-invariants.test.ts reads this sheet too. Every icon is 18px
|
|
@@ -9,6 +9,7 @@ import type { ProcessRow, RowDot } from '../../home/process-rows.ts'
|
|
|
9
9
|
import { checkVerdictWords, plural } from '../../core/outcome-words.ts'
|
|
10
10
|
import { runsWords } from '../../core/runs-matrix.ts'
|
|
11
11
|
import { checkName } from './check-words.ts'
|
|
12
|
+
import type { WhenAt } from './hub-types.ts'
|
|
12
13
|
import { durationTone, fmtMs, type DurTone } from './dur.ts'
|
|
13
14
|
import type { HashSel } from './hash-route.ts'
|
|
14
15
|
import { SECTION_LETTER, sectionLabel, type SectionId } from './keys.ts'
|
|
@@ -234,8 +235,8 @@ export function flattenTree(nodes: TreeNode[], expanded: Set<string>, depth = 0)
|
|
|
234
235
|
/** the line at the top of the checks page: how much of this repo runs on
|
|
235
236
|
* every gate, and how much does not. Counts CHECKS only, because nothing
|
|
236
237
|
* else declares a place to run */
|
|
237
|
-
export function whenCounts(resources: Resource[]): Record<
|
|
238
|
-
const out = { always: 0, release: 0, health: 0, off: 0 }
|
|
238
|
+
export function whenCounts(resources: Resource[]): Record<WhenAt, number> {
|
|
239
|
+
const out: Record<WhenAt, number> = { always: 0, release: 0, health: 0, commit: 0, off: 0 }
|
|
239
240
|
for (const r of resources) if (r.check != null) out[r.check.when]++
|
|
240
241
|
return out
|
|
241
242
|
}
|
|
@@ -95,9 +95,9 @@ export class TreeSvc {
|
|
|
95
95
|
/** the line at the top of the checks page: what runs always and what does not */
|
|
96
96
|
get whenLine(): string {
|
|
97
97
|
const c = whenCounts(this.st.cols.resources)
|
|
98
|
-
const total = c.always + c.release + c.health + c.off
|
|
98
|
+
const total = c.always + c.release + c.health + c.commit + c.off
|
|
99
99
|
if (total === 0) return ''
|
|
100
|
-
return [`${c.always} always`, `${c.release} release`, `${c.health} health`, `${c.off} off`].join(' · ')
|
|
100
|
+
return [`${c.always} always`, `${c.release} release`, `${c.health} health`, `${c.commit} commit`, `${c.off} off`].join(' · ')
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
get redCount(): number {
|
package/src/serve/hub-port.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// the hub's default port, alone in its own module ON PURPOSE.
|
|
2
2
|
//
|
|
3
3
|
// It is four lines of arithmetic that several places need BEFORE any server
|
|
4
|
-
// exists: the repo scaffold writes it into
|
|
5
|
-
//
|
|
4
|
+
// exists: the repo scaffold writes it into agent/index.md, `init` prints it,
|
|
5
|
+
// the config resolver defaults to it. Importing it from run-serve.ts drags
|
|
6
6
|
// the whole hub (react, the markdown renderer, the sanitiser, every panel)
|
|
7
7
|
// into a caller that only wanted a number, which measured 393ms of module
|
|
8
8
|
// load in the scaffold alone.
|
|
9
9
|
//
|
|
10
10
|
// A port derived from the repo NAME rather than assigned means two repos open
|
|
11
11
|
// side by side almost never collide, and the same repo always reopens on the
|
|
12
|
-
// same port, which is what
|
|
12
|
+
// same port, which is what lets a page advertise a URL before the hub runs.
|
|
13
13
|
export function defaultHubPort(name: string): number {
|
|
14
14
|
let h = 0
|
|
15
15
|
for (const c of name) h = (h * 31 + c.charCodeAt(0)) >>> 0
|
package/src/serve/hub-runtime.ts
CHANGED
|
@@ -27,8 +27,6 @@ import { wikisCollection } from '../collections/wikis.ts'
|
|
|
27
27
|
import { pingTick } from '../collections/pings.ts'
|
|
28
28
|
import { buildCollections } from '../collections/providers.ts'
|
|
29
29
|
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
30
|
-
import { CheckCtx } from '../core/check-ctx.ts'
|
|
31
|
-
import { proseView } from '../prose/prose-view.ts'
|
|
32
30
|
import { allTasks, gateInventory } from '../ledger/gate-checks.ts'
|
|
33
31
|
import { runCheck, type CheckEvent } from '../core/run-check.ts'
|
|
34
32
|
import { runCi, type CiOpts } from '../core/run-ci.ts'
|
|
@@ -391,7 +389,6 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
|
|
|
391
389
|
const input = { facts, runs: readRuns(record), suiteRuns: readSuiteRuns(record) }
|
|
392
390
|
return check == null || check === '' ? shapeTestsRoot(input) : shapeTestCheck(check, input)
|
|
393
391
|
},
|
|
394
|
-
prose: async () => proseView(CheckCtx.open({ root, config: c }), c),
|
|
395
392
|
needs: () => projectNeeds(allCollections(), hubProblems.collector),
|
|
396
393
|
hubProblems,
|
|
397
394
|
// a job outlives this image, so only an in process run holds a reload back
|
package/src/serve/serve-hub.ts
CHANGED
|
@@ -16,7 +16,6 @@ import type { CheckEvent } from '../core/run-check.ts'
|
|
|
16
16
|
import { configDirty, readGateBlock, writeGateBlock, type GateEdit } from '../core/when-block.ts'
|
|
17
17
|
import type { InvokedBy } from '../ledger/run-log.ts'
|
|
18
18
|
import type { GraphMount } from '../wiki/graph-mount.ts'
|
|
19
|
-
import type { ProseView } from '../prose/prose-view.ts'
|
|
20
19
|
import type { Timeline } from '../ledger/run-timeline.ts'
|
|
21
20
|
import type { TestCheckView, TestsRootView } from '../collections/test-time.ts'
|
|
22
21
|
import type { LiveView } from '../ledger/live-timeline.ts'
|
|
@@ -105,9 +104,6 @@ export type HubDeps = {
|
|
|
105
104
|
/** the comfy panel; null when the repo declares no workflow module, which
|
|
106
105
|
* is also when the comfy section is absent from the page */
|
|
107
106
|
comfy: ComfyDeps | null
|
|
108
|
-
/** rule SK014's hits, grouped by word, over the config on disk: the data
|
|
109
|
-
* of the SK014 page. Absent = the endpoint answers 404 */
|
|
110
|
-
prose?: () => Promise<ProseView>
|
|
111
107
|
/** re-read repo.config.ts when its bytes moved. The switch WRITES that
|
|
112
108
|
* file, so every surface built on the config asks for this first, or a
|
|
113
109
|
* toggle reads as a toggle that did nothing until the next restart */
|
|
@@ -367,7 +363,7 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
|
|
|
367
363
|
if (req.method === 'POST') {
|
|
368
364
|
try {
|
|
369
365
|
const body = (await req.json()) as GateEdit & { expect?: string }
|
|
370
|
-
const b = await writeGateBlock(d.root, { off: body.off, slow: body.slow, slowRemove: body.slowRemove
|
|
366
|
+
const b = await writeGateBlock(d.root, { off: body.off, slow: body.slow, slowRemove: body.slowRemove }, { expect: body.expect })
|
|
371
367
|
await d.refresh?.()
|
|
372
368
|
return Response.json({ ...b, dirty: configDirty(d.root) })
|
|
373
369
|
} catch (e) {
|
|
@@ -375,17 +371,6 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
|
|
|
375
371
|
}
|
|
376
372
|
}
|
|
377
373
|
}
|
|
378
|
-
if (path === '/api/prose.json' && d.prose != null) {
|
|
379
|
-
// every SK014 hit, grouped by word, noisiest first. The word
|
|
380
|
-
// lists are written through /api/checks/when like every other
|
|
381
|
-
// edit of repo.config.ts
|
|
382
|
-
await d.refresh?.()
|
|
383
|
-
try {
|
|
384
|
-
return Response.json(await d.prose())
|
|
385
|
-
} catch (e) {
|
|
386
|
-
return new Response(`the prose check could not run: ${(e as Error).message}`, { status: 500 })
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
374
|
if (path === '/api/check/run' && req.method === 'POST') {
|
|
390
375
|
const body = (await req.json().catch(() => ({}))) as { only?: string[]; gate?: boolean }
|
|
391
376
|
// NDJSON: one line per event, so the page colours a row the
|
|
@@ -48,7 +48,7 @@ export async function gitIgnored(root: string, paths: string[]): Promise<Set<str
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** a commit, a fetch or a tag moved a ref: the one write inside .git a check
|
|
51
|
-
* keyed on the refs
|
|
51
|
+
* keyed on the refs must hear. Lock files are git mid write */
|
|
52
52
|
export function isRefMove(rel: string): boolean {
|
|
53
53
|
if (rel.endsWith('.lock')) return false
|
|
54
54
|
return rel === '.git/HEAD' || rel === '.git/packed-refs' || rel.startsWith('.git/refs/')
|
package/src/start-repo.ts
CHANGED
|
@@ -13,19 +13,24 @@ import { join } from 'path'
|
|
|
13
13
|
import { ensureGitRepo, ensureIgnoreBaseline, firstGhLine, isScratch, publishRepo, runner, type Run } from './git-bootstrap.ts'
|
|
14
14
|
import { initRepo } from './init-repo.ts'
|
|
15
15
|
import { AGENT_LINE } from './checks/SK003-claude-md-shape.check.ts'
|
|
16
|
-
import { scaffoldProject, TOOLCHAIN } from './scaffold/project-scaffold.ts'
|
|
16
|
+
import { adoptHouse, scaffoldProject, TOOLCHAIN } from './scaffold/project-scaffold.ts'
|
|
17
17
|
import { defaultHubPort } from './serve/hub-port.ts'
|
|
18
18
|
|
|
19
19
|
/** the one template: `shipkit start <name>` is the only thing anyone types,
|
|
20
|
-
* and a repo without the
|
|
20
|
+
* and a repo without the house wiki in its context is not a started repo.
|
|
21
21
|
* Base context first, the repo's own router last, so repo docs override.
|
|
22
22
|
* A public repo swaps line 1 for the gitignored `@./CLAUDE.local.md`,
|
|
23
23
|
* which is why that line satisfies the first include (SK003) */
|
|
24
|
+
// sk-disable-next-line RV003 -- the start template writes the house include line itself
|
|
24
25
|
export const HOUSE_INCLUDE = ['@~/dev/corvion/wiki/index.md', '@./agent/index.md']
|
|
25
26
|
export const HOUSE_ALSO_OK = ['@./CLAUDE.local.md']
|
|
26
27
|
/** the day-job checkout: never wired, never given an include */
|
|
28
|
+
// sk-disable-next-line RV003 -- start refuses the day-job checkout by its name
|
|
27
29
|
export const HOUSE_SKIP = ['monoloco']
|
|
28
30
|
|
|
31
|
+
/** a repo shipkit never touches: no CLAUDE.md, no git, no scaffold, no push */
|
|
32
|
+
export const isSkipped = (name: string, skip: readonly string[] = HOUSE_SKIP): boolean => skip.includes(name)
|
|
33
|
+
|
|
29
34
|
/** where the first house line points, `~` resolved against `home` */
|
|
30
35
|
export const housePath = (home: string): string => {
|
|
31
36
|
const p = HOUSE_INCLUDE[0]!.replace(/^@/, '')
|
|
@@ -72,7 +77,7 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
|
|
|
72
77
|
const house = opts.include ?? (houseOnDisk ? HOUSE_INCLUDE : [])
|
|
73
78
|
if (opts.include == null && !houseOnDisk) console.error(`skip: no house wiki at ${housePath(home)} — CLAUDE.md gets the agent route only, and no .claude/ policy files`)
|
|
74
79
|
const alt = opts.alsoOk ?? HOUSE_ALSO_OK
|
|
75
|
-
const skipped = (opts.skip
|
|
80
|
+
const skipped = isSkipped(name, opts.skip)
|
|
76
81
|
if (skipped && house.length > 0) console.error(`skip: ${name} is include-exempt — CLAUDE.md untouched`)
|
|
77
82
|
// start is the from-anywhere verb, and a STARTED repo comes out READY:
|
|
78
83
|
// any dir with no commit history gets a minimal package.json, a first
|
|
@@ -146,6 +151,13 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
|
|
|
146
151
|
else console.error(`WARN dependency install failed: ${firstGhLine(r.out)}`)
|
|
147
152
|
}
|
|
148
153
|
}
|
|
154
|
+
// a repo with history that a house adopts: its own files win, and the
|
|
155
|
+
// house half of the day-one shape lands beside them, so the include
|
|
156
|
+
// lines below route at an agent wiki that exists
|
|
157
|
+
if (wired && !startable && house.length > 0) {
|
|
158
|
+
try { adoptHouse(dir, { name, print }) }
|
|
159
|
+
catch (e) { console.error(`WARN could not seed the house files in ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
|
|
160
|
+
}
|
|
149
161
|
let unscaffolded = false
|
|
150
162
|
if (!existsSync(join(dir, 'repo.config.ts'))) {
|
|
151
163
|
if (existsSync(join(dir, 'package.json'))) {
|
|
@@ -18,7 +18,7 @@ export type SerialSuite = {
|
|
|
18
18
|
why: string
|
|
19
19
|
}
|
|
20
20
|
export type TestsConfig = {
|
|
21
|
-
/** per suite time in the quick tier (default 2000). A suite whose wall time goes over it under the parallel run is run again alone, and fails when the smaller of its CPU and its wall passes this, grown with the load share by up to half, or when its wall passes twice this, an allowance that grows with the square of the load share and has no end on a saturated machine. A suite whose CPU inside the parallel run, children included,
|
|
21
|
+
/** per suite time in the quick tier (default 2000). A suite whose wall time goes over it under the parallel run is run again alone, and fails when the smaller of its CPU and its wall passes this, grown with the load share by up to half, or when its wall passes twice this, an allowance that grows with the square of the load share and has no end on a saturated machine. A suite whose own CPU inside the parallel run, children included, is inside that CPU allowance skips that run, unless its wall is past the wait allowance or it ended with a child not yet reaped. What earlier suites of its worker burned is never charged to it. The full tier fails nothing on time */
|
|
22
22
|
budgetMs?: number
|
|
23
23
|
/** suites that break the budget on purpose: the full tier runs them, the quick tier does not */
|
|
24
24
|
slow?: SlowSuite[]
|
|
@@ -56,6 +56,10 @@ const OWN_FLAGS = ['--budget-ms', '--slow', '--serial', '--declared-in', '--ceil
|
|
|
56
56
|
|
|
57
57
|
export const normSuitePath = (file: string): string => posix.normalize(file.replace(/\\/g, '/')).replace(/^\.\//, '')
|
|
58
58
|
|
|
59
|
+
/** bun never discovers a file under a dot dir (`.shipkit/repros/` relies on
|
|
60
|
+
* it), so neither does the gate */
|
|
61
|
+
export const inDotDir = (file: string): boolean => normSuitePath(file).split('/').slice(0, -1).some((d) => d.startsWith('.') && d !== '.' && d !== '..')
|
|
62
|
+
|
|
59
63
|
/** the arguments after `bun test`, or null when a shell would interpret the script
|
|
60
64
|
* (cd, env prefix, pipes, quotes, substitution): that body stays the repo's own */
|
|
61
65
|
export type BunfigTest = { root: string | null; ignores: string[]; error: string | null }
|
|
@@ -282,49 +286,46 @@ export const overWords = (c: Confirmed, load: MachineLoad, budgetMs: number = DE
|
|
|
282
286
|
* (suite-cpu-preload.ts writes it, readPhaseCpu reads it) */
|
|
283
287
|
export const SUITE_CPU_ENV = 'SHIPKIT_SUITE_CPU'
|
|
284
288
|
|
|
285
|
-
/** one suite's figures from the parallel phase: its own CPU, children
|
|
286
|
-
* included, and all its worker burned before it began (suite-cpu-preload.ts
|
|
287
|
-
* says why the sum bounds a lone run) */
|
|
288
|
-
export type PhaseCpu = { cpuMs: number; priorMs: number }
|
|
289
|
-
|
|
290
289
|
/** what the workers wrote, keyed like the timings: relative to the run's cwd.
|
|
291
|
-
* `ran` is every suite that finished in the phase, `cpu` the
|
|
292
|
-
* figure can be trusted. A file bun ran
|
|
293
|
-
* none at all once one of its runs could
|
|
294
|
-
|
|
290
|
+
* `ran` is every suite that finished in the phase, `cpu` the own CPU of the
|
|
291
|
+
* ones whose figure can be trusted (suite-cpu-preload.ts). A file bun ran
|
|
292
|
+
* twice keeps its larger figure, and none at all once one of its runs could
|
|
293
|
+
* not be trusted */
|
|
294
|
+
export function readPhaseCpu(text: string, cwd: string): { ran: string[]; cpu: Record<string, number> } {
|
|
295
295
|
const ran = new Set<string>()
|
|
296
296
|
const unsure = new Set<string>()
|
|
297
|
-
const cpu: Record<string,
|
|
297
|
+
const cpu: Record<string, number> = {}
|
|
298
298
|
const base = cwd.replace(/\\/g, '/')
|
|
299
299
|
for (const line of text.split('\n')) {
|
|
300
|
-
const [file, ms
|
|
301
|
-
if (file == null || file === '' || ms == null
|
|
300
|
+
const [file, ms] = line.split('\t')
|
|
301
|
+
if (file == null || file === '' || ms == null) continue
|
|
302
302
|
const rel = normSuitePath(posix.relative(base, file.replace(/\\/g, '/')))
|
|
303
|
-
if (ms === '-'
|
|
304
|
-
if (!/^\d+$/.test(ms)
|
|
303
|
+
if (ms === '-') { ran.add(rel); unsure.add(rel); continue }
|
|
304
|
+
if (!/^\d+$/.test(ms)) continue
|
|
305
305
|
ran.add(rel)
|
|
306
|
-
|
|
307
|
-
const had = cpu[rel]
|
|
308
|
-
if (had == null || next.cpuMs + next.priorMs > had.cpuMs + had.priorMs) cpu[rel] = next
|
|
306
|
+
cpu[rel] = Math.max(cpu[rel] ?? 0, Number(ms))
|
|
309
307
|
}
|
|
310
308
|
for (const f of unsure) delete cpu[f]
|
|
311
309
|
return { ran: [...ran].sort(), cpu }
|
|
312
310
|
}
|
|
313
311
|
|
|
314
|
-
/** which suspects still need their alone run. A
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
|
|
312
|
+
/** which suspects still need their alone run. A suspect is judged on its OWN
|
|
313
|
+
* CPU in the parallel phase, charged and allowed as a lone run is
|
|
314
|
+
* (confirmOver): the load it suffered there only raised that figure, so one
|
|
315
|
+
* inside the allowance is cleared. What the earlier files of its worker
|
|
316
|
+
* burned is never its cost, a module they loaded or a memo they filled
|
|
317
|
+
* included: the first suite of a worker that needs shared work pays for it.
|
|
318
|
+
* A suspect with no figure (a child not yet reaped) or over its allowance
|
|
319
|
+
* runs alone, and only the alone run can see a WAIT, so a suite whose wall
|
|
320
|
+
* is past its wait allowance still gets it */
|
|
321
|
+
export function aloneRunsNeeded(suspects: Timed[], parallelCpu: Record<string, number>, budgetMs: number, load: MachineLoad): { rerun: Timed[]; cleared: Confirmed[] } {
|
|
322
322
|
const rerun: Timed[] = []
|
|
323
323
|
const cleared: Confirmed[] = []
|
|
324
324
|
for (const t of suspects) {
|
|
325
|
-
const
|
|
325
|
+
const cpuMs = parallelCpu[t.file]
|
|
326
326
|
const mayWait = t.ms > waitAllowance(budgetMs, load)
|
|
327
|
-
|
|
327
|
+
const charged = cpuMs == null ? null : Math.min(cpuMs, t.ms)
|
|
328
|
+
if (charged != null && charged <= cpuAllowance(budgetMs, load) && !mayWait) cleared.push({ file: t.file, ms: charged, wallMs: t.ms, cpuMs: cpuMs ?? null, over: null })
|
|
328
329
|
else rerun.push(t)
|
|
329
330
|
}
|
|
330
331
|
return { rerun, cleared }
|
package/src/suite/select.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { blobOid, listingHash } from '../core/read-set.ts'
|
|
|
31
31
|
import { shipkitRoot } from '../ledger/db.ts'
|
|
32
32
|
import { extractSpecs, resolveSpec } from '../ts/source-graph.ts'
|
|
33
33
|
import { type RvtConfig } from '../ts/config.ts'
|
|
34
|
+
import { inDotDir } from './judge-suites.ts'
|
|
34
35
|
|
|
35
36
|
export const SELECT_VERSION = 5
|
|
36
37
|
|
|
@@ -489,6 +490,7 @@ export function suiteFiles(top: string, cwdRel: string, testRoot: string | null,
|
|
|
489
490
|
for (const f of listed) {
|
|
490
491
|
if (f === '' || !isTest(f) || !existsSync(join(top, f))) continue
|
|
491
492
|
const rel = cwdRel === '' || cwdRel === '.' ? f : f.slice(cwdRel.length + 1)
|
|
493
|
+
if (inDotDir(rel)) continue
|
|
492
494
|
if (res.some((re) => re.test(rel))) continue
|
|
493
495
|
out.push(rel)
|
|
494
496
|
}
|
package/src/suite/suite-cli.ts
CHANGED
|
@@ -57,7 +57,10 @@ async function runPhase(args: string[], timingsFile: string | null, ceilingMs: n
|
|
|
57
57
|
if (echo) sink.write(text)
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
// bun reports on stderr, and it is echoed on stdout: two pipes reach the
|
|
61
|
+
// gate in either order, so bun's `Ran 0 tests` could land after the FAIL
|
|
62
|
+
// printed below and become the headline. One stream keeps the order
|
|
63
|
+
await Promise.all([pump(proc.stdout, process.stdout), pump(proc.stderr, process.stdout)])
|
|
61
64
|
const code = await proc.exited
|
|
62
65
|
clearTimeout(ceiling)
|
|
63
66
|
clearTimeout(killer)
|
|
@@ -224,7 +227,7 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
224
227
|
}
|
|
225
228
|
const aloneMs = Date.now() - aloneAt
|
|
226
229
|
const { over, cleared } = rerun.length > 0 ? confirmOver(rerun, aloneRuns, budget, load) : { over: [], cleared: [] }
|
|
227
|
-
for (const c of clearedInPhase) notes.push(`note: ${c.file} took ${secs(c.wallMs)} in the parallel run
|
|
230
|
+
for (const c of clearedInPhase) notes.push(`note: ${c.file} took ${secs(c.wallMs)} in the parallel run but burned ${secs(c.ms)} of its own CPU there, inside the ${budget}ms budget (load ${load.load1.toFixed(1)} on ${load.cores} cores), so it did not run again alone`)
|
|
228
231
|
for (const c of cleared) notes.push(`note: ${c.file} took ${secs(c.wallMs)} alone but burned ${c.cpuMs != null ? secs(c.cpuMs) : '?'} of CPU, inside the ${budget}ms budget (load ${load.load1.toFixed(1)} on ${load.cores} cores)`)
|
|
229
232
|
|
|
230
233
|
// a ceiling kill makes bun report every worker it held as crashed, and
|
|
@@ -254,7 +257,7 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
254
257
|
// rerun, the parallel phase's own figure, or a phase that ran that one file
|
|
255
258
|
const phaseOf = (f: string): SuitePhase => (p2 != null && Object.keys(p2.timings).some((k) => normSuitePath(k) === f) ? 'serial' : parallel ? 'parallel' : 'sequential')
|
|
256
259
|
const soloCpu = (p: Phase | null): number | null => (p != null && Object.keys(p.timings).length === 1 ? p.cpuMs : null)
|
|
257
|
-
const cpuOf = (f: string): number | null => aloneRuns[f]?.cpuMs ?? (phaseOf(f) === 'serial' ? soloCpu(p2) : parallelCpu[f]
|
|
260
|
+
const cpuOf = (f: string): number | null => aloneRuns[f]?.cpuMs ?? (phaseOf(f) === 'serial' ? soloCpu(p2) : parallelCpu[f] ?? soloCpu(p1))
|
|
258
261
|
const check = process.env[CHECK_ID_ENV] ?? ''
|
|
259
262
|
const serialFiles = p2 == null ? 0 : Object.keys(p2.timings).length
|
|
260
263
|
const unrecorded = recordSuites(process.cwd(), Object.entries(timings).map(([file, ms]) => {
|