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,22 +1,30 @@
1
- // THE KEY. What question did this unit answer, and is that still the same
1
+ // THE KEY. What question did this check answer, and is that still the same
2
2
  // question? Nothing in this file caches anything: it produces the identity a
3
3
  // later ledger will store results under, and it refuses to produce one
4
4
  // whenever the answer would be a guess.
5
5
  //
6
- // THREE POPULATIONS, declared at registration, never inferred:
6
+ // FOUR POPULATIONS, set at registration, never inferred from source:
7
7
  //
8
8
  // CONTENT declares `inputs` — the paths it reads. Keyed on their git object
9
- // ids, plus the unit's own code (module graph), plus the toolchain
9
+ // ids, plus the check's own code (module graph), plus the toolchain
10
10
  // (which content-hashes the gate binary, so editing shipkit moves
11
11
  // every key in every repo), plus argv and declared env. No TTL: a
12
12
  // content key is either the same question or a different one.
13
13
  // WORLD declares `ttl` — it reads the network, the keychain, $PATH, a git
14
14
  // remote or the clock. Nothing on disk describes its answer, so it
15
15
  // keeps the existing clean-pass TTL and its `cacheKey`, unchanged.
16
- // NEITHER declares nothing. Never cached. This is the DEFAULT, and it is
17
- // the whole safety story: forgetting to declare costs speed, never
18
- // correctness, so the burn-down list below is a to-do rather than a
19
- // hazard. It holds only while nothing declares ON a unit's behalf:
16
+ // READS a `run` check file that declares none of the three: the default
17
+ // for that shape. Its body reads through the check context, which
18
+ // records every read, so the key here is only the question (check,
19
+ // code, toolchain, tier) and the ledger stores the recorded read set
20
+ // beside the answer, re-checking it before any replay
21
+ // (ledger/ledger.ts). One whose code reaches the disk or a
22
+ // process around the context stays NEITHER (core/raw-access.ts).
23
+ // NEITHER a `cmd` check or a legacy shape that declares nothing, or any
24
+ // check that says `never`. Never cached. For those it is the
25
+ // DEFAULT, and it is the whole safety story: forgetting to declare
26
+ // costs speed, never correctness, so the burn-down list below is a
27
+ // to-do rather than a hazard. It holds only while nothing declares ON a check's behalf:
20
28
  // the gate floor gives `['.']` to the four slots tracked source
21
29
  // decides and deliberately leaves `test` here (core/floor.ts says
22
30
  // why). Declaring BOTH is a registration failure, loud, because the
@@ -39,40 +47,48 @@ import { installIdentity, toolchainIdentity } from './toolchain-identity.ts'
39
47
 
40
48
  /** the key format. Bumped when a component is added or its rendering changes,
41
49
  * so an old ledger can never answer a new question. */
42
- export const KEY_VERSION = 2
50
+ export const KEY_VERSION = 3
43
51
 
44
- export type Population = 'content' | 'world' | 'none'
52
+ export type Population = 'content' | 'world' | 'reads' | 'none'
45
53
 
