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.
Files changed (252) hide show
  1. package/CHANGELOG.md +371 -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/generate-roll.ts +1 -1
  6. package/src/assets/kit-config.ts +6 -6
  7. package/src/assets/run-assets.ts +14 -10
  8. package/src/assets/server/studio-server.ts +6 -5
  9. package/src/check.ts +343 -0
  10. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  11. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  12. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  13. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  14. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
  15. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  16. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  17. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  18. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  19. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  20. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  21. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  22. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  23. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  24. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  25. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  26. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  27. package/src/checks/SK018-test-files-run.check.ts +28 -0
  28. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  29. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  30. package/src/checks/contracts.check.ts +20 -0
  31. package/src/cli/cmd_Asks.ts +3 -2
  32. package/src/cli/cmd_Assets.ts +2 -2
  33. package/src/cli/cmd_Bump.ts +1 -1
  34. package/src/cli/cmd_Check.ts +6 -6
  35. package/src/cli/cmd_Checks.ts +95 -0
  36. package/src/cli/cmd_Ci.ts +48 -13
  37. package/src/cli/cmd_Commands.ts +2 -6
  38. package/src/cli/cmd_ConfigKeys.ts +69 -0
  39. package/src/cli/cmd_Contracts.ts +1 -1
  40. package/src/cli/cmd_Fleet.ts +6 -2
  41. package/src/cli/cmd_Hooks.ts +1 -1
  42. package/src/cli/cmd_Keys.ts +43 -18
  43. package/src/cli/cmd_Mv.ts +2 -2
  44. package/src/cli/cmd_Prose.ts +10 -1
  45. package/src/cli/cmd_Releases.ts +2 -2
  46. package/src/cli/cmd_Serve.ts +1 -1
  47. package/src/cli/cmd_Start.ts +6 -6
  48. package/src/cli/cmd_Status.ts +25 -0
  49. package/src/cli/cmd_Tui.ts +2 -2
  50. package/src/cli.ts +7 -4
  51. package/src/collections/actions.ts +4 -4
  52. package/src/collections/checks.ts +258 -0
  53. package/src/collections/collection-types.ts +254 -1
  54. package/src/collections/comfy.ts +23 -0
  55. package/src/collections/md-body.ts +14 -0
  56. package/src/collections/pings.ts +38 -12
  57. package/src/collections/posts.ts +19 -3
  58. package/src/collections/providers.ts +198 -91
  59. package/src/collections/reflections.ts +41 -0
  60. package/src/collections/state-rules.ts +73 -0
  61. package/src/collections/test-time.ts +288 -0
  62. package/src/collections/tests.ts +181 -0
  63. package/src/collections/wikis.ts +66 -0
  64. package/src/comfy/comfy-panel.ts +292 -0
  65. package/src/contracts/apply-contracts.ts +17 -12
  66. package/src/contracts/contract-types.ts +16 -4
  67. package/src/contracts/contracts-engine.ts +78 -42
  68. package/src/contracts/manifests/discord.contract.ts +6 -6
  69. package/src/contracts/manifests/identity.contract.ts +3 -3
  70. package/src/contracts/manifests/itch.contract.ts +12 -15
  71. package/src/contracts/manifests/social.contract.ts +5 -5
  72. package/src/contracts/manifests/steam.contract.ts +16 -16
  73. package/src/contracts/run-contracts.ts +4 -3
  74. package/src/core/attach.ts +96 -0
  75. package/src/core/canonical-scripts.ts +25 -0
  76. package/src/core/check-ctx.ts +204 -14
  77. package/src/core/check-file-names.ts +112 -0
  78. package/src/core/check-pass.ts +84 -0
  79. package/src/core/check-runner.ts +796 -0
  80. package/src/core/config-keys.ts +256 -0
  81. package/src/core/config-rewrite.ts +77 -0
  82. package/src/core/discover-checks.ts +166 -90
  83. package/src/core/discover-legacy-checks.ts +26 -0
  84. package/src/core/doc-block.ts +13 -0
  85. package/src/core/floor-resolve.ts +264 -0
  86. package/src/core/floor.ts +20 -137
  87. package/src/core/git-bin.ts +24 -0
  88. package/src/core/git-root.ts +108 -0
  89. package/src/core/instance.ts +195 -0
  90. package/src/core/jobs.ts +118 -0
  91. package/src/core/layout.ts +52 -0
  92. package/src/core/legacy-shapes.ts +207 -0
  93. package/src/core/lib-root.ts +5 -0
  94. package/src/core/locked-write.ts +110 -39
  95. package/src/core/machine-commit.ts +5 -4
  96. package/src/core/migrate-layout.ts +557 -0
  97. package/src/core/motherhome.ts +2 -8
  98. package/src/core/nesting.ts +4 -12
  99. package/src/core/one-way-in-policy.ts +73 -17
  100. package/src/core/outcome-words.ts +45 -0
  101. package/src/core/package-manager.ts +86 -0
  102. package/src/core/parallel.ts +21 -0
  103. package/src/core/process-tree.ts +44 -0
  104. package/src/core/raw-access.ts +93 -0
  105. package/src/core/read-set.ts +152 -0
  106. package/src/core/run-check.ts +155 -213
  107. package/src/core/run-ci.ts +153 -189
  108. package/src/core/run-fix.ts +96 -0
  109. package/src/core/run-fixers.ts +53 -30
  110. package/src/core/self-update.ts +9 -3
  111. package/src/core/slots.ts +83 -0
  112. package/src/core/spawn-timeout.ts +34 -3
  113. package/src/core/when-block.ts +401 -0
  114. package/src/core/when.ts +135 -0
  115. package/src/core/write-status.ts +5 -2
  116. package/src/hooks/leak-guard.ts +10 -8
  117. package/src/hooks/one-way-in-guard.ts +13 -5
  118. package/src/hooks/root-identity.ts +2 -1
  119. package/src/hooks/run-hooks.ts +35 -23
  120. package/src/init-repo.ts +39 -23
  121. package/src/ledger/check-stats.ts +155 -0
  122. package/src/ledger/check-table.ts +179 -0
  123. package/src/ledger/db.ts +255 -0
  124. package/src/ledger/gate-checks.ts +433 -0
  125. package/src/ledger/git-key.ts +116 -77
  126. package/src/ledger/git-scan.ts +96 -35
  127. package/src/ledger/ledger.ts +116 -42
  128. package/src/ledger/live-runs.ts +111 -0
  129. package/src/ledger/live-timeline.ts +97 -0
  130. package/src/ledger/repo-view.ts +73 -0
  131. package/src/ledger/run-log.ts +448 -0
  132. package/src/ledger/run-timeline.ts +173 -0
  133. package/src/ledger/store.ts +23 -12
  134. package/src/ledger/suite-log.ts +161 -0
  135. package/src/ledger/toolchain-identity.ts +7 -1
  136. package/src/ledger/world-cache.ts +111 -0
  137. package/src/mv/mv-md.ts +16 -6
  138. package/src/problems/extract.ts +182 -0
  139. package/src/problems/problem-board.ts +37 -0
  140. package/src/problems/problem-line.ts +77 -0
  141. package/src/prose/prose-view.ts +90 -0
  142. package/src/prose/run-prose.ts +60 -19
  143. package/src/prose/ste.ts +140 -24
  144. package/src/reflections/reflections-queue.ts +81 -0
  145. package/src/release/git-facts.ts +15 -9
  146. package/src/release/run-bump.ts +6 -5
  147. package/src/release/run-release.ts +25 -11
  148. package/src/release/run-releases.ts +2 -2
  149. package/src/release/unit-gate.ts +7 -54
  150. package/src/release/unit-resolve.ts +282 -0
  151. package/src/release/units.ts +33 -217
  152. package/src/repo-config.ts +155 -85
  153. package/src/rules/rule.ts +36 -96
  154. package/src/rules/untrack-generated.ts +5 -4
  155. package/src/scaffold/files/vscode-tasks.json +10 -31
  156. package/src/scaffold/jsonc-spans.ts +113 -0
  157. package/src/scaffold/project-scaffold.ts +16 -43
  158. package/src/scan.ts +98 -0
  159. package/src/serve/gate-trigger.ts +148 -0
  160. package/src/serve/health-schedule.ts +102 -0
  161. package/src/serve/hub-app/chart-view.ts +179 -0
  162. package/src/serve/hub-app/check-view.tsx +345 -0
  163. package/src/serve/hub-app/check-words.ts +164 -0
  164. package/src/serve/hub-app/checks-svc.ts +280 -0
  165. package/src/serve/hub-app/clock-svc.ts +33 -0
  166. package/src/serve/hub-app/col-card.tsx +23 -39
  167. package/src/serve/hub-app/cols-svc.ts +44 -20
  168. package/src/serve/hub-app/comfy-svc.ts +171 -0
  169. package/src/serve/hub-app/comfy-view.tsx +81 -0
  170. package/src/serve/hub-app/dur.ts +41 -0
  171. package/src/serve/hub-app/hash-route.ts +121 -0
  172. package/src/serve/hub-app/header-bar.tsx +117 -0
  173. package/src/serve/hub-app/health-svc.ts +23 -28
  174. package/src/serve/hub-app/help.tsx +29 -0
  175. package/src/serve/hub-app/hub-app.tsx +122 -119
  176. package/src/serve/hub-app/hub-st.ts +115 -42
  177. package/src/serve/hub-app/hub-types.ts +117 -2
  178. package/src/serve/hub-app/hub.css +534 -192
  179. package/src/serve/hub-app/index.html +2 -0
  180. package/src/serve/hub-app/live-svc.ts +174 -0
  181. package/src/serve/hub-app/md.ts +15 -0
  182. package/src/serve/hub-app/media-svc.ts +4 -1
  183. package/src/serve/hub-app/media-view.tsx +15 -0
  184. package/src/serve/hub-app/next-run.ts +68 -0
  185. package/src/serve/hub-app/project-gutter.css +65 -0
  186. package/src/serve/hub-app/project-gutter.tsx +128 -0
  187. package/src/serve/hub-app/projects-svc.ts +226 -0
  188. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  189. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  190. package/src/serve/hub-app/prose-hits.css +21 -0
  191. package/src/serve/hub-app/run-strip.ts +47 -0
  192. package/src/serve/hub-app/runs-tabs.ts +24 -0
  193. package/src/serve/hub-app/test-view.tsx +107 -0
  194. package/src/serve/hub-app/tests-svc.ts +62 -0
  195. package/src/serve/hub-app/tests-view.tsx +238 -0
  196. package/src/serve/hub-app/time-ago.tsx +34 -0
  197. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  198. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  199. package/src/serve/hub-app/timeline-svc.ts +62 -0
  200. package/src/serve/hub-app/timeline-view.tsx +245 -0
  201. package/src/serve/hub-app/tree-build.ts +410 -0
  202. package/src/serve/hub-app/tree-row.tsx +42 -0
  203. package/src/serve/hub-app/tree-svc.ts +174 -0
  204. package/src/serve/hub-app/wikis-view.tsx +53 -0
  205. package/src/serve/other-repos.ts +79 -0
  206. package/src/serve/project-gutter-route.ts +350 -0
  207. package/src/serve/project-gutter.ts +449 -0
  208. package/src/serve/run-serve.ts +245 -16
  209. package/src/serve/same-origin.ts +14 -0
  210. package/src/serve/serve-hub.ts +213 -18
  211. package/src/shipkit.ts +19 -12
  212. package/src/smoke/run-smoke.ts +3 -2
  213. package/src/start-repo.ts +75 -36
  214. package/src/suite/judge-suites.ts +216 -18
  215. package/src/suite/kept-timings.ts +58 -0
  216. package/src/suite/runner-shapes.ts +88 -0
  217. package/src/suite/suite-cli.ts +147 -24
  218. package/src/suite/suite-cpu-preload.ts +92 -0
  219. package/src/suite/test-units.ts +243 -0
  220. package/src/ts/config.ts +4 -69
  221. package/src/ts/jsonc.ts +69 -0
  222. package/src/ts/lex.ts +143 -0
  223. package/src/ts/rename.ts +73 -10
  224. package/src/ts/source-graph.ts +28 -8
  225. package/src/ts/tsconfig-projects.ts +6 -97
  226. package/src/ts/tsconfig-resolve.ts +108 -0
  227. package/src/tui/build-steps-panel.ts +61 -0
  228. package/src/tui/checks-panel.ts +24 -16
  229. package/src/tui/run-tui.ts +2 -2
  230. package/src/tui/tui-panel.ts +5 -4
  231. package/src/wiki/graph-colors.ts +68 -0
  232. package/src/wiki/graph-mount.ts +7 -9
  233. package/src/wiki/graph-page.ts +42 -25
  234. package/src/wiki/graph-positions.ts +105 -0
  235. package/src/wiki/run-wiki.ts +1 -1
  236. package/src/wiki/serve-graph.ts +33 -3
  237. package/src/cli/cmd_Rules.ts +0 -51
  238. package/src/cli/cmd_Units.ts +0 -82
  239. package/src/collections/graphs.ts +0 -32
  240. package/src/contracts/contracts-once.ts +0 -43
  241. package/src/core/check-suite.ts +0 -302
  242. package/src/core/repair-contract-dirs.ts +0 -136
  243. package/src/core/task-runner.ts +0 -339
  244. package/src/ledger/gate-units.ts +0 -138
  245. package/src/ledger/unit-table.ts +0 -105
  246. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  247. package/src/rules/SK003-claude-md-shape.ts +0 -70
  248. package/src/rules/SK004-gitignore-policy.ts +0 -238
  249. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  250. package/src/rules/rule-registry.ts +0 -64
  251. package/src/rules/run-fix.ts +0 -72
  252. package/src/tui/tasks-panel.ts +0 -60
