@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.
- package/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- 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 {
|
|
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
|
|
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
|
-
|
|
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()
|