@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
@@ -0,0 +1,120 @@
1
+ import uniqBy from 'lodash/uniqBy'
2
+
3
+ import { endedText, type GateCheck, type TestRun } from './gate-schemas'
4
+ import { findCase } from './red-check'
5
+
6
+ import type { CriterionTests, TestRef } from '../agents/role-results'
7
+ import type { CommitPlace } from '../git/git-adapter'
8
+
9
+ /** What the engine's check of `already_done` evidence found (#495). */
10
+ export type AlreadyDoneCheckResult = { ok: boolean; problems: string[] }
11
+
12
+ /** What is wrong with one named test, or `null` if it passes on the base. */
13
+ const testProblem = ({
14
+ test,
15
+ missing,
16
+ test_files,
17
+ changed,
18
+ run,
19
+ }: {
20
+ test: TestRef
21
+ missing: Set<string>
22
+ test_files: Set<string>
23
+ changed: Set<string>
24
+ run: TestRun | null
25
+ }): string | null => {
26
+ const label = `"${test.name}" in ${test.file}`
27
+ if (missing.has(test.file)) return `${label}: the file does not exist`
28
+ if (!test_files.has(test.file)) {
29
+ return `${label}: the file is not a test file`
30
+ }
31
+ if (changed.has(test.file)) {
32
+ return `${label}: the file has changes that are not on the base`
33
+ }
34
+ // No run: the prepare command failed, and says so itself.
35
+ if (run === null) return null
36
+ const found = findCase({ cases: run.cases, test })
37
+ if (found?.status === 'passed') return null
38
+ if (found?.status === 'skipped') return `${label}: is skipped`
39
+ if (found !== undefined) return `${label}: fails on the base`
40
+ return run.files_without_results.includes(test.file)
41
+ ? `${label}: its file did not load, so none of its tests ran`
42
+ : `${label}: not found in the test results`
43
+ }
44
+
45
+ /**
46
+ * The engine's check of a test-writer's `already_done` evidence (#495).
47
+ * Pure: every named commit is on the base (the commit the ticket's
48
+ * worktree started from), every criterion names a test, and every named
49
+ * test is in a test file that exists, is as it is on the base, and passes
50
+ * there.
51
+ *
52
+ * @param commits - Each named commit, and where git found it.
53
+ * @param missing - The named test files that do not exist in the worktree.
54
+ * @param test_files - The worktree's test files, per the engine config.
55
+ * @param changed - The worktree's uncommitted changes.
56
+ * @param prepare - The prepare command's run before the tests, if any.
57
+ * @param run - The run of the named test files; `null` if none ran.
58
+ *
59
+ * @example
60
+ * const result = checkAlreadyDone({ base_sha, criteria_ids: ['AC1'], criteria, commits, missing: [], test_files, changed: [], prepare: null, run })
61
+ * if (!result.ok) console.error(result.problems)
62
+ */
63
+ export const checkAlreadyDone = ({
64
+ base_sha,
65
+ criteria_ids,
66
+ criteria,
67
+ commits,
68
+ missing,
69
+ test_files,
70
+ changed,
71
+ prepare,
72
+ run,
73
+ }: {
74
+ base_sha: string
75
+ criteria_ids: string[]
76
+ criteria: CriterionTests[]
77
+ commits: { sha: string; place: CommitPlace }[]
78
+ missing: string[]
79
+ test_files: string[]
80
+ changed: string[]
81
+ prepare: GateCheck | null
82
+ run: TestRun | null
83
+ }): AlreadyDoneCheckResult => {
84
+ const problems: string[] = []
85
+ for (const { sha, place } of commits) {
86
+ if (place === 'unknown') {
87
+ problems.push(`commit ${sha}: not found in the repo`)
88
+ } else if (place === 'not_on_base') {
89
+ problems.push(
90
+ `commit ${sha}: not on the base (${base_sha}, where this ticket started)`
91
+ )
92
+ }
93
+ }
94
+ for (const id of criteria_ids) {
95
+ const entry = criteria.find(({ criterion_id }) => criterion_id === id)
96
+ if (entry === undefined || entry.tests.length === 0) {
97
+ problems.push(`${id} names no test`)
98
+ }
99
+ }
100
+ if (prepare !== null && !prepare.ok) {
101
+ problems.push(
102
+ `The prepare command \`${prepare.command}\` failed (${endedText(prepare)}), so no test ran:\n${prepare.output}`
103
+ )
104
+ }
105
+ const tests = uniqBy(
106
+ criteria.flatMap(({ tests }) => tests),
107
+ ({ file, name }) => `${file}\0${name}`
108
+ )
109
+ for (const test of tests) {
110
+ const problem = testProblem({
111
+ test,
112
+ missing: new Set(missing),
113
+ test_files: new Set(test_files),
114
+ changed: new Set(changed),
115
+ run,
116
+ })
117
+ if (problem !== null) problems.push(problem)
118
+ }
119
+ return { ok: problems.length === 0, problems }
120
+ }
@@ -0,0 +1,96 @@
1
+ import { readdir } from 'node:fs/promises'
2
+ import { join, normalize, relative } from 'node:path'
3
+
4
+ import { testFilesAmong } from './test-runner'
5
+
6
+ import { isFeatureMapFolder } from '../config/engine-config'
7
+ import { pathText } from '../git/path-text'
8
+
9
+ /**
10
+ * The test files a feature map lists (#583): every backticked path in it that
11
+ * matches the test file patterns, in order, once each. A path written with a
12
+ * leading `./` counts as the path without it, as a worktree-relative path is
13
+ * everywhere else.
14
+ *
15
+ * @param text - The feature map's markdown.
16
+ * @param test_file_patterns - The engine config's test file patterns.
17
+ * @returns The listed test files, in the order the map first names them.
18
+ *
19
+ * @example
20
+ * featureMapFiles({ text: '- `src/sum.test.ts`', test_file_patterns: ['**\/*.test.ts'] }) // ['src/sum.test.ts']
21
+ * featureMapFiles({ text: '- `./src/sum.test.ts`', test_file_patterns: ['**\/*.test.ts'] }) // ['src/sum.test.ts']
22
+ */
23
+ export const featureMapFiles = ({
24
+ text,
25
+ test_file_patterns,
26
+ }: {
27
+ text: string
28
+ test_file_patterns: string[]
29
+ }): string[] => {
30
+ const paths = [...text.matchAll(/`([^`\s]+)`/g)].map(([, path = '']) =>
31
+ path.replace(/^\.\//, '')
32
+ )
33
+ return [...new Set(testFilesAmong({ files: paths, test_file_patterns }))]
34
+ }
35
+
36
+ /**
37
+ * The text of every `*.md` page under a folder, nested ones too, in sorted
38
+ * order of their paths (#597). `null` when the folder is missing or a page
39
+ * can't be read.
40
+ */
41
+ const folderText = async ({
42
+ folder,
43
+ }: {
44
+ folder: string
45
+ }): Promise<string | null> => {
46
+ try {
47
+ const entries = await readdir(folder, {
48
+ recursive: true,
49
+ withFileTypes: true,
50
+ })
51
+ const pages = entries
52
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
53
+ .map((entry) =>
54
+ relative(folder, join(entry.parentPath, entry.name))
55
+ )
56
+ .toSorted()
57
+ const texts = await Promise.all(
58
+ pages.map((page) => Bun.file(join(folder, page)).text())
59
+ )
60
+ return texts.join('\n')
61
+ } catch {
62
+ return null
63
+ }
64
+ }
65
+
66
+ /**
67
+ * The test files a worktree's feature map lists (#583, #597). A path ending
68
+ * in `.md` is one file, read as it always was; any other path is a folder,
69
+ * and every `*.md` page under it, in sorted order, together is the map.
70
+ * (`loadEngineConfig` refuses a map file whose name doesn't end in `.md`.)
71
+ *
72
+ * @param cwd - The worktree.
73
+ * @param path - The map's worktree-relative path, as the config spells it.
74
+ * @param test_file_patterns - The engine config's test file patterns.
75
+ * @returns The listed test files (see `featureMapFiles`), or `null` when the
76
+ * map is missing or can't be read, so the red check can name it rather
77
+ * than crash.
78
+ *
79
+ * @example
80
+ * await readFeatureMap({ cwd, path: 'docs/feature-map', test_file_patterns }) // ['src/sum.test.ts']
81
+ */
82
+ export const readFeatureMap = async ({
83
+ cwd,
84
+ path,
85
+ test_file_patterns,
86
+ }: {
87
+ cwd: string
88
+ path: string
89
+ test_file_patterns: string[]
90
+ }): Promise<string[] | null> => {
91
+ const clean = normalize(path)
92
+ const text = isFeatureMapFolder({ path: clean })
93
+ ? await folderText({ folder: join(cwd, clean) })
94
+ : await pathText({ path: join(cwd, clean) })
95
+ return text === null ? null : featureMapFiles({ text, test_file_patterns })
96
+ }
@@ -1,100 +1,47 @@
1
- import type { GateCheck, GateName } from './gate-schemas'
2
- import { reportFileFor, runTests } from './test-runner'
1
+ import type { GateCheck, GateName, TestCase } from './gate-schemas'
2
+ import { ownFilesCheck } from './own-files-check'
3
+ import { prepareCheck } from './prepare-check'
4
+ import { shellCheck } from './shell-check'
5
+ import { testChecks } from './test-checks'
3
6
 
4
7
  import {
5
- prepareTimeoutOf,
8
+ caseLimitOf,
9
+ slowTestCommands,
6
10
  testCommands,
11
+ testTimeoutOf,
7
12
  type EngineConfig,
8
13
  } from '../config/engine-config'
9
- import {
10
- clipOutput,
11
- DEFAULT_COMMAND_TIMEOUT_MS,
12
- runShell,
13
- } from '../shell/run-command'
14
-
15
- /** A time limit in words: `30 minutes`, `1 minute`, `0.3 seconds`. */
16
- const durationText = (ms: number): string => {
17
- const [amount, unit] =
18
- ms >= 60_000 ? [ms / 60_000, 'minute'] : [ms / 1000, 'second']
19
- const rounded = Math.round(amount * 10) / 10
20
- return `${rounded} ${unit}${rounded === 1 ? '' : 's'}`
21
- }
22
14
 
23
- /**
24
- * Runs one shell command as a named check: `ok` on exit 0, with the clipped
25
- * output only when it failed. One that runs past `timeout_ms` is stopped,
26
- * with everything it started, and its output starts by saying so, then
27
- * `timeout_hint`, if any.
28
- *
29
- * @example
30
- * const install = await shellCheck({ name: 'install', command: 'bun install', cwd })
31
- */
32
- export const shellCheck = async ({
33
- name,
34
- command,
35
- cwd,
36
- timeout_ms,
37
- timeout_hint,
38
- }: {
39
- name: GateName
40
- command: string
41
- cwd: string
42
- /** Defaults to `DEFAULT_COMMAND_TIMEOUT_MS`. */
43
- timeout_ms?: number
44
- /** How to give the command longer, for the timed-out message. */
45
- timeout_hint?: string
46
- }): Promise<GateCheck> => {
47
- const limit = timeout_ms ?? DEFAULT_COMMAND_TIMEOUT_MS
48
- const result = await runShell({ command, cwd, timeout_ms: limit })
49
- const ok = result.exit_code === 0
50
- const timedOut = result.timed_out
51
- ? `Timed out after ${durationText(limit)}, so the engine stopped it and everything it started.${timeout_hint === undefined ? '' : ` ${timeout_hint}`}\n`
52
- : ''
53
- return {
54
- name,
55
- command,
56
- ok,
57
- exit_code: result.exit_code,
58
- output: ok
59
- ? ''
60
- : `${timedOut}${clipOutput({ text: `${result.stdout}\n${result.stderr}` })}`,
61
- }
62
- }
63
-
64
- /**
65
- * Runs the config's prepare command in `cwd`, as the `prepare` check, or
66
- * `null` when the config has none. The engine runs it before every test run
67
- * in a checkout, so tests that need build outputs find them up to date.
68
- * It gets its own time limit (see `prepareTimeoutOf`), since a first build
69
- * can take far longer than other commands.
70
- *
71
- * @example
72
- * const prepare = await prepareCheck({ config, cwd })
73
- * if (prepare !== null && !prepare.ok) console.log(prepare.output)
74
- */
75
- export const prepareCheck = async ({
76
- config,
77
- cwd,
78
- }: {
79
- config: EngineConfig
80
- cwd: string
81
- }): Promise<GateCheck | null> =>
82
- config.prepare === undefined
83
- ? null
84
- : shellCheck({
85
- name: 'prepare',
86
- command: config.prepare,
87
- cwd,
88
- timeout_ms: prepareTimeoutOf({ config }),
89
- timeout_hint:
90
- 'To give it longer, raise `prepare_timeout_ms` in `.luca/config.json`.',
91
- })
15
+ export { prepareCheck } from './prepare-check'
16
+ export { runSlowTests } from './slow-test-runner'
17
+ export { shellCheck } from './shell-check'
18
+ export { FAST_TEST_HINT } from './test-checks'
92
19
 
93
20
  /**
94
21
  * Runs every gate the engine config names, in order: each test command,
95
- * types, lint. All must pass. Unset gates are left out; intake refuses a
22
+ * then (the final review's gates only) each slow test command, types, lint.
23
+ * All must pass. Unset gates are left out; intake refuses a
96
24
  * config with no test command. A `bun` test command runs with bun's JUnit
97
25
  * reporter; a `pass_fail` one runs as written and is judged by its exit code.
26
+ * Each test command gets the config's time limit (see `testTimeoutOf`);
27
+ * types and lint get the engine's default. With `test_concurrency` (#544),
28
+ * each test command (the own-files run too) waits for a test slot first,
29
+ * and its check records the wait as `wait_ms`.
30
+ *
31
+ * With a fast test speed limit (`test_case_timeout_ms`, #580), every `bun`
32
+ * command of `checks.test` gets `--timeout=<limit>`, and the tests in
33
+ * `measured_files` are also held to it by measured time. Slow commands, the
34
+ * own-files run, and `pass_fail` commands never get it.
35
+ *
36
+ * With `own_test_files` (a ticket's gates, with a slow command, #564), the
37
+ * ones the fast runs gave no results for run next on their own with plain
38
+ * `bun test`, as one more `test` check, with the same time limit. A run that
39
+ * gives no results (its files don't load) adds no check after a failed fast
40
+ * run, which already fails the gate.
41
+ *
42
+ * Also returns `cases`, the test cases of the fast `bun` runs and the
43
+ * own-files run, each with its `expect()` count, so the engine can find a
44
+ * mapped test that checks nothing (#581). A `pass_fail` command has none.
98
45
  *
99
46
  * With an `install` command (see `installCommand`), the engine runs it first,
100
47
  * as the `install` check. A failed install stops there: the other gates
@@ -103,8 +50,11 @@ export const prepareCheck = async ({
103
50
  * `around_prepare`, if given, runs the prepare check (so the engine can see
104
51
  * what it made).
105
52
  *
53
+ * With `exclusive` (a lone rerun, #544), each test command waits until no
54
+ * other test run is going, and holds every test slot while it runs.
55
+ *
106
56
  * @example
107
- * const { ok, checks } = await runGates({ cwd, config, test_files, report_file, install: null })
57
+ * const { ok, checks, cases } = await runGates({ cwd, config, test_files, report_file, install: null })
108
58
  */
109
59
  export const runGates = async ({
110
60
  cwd,
@@ -113,6 +63,10 @@ export const runGates = async ({
113
63
  report_file,
114
64
  install,
115
65
  around_prepare,
66
+ with_slow_tests,
67
+ own_test_files,
68
+ measured_files,
69
+ exclusive = false,
116
70
  }: {
117
71
  cwd: string
118
72
  config: EngineConfig
@@ -123,7 +77,24 @@ export const runGates = async ({
123
77
  around_prepare?: (
124
78
  prepare: () => Promise<GateCheck | null>
125
79
  ) => Promise<GateCheck | null>
126
- }): Promise<{ ok: boolean; checks: GateCheck[] }> => {
80
+ /**
81
+ * The final review's gates also run the config's slow tests (#563), after
82
+ * the test commands, as `slow_test` checks. Left out, they don't.
83
+ */
84
+ with_slow_tests?: boolean
85
+ /**
86
+ * A ticket's changed test files (#564): those no fast run gave results
87
+ * for are run on their own, as one more `test` check. Left out, none.
88
+ */
89
+ own_test_files?: string[]
90
+ /**
91
+ * A ticket's changed test files: their tests are held to
92
+ * `test_case_timeout_ms` by measured time (#580); left out, none.
93
+ */
94
+ measured_files?: string[]
95
+ /** Run each test command alone (#544). Left out, they don't. */
96
+ exclusive?: boolean
97
+ }): Promise<{ ok: boolean; checks: GateCheck[]; cases: TestCase[] }> => {
127
98
  const checks: GateCheck[] = []
128
99
  if (install !== null) {
129
100
  const installed = await shellCheck({
@@ -132,35 +103,64 @@ export const runGates = async ({
132
103
  cwd,
133
104
  })
134
105
  checks.push(installed)
135
- if (!installed.ok) return { ok: false, checks }
106
+ if (!installed.ok) return { ok: false, checks, cases: [] }
136
107
  }
137
108
  const prepare = () => prepareCheck({ config, cwd })
138
109
  const prepared = await (around_prepare?.(prepare) ?? prepare())
139
110
  if (prepared !== null) {
140
111
  checks.push(prepared)
141
- if (!prepared.ok) return { ok: false, checks }
112
+ if (!prepared.ok) return { ok: false, checks, cases: [] }
142
113
  }
143
114
  const { types, lint } = config.checks
144
- for (const [index, { run: command, results }] of testCommands({
145
- config,
146
- }).entries()) {
147
- if (results === 'pass_fail') {
148
- checks.push(await shellCheck({ name: 'test', command, cwd }))
149
- continue
150
- }
151
- const run = await runTests({
115
+ const timeout_ms = testTimeoutOf({ config })
116
+ const { test_concurrency } = config
117
+ const limit_ms = caseLimitOf({ config })
118
+ const fast = testCommands({ config })
119
+ const fastChecks = await testChecks({
120
+ commands: fast,
121
+ name: 'test',
122
+ cwd,
123
+ test_files,
124
+ report_file,
125
+ first_index: 0,
126
+ timeout_ms,
127
+ limit:
128
+ limit_ms === null
129
+ ? null
130
+ : { limit_ms, measured_files: measured_files ?? [] },
131
+ test_concurrency,
132
+ exclusive,
133
+ })
134
+ checks.push(...fastChecks.checks)
135
+ // The test cases the gates saw, for the empty-test check (#581).
136
+ const cases = fastChecks.runs.flatMap((run) => run.cases)
137
+ const own = await ownFilesCheck({
138
+ own_test_files: own_test_files ?? [],
139
+ fast_checks: fastChecks.checks,
140
+ fast_runs: fastChecks.runs,
141
+ cwd,
142
+ report_file,
143
+ report_index: fast.length + slowTestCommands({ config }).length,
144
+ timeout_ms,
145
+ test_concurrency,
146
+ exclusive,
147
+ })
148
+ cases.push(...own.cases)
149
+ if (own.check !== null) checks.push(own.check)
150
+ if (with_slow_tests === true) {
151
+ const slow = await testChecks({
152
+ commands: slowTestCommands({ config }),
153
+ name: 'slow_test',
152
154
  cwd,
153
- command,
154
155
  test_files,
155
- report_file: reportFileFor({ report_file, index }),
156
- })
157
- checks.push({
158
- name: 'test',
159
- command,
160
- ok: run.ok,
161
- exit_code: run.exit_code,
162
- output: run.ok ? '' : run.output,
156
+ report_file,
157
+ first_index: fast.length,
158
+ timeout_ms,
159
+ limit: null,
160
+ test_concurrency,
161
+ exclusive,
163
162
  })
163
+ checks.push(...slow.checks)
164
164
  }
165
165
  const others: { name: GateName; command: string | undefined }[] = [
166
166
  { name: 'types', command: types },
@@ -170,5 +170,5 @@ export const runGates = async ({
170
170
  if (command === undefined) continue
171
171
  checks.push(await shellCheck({ name, command, cwd }))
172
172
  }
173
- return { ok: checks.every(({ ok }) => ok), checks }
173
+ return { ok: checks.every(({ ok }) => ok), checks, cases }
174
174
  }
@@ -6,10 +6,22 @@ export const TestCaseSchema = z.object({
6
6
  /** Describe names and the test name joined by " > ". */
7
7
  full_name: z.string(),
8
8
  status: z.enum(['passed', 'failed', 'skipped']),
9
+ /** How long it ran, from bun's JUnit `time` (#518). Missing in old journals. */
10
+ duration_ms: z.number().int().nonnegative().optional(),
11
+ /** How many `expect()` calls it made, from bun's JUnit `assertions` (#581). */
12
+ assertions: z.number().int().nonnegative().optional(),
9
13
  })
10
14
 
11
15
  export type TestCase = z.infer<typeof TestCaseSchema>
12
16
 
17
+ /** A test case by its file and full name, without its outcome. */
18
+ export const TestCaseRefSchema = TestCaseSchema.pick({
19
+ file: true,
20
+ full_name: true,
21
+ })
22
+
23
+ export type TestCaseRef = z.infer<typeof TestCaseRefSchema>
24
+
13
25
  /** One run of the engine config's test command, with each case's outcome. */
14
26
  export const TestRunSchema = z.object({
15
27
  command: z.string(),
@@ -22,21 +34,31 @@ export const TestRunSchema = z.object({
22
34
  files_without_results: z.array(z.string()),
23
35
  /** The command's output, clipped. */
24
36
  output: z.string(),
37
+ /**
38
+ * How long it waited for a test slot (#544), only with
39
+ * `test_concurrency` set.
40
+ */
41
+ wait_ms: z.number().int().nonnegative().optional(),
25
42
  })
26
43
 
27
44
  export type TestRun = z.infer<typeof TestRunSchema>
28
45
 
29
46
  /**
30
47
  * The gates the engine config can name, plus `install`: the engine's own
31
- * install, run first when a package manifest changed, and `prepare`: the
32
- * config's prepare command, run before the tests.
48
+ * install, run first when a package manifest changed, `prepare`: the
49
+ * config's prepare command, run before the tests, and `slow_test`: the
50
+ * config's slow tests, run only at the final review (#563). `generate` is
51
+ * one of the config's generators, run after a writer's turn when it asked
52
+ * (#618); it is journaled in `chores_done`, never in the gates.
33
53
  */
34
54
  export const GateNameSchema = z.enum([
35
55
  'install',
36
56
  'prepare',
37
57
  'test',
58
+ 'slow_test',
38
59
  'types',
39
60
  'lint',
61
+ 'generate',
40
62
  ])
41
63
 
42
64
  export type GateName = z.infer<typeof GateNameSchema>
@@ -48,6 +70,14 @@ export const GateCheckSchema = z.object({
48
70
  ok: z.boolean(),
49
71
  exit_code: z.number().int().nullable(),
50
72
  output: z.string(),
73
+ /** How long the command ran (#518). Missing in old journals. */
74
+ duration_ms: z.number().int().nonnegative().optional(),
75
+ /**
76
+ * How long it waited for its slot: a prepare slot for `prepare` (#518),
77
+ * a test slot for `test` and `slow_test` with `test_concurrency` set
78
+ * (#544).
79
+ */
80
+ wait_ms: z.number().int().nonnegative().optional(),
51
81
  })
52
82
 
53
83
  export type GateCheck = z.infer<typeof GateCheckSchema>
@@ -82,3 +112,32 @@ export const LeftoverHitSchema = z.object({
82
112
  })
83
113
 
84
114
  export type LeftoverHit = z.infer<typeof LeftoverHitSchema>
115
+
116
+ /**
117
+ * Why the leftover scan let a new markdown file through (#520): it sits in a
118
+ * doc place (a `docs` or `adr` folder, or a `README.md`), a tracked or newly
119
+ * added non-test file names it, or a `leftover_allow` pattern matches it.
120
+ */
121
+ export const LeftoverPassReasonSchema = z.enum([
122
+ 'doc_place',
123
+ 'named_by_repo',
124
+ 'config_pattern',
125
+ ])
126
+
127
+ export type LeftoverPassReason = z.infer<typeof LeftoverPassReasonSchema>
128
+
129
+ /**
130
+ * A new markdown file the leftover scan let through, and why, as the journal
131
+ * keeps it. The reason is a string there, so a later reason still reads.
132
+ */
133
+ export const LeftoverPassSchema = z.object({
134
+ path: z.string(),
135
+ reason: z.string(),
136
+ })
137
+
138
+ /** A new markdown file the leftover scan let through, as the scan returns it. */
139
+ export const LeftoverPassResultSchema = LeftoverPassSchema.extend({
140
+ reason: LeftoverPassReasonSchema,
141
+ })
142
+
143
+ export type LeftoverPass = z.infer<typeof LeftoverPassResultSchema>