@@ -0,0 +1,179 @@
1
+ // The view state of THE run chart: its zoom, the width it has on screen and
2
+ // what is under the pointer, over the geometry (timeline-geometry.ts) and the
3
+ // drawing (timeline-chart.tsx). The live strip owns it, and nothing else in
4
+ // the hub draws a run. What differs is the source: folded, a rolling window
5
+ // that ends now; unfolded, the whole record opened on its last minutes.
6
+ import { makeAutoObservable, observableRef } from 'mobx'
7
+ import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
8
+ import {
9
+ anchorScroll, buildAxis, fitScale, fitWindow, layoutTimeline, maxZoom, PAD_PX, windowAxis, ZOOM_MIN, zoomStep, type Axis, type Layout, type Span,
10
+ } from './timeline-geometry.ts'
11
+
12
+ /** what a chart draws. `windowMs` null = the whole record, with idle
13
+ * stretches folded; a number = the rolling window that ends at `now` */
14
+ export type ChartSource = {
15
+ invocations: TimelineInvocation[]
16
+ now: number
17
+ windowMs: number | null
18
+ laneCap?: number
19
+ /** false = drawn at zoom 1 whatever the stored zoom says */
20
+ zoomable?: boolean
21
+ /** the runner's cap, drawn over the concurrency band */
22
+ cap?: number
23
+ /** the span the view opens on: at its home zoom this much time, ending
24
+ * now, fills the width. Absent = home is zoom 1, the whole source */
25
+ homeSpanMs?: number
26
+ }
27
+
28
+ /** what the tooltip says, and the viewport point it is fixed at */
29
+ export type ChartTip = { x: number; y: number; head: string; tone: string; lines: string[] }
30
+
31
+ export class ChartView {
32
+ /** 1 fits the whole source in the view. null = home: the zoom the source
33
+ * opens on, which moves with the width and the source */
34
+ zoom: number | null
35
+ /** the width the chart has on screen, 0 until it is measured */
36
+ viewW = 0
37
+ hover: ChartTip | null = null
38
+ /** FOLLOW: the right edge is now and the view advances with the clock.
39
+ * Off, the view stays put in absolute time until the reader comes back
40
+ * to the right edge or asks for live again */
41
+ follow = true
42
+ /** the scale the view keeps while it does not follow, so nothing on it
43
+ * moves as the record grows */
44
+ pinned: number | null = null
45
+ /** the moment the view stands still at while a tooltip is open, so the
46
+ * thing under the pointer can be read */
47
+ heldNow: number | null = null
48
+
49
+ /** `key` persists the zoom per browser; null keeps it for the tab */
50
+ constructor(private key: string | null, private source: () => ChartSource | null) {
51
+ this.zoom = this.readZoom()
52
+ makeAutoObservable<this, 'key' | 'source'>(this, { key: false, source: false, hover: observableRef })
53
+ }
54
+
55
+ private readZoom(): number | null {
56
+ if (this.key == null) return null
57
+ try {
58
+ const z = Number(globalThis.localStorage?.getItem(this.key) ?? '')
59
+ return Number.isFinite(z) && z >= ZOOM_MIN ? z : ZOOM_MIN
60
+ } catch {
61
+ return ZOOM_MIN
62
+ }
63
+ }
64
+
65
+ private get src(): ChartSource | null {
66
+ return this.source()
67
+ }
68
+ /** the moment the view is drawn at: the source's now, unless a tooltip
69
+ * holds it still */
70
+ get now(): number {
71
+ return this.heldNow ?? this.src?.now ?? 0
72
+ }
73
+ private get spans(): Span[] {
74
+ return (this.src?.invocations ?? []).map((i) => ({ start: i.started, end: i.running === true ? this.now : i.ended }))
75
+ }
76
+ /** where a rolling window starts, null over the whole record */
77
+ get from(): number | null {
78
+ const s = this.src
79
+ return s == null || s.windowMs == null ? null : this.now - s.windowMs
80
+ }
81
+ /** the scale at zoom 1 */
82
+ get fit(): number {
83
+ const s = this.src
84
+ const w = Math.max(200, this.viewW)
85
+ if (s == null) return 1
86
+ return this.from == null ? fitScale(this.spans, this.now, w) : fitWindow(this.from, this.now, w)
87
+ }
88
+ /** the runner's cap the source carries, drawn over the concurrency band */
89
+ get cap(): number | undefined {
90
+ return this.src?.cap
91
+ }
92
+ get maxZoom(): number {
93
+ return maxZoom(this.fit)
94
+ }
95
+ /** the zoom at which the source's home span of drawn time fills the
96
+ * width. Idle stretches are folded, so a record with less drawn time
97
+ * than the span opens fitted */
98
+ get homeZoom(): number {
99
+ const s = this.src
100
+ if (s?.homeSpanMs == null) return ZOOM_MIN
101
+ const px = Math.max(1, Math.max(200, this.viewW) - 2 * PAD_PX) / s.homeSpanMs
102
+ return Math.min(this.maxZoom, Math.max(ZOOM_MIN, px / this.fit))
103
+ }
104
+ /** the zoom in effect */
105
+ get level(): number {
106
+ if (this.src?.zoomable === false) return ZOOM_MIN
107
+ return Math.min(this.zoom ?? this.homeZoom, this.maxZoom)
108
+ }
109
+ /** the reader zoomed away from home */
110
+ get zoomed(): boolean {
111
+ return this.zoom != null
112
+ }
113
+ /** px per ms: the zoom's while following, the pinned one while not */
114
+ get scale(): number {
115
+ return this.follow || this.pinned == null ? this.fit * this.level : this.pinned
116
+ }
117
+ get layout(): Layout | null {
118
+ const s = this.src
119
+ if (s == null || this.viewW === 0) return null
120
+ return layoutTimeline(s.invocations, this.now, this.scale, { from: this.from ?? undefined, laneCap: s.laneCap, cap: s.cap })
121
+ }
122
+
123
+ private axisAt(scale: number): Axis {
124
+ return this.from == null ? buildAxis(this.spans, this.now, scale) : windowAxis(this.from, this.now, scale)
125
+ }
126
+
127
+ /** stop following: the view keeps its scale and its place in time */
128
+ detach(): void {
129
+ if (!this.follow) return
130
+ this.pinned = this.scale
131
+ this.follow = false
132
+ }
133
+ /** follow again: the right edge is now */
134
+ attach(): void {
135
+ this.follow = true
136
+ this.pinned = null
137
+ }
138
+ /** the reader moved the view: away from the right edge it stops
139
+ * following, back at the right edge it follows again */
140
+ scrolled(scrollLeft: number, clientWidth: number): void {
141
+ const w = this.layout?.axis.width
142
+ if (w == null) return
143
+ const atRight = scrollLeft + clientWidth >= w - 4
144
+ if (atRight && !this.follow) this.attach()
145
+ else if (!atRight && this.follow) this.detach()
146
+ }
147
+
148
+ setViewW(w: number): void {
149
+ this.viewW = w
150
+ }
151
+ /** a tooltip opens or closes. While one is open the view stands still */
152
+ setHover(t: ChartTip | null): void {
153
+ this.hover = t
154
+ if (t == null) this.heldNow = null
155
+ else if (this.heldNow == null) this.heldNow = this.src?.now ?? null
156
+ }
157
+ /** back to the zoom the source opens on */
158
+ home(): void {
159
+ this.zoom = null
160
+ }
161
+
162
+ /** zoom to `next` around the point `cx` of the viewport, and say where to
163
+ * scroll so the moment under that point stays under it */
164
+ private zoomTo(next: number, cx: number, scrollLeft: number): number {
165
+ const before = this.axisAt(this.scale)
166
+ this.zoom = Math.min(this.maxZoom, Math.max(ZOOM_MIN, next))
167
+ if (!this.follow) this.pinned = this.fit * this.level
168
+ if (this.key != null) {
169
+ try { globalThis.localStorage?.setItem(this.key, String(this.zoom)) } catch { /* private window: the zoom lasts the tab */ }
170
+ }
171
+ return anchorScroll(before, this.axisAt(this.scale), scrollLeft, cx)
172
+ }
173
+ wheel(deltaY: number, cx: number, scrollLeft: number): number {
174
+ return this.zoomTo(zoomStep(this.level, deltaY, this.maxZoom), cx, scrollLeft)
175
+ }
176
+ zoomBy(factor: number, cx: number, scrollLeft: number): number {
177
+ return this.zoomTo(this.level * factor, cx, scrollLeft)
178
+ }
179
+ }
@@ -0,0 +1,345 @@
1
+ // ONE GATE CHECK, on its own page, as ONE compact block: the id as a tag, the
2
+ // title, the sentence it wrote about itself, the exact command it runs, then
3
+ // one grid of facts, each a dim label and a short value whose explanation is
4
+ // the row's `(?)`. No box holds a single field. Navigation is the tree on the
5
+ // left, so there is no back link.
6
+ import { observer } from 'mobx-react-lite'
7
+ import { useEffect, useRef } from 'react'
8
+ import { CACHED } from '../../core/outcome-words.ts'
9
+ import { durationTone, fmtMs } from './dur.ts'
10
+ import { Help, TERMS } from './help.tsx'
11
+ import type { HubSt } from './hub-st.ts'
12
+ import type { SlowSuite, CheckFacts, RecentRun, WhenAt } from './hub-types.ts'
13
+ import { renderMd, renderMdInline } from './md.ts'
14
+ import { nextRun } from './next-run.ts'
15
+ import { ProseHits } from './prose-hits-view.tsx'
16
+ import { stampWords, TimeAgo, TimeAhead } from './time-ago.tsx'
17
+ import { checkName, commandOf, docPath, editorHref, lastRun, rememberedFact, repoPath, whatFacts, WHEN_OPTION, type ShortPath } from './check-words.ts'
18
+
19
+ /** which declaration is answering, so the switch never looks like it wrote
20
+ * something it did not. The CHIP is the short one, its help is the sentence */
21
+ const SOURCE_CHIP: Record<CheckFacts['whenSource'], string> = {
22
+ when: 'repo.config.ts',
23
+ 'check-file': 'the check file',
24
+ ruleExempt: 'ruleExempt',
25
+ optIn: 'optIn',
26
+ 'gate-export': 'gate = false',
27
+ defaults: 'defaults',
28
+ default: 'default',
29
+ }
30
+
31
+ const SOURCE_WORD: Record<CheckFacts['whenSource'], string> = {
32
+ when: 'set in the `when` block of `repo.config.ts`, which this switch writes',
33
+ 'check-file': 'declared by the check file itself, and the `when` block overrides it',
34
+ ruleExempt: 'from the older `ruleExempt`, which reads as off',
35
+ optIn: 'from the older `optIn` key, which reads as health',
36
+ 'gate-export': 'from `export const gate = false` in its own file',
37
+ defaults: 'from the `defaults` block, which removes the floor slot entirely',
38
+ default: 'nobody declared where this check runs, so it takes the default',
39
+ }
40
+
41
+ const WHEN_VALUES: WhenAt[] = ['always', 'release', 'health', 'off']
42
+
43
+ const VERDICT_WORD: Record<'pass' | 'warn' | 'fail' | 'skip', string> = {
44
+ pass: 'passed', warn: 'warned', fail: 'failed', skip: 'skipped',
45
+ }
46
+
47
+ // ── the pieces ───────────────────────────────────────────────────────────
48
+
49
+ /** one duration, in the one unit, in the one tone */
50
+ const Dur = ({ ms, ceiling }: { ms: number | null | undefined; ceiling?: number }) =>
51
+ ms == null
52
+ ? <span className="dim">not timed yet</span>
53
+ : <span className={`dnum ${durationTone(ms, ceiling) ?? ''}`}>{fmtMs(ms)}</span>
54
+
55
+ const MdHelp = ({ text }: { text: string }) => <Help text={<span className="md" dangerouslySetInnerHTML={{ __html: renderMd(text) }} />} />
56
+
57
+ /** one fact: the label dim and small on the left, the value on the right. On
58
+ * a wide screen the grid holds two of them per line */
59
+ export const Row = ({ k, help, wide, children }: { k: string; help?: string; wide?: boolean; children: React.ReactNode }) => (
60
+ <div className={wide === true ? 'cf wide' : 'cf'}>
61
+ <div className="ck">{k}{help == null ? null : <MdHelp text={help} />}</div>
62
+ <div className="cv">{children}</div>
63
+ </div>
64
+ )
65
+
66
+ /** a path, short, never split mid token, the full one in its tooltip */
67
+ export const PathCode = ({ p, href }: { p: ShortPath; href?: string | null }) => {
68
+ const code = <code className="cpath" title={p.full}>{p.short}</code>
69
+ return href == null ? code : <a href={href} title={p.full}>{code}</a>
70
+ }
71
+
72
+ /** the section title for what is not a fact: the output, the slow suites */
73
+ export const Section = ({ title, help, children }: { title: string; help?: string; children: React.ReactNode }) => (
74
+ <section className="csec">
75
+ <div className="cstitle">{title}{help != null ? <Help text={help} /> : null}</div>
76
+ {children}
77
+ </section>
78
+ )
79
+
80
+ /** the sentence the check states about itself, as markdown, under the title */
81
+ export const Says = ({ description }: { description: string }) =>
82
+ <div className="usays md" dangerouslySetInnerHTML={{ __html: renderMd(description) }} />
83
+
84
+ // ── what it runs ─────────────────────────────────────────────────────────
85
+
86
+ export const copy = (text: string): void => {
87
+ try { void navigator.clipboard?.writeText(text) } catch { /* a page served over plain http has no clipboard */ }
88
+ }
89
+
90
+ /** THE COMMAND, exactly as the gate spawns it from the repo root, or the one
91
+ * line saying its body is a function inside the gate */
92
+ export const CommandLine = ({ app, u }: { app: HubSt; u: CheckFacts }) => {
93
+ const c = commandOf(u, app.root)
94
+ if (c.kind === 'in-process')
95
+ return (
96
+ <div className="ccmd">
97
+ <Row k="runs" help="its body is a function, so it runs inside the gate process and a throw of its own is its own failure rather than the gate crashing" wide>
98
+ in the gate process
99
+ {c.file == null
100
+ ? <span className="dim">, its source ships inside the toolkit</span>
101
+ : <>, source <PathCode p={repoPath(c.file, app.root)} href={editorHref(app.root, c.file)} /></>}
102
+ </Row>
103
+ </div>
104
+ )
105
+ return (
106
+ <div className="ccmd">
107
+ <Row k="command" help="the exact argv the gate spawns from the repo root. Its exit code is truth: a non zero exit with no FAIL line still fails loud" wide>
108
+ <code className="cargv" title={u.cmd?.join(' ')}>
109
+ {c.tokens.map((t, n) => (
110
+ <span key={n}>
111
+ {n > 0 ? ' ' : null}
112
+ {t.short === u.file && editorHref(app.root, u.file) != null
113
+ ? <a className="tok" href={editorHref(app.root, u.file)!} title={t.full}>{t.short}</a>
114
+ : <span className="tok" title={t.full}>{t.short}</span>}
115
+ </span>
116
+ ))}
117
+ </code>
118
+ <button className="ccopy" title="copy the command" onClick={() => copy(c.copy)}>copy</button>
119
+ {c.script == null
120
+ ? null
121
+ : <div className="cscript">
122
+ <span className="dim">script <code className="cpath">{c.script.name}</code> runs</span> <code className="cargv">{c.script.body}</code>
123
+ </div>}
124
+ </Row>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ // ── the switch ───────────────────────────────────────────────────────────
130
+
131
+ /** RUN WHEN, one select named by effect. It writes `repo.config.ts` itself,
132
+ * through the managed block, and leaves the file dirty: a change of place is
133
+ * a change its owner reviews in a git client, never a commit made on their
134
+ * behalf. Choosing never asks for the reason before it writes. */
135
+ const RunWhen = observer(({ app, u }: { app: HubSt; u: CheckFacts }) => {
136
+ const s = app.checks
137
+ const g = s.gate
138
+ const shown = s.whenShown(u)
139
+ const reason = s.reasonFor(u.checkId)
140
+ return (
141
+ <Row k="run when" help="where this check runs. Changing it writes repo.config.ts and leaves the file for you to commit">
142
+ <span className="cwhen">
143
+ <select className="wsel" value={shown} disabled={s.gateBusy} onChange={(e) => s.chooseWhen(u.checkId, e.target.value as WhenAt)}>
144
+ {WHEN_VALUES.map((v) => <option key={v} value={v}>{WHEN_OPTION[v]}</option>)}
145
+ </select>
146
+ <Help text={
147
+ <span className="legend">
148
+ {WHEN_VALUES.map((v) => <span key={v} className="lrow"><b>{WHEN_OPTION[v]}</b> ({v}): {TERMS[v]}</span>)}
149
+ </span>
150
+ } />
151
+ <span className="stchip">{SOURCE_CHIP[u.whenSource]}</span>
152
+ <MdHelp text={SOURCE_WORD[u.whenSource]} />
153
+ {u.whenSource === 'when'
154
+ ? <button className="wclear" disabled={s.gateBusy} onClick={() => void s.clearWhen(u.checkId)}>drop the entry</button>
155
+ : null}
156
+ </span>
157
+ {shown === 'off'
158
+ ? <span className="cblock">
159
+ <input className="wreason" placeholder="why it is off (required, and it rides in the config)"
160
+ value={reason} onChange={(e) => s.setReason(u.checkId, e.target.value)} />
161
+ {u.when === 'off' && u.whenReason != null ? <span className="note">reason today: {u.whenReason}</span> : null}
162
+ <button disabled={s.gateBusy || reason.trim() === ''} onClick={() => void s.setWhen(u.checkId, 'off')}>
163
+ {u.when === 'off' ? 'change the reason' : 'turn it off'}
164
+ </button>
165
+ </span>
166
+ : null}
167
+ {g != null && g.state === 'stale'
168
+ ? <span className="cblock warnline">the managed block in {g.path} was hand edited: reconcile it, then the switch works again</span>
169
+ : null}
170
+ {g != null && g.dirty ? <span className="cblock note">{g.path} is edited and not committed, review it in your git client</span> : null}
171
+ {s.gateNote === '' ? null : <span className="cblock note">{s.gateNote}</span>}
172
+ </Row>
173
+ )
174
+ })
175
+
176
+ // ── the numbers ──────────────────────────────────────────────────────────
177
+
178
+ /** the last N outcomes as a bar row: an average hides the shape, and the
179
+ * reds it cannot even see */
180
+ const Bars = ({ runs, ceiling }: { runs: RecentRun[]; ceiling: number }) => {
181
+ if (runs.length === 0) return <div className="dim">no run on record yet</div>
182
+ const top = Math.max(...runs.map((r) => r.ms), 1)
183
+ return (
184
+ <div className="bars">
185
+ {runs.map((r, n) => (
186
+ <i key={`${r.at}-${n}`}
187
+ className={`bar ${r.replayed ? 'rep' : r.status === 'fail' ? 'red' : r.status === 'warn' ? 'warn' : r.status === 'skip' ? 'rep' : durationTone(r.ms, ceiling) ?? 'plain'}`}
188
+ style={{ height: `${r.replayed ? 8 : Math.max(10, Math.round((r.ms / top) * 100))}%` }}
189
+ title={`${stampWords(r.at)}: ${r.replayed ? CACHED : `${VERDICT_WORD[r.status]} in ${fmtMs(r.ms)}`}`} />
190
+ ))}
191
+ </div>
192
+ )
193
+ }
194
+
195
+ /** how the last answer came: `ran 3m ago in 412ms`, or `cached 16m ago` */
196
+ export const LastRunValue = observer(({ app, u }: { app: HubSt; u: CheckFacts }) => {
197
+ const l = lastRun(u)
198
+ if (l == null) return <span className="dim">never here</span>
199
+ return (
200
+ <>
201
+ <span className={`tl ${l.tone}`}>{l.word}</span>
202
+ {u.lastAt == null ? null : <> <TimeAgo clock={app.clock} at={u.lastAt} /></>}
203
+ {u.lastMs == null || u.replayed ? null : <> in <Dur ms={u.lastMs} ceiling={u.timeoutMs} /></>}
204
+ {l.verdict == null ? null : <span className={`tl ${l.tone}`}>, {l.verdict}</span>}
205
+ </>
206
+ )
207
+ })
208
+
209
+ export const Timings = ({ u }: { u: CheckFacts }) => (
210
+ <Row k="timings" wide help="what this check costs: its last run, the mean of the runs that really ran, the worst one on record, and the limit it is killed at">
211
+ <span className="ctimes">
212
+ <span><span className="ck">last</span> <Dur ms={u.lastMs} ceiling={u.timeoutMs} /></span>
213
+ <span><span className="ck">average</span> {u.samples === 0 ? <span className="dim">not timed yet</span> : <><Dur ms={u.avgMs} ceiling={u.timeoutMs} /> <span className="dim">over {u.samples}</span></>}</span>
214
+ <span><span className="ck">slowest</span> <Dur ms={u.slowestMs} ceiling={u.timeoutMs} />{u.failures > 0 ? <span className="dim">, {u.failures} red</span> : null}</span>
215
+ {/* a limit, never a measurement: it wears no tone of its own */}
216
+ <span><span className="ck">limit</span> <span className="dnum dim">{fmtMs(u.timeoutMs)}</span> <Help term="ceiling" /></span>
217
+ </span>
218
+ <Bars runs={u.recent ?? []} ceiling={u.timeoutMs} />
219
+ </Row>
220
+ )
221
+
222
+ const Remembered = ({ u }: { u: CheckFacts }) => {
223
+ const kept = rememberedFact(u)
224
+ return (
225
+ <Row k="remembered" help={kept.help}>
226
+ {kept.inputs.length === 0
227
+ ? kept.v
228
+ : <details className="cinputs">
229
+ <summary>{kept.v} <span className="dim">({kept.inputs.length})</span></summary>
230
+ <span className="winputs">{kept.inputs.map((p) => <code key={p} className="cpath">{p}</code>)}</span>
231
+ </details>}
232
+ </Row>
233
+ )
234
+ }
235
+
236
+ // ── what it printed ──────────────────────────────────────────────────────
237
+
238
+ /** each wire line renders as INLINE markdown, never as a fenced block: a
239
+ * `fix:` pointer is made of backtick spans and urls, and a fence turns a
240
+ * pointer a reader could click into text they have to retype */
241
+ const LogLines = ({ lines }: { lines: string[] }) => (
242
+ <>{lines.map((l, n) => <div key={`${n}-${l}`} className="wlogline" dangerouslySetInnerHTML={{ __html: renderMdInline(l) }} />)}</>
243
+ )
244
+
245
+ /** the lines of the run in flight, then the lines of the last one. It
246
+ * scrolls itself while a run streams, so the newest line is on screen */
247
+ const LastOutput = observer(({ app, lines }: { app: HubSt; lines: string[] }) => {
248
+ const live = app.checks.liveLines
249
+ const box = useRef<HTMLPreElement>(null)
250
+ useEffect(() => { if (box.current != null) box.current.scrollTop = box.current.scrollHeight }, [live.length])
251
+ return (
252
+ <Section title="last output" help="what it printed, with the fix pointers it emitted. While a run streams, this is the run">
253
+ {live.length > 0
254
+ ? <pre className="wlog" ref={box}><LogLines lines={live} /></pre>
255
+ : lines.length > 0
256
+ ? <pre className="wlog"><LogLines lines={lines} /></pre>
257
+ : <div className="dim">no lines from its last run: run it to see what it says</div>}
258
+ </Section>
259
+ )
260
+ })
261
+
262
+ /** the slow suites declared under the test step, each with the case it
263
+ * argued. Moving a suite between the tiers is the SAME managed block write,
264
+ * and it demands both answers exactly as `defineRepo` does. */
265
+ const SlowSuites = observer(({ app }: { app: HubSt }) => {
266
+ const s = app.checks
267
+ const declared: SlowSuite[] = s.gate?.tests.slow ?? []
268
+ return (
269
+ <Section title="slow suites" help="the suites the quick tier leaves out. Each one declares what it catches and why it cannot be fast">
270
+ <div className="wline">
271
+ {declared.length === 0
272
+ ? <span className="dim">none: every suite runs in the quick tier</span>
273
+ : <>the quick tier leaves {declared.length} out, `shipkit ci --full` runs them</>}
274
+ </div>
275
+ {declared.map((x) => (
276
+ <div key={x.file} className="wsuite">
277
+ <div className="wfile"><a href="#" title="its test file page" onClick={(e) => { e.preventDefault(); app.timeline.openTest(x.file) }}><code className="cpath">{x.file}</code></a>
278
+ <button disabled={s.gateBusy} onClick={() => void s.removeSlow(x.file)}>back to the quick tier</button>
279
+ </div>
280
+ <div className="note">catches: {x.catches}</div>
281
+ <div className="note">why not fast: {x.whyNotFast}</div>
282
+ </div>
283
+ ))}
284
+ <div className="wform">
285
+ <input placeholder="tests/heavy.test.ts" value={s.suite.file} onChange={(e) => s.setSuite('file', e.target.value)} />
286
+ <input placeholder="catches: the regression no fast test can catch" value={s.suite.catches} onChange={(e) => s.setSuite('catches', e.target.value)} />
287
+ <input placeholder="why not fast: what the cost is, and what you tried" value={s.suite.whyNotFast} onChange={(e) => s.setSuite('whyNotFast', e.target.value)} />
288
+ <button disabled={s.gateBusy || s.suite.file.trim() === ''} onClick={() => void s.addSlow()}>declare it slow</button>
289
+ </div>
290
+ </Section>
291
+ )
292
+ })
293
+
294
+ // ── the pages ────────────────────────────────────────────────────────────
295
+
296
+ export const CheckCard = observer(({ app, id }: { app: HubSt; id: string }) => {
297
+ const i = app.cols.find('checks', id)
298
+ if (i == null) return <div className="ph">check vanished, the inventory moved on</div>
299
+ const u = i.check
300
+ if (u == null) return <div className="ph">this row is not a gate check</div>
301
+ const live = app.checks.isRunning(u.checkId)
302
+ const name = checkName(u.checkId, i.title)
303
+ const doc = u.doc == null ? null : docPath(u.doc, app.root)
304
+ return (
305
+ <div className="colcard checkcard">
306
+ {/* the run button sits beside the thing it runs, and holds its
307
+ width so a click moves nothing on the page */}
308
+ <div className="chead">
309
+ {name.tag == null ? null : <span className="tid">{name.tag}</span>}
310
+ <h3 className={`ctitle tl ${i.state}`}>{name.title}</h3>
311
+ {u.advisory ? <span className="stchip warn">fails, does not block</span> : null}
312
+ <span className="sp" />
313
+ <button className="primary urun" disabled={app.checks.busy} onClick={() => void app.checks.run([u.checkId])}>
314
+ {live ? <><span className="spin" />running</> : 'run it now'}
315
+ </button>
316
+ </div>
317
+ <Says description={u.description} />
318
+ <CommandLine app={app} u={u} />
319
+ <div className="cfacts">
320
+ {whatFacts(u).map((f) => (
321
+ <Row key={f.k} k={f.k} help={f.help}>{f.tone == null ? f.v : <span className={`tl ${f.tone}`}>{f.v}</span>}</Row>
322
+ ))}
323
+ <RunWhen app={app} u={u} />
324
+ <Remembered u={u} />
325
+ {/* the help never depends on the second, so it reads the wall
326
+ clock and the row does not repaint on every tick */}
327
+ <Row k="next run" help={nextRun(u, app.cols.schedule, Date.now()).help}>
328
+ <TimeAhead clock={app.clock} words={(now) => nextRun(u, app.cols.schedule, now)} />
329
+ </Row>
330
+ <Row k="last run" help={u.status == null
331
+ ? 'no run of it is on record in this checkout'
332
+ : u.replayed ? TERMS.cached! : TERMS.ran!}>
333
+ <LastRunValue app={app} u={u} />
334
+ </Row>
335
+ {doc == null ? null : <Row k="owner" help="the page that OWNS the invariant, where a `fix:` pointer lands"><PathCode p={doc} /></Row>}
336
+ {u.deps != null && u.deps.length > 0 ? <Row k="waits on" help="the checks that must pass before this one starts">{u.deps.join(', ')}</Row> : null}
337
+ {u.group == null ? null : <Row k="group" help="a label several checks share, so `--skip <group>` parks all of them at once">{u.group}</Row>}
338
+ <Timings u={u} />
339
+ </div>
340
+ {u.build && u.checkId === 'test' ? <SlowSuites app={app} /> : null}
341
+ {u.checkId === 'SK014' ? <ProseHits app={app} /> : null}
342
+ <LastOutput app={app} lines={u.lastLines} />
343
+ </div>
344
+ )
345
+ })