rvlib-shipkit 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/asks/asks-queue.ts +4 -2
  5. package/src/assets/kits/identity-kit.ts +15 -13
  6. package/src/assets/run-assets.ts +12 -2
  7. package/src/check.ts +73 -98
  8. package/src/checks/SK001-package-json-shape.check.ts +26 -4
  9. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  10. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  11. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  12. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  13. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  14. package/src/checks/SK007-github-visibility.check.ts +4 -2
  15. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  16. package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
  17. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  18. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  19. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  20. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  21. package/src/checks/SK014-prose-ste.check.ts +20 -6
  22. package/src/checks/SK015-nested-repo.check.ts +4 -2
  23. package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
  24. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  25. package/src/checks/SK018-test-files-run.check.ts +6 -2
  26. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  27. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  28. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  29. package/src/checks/SK024-identity-colors.check.ts +5 -3
  30. package/src/checks/SK025-release-drift.check.ts +4 -5
  31. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  32. package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
  33. package/src/checks/SK028-readme.check.ts +10 -2
  34. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  35. package/src/cli/cmd_Asks.ts +5 -5
  36. package/src/cli/cmd_Assets.ts +7 -7
  37. package/src/cli/cmd_Bump.ts +12 -11
  38. package/src/cli/cmd_Check.ts +8 -7
  39. package/src/cli/cmd_Checks.ts +7 -6
  40. package/src/cli/cmd_Ci.ts +12 -11
  41. package/src/cli/cmd_Commands.ts +16 -13
  42. package/src/cli/cmd_ConfigKeys.ts +7 -6
  43. package/src/cli/cmd_Contracts.ts +3 -3
  44. package/src/cli/cmd_CrossRepoActivity.ts +10 -6
  45. package/src/cli/cmd_Fleet.ts +9 -7
  46. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  47. package/src/cli/cmd_Home.ts +23 -17
  48. package/src/cli/cmd_Hooks.ts +6 -5
  49. package/src/cli/cmd_Init.ts +5 -4
  50. package/src/cli/cmd_Keys.ts +8 -7
  51. package/src/cli/cmd_Mv.ts +7 -6
  52. package/src/cli/cmd_Needs.ts +4 -3
  53. package/src/cli/cmd_Overview.ts +4 -3
  54. package/src/cli/cmd_Pb.ts +2 -2
  55. package/src/cli/cmd_Planned.ts +6 -5
  56. package/src/cli/cmd_Prose.ts +5 -5
  57. package/src/cli/cmd_Reflect.ts +7 -7
  58. package/src/cli/cmd_Release.ts +6 -5
  59. package/src/cli/cmd_Releases.ts +5 -4
  60. package/src/cli/cmd_Serve.ts +4 -3
  61. package/src/cli/cmd_Smoke.ts +5 -4
  62. package/src/cli/cmd_Start.ts +7 -6
  63. package/src/cli/cmd_Status.ts +4 -3
  64. package/src/cli/cmd_Tui.ts +3 -2
  65. package/src/cli/cmd_When.ts +71 -0
  66. package/src/cli/cmd_Wiki.ts +4 -4
  67. package/src/cli.ts +7 -2
  68. package/src/collections/checks.ts +2 -0
  69. package/src/collections/collection-types.ts +24 -4
  70. package/src/collections/providers.ts +6 -2
  71. package/src/contracts/apply-contracts.ts +9 -8
  72. package/src/contracts/contract-types.ts +8 -2
  73. package/src/contracts/contracts-engine.ts +34 -13
  74. package/src/contracts/manifests/identity.contract.ts +6 -5
  75. package/src/contracts/manifests/social.contract.ts +4 -2
  76. package/src/contracts/manifests/steam.contract.ts +7 -3
  77. package/src/contracts/web-surface.ts +59 -0
  78. package/src/core/budget.ts +19 -9
  79. package/src/core/canonical-scripts.ts +1 -1
  80. package/src/core/check-runner.ts +63 -25
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +31 -7
  83. package/src/core/layout.ts +2 -0
  84. package/src/core/legacy-shapes.ts +39 -10
  85. package/src/core/motherhome.ts +88 -66
  86. package/src/core/one-way-in-policy.ts +2 -2
  87. package/src/core/outcome-words.ts +18 -2
  88. package/src/core/port-check-cli.ts +52 -0
  89. package/src/core/port-check-file.ts +295 -0
  90. package/src/core/port-check-files.ts +31 -0
  91. package/src/core/retention.ts +0 -14
  92. package/src/core/run-check.ts +14 -14
  93. package/src/core/run-ci.ts +11 -7
  94. package/src/core/run-fix.ts +30 -23
  95. package/src/core/run-fixers.ts +42 -5
  96. package/src/core/runs-matrix.ts +179 -0
  97. package/src/core/runs.ts +242 -0
  98. package/src/core/update-policy.ts +20 -0
  99. package/src/core/when-block.ts +29 -30
  100. package/src/core/when.ts +41 -103
  101. package/src/core/wire.ts +34 -9
  102. package/src/fleet/scan-fleet.ts +20 -4
  103. package/src/fleet/update-fleet.ts +6 -6
  104. package/src/home/activity-shape.ts +4 -1
  105. package/src/home/activity.ts +22 -15
  106. package/src/home/gui-app/ShipkitGui.swift +144 -25
  107. package/src/home/gui-app.ts +93 -30
  108. package/src/home/home-launchd.ts +30 -6
  109. package/src/home/home-routes.ts +14 -2
  110. package/src/home/home-start.ts +33 -5
  111. package/src/home/hubs.ts +5 -5
  112. package/src/home/machine-stores.ts +4 -2
  113. package/src/home/open-app.ts +23 -10
  114. package/src/home/pm-config.ts +15 -11
  115. package/src/home/pm-inputs.ts +76 -0
  116. package/src/home/pm.ts +11 -3
  117. package/src/home/process-rows.ts +23 -6
  118. package/src/home/resources.ts +67 -15
  119. package/src/home/run-daemon.ts +6 -1
  120. package/src/home/run-pm.ts +30 -5
  121. package/src/home/serve-web.ts +2 -2
  122. package/src/home/supervise.ts +24 -1
  123. package/src/home/widgets.ts +32 -3
  124. package/src/hooks/commit-message.ts +2 -3
  125. package/src/hooks/run-hooks.ts +60 -6
  126. package/src/init-repo.ts +6 -6
  127. package/src/ledger/gate-checks.ts +115 -61
  128. package/src/ledger/git-key.ts +5 -1
  129. package/src/ledger/ledger.ts +5 -5
  130. package/src/ledger/store.ts +11 -0
  131. package/src/ledger/toolchain-identity.ts +45 -27
  132. package/src/ledger/world-cache.ts +17 -15
  133. package/src/plan/repo-source.ts +18 -39
  134. package/src/release/run-release.ts +5 -0
  135. package/src/repo-config.ts +36 -56
  136. package/src/rules/rule.ts +2 -2
  137. package/src/scaffold/files/agent-coding.md +1 -1
  138. package/src/scaffold/files/agent-index.md +1 -1
  139. package/src/scaffold/files/verify-skill.md +2 -2
  140. package/src/serve/hub-app/activity-st.ts +5 -4
  141. package/src/serve/hub-app/activity-view.tsx +4 -4
  142. package/src/serve/hub-app/admin-view.tsx +13 -11
  143. package/src/serve/hub-app/check-view.tsx +66 -46
  144. package/src/serve/hub-app/check-words.ts +1 -11
  145. package/src/serve/hub-app/checks-svc.ts +16 -36
  146. package/src/serve/hub-app/col-tables-view.tsx +131 -0
  147. package/src/serve/hub-app/col-tables.ts +115 -0
  148. package/src/serve/hub-app/cols-svc.ts +33 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +7 -1
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +15 -3
  160. package/src/serve/hub-app/home-strip.css +69 -19
  161. package/src/serve/hub-app/home-strip.tsx +192 -118
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +21 -5
  164. package/src/serve/hub-app/hub-types.ts +11 -5
  165. package/src/serve/hub-app/hub.css +88 -44
  166. package/src/serve/hub-app/icons.tsx +15 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +11 -8
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +34 -49
  171. package/src/serve/hub-app/palette-st.ts +5 -5
  172. package/src/serve/hub-app/palette-view.tsx +1 -1
  173. package/src/serve/hub-app/plan-view.tsx +51 -0
  174. package/src/serve/hub-app/planned-st.ts +9 -4
  175. package/src/serve/hub-app/processes-view.tsx +38 -0
  176. package/src/serve/hub-app/project-list.tsx +3 -3
  177. package/src/serve/hub-app/projects-svc.ts +15 -10
  178. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  179. package/src/serve/hub-app/reach.ts +36 -0
  180. package/src/serve/hub-app/readme-svc.ts +3 -2
  181. package/src/serve/hub-app/repo-sections.ts +117 -0
  182. package/src/serve/hub-app/repo-st.ts +47 -10
  183. package/src/serve/hub-app/runs-tabs.ts +1 -1
  184. package/src/serve/hub-app/sidebar.tsx +12 -37
  185. package/src/serve/hub-app/timeline-svc.ts +3 -2
  186. package/src/serve/hub-app/timeline-view.tsx +14 -11
  187. package/src/serve/hub-app/tree-build.ts +149 -155
  188. package/src/serve/hub-app/tree-row.tsx +39 -9
  189. package/src/serve/hub-app/tree-svc.ts +70 -49
  190. package/src/serve/hub-app/when-view.tsx +89 -0
  191. package/src/serve/hub-app/when.css +17 -0
  192. package/src/serve/hub-runtime.ts +3 -16
  193. package/src/serve/project-gutter.ts +1 -2
  194. package/src/serve/run-serve.ts +1 -1
  195. package/src/serve/serve-hub.ts +4 -14
  196. package/src/serve/warm-cell.ts +49 -16
  197. package/src/suite/judge-suites.ts +42 -17
  198. package/src/suite/suite-cli.ts +5 -8
  199. package/src/wiki/lint-md.ts +2 -0
  200. package/src/wiki/scan-md.ts +37 -10
  201. package/src/serve/hub-app/processes-svc.ts +0 -63
  202. package/src/serve/hub-app/repo-sections.tsx +0 -61
  203. package/src/serve/repo-processes.ts +0 -89
