sillyspec 3.26.7 → 3.26.8
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-doctor/SKILL.md +2 -0
- package/.husky/pre-push +9 -2
- package/bin/sillyspec.js +1 -1
- package/package.json +1 -1
- package/src/config-schema.js +305 -287
- package/src/docs-check.js +333 -0
- package/src/docs-debt.js +226 -0
- package/src/docs-gate.js +106 -0
- package/src/doctor-diagnostics.js +23 -2
- package/src/index.js +1880 -1787
- package/src/init.js +81 -14
- package/src/modules.js +490 -486
- package/src/progress.js +37 -13
- package/src/run/command.js +54 -24
- package/src/run/complete-handlers.js +1254 -1128
- package/src/run/complete.js +0 -32
- package/src/run/gates.js +18 -0
- package/src/run/prompt.js +713 -640
- package/src/run/quick-audit.js +137 -123
- package/src/run/shared.js +1109 -929
- package/src/scan-staleness.js +90 -0
- package/src/stage-contract-spec.js +15 -0
- package/src/stage-contract.js +8 -5
- package/src/stages/brainstorm-auto.js +0 -1
- package/src/stages/brainstorm.js +1 -1
- package/src/stages/execute.js +985 -967
- package/src/stages/plan-postcheck.js +93 -36
- package/src/stages/verify.js +1 -1
- package/src/sync.js +27 -17
- package/src/task-review.js +2 -1
- package/src/worktree-apply.js +6 -3
|
@@ -31,6 +31,7 @@ import { validateScriptCommands } from './cmd-existence.js'
|
|
|
31
31
|
* @returns {string[]}
|
|
32
32
|
*/
|
|
33
33
|
function parseDependsOn(content) {
|
|
34
|
+
content = content.replace(/\r\n/g, '\n') // 外部调用方(worktree-apply 等)原生 readFileSync 喂 CRLF 时 ^---\n 锚点失配
|
|
34
35
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
35
36
|
if (!fmMatch) return []
|
|
36
37
|
const fm = fmMatch[1]
|
|
@@ -38,9 +39,11 @@ function parseDependsOn(content) {
|
|
|
38
39
|
if (inlineMatch) {
|
|
39
40
|
return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
// 块列表:列表项允许顶格或缩进([ \t]*,坑6——原 \s* 贪婪吃掉换行与前导空白,
|
|
43
|
+
// 顶格 `depends_on:\n- task-01` 标准 YAML 写法永远失配,静默判「无依赖」)
|
|
44
|
+
const blockMatch = fm.match(/depends_on:[ \t]*\n((?:[ \t]*-[ \t]+.+\n?)+)/)
|
|
42
45
|
if (blockMatch) {
|
|
43
|
-
return blockMatch[1].match(
|
|
46
|
+
return blockMatch[1].match(/[ \t]*-[ \t]+(.+)/g)?.map(s => s.replace(/^[ \t]*-[ \t]+/, '').trim().replace(/['"]/g, '')) || []
|
|
44
47
|
}
|
|
45
48
|
return []
|
|
46
49
|
}
|
|
@@ -67,16 +70,25 @@ function parseTaskId(content, filename) {
|
|
|
67
70
|
* @returns {string[]}
|
|
68
71
|
*/
|
|
69
72
|
export function parseAllowedPaths(content) {
|
|
73
|
+
content = content.replace(/\r\n/g, '\n') // 同 parseDependsOn:入口归一,保护喂原始 CRLF 内容的外部调用方
|
|
70
74
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
71
75
|
if (!fmMatch) return []
|
|
72
76
|
const fm = fmMatch[1]
|
|
73
77
|
const inlineMatch = fm.match(/allowed_paths:\s*\[([^\]]*)\]/)
|
|
74
78
|
if (inlineMatch) {
|
|
75
|
-
return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
|
|
79
|
+
return inlineMatch[1].split(',').map(s => s.trim().replace(/['"`]/g, '')).filter(Boolean)
|
|
76
80
|
}
|
|
77
|
-
|
|
81
|
+
// 块列表:列表项允许顶格或缩进(坑6①——原 `\s*\n` 的 \s 贪婪匹配换行+前导空白,
|
|
82
|
+
// 标准 YAML 块列表 `allowed_paths:\n- src/a.js`(顶格)永远失配,静默判「缺 allowed_paths」;
|
|
83
|
+
// 缩进式 \s+- 恰可命中是巧合幸存。改 [ \t]*(不吃换行)+ 行内 [ \t]*-[ \t]+ 顶格缩进通吃)
|
|
84
|
+
const blockMatch = fm.match(/allowed_paths:[ \t]*\n((?:[ \t]*-[ \t]+.+\n?)+)/)
|
|
78
85
|
if (blockMatch) {
|
|
79
|
-
|
|
86
|
+
// 剥首尾成对反引号(坑7 家族:`src/a.js` 整项以反引号开头时 js-yaml/正则路径带 ` 匹配失败报未覆盖;
|
|
87
|
+
// 与 change-list.js 的 cell 归一化同语义——剥两端包裹引号,不动路径内部字符)
|
|
88
|
+
return blockMatch[1].match(/[ \t]*-[ \t]+(.+)/g)?.map(s => {
|
|
89
|
+
const raw = s.replace(/^[ \t]*-[ \t]+/, '').trim()
|
|
90
|
+
return raw.replace(/^`([^`]*)`$/, '$1').replace(/^['"]|['"]$/g, '').trim()
|
|
91
|
+
}).filter(Boolean) || []
|
|
80
92
|
}
|
|
81
93
|
return []
|
|
82
94
|
}
|
|
@@ -96,6 +108,7 @@ export function parseAllowedPaths(content) {
|
|
|
96
108
|
* @returns {string|null}
|
|
97
109
|
*/
|
|
98
110
|
function parseFrontmatterScalar(content, field) {
|
|
111
|
+
content = content.replace(/\r\n/g, '\n') // 同 parseAllowedPaths:入口归一 CRLF
|
|
99
112
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
100
113
|
if (!fmMatch) return null
|
|
101
114
|
let fm
|
|
@@ -439,6 +452,39 @@ export function validateBlueprintConsistency(changeDir) {
|
|
|
439
452
|
}
|
|
440
453
|
}
|
|
441
454
|
|
|
455
|
+
// plan.md 声明任务 ↔ tasks/ 卡片双向对账(债单 D-2b):
|
|
456
|
+
// plan 列 17 任务只出 12 卡(尾部文档同步类任务漏卡)时,无卡任务不进 Wave、
|
|
457
|
+
// 不受 execute 审计 → 零失败信号漏做。口径与 plan.js parseTaskCount/parseTaskNames 一致
|
|
458
|
+
// (`- [ ] task-NN` checkbox;file 命名 task-NN.md + 卡内 id task-NN 均取,缺一即漏卡)。
|
|
459
|
+
const planMdPath = pJoin(changeDir, 'plan.md')
|
|
460
|
+
if (existsSync(planMdPath)) {
|
|
461
|
+
const declaredIds = new Set()
|
|
462
|
+
for (const line of readFileSync(planMdPath, 'utf8').split('\n')) {
|
|
463
|
+
const m = line.match(/^[-*]\s*\[[ x]\]\s*(task-\d+)\b/i)
|
|
464
|
+
if (m) declaredIds.add(m[1].toLowerCase())
|
|
465
|
+
}
|
|
466
|
+
if (declaredIds.size > 0) {
|
|
467
|
+
const tprRule = getRule('plan.task-plan-reconciliation')
|
|
468
|
+
const cardIds = new Set([...taskInfo.keys()].map(id => id.toLowerCase()))
|
|
469
|
+
const missing = [...declaredIds].filter(id => !cardIds.has(id))
|
|
470
|
+
if (missing.length > 0) {
|
|
471
|
+
errors.push(
|
|
472
|
+
tprRule.data.messageMissingCards
|
|
473
|
+
.replaceAll('${declared}', declaredIds.size)
|
|
474
|
+
.replaceAll('${missing}', missing.join(', '))
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
const orphans = [...cardIds].filter(id => !declaredIds.has(id))
|
|
478
|
+
if (orphans.length > 0) {
|
|
479
|
+
errors.push(
|
|
480
|
+
tprRule.data.messageOrphanCards.replaceAll('${orphans}', orphans.join(', '))
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// declaredIds.size === 0:plan.md 无 task checkbox 声明(极端格式)→ 跳过对账,
|
|
485
|
+
// 由 validatePlanArtifacts/validatePlanForExecute 把关,不在此误伤。
|
|
486
|
+
}
|
|
487
|
+
|
|
442
488
|
// 拓扑排序 + 循环依赖
|
|
443
489
|
const depMap = new Map()
|
|
444
490
|
for (const [taskId, info] of taskInfo) {
|
|
@@ -691,8 +737,10 @@ function parseDesignCoverageByRepo(designPath) {
|
|
|
691
737
|
}
|
|
692
738
|
|
|
693
739
|
if (!hasSegmentHeader) {
|
|
694
|
-
// 无段头 → 整章节归 main(等价原 parseFileChangeList
|
|
695
|
-
|
|
740
|
+
// 无段头 → 整章节归 main(等价原 parseFileChangeList 行为)。
|
|
741
|
+
// keepSillyspecDocs=true:.sillyspec/docs/ 模块文档 = 交付物,与 apply 阶段
|
|
742
|
+
// resolveApplyAllowSet 口径一致(债单 D-2)——design 声明的模块文档更新必须被 task 认领。
|
|
743
|
+
const main = new Set(parseFileChangeList(designPath, { keepSillyspecDocs: true }))
|
|
696
744
|
if (main.size > 0) byRepo.set('main', main)
|
|
697
745
|
for (const p of main) allFiles.push(p)
|
|
698
746
|
return { byRepo, hasSegmentHeader: false, allFiles }
|
|
@@ -731,7 +779,7 @@ function parseDesignCoverageByRepo(designPath) {
|
|
|
731
779
|
const virtualDesign = ['## 文件变更清单', '', ...segLines, ''].join('\n')
|
|
732
780
|
const tmpPath = pJoin(tmpDir, `${repo}.md`)
|
|
733
781
|
writeFileSync(tmpPath, virtualDesign)
|
|
734
|
-
const segFiles = [...parseFileChangeList(tmpPath)]
|
|
782
|
+
const segFiles = [...parseFileChangeList(tmpPath, { keepSillyspecDocs: true })]
|
|
735
783
|
if (segFiles.length === 0) continue
|
|
736
784
|
const set = byRepo.get(repo) || new Set()
|
|
737
785
|
for (const p of segFiles) { set.add(p); allFiles.push(p) }
|
|
@@ -1062,41 +1110,41 @@ export async function executePlanPostcheck(context) {
|
|
|
1062
1110
|
|
|
1063
1111
|
console.log(` 📂 变更目录: ${changeDir}`)
|
|
1064
1112
|
|
|
1113
|
+
// ── 报错聚合(坑6③):六个检查全部跑完后统一输出全部失败项,一轮 --done 暴露全部问题,
|
|
1114
|
+
// 不再「失败一个抛一个、修一个冒一个」的迭代盲盒。各检查相互独立(都只读 changeDir 产物),
|
|
1115
|
+
// 前置检查失败不使后续检查失真——唯一例外是 tasks/ 目录缺失时后续检查都无意义,仍提前短路。
|
|
1116
|
+
const failures = [] // { name, errors, hint }
|
|
1117
|
+
const printSectionWarnings = (label, warnings) => {
|
|
1118
|
+
if (warnings.length > 0) {
|
|
1119
|
+
console.warn(`\n⚠️ ${label}警告(不阻断):`)
|
|
1120
|
+
for (const w of warnings) console.warn(` - ${w}`)
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1065
1124
|
// ── 1. 一致性校验 ──
|
|
1066
1125
|
const consistency = validateBlueprintConsistency(changeDir)
|
|
1067
1126
|
if (consistency.errors.length > 0) {
|
|
1068
|
-
|
|
1069
|
-
for (const err of consistency.errors) console.error(` - ${err}`)
|
|
1070
|
-
console.error('\n 请修复上述问题后重新完成此步骤。')
|
|
1071
|
-
throw new Error('planPostcheck: blueprint consistency check failed')
|
|
1072
|
-
}
|
|
1073
|
-
if (consistency.warnings.length > 0) {
|
|
1074
|
-
console.warn('\n⚠️ 蓝图一致性警告(不阻断):')
|
|
1075
|
-
for (const w of consistency.warnings) console.warn(` - ${w}`)
|
|
1127
|
+
failures.push({ name: '蓝图一致性校验', errors: consistency.errors, hint: '请修复上述问题后重新完成此步骤。' })
|
|
1076
1128
|
}
|
|
1129
|
+
printSectionWarnings('蓝图一致性', consistency.warnings)
|
|
1077
1130
|
|
|
1078
1131
|
// ── 1b. 可行性校验 ──
|
|
1079
1132
|
const feasibility = validatePlanFeasibility(changeDir, context.cwd)
|
|
1080
1133
|
if (feasibility.errors.length > 0) {
|
|
1081
|
-
|
|
1082
|
-
for (const err of feasibility.errors) console.error(` - ${err}`)
|
|
1083
|
-
throw new Error('planPostcheck: feasibility check failed')
|
|
1084
|
-
}
|
|
1085
|
-
if (feasibility.warnings.length > 0) {
|
|
1086
|
-
console.warn('\n⚠️ Plan 可行性警告(不阻断):')
|
|
1087
|
-
for (const w of feasibility.warnings) console.warn(` - ${w}`)
|
|
1134
|
+
failures.push({ name: 'Plan 可行性校验', errors: feasibility.errors, hint: null })
|
|
1088
1135
|
}
|
|
1136
|
+
printSectionWarnings('Plan 可行性', feasibility.warnings)
|
|
1089
1137
|
|
|
1090
1138
|
// ── 1c. 跨任务契约校验 ──
|
|
1091
1139
|
// 对账 consumer.expects_from ↔ provider.provides,拦截「consumer 期望字段
|
|
1092
1140
|
// 但 provider 未承诺」的契约断裂(避免到 execute/verify 才暴露成 403/500)
|
|
1093
1141
|
const crossTask = validateCrossTaskContracts(changeDir)
|
|
1094
1142
|
if (crossTask.errors.length > 0) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1143
|
+
failures.push({
|
|
1144
|
+
name: '跨任务契约校验(consumer 期望的字段未被 provider 承诺)',
|
|
1145
|
+
errors: crossTask.errors,
|
|
1146
|
+
hint: '修复方式:要么在 provider task 的 provides.fields 补上缺失字段,要么修正 consumer task 的 expects_from.needs(确认依赖是否真实)。',
|
|
1147
|
+
})
|
|
1100
1148
|
}
|
|
1101
1149
|
|
|
1102
1150
|
// ── 1c-b. TaskCard 命令存在性校验 ──
|
|
@@ -1111,11 +1159,11 @@ export async function executePlanPostcheck(context) {
|
|
|
1111
1159
|
}
|
|
1112
1160
|
const taskCmds = validateTaskCommands(changeDir, context.cwd, taskModules)
|
|
1113
1161
|
if (taskCmds.errors.length > 0) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1162
|
+
failures.push({
|
|
1163
|
+
name: 'TaskCard 命令存在性校验(verify/implementation 的 npm/pnpm/yarn run <script> 不存在)',
|
|
1164
|
+
errors: taskCmds.errors,
|
|
1165
|
+
hint: '修复方式:要么在 package.json scripts 补上缺失命令(monorepo 注意子包路径),要么修正 TaskCard 的 verify/implementation(确认命令是否真实、是否需 cd 子包)。',
|
|
1166
|
+
})
|
|
1119
1167
|
}
|
|
1120
1168
|
|
|
1121
1169
|
// ── 1d. design 文件覆盖对账 ──
|
|
@@ -1124,14 +1172,23 @@ export async function executePlanPostcheck(context) {
|
|
|
1124
1172
|
const coverage = validateDesignFileCoverage(changeDir)
|
|
1125
1173
|
for (const w of coverage.warnings) console.warn(`\n ⚠️ ${w}`)
|
|
1126
1174
|
if (coverage.errors.length > 0) {
|
|
1127
|
-
|
|
1128
|
-
for (const err of coverage.errors) console.error(` - ${err}`)
|
|
1129
|
-
throw new Error('planPostcheck: design file coverage check failed')
|
|
1175
|
+
failures.push({ name: 'design.md 文件覆盖对账(清单中的文件未被任何 task 覆盖)', errors: coverage.errors, hint: null })
|
|
1130
1176
|
}
|
|
1131
1177
|
if (coverage.designFiles.length > 0 && coverage.uncovered.length === 0) {
|
|
1132
1178
|
console.log(` ✅ design.md ${coverage.designFiles.length} 个文件全部被 task allowed_paths 覆盖`)
|
|
1133
1179
|
}
|
|
1134
1180
|
|
|
1181
|
+
// ── 聚合输出:一轮 --done 暴露全部失败项(坑6③)──
|
|
1182
|
+
if (failures.length > 0) {
|
|
1183
|
+
console.error(`\n❌ plan postcheck 失败(${failures.length} 类问题,已全部列出——一次修复后重跑,无需逐个迭代):`)
|
|
1184
|
+
for (const f of failures) {
|
|
1185
|
+
console.error(`\n 【${f.name}】`)
|
|
1186
|
+
for (const err of f.errors) console.error(` - ${err}`)
|
|
1187
|
+
if (f.hint) console.error(` ${f.hint}`)
|
|
1188
|
+
}
|
|
1189
|
+
throw new Error(`planPostcheck: ${failures.length} 组校验失败(${failures.map(f => f.name).join(';')})`)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1135
1192
|
// ── 2. Wave 重排 ──
|
|
1136
1193
|
const tasksDir = pJoin(changeDir, 'tasks')
|
|
1137
1194
|
if (existsSync(tasksDir)) {
|
package/src/stages/verify.js
CHANGED
|
@@ -134,7 +134,7 @@ export const definition = {
|
|
|
134
134
|
3. 数据模型是否符合
|
|
135
135
|
4. API 设计是否符合
|
|
136
136
|
5. **Reverse Sync 检查**:如果发现实现合理但 design.md 未覆盖,先更新 design.md 补充遗漏
|
|
137
|
-
6. **模块文档一致性检查**:如果在"加载规范并锚定"
|
|
137
|
+
6. **模块文档一致性检查**:如果在"加载规范并锚定"步骤中加载了模块文档,检查实现是否符合模块文档描述的当前设计(特别关注接口签名、数据流、依赖关系)。不一致时**当场同步模块文档**(这是 verify 的收尾义务,不是可选项);module-impact.md「更新结果」表的文档同步项也须在本阶段回填 done/skipped——CLI 在 verify --done 时硬校验该表无 pending/待办死信行,未清即阻断完成
|
|
138
138
|
7. **决策链路检查**:如果存在 decisions.md,输出 D-xxx@vN → FR-xxx → task-xx → evidence 的追踪矩阵;缺失项必须列为风险(CLI 只校验每个 D-xxx@vN ID 字面出现在 verify-result.md,warning 不阻断;矩阵的 D→FR→task→evidence 映射完整性供人类追溯,CLI 不校验——是否真覆盖由你诚实判定)
|
|
139
139
|
|
|
140
140
|
### 输出
|
package/src/sync.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlink
|
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { resolvePlatformSpecDir } from './progress.js';
|
|
14
14
|
import { safeGit } from './git-helper.js';
|
|
15
|
+
import { PLATFORM_MANAGED_FILENAME } from './run/shared.js';
|
|
15
16
|
|
|
16
17
|
// sync 是 best-effort(网络失败只 warn):平台指针失效时不抛,跳过平台、回退本地。
|
|
17
18
|
function safePlatformSpecDir(cwd) {
|
|
@@ -327,28 +328,37 @@ export class SyncManager {
|
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
/**
|
|
330
|
-
*
|
|
331
|
-
* 从 local.yaml 删除 platform
|
|
331
|
+
* 断开平台连接(三清,D-C@v2)。
|
|
332
|
+
* 1. 从 local.yaml 删除 platform 配置段;
|
|
333
|
+
* 2. 删恢复指针 <cwd>/.sillyspec-platform.json;
|
|
334
|
+
* 3. 删平台接管声明 <cwd>/.sillyspec-platform-managed。
|
|
335
|
+
* 不删后两者则"disconnect 后恢复本地模式"不可达(指针健在时 resolvePlatformSpecDir
|
|
336
|
+
* 仍解析平台 specRoot;声明健在时指针缺失会 fail-closed)。
|
|
332
337
|
*/
|
|
333
338
|
disconnect() {
|
|
334
339
|
const p = join(this.cwd, LOCAL_YAML);
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
if (existsSync(p)) {
|
|
341
|
+
const text = readFileSync(p, 'utf8');
|
|
342
|
+
if (findTopLevelSectionRange(text, 'platform') !== null) {
|
|
343
|
+
// 文本级删除 platform 段,保留注释/其他段/数组/深嵌套/CRLF
|
|
344
|
+
const newText = replaceTopLevelSection(text, 'platform', null);
|
|
345
|
+
if (newText.trim() === '') {
|
|
346
|
+
// 删段后纯空白(无任何段也无注释)→ 删除整个文件;注释算内容,有注释则保留
|
|
347
|
+
try { unlinkSync(p); } catch { /* best effort */ }
|
|
348
|
+
} else {
|
|
349
|
+
writeFileSync(p, newText, 'utf8');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
343
352
|
}
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
353
|
+
// 三清之二/三:指针 + 接管声明(disconnect 是声明的唯一退出路径——design.md §5.4)
|
|
354
|
+
let cleaned = [];
|
|
355
|
+
for (const f of ['.sillyspec-platform.json', PLATFORM_MANAGED_FILENAME]) {
|
|
356
|
+
const fp = join(this.cwd, f);
|
|
357
|
+
if (existsSync(fp)) {
|
|
358
|
+
try { unlinkSync(fp); cleaned.push(f); } catch { /* best effort */ }
|
|
359
|
+
}
|
|
351
360
|
}
|
|
361
|
+
if (cleaned.length > 0) console.log(`[sync] 已清理平台指针与接管声明: ${cleaned.join(', ')}`);
|
|
352
362
|
console.log('[sync] 已断开连接');
|
|
353
363
|
}
|
|
354
364
|
|
package/src/task-review.js
CHANGED
|
@@ -442,7 +442,8 @@ export function validateTaskReviews(opts) {
|
|
|
442
442
|
} else if (repoKey !== 'main') {
|
|
443
443
|
// 跨仓 repo 在 ctx 中未注册 → 不应发生(MultiRepoContext 构造已 fail-closed 拦截未注册),
|
|
444
444
|
// 防御性降级为 warning 提示,不阻断(review.json schema 已过,仓库解析交给 ctx 上游负责)。
|
|
445
|
-
|
|
445
|
+
// 坑7 教训:此分支发生时后续主仓校验必报「疑似伪造」,文案须直接指向真实排查方向。
|
|
446
|
+
warnings.push(`${taskId}: review.repo="${repoKey}" 在 MultiRepoContext 未解析到 entry,退回主仓 gitDir 校验——下方若报 commit 不真实/疑似伪造,先查 repo 声明(plan.md 内联块或 tasks/task-NN.md 的 repo:)与 local.yaml repos: 注册,非 review 伪造`)
|
|
446
447
|
}
|
|
447
448
|
}
|
|
448
449
|
if (reviewGitDir) {
|
package/src/worktree-apply.js
CHANGED
|
@@ -877,7 +877,8 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
877
877
|
}
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
-
// 检查 6: diff
|
|
880
|
+
// 检查 6: diff 规模异常(两档:>5000 行 BLOCKED;2000~5000 行 WARNING——正常规模 change
|
|
881
|
+
// 常超 2000 行(如 +2368),单档硬拦把正常变更逼进 rescue cp 手动路径,见 ql-20260815-001 复盘)
|
|
881
882
|
const wtPath = meta?.worktreePath;
|
|
882
883
|
const diffBase = meta?.baselineCommit || meta?.baseHash;
|
|
883
884
|
let additions = 0, deletions = 0;
|
|
@@ -888,8 +889,10 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
888
889
|
const delMatch = shortstat?.match(/(\d+) deletion/);
|
|
889
890
|
additions = insMatch ? parseInt(insMatch[1]) : 0;
|
|
890
891
|
deletions = delMatch ? parseInt(delMatch[1]) : 0;
|
|
891
|
-
if (additions + deletions >
|
|
892
|
-
reasons.push(`diff 规模异常(${additions} additions + ${deletions} deletions = ${additions + deletions}
|
|
892
|
+
if (additions + deletions > 5000) {
|
|
893
|
+
reasons.push(`diff 规模异常(${additions} additions + ${deletions} deletions = ${additions + deletions} 行,超过 5000 硬上限)`);
|
|
894
|
+
} else if (additions + deletions > 2000) {
|
|
895
|
+
warnings.push(`diff 规模偏大(${additions} additions + ${deletions} deletions = ${additions + deletions} 行,2000~5000 区间放行为 WARNING,请确认属正常变更规模)`);
|
|
893
896
|
}
|
|
894
897
|
} catch {}
|
|
895
898
|
}
|