rvlib-shipkit 0.38.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +379 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/derived.ts +1 -1
  6. package/src/assets/gen/generate-roll.ts +1 -1
  7. package/src/assets/kit-config.ts +6 -6
  8. package/src/assets/run-assets.ts +14 -10
  9. package/src/assets/server/studio-server.ts +6 -5
  10. package/src/check.ts +343 -0
  11. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  12. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  13. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  14. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  15. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +49 -32
  16. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  17. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  18. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  19. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  20. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  21. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  22. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  23. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  24. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  25. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  26. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  27. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  28. package/src/checks/SK018-test-files-run.check.ts +28 -0
  29. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  30. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  31. package/src/checks/contracts.check.ts +20 -0
  32. package/src/cli/cmd_Asks.ts +3 -2
  33. package/src/cli/cmd_Assets.ts +2 -2
  34. package/src/cli/cmd_Bump.ts +1 -1
  35. package/src/cli/cmd_Check.ts +6 -6
  36. package/src/cli/cmd_Checks.ts +95 -0
  37. package/src/cli/cmd_Ci.ts +48 -13
  38. package/src/cli/cmd_Commands.ts +2 -6
  39. package/src/cli/cmd_ConfigKeys.ts +69 -0
  40. package/src/cli/cmd_Contracts.ts +1 -1
  41. package/src/cli/cmd_Fleet.ts +16 -3
  42. package/src/cli/cmd_Hooks.ts +1 -1
  43. package/src/cli/cmd_Keys.ts +43 -18
  44. package/src/cli/cmd_Mv.ts +2 -2
  45. package/src/cli/cmd_Prose.ts +10 -1
  46. package/src/cli/cmd_Releases.ts +2 -2
  47. package/src/cli/cmd_Serve.ts +1 -1
  48. package/src/cli/cmd_Start.ts +6 -6
  49. package/src/cli/cmd_Status.ts +25 -0
  50. package/src/cli/cmd_Tui.ts +2 -2
  51. package/src/cli.ts +7 -4
  52. package/src/collections/actions.ts +4 -4
  53. package/src/collections/checks.ts +257 -0
  54. package/src/collections/collection-types.ts +254 -1
  55. package/src/collections/comfy.ts +23 -0
  56. package/src/collections/md-body.ts +14 -0
  57. package/src/collections/pings.ts +38 -12
  58. package/src/collections/posts.ts +19 -3
  59. package/src/collections/providers.ts +198 -91
  60. package/src/collections/reflections.ts +41 -0
  61. package/src/collections/state-rules.ts +73 -0
  62. package/src/collections/test-time.ts +288 -0
  63. package/src/collections/tests.ts +181 -0
  64. package/src/collections/wikis.ts +66 -0
  65. package/src/comfy/comfy-panel.ts +292 -0
  66. package/src/contracts/apply-contracts.ts +17 -12
  67. package/src/contracts/contract-types.ts +16 -4
  68. package/src/contracts/contracts-engine.ts +78 -42
  69. package/src/contracts/manifests/discord.contract.ts +6 -6
  70. package/src/contracts/manifests/identity.contract.ts +3 -3
  71. package/src/contracts/manifests/itch.contract.ts +12 -15
  72. package/src/contracts/manifests/social.contract.ts +5 -5
  73. package/src/contracts/manifests/steam.contract.ts +16 -16
  74. package/src/contracts/run-contracts.ts +4 -3
  75. package/src/core/attach.ts +96 -0
  76. package/src/core/canonical-scripts.ts +25 -0
  77. package/src/core/check-ctx.ts +204 -14
  78. package/src/core/check-file-names.ts +112 -0
  79. package/src/core/check-pass.ts +84 -0
  80. package/src/core/check-runner.ts +796 -0
  81. package/src/core/config-keys.ts +256 -0
  82. package/src/core/config-rewrite.ts +77 -0
  83. package/src/core/discover-checks.ts +166 -90
  84. package/src/core/discover-legacy-checks.ts +26 -0
  85. package/src/core/doc-block.ts +13 -0
  86. package/src/core/floor-resolve.ts +264 -0
  87. package/src/core/floor.ts +20 -137
  88. package/src/core/git-bin.ts +24 -0
  89. package/src/core/git-root.ts +108 -0
  90. package/src/core/instance.ts +195 -0
  91. package/src/core/jobs.ts +118 -0
  92. package/src/core/layout.ts +52 -0
  93. package/src/core/legacy-shapes.ts +207 -0
  94. package/src/core/lib-root.ts +5 -0
  95. package/src/core/locked-write.ts +110 -39
  96. package/src/core/machine-commit.ts +5 -4
  97. package/src/core/migrate-layout.ts +616 -0
  98. package/src/core/motherhome.ts +2 -8
  99. package/src/core/nesting.ts +4 -12
  100. package/src/core/one-way-in-policy.ts +73 -17
  101. package/src/core/outcome-words.ts +45 -0
  102. package/src/core/package-manager.ts +88 -0
  103. package/src/core/parallel.ts +21 -0
  104. package/src/core/process-tree.ts +44 -0
  105. package/src/core/raw-access.ts +93 -0
  106. package/src/core/read-set.ts +152 -0
  107. package/src/core/run-check.ts +155 -213
  108. package/src/core/run-ci.ts +152 -189
  109. package/src/core/run-fix.ts +99 -0
  110. package/src/core/run-fixers.ts +64 -33
  111. package/src/core/self-update.ts +11 -3
  112. package/src/core/slots.ts +83 -0
  113. package/src/core/spawn-timeout.ts +34 -3
  114. package/src/core/when-block.ts +401 -0
  115. package/src/core/when.ts +135 -0
  116. package/src/core/write-status.ts +5 -2
  117. package/src/fleet/scan-fleet.ts +16 -0
  118. package/src/hooks/leak-guard.ts +20 -9
  119. package/src/hooks/one-way-in-guard.ts +13 -5
  120. package/src/hooks/root-identity.ts +2 -1
  121. package/src/hooks/run-hooks.ts +38 -26
  122. package/src/init-repo.ts +39 -23
  123. package/src/ledger/check-stats.ts +155 -0
  124. package/src/ledger/check-table.ts +179 -0
  125. package/src/ledger/db.ts +255 -0
  126. package/src/ledger/gate-checks.ts +433 -0
  127. package/src/ledger/git-key.ts +116 -77
  128. package/src/ledger/git-scan.ts +96 -35
  129. package/src/ledger/ledger.ts +116 -42
  130. package/src/ledger/live-runs.ts +111 -0
  131. package/src/ledger/live-timeline.ts +97 -0
  132. package/src/ledger/repo-view.ts +73 -0
  133. package/src/ledger/run-log.ts +448 -0
  134. package/src/ledger/run-timeline.ts +173 -0
  135. package/src/ledger/store.ts +23 -12
  136. package/src/ledger/suite-log.ts +161 -0
  137. package/src/ledger/toolchain-identity.ts +7 -1
  138. package/src/ledger/world-cache.ts +111 -0
  139. package/src/mv/mv-md.ts +16 -6
  140. package/src/problems/extract.ts +182 -0
  141. package/src/problems/problem-board.ts +37 -0
  142. package/src/problems/problem-line.ts +77 -0
  143. package/src/prose/prose-view.ts +90 -0
  144. package/src/prose/run-prose.ts +60 -19
  145. package/src/prose/ste.ts +140 -24
  146. package/src/reflections/reflections-queue.ts +81 -0
  147. package/src/release/git-facts.ts +15 -9
  148. package/src/release/run-bump.ts +6 -5
  149. package/src/release/run-release.ts +25 -11
  150. package/src/release/run-releases.ts +2 -2
  151. package/src/release/unit-gate.ts +7 -54
  152. package/src/release/unit-resolve.ts +282 -0
  153. package/src/release/units.ts +33 -217
  154. package/src/repo-config.ts +157 -88
  155. package/src/rules/rule.ts +36 -96
  156. package/src/rules/untrack-generated.ts +5 -4
  157. package/src/scaffold/files/vscode-tasks.json +10 -31
  158. package/src/scaffold/jsonc-spans.ts +113 -0
  159. package/src/scaffold/project-scaffold.ts +16 -43
  160. package/src/scan.ts +98 -0
  161. package/src/serve/gate-trigger.ts +148 -0
  162. package/src/serve/health-schedule.ts +102 -0
  163. package/src/serve/hub-app/chart-view.ts +179 -0
  164. package/src/serve/hub-app/check-view.tsx +345 -0
  165. package/src/serve/hub-app/check-words.ts +164 -0
  166. package/src/serve/hub-app/checks-svc.ts +280 -0
  167. package/src/serve/hub-app/clock-svc.ts +33 -0
  168. package/src/serve/hub-app/col-card.tsx +23 -39
  169. package/src/serve/hub-app/cols-svc.ts +44 -20
  170. package/src/serve/hub-app/comfy-svc.ts +171 -0
  171. package/src/serve/hub-app/comfy-view.tsx +81 -0
  172. package/src/serve/hub-app/dur.ts +41 -0
  173. package/src/serve/hub-app/hash-route.ts +121 -0
  174. package/src/serve/hub-app/header-bar.tsx +117 -0
  175. package/src/serve/hub-app/health-svc.ts +23 -28
  176. package/src/serve/hub-app/help.tsx +29 -0
  177. package/src/serve/hub-app/hub-app.tsx +122 -119
  178. package/src/serve/hub-app/hub-st.ts +115 -42
  179. package/src/serve/hub-app/hub-types.ts +117 -2
  180. package/src/serve/hub-app/hub.css +534 -192
  181. package/src/serve/hub-app/index.html +2 -0
  182. package/src/serve/hub-app/live-svc.ts +174 -0
  183. package/src/serve/hub-app/md.ts +15 -0
  184. package/src/serve/hub-app/media-svc.ts +4 -1
  185. package/src/serve/hub-app/media-view.tsx +15 -0
  186. package/src/serve/hub-app/next-run.ts +68 -0
  187. package/src/serve/hub-app/project-gutter.css +65 -0
  188. package/src/serve/hub-app/project-gutter.tsx +128 -0
  189. package/src/serve/hub-app/projects-svc.ts +226 -0
  190. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  191. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  192. package/src/serve/hub-app/prose-hits.css +21 -0
  193. package/src/serve/hub-app/run-strip.ts +47 -0
  194. package/src/serve/hub-app/runs-tabs.ts +24 -0
  195. package/src/serve/hub-app/test-view.tsx +107 -0
  196. package/src/serve/hub-app/tests-svc.ts +62 -0
  197. package/src/serve/hub-app/tests-view.tsx +238 -0
  198. package/src/serve/hub-app/time-ago.tsx +34 -0
  199. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  200. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  201. package/src/serve/hub-app/timeline-svc.ts +62 -0
  202. package/src/serve/hub-app/timeline-view.tsx +245 -0
  203. package/src/serve/hub-app/tree-build.ts +410 -0
  204. package/src/serve/hub-app/tree-row.tsx +42 -0
  205. package/src/serve/hub-app/tree-svc.ts +174 -0
  206. package/src/serve/hub-app/wikis-view.tsx +53 -0
  207. package/src/serve/other-repos.ts +79 -0
  208. package/src/serve/project-gutter-route.ts +350 -0
  209. package/src/serve/project-gutter.ts +449 -0
  210. package/src/serve/run-serve.ts +245 -16
  211. package/src/serve/same-origin.ts +14 -0
  212. package/src/serve/serve-hub.ts +213 -18
  213. package/src/shipkit.ts +19 -12
  214. package/src/smoke/run-smoke.ts +9 -4
  215. package/src/start-repo.ts +75 -36
  216. package/src/suite/judge-suites.ts +209 -15
  217. package/src/suite/kept-timings.ts +58 -0
  218. package/src/suite/runner-shapes.ts +88 -0
  219. package/src/suite/suite-cli.ts +147 -24
  220. package/src/suite/suite-cpu-preload.ts +92 -0
  221. package/src/suite/test-units.ts +243 -0
  222. package/src/ts/config.ts +4 -69
  223. package/src/ts/jsonc.ts +69 -0
  224. package/src/ts/lex.ts +143 -0
  225. package/src/ts/rename.ts +73 -10
  226. package/src/ts/source-graph.ts +28 -8
  227. package/src/ts/tsconfig-projects.ts +6 -97
  228. package/src/ts/tsconfig-resolve.ts +108 -0
  229. package/src/tui/build-steps-panel.ts +61 -0
  230. package/src/tui/checks-panel.ts +24 -16
  231. package/src/tui/run-tui.ts +2 -2
  232. package/src/tui/tui-panel.ts +5 -4
  233. package/src/wiki/graph-colors.ts +68 -0
  234. package/src/wiki/graph-mount.ts +7 -9
  235. package/src/wiki/graph-page.ts +42 -25
  236. package/src/wiki/graph-positions.ts +105 -0
  237. package/src/wiki/run-wiki.ts +1 -1
  238. package/src/wiki/scan-md.ts +1 -1
  239. package/src/wiki/serve-graph.ts +33 -3
  240. package/src/cli/cmd_Rules.ts +0 -51
  241. package/src/cli/cmd_Units.ts +0 -82
  242. package/src/collections/graphs.ts +0 -32
  243. package/src/contracts/contracts-once.ts +0 -43
  244. package/src/core/check-suite.ts +0 -302
  245. package/src/core/repair-contract-dirs.ts +0 -136
  246. package/src/core/task-runner.ts +0 -339
  247. package/src/ledger/gate-units.ts +0 -138
  248. package/src/ledger/unit-table.ts +0 -105
  249. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  250. package/src/rules/SK003-claude-md-shape.ts +0 -70
  251. package/src/rules/SK004-gitignore-policy.ts +0 -238
  252. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  253. package/src/rules/rule-registry.ts +0 -64
  254. package/src/rules/run-fix.ts +0 -72
  255. package/src/tui/tasks-panel.ts +0 -60
