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,433 @@
1
+ // EVERY CHECK THIS REPO HAS, composed ONCE.
2
+ //
3
+ // One walk feeds three readers: the runner (core/check-runner.ts) gets bodies,
4
+ // the key layer gets declarations, and every surface gets rows. They cannot
5
+ // disagree about what exists, which is the whole reason this file is the only
6
+ // place that enumerates.
7
+ //
8
+ // What feeds it: the floor slots derived from package.json scripts, the
9
+ // repo's own `tasks: []`, every discovered `*.check.ts`, and the two legacy
10
+ // forms still read behind the compatibility fence (`rules: []` and a legacy
11
+ // check script, a `*.check.ts` with no check object). The declaration always lives WITH the check, never in a table
12
+ // over here: a table beside the registry is a second source of truth that
13
+ // drifts the first time somebody adds one.
14
+ import { relative, resolve } from 'path'
15
+ import type { CheckCtx } from '../core/check-ctx.ts'
16
+ import { type RepoConfig } from '../repo-config.ts'
17
+ import { listRepoFiles, resolveFloor, slotDescription, type FloorResolution } from '../core/floor.ts'
18
+ import { discoverLegacyChecks } from '../core/discover-legacy-checks.ts'
19
+ import { checkFileRunBy } from '../core/legacy-shapes.ts'
20
+ import { ruleCacheExtra } from '../rules/rule.ts'
21
+ import { discoverChecks } from '../core/discover-checks.ts'
22
+ import { checkCacheExtra, describeFirst, type TaskDef, type CheckFrom } from '../check.ts'
23
+ import { CHECK_TIMEOUT_MS, type RunnableCheck, type CheckBody } from '../core/check-runner.ts'
24
+ import { parseDuration } from '../core/wire.ts'
25
+ import { whenOfLegacyCheck, whenOfDisabledSlot, whenOfRule, whenOfTask, whenOfCheck, type WhenDecl } from '../core/when.ts'
26
+ import type { CheckDecl } from './git-key.ts'
27
+ import { populationOf } from '../problems/problem-line.ts'
28
+ import { rawAccessInClosure, rawAccessLine } from '../core/raw-access.ts'
29
+ import type { RvtConfig } from '../ts/config.ts'
30
+ import { LIB_ROOT } from '../core/lib-root.ts'
31
+ import type { DiscoveredCheck } from '../core/discover-checks.ts'
32
+ import { declaredTestUnits, inputsOutsideUnits, isTestFile } from '../suite/test-units.ts'
33
+
34
+ /** why an undeclared `run` check cannot be keyed on its reads, or null when it
35
+ * can. A built-in is walked inside the toolkit, a repo check inside the repo
36
+ * with the toolkit left out (the toolchain id covers it) */
37
+ function rawAccessOf(root: string, u: DiscoveredCheck, configs: Map<string, RvtConfig>): string | null {
38
+ try {
39
+ const r = u.rel == null ? rawAccessInClosure(LIB_ROOT, u.path, { configs }) : rawAccessInClosure(root, u.path, { skip: LIB_ROOT, configs })
40
+ return r == null ? null : rawAccessLine(r)
41
+ } catch (e) {
42
+ return `the modules ${u.rel ?? u.path} imports could not be listed (${String(e).slice(0, 120)}): declare inputs or never`
43
+ }
44
+ }
45
+
46
+ /** the env var a repo's own suite reads to widen itself. It lives HERE because
47
+ * it rewrites every build step's env, which is part of the key: a run that
48
+ * asked for the quick tier and one that asked for the full tier are different
49
+ * questions, and a surface that renders keys without applying it renders a
50
+ * tier nobody ran. */
51
+ export const FULL_TESTS_ENV = 'SHIPKIT_TEST_FULL'
52
+
53
+ export type TestTier = 'quick' | 'full'
54
+
55
+ /** the tier this process is asking for, absent an explicit choice */
56
+ export function envTier(): TestTier {
57
+ return process.env[FULL_TESTS_ENV] === '1' ? 'full' : 'quick'
58
+ }
59
+
60
+ /** a BUILD step's ceiling when it names none: a whole tool chain is not one
61
+ * question about the world (core/check-runner.ts owns the other default) */
62
+ export const BUILD_TIMEOUT_MS = 600_000
63
+
64
+ /** the floor plus the repo's own `tasks: []`, with the slots nothing could
65
+ * fill and the ones a config turned off. The gate prints those, and the TUI
66
+ * draws its rows from them before anything has run. */
67
+ export async function allTasks(c: RepoConfig, root: string): Promise<FloorResolution> {
68
+ const floor = resolveFloor(c, root)
69
+ return { ...floor, tasks: [...floor.tasks, ...(c.tasks ?? [])] }
70
+ }
71
+
72
+ /** one gate check with everything a reader asks about it, not only what the
73
+ * key layer asks. The cache half stays in `decl` so `CheckDecl` never grows
74
+ * fields the key layer must then decide to ignore. */
75
+ export type CheckRow = {
76
+ decl: CheckDecl
77
+ /** the display name an operator reads (its title, else the id) */
78
+ title: string
79
+ /** the check's one line description (the shape: check.ts
80
+ * descriptionProblem). Every row carries one: a check file declares it,
81
+ * and a row with no file to say it in (a floor slot, a unit test check,
82
+ * a task, the legacy forms) gets it from what generated the row. */
83
+ description: string
84
+ /** does `shipkit ci` run it? false = PROACTIVE: `shipkit check` reports it,
85
+ * the gate never does, and it may sit red for months without anything
86
+ * being wrong with the change in front of you. */
87
+ gate: boolean
88
+ /** a BUILD STEP: the gate runs it, the health pass does not. `shipkit ci`
89
+ * asks "is my change good" and builds, `shipkit check` asks "is this
90
+ * repo healthy" and reports, and a repo's test suite has no business in
91
+ * the second question. DERIVED true for a floor slot and a `tasks: []`
92
+ * entry, which have no file to say it in, and DECLARED by a `*.check.ts`
93
+ * that is one (check.ts `build`). */
94
+ build: boolean
95
+ /** can a failure stop you? null = the check never said AND the repo set no
96
+ * `checkDefaults.blocking`, which the runner treats as fatal. */
97
+ blocking: boolean | null
98
+ /** where the answer is owned: a wiki page, a file, a command. Null when
99
+ * the check names none. */
100
+ owner: string | null
101
+ /** the group a check was filed under, for the few that declare one */
102
+ group: string | null
103
+ /** the wall clock ceiling it DECLARED, null when it named none and takes
104
+ * the default for its kind of body */
105
+ timeoutMs: number | null
106
+ /** excluded from a default run unless named with `--with` */
107
+ optIn: boolean
108
+ /** an idempotent repair exists: `shipkit check --fix <id>` applies it,
109
+ * then re-runs the check. `shipkit checks --fixable` is the catalog of
110
+ * everything in this repo that repairs itself. */
111
+ fixable: boolean
112
+ /** WHERE IT RUNS, the one reading of the `when` key, of the three
113
+ * spellings it replaces, and of a floor slot turned off (core/when.ts) */
114
+ when: WhenDecl
115
+ /** what it waits on, null when it waits on nothing */
116
+ deps: string[] | null
117
+ /** whose check it is: `shipkit` = it ships with the toolkit (a built-in
118
+ * SK check, the contracts pass, a floor slot nothing in the repo filled),
119
+ * `repo` = this repo declared it (a package.json script behind a floor
120
+ * slot, a task, a rule, a check file, a check file). */
121
+ from: CheckFrom
122
+ }
123
+
124
+ /** the one axis every check answers: who wrote the declaration */
125
+ export type { CheckFrom } from '../check.ts'
126
+
127
+ /** one composed check: the row every surface reads, and the runnable the
128
+ * runner takes. Two views of one declaration, never two enumerations. */
129
+ export type GateCheck = { row: CheckRow; runnable: RunnableCheck }
130
+
131
+ export type ComposeOpts = {
132
+ /** which tier the caller is asking for: `full` widens every build step's
133
+ * env, which is part of its key */
134
+ tier?: TestTier
135
+ }
136
+
137
+ /** a legacy `TaskDef` as the runner takes it. The config key still reads
138
+ * through the compatibility window, so the shape survives one release longer
139
+ * than the word for it. */
140
+ function taskBody(t: TaskDef): CheckBody {
141
+ return {
142
+ ...(t.cmd == null ? {} : { cmd: t.cmd }),
143
+ ...(t.logic == null ? {} : { run: async (ctx: CheckCtx) => { await t.logic!(ctx) } }),
144
+ ...(t.env == null ? {} : { env: t.env }),
145
+ ...(t.cwd == null ? {} : { cwd: t.cwd }),
146
+ }
147
+ }
148
+
149
+ const ttlMsOf = (ttl: string | undefined): number => (ttl == null || ttl === '' ? 0 : parseDuration(ttl))
150
+
151
+ /** the runnable half of a row: everything the runner needs, and nothing it
152
+ * has to look up again */
153
+ function runnableOf(row: CheckRow, body: CheckBody, o: { fix?: unknown; pointer?: string | null } = {}): RunnableCheck {
154
+ return {
155
+ id: row.decl.id,
156
+ title: row.title,
157
+ owner: row.owner ?? row.decl.id,
158
+ blocking: row.blocking,
159
+ build: row.build,
160
+ when: row.when,
161
+ deps: row.deps ?? [],
162
+ group: row.group,
163
+ timeoutMs: row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS),
164
+ ttlMs: ttlMsOf(row.decl.ttl),
165
+ rotation: row.decl.extra,
166
+ writes: row.decl.writes === true,
167
+ body,
168
+ fix: o.fix,
169
+ pointer: o.pointer ?? null,
170
+ population: populationOf(row.decl),
171
+ }
172
+ }
173
+
174
+ /** what the ROOT `test` check is keyed on, when the repo declared its read
175
+ * set (`defaults.test.inputs`): its own test files ride in as code, so the
176
+ * modules they import move the key, and every sub-unit dir is cut out of the
177
+ * declared paths, because those suites run in `test:<unit>`. A repo that
178
+ * declared nothing stays unkeyed, exactly as before (core/floor.ts says why) */
179
+ function rootTestKey(root: string, c: RepoConfig, floor: FloorResolution): { inputs: string[]; code: string[] } | null {
180
+ const t = floor.tasks.find((x) => x.id === 'test')
181
+ if (t?.inputs == null || t.cmd == null) return null
182
+ const files = listRepoFiles(root)
183
+ const subs = declaredTestUnits(c.units)
184
+ const own = files.filter((f) => isTestFile(f) && !subs.some((u) => f.startsWith(`${u.rel}/`)))
185
+ return { inputs: subs.length === 0 ? t.inputs : inputsOutsideUnits(t.inputs, subs.map((u) => u.rel), files), code: own }
186
+ }
187
+
188
+ /** a task's own description, else its command exiting 0 */
189
+ const taskDescription = (t: TaskDef): string =>
190
+ describeFirst(t.description, t.cmd == null ? null : `\`${t.cmd.join(' ')}\` exits 0`, `the ${t.id} task ${t.cmd == null ? 'passes' : 'exits 0'}`)
191
+
192
+ /** THE enumeration. Everything below reads it, nothing else walks. */
193
+ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts = {}): Promise<GateCheck[]> {
194
+ const out: GateCheck[] = []
195
+ const checks = await discoverChecks(root)
196
+ const floor = resolveFloor(c, root)
197
+ const env = { root, config: c }
198
+ const full = o.tier === 'full'
199
+ /** a check file's own path IS its code closure, so nothing declares it */
200
+ const relOf = new Map(checks.filter((u) => u.rel != null).map((u) => [u.id, u.rel!]))
201
+ const fromOf = new Map(checks.map((u) => [u.id, u.from]))
202
+ // a floor slot the REPO filled (its package.json script, its config
203
+ // override) is the repo's; one shipkit filled with its own module or the
204
+ // repo's tsc is the toolkit's. A task the config declares is the repo's.
205
+ const taskFrom = (id: string): CheckFrom => {
206
+ const discovered = fromOf.get(id)
207
+ if (discovered != null) return discovered
208
+ const src = floor.sources[id]
209
+ if (src == null) return 'repo'
210
+ return src === 'package.json script' || src === 'repo.config override' || src === 'unit' ? 'repo' : 'shipkit'
211
+ }
212
+ // LEGACY: a task that runs a check file under the check's own name was
213
+ // that file run twice, once per engine, when tasks and checks had a
214
+ // namespace each. One namespace makes the pair one check: the check row
215
+ // runs, carrying what the task added (core/legacy-shapes.ts rewrites the
216
+ // pair the same way on disk)
217
+ const legacy = discoverLegacyChecks(root)
218
+ const foldedInto = new Map<string, TaskDef>()
219
+ for (const t of c.tasks ?? []) {
220
+ const d = legacy.find((x) => x.name === t.id)
221
+ if (d == null || t.logic != null) continue
222
+ const r = checkFileRunBy(t, root)
223
+ if (r != null && r.rest.length === 0 && r.rel === relative(root, d.path).replace(/\\/g, '/')) foldedInto.set(t.id, t)
224
+ }
225
+ // BUILD STEPS: the floor, then the repo's own `tasks: []`. The whole run
226
+ // carries the tier flag rather than one task id: a repo names its suites
227
+ // what it likes, and a gate that guesses which one is the tests will miss
228
+ // one.
229
+ const rootTest = rootTestKey(root, c, floor)
230
+ for (const raw of [...floor.tasks, ...(c.tasks ?? []).filter((t) => foldedInto.get(t.id) !== t)]) {
231
+ const t: TaskDef = full ? { ...raw, env: { ...raw.env, [FULL_TESTS_ENV]: '1' } } : raw
232
+ const own = t.id === 'test' && floor.tasks.includes(raw) ? rootTest : null
233
+ const row: CheckRow = {
234
+ decl: {
235
+ id: t.id,
236
+ inputs: own?.inputs ?? t.inputs,
237
+ ttl: t.ttl,
238
+ never: t.never,
239
+ argv: t.cmd,
240
+ code: relOf.has(t.id) ? [relOf.get(t.id)!] : own?.code,
241
+ extra: t.env == null ? undefined : JSON.stringify(Object.entries(t.env).sort()),
242
+ },
243
+ title: t.id,
244
+ description: taskDescription(t),
245
+ // an optIn task is off the default run, which is the same shape as
246
+ // a check that opted out of the gate
247
+ gate: t.optIn !== true,
248
+ build: true,
249
+ // a build step IS the build: it has no non-blocking mode
250
+ blocking: true,
251
+ owner: t.cmd == null ? null : t.cmd.join(' '),
252
+ group: t.group ?? null,
253
+ timeoutMs: t.timeoutMs ?? null,
254
+ optIn: t.optIn === true,
255
+ // a build step runs a TOOL, and repairing what a tool reports is
256
+ // that tool's own job (a formatter, a codemod), never the gate's
257
+ fixable: false,
258
+ when: whenOfTask(c, t),
259
+ deps: t.deps ?? null,
260
+ from: taskFrom(t.id),
261
+ }
262
+ out.push({ row, runnable: runnableOf(row, taskBody(t)) })
263
+ }
264
+ // a slot `defaults: { <id>: false }` turned off never resolves to a task,
265
+ // so without this it had no row at all: no `runs in` value, no reason, and
266
+ // a count of off checks that read zero while a gate step sat dark. It has
267
+ // no command, which is why it declares `never` rather than joining the
268
+ // burn-down of checks nobody has keyed yet.
269
+ for (const id of floor.disabled) {
270
+ const row: CheckRow = {
271
+ decl: { id, never: `turned off by defaults.${id}: false, so there is no command to key` },
272
+ title: id,
273
+ description: slotDescription(id, declaredTestUnits(c.units).length > 0),
274
+ gate: false,
275
+ build: true,
276
+ blocking: true,
277
+ owner: null,
278
+ group: null,
279
+ timeoutMs: null,
280
+ optIn: false,
281
+ fixable: false,
282
+ when: whenOfDisabledSlot(id),
283
+ deps: null,
284
+ // the repo wrote the line that turned it off
285
+ from: 'repo',
286
+ }
287
+ // it is OFF, so the body is never reached: the runner reports the
288
+ // reason rather than running anything
289
+ out.push({ row, runnable: runnableOf(row, { run: () => {} }) })
290
+ }
291
+ // every `*.check.ts` this repo and the toolkit declare
292
+ const configs = new Map<string, RvtConfig>()
293
+ for (const u of checks) {
294
+ const w = whenOfCheck(c, u)
295
+ // an in process body declaring nothing is keyed on what it reads
296
+ // through its context (git-key.ts READS). The tier is part of that
297
+ // question: a body can read the env, and a quick answer must never
298
+ // stand in for a full one
299
+ const undeclared = u.def.run != null && u.def.inputs == null && u.def.ttl == null && u.def.never == null
300
+ // conservative: any module its body can reach that touches the disk or
301
+ // a process around `c` keeps it off its reads (core/raw-access.ts)
302
+ const raw = undeclared ? rawAccessOf(root, u, configs) : null
303
+ const reads = undeclared && raw == null
304
+ const extra = checkCacheExtra({ id: u.id, cacheKey: u.def.cacheKey }, env, w)
305
+ const row: CheckRow = {
306
+ decl: {
307
+ id: u.id,
308
+ inputs: u.def.inputs,
309
+ code: u.rel == null ? undefined : [u.rel],
310
+ ttl: u.def.ttl,
311
+ never: u.def.never,
312
+ argv: u.def.cmd,
313
+ extra: reads ? `${extra ?? ''}|tier=${full ? 'full' : 'quick'}` : extra,
314
+ writes: u.def.writes,
315
+ ...(reads ? { reads: true } : {}),
316
+ ...(raw != null ? { uncacheable: raw } : {}),
317
+ },
318
+ title: u.title,
319
+ description: u.description,
320
+ gate: w.at !== 'health',
321
+ build: u.def.build === true,
322
+ blocking: u.def.blocking,
323
+ owner: u.owner,
324
+ group: u.def.group ?? null,
325
+ timeoutMs: u.def.timeoutMs ?? null,
326
+ optIn: false,
327
+ fixable: u.def.fix != null,
328
+ when: w,
329
+ deps: u.def.deps ?? null,
330
+ from: u.from,
331
+ }
332
+ const body: CheckBody = {
333
+ ...(u.def.cmd == null ? {} : { cmd: u.def.cmd }),
334
+ ...(u.def.run == null ? {} : { run: u.def.run }),
335
+ ...(u.def.env == null ? {} : { env: u.def.env }),
336
+ // relative to the root, and an absolute one stands as written
337
+ ...(u.def.cwd == null ? {} : { cwd: resolve(root, u.def.cwd) }),
338
+ }
339
+ out.push({ row, runnable: runnableOf(row, body, { fix: u.def.fix, pointer: u.def.cmd?.join(' ') ?? null }) })
340
+ }
341
+ // LEGACY: `rules: []`, read as a check whose body is its lint. Dies with
342
+ // that config key once `shipkit fleet` shows no repo below 0.40.
343
+ for (const r of c.rules ?? []) {
344
+ const row: CheckRow = {
345
+ decl: {
346
+ id: r.id,
347
+ inputs: r.inputs,
348
+ code: r.code,
349
+ ttl: r.ttl,
350
+ never: r.never,
351
+ extra: ruleCacheExtra(r, env),
352
+ },
353
+ title: r.title,
354
+ description: describeFirst(r.title, `rule ${r.id} holds`),
355
+ gate: true,
356
+ build: false,
357
+ blocking: r.blocking,
358
+ owner: r.owner,
359
+ group: null,
360
+ timeoutMs: null,
361
+ optIn: false,
362
+ fixable: r.fix != null,
363
+ when: whenOfRule(c, r.id),
364
+ deps: null,
365
+ from: 'repo',
366
+ }
367
+ out.push({ row, runnable: runnableOf(row, { run: (ctx) => r.lint(ctx, { root: ctx.root, config: ctx.config }) }, { fix: r.fix }) })
368
+ }
369
+ // LEGACY: a check script with no check object, read as a `cmd` check
370
+ // running itself. The reader is a source scanner rather than an import,
371
+ // which is why it cannot carry a fixer or a typed declaration
372
+ // (core/discover-legacy-checks.ts).
373
+ for (const found of legacy) {
374
+ const rel = relative(root, found.path).replace(/\\/g, '/')
375
+ const task = foldedInto.get(found.name)
376
+ // a task that is not optIn put an off gate file in the gate, and the
377
+ // file's own cache declaration wins over the task's
378
+ const d = task == null ? found : { ...found, gate: found.gate || task.optIn !== true }
379
+ const cached = d.inputs != null || d.ttl != null || d.never != null
380
+ const row: CheckRow = {
381
+ decl: {
382
+ id: d.name,
383
+ inputs: d.inputs ?? (cached ? undefined : task?.inputs),
384
+ ttl: d.ttl ?? (cached ? undefined : task?.ttl),
385
+ never: d.never ?? (cached ? undefined : task?.never),
386
+ code: [rel],
387
+ argv: ['bun', rel],
388
+ // no `extra`, so WHERE it runs stays out of a legacy check's
389
+ // key, deliberately: the field is read by a source scanner
390
+ // that cannot carry a `cacheKey` either, and adding the
391
+ // rotation now would bust every field repo's TTL cache once
392
+ // for a file form that is refused after 0.40 anyway
393
+ },
394
+ title: d.name,
395
+ description: describeFirst(`\`bun ${rel}\` exits 0`, `${d.name} exits 0`),
396
+ gate: d.gate,
397
+ build: false,
398
+ // the file wins, then the repo default, then nothing: an
399
+ // unclassified check is an error, never a guess
400
+ blocking: d.blocking ?? c.checkDefaults?.blocking ?? null,
401
+ owner: rel,
402
+ group: task?.group ?? null,
403
+ timeoutMs: task?.timeoutMs ?? null,
404
+ optIn: false,
405
+ // the reader is a source scanner, so it cannot carry a function
406
+ fixable: false,
407
+ when: whenOfLegacyCheck(c, d),
408
+ deps: task?.deps ?? null,
409
+ from: 'repo',
410
+ }
411
+ // the pointer names the file the way a reader types it, never the
412
+ // absolute path discovery happened to hand the spawner
413
+ const body: CheckBody = { cmd: ['bun', d.path], ...(task?.env == null ? {} : { env: task.env }), ...(task?.cwd == null ? {} : { cwd: resolve(root, task.cwd) }) }
414
+ out.push({ row, runnable: runnableOf(row, body, { pointer: `bun ${rel}` }) })
415
+ }
416
+ return out
417
+ }
418
+
419
+ /** every check the gate knows, with its metadata. */
420
+ export async function gateInventory(root: string, c: RepoConfig, o: ComposeOpts = {}): Promise<CheckRow[]> {
421
+ return (await composeChecks(root, c, o)).map((g) => g.row)
422
+ }
423
+
424
+ /** the same list, as the runner takes it */
425
+ export async function gateRunnables(root: string, c: RepoConfig, o: ComposeOpts = {}): Promise<RunnableCheck[]> {
426
+ return (await composeChecks(root, c, o)).map((g) => g.runnable)
427
+ }
428
+
429
+ /** the inventory narrowed to the cache declaration, which is what the key
430
+ * layer keys. Same walk, so a surface can never enumerate a different set. */
431
+ export async function gateChecks(root: string, c: RepoConfig, o: ComposeOpts = {}): Promise<CheckDecl[]> {
432
+ return (await composeChecks(root, c, o)).map((g) => g.row.decl)
433
+ }