rvlib-shipkit 0.38.0 → 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 +371 -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 +216 -18
  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
@@ -1,35 +1,31 @@
1
- // periodic health — contracts + every *.check.ts in the repo. Discovery IS
2
- // the registration (core/discover-checks.ts); each check runs as a
3
- // subprocess: wire lines parsed back, exit code is truth.
1
+ // THE HEALTH PASS — `shipkit check`: every check this repo has except the
2
+ // build steps, through the one runner (core/check-runner.ts). `shipkit ci` asks
3
+ // the same runner for the gate's set instead (core/run-ci.ts), so the two
4
+ // commands differ by the QUESTION and never by the engine.
4
5
  //
5
- // NOTHING here decides what may be remembered any more. A rule or a check is
6
- // replayed when it DECLARED how ("inputs" makes it content-keyed and the
7
- // ledger owns it, "ttl" makes it a world probe and CheckSuite owns it), and
8
- // otherwise it is measured every run. The blanket 30-minute orchestrator TTL
9
- // that used to cover every plain check died with that: it cached by default,
10
- // on top of whatever the check cached for itself, so one question had two
11
- // staleness windows and neither was declared by anyone. `--fresh`
12
- // (SHIPKIT_FRESH=1, which travels to children) busts the one that is left.
13
- import { join, relative } from 'path'
14
- import { CheckSuite, type CheckOutcome } from './check-suite.ts'
15
- import { gateUnits } from '../ledger/gate-units.ts'
16
- import { Ledger, stampReplayed } from '../ledger/ledger.ts'
6
+ // NOTHING here decides what may be remembered. A check is replayed when it
7
+ // DECLARED how ("inputs" makes it content-keyed and the ledger owns it, "ttl"
8
+ // makes it a world probe and the world cache owns it), and otherwise it is
9
+ // measured every run. `--fresh` (SHIPKIT_FRESH=1, which travels to children)
10
+ // busts both.
11
+ import { join } from 'path'
12
+ import { composeChecks, envTier, gateInventory, type GateCheck, type TestTier } from '../ledger/gate-checks.ts'
13
+ import { Ledger } from '../ledger/ledger.ts'
14
+ import { RunLog, type InvokedBy } from '../ledger/run-log.ts'
17
15
  import { storeRoot } from '../ledger/store.ts'
18
- import { CheckCtx } from './check-ctx.ts'
19
- import { discoverChecks } from './discover-checks.ts'
20
- import { spawnWithTimeout } from './spawn-timeout.ts'
16
+ import { gateBinary } from '../ledger/toolchain-identity.ts'
17
+ import { runChecks, type Audience, type CheckEvent, type CheckResult } from './check-runner.ts'
21
18
  import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
22
19
  import { reportToMotherhome } from './motherhome.ts'
23
- import { RULES } from '../rules/rule-registry.ts'
24
- import { ruleCacheExtra, runRuleLint } from '../rules/rule.ts'
25
- import { formatLine, parseWireOutput, type Line } from './wire.ts'
20
+ import { unknownWhenIds } from './when.ts'
21
+ import { formatLine, type Line } from './wire.ts'
26
22
  import { writeStatus } from './write-status.ts'
27
- import { gateInventory } from '../ledger/gate-units.ts'
28
- import { UNIT_TABLE_FILE, writeUnitTable } from '../ledger/unit-table.ts'
23
+ import { CHECK_TABLE_FILE, writeCheckTable } from '../ledger/check-table.ts'
29
24
  import { SHARE_ENV } from '../release/releases-share.ts'
30
25
  import { rmSync } from 'fs'
26
+ import { CI_LOG_DIR } from './layout.ts'
31
27
 
32
- const CHECK_TIMEOUT_MS = 120_000
28
+ export type { CheckEvent } from './check-runner.ts'
33
29
 