@@ -1,17 +1,28 @@
1
- // Health service — STATUS.md + the streaming check runner (the health
2
- // section's card in THE page).
1
+ // Health service — the RAW view of the last run: STATUS.md as it sits on
2
+ // disk, plus the wire lines of a run happening now, streamed in.
3
+ //
4
+ // It is one node on the tree and nothing routes here by accident any more:
5
+ // every check has its own page (checks-svc.ts owns the run itself). This card
6
+ // exists so the whole text of a run stays reachable.
3
7
  import { makeAutoObservable, runInAction } from 'mobx'
4
8
  import type { HubSt } from './hub-st.ts'
5
9
 
6
10
  export class HealthSvc {
7
11
  statusMd: string | null = null
8
- running = false
9
- note = ''
12
+ /** the lines of the run in flight, newest last */
13
+ live: string[] = []
10
14
 
11
15
  constructor(private st: HubSt) {
12
16
  makeAutoObservable<this, 'st'>(this, { st: false })
13
17
  }
14
18
 
19
+ get running(): boolean {
20
+ return this.st.checks.busy
21
+ }
22
+ get note(): string {
23
+ return this.st.checks.note
24
+ }
25
+
15
26
  async load(): Promise<void> {
16
27
  try {
17
28
  const s = (await (await fetch('/api/status.json')).json()) as { statusMd: string | null }
@@ -21,29 +32,13 @@ export class HealthSvc {
21
32
  }
22
33
  }
23
34
 
24
- async runCheck(): Promise<void> {
25
- this.running = true
26
- this.note = 'running…'
27
- let text = ''
28
- try {
29
- const r = await fetch('/api/check/run', { method: 'POST' })
30
- const rd = r.body!.getReader()
31
- const dec = new TextDecoder()
32
- for (;;) {
33
- const { done, value } = await rd.read()
34
- if (done) break
35
- text += dec.decode(value, { stream: true })
36
- runInAction(() => { this.statusMd = text })
37
- }
38
- runInAction(() => { this.note = 'done — STATUS.md refreshed on disk' })
39
- void this.load()
40
- void this.st.cols.poll()
41
- } catch (e) {
42
- runInAction(() => {
43
- this.statusMd = `${text}\nFAIL stream broke: ${(e as Error).message}`
44
- this.note = 'stream broke'
45
- })
46
- }
47
- runInAction(() => { this.running = false })
35
+ appendLine(l: string): void {
36
+ runInAction(() => { this.live = [...this.live, l] })
37
+ }
38
+
39
+ /** the whole health pass, through the one in-process runner */
40
+ async runHealthPass(): Promise<void> {
41
+ runInAction(() => { this.live = [] })
42
+ await this.st.checks.run()
48
43
  }
49
44
  }
@@ -0,0 +1,29 @@
1
+ // THE help affordance, once, for every surface of the hub: a small `(?)` that
2
+ // shows one sentence the moment it is hovered or focused. CSS does the
3
+ // showing (.hlp in hub.css), so there is no library, no delay and no state.
4
+ //
5
+ // A term lives in TERMS rather than in the component that shows it, so the
6
+ // same word is explained the same way wherever it appears.
7
+
8
+ export const TERMS: Record<string, string> = {
9
+ always: 'it runs on every gate and every health pass',
10
+ release: 'it runs on the release path only: the full gate and the health pass run it, the quick gate does not',
11
+ health: 'it runs in the health pass (`shipkit check`) only, never on a gate and never on a commit',
12
+ off: 'it does not run at all, and the reason it was turned off rides in the repo config',
13
+ default: 'nobody declared where this check runs, so it takes the default',
14
+ advisory: 'it can fail without stopping anything: the gate reports it and goes on',
15
+ blocking: 'its failure stops the gate, and stops you',
16
+ ceiling: 'the time it is killed at: a check creeping toward its ceiling is visible before it trips',
17
+ cached: 'it did not run: the answer came from the cache, because nothing it depends on moved since it last ran',
18
+ ran: 'it ran and was timed, so the number is what it costs today',
19
+ slowest: 'the worst run on record, reds included',
20
+ average: 'the mean of the runs that really ran, cached ones left out',
21
+ }
22
+
23
+ /** one sentence, either given here or named by its term. `text` takes a node
24
+ * so a legend can ride in the same bubble as a sentence */
25
+ export const Help = ({ text, term }: { text?: React.ReactNode; term?: string }): React.ReactElement => (
26
+ <span className="hlp" tabIndex={0} role="note">
27
+ ?<span className="hbub">{text ?? (term != null ? TERMS[term] ?? term : '')}</span>
28
+ </span>
29
+ )
@@ -1,104 +1,79 @@
1
1
  // THE page (Bun.serve HTML import — no vite, no build step). One surface
2
- // per repo: media atelier + asks + creds + posts + health + graphs as
3
- // foldable sections, one severity filter persisted at /?f=needs · /?f=red.
4
- // Section title = collection view (grid of cards), row = item view. mobx+react per
5
- // build/app-state-tree: HubSt is the single root (window.app), services
6
- // own state, components read and call.
2
+ // per repo: ONE tree of every repo resource, by path or by type, coloured by
3
+ // state in the label itself, over one severity filter persisted at
4
+ // /?f=needs · /?f=red. A row opens its item view, an asset row opens the
5
+ // atelier. mobx+react per build/app-state-tree: HubSt is the single root
6
+ // (window.app), services own state, components read and call.
7
7
  import { runInAction } from 'mobx'
8
8
  import { observer } from 'mobx-react-lite'
9
9
  import { StrictMode, useEffect } from 'react'
10
10
  import { createRoot } from 'react-dom/client'
11
- import { ColCard, ColGrid, GraphView, StatusCard } from './col-card.tsx'
12
- import { HubSt } from './hub-st.ts'
11
+ import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
12
+ import { WIKIS, WikisView } from './wikis-view.tsx'
13
+ import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
14
+ import { HubSt, type Filter } from './hub-st.ts'
15
+ import { TimelineView } from './timeline-view.tsx'
16
+ import { HeaderBar, LIVE_KEY, LiveStrip } from './header-bar.tsx'
17
+ import { FOLLOW_KEY } from './timeline-chart.tsx'
18
+ import { CheckCard } from './check-view.tsx'
19
+ import { TestCard } from './test-view.tsx'
20
+ import { TestsPage } from './tests-view.tsx'
13
21
  import { AssetStage, CatGrid, InfoChips, MediaTopbar, ToolsBar, VariantsBar } from './media-view.tsx'
14
- import type { SideAsset } from './hub-types.ts'
22
+ import { TYPE_MARK } from './tree-build.ts'
23
+ import { TreeRowView } from './tree-row.tsx'
24
+ import { Help } from './help.tsx'
25
+ import { ProjectGutter } from './project-gutter.tsx'
15
26
 
16
27
  const app = new HubSt()
17
28
  // the root as a global — app code, console debugging (build/app-state-tree)
18
29
  ;(window as unknown as { app: HubSt }).app = app
19
30
  app.boot()
20
31
 
21
- // ── sidebar: media categories + one foldable section per collection ──────
22
- const AssetRow = observer(({ a }: { a: SideAsset }) => (
23
- <div className={`arow${a.id === app.media.curId ? ' sel' : ''}`} onClick={() => app.selectAsset(a.id)}>
24
- <div className="lb"><b>{a.label}</b><span>{a.rolls} rolls</span></div>
25
- {app.media.pending.includes(a.id)
26
- ? <div className="spin" />
27
- : <div className={`dot ${a.stale ? 'stale' : a.picked ? 'done' : a.rolls > 0 ? 'some' : a.manual ? 'miss' : ''}`}
28
- title={a.manual && a.rolls === 0 ? 'manual asset — nothing uploaded yet' : undefined} />}
29
- </div>
30
- ))
32
+ // ── the tree: every resource of the repo, one row deep in its path ──────
33
+ // the row itself is tree-row.tsx, so a test renders it headless
31
34
 
32
- const MediaSection = observer(() => {
33
- const m = app.media
34
- if (m.dead != null) return <div className="gname"><span className="warnline">media API down: {m.dead}</span></div>
35
- if (m.assets.length === 0) return null // kit-less repo — no media section, same page
35
+ const Tree = observer(() => {
36
+ const t = app.tree
36
37
  return (
37
- <>
38
- {m.cats.map((c) => {
39
- // a live search must SHOW its matches — folds yield while it runs
40
- const open = m.openCats.has(c) || app.query.trim() !== ''
41
- const inCat = m.visibleAssets.filter((a) => a.cat === c)
42
- return (
43
- <div key={c}>
44
- <div className="gname" id={`sec-cat-${c}`}>
45
- <span>
46
- <span className="fold" onClick={() => m.toggleFold(c)}>{open ? '▾' : '▸'}</span>{' '}
47
- <span className="cn" title="grid view" onClick={() => app.showCat(c)}>{c}</span>
48
- </span>
49
- {inCat.every((a) => a.manual) ? null : (
50
- <span className="catgen">{[1, 2, 3].map((n) => <button key={n} onClick={() => void m.genCat(c, n)}>+{n}</button>)}</span>
51
- )}
52
- </div>
53
- {open ? inCat.map((a) => <AssetRow key={a.id} a={a} />) : null}
54
- </div>
55
- )
56
- })}
57
- </>
38
+ // roving focus: the arrows belong to the tree only while it HAS the
39
+ // focus, so the atelier keeps its own keys
40
+ <div className="tree" tabIndex={0} onKeyDown={(e) => { if (t.key(e.key)) e.preventDefault() }}>
41
+ {t.rows.map((r) => <TreeRowView key={r.key} app={app} row={r} />)}
42
+ {t.rows.length === 0 ? <div className="tempty">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : app.filter === 'needs' ? 'nothing needs you ✨' : 'no resources yet'}</div> : null}
43
+ </div>
58
44
  )
59
45
  })
60
46
 
61
- const ColSections = observer(() => {
62
- const c = app.cols
63
- return (
64
- <>
65
- {c.sidebarCols.map((col) => {
66
- const items = c.visibleInstances(col)
67
- // every section header ALWAYS renders, 0 items included — a
68
- // quiet section must stay reachable (done asks, present secrets)
69
- const open = c.isOpen(col)
70
- const health = col.id === 'health'
71
- return (
72
- <div key={col.id}>
73
- <div className="gname" id={`sec-${col.id}`}>
74
- <span>
75
- <span className="fold" onClick={() => c.toggleFold(col)}>{open ? '▾' : '▸'}</span>{' '}
76
- <span className="cn" onClick={() => (health ? app.showStatus() : app.showCol(col.id))}
77
- title={health ? 'STATUS.md + run check' : 'collection view'}>{col.title}</span>
78
- </span>
79
- <span className="cnt">{items.length}</span>
80
- </div>
81
- {open ? items.map((i) => (
82
- // a health line's action IS the runner — an echo card would be a dead end
83
- <div key={i.id} className={`arow${app.sel?.t === 'item' && app.sel.id === i.id ? ' sel' : ''}`}
84
- onClick={() => (health ? app.showStatus() : c.select(col.id, i.id))}>
85
- <div className="lb"><b>{i.text}</b>{i.file != null ? <span>{i.file}</span> : null}</div>
86
- <div className={`cdot ${i.severity ?? ''}`} />
87
- </div>
88
- )) : null}
89
- </div>
90
- )
91
- })}
92
- </>
93
- )
94
- })
47
+ /** what every glyph in the type slot means, behind one `(?)` in the header */
48
+ const TypeLegend = () => (
49
+ <Help text={
50
+ <span className="legend">
51
+ {Object.entries(TYPE_MARK).map(([kind, m]) => (
52
+ <span key={kind} className="lrow"><i className={`ty ${m.hue}`}>{m.glyph}</i>{m.word}</span>
53
+ ))}
54
+ </span>
55
+ } />
56
+ )
57
+
58
+ const TreeTabs = observer(() => (
59
+ <span className="seg">
60
+ {(['path', 'type'] as const).map((t) => (
61
+ <button key={t} className={app.tree.tab === t ? 'on' : ''} onClick={() => app.tree.setTab(t)}>by {t}</button>
62
+ ))}
63
+ </span>
64
+ ))
95
65
 
96
66
  // segmented severity filter — all · needs (yellow+red) · red, the address
97
67
  // rides at /?f=… so a filtered view is shareable
68
+ // the shortcut letter rides INSIDE its own button, so the binding is
69
+ // discoverable without a legend
70
+ const FILTER_KEY = { all: 'a', needs: 'n', red: 'r' } as const
98
71
  const SegFilter = observer(() => (
99
72
  <span className="seg">
100
73
  {(['all', 'needs', 'red'] as const).map((f) => (
101
- <button key={f} className={app.filter === f ? 'on' : ''} onClick={() => app.setFilter(f)}>{f}</button>
74
+ <button key={f} className={app.filter === f ? 'on' : ''} onClick={() => app.setFilter(f)}>
75
+ {f}<span className="sk">{FILTER_KEY[f]}</span>
76
+ </button>
102
77
  ))}
103
78
  </span>
104
79
  ))
@@ -106,43 +81,26 @@ const SegFilter = observer(() => (
106
81
  const Sidebar = observer(() => (
107
82
  <aside>
108
83
  <div className="shead">
109
- {app.name !== '' ? app.name : 'hub'}
84
+ resources
110
85
  <SegFilter />
111
86
  </div>
112
87
  <div className="ssearch">
113
- <input placeholder="filter… (⌘J to jump)" value={app.query}
88
+ <input id="treeq" placeholder="filter… (/ to focus, ⌘J to jump)" value={app.query}
114
89
  onChange={(e) => app.setQuery(e.target.value)}
115
90
  onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
116
91
  </div>
117
- <MediaSection />
118
- <ColSections />
92
+ <div className="stabs">
93
+ <TreeTabs />
94
+ <span className="sp" />
95
+ <TypeLegend />
96
+ <span className="cnt">{app.tree.visible.length}</span>
97
+ </div>
98
+ {app.tree.whenLine === '' ? null : <div className="swhen">{app.tree.whenLine}</div>}
99
+ {app.media.dead != null ? <div className="tempty"><span className="warnline">media API down: {app.media.dead}</span></div> : null}
100
+ <Tree />
119
101
  </aside>
120
102
  ))
121
103
 
122
- // ── the icon rail: quick jump to a section header (scroll + unfold,
123
- // NEVER a navigation) — red dot = the section holds a red ──
124
- const RAIL_ICON: Record<string, string> = { asks: '🙋', creds: '🔑', posts: '📣', health: '📊', graphs: '🕸' }
125
- const Rail = observer(() => {
126
- const c = app.cols
127
- const m = app.media
128
- // unfold flips state; the scroll waits for react to paint the rows
129
- const jump = (id: string): void => { setTimeout(() => document.getElementById(id)?.scrollIntoView({ block: 'start' }), 0) }
130
- return (
131
- <nav className="skrail">
132
- {m.assets.length > 0 ? (
133
- <span className="ric" title="assets" onClick={() => { m.openAllCats(); if (m.cats[0] != null) jump(`sec-cat-${m.cats[0]}`) }}>
134
- 🖼{c.mediaHasRed ? <i /> : null}
135
- </span>
136
- ) : null}
137
- {c.sidebarCols.map((col) => (
138
- <span key={col.id} className="ric" title={col.title} onClick={() => { c.openSection(col); jump(`sec-${col.id}`) }}>
139
- {RAIL_ICON[col.id] ?? '•'}{c.hasRed(col) ? <i /> : null}
140
- </span>
141
- ))}
142
- </nav>
143
- )
144
- })
145
-
146
104
  // ── cmd+j palette: every jump target behind one input ────────────────────
147
105
  const Palette = observer(() => {
148
106
  if (!app.palOpen) return null
@@ -173,7 +131,7 @@ const Palette = observer(() => {
173
131
  )
174
132
  })
175
133
 
176
- // remark box on the ASSET view too: remarks land in .rv-assets/notes.md
134
+ // remark box on the ASSET view too: remarks land in .shipkit/assets/notes.md
177
135
  // and come back as `remarks` (exchange both ways, media included)
178
136
  const AssetRemarks = observer(() => {
179
137
  const m = app.media
@@ -184,7 +142,7 @@ const AssetRemarks = observer(() => {
184
142
  <div className="assetrem">
185
143
  {remarks.length > 0 ? <div className="rem">{remarks.join('\n')}</div> : null}
186
144
  <div className="row">
187
- <input placeholder="remark → lands in .rv-assets/notes.md, agents read it" value={c.remarkDraft}
145
+ <input placeholder="remark → lands in .shipkit/assets/notes.md, agents read it" value={c.remarkDraft}
188
146
  onChange={(e) => runInAction(() => { c.remarkDraft = e.target.value })} />
189
147
  <button className="ghost" onClick={() => void c.remark({ kind: 'media', id: m.curId })}>remark</button>
190
148
  </div>
@@ -199,12 +157,21 @@ const Main = observer(() => {
199
157
  if (app.sel?.t === 'item') return (
200
158
  <main>
201
159
  {errs}
202
- {app.sel.col === 'graphs'
203
- ? <GraphView app={app} id={app.sel.id} />
204
- : <div className="stagewrap"><ColCard app={app} col={app.sel.col} id={app.sel.id} /></div>}
160
+ {app.sel.col === WIKIS
161
+ ? <WikisView app={app} id={app.sel.id} />
162
+ : app.sel.col === 'comfy'
163
+ ? <ComfyView app={app} id={app.sel.id} />
164
+ : app.sel.col === 'checks'
165
+ ? <div className="stagewrap"><CheckCard app={app} id={app.sel.id} /></div>
166
+ : app.sel.col === 'tests'
167
+ ? <div className="stagewrap"><TestCard app={app} id={app.sel.id} /></div>
168
+ : app.sel.col === 'runs'
169
+ ? <div className="stagewrap"><TimelineView app={app} /></div>
170
+ : <div className="stagewrap"><ColCard app={app} col={app.sel.col} id={app.sel.id} /></div>}
205
171
  </main>
206
172
  )
207
173
  if (app.sel?.t === 'col') return <main>{errs}<ColGrid app={app} id={app.sel.id} /></main>
174
+ if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
208
175
  if (app.sel?.t === 'status') return <main>{errs}<div className="stagewrap"><StatusCard app={app} /></div></main>
209
176
  if (app.sel?.t === 'cat') return <main>{errs}<CatGrid app={app} /></main>
210
177
  if (app.sel?.t === 'asset') return (
@@ -218,22 +185,24 @@ const Main = observer(() => {
218
185
  <VariantsBar app={app} />
219
186
  </div>
220
187
  <AssetRemarks />
221
- <div className="help"><kbd>←</kbd><kbd>→</kbd> roll · <kbd>Enter</kbd> pick · <kbd>A</kbd> alt · <kbd>X</kbd> discard — picks ship via the kit's ship hook when present · category title = grid view</div>
188
+ <div className="help"><kbd>←</kbd><kbd>→</kbd> roll · <kbd>Enter</kbd> pick · <kbd>⇧A</kbd> alt · <kbd>X</kbd> discard — picks ship via the kit's ship hook when present · category title = grid view</div>
222
189
  </main>
223
190
  )
224
191
  // a filtered view with zero hits is the WIN state — say so, never point at a void
225
- const allClear = app.filter !== 'all' && m.visibleAssets.length === 0 && app.cols.sidebarCols.every((col) => app.cols.visibleInstances(col).length === 0)
192
+ const allClear = app.filter !== 'all' && app.tree.visible.length === 0
226
193
  return (
227
194
  <main>
228
195
  {errs}
229
196
  <div className="stagewrap"><div className="ph">
230
197
  {allClear ? (app.filter === 'red' ? 'no reds — nothing is blocked ✨' : 'all clear — nothing needs you ✨')
231
- : m.assets.length > 0 ? 'Pick an asset or an item on the left.' : 'Pick an item on the left.'}
198
+ : 'Pick a row on the left.'}
232
199
  </div></div>
233
200
  </main>
234
201
  )
235
202
  })
236
203
 
204
+ const FILTER_BY_KEY: Record<string, Filter | undefined> = { a: 'all', n: 'needs', r: 'red' }
205
+
237
206
  const App = observer(() => {
238
207
  useEffect(() => {
239
208
  const onKey = (e: KeyboardEvent): void => {
@@ -246,18 +215,52 @@ const App = observer(() => {
246
215
  const t = e.target as HTMLElement
247
216
  if (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') return
248
217
  if (app.palOpen) return // the palette input owns its keys
249
- if (app.sel?.t !== 'asset') return
250
- if (app.media.key(e.key)) e.preventDefault()
218
+ // the atelier owns its roll keys while an asset is open, and
219
+ // says so by consuming them
220
+ if (app.sel?.t === 'asset' && app.media.key(e.key)) {
221
+ e.preventDefault()
222
+ return
223
+ }
224
+ if (e.key === '/') {
225
+ e.preventDefault()
226
+ document.getElementById('treeq')?.focus()
227
+ return
228
+ }
229
+ if (e.metaKey || e.ctrlKey || e.altKey) return
230
+ if (e.key === LIVE_KEY) {
231
+ e.preventDefault()
232
+ app.live.toggle()
233
+ return
234
+ }
235
+ if (e.key === FOLLOW_KEY && !app.live.chart.follow) {
236
+ e.preventDefault()
237
+ app.live.chart.attach()
238
+ return
239
+ }
240
+ const f = FILTER_BY_KEY[e.key.toLowerCase()]
241
+ if (f != null) {
242
+ e.preventDefault()
243
+ app.setFilter(f)
244
+ }
251
245
  }
252
246
  document.addEventListener('keydown', onKey)
253
247
  return () => document.removeEventListener('keydown', onKey)
254
248
  }, [])
255
249
  return (
250
+ // a flex ROW: a column mounted before `.appcol` sits left of the
251
+ // header and everything under it
256
252
  <div className="app">
257
- <Rail />
258
- <Sidebar />
259
- <Main />
253
+ <ProjectGutter app={app} />
254
+ <div className="appcol">
255
+ <HeaderBar app={app} />
256
+ <LiveStrip app={app} />
257
+ <div className="appbody">
258
+ <Sidebar />
259
+ <Main />
260
+ </div>
261
+ </div>
260
262
  <Palette />
263
+ <ComfyOverlay app={app} />
261
264
  </div>
262
265
  )
263
266
  })