rvlib-shipkit 1.4.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 (161) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/check.ts +68 -95
  5. package/src/checks/SK001-package-json-shape.check.ts +6 -2
  6. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  7. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  8. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  9. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  10. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  11. package/src/checks/SK007-github-visibility.check.ts +4 -2
  12. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  13. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  14. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  15. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  16. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  17. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  18. package/src/checks/SK014-prose-ste.check.ts +4 -0
  19. package/src/checks/SK015-nested-repo.check.ts +4 -2
  20. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  21. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  22. package/src/checks/SK018-test-files-run.check.ts +6 -2
  23. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  24. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  25. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  26. package/src/checks/SK024-identity-colors.check.ts +5 -3
  27. package/src/checks/SK025-release-drift.check.ts +4 -5
  28. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  29. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  30. package/src/checks/SK028-readme.check.ts +5 -1
  31. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  32. package/src/cli/cmd_Asks.ts +5 -5
  33. package/src/cli/cmd_Assets.ts +7 -7
  34. package/src/cli/cmd_Bump.ts +12 -11
  35. package/src/cli/cmd_Check.ts +8 -7
  36. package/src/cli/cmd_Checks.ts +7 -6
  37. package/src/cli/cmd_Ci.ts +12 -11
  38. package/src/cli/cmd_Commands.ts +16 -13
  39. package/src/cli/cmd_ConfigKeys.ts +7 -6
  40. package/src/cli/cmd_Contracts.ts +3 -3
  41. package/src/cli/cmd_CrossRepoActivity.ts +6 -5
  42. package/src/cli/cmd_Fleet.ts +9 -7
  43. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  44. package/src/cli/cmd_Home.ts +23 -17
  45. package/src/cli/cmd_Hooks.ts +6 -5
  46. package/src/cli/cmd_Init.ts +5 -4
  47. package/src/cli/cmd_Keys.ts +8 -7
  48. package/src/cli/cmd_Mv.ts +7 -6
  49. package/src/cli/cmd_Needs.ts +4 -3
  50. package/src/cli/cmd_Overview.ts +4 -3
  51. package/src/cli/cmd_Pb.ts +2 -2
  52. package/src/cli/cmd_Planned.ts +6 -5
  53. package/src/cli/cmd_Prose.ts +5 -5
  54. package/src/cli/cmd_Reflect.ts +7 -7
  55. package/src/cli/cmd_Release.ts +6 -5
  56. package/src/cli/cmd_Releases.ts +5 -4
  57. package/src/cli/cmd_Serve.ts +4 -3
  58. package/src/cli/cmd_Smoke.ts +5 -4
  59. package/src/cli/cmd_Start.ts +7 -6
  60. package/src/cli/cmd_Status.ts +4 -3
  61. package/src/cli/cmd_Tui.ts +3 -2
  62. package/src/cli/cmd_When.ts +71 -0
  63. package/src/cli/cmd_Wiki.ts +4 -4
  64. package/src/cli.ts +7 -2
  65. package/src/collections/checks.ts +2 -0
  66. package/src/collections/collection-types.ts +9 -4
  67. package/src/core/canonical-scripts.ts +1 -1
  68. package/src/core/check-runner.ts +37 -20
  69. package/src/core/config-rewrite.ts +37 -24
  70. package/src/core/discover-checks.ts +23 -2
  71. package/src/core/legacy-shapes.ts +39 -10
  72. package/src/core/one-way-in-policy.ts +2 -2
  73. package/src/core/outcome-words.ts +18 -2
  74. package/src/core/port-check-cli.ts +52 -0
  75. package/src/core/port-check-file.ts +295 -0
  76. package/src/core/port-check-files.ts +31 -0
  77. package/src/core/retention.ts +0 -14
  78. package/src/core/run-check.ts +12 -9
  79. package/src/core/run-ci.ts +8 -4
  80. package/src/core/run-fix.ts +30 -23
  81. package/src/core/run-fixers.ts +42 -5
  82. package/src/core/runs-matrix.ts +179 -0
  83. package/src/core/runs.ts +242 -0
  84. package/src/core/update-policy.ts +20 -0
  85. package/src/core/when-block.ts +29 -30
  86. package/src/core/when.ts +41 -103
  87. package/src/core/wire.ts +10 -14
  88. package/src/fleet/scan-fleet.ts +20 -4
  89. package/src/fleet/update-fleet.ts +6 -6
  90. package/src/home/home-launchd.ts +20 -4
  91. package/src/home/home-routes.ts +12 -1
  92. package/src/home/hubs.ts +5 -5
  93. package/src/home/pm.ts +8 -0
  94. package/src/home/run-pm.ts +1 -0
  95. package/src/home/serve-web.ts +2 -2
  96. package/src/home/supervise.ts +24 -1
  97. package/src/hooks/run-hooks.ts +48 -4
  98. package/src/init-repo.ts +6 -6
  99. package/src/ledger/gate-checks.ts +113 -59
  100. package/src/ledger/git-key.ts +5 -1
  101. package/src/ledger/ledger.ts +5 -5
  102. package/src/ledger/toolchain-identity.ts +45 -27
  103. package/src/plan/repo-source.ts +18 -39
  104. package/src/release/run-release.ts +5 -0
  105. package/src/repo-config.ts +30 -55
  106. package/src/rules/rule.ts +2 -2
  107. package/src/scaffold/files/agent-coding.md +1 -1
  108. package/src/scaffold/files/agent-index.md +1 -1
  109. package/src/scaffold/files/verify-skill.md +2 -2
  110. package/src/serve/hub-app/activity-st.ts +5 -4
  111. package/src/serve/hub-app/activity-view.tsx +1 -1
  112. package/src/serve/hub-app/admin-view.tsx +13 -11
  113. package/src/serve/hub-app/check-view.tsx +66 -46
  114. package/src/serve/hub-app/check-words.ts +1 -11
  115. package/src/serve/hub-app/checks-svc.ts +14 -35
  116. package/src/serve/hub-app/col-tables-view.tsx +13 -6
  117. package/src/serve/hub-app/cols-svc.ts +5 -4
  118. package/src/serve/hub-app/dock-st.ts +3 -2
  119. package/src/serve/hub-app/dock-view.tsx +36 -0
  120. package/src/serve/hub-app/dock.css +6 -0
  121. package/src/serve/hub-app/float-cards.tsx +30 -0
  122. package/src/serve/hub-app/floating.tsx +41 -4
  123. package/src/serve/hub-app/git-st.ts +42 -0
  124. package/src/serve/hub-app/git-view.tsx +61 -0
  125. package/src/serve/hub-app/hash-route.ts +45 -1
  126. package/src/serve/hub-app/header-bar.tsx +62 -72
  127. package/src/serve/hub-app/health-svc.ts +3 -2
  128. package/src/serve/hub-app/home-st.ts +14 -2
  129. package/src/serve/hub-app/home-strip.css +43 -8
  130. package/src/serve/hub-app/home-strip.tsx +111 -47
  131. package/src/serve/hub-app/home.html +1 -0
  132. package/src/serve/hub-app/hub-app.tsx +14 -4
  133. package/src/serve/hub-app/hub-types.ts +5 -2
  134. package/src/serve/hub-app/hub.css +65 -43
  135. package/src/serve/hub-app/icons.tsx +11 -0
  136. package/src/serve/hub-app/index.html +1 -0
  137. package/src/serve/hub-app/keys.ts +5 -2
  138. package/src/serve/hub-app/live-svc.ts +3 -2
  139. package/src/serve/hub-app/machine-st.ts +18 -22
  140. package/src/serve/hub-app/plan-view.tsx +51 -0
  141. package/src/serve/hub-app/planned-st.ts +9 -4
  142. package/src/serve/hub-app/processes-view.tsx +38 -0
  143. package/src/serve/hub-app/projects-svc.ts +15 -10
  144. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  145. package/src/serve/hub-app/reach.ts +36 -0
  146. package/src/serve/hub-app/readme-svc.ts +3 -2
  147. package/src/serve/hub-app/repo-sections.ts +117 -0
  148. package/src/serve/hub-app/repo-st.ts +46 -9
  149. package/src/serve/hub-app/sidebar.tsx +12 -37
  150. package/src/serve/hub-app/timeline-svc.ts +3 -2
  151. package/src/serve/hub-app/tree-build.ts +149 -155
  152. package/src/serve/hub-app/tree-row.tsx +39 -9
  153. package/src/serve/hub-app/tree-svc.ts +70 -49
  154. package/src/serve/hub-app/when-view.tsx +89 -0
  155. package/src/serve/hub-app/when.css +17 -0
  156. package/src/serve/hub-runtime.ts +3 -16
  157. package/src/serve/run-serve.ts +1 -1
  158. package/src/serve/serve-hub.ts +4 -14
  159. package/src/serve/hub-app/processes-svc.ts +0 -63
  160. package/src/serve/hub-app/repo-sections.tsx +0 -61
  161. package/src/serve/repo-processes.ts +0 -89
