rvlib-shipkit 1.2.0 → 1.3.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 (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/assets/run-assets.ts +3 -8
  5. package/src/check.ts +20 -6
  6. package/src/checks/SK001-package-json-shape.check.ts +14 -3
  7. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  8. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  9. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  10. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  11. package/src/checks/SK007-github-visibility.check.ts +9 -9
  12. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  13. package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
  14. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  15. package/src/checks/SK015-nested-repo.check.ts +5 -1
  16. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  17. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
  18. package/src/checks/SK024-identity-colors.check.ts +5 -0
  19. package/src/checks/SK025-release-drift.check.ts +58 -0
  20. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  21. package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
  22. package/src/checks/SK028-readme.check.ts +79 -0
  23. package/src/cli/cmd_Assets.ts +1 -1
  24. package/src/cli/cmd_Ci.ts +5 -0
  25. package/src/cli/cmd_CrossRepoActivity.ts +116 -0
  26. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  27. package/src/cli/cmd_Keys.ts +17 -3
  28. package/src/cli/cmd_Overview.ts +98 -0
  29. package/src/cli/cmd_Planned.ts +117 -0
  30. package/src/cli.ts +9 -1
  31. package/src/collections/checks.ts +3 -2
  32. package/src/collections/providers.ts +1 -1
  33. package/src/collections/test-time.ts +10 -1
  34. package/src/core/agent-spawn.ts +46 -0
  35. package/src/core/archived.ts +82 -0
  36. package/src/core/canonical-scripts.ts +12 -3
  37. package/src/core/check-runner.ts +40 -12
  38. package/src/core/discover-checks.ts +2 -0
  39. package/src/core/file-stamp.ts +20 -0
  40. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  41. package/src/core/identity-color.ts +1 -1
  42. package/src/core/repo-facts.ts +14 -0
  43. package/src/core/retention.ts +14 -5
  44. package/src/core/retired-checks.ts +7 -0
  45. package/src/core/run-check.ts +6 -2
  46. package/src/core/run-ci.ts +43 -12
  47. package/src/core/run-fix.ts +7 -2
  48. package/src/core/run-fixers.ts +10 -6
  49. package/src/core/wire.ts +11 -0
  50. package/src/fleet/scan-fleet.ts +2 -1
  51. package/src/home/activity-shape.ts +135 -0
  52. package/src/home/activity.ts +249 -0
  53. package/src/home/global-units.ts +3 -2
  54. package/src/home/home-routes.ts +32 -5
  55. package/src/home/pm.ts +37 -4
  56. package/src/home/process-rows.ts +205 -0
  57. package/src/home/resources.ts +35 -8
  58. package/src/home/run-daemon.ts +6 -6
  59. package/src/home/run-pm.ts +1 -0
  60. package/src/home/serve-web.ts +6 -3
  61. package/src/home/supervise.ts +16 -1
  62. package/src/home/widget-text.ts +119 -0
  63. package/src/hooks/run-hooks.ts +20 -3
  64. package/src/ledger/db.ts +1 -1
  65. package/src/ledger/gate-checks.ts +17 -3
  66. package/src/ledger/house-runs.ts +43 -0
  67. package/src/ledger/live-timeline.ts +4 -19
  68. package/src/ledger/run-log.ts +39 -10
  69. package/src/ledger/store.ts +19 -2
  70. package/src/ledger/world-cache.ts +51 -4
  71. package/src/plan/cadence.ts +21 -5
  72. package/src/plan/catch-up.ts +37 -0
  73. package/src/plan/plan-timeline.ts +99 -0
  74. package/src/plan/planner.ts +138 -13
  75. package/src/plan/repo-source.ts +120 -8
  76. package/src/release/run-release.ts +9 -2
  77. package/src/release/unit-resolve.ts +8 -12
  78. package/src/repo-config.ts +11 -3
  79. package/src/serve/house-runs.ts +53 -0
  80. package/src/serve/hub-app/activity-st.ts +168 -0
  81. package/src/serve/hub-app/activity-view.tsx +218 -0
  82. package/src/serve/hub-app/activity.css +78 -0
  83. package/src/serve/hub-app/admin-svc.ts +41 -0
  84. package/src/serve/hub-app/admin-view.tsx +42 -0
  85. package/src/serve/hub-app/chart-view.ts +12 -5
  86. package/src/serve/hub-app/check-words.ts +2 -0
  87. package/src/serve/hub-app/controls.css +32 -0
  88. package/src/serve/hub-app/cycle-button.tsx +36 -0
  89. package/src/serve/hub-app/dock-anim.ts +25 -0
  90. package/src/serve/hub-app/dock-st.ts +329 -0
  91. package/src/serve/hub-app/dock-view.tsx +68 -0
  92. package/src/serve/hub-app/dock.css +35 -0
  93. package/src/serve/hub-app/hash-route.ts +7 -1
  94. package/src/serve/hub-app/header-bar.tsx +79 -104
  95. package/src/serve/hub-app/home-st.ts +49 -12
  96. package/src/serve/hub-app/home-strip.css +65 -22
  97. package/src/serve/hub-app/home-strip.tsx +228 -97
  98. package/src/serve/hub-app/home.html +4 -0
  99. package/src/serve/hub-app/hub-app.tsx +96 -160
  100. package/src/serve/hub-app/hub.css +77 -37
  101. package/src/serve/hub-app/icons.tsx +23 -3
  102. package/src/serve/hub-app/index.html +4 -0
  103. package/src/serve/hub-app/keys.ts +42 -7
  104. package/src/serve/hub-app/live-svc.ts +24 -64
  105. package/src/serve/hub-app/machine-st.ts +64 -1
  106. package/src/serve/hub-app/md.ts +38 -5
  107. package/src/serve/hub-app/next-run.ts +1 -1
  108. package/src/serve/hub-app/palette-st.ts +11 -7
  109. package/src/serve/hub-app/palette-view.tsx +47 -0
  110. package/src/serve/hub-app/pill-row.tsx +65 -0
  111. package/src/serve/hub-app/planned-st.ts +21 -85
  112. package/src/serve/hub-app/planned-view.tsx +12 -7
  113. package/src/serve/hub-app/planned.css +5 -2
  114. package/src/serve/hub-app/project-list.css +43 -21
  115. package/src/serve/hub-app/project-list.tsx +122 -23
  116. package/src/serve/hub-app/projects-svc.ts +120 -12
  117. package/src/serve/hub-app/readme-svc.ts +38 -0
  118. package/src/serve/hub-app/readme-view.tsx +19 -0
  119. package/src/serve/hub-app/readme.css +11 -0
  120. package/src/serve/hub-app/repo-sections.tsx +61 -0
  121. package/src/serve/hub-app/repo-st.ts +25 -6
  122. package/src/serve/hub-app/runs-tabs.ts +1 -1
  123. package/src/serve/hub-app/sidebar.tsx +84 -0
  124. package/src/serve/hub-app/tests-view.tsx +31 -4
  125. package/src/serve/hub-app/timeline-chart.tsx +31 -27
  126. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  127. package/src/serve/hub-app/timeline-svc.ts +2 -2
  128. package/src/serve/hub-app/timeline-view.tsx +20 -3
  129. package/src/serve/hub-runtime.ts +17 -9
  130. package/src/serve/other-repos.ts +8 -45
  131. package/src/serve/project-gutter-route.ts +100 -20
  132. package/src/serve/project-gutter.ts +33 -11
  133. package/src/serve/readme-route.ts +57 -0
  134. package/src/serve/serve-hub.ts +13 -8
  135. package/src/serve/tracked-watch.ts +33 -1
  136. package/src/shipkit-files.ts +20 -4
  137. package/src/checks/SK023-stores-under-cap.check.ts +0 -49
  138. package/src/checks/contracts.check.ts +0 -20
@@ -1,18 +1,17 @@
1
1
  // THE ONE RUN CHART: every check run a bar at its true start and length,
2
2
  // parallel ones stacked in lanes, each invocation a band behind its bars, now
3
- // on the right. The live strip under the header draws it, folded over a
4
- // rolling window and unfolded over the whole record, and nothing else draws a
5
- // run. Geometry is timeline-geometry.ts, view state chart-view.ts; this file
6
- // only draws.
3
+ // on the right. Two views draw it and nothing else draws a run: the dock at
4
+ // the bottom of the page, what runs now in every repo, one group of lanes
5
+ // each; and a repo's runs page, that repo's whole record. Geometry is
6
+ // timeline-geometry.ts, view state chart-view.ts; this file only draws.
7
7
  import { observer } from 'mobx-react-lite'
8
8
  import { autoUpdate, FloatingPortal, useFloating } from '@floating-ui/react'
9
- import { useEffect, useLayoutEffect, useRef } from 'react'
9
+ import { useEffect, useLayoutEffect, useRef, type ReactNode } from 'react'
10
10
  import { middleware } from './floating.tsx'
11
- import type { RepoLane } from '../../ledger/live-timeline.ts'
12
11
  import type { TimelineBar } from '../../ledger/run-timeline.ts'
13
12
  import type { ChartTip, ChartView } from './chart-view.ts'
14
13
  import { durationTone, fmtMs } from './dur.ts'
15
- import { bandLabels, gapSlices, ROWS, ticks, xOf, type GapSlice, type PlacedBar } from './timeline-geometry.ts'
14
+ import { bandLabels, gapSlices, ROWS, ticks, type GapSlice, type PlacedBar, type PlacedGroup, type RowSize } from './timeline-geometry.ts'
16
15
  import { CACHED } from '../../core/outcome-words.ts'
17
16
 
18
17
  /** who started a run, in words. A starter this build does not name reads as
@@ -45,7 +44,7 @@ export type TipAt = { x: number; y: number }
45
44
  * elapsed time, when it started, the run and who started it */
46
45
  export const barTip = (b: PlacedBar, now: number, at: TipAt): ChartTip => ({
47
46
  ...at,
48
- head: b.id,
47
+ head: b.group == null ? b.id : `${b.group} · ${b.id}`,
49
48
  tone: b.status === 'fail' ? 'red' : b.status === 'warn' ? 'warn' : 'idle',
50
49
  lines: [verdictWords(b, now), `${clockWords(b.start)} · ${b.label} · ${byWords(b.by)}`],
51
50
  })
@@ -82,13 +81,14 @@ const Tip = observer(({ view }: { view: ChartView }) => {
82
81
  export type ChartProps = {
83
82
  view: ChartView
84
83
  compact?: boolean
85
- /** other repos with something in flight, one collapsed lane each */
86
- repos?: RepoLane[]
84
+ /** the lane sizes; compact draws `compact` and anything else `full` */
85
+ rows?: RowSize
87
86
  /** keep now in sight: while the reader has not scrolled away, the view
88
87
  * follows the right edge as the window slides */
89
88
  follow?: boolean
90
89
  onBar: (b: PlacedBar) => void
91
- onRepo?: (r: RepoLane) => void
90
+ /** what stands left of a group's lanes, when the chart has groups */
91
+ groupLabel?: (g: PlacedGroup) => ReactNode
92
92
  /** the zoom control, floating over the top right corner: shown while the
93
93
  * pointer is on the chart and while it is zoomed away from home */
94
94
  zoomable?: boolean
@@ -103,7 +103,7 @@ export const DRAG_PX = 4
103
103
  /** a press that moved this far is a pan, and never also a click */
104
104
  export const isPan = (x0: number, x: number): boolean => Math.abs(x - x0) >= DRAG_PX
105
105
 
106
- export const TimelineChart = observer(({ view, compact = false, repos = [], follow = false, onBar, onRepo, zoomable = false }: ChartProps) => {
106
+ export const TimelineChart = observer(({ view, compact = false, rows, follow = false, onBar, groupLabel, zoomable = false }: ChartProps) => {
107
107
  const cap = view.cap
108
108
  const ref = useRef<HTMLDivElement>(null)
109
109
  /** where to scroll once the next zoom has rendered */
@@ -188,8 +188,8 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
188
188
  placed.current = true
189
189
  }
190
190
  })
191
- const r = compact ? ROWS.compact : ROWS.full
192
- const lanes = (l?.lanes ?? 0) + repos.length
191
+ const r = ROWS[rows ?? (compact ? 'compact' : 'full')]
192
+ const lanes = l?.lanes ?? 0
193
193
  const lanesH = lanes * (r.lane + r.gap)
194
194
  const top = r.band + r.conc
195
195
  const h = top + Math.max(r.lane, lanesH) + r.axis
@@ -208,7 +208,8 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
208
208
  <div className="tlaxin" style={{ width: l.axis.width }}>
209
209
  <svg className="tlsvg" width={l.axis.width} height={h}>
210
210
  {l.bands.map((b) => (
211
- <rect key={b.runId} className={`tlband${b.reds > 0 ? ' red' : ''}${b.running ? ' run' : ''}`} x={b.x} y={0} width={Math.max(1, b.w)} height={top + lanesH} />
211
+ <rect key={b.runId} className={`tlband${b.reds > 0 ? ' red' : ''}${b.running ? ' run' : ''}`} x={b.x} width={Math.max(1, b.w)}
212
+ y={b.lane0 == null ? 0 : laneY(b.lane0)} height={b.lane0 == null ? top + lanesH : (b.lanes ?? 1) * (r.lane + r.gap) - r.gap} />
212
213
  ))}
213
214
  {compact ? null : bandLabels(l.bands).map((b) => (
214
215
  <text key={b.runId} className="tlbandlbl" x={b.x} y={11}
@@ -250,16 +251,9 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
250
251
  onMouseLeave={() => hover(null)}
251
252
  onClick={() => onBar(b)} />
252
253
  ))}
253
- {repos.map((rl, k) => {
254
- const x = xOf(l.axis, Math.max(rl.start, l.axis.segs[0]?.t0 ?? rl.start))
255
- const lane = l.lanes + k
256
- return (
257
- <rect key={rl.repo} className="tlb repo" x={x} y={laneY(lane)} width={Math.max(2, l.axis.nowX - x)} height={r.lane}
258
- onMouseEnter={(e) => hover({ ...tipAtEvent(e), head: rl.repo, tone: 'idle', lines: [`${rl.running} running: ${rl.checks.slice(0, 4).join(', ')}${rl.checks.length > 4 ? ', …' : ''}`, `${rl.label} · ${byWords(rl.by)}`, onRepo == null ? '' : 'click opens its hub when it serves'].filter((x) => x !== '') })}
259
- onMouseLeave={() => hover(null)}
260
- onClick={() => onRepo?.(rl)} />
261
- )
262
- })}
254
+ {(l.groups ?? []).slice(1).map((g) => (
255
+ <line key={g.key} className="tlgrp" x1={0} x2={l.axis.width} y1={laneY(g.lane0) - r.gap / 2} y2={laneY(g.lane0) - r.gap / 2} />
256
+ ))}
263
257
  <line className="tlnow" x1={l.axis.nowX} x2={l.axis.nowX} y1={0} y2={top + lanesH} />
264
258
  </svg>
265
259
  <Tip view={view} />
@@ -267,10 +261,20 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
267
261
  )}
268
262
  </div>
269
263
  )
270
- if (!zoomable && !follow) return chart
264
+ // a group's label stands in a gutter of its own, level with its lanes,
265
+ // so it stays in sight however far the chart is panned
266
+ const gutter = groupLabel == null || l?.groups == null ? null : (
267
+ <div className="tlgutter" style={{ height: h }}>
268
+ {l.groups.map((g) => (
269
+ <div key={g.key} className="tlglbl" style={{ top: laneY(g.lane0), height: g.lanes * (r.lane + r.gap) - r.gap }}>{groupLabel(g)}</div>
270
+ ))}
271
+ </div>
272
+ )
273
+ const body = gutter == null ? chart : <div className="tlgroups">{gutter}{chart}</div>
274
+ if (!zoomable && !follow) return body
271
275
  return (
272
276
  <div className={`tlchart${view.zoomed ? ' zoomed' : ''}`}>
273
- {chart}
277
+ {body}
274
278
  <span className="tlfloat">
275
279
  {follow && !view.follow
276
280
  ? <button className="tllive" onClick={() => view.attach()} title={`the view stays where it is; back to now and follow it (${FOLLOW_KEY})`}>● live</button>
@@ -1,8 +1,9 @@
1
1
  // The horizontal run timeline, as geometry: one time axis with now on the
2
2
  // right, every bar at its true start and length, parallel bars stacked in
3
3
  // lanes, and the zoom math. Pure, so a test pins every position and the view
4
- // only draws what this returns. The live strip is its one user, with two
5
- // windows: folded, a rolling window that ends now; unfolded, the whole record.
4
+ // only draws what this returns. Two users: the dock at the bottom of the
5
+ // page, a rolling window that ends now with one group of lanes per repo, and
6
+ // a repo's runs page, its whole record.
6
7
  //
7
8
  // ONE rule bends a plain linear axis over the record: an idle stretch longer
8
9
  // than GAP_MS is folded to a fixed GAP_PX slice that says how long it was,
@@ -30,11 +31,14 @@ export const TICK_MIN_PX = 72
30
31
  export type Span = { start: number; end: number }
31
32
 
32
33
  /** the rows of a chart, in px: band labels, the concurrency band, the lanes,
33
- * the time axis. Compact is the folded strip: thin lanes and nothing else */
34
+ * the time axis. Compact is thin lanes and nothing else */
34
35
  export const ROWS = {
35
36
  full: { band: 16, conc: 20, lane: 8, gap: 2, axis: 18 },
36
37
  compact: { band: 0, conc: 0, lane: 3, gap: 1, axis: 0 },
38
+ /** the slim dock: compact, with lanes a repo's label fits beside */
39
+ slim: { band: 0, conc: 0, lane: 4, gap: 1, axis: 0 },
37
40
  } as const
41
+ export type RowSize = keyof typeof ROWS
38
42
 
39
43
  /** how tall a chart with `lanes` lanes draws */
40
44
  export const chartHeight = (lanes: number, compact = false): number => {
@@ -209,8 +213,15 @@ export function gapSlices(a: Axis, charPx = 6, padPx = 3): GapSlice[] {
209
213
  })
210
214
  }
211
215
 
212
- export type PlacedBar = TimelineBar & { x: number; w: number; lane: number; runId: string; label: string; by: string }
213
- export type PlacedBand = { runId: string; label: string; x: number; w: number; reds: number; running: boolean }
216
+ export type PlacedBar = TimelineBar & { x: number; w: number; lane: number; runId: string; label: string; by: string; group?: string }
217
+ /** `lane0` and `lanes`: the lanes of its group, when the chart has groups */
218
+ export type PlacedBand = { runId: string; label: string; x: number; w: number; reds: number; running: boolean; lane0?: number; lanes?: number }
219
+
220
+ /** one repo's runs, a group of lanes of its own */
221
+ export type ChartGroup = { key: string; invocations: TimelineInvocation[] }
222
+ /** a group as drawn: its first lane, how many it takes, how many of its bars
223
+ * did not fit */
224
+ export type PlacedGroup = { key: string; lane0: number; lanes: number; hidden: number }
214
225
 
215
226
  /** one step of the concurrency band: `n` checks in flight from x for w px */
216
227
  export type ConcStep = { x: number; w: number; n: number; over: boolean }
@@ -225,6 +236,8 @@ export type Layout = {
225
236
  conc: ConcStep[]
226
237
  /** the most in flight at once in view */
227
238
  concMax: number
239
+ /** the groups, top to bottom, when the chart has groups */
240
+ groups?: PlacedGroup[]
228
241
  }
229
242
 
230
243
  /** how many bars are in flight at each moment, as a step function: the
@@ -312,3 +325,25 @@ export function layoutTimeline(invs: TimelineInvocation[], now: number, pxPerMs:
312
325
  })
313
326
  return { axis, bars, bands, lanes: count, hidden, conc, concMax: Math.max(0, ...series.map((p) => p.n)) }
314
327
  }
328
+
329
+ /** every group on one rolling window, stacked top to bottom: each packs its
330
+ * own lanes, at least `minLanes` so its label fits beside them and at most
331
+ * `laneCap`, and the concurrency band counts every group's bars together. A
332
+ * window only: over the whole record each group would fold its own idle
333
+ * stretches, and the groups would not share one axis */
334
+ export function layoutGroups(groups: ChartGroup[], now: number, pxPerMs: number, o: { from: number; laneCap?: number; minLanes?: number; cap?: number }): Layout {
335
+ const all = layoutTimeline(groups.flatMap((g) => g.invocations), now, pxPerMs, { from: o.from, cap: o.cap, laneCap: 0 })
336
+ const bars: PlacedBar[] = []
337
+ const bands: PlacedBand[] = []
338
+ const placed: PlacedGroup[] = []
339
+ let lane0 = 0
340
+ for (const g of groups) {
341
+ const l = layoutTimeline(g.invocations, now, pxPerMs, { from: o.from, laneCap: o.laneCap })
342
+ const lanes = Math.max(o.minLanes ?? 1, l.lanes)
343
+ for (const b of l.bars) bars.push({ ...b, lane: b.lane + lane0, group: g.key })
344
+ for (const b of l.bands) bands.push({ ...b, lane0, lanes })
345
+ placed.push({ key: g.key, lane0, lanes, hidden: l.hidden })
346
+ lane0 += lanes
347
+ }
348
+ return { axis: all.axis, bars, bands, lanes: lane0, hidden: placed.reduce((a, g) => a + g.hidden, 0), conc: all.conc, concMax: all.concMax, groups: placed }
349
+ }
@@ -1,8 +1,8 @@
1
1
  // The runs page, as it holds the record: what the server shaped from the run
2
2
  // record (ledger/run-timeline.ts), and which invocations the reader opened.
3
3
  // Loaded when the page opens and again after a run ends, so it is never a
4
- // poll the rest of the page pays for. The live strip's unfolded chart reads
5
- // the same record from here for the runs older than its own window.
4
+ // poll the rest of the page pays for. The chart over the runs page reads the
5
+ // same record from here for the runs older than the live window.
6
6
  import { testId } from '../../collections/collection-types.ts'
7
7
  import { makeAutoObservable, observableRef, runInAction } from 'mobx'
8
8
  import type { Timeline } from '../../ledger/run-timeline.ts'
@@ -5,13 +5,14 @@
5
5
  // Every line sits on a grid of fixed columns (the .rline grids in hub.css):
6
6
  // numbers right aligned in tabular figures, ran and cached each a column of
7
7
  // their own told apart by colour, and no separator glyph on any line. The
8
- // shaping is the server's (ledger/run-timeline.ts). The chart of every run in
9
- // time is the live strip under the header, not this page.
8
+ // shaping is the server's (ledger/run-timeline.ts). Over the tabs, the one run
9
+ // chart (timeline-chart.tsx) draws this repo's whole record in time; what
10
+ // runs now in every repo is the dock at the bottom of the page.
10
11
  import { observer } from 'mobx-react-lite'
11
12
  import { useEffect } from 'react'
12
13
  import { CACHED, RAN } from '../../core/outcome-words.ts'
13
14
  import type { InvocationSummary, SlowCheck, SuiteToFix, Timeline, TimelineBar, TimelineInvocation } from '../../ledger/run-timeline.ts'
14
- import { byWords, verdictWords } from './timeline-chart.tsx'
15
+ import { byWords, TimelineChart, verdictWords } from './timeline-chart.tsx'
15
16
  import { durationTone, fmtMs } from './dur.ts'
16
17
  import { Help } from './help.tsx'
17
18
  import type { RepoSt } from './repo-st.ts'
@@ -223,6 +224,21 @@ const Tabs = observer(({ app, on }: { app: RepoSt; on: RunsTab }) => {
223
224
  )
224
225
  })
225
226
 
227
+ /** what the chart over the tabs is, behind its (?) */
228
+ export const RECORD_HELP = 'every check this repo ran, on record: the run bands, the time axis, and over the lanes how many checks ran at once, with the cap dashed and the stretches past it in orange. It opens on the last ten minutes and follows now: the right edge is the present. The wheel zooms and a drag pans; leaving the right edge stops the view where it is so a bar can be read, and ● live, the k key or panning back to the right edge follows now again. While a tooltip is open the view stands still. fit shows every run on record, and a click on a bar opens its check. What runs in every repo right now is the dock at the bottom of the page'
229
+
230
+ /** this repo's whole record in time, its height held so it never jumps
231
+ * under the eye as a run adds lanes and ends */
232
+ const RecordChart = observer(({ app }: { app: RepoSt }) => {
233
+ const l = app.live
234
+ return (
235
+ <div className="rchart" style={{ height: l.held.h }}>
236
+ <span className="rchelp"><Help text={RECORD_HELP} /></span>
237
+ <TimelineChart view={l.chart} follow zoomable onBar={(b) => app.cols.select('checks', `check:${b.id}`)} />
238
+ </div>
239
+ )
240
+ })
241
+
226
242
  export const TimelineView = observer(({ app }: { app: RepoSt }) => {
227
243
  const t = app.timeline
228
244
  useEffect(() => { void t.load() }, [t])
@@ -230,6 +246,7 @@ export const TimelineView = observer(({ app }: { app: RepoSt }) => {
230
246
  const on = runsTab(app.sel?.t === 'item' ? app.sel.id : null)
231
247
  return (
232
248
  <div className="colcard tlcard">
249
+ <RecordChart app={app} />
233
250
  <Tabs app={app} on={on} />
234
251
  {t.error !== '' ? <div className="warnline">{t.error}</div> : null}
235
252
  {d == null ? <div className="dim twait"><span className="spin" />reading the run record…</div> : (
@@ -42,6 +42,7 @@ import { liveJobs, startJob } from '../core/jobs.ts'
42
42
  import { followJob } from '../core/job-follow.ts'
43
43
  import { RUN_JOB } from '../home/home-words.ts'
44
44
  import { OtherRepos } from './other-repos.ts'
45
+ import { HouseRuns } from './house-runs.ts'
45
46
  import { recordStamp } from './project-gutter.ts'
46
47
  import { RepoProcesses } from './repo-processes.ts'
47
48
  import { shapeTimeline } from '../ledger/run-timeline.ts'
@@ -63,6 +64,13 @@ export { defaultHubPort }
63
64
 
64
65
  export type ServeIo = { print: (l: string) => void; printErr: (l: string) => void }
65
66
 
67
+ /** a hub serving on its own's view of the house: its own repo first, then
68
+ * every other house repo on this disk */
69
+ const houseOf = (root: string, name: string): HouseRuns => {
70
+ const others = new OtherRepos(root)
71
+ return new HouseRuns(() => [{ name, path: root }, ...others.repos().map((r) => ({ name: r.name, path: r.path }))])
72
+ }
73
+
66
74
 
67
75
  // the BASE-CONTEXT wiki: the shared wiki this repo's CLAUDE.md include chain
68
76
  // points at (`@~/dev/<other-repo>/wiki/index.md`). It lives in ANOTHER repo and
@@ -291,10 +299,10 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
291
299
  // nothing runs in this process yet, so a live row holding its pid was
292
300
  // left by an earlier image of it: `bun --watch` restarts in place
293
301
  if (record !== '') sweepLive(record, Date.now(), { ownPid: process.pid })
294
- // the live strip asks every second, from every open tab: the record is
295
- // read only when its files moved, and one shaped answer serves every
296
- // request inside LIVE_SHARE_MS
297
- const others = new OtherRepos(root)
302
+ // the page asks every second, from every open tab: the record is read
303
+ // only when its files moved, and one shaped answer serves every request
304
+ // inside LIVE_SHARE_MS
305
+ let house: HouseRuns | null = null
298
306
  let local: { stamp: string; at: number; live: ReturnType<typeof readLive>; runs: ReturnType<typeof readRunsSince>; invocations: ReturnType<typeof readInvocations> } | null = null
299
307
  let shaped: { at: number; view: LiveView } | null = null
300
308
  const LIVE_SHARE_MS = 500
@@ -308,7 +316,7 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
308
316
  const since = now - LIVE_HISTORY_MS
309
317
  local = { stamp, at: now, live: readLive(record), runs: readRunsSince(record, since), invocations: readInvocations(record, since) }
310
318
  }
311
- const view = shapeLive({ now, cap: parallelCap(), live: local.live, runs: local.runs, invocations: local.invocations, others: others.read() })
319
+ const view = shapeLive({ now, cap: parallelCap(), live: local.live, runs: local.runs, invocations: local.invocations })
312
320
  shaped = { at: now, view }
313
321
  return view
314
322
  }
@@ -342,7 +350,10 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
342
350
  // every run in flight, whoever started it: they all write the same
343
351
  // record, so a terminal gate shows here the same as a button's
344
352
  live,
345
- repoHub: (repo) => others.hubOf(repo),
353
+ // the dock of a hub serving on its own: this repo and every other
354
+ // house repo, built on the first ask (a hub the daemon loads is never
355
+ // asked, the daemon answers for the house)
356
+ house: () => (house ??= houseOf(root, c.name)).view(),
346
357
  timeline: async () => {
347
358
  const ceilings: Record<string, number> = {}
348
359
  const budgets: Record<string, number> = {}
@@ -456,8 +467,6 @@ export type LoadHubOpts = {
456
467
  ownTimers: boolean
457
468
  /** re-import repo.config.ts rather than reuse the module already loaded */
458
469
  fresh?: boolean
459
- /** overrides where a lane click of the live strip sends another repo */
460
- repoHub?: HubDeps['repoHub']
461
470
  /** every gate runs as a detached job this host follows */
462
471
  jobs?: GateJobs
463
472
  }
@@ -472,7 +481,6 @@ let warming: Promise<void> = Promise.resolve()
472
481
  export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promise<HubRuntime> {
473
482
  const c = await loadRepoConfig(root, { fresh: o.fresh === true })
474
483
  const deps = await resolveHubDeps(root, c, io, { port: o.port, base: o.base, jobs: o.jobs, planName: o.planName })
475
- if (o.repoHub != null) deps.repoHub = o.repoHub
476
484
  const name = deps.plan.repo
477
485
  deps.planned = () => o.plan.rows(name)
478
486
  deps.ask = (check) => {
@@ -1,16 +1,11 @@
1
- // The OTHER house repos on this disk, as the live strip reads them: their
2
- // runs in flight from their own run record, read only, and their hub url
3
- // when one serves. Which repos count is houseRepos (project-gutter.ts), the
4
- // same discovery the project gutter lists, minus this repo.
5
- //
6
- // Cheap on purpose, because the strip asks every second: the list is read at
7
- // most every LIST_TTL_MS, and a repo's record is opened only when its file or
8
- // its write ahead log moved since the last read.
9
- import { existsSync, realpathSync } from 'fs'
1
+ // The OTHER house repos on this disk, as a hub serving on its own lists them
2
+ // for the dock (house-runs.ts). Which repos count is houseRepos
3
+ // (project-gutter.ts), the same discovery the project gutter lists, minus
4
+ // this repo. The list is read at most every LIST_TTL_MS, since the dock asks
5
+ // every few seconds.
6
+ import { realpathSync } from 'fs'
10
7
  import { homedir } from 'os'
11
- import type { LiveRow } from '../ledger/live-runs.ts'
12
- import { readLiveReadonly } from '../ledger/run-log.ts'
13
- import { houseRepos, hubPortOf, recordOf, recordStamp, type WiredRepo } from './project-gutter.ts'
8
+ import { houseRepos, type WiredRepo } from './project-gutter.ts'
14
9
 
15
10
  /** how long the list of repos is trusted before the dev root is read again */
16
11
  export const LIST_TTL_MS = 30_000
@@ -30,11 +25,10 @@ export function otherHouseRepos(root: string, userHome: string = homedir()): Wir
30
25
  return repos.filter((r) => real(r.path) !== me)
31
26
  }
32
27
 
33
- /** the other repos and their live rows, re-read only when something moved */
28
+ /** the other repos, the list re-read at most every LIST_TTL_MS */
34
29
  export class OtherRepos {
35
30
  private list: WiredRepo[] = []
36
31
  private listedAt = -Infinity
37
- private rows = new Map<string, { stamp: string; rows: LiveRow[] }>()
38
32
 
39
33
  constructor(private root: string, private now: () => number = Date.now, private userHome: string = homedir()) {}
40
34
 
@@ -45,35 +39,4 @@ export class OtherRepos {
45
39
  }
46
40
  return this.list
47
41
  }
48
-
49
- /** every repo's live rows, as its record holds them */
50
- read(): { repo: string; rows: LiveRow[] }[] {
51
- const out: { repo: string; rows: LiveRow[] }[] = []
52
- for (const r of this.repos()) {
53
- const path = recordOf(r.path)
54
- if (path == null || !existsSync(path)) continue
55
- const stamp = recordStamp(path)
56
- let hit = this.rows.get(r.path)
57
- if (hit == null || hit.stamp !== stamp) {
58
- hit = { stamp, rows: readLiveReadonly(path) }
59
- this.rows.set(r.path, hit)
60
- }
61
- out.push({ repo: r.name, rows: hit.rows })
62
- }
63
- return out
64
- }
65
-
66
- /** a repo's hub url when it serves, checked by asking it who it is */
67
- async hubOf(repo: string): Promise<string | null> {
68
- const r = this.repos().find((x) => x.name === repo)
69
- if (r == null) return null
70
- const url = `http://localhost:${hubPortOf(r.path)}`
71
- try {
72
- const res = await fetch(`${url}/hub.json`, { signal: AbortSignal.timeout(500) })
73
- const j = (await res.json()) as { hub?: string; root?: string }
74
- return j.hub === 'shipkit' && j.root != null && real(j.root) === real(r.path) ? url : null
75
- } catch {
76
- return null
77
- }
78
- }
79
42
  }