34
30
  export type CheckOpts = {
35
31
  json?: boolean
@@ -40,65 +36,64 @@ export type CheckOpts = {
40
36
  status?: boolean
41
37
  /** line sink — commands pass their context stream; defaults to console */
42
38
  print?: (line: string) => void
43
- /** per-check completion tap — the TUI checks panel streams results in */
44
- onResult?: (name: string, r: CheckOutcome) => void
45
- /** the already-resolved config, same contract as runCi: the caller that
46
- * decides severity and the engine that runs the rules must read the same
47
- * object, or a blocking rule failure silently reclassifies as a probe */
39
+ /** per-CHECK completion tap: the real status, whether it replayed, what it
40
+ * cost. The demotion of a non-blocking failure happens in the LINES (a
41
+ * FAIL is rewritten to a warn before anything sees it), so `status` here
42
+ * is the check's REAL outcome and `advisory` says it cannot block. */
43
+ onCheck?: (e: CheckEvent) => void
44
+ /** the run's timings log, when the caller already opened one (runCi does),
45
+ * and the caller then owns the flush. null = record nothing. */
46
+ runLog?: RunLog | null
47
+ /** who started this run, as the run record files it: a terminal, the
48
+ * hub's buttons, or the hub's daily timer */
49
+ invokedBy?: InvokedBy
50
+ /** the already-resolved config: the caller that decides severity and the
51
+ * engine that runs the checks must read the same object */
48
52
  config?: RepoConfig
49
- /** run only the checks that can BLOCK. A check declaring blocking:false
50
- * cannot change the outcome, so running it is pure cost: a release does
51
- * not need to know whether a DNS provider is reachable, and probing one
52
- * spends a third-party API call per publish. Undeclared checks are still
53
- * collected, because an unclassified check stays fatal. */
53
+ /** run only the checks that can BLOCK (the release path) */
54
54
  onlyBlocking?: boolean
55
- /** the caller is the GATE (`shipkit ci`), so checks that declared
56
- * `export const gate = false` are skipped. `shipkit check` leaves it unset
57
- * and runs everything: the health surface is where a human todo list
58
- * belongs, and the gate is not. */
59
- gateOnly?: boolean
60
- /** the caller narrowed the run to these ids (`ci --only`). Empty or absent
61
- * means everything. A `--only` that narrowed TASKS while every check still
62
- * ran made the flag actively harmful: `ci --only cycles`, a 0.03s task,
63
- * probed every third-party service the repo knows about. One flag, one
64
- * meaning, across all three surfaces. */
55
+ /** which test tier the caller is asking for */
56
+ tier?: TestTier
57
+ /** which question: the gate's set (`shipkit ci`) or the health pass's */
58
+ audience?: Audience
59
+ /** the caller narrowed the run to these ids (`--only`) */
65
60
  only?: string[]
61
+ /** ids ADDED to a default run (`--with`) */
62
+ withIds?: string[]
63
+ /** a label several checks share, parked at once */
64
+ skipGroups?: string[]
65
+ /** stop at the first blocking failure */
66
+ bail?: boolean
67
+ /** the BUILD steps alone (the TUI's tasks panel) */
68
+ buildOnly?: boolean
69
+ /** the gate asks the proactive checks too (`release.requireHealthy`) */
70
+ includeProactive?: boolean
71
+ /** ids this caller does not need (a sub-unit release) */
72
+ exclude?: string[]
73
+ /** the already-composed check list, when the caller built one (runCi does,
74
+ * to open the ledger on it). One walk per run, the same contract the
75
+ * ledger and the run record follow. */
76
+ composed?: GateCheck[]
66
77
  /** the run's ledger, when the caller already opened one (runCi does). A
67
78
  * standalone `shipkit check` opens its own. */
68
79
  ledger?: Ledger | null
80
+ /** the gate binary that own ledger is keyed on (default: the one running
81
+ * it). Ignored when `ledger` is passed, which carries its own. */
82
+ cliEntry?: string
69
83
  /** refresh the motherhome registry row. runCi passes false and does it
70
84
  * ITSELF, after the ledger has committed: that row is a TRACKED file under
71
85
  * `wiki/`, so writing it before the post-run re-hash makes the gate look
72
86
  * like an agent editing the tree and throws away every result the run just
73
- * measured. Two reports per gate run was duplication anyway. */
87
+ * measured. */
74
88
  reportHome?: boolean
75
89
  }
76
90
 
77
- /** does this rule or check answer to one of the `--only` ids? Matched against
78
- * every name the surface is known by (a rule's id AND its title, a discovered
79
- * check's name AND its path), because the id an operator types is whichever
80
- * one they last saw on the wire. */
81
- export function matchesOnly(only: string[] | undefined, ...names: string[]): boolean {
82
- if (!isNarrowed(only)) return true
83
- return only.some((want) => names.some((n) => n.toLowerCase() === want.toLowerCase()))
84
- }
85
-
86
91
  /** did the operator narrow this run? The ONE reading of `--only` everywhere: an
87
92
  * empty list is not a selection, it is the absence of one. */
88
93
  export function isNarrowed(only: string[] | undefined): only is string[] {
89
94
  return only != null && only.length > 0
90
95
  }
91
96
 
92
- /** every id `--only` can name that is NOT a task: rule ids, rule titles,
93
- * discovered check names and their repo-relative paths. `--only` addresses all
94
- * three surfaces, so the task runner has to be told which ids belong to the
95
- * other two, or it answers `unknown task id` for a check that does run. */
96
- export function checkSideIds(root: string, c: RepoConfig): string[] {
97
- const ids = [...RULES, ...(c.rules ?? [])].flatMap((r) => [r.id, r.title])
98
- for (const d of discoverChecks(root)) ids.push(d.name, relative(root, d.path))
99
- return ids
100
- }
101
-
102
97
  export type CheckRun = {
103
98
  /** wire lines, with non-blocking failures already downgraded to warnings */
104
99
  lines: Line[]
@@ -108,201 +103,148 @@ export type CheckRun = {
108
103
  advisoryFailures: string[]
109
104
  /** checks that never declared `blocking` — always fatal */
110
105
  undeclared: string[]
106
+ /** `when` entries naming no check of this repo — a toggle that does
107
+ * nothing is worse than none, so it is fatal too */
108
+ unknownWhen: string[]
109
+ /** what every asked-for check did, in declaration order */
110
+ results: CheckResult[]
111
+ /** the run was interrupted or bailed out */
112
+ interrupted: boolean
111
113
  }
112
114
 
113
- /** run every check and classify by what it declared. This is the whole engine;
114
- * runCi wraps it with the task DAG, and nothing else decides severity. */
115
- export async function collectChecks(root: string, o: CheckOpts = {}): Promise<CheckRun> {
115
+ /** THE run body: compose this repo's checks, run the set the audience asks
116
+ * for, and classify the outcome by what each check declared. `shipkit check`
117
+ * and `shipkit ci` both come through here, and nothing else decides
118
+ * severity. */
119
+ export async function runCheckPass(root: string, o: CheckOpts = {}): Promise<CheckRun> {
116
120
  const c = o.config ?? (await loadRepoConfig(root))
121
+ const print = o.print ?? ((l: string) => console.log(l))
117
122
  // env carries the flags to child checks — restored after the run so a
118
- // long-lived caller (doctor, TUI, hub) is never poisoned by one
123
+ // long-lived caller (the TUI, the hub) is never poisoned by one
119
124
  // fresh/frozen invocation
120
125
  const prevFresh = process.env.SHIPKIT_FRESH
121
126
  const prevFrozen = process.env.SHIPKIT_FROZEN
122
127
  if (o.fresh === true) process.env.SHIPKIT_FRESH = '1'
123
128
  if (o.frozen === true) process.env.SHIPKIT_FROZEN = '1'
124
- // the caller's ledger, or this run's own. `shipkit check` is a full run
125
- // of every rule and check, so it deserves the same replay the gate gets.
129
+ const tier: TestTier = o.tier ?? envTier()
130
+ const audience: Audience = o.audience ?? 'health'
131
+ const composed = o.composed ?? (await composeChecks(root, c, { tier }))
132
+ const checks = composed.map((g) => g.runnable)
133
+ // the caller's ledger, or this run's own. `shipkit check` is a full run of
134
+ // every check it owns, so it deserves the same replay the gate gets.
126
135
  let ledger: Ledger | null = o.ledger ?? null
127
136
  if (ledger === null && o.ledger === undefined) {
128
137
  try {
129
- ledger = Ledger.open(root, gateUnits(root, c), { cliEntry: new URL('../cli.ts', import.meta.url).pathname, fresh: o.fresh })
138
+ ledger = Ledger.open(root, composed.map((g) => g.row.decl), { cliEntry: o.cliEntry ?? gateBinary(), fresh: o.fresh })
130
139
  } catch { ledger = null }
131
140
  }
132
- const replayed = new Set<string>()
133
- const unitIdByName = new Map<string, string>()
134
- /** what a TTL'd answer is remembered under, beside its rotation key. A TTL
135
- * says how long the WORLD keeps; it cannot say that the probe was
136
- * rewritten, or that a different shipkit measured it. Without these two a
137
- * fixed check replayed its old verdict for the rest of its TTL. */
138
- const worldKey = (unitId: string, rotation?: string): string => {
139
- let code = ''
140
- try { code = ledger?.layer.keyFor(unitId).codeKey ?? '' } catch { code = '' }
141
- return `tc=${ledger?.layer.toolchainId ?? ''}\0code=${code}\0${rotation ?? ''}`
142
- }
143
- const suite = new CheckSuite({
144
- name: c.name,
145
- storeDir: storeRoot(root),
146
- onResult: (name, r) => {
147
- o.onResult?.(name, r)
148
- // a replay measured nothing, and a red is never stored. A check
149
- // that cannot block had its FAIL rewritten to a warn before this
150
- // fires, so the demotion list is what says it failed: a content
151
- // key has no TTL, so storing that red would pin it until an
152
- // unrelated edit moved the tree, and "the box is off" is fixed by
153
- // turning the box on, which changes no file.
154
- if (ledger == null || r.cached || replayed.has(name) || r.status === 'skip' || r.status === 'fail') return
155
- if (advisoryFailures.includes(name)) return
156
- const id = unitIdByName.get(name)
157
- if (id == null) return
158
- ledger.record(id, { status: r.status, lines: r.lines, measuredMs: r.ms })
141
+ // the caller's log, or this run's own: `shipkit check` measures checks too,
142
+ // and its timings are as honest as the gate's
143
+ const log: RunLog | null = o.runLog !== undefined ? o.runLog : RunLog.open(root, {
144
+ invocation: {
145
+ kind: audience === 'gate' ? 'ci' : 'check',
146
+ label: isNarrowed(o.only) ? `shipkit check --only ${o.only.join(',')}` : 'shipkit check',
147
+ by: o.invokedBy ?? 'cli',
148
+ narrowed: isNarrowed(o.only),
159
149
  },
160
150
  })
161
- /** a stored answer, re-emitted as a runner that does no work. Keeping it in
162
- * the suite rather than splicing lines afterwards is what preserves
163
- * declaration order, which the wire contract is frozen on. */
164
- const addReplay = (unitId: string, name: string, blocking: boolean): boolean => {
165
- const hit = ledger?.replay(unitId)
166
- if (hit == null) return false
167
- replayed.add(name)
168
- unitIdByName.set(name, unitId)
169
- suite.addCheck({
170
- blocking,
171
- name,
172
- logic: (t) => {
173
- for (const l of stampReplayed(hit.lines, hit.measuredMs)) t.lines.push(l)
174
- if (t.lines.some((l) => l.kind === 'warn' || l.kind === 'fix-warn')) t.warned = true
175
- },
176
- })
177
- return true
178
- }
179
- // built-in SK rules (rules/rule-registry.ts) THEN the repo's own
180
- // (repo.config rules: []), id-tagged, crash-safe, fix-pointer floor
181
- // (rules/rule.ts runRuleLint owns the semantics). `shipkit ci` runs this
182
- // same engine, so a blocking rule blocks a commit as well as a doctor run
183
- const undeclared: string[] = []
184
- const advisoryFailures: string[] = []
185
- // a non-blocking failure becomes a WARNING here, at the source. printing
186
- // FAIL and exiting 0 would be the tool lying about its own severity, and
187
- // demoting in the check means the suite's exit code already means exactly
188
- // "something that blocks you failed".
189
- const demote = (t: CheckCtx, blocking: boolean, name: string): void => {
190
- if (blocking || !t.failed) return
191
- if (!advisoryFailures.includes(name)) advisoryFailures.push(name)
192
- t.lines = t.lines.map((l) => (l.kind === 'fail' ? { ...l, kind: 'warn' as const } : l.kind === 'fix-fail' ? { ...l, kind: 'fix-warn' as const } : l))
193
- t.failed = false
194
- t.warned = true
195
- }
196
- for (const rule of [...RULES, ...(c.rules ?? [])]) {
197
- if (!matchesOnly(o.only, rule.id, rule.title)) continue
198
- if (o.onlyBlocking === true && !rule.blocking) continue
199
- unitIdByName.set(rule.title, rule.id)
200
- if (addReplay(rule.id, rule.title, rule.blocking)) continue
201
- suite.addCheck({
202
- blocking: rule.blocking,
203
- name: rule.title,
204
- remainValidFor: rule.ttl,
205
- cacheExtra: () => worldKey(rule.id, ruleCacheExtra(rule, { root, config: c })),
206
- logic: async (t) => { await runRuleLint(rule, t, { root, config: c }); demote(t, rule.blocking, rule.title) },
207
- })
208
- }
209
- // ONE release survey for the whole run: several discovered checks call
151
+ // ONE release survey for the whole run: several probes call
210
152
  // collectReports, each in its own subprocess, and the survey is ~3s of
211
153
  // network. The path is per RUN and never reused across runs, so nothing
212
154
  // here can serve a stale answer to a later gate (releases-share.ts).
213
- // This is the ONLY owner: nothing else in shipkit sets the variable, and an
214
- // inherited one can only come from a caller outside the binary, which then
215
- // owns its own cleanup.
216
155
  const inherited = process.env[SHARE_ENV]
217
- const sharePath = inherited != null && inherited !== '' ? inherited : join(root, 'tmp/shipkit-ci', `releases-${process.pid}-${Date.now()}.json`)
218
- const childEnv: Record<string, string | undefined> = {
219
- ...process.env,
220
- [SHARE_ENV]: sharePath,
221
- ...(o.fresh === true ? { SHIPKIT_FRESH: '1' } : {}),
222
- }
223
- for (const d of discoverChecks(root)) {
224
- const rel = relative(root, d.path)
225
- // file wins, then the repo default, then nothing: an unclassified
226
- // check is an error, never a guess
227
- if (!matchesOnly(o.only, d.name, rel)) continue
228
- // `ci` answers "is my change good". A check that answers something else,
229
- // and that can sit red for months with nothing wrong, is not part of
230
- // that answer at any interval. `shipkit check` still runs it, and
231
- // naming it in `--only` still runs it, so opting out is never a way to
232
- // make a check unreachable. "not narrowed" is an EMPTY list as much as
233
- // an absent one: the CLI's --only is an Option.Array defaulting to [],
234
- // so a `== null` test here reads every plain gate run as narrowed and
235
- // opts nothing out (matchesOnly above uses the same predicate).
236
- if (o.gateOnly === true && !d.gate && !isNarrowed(o.only)) continue
237
- const blocking = d.blocking ?? c.checkDefaults?.blocking
238
- if (blocking == null) undeclared.push(rel)
239
- if (o.onlyBlocking === true && blocking !== true) continue
240
- unitIdByName.set(d.name, d.name)
241
- if (addReplay(d.name, d.name, blocking ?? true)) continue
242
- suite.addCheck({
243
- // a check about the WORLD may remember a measured negative; one
244
- // about this repo never does (check-suite.ts, the rule)
245
- blocking: blocking ?? true,
246
- name: d.name,
247
- // a WORLD check says how long its answer keeps; everything else is
248
- // measured every run rather than blanket-cached by this layer
249
- remainValidFor: d.ttl ?? undefined,
250
- cacheExtra: () => worldKey(d.name),
251
- logic: async (t) => {
252
- const r = await spawnWithTimeout(['bun', d.path], { cwd: root, timeoutMs: CHECK_TIMEOUT_MS, env: childEnv })
253
- for (const l of parseWireOutput(r.out)) t.lines.push(l)
254
- if (t.lines.some((l) => l.kind === 'warn')) t.warned = true
255
- if (t.lines.some((l) => l.kind === 'fail')) t.failed = true
256
- // exit code is truth — a non-zero exit with no FAIL line
257
- // (crash, non-wire output, timeout) must still fail loud
258
- if (r.code !== 0 && !t.failed)
259
- t.fail(`${d.name} exited ${r.code}${r.timedOut ? ` (timeout after ${CHECK_TIMEOUT_MS}ms)` : ''} — ${r.out.trim().split('\n').at(-1)?.slice(0, 160) ?? ''}`, `bun ${rel}`)
260
- demote(t, blocking ?? true, d.name)
261
- },
262
- })
156
+ const sharePath = inherited != null && inherited !== '' ? inherited : join(root, CI_LOG_DIR, `releases-${process.pid}-${Date.now()}.json`)
157
+ const collected: Line[] = []
158
+ const sink = (l: string): void => print(l)
159
+ const summary = await runChecks({
160
+ checks,
161
+ root,
162
+ config: c,
163
+ audience,
164
+ fullTier: tier === 'full',
165
+ logDir: c.logDir ?? join(root, CI_LOG_DIR),
166
+ cache: { storeDir: storeRoot(root), ns: c.name, now: Date.now },
167
+ ledger,
168
+ log,
169
+ childEnv: { [SHARE_ENV]: sharePath, ...(o.fresh === true ? { SHIPKIT_FRESH: '1' } : {}) },
170
+ bail: o.bail,
171
+ skipGroups: o.skipGroups,
172
+ onlyIds: o.only,
173
+ withIds: o.withIds,
174
+ onlyBlocking: o.onlyBlocking,
175
+ buildOnly: o.buildOnly,
176
+ includeProactive: o.includeProactive,
177
+ exclude: o.exclude,
178
+ print: sink,
179
+ onCheck: (e) => { collected.push(...e.lines); o.onCheck?.(e) },
180
+ })
181
+ const lines: Line[] = []
182
+ const say = (l: Line): void => { lines.push(l); print(formatLine(l, false)) }
183
+ // a declaration in a shape the check file replaced is reported by check
184
+ // SK016, which also carries the fixer that rewrites it: one owner, one
185
+ // line, one command, rather than a warning per surface.
186
+ //
187
+ // checks this run did not ask about: named here rather than printed one
188
+ // line each, because twenty four `skip:` lines is the wall of text the
189
+ // page just replaced
190
+ if (summary.parked.length > 0) {
191
+ const head = summary.parked.slice(0, 8).join(', ')
192
+ say({ kind: 'skip', text: `${summary.parked.length} check(s) not in this run (health only, release only at the quick tier, or a skipped group): ${head}${summary.parked.length > 8 ? `, and ${summary.parked.length - 8} more` : ''}` })
263
193
  }
264
- const r = await suite.execute()
265
194
  // the survey dies with the run that made it, and only the run that made it
266
195
  if (inherited == null || inherited === '') for (const f of [sharePath, `${sharePath}.lock`]) { try { rmSync(f, { force: true }) } catch {} }
267
196
  if (prevFresh == null) delete process.env.SHIPKIT_FRESH; else process.env.SHIPKIT_FRESH = prevFresh
268
197
  if (prevFrozen == null) delete process.env.SHIPKIT_FROZEN; else process.env.SHIPKIT_FROZEN = prevFrozen
269
198
  // frozen = a release-shaped run: reporting would re-dirty the tree the
270
- // caller is about to demand clean (field wart 2026-08-05, second head)
199
+ // caller is about to demand clean
271
200
  if (o.frozen !== true && o.reportHome !== false) reportToMotherhome(root, c)
272
201
  // the caller that OPENED the ledger owns committing it: runCi commits once
273
- // after its tasks and its checks, so a task and a check in one run share
274
- // one post-run re-hash
275
- if (o.ledger === undefined && ledger != null) r.lines.push(...ledger.header(ledger.commit()))
276
- return { lines: r.lines, blocked: r.exitCode !== 0, advisoryFailures, undeclared }
202
+ // after everything, so one post-run re-hash covers the whole gate
203
+ if (o.ledger === undefined && ledger != null) for (const l of ledger.header(ledger.commit())) say(l)
204
+ // the caller that OPENED the record owns writing it, the same rule
205
+ if (o.runLog === undefined && log != null) for (const l of log.flush()) say(l)
206
+ // a `when` id naming nothing is a switch wired to no check: the id is read
207
+ // against the WHOLE inventory this run composed, never its narrowed list
208
+ const unknownWhen = unknownWhenIds(composed.map((g) => g.row), c)
209
+ return {
210
+ lines: [...collected, ...lines],
211
+ blocked: !summary.ok,
212
+ advisoryFailures: summary.advisoryFailures,
213
+ undeclared: summary.undeclared,
214
+ unknownWhen,
215
+ results: summary.results,
216
+ interrupted: summary.interrupted,
217
+ }
277
218
  }
278
219
 
279
- /** `shipkit check` as a standalone command: the same engine, printed. */
220
+ /** `shipkit check` as a standalone command: the same runner, printed. */
280
221
  export async function runCheck(root: string, o: CheckOpts = {}): Promise<number> {
281
222
  const print = o.print ?? ((l: string) => console.log(l))
282
223
  const c = o.config ?? (await loadRepoConfig(root))
283
- const run = await collectChecks(root, { ...o, config: c })
284
- const code = run.blocked || run.undeclared.length > 0 ? 1 : 0
224
+ const run = await runCheckPass(root, { ...o, config: c })
225
+ const code = run.blocked || run.undeclared.length > 0 || run.unknownWhen.length > 0 ? 1 : 0
285
226
  if (o.json === true) { print(JSON.stringify(run)); return code }
286
- const color = process.stdout.isTTY && process.env.NO_COLOR == null
287
- for (const l of run.lines) print(formatLine(l, color))
288
227
  for (const u of run.undeclared) {
289
228
  print(`FAIL: ${u} does not declare whether it blocks`)
290
- print(` fix: add \`export const blocking = true\` (about this repo) or \`false\` (about the world) to that file`)
229
+ print(` fix: add \`blocking: true\` (about this repo) or \`false\` (about the world) to that check`)
230
+ }
231
+ for (const id of run.unknownWhen) {
232
+ print(`FAIL: when.${id} names no check of this repo`)
233
+ print(` fix: \`shipkit checks\` lists every id; drop the entry or correct it`)
291
234
  }
292
235
  if (o.status !== false) {
293
- // the producing command is named honestly: a checks-only run must not
236
+ // the producing command is named honestly: a health run must not
294
237
  // overwrite the gate verdict with a green nobody gated
295
238
  const st = writeStatus(root, {
296
239
  name: c.name,
297
240
  command: 'shipkit check',
298
241
  lines: run.lines.map((l) => formatLine(l, false)),
299
242
  exitCode: code,
300
- sidecar: { file: UNIT_TABLE_FILE, says: 'Every unit this repo can run, and whether it blocks you' },
243
+ sidecar: { file: CHECK_TABLE_FILE, says: 'Every check this repo can run, and whether it blocks you' },
301
244
  })
302
245
  if (!st.written) print(`WARN ${st.warn}`)
303
- const units = writeUnitTable(root, { name: c.name, rows: gateInventory(root, c) })
304
- if (!units.written) print(`WARN ${units.warn}`)
305
-
246
+ const checks = writeCheckTable(root, { name: c.name, rows: await gateInventory(root, c) })
247
+ if (!checks.written) print(`WARN ${checks.warn}`)
306
248
  }
307
249
  return code
308
250
  }