@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

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 (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -1,6 +1,10 @@
1
1
  import { basename, extname } from 'node:path'
2
2
 
3
- import type { LeftoverHit } from './gate-schemas'
3
+ import type {
4
+ LeftoverHit,
5
+ LeftoverPass,
6
+ LeftoverPassReason,
7
+ } from './gate-schemas'
4
8
 
5
9
  import type { FileChange } from '../git/git-adapter'
6
10
 
@@ -21,6 +25,35 @@ const CODE_EXTENSIONS = new Set([
21
25
  const TOOL_LEFTOVER = /\.(log|orig|rej|bak|swp|swo|tmp|temp)$/i
22
26
  const SCRATCH_NAME =
23
27
  /^(tmp|temp|scratch|scratchpad|debug|notes?|todo)([-_.]|$)/i
28
+ const SCRATCH_REASON = 'a scratch file'
29
+ const MARKDOWN_REASON = 'a new markdown file the spec and ticket do not name'
30
+
31
+ /**
32
+ * Files a tool or framework loads by name, so nothing imports them: tool
33
+ * configs (`vite.config.ts`, `eslint.config.mjs`), framework entry, root,
34
+ * routes, and middleware files, and anything in a file-based router's
35
+ * folder.
36
+ */
37
+ const LOADED_BY_NAME = [
38
+ /\.config\.(ts|js|mjs|cjs|mts|cts)$/i,
39
+ /^entry\.(server|client)\./i,
40
+ /^(root|routes|middleware)\./i,
41
+ ]
42
+ const ROUTER_FOLDER = /(^|\/)(app\/routes|pages|src\/app|src\/pages)\//
43
+
44
+ /**
45
+ * A changeset an agent wrote: a new markdown file in `.changeset/` other than
46
+ * its README. The engine writes the run's one changeset itself.
47
+ */
48
+ export const isAgentChangeset = ({ path, change }: FileChange): boolean =>
49
+ change === 'added' &&
50
+ /^\.changeset\/[^/]+\.md$/.test(path) &&
51
+ basename(path) !== 'README.md'
52
+
53
+ /** Whether a tool or framework loads this file by its name or folder. */
54
+ const loadedByName = (path: string): boolean =>
55
+ LOADED_BY_NAME.some((pattern) => pattern.test(basename(path))) ||
56
+ ROUTER_FOLDER.test(path)
24
57
 
25
58
  /** A new code file's name without its extension, as imports name it. */
26
59
  export const importStem = (path: string): string =>
@@ -40,6 +73,42 @@ export const newCodeFiles = ({
40
73
  .filter((path) => CODE_EXTENSIONS.has(extname(path).toLowerCase()))
41
74
  .filter((path) => !test_files.includes(path))
42
75
  .filter((path) => importStem(path) !== 'index')
76
+ .filter((path) => !loadedByName(path))
77
+
78
+ const isNewMarkdown = ({ path, change }: FileChange): boolean =>
79
+ change === 'added' && extname(path).toLowerCase() === '.md'
80
+
81
+ /** Folders whose markdown is docs the repo asks for, at any depth (#520). */
82
+ const DOC_FOLDERS = new Set(['docs', 'adr'])
83
+
84
+ /** Whether a markdown file is a `README.md` or sits in a doc folder. */
85
+ const inDocPlace = (path: string): boolean =>
86
+ basename(path) === 'README.md' ||
87
+ path
88
+ .split('/')
89
+ .slice(0, -1)
90
+ .some((folder) => DOC_FOLDERS.has(folder))
91
+
92
+ /**
93
+ * Files the "named by the repo" rule can clear: scratch-named files, and
94
+ * new markdown outside a doc place (#520). Each needs a "does a tracked or
95
+ * newly added non-test file name it?" answer. (A new code file needs none:
96
+ * its basename holds its import stem, so a file that names it already
97
+ * makes it used.)
98
+ */
99
+ export const nameCheckedFiles = ({
100
+ changes,
101
+ }: {
102
+ changes: FileChange[]
103
+ }): string[] =>
104
+ changes
105
+ .filter(
106
+ (file) =>
107
+ (file.change !== 'deleted' &&
108
+ SCRATCH_NAME.test(basename(file.path))) ||
109
+ (isNewMarkdown(file) && !inDocPlace(file.path))
110
+ )
111
+ .map(({ path }) => path)
43
112
 
44
113
  const nameHit = (path: string): string | null => {
45
114
  // git lists a folder with its own repo as one path ending in a slash.
@@ -51,13 +120,71 @@ const nameHit = (path: string): string | null => {
51
120
  if (TOOL_LEFTOVER.test(name) || name.endsWith('~')) {
52
121
  return 'a log, backup, or merge leftover'
53
122
  }
54
- if (SCRATCH_NAME.test(name)) return 'a scratch file'
123
+ if (SCRATCH_NAME.test(name)) return SCRATCH_REASON
55
124
  if (/^junit.*\.xml$/i.test(name) || path.startsWith('coverage/')) {
56
125
  return 'test tool output'
57
126
  }
58
127
  return null
59
128
  }
60
129
 
130
+ /** The leftover scan's inputs; see `scanLeftovers`. */
131
+ type ScanInputs = {
132
+ changes: FileChange[]
133
+ test_files: string[]
134
+ mention_text: string
135
+ used_code: Record<string, boolean>
136
+ named?: string[]
137
+ leftover_allow?: string[]
138
+ }
139
+
140
+ /** Why a new markdown file the spec and ticket don't name may stay, if it may. */
141
+ const markdownPass = ({
142
+ path,
143
+ named,
144
+ leftover_allow,
145
+ }: {
146
+ path: string
147
+ named: string[]
148
+ leftover_allow: string[]
149
+ }): LeftoverPassReason | null => {
150
+ if (inDocPlace(path)) return 'doc_place'
151
+ if (named.includes(path)) return 'named_by_repo'
152
+ if (leftover_allow.some((pattern) => new Bun.Glob(pattern).match(path))) {
153
+ return 'config_pattern'
154
+ }
155
+ return null
156
+ }
157
+
158
+ /** The hits and passes among the changed files, before new code's use. */
159
+ const fileVerdicts = ({
160
+ changes,
161
+ mention_text,
162
+ named = [],
163
+ leftover_allow = [],
164
+ }: ScanInputs): { hits: LeftoverHit[]; passes: LeftoverPass[] } => {
165
+ const hits: LeftoverHit[] = []
166
+ const passes: LeftoverPass[] = []
167
+ for (const file of changes) {
168
+ const { path } = file
169
+ if (file.change === 'deleted') continue
170
+ const hit = nameHit(path)
171
+ // A module the repo names, such as a TOC's `debug.lua`, isn't scratch.
172
+ const reason =
173
+ hit === SCRATCH_REASON && named.includes(path) ? null : hit
174
+ if (reason !== null) hits.push({ path, reason })
175
+ else if (
176
+ isNewMarkdown(file) &&
177
+ !mention_text.includes(basename(path))
178
+ ) {
179
+ const pass = markdownPass({ path, named, leftover_allow })
180
+ if (pass === null) {
181
+ hits.push({ path, reason: MARKDOWN_REASON })
182
+ } else passes.push({ path, reason: pass })
183
+ }
184
+ }
185
+ return { hits, passes }
186
+ }
187
+
61
188
  /**
62
189
  * The leftover scan. Pure: given a worktree's changes, it lists the files the
63
190
  * engine must not commit. It runs before every engine commit.
@@ -65,44 +192,28 @@ const nameHit = (path: string): string | null => {
65
192
  * Blocks scratch files, logs, `.orig` and other tool leftovers, `.DS_Store`,
66
193
  * new markdown the spec and ticket don't name, new scripts or modules
67
194
  * that nothing uses, and a folder with its own git repo in it (which
68
- * `git add` can't take as files).
195
+ * `git add` can't take as files). A file a tool or framework loads by name
196
+ * (`*.config.*`, `entry.server.*`, a route file) is never "unused", and a
197
+ * scratch-named file the repo names is not scratch. New markdown in a doc
198
+ * place, named by the repo, or matching `leftover_allow` passes (#520; see
199
+ * `leftoverPasses`).
69
200
  *
70
201
  * @param mention_text - The spec's and ticket's text; a new markdown file is
71
202
  * fine if it names the file.
72
203
  * @param used_code - For each of `newCodeFiles(...)`, whether another file
73
204
  * mentions it.
205
+ * @param named - Those of `nameCheckedFiles(...)` whose basename a tracked
206
+ * or newly added non-test file names (a TOC, `package.json`, a config).
207
+ * @param leftover_allow - The config's glob patterns for more new markdown
208
+ * to let through.
74
209
  *
75
210
  * @example
76
- * const hits = scanLeftovers({ changes, test_files, mention_text, used_code })
211
+ * const hits = scanLeftovers({ changes, test_files, mention_text, used_code, named })
77
212
  * if (hits.length > 0) console.error(hits)
78
213
  */
79
- export const scanLeftovers = ({
80
- changes,
81
- test_files,
82
- mention_text,
83
- used_code,
84
- }: {
85
- changes: FileChange[]
86
- test_files: string[]
87
- mention_text: string
88
- used_code: Record<string, boolean>
89
- }): LeftoverHit[] => {
90
- const hits: LeftoverHit[] = []
91
- for (const { path, change } of changes) {
92
- if (change === 'deleted') continue
93
- const reason = nameHit(path)
94
- if (reason !== null) hits.push({ path, reason })
95
- else if (
96
- change === 'added' &&
97
- extname(path).toLowerCase() === '.md' &&
98
- !mention_text.includes(basename(path))
99
- ) {
100
- hits.push({
101
- path,
102
- reason: 'a new markdown file the spec and ticket do not name',
103
- })
104
- }
105
- }
214
+ export const scanLeftovers = (inputs: ScanInputs): LeftoverHit[] => {
215
+ const { changes, test_files, used_code } = inputs
216
+ const { hits } = fileVerdicts(inputs)
106
217
  for (const path of newCodeFiles({ changes, test_files })) {
107
218
  if (used_code[path] === false) {
108
219
  hits.push({
@@ -113,3 +224,15 @@ export const scanLeftovers = ({
113
224
  }
114
225
  return hits
115
226
  }
227
+
228
+ /**
229
+ * The new markdown files `scanLeftovers` let through on the same inputs,
230
+ * each with why (#520), for the journal. Markdown the spec or ticket names
231
+ * isn't listed: it always passed.
232
+ *
233
+ * @example
234
+ * leftoverPasses({ ...inputs, changes: [{ path: 'docs/adr/0001.md', change: 'added' }] })
235
+ * // [{ path: 'docs/adr/0001.md', reason: 'doc_place' }]
236
+ */
237
+ export const leftoverPasses = (inputs: ScanInputs): LeftoverPass[] =>
238
+ fileVerdicts(inputs).passes
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Test names in a test file's text (#581): the plain string names of `test`
3
+ * and `it` calls (with `.only`, `.skip`, `.todo`, `.failing`), in any quote.
4
+ * A template name with `${}` and a `test.each` table are left out: their
5
+ * names aren't known until they run. The test-writer must use plain names.
6
+ * A call on something else, like `pattern.test('abc')`, is not a test. This
7
+ * is a text scan, not a parse, so a `test('...')` inside a string or comment
8
+ * of a changed file counts as a test name. The red check trusts the scan
9
+ * alone only for a file that doesn't load yet; for a file that ran, it keeps
10
+ * just the names of cases that ran.
11
+ *
12
+ * @example
13
+ * testNamesIn({ source: "test('adds', () => {})" }) // ['adds']
14
+ */
15
+ export const testNamesIn = ({ source }: { source: string }): string[] => {
16
+ const names: string[] = []
17
+ const call =
18
+ /(?<![\w$.])(?:test|it)(?:\.(?:only|skip|todo|failing|concurrent|serial))?\(\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1/g
19
+ for (const match of source.matchAll(call)) {
20
+ const [, quote, raw = ''] = match
21
+ if (quote === '`' && raw.includes('${')) continue
22
+ names.push(raw.replace(/\\(.)/g, '$1'))
23
+ }
24
+ return names
25
+ }
26
+
27
+ /**
28
+ * The test names a change added to one test file (#581): names in `current`
29
+ * that `base` doesn't have. A new file (`base` null) adds all of its names;
30
+ * a deleted one (`current` null) adds none.
31
+ *
32
+ * @example
33
+ * addedTestNames({ base: "test('a', () => {})", current: "test('a', () => {}); test('b', () => {})" }) // ['b']
34
+ */
35
+ export const addedTestNames = ({
36
+ base,
37
+ current,
38
+ }: {
39
+ base: string | null
40
+ current: string | null
41
+ }): string[] => {
42
+ if (current === null) return []
43
+ const before = new Set(base === null ? [] : testNamesIn({ source: base }))
44
+ return testNamesIn({ source: current }).filter((name) => !before.has(name))
45
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The ticket's own test files (#564): the changed test files no fast `bun`
3
+ * run gave results for, run on their own with plain `bun test` as one more
4
+ * `test` check, after the fast runs.
5
+ */
6
+ import type { GateCheck, TestCase, TestRun } from './gate-schemas'
7
+ import { msSince } from './shell-check'
8
+ import {
9
+ fileTestsCommand,
10
+ reportFileFor,
11
+ runTestsInHeldSlot,
12
+ } from './test-runner'
13
+ import { inTestSlot } from './test-slots'
14
+
15
+ /**
16
+ * Runs the `own_test_files` that every fast run left without results (a file
17
+ * with no fast `bun` run at all has no run to have left it out, so it is not
18
+ * one) with plain `bun test`, in a test slot (#544), with the test commands'
19
+ * time limit.
20
+ *
21
+ * Returns the run's test cases, and its check: `null` when there was nothing
22
+ * to run, or when the run gave no results at all (its files don't load) after
23
+ * a failed fast run. That is no news (the gate fails anyway, and the slow
24
+ * file's load error shows on the next run, once that is fixed); with every
25
+ * fast run passing, it is a real failure of its own.
26
+ *
27
+ * @example
28
+ * const { check, cases } = await ownFilesCheck({ own_test_files, fast_checks, fast_runs, cwd, report_file, report_index, timeout_ms, test_concurrency, exclusive })
29
+ */
30
+ export const ownFilesCheck = async ({
31
+ own_test_files,
32
+ fast_checks,
33
+ fast_runs,
34
+ cwd,
35
+ report_file,
36
+ report_index,
37
+ timeout_ms,
38
+ test_concurrency,
39
+ exclusive,
40
+ }: {
41
+ /** The ticket's changed test files. */
42
+ own_test_files: string[]
43
+ /** The fast test checks, already run. */
44
+ fast_checks: GateCheck[]
45
+ /** The fast `bun` runs, already done. */
46
+ fast_runs: TestRun[]
47
+ cwd: string
48
+ report_file: string
49
+ /** The index for this run's own report file. */
50
+ report_index: number
51
+ timeout_ms: number
52
+ /** The config's `test_concurrency`; left out, no cap. */
53
+ test_concurrency: number | undefined
54
+ exclusive: boolean
55
+ }): Promise<{ check: GateCheck | null; cases: TestCase[] }> => {
56
+ // A file with no fast bun run at all has no run to have given it results,
57
+ // so it is not one the fast run left out.
58
+ const unrun = own_test_files.filter(
59
+ (file) =>
60
+ fast_runs.length > 0 &&
61
+ fast_runs.every(({ files_without_results }) =>
62
+ files_without_results.includes(file)
63
+ )
64
+ )
65
+ if (unrun.length === 0) return { check: null, cases: [] }
66
+ const { run, ...check } = await inTestSlot({
67
+ test_concurrency,
68
+ exclusive,
69
+ work: async () => {
70
+ const started = performance.now()
71
+ const run = await runTestsInHeldSlot({
72
+ cwd,
73
+ command: fileTestsCommand({ files: unrun }),
74
+ test_files: unrun,
75
+ report_file: reportFileFor({
76
+ report_file,
77
+ index: report_index,
78
+ }),
79
+ timeout_ms,
80
+ })
81
+ const check: GateCheck = {
82
+ name: 'test',
83
+ command: run.command,
84
+ ok: run.ok,
85
+ exit_code: run.exit_code,
86
+ output: run.ok ? '' : run.output,
87
+ duration_ms: msSince(started),
88
+ }
89
+ return { ...check, run }
90
+ },
91
+ })
92
+ // A run with no results at all means its files don't load. After a
93
+ // failed fast run that is no news (the gate fails anyway, and the slow
94
+ // file's load error shows on the next run, once that is fixed);
95
+ // with every fast run passing, it is a real failure of its own.
96
+ const counts = run.cases.length > 0 || fast_checks.every(({ ok }) => ok)
97
+ return { check: counts ? check : null, cases: run.cases }
98
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The prepare check: the config's prepare command, run before every test run
3
+ * in a checkout, one at a time per the config's `prepare_concurrency`.
4
+ */
5
+ import type { GateCheck } from './gate-schemas'
6
+ import { PREPARE_SLOTS } from './prepare-slots'
7
+ import { msSince, shellCheck } from './shell-check'
8
+ import { onSlotWait } from './slot-waits'
9
+
10
+ import {
11
+ prepareConcurrencyOf,
12
+ prepareTimeoutOf,
13
+ type EngineConfig,
14
+ } from '../config/engine-config'
15
+
16
+ /**
17
+ * Runs the config's prepare command in `cwd`, as the `prepare` check, or
18
+ * `null` when the config has none. The engine runs it before every test run
19
+ * in a checkout, so tests that need build outputs find them up to date.
20
+ * It gets its own time limit (see `prepareTimeoutOf`), since a first build
21
+ * can take far longer than other commands.
22
+ *
23
+ * At most `prepareConcurrencyOf` prepare runs go at once in the engine (see
24
+ * `PREPARE_SLOTS`); the rest wait their turn. The time limit starts when a
25
+ * run starts, so waiting for a turn never counts toward it. The check says
26
+ * how long it waited for its turn, as `wait_ms` (#518), and the step's
27
+ * slot-wait listener hears the wait while it lasts (see `slot-waits.ts`).
28
+ *
29
+ * @example
30
+ * const prepare = await prepareCheck({ config, cwd })
31
+ * if (prepare !== null && !prepare.ok) console.log(prepare.output)
32
+ */
33
+ export const prepareCheck = async ({
34
+ config,
35
+ cwd,
36
+ }: {
37
+ config: EngineConfig
38
+ cwd: string
39
+ }): Promise<GateCheck | null> => {
40
+ const { prepare } = config
41
+ if (prepare === undefined) return null
42
+ const asked = performance.now()
43
+ const limit = prepareConcurrencyOf({ config })
44
+ return PREPARE_SLOTS.run({
45
+ limit,
46
+ on_wait: onSlotWait({ kind: 'prepare_slot', limit }),
47
+ work: async () => {
48
+ const wait_ms = msSince(asked)
49
+ const check = await shellCheck({
50
+ name: 'prepare',
51
+ command: prepare,
52
+ cwd,
53
+ timeout_ms: prepareTimeoutOf({ config }),
54
+ timeout_hint:
55
+ 'To give it longer, raise `prepare_timeout_ms` in `.luca/config.json`.',
56
+ })
57
+ return { ...check, wait_ms }
58
+ },
59
+ })
60
+ }
@@ -0,0 +1,99 @@
1
+ /** Runs work once a slot is free, and frees the slot when it settles. */
2
+ export type Slots = {
3
+ /**
4
+ * Waits for a slot (at most `limit` pieces of work at once), then runs
5
+ * `work`. The slot is freed when `work` settles, even when it throws.
6
+ * An `exclusive` take waits until no work holds a slot, then holds them
7
+ * all: nothing else starts until it settles. A take that must wait
8
+ * calls `on_wait` with the slots in use, and what that gives back once
9
+ * the slot is taken.
10
+ */
11
+ run: <T>(args: {
12
+ limit: number
13
+ work: () => Promise<T>
14
+ exclusive?: boolean
15
+ on_wait?: (in_use: number) => () => void
16
+ }) => Promise<T>
17
+ /** How many pieces of work hold a slot now. */
18
+ busy: () => number
19
+ }
20
+
21
+ /**
22
+ * A counting semaphore: at most `limit` pieces of work at once, the rest
23
+ * wait their turn in call order. Each call names its own `limit`, so a
24
+ * later call can raise or lower it; a lower limit lets nothing new start
25
+ * until enough work has finished. An `exclusive` call (#544) waits for
26
+ * every running piece to end, and the work queued behind it waits for it.
27
+ *
28
+ * @example
29
+ * const slots = createSlots()
30
+ * await Promise.all([
31
+ * slots.run({ limit: 1, work: () => build('a') }),
32
+ * slots.run({ limit: 1, work: () => build('b') }), // starts once 'a' ends
33
+ * ])
34
+ */
35
+ export const createSlots = (): Slots => {
36
+ let taken = 0
37
+ let alone = false
38
+ const waiting: { limit: number; exclusive: boolean; start: () => void }[] =
39
+ []
40
+
41
+ const startWaiting = () => {
42
+ while (waiting.length > 0 && !alone) {
43
+ const [next] = waiting
44
+ if (next === undefined) return
45
+ if (next.exclusive ? taken > 0 : taken >= next.limit) return
46
+ waiting.shift()
47
+ taken += 1
48
+ alone = next.exclusive
49
+ next.start()
50
+ }
51
+ }
52
+
53
+ const take = ({
54
+ limit,
55
+ exclusive,
56
+ on_wait,
57
+ }: {
58
+ limit: number
59
+ exclusive: boolean
60
+ on_wait: ((in_use: number) => () => void) | undefined
61
+ }): Promise<void> => {
62
+ let started = false
63
+ const taken_now = new Promise<void>((start) => {
64
+ waiting.push({
65
+ limit,
66
+ exclusive,
67
+ start: () => {
68
+ started = true
69
+ start()
70
+ },
71
+ })
72
+ startWaiting()
73
+ })
74
+ if (started || on_wait === undefined) return taken_now
75
+ const slot_taken = on_wait(taken)
76
+ return taken_now.then(() => slot_taken())
77
+ }
78
+
79
+ return {
80
+ run: async ({ limit, work, exclusive = false, on_wait }) => {
81
+ await take({ limit, exclusive, on_wait })
82
+ try {
83
+ return await work()
84
+ } finally {
85
+ taken -= 1
86
+ if (exclusive) alone = false
87
+ startWaiting()
88
+ }
89
+ },
90
+ busy: () => taken,
91
+ }
92
+ }
93
+
94
+ /**
95
+ * The engine's prepare slots: one set for the whole engine process, so the
96
+ * cap spans every ticket that builds at the same time. A `luca` process runs
97
+ * one run, so in practice the cap is the run's.
98
+ */
99
+ export const PREPARE_SLOTS: Slots = createSlots()