rvlib-shipkit 0.38.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/CHANGELOG.md +367 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/generate-roll.ts +1 -1
  6. package/src/assets/kit-config.ts +6 -6
  7. package/src/assets/run-assets.ts +14 -10
  8. package/src/assets/server/studio-server.ts +6 -5
  9. package/src/check.ts +343 -0
  10. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  11. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  12. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  13. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  14. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
  15. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  16. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  17. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  18. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  19. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  20. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  21. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  22. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  23. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  24. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  25. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  26. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  27. package/src/checks/SK018-test-files-run.check.ts +28 -0
  28. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  29. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  30. package/src/checks/contracts.check.ts +20 -0
  31. package/src/cli/cmd_Asks.ts +3 -2
  32. package/src/cli/cmd_Assets.ts +2 -2
  33. package/src/cli/cmd_Bump.ts +1 -1
  34. package/src/cli/cmd_Check.ts +6 -6
  35. package/src/cli/cmd_Checks.ts +95 -0
  36. package/src/cli/cmd_Ci.ts +48 -13
  37. package/src/cli/cmd_Commands.ts +2 -6
  38. package/src/cli/cmd_ConfigKeys.ts +69 -0
  39. package/src/cli/cmd_Contracts.ts +1 -1
  40. package/src/cli/cmd_Fleet.ts +6 -2
  41. package/src/cli/cmd_Hooks.ts +1 -1
  42. package/src/cli/cmd_Keys.ts +43 -18
  43. package/src/cli/cmd_Mv.ts +2 -2
  44. package/src/cli/cmd_Prose.ts +10 -1
  45. package/src/cli/cmd_Releases.ts +2 -2
  46. package/src/cli/cmd_Serve.ts +1 -1
  47. package/src/cli/cmd_Start.ts +6 -6
  48. package/src/cli/cmd_Status.ts +25 -0
  49. package/src/cli/cmd_Tui.ts +2 -2
  50. package/src/cli.ts +7 -4
  51. package/src/collections/actions.ts +4 -4
  52. package/src/collections/checks.ts +258 -0
  53. package/src/collections/collection-types.ts +254 -1
  54. package/src/collections/comfy.ts +23 -0
  55. package/src/collections/md-body.ts +14 -0
  56. package/src/collections/pings.ts +38 -12
  57. package/src/collections/posts.ts +19 -3
  58. package/src/collections/providers.ts +198 -91
  59. package/src/collections/reflections.ts +41 -0
  60. package/src/collections/state-rules.ts +73 -0
  61. package/src/collections/test-time.ts +288 -0
  62. package/src/collections/tests.ts +181 -0
  63. package/src/collections/wikis.ts +66 -0
  64. package/src/comfy/comfy-panel.ts +292 -0
  65. package/src/contracts/apply-contracts.ts +17 -12
  66. package/src/contracts/contract-types.ts +16 -4
  67. package/src/contracts/contracts-engine.ts +78 -42
  68. package/src/contracts/manifests/discord.contract.ts +6 -6
  69. package/src/contracts/manifests/identity.contract.ts +3 -3
  70. package/src/contracts/manifests/itch.contract.ts +12 -15
  71. package/src/contracts/manifests/social.contract.ts +5 -5
  72. package/src/contracts/manifests/steam.contract.ts +16 -16
  73. package/src/contracts/run-contracts.ts +4 -3
  74. package/src/core/attach.ts +96 -0
  75. package/src/core/canonical-scripts.ts +25 -0
  76. package/src/core/check-ctx.ts +204 -14
  77. package/src/core/check-file-names.ts +112 -0
  78. package/src/core/check-pass.ts +84 -0
  79. package/src/core/check-runner.ts +796 -0
  80. package/src/core/config-keys.ts +256 -0
  81. package/src/core/config-rewrite.ts +77 -0
  82. package/src/core/discover-checks.ts +166 -90
  83. package/src/core/discover-legacy-checks.ts +26 -0
  84. package/src/core/doc-block.ts +13 -0
  85. package/src/core/floor-resolve.ts +264 -0
  86. package/src/core/floor.ts +20 -137
  87. package/src/core/git-bin.ts +24 -0
  88. package/src/core/git-root.ts +108 -0
  89. package/src/core/instance.ts +195 -0
  90. package/src/core/jobs.ts +118 -0
  91. package/src/core/layout.ts +52 -0
  92. package/src/core/legacy-shapes.ts +207 -0
  93. package/src/core/lib-root.ts +5 -0
  94. package/src/core/locked-write.ts +110 -39
  95. package/src/core/machine-commit.ts +5 -4
  96. package/src/core/migrate-layout.ts +557 -0
  97. package/src/core/motherhome.ts +2 -8
  98. package/src/core/nesting.ts +4 -12
  99. package/src/core/one-way-in-policy.ts +73 -17
  100. package/src/core/outcome-words.ts +45 -0
  101. package/src/core/package-manager.ts +86 -0
  102. package/src/core/parallel.ts +21 -0
  103. package/src/core/process-tree.ts +44 -0
  104. package/src/core/raw-access.ts +93 -0
  105. package/src/core/read-set.ts +152 -0
  106. package/src/core/run-check.ts +155 -213
  107. package/src/core/run-ci.ts +153 -189
  108. package/src/core/run-fix.ts +96 -0
  109. package/src/core/run-fixers.ts +53 -30
  110. package/src/core/self-update.ts +9 -3
  111. package/src/core/slots.ts +83 -0
  112. package/src/core/spawn-timeout.ts +34 -3
  113. package/src/core/when-block.ts +401 -0
  114. package/src/core/when.ts +135 -0
  115. package/src/core/write-status.ts +5 -2
  116. package/src/hooks/leak-guard.ts +10 -8
  117. package/src/hooks/one-way-in-guard.ts +13 -5
  118. package/src/hooks/root-identity.ts +2 -1
  119. package/src/hooks/run-hooks.ts +35 -23
  120. package/src/init-repo.ts +39 -23
  121. package/src/ledger/check-stats.ts +155 -0
  122. package/src/ledger/check-table.ts +179 -0
  123. package/src/ledger/db.ts +255 -0
  124. package/src/ledger/gate-checks.ts +433 -0
  125. package/src/ledger/git-key.ts +116 -77
  126. package/src/ledger/git-scan.ts +96 -35
  127. package/src/ledger/ledger.ts +116 -42
  128. package/src/ledger/live-runs.ts +111 -0
  129. package/src/ledger/live-timeline.ts +97 -0
  130. package/src/ledger/repo-view.ts +73 -0
  131. package/src/ledger/run-log.ts +448 -0
  132. package/src/ledger/run-timeline.ts +173 -0
  133. package/src/ledger/store.ts +23 -12
  134. package/src/ledger/suite-log.ts +161 -0
  135. package/src/ledger/toolchain-identity.ts +7 -1
  136. package/src/ledger/world-cache.ts +111 -0
  137. package/src/mv/mv-md.ts +16 -6
  138. package/src/problems/extract.ts +182 -0
  139. package/src/problems/problem-board.ts +37 -0
  140. package/src/problems/problem-line.ts +77 -0
  141. package/src/prose/prose-view.ts +90 -0
  142. package/src/prose/run-prose.ts +60 -19
  143. package/src/prose/ste.ts +140 -24
  144. package/src/reflections/reflections-queue.ts +81 -0
  145. package/src/release/git-facts.ts +15 -9
  146. package/src/release/run-bump.ts +6 -5
  147. package/src/release/run-release.ts +25 -11
  148. package/src/release/run-releases.ts +2 -2
  149. package/src/release/unit-gate.ts +7 -54
  150. package/src/release/unit-resolve.ts +282 -0
  151. package/src/release/units.ts +33 -217
  152. package/src/repo-config.ts +155 -85
  153. package/src/rules/rule.ts +36 -96
  154. package/src/rules/untrack-generated.ts +5 -4
  155. package/src/scaffold/files/vscode-tasks.json +10 -31
  156. package/src/scaffold/jsonc-spans.ts +113 -0
  157. package/src/scaffold/project-scaffold.ts +16 -43
  158. package/src/scan.ts +98 -0
  159. package/src/serve/gate-trigger.ts +148 -0
  160. package/src/serve/health-schedule.ts +102 -0
  161. package/src/serve/hub-app/chart-view.ts +179 -0
  162. package/src/serve/hub-app/check-view.tsx +345 -0
  163. package/src/serve/hub-app/check-words.ts +164 -0
  164. package/src/serve/hub-app/checks-svc.ts +280 -0
  165. package/src/serve/hub-app/clock-svc.ts +33 -0
  166. package/src/serve/hub-app/col-card.tsx +23 -39
  167. package/src/serve/hub-app/cols-svc.ts +44 -20
  168. package/src/serve/hub-app/comfy-svc.ts +171 -0
  169. package/src/serve/hub-app/comfy-view.tsx +81 -0
  170. package/src/serve/hub-app/dur.ts +41 -0
  171. package/src/serve/hub-app/hash-route.ts +121 -0
  172. package/src/serve/hub-app/header-bar.tsx +117 -0
  173. package/src/serve/hub-app/health-svc.ts +23 -28
  174. package/src/serve/hub-app/help.tsx +29 -0
  175. package/src/serve/hub-app/hub-app.tsx +122 -119
  176. package/src/serve/hub-app/hub-st.ts +115 -42
  177. package/src/serve/hub-app/hub-types.ts +117 -2
  178. package/src/serve/hub-app/hub.css +534 -192
  179. package/src/serve/hub-app/index.html +2 -0
  180. package/src/serve/hub-app/live-svc.ts +174 -0
  181. package/src/serve/hub-app/md.ts +15 -0
  182. package/src/serve/hub-app/media-svc.ts +4 -1
  183. package/src/serve/hub-app/media-view.tsx +15 -0
  184. package/src/serve/hub-app/next-run.ts +68 -0
  185. package/src/serve/hub-app/project-gutter.css +65 -0
  186. package/src/serve/hub-app/project-gutter.tsx +128 -0
  187. package/src/serve/hub-app/projects-svc.ts +226 -0
  188. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  189. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  190. package/src/serve/hub-app/prose-hits.css +21 -0
  191. package/src/serve/hub-app/run-strip.ts +47 -0
  192. package/src/serve/hub-app/runs-tabs.ts +24 -0
  193. package/src/serve/hub-app/test-view.tsx +107 -0
  194. package/src/serve/hub-app/tests-svc.ts +62 -0
  195. package/src/serve/hub-app/tests-view.tsx +238 -0
  196. package/src/serve/hub-app/time-ago.tsx +34 -0
  197. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  198. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  199. package/src/serve/hub-app/timeline-svc.ts +62 -0
  200. package/src/serve/hub-app/timeline-view.tsx +245 -0
  201. package/src/serve/hub-app/tree-build.ts +410 -0
  202. package/src/serve/hub-app/tree-row.tsx +42 -0
  203. package/src/serve/hub-app/tree-svc.ts +174 -0
  204. package/src/serve/hub-app/wikis-view.tsx +53 -0
  205. package/src/serve/other-repos.ts +79 -0
  206. package/src/serve/project-gutter-route.ts +350 -0
  207. package/src/serve/project-gutter.ts +449 -0
  208. package/src/serve/run-serve.ts +245 -16
  209. package/src/serve/same-origin.ts +14 -0
  210. package/src/serve/serve-hub.ts +213 -18
  211. package/src/shipkit.ts +19 -12
  212. package/src/smoke/run-smoke.ts +3 -2
  213. package/src/start-repo.ts +75 -36
  214. package/src/suite/judge-suites.ts +209 -15
  215. package/src/suite/kept-timings.ts +58 -0
  216. package/src/suite/runner-shapes.ts +88 -0
  217. package/src/suite/suite-cli.ts +147 -24
  218. package/src/suite/suite-cpu-preload.ts +92 -0
  219. package/src/suite/test-units.ts +243 -0
  220. package/src/ts/config.ts +4 -69
  221. package/src/ts/jsonc.ts +69 -0
  222. package/src/ts/lex.ts +143 -0
  223. package/src/ts/rename.ts +73 -10
  224. package/src/ts/source-graph.ts +28 -8
  225. package/src/ts/tsconfig-projects.ts +6 -97
  226. package/src/ts/tsconfig-resolve.ts +108 -0
  227. package/src/tui/build-steps-panel.ts +61 -0
  228. package/src/tui/checks-panel.ts +24 -16
  229. package/src/tui/run-tui.ts +2 -2
  230. package/src/tui/tui-panel.ts +5 -4
  231. package/src/wiki/graph-colors.ts +68 -0
  232. package/src/wiki/graph-mount.ts +7 -9
  233. package/src/wiki/graph-page.ts +42 -25
  234. package/src/wiki/graph-positions.ts +105 -0
  235. package/src/wiki/run-wiki.ts +1 -1
  236. package/src/wiki/serve-graph.ts +33 -3
  237. package/src/cli/cmd_Rules.ts +0 -51
  238. package/src/cli/cmd_Units.ts +0 -82
  239. package/src/collections/graphs.ts +0 -32
  240. package/src/contracts/contracts-once.ts +0 -43
  241. package/src/core/check-suite.ts +0 -302
  242. package/src/core/repair-contract-dirs.ts +0 -136
  243. package/src/core/task-runner.ts +0 -339
  244. package/src/ledger/gate-units.ts +0 -138
  245. package/src/ledger/unit-table.ts +0 -105
  246. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  247. package/src/rules/SK003-claude-md-shape.ts +0 -70
  248. package/src/rules/SK004-gitignore-policy.ts +0 -238
  249. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  250. package/src/rules/rule-registry.ts +0 -64
  251. package/src/rules/run-fix.ts +0 -72
  252. package/src/tui/tasks-panel.ts +0 -60
