sillyspec 3.24.2 → 3.24.4
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/.claude/skills/sillyspec-quick/SKILL.md +12 -3
- package/package.json +1 -1
- package/src/machine-interface.js +19 -2
- package/src/run.js +60 -15
- package/src/verify-postcheck.js +81 -8
|
@@ -18,7 +18,7 @@ quick 阶段的 `--change` 语义是「关联变更」**且会触发步骤重置
|
|
|
18
18
|
- `--linked-changes a,b`:显式关联到变更 a、b
|
|
19
19
|
- `--non-interactive`:CI/脚本环境,默认不关联(避免交互 prompt 崩溃)
|
|
20
20
|
|
|
21
|
-
首次 `sillyspec run quick` 选定的关联会持久化到 `.runtime/quick-guard.json
|
|
21
|
+
首次 `sillyspec run quick` 选定的关联会持久化到 `.runtime/quick-sessions/<quick-session-id>/guard.json`(每个 quick 会话独立,多会话并发不互覆盖),后续 `--done` 自动复用,**不会重复弹交互 prompt**。
|
|
22
22
|
|
|
23
23
|
## 步骤生命周期(所有阶段通用)
|
|
24
24
|
|
|
@@ -50,8 +50,17 @@ sillyspec run quick --reopen --from-step N # 重新打开已完成阶段修
|
|
|
50
50
|
| `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
|
|
51
51
|
| `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护) |
|
|
52
52
|
| `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
|
|
53
|
-
| `--force-baseline` | 允许覆盖 baseline
|
|
54
|
-
| `--confirm` |
|
|
53
|
+
| `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
|
|
54
|
+
| `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new` |
|
|
55
|
+
|
|
56
|
+
## 审计与并发变更(`--done` 边界审计)
|
|
57
|
+
|
|
58
|
+
`--done` 收尾时对比 step1 baseline 与当前 `git status`,拦「危险文件 / 新增文件 / 覆盖 baseline」。要点:
|
|
59
|
+
|
|
60
|
+
- **并发其他会话的 `.sillyspec/changes/<非关联变更>/` 不再被本 quick 拦截**。quick 自己没有 `changes/` 目录,该路径下非关联内容视为并发会话的工作,整体放行(确定性审计无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
|
|
61
|
+
- **关联变更的文件仍走审计**:reverse-sync 改自己关联变更的 `design.md` 会被拦,需 `--force-baseline` 显式确认。
|
|
62
|
+
- **baseline 折叠目录前缀匹配**:step1 启动时整片 `changes/` 未跟踪会被 git 折叠成 `?? .sillyspec/changes/`(带尾斜杠 token);审计时若该目录下文件被并发会话跟踪而展开成文件级路径,按尾斜杠 token 前缀放行其下所有文件,不误判。
|
|
63
|
+
- **`--confirm` ≠ 解锁**:它只打印概览。blocked 时真正解锁的是 `--force-baseline`(压制 `.sillyspec/` 危险判定 + baseline 覆盖)/`--allow-new`(放行新增),两者在 step1 或 `--done` 传都生效。
|
|
55
64
|
|
|
56
65
|
## 典型用法
|
|
57
66
|
|
package/package.json
CHANGED
package/src/machine-interface.js
CHANGED
|
@@ -217,16 +217,27 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
|
|
|
217
217
|
// ── d. verify 阶段追加 verify-test(CLI 实测 local.yaml commands.test)──
|
|
218
218
|
if (stage === 'verify') {
|
|
219
219
|
const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
|
|
220
|
+
const vtWarnings = [];
|
|
221
|
+
if (vt.status === 'skipped') {
|
|
222
|
+
vtWarnings.push('⚠️ verify-test SKIPPED — gate 未核验测试(local.yaml 未配置 commands.test 或显式无测试)。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL');
|
|
223
|
+
}
|
|
224
|
+
// 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
|
|
225
|
+
// 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
|
|
226
|
+
if (vt.status !== 'skipped' && vt.mode === 'full' && vt.fallbackReason) {
|
|
227
|
+
vtWarnings.push(`⚠️ verify-test 跑的是全量 commands.test(${vt.fallbackReason});失败可能含未变更模块的预存错误,driver 不应据 exit 0 判定本次变更范围已测`);
|
|
228
|
+
}
|
|
220
229
|
checks.push({
|
|
221
230
|
id: 'verify-test',
|
|
222
231
|
ok: vt.status !== 'failed',
|
|
223
232
|
errors: vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [],
|
|
224
|
-
warnings:
|
|
233
|
+
warnings: vtWarnings,
|
|
225
234
|
data: {
|
|
226
235
|
status: vt.status,
|
|
227
236
|
exitCode: vt.exitCode,
|
|
228
237
|
durationMs: vt.durationMs,
|
|
229
238
|
resultPath: vt.resultPath,
|
|
239
|
+
mode: vt.mode ?? null,
|
|
240
|
+
fallbackReason: vt.fallbackReason ?? null,
|
|
230
241
|
},
|
|
231
242
|
});
|
|
232
243
|
}
|
|
@@ -351,10 +362,16 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot
|
|
|
351
362
|
exitCode: vt.exitCode,
|
|
352
363
|
durationMs: vt.durationMs,
|
|
353
364
|
resultPath: vt.resultPath,
|
|
365
|
+
mode: vt.mode ?? null,
|
|
366
|
+
fallbackReason: vt.fallbackReason ?? null,
|
|
354
367
|
};
|
|
355
368
|
ok = vt.status !== 'failed';
|
|
356
369
|
errors = vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [];
|
|
357
|
-
warnings = vt.status === 'skipped'
|
|
370
|
+
warnings = vt.status === 'skipped'
|
|
371
|
+
? ['测试被跳过']
|
|
372
|
+
: (vt.mode === 'full' && vt.fallbackReason)
|
|
373
|
+
? [`⚠️ verify-test 跑的是全量 commands.test(${vt.fallbackReason});失败可能含未变更模块的预存错误`]
|
|
374
|
+
: [];
|
|
358
375
|
exitCode = ok ? EXIT_OK : EXIT_BLOCKED;
|
|
359
376
|
break;
|
|
360
377
|
}
|
package/src/run.js
CHANGED
|
@@ -332,6 +332,27 @@ function parseModuleMapSimple(content) {
|
|
|
332
332
|
/**
|
|
333
333
|
* 同步触发辅助函数:_write 后 best-effort 同步到平台
|
|
334
334
|
*/
|
|
335
|
+
/**
|
|
336
|
+
* 解析单行 `git status --porcelain` 输出为文件路径。
|
|
337
|
+
* porcelain 格式:`XY<SP>path`(XY = 2 字符状态码)。
|
|
338
|
+
* - 路径含空格/特殊字符时 git 以 C 风格加引号(如 `?? "front end.txt"`),需去引号 + 反转义。
|
|
339
|
+
* - 重命名显示为 `R old -> new`,取新路径(当前存在的文件)。
|
|
340
|
+
* - 反斜杠归一化为正斜杠(Windows 路径)。
|
|
341
|
+
* ⚠️ 调用方不要对【整段多行】输出先 .trim() 再 split——会削掉首行前导空格(首行 status 的空格),
|
|
342
|
+
* 使 slice(3) 误吃首文件路径首字符(实测 ` M frontend/...` → "rontend/...",见 guard.json baseline)。
|
|
343
|
+
* 应直接 split('\n').filter(Boolean),每行单独解析。
|
|
344
|
+
*/
|
|
345
|
+
export function parsePorcelainPath(line) {
|
|
346
|
+
if (!line) return ''
|
|
347
|
+
let path = line.slice(3).trim()
|
|
348
|
+
if (path.length >= 2 && path.startsWith('"') && path.endsWith('"')) {
|
|
349
|
+
path = path.slice(1, -1).replace(/\\(.)/g, (_, c) => c)
|
|
350
|
+
}
|
|
351
|
+
const arrow = path.indexOf(' -> ')
|
|
352
|
+
if (arrow !== -1) path = path.slice(arrow + 4)
|
|
353
|
+
return path.replace(/\\/g, '/')
|
|
354
|
+
}
|
|
355
|
+
|
|
335
356
|
/**
|
|
336
357
|
* quick 完成审计:对比 baseline 与实际变更
|
|
337
358
|
*/
|
|
@@ -343,20 +364,41 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
343
364
|
try {
|
|
344
365
|
const { execSync } = await import('child_process')
|
|
345
366
|
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
346
|
-
|
|
367
|
+
// 不对整段 .trim():会削首行前导空格致首文件路径丢首字符(见 parsePorcelainPath 注释)。
|
|
368
|
+
const currentEntries = gitStatus.split('\n').filter(Boolean)
|
|
347
369
|
|
|
348
370
|
const normalizeGitPath = (p) => p.replace(/\\/g, '/')
|
|
349
371
|
// step1 启动时记录的全量脏文件 = 预存改动(非本次 quick 产生)。审计必须排除它们,
|
|
350
372
|
// 否则脏工作区下预存文件持续留在 git status → 命中 baselineFiles → 误判「覆盖 baseline」
|
|
351
373
|
// → 永远 blocked(--force-baseline 也救不回来,因为 status 判定看 baselineHit 数组)。
|
|
352
|
-
|
|
374
|
+
// 前缀匹配:baseline 录入时未跟踪目录会被 git 折叠成 `dir/`(带尾斜杠)token,审计时若该
|
|
375
|
+
// 目录下文件被跟踪则展开成文件级 `dir/file`——精确匹配对不上,故尾斜杠 token 按目录前缀放行。
|
|
376
|
+
const normBaseline = (baselineFiles || []).map(f => normalizeGitPath(f))
|
|
377
|
+
const baselineExact = new Set(normBaseline.filter(f => !f.endsWith('/')))
|
|
378
|
+
const baselineDirs = normBaseline.filter(f => f.endsWith('/'))
|
|
379
|
+
const isBaselineFile = (p) => {
|
|
380
|
+
const f = normalizeGitPath(p)
|
|
381
|
+
return baselineExact.has(f) || baselineDirs.some(d => f.startsWith(d))
|
|
382
|
+
}
|
|
383
|
+
// quick 自己没有 .sillyspec/changes/ 目录——该路径下任何文件要么是关联变更(reverse-sync),
|
|
384
|
+
// 要么是并发其他会话的变更工作。多会话并行时别人的 changes/ 不应被本 quick 审计拦截
|
|
385
|
+
// (确定性校验无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
|
|
386
|
+
// 故:非关联变更目录整体放行;关联变更的文件仍走正常审计(reverse-sync 可见,需 --force-baseline)。
|
|
387
|
+
const linkedChangeNames = new Set((Array.isArray(guard.linkedChanges) ? guard.linkedChanges : []).map(c => normalizeGitPath(c)))
|
|
353
388
|
const isQuickMetadata = (p) => {
|
|
354
389
|
const file = normalizeGitPath(p)
|
|
355
|
-
|
|
390
|
+
if (file.startsWith('.sillyspec/quicklog/')
|
|
356
391
|
|| file.startsWith('.sillyspec/.runtime/')
|
|
357
392
|
|| file === '.sillyspec/knowledge/uncategorized.md'
|
|
358
393
|
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
|
|
359
|
-
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))
|
|
394
|
+
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
|
|
395
|
+
// .sillyspec/changes/ 下:bare 折叠 token(git 把全新未跟踪 changes/ 折叠成 `changes/`)
|
|
396
|
+
// 或具体但【非关联】的变更 → 都属并发其他会话工作,放行;关联变更文件仍走审计。
|
|
397
|
+
if (file.startsWith('.sillyspec/changes/')) {
|
|
398
|
+
const m = file.match(/^\.sillyspec\/changes\/([^/]+)(\/|$)/)
|
|
399
|
+
if (!m || !linkedChangeNames.has(m[1])) return true
|
|
400
|
+
}
|
|
401
|
+
return false
|
|
360
402
|
}
|
|
361
403
|
const DANGEROUS_PATTERNS = [
|
|
362
404
|
'package.json',
|
|
@@ -376,11 +418,11 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
376
418
|
|
|
377
419
|
for (const entry of currentEntries) {
|
|
378
420
|
const status = entry.slice(0, 2).trim()
|
|
379
|
-
const file =
|
|
380
|
-
if (!file
|
|
421
|
+
const file = parsePorcelainPath(entry) // 已去引号/处理 rename/归一化,修正首行丢首字符
|
|
422
|
+
if (!file) continue
|
|
381
423
|
|
|
382
|
-
// 预存脏文件:step1 baseline 已记录,非本次 quick
|
|
383
|
-
if (
|
|
424
|
+
// 预存脏文件:step1 baseline 已记录,非本次 quick 产生,跳过审计(含折叠目录前缀匹配)
|
|
425
|
+
if (isBaselineFile(file)) continue
|
|
384
426
|
|
|
385
427
|
result.changedFiles.push(file)
|
|
386
428
|
if (status === 'D' || status === ' D') result.deletedFiles.push(file)
|
|
@@ -1859,8 +1901,9 @@ async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles, taskDes
|
|
|
1859
1901
|
try {
|
|
1860
1902
|
const { execSync } = await import('child_process')
|
|
1861
1903
|
const gs = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
1862
|
-
baselineFiles = gs.
|
|
1863
|
-
.map(l => l
|
|
1904
|
+
baselineFiles = gs.split('\n').filter(Boolean)
|
|
1905
|
+
.map(l => parsePorcelainPath(l))
|
|
1906
|
+
.filter(Boolean)
|
|
1864
1907
|
.filter(f => !f.startsWith('.sillyspec/'))
|
|
1865
1908
|
} catch {}
|
|
1866
1909
|
|
|
@@ -2100,8 +2143,9 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
2100
2143
|
// 不需要这里粗放过滤 .sillyspec/——旧过滤致预存 untracked .sillyspec/changes/ 不进 baseline,
|
|
2101
2144
|
// 却在 audit 被当「危险(.sillyspec/)+新增」误判永久 blocked(ql-20260713-002-7628 修复)。
|
|
2102
2145
|
const baselineFiles = gitStatus
|
|
2103
|
-
.
|
|
2104
|
-
.map(line => line
|
|
2146
|
+
.split('\n').filter(Boolean)
|
|
2147
|
+
.map(line => parsePorcelainPath(line))
|
|
2148
|
+
.filter(Boolean)
|
|
2105
2149
|
const allowedFiles = quickOpts?.quickFiles || []
|
|
2106
2150
|
const allowNew = quickOpts?.isAllowNew || false
|
|
2107
2151
|
const forceBaseline = quickOpts?.isForceBaseline || false
|
|
@@ -3163,11 +3207,12 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3163
3207
|
const resultCheck = validateQuickResult(outputText)
|
|
3164
3208
|
if (!resultCheck.ok) {
|
|
3165
3209
|
console.error(`\n❌ quick 结果摘要结构不完整:缺少字段 ${resultCheck.missing.join('、')}`)
|
|
3166
|
-
console.error(` --output
|
|
3167
|
-
console.error(`
|
|
3210
|
+
console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
|
|
3211
|
+
console.error(` 补全后重跑 --done(不丢进度),直接照抄此模板:`)
|
|
3212
|
+
console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
|
|
3168
3213
|
console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
|
|
3169
3214
|
console.error(` 方案:怎么改的`)
|
|
3170
|
-
console.error(` 结果:验证情况(测试数 / lint / typecheck /
|
|
3215
|
+
console.error(` 结果:验证情况(测试数 / lint / typecheck / 部署状态)"`)
|
|
3171
3216
|
steps[currentIdx].status = 'pending'
|
|
3172
3217
|
steps[currentIdx].completedAt = null
|
|
3173
3218
|
if (outputText) steps[currentIdx].output = null
|
package/src/verify-postcheck.js
CHANGED
|
@@ -61,6 +61,47 @@ export function extractTestStrategy(yamlText) {
|
|
|
61
61
|
return null
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 计算全量 fallback 的原因文本(供 printVerifyTestCheck / facet 给 agent/daemon 明示)。
|
|
66
|
+
*
|
|
67
|
+
* 返回 null 表示不需要 hint:显式 test_strategy:full(用户有意跑全量),
|
|
68
|
+
* 或 test_strategy:module 已成功命中模块子集(该情况不会走到全量路径)。
|
|
69
|
+
* 其余走全量路径的情况都返回原因字符串,让结果可正确解读——
|
|
70
|
+
* 否则 agent/daemon 会把"全量 commands.test"当成"按变更范围测的",误把
|
|
71
|
+
* 未变更模块的预存错误归因到本次变更(见 3.24 verify 坑1)。
|
|
72
|
+
*
|
|
73
|
+
* 语义对齐 runVerifyTestCheck 的分支:
|
|
74
|
+
* - strategy==='full' → null
|
|
75
|
+
* - strategy==='module' 但无有效 modules 块 → hint
|
|
76
|
+
* - strategy==='module' 有块但 git 不可用(hitCount=-1)→ hint
|
|
77
|
+
* - strategy==='module' 有块但 0 命中(hitCount=0)→ hint
|
|
78
|
+
* - strategy==='module' 有块且命中(hitCount>0)→ null(走子集)
|
|
79
|
+
* - strategy===null(缺省)→ hint(默认全量)
|
|
80
|
+
*
|
|
81
|
+
* @param {object} ctx
|
|
82
|
+
* @param {'full'|'module'|null} ctx.strategy
|
|
83
|
+
* @param {boolean} ctx.modulesPresent - extractModules 是否返回有效映射
|
|
84
|
+
* @param {number} ctx.hitCount - git diff 命中的模块数;-1 表示 git 不可用/非仓库
|
|
85
|
+
* @returns {string|null}
|
|
86
|
+
*/
|
|
87
|
+
export function computeFullFallbackReason({ strategy, modulesPresent, hitCount }) {
|
|
88
|
+
if (strategy === 'full') return null
|
|
89
|
+
if (strategy === 'module') {
|
|
90
|
+
if (!modulesPresent) {
|
|
91
|
+
return 'test_strategy: module 但 local.yaml 未配置有效的 modules: 块(需 inline flow: name: { path, test }),回退全量'
|
|
92
|
+
}
|
|
93
|
+
if (hitCount < 0) {
|
|
94
|
+
return 'test_strategy: module 但 git 不可用/非 git 仓库,无法判定命中模块,回退全量'
|
|
95
|
+
}
|
|
96
|
+
if (hitCount === 0) {
|
|
97
|
+
return 'test_strategy: module 但本次 git diff 未命中任何已配置 modules,回退全量'
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
// strategy === null(缺省 → 默认全量)
|
|
102
|
+
return 'local.yaml 未配置 test_strategy(默认全量 commands.test,未按变更范围收窄)'
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
/**
|
|
65
106
|
* 从 local.yaml 文本解析 modules 映射块。
|
|
66
107
|
* 设计约定(见 change 2026-07-10-tooling-followups design.md #2 方案 A):
|
|
@@ -258,29 +299,42 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null }) {
|
|
|
258
299
|
const localYamlPath = join(specBase, 'local.yaml')
|
|
259
300
|
const yamlText = existsSync(localYamlPath) ? readFileSync(localYamlPath, 'utf8') : null
|
|
260
301
|
|
|
261
|
-
// —— 模块子集路径(test_strategy: module)——
|
|
262
302
|
const strategy = extractTestStrategy(yamlText)
|
|
303
|
+
|
|
304
|
+
// —— 模块子集路径(test_strategy: module)——
|
|
305
|
+
// 算出 modulesPresent / hitCount 供 fallback hint 判定(computeFullFallbackReason);
|
|
306
|
+
// 命中即走子集。gitChangedFiles 返回 null 表示 git 不可用 → hitCount=-1(与 0 命中区分)。
|
|
307
|
+
let modulesPresent = false
|
|
308
|
+
let hitCount = 0
|
|
263
309
|
if (strategy === 'module') {
|
|
264
310
|
const modules = extractModules(yamlText)
|
|
265
311
|
if (modules) {
|
|
312
|
+
modulesPresent = true
|
|
266
313
|
const changedFiles = gitChangedFiles(cwd)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
if (changedFiles === null) {
|
|
315
|
+
hitCount = -1 // git 不可用 / 非仓库
|
|
316
|
+
} else {
|
|
317
|
+
const hits = pickHitModules(changedFiles, modules)
|
|
318
|
+
hitCount = hits.length
|
|
319
|
+
if (hitCount > 0) {
|
|
320
|
+
return runModuleSubset({ cwd, specBase, changeName, hits })
|
|
321
|
+
}
|
|
322
|
+
// 有 modules 配置但无命中 → fallback commands.test(D-002@v1 向后兼容)
|
|
270
323
|
}
|
|
271
|
-
// 有 modules 配置但无命中 / git 不可用 → fallback commands.test(D-002@v1 向后兼容)
|
|
272
324
|
}
|
|
273
325
|
// test_strategy:module 但无 modules 配置 → fallback commands.test(brownfield 友好)
|
|
274
326
|
}
|
|
275
327
|
|
|
276
328
|
// —— 全量路径(默认 full / fallback)——
|
|
277
|
-
|
|
329
|
+
// fallbackReason 非 null 表示本次全量是"非显式"的(缺省/配置不全/未命中),需明示。
|
|
330
|
+
const fallbackReason = computeFullFallbackReason({ strategy, modulesPresent, hitCount })
|
|
331
|
+
return runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fallbackReason })
|
|
278
332
|
}
|
|
279
333
|
|
|
280
334
|
/**
|
|
281
335
|
* 全量跑 commands.test(现有逻辑,brownfield 行为不变)。
|
|
282
336
|
*/
|
|
283
|
-
function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName }) {
|
|
337
|
+
function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fallbackReason = null }) {
|
|
284
338
|
const command = extractTestCommand(yamlText)
|
|
285
339
|
|
|
286
340
|
if (!command) {
|
|
@@ -294,6 +348,8 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName })
|
|
|
294
348
|
? 'local.yaml 未配置 commands.test(或标记 unavailable)'
|
|
295
349
|
: `local.yaml 不存在(${localYamlPath})`,
|
|
296
350
|
resultPath: null,
|
|
351
|
+
mode: 'full',
|
|
352
|
+
fallbackReason,
|
|
297
353
|
}
|
|
298
354
|
}
|
|
299
355
|
|
|
@@ -327,9 +383,11 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName })
|
|
|
327
383
|
outputTail,
|
|
328
384
|
reason,
|
|
329
385
|
resultPath: null,
|
|
386
|
+
mode: 'full',
|
|
387
|
+
fallbackReason,
|
|
330
388
|
}
|
|
331
389
|
|
|
332
|
-
writeRunResult({ specBase, changeName, result })
|
|
390
|
+
writeRunResult({ specBase, changeName, result, extra: fallbackReason ? { fallback_reason: fallbackReason } : {} })
|
|
333
391
|
return result
|
|
334
392
|
}
|
|
335
393
|
|
|
@@ -362,6 +420,8 @@ function runModuleSubset({ cwd, specBase, changeName, hits }) {
|
|
|
362
420
|
outputTail: outputTail.length > OUTPUT_TAIL_CHARS ? '…' + outputTail.slice(-OUTPUT_TAIL_CHARS) : outputTail,
|
|
363
421
|
reason,
|
|
364
422
|
resultPath: null,
|
|
423
|
+
mode: 'module-subset',
|
|
424
|
+
fallbackReason: null,
|
|
365
425
|
}
|
|
366
426
|
|
|
367
427
|
writeRunResult({
|
|
@@ -427,6 +487,19 @@ export function printVerifyTestCheck(result) {
|
|
|
427
487
|
for (const line of tail.split('\n')) console.error(` | ${line}`)
|
|
428
488
|
}
|
|
429
489
|
}
|
|
490
|
+
// —— 全量 fallback 明示(skipped 已提前 return,此处仅 passed/failed)——
|
|
491
|
+
// 让 agent 知道本次跑的是全量 commands.test、非变更范围子集;失败可能含未变更模块
|
|
492
|
+
// 的预存错误,需先核对用例归属再归因到本次变更(见 3.24 verify 坑1)。
|
|
493
|
+
if (result.mode === 'full' && result.fallbackReason) {
|
|
494
|
+
if (result.status === 'failed') {
|
|
495
|
+
console.warn(` ⚠️ 本次跑的是 commands.test 全量(${result.fallbackReason})。`)
|
|
496
|
+
console.warn(' 失败可能含与本变更无关的预存错误——先核对失败用例是否属于你的变更范围;')
|
|
497
|
+
console.warn(' 或在 local.yaml 配置 test_strategy: module + modules: 块以收窄到变更模块。')
|
|
498
|
+
} else {
|
|
499
|
+
console.warn(` ⚠️ 本次跑的是 commands.test 全量(${result.fallbackReason})。`)
|
|
500
|
+
console.warn(' 如耗时过长,可在 local.yaml 配置 test_strategy: module + modules: 块按模块收窄。')
|
|
501
|
+
}
|
|
502
|
+
}
|
|
430
503
|
if (result.resultPath) {
|
|
431
504
|
console.log(`📄 实测结果已写入: ${result.resultPath}`)
|
|
432
505
|
}
|