rvlib-shipkit 0.38.1 → 1.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.
Files changed (255) hide show
  1. package/CHANGELOG.md +379 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/derived.ts +1 -1
  6. package/src/assets/gen/generate-roll.ts +1 -1
  7. package/src/assets/kit-config.ts +6 -6
  8. package/src/assets/run-assets.ts +14 -10
  9. package/src/assets/server/studio-server.ts +6 -5
  10. package/src/check.ts +343 -0
  11. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  12. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  13. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  14. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  15. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +49 -32
  16. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  17. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  18. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  19. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  20. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  21. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  22. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  23. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  24. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  25. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  26. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  27. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  28. package/src/checks/SK018-test-files-run.check.ts +28 -0
  29. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  30. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  31. package/src/checks/contracts.check.ts +20 -0
  32. package/src/cli/cmd_Asks.ts +3 -2
  33. package/src/cli/cmd_Assets.ts +2 -2
  34. package/src/cli/cmd_Bump.ts +1 -1
  35. package/src/cli/cmd_Check.ts +6 -6
  36. package/src/cli/cmd_Checks.ts +95 -0
  37. package/src/cli/cmd_Ci.ts +48 -13
  38. package/src/cli/cmd_Commands.ts +2 -6
  39. package/src/cli/cmd_ConfigKeys.ts +69 -0
  40. package/src/cli/cmd_Contracts.ts +1 -1
  41. package/src/cli/cmd_Fleet.ts +16 -3
  42. package/src/cli/cmd_Hooks.ts +1 -1
  43. package/src/cli/cmd_Keys.ts +43 -18
  44. package/src/cli/cmd_Mv.ts +2 -2
  45. package/src/cli/cmd_Prose.ts +10 -1
  46. package/src/cli/cmd_Releases.ts +2 -2
  47. package/src/cli/cmd_Serve.ts +1 -1
  48. package/src/cli/cmd_Start.ts +6 -6
  49. package/src/cli/cmd_Status.ts +25 -0
  50. package/src/cli/cmd_Tui.ts +2 -2
  51. package/src/cli.ts +7 -4
  52. package/src/collections/actions.ts +4 -4
  53. package/src/collections/checks.ts +257 -0
  54. package/src/collections/collection-types.ts +254 -1
  55. package/src/collections/comfy.ts +23 -0
  56. package/src/collections/md-body.ts +14 -0
  57. package/src/collections/pings.ts +38 -12
  58. package/src/collections/posts.ts +19 -3
  59. package/src/collections/providers.ts +198 -91
  60. package/src/collections/reflections.ts +41 -0
  61. package/src/collections/state-rules.ts +73 -0
  62. package/src/collections/test-time.ts +288 -0
  63. package/src/collections/tests.ts +181 -0
  64. package/src/collections/wikis.ts +66 -0
  65. package/src/comfy/comfy-panel.ts +292 -0
  66. package/src/contracts/apply-contracts.ts +17 -12
  67. package/src/contracts/contract-types.ts +16 -4
  68. package/src/contracts/contracts-engine.ts +78 -42
  69. package/src/contracts/manifests/discord.contract.ts +6 -6
  70. package/src/contracts/manifests/identity.contract.ts +3 -3
  71. package/src/contracts/manifests/itch.contract.ts +12 -15
  72. package/src/contracts/manifests/social.contract.ts +5 -5
  73. package/src/contracts/manifests/steam.contract.ts +16 -16
  74. package/src/contracts/run-contracts.ts +4 -3
  75. package/src/core/attach.ts +96 -0
  76. package/src/core/canonical-scripts.ts +25 -0
  77. package/src/core/check-ctx.ts +204 -14
  78. package/src/core/check-file-names.ts +112 -0
  79. package/src/core/check-pass.ts +84 -0
  80. package/src/core/check-runner.ts +796 -0
  81. package/src/core/config-keys.ts +256 -0
  82. package/src/core/config-rewrite.ts +77 -0
  83. package/src/core/discover-checks.ts +166 -90
  84. package/src/core/discover-legacy-checks.ts +26 -0
  85. package/src/core/doc-block.ts +13 -0
  86. package/src/core/floor-resolve.ts +264 -0
  87. package/src/core/floor.ts +20 -137
  88. package/src/core/git-bin.ts +24 -0
  89. package/src/core/git-root.ts +108 -0
  90. package/src/core/instance.ts +195 -0
  91. package/src/core/jobs.ts +118 -0
  92. package/src/core/layout.ts +52 -0
  93. package/src/core/legacy-shapes.ts +207 -0
  94. package/src/core/lib-root.ts +5 -0
  95. package/src/core/locked-write.ts +110 -39
  96. package/src/core/machine-commit.ts +5 -4
  97. package/src/core/migrate-layout.ts +616 -0
  98. package/src/core/motherhome.ts +2 -8
  99. package/src/core/nesting.ts +4 -12
  100. package/src/core/one-way-in-policy.ts +73 -17
  101. package/src/core/outcome-words.ts +45 -0
  102. package/src/core/package-manager.ts +88 -0
  103. package/src/core/parallel.ts +21 -0
  104. package/src/core/process-tree.ts +44 -0
  105. package/src/core/raw-access.ts +93 -0
  106. package/src/core/read-set.ts +152 -0
  107. package/src/core/run-check.ts +155 -213
  108. package/src/core/run-ci.ts +152 -189
  109. package/src/core/run-fix.ts +99 -0
  110. package/src/core/run-fixers.ts +64 -33
  111. package/src/core/self-update.ts +11 -3
  112. package/src/core/slots.ts +83 -0
  113. package/src/core/spawn-timeout.ts +34 -3
  114. package/src/core/when-block.ts +401 -0
  115. package/src/core/when.ts +135 -0
  116. package/src/core/write-status.ts +5 -2
  117. package/src/fleet/scan-fleet.ts +16 -0
  118. package/src/hooks/leak-guard.ts +20 -9
  119. package/src/hooks/one-way-in-guard.ts +13 -5
  120. package/src/hooks/root-identity.ts +2 -1
  121. package/src/hooks/run-hooks.ts +38 -26
  122. package/src/init-repo.ts +39 -23
  123. package/src/ledger/check-stats.ts +155 -0
  124. package/src/ledger/check-table.ts +179 -0
  125. package/src/ledger/db.ts +255 -0
  126. package/src/ledger/gate-checks.ts +433 -0
  127. package/src/ledger/git-key.ts +116 -77
  128. package/src/ledger/git-scan.ts +96 -35
  129. package/src/ledger/ledger.ts +116 -42
  130. package/src/ledger/live-runs.ts +111 -0
  131. package/src/ledger/live-timeline.ts +97 -0
  132. package/src/ledger/repo-view.ts +73 -0
  133. package/src/ledger/run-log.ts +448 -0
  134. package/src/ledger/run-timeline.ts +173 -0
  135. package/src/ledger/store.ts +23 -12
  136. package/src/ledger/suite-log.ts +161 -0
  137. package/src/ledger/toolchain-identity.ts +7 -1
  138. package/src/ledger/world-cache.ts +111 -0
  139. package/src/mv/mv-md.ts +16 -6
  140. package/src/problems/extract.ts +182 -0
  141. package/src/problems/problem-board.ts +37 -0
  142. package/src/problems/problem-line.ts +77 -0
  143. package/src/prose/prose-view.ts +90 -0
  144. package/src/prose/run-prose.ts +60 -19
  145. package/src/prose/ste.ts +140 -24
  146. package/src/reflections/reflections-queue.ts +81 -0
  147. package/src/release/git-facts.ts +15 -9
  148. package/src/release/run-bump.ts +6 -5
  149. package/src/release/run-release.ts +25 -11
  150. package/src/release/run-releases.ts +2 -2
  151. package/src/release/unit-gate.ts +7 -54
  152. package/src/release/unit-resolve.ts +282 -0
  153. package/src/release/units.ts +33 -217
  154. package/src/repo-config.ts +157 -88
  155. package/src/rules/rule.ts +36 -96
  156. package/src/rules/untrack-generated.ts +5 -4
  157. package/src/scaffold/files/vscode-tasks.json +10 -31
  158. package/src/scaffold/jsonc-spans.ts +113 -0
  159. package/src/scaffold/project-scaffold.ts +16 -43
  160. package/src/scan.ts +98 -0
  161. package/src/serve/gate-trigger.ts +148 -0
  162. package/src/serve/health-schedule.ts +102 -0
  163. package/src/serve/hub-app/chart-view.ts +179 -0
  164. package/src/serve/hub-app/check-view.tsx +345 -0
  165. package/src/serve/hub-app/check-words.ts +164 -0
  166. package/src/serve/hub-app/checks-svc.ts +280 -0
  167. package/src/serve/hub-app/clock-svc.ts +33 -0
  168. package/src/serve/hub-app/col-card.tsx +23 -39
  169. package/src/serve/hub-app/cols-svc.ts +44 -20
  170. package/src/serve/hub-app/comfy-svc.ts +171 -0
  171. package/src/serve/hub-app/comfy-view.tsx +81 -0
  172. package/src/serve/hub-app/dur.ts +41 -0
  173. package/src/serve/hub-app/hash-route.ts +121 -0
  174. package/src/serve/hub-app/header-bar.tsx +117 -0
  175. package/src/serve/hub-app/health-svc.ts +23 -28
  176. package/src/serve/hub-app/help.tsx +29 -0
  177. package/src/serve/hub-app/hub-app.tsx +122 -119
  178. package/src/serve/hub-app/hub-st.ts +115 -42
  179. package/src/serve/hub-app/hub-types.ts +117 -2
  180. package/src/serve/hub-app/hub.css +534 -192
  181. package/src/serve/hub-app/index.html +2 -0
  182. package/src/serve/hub-app/live-svc.ts +174 -0
  183. package/src/serve/hub-app/md.ts +15 -0
  184. package/src/serve/hub-app/media-svc.ts +4 -1
  185. package/src/serve/hub-app/media-view.tsx +15 -0
  186. package/src/serve/hub-app/next-run.ts +68 -0
  187. package/src/serve/hub-app/project-gutter.css +65 -0
  188. package/src/serve/hub-app/project-gutter.tsx +128 -0
  189. package/src/serve/hub-app/projects-svc.ts +226 -0
  190. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  191. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  192. package/src/serve/hub-app/prose-hits.css +21 -0
  193. package/src/serve/hub-app/run-strip.ts +47 -0
  194. package/src/serve/hub-app/runs-tabs.ts +24 -0
  195. package/src/serve/hub-app/test-view.tsx +107 -0
  196. package/src/serve/hub-app/tests-svc.ts +62 -0
  197. package/src/serve/hub-app/tests-view.tsx +238 -0
  198. package/src/serve/hub-app/time-ago.tsx +34 -0
  199. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  200. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  201. package/src/serve/hub-app/timeline-svc.ts +62 -0
  202. package/src/serve/hub-app/timeline-view.tsx +245 -0
  203. package/src/serve/hub-app/tree-build.ts +410 -0
  204. package/src/serve/hub-app/tree-row.tsx +42 -0
  205. package/src/serve/hub-app/tree-svc.ts +174 -0
  206. package/src/serve/hub-app/wikis-view.tsx +53 -0
  207. package/src/serve/other-repos.ts +79 -0
  208. package/src/serve/project-gutter-route.ts +350 -0
  209. package/src/serve/project-gutter.ts +449 -0
  210. package/src/serve/run-serve.ts +245 -16
  211. package/src/serve/same-origin.ts +14 -0
  212. package/src/serve/serve-hub.ts +213 -18
  213. package/src/shipkit.ts +19 -12
  214. package/src/smoke/run-smoke.ts +9 -4
  215. package/src/start-repo.ts +75 -36
  216. package/src/suite/judge-suites.ts +209 -15
  217. package/src/suite/kept-timings.ts +58 -0
  218. package/src/suite/runner-shapes.ts +88 -0
  219. package/src/suite/suite-cli.ts +147 -24
  220. package/src/suite/suite-cpu-preload.ts +92 -0
  221. package/src/suite/test-units.ts +243 -0
  222. package/src/ts/config.ts +4 -69
  223. package/src/ts/jsonc.ts +69 -0
  224. package/src/ts/lex.ts +143 -0
  225. package/src/ts/rename.ts +73 -10
  226. package/src/ts/source-graph.ts +28 -8
  227. package/src/ts/tsconfig-projects.ts +6 -97
  228. package/src/ts/tsconfig-resolve.ts +108 -0
  229. package/src/tui/build-steps-panel.ts +61 -0
  230. package/src/tui/checks-panel.ts +24 -16
  231. package/src/tui/run-tui.ts +2 -2
  232. package/src/tui/tui-panel.ts +5 -4
  233. package/src/wiki/graph-colors.ts +68 -0
  234. package/src/wiki/graph-mount.ts +7 -9
  235. package/src/wiki/graph-page.ts +42 -25
  236. package/src/wiki/graph-positions.ts +105 -0
  237. package/src/wiki/run-wiki.ts +1 -1
  238. package/src/wiki/scan-md.ts +1 -1
  239. package/src/wiki/serve-graph.ts +33 -3
  240. package/src/cli/cmd_Rules.ts +0 -51
  241. package/src/cli/cmd_Units.ts +0 -82
  242. package/src/collections/graphs.ts +0 -32
  243. package/src/contracts/contracts-once.ts +0 -43
  244. package/src/core/check-suite.ts +0 -302
  245. package/src/core/repair-contract-dirs.ts +0 -136
  246. package/src/core/task-runner.ts +0 -339
  247. package/src/ledger/gate-units.ts +0 -138
  248. package/src/ledger/unit-table.ts +0 -105
  249. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  250. package/src/rules/SK003-claude-md-shape.ts +0 -70
  251. package/src/rules/SK004-gitignore-policy.ts +0 -238
  252. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  253. package/src/rules/rule-registry.ts +0 -64
  254. package/src/rules/run-fix.ts +0 -72
  255. package/src/tui/tasks-panel.ts +0 -60