@@ -0,0 +1,258 @@
1
+ // THE GATE CHECKS, one resource each. Repo health used to be the text of
2
+ // STATUS.md parsed back into one row per wire line, so seventy checks shared
3
+ // one destination, an id was a whole sentence, and nothing could be timed,
4
+ // opened or addressed on its own.
5
+ //
6
+ // Everything here is READ: `gateInventory` is the one enumeration of what the
7
+ // gate runs (ledger/gate-checks.ts), the timings come from the store and the
8
+ // run record beside it, and the last wire lines come from what the last run
9
+ // wrote. Nothing in this file decides anything about a run.
10
+ //
11
+ // THE DEMOTION, which is why `status` is not read off the lines: a check that
12
+ // cannot block has its FAIL rewritten to a warn inside the check engine, so
13
+ // the wire is honest about the GATE and silent about the check. The run
14
+ // record keeps the real outcome, and `advisory` is what says a red here
15
+ // stops nothing.
16
+ import { existsSync, readFileSync } from 'fs'
17
+ import { homedir } from 'os'
18
+ import { isAbsolute, join, relative, resolve } from 'path'
19
+ import { rawName, routesToShipkit } from '../core/canonical-scripts.ts'
20
+ import { FLOOR_IDS } from '../core/floor.ts'
21
+ import { formatLine, parseDuration } from '../core/wire.ts'
22
+ import { gateInventory, type CheckRow } from '../ledger/gate-checks.ts'
23
+ import { readLastLines, readRuns, runsPath, type LastLine, type RunRow } from '../ledger/run-log.ts'
24
+ import { checkStatsFor, type CheckStat } from '../ledger/check-stats.ts'
25
+ import { NEGATIVE_TTL_CAP_MS } from '../ledger/world-cache.ts'
26
+ import type { RepoConfig } from '../repo-config.ts'
27
+ import type { Collection, Instance, NeedSeverity, ResourceState, CheckFacts, RecentRun } from './collection-types.ts'
28
+ import { checkVerdictWords, plural } from '../core/outcome-words.ts'
29
+
30
+ /** what a check is killed at when it names no `timeoutMs`: a build step runs a
31
+ * whole tool chain, everything else answers one question */
32
+ export const CHECK_TIMEOUT_MS = 120_000
33
+ export const BUILD_TIMEOUT_MS = 600_000
34
+
35
+ export type ChecksOpts = {
36
+ /** injected by tests; production reads the store plus the run record */
37
+ stats?: Map<string, CheckStat>
38
+ lastLines?: Record<string, LastLine>
39
+ runs?: RunRow[]
40
+ scripts?: Record<string, string>
41
+ }
42
+
43
+ /** how many outcomes a check's page draws as its bar row. Enough to see a
44
+ * shape, few enough that one wide widget holds them all */
45
+ const KEEP_RUNS = 16
46
+
47
+ /** the last outcomes of every check, oldest first. The AVERAGE hides the
48
+ * shape: what a page draws is the run after run verdict, reds and replays
49
+ * included, which only the log has */
50
+ export function recentRuns(runs: RunRow[], keep = KEEP_RUNS): Record<string, RecentRun[]> {
51
+ const out: Record<string, RecentRun[]> = {}
52
+ for (const r of [...runs].sort((a, b) => a.at - b.at)) {
53
+ const list = out[r.id] ?? []
54
+ list.push({ at: r.at, ms: r.ms, status: r.status, replayed: r.replayed })
55
+ out[r.id] = list.length > keep ? list.slice(-keep) : list
56
+ }
57
+ return out
58
+ }
59
+
60
+ export function checksCollection(root: string, c: RepoConfig, o: ChecksOpts = {}): Collection {
61
+ return {
62
+ id: 'checks',
63
+ title: 'gate checks',
64
+ media: 'health',
65
+ list: async () => {
66
+ const rows = await gateInventory(root, c)
67
+ const stats = o.stats ?? checkStatsFor(root, rows.map((r) => ({ id: r.decl.id })))
68
+ const lines = o.lastLines ?? readLastLines(runsPath(root))
69
+ const recent = recentRuns(o.runs ?? readRuns(runsPath(root)))
70
+ const scripts = o.scripts ?? readScripts(root)
71
+ return rows.map((row) => {
72
+ const key = row.decl.id
73
+ return instanceOf(root, c, row, stats.get(key), lines[key], recent[key], scriptOf(scripts, row))
74
+ })
75
+ },
76
+ }
77
+ }
78
+
79
+ /** the package.json scripts, or none when the file is absent or unreadable:
80
+ * the page then shows the argv alone, which is still the truth */
81
+ function readScripts(root: string): Record<string, string> {
82
+ try { return (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { scripts?: Record<string, string> }).scripts ?? {} }
83
+ catch { return {} }
84
+ }
85
+
86
+ /** the script behind a build step: the one `<pm> run <name>` names, else for a
87
+ * floor slot the body the floor resolved (`lint:raw`, then `lint` when it
88
+ * does not route back to shipkit), which is what the test slot runs through
89
+ * the suite runner */
90
+ export function scriptOf(scripts: Record<string, string>, row: CheckRow): { name: string; body: string } | null {
91
+ if (!row.build) return null
92
+ const argv = row.decl.argv ?? []
93
+ const floor = (FLOOR_IDS as readonly string[]).includes(row.decl.id)
94
+ const names = [argv[1] === 'run' ? argv[2] : undefined, ...(floor ? [rawName(row.decl.id), row.decl.id] : [])]
95
+ for (const name of names) {
96
+ const body = name == null ? undefined : scripts[name]
97
+ if (name != null && body != null && !routesToShipkit(body)) return { name, body }
98
+ }
99
+ return null
100
+ }
101
+
102
+ function instanceOf(root: string, c: RepoConfig, row: CheckRow, stat: CheckStat | undefined, last: LastLine | undefined, recent: RecentRun[] | undefined, script: { name: string; body: string } | null = null): Instance {
103
+ const id = row.decl.id
104
+ const wireLines = (last?.lines ?? []).map((l) => formatLine(l, false))
105
+ const status = stat?.last?.status ?? null
106
+ const replayed = stat?.last?.replayed === true
107
+ // the real outcome against what it declared: a FAIL that cannot block
108
+ // stops nothing, and is the one case the wire lines hide
109
+ const advisory = status === 'fail' && row.blocking === false
110
+ const facts: CheckFacts = {
111
+ checkId: id,
112
+ description: row.description,
113
+ lastLines: wireLines,
114
+ gate: row.gate,
115
+ build: row.build,
116
+ blocking: row.blocking,
117
+ advisory,
118
+ optIn: row.optIn,
119
+ ...where(row),
120
+ file: sourceFile(root, row),
121
+ doc: docPage(root, c, row),
122
+ cmd: row.decl.argv ?? null,
123
+ script,
124
+ deps: row.deps,
125
+ from: row.from,
126
+ group: row.group,
127
+ inputs: row.decl.inputs ?? undefined,
128
+ ttl: row.decl.ttl ?? undefined,
129
+ never: row.decl.never ?? undefined,
130
+ reads: row.decl.reads === true ? true : undefined,
131
+ timeoutMs: row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS),
132
+ status,
133
+ replayed,
134
+ lastMs: stat?.last == null || replayed ? null : stat.last.ms,
135
+ lastAt: stat?.last?.at ?? null,
136
+ avgMs: stat?.avgMs ?? null,
137
+ samples: stat?.samples ?? 0,
138
+ slowestMs: stat?.slowestMs ?? null,
139
+ failures: stat?.failures ?? 0,
140
+ measuredAt: stat?.measuredAt ?? null,
141
+ measuredStatus: stat?.measuredStatus ?? null,
142
+ keepsUntil: keepsUntil(row.decl.ttl, stat),
143
+ recent: recent ?? [],
144
+ }
145
+ const wire = last?.lines ?? []
146
+ return {
147
+ id: `check:${id}`,
148
+ text: feedLine(row, facts),
149
+ label: rowLabel(row, facts),
150
+ type: 'check',
151
+ ...place(root, c, row),
152
+ state: stateOf(facts),
153
+ severity: severityOf(facts),
154
+ check: facts,
155
+ md: wireLines.length === 0 ? undefined : `\`\`\`\n${wireLines.join('\n')}\n\`\`\``,
156
+ file: facts.file ?? undefined,
157
+ }
158
+ }
159
+
160
+ /** when a world probe's remembered answer runs out: its ttl from the last
161
+ * MEASUREMENT, or the shorter negative window when that measurement was not
162
+ * clean (ledger/world-cache.ts rememberWorld keeps the same two). null for a
163
+ * check that remembers nothing on a clock, or was never measured */
164
+ export function keepsUntil(ttl: string | undefined, stat: Pick<CheckStat, 'measuredAt' | 'measuredStatus'> | undefined): number | null {
165
+ if (ttl == null || ttl === '' || stat?.measuredAt == null) return null
166
+ let ms = 0
167
+ try { ms = parseDuration(ttl) } catch { return null }
168
+ if (ms <= 0) return null
169
+ return stat.measuredAt + (stat.measuredStatus === 'pass' ? ms : Math.min(ms, NEGATIVE_TTL_CAP_MS))
170
+ }
171
+
172
+ /** where this check runs today, in the one vocabulary. The inventory already
173
+ * read it (core/when.ts owns the `when` key and the two spellings it
174
+ * replaces), so this only spreads it onto the wire shape */
175
+ function where(row: CheckRow): Pick<CheckFacts, 'when' | 'whenReason' | 'whenSource'> {
176
+ return { when: row.when.at, whenReason: row.when.reason, whenSource: row.when.source }
177
+ }
178
+
179
+ /** a check whose OWNER is the argv it runs: a floor slot and a `tasks: []`
180
+ * entry have a command where a path would be, so neither has a file to open
181
+ * nor a page to link. A `*.check.ts` that declares itself a build step still
182
+ * has both. */
183
+ const ownerIsCommand = (row: CheckRow): boolean => row.owner != null && row.owner === row.decl.argv?.join(' ')
184
+
185
+ /** the file a reader opens to see what this check DOES: its own module, when
186
+ * it has one in this repo. A build step derived from a script is a command,
187
+ * and a built-in ships inside the toolkit, so both answer null. A test check
188
+ * carries its test files as code for its KEY, and none of them is what it is */
189
+ function sourceFile(root: string, row: CheckRow): string | null {
190
+ if (ownerIsCommand(row)) return null
191
+ const code = row.decl.code?.[0]
192
+ return code == null ? null : repoRelative(root, code) ?? code
193
+ }
194
+
195
+ /** the PAGE that owns the invariant, when the owner names one. A check whose
196
+ * owner is its own file in this repo has no page: `file` already points at
197
+ * it, and a doc link to the same path is one link saying nothing twice. */
198
+ function docPage(root: string, c: RepoConfig, row: CheckRow): string | null {
199
+ if (row.owner == null || ownerIsCommand(row)) return null
200
+ return repoRelative(root, row.owner) != null ? null : `${c.docsBase ?? ''}${row.owner}`
201
+ }
202
+
203
+ /** the TREE label: the name, and nothing else. The verdict, the duration and
204
+ * where it runs are slots of their own on the row, aligned down the column,
205
+ * and the words behind them are the row's tooltip. */
206
+ function rowLabel(row: CheckRow, f: CheckFacts): string {
207
+ return row.title === f.checkId ? f.checkId : `${f.checkId} ${row.title}`
208
+ }
209
+
210
+ /** the FEED line: a terminal has no slots, so `shipkit needs` and the ping
211
+ * still need one sentence carrying what it last did and what it costs. */
212
+ function feedLine(row: CheckRow, f: CheckFacts): string {
213
+ const timing = f.lastMs != null ? `, took ${fmtMs(f.lastMs)}` : ''
214
+ const avg = f.avgMs != null && f.samples > 1 ? `, average ${fmtMs(f.avgMs)} over ${plural(f.samples, 'run')}` : ''
215
+ return `${rowLabel(row, f)} ${checkVerdictWords(f.status, f)}${timing}${avg}`
216
+ }
217
+
218
+ export const fmtMs = (ms: number): string => (ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`)
219
+
220
+ /** green is MEASURED passing and nothing else may claim it, so a replay is
221
+ * quiet: it is the last verdict, not this tree's evidence. */
222
+ function stateOf(f: CheckFacts): ResourceState {
223
+ if (f.status === 'fail') return f.advisory ? 'warn' : 'red'
224
+ if (f.status === 'warn') return 'warn'
225
+ if (f.status === 'skip') return 'quiet'
226
+ if (f.status === 'pass') return f.replayed ? 'quiet' : 'ok'
227
+ return 'idle'
228
+ }
229
+
230
+ /** what reaches `shipkit needs` and the ping. An advisory failure is about
231
+ * the world rather than this change, so it is taste debt, never a phone
232
+ * notification at night. */
233
+ function severityOf(f: CheckFacts): NeedSeverity | undefined {
234
+ if (f.status === 'fail') return f.advisory ? 'yellow' : 'red'
235
+ if (f.status === 'warn') return 'yellow'
236
+ return undefined
237
+ }
238
+
239
+ /** where the check lives on the tree. A check that IS a file in this repo, or
240
+ * whose owner page is one, sits beside everything else in that folder, which
241
+ * is the whole payoff of pathing resources. A build step is a command, so it
242
+ * has no file and says so under a virtual root. */
243
+ function place(root: string, c: RepoConfig, row: CheckRow): Pick<Instance, 'path' | 'virtual'> {
244
+ const virtual = { path: `health/check/${row.decl.id}`, virtual: true as const }
245
+ if (row.owner == null || ownerIsCommand(row)) return virtual
246
+ const rel = repoRelative(root, row.owner) ?? repoRelative(root, `${c.docsBase ?? ''}${row.owner}`)
247
+ return rel == null ? virtual : { path: rel }
248
+ }
249
+
250
+ /** a rule's owner is a page of the docs wiki, which may live in ANOTHER repo
251
+ * entirely (docsBase is a path, `~` included). Only a file inside this repo
252
+ * can be a node of this repo's tree. */
253
+ function repoRelative(root: string, p: string): string | null {
254
+ const abs = resolve(root, p.startsWith('~/') ? join(homedir(), p.slice(2)) : p)
255
+ if (!existsSync(abs)) return null
256
+ const rel = relative(root, abs).replace(/\\/g, '/')
257
+ return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? null : rel
258
+ }
@@ -6,14 +6,217 @@
6
6
  // default, or a paste-from-elsewhere. Doctrine page: build/collections in
7
7
  // the docs wiki this lib ships beside.
8
8
 
9
+ import type { WhenAt, WhenSource } from '../core/when.ts'
10
+ import type { CheckFrom } from '../ledger/gate-checks.ts'
11
+
9
12
  export type NeedSeverity = 'red' | 'yellow'
10
13
 
11
14
  export type CurationVerb = 'pick' | 'approve' | 'rate' | 'provide' | 'paste-proof' | 'set' | 'done' | 'archive'
12
15
 
16
+ /** the id the runs page opens by. The page is a PAGE, reached from the
17
+ * header, never a resource of the tree. Its tab rides after it
18
+ * (`runs:files`), so a reload lands on the same list */
19
+ export const RUNS_ID = 'runs'
20
+
21
+ /** the resource id of one test file, from its repo relative path: the tree
22
+ * row, the runs page and the to fix list all open the same page by it */
23
+ export const testId = (file: string): string => `test:${file}`
24
+
25
+ /** where the tree files the test files no check reaches, in both views */
26
+ export const NO_CHECK = 'in no check'
27
+
28
+ /** the resource KIND, never the collection it came from: one folder holds
29
+ * several kinds, which is what makes the path tree worth having. `health`
30
+ * is what belongs to no check (the hub's own staleness line) */
31
+ export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'health', 'check', 'test', 'reflection'] as const
32
+
33
+ export type ResourceType = (typeof RESOURCE_TYPES)[number]
34
+
35
+ /** what a gate check is and what it last cost. The wire lines say none of
36
+ * this: a non-blocking failure is demoted to a warn before anything reads
37
+ * them, and a duration is nowhere on them at all. */
38
+ /** one outcome of one check, as its page draws it */
39
+ export type RecentRun = { at: number; ms: number; status: 'pass' | 'warn' | 'fail' | 'skip'; replayed: boolean }
40
+
41
+ export type CheckFacts = {
42
+ /** the id `--only` takes, which is NOT the resource id */
43
+ checkId: string
44
+ /** the check's one line description (the shape: check.ts
45
+ * descriptionProblem). Every inventory row carries one, a generated row
46
+ * included (ledger/gate-checks.ts CheckRow) */
47
+ description: string
48
+ /** the wire lines of its last run, already formatted, one per entry. The
49
+ * page renders each one as INLINE markdown, so a `fix:` pointer's url is
50
+ * clickable and its backtick spans read as code, while the line
51
+ * structure the wire protocol fixes stays exactly as it was printed. */
52
+ lastLines: string[]
53
+ /** does `shipkit ci` run it? false = proactive, `shipkit check` only */
54
+ gate: boolean
55
+ /** a BUILD STEP: the gate runs it, the health pass does not (derived from
56
+ * a floor slot or the repo's `tasks: []`, never declared by a check) */
57
+ build: boolean
58
+ /** null = the check never declared, which the check runner treats as fatal */
59
+ blocking: boolean | null
60
+ /** it FAILED and cannot block: orange here, nothing on the gate */
61
+ advisory: boolean
62
+ /** off the default run unless named with `--with` */
63
+ optIn: boolean
64
+ /** where it runs TODAY, in the one vocabulary the switch writes:
65
+ * always (every gate) · release (`--full` and the release path only) ·
66
+ * health (the health pass only) · off (not at all, with a reason) */
67
+ when: WhenAt
68
+ /** mandatory on off, null otherwise */
69
+ whenReason: string | null
70
+ /** which declaration decided it, so a page can say whether the switch is
71
+ * reading `when` or one of the two spellings it replaces */
72
+ whenSource: WhenSource
73
+ /** the file that IS the check, when it has one in this repo. A build step
74
+ * is a command and a built-in ships inside the toolkit, so both say
75
+ * null */
76
+ file: string | null
77
+ /** the page that owns the answer, when the owner is a docs page */
78
+ doc: string | null
79
+ /** the argv a subprocess check runs */
80
+ cmd: string[] | null
81
+ /** a build step filled by a package.json script: its name and the body
82
+ * it expands to, so a page can say what `bun run lint` really runs */
83
+ script?: { name: string; body: string } | null
84
+ /** what it waits on, when it waits on anything */
85
+ deps: string[] | null
86
+ /** whose check it is: `shipkit` = the toolkit ships it, `repo` = this repo
87
+ * declared it (its package.json script behind a floor slot, a task, a
88
+ * rule, a check file) */
89
+ from: CheckFrom
90
+ group: string | null
91
+ /** the cache population it declared: content, world, or decided never */
92
+ inputs?: string[]
93
+ ttl?: string
94
+ never?: string
95
+ /** keyed on what its body read through the check context */
96
+ reads?: boolean
97
+ /** where it is killed: a check creeping toward its ceiling is visible
98
+ * before it trips */
99
+ timeoutMs: number
100
+ status: 'pass' | 'warn' | 'fail' | 'skip' | null
101
+ replayed: boolean
102
+ /** what the last run cost, ms. null on a replay: it measured nothing */
103
+ lastMs: number | null
104
+ lastAt: number | null
105
+ /** mean over the MEASURED runs, reds included */
106
+ avgMs: number | null
107
+ samples: number
108
+ slowestMs: number | null
109
+ failures: number
110
+ /** when it was last MEASURED rather than replayed, and what that said.
111
+ * A remembered answer expires from its measurement */
112
+ measuredAt?: number | null
113
+ measuredStatus?: 'pass' | 'warn' | 'fail' | 'skip' | null
114
+ /** a world probe only: when its remembered answer runs out, so the next
115
+ * health pass after it measures again. null = nothing kept on a clock */
116
+ keepsUntil?: number | null
117
+ /** the last outcomes, oldest first: the shape an average hides */
118
+ recent?: RecentRun[]
119
+ }
120
+
121
+ /** the declaration of a slow suite, carried by the test file it names. A
122
+ * suite is not a gate check: it is suite granularity inside one task, so it
123
+ * carries its own facts rather than borrowing CheckFacts it cannot fill */
124
+ export type SuiteFacts = {
125
+ /** the suite path as the declaration writes it, relative to the repo root
126
+ * or to the unit's own dir */
127
+ file: string
128
+ /** what it catches that no fast test can */
129
+ catches: string
130
+ /** why it cannot be made fast */
131
+ whyNotFast: string
132
+ /** the config file that declares it */
133
+ configPath: string
134
+ /** the unit whose `tests.slow` holds it, null = the repo root's */
135
+ unit: string | null
136
+ /** the per suite quick tier budget it broke, ms */
137
+ budgetMs: number
138
+ }
139
+
140
+ /** one recorded run of one test file, newest first on its page. The three
141
+ * optional fields are absent on a row recorded before the record kept them
142
+ * (ledger/suite-log.ts SuiteRow says what each one means) */
143
+ export type TestRun = {
144
+ at: number
145
+ ms: number
146
+ status: 'pass' | 'fail'
147
+ tier: 'quick' | 'full'
148
+ budgetMs: number
149
+ cpuMs?: number | null
150
+ phase?: 'parallel' | 'sequential' | 'serial' | ''
151
+ phaseMs?: number | null
152
+ failLine?: string
153
+ }
154
+
155
+ /** where a test file stands with the checks: a check runs it, a bunfig
156
+ * ignore keeps it out on purpose, no check reaches it (what SK018 fails),
157
+ * its check's script is not a plain `bun test` so nothing can say, or a slow
158
+ * declaration names a file that is not on disk */
159
+ export type TestStanding = 'runs' | 'ignored' | 'orphan' | 'opaque' | 'missing'
160
+
161
+ /** one test file, as its row and its page read it */
162
+ export type TestFacts = {
163
+ /** repo relative */
164
+ file: string
165
+ /** relative to its unit's dir, the name its row carries */
166
+ inUnit: string
167
+ /** the unit whose dir holds it: `root` or a unit id */
168
+ unit: string
169
+ /** that unit's dir, '' for the root */
170
+ unitDir: string
171
+ /** the check that runs it (or would, for an ignored or opaque file), null
172
+ * when no check reaches it */
173
+ checkId: string | null
174
+ standing: TestStanding
175
+ /** the ignore pattern, or why no check reaches it, or why nothing can say */
176
+ why: string | null
177
+ /** `quick` = every gate, `full` = declared slow, the release path only */
178
+ tier: 'quick' | 'full'
179
+ /** the per file quick tier budget, ms */
180
+ budgetMs: number
181
+ /** THE to fix verdict, from ledger/run-timeline.ts testFilesToFix, the one
182
+ * function the runs page tab reads too. null = nothing to fix */
183
+ toFix: { red: boolean; slow: boolean } | null
184
+ /** the last recorded runs, newest first */
185
+ runs: TestRun[]
186
+ /** how to run it alone: the command, from `cwd` (repo relative, '' the root) */
187
+ cmd: { cwd: string; line: string }
188
+ /** a declared slow suite: the case its declaration argued */
189
+ suite?: SuiteFacts
190
+ }
191
+
192
+ /** the colour source on a tree label, wider than severity:
193
+ * red = broken · warn = taste pending or a default in use · ok = MEASURED
194
+ * passing (nothing unmeasured may claim it) · idle = healthy and worth
195
+ * reading, the neutral default · quiet = archived, replayed, out of play */
196
+ export type ResourceState = 'red' | 'warn' | 'ok' | 'idle' | 'quiet'
197
+
13
198
  export type Instance = {
14
199
  id: string
15
200
  /** one-line human text — the feed line */
16
201
  text: string
202
+ /** the tree label, when the feed line is not one. A row carries a NAME
203
+ * and puts its verdict in the slots beside it; the feed keeps the
204
+ * sentence, because a terminal has no slots */
205
+ label?: string
206
+ /** the ONE short fact this kind shows in the tree's note slot: an ask its
207
+ * kind, a secret present or missing, a post its venue, an asset its roll
208
+ * count. Absent = this kind has nothing to say there */
209
+ slot?: string
210
+ type: ResourceType
211
+ /** a slash path. Where a truth file exists this IS its repo-relative
212
+ * path; otherwise a VIRTUAL path under a root named for the type, and
213
+ * `virtual` says so rather than sending a reader hunting for a file */
214
+ path: string
215
+ /** true when `path` names no file on disk */
216
+ virtual?: boolean
217
+ /** absent = derived from severity (red → red, yellow → warn, none →
218
+ * idle). Set it directly to claim `ok` or `quiet` */
219
+ state?: ResourceState
17
220
  /** absent = healthy, nothing needed */
18
221
  severity?: NeedSeverity
19
222
  /** deep link (studio anchor, /status, external editor) */
@@ -33,6 +236,10 @@ export type Instance = {
33
236
  /** the truth file's markdown BODY (frontmatter stripped) — clamped card
34
237
  * in the grid, full in the item view; capped server-side, never a dump */
35
238
  md?: string
239
+ /** gate checks only: what it is, and what it last cost */
240
+ check?: CheckFacts
241
+ /** test files only: where it runs and what it last cost */
242
+ test?: TestFacts
36
243
  /** an open ask owns the action (dedup = acted-on once, never hidden):
37
244
  * the instance stays red HERE but verbs and pings live with the ask */
38
245
  askOwned?: boolean
@@ -48,6 +255,52 @@ export type Collection = {
48
255
 
49
256
  export type NeedItem = Instance & { collection: string }
50
257
 
258
+ /** the wire shape the hub tree reads: one flat array for the whole repo.
259
+ * `title` is the row label and `state` its colour, both filled HERE so no
260
+ * provider states them twice. `text` stays as the alias every older
261
+ * consumer reads, and dies with the last provider that writes it */
262
+ export type Resource = Instance & { collection: string; title: string; state: ResourceState }
263
+
264
+ /** PROACTIVE kinds: work waiting on a human, which is not the same thing as
265
+ * something being broken. Red is reserved for broken (a blocking check that
266
+ * failed, a secret a page needs and does not have, a collector that threw),
267
+ * so these read as taste pending on the tree. Their `severity` is untouched,
268
+ * because that is the axis `shipkit needs` and the once ever ping read */
269
+ const PROACTIVE: ReadonlySet<ResourceType> = new Set<ResourceType>(['ask'])
270
+
271
+ /** THE mapping from what a provider declared to the colour a row wears. One
272
+ * place, so a kind cannot be red on the tree and yellow in the feed */
273
+ export const stateOf = (i: Instance): ResourceState =>
274
+ i.state ?? (i.severity === 'red' ? (PROACTIVE.has(i.type) ? 'warn' : 'red') : i.severity === 'yellow' ? 'warn' : 'idle')
275
+
276
+ export async function listResources(c: Collection): Promise<Resource[]> {
277
+ return (await c.list()).map((i) => ({ ...i, collection: c.id, title: i.label ?? i.text, state: stateOf(i) }))
278
+ }
279
+
280
+ /** every resource of every collection, failure-isolated per collection: a
281
+ * broken source reports itself as ONE red row, never blanks the tree */
282
+ export async function projectResources(collections: Collection[]): Promise<Resource[]> {
283
+ const out: Resource[] = []
284
+ for (const c of collections) {
285
+ try {
286
+ out.push(...(await listResources(c)))
287
+ } catch (e) {
288
+ out.push({
289
+ collection: c.id,
290
+ id: `${c.id}:broken`,
291
+ type: 'health',
292
+ path: `health/${c.id}-collector`,
293
+ virtual: true,
294
+ title: `collector broke: ${(e as Error).message}`,
295
+ text: `collector broke: ${(e as Error).message}`,
296
+ severity: 'red',
297
+ state: 'red',
298
+ })
299
+ }
300
+ }
301
+ return out
302
+ }
303
+
51
304
  /** the flat needs projection — failure-isolated per collection: a broken
52
305
  * source reports itself as ONE red item, never blanks the feed */
53
306
  export async function projectNeeds(collections: Collection[]): Promise<NeedItem[]> {
@@ -56,7 +309,7 @@ export async function projectNeeds(collections: Collection[]): Promise<NeedItem[
56
309
  try {
57
310
  for (const i of await c.list()) if (i.severity != null) out.push({ ...i, collection: c.id })
58
311
  } catch (e) {
59
- out.push({ collection: c.id, id: `${c.id}:broken`, text: `collector broke: ${(e as Error).message}`, severity: 'red' })
312
+ out.push({ collection: c.id, id: `${c.id}:broken`, type: 'health', path: `health/${c.id}-collector`, virtual: true, text: `collector broke: ${(e as Error).message}`, severity: 'red' })
60
313
  }
61
314
  }
62
315
  return out
@@ -0,0 +1,23 @@
1
+ // comfy collection — the repo's workflow modules as section rows, so the
2
+ // sidebar, the grid, the search and the cmd+j palette all reach a graph with
3
+ // no second surface. Every row is QUIET on purpose: a module nobody tuned
4
+ // needs nothing from him, and a GPU box that is off is the world's state, not
5
+ // this repo's (the panel header states it instead).
6
+ import { scanComfyWorkflows } from '../comfy/comfy-panel.ts'
7
+ import type { Collection, Instance } from './collection-types.ts'
8
+
9
+ export function comfyCollection(root: string): Collection {
10
+ return {
11
+ id: 'comfy',
12
+ title: 'comfy',
13
+ media: 'data',
14
+ list: async () =>
15
+ scanComfyWorkflows(root).map((w): Instance => ({
16
+ id: `comfy:${w.id}`,
17
+ text: w.id,
18
+ type: 'workflow',
19
+ path: `.shipkit/comfy/workflows/${w.id}.cflow.ts`,
20
+ file: `.shipkit/comfy/workflows/${w.id}.cflow.ts`,
21
+ })),
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ // a truth file's markdown body as a hub card: frontmatter stripped,
2
+ // trimmed, capped. A card, not a document dump, and the cut is loud (…) so a
3
+ // clamped card never passes for the whole file.
4
+ import { existsSync, readFileSync } from 'fs'
5
+
6
+ const MD_CAP = 4000
7
+
8
+ export const mdBody = (path: string): string | undefined => {
9
+ if (!existsSync(path)) return undefined
10
+ const body = readFileSync(path, 'utf8').replace(/^---\n[\s\S]*?\n---\n?/, '').trim()
11
+ if (body === '') return undefined
12
+ // code-point slice, a UTF-16 cut can strand half a surrogate pair
13
+ return body.length > MD_CAP ? `${[...body].slice(0, MD_CAP).join('')}…` : body
14
+ }