@@ -1,9 +1,14 @@
1
- // ONE ROW of the resource tree: a fixed grid of caret, kind, label, duration,
2
- // note and state glyph. The indent rides in the first column, the label eats
3
- // the slack, and the glyph is LAST so the wide duration column never pushes it
4
- // around. No row carries prose: the words are the tooltip and the page it opens.
1
+ // ONE ROW of the left pane's tree. A section row (the top level) is drawn as
2
+ // the project list draws its lines: caret, icon, label, count, ⌥ key, the
3
+ // current one lit. Every row under it is a fixed grid of caret, kind, label,
4
+ // duration clock, note and state glyph. The indent rides in the first
5
+ // column, the label eats the slack, and the glyph is LAST, on the right edge.
6
+ // No row carries prose: the words (the duration too) are the tooltips and
7
+ // the page it opens. A click opens, only the chevron folds.
5
8
  import { observer } from 'mobx-react-lite'
6
9
  import type { CSSProperties } from 'react'
10
+ import { Icon, ICON } from './icons.tsx'
11
+ import { sectionKeyLabel } from './keys.ts'
7
12
  import type { RepoSt } from './repo-st.ts'
8
13
  import { GLYPH, markOf, rowName, rowSlots, type TreeRow } from './tree-build.ts'
9
14
 