@@ -0,0 +1,107 @@
1
+ // ONE TEST FILE, on its own page, in the compact shape of a check page: its
2
+ // path, the check that runs it, its unit and tier, the command that runs it
3
+ // alone, then its last recorded runs against its budget. A declared slow
4
+ // suite adds the case its declaration argued and the way back to the quick
5
+ // tier.
6
+ import { observer } from 'mobx-react-lite'
7
+ import { durationTone, fmtMs } from './dur.ts'
8
+ import type { HubSt } from './hub-st.ts'
9
+ import type { Resource, SuiteFacts, TestFacts, TestRun } from './hub-types.ts'
10
+ import { copy, PathCode, Row, Section } from './check-view.tsx'
11
+ import { editorHref, repoPath } from './check-words.ts'
12
+ import { stampWords, TimeAgo } from './time-ago.tsx'
13
+
14
+ /** what a file's standing means, in one line under its title */
15
+ export function standingWords(t: TestFacts): string {
16
+ if (t.standing === 'runs') return t.tier === 'full' ? `runs in ${t.checkId} on the release path only: it is declared slow` : `runs in ${t.checkId} on every gate`
17
+ if (t.standing === 'ignored') return `ignored on purpose: the ignore pattern ${t.why} keeps it out of ${t.checkId}`
18
+ if (t.standing === 'orphan') return `runs in no check: ${t.why}`
19
+ if (t.standing === 'opaque') return t.why ?? 'which check runs it cannot be read'
20
+ return `declared slow in ${t.suite?.configPath ?? 'repo.config.ts'}, but no such file is on disk`
21
+ }
22
+
23
+ /** the to fix verdict, as the runs page tab words it */
24
+ export function fixWords(t: TestFacts): string | null {
25
+ if (t.toFix == null) return null
26
+ return t.toFix.red ? 'its last run failed' : `its last quick run broke the ${fmtMs(t.budgetMs)} budget`
27
+ }
28
+
29
+ /** the command to copy: `cd` into the unit first when it has a dir of its own */
30
+ export const testCommand = (t: TestFacts): string => (t.cmd.cwd === '' ? t.cmd.line : `cd ${t.cmd.cwd} && ${t.cmd.line}`)
31
+
32
+ /** over its budget: only a quick run of a file the quick tier holds to one */
33
+ const overBudget = (r: TestRun, slow: boolean): boolean => !slow && r.tier === 'quick' && r.ms > r.budgetMs
34
+
35
+ const dot = (r: TestRun, slow: boolean): string => (r.status === 'fail' ? 'red' : overBudget(r, slow) ? 'warn' : 'ok')
36
+
37
+ const RunLine = ({ app, r, slow }: { app: HubSt; r: TestRun; slow: boolean }) => (
38
+ <div className={`rline rtrun${r.status === 'fail' ? ' red' : ''}`} title={stampWords(r.at)}>
39
+ <TimeAgo clock={app.clock} at={r.at} />
40
+ <span className="rid"><i className={`rdot ${dot(r, slow)}`} />{r.status === 'fail' ? 'failed' : 'passed'}</span>
41
+ <span className={`rnum ${overBudget(r, slow) ? 'bad' : durationTone(r.ms) ?? ''}`}>{fmtMs(r.ms)}</span>
42
+ <span className="rtag">{r.tier === 'full' ? 'full tier' : 'quick tier'}</span>
43
+ </div>
44
+ )
45
+
46
+ const Declaration = observer(({ app, s }: { app: HubSt; s: SuiteFacts }) => {
47
+ const where = s.unit == null ? 'tests.slow' : `units['${s.unit}'].tests.slow`
48
+ return (
49
+ <Section title="declared slow" help="the quick tier leaves it out: its declaration says what it catches and why it cannot be fast">
50
+ <div className="cfacts">
51
+ <Row k="declared in"><code className="cpath">{s.configPath}</code> <code className="cpath">{where}</code></Row>
52
+ <Row k="back">
53
+ {s.unit == null
54
+ ? <span className="cblock">
55
+ <button disabled={app.checks.gateBusy} onClick={() => void app.checks.removeSlow(s.file)}>back to the quick tier</button>
56
+ {app.checks.gateNote === '' ? null : <span className="note"> {app.checks.gateNote}</span>}
57
+ </span>
58
+ // the managed block owns the ROOT tests declaration
59
+ // only, so a unit's own entry is edited where
60
+ // it lives rather than silently written somewhere else
61
+ : <span className="cblock dim">edit <code className="cpath">{where}</code> in <code className="cpath">{s.configPath}</code></span>}
62
+ </Row>
63
+ <Row k="catches" wide>{s.catches}</Row>
64
+ <Row k="why not fast" wide>{s.whyNotFast}</Row>
65
+ </div>
66
+ </Section>
67
+ )
68
+ })
69
+
70
+ export const TestCard = observer(({ app, id }: { app: HubSt; id: string }) => {
71
+ const i: Resource | null = app.cols.find('tests', id)
72
+ if (i == null || i.test == null) return <div className="ph">this test file is gone: the repo moved on</div>
73
+ const t = i.test
74
+ const fix = fixWords(t)
75
+ const cmd = testCommand(t)
76
+ return (
77
+ <div className="colcard checkcard">
78
+ <div className="chead">
79
+ <span className="tid">test</span>
80
+ <h3 className={`ctitle tl ${i.state}`}>{t.inUnit}</h3>
81
+ </div>
82
+ <div className="usays">{standingWords(t)}{fix == null ? null : <span className={t.toFix?.red === true ? 'tl red' : 'tl warn'}>, to fix: {fix}</span>}</div>
83
+ <div className="ccmd">
84
+ <Row k="command" help="runs this file alone, from its unit's dir, with the unit's own preload and bunfig" wide>
85
+ <code className="cargv" title={cmd}>{cmd}</code>
86
+ <button className="ccopy" title="copy the command" onClick={() => copy(cmd)}>copy</button>
87
+ </Row>
88
+ </div>
89
+ <div className="cfacts">
90
+ <Row k="file">{t.standing === 'missing' ? <code className="cpath">{t.file}</code> : <PathCode p={repoPath(t.file, app.root)} href={editorHref(app.root, t.file)} />}</Row>
91
+ <Row k="check" help="the test check whose `bun test` reaches it: `test` for the root, `test:<unit>` for a unit">
92
+ {t.checkId == null ? <span className="tl red">none</span> : <a href="#" onClick={(e) => { e.preventDefault(); app.timeline.openCheck(t.checkId!) }}><code className="cpath">{t.checkId}</code></a>}
93
+ </Row>
94
+ <Row k="unit" help="the unit whose dir holds it: the root takes every file no unit holds">{t.unit}{t.unitDir === '' ? null : <span className="dim"> ({t.unitDir})</span>}</Row>
95
+ <Row k="tier" help="quick = every gate. release only = declared slow, so `shipkit ci --full` and the release path run it">{t.tier === 'full' ? 'release only' : 'quick'}</Row>
96
+ {/* a budget is a limit, so it is never coloured like a measurement */}
97
+ <Row k="budget" help="what one file may cost in the quick tier"><span className="dnum dim">{fmtMs(t.budgetMs)}</span> per file</Row>
98
+ </div>
99
+ <Section title="last runs" help="the suite record: every run of this file the suite runner timed, newest first">
100
+ {t.runs.length === 0
101
+ ? <div className="dim">no run recorded yet: the next run of {t.checkId ?? 'a test check'} records one</div>
102
+ : <div className="rlist">{t.runs.map((r, n) => <RunLine key={`${r.at}:${n}`} app={app} r={r} slow={t.suite != null} />)}</div>}
103
+ </Section>
104
+ {t.suite == null ? null : <Declaration app={app} s={t.suite} />}
105
+ </div>
106
+ )
107
+ })
@@ -0,0 +1,62 @@
1
+ // The pages behind the test groups of the tree: what the server shaped from
2
+ // the test files and the two records (collections/test-time.ts), one view per
3
+ // page. Fetched when a page opens, never on the ten second poll, and kept per
4
+ // page so going back shows the last answer while the fresh one loads.
5
+ import { makeAutoObservable, observableRef, runInAction } from 'mobx'
6
+ import type { TestCheckView, TestsRootView } from '../../collections/test-time.ts'
7
+ import { testId } from '../../collections/collection-types.ts'
8
+ import type { HubSt } from './hub-st.ts'
9
+
10
+ export type { TestCheckView, TestsRootView }
11
+
12
+ /** the key of a page: '' is the `tests` page, a check id its own */
13
+ const keyOf = (check: string | null): string => check ?? ''
14
+
15
+ export class TestsSvc {
16
+ views = new Map<string, TestsRootView | TestCheckView>()
17
+ errors = new Map<string, string>()
18
+ loading = new Set<string>()
19
+
20
+ constructor(private st: HubSt) {
21
+ makeAutoObservable<this, 'st'>(this, { st: false, views: observableRef })
22
+ }
23
+
24
+ root(): TestsRootView | null {
25
+ return (this.views.get('') as TestsRootView | undefined) ?? null
26
+ }
27
+ check(id: string): TestCheckView | null {
28
+ return (this.views.get(id) as TestCheckView | undefined) ?? null
29
+ }
30
+ error(check: string | null): string {
31
+ return this.errors.get(keyOf(check)) ?? ''
32
+ }
33
+ isLoading(check: string | null): boolean {
34
+ return this.loading.has(keyOf(check))
35
+ }
36
+
37
+ open(check: string | null): void {
38
+ this.st.showTests(check)
39
+ }
40
+ openFile(file: string): void {
41
+ this.st.cols.select('tests', testId(file))
42
+ }
43
+
44
+ async load(check: string | null): Promise<void> {
45
+ const k = keyOf(check)
46
+ runInAction(() => { this.loading.add(k) })
47
+ try {
48
+ const r = await fetch(check == null ? '/api/tests.json' : `/api/tests.json?check=${encodeURIComponent(check)}`)
49
+ if (!r.ok) throw new Error(await r.text())
50
+ const v = (await r.json()) as TestsRootView | TestCheckView
51
+ runInAction(() => {
52
+ // a new map, so the ref annotation sees the swap
53
+ this.views = new Map(this.views).set(k, v)
54
+ this.errors.delete(k)
55
+ })
56
+ } catch (e) {
57
+ runInAction(() => { this.errors.set(k, (e as Error).message) })
58
+ } finally {
59
+ runInAction(() => { this.loading.delete(k) })
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,238 @@
1
+ // THE PAGES OF THE TEST GROUPS: `tests` itself, one line per test check, and
2
+ // each test check (`test`, `test:<unit>`), which answers one question: where
3
+ // does its time go. The check's own facts (its sentence, its command, its
4
+ // last wall time and its trend) are the check page's pieces, read from the
5
+ // resource the tree already holds. The breakdown is the one fetch the page
6
+ // makes (tests-svc.ts), shaped on the server (collections/test-time.ts).
7
+ import { observer } from 'mobx-react-lite'
8
+ import type { FailingFile, FileTime, LastRun, Phases, Shape, TestCheckRow, TestCheckView, TestsRootView } from '../../collections/test-time.ts'
9
+ import { CommandLine, LastRunValue, Row, Says, Section, Timings } from './check-view.tsx'
10
+ import { durationTone, fmtMs } from './dur.ts'
11
+ import type { HubSt } from './hub-st.ts'
12
+ import { TimeAgo } from './time-ago.tsx'
13
+
14
+ const pct = (n: number | null): string => (n == null ? '' : `${Math.round(n * 100)}%`)
15
+
16
+ /** a file's duration, coloured against its budget: over it is red whatever
17
+ * the table says, the way its tree row reads */
18
+ const fileTone = (f: FileTime): string => (f.ms == null ? '' : f.over ? 'bad' : durationTone(f.ms) ?? '')
19
+
20
+ const TREND_GLYPH = { up: '↑', down: '↓', flat: '→' } as const
21
+ const TREND_TONE = { up: 'bad', down: 'fast', flat: 'dim' } as const
22
+
23
+ export const SHAPE_WORD: Record<Shape, string> = { tail: 'long tail', volume: 'volume', overhead: 'overhead' }
24
+
25
+ const FileLink = ({ app, file, name }: { app: HubSt; file: string; name: string }) => (
26
+ <a href="#" title={`${file}: its own page`} onClick={(e) => { e.preventDefault(); e.stopPropagation(); app.tests.openFile(file) }}>{name}</a>
27
+ )
28
+
29
+ // ── failing now ─────────────────────────────────────────────────────────
30
+
31
+ const Failing = observer(({ app, rows, showCheck }: { app: HubSt; rows: FailingFile[]; showCheck: boolean }) => (
32
+ <Section title={`failing now (${rows.length})`} help="the files whose last recorded run failed: the same verdict that colours them red in the tree and lists them on the runs page tab">
33
+ {rows.length === 0
34
+ ? <div className="dim">nothing: every file passed its last recorded run</div>
35
+ : <div className="rlist">
36
+ {rows.map((r) => (
37
+ <div key={r.file} className="tfail" onClick={() => app.tests.openFile(r.file)}>
38
+ <div className={`rline tfline red${showCheck ? ' wc' : ''}`}>
39
+ {showCheck ? <span className="rtag" title="the check that runs it">{r.check ?? 'no check'}</span> : null}
40
+ <span className="rwhat" title={r.file}>{r.inUnit}</span>
41
+ <span className="rnum">{fmtMs(r.ms)}</span>
42
+ {r.at == null ? <span /> : <TimeAgo clock={app.clock} at={r.at} />}
43
+ </div>
44
+ <div className={r.failLine == null ? 'tfwhy dim' : 'tfwhy'}>{r.failLine ?? 'no failure line recorded: the next run records one'}</div>
45
+ </div>
46
+ ))}
47
+ </div>}
48
+ </Section>
49
+ ))
50
+
51
+ // ── where the time goes ─────────────────────────────────────────────────
52
+
53
+ const Summary = observer(({ app, l }: { app: HubSt; l: LastRun }) => (
54
+ <div className="tsum">
55
+ <div className="tsline">
56
+ <span><b>{l.files}</b> files</span>
57
+ <span>sum <b className="dnum">{fmtMs(l.sumMs)}</b></span>
58
+ <span title={l.wallFrom === 'runner' ? 'the check run is not on record, so this is the suite runner alone' : 'the check, start to end'}>
59
+ wall <b className="dnum">{l.wallMs == null ? 'not recorded' : fmtMs(l.wallMs)}</b>{l.wallFrom === 'runner' ? <span className="dim"> (runner)</span> : null}
60
+ </span>
61
+ <span>{l.workers == null ? <span className="dim">workers not recorded</span> : l.workers === 0 ? 'one file at a time' : <><b>{l.workers}</b> workers</>}</span>
62
+ {l.ratio == null ? null : <span title="the sum of file times over the wall: how many files ran at once, on average">sum/wall <b className="dnum">{l.ratio.toFixed(1)}×</b></span>}
63
+ <span className="dim"><TimeAgo clock={app.clock} at={l.at} /></span>
64
+ </div>
65
+ <div className={`tshape ${l.shape ?? ''}`}>{l.shape == null ? null : <b>{SHAPE_WORD[l.shape]}: </b>}{l.shapeWords}</div>
66
+ {l.top.length === 0 ? null : (
67
+ <div className="tstop">
68
+ top {l.top.length}:{' '}
69
+ {l.top.map((t, n) => (
70
+ <span key={t.file}>{n > 0 ? ', ' : null}<FileLink app={app} file={t.file} name={t.inUnit} /> <span className="dnum">{fmtMs(t.ms)}</span> <span className="dim">({pct(t.share)})</span></span>
71
+ ))}
72
+ </div>
73
+ )}
74
+ </div>
75
+ ))
76
+
77
+ /** the runner's wall, cut into what it spent it on: nothing it spends outside
78
+ * the files may hide */
79
+ const PhaseBar = ({ p, wallMs }: { p: Phases; wallMs: number | null }) => {
80
+ const parts: { k: string; ms: number; cls: string; word: string }[] = [
81
+ { k: 'setup', ms: p.setupMs, cls: 'p-setup', word: 'setup: probing bun, reading the bunfig and the pool' },
82
+ { k: 'parallel', ms: p.parallelMs, cls: 'p-par', word: p.workers === 0 ? 'the files, one at a time' : `the files, on ${p.workers} workers` },
83
+ ...(p.serialMs == null ? [] : [{ k: 'serial', ms: p.serialMs, cls: 'p-ser', word: `the ${p.serialFiles} declared serial files, after` }]),
84
+ ...(p.aloneFiles === 0 ? [] : [{ k: 'alone', ms: p.aloneMs, cls: 'p-alone', word: `${p.aloneFiles} over budget files, run again alone` }]),
85
+ ...(p.hostMs == null ? [] : [{ k: 'host', ms: p.hostMs, cls: 'p-host', word: 'the check around the runner: its process start and module load' }]),
86
+ ]
87
+ const total = Math.max(1, wallMs ?? parts.reduce((n, x) => n + x.ms, 0))
88
+ return (
89
+ <div className="tpbar">
90
+ {parts.map((x) => <i key={x.k} className={x.cls} style={{ width: `${Math.max(0.5, (x.ms / total) * 100)}%` }} title={`${x.word}: ${fmtMs(x.ms)}`} />)}
91
+ </div>
92
+ )
93
+ }
94
+
95
+ const PhaseFacts = ({ p, wallMs }: { p: Phases; wallMs: number | null }) => {
96
+ const other = p.runnerMs - p.setupMs - p.parallelMs - (p.serialMs ?? 0) - p.aloneMs
97
+ return (
98
+ <Section title="phases" help="the suite runner's own record of its last run: where its wall time went, the files included and everything around them">
99
+ <PhaseBar p={p} wallMs={wallMs} />
100
+ <div className="cfacts">
101
+ <Row k="setup" help="the runner before its first phase: probing bun, reading the bunfig, reading the pool"><span className="dnum">{fmtMs(p.setupMs)}</span></Row>
102
+ <Row k={p.workers === 0 ? 'files' : 'parallel'} help="the first phase: every quick tier file, on the workers the pool left it">
103
+ <span className="dnum">{fmtMs(p.parallelMs)}</span>
104
+ <span className="dim"> {p.workers === 0 ? 'one at a time' : `on ${p.workers} workers`}{p.parallelCpuMs == null ? null : `, ${fmtMs(p.parallelCpuMs)} of CPU`}</span>
105
+ </Row>
106
+ <Row k="serial" help="the files declared serial run after, one at a time">
107
+ {p.serialMs == null ? <span className="dim">none declared</span> : <><span className="dnum">{fmtMs(p.serialMs)}</span> <span className="dim">for {p.serialFiles} files</span></>}
108
+ </Row>
109
+ <Row k="alone" help="a file over its budget in the shared run is run again alone before it can fail: that rerun is wall time too">
110
+ {p.aloneFiles === 0 ? <span className="dim">no rerun</span> : <><span className="dnum">{fmtMs(p.aloneMs)}</span> <span className="dim">for {p.aloneFiles} files</span></>}
111
+ </Row>
112
+ <Row k="runner" help="the suite runner, first line to the record write. What is left after the phases is its own bookkeeping">
113
+ <span className="dnum">{fmtMs(p.runnerMs)}</span>{other > 0 ? <span className="dim">, {fmtMs(other)} its own</span> : null}
114
+ </Row>
115
+ <Row k="host" help="the check's wall minus the runner's: the process that hosts it, its start and its module load">
116
+ {p.hostMs == null ? <span className="dim">the check run is not on record</span> : <span className="dnum">{fmtMs(p.hostMs)}</span>}
117
+ </Row>
118
+ {p.load1 == null ? null : <Row k="load" help="the machine's one minute load when the runner judged the budgets, and its cores">{p.load1.toFixed(1)} <span className="dim">on {p.cores ?? '?'} cores</span></Row>}
119
+ </div>
120
+ </Section>
121
+ )
122
+ }
123
+
124
+ const FileRows = observer(({ app, files }: { app: HubSt; files: FileTime[] }) => (
125
+ <div className="rlist">
126
+ <div className="rhead rtime">
127
+ <span className="tleft">file</span>
128
+ <span>duration</span>
129
+ <span title="CPU the file burned alone, known only where a process ran it alone">cpu</span>
130
+ <span className="tleft">share</span>
131
+ <span>budget</span>
132
+ <span title="the last run against the mean of its earlier ones">trend</span>
133
+ <span>tier</span>
134
+ </div>
135
+ {files.map((f) => (
136
+ <div key={f.file} className={`rline rtime${f.status === 'fail' ? ' red' : ''}`} title={f.file} onClick={() => app.tests.openFile(f.file)}>
137
+ <span className="rwhat">{f.inUnit}</span>
138
+ <span className={`rnum ${fileTone(f)}`} title={f.judgedMs == null ? undefined : `its wall in the shared run. Run again alone, the budget judged ${fmtMs(f.judgedMs)}`}>
139
+ {f.ms == null ? <span className="dim">never ran</span> : fmtMs(f.ms)}
140
+ </span>
141
+ <span className="rnum dim">{fmtMs(f.cpuMs)}</span>
142
+ <span className="tshare" title={f.inLastRun ? `${pct(f.share)} of the sum of the last run` : 'not in the last run'}>
143
+ {f.share == null ? <span className="dim">{f.ms == null ? '' : 'older run'}</span> : <><span className="ttrack"><i style={{ width: `${Math.max(1, f.share * 100)}%` }} /></span><span className="rnum">{pct(f.share)}</span></>}
144
+ </span>
145
+ <span className="rnum dim">{fmtMs(f.budgetMs)}</span>
146
+ <span className={`rnum ${f.trend == null ? '' : TREND_TONE[f.trend]}`} title={f.avgMs == null ? 'no earlier run to compare' : `mean of its earlier runs: ${fmtMs(f.avgMs)}`}>{f.trend == null ? '' : TREND_GLYPH[f.trend]}</span>
147
+ <span className="rtag dim">{f.tier === 'full' ? 'release' : 'quick'}{f.phase === 'serial' ? ', serial' : ''}</span>
148
+ </div>
149
+ ))}
150
+ </div>
151
+ ))
152
+
153
+ // ── the pages ────────────────────────────────────────────────────────────
154
+
155
+ const Loading = observer(({ app, check }: { app: HubSt; check: string | null }) => {
156
+ const err = app.tests.error(check)
157
+ if (err !== '') return <div className="warnline">✗ {err}</div>
158
+ return <div className="dim">{app.tests.isLoading(check) ? 'reading the record…' : 'nothing loaded yet'}</div>
159
+ })
160
+
161
+ export const TestCheckPage = observer(({ app, check }: { app: HubSt; check: string }) => {
162
+ const i = app.cols.find('checks', `check:${check}`)
163
+ const u = i?.check ?? null
164
+ const v: TestCheckView | null = app.tests.check(check)
165
+ const live = app.checks.isRunning(check)
166
+ return (
167
+ <div className="colcard checkcard">
168
+ <div className="chead">
169
+ <span className="tid">test check</span>
170
+ <h3 className={`ctitle tl ${i?.state ?? 'idle'}`}>{check}</h3>
171
+ <a href="#" className="dim" title="its check page: when it runs, what it printed" onClick={(e) => { e.preventDefault(); app.timeline.openCheck(check) }}>check page</a>
172
+ <span className="sp" />
173
+ <button className="primary urun" disabled={app.checks.busy} onClick={() => void app.checks.run([check])}>
174
+ {live ? <><span className="spin" />running</> : 'run it now'}
175
+ </button>
176
+ </div>
177
+ {u == null ? null : <Says description={u.description} />}
178
+ {u == null ? null : <CommandLine app={app} u={u} />}
179
+ {u == null ? null : (
180
+ <div className="cfacts">
181
+ <Row k="last run" help="how its last answer came: ran, or cached (answered from the cache, nothing ran)"><LastRunValue app={app} u={u} /></Row>
182
+ <Timings u={u} />
183
+ </div>
184
+ )}
185
+ {v == null ? <Loading app={app} check={check} /> : <>
186
+ <Failing app={app} rows={v.failing} showCheck={false} />
187
+ <Section title="where the time goes" help="every file this check runs, by its last recorded duration. The share is of the sum of file times in the check's last run. Its tone is against the budget: over it reads red">
188
+ {v.last == null ? <div className="dim">no run recorded yet: the next run of {check} records one</div> : <Summary app={app} l={v.last} />}
189
+ <FileRows app={app} files={v.files} />
190
+ </Section>
191
+ {v.last?.phases == null ? null : <PhaseFacts p={v.last.phases} wallMs={v.last.wallMs} />}
192
+ </>}
193
+ </div>
194
+ )
195
+ })
196
+
197
+ const CheckRows = observer(({ app, rows }: { app: HubSt; rows: TestCheckRow[] }) => (
198
+ <div className="rlist">
199
+ <div className="rhead rcheckt">
200
+ <span className="tleft">check</span>
201
+ <span>wall</span>
202
+ <span>files</span>
203
+ <span>failing</span>
204
+ <span className="tleft">slowest file</span>
205
+ <span className="tleft">shape</span>
206
+ </div>
207
+ {rows.map((r) => (
208
+ <div key={r.checkId} className={`rline rcheckt${r.failing > 0 ? ' red' : ''}`} onClick={() => app.tests.open(r.checkId)}>
209
+ <span className="rtag">{r.checkId}</span>
210
+ <span className={`rnum ${durationTone(r.wallMs) ?? ''}`}>{r.wallMs == null ? <span className="dim">never ran</span> : fmtMs(r.wallMs)}</span>
211
+ <span className="rnum">{r.files}</span>
212
+ <span className={`rnum ${r.failing > 0 ? 'bad' : 'dim'}`}>{r.failing}</span>
213
+ <span className="rwhat">{r.slowest == null ? null : <><FileLink app={app} file={r.slowest.file} name={r.slowest.inUnit} /> <span className="dnum dim">{fmtMs(r.slowest.ms)}</span></>}</span>
214
+ <span className="rtag dim">{r.shape == null ? '' : SHAPE_WORD[r.shape]}</span>
215
+ </div>
216
+ ))}
217
+ </div>
218
+ ))
219
+
220
+ export const TestsRootPage = observer(({ app }: { app: HubSt }) => {
221
+ const v: TestsRootView | null = app.tests.root()
222
+ return (
223
+ <div className="colcard checkcard">
224
+ <div className="chead">
225
+ <span className="tid">tests</span>
226
+ <h3 className="ctitle">every test check</h3>
227
+ </div>
228
+ <div className="usays">each test check's last run: its wall time, its files, how many fail, the slowest file. Click a check for where its time goes</div>
229
+ {v == null ? <Loading app={app} check={null} /> : <>
230
+ <Section title="test checks" help="one line per test check, slowest wall first"><CheckRows app={app} rows={v.checks} /></Section>
231
+ <Failing app={app} rows={v.failing} showCheck />
232
+ </>}
233
+ </div>
234
+ )
235
+ })
236
+
237
+ export const TestsPage = observer(({ app, check }: { app: HubSt; check: string | null }) =>
238
+ check == null ? <TestsRootPage app={app} /> : <TestCheckPage app={app} check={check} />)
@@ -0,0 +1,34 @@
1
+ // An age that moves, from the one clock (clock-svc.ts). The words are pure,
2
+ // so a test pins them, and the component is the only thing on the page that
3
+ // observes the second tick.
4
+ import { observer } from 'mobx-react-lite'
5
+ import type { ClockSvc } from './clock-svc.ts'
6
+
7
+ /** how long ago, in the shortest honest unit. A stamp ahead of the clock
8
+ * reads as now: a clock skew is not a negative age */
9
+ export function ageWords(at: number, now: number): string {
10
+ const s = Math.max(0, Math.round((now - at) / 1000))
11
+ if (s < 60) return `${s}s ago`
12
+ if (s < 3600) return `${Math.round(s / 60)}m ago`
13
+ if (s < 86400) return `${Math.round(s / 3600)}h ago`
14
+ return `${Math.round(s / 86400)}d ago`
15
+ }
16
+
17
+ /** the absolute stamp, which is what a reader compares against their own
18
+ * edits. It never moves, so it is not observable */
19
+ export const stampWords = (at: number): string => {
20
+ const d = new Date(at)
21
+ return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`
22
+ }
23
+
24
+ export const TimeAgo = observer(({ clock, at, stamp }: { clock: ClockSvc; at: number; stamp?: boolean }) => (
25
+ <span className="ago" title={stampWords(at)}>{stamp === true ? `${stampWords(at)} ` : ''}{ageWords(at, clock.now)}</span>
26
+ ))
27
+
28
+ /** a moment ahead, in words that move with the clock, with the exact stamp in
29
+ * the tooltip. `words` is computed by the caller from `clock.now`, so this
30
+ * component only has to be the thing that observes the tick */
31
+ export const TimeAhead = observer(({ clock, words }: { clock: ClockSvc; words: (now: number) => { words: string; at: number | null } }) => {
32
+ const w = words(clock.now)
33
+ return <span className="ago" title={w.at == null ? undefined : stampWords(w.at)}>{w.words}</span>
34
+ })