@@ -1,17 +1,16 @@
1
- // The resource tree, as state: which tab, what is expanded, where the
2
- // keyboard cursor sits. The trie itself is pure (tree-build.ts) and cached
3
- // here as computeds. Everything hand tuned persists to localStorage as
4
- // newline separated paths, text encodable and hand editable, and comes back
5
- // on reopen.
1
+ // The left pane's tree, as state: what is unfolded and where the keyboard
2
+ // cursor sits. The trie itself is pure (tree-build.ts) and cached here as
3
+ // computeds, its top rows the sections repo-sections.tsx draws. The folds
4
+ // persist to localStorage as newline separated keys, text encodable and hand
5
+ // editable, and come back on reopen. A live filter unfolds what matches
6
+ // without touching them, so clearing it restores them.
6
7
  import { makeAutoObservable } from 'mobx'
7
8
  import type { RepoSt } from './repo-st.ts'
8
9
  import type { Resource } from './hub-types.ts'
9
- import { buildPathTree, buildTypeTree, dirKeys, flattenTree, whenCounts, type TreeNode, type TreeRow } from './tree-build.ts'
10
-
11
- export type TreeTab = 'path' | 'type'
10
+ import { repoSections, type RepoSection } from './repo-sections.ts'
11
+ import { buildTree, dirKeys, flattenTree, matchesQuery, passesFilter, processResources, whenCounts, type TreeNode, type TreeRow } from './tree-build.ts'
12
12
 