@@ -15,24 +20,49 @@ export const Caret = ({ open }: { open: boolean }): React.ReactElement => (
15
20
  </svg>
16
21
  )
17
22
 
23
+ /** the chevron's own hit target: it folds, and the click never reaches the row */
24
+ const Fold = observer(({ app, row }: { app: RepoSt; row: TreeRow }) => (
25
+ <span className="tw" onClick={row.dir ? (e) => { e.stopPropagation(); app.tree.fold(row) } : undefined}>
26
+ {row.dir ? <Caret open={app.tree.isOpen(row.key)} /> : null}
27
+ </span>
28
+ ))
29
+
30
+ /** a top row: a section of the repo. A section no page backs (a kind whose
31
+ * own section this repo does not draw) still gets its row, with no key */
32
+ export const SectionRowView = observer(({ app, row }: { app: RepoSt; row: TreeRow }) => {
33
+ const t = app.tree
34
+ const id = row.section!
35
+ const s = t.sections.find((x) => x.id === id)
36
+ const key = s == null ? null : sectionKeyLabel(id)
37
+ return (
38
+ <div className={`tsec${s?.on === true ? ' cur' : ''}${t.cursor === row.key ? ' on' : ''}`} aria-current={s?.on === true ? 'page' : undefined}
39
+ title={s == null ? row.label : `${s.title} (${key})`} onClick={() => t.click(row)}>
40
+ <Fold app={app} row={row} />
41
+ <Icon d={ICON[id]} />
42
+ <span className="rsecl">{s?.label ?? row.label}</span>
43
+ {s?.count == null ? null : <b className={`rsecn n${s.tone}`}>{s.count}</b>}
44
+ {key == null ? null : <kbd className="pjkey">{key}</kbd>}
45
+ </div>
46
+ )
47
+ })
48
+
18
49
  export const TreeRowView = observer(({ app, row }: { app: RepoSt; row: TreeRow }) => {
50
+ if (row.section != null) return <SectionRowView app={app} row={row} />
19
51
  const t = app.tree
20
52
  const u = app.checks
21
53
  const running = row.checks.some((id) => u.isRunning(id))
22
54
  const s = rowSlots(row, running)
23
- const mark = markOf(row.res, row.dir, row.kind)
55
+ const mark = markOf(row.res, row.kind)
24
56
  const name = rowName(row)
25
57
  return (
26
58
  <div className={`trow${t.cursor === row.key ? ' on' : ''}`} style={{ '--d': row.depth } as CSSProperties}
27
59
  title={s.hint} onClick={() => t.click(row)}>
28
- {/* the chevron folds, the rest of the row opens: on a folder with a
29
- page of its own the two differ, everywhere else both fold */}
30
60
  <span className="tw" onClick={row.dir ? (e) => { e.stopPropagation(); t.fold(row) } : undefined}>
31
- {row.dir ? <Caret open={t.expanded.has(row.key)} /> : null}
61
+ {row.dir ? <Caret open={t.isOpen(row.key)} /> : null}
32
62
  </span>
33
63
  <span className={`ty ${mark.hue}`} title={mark.word}>{mark.glyph}</span>
34
64
  <span className={`tl ${row.state}`}>{name.tag == null ? null : <span className="tid">{name.tag}</span>}{name.title}</span>
35
- <span className={`tdur ${s.durTone ?? ''}`}>{s.dur}</span>
65
+ <span className={`tdur ${s.durTone ?? ''}`} title={s.dur === '' ? undefined : s.dur}>{s.dur === '' ? null : <Icon d={ICON.clock} />}</span>
36
66
  <span className="tnote">
37
67
  {s.count == null ? s.note : <>{s.count.reds > 0 ? <b>{s.count.reds}</b> : null} {s.count.total}</>}
38
68
  </span>
@@ -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
  }
@@ -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,63 +0,0 @@
1
- // the repo's declared processes (serve.processes) as the hub supervises them,
2
- // polled while its repo is the one shown. A repo that declares none never
3
- // polls again after the first answer.
4
- import { makeAutoObservable, observableRef, runInAction } from 'mobx'
5
- import type { ProcessView } from '../repo-processes.ts'
6
- import type { RepoSt } from './repo-st.ts'
7
-
8
- export const PROCESSES_POLL_MS = 2_000
9
-
10
- export class ProcessesSvc {
11
- list: ProcessView[] = []
12
- error = ''
13
- private timer: ReturnType<typeof setTimeout> | null = null
14
- /** bumped by stop, so a chain that left before it dies at its next await */
15
- private gen = 0
16
- private polling = false
17
-
18
- constructor(private st: RepoSt) {
19
- makeAutoObservable<this, 'st' | 'timer' | 'gen' | 'polling'>(this, { st: false, timer: false, gen: false, polling: false, list: observableRef })
20
- }
21
-
22
- start(): void {
23
- if (this.polling) return
24
- this.polling = true
25
- const gen = ++this.gen
26
- const tick = async (): Promise<void> => {
27
- await this.load()
28
- if (gen === this.gen && this.list.length > 0) this.timer = setTimeout(() => void tick(), PROCESSES_POLL_MS)
29
- }
30
- void tick()
31
- }
32
-
33
- stop(): void {
34
- this.polling = false
35
- this.gen++
36
- if (this.timer != null) clearTimeout(this.timer)
37
- this.timer = null
38
- }
39
-
40
- async load(): Promise<void> {
41
- try {
42
- const r = await fetch(this.st.url('/api/processes.json'))
43
- if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
44
- const j = (await r.json()) as { processes: ProcessView[] }
45
- runInAction(() => {
46
- this.list = j.processes
47
- this.error = ''
48
- })
49
- } catch (e) {
50
- runInAction(() => { this.error = `processes: ${(e as Error).message}` })
51
- }
52
- }
53
-
54
- async act(id: string, act: 'start' | 'stop' | 'restart'): Promise<void> {
55
- try {
56
- const r = await fetch(this.st.url('/api/processes/act'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, act }) })
57
- if (!r.ok) throw new Error(await r.text())
58
- await this.load()
59
- } catch (e) {
60
- runInAction(() => { this.error = `${act} ${id}: ${(e as Error).message}` })
61
- }
62
- }
63
- }
@@ -1,61 +0,0 @@
1
- // A repo page's sections: the pages reached for most, one line each at the top
2
- // of the left pane, over the tree, drawn the way the project list draws its
3
- // lines (an icon, a label, a count where it says something, its ⌥ shortcut at
4
- // the end, keys.ts), the current one lit. A page this repo does not have is
5
- // not a line. The lines come from what the page already holds (the kept
6
- // screen on a reload), so none arrives late and moves the tree.
7
- import { observer } from 'mobx-react-lite'
8
- import { RUNS_ID } from '../../collections/collection-types.ts'
9
- import { Icon, ICON } from './icons.tsx'
10
- import { sectionKeyLabel, type SectionId } from './keys.ts'
11
- import type { RepoSt } from './repo-st.ts'
12
- import { openWikis, WIKIS } from './wikis-view.tsx'
13
-
14
- export type RepoSection = {
15
- id: SectionId
16
- label: string
17
- title: string
18
- /** null when a number would say nothing */
19
- count: number | null
20
- tone: 'red' | 'warn' | null
21
- on: boolean
22
- go: () => void
23
- }
24
-
25
- const counted = (n: number, tone: 'red' | 'warn'): Pick<RepoSection, 'count' | 'tone'> => (n === 0 ? { count: null, tone: null } : { count: n, tone })
26
-
27
- export function repoSections(app: RepoSt): RepoSection[] {
28
- const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
29
- const sel = app.sel
30
- const onCol = (col: string): boolean => (sel?.t === 'col' && sel.id === col) || (sel?.t === 'item' && sel.col === col)
31
- const res = app.cols.resources
32
- const reds = (col: string): number => res.filter((r) => r.collection === col && r.state === 'red').length
33
- const none = { count: null, tone: null }
34
- const out: RepoSection[] = []
35
- if (has('checks')) out.push({ id: 'checks', label: 'checks', title: 'every check the gate and the health pass run, one page each', ...counted(reds('checks'), 'red'), on: onCol('checks'), go: () => app.showCol('checks') })
36
- // a page, never a resource: every hub keeps a run record
37
- out.push({ id: 'runs', label: 'runs', title: 'every run on record, the slowest checks and the longest runs', ...none, on: onCol('runs'), go: () => app.cols.select('runs', RUNS_ID) })
38
- if (has('asks')) {
39
- const open = res.filter((r) => r.collection === 'asks' && r.type === 'ask' && r.slot !== 'done').length
40
- out.push({ id: 'asks', label: 'asks', title: 'what waits on a human', ...counted(open, 'warn'), on: onCol('asks'), go: () => app.showCol('asks') })
41
- }
42
- if (has('tests')) out.push({ id: 'tests', label: 'tests', title: 'every test file, slowest first, then each test check', ...counted(reds('tests'), 'red'), on: sel?.t === 'tests' || onCol('tests'), go: () => app.showTests(null) })
43
- const firstCat = app.media.assets[0]?.cat ?? app.cols.cachedCat ?? undefined
44
- if (firstCat != null) out.push({ id: 'media', label: 'media', title: 'the assets, in the atelier', ...none, on: sel?.t === 'asset' || sel?.t === 'cat', go: () => app.showCat(firstCat) })
45
- if (has(WIKIS)) out.push({ id: 'wikis', label: 'wikis', title: 'every wiki this repo mounts, as a link graph', ...none, on: onCol(WIKIS), go: () => openWikis(app) })
46
- if (app.home.machine != null) out.push({ id: 'admin', label: 'admin', title: 'reload the hub, keep the repo active, archive it', ...none, on: sel?.t === 'admin', go: () => app.showAdmin() })
47
- return out
48
- }
49
-
50
- export const Sections = observer(({ app }: { app: RepoSt }) => (
51
- <nav className="rsecs" aria-label="sections">
52
- {repoSections(app).map((s) => (
53
- <button key={s.id} className={`rsec${s.on ? ' cur' : ''}`} aria-current={s.on ? 'page' : undefined} title={`${s.title} (${sectionKeyLabel(s.id)})`} onClick={s.go}>
54
- <Icon d={ICON[s.id]} />
55
- <span className="rsecl">{s.label}</span>
56
- {s.count == null ? null : <b className={`rsecn n${s.tone}`}>{s.count}</b>}
57
- <kbd className="pjkey">{sectionKeyLabel(s.id)}</kbd>
58
- </button>
59
- ))}
60
- </nav>
61
- ))