rvlib-shipkit 0.38.0 → 1.0.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 +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// reflections collection — this repo's own `.shipkit/reflections/` queue: the
|
|
2
|
+
// friction it logged against the wiki it follows, one row per file still
|
|
3
|
+
// waiting to be folded into its owning page and deleted. Orange, always: the
|
|
4
|
+
// file waits on a human process and nothing here is broken, so it reaches
|
|
5
|
+
// the needs feed and never the red pings. The seed README is not a row.
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { listReflections, REFLECTIONS_DIR } from '../reflections/reflections-queue.ts'
|
|
8
|
+
import type { Collection, Instance } from './collection-types.ts'
|
|
9
|
+
import { mdBody } from './md-body.ts'
|
|
10
|
+
|
|
11
|
+
export function reflectionsCollection(root: string): Collection {
|
|
12
|
+
return {
|
|
13
|
+
id: 'reflections',
|
|
14
|
+
title: 'reflections',
|
|
15
|
+
media: 'text',
|
|
16
|
+
list: async () =>
|
|
17
|
+
listReflections(root).map((r): Instance => {
|
|
18
|
+
const path = `${REFLECTIONS_DIR}/${r.file}`
|
|
19
|
+
const shape = r.unreadable
|
|
20
|
+
? 'UNREADABLE'
|
|
21
|
+
: !r.isRequest
|
|
22
|
+
? 'note'
|
|
23
|
+
: r.missing.length === 0
|
|
24
|
+
? 'change request'
|
|
25
|
+
: `change request, MISSING: ${r.missing.join(', ')}`
|
|
26
|
+
return {
|
|
27
|
+
id: `reflection:${r.file}`,
|
|
28
|
+
text: `${path} — ${shape}: ${r.title}`,
|
|
29
|
+
// the tree row is the line whoever logged it wrote, and
|
|
30
|
+
// the shape sits in the slot beside it
|
|
31
|
+
label: r.title,
|
|
32
|
+
slot: r.unreadable ? 'unreadable' : r.isRequest ? 'request' : 'note',
|
|
33
|
+
type: 'reflection',
|
|
34
|
+
path,
|
|
35
|
+
file: path,
|
|
36
|
+
severity: 'yellow',
|
|
37
|
+
md: mdBody(join(root, path)),
|
|
38
|
+
}
|
|
39
|
+
}),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// WHAT COLOUR A ROW IS, one pure rule per kind, in ONE file. The answer used
|
|
2
|
+
// to be spread over the providers that happened to build the instance, so a
|
|
3
|
+
// kind could be red on the tree and yellow in the feed, and a kind nobody
|
|
4
|
+
// wrote a rule for came out white.
|
|
5
|
+
//
|
|
6
|
+
// Two axes, and they are not the same axis. `state` is the colour on the
|
|
7
|
+
// tree. `severity` is what reaches `shipkit needs` and the once ever ping
|
|
8
|
+
// (red pings, yellow never does). A rule that wants colour WITHOUT a feed
|
|
9
|
+
// line returns a bare `state`, which is how "nothing measured this" says so
|
|
10
|
+
// on screen and stays out of a queue of things asking for his hands.
|
|
11
|
+
import type { Instance } from './collection-types.ts'
|
|
12
|
+
|
|
13
|
+
export type StateRule = Pick<Instance, 'state' | 'severity'>
|
|
14
|
+
|
|
15
|
+
/** what a hub row needs to know about one asset */
|
|
16
|
+
export type AssetFacts = { manual: boolean; rolls: number; picked: boolean; stale: boolean }
|
|
17
|
+
|
|
18
|
+
/** THE asset rule: a pick is a decision made, so it reads green. Rolls with
|
|
19
|
+
* nothing picked is taste pending, a stale pick is the same question asked
|
|
20
|
+
* again, a manual asset with nothing uploaded is broken, and an asset nobody
|
|
21
|
+
* has generated yet is plain */
|
|
22
|
+
export const assetState = (f: AssetFacts): StateRule => {
|
|
23
|
+
if (f.manual && f.rolls === 0) return { severity: 'red' }
|
|
24
|
+
if (f.rolls === 0) return {}
|
|
25
|
+
if (!f.picked || f.stale) return { severity: 'yellow' }
|
|
26
|
+
return { state: 'ok' }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** what the run record says about ONE secret, through the check that
|
|
30
|
+
* probes it (`creds[].probedBy`):
|
|
31
|
+
* pass its last run passed, inside its ttl
|
|
32
|
+
* fail its last run failed
|
|
33
|
+
* warn its last run warned, or it names a check this repo lacks
|
|
34
|
+
* stale it passed, and the pass is older than the check's ttl
|
|
35
|
+
* unmeasured it never ran here, was skipped, or said it did not measure
|
|
36
|
+
* none no check probes this secret */
|
|
37
|
+
export type SecretProbe = 'pass' | 'fail' | 'warn' | 'stale' | 'unmeasured' | 'none'
|
|
38
|
+
|
|
39
|
+
/** THE secret rule. Being there is half the answer and working is the other
|
|
40
|
+
* half, so present plus a probe that passed is the only green. A value that
|
|
41
|
+
* is there and fails its probe is red on the tree, and its feed line stays
|
|
42
|
+
* yellow: the probing check does not block, and its own row already carries
|
|
43
|
+
* the same failure. A pass past its ttl, or a probe that never measured, is
|
|
44
|
+
* plain: nothing is known to be wrong. A secret no check probes is dim,
|
|
45
|
+
* because there is no verdict to wait for. Missing while a page requires it
|
|
46
|
+
* is the broken case, whatever any probe said */
|
|
47
|
+
export const secretState = (f: { missing: boolean; probe: SecretProbe }): StateRule => {
|
|
48
|
+
if (f.missing) return { severity: 'red' }
|
|
49
|
+
if (f.probe === 'pass') return { state: 'ok' }
|
|
50
|
+
if (f.probe === 'fail') return { state: 'red', severity: 'yellow' }
|
|
51
|
+
if (f.probe === 'warn') return { severity: 'yellow' }
|
|
52
|
+
if (f.probe === 'none') return { state: 'quiet' }
|
|
53
|
+
return { state: 'idle' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** THE graph rule: a graph is valid or it is not, and lint says which. Clean
|
|
57
|
+
* is a measured pass, anything else is red on the tree. The feed line is
|
|
58
|
+
* yellow, because a dangling link is work and never a reason to ping. A graph
|
|
59
|
+
* whose lint belongs to another repo is never double judged here, so it
|
|
60
|
+
* claims no colour at all */
|
|
61
|
+
export const graphState = (f: { lint: boolean; issues: number }): StateRule => {
|
|
62
|
+
if (!f.lint) return {}
|
|
63
|
+
return f.issues > 0 ? { state: 'red', severity: 'yellow' } : { state: 'ok' }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** a graph judged by the repo's own checks: red when one of them last
|
|
67
|
+
* failed, green when every one has a verdict and none failed. Each check
|
|
68
|
+
* carries its own feed line already, so the graph adds none. Nothing
|
|
69
|
+
* measured yet claims no colour */
|
|
70
|
+
export const graphChecksState = (last: (string | null)[]): StateRule => {
|
|
71
|
+
if (last.some((s) => s === 'fail')) return { state: 'red' }
|
|
72
|
+
return last.length > 0 && last.every((s) => s != null) ? { state: 'ok' } : {}
|
|
73
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// WHERE A TEST CHECK'S TIME GOES: the page behind a `test` / `test:<unit>`
|
|
2
|
+
// group of the tree, and the `tests` page above them. Pure over what the
|
|
3
|
+
// caller read: the test files as the tests collection shaped them, the run
|
|
4
|
+
// record (the check's own wall time) and the suite runner's own record (its
|
|
5
|
+
// phases, its workers).
|
|
6
|
+
//
|
|
7
|
+
// Nothing here decides what is red: a file is failing when its TestFacts
|
|
8
|
+
// carry the testFilesToFix verdict (ledger/run-timeline.ts), the same one the
|
|
9
|
+
// tree and the runs page tab read.
|
|
10
|
+
import type { RunRow } from '../ledger/run-log.ts'
|
|
11
|
+
import type { SuiteRunRow } from '../ledger/suite-log.ts'
|
|
12
|
+
import { testId, type TestFacts, type TestRun } from './collection-types.ts'
|
|
13
|
+
|
|
14
|
+
/** past this share of the parallel phase, one file IS the wall */
|
|
15
|
+
export const TAIL_SHARE = 0.6
|
|
16
|
+
/** under this share of the wall, the file phases are not where it goes */
|
|
17
|
+
export const FILES_SHARE = 0.5
|
|
18
|
+
/** past this share of the workers, the files keep the pool busy */
|
|
19
|
+
export const BUSY_SHARE = 0.6
|
|
20
|
+
/** how far after the runner wrote its rows the check that hosted it may end
|
|
21
|
+
* and still be that run */
|
|
22
|
+
const HOST_WINDOW_MS = 120_000
|
|
23
|
+
/** a trend arrow needs a real move: this share of the average, and this much */
|
|
24
|
+
const TREND_SHARE = 0.2
|
|
25
|
+
const TREND_MIN_MS = 50
|
|
26
|
+
|
|
27
|
+
export type Trend = 'up' | 'down' | 'flat'
|
|
28
|
+
|
|
29
|
+
export type FileTime = {
|
|
30
|
+
file: string
|
|
31
|
+
/** the resource id its page opens by */
|
|
32
|
+
id: string
|
|
33
|
+
inUnit: string
|
|
34
|
+
/** the last recorded duration in the shared run, null when it never ran
|
|
35
|
+
* here */
|
|
36
|
+
ms: number | null
|
|
37
|
+
/** the time the budget judged, when an alone rerun replaced it (its own
|
|
38
|
+
* CPU, or its wall alone). null when it is `ms` */
|
|
39
|
+
judgedMs: number | null
|
|
40
|
+
at: number | null
|
|
41
|
+
/** CPU it burned alone, when a process ran it alone */
|
|
42
|
+
cpuMs: number | null
|
|
43
|
+
/** it ran in the check's last run, so it has a share of it */
|
|
44
|
+
inLastRun: boolean
|
|
45
|
+
/** of the sum of file times of that last run, 0..1 */
|
|
46
|
+
share: number | null
|
|
47
|
+
budgetMs: number
|
|
48
|
+
/** a quick run over its budget, on a file the quick tier holds to one */
|
|
49
|
+
over: boolean
|
|
50
|
+
/** mean of its earlier recorded runs in the same tier */
|
|
51
|
+
avgMs: number | null
|
|
52
|
+
trend: Trend | null
|
|
53
|
+
tier: 'quick' | 'full'
|
|
54
|
+
phase: string | null
|
|
55
|
+
status: 'pass' | 'fail' | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type FailingFile = {
|
|
59
|
+
file: string
|
|
60
|
+
id: string
|
|
61
|
+
inUnit: string
|
|
62
|
+
check: string | null
|
|
63
|
+
at: number | null
|
|
64
|
+
ms: number | null
|
|
65
|
+
failLine: string | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type Shape = 'tail' | 'volume' | 'overhead'
|
|
69
|
+
|
|
70
|
+
export type Phases = {
|
|
71
|
+
/** the runner before its first phase: probing bun, the bunfig, the pool */
|
|
72
|
+
setupMs: number
|
|
73
|
+
parallelMs: number
|
|
74
|
+
parallelCpuMs: number | null
|
|
75
|
+
/** 0 = one file at a time */
|
|
76
|
+
workers: number
|
|
77
|
+
serialMs: number | null
|
|
78
|
+
serialFiles: number
|
|
79
|
+
/** the over budget files, run again alone before they can fail */
|
|
80
|
+
aloneMs: number
|
|
81
|
+
aloneFiles: number
|
|
82
|
+
/** the runner, first line to the record write */
|
|
83
|
+
runnerMs: number
|
|
84
|
+
/** the check's wall minus the runner's: the process that hosts it, its
|
|
85
|
+
* start and its module load. null when the check's run is not on record */
|
|
86
|
+
hostMs: number | null
|
|
87
|
+
load1: number | null
|
|
88
|
+
cores: number | null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type LastRun = {
|
|
92
|
+
at: number
|
|
93
|
+
files: number
|
|
94
|
+
/** the sum of every file's time in that run */
|
|
95
|
+
sumMs: number
|
|
96
|
+
/** the check's own wall time for that run, else the runner's */
|
|
97
|
+
wallMs: number | null
|
|
98
|
+
wallFrom: 'check' | 'runner' | null
|
|
99
|
+
workers: number | null
|
|
100
|
+
/** sum / wall: how many files ran at once on average */
|
|
101
|
+
ratio: number | null
|
|
102
|
+
shape: Shape | null
|
|
103
|
+
/** the verdict, in one sentence */
|
|
104
|
+
shapeWords: string
|
|
105
|
+
top: { file: string; id: string; inUnit: string; ms: number; share: number }[]
|
|
106
|
+
phases: Phases | null
|
|
107
|
+
/** the whole summary as one line, for a terminal or an agent */
|
|
108
|
+
line: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type TestCheckView = {
|
|
112
|
+
checkId: string
|
|
113
|
+
last: LastRun | null
|
|
114
|
+
failing: FailingFile[]
|
|
115
|
+
/** every file the check runs, slowest first, the never ran ones last */
|
|
116
|
+
files: FileTime[]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type TestCheckRow = {
|
|
120
|
+
checkId: string
|
|
121
|
+
files: number
|
|
122
|
+
failing: number
|
|
123
|
+
at: number | null
|
|
124
|
+
wallMs: number | null
|
|
125
|
+
sumMs: number | null
|
|
126
|
+
slowest: { file: string; id: string; inUnit: string; ms: number } | null
|
|
127
|
+
shape: Shape | null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type TestsRootView = {
|
|
131
|
+
/** slowest wall first, the never ran ones last */
|
|
132
|
+
checks: TestCheckRow[]
|
|
133
|
+
failing: FailingFile[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type TestTimeInput = {
|
|
137
|
+
facts: TestFacts[]
|
|
138
|
+
runs: RunRow[]
|
|
139
|
+
suiteRuns: SuiteRunRow[]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const ms0 = (n: number): string => `${Math.round(n)}ms`
|
|
143
|
+
const pct = (n: number): string => `${Math.round(n * 100)}%`
|
|
144
|
+
|
|
145
|
+
/** what a file cost the shared run: its phase wall, where the record kept it */
|
|
146
|
+
const costOf = (r: TestRun): number => r.phaseMs ?? r.ms
|
|
147
|
+
|
|
148
|
+
const ranIn = (checkId: string) => (f: TestFacts): boolean => f.checkId === checkId && f.standing === 'runs'
|
|
149
|
+
|
|
150
|
+
function trendOf(f: TestFacts): { avgMs: number | null; trend: Trend | null } {
|
|
151
|
+
const [last, ...rest] = f.runs
|
|
152
|
+
if (last == null) return { avgMs: null, trend: null }
|
|
153
|
+
const same = rest.filter((r) => r.tier === last.tier)
|
|
154
|
+
if (same.length === 0) return { avgMs: null, trend: null }
|
|
155
|
+
const avgMs = same.reduce((n, r) => n + costOf(r), 0) / same.length
|
|
156
|
+
const d = costOf(last) - avgMs
|
|
157
|
+
const moved = Math.abs(d) > avgMs * TREND_SHARE && Math.abs(d) > TREND_MIN_MS
|
|
158
|
+
return { avgMs: Math.round(avgMs), trend: !moved ? 'flat' : d > 0 ? 'up' : 'down' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function failingOf(facts: TestFacts[]): FailingFile[] {
|
|
162
|
+
return facts
|
|
163
|
+
.filter((f) => f.toFix?.red === true)
|
|
164
|
+
.map((f) => ({ file: f.file, id: testId(f.file), inUnit: f.inUnit, check: f.checkId, at: f.runs[0]?.at ?? null, ms: f.runs[0]?.ms ?? null, failLine: f.runs[0]?.failLine ?? null }))
|
|
165
|
+
.sort((a, b) => (a.check ?? '').localeCompare(b.check ?? '') || a.file.localeCompare(b.file))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** the check's own measured run that hosted the runner run ending at `at` */
|
|
169
|
+
function hostRun(runs: RunRow[], checkId: string, at: number): RunRow | null {
|
|
170
|
+
let best: RunRow | null = null
|
|
171
|
+
for (const r of runs) {
|
|
172
|
+
if (r.id !== checkId || r.replayed || r.status === 'skip') continue
|
|
173
|
+
if (r.at < at || r.at - at > HOST_WINDOW_MS) continue
|
|
174
|
+
if (best == null || r.at < best.at) best = r
|
|
175
|
+
}
|
|
176
|
+
return best
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** the verdict, and the one sentence that says it. In order: the file
|
|
180
|
+
* phases are not where the wall goes (then the biggest other part is named),
|
|
181
|
+
* one file sets the phase, the files keep the workers busy, or the workers
|
|
182
|
+
* mostly wait */
|
|
183
|
+
function verdict(o: { top: { inUnit: string; ms: number }; sumMs: number; wallMs: number | null; workers: number | null; sr: SuiteRunRow | null; hostMs: number | null }): { shape: Shape; words: string } | null {
|
|
184
|
+
const files = o.sr == null ? null : o.sr.parallelMs + (o.sr.serialMs ?? 0)
|
|
185
|
+
if (o.sr != null && files != null && o.wallMs != null && files < o.wallMs * FILES_SHARE) {
|
|
186
|
+
const parts: [string, number][] = [
|
|
187
|
+
[`running ${o.sr.aloneFiles} over budget files again alone takes ${ms0(o.sr.aloneMs)}`, o.sr.aloneMs],
|
|
188
|
+
[`the runner's setup takes ${ms0(o.sr.setupMs)}`, o.sr.setupMs],
|
|
189
|
+
[`the process around the runner takes ${ms0(o.hostMs ?? 0)}`, o.hostMs ?? 0],
|
|
190
|
+
[`the runner's own bookkeeping takes ${ms0(o.sr.wallMs - o.sr.setupMs - files - o.sr.aloneMs)}`, o.sr.wallMs - o.sr.setupMs - files - o.sr.aloneMs],
|
|
191
|
+
]
|
|
192
|
+
const [what] = parts.sort((a, b) => b[1] - a[1])[0]!
|
|
193
|
+
return { shape: 'overhead', words: `outside the files: they run in ${ms0(files)} of the ${ms0(o.wallMs)} wall, and ${what}` }
|
|
194
|
+
}
|
|
195
|
+
const span = o.sr?.parallelMs ?? o.wallMs
|
|
196
|
+
if (span == null || span <= 0) return null
|
|
197
|
+
if (o.top.ms >= span * TAIL_SHARE) return { shape: 'tail', words: `a long tail: ${o.top.inUnit} alone takes most of the wall, so faster others change nothing` }
|
|
198
|
+
const workers = Math.max(1, o.workers ?? 1)
|
|
199
|
+
if (o.sumMs / span >= workers * BUSY_SHARE)
|
|
200
|
+
return { shape: 'volume', words: `volume: the files keep ${o.workers == null || o.workers === 0 ? 'the runner' : `all ${o.workers} workers`} busy, so the wall is the sum, cut the sum` }
|
|
201
|
+
return { shape: 'overhead', words: `the ${workers} workers mostly wait: the files are few or short, and the wall is fixed cost around them` }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function shapeTestCheck(checkId: string, o: TestTimeInput): TestCheckView {
|
|
205
|
+
const mine = o.facts.filter(ranIn(checkId))
|
|
206
|
+
const lastAt = mine.reduce<number | null>((m, f) => (f.runs[0] == null ? m : m == null ? f.runs[0].at : Math.max(m, f.runs[0].at)), null)
|
|
207
|
+
const inLast = lastAt == null ? [] : mine.filter((f) => f.runs[0]?.at === lastAt)
|
|
208
|
+
const sumMs = inLast.reduce((n, f) => n + costOf(f.runs[0]!), 0)
|
|
209
|
+
|
|
210
|
+
const files: FileTime[] = mine.map((f) => {
|
|
211
|
+
const r = f.runs[0]
|
|
212
|
+
const inLastRun = r != null && r.at === lastAt
|
|
213
|
+
return {
|
|
214
|
+
file: f.file,
|
|
215
|
+
id: testId(f.file),
|
|
216
|
+
inUnit: f.inUnit,
|
|
217
|
+
ms: r == null ? null : costOf(r),
|
|
218
|
+
judgedMs: r == null || r.phaseMs == null || r.phaseMs === r.ms ? null : r.ms,
|
|
219
|
+
at: r?.at ?? null,
|
|
220
|
+
cpuMs: r?.cpuMs ?? null,
|
|
221
|
+
inLastRun,
|
|
222
|
+
share: inLastRun && sumMs > 0 ? costOf(r) / sumMs : null,
|
|
223
|
+
budgetMs: f.budgetMs,
|
|
224
|
+
over: r != null && r.tier === 'quick' && f.tier === 'quick' && r.ms > f.budgetMs,
|
|
225
|
+
...trendOf(f),
|
|
226
|
+
tier: f.tier,
|
|
227
|
+
phase: r?.phase == null || r.phase === '' ? null : r.phase,
|
|
228
|
+
status: r?.status ?? null,
|
|
229
|
+
}
|
|
230
|
+
}).sort((a, b) => (b.ms ?? -1) - (a.ms ?? -1) || a.file.localeCompare(b.file))
|
|
231
|
+
|
|
232
|
+
return { checkId, last: lastAt == null ? null : lastRunOf(checkId, lastAt, files, sumMs, o), failing: failingOf(mine), files }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function lastRunOf(checkId: string, at: number, files: FileTime[], sumMs: number, o: TestTimeInput): LastRun {
|
|
236
|
+
const ran = files.filter((f) => f.inLastRun)
|
|
237
|
+
const sr = [...o.suiteRuns].reverse().find((r) => r.check === checkId && r.at === at) ?? null
|
|
238
|
+
const host = hostRun(o.runs, checkId, at)
|
|
239
|
+
const wallMs = host?.ms ?? sr?.wallMs ?? null
|
|
240
|
+
const wallFrom = host != null ? 'check' : sr != null ? 'runner' : null
|
|
241
|
+
const workers = sr?.workers ?? null
|
|
242
|
+
const ratio = wallMs != null && wallMs > 0 ? sumMs / wallMs : null
|
|
243
|
+
const top = ran.slice(0, 3).map((f) => ({ file: f.file, id: f.id, inUnit: f.inUnit, ms: f.ms!, share: f.share ?? 0 }))
|
|
244
|
+
const hostMs = host == null || sr == null ? null : Math.max(0, host.ms - sr.wallMs)
|
|
245
|
+
const v = top[0] == null ? null : verdict({ top: top[0], sumMs, wallMs, workers, sr, hostMs })
|
|
246
|
+
const shape = v?.shape ?? null
|
|
247
|
+
const shapeWords = v?.words ?? 'not enough recorded to say'
|
|
248
|
+
const phases: Phases | null = sr == null ? null : {
|
|
249
|
+
setupMs: sr.setupMs,
|
|
250
|
+
parallelMs: sr.parallelMs,
|
|
251
|
+
parallelCpuMs: sr.parallelCpuMs,
|
|
252
|
+
workers: sr.workers,
|
|
253
|
+
serialMs: sr.serialMs,
|
|
254
|
+
serialFiles: sr.serialFiles,
|
|
255
|
+
aloneMs: sr.aloneMs,
|
|
256
|
+
aloneFiles: sr.aloneFiles,
|
|
257
|
+
runnerMs: sr.wallMs,
|
|
258
|
+
hostMs,
|
|
259
|
+
load1: sr.load1,
|
|
260
|
+
cores: sr.cores,
|
|
261
|
+
}
|
|
262
|
+
const line = [
|
|
263
|
+
`${ran.length} files, sum ${ms0(sumMs)}`,
|
|
264
|
+
wallMs == null ? 'wall not recorded' : `wall ${ms0(wallMs)}${wallFrom === 'runner' ? ' (runner)' : ''}`,
|
|
265
|
+
workers == null ? null : workers === 0 ? 'one at a time' : `${workers} workers`,
|
|
266
|
+
ratio == null ? null : `sum/wall ${ratio.toFixed(1)}x`,
|
|
267
|
+
].filter((x) => x != null).join(', ') + `: ${shapeWords}. top ${top.length}: ${top.map((t) => `${t.inUnit} ${ms0(t.ms)} (${pct(t.share)})`).join(', ')}`
|
|
268
|
+
return { at, files: ran.length, sumMs, wallMs, wallFrom, workers, ratio, shape, shapeWords, top, phases, line }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function shapeTestsRoot(o: TestTimeInput): TestsRootView {
|
|
272
|
+
const ids = [...new Set(o.facts.filter((f) => f.standing === 'runs' && f.checkId != null).map((f) => f.checkId!))]
|
|
273
|
+
const checks = ids.map((checkId): TestCheckRow => {
|
|
274
|
+
const v = shapeTestCheck(checkId, o)
|
|
275
|
+
const top = v.files.find((f) => f.ms != null)
|
|
276
|
+
return {
|
|
277
|
+
checkId,
|
|
278
|
+
files: v.files.length,
|
|
279
|
+
failing: v.failing.length,
|
|
280
|
+
at: v.last?.at ?? null,
|
|
281
|
+
wallMs: v.last?.wallMs ?? null,
|
|
282
|
+
sumMs: v.last?.sumMs ?? null,
|
|
283
|
+
slowest: top == null ? null : { file: top.file, id: top.id, inUnit: top.inUnit, ms: top.ms! },
|
|
284
|
+
shape: v.last?.shape ?? null,
|
|
285
|
+
}
|
|
286
|
+
}).sort((a, b) => (b.wallMs ?? -1) - (a.wallMs ?? -1) || a.checkId.localeCompare(b.checkId))
|
|
287
|
+
return { checks, failing: failingOf(o.facts) }
|
|
288
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// THE TEST FILES, one resource each: every file bun would discover, filed
|
|
2
|
+
// under the test check that runs it (`test`, `test:<unit>`), plus the files a
|
|
3
|
+
// bunfig ignore keeps out on purpose and the ones no check reaches.
|
|
4
|
+
//
|
|
5
|
+
// Everything here is READ. Which check runs a file is suite/test-units.ts
|
|
6
|
+
// testPlanOf, the same plan rule SK018 judges, and whether a file needs fixing
|
|
7
|
+
// is ledger/run-timeline.ts testFilesToFix, the same function the runs page
|
|
8
|
+
// tab lists. A second answer to either would drift from the first.
|
|
9
|
+
import { posix } from 'path'
|
|
10
|
+
import { CheckCtx } from '../core/check-ctx.ts'
|
|
11
|
+
import { testFilesToFix } from '../ledger/run-timeline.ts'
|
|
12
|
+
import { runsPath } from '../ledger/run-log.ts'
|
|
13
|
+
import { readSuites, type SuiteRow } from '../ledger/suite-log.ts'
|
|
14
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
15
|
+
import { rawName } from '../core/canonical-scripts.ts'
|
|
16
|
+
import { DEFAULT_BUDGET_MS, normSuitePath } from '../suite/judge-suites.ts'
|
|
17
|
+
import { declaredSlowFiles, isTestFile, TEST_GLOB, testCheckId, testPlanOf, type TestUnitDecl } from '../suite/test-units.ts'
|
|
18
|
+
import { NO_CHECK, testId, type Collection, type Instance, type ResourceState, type SuiteFacts, type TestFacts, type TestStanding } from './collection-types.ts'
|
|
19
|
+
|
|
20
|
+
/** how many recorded runs a test file's page lists */
|
|
21
|
+
export const TEST_RUNS_KEPT = 10
|
|
22
|
+
|
|
23
|
+
export type TestsOpts = {
|
|
24
|
+
/** injected by tests; production lists what git sees */
|
|
25
|
+
files?: string[]
|
|
26
|
+
/** injected by tests; production reads the disk. null = absent */
|
|
27
|
+
read?: (rel: string) => string | null
|
|
28
|
+
/** injected by tests; production reads the suite record */
|
|
29
|
+
suites?: SuiteRow[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** the colour of a file's row: red when its last run failed or no check
|
|
33
|
+
* reaches it, warn when it broke its budget, green only when a run passed */
|
|
34
|
+
function stateOf(f: TestFacts): ResourceState {
|
|
35
|
+
if (f.standing === 'orphan' || f.toFix?.red === true) return 'red'
|
|
36
|
+
if (f.standing === 'missing' || f.toFix?.slow === true) return 'warn'
|
|
37
|
+
if (f.standing === 'ignored') return 'quiet'
|
|
38
|
+
if (f.runs.length > 0 && f.runs[0]!.status === 'pass') return 'ok'
|
|
39
|
+
return 'idle'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** the one short fact on the row: the tier it runs in, or why it runs in none */
|
|
43
|
+
const SLOT: Record<TestStanding, (f: TestFacts) => string> = {
|
|
44
|
+
runs: (f) => (f.tier === 'full' ? 'release only' : 'quick'),
|
|
45
|
+
ignored: () => 'ignored',
|
|
46
|
+
orphan: () => 'no check',
|
|
47
|
+
opaque: () => 'unknown',
|
|
48
|
+
missing: () => 'missing',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** the feed line: a terminal has no slots */
|
|
52
|
+
function sentence(f: TestFacts): string {
|
|
53
|
+
const last = f.runs[0]
|
|
54
|
+
const what = last == null ? 'never ran here' : `${last.status === 'fail' ? 'failed' : 'passed'} in ${Math.round(last.ms)}ms of a ${f.budgetMs}ms budget`
|
|
55
|
+
if (f.standing === 'orphan') return `${f.file} runs in no check: ${f.why}`
|
|
56
|
+
if (f.standing === 'ignored') return `${f.file} is ignored on purpose by ${f.why}`
|
|
57
|
+
if (f.standing === 'opaque') return `${f.file}: ${f.why}`
|
|
58
|
+
if (f.standing === 'missing') return `${f.file} is declared slow but is not on disk`
|
|
59
|
+
return `${f.file} runs in ${f.checkId}, ${what}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function testsCollection(root: string, c: RepoConfig, o: TestsOpts = {}): Collection {
|
|
63
|
+
return {
|
|
64
|
+
id: 'tests',
|
|
65
|
+
title: 'tests',
|
|
66
|
+
media: 'health',
|
|
67
|
+
list: async () => {
|
|
68
|
+
let files = o.files
|
|
69
|
+
let read = o.read
|
|
70
|
+
if (files == null || read == null) {
|
|
71
|
+
const disk = CheckCtx.open({ root, config: c })
|
|
72
|
+
files ??= disk.list(TEST_GLOB)
|
|
73
|
+
read ??= (rel) => disk.read(rel)
|
|
74
|
+
}
|
|
75
|
+
let suites = o.suites
|
|
76
|
+
if (suites == null) {
|
|
77
|
+
try { suites = readSuites(runsPath(root)) } catch { suites = [] }
|
|
78
|
+
}
|
|
79
|
+
return testInstances({ config: c, files: files.filter(isTestFile), read, suites })
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** every test file as a resource, pure over what the caller read */
|
|
85
|
+
export function testInstances(o: { config: RepoConfig; files: string[]; read: (rel: string) => string | null; suites: SuiteRow[] }): Instance[] {
|
|
86
|
+
const plan = testPlanOf({ config: o.config, files: o.files, read: o.read })
|
|
87
|
+
const slow = declaredSlowFiles(o.config)
|
|
88
|
+
const toFix = new Map(testFilesToFix(o.suites, slow).map((r) => [r.file, r]))
|
|
89
|
+
const byFile = new Map<string, SuiteRow[]>()
|
|
90
|
+
for (const s of o.suites) {
|
|
91
|
+
const list = byFile.get(s.file) ?? []
|
|
92
|
+
list.push(s)
|
|
93
|
+
byFile.set(s.file, list)
|
|
94
|
+
}
|
|
95
|
+
const decl = slowDeclarations(o.config)
|
|
96
|
+
const unitOf = (file: string): TestUnitDecl | null =>
|
|
97
|
+
plan.units.filter((u) => file === u.rel || file.startsWith(`${u.rel}/`)).sort((a, b) => b.rel.length - a.rel.length)[0] ?? null
|
|
98
|
+
const hasRaw = (dir: string): boolean => {
|
|
99
|
+
try {
|
|
100
|
+
const pkg = JSON.parse(o.read(dir === '' ? 'package.json' : posix.join(dir, 'package.json')) ?? '{}') as { scripts?: Record<string, string> }
|
|
101
|
+
return pkg.scripts?.[rawName('test')] != null
|
|
102
|
+
} catch {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const facts = (file: string, standing: TestStanding, checkId: string | null, why: string | null): TestFacts => {
|
|
108
|
+
const u = unitOf(file)
|
|
109
|
+
const unitDir = u?.rel ?? ''
|
|
110
|
+
const inUnit = unitDir === '' ? file : file.slice(unitDir.length + 1)
|
|
111
|
+
const budgetMs = (u == null ? o.config.tests?.budgetMs : u.tests?.budgetMs) ?? DEFAULT_BUDGET_MS
|
|
112
|
+
const runs = (byFile.get(file) ?? [])
|
|
113
|
+
.slice()
|
|
114
|
+
.sort((a, b) => b.at - a.at)
|
|
115
|
+
.slice(0, TEST_RUNS_KEPT)
|
|
116
|
+
.map((s): TestFacts['runs'][number] => ({
|
|
117
|
+
at: s.at, ms: s.ms, status: s.status, tier: s.tier, budgetMs: s.budgetMs,
|
|
118
|
+
...(s.cpuMs == null ? {} : { cpuMs: s.cpuMs }),
|
|
119
|
+
...(s.phase == null || s.phase === '' ? {} : { phase: s.phase }),
|
|
120
|
+
...(s.phaseMs == null ? {} : { phaseMs: s.phaseMs }),
|
|
121
|
+
...(s.failLine == null || s.failLine === '' ? {} : { failLine: s.failLine }),
|
|
122
|
+
}))
|
|
123
|
+
const fix = toFix.get(file)
|
|
124
|
+
const suite = decl.get(file)
|
|
125
|
+
return {
|
|
126
|
+
file,
|
|
127
|
+
inUnit,
|
|
128
|
+
unit: u?.id ?? 'root',
|
|
129
|
+
unitDir,
|
|
130
|
+
checkId,
|
|
131
|
+
standing,
|
|
132
|
+
why,
|
|
133
|
+
tier: suite != null ? 'full' : 'quick',
|
|
134
|
+
budgetMs,
|
|
135
|
+
toFix: fix == null ? null : { red: fix.red, slow: fix.slow },
|
|
136
|
+
runs,
|
|
137
|
+
cmd: { cwd: unitDir, line: `${hasRaw(unitDir) ? `bun run ${rawName('test')}` : 'bun test'} ./${inUnit}` },
|
|
138
|
+
...(suite == null ? {} : { suite }),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const all: TestFacts[] = []
|
|
143
|
+
for (const [checkId, list] of plan.runs) for (const file of list) all.push(facts(file, 'runs', checkId, null))
|
|
144
|
+
for (const i of plan.ignored) all.push(facts(i.file, 'ignored', i.checkId, i.pattern))
|
|
145
|
+
for (const x of plan.orphans) all.push(facts(x.file, 'orphan', null, x.why))
|
|
146
|
+
for (const x of plan.unread) all.push(facts(x.file, 'opaque', x.checkId, `${x.checkId} runs a script that is not a plain \`bun test\`, so which files it runs cannot be read`))
|
|
147
|
+
const seen = new Set(all.map((f) => f.file))
|
|
148
|
+
for (const [file, suite] of decl) {
|
|
149
|
+
if (seen.has(file)) continue
|
|
150
|
+
const u = unitOf(file)
|
|
151
|
+
all.push(facts(file, 'missing', testCheckId(u?.id ?? 'root'), `${suite.configPath} declares it slow`))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return all.map((f): Instance => {
|
|
155
|
+
const folder = f.checkId == null || f.standing === 'orphan' ? NO_CHECK : f.checkId
|
|
156
|
+
return {
|
|
157
|
+
id: testId(f.file),
|
|
158
|
+
type: 'test',
|
|
159
|
+
path: `tests/${folder}/${f.inUnit}`,
|
|
160
|
+
virtual: true,
|
|
161
|
+
label: posix.basename(f.inUnit),
|
|
162
|
+
text: sentence(f),
|
|
163
|
+
slot: SLOT[f.standing](f),
|
|
164
|
+
state: stateOf(f),
|
|
165
|
+
...(f.standing === 'missing' ? {} : { file: f.file }),
|
|
166
|
+
test: f,
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** every slow declaration, by the repo relative file it names */
|
|
172
|
+
function slowDeclarations(c: RepoConfig): Map<string, SuiteFacts> {
|
|
173
|
+
const out = new Map<string, SuiteFacts>()
|
|
174
|
+
const add = (dir: string | null, s: { file: string; catches: string; whyNotFast: string }, budgetMs: number): void => {
|
|
175
|
+
const rel = normSuitePath(dir == null ? s.file : posix.join(dir, s.file))
|
|
176
|
+
out.set(rel, { file: s.file, catches: s.catches, whyNotFast: s.whyNotFast, configPath: 'repo.config.ts', unit: dir, budgetMs })
|
|
177
|
+
}
|
|
178
|
+
for (const s of c.tests?.slow ?? []) add(null, s, c.tests?.budgetMs ?? DEFAULT_BUDGET_MS)
|
|
179
|
+
for (const u of c.units ?? []) if (typeof u.dir === 'string') for (const s of u.tests?.slow ?? []) add(u.dir, s, u.tests?.budgetMs ?? DEFAULT_BUDGET_MS)
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// wikis collection — the MOUNTED md wikis as CHECKED section entries. The
|
|
2
|
+
// mounts arrive as data from the hub's own route resolution (serve/run-serve),
|
|
3
|
+
// never re-derived here: a card linking at an unmounted route renders the 404
|
|
4
|
+
// body inside its iframe. Each instance carries its lint state (orphans +
|
|
5
|
+
// dangling refs = red with counts; clean = green), or the last verdicts of
|
|
6
|
+
// the repo checks its mount names. A graph whose lint belongs to another repo
|
|
7
|
+
// is link-only, never double-judged here.
|
|
8
|
+
import { checkStatsFor, type CheckStat } from '../ledger/check-stats.ts'
|
|
9
|
+
import { lintMd } from '../wiki/lint-md.ts'
|
|
10
|
+
import type { GraphMount } from '../wiki/graph-mount.ts'
|
|
11
|
+
import type { Collection, Instance } from './collection-types.ts'
|
|
12
|
+
import { graphChecksState, graphState } from './state-rules.ts'
|
|
13
|
+
|
|
14
|
+
export type WikisOpts = {
|
|
15
|
+
/** injected by tests; production reads the store plus the run record */
|
|
16
|
+
stats?: Map<string, CheckStat>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// a wiki's graph is a VIEW, never a place: it files under wikis/<id> whatever
|
|
20
|
+
// dir it scans, so the wiki and the agent graphs sit as two sibling rows.
|
|
21
|
+
// Filed at the dir it scans, the agent graph read as the agent folder itself
|
|
22
|
+
const wikiPath = (m: GraphMount): Pick<Instance, 'path' | 'virtual'> => ({ path: `wikis/${m.id}`, virtual: true })
|
|
23
|
+
|
|
24
|
+
export function wikisCollection(mounts: GraphMount[], root: string | null = null, o: WikisOpts = {}): Collection {
|
|
25
|
+
return {
|
|
26
|
+
id: 'wikis',
|
|
27
|
+
title: 'wikis',
|
|
28
|
+
media: 'data',
|
|
29
|
+
list: async () =>
|
|
30
|
+
mounts.map((m): Instance => {
|
|
31
|
+
const id = `wiki:${m.id}`
|
|
32
|
+
const href = `/graph/${m.id}`
|
|
33
|
+
const label = m.label
|
|
34
|
+
const place = { type: 'wiki' as const, ...wikiPath(m) }
|
|
35
|
+
if (m.lint == null) return { id, text: m.label, label: m.label, md: label, href, ...place }
|
|
36
|
+
if ('checks' in m.lint) {
|
|
37
|
+
const ids = m.lint.checks
|
|
38
|
+
const stats = o.stats ?? (root == null ? new Map<string, CheckStat>() : checkStatsFor(root, ids.map((x) => ({ id: x }))))
|
|
39
|
+
const last = ids.map((x) => stats.get(x)?.last?.status ?? null)
|
|
40
|
+
const red = ids.filter((_, n) => last[n] === 'fail')
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
text: red.length === 0 ? m.label : `${m.label}: ${red.join(', ')} red`,
|
|
44
|
+
label: m.label,
|
|
45
|
+
slot: red.length === 0 ? 'clean' : `${red.length} red`,
|
|
46
|
+
md: `${label}, judged by ${ids.join(', ')}`,
|
|
47
|
+
...graphChecksState(last),
|
|
48
|
+
href,
|
|
49
|
+
...place,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const r = lintMd({ root: m.lint.root, index: m.lint.index })
|
|
53
|
+
const issues = r.orphans.length + r.dangling.length
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
text: issues === 0 ? m.label : `${m.label} — ${r.orphans.length} orphan(s), ${r.dangling.length} dangling ref(s)`,
|
|
57
|
+
label: m.label,
|
|
58
|
+
slot: issues === 0 ? 'clean' : `${issues} issue${issues === 1 ? '' : 's'}`,
|
|
59
|
+
md: label,
|
|
60
|
+
...graphState({ lint: true, issues }),
|
|
61
|
+
href,
|
|
62
|
+
...place,
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
}
|
|
66
|
+
}
|