rvlib-shipkit 1.3.0 → 1.5.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/CHANGELOG.md +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -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
|
|
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, plus all its worker burned before it stays under it skips that run, unless its wall is past that allowance or it ended with a child not yet reaped. 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[]
|
|
@@ -190,32 +190,58 @@ export const WAIT_FACTOR = 2
|
|
|
190
190
|
/** how much more CPU the same suite burns on a saturated machine. CPU time is
|
|
191
191
|
* not load free: measured on 8 cores, one suite went from 1.0s of CPU idle to
|
|
192
192
|
* 1.35 to 1.5s with the cores saturated (load 7 to 19), and did not grow past
|
|
193
|
-
* that. So
|
|
193
|
+
* that. So the CPU allowance grows with the load share up to this factor */
|
|
194
194
|
export const LOADED_CPU_FACTOR = 1.5
|
|
195
|
+
|
|
196
|
+
/** how much of the machine the other processes took, 0 idle to 1 saturated:
|
|
197
|
+
* the one load measure every verdict here reads, so no verdict has a step */
|
|
198
|
+
export const loadShare = (load: MachineLoad): number => (load.cores > 0 ? Math.min(1, Math.max(0, load.load1 / load.cores)) : 0)
|
|
199
|
+
/** the CPU a suite may burn at this load: the budget, plus the measured
|
|
200
|
+
* inflation in proportion to how saturated the machine is */
|
|
201
|
+
export const cpuAllowance = (budgetMs: number, load: MachineLoad): number => budgetMs * (1 + (LOADED_CPU_FACTOR - 1) * loadShare(load))
|
|
202
|
+
/** the wall a suite may take before it reads as a WAIT: WAIT_FACTOR times
|
|
203
|
+
* the budget on an idle machine, grown with the square of the load share,
|
|
204
|
+
* and without end once the machine is saturated. A wall on a busy machine
|
|
205
|
+
* is the machine's (measured: 11 to 19s for suites burning 1s of CPU at
|
|
206
|
+
* load 20.5 on 8 cores, far past any fair share), so only a quiet machine
|
|
207
|
+
* can show a suite that sleeps; the ramp keeps the verdict continuous */
|
|
208
|
+
export const waitAllowance = (budgetMs: number, load: MachineLoad): number => {
|
|
209
|
+
const free = 1 - loadShare(load) ** 2
|
|
210
|
+
return free <= 0 ? Infinity : (budgetMs * WAIT_FACTOR) / free
|
|
211
|
+
}
|
|
195
212
|
/** `ms` is the time the verdict used: cpu, or wall for a wait or when no cpu figure exists */
|
|
196
213
|
export type Confirmed = { file: string; ms: number; wallMs: number; cpuMs: number | null; over: 'cpu' | 'wait' | 'wall' | null }
|
|
197
214
|
|
|
198
|
-
/** the verdict on each suspect once it ran alone
|
|
199
|
-
* of every other process on the machine,
|
|
200
|
-
* the budget judges CPU
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
215
|
+
/** the verdict on each suspect once it ran alone, continuous in the load.
|
|
216
|
+
* A wall time carries the load of every other process on the machine, so
|
|
217
|
+
* the budget judges CPU, against an allowance that grows with the load share
|
|
218
|
+
* (cpuAllowance). A suite is charged the SMALLER of its CPU and its wall:
|
|
219
|
+
* JS runs on one thread, so CPU above the wall is bun's GC and JIT threads
|
|
220
|
+
* or children running side by side, time nobody waits on. A wall past its
|
|
221
|
+
* wait allowance fails as a wait (waitAllowance), never on a busy machine */
|
|
204
222
|
export function confirmOver(suspects: Timed[], alone: Record<string, AloneRun>, budgetMs: number, load: MachineLoad): { over: Confirmed[]; cleared: Confirmed[] } {
|
|
205
223
|
const byFile = new Map(Object.entries(alone).map(([f, run]) => [normSuitePath(f), run]))
|
|
206
|
-
const idle = load.load1 < load.cores
|
|
207
224
|
const judged = suspects.map((t): Confirmed => {
|
|
208
225
|
const run = byFile.get(t.file)
|
|
209
226
|
const wallMs = run?.wallMs ?? t.ms
|
|
210
227
|
const cpuMs = run?.cpuMs ?? null
|
|
211
228
|
if (cpuMs == null) return { file: t.file, ms: wallMs, wallMs, cpuMs, over: wallMs > budgetMs ? 'wall' : null }
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
return { file: t.file, ms:
|
|
229
|
+
const charged = Math.min(cpuMs, wallMs)
|
|
230
|
+
if (charged > cpuAllowance(budgetMs, load)) return { file: t.file, ms: charged, wallMs, cpuMs, over: 'cpu' }
|
|
231
|
+
if (wallMs > waitAllowance(budgetMs, load)) return { file: t.file, ms: wallMs, wallMs, cpuMs, over: 'wait' }
|
|
232
|
+
return { file: t.file, ms: charged, wallMs, cpuMs, over: null }
|
|
215
233
|
})
|
|
216
234
|
return { over: judged.filter((c) => c.over != null), cleared: judged.filter((c) => c.over == null) }
|
|
217
235
|
}
|
|
218
236
|
|
|
237
|
+
const secs = (ms: number): string => `${(ms / 1000).toFixed(1)}s`
|
|
238
|
+
|
|
239
|
+
/** the line a confirmed suite fails with */
|
|
240
|
+
export const overWords = (c: Confirmed, load: MachineLoad, budgetMs: number = DEFAULT_BUDGET_MS): string =>
|
|
241
|
+
c.over === 'cpu' ? `${c.file} ${secs(c.cpuMs ?? c.ms)} of CPU (${secs(c.wallMs)} wall)`
|
|
242
|
+
: c.over === 'wait' ? `${c.file} ${secs(c.wallMs)} wall at load ${load.load1.toFixed(1)} on ${load.cores} cores, past its ${secs(waitAllowance(budgetMs, load))} wait allowance, with ${secs(c.cpuMs ?? 0)} of CPU: it waits (a timer, a sleep, a poll)`
|
|
243
|
+
: `${c.file} ${secs(c.wallMs)} wall (no CPU figure)`
|
|
244
|
+
|
|
219
245
|
/** how the parallel phase's workers hand the runner the CPU of each suite
|
|
220
246
|
* (suite-cpu-preload.ts writes it, readPhaseCpu reads it) */
|
|
221
247
|
export const SUITE_CPU_ENV = 'SHIPKIT_SUITE_CPU'
|
|
@@ -253,16 +279,15 @@ export function readPhaseCpu(text: string, cwd: string): { ran: string[]; cpu: R
|
|
|
253
279
|
* what the worker computed by the suite's end, its whole CPU before the
|
|
254
280
|
* suite (priorMs) plus what the suite burned, and sharing the machine only
|
|
255
281
|
* raises CPU, so a suspect whose sum is inside the budget is inside it alone
|
|
256
|
-
* too. A suspect with no figure (a child not yet reaped) runs alone. Only the
|
|
257
|
-
*
|
|
258
|
-
*
|
|
282
|
+
* too. A suspect with no figure (a child not yet reaped) runs alone. Only the
|
|
283
|
+
* alone run can see a WAIT, so a suite whose wall is past its wait
|
|
284
|
+
* allowance still gets it */
|
|
259
285
|
export function aloneRunsNeeded(suspects: Timed[], parallelCpu: Record<string, PhaseCpu>, budgetMs: number, load: MachineLoad): { rerun: Timed[]; cleared: Confirmed[] } {
|
|
260
|
-
const idle = load.load1 < load.cores
|
|
261
286
|
const rerun: Timed[] = []
|
|
262
287
|
const cleared: Confirmed[] = []
|
|
263
288
|
for (const t of suspects) {
|
|
264
289
|
const f = parallelCpu[t.file]
|
|
265
|
-
const mayWait =
|
|
290
|
+
const mayWait = t.ms > waitAllowance(budgetMs, load)
|
|
266
291
|
if (f != null && f.cpuMs + f.priorMs <= budgetMs && !mayWait) cleared.push({ file: t.file, ms: f.cpuMs, wallMs: t.ms, cpuMs: f.cpuMs, over: null })
|
|
267
292
|
else rerun.push(t)
|
|
268
293
|
}
|
package/src/suite/suite-cli.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { killTree } from '../core/spawn-timeout.ts'
|
|
|
16
16
|
import { parallelCap } from '../core/parallel.ts'
|
|
17
17
|
import { poolDir, poolOccupancy } from '../core/slots.ts'
|
|
18
18
|
import { keepTimings, seedTimings, timingsHome } from './kept-timings.ts'
|
|
19
|
-
import { aloneRunsNeeded, bunfigTest, readPhaseCpu, SUITE_CPU_ENV, type AloneRun, type Confirmed, confirmOver, crashedWorkers,
|
|
19
|
+
import { aloneRunsNeeded, bunfigTest, readPhaseCpu, SUITE_CPU_ENV, type AloneRun, type Confirmed, confirmOver, crashedWorkers, overWords, DEFAULT_BUDGET_MS, FULL_CEILING_MS, failedSuites, failLines, judgeSuites, normSuitePath, parseSuiteFlags, planPhases, QUICK_CEILING_MS, poolWorkers } from './judge-suites.ts'
|
|
20
20
|
|
|
21
21
|
type Phase = { code: number; signal: string | null; stalled: boolean; out: string; timings: Record<string, number>; cpuMs: number | null }
|
|
22
22
|
|
|
@@ -63,11 +63,6 @@ const CPU_PRELOAD = join(import.meta.dir, 'suite-cpu-preload.ts')
|
|
|
63
63
|
|
|
64
64
|
const secs = (ms: number): string => `${(ms / 1000).toFixed(1)}s`
|
|
65
65
|
|
|
66
|
-
const overWords = (c: Confirmed): string =>
|
|
67
|
-
c.over === 'cpu' ? `${c.file} ${secs(c.ms)} of CPU (${secs(c.wallMs)} wall)`
|
|
68
|
-
: c.over === 'wait' ? `${c.file} ${secs(c.wallMs)} wall on an idle machine, more than ${WAIT_FACTOR}x the budget, with ${secs(c.cpuMs ?? 0)} of CPU: it waits (a timer, a sleep, a poll)`
|
|
69
|
-
: `${c.file} ${secs(c.wallMs)} wall (no CPU figure)`
|
|
70
|
-
|
|
71
66
|
/** the suite FILES a phase names: a flag is not a file, and the repo's own
|
|
72
67
|
* `bun test` arguments are not files this runner put there */
|
|
73
68
|
const phaseFiles = (phase: string[], rawArgs: string[]): string[] => phase.filter((a) => !a.startsWith('-') && !rawArgs.includes(a))
|
|
@@ -117,6 +112,9 @@ export async function suiteCli(argv: string[]): Promise<number> {
|
|
|
117
112
|
const kept = timed && parallel ? timingsHome(process.cwd(), unit) : null
|
|
118
113
|
if (kept != null) seedTimings(kept, join(dir, 'parallel.json'))
|
|
119
114
|
const cpuFile = timed && parallel ? join(dir, 'cpu.tsv') : null
|
|
115
|
+
// the load BEFORE this run: read after it, the one minute average
|
|
116
|
+
// carries this run's own parallel phase, which is no other process
|
|
117
|
+
const load = machineLoad()
|
|
120
118
|
const p1At = Date.now()
|
|
121
119
|
const p1 = await runPhase(plan.parallel, timed ? join(dir, 'parallel.json') : null, ceilingMs, tmp, true, cpuFile)
|
|
122
120
|
const p1Ms = Date.now() - p1At
|
|
@@ -148,7 +146,6 @@ export async function suiteCli(argv: string[]): Promise<number> {
|
|
|
148
146
|
// before it can fail the task. One at a time: four alone runs side by
|
|
149
147
|
// side each burned about 40% more CPU than one alone (8 cores)
|
|
150
148
|
const aloneRuns: Record<string, AloneRun> = {}
|
|
151
|
-
const load = machineLoad()
|
|
152
149
|
const parallelCpu = phaseCpu.cpu
|
|
153
150
|
const { rerun, cleared: clearedInPhase } = aloneRunsNeeded(judged.over, parallelCpu, budget, load)
|
|
154
151
|
const aloneAt = Date.now()
|
|
@@ -224,7 +221,7 @@ export async function suiteCli(argv: string[]): Promise<number> {
|
|
|
224
221
|
...(!vacuous && (failedTests > 0 || (phases.some((p) => p.code !== 0 && p.signal == null) && killed.length === 0))
|
|
225
222
|
? [`FAIL: ${failedTests} test(s) failed, Ran ${ran} tests${slowest}`] : []),
|
|
226
223
|
...(over.length > 0
|
|
227
|
-
? [`FAIL: ${over.length} suite(s) over the ${budget}ms budget: ${over.map(overWords).join(', ')} (run alone, load ${load.load1.toFixed(1)} on ${load.cores} cores). Make each fast, or declare it in ${declaredIn}.slow with what it catches and why it cannot be fast`]
|
|
224
|
+
? [`FAIL: ${over.length} suite(s) over the ${budget}ms budget: ${over.map((c) => overWords(c, load, budget)).join(', ')} (run alone, load ${load.load1.toFixed(1)} on ${load.cores} cores before the run). Make each fast, or declare it in ${declaredIn}.slow with what it catches and why it cannot be fast`]
|
|
228
225
|
: []),
|
|
229
226
|
]
|
|
230
227
|
// the most actionable failure prints LAST, where the gate reads its headline
|
package/src/wiki/lint-md.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { scanMd, type MdDanglingRef, type ScanMdConfig } from 'rvlib-shipkit/src
|
|
|
6
6
|
export type MdLintReport = {
|
|
7
7
|
index: string | null // resolved entrypoint (null = neither index.md nor README.md found)
|
|
8
8
|
files: number
|
|
9
|
+
pages: string[] // every scanned .md file, scan-root relative
|
|
9
10
|
orphans: string[] // .md files with no path from the entrypoint
|
|
10
11
|
dangling: MdDanglingRef[] // refs whose target does not exist in the scan
|
|
11
12
|
ok: boolean
|
|
@@ -17,6 +18,7 @@ export function lintMd(cfg: ScanMdConfig): MdLintReport {
|
|
|
17
18
|
return {
|
|
18
19
|
index: scan.index,
|
|
19
20
|
files: scan.graph.nodes.length,
|
|
21
|
+
pages: scan.graph.nodes.map((n) => n.id),
|
|
20
22
|
orphans,
|
|
21
23
|
dangling: scan.dangling,
|
|
22
24
|
ok: scan.index != null && orphans.length === 0 && scan.dangling.length === 0,
|
package/src/wiki/scan-md.ts
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
// → path.md lazy — resolved from the scan root, then file-relative
|
|
5
5
|
// [text](path.md) lazy — file-relative
|
|
6
6
|
// `path.md` lazy — file-relative, then from the scan root
|
|
7
|
+
// [text](dir/) lazy — a folder: an edge to every page under it
|
|
8
|
+
// `dir/` lazy — the same, from a backtick mention
|
|
9
|
+
// A ref that leaves the scan root but stays under `boundary` is checked on
|
|
10
|
+
// disk: no edge, and dangling only when the target is really missing.
|
|
7
11
|
// Depth = 0-1 BFS from the index (eager edge keeps depth, lazy adds 1);
|
|
8
12
|
// unreachable files stay in the graph with no depth (lint-md.ts turns those,
|
|
9
13
|
// plus dangling refs, into failures — the graph itself is a viewer).
|
|
10
|
-
import { readdirSync, readFileSync, statSync } from 'fs'
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
11
15
|
import { basename, dirname, join, relative, resolve } from 'path'
|
|
12
16
|
import type { MdEdge, MdGraph, MdNode } from 'rvlib-shipkit/src/wiki/md-graph.ts'
|
|
13
17
|
|
|
@@ -15,6 +19,7 @@ export type ScanMdConfig = {
|
|
|
15
19
|
root: string // absolute dir to scan
|
|
16
20
|
index?: string // root-relative entry the BFS starts from (default: index.md AND README.md, both)
|
|
17
21
|
skipDirs?: string[] // dir names to skip on top of node_modules/.git/dist/tmp
|
|
22
|
+
boundary?: string // absolute dir enclosing root (a repo): refs leaving root but staying here resolve on disk
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export type MdDanglingRef = { from: string; token: string } // an @/→/[](…) ref whose target is not in the scan
|
|
@@ -46,13 +51,33 @@ export function scanMd(cfg: ScanMdConfig): MdScan {
|
|
|
46
51
|
// writers inside a scanned dir often name paths from the REPO root
|
|
47
52
|
// ("agent/architecture.md" while scanning agent/) — accept that form too
|
|
48
53
|
const rootBase = `${basename(cfg.root)}/`
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
if (token.startsWith(rootBase))
|
|
52
|
-
|
|
54
|
+
const cands = (fromRel: string, token: string): string[] => {
|
|
55
|
+
const out = [resolve(join(cfg.root, dirname(fromRel)), token), resolve(cfg.root, token)]
|
|
56
|
+
if (token.startsWith(rootBase)) out.push(resolve(cfg.root, token.slice(rootBase.length)))
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
// pages a ref reaches: one file, every page under a folder (never the
|
|
60
|
+
// root itself), or none. `onDisk` = the target exists in the boundary
|
|
61
|
+
// outside the scan, which is resolved but carries no edge.
|
|
62
|
+
type Hit = { to: string[]; onDisk: boolean }
|
|
63
|
+
const resolveRef = (fromRel: string, token: string): Hit | null => {
|
|
64
|
+
const folder = token.endsWith('/')
|
|
65
|
+
for (const cand of cands(fromRel, token)) {
|
|
53
66
|
const rel = relative(cfg.root, cand)
|
|
54
|
-
if (
|
|
67
|
+
if (rel.startsWith('..')) continue
|
|
68
|
+
if (!folder && inGraph.has(rel)) return { to: [rel], onDisk: false }
|
|
69
|
+
if (folder && rel === '') return { to: [], onDisk: false }
|
|
70
|
+
if (folder) {
|
|
71
|
+
const under = files.filter((f) => f.startsWith(`${rel}/`))
|
|
72
|
+
if (under.length > 0) return { to: under, onDisk: false }
|
|
73
|
+
if (existsSync(cand)) return { to: [], onDisk: true }
|
|
74
|
+
}
|
|
55
75
|
}
|
|
76
|
+
if (cfg.boundary == null) return null
|
|
77
|
+
const cand = resolve(join(cfg.root, dirname(fromRel)), token)
|
|
78
|
+
const inRoot = !relative(cfg.root, cand).startsWith('..')
|
|
79
|
+
const inBoundary = !relative(cfg.boundary, cand).startsWith('..')
|
|
80
|
+
if (!inRoot && inBoundary && existsSync(cand)) return { to: [], onDisk: true }
|
|
56
81
|
return null
|
|
57
82
|
}
|
|
58
83
|
const edgesOf = (rel: string): MdEdge[] => {
|
|
@@ -64,14 +89,16 @@ export function scanMd(cfg: ScanMdConfig): MdScan {
|
|
|
64
89
|
// strict: @/→/[](…) are deliberate wiki refs — a miss is a dangling ref.
|
|
65
90
|
// Backtick spans mention paths casually (other repos, deleted examples) — resolve or ignore, never fail.
|
|
66
91
|
const push = (token: string, eager: boolean, strict: boolean) => {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
92
|
+
const hit = resolveRef(rel, token)
|
|
93
|
+
if (hit == null) { if (strict) dangling.push({ from: rel, token }); return }
|
|
94
|
+
for (const to of hit.to) if (to !== rel) out.push({ from: rel, to, eager })
|
|
70
95
|
}
|
|
71
|
-
for (const m of prose.matchAll(/(?:^|[\s(])@([\w][\w./-]*\.md)/gm)) push(m[1]!, true, true)
|
|
96
|
+
for (const m of prose.matchAll(/(?:^|[\s(])@((?:\.{1,2}\/)*[\w][\w./-]*\.md)/gm)) push(m[1]!, true, true)
|
|
72
97
|
for (const m of prose.matchAll(/→\s*([\w][\w./-]*\.md)\b/g)) push(m[1]!, false, true)
|
|
73
98
|
for (const m of prose.matchAll(/\]\(([^)\s:]+\.md)\)/g)) push(m[1]!, false, true)
|
|
99
|
+
for (const m of prose.matchAll(/\]\(([^)\s:]*\/)\)/g)) push(m[1]!, false, true)
|
|
74
100
|
for (const s of spans) for (const m of s.matchAll(/[\w][\w./-]*\.md/g)) push(m[0], false, false)
|
|
101
|
+
for (const s of spans) for (const m of s.matchAll(/(?:^|\s)([\w.][\w./-]*\/)(?=$|[\s<*])/g)) push(m[1]!, false, false)
|
|
75
102
|
return out
|
|
76
103
|
}
|
|
77
104
|
const dangling: MdDanglingRef[] = []
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// the repo's declared processes (serve.processes) as the hub supervises them,
|
|
2
|
-
// polled while its repo is the one shown. A repo that declares none never
|
|
3
|
-
// polls again after the first answer.
|
|
4
|
-
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
5
|
-
import type { ProcessView } from '../repo-processes.ts'
|
|
6
|
-
import type { RepoSt } from './repo-st.ts'
|
|
7
|
-
|
|
8
|
-
export const PROCESSES_POLL_MS = 2_000
|
|
9
|
-
|
|
10
|
-
export class ProcessesSvc {
|
|
11
|
-
list: ProcessView[] = []
|
|
12
|
-
error = ''
|
|
13
|
-
private timer: ReturnType<typeof setTimeout> | null = null
|
|
14
|
-
/** bumped by stop, so a chain that left before it dies at its next await */
|
|
15
|
-
private gen = 0
|
|
16
|
-
private polling = false
|
|
17
|
-
|
|
18
|
-
constructor(private st: RepoSt) {
|
|
19
|
-
makeAutoObservable<this, 'st' | 'timer' | 'gen' | 'polling'>(this, { st: false, timer: false, gen: false, polling: false, list: observableRef })
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
start(): void {
|
|
23
|
-
if (this.polling) return
|
|
24
|
-
this.polling = true
|
|
25
|
-
const gen = ++this.gen
|
|
26
|
-
const tick = async (): Promise<void> => {
|
|
27
|
-
await this.load()
|
|
28
|
-
if (gen === this.gen && this.list.length > 0) this.timer = setTimeout(() => void tick(), PROCESSES_POLL_MS)
|
|
29
|
-
}
|
|
30
|
-
void tick()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
stop(): void {
|
|
34
|
-
this.polling = false
|
|
35
|
-
this.gen++
|
|
36
|
-
if (this.timer != null) clearTimeout(this.timer)
|
|
37
|
-
this.timer = null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async load(): Promise<void> {
|
|
41
|
-
try {
|
|
42
|
-
const r = await fetch(this.st.url('/api/processes.json'))
|
|
43
|
-
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
44
|
-
const j = (await r.json()) as { processes: ProcessView[] }
|
|
45
|
-
runInAction(() => {
|
|
46
|
-
this.list = j.processes
|
|
47
|
-
this.error = ''
|
|
48
|
-
})
|
|
49
|
-
} catch (e) {
|
|
50
|
-
runInAction(() => { this.error = `processes: ${(e as Error).message}` })
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async act(id: string, act: 'start' | 'stop' | 'restart'): Promise<void> {
|
|
55
|
-
try {
|
|
56
|
-
const r = await fetch(this.st.url('/api/processes/act'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, act }) })
|
|
57
|
-
if (!r.ok) throw new Error(await r.text())
|
|
58
|
-
await this.load()
|
|
59
|
-
} catch (e) {
|
|
60
|
-
runInAction(() => { this.error = `${act} ${id}: ${(e as Error).message}` })
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// A repo page's sections: the pages reached for most, one line each at the top
|
|
2
|
-
// of the left pane, over the tree, drawn the way the project list draws its
|
|
3
|
-
// lines (an icon, a label, a count where it says something, its ⌥ shortcut at
|
|
4
|
-
// the end, keys.ts), the current one lit. A page this repo does not have is
|
|
5
|
-
// not a line. The lines come from what the page already holds (the kept
|
|
6
|
-
// screen on a reload), so none arrives late and moves the tree.
|
|
7
|
-
import { observer } from 'mobx-react-lite'
|
|
8
|
-
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
9
|
-
import { Icon, ICON } from './icons.tsx'
|
|
10
|
-
import { sectionKeyLabel, type SectionId } from './keys.ts'
|
|
11
|
-
import type { RepoSt } from './repo-st.ts'
|
|
12
|
-
import { openWikis, WIKIS } from './wikis-view.tsx'
|
|
13
|
-
|
|
14
|
-
export type RepoSection = {
|
|
15
|
-
id: SectionId
|
|
16
|
-
label: string
|
|
17
|
-
title: string
|
|
18
|
-
/** null when a number would say nothing */
|
|
19
|
-
count: number | null
|
|
20
|
-
tone: 'red' | 'warn' | null
|
|
21
|
-
on: boolean
|
|
22
|
-
go: () => void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const counted = (n: number, tone: 'red' | 'warn'): Pick<RepoSection, 'count' | 'tone'> => (n === 0 ? { count: null, tone: null } : { count: n, tone })
|
|
26
|
-
|
|
27
|
-
export function repoSections(app: RepoSt): RepoSection[] {
|
|
28
|
-
const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
|
|
29
|
-
const sel = app.sel
|
|
30
|
-
const onCol = (col: string): boolean => (sel?.t === 'col' && sel.id === col) || (sel?.t === 'item' && sel.col === col)
|
|
31
|
-
const res = app.cols.resources
|
|
32
|
-
const reds = (col: string): number => res.filter((r) => r.collection === col && r.state === 'red').length
|
|
33
|
-
const none = { count: null, tone: null }
|
|
34
|
-
const out: RepoSection[] = []
|
|
35
|
-
if (has('checks')) out.push({ id: 'checks', label: 'checks', title: 'every check the gate and the health pass run, one page each', ...counted(reds('checks'), 'red'), on: onCol('checks'), go: () => app.showCol('checks') })
|
|
36
|
-
// a page, never a resource: every hub keeps a run record
|
|
37
|
-
out.push({ id: 'runs', label: 'runs', title: 'every run on record, the slowest checks and the longest runs', ...none, on: onCol('runs'), go: () => app.cols.select('runs', RUNS_ID) })
|
|
38
|
-
if (has('asks')) {
|
|
39
|
-
const open = res.filter((r) => r.collection === 'asks' && r.type === 'ask' && r.slot !== 'done').length
|
|
40
|
-
out.push({ id: 'asks', label: 'asks', title: 'what waits on a human', ...counted(open, 'warn'), on: onCol('asks'), go: () => app.showCol('asks') })
|
|
41
|
-
}
|
|
42
|
-
if (has('tests')) out.push({ id: 'tests', label: 'tests', title: 'every test file, slowest first, then each test check', ...counted(reds('tests'), 'red'), on: sel?.t === 'tests' || onCol('tests'), go: () => app.showTests(null) })
|
|
43
|
-
const firstCat = app.media.assets[0]?.cat ?? app.cols.cachedCat ?? undefined
|
|
44
|
-
if (firstCat != null) out.push({ id: 'media', label: 'media', title: 'the assets, in the atelier', ...none, on: sel?.t === 'asset' || sel?.t === 'cat', go: () => app.showCat(firstCat) })
|
|
45
|
-
if (has(WIKIS)) out.push({ id: 'wikis', label: 'wikis', title: 'every wiki this repo mounts, as a link graph', ...none, on: onCol(WIKIS), go: () => openWikis(app) })
|
|
46
|
-
if (app.home.machine != null) out.push({ id: 'admin', label: 'admin', title: 'reload the hub, keep the repo active, archive it', ...none, on: sel?.t === 'admin', go: () => app.showAdmin() })
|
|
47
|
-
return out
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const Sections = observer(({ app }: { app: RepoSt }) => (
|
|
51
|
-
<nav className="rsecs" aria-label="sections">
|
|
52
|
-
{repoSections(app).map((s) => (
|
|
53
|
-
<button key={s.id} className={`rsec${s.on ? ' cur' : ''}`} aria-current={s.on ? 'page' : undefined} title={`${s.title} (${sectionKeyLabel(s.id)})`} onClick={s.go}>
|
|
54
|
-
<Icon d={ICON[s.id]} />
|
|
55
|
-
<span className="rsecl">{s.label}</span>
|
|
56
|
-
{s.count == null ? null : <b className={`rsecn n${s.tone}`}>{s.count}</b>}
|
|
57
|
-
<kbd className="pjkey">{sectionKeyLabel(s.id)}</kbd>
|
|
58
|
-
</button>
|
|
59
|
-
))}
|
|
60
|
-
</nav>
|
|
61
|
-
))
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// the long running processes a repo declares in `serve.processes` (a dev
|
|
2
|
-
// server, a watcher), supervised by its hub with the same supervisor the home
|
|
3
|
-
// uses for resources. Each runs detached with a pid record in the machine db
|
|
4
|
-
// (named by its path under .shipkit/tmp, where an older copy kept a pid file),
|
|
5
|
-
// so a hub restart in place adopts it, and only a real stop of the hub stops it.
|
|
6
|
-
import { join, resolve } from 'path'
|
|
7
|
-
import { Proc, realDeps, tailLines, type ProcView, type SuperviseDeps } from '../home/supervise.ts'
|
|
8
|
-
import type { ProcessDecl } from '../repo-config.ts'
|
|
9
|
-
import { LAYOUT } from '../core/layout.ts'
|
|
10
|
-
|
|
11
|
-
export type ProcessView = ProcView & { url: string | null; auto: boolean; cmd: string; tail: string[] }
|
|
12
|
-
|
|
13
|
-
export const processDir = (root: string): string => join(root, LAYOUT.tmp, 'processes')
|
|
14
|
-
|
|
15
|
-
const probeUrl = async (url: string): Promise<boolean> => {
|
|
16
|
-
try {
|
|
17
|
-
// any answer means it serves: a dev server may 404 its root
|
|
18
|
-
await fetch(url, { signal: AbortSignal.timeout(1_500) })
|
|
19
|
-
return true
|
|
20
|
-
} catch {
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class RepoProcesses {
|
|
26
|
-
private procs: { decl: ProcessDecl; proc: Proc }[]
|
|
27
|
-
|
|
28
|
-
constructor(root: string, decls: ProcessDecl[], o: { deps?: SuperviseDeps; probe?: (url: string) => Promise<boolean> } = {}) {
|
|
29
|
-
const dir = processDir(root)
|
|
30
|
-
this.procs = decls.map((decl) => ({
|
|
31
|
-
decl,
|
|
32
|
-
proc: new Proc({
|
|
33
|
-
id: decl.id,
|
|
34
|
-
cmd: decl.cmd,
|
|
35
|
-
cwd: resolve(root, decl.cwd ?? '.'),
|
|
36
|
-
log: join(dir, `${decl.id}.log`),
|
|
37
|
-
pidKey: join(dir, `${decl.id}.pid`),
|
|
38
|
-
probe: decl.url == null ? undefined : () => (o.probe ?? probeUrl)(decl.url!),
|
|
39
|
-
}, o.deps ?? realDeps),
|
|
40
|
-
}))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** the auto ones start with the hub; an adopted one is left as it runs */
|
|
44
|
-
async startAuto(): Promise<void> {
|
|
45
|
-
await Promise.all(this.procs.filter((p) => p.decl.auto === true && p.proc.pid == null).map((p) => p.proc.ensure()))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
act(id: string, act: 'start' | 'stop' | 'restart'): ProcessView | null {
|
|
49
|
-
const p = this.procs.find((x) => x.decl.id === id)
|
|
50
|
-
if (p == null) return null
|
|
51
|
-
if (act === 'stop' || act === 'restart') p.proc.stop()
|
|
52
|
-
if (act === 'start') void p.proc.ensure()
|
|
53
|
-
if (act === 'restart') this.restartWhenGone(p.proc)
|
|
54
|
-
return this.view(p)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private restartWhenGone(proc: Proc): void {
|
|
58
|
-
const wait = async (): Promise<void> => {
|
|
59
|
-
for (let i = 0; i < 40 && proc.status === 'stopping'; i++) {
|
|
60
|
-
await Bun.sleep(250)
|
|
61
|
-
await proc.tick()
|
|
62
|
-
}
|
|
63
|
-
proc.start()
|
|
64
|
-
}
|
|
65
|
-
void wait()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async tick(): Promise<void> {
|
|
69
|
-
await Promise.all(this.procs.map((p) => p.proc.tick()))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** a real stop of the hub: every process it started stops with it */
|
|
73
|
-
stopAll(): void {
|
|
74
|
-
for (const p of this.procs) if (p.proc.pid != null) p.proc.stop()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private view(p: { decl: ProcessDecl; proc: Proc }): ProcessView {
|
|
78
|
-
const v = p.proc.view()
|
|
79
|
-
return { ...v, url: p.decl.url ?? null, auto: p.decl.auto === true, cmd: p.decl.cmd.join(' '), tail: v.status === 'up' ? [] : tailLines(p.proc.spec.log, 20) }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
views(): ProcessView[] {
|
|
83
|
-
return this.procs.map((p) => this.view(p))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
get size(): number {
|
|
87
|
-
return this.procs.length
|
|
88
|
-
}
|
|
89
|
-
}
|