@nathapp/nax 0.18.1 → 0.18.2
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/.gitlab-ci.yml +2 -2
- package/docs/ROADMAP.md +21 -23
- package/nax/config.json +12 -9
- package/nax/features/smart-test-runner/plan.md +7 -0
- package/nax/features/smart-test-runner/prd.json +203 -0
- package/nax/features/smart-test-runner/progress.txt +13 -0
- package/nax/features/smart-test-runner/spec.md +7 -0
- package/nax/features/smart-test-runner/tasks.md +8 -0
- package/package.json +1 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schemas.ts +1 -0
- package/src/config/types.ts +2 -0
- package/src/pipeline/stages/routing.ts +4 -4
- package/src/pipeline/stages/verify.ts +20 -1
- package/src/precheck/index.ts +9 -4
- package/src/verification/smart-runner.ts +146 -0
- package/test/US-002-orchestrator.test.ts +5 -5
- package/test/unit/config/smart-runner-flag.test.ts +225 -0
- package/test/unit/pipeline/routing-partial-override.test.ts +141 -0
- package/test/unit/pipeline/verify-smart-runner.test.ts +341 -0
- package/test/unit/verification/smart-runner.test.ts +246 -0
package/.gitlab-ci.yml
CHANGED
|
@@ -80,10 +80,10 @@ release:
|
|
|
80
80
|
# --- Stage: Notify ---
|
|
81
81
|
notify:
|
|
82
82
|
stage: notify
|
|
83
|
-
image:
|
|
83
|
+
image: registry-intl.cn-hongkong.aliyuncs.com/gkci/node:22.14.0-alpine-ci
|
|
84
84
|
needs: [release]
|
|
85
85
|
script:
|
|
86
|
-
- VERSION=$(
|
|
86
|
+
- VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
87
87
|
- 'curl -s -X POST -H "Content-Type: application/json" -d "{\"chat_id\": \"$TELEGRAM_CHAT_ID\", \"text\": \"nax v${VERSION} released\"}" https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage'
|
|
88
88
|
rules:
|
|
89
89
|
- if: '$CI_COMMIT_MESSAGE =~ /release-by-bot/ || $CI_COMMIT_TAG'
|
package/docs/ROADMAP.md
CHANGED
|
@@ -24,32 +24,29 @@
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
## v0.18.1 — Type Safety +
|
|
27
|
+
## v0.18.1 — Type Safety + CI Pipeline ✅
|
|
28
28
|
|
|
29
|
-
**Theme:** Fix all TypeScript/lint errors
|
|
30
|
-
**Status:**
|
|
29
|
+
**Theme:** Fix all TypeScript/lint errors, establish CI pipeline
|
|
30
|
+
**Status:** ✅ Shipped (2026-03-03)
|
|
31
31
|
|
|
32
32
|
### TypeScript Fixes (60 errors across 21 files)
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
50
|
-
|
|
51
|
-
### Re-enable Checks
|
|
52
|
-
- [ ] Re-enable `typecheck` in `nax/config.json` review checks after TS fixes land
|
|
33
|
+
- [x] ~~**TS-001:** Fix context module exports (13 errors)~~
|
|
34
|
+
- [x] ~~**TS-002:** Fix config/command type safety (12 errors)~~
|
|
35
|
+
- [x] ~~**TS-003:** Fix review/verification types (9 errors)~~
|
|
36
|
+
- [x] ~~**TS-004:** Fix escalation PRD type construction (4 errors)~~
|
|
37
|
+
- [x] ~~**TS-005:** Fix misc types (6 errors)~~
|
|
38
|
+
- [x] ~~**LINT-001:** Run biome check --fix + manual review~~
|
|
39
|
+
|
|
40
|
+
### CI Pipeline (new)
|
|
41
|
+
- [x] `.gitlab-ci.yml` — stages: test → release → notify
|
|
42
|
+
- [x] Image: `nathapp/node-bun:22.21.0-1.3.9-alpine` (test/release), `gkci/node:22.14.0-alpine-ci` (notify)
|
|
43
|
+
- [x] `before_script`: apk add git python3 make g++, safe.directory, git identity
|
|
44
|
+
- [x] Test env: `NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000`
|
|
45
|
+
- [x] CI skip guards for env-sensitive tests (claude binary, PID checks, subprocess integration)
|
|
46
|
+
- [x] Fixed `checkClaudeCLI()` ENOENT crash — try/catch around Bun.spawn
|
|
47
|
+
- [x] Release trigger: `[run-release]` in commit message on master
|
|
48
|
+
- [x] Runner requirement: 8GB shared runner (`saas-linux-small-amd64`)
|
|
49
|
+
- [x] **Result: 1952 pass, 56 skip, 0 fail**
|
|
53
50
|
|
|
54
51
|
---
|
|
55
52
|
|
|
@@ -96,6 +93,7 @@
|
|
|
96
93
|
|
|
97
94
|
| Version | Theme | Date | Details |
|
|
98
95
|
|:---|:---|:---|:---|
|
|
96
|
+
| v0.18.1 | Type Safety + CI Pipeline | 2026-03-03 | 60 TS errors + 12 lint errors fixed, GitLab CI green (1952/56/0) |
|
|
99
97
|
| v0.18.0 | Orchestration Quality | 2026-03-03 | BUG-016/017/018/019/020/021/022/023/025 all fixed |
|
|
100
98
|
| v0.17.0 | Config Management | 2026-03-02 | CM-001 --explain, CM-002 --diff, CM-003 default view |
|
|
101
99
|
| v0.16.4 | Bugfixes: Routing + Env Allowlist | 2026-03-02 | BUG-012/013/014 |
|
package/nax/config.json
CHANGED
|
@@ -63,11 +63,14 @@
|
|
|
63
63
|
"verificationTimeoutSeconds": 300,
|
|
64
64
|
"maxStoriesPerFeature": 15,
|
|
65
65
|
"rectification": {
|
|
66
|
-
"enabled":
|
|
66
|
+
"enabled": false,
|
|
67
67
|
"maxRetries": 2,
|
|
68
|
-
"fullSuiteTimeoutSeconds":
|
|
68
|
+
"fullSuiteTimeoutSeconds": 600,
|
|
69
69
|
"maxFailureSummaryChars": 2000,
|
|
70
70
|
"abortOnIncreasingFailures": true
|
|
71
|
+
},
|
|
72
|
+
"regressionGate": {
|
|
73
|
+
"enabled": false
|
|
71
74
|
}
|
|
72
75
|
},
|
|
73
76
|
"quality": {
|
|
@@ -75,9 +78,9 @@
|
|
|
75
78
|
"requireLint": true,
|
|
76
79
|
"requireTests": true,
|
|
77
80
|
"commands": {
|
|
78
|
-
"test": "bun test
|
|
79
|
-
"typecheck": "bun
|
|
80
|
-
"lint": "bun
|
|
81
|
+
"test": "bun run test",
|
|
82
|
+
"typecheck": "bun run typecheck",
|
|
83
|
+
"lint": "bun run lint"
|
|
81
84
|
},
|
|
82
85
|
"forceExit": false,
|
|
83
86
|
"detectOpenHandles": true,
|
|
@@ -114,15 +117,15 @@
|
|
|
114
117
|
"maxCodebaseSummaryTokens": 5000
|
|
115
118
|
},
|
|
116
119
|
"review": {
|
|
117
|
-
"enabled":
|
|
120
|
+
"enabled": false,
|
|
118
121
|
"checks": [
|
|
119
122
|
"test",
|
|
120
123
|
"lint"
|
|
121
124
|
],
|
|
122
125
|
"commands": {
|
|
123
|
-
"test": "bun test
|
|
124
|
-
"typecheck": "bun
|
|
125
|
-
"lint": "bun
|
|
126
|
+
"test": "bun run test",
|
|
127
|
+
"typecheck": "bun run typecheck",
|
|
128
|
+
"lint": "bun run lint"
|
|
126
129
|
}
|
|
127
130
|
},
|
|
128
131
|
"plan": {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "nax",
|
|
3
|
+
"feature": "smart-test-runner",
|
|
4
|
+
"branchName": "feat/v0.18.2-smart-runner",
|
|
5
|
+
"userStories": [
|
|
6
|
+
{
|
|
7
|
+
"id": "FIX-001",
|
|
8
|
+
"title": "Fix partial routing override in routing stage",
|
|
9
|
+
"description": "In `src/pipeline/stages/routing.ts`, the code unconditionally overwrites complexity and testStrategy from story.routing, even if undefined. Fix: only override when the field is actually set. Change line 41 `routing.complexity = ctx.story.routing.complexity` to `if (ctx.story.routing?.complexity) routing.complexity = ctx.story.routing.complexity`. Same for testStrategy on the next line. This allows PRD authors to set just `\"routing\": { \"testStrategy\": \"test-after\" }` without needing complexity/modelTier. Add test in `test/unit/pipeline/routing-partial-override.test.ts` verifying: (1) partial override with only testStrategy works, (2) LLM-classified complexity is preserved when not overridden, (3) full override still works.",
|
|
10
|
+
"complexity": "simple",
|
|
11
|
+
"status": "passed",
|
|
12
|
+
"tags": [
|
|
13
|
+
"bugfix"
|
|
14
|
+
],
|
|
15
|
+
"acceptanceCriteria": [
|
|
16
|
+
"routing.complexity is only overridden when ctx.story.routing.complexity is defined",
|
|
17
|
+
"routing.testStrategy is only overridden when ctx.story.routing.testStrategy is defined",
|
|
18
|
+
"PRD with only routing.testStrategy set uses LLM for complexity",
|
|
19
|
+
"Test file test/unit/pipeline/routing-partial-override.test.ts exists with 3 passing tests"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": [],
|
|
22
|
+
"escalations": [],
|
|
23
|
+
"attempts": 1,
|
|
24
|
+
"passes": true,
|
|
25
|
+
"routing": {
|
|
26
|
+
"testStrategy": "test-after"
|
|
27
|
+
},
|
|
28
|
+
"priorErrors": [],
|
|
29
|
+
"storyPoints": 1
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "STR-001",
|
|
33
|
+
"title": "Git diff file detection",
|
|
34
|
+
"description": "Create module `src/verification/smart-runner.ts` with exported function `getChangedSourceFiles(workdir: string): Promise<string[]>`. Runs `git diff --name-only HEAD~1` in the workdir to get changed files. Filter to only `.ts` files that start with `src/`. Use Bun.spawn for the git command. Handle git errors gracefully (return empty array on failure).",
|
|
35
|
+
"complexity": "simple",
|
|
36
|
+
"status": "passed",
|
|
37
|
+
"tags": [
|
|
38
|
+
"core"
|
|
39
|
+
],
|
|
40
|
+
"acceptanceCriteria": [
|
|
41
|
+
"getChangedSourceFiles returns only .ts files under src/",
|
|
42
|
+
"Returns empty array when git command fails",
|
|
43
|
+
"Uses Bun.spawn (not child_process)"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": [
|
|
46
|
+
"FIX-001"
|
|
47
|
+
],
|
|
48
|
+
"escalations": [],
|
|
49
|
+
"attempts": 0,
|
|
50
|
+
"passes": true,
|
|
51
|
+
"routing": {
|
|
52
|
+
"testStrategy": "test-after",
|
|
53
|
+
"modelTier": "powerful"
|
|
54
|
+
},
|
|
55
|
+
"priorErrors": [
|
|
56
|
+
"REGRESSION: full-suite regression detected",
|
|
57
|
+
"Attempt 2 failed with model tier: fast",
|
|
58
|
+
"Attempt 1 failed with model tier: balanced"
|
|
59
|
+
],
|
|
60
|
+
"storyPoints": 1
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "STR-002",
|
|
64
|
+
"title": "Source-to-test file mapping",
|
|
65
|
+
"description": "Add `mapSourceToTests(sourceFiles: string[], workdir: string): Promise<string[]>` to `src/verification/smart-runner.ts`. Convention: `src/foo/bar.ts` → `test/unit/foo/bar.test.ts`. Also check `test/integration/foo/bar.test.ts`. Only return paths where the file actually exists on disk (use Bun.file(path).exists()). Return empty array if no test files map.",
|
|
66
|
+
"complexity": "simple",
|
|
67
|
+
"status": "passed",
|
|
68
|
+
"tags": [
|
|
69
|
+
"core"
|
|
70
|
+
],
|
|
71
|
+
"acceptanceCriteria": [
|
|
72
|
+
"Maps src/foo/bar.ts to test/unit/foo/bar.test.ts correctly",
|
|
73
|
+
"Also checks test/integration/ path",
|
|
74
|
+
"Only returns files that exist on disk",
|
|
75
|
+
"Returns empty array when no test files match"
|
|
76
|
+
],
|
|
77
|
+
"dependencies": [
|
|
78
|
+
"STR-001"
|
|
79
|
+
],
|
|
80
|
+
"escalations": [],
|
|
81
|
+
"attempts": 0,
|
|
82
|
+
"passes": true,
|
|
83
|
+
"routing": {
|
|
84
|
+
"testStrategy": "test-after"
|
|
85
|
+
},
|
|
86
|
+
"priorErrors": [],
|
|
87
|
+
"storyPoints": 1
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "STR-003",
|
|
91
|
+
"title": "Smart test command builder",
|
|
92
|
+
"description": "Add `buildSmartTestCommand(testFiles: string[], baseCommand: string): string` to `src/verification/smart-runner.ts`. When testFiles is non-empty: replace the last path argument in baseCommand with the specific test file paths joined by spaces. E.g., `bun test test/` with files `[test/unit/foo.test.ts]` → `bun test test/unit/foo.test.ts`. When testFiles is empty: return the original baseCommand unchanged (full suite fallback).",
|
|
93
|
+
"complexity": "simple",
|
|
94
|
+
"status": "passed",
|
|
95
|
+
"tags": [
|
|
96
|
+
"core"
|
|
97
|
+
],
|
|
98
|
+
"acceptanceCriteria": [
|
|
99
|
+
"Builds scoped command with specific test files",
|
|
100
|
+
"Returns original command when testFiles is empty",
|
|
101
|
+
"Works with bun test test/ base command"
|
|
102
|
+
],
|
|
103
|
+
"dependencies": [
|
|
104
|
+
"STR-002"
|
|
105
|
+
],
|
|
106
|
+
"escalations": [],
|
|
107
|
+
"attempts": 0,
|
|
108
|
+
"passes": true,
|
|
109
|
+
"routing": {
|
|
110
|
+
"testStrategy": "test-after"
|
|
111
|
+
},
|
|
112
|
+
"priorErrors": [],
|
|
113
|
+
"storyPoints": 1
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "STR-004",
|
|
117
|
+
"title": "Smart runner config flag",
|
|
118
|
+
"description": "Add `execution.smartTestRunner: boolean` (default: true) to the config in `src/config/types.ts` (ExecutionConfig interface) and the Zod schema in `src/config/loader.ts`. When false, the verify stage skips smart runner entirely and uses the full test suite command.",
|
|
119
|
+
"complexity": "simple",
|
|
120
|
+
"status": "passed",
|
|
121
|
+
"tags": [
|
|
122
|
+
"config"
|
|
123
|
+
],
|
|
124
|
+
"acceptanceCriteria": [
|
|
125
|
+
"execution.smartTestRunner field exists in ExecutionConfig interface",
|
|
126
|
+
"Zod schema accepts and defaults to true",
|
|
127
|
+
"Config loads correctly with and without the field"
|
|
128
|
+
],
|
|
129
|
+
"dependencies": [
|
|
130
|
+
"STR-003"
|
|
131
|
+
],
|
|
132
|
+
"escalations": [],
|
|
133
|
+
"attempts": 0,
|
|
134
|
+
"passes": true,
|
|
135
|
+
"routing": {
|
|
136
|
+
"testStrategy": "test-after"
|
|
137
|
+
},
|
|
138
|
+
"priorErrors": [],
|
|
139
|
+
"storyPoints": 1
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "STR-005",
|
|
143
|
+
"title": "Integrate smart runner into verify stage",
|
|
144
|
+
"description": "Modify `src/pipeline/stages/verify.ts` to use smart runner when `ctx.config.execution.smartTestRunner` is true (default). Before calling regression(): call getChangedSourceFiles(ctx.workdir) → mapSourceToTests(files, ctx.workdir) → buildSmartTestCommand(testFiles, testCommand). Use the result as the command for regression(). Log which mode was selected: `[smart-runner] Running N targeted test files` or `[smart-runner] No mapped tests — falling back to full suite`.",
|
|
145
|
+
"complexity": "medium",
|
|
146
|
+
"status": "passed",
|
|
147
|
+
"tags": [
|
|
148
|
+
"integration"
|
|
149
|
+
],
|
|
150
|
+
"acceptanceCriteria": [
|
|
151
|
+
"Verify stage uses scoped test command when smart runner finds test files",
|
|
152
|
+
"Falls back to full suite when no test files map",
|
|
153
|
+
"Skips smart runner entirely when config.execution.smartTestRunner is false",
|
|
154
|
+
"Logs the mode used"
|
|
155
|
+
],
|
|
156
|
+
"dependencies": [
|
|
157
|
+
"STR-004"
|
|
158
|
+
],
|
|
159
|
+
"escalations": [],
|
|
160
|
+
"attempts": 1,
|
|
161
|
+
"passes": true,
|
|
162
|
+
"routing": {
|
|
163
|
+
"testStrategy": "test-after",
|
|
164
|
+
"modelTier": "powerful"
|
|
165
|
+
},
|
|
166
|
+
"priorErrors": [
|
|
167
|
+
"Attempt 1 failed with model tier: fast",
|
|
168
|
+
"Attempt 1 failed with model tier: balanced"
|
|
169
|
+
],
|
|
170
|
+
"storyPoints": 1
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "STR-006",
|
|
174
|
+
"title": "Unit tests for smart runner module",
|
|
175
|
+
"description": "Write `test/unit/verification/smart-runner.test.ts` with tests: (1) getChangedSourceFiles filters to .ts src/ files only, (2) mapSourceToTests maps paths correctly and checks file existence, (3) mapSourceToTests returns empty when files don't exist, (4) buildSmartTestCommand builds scoped command with test files, (5) buildSmartTestCommand returns original command when testFiles is empty. Use temp dirs with Bun.write for file existence tests.",
|
|
176
|
+
"complexity": "medium",
|
|
177
|
+
"status": "failed",
|
|
178
|
+
"tags": [
|
|
179
|
+
"test"
|
|
180
|
+
],
|
|
181
|
+
"acceptanceCriteria": [
|
|
182
|
+
"5 test cases all pass",
|
|
183
|
+
"Uses temp dirs for file existence tests",
|
|
184
|
+
"No external process spawning in unit tests (mock Bun.spawn)"
|
|
185
|
+
],
|
|
186
|
+
"dependencies": [
|
|
187
|
+
"STR-005"
|
|
188
|
+
],
|
|
189
|
+
"escalations": [],
|
|
190
|
+
"attempts": 1,
|
|
191
|
+
"passes": false,
|
|
192
|
+
"routing": {
|
|
193
|
+
"testStrategy": "test-after",
|
|
194
|
+
"modelTier": "powerful"
|
|
195
|
+
},
|
|
196
|
+
"priorErrors": [
|
|
197
|
+
"Attempt 1 failed with model tier: balanced"
|
|
198
|
+
],
|
|
199
|
+
"storyPoints": 1
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
"updatedAt": "2026-03-03T14:17:09.852Z"
|
|
203
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Progress: smart-test-runner
|
|
2
|
+
|
|
3
|
+
Created: 2026-03-03T11:45:10.257Z
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
[2026-03-03T12:12:58.473Z] FIX-001 — FAILED — Fix partial routing override in routing stage — Review failed: test failed (exit code 132)
|
|
7
|
+
[2026-03-03T12:27:31.803Z] STR-001 — PASSED — Git diff file detection — Cost: $0.0994
|
|
8
|
+
[2026-03-03T12:29:37.971Z] STR-001 — FAILED — Git diff file detection -- REGRESSION: full-suite regression detected
|
|
9
|
+
[2026-03-03T12:52:43.615Z] STR-002 — PASSED — Source-to-test file mapping — Cost: $0.0889
|
|
10
|
+
[2026-03-03T12:59:34.497Z] STR-003 — PASSED — Smart test command builder — Cost: $0.0862
|
|
11
|
+
[2026-03-03T13:05:48.251Z] STR-004 — PASSED — Smart runner config flag — Cost: $0.1583
|
|
12
|
+
[2026-03-03T13:21:09.123Z] STR-005 — FAILED — Integrate smart runner into verify stage — Execution failed
|
|
13
|
+
[2026-03-03T14:17:09.853Z] STR-006 — FAILED — Unit tests for smart runner module — Execution failed
|
package/package.json
CHANGED
package/src/config/defaults.ts
CHANGED
package/src/config/schemas.ts
CHANGED
|
@@ -81,6 +81,7 @@ const ExecutionConfigSchema = z.object({
|
|
|
81
81
|
lintCommand: z.string().nullable().optional(),
|
|
82
82
|
typecheckCommand: z.string().nullable().optional(),
|
|
83
83
|
dangerouslySkipPermissions: z.boolean().default(true),
|
|
84
|
+
smartTestRunner: z.boolean().default(true),
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
const QualityConfigSchema = z.object({
|
package/src/config/types.ts
CHANGED
|
@@ -106,6 +106,8 @@ export interface ExecutionConfig {
|
|
|
106
106
|
typecheckCommand?: string | null;
|
|
107
107
|
/** Use --dangerously-skip-permissions flag for agent (default: true for backward compat, SEC-1 fix) */
|
|
108
108
|
dangerouslySkipPermissions?: boolean;
|
|
109
|
+
/** Enable smart test runner to scope test runs to changed files (default: true) */
|
|
110
|
+
smartTestRunner?: boolean;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
/** Quality gate config */
|
|
@@ -36,10 +36,10 @@ export const routingStage: PipelineStage = {
|
|
|
36
36
|
if (ctx.story.routing) {
|
|
37
37
|
// Use cached complexity/testStrategy, but re-derive modelTier from current config
|
|
38
38
|
routing = await routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
39
|
-
// Override with cached
|
|
40
|
-
routing.complexity = ctx.story.routing.complexity;
|
|
41
|
-
routing.testStrategy = ctx.story.routing.testStrategy;
|
|
42
|
-
// Re-derive modelTier from
|
|
39
|
+
// Override with cached values only when they are actually set
|
|
40
|
+
if (ctx.story.routing?.complexity) routing.complexity = ctx.story.routing.complexity;
|
|
41
|
+
if (ctx.story.routing?.testStrategy) routing.testStrategy = ctx.story.routing.testStrategy;
|
|
42
|
+
// Re-derive modelTier from (possibly overridden) complexity and current config
|
|
43
43
|
routing.modelTier = complexityToModelTier(routing.complexity as import("../../config").Complexity, ctx.config);
|
|
44
44
|
} else {
|
|
45
45
|
// Fresh classification
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { getLogger } from "../../logger";
|
|
24
24
|
import { regression } from "../../verification/gate";
|
|
25
|
+
import { _smartRunnerDeps } from "../../verification/smart-runner";
|
|
25
26
|
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
26
27
|
|
|
27
28
|
export const verifyStage: PipelineStage = {
|
|
@@ -46,10 +47,28 @@ export const verifyStage: PipelineStage = {
|
|
|
46
47
|
|
|
47
48
|
logger.info("verify", "Running verification", { storyId: ctx.story.id });
|
|
48
49
|
|
|
50
|
+
// Determine effective test command (smart runner or full suite)
|
|
51
|
+
let effectiveCommand = testCommand;
|
|
52
|
+
const smartRunnerEnabled = ctx.config.execution.smartTestRunner !== false;
|
|
53
|
+
|
|
54
|
+
if (smartRunnerEnabled) {
|
|
55
|
+
const sourceFiles = await _smartRunnerDeps.getChangedSourceFiles(ctx.workdir);
|
|
56
|
+
const testFiles = await _smartRunnerDeps.mapSourceToTests(sourceFiles, ctx.workdir);
|
|
57
|
+
|
|
58
|
+
if (testFiles.length > 0) {
|
|
59
|
+
effectiveCommand = _smartRunnerDeps.buildSmartTestCommand(testFiles, testCommand);
|
|
60
|
+
logger.info("verify", `[smart-runner] Running ${testFiles.length} targeted test files`, {
|
|
61
|
+
storyId: ctx.story.id,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
logger.info("verify", "[smart-runner] No mapped tests — falling back to full suite", { storyId: ctx.story.id });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
// Use unified regression gate (includes 2s wait for agent process cleanup)
|
|
50
69
|
const result = await regression({
|
|
51
70
|
workdir: ctx.workdir,
|
|
52
|
-
command:
|
|
71
|
+
command: effectiveCommand,
|
|
53
72
|
timeoutSeconds: ctx.config.execution.verificationTimeoutSeconds,
|
|
54
73
|
});
|
|
55
74
|
|
package/src/precheck/index.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface PrecheckOptions {
|
|
|
62
62
|
format?: "human" | "json";
|
|
63
63
|
/** Working directory */
|
|
64
64
|
workdir: string;
|
|
65
|
+
/** Suppress console output (for programmatic use) */
|
|
66
|
+
silent?: boolean;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/** Extended result with exit code for CLI usage */
|
|
@@ -87,6 +89,7 @@ export async function runPrecheck(
|
|
|
87
89
|
): Promise<PrecheckResultWithCode> {
|
|
88
90
|
const workdir = options?.workdir || process.cwd();
|
|
89
91
|
const format = options?.format || "human";
|
|
92
|
+
const silent = options?.silent ?? false;
|
|
90
93
|
|
|
91
94
|
const passed: Check[] = [];
|
|
92
95
|
const blockers: Check[] = [];
|
|
@@ -196,10 +199,12 @@ export async function runPrecheck(
|
|
|
196
199
|
exitCode = hasPRDError ? EXIT_CODES.INVALID_PRD : EXIT_CODES.BLOCKER;
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
if (!silent) {
|
|
203
|
+
if (format === "json") {
|
|
204
|
+
console.log(JSON.stringify(output, null, 2));
|
|
205
|
+
} else {
|
|
206
|
+
printSummary(output);
|
|
207
|
+
}
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
return {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Runner — Git diff file detection
|
|
3
|
+
*
|
|
4
|
+
* Detects changed TypeScript source files using git diff,
|
|
5
|
+
* enabling targeted test runs on only the files that changed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get TypeScript source files changed since the previous commit.
|
|
10
|
+
*
|
|
11
|
+
* Runs `git diff --name-only HEAD~1` in the given workdir and filters
|
|
12
|
+
* results to only `.ts` files under `src/`. Returns an empty array on
|
|
13
|
+
* any git error (not a repo, no previous commit, etc.).
|
|
14
|
+
*
|
|
15
|
+
* @param workdir - Working directory to run git command in
|
|
16
|
+
* @returns Array of changed .ts file paths relative to the repo root
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const files = await getChangedSourceFiles("/path/to/repo");
|
|
21
|
+
* // Returns: ["src/foo/bar.ts", "src/utils/git.ts"]
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Map source files to their corresponding test files.
|
|
26
|
+
*
|
|
27
|
+
* For each file in `sourceFiles`, checks both:
|
|
28
|
+
* - `<workdir>/test/unit/<relative-path>.test.ts`
|
|
29
|
+
* - `<workdir>/test/integration/<relative-path>.test.ts`
|
|
30
|
+
*
|
|
31
|
+
* where `<relative-path>` is the file path with the leading `src/` stripped
|
|
32
|
+
* and the `.ts` extension replaced with `.test.ts`.
|
|
33
|
+
*
|
|
34
|
+
* Only returns paths that actually exist on disk.
|
|
35
|
+
*
|
|
36
|
+
* @param sourceFiles - Array of source file paths (e.g. `["src/foo/bar.ts"]`)
|
|
37
|
+
* @param workdir - Absolute path to the repository root
|
|
38
|
+
* @returns Existing test file paths (absolute)
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const tests = await mapSourceToTests(["src/foo/bar.ts"], "/repo");
|
|
43
|
+
* // Returns: ["/repo/test/unit/foo/bar.test.ts"] (if it exists)
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export async function mapSourceToTests(sourceFiles: string[], workdir: string): Promise<string[]> {
|
|
47
|
+
const result: string[] = [];
|
|
48
|
+
|
|
49
|
+
for (const sourceFile of sourceFiles) {
|
|
50
|
+
// Strip leading "src/" and replace ".ts" with ".test.ts"
|
|
51
|
+
const relative = sourceFile.replace(/^src\//, "").replace(/\.ts$/, ".test.ts");
|
|
52
|
+
|
|
53
|
+
const candidates = [`${workdir}/test/unit/${relative}`, `${workdir}/test/integration/${relative}`];
|
|
54
|
+
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (await Bun.file(candidate).exists()) {
|
|
57
|
+
result.push(candidate);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build a scoped test command targeting specific test files.
|
|
67
|
+
*
|
|
68
|
+
* When `testFiles` is non-empty, replaces the last path-like argument in
|
|
69
|
+
* `baseCommand` (a token containing `/`) with the specific test file paths
|
|
70
|
+
* joined by spaces. If no path argument is found, appends the test files.
|
|
71
|
+
*
|
|
72
|
+
* When `testFiles` is empty, returns `baseCommand` unchanged (full-suite
|
|
73
|
+
* fallback).
|
|
74
|
+
*
|
|
75
|
+
* @param testFiles - Test file paths to scope the run to
|
|
76
|
+
* @param baseCommand - Full test command (e.g. `"bun test test/"`)
|
|
77
|
+
* @returns Scoped command string
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* buildSmartTestCommand(["test/unit/foo.test.ts"], "bun test test/")
|
|
82
|
+
* // => "bun test test/unit/foo.test.ts"
|
|
83
|
+
*
|
|
84
|
+
* buildSmartTestCommand([], "bun test test/")
|
|
85
|
+
* // => "bun test test/"
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function buildSmartTestCommand(testFiles: string[], baseCommand: string): string {
|
|
89
|
+
if (testFiles.length === 0) {
|
|
90
|
+
return baseCommand;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const parts = baseCommand.trim().split(/\s+/);
|
|
94
|
+
|
|
95
|
+
// Find the last token that looks like a path (contains '/')
|
|
96
|
+
let lastPathIndex = -1;
|
|
97
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
98
|
+
if (parts[i].includes("/")) {
|
|
99
|
+
lastPathIndex = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (lastPathIndex === -1) {
|
|
105
|
+
// No path argument — append test files
|
|
106
|
+
return `${baseCommand} ${testFiles.join(" ")}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Replace the last path argument with the specific test files
|
|
110
|
+
const newParts = [...parts.slice(0, lastPathIndex), ...testFiles];
|
|
111
|
+
return newParts.join(" ");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getChangedSourceFiles(workdir: string): Promise<string[]> {
|
|
115
|
+
try {
|
|
116
|
+
const proc = Bun.spawn({
|
|
117
|
+
cmd: ["git", "diff", "--name-only", "HEAD~1"],
|
|
118
|
+
cwd: workdir,
|
|
119
|
+
stdout: "pipe",
|
|
120
|
+
stderr: "pipe",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const exitCode = await proc.exited;
|
|
124
|
+
if (exitCode !== 0) return [];
|
|
125
|
+
|
|
126
|
+
const stdout = await new Response(proc.stdout).text();
|
|
127
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
128
|
+
|
|
129
|
+
return lines.filter((f) => f.startsWith("src/") && f.endsWith(".ts"));
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Injectable dependencies for testing.
|
|
137
|
+
* Allows tests to swap implementations without using mock.module(),
|
|
138
|
+
* which leaks across files in Bun 1.x due to shared module registry.
|
|
139
|
+
*
|
|
140
|
+
* @internal - test use only. Do not use in production code.
|
|
141
|
+
*/
|
|
142
|
+
export const _smartRunnerDeps = {
|
|
143
|
+
getChangedSourceFiles,
|
|
144
|
+
mapSourceToTests,
|
|
145
|
+
buildSmartTestCommand,
|
|
146
|
+
};
|