13
13
  const EXPANDED_KEY = 'shipkit.tree.expanded'
14
- const TAB_KEY = 'shipkit.tree.tab'
15
14
 
16
15
  // every access is guarded: a private window, blocked site data or a stale
17
16
  // origin throws on read AND on write, and a tree that cannot remember is
@@ -32,44 +31,68 @@ const writeStore = (k: string, v: string): void => {
32
31
  }
33
32
 
34
33
  export class TreeSvc {
35
- tab: TreeTab = 'path'
36
34
  expanded = new Set<string>()
37
35
  /** the row key under the keyboard cursor */
38
36
  cursor: string | null = null
39
37
 
40
38
  constructor(private st: RepoSt) {
41
- this.tab = readStore(TAB_KEY) === 'type' ? 'type' : 'path'
42
39
  for (const p of readStore(EXPANDED_KEY).split('\n')) if (p !== '') this.expanded.add(p)
43
40
  makeAutoObservable<this, 'st'>(this, { st: false })
44
41
  }
45
42
 
46
- /** the filter and the search apply BEFORE the grouping, so a filtered
47
- * tree prunes to the branches that still hold something */
43
+ private get q(): string {
44
+ return this.st.query.trim().toLowerCase()
45
+ }
46
+
47
+ /** what the tree holds, filtered BEFORE the grouping so it prunes to the
48
+ * branches that still hold something. A search names a thing to find,
49
+ * so it reads every row: the severity group applies only while it is empty */
50
+ /** every row the tree can show: the hub's resources, then the repo's
51
+ * global processes, which pm reports and the hub never lists */
52
+ private get all(): Resource[] {
53
+ return [...this.st.cols.resources, ...processResources(this.st.globalProcesses)]
54
+ }
55
+
48
56
  get visible(): Resource[] {
57
+ const all = this.all
58
+ if (this.q !== '') return all.filter((r) => matchesQuery(r, this.q))
49
59
  const f = this.st.filter
50
- const q = this.st.query.trim().toLowerCase()
51
- return this.st.cols.resources.filter((r) => {
52
- // the filter reads the COLOUR, so what `red` shows is what reads
53
- // red on the tree. An ask is proactive work, so it is in needs
54
- // and never in red (collection-types stateOf owns that rule)
55
- if (f === 'red' && r.state !== 'red') return false
56
- if (f === 'needs' && r.severity == null) return false
57
- if (q !== '' && !r.title.toLowerCase().includes(q) && !r.path.toLowerCase().includes(q)) return false
58
- return true
59
- })
60
+ return all.filter((r) => passesFilter(r, f))
61
+ }
62
+
63
+ /** rows the severity group keeps out of the tree, said under it so a
64
+ * pruned tree never reads as a broken one */
65
+ get hidden(): number {
66
+ return this.q !== '' ? 0 : this.all.length - this.visible.length
67
+ }
68
+
69
+ /** the sections the pane draws, with their counts, keys and pages */
70
+ get sections(): RepoSection[] {
71
+ return repoSections(this.st)
60
72
  }
61
73
 
62
74
  get nodes(): TreeNode[] {
63
- return this.tab === 'type' ? buildTypeTree(this.visible) : buildPathTree(this.visible)
75
+ const all = buildTree(this.sections.map((s) => s.id), this.visible)
76
+ // a search hides every section with nothing matching, unless its own
77
+ // name is what was typed
78
+ if (this.q === '') return all
79
+ return all.filter((n) => n.children.length > 0 || n.label.includes(this.q))
80
+ }
81
+
82
+ /** what is unfolded right now: a live search must SHOW its matches, so
83
+ * the folds yield while it runs, and they are back once it is cleared */
84
+ get open(): Set<string> {
85
+ return this.q === '' ? this.expanded : new Set(dirKeys(this.nodes, false))
86
+ }
87
+ isOpen(key: string): boolean {
88
+ return this.open.has(key)
64
89
  }
65
90
 
66
91
  get rows(): TreeRow[] {
67
- // a live search must SHOW its matches: folds yield while it runs
68
- if (this.st.query.trim() !== '') return flattenTree(this.nodes, new Set(dirKeys(this.nodes, false)))
69
- return flattenTree(this.nodes, this.expanded)
92
+ return flattenTree(this.nodes, this.open)
70
93
  }
71
94
 
72
- /** the one line above the tree: what runs always and what does not */
95
+ /** the line at the top of the checks page: what runs always and what does not */
73
96
  get whenLine(): string {
74
97
  const c = whenCounts(this.st.cols.resources)
75
98
  const total = c.always + c.release + c.health + c.off
@@ -83,14 +106,8 @@ export class TreeSvc {
83
106
 
84
107
  private save(): void {
85
108
  writeStore(EXPANDED_KEY, [...this.expanded].join('\n'))
86
- writeStore(TAB_KEY, this.tab)
87
109
  }
88
110
 
89
- setTab(t: TreeTab): void {
90
- this.tab = t
91
- this.cursor = null
92
- this.save()
93
- }
94
111
  toggle(key: string): void {
95
112
  this.expanded.has(key) ? this.expanded.delete(key) : this.expanded.add(key)
96
113
  this.save()
@@ -111,14 +128,21 @@ export class TreeSvc {
111
128
  return this.rowAt(this.cursor)
112
129
  }
113
130
 
131
+ /** what a row opens: a section its page, a group with a page that page,
132
+ * a leaf its own page. Only a row with nothing to open folds */
133
+ private activate(row: TreeRow): void {
134
+ const sec = row.section == null ? null : this.sections.find((s) => s.id === row.section)
135
+ if (sec != null) sec.go()
136
+ else if (row.page != null) this.st.go(row.page)
137
+ else if (row.res != null) this.st.openResource(row.res)
138
+ else if (row.dir) this.toggle(row.key)
139
+ }
140
+
114
141
  /** a row click is both a cursor move and the open: one click, one result.
115
- * A folder with a page of its own opens the page, and only its chevron
116
- * folds (fold): the one convention for a row that is both */
142
+ * Only the chevron folds a row that opens something (fold) */
117
143
  click(row: TreeRow): void {
118
144
  this.cursor = row.key
119
- if (row.page != null) this.st.go(row.page)
120
- else if (row.dir) this.toggle(row.key)
121
- else if (row.res != null) this.st.openResource(row.res)
145
+ this.activate(row)
122
146
  }
123
147
  /** the chevron: fold or unfold, never open */
124
148
  fold(row: TreeRow): void {
@@ -133,17 +157,17 @@ export class TreeSvc {
133
157
  const next = i === -1 ? (d > 0 ? 0 : rows.length - 1) : Math.min(rows.length - 1, Math.max(0, i + d))
134
158
  this.cursor = rows[next]!.key
135
159
  }
136
- /** right descends: a collapsed directory opens, anything else steps down */
160
+ /** right descends: a folded row unfolds, anything else steps down */
137
161
  right(): void {
138
162
  const r = this.cur
139
- if (r != null && r.dir && !this.expanded.has(r.key)) this.toggle(r.key)
163
+ if (r != null && r.dir && !this.isOpen(r.key)) this.toggle(r.key)
140
164
  else this.move(1)
141
165
  }
142
- /** left ascends: an open directory closes, anything else climbs to its parent */
166
+ /** left ascends: an unfolded row folds, anything else climbs to its parent */
143
167
  left(): void {
144
168
  const r = this.cur
145
169
  if (r == null) return this.move(-1)
146
- if (r.dir && this.expanded.has(r.key)) return this.toggle(r.key)
170
+ if (r.dir && this.isOpen(r.key)) return this.toggle(r.key)
147
171
  const rows = this.rows
148
172
  const i = rows.findIndex((x) => x.key === r.key)
149
173
  for (let n = i - 1; n >= 0; n--) {
@@ -153,12 +177,9 @@ export class TreeSvc {
153
177
  }
154
178
  }
155
179
  }
156
- open(): void {
180
+ enter(): void {
157
181
  const r = this.cur
158
- if (r == null) return
159
- if (r.page != null) this.st.go(r.page)
160
- else if (r.dir) this.toggle(r.key)
161
- else if (r.res != null) this.st.openResource(r.res)
182
+ if (r != null) this.activate(r)
162
183
  }
163
184
 
164
185
  /** true = the key was ours, so the page must not also scroll */
@@ -167,7 +188,7 @@ export class TreeSvc {
167
188
  else if (k === 'ArrowUp') this.move(-1)
168
189
  else if (k === 'ArrowRight') this.right()
169
190
  else if (k === 'ArrowLeft') this.left()
170
- else if (k === 'Enter' || k === ' ') this.open()
191
+ else if (k === 'Enter' || k === ' ') this.enter()
171
192
  else return false
172
193
  return true
173
194
  }
@@ -0,0 +1,89 @@
1
+ // A repo's when page: what runs when. The legend first (every context, every
2
+ // value, what it means), then one row per check with its value on each axis,
3
+ // read from the facts every check row already carries (core/runs-matrix.ts),
4
+ // so the page reads nothing of its own and never disagrees with `shipkit when`.
5
+ import { observer } from 'mobx-react-lite'
6
+ import { useState } from 'react'
7
+ import { MATRIX_AXES, RUNS_LEGEND, inColumn, type MatrixAxis } from '../../core/runs-matrix.ts'
8
+ import type { RunsCells } from './hub-types.ts'
9
+ import type { RepoSt } from './repo-st.ts'
10
+
11
+ type Row = { id: string; title: string; cells: RunsCells; blocking: boolean | null; build: boolean }
12
+
13
+ const shortOf = (k: MatrixAxis): string => RUNS_LEGEND.find((a) => a.axis === k)!.short
14
+
15
+ const Legend = () => (
16
+ <table className="whlegend">
17
+ <tbody>
18
+ {RUNS_LEGEND.map((a) => (
19
+ <tr key={a.axis}>
20
+ <td className="whaxis">{a.axis}</td>
21
+ <td>
22
+ <div>{a.context}</div>
23
+ {a.values.map((v) => <div key={v.value} className="whval"><code>{v.value}</code> <span className="dim">{v.means}</span></div>)}
24
+ </td>
25
+ </tr>
26
+ ))}
27
+ </tbody>
28
+ </table>
29
+ )
30
+
31
+ const Cell = ({ r, k }: { r: Row; k: MatrixAxis }) => {
32
+ const c = r.cells.cells[k]
33
+ return (
34
+ <td className={`whc${c.on ? ' on' : ''}`} title={`${k}: ${c.text} · from ${c.source}${r.cells.derivedWhy != null ? ` (${r.cells.derivedWhy})` : ''}`}>
35
+ {c.text}
36
+ </td>
37
+ )
38
+ }
39
+
40
+ export const WhenPage = observer(({ app }: { app: RepoSt }) => {
41
+ const [only, setOnly] = useState<MatrixAxis | null>(null)
42
+ const all: Row[] = app.cols.resources
43
+ .filter((r) => r.collection === 'checks' && r.check != null)
44
+ .map((r) => ({ id: r.check!.checkId, title: r.text, cells: r.check!.runs, blocking: r.check!.blocking, build: r.check!.build }))
45
+ const live = all.filter((r) => r.cells.off == null)
46
+ const off = all.filter((r) => r.cells.off != null)
47
+ const shown = only == null ? live : live.filter((r) => inColumn(r.cells, only))
48
+ return (
49
+ <div className="colcard checkcard whpage">
50
+ <div className="chead">
51
+ <span className="tid">when</span>
52
+ <h3 className="ctitle">what runs when</h3>
53
+ </div>
54
+ <div className="usays">every check declares every context, with no default. Click a column to see only what that context runs. `shipkit when` prints the same</div>
55
+ <section className="csec"><div className="cstitle">the legend</div><Legend /></section>
56
+ <section className="csec">
57
+ <div className="cstitle">{only == null ? `${live.length} checks` : `${shown.length} of ${live.length} checks run on ${only}`}{only != null ? <> · <a href="#" onClick={(e) => { e.preventDefault(); setOnly(null) }}>show all</a></> : null}</div>
58
+ <table className="whtable">
59
+ <thead>
60
+ <tr>
61
+ <th>check</th>
62
+ {MATRIX_AXES.map((k) => (
63
+ <th key={k} className={only === k ? 'on' : ''} title={`${k}: ${RUNS_LEGEND.find((a) => a.axis === k)!.context}. Click: only what it runs (${live.filter((r) => inColumn(r.cells, k)).length})`} onClick={() => setOnly(only === k ? null : k)}>
64
+ {shortOf(k)} <span className="dim">{live.filter((r) => inColumn(r.cells, k)).length}</span>
65
+ </th>
66
+ ))}
67
+ <th>cache</th>
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ {shown.map((r) => (
72
+ <tr key={r.id}>
73
+ <td className="whid"><a href="#" title={r.title} onClick={(e) => { e.preventDefault(); app.timeline.openCheck(r.id) }}>{r.id}</a>{r.build ? <span className="dim"> build</span> : r.blocking === false ? <span className="dim"> warns</span> : null}</td>
74
+ {MATRIX_AXES.map((k) => <Cell key={k} r={r} k={k} />)}
75
+ <td className="whcache dim" title={r.cells.cache}>{r.cells.cache}</td>
76
+ </tr>
77
+ ))}
78
+ </tbody>
79
+ </table>
80
+ </section>
81
+ {off.length > 0 ? (
82
+ <section className="csec">
83
+ <div className="cstitle">off in this repo ({off.length}): repo.config.ts `off`, or the check does not apply here</div>
84
+ {off.map((r) => <div key={r.id} className="whoff"><span className="whid">{r.id}</span> <span className="dim">{r.cells.off}</span></div>)}
85
+ </section>
86
+ ) : null}
87
+ </div>
88
+ )
89
+ })
@@ -0,0 +1,17 @@
1
+ /* the when page (when-view.tsx): the legend, then the matrix. A cell that
2
+ runs is washed with the accent, a cell the repo moved carries a star, and
3
+ nothing here picks a colour of its own. */
4
+ .whpage{font-size:13px}
5
+ .whlegend{border-collapse:collapse;margin-bottom:var(--s2)}
6
+ .whlegend td{vertical-align:top;padding:var(--s1) var(--s2);border-bottom:1px solid var(--line)}
7
+ .whaxis{font-family:ui-monospace,monospace;color:var(--accent);white-space:nowrap}
8
+ .whval{padding-left:var(--s3)}
9
+ .whtable{border-collapse:collapse;width:100%}
10
+ .whtable th{position:sticky;top:0;background:var(--panel);text-align:left;font-weight:500;padding:var(--s1) var(--s2);cursor:pointer;white-space:nowrap;border-bottom:1px solid var(--line)}
11
+ .whtable th.on{color:var(--accent);border-bottom-color:var(--accent)}
12
+ .whtable td{padding:var(--s0) var(--s2);border-bottom:1px solid var(--line);white-space:nowrap}
13
+ .whc{color:var(--dim);font-family:ui-monospace,monospace}
14
+ .whc.on{color:var(--ink);background:var(--wash2)}
15
+ .whid{font-family:ui-monospace,monospace}
16
+ .whcache{max-width:320px;overflow:hidden;text-overflow:ellipsis}
17
+ .whoff{padding:var(--s0) var(--s2)}
@@ -44,7 +44,6 @@ import { RUN_JOB } from '../home/home-words.ts'
44
44
  import { OtherRepos } from './other-repos.ts'
45
45
  import { HouseRuns } from './house-runs.ts'
46
46
  import { recordStamp } from './project-gutter.ts'
47
- import { RepoProcesses } from './repo-processes.ts'
48
47
  import { shapeTimeline } from '../ledger/run-timeline.ts'
49
48
  import { readSuiteRuns, readSuites } from '../ledger/suite-log.ts'
50
49
  import { shapeTestCheck, shapeTestsRoot } from '../collections/test-time.ts'
@@ -333,7 +332,6 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
333
332
  root,
334
333
  name: c.name,
335
334
  uses: c.serve?.uses ?? [],
336
- processes: new RepoProcesses(root, c.serve?.processes ?? []),
337
335
  port,
338
336
  graphs,
339
337
  studio,
@@ -443,10 +441,8 @@ export type HubRuntime = {
443
441
  ping: () => Promise<void>
444
442
  /** a run is in flight: a reload now would kill it */
445
443
  busy: () => boolean
446
- /** everything this runtime started stops, and it leaves the plan. `procs`
447
- * also stops the repo's declared processes: a real stop of the hub,
448
- * never a process restart */
449
- stop: (o?: { procs?: boolean }) => void
444
+ /** everything this runtime started stops, and it leaves the plan */
445
+ stop: () => void
450
446
  }
451
447
 
452
448
  export type LoadHubOpts = {
@@ -489,7 +485,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
489
485
  return ok
490
486
  }
491
487
  const handler = hubHandler(deps, { base: o.base, projects: o.projects })
492
- const procs = deps.processes
493
488
  // tick failures are LOUD (the hub's own lines), never swallowed
494
489
  const ping = async (): Promise<void> => {
495
490
  if (deps.pingTick == null) return
@@ -519,13 +514,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
519
514
  // the answers a page waits on, computed now, one hub at a time
520
515
  // across the process, so a first visit reads them from memory
521
516
  warming = warming.then(() => handler.warm())
522
- // the repo's own processes: the auto ones start, a restart adopts the
523
- // ones still running from their pid records
524
- void procs?.startAuto()
525
- if (procs != null && procs.size > 0) {
526
- const t = setInterval(() => void procs.tick(), 2_000)
527
- live.push({ stop: () => clearInterval(t) })
528
- }
529
517
  if (o.ownTimers) {
530
518
  // one tick now (the state file guarantees a restart never re-fires), then every minute
531
519
  void ping()
@@ -541,9 +529,8 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
541
529
  start,
542
530
  ping,
543
531
  busy: () => deps.busy?.() === true,
544
- stop: (s) => {
532
+ stop: () => {
545
533
  for (const l of live.splice(0)) l.stop()
546
- if (s?.procs === true) procs?.stopAll()
547
534
  handler.stop()
548
535
  },
549
536
  }
@@ -14,11 +14,10 @@ import { DB_FILE } from '../ledger/db.ts'
14
14
  import { defaultHubPort } from './hub-port.ts'
15
15
  import { LAYOUT } from '../core/layout.ts'
16
16
  import { isArchived } from '../core/archived.ts'
17
+ import { CLAUDE_FILES } from '../home/pm-inputs.ts'
17
18
 
18
19
  export type WiredRepo = { name: string; path: string; icon: string | null }
19
20
 
20
- /** the files a repo carries its includes in */
21
- const CLAUDE_FILES = ['CLAUDE.md', 'CLAUDE.local.md']
22
21
 
23
22
  /** the absolute md file of every `@<path>.md` include line a repo's CLAUDE
24
23
  * files carry, in order. A sentence that mentions a path is no include */
@@ -142,7 +142,7 @@ export async function runServe(root: string, io: ServeIo, o?: { port?: number })
142
142
  stale.stop()
143
143
  clearInterval(planClock)
144
144
  plan.stop()
145
- rt.stop({ procs: true })
145
+ rt.stop()
146
146
  hub.stop()
147
147
  process.exit(0)
148
148
  })
@@ -28,7 +28,6 @@ import type { HubProblems } from './hub-problems.ts'
28
28
  import hubIndex from './hub-app/index.html'
29
29
  import { homePort, homeUp } from '../home/home-paths.ts'
30
30
  import { RUN_EXIT } from '../home/home-words.ts'
31
- import type { RepoProcesses } from './repo-processes.ts'
32
31
  import { projectGutter } from './project-gutter-route.ts'
33
32
  import { sameOrigin } from './same-origin.ts'
34
33
  import { LAYOUT } from '../core/layout.ts'
@@ -54,8 +53,6 @@ export type HubDeps = {
54
53
  name: string
55
54
  /** the machine resources the repo declares in serve.uses */
56
55
  uses?: string[]
57
- /** the long running processes the repo declares in serve.processes */
58
- processes?: RepoProcesses
59
56
  /** a terminal's `shipkit ci` or `check`, run in this process; resolves to its exit code */
60
57
  runCli?: (kind: 'ci' | 'check', opts: Record<string, unknown>, print: (l: string) => void) => Promise<number>
61
58
  /** 0 = random (tests); real launches pass repo.config/default */
@@ -235,13 +232,6 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
235
232
  })
236
233
  return new Response(stream, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
237
234
  }
238
- if (path === '/api/processes.json') return Response.json({ processes: d.processes?.views() ?? [] })
239
- if (path === '/api/processes/act' && req.method === 'POST') {
240
- const b = (await req.json().catch(() => ({}))) as { id?: string; act?: string }
241
- if (b.act !== 'start' && b.act !== 'stop' && b.act !== 'restart') return new Response('act must be start, stop or restart', { status: 400 })
242
- const v = d.processes?.act(b.id ?? '', b.act)
243
- return v == null ? new Response(`no process '${b.id ?? ''}' in serve.processes`, { status: 404 }) : Response.json(v)
244
- }
245
235
  // the three surfaces built on the config read the config on disk
246
236
  if (path === '/api/needs.json' || path === '/api/resources.json' || path === '/api/checks.json' || path === '/api/collections.json') await d.refresh?.()
247
237
  if (path === '/api/needs.json') {
@@ -249,9 +239,9 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
249
239
  return Response.json({ needs })
250
240
  }
251
241
  if (path === '/api/resources.json') {
252
- // ONE flat list for the whole repo: the page groups it by
253
- // path and by type, and a second endpoint would be a second
254
- // answer to drift from this one
242
+ // ONE flat list for the whole repo: the page groups it
243
+ // under its sections, and a second endpoint would be a
244
+ // second answer to drift from this one
255
245
  const r = await rows.get({ fresh: fresh(req) })
256
246
  return Response.json({
257
247
  collections: r.collections,
@@ -377,7 +367,7 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
377
367
  if (req.method === 'POST') {
378
368
  try {
379
369
  const body = (await req.json()) as GateEdit & { expect?: string }
380
- const b = await writeGateBlock(d.root, { when: body.when, slow: body.slow, slowRemove: body.slowRemove, words: body.words }, { expect: body.expect })
370
+ const b = await writeGateBlock(d.root, { off: body.off, slow: body.slow, slowRemove: body.slowRemove, words: body.words }, { expect: body.expect })
381
371
  await d.refresh?.()
382
372
  return Response.json({ ...b, dirty: configDirty(d.root) })
383
373
  } catch (e) {
@@ -1,12 +1,21 @@
1
1
  // one answer of a hub kept warm in memory: computed when the hub loads,
2
2
  // recomputed in the background when something says it moved (a save, a
3
- // stamp, its age), and served from memory. A request never pays for the
4
- // walk behind it, except the very first one and one that asks for `fresh`.
3
+ // stamp, its age) while a page reads it, and served from memory. A cell
4
+ // nobody read for WARM_WATCH_MS computes nothing on a save: its next reader
5
+ // waits on one compute instead, so an unwatched repo costs no cpu per save.
5
6
  //
6
- // the one constraint a reader breaks first: `get()` answers what is in
7
- // memory even when it is known stale. Whoever just changed the truth (an
8
- // act, the end of a run) asks for `fresh`, which waits on a compute that
9
- // started after its call.
7
+ // the one constraint a reader breaks first: `get()` from a watched cell
8
+ // answers what is in memory even when it is known stale. Whoever just
9
+ // changed the truth (an act, the end of a run) asks for `fresh`, which waits
10
+ // on a compute that started after its call.
11
+
12
+ /** a cell read within this long has a reader: a save recomputes it in the
13
+ * background. Past it, nothing recomputes until a reader asks */
14
+ export const WARM_WATCH_MS = 30_000
15
+
16
+ /** a background recompute waits this many times the last compute's length
17
+ * after it ended: a 1.5s answer at most every 15s, a 50ms one at once */
18
+ export const WARM_SPACING = 10
10
19
 
11
20
  export type WarmCellOpts = {
12
21
  /** a cheap fingerprint of what the answer reads (file stats): a new one
@@ -20,6 +29,10 @@ export type WarmCellOpts = {
20
29
 
21
30
  export class WarmCell<T> {
22
31
  private value: { v: T; at: number; stamp: string } | null = null
32
+ /** when a reader last asked, null before any */
33
+ private readAt: number | null = null
34
+ /** when the last compute ended and how long it took */
35
+ private last: { end: number; ms: number } | null = null
23
36
  private running: Promise<T> | null = null
24
37
  /** bumped by invalidate: a compute that started before it is stale */
25
38
  private gen = 0
@@ -37,6 +50,7 @@ export class WarmCell<T> {
37
50
  const gen = this.gen
38
51
  const stamp = this.o.stamp?.() ?? ''
39
52
  this.runGen = gen
53
+ const start = this.now()
40
54
  const p = (async (): Promise<T> => {
41
55
  try {
42
56
  // never synchronously: `running` is set before this settles
@@ -44,6 +58,8 @@ export class WarmCell<T> {
44
58
  this.value = { v, at: this.now(), stamp }
45
59
  return v
46
60
  } finally {
61
+ const end = this.now()
62
+ this.last = { end, ms: end - start }
47
63
  this.running = null
48
64
  // what said it moved while this ran is not in this answer
49
65
  if (this.gen !== gen) this.kick()
@@ -53,11 +69,19 @@ export class WarmCell<T> {
53
69
  return p
54
70
  }
55
71
 
56
- /** a compute in the background: on a failure the last answer stays */
72
+ /** a compute in the background, at most once per WARM_SPACING times the
73
+ * last one: a kick too soon is dropped, and the next read past the gap
74
+ * kicks again. On a failure the last answer stays */
57
75
  private kick(): void {
76
+ if (this.last != null && this.now() < this.last.end + WARM_SPACING * this.last.ms) return
58
77
  void this.run().catch(() => {})
59
78
  }
60
79
 
80
+ /** a reader asked within WARM_WATCH_MS */
81
+ private get watched(): boolean {
82
+ return this.readAt != null && this.now() - this.readAt < WARM_WATCH_MS
83
+ }
84
+
61
85
  get stale(): boolean {
62
86
  const v = this.value
63
87
  if (v == null) return true
@@ -68,23 +92,32 @@ export class WarmCell<T> {
68
92
  /** the answer in memory, a stale one kicking a recompute behind it. With
69
93
  * nothing in memory yet, or `fresh`, it waits on a compute */
70
94
  async get(o: { fresh?: boolean } = {}): Promise<T> {
71
- if (o.fresh === true) {
72
- // a compute that started before this call may miss what the
73
- // caller just changed: wait for it, then for one of our own
74
- if (this.running != null) await this.running.catch(() => {})
75
- return this.run()
76
- }
95
+ const watched = this.watched
96
+ this.readAt = this.now()
97
+ if (o.fresh === true) return this.fresh()
77
98
  const v = this.value
78
99
  // nothing in memory, a failed compute included: wait on a new one
79
100
  if (v == null) return this.run()
80
- if (this.running == null && (this.stale || this.gen !== this.runGen)) this.kick()
101
+ const moved = this.stale || this.gen !== this.runGen
102
+ // the first reader in a while: what is in memory may be hours old,
103
+ // and no save recomputed it meanwhile
104
+ if (moved && !watched) return this.running != null && this.runGen === this.gen ? this.running : this.fresh()
105
+ if (this.running == null && moved) this.kick()
81
106
  return v.v
82
107
  }
83
108
 
84
- /** something this answer reads moved: recompute in the background */
109
+ /** a compute that started after this call: one that started before may
110
+ * miss what the caller just changed, so wait for it, then for our own */
111
+ private async fresh(): Promise<T> {
112
+ if (this.running != null) await this.running.catch(() => {})
113
+ return this.run()
114
+ }
115
+
116
+ /** something this answer reads moved: recompute in the background while
117
+ * a reader watches, else leave it to the next reader */
85
118
  invalidate(): void {
86
119
  this.gen++
87
- if (this.running == null) this.kick()
120
+ if (this.running == null && this.watched) this.kick()
88
121
  }
89
122
 
90
123
  /** compute now unless an answer is already in memory: the hub's load */