46
- /** what one gate unit (a task, a rule, a discovered check) declares about its
47
- * own cacheability. `gate-units.ts` builds these from the live registries. */
48
- export type UnitDecl = {
49
- /** the id an operator types: a task id, a rule id, a check name */
54
+ /** what one gate check declares about its own cacheability.
55
+ * `gate-checks.ts` builds these from the one enumeration. */
56
+ export type CheckDecl = {
57
+ /** the id an operator types, unique in the repo */
50
58
  id: string
51
- kind: 'task' | 'rule' | 'check'
52
- /** CONTENT: repo-relative paths this unit reads. Presence declares the population. */
59
+ /** CONTENT: repo-relative paths this check reads. Presence declares the population. */
53
60
  inputs?: string[]
54
61
  /** WORLD: clean-pass duration, e.g. '24h'. Presence declares the population. */
55
62
  ttl?: string
56
- /** repo-relative entry files of the unit's OWN code — the module-graph
63
+ /** repo-relative entry files of the check's OWN code — the module-graph
57
64
  * roots. A discovered check gets its own file for free. */
58
65
  code?: string[]
59
- /** the argv a subprocess unit runs */
66
+ /** the argv a subprocess check runs */
60
67
  argv?: string[]
61
68
  /** env var NAMES whose values belong in the key */
62
69
  env?: string[]
63
70
  /** anything else that identifies the question (a rule's `cacheKey`) */
64
71
  extra?: string
65
72
  /** a DECIDED never-cacheable, with the reason. Not the same thing as an
66
- * undeclared unit: this one has been looked at and cannot be described
73
+ * undeclared check: this one has been looked at and cannot be described
67
74
  * (it reads git history, a path outside the repo, the wall clock), so it
68
75
  * leaves the burn-down list. Declaring it beside inputs or ttl is a
69
76
  * registration failure, because those say the opposite. */
70
77
  never?: string
78
+ /** it rewrites the repo unless the run is frozen, so frozen is part of its
79
+ * question (check.ts `writes`) */
80
+ writes?: boolean
81
+ /** READS: an in process body whose reads the check context records. Set
82
+ * by the composer for a `run` check file declaring no other population,
83
+ * never by a repo, and ignored beside `inputs`, `ttl` or `never` */
84
+ reads?: boolean
85
+ /** why an undeclared `run` check stays uncached: the first module its body
86
+ * can reach that touches the disk or a process around the context */
87
+ uncacheable?: string
71
88
  }
72
89
 
73
- export type UnitKey = {
90
+ export type CheckKey = {
74
91
  id: string
75
- kind: UnitDecl['kind']
76
92
  population: Population
77
93
  /** the content key, or null for world/none/refused */
78
94
  key: string | null
@@ -81,50 +97,52 @@ export type UnitKey = {
81
97
  /** every component that went into the key, in key order — this is what a
82
98
  * human reads when a key moved and nobody knows why */
83
99
  components: string[]
84
- /** the input paths this unit was keyed on, with their object ids */
100
+ /** the input paths this check was keyed on, with their object ids */
85
101
  inputOids: [string, string][]
86
- /** the identity of the unit's OWN code, module graph included. Set for
87
- * every unit that has code, WORLD ones too: a TTL'd probe whose source
102
+ /** the identity of the check's OWN code, module graph included. Set for
103
+ * every check that has code, WORLD ones too: a TTL'd probe whose source
88
104
  * changed must not replay its previous answer, and this is the value that
89
- * busts it. null when the unit has no code entry or git could not hash it. */
105
+ * busts it. null when the check has no code entry or git could not hash it. */
90
106
  codeKey: string | null
91
107
  }
92
108
 
93
109
  export type Registration = {
94
- /** loud, blocking: a unit that declared two contradictory populations */
110
+ /** loud, blocking: a check that declared two contradictory populations */
95
111
  errors: string[]
96
- /** the default-deny burn-down, one line per undeclared unit */
112
+ /** the default-deny burn-down, one line per undeclared check */
97
113
  burnDown: string[]
98
- /** every unit that was classified */
99
- units: number
100
- /** units in a cacheable population (content or world) */
114
+ /** every check that was classified */
115
+ checks: number
116
+ /** checks in a cacheable population (content or world) */
101
117
  declared: number
102
118
  }
103
119
 
104
- /** which population a declaration puts a unit in, before any git work.
105
- * `inputs` on a `ttl` unit is a registration FAIL: a unit cannot both be
120
+ /** which population a declaration puts a check in, before any git work.
121
+ * `inputs` on a `ttl` check is a registration FAIL: a check cannot both be
106
122
  * described by files on disk and be un-describable by them. */
107
- export function classify(d: UnitDecl): { population: Population; error?: string; reason?: string; decided?: boolean } {
123
+ export function classify(d: CheckDecl): { population: Population; error?: string; reason?: string; decided?: boolean } {
108
124
  const hasInputs = (d.inputs ?? []).length > 0
109
125
  const hasTtl = d.ttl != null && d.ttl !== ''
110
126
  const hasNever = d.never != null && d.never !== ''
111
127
  if (hasInputs && hasTtl)
112
128
  return {
113
129
  population: 'none',
114
- error: `${d.kind} '${d.id}' declares BOTH inputs (CONTENT) and ttl (WORLD) — a unit reads files or it reads the world, never both`,
130
+ error: `check '${d.id}' declares BOTH inputs (CONTENT) and ttl (WORLD) — a check reads files or it reads the world, never both`,
115
131
  }
116
132
  if (hasNever && (hasInputs || hasTtl))
117
133
  return {
118
134
  population: 'none',
119
- error: `${d.kind} '${d.id}' declares never:'${d.never}' AND ${hasInputs ? 'inputs' : 'ttl'} — never means nothing can describe this unit, which the other declaration contradicts`,
135
+ error: `check '${d.id}' declares never:'${d.never}' AND ${hasInputs ? 'inputs' : 'ttl'} — never means nothing can describe this check, which the other declaration contradicts`,
120
136
  }
121
137
  if (hasInputs) return { population: 'content' }
122
138
  if (hasTtl) return { population: 'world' }
123
139
  if (hasNever) return { population: 'none', reason: d.never, decided: true }
140
+ if (d.reads === true) return { population: 'reads' }
141
+ if (d.uncacheable != null) return { population: 'none', reason: d.uncacheable }
124
142
  return { population: 'none', reason: 'declares neither inputs nor ttl' }
125
143
  }
126
144
 
127
- export function validateRegistration(decls: UnitDecl[]): Registration {
145
+ export function validateRegistration(decls: CheckDecl[]): Registration {
128
146
  const errors: string[] = []
129
147
  const burnDown: string[] = []
130
148
  let declared = 0
@@ -134,19 +152,19 @@ export function validateRegistration(decls: UnitDecl[]): Registration {
134
152
  // a DECIDED never leaves the list: the burn-down is what nobody has
135
153
  // looked at yet, and mixing the two turns it into wallpaper
136
154
  else if (c.population !== 'none') declared++
137
- else if (c.decided !== true) burnDown.push(`${d.kind} ${d.id} — ${c.reason ?? 'undeclared'}`)
155
+ else if (c.decided !== true) burnDown.push(`${d.id} — ${c.reason ?? 'undeclared'}`)
138
156
  // a declaration that cannot even be turned into a repo-relative path
139
- // is a registration bug, not a cache miss: it would key the unit on a
157
+ // is a registration bug, not a cache miss: it would key the check on a
140
158
  // path in another repo, or on the repo root
141
159
  for (const raw of [...(d.inputs ?? []), ...(d.code ?? [])]) {
142
160
  try {
143
161
  normalizeInput(raw)
144
162
  } catch (e) {
145
- errors.push(`${d.kind} '${d.id}': ${e instanceof Error ? e.message : String(e)}`)
163
+ errors.push(`check '${d.id}': ${e instanceof Error ? e.message : String(e)}`)
146
164
  }
147
165
  }
148
166
  }
149
- return { errors, burnDown, units: decls.length, declared }
167
+ return { errors, burnDown, checks: decls.length, declared }
150
168
  }
151
169
 
152
170
  export type KeyLayerOpts = {
@@ -159,17 +177,17 @@ export type KeyLayerOpts = {
159
177
 
160
178
  /**
161
179
  * one scan, N keys. Build it once per run: the git work is shared across every
162
- * unit, and building two of them in one run would pay for the scan twice and
180
+ * check, and building two of them in one run would pay for the scan twice and
163
181
  * could straddle an edit.
164
182
  */
165
183
  export class KeyLayer {
166
184
  private constructor(
167
185
  readonly root: string,
168
- readonly decls: UnitDecl[],
186
+ readonly decls: CheckDecl[],
169
187
  readonly registration: Registration,
170
188
  private readonly scan: ScanResult | null,
171
189
  private readonly scanError: string | null,
172
- private readonly closureByUnit: Map<string, string[]>,
190
+ private readonly closureByCheck: Map<string, string[]>,
173
191
  private readonly codeOids: Map<string, string>,
174
192
  private readonly toolchain: { id: string; uncacheable: string[] },
175
193
  private readonly scanPathsUsed: string[],
@@ -183,7 +201,7 @@ export class KeyLayer {
183
201
  private readonly installIds = new Map<string, string>()
184
202
 
185
203
  /** the identity of the program this run is driving: bun version, the gate
186
- * binary's whole source, and every installed package. A WORLD unit needs
204
+ * binary's whole source, and every installed package. A WORLD check needs
187
205
  * it too, because a TTL'd answer measured by a different shipkit is a
188
206
  * different answer, and the TTL alone cannot see that. */
189
207
  get toolchainId(): string {
@@ -199,7 +217,7 @@ export class KeyLayer {
199
217
  return this.scanError
200
218
  }
201
219
 
202
- static open(root: string, decls: UnitDecl[], o: KeyLayerOpts): KeyLayer {
220
+ static open(root: string, decls: CheckDecl[], o: KeyLayerOpts): KeyLayer {
203
221
  const started = performance.now()
204
222
  const registration = validateRegistration(decls)
205
223
  const cfg = o.cfg ?? loadConfig(root)
@@ -213,15 +231,15 @@ export class KeyLayer {
213
231
  toolchain = { id: 'unresolved', uncacheable: [`gate binary unreadable at ${o.cliEntry}: ${e instanceof Error ? e.message : String(e)}`] }
214
232
  }
215
233
 
216
- // the unit's OWN code, resolved through the same module graph the
234
+ // the check's OWN code, resolved through the same module graph the
217
235
  // `cycles` task walks. A check that imports a shared helper carries
218
236
  // that helper, so fixing the helper moves the check's key.
219
- const closureByUnit = new Map<string, string[]>()
220
- // computed for EVERY unit with code, not only the content ones: a
221
- // world unit's TTL cache must break when the probe itself is rewritten
237
+ const closureByCheck = new Map<string, string[]>()
238
+ // computed for EVERY check with code, not only the content ones: a
239
+ // world check's TTL cache must break when the probe itself is rewritten
222
240
  for (const d of decls) {
223
241
  const entries = d.code ?? []
224
- if (entries.length > 0) closureByUnit.set(d.id, importClosure(cfg, entries))
242
+ if (entries.length > 0) closureByCheck.set(d.id, importClosure(cfg, entries))
225
243
  }
226
244
 
227
245
  // ONE scan for the declared inputs plus shipkit's own source: those
@@ -237,7 +255,7 @@ export class KeyLayer {
237
255
  // goes through one `hash-object --stdin-paths` instead of one `git
238
256
  // add` spawn per file (measured: 15ms for the whole set)
239
257
  const closureFiles = new Set<string>()
240
- for (const files of closureByUnit.values()) for (const f of files) closureFiles.add(f)
258
+ for (const files of closureByCheck.values()) for (const f of files) closureFiles.add(f)
241
259
 
242
260
  let scan: ScanResult | null = null
243
261
  let codeOids = new Map<string, string>()
@@ -250,28 +268,45 @@ export class KeyLayer {
250
268
  // downstream may key on it. Loud, and the whole run goes uncached.
251
269
  scanError = e instanceof GitScanError ? e.message : String(e)
252
270
  }
253
- return new KeyLayer(root, decls, registration, scan, scanError, closureByUnit, codeOids, toolchain, [...paths].sort(), performance.now() - started)
271
+ return new KeyLayer(root, decls, registration, scan, scanError, closureByCheck, codeOids, toolchain, [...paths].sort(), performance.now() - started)
254
272
  }
255
273
 
256
- /** the key for one unit, or the reason there is none. */
257
- keyFor(id: string): UnitKey {
274
+ /** the key for one check, or the reason there is none. */
275
+ keyFor(id: string): CheckKey {
258
276
  const d = this.decls.find((u) => u.id === id)
259
- if (d == null) throw new Error(`keyFor: no unit '${id}' in this layer`)
277
+ if (d == null) throw new Error(`keyFor: no check '${id}' in this layer`)
260
278
  const c = classify(d)
261
- const own = (this.closureByUnit.get(d.id) ?? []).slice().sort()
279
+ const own = (this.closureByCheck.get(d.id) ?? []).slice().sort()
262
280
  const ownReadable = own.every((p) => this.codeOids.get(p) != null)
263
281
  const codeLines = own.map((p) => `code:${p}=${this.codeOids.get(p)!}`)
264
- const base: UnitKey = {
265
- id: d.id, kind: d.kind, population: c.population, key: null, reason: null, components: [], inputOids: [],
282
+ const base: CheckKey = {
283
+ id: d.id, population: c.population, key: null, reason: null, components: [], inputOids: [],
266
284
  codeKey: own.length === 0 || !ownReadable ? null : createHash('sha256').update(codeLines.join('\n')).digest('hex').slice(0, 16),
267
285
  }
268
286
  if (c.error != null) return { ...base, reason: c.error }
269
- if (c.population === 'world') return { ...base, reason: `world unit — TTL ${d.ttl}, no content key` }
287
+ if (c.population === 'world') return { ...base, reason: `world check — TTL ${d.ttl}, no content key` }
270
288
  if (c.population === 'none') return { ...base, reason: c.reason ?? 'undeclared' }
271
289
  if (this.scanError != null) return { ...base, reason: `git scan failed, nothing may be keyed: ${this.scanError}` }
272
290
  const scan = this.scan!
273
291
  if (this.toolchain.uncacheable.length > 0)
274
292
  return { ...base, reason: `toolchain not describable: ${this.toolchain.uncacheable.join('; ')}` }
293
+ // the question alone: what it read is stored with each answer and
294
+ // re-checked before a replay, so no read belongs in this key
295
+ if (c.population === 'reads') {
296
+ // a built-in has no code entry: the toolchain id hashes its source
297
+ if (!ownReadable) return { ...base, reason: `git could not hash this check's own source: ${own.filter((p) => this.codeOids.get(p) == null).join(', ')}` }
298
+ const components = [
299
+ `v=${KEY_VERSION}`,
300
+ `check=${d.id}`,
301
+ `bun=${Bun.version}`,
302
+ ...(d.writes === true ? [`frozen=${process.env['SHIPKIT_FROZEN'] === '1'}`] : []),
303
+ `toolchain=${this.toolchain.id}`,
304
+ 'reads=recorded',
305
+ ...codeLines,
306
+ ...(d.extra != null ? [`extra=${d.extra}`] : []),
307
+ ]
308
+ return { ...base, key: createHash('sha256').update(components.join('\n')).digest('hex').slice(0, 32), components }
309
+ }
275
310
 
276
311
  let inputs: string[]
277
312
  try {
@@ -285,15 +320,17 @@ export class KeyLayer {
285
320
  const inputOids: [string, string][] = inputs.map((p) => [p, scan.oidByPath.get(p) ?? ''])
286
321
  if (inputOids.some(([, oid]) => oid === '')) return { ...base, reason: 'git could not name one of the declared inputs' }
287
322
 
288
- if (!ownReadable) return { ...base, reason: `git could not hash this unit's own source: ${own.filter((p) => this.codeOids.get(p) == null).join(', ')}` }
323
+ if (!ownReadable) return { ...base, reason: `git could not hash this check's own source: ${own.filter((p) => this.codeOids.get(p) == null).join(', ')}` }
289
324
  const components: string[] = [
290
325
  `v=${KEY_VERSION}`,
291
- `unit=${d.kind}:${d.id}`,
326
+ `check=${d.id}`,
292
327
  `bun=${Bun.version}`,
293
- // report-only mode changes what a self-maintaining rule DOES, and
294
- // a TTL cannot see a mode. A frozen answer replayed into a normal
295
- // run, or the reverse, is an answer to the other question.
296
- `frozen=${process.env['SHIPKIT_FROZEN'] === '1'}`,
328
+ // report-only mode changes what a check that WRITES does, so for
329
+ // that check a frozen answer replayed into a normal run, or the
330
+ // reverse, answers the other question. Every other check does the
331
+ // same thing either way, and keying it on the mode only made a
332
+ // frozen run and a normal one measure it twice.
333
+ ...(d.writes === true ? [`frozen=${process.env['SHIPKIT_FROZEN'] === '1'}`] : []),
297
334
  `toolchain=${this.toolchain.id}`,
298
335
  `argv=${JSON.stringify(d.argv ?? [])}`,
299
336
  ...(d.env ?? []).slice().sort().map((n) => `env:${n}=${process.env[n] ?? '<unset>'}`),
@@ -305,7 +342,7 @@ export class KeyLayer {
305
342
  // questions, and a silently narrowed key is the false green.
306
343
  //
307
344
  // An excluded INSTALL carries its contents rather than only its
308
- // name. `test:libs` declares `wiki/build`, every sub-package's
345
+ // name. A unit's test check declares its dir, and the unit's
309
346
  // `node_modules` sits inside it and is ignored, and the toolchain
310
347
  // component describes the ROOT install only: without this, a lib
311
348
  // whose own dependencies moved replayed a green measured against
@@ -316,7 +353,7 @@ export class KeyLayer {
316
353
  return { ...base, key, components, inputOids }
317
354
  }
318
355
 
319
- /** an excluded install's identity, once per layer: several units can name
356
+ /** an excluded install's identity, once per layer: several checks can name
320
357
  * the same one, and hashing a `node_modules` twice in a run is pure cost. */
321
358
  private installOf(rel: string): string {
322
359
  let id = this.installIds.get(rel)
@@ -324,22 +361,22 @@ export class KeyLayer {
324
361
  return id
325
362
  }
326
363
 
327
- keys(): UnitKey[] {
364
+ keys(): CheckKey[] {
328
365
  return this.decls.map((d) => this.keyFor(d.id))
329
366
  }
330
367
 
331
- /** which units READ one of these paths. Answers the only question a
368
+ /** which checks READ one of these paths. Answers the only question a
332
369
  * post-run re-hash has: whose result now describes a tree that no longer
333
- * exists. A unit declaring the whole work tree reads everything, so any
370
+ * exists. A check declaring the whole work tree reads everything, so any
334
371
  * drift at all touches it. */
335
372
  unitsTouchedBy(drifted: string[]): Set<string> {
336
373
  const hit = new Set<string>()
337
374
  if (drifted.length === 0) return hit
338
375
  for (const d of this.decls) {
339
- // the unit's own source counts exactly like a declared input: an
376
+ // the check's own source counts exactly like a declared input: an
340
377
  // agent fixing a check WHILE it runs must not store the old
341
378
  // verdict under the new code's key
342
- if ((this.closureByUnit.get(d.id) ?? []).some((f) => drifted.includes(f))) { hit.add(d.id); continue }
379
+ if ((this.closureByCheck.get(d.id) ?? []).some((f) => drifted.includes(f))) { hit.add(d.id); continue }
343
380
  for (const raw of d.inputs ?? []) {
344
381
  let p: string
345
382
  try { p = normalizeInput(raw) } catch { hit.add(d.id); break }
@@ -351,24 +388,26 @@ export class KeyLayer {
351
388
 
352
389
  /**
353
390
  * POST RUN RE HASH. Re-scan exactly what this layer keyed on and report
354
- * every path whose object id moved while the units were running.
391
+ * every path whose object id moved while the checks were running.
355
392
  *
356
- * This is the answer to the poisoning class: a unit reads a file, an agent
393
+ * This is the answer to the poisoning class: a check reads a file, an agent
357
394
  * in the next window edits it, and the result gets stored under a key that
358
395
  * describes the file as it was BEFORE the edit. Any drift here means the
359
396
  * run's results describe a tree that no longer exists, and the caller
360
- * discards them rather than storing a lie. Measured at ~140ms, which is
361
- * the price of never having to explain a hit that was wrong.
397
+ * discards them rather than storing a lie. It reuses the first scan's
398
+ * ignored answer, and an unchanged root tree answers every path without
399
+ * asking git per path: about 60ms on this repo when nothing moved, which
400
+ * is the price of never having to explain a hit that was wrong.
362
401
  */
363
402
  rescan(): { drifted: string[]; error: string | null; ms: number } {
364
403
  if (this.scanError != null) return { drifted: [], error: this.scanError, ms: 0 }
365
404
  const started = performance.now()
366
405
  try {
367
- const after = scanPaths(this.root, this.scanPathsUsed)
406
+ const after = scanPaths(this.root, this.scanPathsUsed, this.scan!)
368
407
  const drifted: string[] = []
369
408
  for (const [p, oid] of this.scan!.oidByPath) if (after.oidByPath.get(p) !== oid) drifted.push(p)
370
409
  for (const [p] of after.oidByPath) if (!this.scan!.oidByPath.has(p)) drifted.push(p)
371
- // the unit's own code drifts the same way a declared input does: an
410
+ // the check's own code drifts the same way a declared input does: an
372
411
  // agent fixing a check WHILE that check runs would otherwise store
373
412
  // the old verdict under the new code's key
374
413
  const files = [...this.codeOids.keys()]